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. 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 : js 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 https://callejear.com/mcp/ 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 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. 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.