Skip to main content

🇨🇭 Part of the Swiss Public Data MCP Portfolio

🗺️ swisstopo-mcp

Version License: MIT Python 3.11+ MCP No Auth Required CI

MCP server for Swiss federal geodata -- maps, elevation, geocoding, cadastral extracts, and downloadable datasets via Swisstopo APIs

🇩🇪 Deutsche Version


Overview

swisstopo-mcp gives AI assistants access to Switzerland's official geodata infrastructure through 19 tools, all without authentication:

Source Data API
Swisstopo REST API 500+ geodata layers (buildings, boundaries, land use) REST/JSON
Geocoding Official addresses, place names, postal codes REST/JSON
Height Service Elevation above sea level, elevation profiles REST/JSON
STAC Catalog Orthophotos, elevation models, 3D buildings STAC 0.9
WMTS National maps, aerial images, zoning maps URL builder
OEREB Cadastre Public-law restrictions, parcels REST/JSON (cantonal)
geodienste.ch Interkantonale Basisgeodaten (cadastral survey, contaminated sites, hazard maps, …) OGC API Features / WMS / WFS
OpenStreetMap Points of interest (schools, playgrounds, pharmacies, …) Overpass API (ODbL)
OpenPLZ API Administrative address level: postal codes → commune (BFS number) → district → canton REST/JSON (BFS + swisstopo OGD)

Anchor demo query: "Which communes are in the Uster district, and what are their BFS numbers for joining with BFS statistics data?" (The BFS commune number is the official join key to swiss-statistics-mcp and zurich-opendata-mcp — this is what turns a geodata wrapper into a semantic connector at the commune level.) → More use cases by audience →


Features

  • 🗺️ 19 tools (REST, Geocoding, Height, STAC, WMTS, OEREB, geodienste.ch, OpenStreetMap/Overpass, OpenPLZ)
  • 🏛️ Resolve the administrative address level (PLZ → commune/BFS number → district → canton) via OpenPLZ
  • 🔍 Geocode Swiss addresses and reverse-geocode coordinates
  • 🏔️ Query elevation and compute elevation profiles
  • 📦 Discover and download geodatasets (orthophotos, 3D buildings, historical maps)
  • 🏗️ Identify map features at coordinates across 500+ Swisstopo layers
  • 🔗 Generate shareable map.geo.admin.ch links
  • 📋 Look up cadastral property IDs (EGRID) and retrieve OEREB extracts
  • 🔓 No API key required for all tools (OEREB extract needs a supported canton)
  • ☁️ Dual transport -- stdio (Claude Desktop) + Streamable HTTP (cloud)

Prerequisites

  • Python 3.11+
  • uv (recommended) or pip

Installation

# Clone the repository
git clone https://github.com/malkreide/swisstopo-mcp.git
cd swisstopo-mcp

# Install
pip install -e .
# or with uv:
uv pip install -e .

Or with uvx (no permanent installation):

uvx swisstopo-mcp

Quickstart

# stdio (for Claude Desktop)
python -m swisstopo_mcp.server

# Streamable HTTP (port 8000)
python -m swisstopo_mcp.server --http --port 8000

Try it immediately in Claude Desktop:

"Where is Bahnhofstrasse 1, Zurich? Give me the coordinates." "What is the elevation at the Uetliberg summit?" "What buildings are at coordinates 2683500, 1247500 (LV95)?"


Configuration

Claude Desktop

Edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "swisstopo": {
      "command": "python",
      "args": ["-m", "swisstopo_mcp.server"]
    }
  }
}

Or with uvx:

{
  "mcpServers": {
    "swisstopo": {
      "command": "uvx",
      "args": ["swisstopo-mcp"]
    }
  }
}

Config file locations:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Cloud Deployment (SSE for browser access)

For use via claude.ai in the browser (e.g. on managed workstations without local software):

Render.com (recommended):

  1. Push/fork the repository to GitHub
  2. On render.com: New Web Service -> connect GitHub repo
  3. Set start command: python -m swisstopo_mcp.server --http --port 8000
  4. In claude.ai under Settings -> MCP Servers, add: https://your-app.onrender.com/sse

Available Tools

REST API (Layer & Feature Queries)

Tool Description
swisstopo_search_layers Search the Swisstopo layer catalog (500+ layers) by keyword
swisstopo_identify_features Find map features at a specific coordinate (spatial query)
swisstopo_find_features Search features by attribute value within a layer (e.g. buildings by EGID)
swisstopo_get_feature Retrieve full attributes and geometry for a feature by ID

Geocoding

Tool Description
swisstopo_geocode Convert Swiss addresses, place names, or postal codes to coordinates
swisstopo_reverse_geocode Find the nearest address for given coordinates

Height Service

Tool Description
swisstopo_get_height Get elevation above sea level (m a.s.l.) at a coordinate
swisstopo_elevation_profile Compute an elevation profile along a line

STAC Catalog (Geodata Downloads)

Tool Description
swisstopo_search_geodata Search the STAC catalog for downloadable geodatasets
swisstopo_get_collection Get details and download links for a STAC collection

WMTS (Map URLs)

Tool Description
swisstopo_map_url Generate a map.geo.admin.ch URL for browser display

OEREB Cadastre

Tool Description
swisstopo_get_egrid Resolve a cadastral property ID (EGRID) from coordinates
swisstopo_get_oereb_extract Retrieve public-law land-use restrictions (OEREB) for a parcel

Consolidated Geodata Façade

One façade over several map/layer sources, kept under the 18-tool budget (see docs/geodaten-erweiterung-phase1.md):

Tool Description
list_available_layers Discover layer keys for query_geodata (strassenverzeichnis, oereb-verfuegbarkeit, geodienste:<topic>:<canton>); filters to contract-free geodienste datasets
query_geodata Query a chosen layer by point / bbox / commune — amtliches Strassenverzeichnis, interkantonale geodienste.ch data (OGC API Features), or ÖREB availability
query_osm_features OpenStreetMap POIs (schools, playgrounds, pharmacies, …) around a point via Overpass — separate source, ODbL (© OpenStreetMap contributors)

Administrative Address Level (OpenPLZ)

The amtliche address hierarchy PLZ → commune → district → canton, served by the OpenPLZ API (data: BFS municipal directory + swisstopo street directory, Swiss OGD — a separate source and licence from the swisstopo geodata above). Every commune-bearing response exposes bfs_commune_number as a named top-level field: the official join key to BFS statistics (swiss-statistics-mcp) and zurich-opendata-mcp.

Tool Description
lookup_postal_code Resolve a Swiss postal code → locality, commune (+BFS number), district, canton
find_commune Resolve a commune both directions (namebfs_number) or list all communes of a canton / district. Accepts canton abbreviation (ZH) or key (1); resolution happens server-side
search_address Full-text search over Swiss streets and localities, returning commune + BFS number per hit

Example Use Cases

Query Tool
"Where is Bahnhofstrasse 1, Zurich?" swisstopo_geocode
"What is the elevation at the Uetliberg summit?" swisstopo_get_height
"What buildings are at coordinates 2683500, 1247500?" swisstopo_identify_features
"Find orthophoto datasets for download" swisstopo_search_geodata
"Show me a map of Bern at zoom level 10" swisstopo_map_url
"What restrictions apply to parcel at Musterstrasse 5?" swisstopo_get_egrid + swisstopo_get_oereb_extract
"Which schools are within 500 m of Bederstrasse 109, 8002 Zürich, and which streets lead there?" query_osm_features + query_geodata (strassenverzeichnis)
"Which contaminated-sites data is free for canton ZH?" list_available_layers + query_geodata (geodienste:kataster_belasteter_standorte:ZH)
"Which communes are in the Uster district and what are their BFS numbers?" find_commune (district=109)
"Which commune and canton does postal code 8001 belong to?" lookup_postal_code
"What is the BFS number of Winterthur (to join with BFS statistics)?" find_commune (name=Winterthur)

Architecture

┌─────────────────┐     ┌──────────────────────────────┐     ┌──────────────────────────┐
│   Claude / AI   │────▶│  swisstopo-mcp               │────▶│  Swisstopo REST API      │
│   (MCP Host)    │◀────│  (MCP Server)                │◀────│  api3.geo.admin.ch       │
└─────────────────┘     │                              │     ├──────────────────────────┤
                        │  19 Tools                    │────▶│  Geocoding               │
                        │  Stdio | Streamable HTTP     │◀────│  api3.geo.admin.ch       │
                        │                              │     ├──────────────────────────┤
                        │  No authentication required  │────▶│  STAC Catalog            │
                        │  (all tools; OEREB canton opt) │◀────│  data.geo.admin.ch       │
                        │                              │     ├──────────────────────────┤
                        │                              │────▶│  OEREB Cadastre          │
                        │                              │◀────│  (cantonal endpoints)    │
                        │                              │     ├──────────────────────────┤
                        │                              │────▶│  geodienste.ch (OGC API) │
                        │                              │◀────│  overpass.osm.ch (ODbL)  │
                        │                              │     ├──────────────────────────┤
                        │  BFS-Nr = join key to        │────▶│  OpenPLZ API             │
                        │  swiss-statistics-mcp        │◀────│  openplzapi.org (BFS/OGD)│
                        └──────────────────────────────┘     └──────────────────────────┘

Project Structure

swisstopo-mcp/
├── src/swisstopo_mcp/
│   ├── __init__.py              # Package version
│   ├── server.py                # MCP server wiring (tool registrations)
│   ├── api_client.py            # Shared HTTP client (httpx + error handling)
│   ├── geocoding.py             # swisstopo_geocode, swisstopo_reverse_geocode
│   ├── rest_api.py              # swisstopo_search_layers, identify, find, get_feature
│   ├── height.py                # swisstopo_get_height, swisstopo_elevation_profile
│   ├── stac.py                  # swisstopo_search_geodata, swisstopo_get_collection
│   ├── wmts.py                  # swisstopo_map_url
│   ├── oereb.py                 # swisstopo_get_egrid, swisstopo_get_oereb_extract
│   ├── geodata.py               # query_geodata + list_available_layers (façade)
│   ├── overpass.py              # query_osm_features (OpenStreetMap / Overpass)
│   └── openplz.py               # lookup_postal_code, find_commune, search_address (OpenPLZ)
├── tests/
│   ├── test_api_client.py
│   ├── test_geocoding.py
│   ├── test_height.py
│   ├── test_oereb.py
│   ├── test_rest_api.py
│   ├── test_stac.py
│   ├── test_wmts.py
│   ├── test_geodata.py
│   ├── test_overpass.py
│   ├── test_openplz.py
│   └── test_retry.py
├── .github/workflows/ci.yml     # GitHub Actions (Python 3.11/3.12/3.13)
├── pyproject.toml
├── CHANGELOG.md
├── CONTRIBUTING.md               # Contribution guide (English)
├── CONTRIBUTING.de.md            # Contribution guide (German)
├── SECURITY.md                   # Security policy (English)
├── SECURITY.de.md                # Security policy (German)
├── LICENSE
├── README.md                    # This file (English)
└── README.de.md                 # German version

Security & Compliance

The full security policy and posture is documented in SECURITY.md.

Phase

This server is in Phase 1 — Read-only wrapper. All 19 tools are readOnlyHint: true / destructiveHint: false; there are no write or send capabilities. See docs/roadmap.md for later phases.

Lethal Trifecta assessment

Capability Status Rationale
Access to private data ❌ No Public Open Data only (federal/cantonal geodata)
Exposure to untrusted content ⚠️ Limited Reads only from a fixed allow-list of trusted geo.admin / OEREB hosts
External communication (write/send) ❌ No Read-only; no mail/webhook/write tools

Trifecta score: at most 1 of 3 — safe by design.

Egress

Outbound requests are restricted to an explicit code-layer allow-list and redirects are disabled — see docs/network-egress.md.

Container deployment

For containerised HTTP deployments, a hardened Dockerfile and Kubernetes manifests (non-root, read-only root filesystem, dropped capabilities, egress NetworkPolicy) are provided — see docs/deployment.md.

MCP Protocol Version

The MCP protocol version is negotiated by the mcp SDK, which is pinned to the 1.x major in pyproject.toml so an update cannot silently change the negotiated version. SDK bumps are proposed monthly via Dependabot and tracked in CHANGELOG.md.

Sessions & Authentication

The server is unauthenticated by design — it serves only public open data. Over HTTP, session IDs are managed entirely by the FastMCP framework; there is no per-user state, so there is nothing user-specific to bind a session to. If an authenticated deployment is ever introduced, session IDs must be bound to the validated user identity (audit finding SEC-009).

Error handling

  • Execution errors (upstream failure, invalid value) are returned as a ToolResponse with is_error: true and a user-friendly summary; raw exception text is never leaked to the client (it is logged to stderr instead).
  • Protocol errors (unknown tool, malformed/invalid arguments) are emitted by the MCP SDK as JSON-RPC errors with standard codes (e.g. -32602 invalid params). Input validation happens at the Pydantic boundary (SEC-018).

MCP Primitives

This server intentionally exposes Tools only (no Resources or Prompts): it is a Phase-1 read-only wrapper, and every result is a live, parameterised API query rather than a static addressable document. Resources/Prompts may be added in a later phase if stable URI schemes emerge.

Tool workflows

Most tools return a thought-complete result in a single call. Two domains use a short, documented discovery chain (each tool's description states the next step):

  • Feature query: swisstopo_search_layers (find layer IDs) → swisstopo_identify_features / swisstopo_find_featuresswisstopo_get_feature (full detail).
  • Cadastre: swisstopo_geocodeswisstopo_get_egridswisstopo_get_oereb_extract.
  • Downloads: swisstopo_search_geodataswisstopo_get_collection.

Response Format

Every tool returns a structured ToolResponse (FastMCP emits it as structured content with an output schema, plus a JSON text block):

Field Meaning
summary Human-readable Markdown summary
results Machine-readable structured records
count Number of results
match_type exact / fuzzy / none (search-style tools)
source / license Data attribution (OGD-CH, CC/OGD terms)
provenance / retrieved_at How and when the data was obtained
is_error true for handled errors

Known Limitations

  • OEREB tools require a canton parameter; not all cantons expose the same API format
  • STAC catalog uses Swisstopo's v0.9 endpoint; some collections may lack complete metadata
  • Geocoding covers Swiss addresses only (no Liechtenstein)
  • Rate limits are enforced by Swisstopo; high-frequency usage may be throttled

Known findings — OpenPLZ live probe (2026-07-20)

The OpenPLZ endpoints were probed live before implementation. Findings baked into the tools:

Endpoint / behaviour Result Handling
/Cantons 200, 26 records, key = BFS canton number (ZH = 1) canton abbreviation resolved from this list
/Cantons/{key}/Districts|Communes 200 path param is the numeric key
/Cantons/ZH/Districts (abbreviation) 200 + [] — not an error ZH1 resolved server-side; empty answer gets an explanatory note
/Localities?postalCode=8001 200, commune.key = 261 (BFS Zürich) bfs_commune_number surfaced top-level
/Localities?postalCode=9999 (unknown) 200 + [] reported as a note — empty ≠ absent
list endpoints pagination default pageSize=10, hard max 50 (100 → HTTP 400) tools iterate pages via x-total-count
raw umlaut in query (?name=Zürich) HTTP 400 httpx URL-encodes params automatically
historicalCode field key for communes (historized-directory id) not used; the join key is the current key
bulk dump none from OpenPLZ (only /swagger) Architecture A (live-API-only) — adequate for a lookup connector

The abbreviation-vs-key trap in one line: an empty OpenPLZ list is almost never proof that something does not exist — it usually means a wrong path parameter (an abbreviation where a numeric key was expected). The tools resolve abbreviations server-side and annotate every empty result.


Testing

# Unit tests (no network required)
pytest tests/ -m "not live"

# Integration tests (live API calls)
pytest tests/ -m "live"

Changelog

See CHANGELOG.md


Contributing

See CONTRIBUTING.md


License

MIT License -- see LICENSE

Data provided by swisstopo under Open Government Data terms.


Author

Hayal Oezkan · malkreide


Credits & Related Projects

Installation

Run via uv's uvx — no clone or manual install needed. Add to your MCP client config (mcpServers for Claude Desktop, Cursor and Windsurf; use a top-level servers key for VS Code in .vscode/mcp.json):

{
  "mcpServers": {
    "swisstopo-mcp": {
      "command": "uvx",
      "args": [
        "swisstopo-mcp"
      ]
    }
  }
}

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

swisstopo_mcp-0.2.0.tar.gz (137.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

swisstopo_mcp-0.2.0-py3-none-any.whl (56.7 kB view details)

Uploaded Python 3

File details

Details for the file swisstopo_mcp-0.2.0.tar.gz.

File metadata

  • Download URL: swisstopo_mcp-0.2.0.tar.gz
  • Upload date:
  • Size: 137.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for swisstopo_mcp-0.2.0.tar.gz
Algorithm Hash digest
SHA256 a6db9a6c3d4fa0fcc4dc05ba346571bbe0e1aaf5a23ff1e60b4d9f4d3b20d4e7
MD5 daf85cc9cb5b43184285d95cfe551fb4
BLAKE2b-256 655a155de21c72e46390e90b64d6d261c8938133e537a9cecb143d10b5c5155f

See more details on using hashes here.

Provenance

The following attestation bundles were made for swisstopo_mcp-0.2.0.tar.gz:

Publisher: publish.yml on malkreide/swisstopo-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file swisstopo_mcp-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: swisstopo_mcp-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 56.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for swisstopo_mcp-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 329149db19faffe88525f29d0a108f687f9ed02abf48d8fc9dbf36d0f98fe92b
MD5 ba4dd7d966ac1339c5c46a9387dcb353
BLAKE2b-256 735438354020961dd0b4f77e2c9fe12fc0bf52f4fe70aaa145d806b9a3dbe0a5

See more details on using hashes here.

Provenance

The following attestation bundles were made for swisstopo_mcp-0.2.0-py3-none-any.whl:

Publisher: publish.yml on malkreide/swisstopo-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.4.1

2 files

0.4.0

2 files

This release

0.2.0 This release

2 files

0.1.3

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page