{
  "markdown": "<!-- mcp-name: io.github.malkreide/swiss-geodata-mcp -->\n\n> # ⚠️ DEPRECATED — use [`swisstopo-mcp`](https://github.com/malkreide/swisstopo-mcp) instead\n>\n> This server has been **consolidated into [`swisstopo-mcp`](https://github.com/malkreide/swisstopo-mcp)**\n> and is no longer maintained. Every capability it offered now exists there,\n> including the one it used to have exclusively — the official REFRAME\n> coordinate conversion.\n>\n> **Why:** both servers wrapped the same `api3.geo.admin.ch` endpoints and\n> overlapped in five core tools (layer search, identify, find, height,\n> elevation profile). Maintaining two servers for one data source meant double\n> the audits, double the CVE bumps, and an unclear choice for users. The\n> rationale and the migration steps are documented in\n> [`docs/merge-plan-swiss-geodata-mcp.md`](https://github.com/malkreide/swisstopo-mcp/blob/master/docs/merge-plan-swiss-geodata-mcp.md).\n>\n> **This repository will be archived.** The code below still works, but it\n> receives no further fixes or dependency updates. See\n> [Migration](#migration-to-swisstopo-mcp) below.\n\n> 🇨🇭 **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)**\n\n# 🗺️ swiss-geodata-mcp\n\n[![CI](https://github.com/malkreide/swiss-geodata-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/malkreide/swiss-geodata-mcp/actions/workflows/ci.yml)\n[![PyPI](https://img.shields.io/pypi/v/swiss-geodata-mcp)](https://pypi.org/project/swiss-geodata-mcp/)\n[![Python](https://img.shields.io/pypi/pyversions/swiss-geodata-mcp)](https://pypi.org/project/swiss-geodata-mcp/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![swiss-public-data-mcp](https://img.shields.io/badge/portfolio-swiss--public--data--mcp-blue)](https://github.com/malkreide/swiss-public-data-mcp)\n\n**MCP server for Swiss federal geodata (geo.admin.ch).**\n\nConnects AI models to the federal geodata infrastructure: ~700 layers discoverable by keyword, spatial identify at any point, building zones (ARE), municipality lookup (swissBOUNDARIES3D), terrain heights and elevation profiles (swissALTI3D), and WGS84↔LV95 coordinate conversion. Part of the [swiss-public-data-mcp](https://github.com/malkreide/swiss-public-data-mcp) portfolio. **Private project, independent of any employer or institutional affiliation.**\n\n🇩🇪 [Deutsche Version](README.de.md)\n\n---\n\n## Demo query (anchor example)\n\n```\nIn which building zone is the school building at Seilergraben 76 in Zurich,\nand how high above sea level is it?\n```\n\n→ `geo_zoning_at(2683531, 1247914)` + `geo_height(2683531, 1247914)` returns the harmonised ARE zone and 411 m a.s.l. — verified live 2026-07-24.\n\n**Combined with [swiss-housing-mcp](https://github.com/malkreide/swiss-housing-mcp):**\n\n```\nWhat applies at this address?\n```\n\n→ `address_to_egid(\"Seilergraben 76 Zürich\")` (housing) delivers the LV95 coordinates → `geo_zoning_at` + `geo_municipality_at` (geodata) deliver zone and municipality. If the GWR is the *address book*, geo.admin.ch is the *atlas*.\n\n---\n\n## Tools (9)\n\n| Tool | Description | Data source |\n|------|-------------|-------------|\n| `geo_search_layers` | Keyword search over the ~700-layer federal catalogue | geo.admin.ch SearchServer |\n| `geo_identify` | What is at this LV95 point? (any layer) | geo.admin.ch MapServer identify |\n| `geo_find` | Find features on a layer by attribute value | geo.admin.ch MapServer find |\n| `geo_municipality_at` | Municipality + canton containing a point | swissBOUNDARIES3D |\n| `geo_zoning_at` | Building zone(s) at a point | ch.are.bauzonen (ARE) |\n| `geo_height` | Terrain height at a point | swissALTI3D height service |\n| `geo_elevation_profile` | Elevation profile along a line | geo.admin.ch profile service |\n| `geo_layer_info` | Queryable fields + legend (plain text) for a layer | geo.admin.ch MapServer |\n| `geo_convert_coordinates` | WGS84 ↔ LV95 conversion | geodesy.geo.admin.ch reframe |\n\n`geo_search_layers` is the discovery entry point that scales the whole catalogue without one tool per layer; `geo_layer_info` then reveals a layer's queryable fields for `geo_find`.\n\n### Tool annotations (MCP hints)\n\nAll tools are read-only (`readOnlyHint: ✅`, `destructiveHint: ✗`) and query live upstream services (`openWorldHint: ✅`). None are idempotent in the strict caching sense, as upstream data may change between calls.\n\n## Architecture decision\n\nThis server uses **Architecture A (Live-API-only)** — a deliberate deviation from the portfolio's dump-first default, documented per portfolio convention:\n\n- The federal geodata infrastructure spans ~700 layers and terabytes; dump-caching is neither feasible nor useful.\n- `api3.geo.admin.ch` is built exactly for point/feature queries and answered every probe reliably without authentication (live probe 2026-07-24: SearchServer, identify, find, height, profile, legend, layer metadata, reframe — all HTTP 200, No-Auth).\n- Consequence: no local cache, no TTL logic; every response carries `provenance: live_api`.\n\n### Live probe findings (2026-07-24)\n\n| Endpoint | HTTP | Status | Note |\n|---|---|---|---|\n| SearchServer `type=layers` | 200 | ✅ works | catalogue full-text searchable |\n| MapServer identify (bauzonen, boundaries) | 200 | ✅ works | tolerance 0 works for polygon layers |\n| height service | 200 | ✅ works | value arrives as JSON *string* |\n| profile.json (GET + geom) | 200 | ✅ works | COMB/DTM2/DTM25 altitudes |\n| MapServer `{layer}/legend` | 200 | ⚠️ HTML | stripped to plain text in `geo_layer_info` |\n| reframe wgs84↔lv95 | 200 | ✅ works | coordinates arrive as JSON *strings* |\n| Miss on identify/find | 200 | ⚠️ soft | empty `results` array — not an HTTP error |\n\n## Quick start\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"swiss-geodata\": {\n      \"command\": \"uvx\",\n      \"args\": [\"swiss-geodata-mcp\"]\n    }\n  }\n}\n```\n\n### Cloud / Render.com (Streamable HTTP)\n\n```bash\nSWISS_GEODATA_TRANSPORT=streamable-http PORT=8000 swiss-geodata-mcp\n```\n\n## Configuration\n\n| Variable | Default | Purpose |\n|---|---|---|\n| `SWISS_GEODATA_TRANSPORT` | `stdio` | `stdio` \\| `streamable-http` \\| `sse` |\n| `HOST` / `PORT` | `127.0.0.1` / `8000` | HTTP binding (cloud transports only). Defaults to loopback; set `HOST=0.0.0.0` explicitly to expose all interfaces in a cloud deployment. |\n\nNo API keys — Phase 1 is authentication-free.\n\n## Example queries\n\n### School planning\n\n- «Which building zone applies at our planned school extension site?» → `geo_zoning_at`\n- «Which municipality and canton is this coordinate in?» → `geo_municipality_at` (bridges to BFS numbers used by `swiss-statistics-mcp` and `swiss-housing-mcp`)\n- «How steep is the school route between these two points?» → `geo_elevation_profile`\n\n### Layer discovery\n\n- «Is there federal data on noise exposure?» → `geo_search_layers(\"lärm\")` → `geo_layer_info` → `geo_identify`\n\nSee [EXAMPLES.md](EXAMPLES.md) for use cases grouped by audience (schools, parents, general public, developers) and a tool-selection reference table.\n\n## Testing\n\n```bash\nPYTHONPATH=src pytest tests/ -m \"not live\"   # CI-safe (respx-mocked)\nPYTHONPATH=src pytest tests/ -m live         # against real upstream\n```\n\n## Project structure\n\n```\nswiss-geodata-mcp/\n├── src/swiss_geodata_mcp/\n│   ├── server.py      # FastMCP tools (9, prefix geo_*)\n│   ├── geoadmin.py    # geo.admin.ch client + retry + normalisation\n│   ├── models.py      # Pydantic v2 envelopes (source + provenance)\n│   └── __main__.py    # Dual-transport entry point\n├── tests/             # respx-mocked + @pytest.mark.live\n└── .github/workflows/ # CI + OIDC PyPI publish\n```\n\n## Known limitations\n\n- **Registers live elsewhere:** building/dwelling entities (EGID/EWID) belong to [`swiss-housing-mcp`](https://github.com/malkreide/swiss-housing-mcp); this server is the *spatial layer* (zones, boundaries, heights). Deliberate separation to avoid twin servers.\n- The harmonised zoning layer (ch.are.bauzonen) is an ARE synthesis; legally binding is only the cantonal/communal Nutzungsplanung (noted in every `geo_zoning_at` response).\n- School-district polygons are municipal data (→ `zurich-opendata-mcp`), not federal; this server provides municipality boundaries, not Schulkreise.\n- `geo_identify` result counts are capped upstream; area-wide aggregations are out of scope here (see `buildings_in_bbox` in swiss-housing-mcp for the register case).\n- Coordinates must be LV95; WGS84 input fails fast with a pointer to `geo_convert_coordinates`.\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md)\n\n## Contributing\n\nContributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) for how to report bugs, suggest a new layer, or submit code.\n\n## Security\n\nThis is a read-only, no-PII, public-open-data server. See [SECURITY.md](SECURITY.md) for the security posture and how to report a vulnerability.\n\n## License\n\nMIT License — see [LICENSE](LICENSE). Data: Swiss federal geodata infrastructure (geo.admin.ch / swisstopo and publishing federal offices), open government data with attribution.\n\n## Credits & related projects\n\n- Data & services: [geo.admin.ch](https://api3.geo.admin.ch/) (swisstopo), [ARE](https://www.are.admin.ch/), [swisstopo geodesy](https://geodesy.geo.admin.ch/)\n- Portfolio siblings: [`swiss-housing-mcp`](https://github.com/malkreide/swiss-housing-mcp) (register layer), [`swiss-statistics-mcp`](https://github.com/malkreide/swiss-statistics-mcp) (statistics layer), [`zurich-opendata-mcp`](https://github.com/malkreide/zurich-opendata-mcp) (municipal layer)\n\n## Author\n\nmalkreide · [GitHub](https://github.com/malkreide)\n\n---\n\n## Migration to `swisstopo-mcp`\n\nReplace this server in your MCP client config:\n\n```jsonc\n// before\n{ \"swiss-geodata\": { \"command\": \"uvx\", \"args\": [\"swiss-geodata-mcp\"] } }\n// after\n{ \"swisstopo\":     { \"command\": \"uvx\", \"args\": [\"swisstopo-mcp\"] } }\n```\n\n### Tool mapping\n\n| this server | `swisstopo-mcp` | note |\n|---|---|---|\n| `geo_search_layers` | `swisstopo_search_layers` | — |\n| `geo_identify` | `swisstopo_identify_features` | — |\n| `geo_find` | `swisstopo_find_features` | — |\n| `geo_height` | `swisstopo_get_height` | — |\n| `geo_elevation_profile` | `swisstopo_elevation_profile` | takes a coordinate string; set `coordinate_system=\"lv95\"` for LV95 support points |\n| `geo_zoning_at` | `swisstopo_zoning_at` | the non-binding ARE caveat now travels on every result record |\n| `geo_municipality_at` | `swisstopo_municipality_at` | BFS number is named `bfs_commune_number` and normalised to `int` |\n| `geo_layer_info` | `swisstopo_layer_info` | — |\n| `geo_convert_coordinates` | `swisstopo_convert_coordinates` | same `direction` values; same REFRAME service |\n\n### Two differences worth knowing\n\n**Coordinates.** This server was LV95-only and rejected WGS84. `swisstopo-mcp`\naccepts **either** `lat`/`lon` (WGS84) **or** `easting`/`northing` (LV95) on the\npoint-based tools — pass one pair, not both. Existing LV95 call sites keep\nworking; the argument names are the same.\n\n**Response shape.** This server returned a JSON string (`GeoEnvelope`) with the\npayload under `result`. `swisstopo-mcp` returns a structured `ToolResponse`:\nrecords live in `results` (plural, always a list), with `count`, `match_type`,\n`source`, `license` and a Markdown `summary` alongside. Code that parsed\n`result` needs to read `results`.\n",
  "bytes": 11321,
  "sha": "00ba4a713add6f573cfc69522a35178e5789dc7f7de7f5956b15c5cbfccfa974",
  "repo_slug": "malkreide/swiss-geodata-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_malkreide_swiss_geodata_mcp_ccd30295/readme"
}