Open Data MCP
Click on "Deploy 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., "@Open Data MCP Скільки звернень у Центральному районі?"
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.
Open Data MCP
An MCP server and a small reference agent that answer questions about Ukrainian open data (CKAN portal data.gov.ua) from retrieved data and deterministic SQL, not from model memory. Every successful data-backed answer carries machine-readable provenance.
Status: working prototype, not production software. The CKAN adapter, SQL safety layer, MCP server, agent and evaluation are implemented and tested offline against a mocked portal, SYNTHETIC fixtures and one committed real
data.gov.uasnapshot (CC BY). A live smoke test againstdata.gov.uapassed on 2026-10-18 (CSV path only; see Verification status for exactly what was and was not exercised).
The problem
Asking an LLM "how many … were registered in region X in 2023?" invites a confident, unsourced, possibly invented number. Open-data portals publish the real figures, but finding the right resource, understanding its columns and computing an aggregate is tedious. This project lets a model do exactly that through tools, with a hard boundary: numbers come from a SQL query over the real resource, and the answer shows where the data came from and when it was fetched.
Related MCP server: Datos.gob.es-MCP
What you can ask
Questions whose answer is a computation over one tabular resource, in Ukrainian or English:
"Скільки звернень у Центральному районі?" →
COUNTwith a filter"Total planned budget for 2024 across regions?" →
SUMover a DataStore resource"Which region executed the most in 2023?" →
GROUP BY … ORDER BY"Find datasets about air quality" → dataset search
It should refuse or abstain when the data cannot answer (forecasts, opinions, unsupported formats, oversized files, columns that do not exist).
Architecture
flowchart LR
U[User question] --> A[Reference agent<br/>Anthropic Messages API<br/>bounded tool loop]
A <-->|MCP over stdio| S[MCP server<br/>search_datasets · get_schema · query]
S --> C[CKAN client<br/>retries · TTL cache · success check]
S --> F[Safe fetch<br/>HTTPS only · no private IPs · size cap]
C -->|package_search / package_show / datastore_search| P[(data.gov.ua CKAN API)]
F -->|resource URL from validated metadata| P
S --> Q[SQL guard<br/>SQLGlot AST allowlist]
Q --> D[DuckDB in-memory<br/>one table: data<br/>external access disabled]
D --> R[rows + source object]
R --> ASource layout (src/open_data_mcp/): ckan_client.py, safe_fetch.py, query_engine.py,
provenance.py, service.py (pure-Python logic), server.py (MCP), agent.py, cli.py,
fakeportal.py (synthetic fixtures for offline use).
Why MCP, and the three tools
MCP keeps the data layer independent from any one model or UI: the same server works with the bundled agent, Claude Desktop/Code, or any MCP client. All tools are read-only and publish JSON input and output schemas.
Tool | Purpose |
| Dataset ID, title, short description, organization, license, URL, resource formats. |
| Columns/types and how they were obtained: |
| One read-only |
Every tool returns an envelope with ok. On failure: ok: false, an error object with a stable
code (unsafe_sql, unsafe_url, unsupported_resource, resource_too_large, sql_error,
not_found, upstream_error, …) and a hint; no rows, no numbers.
Provenance
build_source() is the only way a source object is made, and assert_provenance() refuses to
return a query result without portal, dataset_id, resource_id and retrieved_at_utc.
"source": {
"portal": "...", "dataset_id": "...", "dataset_title": "...", "dataset_url": "...",
"resource_id": "...", "resource_name": "...", "resource_url": "...",
"retrieved_at_utc": "2026-10-18T20:20:04Z", // when THIS SERVER fetched the data
"metadata_retrieved_at_utc": "...", // metadata may be older (TTL cache)
"dataset_modified_at": null, // the portal's own date; null if not supplied
"resource_modified_at": null, "license": null, "access_method": "file_download"
}retrieved_at_utc is never used as a stand-in for the portal's publication/modification date;
unknown dates stay null. The tool also returns a one-line citation, and the reference agent
appends any citation the model forgot to include, so the source always reaches the user.
How read-only SQL is constrained
AST allowlist (SQLGlot, DuckDB dialect): exactly one statement; root must be
SELECT/UNION/EXCEPT/INTERSECT(CTEs allowed). Every table reference must bedataor a CTE defined in the query: no file paths, URLs,schema.table, or table functions (read_csv,glob,range, …). Functions must be typed SQLGlot functions or on a small allowlist;getenv,read_text,current_setting, … are rejected. Parameters are rejected.Regenerated SQL: the statement DuckDB runs is rendered from the validated AST, with a server-side
LIMIT(default 20, hard max 100).Engine lockdown: data is loaded into an in-memory table first; then
enable_external_access=false, extension auto-loading off andlock_configuration=true. Tests confirm that even rawread_csv('/etc/passwd'),COPY … TO,INSTALLandSETfail at the DuckDB level.Resource limits: 25 MB download cap, 500 000 row cap (refused, never silently truncated, so an aggregate always covers the entire resource), 20 s query timeout, 512 MB memory limit.
No LLM-supplied URLs: the model passes
dataset_id+resource_id; the URL comes from CKAN metadata, must behttps://on port 443, must resolve only to public addresses, and every redirect hop is re-validated.
Supported formats: CKAN DataStore, CSV (UTF-8 or Windows-1251, any delimiter DuckDB sniffs) and
JSON (array of flat records). XLS/XLSX, PDF, archives and other binaries are refused with
unsupported_resource.
Setup
Requires Python ≥ 3.11 (developed and tested on 3.13.16; the suite also passes on 3.11.17) and
uv.
git clone https://github.com/AnatoliiBalakiriev/Open-Data-MCP && cd Open-Data-MCP
uv sync --lockedRun the MCP server
uv run open-data-mcp serve # live portal (default https://data.gov.ua)
uv run open-data-mcp serve --offline-fixtures # SYNTHETIC data, no network
uv run open-data-mcp serve --offline-snapshot # replay of one real data.gov.ua resource, no networkConnect an MCP client (stdio). Example for Claude Code / Claude Desktop style config:
{
"mcpServers": {
"open-data": {
"command": "uv",
"args": ["run", "--directory", "/path/to/Open-Data-MCP", "open-data-mcp", "serve"]
}
}
}(The stdio protocol path is covered by an automated test with the official MCP client; the config above has not been tried in Claude Desktop itself.)
Ask a question through the reference agent
cp .env.example .env # then put ANTHROPIC_API_KEY in .env
uv run open-data-mcp ask "Скільки звернень у Центральному районі?" --offline-fixtures
uv run open-data-mcp ask "Find datasets about air quality" # live portalThe agent uses the Anthropic Messages API (ANTHROPIC_MODEL, default claude-sonnet-5-5, checked
against the official models overview page on 2026-10-18), connects to the server over stdio,
forwards MCP tool definitions as Anthropic tools, and runs a loop capped at 8 model calls
(--max-iterations). Without an API key it prints setup instructions and exits with code 2;
the server, tests and offline evaluation never need a key.
Behind an HTTP proxy that does the DNS resolving (no local DNS), the SSRF guard cannot resolve
resource hosts and refuses them. List hosts you trust explicitly:
OPEN_DATA_TRUSTED_DOWNLOAD_HOSTS=data.gov.ua,opendata.gov.ua (HTTPS is still required).
Tests, lint, evaluation
uv run pytest # offline: mocked CKAN, synthetic fixtures, real snapshot
uv run ruff check . && uv run ruff format --check .
uv run mypy
uv run python evals/run_eval.py # offline evaluation (Level 1)
uv run python scripts/smoke_live.py # live smoke test against data.gov.ua (needs network)
uv run python scripts/capture_snapshot.py # re-capture the real snapshot (needs network)Evaluation results
Generated by uv run python evals/run_eval.py --update-readme. Level 1 only: it sends the
gold SQL through the real MCP query tool and compares with answers computed independently in
plain Python. Two data sources are used: synthetic fixtures (invented, behind a fake CKAN
portal) and one real data.gov.ua resource (education institutions of Ivano-Frankivsk, CC BY,
a committed byte-exact snapshot captured 2026-10-19 and replayed offline; see
ATTRIBUTION.md). Level 1 validates the data
path (SQL safety, loading, provenance, refusals). It does not measure how accurately an LLM
writes SQL or words its answer; that is Level 2 (--live, needs an API key, costs money, not
run for this README).
Level 1 - tool pipeline (offline, deterministic)
Synthetic fixtures are invented test data; the real snapshot is one data.gov.ua resource (CC BY) replayed offline. Details: evals/README.md.
Metric | Synthetic fixtures | Real data.gov.ua snapshot | Total |
Answer correctness (numeric/categorical) | 20 / 20 | 11 / 11 | 31 / 31 |
Provenance present and correct | 20 / 20 | 11 / 11 | 31 / 31 |
Correct abstention / refusal | 7 / 7 | 1 / 1 | 8 / 8 |
Skipped (agent-level only, need a live LLM): live-forecast, live-opinion, real-live-forecast.
Failed cases: 0
Details, case list and limitations: evals/README.md.
Verification status
What | Status |
Unit/integration tests, Ruff, mypy, offline evaluation | Run locally (see results above and in your own run). |
MCP server over real stdio subprocess + official MCP client | Tested (automated, offline fixtures). |
GitHub Actions workflow | Written; not yet executed on GitHub. |
Live | Passed on 2026-10-18 (~21:04 UTC): |
Real-data evaluation | One real resource (128 rows, Windows-1251, |
Not exercised live | The DataStore path: none of the ~25 sampled resources had |
Live behaviour that shaped the code | The portal sits behind Cloudflare and returns intermittent HTTP 522 (now retried for API calls and downloads); some resources declare their format with Cyrillic look-alike letters ( |
Live agent run with a real Anthropic key | Not run (no key in the build environment). The loop is tested with a scripted model, including over a real stdio subprocess. |
Data source, license, limits
Source: the CKAN Action API of data.gov.ua (
/api/3/action/…). Base URL is configurable (OPEN_DATA_PORTAL_URL), so other CKAN portals can be tried.Licensing: each dataset has its own license; the server returns it in
source.license(ornull). This repository's code is MIT-licensed. The synthetic fixtures are invented test data, not government data. The one real snapshot undersrc/open_data_mcp/fixtures/real/is © its publisher (Департамент освіти та науки Івано-Франківської міської ради), licensed CC BY, and attributed inATTRIBUTION.md.Freshness: dataset metadata is cached for 300 s by default (
OPEN_DATA_CACHE_TTL_S); data is fetched on every query, never cached. Provenance records both timestamps.Rate limits: unknown for data.gov.ua; the client retries 429/5xx, Cloudflare 52x and transient network errors up to 3 times with exponential backoff, honouring
Retry-After. There is no throttling.Known limitations: messy real-world CSVs (title rows, merged cells) may load as garbage columns; always check
get_schemasample rows; each query re-downloads the resource; no XLSX/Parquet; CSV type inference is DuckDB's best guess; very wide or huge files are refused; DNS answers are validated before the request but the HTTP library resolves again (a determined DNS-rebinding attacker who controls a resource host is not fully excluded); no authentication, so do not expose the server to untrusted networks; one dataset resource per query (no joins across resources).
Security notes
Treat all portal metadata and file contents as untrusted. The LLM never supplies URLs or paths;
SQL is validated by AST, executed in an engine with external access disabled, and bounded in time,
memory, rows and bytes. Tool errors never include response bodies, and secrets are only read from
the environment (.env is git-ignored).
Roadmap
Verify the DataStore path on a portal resource that has it enabled, and grow the real-data part of the evaluation beyond a single resource.
Level 2 evaluation of the agent with a recorded run (tracked cost and failure analysis).
XLSX support and a cached, size-bounded download layer with freshness checks.
Available Tools
3 toolsget_schemaBRead-only
Describe a resource's columns and how the schema was obtained (DataStore metadata = authoritative; inferred_from_file = best guess). Also lists the dataset's resources.
Inputs: dataset_id (required), resource_id. Returns: ok, note, error, schema, +13 more
queryARead-only
Run read-only SQL over one tabular resource (DataStore, CSV or JSON). The SQL always covers the whole resource or the call fails; only the returned rows are capped.
Inputs: sql (required), dataset_id (required), resource_id (required), limit. Returns: ok, note, rows, error, +8 more
search_datasetsARead-only
Search open-data datasets. Returns IDs, titles, short descriptions, organization, license and dataset URL for each hit.
Inputs: query (required), limit. Returns: ok, error, query, portal, +4 more
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 3 tools
Each tool has a distinct purpose: search_datasets for discovery, get_schema for structural metadata, and query for data extraction. There is no overlap or ambiguity in what each tool does.
search_datasets and get_schema follow a clear verb_noun pattern, but 'query' deviates as a bare verb. The inconsistency is minor and the names remain readable.
Three tools are within the typical well-scoped range (3-15). Each tool is essential for the core open-data workflow: discovery, inspection, and querying.
The surface covers search → schema → query, but there is no direct way to list resources for a dataset or retrieve full dataset metadata. The gap between search_datasets (returns dataset IDs) and get_schema (requires a resource ID) forces workarounds.
Maintenance
Related MCP Connectors
Public Data Ukraine Mcp connects AI agents to real public APIs via MCP. Tools include
Experimental read-only MCP over metadata of Ukrainian court decisions (DSA/EDRSR open data).
Macroeconomic and other official data from 170+ publishers, resolved from natural language with provenance.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceProvides seamless access to official Ukrainian government statistics from the State Statistics Service via their SDMX API. It enables users to query bilingual datasets across domains like energy, demographics, and trade using flexible filtering and metadata exploration tools.8 npm50MIT
- AlicenseAqualityDmaintenanceEnables querying and analyzing over 90,000 public datasets from the Spanish Government Open Data Portal (datos.gob.es) using natural language, with tools for search, filtering, metadata access, and SPARQL queries.105MIT
- AlicenseAqualityAmaintenanceEnables AI assistants to search, explore, and query any CKAN open data portal through natural language, making public datasets accessible without requiring knowledge of the portal's API.202,654 npm60MIT
- AlicenseAqualityBmaintenanceEnables AI agents to search and analyze Ukraine's national open-data portal (data.gov.ua) using natural language, with tools for finding datasets, inspecting metadata, and retrieving actual data.6MIT