Skip to main content
Glama
chris-page-gov

mcp-geo

MCP Geo Server

A research Model Context Protocol (MCP) server for geospatial (Ordnance Survey) and statistical (Office of National Statistics) data. If you have Docker installed and Internet access, have this running in 3 minutes.

Public Launch Caveat

This repository is a personal development project. It is not production code and is not approved by Warwickshire County Council or the Department for Science, Innovation and Technology.

All opinions expressed, decisions made, and implementation choices documented in this repository come from a novice learning the opportunities afforded by emerging AI and MCP technology.

This repository is a diary of that learning journey, not a recommended course of action and not a formal proposal.

Related MCP server: ontario-data-mcp

Start here — run the server and ask it a question

This section is for first-time users. You do not need to understand MCP or the internal architecture — just follow the steps and your AI assistant will gain UK geographic and statistics awareness.

1) Get access key (2 minutes)

Create a free Ordnance Survey Data Hub key:

Optional:

  • NOMIS_UID and NOMIS_SIGNATURE if you want higher-rate NOMIS access

You only need the OS API key for the default live setup. The ONS live endpoints used by MCP-Geo do not require a separate ONS API key.

2) Run the server in a folder

git clone https://github.com/chris-page-gov/mcp-geo.git
cd mcp-geo
cp .env.example .env

Path portability note:

  • Any absolute path shown later in this README is an example, not a required location.

  • Replace maintainer-specific examples such as /Users/... or /Volumes/... with paths that exist on your machine.

  • The main path-bearing settings are ADDRESSBASE_PREMIUM_XREF_PATH, LANDIS_LOCAL_DATA_ROOT, LANDIS_PORTAL_ARCHIVE_DIR, LANDIS_FULL_RELEASE_ARCHIVE_DIR, BOUNDARY_RUNS_DIR, BOUNDARY_RUNS_SEARCH_DIRS, and, for GUI-launched wrappers on macOS, MCP_GEO_DOCKER_BIN.

  • Docker-backed local wrappers (scripts/claude-mcp-local, scripts/codex-mcp-local, scripts/mcp-docker-local) now hydrate those path-bearing settings from the repo .env and mount the configured host paths into the container automatically.

  • Generated reports, research artifacts, and knowledge-base outputs in this repo may embed the maintainer's local paths; they are evidence artifacts, not portable setup inputs.

Open .env and paste the required key:

OS_API_KEY=your-key-here
OS_API_AUTH_MODE=query

Do not add quote marks around .env values. Set either OS_API_KEY or OS_API_KEY_FILE, not both. For a secret-file setup and first-run OS Data Hub account details, use docs/os_data_hub_public_account_setup.md.

Optional:

NOMIS_UID=your-nomis-uid
NOMIS_SIGNATURE=your-nomis-signature

Now build the MCP server image:

docker build -t mcp-geo-server .

The STDIO server is normally started by your MCP client. A manual docker run -i --env-file .env mcp-geo-server waits for JSON-RPC on stdin and may print nothing until a request arrives.

Or skip the build and replace mcp-geo-server in the smoke test below with the pre-built image:

ghcr.io/chris-page-gov/mcp-geo:latest

3) Verify it works

Send a JSON-RPC request that includes an id:

printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
  | docker run --rm -i --env-file .env mcp-geo-server

If JSON tool definitions appear, the server is working. Requests without an id are JSON-RPC notifications, so the STDIO adapter correctly sends no response.


4) Connect your AI assistant

Example: Claude Desktop configuration

{
  "mcpServers": {
    "geo": {
      "command": "docker",
      "args": ["run", "-i", "--env-file", "/absolute/path/to/mcp-geo/.env", "mcp-geo-server"]
    }
  }
}

Replace /absolute/path/to/mcp-geo/.env with the actual path to your .env file. See .env.example for available settings; at minimum you need OS_API_KEY.

Or use the pre-built image in the same Claude Desktop config:

{
  "mcpServers": {
    "geo": {
      "command": "docker",
      "args": ["run", "-i", "--env-file", "/absolute/path/to/mcp-geo/.env", "ghcr.io/chris-page-gov/mcp-geo:latest"]
    }
  }
}

Restart Claude Desktop.


5) Ask a real question

Try:

“Which administrative areas contain postcode SW1A 1AA?” “Describe the geography hierarchy around this coordinate 52.4862, -1.8904” “Is this location inside an AONB?”

If the assistant answers using real UK geography — everything is working.


What you just did

You connected an AI assistant to live UK spatial data via the Model Context Protocol.

The server runs locally on your machine and calls:

  • Ordnance Survey APIs

  • ONS statistical geography services

  • NOMIS datasets

No data is stored or redistributed.


MCP Specification

See Latest stable specification target. The MCP 2026-07-28 release candidate is tracked, but not the default runtime protocol. Tracking and review cadence live in docs/spec_tracking.md; the current RC alignment ledger is Plans/PLAN-MCP-2026-07-28-RC-alignment.md. OpenAI's Documentation MCP guide is at https://developers.openai.com/resources/docs-mcp, with the shared server available at https://developers.openai.com/mcp (preview; tracked in docs/spec_tracking.md).

Protocol negotiation behavior:

  • Preferred MCP core protocol revision: 2025-11-25

  • Supported MCP core protocol versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05

  • Opt-in release-candidate protocol: 2026-07-28 when MCP_2026_RC_ENABLED=1 or MCP_PROTOCOL_2026_07_28_ENABLED=1

  • Streamable HTTP enforces MCP-Protocol-Version when provided and returns negotiated mcp-protocol-version on responses

  • In RC mode, /mcp supports server/discover, stateless requests without Mcp-Session-Id, per-request _meta, strict Mcp-Method / Mcp-Name validation, cache metadata on list/read results, MRTR-style input-required responses for supported elicitation flows, and JSON Schema 2020-12 guardrails

  • MCP-Apps extension tracked at 2026-01-26 (io.modelcontextprotocol/ui)

Key Features

  • MCP endpoints: /mcp (streamable HTTP JSON-RPC), /tools/list, /tools/call, /tools/describe, /tools/search, /resources/list, /resources/describe, /resources/read

  • Uniform error envelope and pagination (nextPageToken)

  • Dynamic tool registration with schema introspection

  • Tool annotations + defer-loading metadata for tool search integrations

  • Agent skills resource (skills://mcp-geo/getting-started)

  • MCP-Apps UI resources (ui://mcp-geo/...) with helper os_apps.* tools

  • pgRouting-backed route planning surface (os_route.get, os_route.descriptor)

  • Svelte playground UI for MCP tool calls, prompt capture, and auditing

  • Routing tool os_mcp.route_query for intent classification and workflow guidance

  • Structured logging & correlation IDs

  • OS API client with retries and explicit upstream error codes

  • High coverage test suite exercising success + failure paths

  • Evaluation harness with question suite and scoring rubric

Developer setup

git clone <repo-url>
cd mcp-geo
pip install -e .[test]
uvicorn server.main:app --reload

Then visit:

  • GET /health

  • GET /tools/list

  • GET /tools/describe

  • POST /tools/call with { "tool": "os_places.by_postcode", "postcode": "SW1A1AA" }

  • POST /mcp with {"jsonrpc":"2.0","id":"1","method":"tools/list","params":{}}

Set OS_API_KEY in the environment (or .env) for Ordnance Survey API-key calls. OS_API_AUTH_MODE=query is the default and sends the key as the documented key query parameter. OS_API_AUTH_MODE=header sends the same project key in the documented key header. OS_API_AUTH_MODE=bearer sends OS_API_ACCESS_TOKEN as an OAuth2 bearer token; MCP-Geo does not mint OS OAuth tokens itself, so the caller must refresh that token out of band. Missing or invalid credentials return NO_API_KEY, OS_API_KEY_INVALID, or OS_API_KEY_EXPIRED.

If MCP HTTP auth is enabled, only GET /health remains public. The raw HTTP tool, resource, metrics, and playground routes all require the same bearer auth policy as POST /mcp.

Canonical Map Delivery Baseline

Use this order for reliable cross-host map delivery:

  1. os_maps.render (static contract baseline; works without widgets)

  2. overlay_bundle layers for map annotations and features

  3. os_apps.render_* widgets only when the host advertises MCP-Apps UI support

  4. Explicit fallback skeletons (map_card, overlay_bundle, export_handoff)

References:

  • docs/spec_package/06_api_contracts.md

  • docs/spec_package/06a_map_delivery_fallback_contracts.md

  • docs/map_delivery_support_matrix.md

  • docs/map_embedding_best_practices.md

  • docs/simple_map_lab.md (minimal auth + PMTiles exploration runbook)

Getting Started (User Guide)

See docs/getting_started.md for a quick way to discover available data, run MCP Inspector or the playground UI, and explore tools/resources.

Full Specification Package

For a complete design specification (aims, personas, architecture, scenarios, diagrams, backlog), see docs/spec_package/README.md.

UK Public Sector AI Community Documentation Set

For the full repository journey narrative (novice-readable chapters, timeline, evaluation, troubleshooting evidence index, and extension planning), see:

  • docs/public_sector_ai_community/README.md

For publication output (LaTeX / Prism-ready with ToC and bibliography), see:

  • docs/public_sector_ai_community/prism/main.tex

Codex Context (Mac App)

  • Use CONTEXT.md as the durable project context for Codex across environments.

  • If you use the Codex Mac app, open this repo as a project and read CONTEXT.md at session start.

  • The repo MCP configs include openaiDeveloperDocs (https://developers.openai.com/mcp) so OpenAI/Codex/API/App SDK docs can be read through MCP instead of the legacy docs/vendor/openai/ copies.

  • Codex app documentation: https://developers.openai.com/codex/app/

  • Codex app features: https://developers.openai.com/codex/app/features

Docker (STDIO / Claude Desktop)

Build the image:

docker build -t mcp-geo-server .

Or pull the pre-built multi-arch image:

docker pull ghcr.io/chris-page-gov/mcp-geo:latest

Available tags:

  • latest for the default branch image

  • <sha> for a specific commit image

  • <version> for release tags such as 0.8.0

The published image targets linux/amd64 and linux/arm64. For the Docker MCP catalog submission draft and validation checklist, see docs/docker_mcp_catalog_submission.md.

Claude Desktop config example (STDIO transport):

{
  "mcpServers": {
    "mcp-geo": {
      "command": "/absolute/path/to/mcp-geo/scripts/claude-mcp-local",
      "env": {
        "OS_API_KEY": "${env:OS_API_KEY}",
        "OS_API_KEY_FILE": "${env:OS_API_KEY_FILE}",
        "MCP_STDIO_UI_SUPPORTED": "1",
        "MCP_STDIO_FRAMING": "line",
        "MCP_STDIO_ELICITATION_ENABLED": "1"
      }
    }
  }
}

The wrapper script builds and starts the repo's PostGIS+pgRouting sidecar image locally (Docker), bootstraps the boundary-cache and route-graph schemas idempotently, builds the app image if needed, and runs STDIO with the cache/routing DSNs pointed at that sidecar. Set either OS_API_KEY or OS_API_KEY_FILE in the host environment (if both are set, OS_API_KEY wins). Use MCP_GEO_DOCKER_BUILD=always|missing|never to control rebuild behavior. By default it now stores PostGIS data in a Docker named volume (mcp-geo-postgis-claude) and uses the dedicated sidecar container/network names mcp-geo-postgis-claude / mcp-geo-claude, so raw database files are not written into the repo and Claude no longer shares a fallback volume with other host-side wrappers. Set MCP_GEO_POSTGIS_STORAGE_MODE=bind only if you explicitly want a host path mount (MCP_GEO_POSTGIS_DATA_DIR). Set MCP_GEO_POSTGIS_VOLUME differently per worktree if you want isolated local PostGIS state for each branch workspace. Override MCP_GEO_POSTGIS_IMAGE only if you need a different pgRouting-capable tag. The repo-local image currently builds on postgis/postgis:16-3.4, which is upstream-amd64-only for this tag, so Apple Silicon still runs the sidecar as linux/amd64 under Docker emulation. The generic scripts/mcp-docker-local fallback now uses its own default sidecar identity (mcp-geo-postgis-sidecar on network mcp-geo-sidecar), and the devcontainer defaults to a separate named volume mcp-geo-postgis-devcontainer, so the normal host wrappers no longer collide with the devcontainer cache by default. If a sidecar fails to become ready, the wrapper now inspects the recent Postgres logs and calls out checkpoint-corrupted volumes explicitly instead of only timing out. Wrapper-managed PostGIS sidecars no longer publish 5432 to the host by default; set MCP_GEO_POSTGIS_PUBLISH_PORT only when you explicitly need host access to that sidecar database. Docker-backed host wrappers now default to isolated PostGIS sidecars per client. That is the anti-corruption default and should remain the normal operator assumption. For comparison runs, use ./scripts/check_shared_benchmark_cache.sh before launching the clients:

  • default isolated mode verifies Claude, Codex, and Gemini are each using their dedicated sidecar with matching mounted data roots and matching cache counts

  • opt-in shared mode is available only when you explicitly set MCP_GEO_POSTGIS_REUSE_DEVCONTAINER=1 and MCP_GEO_BENCHMARK_CACHE_MODE=shared, which makes every wrapper reuse the same devcontainer PostGIS container

If Docker isn't on the GUI PATH (common on macOS), set MCP_GEO_DOCKER_BIN in Claude Desktop to the absolute Docker path (for example /opt/homebrew/bin/docker).

Before demos, run the readiness check:

./scripts/prepare-for-demo

It verifies that the checkout matches origin/main, the local mcp-geo-server Docker image was built after that ref, stale app containers are not still running, the Claude/Codex/Gemini wrappers resolve as expected, and .vscode/mcp.json is present. If the image is stale, either rebuild it directly or rerun the check with --rebuild:

./scripts/prepare-for-demo --rebuild

Optional HTTP transport:

./scripts/mcp-http-demo-local

The HTTP demo launcher uses the same Docker wrapper hydration as the STDIO client wrappers: it reads OS_API_KEY / OS_API_KEY_FILE, mounts ONS/OS cache directories when present, enables the MCP 2026 release-candidate flag by default, and starts http://127.0.0.1:8000/mcp for HTTP-capable clients such as Codex, VS Code, Inspector, and Claude Code HTTP connections. If you are running from a clean worktree but want to reuse an existing checkout's .env and cache paths, set MCP_GEO_ENV_FILE=/absolute/path/to/mcp-geo/.env before launching.

Tip: Replace mcp-geo-server with ghcr.io/chris-page-gov/mcp-geo:latest in any Docker command to use the pre-built image instead of a local build.

Tutorial

See docs/tutorial.md for an evaluation-style walkthrough covering tool discovery, admin lookup, OS tools, ONS tools, resources/ETags, and STDIO.

Evaluation

See docs/evaluation.md for the question suite, rubric, and harness usage.

Research (ONS Dataset Selection)

See research/ons_dataset_selection/report.md for the ONS dataset selection research pack, including the taxonomy options, DataPack schema, sample DataPacks, and linking rules used to improve AI dataset selection and explainability.

Research (Map Delivery Interoperability)

See research/map_delivery_research_2026-02/README.md for the map delivery research pack covering personas, map delivery option analysis, containerized Playwright trials, screenshots/log evidence, and final recommendations for cross-client map reliability.

For validated host/browser behavior by capability mode, see docs/map_delivery_support_matrix.md. For notebook-to-resource scenario pack workflow, see docs/map_scenario_packs.md. For mixed-host embedding and constrained-style patterns, see docs/map_embedding_best_practices.md.

Client Tracing

See docs/client_trace_strategy.md for MCP traffic and MCP-Apps UI interaction capture using the stdio and HTTP trace proxies.

Tool Catalog (Epics B–D)

Tools are discoverable via /tools/list and rich metadata via /tools/describe. Discovery responses use sanitized tool names (for example os_places_by_postcode) for client compatibility; map back to canonical dotted names via annotations.originalName. Tool calls accept both sanitized and dotted names. Use toolset, includeToolsets, and excludeToolsets filters to focus discovery responses by capability groups (for example ons_selection, maps_tiles, apps_ui). For clients that always request tools/list with empty params, set MCP_TOOLS_DEFAULT_TOOLSET=starter (or MCP_TOOLS_DEFAULT_INCLUDE_TOOLSETS=<csv>) to keep initialization payloads small. For the current local development profile, the checked-in examples use MCP_TOOLS_DEFAULT_INCLUDE_TOOLSETS=ons_geo_lookup,property_tax,features_layers,landis_soils alongside starter so constrained hosts still see the active ONS geo, AddressBase/council-tax, map export, and LandIS surfaces. council_tax.band_lookup and council_tax.query are always loaded by default so MCP clients do not need a separate property-tax discovery step before using the council-tax surfaces.

Tool

Purpose

os_places.search

Free text address search

os_places.by_postcode

UPRNs + addresses for a postcode

os_places.by_uprn

Single address lookup

os_places.nearest

Nearest addresses to a coordinate

os_places.within

Addresses within bbox

os_names.find

OS Names gazetteer search for named places and features

os_names.nearest

Nearest named features

os_features.query

NGD features by bbox & collection

os_linked_ids.get

Relationship lookup between UPRN/USRN/TOID

os_maps.render

Static map render metadata (proxy URL)

os_vector_tiles.descriptor

Vector tiles style/source descriptor

os_offline.descriptor

Offline PMTiles/MBTiles pack catalog + retrieval contracts

os_offline.get

Offline map handoff payloads (map_card, overlay_bundle, export_handoff)

admin_lookup.containing_areas

Administrative area containment for a point

admin_lookup.reverse_hierarchy

Ancestor chain for an administrative area

admin_lookup.area_geometry

Bounding box geometry for an administrative or statistical area

admin_lookup.find_by_name

Boundary/admin area name search, including parish/PARNCP areas

council_tax.band_lookup

Experimental England/Wales Council Tax band lookup

council_tax.query

AddressBase Premium UPRN check for Council Tax and non-domestic rates

landis_catalog.list_products

LandIS callable product registry, exact thematic IDs, and access tiers

landis_metadata.get

LandIS product metadata, provenance, and linked resources

landis_soilscapes.point

LandIS Soilscapes class lookup for a WGS84 point

landis_soilscapes.area_summary

LandIS Soilscapes area composition summary

landis_derive.pipe_risk

LandIS-derived corrosion and shrink-swell pipe risk screening

ons_data.query

Query live ONS observations (dataset/edition/version or term)

ons_data.dimensions

List ONS observation dimensions for a live dataset

ons_data.get_observation

Retrieve a single live observation

ons_data.create_filter

Create a live ONS filter

ons_data.get_filter_output

Retrieve filter output in JSON/CSV/XLSX

ons_select.search

Rank ONS datasets with explainable scoring

ons_search.query

Search live ONS datasets (beta API)

ons_codes.list

List live dimension IDs

ons_codes.options

List live dimension options

nomis.datasets

List NOMIS datasets or dataset definitions

nomis.concepts

List NOMIS concepts

nomis.codelists

List NOMIS code lists

nomis.query

Query NOMIS datasets (JSON-stat/SDMX)

os_mcp.descriptor

Server capabilities and tool search configuration

os_mcp.route_query

Intent classification and tool/workflow recommendation

os_route.descriptor

Route solver capabilities, supported profiles, and graph readiness

os_route.get

Resolve stops and compute a graph-backed route

os_apps.render_geography_selector

Open the MCP-Apps geography selector widget

os_apps.render_statistics_dashboard

Open the MCP-Apps statistics dashboard widget

os_apps.render_feature_inspector

Open the MCP-Apps feature inspector widget

os_apps.render_route_planner

Open the MCP-Apps route planner widget backed by os_route.get

os_apps.render_ui_probe

Probe MCP-Apps UI rendering support

Use OS Names for gazetteer-style named-place and named-feature lookup, such as settlements, hamlets, villages, hills, woods, or other named map features. Use admin_lookup.* when you need an official boundary, hierarchy, containment, or geometry for a statistical/admin area. PARISH is the public normalized level for civil parishes, Welsh communities, and non-civil-parished areas; source fields remain PARNCP25CD, PARNCP25NM, and PARNCP25NW where present.

ons_geo.by_postcode, ons_geo.by_uprn, and ons_geo.area_summary expose normalized OA, LSOA, MSOA, PARISH, ward, district, region, and country fields when the local ONS cache has those source columns. House of Commons Library 2021 MSOA names are carried only as displayName labels with provenance; they do not replace the official ONS/RGC currentName.

Resources, Filtering & Provenance

The resources API exposes skills, UI widgets, and data resources (boundary manifest, cache status, and local ONS code cache entries).

  • GET /resources/list returns skill, UI, and data resource descriptors (with provenance metadata).

  • GET /resources/read?uri=skills://mcp-geo/getting-started returns skills guidance.

  • GET /resources/read?uri=ui://mcp-geo/geography-selector returns MCP-Apps UI HTML.

  • GET /resources/read?uri=resource://mcp-geo/landis-products returns the checked-in LandIS MVP registry.

LandIS Local Archive And Phase 2 Surface

LandIS now has two layers in this repo:

  • a validated MVP screening surface

  • an additive phase-2 local-archive surface for NATMAP, NSI, and archive discovery

The checked-in registry and prompt resources work offline. The phase-2 archive resources also work offline from the local mirror. Spatial queries still require a normalized PostGIS warehouse, but the source of truth for follow-on LandIS ingestion is now the local archive under ~/Data rather than a live portal session.

Use:

  • landis_catalog.list_products to discover the supported callable products, including the exact NATMAP thematic productId values accepted by landis_natmap.thematic_area_summary, plus linked resources and tool bindings.

  • landis_metadata.get to retrieve provenance and limitations for a specific LandIS product.

  • landis_soilscapes.point and landis_soilscapes.area_summary for generalized Soilscapes lookups.

  • landis_derive.pipe_risk for caveated corrosion and shrink-swell screening.

  • landis_archive.list_items and landis_archive.get_item to inspect the locally mirrored LandIS archive and its surfacing classification, including supplementary full-release items such as HOST, wetness, Series Hydrology, Series Leacs, and matched data.gov.uk package metadata.

  • landis_natmap.point, landis_natmap.area_summary, and landis_natmap.thematic_area_summary for local-archive-backed NATMAP map-unit and thematic summaries once loaded into PostGIS.

  • landis_nsi.nearest_sites, landis_nsi.within_area, and landis_nsi.profile_summary for explicit evidence-first NSI lookups once loaded into PostGIS.

Additional LandIS resources:

  • resource://mcp-geo/landis-portal-inventory

  • resource://mcp-geo/landis-archive-triage

  • resource://mcp-geo/landis-full-release-manifest

Reference documentation for the LandIS strategy and dataset surface is also now checked in as an Obsidian vault under Obsidian/LandIS Knowledge Base/, including the strategy PDF, dataset notes, use-case summaries, reference pages, and the MCP architecture roadmap in a form that can be browsed directly in Obsidian or as Markdown in the repo.

The repo now also carries a generated, repo-wide Obsidian knowledge base under Obsidian/MCP Geo Knowledge Base/. Unlike the LandIS example vault, this surface is built automatically from tracked repo content, excludes Obsidian/** from source scanning to avoid recursion, records commit-pinned GitHub links and source hashes in note frontmatter, and supports an ignored 98 Local Overlay/ subtree for machine-local trace/session evidence.

Refresh the canonical vault with:

python3 scripts/build_obsidian_kb.py \
  --mode canon \
  --git-ref WORKTREE \
  --output-root "Obsidian/MCP Geo Knowledge Base" \
  --manifest-out data/knowledge_base/obsidian_kb_manifest.json

Validate it with:

python3 scripts/validate_obsidian_kb.py \
  --manifest data/knowledge_base/obsidian_kb_manifest.json \
  --fail-on drift coverage recursion orphan

Enable the live warehouse with LANDIS_ENABLED=true, LANDIS_LIVE_ENABLED=true, and LANDIS_WAREHOUSE_DSN=.... Load normalized tables with python scripts/landis_ingest.py --dsn ... --soilscapes <file> --pipe-risk <file>. To inventory the authenticated LandIS portal itself from a local Atlas sign-in, run python scripts/landis_portal_inventory.py. The generated machine-readable catalog lands in research/landis-data-source/landis_portal_inventory_2026-04-04.json and the human-readable index lands in docs/reports/landis_portal_inventory_2026-04-04.md. To mirror the authenticated portal payloads to local storage, run python scripts/landis_portal_download.py --destination /absolute/path/to/Data/landis_portal_archive_2026-04-04. The downloader reuses the Atlas session, stores per-item metadata plus raw item payloads, and exports Feature Service layers/tables in chunked GeoJSON/JSON files under the destination root without storing the session token itself. To classify the local archive for runtime surfacing, run python scripts/landis_archive_triage.py. To ingest the local NATMAP and NSI phase-2 slice from ~/Data into PostGIS, run python scripts/landis_phase2_ingest.py --dsn .... The Docker wrapper scripts/mcp-docker-local now mounts the configured LandIS data root into the app container at /landis-data and sets LANDIS_LOCAL_DATA_ROOT there automatically. If no explicit root is configured it still falls back to ~/Data. It also hydrates and mounts any configured LANDIS_PORTAL_ARCHIVE_DIR, LANDIS_FULL_RELEASE_ARCHIVE_DIR, ADDRESSBASE_PREMIUM_XREF_PATH, BOUNDARY_RUNS_DIR, and BOUNDARY_RUNS_SEARCH_DIRS paths from the repo .env, so the normal mcp-geo + PostGIS container workflow can use repo-local data and external archives directly without copying raw mirrors into the image or database volume. On a fresh sidecar, the wrapper still auto-bootstraps the LandIS warehouse tables from the mounted data before it starts the stdio server: it runs scripts/landis_phase2_ingest.py for the portal-archive NATMAP/NSI slice and scripts/landis_ingest.py for the Warwickshire Soilscapes and pipe-risk validation layers. Expect the first start to take materially longer than a warm restart because this load is large. The verified phase-2 warehouse load currently covers NationalSoilMap, eight NATMAP thematic products, NSIsite, and six mirrored NSI observation datasets from the local archive, plus the existing Soilscapes and pipe-risk validation layers.

For a clean setup path aimed at a full spatial LandIS warehouse, use docs/landis_spatial_warehouse_setup.md. The recommended topology is still one MCP-Geo server with a PostGIS LandIS warehouse behind it; a separate LandIS MCP server is only a later governance, licensing, or performance-isolation decision.

  • GET /resources/read?uri=resource://mcp-geo/boundary-manifest returns the boundary manifest.

Skills and MCP-Apps Resources

In addition to data resources, MCP Geo exposes:

  • skills://mcp-geo/getting-started (Agent Skills guidance)

  • ui://mcp-geo/geography-selector

  • ui://mcp-geo/statistics-dashboard

  • ui://mcp-geo/feature-inspector

  • ui://mcp-geo/route-planner

Use GET /resources/read?uri=... to fetch these resources. When a host can call tools but cannot invoke protocol-level resources/read, use os_resources.get as the portable fallback bridge. MCP-Apps widgets are HTML documents with text/html;profile=mcp-app MIME types.

Route Planning

Route planning now has a deterministic tool path as well as a widget path.

  • Call os_mcp.route_query to classify free-text prompts and extract stop hints.

  • Call os_route.descriptor to check whether the active PostGIS/pgRouting graph is ready.

  • Call os_route.get to resolve stops and return distance, duration, geometry, legs, steps, mode changes, warnings, and graph provenance.

  • Call os_apps.render_route_planner when the host can open MCP-Apps UI; the widget mirrors the os_route.get contract and delegates calculation to that tool.

The intended backend is an OS Multi-modal Routing Network build loaded into PostGIS, with pgRouting used for shortest-path execution and route warnings enriched from restriction tables when available.

MCP-Apps support varies by client. If the client does not advertise UI support, the stdio adapter injects a fallback static map payload for os_apps.render_geography_selector (computed via os_maps.render). Set MCP_STDIO_UI_SUPPORTED=1 to force UI mode, or MCP_STDIO_FALLBACK_BBOX_DEG to control the fallback map span. Set MCP_APPS_CONTENT_MODE=embedded to embed UI HTML as a resource content block, or MCP_APPS_CONTENT_MODE=resource_link to emit a resource_link content block. Use MCP_APPS_CONTENT_MODE=text to suppress UI content blocks. Set MCP_STDIO_ELICITATION_ENABLED=0 to disable form elicitation in STDIO (os_mcp.stats_routing, ons_select.search). For Streamable HTTP (/mcp), set MCP_HTTP_ELICITATION_ENABLED=0.

Conditional Requests (ETag)

Clients should cache UI/skills responses and revalidate using If-None-Match. If unchanged, the server returns 304 Not Modified with the same ETag header.

Dataset Notes

Compression (GZip)

GZip compression is enabled (minimum payload size 512 bytes). Send:

Accept-Encoding: gzip

to receive a compressed response (check Content-Encoding: gzip).

Rate Limiting

Basic per-minute in-memory rate limiting is enabled by default:

  • Environment variable: RATE_LIMIT_PER_MIN (default 207 per IP per top-level path segment)

  • Bypass (tests/dev): RATE_LIMIT_BYPASS=false by default; set true only for explicit local/dev bypass.

  • Path exemptions: RATE_LIMIT_EXEMPT_PATH_PREFIXES (defaults to /maps/vector/vts/tile,/maps/raster/osm,/maps/static/osm) to prevent normal map tile fan-out from triggering 429 responses. Responses over the limit return:

{ "isError": true, "code": "RATE_LIMITED", "message": "Rate limit exceeded" }

Note: In-memory approach is not multi-process safe; replace with Redis or a shared store for production.

Rate-Limit Calibration Helper

You can run an active probe to estimate a suitable RATE_LIMIT_PER_MIN:

python3 scripts/rate_limit_assessor.py \
  --base-url http://127.0.0.1:8000 \
  --path /tools/list \
  --start-rpm 60 \
  --step-rpm 30 \
  --max-rpm 300 \
  --duration-sec 20 \
  --target-429-ratio 0.01 \
  --headroom-percent 15 \
  --output logs/rate-limit-assessment.json

Notes:

  • Set RATE_LIMIT_BYPASS=false before probing, otherwise no limiter behavior will be observed.

  • The recommendation is per client IP and top-level path segment, matching middleware behavior.

  • Requests under RATE_LIMIT_EXEMPT_PATH_PREFIXES are excluded from this probe scope.

Metrics

Prometheus-style metrics exposed at GET /metrics (if METRICS_ENABLED=true). When MCP HTTP auth is enabled, /metrics follows the same auth policy as /mcp, /tools/*, /resources/*, and /playground/*.

  • app_requests_total counter

  • app_rate_limited_total counter

  • app_request_latency_ms_bucket / _count histogram (client-observed wall time per request)

  • mcp_http_auth_failures_total counter by auth failure reason

  • mcp_http_session_quota_rejections_total counter

  • mcp_http_sessions_active gauge

  • mcp_tool_errors_total counter by tool and transport

Example scrape output snippet:

# HELP app_requests_total Total HTTP requests
# TYPE app_requests_total counter
app_requests_total 42
# HELP app_request_latency_ms Request latency histogram (ms)
# TYPE app_request_latency_ms histogram
app_request_latency_ms_bucket{le="50"} 40
app_request_latency_ms_bucket{le="100"} 41
app_request_latency_ms_bucket{le="+Inf"} 42
app_request_latency_ms_count 42

Admin lookup tools call the live ONS Open Geography services by default. Static boundary resources are not advertised in the resources API.

ONS Observations & Discovery (Epic D)

ONS data tools require live mode (ONS_LIVE_ENABLED=true). You can supply dataset, edition, and version directly, or provide a term and let ons_data.query auto-resolve the latest version. ons_codes.* supports an optional on-disk cache via ONS_DATASET_CACHE_ENABLED.

Tool ons_data.query supports:

  • geography (single code)

  • measure (single code)

  • timeRange — either single period (2024 Q2) or inclusive range (2024 Q1-2024 Q4)

  • Pagination: limit (1–500, default 100) and page (1-based)

ONS Client & Dataset Caching

tools/ons_common.py provides:

  • Retry + error mapping

  • In-memory TTL cache (short-lived request cache)

  • get_all_pages helper for full dataset paging

Full dataset cache snapshots are stored on disk when enabled via ONS_DATASET_CACHE_ENABLED=true and ONS_DATASET_CACHE_DIR.

Live ONS Mode & Codes

Enable live mode by setting ONS_LIVE_ENABLED=true. If you do not supply dataset metadata, ons_data.query will attempt to resolve the latest edition and version using term.

ons_data.query (live):

GET https://api.ons.gov.uk/dataset/{dataset}/edition/{edition}/version/{version}/observations?limit=...&page=...

ons_data.dimensions (live):

  1. Fetch version metadata:

GET https://api.ons.gov.uk/dataset/{dataset}/edition/{edition}/version/{version}
  1. For each dimension id returned, fetch its codes (paged, currently requesting up to 1000):

GET https://api.ons.gov.uk/dataset/{dataset}/edition/{edition}/version/{version}/dimensions/{dimensionId}/options?limit=1000&page=1

Provide an optional dimension field to retrieve only a single dimension's codes.

ons_search.query (live dataset search):

GET https://api.beta.ons.gov.uk/v1/datasets?search=<term>&limit=...&offset=...

You can override the base with ONS_DATASET_API_BASE or disable live search with ONS_SEARCH_LIVE_ENABLED=false.

NOMIS Labour & Census Statistics

Enable live mode with NOMIS_LIVE_ENABLED=true (default). Optional credentials may be provided via NOMIS_UID and NOMIS_SIGNATURE if you need higher limits.

Use:

  • nomis.datasets for dataset discovery

  • nomis.concepts / nomis.codelists for metadata

  • nomis.query for JSON-stat or SDMX JSON observations

Council Tax Band Lookup Pilot

council_tax.band_lookup is an experimental England/Wales-only pilot backed by the public GOV.UK Council Tax band service. It currently uses an HTML form flow rather than a published API, so treat it as a pilot integration with explicit failure handling rather than a guaranteed stable machine-to-machine contract.

Supported inputs include postcode, propertyName, street, town, billingAuthorityReference, and optional filters such as band and bandStatus. Set COUNCIL_TAX_BAND_LIVE_ENABLED=true to enable the live lookup surface.

AddressBase Premium UPRN Tax Status

council_tax.query checks a batch of UPRNs against the AddressBase Premium Application Cross Reference Type 23 table. By default it only counts current matches with a blank END_DATE, so historical cross references do not get reported as current liabilities.

Configure ADDRESSBASE_PREMIUM_XREF_PATH to either an extracted AddressBase Premium xref CSV/Parquet file or a directory containing one. CSV sources are stream-scanned; Parquet sources are queried directly with DuckDB so large local lookup workloads can stay memory-bounded without building a separate indexed database. The council_tax.query tool still classifies SOURCE=7666VC as Council Tax and SOURCE=7666VN as non-domestic rates, based on the current OS documentation.

For local runtime use, the recommended workflow is to keep the licensed source extract outside git and build a smaller serving Parquet, for example:

python -m scripts.addressbase_build_xref \
  --input /absolute/path/to/ABP/xref.parquet \
  --output /absolute/path/to/mcp-geo/data/addressbase_premium/2026-03-03/xref_voa_os.parquet

The builder keeps the xref columns used by MCP Geo, drops only SOURCE=7666OW and SOURCE=7666OP, writes a sorted xref_voa_os.parquet, and leaves the wider VOA/OS-linked cross references available for future UPRN/TOID-linked workflows. Optional runtime knobs:

  • ADDRESSBASE_PREMIUM_DUCKDB_THREADS

  • ADDRESSBASE_PREMIUM_DUCKDB_MEMORY_LIMIT

For local runtime use, the recommended workflow is to keep the licensed source extract outside git and build a smaller serving Parquet, for example:

python -m scripts.addressbase_build_xref \
  --input /absolute/path/to/ABP/xref.parquet \
  --output /absolute/path/to/mcp-geo/data/addressbase_premium/2026-03-03/xref_voa_os.parquet

The builder keeps the xref columns used by MCP Geo, drops SOURCE=7666OW and SOURCE=7666OP, writes a sorted xref_voa_os.parquet, and leaves the wider VOA/OS-linked cross references available for future UPRN/TOID workflows. Optional runtime knobs:

  • ADDRESSBASE_PREMIUM_DUCKDB_THREADS

  • ADDRESSBASE_PREMIUM_DUCKDB_MEMORY_LIMIT

The checked-in repo Docker image now installs the addressbase extra, so Parquet-backed council_tax.query runs fully server-side inside the container rather than depending on a host Python environment.

Error Model

All errors conform to:

{ "isError": true, "code": "<CODE>", "message": "..." }

Primary codes: INVALID_INPUT, UNKNOWN_TOOL, NO_API_KEY, OS_API_KEY_INVALID, OS_API_KEY_EXPIRED, LIVE_DISABLED, OS_API_ERROR, ONS_API_ERROR, NOMIS_API_ERROR, ADMIN_LOOKUP_API_ERROR, COUNCIL_TAX_API_ERROR, UPSTREAM_TLS_ERROR, UPSTREAM_CONNECT_ERROR, INTEGRATION_ERROR, RATE_LIMITED, UNKNOWN_FILTER, NO_OBSERVATION.

Project Structure

server/        FastAPI app & routers
tools/         Tool implementations (one module per domain)
resources/     Static datasets (future expansion)
playground/    Svelte + Vite playground UI
tests/         Pytest suite (≥90% coverage)
docs/          Backlog & design notes
.devcontainer/ Dev environment setup

Note: The Svelte playground is served by Vite (npm run dev). The legacy playground/app.py stub does not serve the UI.

Dynamic Tool Registration

server/mcp/tools.py explicitly imports each tools.* module at startup to guarantee registration in environments where implicit side-effect imports are skipped (e.g. selective packaging or lazy loaders). This ensures /tools/describe always reflects the full catalog without relying on import order.

Testing & Coverage

Run tests with:

./scripts/pytest-local -q

Coverage gate (configured) requires ≥90%. Add tests for both success and error branches (retry paths, validation failures, upstream errors). Avoid broad mocks that skip normalization logic.

Host-side wrappers:

  • ./scripts/pytest-local, ./scripts/ruff-local, and ./scripts/mypy-local run the current repo-supported phased CI slice by default.

  • ./scripts/ruff-local [paths...] and ./scripts/mypy-local [paths...] still prefer the running repo devcontainer app container.

  • If no devcontainer is running, they fall back to the repo .venv.

  • If the tool is still unavailable, they fall back to uv run.

  • Passing explicit paths overrides the default curated slice.

  • Override with MCP_GEO_LOCAL_TOOL_MODE=devcontainer|venv|uv|path.

Strict OWASP MCP validation:

./scripts/validate-owasp-mcp-local

This writes JSON/Markdown report artifacts plus a remediation backlog under output/owasp-mcp-validation/ and fails when any minimum_bar or required control is unmet.

MCP HTTP Hardening

Remote MCP HTTP deployments should enable authenticated access and bounded session state. When auth is enabled, only GET /health remains public; the raw HTTP routes under /tools/*, /resources/*, /playground/*, and /metrics share the same auth boundary as /mcp.

  • MCP_HTTP_AUTH_MODE=hs256_jwt enables bearer JWT enforcement.

  • MCP_HTTP_AUTH_MODE=static_bearer enables a fixed bearer token for /mcp, raw /tools/*, raw /resources/*, /metrics, and /playground/*.

  • MCP_HTTP_AUTH_TOKEN and MCP_HTTP_JWT_HS256_SECRET are included in the shared log/exception redaction path alongside the OS/NOMIS credentials.

  • MCP_HTTP_JWT_HS256_SECRET_FILE loads the signing secret from a mounted file.

  • MCP_HTTP_JWT_ISSUER, MCP_HTTP_JWT_AUDIENCE, and MCP_HTTP_JWT_REQUIRED_SCOPES constrain accepted tokens.

  • MCP_HTTP_SESSION_TTL and MCP_HTTP_SESSION_TOOL_CALL_LIMIT bound session lifetime and tool-call volume.

  • MCP_2026_RC_ENABLED=1 or MCP_PROTOCOL_2026_07_28_ENABLED=1 enables the feature-gated MCP 2026-07-28 release-candidate path for interop testing. Leave these unset for normal stable clients.

  • OS_API_KEY_FILE, OS_API_ACCESS_TOKEN_FILE, NOMIS_UID_FILE, and NOMIS_SIGNATURE_FILE support secret-file delivery without committing live secrets.

  • ops/deployment/docker-compose.prod.yml is the hardened reference deployment used by the OWASP MCP strict evidence set.

Contributing

  • Use Conventional Commits (e.g. feat(tools): add os_places.within pagination).

  • Every PR: update CHANGELOG.md, add/adjust tests, keep coverage ≥90%.

  • Include JSON schemas (input/output) when adding a tool.

  • Prefer incremental refactors; avoid unrelated changes in feature PRs.

Security

Report vulnerabilities through GitHub Private Vulnerability Reporting for this repository. See SECURITY.md for reporting instructions and scope.

Enriched Address Data

os_places.* currently return raw OS Places fields only. Enrichment via local code lists is not implemented yet.

Examples & Golden Tests

See docs/examples.md for sample payloads, conversation flows, and guidance on chaining tools. Golden scenario tests (test_golden_scenarios.py) ensure transformation stability with deterministic mocked upstream responses.

Resource Caching & Provenance

All /resources/read responses include:

  • etag (weak) for conditional requests

  • provenance.retrievedAt timestamp

  • Cache-Control header Clients should respect TTL and still perform ETag revalidation for freshness.

Troubleshooting

See docs/troubleshooting.md for a table of common error codes (INVALID_INPUT, UNKNOWN_TOOL, NO_API_KEY, OS_API_KEY_INVALID, etc.) and remediation steps.

Configuration

Copy .env.example.env and set OS_API_KEY. Optional flags:

  • DEBUG_ERRORS (if present / truthy) enables traceback in error responses; otherwise stack traces are suppressed.

  • CIRCUIT_BREAKER_ENABLED, CIRCUIT_BREAKER_FAILURE_THRESHOLD, CIRCUIT_BREAKER_RESET_SECONDS to control upstream circuit breaker behavior.

SSL & Certificates

Container and dev setup now use the system CA bundle path /etc/ssl/certs/ca-certificates.crt so local corporate root CAs can be added without code changes.

  • Put local proxy/root CA .crt files in .devcontainer/certs/ before rebuilding the devcontainer or Docker image.

  • For proxied networks, set HTTP_PROXY, HTTPS_PROXY, and NO_PROXY via .devcontainer/.env (or host env exports).

  • In this Docker Compose-based devcontainer, container-wide env is sourced from .devcontainer/docker-compose.yml; keep machine-specific values in .devcontainer/.env or your host shell rather than devcontainer.json.

  • Proxy settings are only used at build/runtime injection points and are not persisted into the final runtime image metadata.

  • INSTALL_NGROK is opt-in for the devcontainer build so TLS-inspected networks do not fail on the optional tunnel binary fetch.

License

See LICENSE.

MCP STDIO Adapter (Local Dev)

The JSON-RPC 2.0 STDIO adapter lives in server/stdio_adapter.py (refactored from the prior scripts/os_mcp.py). Legacy entry points remain:

  • Console script: mcp-geo-stdio

  • Wrapper script: scripts/os-mcp (delegates to server/stdio_adapter.py)

This adapter is referenced by mcp.json (mcp-geo-stdio).

Framing

Each request/response:

Content-Length: <bytes>\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}

Methods

Method

Description

initialize

Returns server metadata & capabilities

tools/list

Lists tools (name, description, schemas)

tools/call

Invoke a tool (params.tool, optional params.args)

resources/list

Lists resource descriptors (skills + UI resources)

resources/describe

Returns resource metadata (name, description, license)

resources/read

Fetch resource content (ETag supported)

shutdown

Graceful shutdown (result null)

exit (notify)

Process terminates (no response)

Tool call result shape:

{
 "jsonrpc": "2.0",
 "id": 3,
 "result": { "status": 200, "ok": true, "data": { ...tool output... } }
}

Manual Test

python scripts/os-mcp & PID=$!
printf 'Content-Length: 60\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | nc -U /dev/fd/0 # or use a small Python helper
kill $PID

Simpler: write a tiny Python snippet to send framed messages (see tests/test_stdio_adapter.py).

VS Code

VS Code reads MCP server configuration from .vscode/mcp.json (workspace) or your user-level mcp.json. This repo includes .vscode/mcp.json with:

  • mcp-geo (STDIO, with MCP-Apps UI enabled)

  • mcp-geo-trace (STDIO + JSON-RPC trace log under logs/)

  • mcp-geo-http (HTTP transport at http://127.0.0.1:8000/mcp)

  • .vscode/mcp-geo.toolsets.jsonc (copy to your VS Code user prompts folder as mcp-geo.toolsets.jsonc to group tools in Configure Tools)

See docs/vscode.md for step-by-step setup, MCP-Apps UI validation, and tracing.

Notes

  • resources/read now emits a weak ETag (etag) and supports conditional retrieval via ifNoneMatch param. If matched, response shape: { "jsonrpc":"2.0", "id": <n>, "result": { "notModified": true, "etag": "W/\"...\"" } }.

  • Use the same pagination/filter parameters when revalidating or the variant key changes and a full payload is returned.

  • resources/describe returns the static metadata list (extend as resources grow).

  • Errors follow JSON-RPC error envelope with custom positive codes (1001-1003) for validation and -32603 for internal errors.

Helper Client Script

For quick one-shot invocations without crafting frames manually, use the helper script added in scripts/mcp_client.py (it spawns the adapter, performs initialize, your requested method, then shutdown/exit).

Examples:

# List tools
python scripts/mcp_client.py tools/list

# Describe available ONS dimensions (live mode)
ONS_LIVE_ENABLED=true python scripts/mcp_client.py tools/call ons_data.dimensions '{"params":{"dataset":"gdp","edition":"time-series","version":"1"}}'

# Query observations (live)
ONS_LIVE_ENABLED=true python scripts/mcp_client.py tools/call ons_data.query '{"params":{"dataset":"gdp","edition":"time-series","version":"1","geography":"K02000001","limit":2}}'

# Fetch resource with ETag then conditional request
R1=$(python scripts/mcp_client.py resources/read '{"uri":"skills://mcp-geo/getting-started"}' | jq -r '.response.result.etag')
python scripts/mcp_client.py resources/read '{"uri":"skills://mcp-geo/getting-started","ifNoneMatch":"'$R1'"}'

# Or using the convenience flag (no JSON escaping needed):
python scripts/mcp_client.py resources/read --if-none-match "$R1" '{"uri":"skills://mcp-geo/getting-started"}'

The JSON argument after the tool name is merged into the request params object. Include nested objects as required by each tool schema.

Correct Inline Heredoc Helper (Advanced)

If you prefer piping multiple framed requests to a persistently running adapter instance:

python scripts/os-mcp & APP_PID=$!
python - <<'PY'
import sys, json
def send(mid, method, params=None):
 msg = {"jsonrpc":"2.0","id":mid,"method":method,"params":params or {}}
 body = json.dumps(msg).encode()
 sys.stdout.buffer.write(b"Content-Length: "+str(len(body)).encode()+b"\r\n\r\n"+body)
 sys.stdout.flush()

# Emit two requests (initialize then list tools)
send(1, "initialize")
send(2, "tools/list")
PY | ./scripts/os-mcp
kill $APP_PID

Be careful to avoid duplicating or truncating function definitions when editing inline; each framed JSON-RPC message must be complete and preceded by a correct Content-Length header.

REPL Mode

Interactive session:

python scripts/mcp_client.py --repl
mcp> resources/describe
mcp> resources/read {"uri":"skills://mcp-geo/getting-started"}
mcp> resources/read {"uri":"skills://mcp-geo/getting-started","ifNoneMatch":"W/\"abc123deadbeef00\""}
mcp> exit

notModified responses are compacted by the client for readability.

A
license - permissive license
-
quality - not tested
A
maintenance

Maintenance

Maintainers
Response time
3wRelease cycle
8Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    B
    quality
    D
    maintenance
    A comprehensive MCP server providing 30 tools for geocoding, routing, and OpenStreetMap data analysis. It enables AI assistants to search for locations, calculate travel routes, and perform quality assurance checks on map data.
    30
    3
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    An MCP server for discovering, downloading, querying, and analyzing datasets from Ontario's open data portals, allowing natural language questions and high-performance analytics via DuckDB.
    23
    1
    MIT
  • A
    license
    -
    quality
    B
    maintenance
    An MCP server that gives AI assistants access to UK Parliament data. Query MPs, Lords, bills, votes, committees, debates, and more through AI assistants like Claude Desktop and VS Code Copilot.
    4
    MIT

View all related MCP servers

Related MCP Connectors

  • UK NIHR (National Institute for Health and Care Research) open-data MCP.

  • UK ONS MCP — Office for National Statistics (no auth)

  • Hosted MCP server exposing US hospital procedure cost data to AI assistants

View all MCP Connectors

Latest Blog Posts

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/chris-page-gov/mcp-geo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server