ONS + Nomis MCP server
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., "@ONS + Nomis MCP server get the latest UK unemployment rate"
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.
ONS + Nomis MCP server
An MCP server over two UK statistics platforms, exposed from one server:
ONS beta Customise My Data (
https://api.beta.ons.gov.uk/v1) — headline economic and social time series (CPIH, regional GDP, wellbeing). Open, no key, rate-limited to 120 req/10s and 200/min.Nomis (
https://www.nomisweb.co.uk/api/v01) — census and labour-market statistics (Claimant Count, Annual Population Survey, Census 2011/2021 tables, workforce jobs). Open; an optionaluid(NOMIS_API_KEY) lifts limits for large queries.
The two APIs share no dimension model, so the Nomis tools are namespaced
nomis_*. Rule of thumb: ONS beta for headline economic indicators,
Nomis for granular local-area, census and labour-market tables.
Implementations
Two interchangeable implementations of the same 18-tool surface live here:
Python (
ons_mcp/, FastMCP) — the reference implementation..NET / C# (
dotnet/, ASP.NET Core + the MCP C# SDK) — a like-for-like port, and what runs in production.
Both expose the identical tools and behaviour. The live service is
https://ons-mcp.library-apps.dev/mcp (the .NET container, behind Cloudflare
Access). See dotnet/README.md for the port and
docs/deploy.md for deployment.
Related MCP server: mcp-uk-ons
Which route to use
For headline indicators — use get_timeseries. It serves the
published figure straight from the ONS website's series (the same number
ONS quotes), already chronologically ordered.
For multi-dimensional slicing — use the CMD dataset tools. They can filter by geography, age, category etc. in ways the published series can't.
CMD curation is uneven. Some datasets track publication closely (
weekly-deaths-region, updated within days); others lag badly (cpih01was five months behind the published CPIH series in July 2026;regional-gdp-by-yearwas three years behind).get_datasetandsearch_datasetsreturnlast_updatedand astaleness_warning— check them before quoting a CMD figure.
Option codes can also change between versions (cpih1dim1A0 in v1 became
CP00 in v67). A wrong code returns zero observations, not an error —
get_observations now diagnoses this and lists valid codes.
ONS beta API — data model
Datasets are held in tidy format. Every dataset has a time and a
geography dimension plus one or more topic dimensions. A dataset is
published as versions, grouped into editions. A single observation is
addressed by one option code per dimension; one dimension may be a *
wildcard to sweep a whole axis (e.g. a full time series) in one call.
Tools
The tools are designed to be called in sequence, discovery-first:
Tool | Purpose |
| Published headline series by URI — prefer for latest rates |
| Page through the catalogue |
| Client-side substring search over title/description/keywords |
| Resolve a dataset's latest edition + version |
| List a version's variables |
| List valid option codes for one dimension |
| Fetch observation(s); one dimension may be |
| Provenance and bulk CSV/XLSX download links |
| Live release calendar — upcoming/published/cancelled |
| One release: confirmed vs provisional date, related datasets |
| Browse the ONS topic taxonomy (root, or a topic's subtopics) |
| Publications under a topic; bridges series to |
Typical flow: search_datasets("inflation") → get_dataset("cpih01") →
get_dimensions(...) → get_options(..., "aggregate") →
get_observations(..., {"time": "*", "geography": "K02000001", "aggregate": "CP00"}).
Geography uses ONS codes, e.g. K02000001 = United Kingdom.
Discovery: releases and topics
Release calendar. list_releases(status="upcoming") is the
authoritative schedule of ONS publications — use it for "when is the next
X out", not the next_release field on get_timeseries/get_dataset,
which is a snapshot from the last publication and can be stale (it reported
15 July when the calendar said 22 July). get_release(uri) gives one
entry's confirmed-or-provisional date, any cancellation notice, and the
datasets it will contain.
Topics. list_topics() returns the top-level taxonomy; pass a topic id
(list_topics(parent_id="1245")) to drill into subtopics.
get_topic_content(topic_id) lists the bulletins, articles and key series
under a topic — and fills in timeseries_uri on series items so you can
hand them straight to get_timeseries.
Nomis tools
Tool | Purpose |
| Keyword search over the Nomis catalogue |
| Dataset overview + dimensions (with |
| Valid codes for a dimension (e.g. sex, age, measures) |
| Resolve a place name to Nomis geography code(s) |
| Fetch observations (simple flat JSON) |
| Build a direct CSV/XLS/JSON bulk-download URL |
Nomis uses internal integer codes, not ONS mnemonics, and they differ
per dataset — always resolve via nomis_dimension_options /
nomis_geography_search first. Data retrieval uses Nomis' simple JSON
format (flat records with named dimensions), avoiding SDMX positional-key
decoding. measures=20100 (value) is usually required.
Typical flow: nomis_search_datasets("claimant") →
nomis_get_dataset("NM_1_1") → nomis_geography_search("NM_1_1", "Birmingham") and nomis_dimension_options("NM_1_1", "sex") →
nomis_get_data("NM_1_1", {"geography": "...", "sex": "5,6,7", "time": "latest", "measures": "20100"}).
Install
python -m venv .venv && . .venv/bin/activate
pip install -e .Run
Stdio (default, for desktop clients):
ons-mcpStreamable HTTP (for a self-hosted deployment behind a tunnel), served at
/mcp:
ONS_MCP_HTTP=1 ONS_MCP_PORT=8095 ons-mcpBound to 127.0.0.1 by design — front it with Cloudflare Tunnel rather
than exposing the port. In production it runs at
https://ons-mcp.library-apps.dev/mcp, fronted by Cloudflare Access
(Managed OAuth), with the origin re-verifying the Access JWT for defence in
depth. See docs/deploy.md for the systemd unit, tunnel
ingress and auth setup.
Verify
scripts/verify.py exercises both halves — the published-series route
(get_timeseries) and the CMD route (search → observations, with
staleness and the invalid-code diagnosis) — through the real in-memory MCP
dispatch path against the live ONS API, plus the Cloudflare Access guard
wiring:
python scripts/verify.pyClient config (stdio)
{
"mcpServers": {
"ons-beta": {
"command": "ons-mcp"
}
}
}Environment
Variable | Default | Meaning |
|
| API root |
|
| Per-request timeout (s) |
| unset | Set to |
|
| HTTP port (bound to |
|
| DNS-rebinding Host allowlist |
| unset | Cloudflare Access team domain; enables origin-side JWT checks (with |
| unset | Access application AUD; both must be set to enforce auth |
|
| Nomis API root |
| unset | Optional Nomis |
|
| Nomis per-request timeout (s) |
Notes
The beta API has no server-side text search, so search_datasets pages
the catalogue and filters locally. It's fine for a few hundred datasets
but does one request per 100 items; cache the result if you call it often.
The ONS beta tools are verified end-to-end against the live API by
scripts/verify.py (see Verify above). The Nomis tools were built
against the documented SDMX-JSON structure and unit-tested for parsing, but
not smoke-tested against the live Nomis service — validate the first few
nomis_* calls in your environment.
Licence: the code is MIT — see LICENSE. The data it serves contains public sector information licensed under the Open Government Licence v3.0.
Available Tools
18 toolsget_datasetB
Get a single dataset's metadata and its latest edition/version.
Follows the dataset's latest_version link so you get the concrete edition and version numbers needed by the downstream tools, plus the available download formats (csv, xlsx) for the whole dataset.
Inputs: dataset_id (required). Returns: object
get_dimensionsA
List the dimensions (variables) of a dataset version.
Every dataset has time and geography; the rest are topic
dimensions. number_of_options tells you how many codes each holds.
Feed a dimension name into get_options to see valid codes.
Inputs: edition (required), version (required), dataset_id (required). Returns: object
get_metadataA
Get full metadata for a dataset version (provenance, downloads).
Includes the CSV/XLSX/CSVW download hrefs for the complete version, dimension descriptions, release date and usage notes. Use this when bulk download is more sensible than per-observation queries.
Inputs: edition (required), version (required), dataset_id (required). Returns: object
get_observationsA
Retrieve observation(s) for a dataset version.
Supply exactly one option code per dimension in dimensions,
e.g. {"time": "Mar-18", "geography": "K02000001",
"aggregate": "cpih1dim1A0"}. One (and only one) dimension may be the
wildcard "*" to return every value along that axis — useful for
pulling a whole time series in a single call.
Returns the observations plus the unit of measure.
Inputs: edition (required), version (required), dataset_id (required), dimensions (required). Returns: object
get_optionsA
List valid option codes for one dimension of a dataset version.
Each option has an option code (what you pass to get_observations)
and a human label. Dimensions can be large (e.g. thousands of
geographies or hundreds of time periods), so page with offset/limit.
Inputs: edition (required), version (required), dimension (required), dataset_id (required), +2 more. Returns: object
get_releaseA
Full detail for one release-calendar entry by its uri.
uri is the release path (e.g. "/releases/consumerpriceinflationjuly2026"),
as returned by list_releases. Gives the definitive release date and
whether it is confirmed (finalised) or still provisional, any
cancellation notice, the publishing contact, and links to the datasets
and documents the release will contain — the reliable answer for a real
next-release date.
Inputs: uri (required). Returns: object
get_timeseriesA
Get a PUBLISHED ONS time series by its website URI. PREFER THIS for headline indicators (inflation rates, unemployment rate, GDP growth).
This hits /v1/data, which serves the same figures as the ONS website — the published headline value, not something derived from an index. It returns months/quarters/years already in chronological order, so the last element is the latest period.
uri is the ONS website path for the series, e.g.
"/economy/inflationandpriceindices/timeseries/l55o/mm23" for the CPIH
annual rate. These URIs are evergreen and don't change between
releases. Find them via the series CDID (l55o, d7g7, mgsx ...) and the
dataset (mm23, lms ...).
Prefer this over get_observations for "what is the latest X rate" questions: the CMD datasets behind get_observations are curated separately and some lag the published series by months or years.
CAVEAT on next_release: it is a snapshot captured at the LAST
publication, not a live release-calendar lookup, so it can be wrong.
Observed reporting "15 July" when the ONS release calendar said "22
July". Treat it as indicative only; for a real next-release date use
list_releases / get_release (the live calendar), not this field.
Inputs: uri (required). Returns: object
get_topic_contentB
List the content published under a topic (bulletins, articles, key time series, dataset pages).
topic_id is the numeric id from list_topics (e.g. "4972" = Inflation
and price indices). Items carry a type and title; where an item is a
published time series, timeseries_uri is filled in ready to pass
straight to get_timeseries. A convenient bridge from a topic to the
headline series under it.
Inputs: topic_id (required), limit, offset. Returns: object
list_datasetsA
List available ONS datasets (paginated).
Returns id, title, description, release_frequency and the latest
edition/version links for each dataset. Use offset to page through
the full catalogue (there are several hundred datasets).
Inputs: limit, offset. Returns: object
list_releasesA
Browse the ONS release calendar — the AUTHORITATIVE schedule of
statistical publications. USE THIS to answer "when is the next X
released", not the next_release field on get_timeseries/get_dataset
(which is a snapshot from the last publication and can be wrong).
status is one of: "upcoming" (provisional + confirmed, not yet out —
the default), "published" (already released), "cancelled". query
filters by title/summary (e.g. "inflation", "GDP"). sort defaults to
soonest-first for upcoming and most-recent-first otherwise; override
with "release_date_asc" / "release_date_desc" / "title_asc".
Each item carries the release uri (pass to get_release for detail),
title, summary, the ISO release_date, and a status of confirmed /
provisional / cancelled. Dates that are still provisional may move —
get_release shows whether a date is finalised.
Inputs: sort, limit, query, offset, +1 more. Returns: object
list_topicsA
Browse the ONS topic hierarchy (a taxonomy over the whole catalogue).
Call with no argument for the top-level topics (Economy, Employment and
labour market, Business industry and trade, People population and
community, Census ...). Pass a topic id (numeric, e.g. "1245" =
Economy) as parent_id to list its subtopics. Each item reports whether
it has_subtopics (drill down again) and has_content (list its
publications with get_topic_content).
Inputs: parent_id. Returns: object
nomis_data_urlA
Build a direct Nomis download URL for bulk retrieval.
For large pulls it's often better to hand back a URL than to
stream every observation through the model. Returns a ready-to-use
link in the requested format (csv, xls, data.json). Nomis time and
list syntax is supported in dimensions values.
Inputs: dataset_id (required), dimensions (required), fmt. Returns: object
nomis_dimension_optionsA
List valid codes for one dimension of a Nomis dataset.
dimension is the conceptref from nomis_get_dataset (e.g. sex,
age, measures, item). Each option has a Nomis value (the code
you pass to nomis_get_data) and a human name. Optional search
filters options by substring — useful for large codelists.
For geography specifically, prefer nomis_geography_search, which also lets you restrict by area type.
Inputs: dimension (required), dataset_id (required), limit, search. Returns: object
nomis_geography_searchA
Resolve a place name to Nomis geography code(s) for a dataset.
Nomis geography codes are internal integers, not ONS mnemonics,
and differ per dataset. Pass a place name (e.g. "Birmingham",
"Wales") and get back matching {value, name} pairs to use as the
geography param in nomis_get_data. Supports Nomis '*' wildcards.
Inputs: search (required), dataset_id (required), limit. Returns: object
nomis_get_dataA
Retrieve observations from a Nomis dataset (simple JSON).
dimensions maps conceptref -> code(s), e.g.
{"geography": "2038432081", "sex": "5,6,7", "time": "latest",
"measures": "20100"}. Values may be comma-separated lists or Nomis
time syntax such as "latest" or "latestMINUS12-latest". measures
is usually required (20100 = value).
Returns flat records with named dimension values and the numeric obs_value, plus the record count. Use nomis_get_dataset and nomis_dimension_options / nomis_geography_search first to find the right codes.
Inputs: dataset_id (required), dimensions (required), select. Returns: object
nomis_get_datasetA
Get one Nomis dataset's overview and its dimensions.
dataset_id is a Nomis table id like NM_1_1 (Claimant Count) or
NM_2021_1. Returns name, description and the dimensions, each with
its conceptref — the parameter name to use in nomis_get_data.
Every dataset has at least a geography and a time dimension.
Inputs: dataset_id (required). Returns: object
nomis_search_datasetsA
Search Nomis census / labour-market datasets by keyword.
Matches query (case-insensitive substring) against dataset id,
name and description across the whole Nomis catalogue. Returns
each dataset's id (e.g. NM_1_1), name, description and the list of
its dimensions with the conceptref you pass as a query param to
nomis_get_data. Nomis covers Claimant Count, APS, Census 2011/2021
tables, workforce jobs and more — data the ONS beta API lacks.
Inputs: query (required), limit. Returns: object
search_datasetsA
Find datasets whose title, description or keywords match query.
The beta API has no server-side search, so this pages through the catalogue and filters client-side on a case-insensitive substring. Good for turning a topic ("inflation", "life expectancy") into a dataset id.
Inputs: query (required), limit. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 18 tools
Tools are clearly segmented into ONS and Nomis groups by prefix, and within each group the purposes are distinct: metadata retrieval, dimension listing, data fetching, etc. The only slight overlap is between get_timeseries and get_observations, but they are explicitly documented for different use cases.
ONS tools follow a consistent verb_noun pattern (get_, list_, search_), while Nomis tools use a consistent nomis_verb pattern. However, 'nomis_data_url' breaks the verb pattern, and mixed verb initiatives (get vs list) across the server are minor inconsistencies.
18 tools is a reasonable count for covering two distinct APIs (ONS and Nomis) without being overwhelming. Each tool serves a clear purpose, and the number aligns well with the server's scope of data query and exploration.
The tool surface covers core operations for both APIs: browse, search, metadata, dimension options, data retrieval, and even a download URL builder. Minor gaps exist, such as lacking a tool to list all Nomis datasets without a search query, but this does not critically hinder functionality.
Maintenance
Related MCP Connectors
UK Office for National Statistics dataset catalogue + Beta JSON API
Query UK Parliament, elections, crime stats, ONS census data, and national archives
ONS statistics and time series (CPI, GDP, unemployment) and Bank of England rates
UK ONS MCP — Office for National Statistics (no auth)
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables access to official UK Office for National Statistics data including demographics, economics, and social statistics through the ONS Beta API. Supports browsing, searching, and querying datasets with built-in shortcuts for popular statistics like inflation, regional GDP, and wellbeing data.529 npm1MIT
- AlicenseNot gradedqualityCmaintenanceEnables querying UK Office for National Statistics datasets and their editions through natural language, with no authentication required.282 npmMIT
- AlicenseNot gradedqualityFmaintenanceProvides tools to list, retrieve metadata, editions, latest observations, and time series from the UK Office for National Statistics (ONS) datasets.6 npmMIT
- AlicenseAqualityCmaintenanceEnables querying the ONS NOMIS labour market and census API from Claude, with tools for dataset discovery, dimension exploration, code resolution, and data fetching while guarding against silent truncation.6MIT