{"slug": "building-a-read-only-city-data-mcp-street-identity-missing-data-and-honest", "title": "Building a read-only city-data MCP: street identity, missing data and honest comparisons", "summary": "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.", "body_md": "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.\n\nHere 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.\n\nThe 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.\n\nThat distinction matters: complete coverage of a street catalogue does not imply complete coverage of the services near every street.\n\nThe first tool call should therefore be:\n\n```\n{\n  \"name\": \"callejear_get_coverage\",\n  \"arguments\": { \"city_id\": \"28079\" }\n}\n```\n\nA 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.\n\n```\n{\n  \"name\": \"callejear_search_streets\",\n  \"arguments\": {\n    \"city_id\": \"28079\",\n    \"query\": \"Alcalá\",\n    \"limit\": 5\n  }\n}\n```\n\nUse 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.\n\nThe project's documented client example uses Node 22.22.x and the pinned MCP client package below:\n\n```\nnpm install --save-exact @modelcontextprotocol/client@2.3.1\n```\n\nSave this as `consulta.mjs` and run `node consulta.mjs`:\n\n``` js\nimport {\n  Client,\n  StreamableHTTPClientTransport,\n} from '@modelcontextprotocol/client';\n\nconst client = new Client({ name: 'madrid-data-example', version: '1.0.0' });\n\ntry {\n  await client.connect(new StreamableHTTPClientTransport(\n    new URL('https://callejear.com/api/mcp')\n  ));\n\n  for (const request of [\n    { name: 'callejear_get_coverage', arguments: { city_id: '28079' } },\n    {\n      name: 'callejear_search_streets',\n      arguments: { city_id: '28079', query: 'Alcalá', limit: 5 },\n    },\n  ]) {\n    const result = await client.callTool(request);\n    if (result.isError) {\n      console.error('Tool returned an error:', result.content);\n      break;\n    }\n    console.log(JSON.stringify(result.structuredContent ?? result.content, null, 2));\n  }\n} finally {\n  await client.close();\n}\n```\n\nThis is an anonymous Streamable HTTP endpoint. The service performs reads only and does not require credentials for other services.\n\nThe four available dimensions count observed records for pharmacies, schools, supermarkets and libraries within 500 metres of a representative point, using Haversine distance.\n\nThey 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.\n\nConsequently:\n\n`null` means unavailable, not zero.\n`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.\n\nResponses 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.\n\nLikewise, 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.\n\nThe 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.\n\nThe [Callejear MCP documentation](https://callejear.com/mcp/) includes the connection details, method, data downloads and limitations. The official registry identifier is `com.callejear/madrid`.\n\nThe street catalogue comes from the [Ayuntamiento de Madrid's official street dataset](https://datos.madrid.es/dataset/213605-0-callejero-oficial-madrid), with municipal attribution and CC BY 4.0 terms. The OSM-derived extract retains [OpenStreetMap attribution and ODbL terms](https://www.openstreetmap.org/copyright). These are data licences, not a claim about a software licence. Neither source endorses this independent service.\n\nFor 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.", "url": "https://wpnews.pro/news/building-a-read-only-city-data-mcp-street-identity-missing-data-and-honest", "canonical_source": "https://dev.to/yoel_castao_d5cdcbe81850/building-a-read-only-city-data-mcp-street-identity-missing-data-and-honest-comparisons-fcd", "published_at": "2026-10-16 17:14:35+00:00", "updated_at": "2026-10-16 17:20:21.554109+00:00", "lang": "en", "topics": ["agent-protocols", "ai-agents", "ai-tools", "structured-data"], "entities": ["Callejear", "Madrid", "Model Context Protocol", "@modelcontextprotocol/client", "INE"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/building-a-read-only-city-data-mcp-street-identity-missing-data-and-honest", "markdown": "https://wpnews.pro/news/building-a-read-only-city-data-mcp-street-identity-missing-data-and-honest.md", "text": "https://wpnews.pro/news/building-a-read-only-city-data-mcp-street-identity-missing-data-and-honest.txt", "jsonld": "https://wpnews.pro/news/building-a-read-only-city-data-mcp-street-identity-missing-data-and-honest.jsonld"}}