{
  "markdown": "> 🇨🇭 **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)**\n\n# 🏙️ Zurich Open Data MCP Server\n\n[![PyPI](https://img.shields.io/pypi/v/zurich-opendata-mcp)](https://pypi.org/project/zurich-opendata-mcp/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)\n[![MCP](https://img.shields.io/badge/MCP-Model%20Context%20Protocol-purple)](https://modelcontextprotocol.io/)\n[![No Auth Required](https://img.shields.io/badge/auth-none%20required-brightgreen)](https://github.com/malkreide/zurich-opendata-mcp)\n![CI](https://github.com/malkreide/zurich-opendata-mcp/actions/workflows/ci.yml/badge.svg)\n\n🌐 **English** | **[Deutsch](README.de.md)**\n\nAn MCP (Model Context Protocol) server providing AI-powered access to **Open Data from the City of Zurich, Switzerland**.\n\n> Enables Claude, ChatGPT, and other MCP-compatible AI assistants to directly query 900+ datasets, geodata, parliamentary proceedings, council resolutions, tourism data, linked data, and real-time environmental and mobility information from the City of Zurich. **23 Tools (+3 deprecated aliases), 5 Resources, 6 APIs.**\n\n### Demo\n\n![Demo: Claude using zurich_parking_live and zurich_air_quality](docs/assets/demo.svg)\n\n## ✨ Features\n\n### CKAN Open Data (data.stadt-zuerich.ch)\n- **`zurich_search_datasets`** – Full-text search with Solr syntax across 900+ datasets\n- **`zurich_get_dataset`** – Complete metadata and download URLs for a dataset\n- **`zurich_datastore_query`** – Query tabular data directly (filters, sorting)\n- **`zurich_datastore_sql`** – SQL queries on the DataStore\n- **`zurich_list_categories`** – Browse 19 thematic categories\n- **`zurich_list_tags`** – Tag-based thematic search\n\n### Real-Time Environmental Data\n- **`zurich_weather_live`** – 🌤️ Current weather (temperature, humidity, pressure, rain) from 4 UGZ stations\n- **`zurich_air_quality`** – 🌬️ Live air quality (NO₂, O₃, PM10, PM2.5) with WHO thresholds\n- **`zurich_water_weather`** – 🌊 Lake Zurich data (water temperature, level, wind) every 10 min\n\n### Real-Time Mobility Data\n- **`zurich_pedestrian_traffic`** – 🚶 Pedestrian counts on Bahnhofstrasse (3 locations, hourly)\n- **`zurich_vbz_passengers`** – 🚊 VBZ public transit ridership (800,000+ records, all lines/stops)\n- **`zurich_parking_live`** – 🅿️ Real-time occupancy of 36 parking garages (ParkenDD)\n\n### Geoportal (WFS Geodata)\n- **`zurich_geo_layers`** – 📍 List 14 available geodata layers\n- **`zurich_geo_features`** – 📍 Fetch GeoJSON features (schools, districts, playgrounds, climate data, etc.)\n\n### City Parliament (Paris API)\n- **`zurich_parliament_search`** – 🏛️ Search parliamentary proceedings (interpellations, motions, postulates)\n- **`zurich_parliament_members`** – 🏛️ Search council members (party, commissions, mandates)\n\n### Zurich Tourism\n- **`zurich_tourism`** – 🏨 Attractions, restaurants, hotels, events (Schema.org data, 4 languages)\n\n### Linked Data (SPARQL)\n- **`zurich_sparql`** – 📊 SPARQL queries on the statistical linked data endpoint *(endpoint not productive yet — the tool is **not registered by default**; opt in with the environment variable `ZURICH_OPENDATA_ENABLE_SPARQL=1`)*\n\n### Stadtratsbeschlüsse (Council Resolutions)\n- **`zurich_strb_search`** – 📜 Full-text search of public council resolutions (title, department, date range)\n- **`zurich_strb_by_department`** – 📜 List all resolutions of a department (e.g. `SSD`, `FD`, `PRD`)\n- **`zurich_strb_detail`** – 📜 Single resolution by `NNNN/YYYY` number\n\n*(The former names `search_stadtratsbeschluesse`, `get_beschluesse_by_departement` and `get_stadtratsbeschluss_detail` remain available as deprecated aliases until the next major release.)*\n\n### Analysis Tools\n- **`zurich_analyze_datasets`** – Comprehensive analysis: relevance, recency, data structure\n- **`zurich_catalog_stats`** – Catalog overview with statistics\n- **`zurich_find_school_data`** – Curated search for education-related datasets\n\n### MCP Resources\n- `zurich://dataset/{name}` – Dataset metadata\n- `zurich://category/{group_id}` – Category details\n- `zurich://parking` – Current parking data\n- `zurich://geo/{layer_id}` – GeoJSON geodata (14 layers)\n- `zurich://tourism/categories` – Tourism categories\n\n## 🚀 Installation\n\n### Prerequisites\n- Python 3.11+\n- pip or uv\n- `mcp[cli]` 2.x — installed automatically; the server uses the 2.x API\n  (`mcp.server.mcpserver`) and cannot run on `mcp` 1.x\n\n> **Use 0.6.0 or newer.** Release `0.5.1` declared `mcp[cli]>=1.28.1` with no\n> upper bound. Once `mcp` 2.0.0 removed `mcp.server.fastmcp`, every fresh\n> install of `0.5.1` resolved to 2.0.0 and failed at import with\n> `ModuleNotFoundError`. `0.6.0` runs on the 2.x API and pins `>=2.0.0,<3`.\n> If you are pinned to `0.5.1`, upgrade — there is no working configuration of\n> that release left.\n\n### Install\n```bash\n# Clone\ngit clone https://github.com/malkreide/zurich-opendata-mcp.git\ncd zurich-opendata-mcp\n\n# Install\npip install -e .\n\n# Or with uv\nuv pip install -e .\n```\n\n## ⚙️ Configuration\n\n### Claude Desktop\n\nEdit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):\n\n```json\n{\n  \"mcpServers\": {\n    \"zurich-opendata\": {\n      \"command\": \"python\",\n      \"args\": [\"-m\", \"zurich_opendata_mcp.server\"],\n      \"env\": {}\n    }\n  }\n}\n```\n\nAlternatively, using the installed command:\n\n```json\n{\n  \"mcpServers\": {\n    \"zurich-opendata\": {\n      \"command\": \"zurich-opendata-mcp\"\n    }\n  }\n}\n```\n\n### Claude Code (CLI)\n\n```bash\nclaude mcp add zurich-opendata -- python -m zurich_opendata_mcp.server\n```\n\n### Cursor / VS Code\n\nAdd to `.vscode/settings.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"zurich-opendata\": {\n      \"command\": \"python\",\n      \"args\": [\"-m\", \"zurich_opendata_mcp.server\"]\n    }\n  }\n}\n```\n\n## 💬 Example Queries\n\nOnce configured, you can ask Claude:\n\n### Open Data\n- *\"What datasets are available about schools in Zurich?\"*\n- *\"Show me school holidays for public schools\"*\n- *\"Analyze the available geodata\"*\n\n### Real-Time Data\n- *\"What's the current temperature in Zurich?\"* → `zurich_weather_live`\n- *\"How is the air quality today?\"* → `zurich_air_quality`\n- *\"What's the water temperature in Lake Zurich?\"* → `zurich_water_weather`\n- *\"How many parking spaces are free right now?\"* → `zurich_parking_live`\n- *\"How many people are on Bahnhofstrasse right now?\"* → `zurich_pedestrian_traffic`\n\n### Geodata\n- *\"Show me all school facilities in Zurich as GeoJSON\"* → `zurich_geo_features`\n- *\"What geodata layers are available?\"* → `zurich_geo_layers`\n- *\"Where are the playgrounds in Zurich?\"*\n\n### City Parliament\n- *\"What parliamentary motions about schools were filed?\"* → `zurich_parliament_search`\n- *\"Which council members belong to the SP party?\"* → `zurich_parliament_members`\n\n### Council Resolutions (Stadtratsbeschlüsse)\n- *\"Find council resolutions about Volksschule from 2025\"* → `zurich_strb_search`\n- *\"List all SSD resolutions in 2025\"* → `zurich_strb_by_department`\n- *\"Show council resolution 1203/2025\"* → `zurich_strb_detail`\n\n### Tourism\n- *\"What restaurants does Zurich Tourism recommend?\"* → `zurich_tourism`\n\n## 🔗 Data Sources\n\n| API | Endpoint | Data |\n|-----|----------|------|\n| **CKAN** | data.stadt-zuerich.ch/api/3/ | 900+ open datasets |\n| **Geoportal WFS** | ogd.stadt-zuerich.ch/wfs/geoportal | 14 geodata layers (GeoJSON) |\n| **Paris API** | gemeinderat-zuerich.ch/api | Parliamentary proceedings & members |\n| **Zurich Tourism** | zuerich.com/en/api/v2/data | Attractions, restaurants, hotels |\n| **SPARQL** | ld.stadt-zuerich.ch/query | Linked Open Data / statistics |\n| **ParkenDD** | api.parkendd.de/Zuerich | Real-time parking occupancy |\n\n## 📊 Available Data Categories\n\n| Category | ID |\n|----------|-----|\n| Employment | `arbeit-und-erwerb` |\n| Base Maps | `basiskarten` |\n| Construction & Housing | `bauen-und-wohnen` |\n| Population | `bevolkerung` |\n| **Education** | **`bildung`** |\n| Energy | `energie` |\n| Finance | `finanzen` |\n| Leisure | `freizeit` |\n| Health | `gesundheit` |\n| Crime | `kriminalitat` |\n| Culture | `kultur` |\n| Mobility | `mobilitat` |\n| Politics | `politik` |\n| Prices | `preise` |\n| Social Affairs | `soziales` |\n| Tourism | `tourismus` |\n| Environment | `umwelt` |\n| Administration | `verwaltung` |\n| Economy | `volkswirtschaft` |\n\n## 📍 Available Geo Layers\n\nSource of truth: `GEOPORTAL_LAYERS` in [`src/zurich_opendata_mcp/config.py`](src/zurich_opendata_mcp/config.py).\n\n| Layer ID | Description |\n|----------|-------------|\n| `schulanlagen` | School facilities (kindergartens, schools, after-school care) |\n| `schulkreise` | School district boundaries (polygons) |\n| `schulwege` | School-route crossings and hazard points |\n| `stadtkreise` | City district boundaries (polygons) |\n| `spielplaetze` | Public playgrounds |\n| `kreisbuero` | City district offices |\n| `sammelstelle` | Waste collection points |\n| `sport` | Sports facilities |\n| `klimadaten` | Climate data (raster, temperatures, heat islands) |\n| `lehrpfade` | Educational trails |\n| `stimmlokale` | Polling stations |\n| `sozialzentrum` | Social centres |\n| `velopruefstrecken` | Bicycle exam routes for schools |\n| `familienberatung` | Family-counselling meeting points |\n\n## 🏗️ Project Structure\n\n```\nzurich-opendata-mcp/\n├── src/zurich_opendata_mcp/\n│   ├── __init__.py\n│   ├── app.py               # Shared FastMCP instance\n│   ├── server.py            # Console entry + back-compat re-exports\n│   ├── config.py            # Endpoints, layer maps, resource IDs\n│   ├── http_client.py       # Shared httpx client + CKAN wrapper\n│   ├── formatters.py        # CKAN→model mapping + Markdown rendering\n│   ├── models.py            # Pydantic structured-output models\n│   ├── clients/             # API clients: paris, sparql, tourism, wfs\n│   └── tools/               # @mcp.tool implementations:\n│                            #   catalog, datastore, geo, parliament,\n│                            #   realtime, sparql, strb, tourism,\n│                            #   resources (zurich:// URIs)\n├── tests/                   # respx round-trip, unit and live-marked tests\n├── audits/                  # Code-audit reports\n├── .github/workflows/       # ci.yml + publish.yml (Trusted Publisher)\n├── pyproject.toml\n├── README.md / README.de.md\n├── CONTRIBUTING.md / .de.md\n├── SECURITY.md / .de.md\n├── CHANGELOG.md\n├── CLAUDE.md                # Project conventions for Claude\n├── LICENSE\n└── claude_desktop_config.json\n```\n\n## 🧪 Development\n\n```bash\n# Install dev dependencies\npip install -e \".[dev]\"\n\n# Unit + validation tests (no network)\npytest tests/ -m \"not live\"\n\n# Live integration tests (against live APIs — opt-in)\npytest tests/ -m live\n\n# Linting\nruff check src/ tests/\n```\n\n## 🌐 HTTP transport\n\nBy default the server speaks MCP over stdio. `--http` serves Streamable HTTP\ninstead:\n\n```bash\nzurich-opendata-mcp --http --port 8000              # binds 127.0.0.1 (default)\nzurich-opendata-mcp --http --host 0.0.0.0 --port 8000\n```\n\n| Option | Meaning | Default |\n|---|---|---|\n| `--http` | Serve Streamable HTTP instead of stdio | _(off → stdio)_ |\n| `--host` | Bind address | `127.0.0.1` |\n| `--port` | Bind port (1–65535) | `8000` |\n| `MCP_ALLOWED_HOSTS` | Comma-separated names this server is reachable under, **port included** (e.g. `zurich.example.ch:8000`). Requests under any other `Host` get **421**; loopback stays allowed so container health checks keep working. | _(unset)_ |\n\n**The loopback default is deliberate.** Binding `0.0.0.0` exposes the server on\nevery interface, to everyone who can reach the machine — there is no\nauthentication in front of it.\n\n**Set `MCP_ALLOWED_HOSTS` whenever you bind beyond loopback.** It guards against\n**DNS rebinding**: a page on your network resolves its own hostname to this\nserver's address and then talks to it from the browser. From the browser's point\nof view that request is same-origin, so no origin rule stops it — only the\n`Host` check does.\n\nLeft unset on a non-loopback bind, the check stays **off** and a warning is\nlogged. That is the right default only when something in front of the server\nvalidates `Host`. It is deliberately not guessed: on `0.0.0.0` the reachable\nname is unknowable inside the process, and a wrong guess would answer the very\ndeployment it is meant to protect with 421 on every request.\n\n## MCP Protocol Version\n\nThis server speaks **two protocol eras** over the same endpoint. The client's\nfirst request on a connection decides which one applies; a later claim from the\nother era is refused.\n\n| Era | Revision | Who reaches it |\n|---|---|---|\n| `initialize` handshake | `2024-11-05` … **`2025-11-25`** | What today's clients speak. The server answers with the revision asked for, or with the `2025-11-25` ceiling when the request asks for something newer. |\n| Per-request envelope | **`2026-07-28`** | A request carrying the `2026-07-28` `_meta` envelope opens a modern connection. |\n\nBoth revisions are pinned in\n[`tests/test_protocol_version.py`](tests/test_protocol_version.py) and asserted\nagainst the installed SDK, so a Dependabot bump of `mcp` cannot move either one\nsilently. This server builds no ASGI app to send an `initialize` through, so\nthe gate asserts the SDK constants rather than a measured response — the\nweaker form, named rather than left unsaid.\n\nNote that the SDK's `LATEST_PROTOCOL_VERSION` is an alias for the **modern**\nera, not for the handshake era — pinning against it alone would leave the era\nthat current clients actually negotiate free to drift.\n\n**Update policy.** When the gate fails, do not edit the constant blindly: read\nthe spec changelog between the two revisions, verify the server still behaves,\nthen move the constant, this section, `README.de.md` and\n[`CHANGELOG.md`](CHANGELOG.md) together.\n\n## Safety & Limits\n\n- **Read-only:** All tools perform HTTP GET requests only — no data is written, modified, or deleted.\n- **No personal data:** The APIs return open civic datasets (parking occupancy, weather readings, parliamentary proceedings). No personally identifiable information (PII) is processed or stored by this server.\n- **Rate limits:** CKAN Solr search and ParkenDD are public APIs without documented rate limits; use `rows` and `limit` parameters conservatively. The server enforces a 30s timeout per request; transient upstream errors (connect failures, HTTP 502/503/504) are retried once with a short backoff.\n- **Data freshness:** Real-time tools (parking, weather, air quality) reflect the upstream source at query time. Measurement data is never cached; only the lookup of the current per-year UGZ resource ID (weather/air quality) is cached in-process for 24h.\n- **Terms of service:** Data is subject to the ToS of each source — [data.stadt-zuerich.ch](https://data.stadt-zuerich.ch), [ParkenDD](https://github.com/offenesdresden/ParkAPI), [gemeinderat-zuerich.ch](https://www.gemeinderat-zuerich.ch). All City of Zurich data is published under CC0 (Open by Default since 2021).\n- **No guarantees:** This server is a community project, not affiliated with the City of Zurich or any of the API providers. Availability depends on upstream APIs.\n\n---\n\n## 🤝 Contributing\n\nContributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) ([Deutsch](CONTRIBUTING.de.md)).\n\n## 🔒 Security\n\nRead-only, no PII, no authentication, a fixed set of public-data endpoints. See\n[SECURITY.md](SECURITY.md) ([Deutsch](SECURITY.de.md)) for the full security\nposture and accepted-risk decisions.\n\n## 📜 License\n\nMIT License — see [LICENSE](LICENSE). All data used is published under open\nlicenses (CC0 / Open by Default since 2021).\n\n## 👤 Author\n\nHayal Oezkan · [malkreide](https://github.com/malkreide)\n\n---\n\n*Powered by [Model Context Protocol](https://modelcontextprotocol.io/) • 6 APIs • 23 Tools • 5 Resources*\n\n<!-- mcp-name: io.github.malkreide/zurich-opendata-mcp -->\n\n<!-- BEGIN GENERATED: install -->\n## Installation\n\nRun via [`uv`](https://docs.astral.sh/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`):\n\n```json\n{\n  \"mcpServers\": {\n    \"zurich-opendata-mcp\": {\n      \"command\": \"uvx\",\n      \"args\": [\n        \"zurich-opendata-mcp\"\n      ]\n    }\n  }\n}\n```\n<!-- END GENERATED: install -->\n",
  "bytes": 16426,
  "sha": "2972b2cd794ef7d82cd81a67de4ecfb76297d9b53d6e5ca3f180d5f62ebc70b2",
  "repo_slug": "malkreide/zurich-opendata-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_malkreide_zurich_opendata_mcp_0821ea74/readme"
}