discover-swiss-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@discover-swiss-mcpSuggest museums near Zurich HB and vegetarian lunch spots nearby."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
π¨π Part of the Swiss Public Data MCP Portfolio
discover-swiss-mcp
MCP server for discover.swiss Infocenter Open β search Swiss tourism data with per-object attribution
π©πͺ Deutsche Version
Status
pre-release β search entitlement confirmation from discover.swiss: pending.
Item | State |
Search entitlement (written confirmation by discover.swiss) | pending β release gate |
Tools | all eight registered (P3), live canaries in place (P4) |
Phase | P5 β remediation of the audit of 2026-09-26 (see Phases and gates) |
Release | none; version 0.1.0 is not published |
Release P5 (PyPI, MCP registry, public reference instance) | on hold since 2026-09-26 β waits for the written confirmation |
The live probe of 2026-09-17 found that /search works for the Open
subscription β full text, distance ranking, date filters, 31 facets β although
the official documentation states the opposite ("You can't use the search
functionality"). The whole server is built on that endpoint, which makes the
contradiction its main risk: an entitlement that contradicts the docs can be
withdrawn without notice.
Written confirmation from discover.swiss is a release gate. No release before that conversation has happened. Once it has, set
DISCOVER_SWISS_ENTITLEMENT_CONFIRMED=YYYY-MM-DDand this section changes from pending to confirmed with that date.source_statusreports the same state at runtime, so a host can see it without reading this file.If the entitlement is withdrawn, the server keeps answering through a narrower list fallback (see Architecture decision).
Release on hold (2026-09-26). The release step was started on 2026-09-26 and stopped at its precondition: no written confirmation from discover.swiss is on record. Held until it arrives: the PyPI and MCP-registry release of 0.1.0, the public reference instance (it would run on the operator's key, which the confirmation has to cover), the portfolio status change and gate G1 over a remote endpoint. Until then the server runs locally with your own key β see docs/DEMO.md.
Related MCP server: meteoswiss-mcp
Phases and gates
The server is read-only in every phase β no tool writes, and none will before a phase that says so explicitly. Each phase ends at a gate: what was built, what is open, and a decision to continue or to discard.
Phase | Content | Gate |
P1 | Live probe, scaffold, client | probe report, architecture decision |
P2 | Core tools | anchor queries live |
P3 |
| stop-gate run live |
P4 | Live canaries, audit, documentation | canaries green, audit published |
P5 | Remediation of the audit findings marked fix before release | targeted re-audit, canaries green |
Release | 0.1.0 on PyPI and in the MCP registry | written search confirmation by discover.swiss, no open release blocker |
Anchor demo queries
Three questions this server is built to answer, chosen to fit the coverage it actually has. The tool chain for each β which tool, in which order, with which parameters β is in docs/DEMO.md, reproducible by anyone with a key.
City (Zurich pilot) β "I have a rainy day in Zurich β which museums are within walking distance of the main station, and where do I eat vegetarian afterwards?"
searchβget_detailsβsearch; ZΓΌrich Tourismus as the source (CC BY-SA), attribution visible.Outdoor (Glarnerland) β "I'm in Braunwald: which hikes with little ascent are there, what does the webcam show right now, and is anything closed?"
find_toursβwebcams_near, together withswiss-tourism-mcp(closures, cable cars) andmeteoswiss-mcp(weather). Shows two servers working together.Lodging (nationwide) β "Family-friendly hotel near Interlaken, three stars, accessible β and how do I get there from Zurich airport?"
find_accommodationwith distance ranking βget_details, thenswiss-transport-mcpfor the journey.
Scope map
What the open index holds, counted live on 2026-09-17
(probes/PROBE_REPORT_discover-swiss-mcp.md). 20,817 objects in total.
In scope | Count | Note |
Lodging businesses, nationwide | 5,275 | Zermatt to Geneva; HotellerieSuisse, Schweiz Tourismus, TOMAS, contentdesk |
Hotel rooms and meeting rooms | 10,112 rooms | excluded from |
Points of interest β Zurich (ZΓΌrich Tourismus) | 1,311 | museums, restaurants, shops, nightlife |
Points of interest β Eastern Switzerland | 6,211 | Glarnerland 1,752 Β· Thurgau 1,381 Β· St. Gallen-Bodensee 1,338 Β· Heidiland 711 Β· Appenzellerland 608 Β· Toggenburg 421 |
Liechtenstein Β· Engadin Scuol | 553 Β· 429 | |
Tours | 223 | Eastern Switzerland, Zurich region; 16 from SchweizMobil |
Webcams | 73 | all in Eastern Switzerland |
Ski resorts Β· cable cars and lifts | 21 Β· 36 |
Not in scope | Why it matters |
Points of interest, tours, webcams for the Bernese Oberland, Central Switzerland, Valais, Ticino, Romandie | Hotels there are covered; sights, restaurants and hikes are not. An empty answer there means no data, not nothing there. |
Events | 21 entries in the whole index, a test record among them β practically empty. |
Nightly prices | Only a provider-declared price band (Niedrig / Mittel / Hoch). |
Availability | Not in the Infocenter product. |
Booking | Marketplace product, not this server. |
explore_area and source_status report this coverage at runtime, so a model
can check it before it searches.
Features
8 read-only tools over search, detail, accommodation, tours, events, webcams, area exploration and source status
Per-object attribution β provider, licence and copyright notice travel in the response, not in this README
Licence whitelist on the root
licensefield; everything else is counted intoexcluded_by_licenseand never servedEmpty results carry a reason β a
hintnaming what to change, never an unexplained empty listDegraded states are named β
quota_exhausted,upstream_unreachable,search_unavailableDual transport β stdio (Claude Desktop) and Streamable HTTP (cloud)
Prerequisites
Python 3.11, 3.12 or 3.13
A discover.swiss Infocenter Open subscription key (self-service at portal.discover.swiss) β bring your own key
Rate limits of that subscription: 60 calls/minute, 50,000 calls/month
Installation
git clone https://github.com/malkreide/discover-swiss-mcp.git
cd discover-swiss-mcp
pip install -e ".[dev]"Usage / Quickstart
export DISCOVER_SWISS_KEY="your-subscription-key"
# stdio (Claude Desktop and other local clients)
python -m discover_swiss_mcp
# Streamable HTTP β binds to 127.0.0.1:8000 by default (localhost only)
DISCOVER_SWISS_MCP_TRANSPORT=streamable-http python -m discover_swiss_mcpThe key is read from the environment only. It is held as a secret value, is
never written to a log line, and belongs in no file that gets committed.
.env.example lists every variable with placeholders.
The HTTP transport answers only under its exact host:port. Binding to any
other address than loopback is refused at start-up unless inbound OAuth and
DISCOVER_SWISS_MCP_ALLOWED_HOSTS are configured β see
SECURITY.md.
Container. A hardened image (non-root, read-only root filesystem, no
capabilities) and Kubernetes manifests with an egress NetworkPolicy are in
Dockerfile and deploy/k8s/; commands and a Claude Desktop configuration are
in docs/network-egress.md.
Available Tools
All eight tools are read-only (readOnlyHint: true, openWorldHint: true).
Every hit carries its own attribution; every response counts what it withheld
(excluded_by_license, excluded_test_objects, excluded_by_default_types)
and explains an empty result in hint.
Tool | Source | Purpose |
|
| Full text ( |
|
| Description, fees, accessibility, opening hours, amenities β trimmed to ~8 KB, HTML resolved to text; |
| Search | Stars, garni, price band, amenities, accessibility (Pro Infirmis / OK:GO), distance; no availability, no nightly prices |
| Search | Kind, difficulty, length, ascent, season month, region, distance |
| Search | Date range (default 30 days); thin coverage stated in the description; test objects and all-rights-reserved events withheld and counted; the 2099 sentinel is reported as |
| Search | Radius (default 25 km) or region; |
| Search with facets | Counts by type, data owner, season, price band for a region, locality or radius; short facet names mapped to OData, dropped names reported in |
|
| Reachability, search availability, calls per minute, quota state, index size, coverage, entitlement state; works without a key |
Tool definitions are pinned in docs/tool-hashes.json; after an intended
change, run python scripts/gen_tool_hashes.py --write in the same PR.
Attribution
Every response names its providers, and the host has to show them.
Each hit carries an attribution object β provider, licence and the
provider's own copyright notice (for example Β«ZΓΌrich Tourismus
www.zuerich.comΒ», CC BY-SA). One result set can mix three licences: ZΓΌrich
Tourismus (CC BY-SA), SchweizMobil (CC BY), TOMAS (CC BY-ND). A single licence
line in a footer covers none of them correctly.
Hosts and assistants: display provider and licence with the content taken from a hit. CC BY-SA content passed on stays CC BY-SA.
CC BY-ND (
no_derivatives: true, mostly TOMAS rooms): quote or state facts, never rewrite the description.Withheld objects (all rights reserved, no licence) are not served at all;
excluded_by_licensesays how many there were.
The rules and the whitelist are in docs/LICENSES.md.
Architecture decision
ARCH A β live API only, search-centred, with a list fallback.
(probes/PROBE_REPORT_discover-swiss-mcp.md, section 6)
/searchcovers full text, type, place, distance, dates and facets in one endpoint. Seven of eight tools are views on it;/vertices/{id}serves the detail.There is no dump to mirror, and at 60 calls/minute and 50,000 a month a cache is enough: 15 minutes for search, 24 hours for detail. A monthly quota exhausted (
403with Β«quotaΒ») is a state βdegraded: quota_exhaustedβ and is never retried.The risk is the entitlement. Search contradicts the documentation and can be withdrawn. On
401/403from/search,searchandfind_accommodationfall back to the typed list endpoints with client-side locality and distance filtering, answerprovenance: list_fallback,degraded: search_unavailable, and say inhintwhat they ignored. The fallback is deliberately narrow: no full text, no star/price/amenity filters, at most eight list calls per tool call.One project only:
dsod-content.dsod-hsis a subset.
Known limitations
The twelve findings of the live probe that shape how this server behaves (probe report, section 10; details in CHANGELOG.md):
The documentation denies search; the API grants it. Release gate: written confirmation.
The documented example project
demo-webanswers 400. Projects come from/projects.Paging is
nextPageToken, notcontinuation. Field names from live answers, not from the docs.top=1000does not mean 1000. Cosmos DB cuts at ~4 MB; the token is followed.An open product is not an open licence. All-rights-reserved and unlicensed objects are withheld and counted.
ds-containedInPlaceFilteris not a filter. It trims the response; the hit filter is the query parameter.Schweiz Tourismus already supplies data (2,769 hotels) β the open channel carries their content.
Misspelt facet names are dropped silently β an invented one fails the whole request with 400. Only the eight verified names are sent; anything else is reported in
missing_facets.One hotel weighs 78 KB.
get_detailstrims to ~8 KB.There is test data in the production index. Β«Demo EventΒ» is withheld and counted in
excluded_test_objects.Descriptions are HTML with entities. Resolved to plain text server-side.
Half the index is rooms and meeting rooms.
searchexcludes them unless asked.
Configuration
Variable | Required | Default | Purpose |
| yes | β | Subscription key, sent as |
| no |
| Project to query; |
| no |
|
|
| no |
| Bind address for HTTP transport |
| no |
| TCP port for HTTP transport |
| no |
| structlog level; JSON goes to stderr |
| no |
| Date ( |
| for a non-loopback bind | loopback | Exact Host values the HTTP transport answers to; no wildcards |
| no | loopback origins | Browser origins the HTTP transport accepts |
| for a non-loopback bind | β | Inbound OAuth: issuer, resource URL, introspection URL, client id and secret β see SECURITY.md |
Project Structure
discover-swiss-mcp/
βββ src/discover_swiss_mcp/
β βββ __main__.py # python -m entry point, transport chosen by env
β βββ server.py # MCP server, lifespan, tool wrappers
β βββ tools.py # the *_impl functions, input and output models
β βββ config.py # settings from env; the key is a SecretStr
β βββ models.py # the response envelope
β βββ licenses.py # licence whitelist, attribution, test-object filter
β βββ transform.py # HTML to text, detail trimming
β βββ net.py # SSRF guard, DNS pinning, egress allow-list
β βββ client.py # API client: rate limit, retries, budget, cache, list fallback
β βββ http_app.py # HTTP transport: bind policy, Host/Origin lists
β βββ auth.py # inbound OAuth: scopes, token introspection, 401/403
β βββ _version.py # the package version, a leaf module
β βββ logging_config.py # structlog, JSON to stderr
βββ tests/ # unit tests; test_live.py is the `live` marker, not run by CI
βββ probes/ # live probe: scripts, raw responses, report
βββ audits/ # audit reports (mcp-audit)
βββ deploy/k8s/ # Deployment, NetworkPolicy, CiliumNetworkPolicy
βββ docs/ # LICENSES, DEMO, DEFAULTS, network-egress, tool-hashes.json
βββ scripts/ # repo validation, release gate, tool hashes, default matrix
βββ Dockerfile # hardened image, base pinned by digestTesting
# What CI runs β no network
pytest -m "not live"
ruff check .
ruff format --check .
python scripts/validate_repo.py .
python scripts/default_matrix.py --check # docs/DEFAULTS.md matches the spec
# secrets over the whole history (what the CI job runs)
docker run --rm -v "$PWD:/repo" ghcr.io/gitleaks/gitleaks:v8.30.1 git /repo --config /repo/.gitleaks.toml --redact
# Live canaries β real API, key from the environment, never in CI
export DISCOVER_SWISS_KEY="your-subscription-key"
pytest -m live -rAThe live canaries (tests/test_live.py, about twenty calls) hold each tool to
roughly half of what the index held on 2026-09-17 β Β«LandesmuseumΒ» β₯ 20 hits,
hotels β₯ 2,000, webcams within 100 km of St. Gallen β₯ 30, and so on β and
check that scope parameters take effect: match="name" narrows,
containedInPlace/id comes back as a facet, the Landesmuseum description
arrives without HTML entities, the nearest hotel to Interlaken is in
Interlaken. A floor that fails means a tool stopped finding what is there.
Without a key the module skips; a skip is not a pass.
Changelog
See CHANGELOG.md
Contributing
Contributions are welcome β see CONTRIBUTING.md.
Security
Please report vulnerabilities as described in SECURITY.md.
License
MIT License β see LICENSE
The software is MIT. The data is not: it stays under the licence of the provider named in each hit. See docs/LICENSES.md.
Author
Hayal Oezkan Β· malkreide
Credits & Related Projects
Data: discover.swiss Infocenter Open β data by the providers named per object
Protocol: Model Context Protocol β Anthropic / Linux Foundation
Conventions: openlex-mcp β reference implementation of this portfolio
Portfolio: Swiss Public Data MCP Portfolio
Related MCP Connectors
Swiss weather data for AI assistants β forecasts, measurements, stations, pollen.
Give AI assistants access to real-time data. Search the web, compare flights, find hotels, and more.
Search and explore a global travel points-of-interest catalog (cities, countries, POIs).
Search Swiss federal legislation: laws, articles, amendments via the Fedlex SPARQL endpoint.
Related MCP Servers
- AlicenseAqualityAmaintenanceEnables AI models to search and retrieve bibliographic and digitized records from Swiss academic libraries (swisscovery, e-rara, e-periodica, e-manuscripta) via open protocols without requiring API keys.16148 PyPI1MIT
- AlicenseAqualityAmaintenanceEnables AI models to access Swiss weather and climate data from MeteoSwiss, including current observations, forecasts, and warnings.648 PyPIMIT
- AlicenseAqualityAmaintenanceMCP server for Switzerland's national metadata catalogue, enabling AI agents to discover datasets, APIs, public services, and publishers through free-text search and structured queries.13MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to explore Swiss hiking routes with real-time closures, weather, public transport, and POIs, including GPX generation.1MIT