cd /news/agent-protocols/building-a-read-only-city-data-mcp-s… · home › topics › agent-protocols › article
[ARTICLE · art-151499] src=dev.to ↗ pub= topic=agent-protocols verified=true sentiment=· neutral

Building a read-only city-data MCP: street identity, missing data and honest comparisons

The independent Spanish municipal-data project Callejear has exposed a read-only MCP service for Madrid (INE code 28079) that lets AI assistants query 9,400 official street codes, of which 9,151 have validated reference points while 249 remain searchable with missing location and metrics. The project's engineers designed the response contract to prevent assistants from drawing conclusions the data cannot support, including explicit null-versus-zero semantics, preserved official-code/name conflicts, and paginated search with 15-minute cursors. The service caps responses at 32 KiB and returns MCP error results over HTTP 200 when a tool quota is exhausted, so clients must check isError rather than rely on HTTP status.

by read3 min views1 publishedOct 16, 2026

I work on Callejear, an independent project for exploring municipal data in Spain. We recently exposed a read-only MCP service for Madrid. The useful engineering lesson was that making data callable is only part of the job: the response contract must also prevent an assistant from drawing conclusions the data cannot support.

Here is a small integration example, followed by the decisions behind the interface. This article was prepared with AI assistance and checked against the project's implementation and documentation.

The service covers the municipality of Madrid, INE code 28079, rather than the whole Madrid region. Its current catalogue contains 9,400 official street codes from the municipal source dated 2026-10-13. Of these, 9,151 have a validated reference point; 249 remain searchable with missing location and metrics.

That distinction matters: complete coverage of a street catalogue does not imply complete coverage of the services near every street.

The first tool call should therefore be:

{
  "name": "callejear_get_coverage",
  "arguments": { "city_id": "28079" }
}

A street name is a search term, not a reliable database key. Names have variants, and source records can disagree. The catalogue preserves four official-code/name conflicts explicitly instead of silently choosing a name.

{
  "name": "callejear_search_streets",
  "arguments": {
    "city_id": "28079",
    "query": "Alcalá",
    "limit": 5
  }
}

Use the identifier returned by the search when calling callejear_get_street_context. Do not construct an ID from a guessed slug. Search is paginated, with at most 20 results per page; cursors belong to a specific search and expire after 15 minutes.

The project's documented client example uses Node 22.22.x and the pinned MCP client package below:

npm install --save-exact @modelcontextprotocol/client@2.3.1

Save this as consulta.mjs and run node consulta.mjs:

import {
  Client,
  StreamableHTTPClientTransport,
} from '@modelcontextprotocol/client';

const client = new Client({ name: 'madrid-data-example', version: '1.0.0' });

try {
  await client.connect(new StreamableHTTPClientTransport(
    new URL('https://callejear.com/api/mcp')
  ));

  for (const request of [
    { name: 'callejear_get_coverage', arguments: { city_id: '28079' } },
    {
      name: 'callejear_search_streets',
      arguments: { city_id: '28079', query: 'Alcalá', limit: 5 },
    },
  ]) {
    const result = await client.callTool(request);
    if (result.isError) {
      console.error('Tool returned an error:', result.content);
      break;
    }
    console.log(JSON.stringify(result.structuredContent ?? result.content, null, 2));
  }
} finally {
  await client.close();
}

This is an anonymous Streamable HTTP endpoint. The service performs reads only and does not require credentials for other services.

The four available dimensions count observed records for pharmacies, schools, supermarkets and libraries within 500 metres of a representative point, using Haversine distance.

They are partial historical observations. They do not measure an entire street, guarantee that a business is still open, or represent a complete inventory of unique establishments.

Consequently:

null means unavailable, not zero. callejear_compare_streets accepts two to five distinct street IDs. A client should preserve the definitions, radius, sources and limitations when presenting a comparison, rather than turning a count into an unsupported recommendation.

Responses are capped at 32 KiB. For larger comparisons, the full result is in structuredContent, while the text content may be a readable summary. A client that always parses content[0].text as JSON will break this contract.

Likewise, HTTP success does not guarantee tool success: an exhausted tool quota returns an MCP error result over HTTP 200. Check isError. HTTP throttling separately uses 429.

The public pilot has modest limits: 30 tool calls per minute and 100 per UTC day per origin, with a shared daily budget of 1,000. Cached responses also count. Use targeted queries rather than attempting a bulk crawl through the tools.

The Callejear MCP documentation includes the connection details, method, data downloads and limitations. The official registry identifier is com.callejear/madrid.

The street catalogue comes from the Ayuntamiento de Madrid's official street dataset, with municipal attribution and CC BY 4.0 terms. The OSM-derived extract retains OpenStreetMap attribution and ODbL terms. These are data licences, not a claim about a software licence. Neither source endorses this independent service.

For similar city-data tools, I would make coverage, missingness and provenance explicit in the first version of the response schema. Those details determine which answers a client can responsibly produce.

── more in #agent-protocols 4 stories · sorted by recency
── more on @callejear 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-a-read-only…] indexed:0 read:3min 2026-10-16 · —