discover-swiss-mcp
Provides tools for searching Swiss tourism data via the discover.swiss Infocenter Open API, including full-text search, accommodations, tours, events, webcams, area exploration, and source status, with per-object attribution and license filtering.
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
Frozen β search is not allowed on Infocenter Open. Release on hold.
Item | State |
Search on Infocenter Open | not allowed β written answer from discover.swiss, recorded 2026-10-05 |
Search in this server | off by default; only with a key whose product includes search ( |
Public reference instance with search | asked β request to discover.swiss open |
Tools | all eight registered; without search, |
Release | none; version 0.1.0 is not published |
The live probe of 2026-09-17 found that /search answered 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"). discover.swiss has since answered in writing: the
documentation is right. Search is not part of Infocenter Open; that the
endpoint answers an Open key is a fault they will correct (it should answer
403). Using search takes a paid key.
What follows from that:
The server never calls
/searchwith an Open key β not even once to find out. Search is off unless the operator setsDISCOVER_SWISS_SEARCH_ENTITLED=true, which is only correct for a key whose product includes search.source_statusreports which mode a deployment is in.Without search the server is a narrow tool.
searchandfind_accommodationanswer from the typed list endpoints (area and distance, no full text);find_tours,find_events,webcams_nearandexplore_areaanswerdegraded: search_unavailable. That is not the server this project set out to build.The project is frozen until discover.swiss has answered a request for a non-commercial public reference instance with search. If that is not possible, the server is not released.
Using the data via MCP binds the people who use it to discover.swiss's terms of use and the data licences, not only the key holder.
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 | search for a public reference instance agreed with 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 was the entitlement, and it materialised. Search is not part of Infocenter Open (discover.swiss, recorded 2026-10-05); the server calls it only with
DISCOVER_SWISS_SEARCH_ENTITLED=true. Without it, and on401/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 answered anyway. The documentation is right: discover.swiss calls the answer a fault. Search is off unless the key includes it.
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 |
|
|
| 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
Maintenance
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.1660 PyPI1MIT
- AlicenseAqualityAmaintenanceEnables AI models to access Swiss weather and climate data from MeteoSwiss, including current observations, forecasts, and warnings.668 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.1367 PyPIMIT
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to explore Swiss hiking routes with real-time closures, weather, public transport, and POIs, including GPX generation.1MIT