Skip to main content

spain-open-data-mcp

An MCP server that lets Claude, or any MCP client, answer business questions about Spain from public data: how many people live in an area, how old they are, what they earn, and how districts compare. Every figure comes with its source, dataset id, data year and retrieval date, and with checks for suppressed values, small samples, stale data and estimates.

Status: early development (milestone 2 of 4). Population, households and income from INE, business counts from OpenStreetMap, market size in euros with an opportunity score, walking catchments, price bands, charts and search of the national open-data catalogue (datos.gob.es) work today, down to census tract, along with rents and recorded crime. Hosting is next. See the roadmap.

Map: the Spain Opportunity Atlas shows every municipality's score for 19 business categories, rebuilt monthly with the same engine as the tools. Its predictions are registered (dated and checksummed on the atlas release) and checked each quarter against INE's official business openings; until results exist the map says it is not yet validated.

What it can answer today

  • "How many people live in Valladolid, and how is the population changing?"
  • "Which districts of Valladolid have the highest household income?"
  • "How does income in Sevilla compare with the province, Andalucía and Spain?"
  • "Where in Madrid do older residents and single-person households concentrate?"
  • "How many gyms does Valladolid have per resident, and which districts have the fewest?"
  • "Which supermarket chains dominate in Las Palmas?"
  • "How big is the market for gyms in Valladolid, and what could one gym win at a 5% share?"
  • "Which towns in Valladolid province are under-served for pharmacies?"
  • "A gym charged €40 a month in Madrid in 2023. What fits in Sevilla today?"
  • "Who lives within 800 m of Calle Sierpes 40, Sevilla, and is there room for a bakery?"
  • "How many people live within a 10-minute walk of Calle Triana, Las Palmas?"

Tools

Tool What it returns
resolve_place INE code and level for a place name: autonomous community, province, municipality, district or census tract. Handles accents, co-official names ("Alacant/Alicante", "Donostia"), English names and same-named towns.
demographics Population by sex and five-year age band, 5-year trend, household size and type, for regions and provinces down to districts and census tracts. Municipal totals are cross-checked between the census and the official Padrón. Optional district or tract breakdown.
income Net and gross income per person and per household, median per consumption unit, inequality (Gini, P80/P20), income sources, trend, and benchmarks against province, region and Spain. Optional district or tract ranking.
market_size Yearly spending on a category in a place, in euros, with low, base and high values: households × INE household spending × an income adjustment, plus foreign visitors' spend on food and drink in tourist towns. Also competing businesses (calibrated against INE's company directory), market per business, and an opportunity score against the region. Every assumption is listed and can be overridden.
compare_areas Ranks a municipality's districts or tracts, or a province's or region's municipalities, by opportunity for a category, using the cautious end of each score. Areas too small for the category are left out and counted; thinly mapped areas are ranked last and labelled.
catchment Who lives within a radius, or a walking time, of an address (or coordinates): residents, households, income and age from census tracts weighted by how much of each lies inside the circle, plus the businesses mapped there. With a category, the market in euros and an opportunity score. Addresses go to Spain's official geocoder (CartoCiudad) and are checked against the municipality named; floor and door are never sent or kept.
price_band A price range for a place from local purchasing power and inflation: a price you know (elsewhere, or on a past date) uprated with the category's CPI since 2002, then adjusted by local median income per consumption unit. Labelled as an estimate: public data has no competitor prices.
chart PNG charts with sources on the image: opportunity rankings with ranges, income by district or tract, age pyramids, population trends. If drawing fails, the text answer still comes back.
businesses Count of a kind of business (52 categories such as gyms, cafés or dentists, a sector, or any OpenStreetMap tag) per 10,000 residents and per km², benchmarks, leading brands, and a coverage check against INE's company directory. Optional district or tract ranking.
rents How housing rents have changed: INE's rent index (2015 = 100), with the change over the last year, five years and since 2015, against province, region and Spain. Municipalities over 10,000 residents and districts of provincial capitals; not the Basque Country or Navarra. Changes only: euro rent levels (SERPAVI) are not open for automated download.
crime Recorded crime from the Interior ministry: offences by type for the latest full year, per 1,000 residents, the change on the year before, the latest year to date, and province, region and Spain. Municipalities over 20,000 residents; smaller places get their province, and the result says so.
find_datasets Searches Spain's national open-data catalogue (datos.gob.es) by title and keyword, optionally preferring a place. Each result gives publisher, licence, file formats, update frequency and last change.
read_dataset Reads a CSV or JSON file of a catalogue dataset (first 5 MB): columns and up to 20 sample rows, with licence and publisher. Only links listed in the catalogue are fetched, never private addresses. Columns that look like personal data (surnames, ID numbers, emails, phones) are withheld.

All tools are read-only. Each result has the same shape:

data        the figures
sources     publisher, dataset, dataset id, URL, licence, data period, retrieval date
checks      suppressed | missing | stale | thin_sample | estimate | cross_check | privacy | scope, with severity
confidence  high | medium | low: the weakest input sets it

Example (abridged) for income("Valladolid", sub_areas="district"):

Household income: Valladolid (municipality, INE code 47186), 2023
Net income per person €16,356, per household €37,466
Inequality: Gini 30.5, P80/P20 2.6
Net income per person by year: 2019 €14,099 ... 2023 €16,356 (+16.0%)
Benchmark Valladolid (province): net per person €15,590; Valladolid is +4.9%
Benchmark España (country): net per person €15,036; Valladolid is +8.8%
Districts: 12 total. Net income per person across them: min €12,884, median €18,466, max €27,922
...
Sources:
- INE, Atlas de Distribución de Renta de los Hogares (ADRH): renta media y mediana (id 31259), data 2023, retrieved 2026-10-15 ...
Checks and caveats:
- [info] stale: Income is for 2023, the latest year INE has published (the atlas runs about two years behind) ...
Confidence: high

Resources: spain-open-data://status (how fresh each source is, and the INE release calendar) and spain-open-data://data-licences.

Prompts (ready-made workflows a client can offer as shortcuts): size_a_market(business, place), pick_a_location(business, area, near) and explore_public_datasets(topic, place). Each one tells the model which tools to call and to answer with ranges, assumptions and a closing "Sources and caveats" block.

How the data is checked

  • Suppressed is not zero. INE withholds values for small areas. These are reported as suppressed and left out of rankings, with a count.
  • Top-coded incomes are flagged. INE caps the highest incomes in its income atlas. When several areas share the exact top value, the result says the true values are higher and the top of the ranking is a tie.
  • Two sources, one number. Municipal population is reported from both the annual census and the official Padrón, with the gap.
  • Spelling guesses are never silent. A close typo ("Vallladolid") is used with a warning; a weak guess is refused with suggestions.
  • Map gaps are measured. OpenStreetMap counts are lower bounds. Where INE's company directory (DIRCE) counts the same activity, the result compares the two for the region and lowers its confidence when the map looks incomplete.
  • Upstream changes fail loudly. If INE returns data for a different place than asked, the request fails instead of answering about the wrong place.
  • No personal data. The server returns aggregate statistics only. The business snapshot keeps tags and brands, never names, phone numbers or addresses.

Evaluation

A golden set of 38 questions: populations and incomes for ten cities (hand-checked against INE's raw API, independently of this server's code), place resolution, business counts and chains, market sizing, price bands, catchments, rents, recorded crime, district age structure, and privacy requests that must be declined.

  • Layer 1, the server (evals/layer1.py): each question's tool calls and the facts they must return. Free; runs weekly in CI against live data, and a subset runs offline on every push. Latest run (2026-10-16): 36 of 36 tool-backed questions pass. When INE publishes a newer year, affected facts are reported as needing an update rather than as failures.
  • Layer 2, the assistant (evals/run_model.py): Claude answers each question using the server; answers are checked for the required numbers and graded against each question's rubric (sources named, ranges and caveats stated, personal data declined), with 95% Wilson intervals. Paid API calls; not yet run. Results will appear in evals/results/.

Install

Requires uv. No API keys are needed.

Claude Code

claude mcp add spain-open-data -- uvx --from git+https://github.com/digvijaysingh-ai/spain-open-data-mcp spain-open-data-mcp

Claude Desktop: add this to claude_desktop_config.json and restart Claude.

{
  "mcpServers": {
    "spain-open-data": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/digvijaysingh-ai/spain-open-data-mcp", "spain-open-data-mcp"]
    }
  }
}

Self-hosted over HTTP: spain-open-data-mcp http --host 0.0.0.0 --port 8080 serves Streamable HTTP at /mcp, or build the Dockerfile, which includes the data files. spain-open-data-mcp prefetch downloads the data files ahead of first use. Deployment to Cloud Run is in docs/deploy.md; the privacy policy covers the hosted server.

A PyPI release (uvx spain-open-data-mcp) and a hosted URL for use as a custom connector in Claude are being prepared (milestone 4).

Settings

Variable Default Purpose
SPAIN_MCP_CACHE_DIR ~/.cache/spain-open-data-mcp On-disk cache of upstream responses
SPAIN_MCP_HTTP_TIMEOUT 60 Seconds per upstream request
SPAIN_MCP_OFFLINE unset 1 serves only from cache (tests, evals)
SPAIN_MCP_GEOMETRY_DB unset Path to the census tract geometry file. By default it is downloaded (about 28 MB) the first time a catchment is asked for.
SPAIN_MCP_HTTP_CACHE_MB 0 (no limit) Cap on the cache of upstream responses; oldest dropped first
SPAIN_MCP_CENSUS_DB unset Path to the census-by-tract file (age bands for districts and tracts). By default it is downloaded (about 3 MB) on first use.
SPAIN_MCP_OSM_DB unset Path to an OpenStreetMap snapshot file. By default it is downloaded (about 21 MB) on first use and refreshed monthly.

Architecture

src/spain_open_data_mcp/
  server.py        MCP tools, resource and error handling
  tools/           one module per tool: fetch, check, render
  sources/ine/     INE Tempus API client and table definitions
  sources/osm/     OpenStreetMap snapshot: business categories, download, queries
  places/          place resolver and INE codes
  core/            cached HTTP client, response envelope, errors
  data/            bundled municipality list and INE table map (built by scripts/build_reference.py)
  • Upstream responses are cached on disk with their original retrieval time, so cached figures still report when they were fetched. If an upstream service is down, the last cached copy is served and marked stale.
  • Requests are rate limited per host, retried with backoff, and identify the project in the User-Agent.
  • Business counts come from a monthly snapshot rather than live Overpass queries, which are slow and rate limited. scripts/build_osm_snapshot.py reads the Geofabrik extracts for Spain and the Canary Islands, keeps shops and services, and places each one in an INE census tract. A GitHub Action publishes it on the osm-snapshot-v1 release each month.
  • INE table ids are not hard-coded in the tools. scripts/build_reference.py rebuilds the map when INE publishes new tables, for example each October's income atlas.

Development

uv sync
uv run pytest            # offline tests against recorded INE responses
uv run ruff check . && uv run ruff format --check . && uv run pyright
uv run spain-open-data-mcp          # stdio
uv run spain-open-data-mcp http     # Streamable HTTP on 127.0.0.1:8000/mcp

Refresh the recorded test fixtures with uv run python tests/fixtures/record_ine.py. Build the OpenStreetMap snapshot locally (a 1.5 GB download, then about 5 minutes) with uv run --group snapshot python scripts/build_osm_snapshot.py, then point SPAIN_MCP_OSM_DB at build/osm/osm_businesses.sqlite.

Roadmap

  1. Skeleton: place resolver, demographics, income, CI.
  2. Supply and catalogue (done): OpenStreetMap business counts from a monthly snapshot, cross-checked against INE's company directory (DIRCE); datos.gob.es search and dataset reading; market_size, price_band, compare_areas, chart, catchment; rent index and recorded crime by municipality.
  3. Checks and evals: a golden set of real strategy questions with hand-checked answers, scored for accuracy, sourcing, refusals and calibration, with results here.
  4. Publishing: hosted connector, PyPI, MCP Registry, Claude connectors directory.

Data licences

The code is MIT-licensed. Data keeps its source licence; see data_licences.md. INE data is reused under CC BY 4.0: "Fuente: INE. Elaboración propia con datos extraídos del sitio web del INE: www.ine.es". OpenStreetMap data is © OpenStreetMap contributors, under the Open Database Licence.

Metadata

Release files for spain-open-data-mcp 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for spain-open-data-mcp 0.1.0
File Size Uploaded
spain_open_data_mcp-0.1.0.tar.gz 334.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for spain-open-data-mcp 0.1.0
File Interpreter ABI Platform
spain_open_data_mcp-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 691.2 kB

Release files / spain_open_data_mcp-0.1.0.tar.gz

Download URL spain_open_data_mcp-0.1.0.tar.gz
Size 334.2 kB
Tags Source
SHA-256 checksum
How to use checksums
bc44d104b8cb5f39dd57f5cec20a8bc598268a336c58feb3daa0589e18ced8b3
BLAKE2b-256 checksum
How to use checksums
94a374fe74aafe9397b9dbac0bf44ba42e74894ca2da9f7d6da35b07ad58d1db
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / spain_open_data_mcp-0.1.0-py3-none-any.whl

Download URL spain_open_data_mcp-0.1.0-py3-none-any.whl
Size 356.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ee763cfafde5740e80e6853a1514e33af2832ea3b6e81167f9e530b07999ccac
BLAKE2b-256 checksum
How to use checksums
c796939545a8e9e1e1b5c4ec859c11a7663e8dcb24afe5d2904096a7871cdff1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page