Skip to main content
Glama
ONS + Nomis MCP server

ONS + Nomis MCP server

by brynmrgn

ONS + Nomis MCP server

An MCP server over two UK statistics platforms, exposed from one server:

  1. 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.

  2. 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 optional uid (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 (cpih01 was five months behind the published CPIH series in July 2026; regional-gdp-by-year was three years behind). get_dataset and search_datasets return last_updated and a staleness_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

get_timeseries

Published headline series by URI — prefer for latest rates

list_datasets

Page through the catalogue

search_datasets

Client-side substring search over title/description/keywords

get_dataset

Resolve a dataset's latest edition + version

get_dimensions

List a version's variables

get_options

List valid option codes for one dimension

get_observations

Fetch observation(s); one dimension may be *

get_metadata

Provenance and bulk CSV/XLSX download links

list_releases

Live release calendar — upcoming/published/cancelled

get_release

One release: confirmed vs provisional date, related datasets

list_topics

Browse the ONS topic taxonomy (root, or a topic's subtopics)

get_topic_content

Publications under a topic; bridges series to get_timeseries

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

nomis_search_datasets

Keyword search over the Nomis catalogue

nomis_get_dataset

Dataset overview + dimensions (with conceptrefs)

nomis_dimension_options

Valid codes for a dimension (e.g. sex, age, measures)

nomis_geography_search

Resolve a place name to Nomis geography code(s)

nomis_get_data

Fetch observations (simple flat JSON)

nomis_data_url

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-mcp

Streamable HTTP (for a self-hosted deployment behind a tunnel), served at /mcp:

ONS_MCP_HTTP=1 ONS_MCP_PORT=8095 ons-mcp

Bound 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.py

Client config (stdio)

{
  "mcpServers": {
    "ons-beta": {
      "command": "ons-mcp"
    }
  }
}

Environment

Variable

Default

Meaning

ONS_API_BASE

https://api.beta.ons.gov.uk/v1

API root

ONS_API_TIMEOUT

30

Per-request timeout (s)

ONS_MCP_HTTP

unset

Set to 1 for HTTP transport (serves /mcp)

ONS_MCP_PORT

8095

HTTP port (bound to 127.0.0.1)

ONS_MCP_ALLOWED_HOSTS

ons-mcp.library-apps.dev,127.0.0.1,localhost

DNS-rebinding Host allowlist

CF_ACCESS_TEAM_DOMAIN

unset

Cloudflare Access team domain; enables origin-side JWT checks (with CF_ACCESS_AUD)

CF_ACCESS_AUD

unset

Access application AUD; both must be set to enforce auth

NOMIS_API_BASE

https://www.nomisweb.co.uk/api/v01

Nomis API root

NOMIS_API_KEY

unset

Optional Nomis uid for larger queries

NOMIS_API_TIMEOUT

60

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 tools
get_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_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

    A3.9/5.0

    Scored across 18 tools

    Disambiguation5/5

    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.

    Naming Consistency4/5

    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.

    Tool Count5/5

    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.

    Completeness4/5

    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

    ActivityInactive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      D
      maintenance
      Enables 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.
      5
      29 npm
      1
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables querying UK Office for National Statistics datasets and their editions through natural language, with no authentication required.
      282 npm
      MIT
    • A
      license
      Not graded
      quality
      F
      maintenance
      Provides tools to list, retrieve metadata, editions, latest observations, and time series from the UK Office for National Statistics (ONS) datasets.
      6 npm
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Enables 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.
      6
      MIT