sdmx-data-mcp
An MCP server that lets AI assistants discover and retrieve official statistics from SDMX services, returning actual data observations rather than just query URLs.
README
sdmx-data-mcp
An MCP server that lets an AI assistant discover and retrieve official statistics from SDMX services.
Built on the BIS's own pysdmx library.
Why this exists
Other SDMX MCP servers navigate metadata well and then hand back a query URL. The assistant ends up with a link, not numbers — and cannot answer the question it was asked.
sdmx-data-mcp finishes the job. get_data returns the observations.
Quick start
pip install sdmx-data-mcp
Then register it with your client:
claude mcp add sdmx -- sdmx-data-mcp
That is the whole setup. Ask your assistant something like "what is the Swiss policy rate since 2020?" and it will find the dataflow, check what it can filter on, and come back with the actual series.
<details> <summary>Other clients</summary>
Claude Desktop — in claude_desktop_config.json:
{
"mcpServers": {
"sdmx": {
"command": "sdmx-data-mcp"
}
}
}
Cursor — in .cursor/mcp.json:
{
"mcpServers": {
"sdmx": {
"command": "sdmx-data-mcp"
}
}
}
If sdmx-data-mcp is not on your PATH, use the absolute path to the
executable, or "command": "python", "args": ["-m", "sdmx_data_mcp"].
Over HTTP, to share one instance:
sdmx-data-mcp --transport http --host 127.0.0.1 --port 8000
</details>
The four tools
Meant to be called in order. Only the last one moves data.
| Tool | Purpose |
|---|---|
list_services |
Known endpoints, plus any SDMX-REST v2 base URL you supply |
search_dataflows |
One dataflows() call, then local term matching |
inspect_dataflow |
Components, available codes, size signals |
get_data |
Retrieves the observations |
A real session
Transcripts below are actual tool output, not illustrations. More in docs/EXAMPLES.md.
1. Find the dataflow
search_dataflows(query="policy rate central bank")
{
"search_terms": ["policy", "rate", "central", "bank"],
"total_dataflows_on_service": 32,
"match_count": 12,
"dataflows": [
{ "ref": "BIS:WS_CBPOL(1.0)", "name": "Central bank policy rates",
"matched_on": "name" },
{ "ref": "BIS:WS_CBS_PUB(1.0)", "name": "Consolidated banking",
"matched_on": "name" }
// ...
]
}
One request to the service; the terms are matched locally. Searching for synonyms costs nothing extra, so put them all in one query.
2. See what you can filter on
inspect_dataflow(ref="BIS:WS_CBPOL(1.0)", find_code="CH")
{
"series_count": 98,
"obs_count": null, // BIS does not report this
"size_warning": null, // small enough to retrieve
"dimensions": [
{ "id": "FREQ", "name": "Frequency", "code_count": 2,
"codes": [{"id": "D", "name": "Daily"}, {"id": "M", "name": "Monthly"}] },
{ "id": "REF_AREA", "name": "Reference area", "code_count": 49,
"codes": [ /* ... */ {"id": "CH", "name": "Switzerland"} /* ... */ ] }
]
}
3. Get the numbers
get_data(
ref="BIS:WS_CBPOL(1.0)",
filters="FREQ = 'M' AND REF_AREA = 'CH' AND TIME_PERIOD >= '2020-01'",
columns=["OBS_VALUE"],
)
{
"filter_applied": "FREQ = 'M' AND REF_AREA = 'CH' AND TIME_PERIOD >= '2020-01'",
"filter_fallback": null,
"row_count": 79,
"total_rows_available": 79,
"truncated": false,
"records": [
{"SERIES_KEY": "M.CH", "OBS_VALUE": "-0.75", "TIME_PERIOD": "2020-01"},
{"SERIES_KEY": "M.CH", "OBS_VALUE": "-0.75", "TIME_PERIOD": "2020-02"}
// ...
],
"next_step": "Complete: all 79 matching rows were returned. Safe to aggregate."
}
truncated: false and row_count == total_rows_available, so this really is
the whole series and it is safe to average or chart.
Several values of one component
The query parser accepts AND but not OR. Use IN (...):
get_data(
ref="BIS:WS_XRU(1.0)",
filters="FREQ = 'M' AND CURRENCY IN ('CHF', 'KES') AND TIME_PERIOD >= '2026-01'",
columns=["OBS_VALUE"],
)
{ "row_count": 14, "truncated": false, "records": [
{"SERIES_KEY": "M.CH.CHF.E", "OBS_VALUE": "0.768269", "TIME_PERIOD": "2026-01"},
{"SERIES_KEY": "M.KE.KES.A", "OBS_VALUE": "129.126615", "TIME_PERIOD": "2025-12"}
// ...
] }
With both legs retrieved, an assistant can compute what neither service publishes — a CHF/KES cross rate — and correctly note that the newest period the two currencies share is December 2025, because KES lags CHF.
What the server enforces
These rules are handled by the server rather than left for the assistant to remember. Each one prevents a specific class of confidently wrong answer.
Conjunctions only. AND between clauses, never OR. Several values of one
component use IN ('A', 'B').
Availability is not validity. inspect_dataflow reports codes for which
data currently exist. A code missing from that list may still be valid in the
full codelist, so its absence is never reported as proof that something does
not exist.
Ambiguous codes are surfaced, not guessed. On BIS consolidated banking,
CH is available in three components at once — reporting country, counterparty
country, and a bank type that happens to share the country codelist:
inspect_dataflow(ref="BIS:WS_CBS_PUB(1.0)", find_code="CH")
{ "code_locations": [
{"component_id": "L_REP_CTY", "role_hint": "reporting country"},
{"component_id": "CBS_BANK_TYPE", "role_hint": "bank type (shares a country codelist)"},
{"component_id": "L_CP_COUNTRY", "role_hint": "counterparty country"}
],
"next_step": "That code is ambiguous - it appears in 3 components ..." }
Claims by Swiss banks and claims on Switzerland are different questions. The server makes the assistant choose rather than silently pick one.
Size before retrieval. obs_count is frequently unreported — the BIS
returns null for it on both full and filtered scopes — so series_count is
the signal relied on. A size_warning appears when retrieval would truncate.
Truncation is not sampling. When truncated is true, the rows are the
first N in service order, and next_step says so explicitly:
Truncated: 112,648 rows matched but only 500 were returned. These are the first rows in service order, not a sample — do not compute totals or averages from them.
Time filters degrade gracefully. TIME_PERIOD is pushed down to the
service first. On a client-side rejection the clause is stripped, the narrower
query is retried, and the cutoff is applied with pandas — reported in
filter_fallback. It deliberately does not fall back on NotFound,
Unavailable or InternalError, where dropping a clause cannot help and would
only obscure the real error.
Errors keep their meaning. The pysdmx error hierarchy is preserved rather than flattened, so an assistant can tell a transient outage from a bad reference instead of retrying blindly or giving up too early:
[internal_error] Unexpected message format - The payload could not be
deserialized. | retriable=false | next_step: The service failed, or returned a
response that could not be parsed. Do not repeat the identical call - narrow
the filter or try a different dataflow, since the fault is server-side.
| Kind | Retriable | Means |
|---|---|---|
unavailable |
yes | Service unreachable; retry after a delay |
retriable_error |
yes | Transient failure |
not_found |
no | Resource does not exist; do not repeat |
invalid_request |
no | Malformed filter; check syntax and code IDs |
unauthorized |
no | Credentials rejected |
not_implemented |
no | Service lacks the required API |
internal_error |
no | Server-side fault; narrow or change the query |
unexpected_error |
no | Bug in this server; please report it |
Services
pysdmx.api.dc.Endpoints currently ships exactly one endpoint, the BIS. Every
tool therefore takes a service argument accepting any SDMX-REST v2 base
URL as a first-class input, not a fallback:
search_dataflows(query="prices", service="https://your-service.org/api/v2")
The service must return structural metadata as SDMX-JSON 2.0.0 and data as SDMX-CSV. Other providers (ECB, OECD, IMF, Eurostat, ILO) are deliberately not hardcoded: each needs verifying against those requirements first, and listing them unverified would invite confident failures.
Development
pip install -e ".[dev]"
ruff format && ruff check && mypy
pytest --cov=sdmx_data_mcp --cov-branch --cov-report=term-missing
162 tests at 100% statement and branch coverage, mypy in strict mode, CI across Linux, Windows and macOS on Python 3.10–3.13.
Most server tests inject a fake connector so every branch is reachable
deterministically. A separate end-to-end module drives the real
PandasConnector against respx-mocked responses, so drift in URL
construction or SDMX-CSV parsing surfaces there.
Relationship to pysdmx
This package depends on pysdmx[data] from PyPI. It does not fork or vendor
it, and uses only the public API — PandasConnector, Endpoints and the
errors hierarchy.
The same server has also been proposed upstream as bis-med-it/pysdmx#669. This package exists so it is installable today regardless of what happens there.
Licence
Apache-2.0. See LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.