{
  "markdown": "> **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide/swiss-public-data-mcp)** — a collection of open-source MCP servers connecting AI agents to Swiss public and open data.\n> This is a private project. It is not affiliated with, endorsed by, or operated on behalf of any employer or public authority.\n\n# i14y-mcp\n\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)\n[![MCP](https://img.shields.io/badge/MCP-server-orange.svg)](https://modelcontextprotocol.io/)\n[![Data: I14Y](https://img.shields.io/badge/data-I14Y%20%7C%20BFS-red.svg)](https://www.i14y.admin.ch)\n\n**MCP server for the I14Y interoperability platform — Switzerland's national metadata catalogue.**\n\n🇩🇪 [Deutsche Version](README.de.md)\n\n---\n\n## Why this server exists\n\nThe other servers in this portfolio answer *«what does the data say?»*.\nThis one answers the question that comes first: **«who publishes data on this\ntopic, through which interface, under which licence?»**\n\nI14Y is the national data catalogue maintained by the Federal Statistical\nOffice. It describes datasets, registered APIs, public services and harmonised\nconcepts from the Confederation, cantons and communes, using the DCAT-AP-CH\nprofile (eCH-0200).\n\n> **Mnemonic: «Catalogue before shelf.»** Without a catalogue, an agent has to\n> already know a data source exists. With one, it can find it.\n\n---\n\n## 🎯 Anchor Demo Query\n\n> *«Which authority publishes data on special needs education, through which\n> interface is it available, and under which licence?»*\n\n```\nsearch_catalog(query=\"Sonderpädagogik\")\n  → «Statistik der Sonderpädagogik» — Federal Statistical Office (BFS), theme: Bildung\n\nget_dataset(dataset_id=...)\n  → 2 distributions, licence: «Opendata BY ASK — attribution required,\n    commercial use only with permission from the data supplier»\n  → contact: auskunftsdienst@bfs.admin.ch\n```\n\nTwo tool calls turn a vague topic into a named authority, a download URL and a\nlicence you can act on — `get_dataset` aggregates the distributions, licences\nand contact point into one record.\n\n### Demo\n\n![Demo: Claude using search_catalog and get_dataset](docs/assets/demo.svg)\n\n---\n\n## Architecture\n\n```\n                 ┌──────────────────────────────┐\n                 │      MCP Host (Claude)       │\n                 └───────────────┬──────────────┘\n                                 │ stdio | streamable-http\n                 ┌───────────────▼──────────────┐\n                 │          i14y-mcp            │\n                 │  ┌────────────────────────┐  │\n                 │  │ server.py  (13 tools)  │  │\n                 │  ├────────────────────────┤  │\n                 │  │ mappers.py             │  │  DCAT → flat, one language\n                 │  ├────────────────────────┤  │\n                 │  │ models.py  (Pydantic)  │  │  source + provenance envelope\n                 │  ├────────────────────────┤  │\n                 │  │ client.py              │  │  retry 2s/4s/8s, no-retry 4xx\n                 │  └────────────────────────┘  │\n                 └───────────────┬──────────────┘\n                                 │ HTTPS, no auth\n                 ┌───────────────▼──────────────┐\n                 │  api.i14y.admin.ch/api       │\n                 │  datasets · dataservices ·   │\n                 │  concepts · publicservices · │\n                 │  catalogs · agents · search  │\n                 └──────────────────────────────┘\n```\n\n### Architecture decision\n\nThis server uses **Architecture A (live API only)**.\n\nRationale (verified live on 2026-07-21):\n- All read endpoints respond without authentication and paginate correctly.\n- No bulk download of catalogue metadata is offered, and none is needed.\n- Error responses follow RFC 7807, so failure modes are distinguishable.\n\nConsequences:\n- Every HTTP call retries transient failures with 2 s / 4 s / 8 s backoff.\n- `search_catalog` caps results client-side because the upstream ignores paging.\n- `api_status` always returns an evaluable state instead of empty records.\n\nFull probe report: [`docs/probe-i14y.md`](docs/probe-i14y.md).\n\n### Project phase\n\nThis server is in **Phase 1 (read-only)** of the portfolio's «Read-only First»\nphase architecture: all tools are read-only, there is no authentication and no\npersonal data. See [`docs/roadmap.md`](docs/roadmap.md) for the phase model and\nthe prerequisites for any future write capability.\n\n---\n\n## Tools\n\n| Tool | Purpose |\n|---|---|\n| `search_catalog` | Free-text search across the catalogue. Entry point. |\n| `list_datasets` | Paginated dataset register (complete, unlike search). |\n| `get_dataset` | Full metadata record for one dataset. |\n| `get_dataset_distributions` | Download URLs, formats and **licences**. |\n| `list_data_services` | Register of official Swiss APIs with endpoint URLs. |\n| `get_data_service` | Full record for one registered interface. |\n| `list_public_services` | Administrative services for citizens. |\n| `list_concepts` | Harmonised concepts and code lists. |\n| `get_concept` | One concept definition. |\n| `search_codelist_entries` | Individual codes of a code list. |\n| `list_publishers` | Publishing bodies, with Swiss UID. |\n| `list_catalogs` | Contributing catalogues. |\n| `api_status` | Reachability check with graceful degradation. |\n\nAll tools are annotated `readOnlyHint: true`. Write operations exist in the\nupstream API but are deliberately not exposed.\n\n### MCP primitives\n\nThis server exposes **Tools only** — no Resources, no Prompts. That is a\ndeliberate choice, not an omission: I14Y is queried by free-text search and by\nopaque UUIDs, so there is no small, stable set of addressable URIs that would map\ncleanly onto MCP Resources, and the server ships no opinionated prompt templates.\nEvery tool is read-only and idempotent; if a future stable entry point emerges\n(e.g. a fixed theme list) it is a candidate for a Resource.\n\n### MCP protocol version\n\nBuilt against the MCP Python SDK (`mcp >= 1.28.1`), which negotiates the protocol\nversion with the client at initialize time. The tested SDK floor is pinned in\n`pyproject.toml`; [Dependabot](.github/dependabot.yml) opens monthly SDK-update\nPRs, and any change that bumps the negotiated spec version is called out in\n[`CHANGELOG.md`](CHANGELOG.md).\n\n---\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. The handshake ceiling is measured against a live `initialize` through\nthe assembled ASGI stack, not read off a constant name.\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---\n\n## Installation\n\n```bash\nuvx i14y-mcp\n```\n\nOr from source:\n\n```bash\ngit clone https://github.com/malkreide/i14y-mcp\ncd i14y-mcp\npip install -e \".[dev]\"\n```\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"i14y\": {\n      \"command\": \"uvx\",\n      \"args\": [\"i14y-mcp\"]\n    }\n  }\n}\n```\n\n### Remote deployment (Render, Railway)\n\n```bash\nI14Y_MCP_TRANSPORT=sse HOST=0.0.0.0 PORT=8000 i14y-mcp\n```\n\n`I14Y_MCP_TRANSPORT` accepts `stdio` (default), `sse` or `streamable-http`. The\nHTTP transports bind to `HOST`, which defaults to `127.0.0.1` (loopback); set\n`HOST=0.0.0.0` to expose the port on a PaaS (the Docker image already does).\nCORS exposes the `Mcp-Session-Id` header so browser MCP clients keep their session.\nWhich browser origins may call the server comes from `I14Y_MCP_CORS_ORIGINS`, a\ncomma-separated list — **unset means no browser client is permitted at all**,\nwhich is the default. `*` is still accepted and logs a warning. stdio and other\nnon-browser clients are unaffected either way.\n\n### Docker\n\n```bash\ndocker compose up --build      # SSE transport on http://localhost:8000\n```\n\nThe image is a hardened multi-stage build: it runs as a non-root user, ships no\nbuild tools, and needs no secrets (the API is unauthenticated). See\n[`Dockerfile`](Dockerfile) and [`compose.yaml`](compose.yaml).\n\n---\n\n## Join keys\n\nI14Y is a connector layer. Two identifiers make it composable with the rest of\nthe portfolio:\n\n| Key | Field | Joins to |\n|---|---|---|\n| Swiss UID | `Publisher.uid` | [`register-mcp`](https://github.com/malkreide/register-mcp) (Zefix) |\n| Endpoint URL | `DataServiceSummary.endpoint_urls` | any portfolio server wrapping that API |\n\n---\n\n## Known limitations\n\nVerified live on 2026-07-21.\n\n1. **The search index covers roughly half the register.** `search_catalog`\n   returns at most 1013 records; `list_datasets` reaches about 2003. Use\n   `list_datasets` when completeness matters.\n2. **Search returns Datasets only.** Filtering by `types=[\"Concept\"]` or\n   `types=[\"DataService\"]` yields zero results even though those entities\n   exist. Use `list_concepts` and `list_data_services` instead.\n3. **The upstream ignores paging on search.** The full result set is always\n   returned; this server caps it at 200 records and sets `truncated: true`.\n4. **Licences vary per distribution**, not per dataset. Most carry\n   «Opendata BY ASK», which requires attribution and restricts commercial use.\n   Always read the `licence` field before reuse.\n5. **Some metadata fields are simply empty.** Frequency, temporal coverage and\n   distribution format are optional and frequently unset by publishers. This is\n   a data-quality property of the catalogue, not a bug in this server.\n6. **Not every entry with an endpoint has a URL.** Entries labelled only\n   «OpenAPI Spezifikation» without a URI are surfaced as `(no URI) <label>`\n   rather than dropped.\n\n---\n\n## Testing\n\n```bash\nPYTHONPATH=src pytest tests/ -m \"not live\"   # offline, used in CI\nPYTHONPATH=src pytest tests/ -m \"live\"       # hits the real API\nPYTHONPATH=src pytest tests/                 # everything\npython -m ruff check src tests\n```\n\nThe live tests are not decoration: fundstück 4 in the probe report — keywords\nnesting their language object under `label` — was caught by a live test after\nthe unit tests were already green.\n\n---\n\n## Contributing\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md) for the ground rules (read-only, one\negress host, no secrets) and the local dev loop. Maintainers: [`PUBLISHING.md`](PUBLISHING.md)\ncovers the PyPI / MCP Registry release process.\n\n---\n\n## Security\n\nSee [`SECURITY.md`](SECURITY.md) for the security posture and how to report a\nvulnerability.\n\n---\n\n## License\n\nMIT License — see [LICENSE](LICENSE). The catalogue data remains subject to the\nterms declared by each publisher.\n\n---\n\n## Author\n\n**Hayal Oezkan** · [github.com/malkreide](https://github.com/malkreide)\n\n---\n\n## Credits & related projects\n\n- Data: [I14Y Interoperability Platform](https://www.i14y.admin.ch), Federal Statistical Office (BFS)\n- Standard: [eCH-0200 / DCAT-AP-CH](https://www.ech.ch/de/ech/ech-0200/1.0)\n- Source discovery inspired by [rnckp/awesome-ogd-switzerland](https://github.com/rnckp/awesome-ogd-switzerland)\n- Portfolio: [swiss-public-data-mcp](https://github.com/malkreide/swiss-public-data-mcp)\n- Protocol: [Model Context Protocol](https://modelcontextprotocol.io/)\n\nLicence: MIT. The catalogue data remains subject to the terms declared by each\npublisher.\n\n---\n\n## MCP Registry\n\nOwnership marker used by the [MCP Registry](https://registry.modelcontextprotocol.io)\nto link this PyPI package to the GitHub namespace:\n\n```\nmcp-name: io.github.malkreide/i14y-mcp\n```\n",
  "bytes": 12449,
  "sha": "c9d773568b2ac7571b020f8a8e001c350b82114bf6429ced5251149001bcc19f",
  "repo_slug": "malkreide/i14y-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_malkreide_i14y_mcp_253647f5/readme"
}