Statline MCP
Provides access to CBS StatLine, the open data platform of Statistics Netherlands, enabling search of statistical tables by keyword or theme, inspection of table dimensions and measures, resolution of dimension codes, and retrieval of filtered observations with readable labels.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Statline MCPWhat was the population of Amsterdam in 2023?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Statline MCP
Dutch official statistics, queryable by any language model.
An MCP server over CBS StatLine, the open data platform of Statistics Netherlands. It turns a plain question into a real, citable statistic: find the right table, read its structure, resolve the codes, return the numbers.
5,956 tables · 4.1 billion observations · Dutch and English
Contents
Related MCP server: nl-opendata-mcp
What it does
StatLine holds nearly 6,000 statistical tables, but they are addressed by opaque codes.
83583NED is a table, 2023JJ00 is the year 2023, T001098 is a total, GM0363 is
Amsterdam. No model can guess these. Statline MCP gives a model typed tools to discover
tables, look codes up, and query observations, so every figure it reports is traceable to a
named table rather than invented.
Tools
Tool | Purpose |
| Find tables by keyword, in Dutch or English ( |
| Find tables by topic through the subject taxonomy. Full English tree. |
| A table's dimensions (what you filter on) and measures (the numbers). |
| The valid codes of one dimension, with |
| Filtered observations, with codes resolved to readable labels. |
Every response ends with a Next: line naming the tool to call next, so the chain is
self-guiding.
Architecture
Two hops and two protocols:
Hop | From | To | Protocol |
1 | Your language model, via an MCP client | Statline MCP | MCP, over stdio or Streamable HTTP |
2 | Statline MCP | StatLine open data | HTTPS, OData |
server.py publishes the five typed tools and a /health route. cbs.py holds the async
OData client and the themes cache, and carries no MCP dependency, so it is usable on its
own. Everything is read-only.
How to use it: the two connections
Using Statline MCP means wiring up two links. Get both right and any tool-calling model can query Dutch statistics.
Connection 1: the Statline MCP server
Pick one of these. The server is identical in each case, only the transport differs.
Local subprocess (stdio), simplest for desktop use:
pip install -r requirements.txt fastmcp run server.pyLocal HTTP, when a client needs a URL:
python server.py # http://localhost:8000/mcp (override with PORT)Hosted, a URL anyone on your team can point at:
https://<your-server>.fastmcp.app/mcp # FastMCP Cloud https://statline-mcp.<your-cluster>/mcp # your own KubernetesSee Deployment for both.
Confirm any running server with:
python scripts/health_check.py --url http://localhost:8000/mcpConnection 2: your language model
Claude Code, local server:
claude mcp add statline -- fastmcp run /absolute/path/to/Statline_MCP/server.pyClaude Code, hosted server:
claude mcp add --transport http statline https://<your-server>.fastmcp.app/mcpIf the server has authentication enabled it answers
401until you sign in. Run/mcpinside an interactive Claude Code session and complete the browser flow. For CI or a non-interactive agent, pass a token instead:claude mcp add --transport http statline https://<your-server>.fastmcp.app/mcp \ --header "Authorization: Bearer $TOKEN"Verify with
claude mcp list, where you want✔ Connected.An open-weights model (Qwen, Llama, Mistral, DeepSeek) behind vLLM, Ollama, llama.cpp, TGI or SGLang. An MCP tool's
inputSchemais already valid JSON Schema, so it drops straight into an OpenAIfunction.parametersblock:def to_openai_tools(mcp_tools): return [{"type": "function", "function": {"name": t.name, "description": t.description or "", "parameters": t.inputSchema}} for t in mcp_tools]examples/open_llm_client.pyis a complete working loop:# prove the wiring with no model at all python examples/open_llm_client.py --dry-run # a self-hosted model against a deployed server python examples/open_llm_client.py \ --mcp https://statline-mcp.example.k8s.nl/mcp \ --api-base http://localhost:8000/v1 \ --model Qwen/Qwen3-32B-Instruct \ "How many births were registered in 2023?"Any other MCP client, such as LangChain, LlamaIndex, Cursor or Continue, connects to the same URL with no bridge at all.
Driving it from a smaller model
Four things matter:
Keep tool descriptions verbatim. They carry the operational knowledge: codes are opaque, Dutch has wider coverage, use
browse_themeswhen search fails. Truncating them to save context is what makes a model start inventing table identifiers.Return tool errors as text, not exceptions. Error messages name the valid dimensions and measures, so a model that guessed wrong can correct itself on the next turn.
Pin a system prompt against fabrication. Require every figure to come from a tool result, and require the table identifier to be stated.
Cap the loop so a model that never commits to an answer cannot spin forever.
A question, end to end
"How many employee jobs were there in Dutch manufacturing in December 2023?"
Find the table. The keywords become a catalog query. Every word must appear in a title or description, so three words narrow hard.
search_tables(query="banen werknemers bedrijfsgrootte", language="nl") -> 83583NED Banen van werknemers; bedrijfsgrootte en economische activiteit period 2010-2024 · yearly · 11,160 rowsRead its structure.
get_table_info(table_id="83583NED") dimensions: BedrijfstakkenBranchesSBI2008, Bedrijfsgrootte, Perioden measures: BanenVanWerknemersInDecember_1 [x 1 000]Resolve the codes. "Manufacturing" and "December 2023" are words, but the table wants codes.
get_dimension_codes(table_id="83583NED", dimension="BedrijfstakkenBranchesSBI2008", search="industrie") -> 307500 "C Industrie"Query.
get_data(table_id="83583NED", filters={"Perioden": ["2023JJ00"], "BedrijfstakkenBranchesSBI2008": ["307500"], "Bedrijfsgrootte": ["T001098"]})Bedrijfstakken…
…_label
Bedrijfsgrootte
…_label
Perioden
…_label
BanenVanWerknemers…
307500
C Industrie
T001098
Totaal
2023JJ00
2023 december
793.3
The answer: roughly 793,300 employee jobs in Dutch manufacturing in December 2023,
since the measure's unit is x 1 000, from table 83583NED.
Each step's output is the next step's input, and codes are always looked up rather than guessed. That is the whole design: the model never has to invent an identifier, so its answer stays traceable.
The StatLine data model
A table is a hypercube stored long-format. Four terms cover everything:
Term | What it is | Example |
Table | One dataset, identified by a code |
|
Dimension | An axis you filter on |
|
Code | One allowed value of a dimension |
|
Measure | A column holding actual numbers, with a unit |
|
One observation carries a code per dimension and a value per measure:
{ "BedrijfstakkenBranchesSBI2008": "307500",
"Bedrijfsgrootte": "T001098",
"Perioden": "2023JJ00",
"BanenVanWerknemersInDecember_1": 793.3 }Columns are typed. Dimensions come as Dimension, TimeDimension (usually Perioden), or
GeoDimension and GeoDetail for geography. Measures come as Topic, each with a Unit
and Decimals. A TopicGroup is only a heading, never a column.
Scale
Count | |
Tables | 5,956 |
Observations across all tables | 4,108,824,238 |
Theme nodes | 1,299 |
Table to theme links | 8,122 |
Two languages. The catalog is bilingual but lopsided: 4,889 Dutch tables and 1,067
English, the English set being a translated subset with identifiers ending ENG. A table
exists in one language only, and its title, dimension names and code labels all follow it.
Both discovery tools therefore take language. Dutch gives far wider coverage, English
needs no translation.
StatLine architecture
StatLine is exposed as OData. A catalog describes what exists, and per-table endpoints describe and hold each dataset.
flowchart TD
subgraph cat["Catalog: what exists"]
T["Tables<br/>5,956 · titles, periods, language"]
TH["Themes<br/>1,299 · subject hierarchy"]
TT["Tables_Themes<br/>8,122 links"]
end
subgraph tbl["Per table, e.g. 83583NED"]
TI["TableInfos<br/>title, period, status"]
DP["DataProperties<br/>dimensions + measures"]
CL["One code list<br/>per dimension"]
DS["TypedDataSet<br/>the observations"]
end
TH --- TT
TT --- T
T -->|"Identifier"| TI
TI --- DP
DP -->|"names each"| CL
CL -->|"codes filter"| DS
style cat fill:#e7f5ff,stroke:#0b7285
style tbl fill:#f3f0ff,stroke:#5f3dc4Resource | Contents |
| Every table, with 26 metadata fields |
| The subject hierarchy, 1,299 nodes |
| 8,122 links joining themes to tables |
| Title, period, frequency, language, status, source |
| The table's dimensions and measures |
| That dimension's code list |
| The observations, numbers typed |
Discovery flows top to bottom. The catalog yields an identifier, DataProperties names the
dimensions, each dimension names its codes, and the codes filter the dataset. The five tools
map one to one onto that path:
search_tablesandbrowse_themesquery the catalog and the taxonomy.get_table_inforeadsTableInfosandDataProperties.get_dimension_codesreads one dimension's code list.get_datafiltersTypedDataSetand joins the code lists back in as labels.
Themes and the taxonomy
CBS classifies its tables in the subject hierarchy behind the StatLine website's navigation,
published as data so browse_themes can walk it.
It is two parallel trees, not one translated tree:
Theme nodes | Leads to | |
Dutch ( | 1,060 | Dutch tables |
English ( | 239 | English ( |
Structure is expressed purely by ParentID, so the tree is rebuilt client-side. All 1,299
nodes arrive in one request, so the server fetches them once, caches them for the process
behind an asyncio.Lock, and computes paths in memory. A resolved path:
Arbeid en sociale zekerheid > Arbeid en arbeidsmarkt > Banen, vacatures, werkgelegenheid > Banen
-> 83583NED, 83582NED, 86205NED, 84826NED, …Tables sit on leaves, so browsing means descending: a parent theme usually lists sub-themes and no tables. 8,122 links across 5,956 tables means many tables are filed under several themes.
Why two discovery tools
They fail in opposite directions:
|
| |
Finds | specifically named tables | topic areas |
Needs | the right word, right language | only a rough topic |
English reach | ~1,100 of 5,956 tables | full parallel tree |
Fails when | your vocabulary is wrong | your term is too specific |
Searching themes for "Births" returns nothing, because that is a table name and not a
topic (Population development is the theme). Conversely an English question can navigate
the English tree to real data with nothing translated. When one route is empty the other is
the intended recovery, and the server's instructions say so.
Deployment
FastMCP Cloud
Builds from requirements.txt, entrypoint server.py:mcp, redeploys on every push to
main, and gives you https://<name>.fastmcp.app/mcp. Authentication is a per-server
toggle: turn it off for an open endpoint, or keep it and connect with OAuth as above.
Docker
docker build -t statline-mcp .
docker run --rm -p 8000:8000 statline-mcpRuns unprivileged (uid 10001) with a read-only root filesystem, and its HEALTHCHECK
completes a real MCP handshake rather than probing the port.
Kubernetes
deploy/k8s/ holds a Deployment, Service and Ingress for the
GitHub to registry to cluster pattern:
kubectl kustomize deploy/k8s | kubectl apply -f -The server lands at https://<host>/mcp. Three things worth knowing:
Probes hit
/health, not/mcp. A bareGET /mcpis not a valid protocol request, so anhttpGetprobe against it would never pass.server.pyexposes a plain200at/healthfor exactly this./healthdoes not query upstream, on purpose. A probe reports whether this process is serving, and a slow upstream should not restart your pods. Usescripts/health_check.pyas astartupProbefor a deeper gate.Raise the ingress read timeout. Streamable HTTP holds responses open, and a default 60s cuts long tool calls off mid-stream. The manifest sets 300s and disables proxy buffering.
.github/workflows/release.yml builds and pushes the image
to GHCR on every push to main, then boots it and probes /health before calling the
release good.
Languages and stack
Language | Lines | Used for |
Python | ~1,500 | The server, the OData client, tests, the open-LLM example |
YAML | ~270 | Kubernetes manifests and GitHub Actions workflows |
Markdown | ~460 | This documentation |
Dockerfile | ~40 | Container image |
TOML | 8 | Ruff lint configuration |
Python 3.10+, tested on 3.10, 3.12 and 3.13. Two runtime dependencies:
Package | Role |
| MCP server framework, stdio and Streamable HTTP |
| Async HTTP with connection pooling |
Tooling: ruff for lint and formatting, GitHub Actions for CI, Docker and Kustomize for deployment.
Layout
Path | Purpose |
| The five tools and |
| Async StatLine client, with no MCP dependency, usable on its own. |
| 41-check end-to-end test against the live API. |
| Probe a running server, shallow or deep. |
| Tool-calling loop for any OpenAI-compatible model. |
| Self-hosting. |
|
|
Development
python smoke.py # end-to-end against the live API
ruff check . && ruff format --check . # lintCI runs on push, on pull request, and weekly. The weekly run catches upstream changes before a user does.
Configuration
Variable | Default | Purpose |
|
| Port for the HTTP transport. |
|
| How your traffic identifies itself upstream. |
|
| Default endpoint for the scripts. |
| none | Bearer token for |
If you fork or self-host, set STATLINE_USER_AGENT. Every upstream request carries this
header so the data provider can see who is calling and has a contact point. Left at the
default, your deployment's traffic is attributed to this repository rather than to you, and
any rate limit ever applied to that identifier would follow you with it. Give it your own
name and an address someone could reach you at:
export STATLINE_USER_AGENT="acme-stats/1.0 (+https://acme.example/contact)"Licensing
Code is MIT. Use it, fork it, ship it commercially, and keep the copyright notice.
Data retrieved through this server is © Statistics Netherlands (CBS) and published under CC BY 4.0. You may reuse it freely, including commercially, provided you attribute Statistics Netherlands.
If you publish figures obtained through Statline MCP, credit them like this:
Source: Statistics Netherlands (CBS), StatLine table
83583NED, retrieved 2026-08-31. Licensed under CC BY 4.0.
Citing the table identifier and the retrieval date matters, because StatLine tables are revised and figures are marked provisional or final. The identifier makes any number reproducible.
This project is independent, and is not endorsed by or affiliated with Statistics Netherlands.
Author
Built by @athithyai. Contributions welcome, so open an issue or a pull request.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Statistics Netherlands (CBS / StatLine) OData MCP.
Search and query 1,500+ OECD statistical datasets via SDMX. Keyless.
Query official statistics of Catalonia (Idescat): tables, metadata and JSON-stat data via MCP.
Query UK Parliament, elections, crime stats, ONS census data, and national archives
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceEnables LLMs to search, access, and retrieve official Swedish statistics from Statistics Sweden (SCB), providing access to 1,200+ tables covering demographics, economy, environment, labor market, and education with 75+ years of historical data.7
- AlicenseAqualityAmaintenanceEnables accessing and querying Dutch government open datasets from CBS and data.overheid.nl, with tools for searching, filtering, downloading, and analyzing data using CSV, Parquet, DuckDB, or Pandas.9MIT
- AlicenseNot gradedqualityCmaintenanceEnables querying Germany's official statistics from Destatis GENESIS-Online through natural language or direct tool calls.17MIT
- FlicenseNot gradedqualityDmaintenanceExposes the Eurostat Statistics API, enabling LLMs to discover, explore, and retrieve official EU statistical data through search, dimension inspection, and data retrieval tools.3
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/athithyai/Statline_MCP'
If you have feedback or need assistance with the MCP directory API, please join our Discord server