
Connects Claude to the Swiss Federal Food Safety and Veterinary Office's open data APIs, exposing 11 tools that query official datasets without authentication. You get live food recalls via RSS, notifiable animal disease records since 1991 through SPARQL, cantonal food inspection results, veterinary antibiotic usage stats, children's nutrition survey data, and the national pesticide register. Useful when you need authoritative Swiss public health data in conversational workflows, like checking active product warnings for a region or tracking avian influenza cases in wild birds. Supports both stdio for local Claude Desktop and HTTP transport for browser-based deployments. Part of a broader Swiss public sector MCP portfolio maintained by malkreide.
🇨🇭 Part of the Swiss Public Data MCP Portfolio
🌐 English | Deutsch
MCP server connecting AI models to Swiss Federal Food Safety and Veterinary Office (BLV) open data — food recalls, animal disease surveillance, food control results, antibiotic usage, children's nutrition surveys and the pesticide register. No authentication required.
swiss-food-safety-mcp gives AI assistants like Claude direct access to official Swiss food safety and veterinary data from the Federal Food Safety and Veterinary Office (BLV / Bundesamt für Lebensmittelsicherheit und Veterinärwesen). It provides 11 tools covering food recalls, animal disease surveillance, food control results, antibiotic usage in veterinary medicine, nutrition surveys for children, and the pesticide register.
All data comes from official Swiss federal sources (opendata.swiss, lindas.admin.ch, news.admin.ch). No API keys or authentication are required.
This server follows the No-Auth-First philosophy and is part of a Swiss public sector MCP portfolio.
Anchor demo query: "Are there any current BLV food warnings relevant to Zurich school canteens — and which notifiable animal diseases are currently reported in the canton?"
→ More use cases by audience →
uv or uvx (recommended) — install uvuvx swiss-food-safety-mcp
uv tool install swiss-food-safety-mcp
swiss-food-safety-mcp
git clone https://github.com/malkreide/swiss-food-safety-mcp
cd swiss-food-safety-mcp
uv sync
uv run swiss-food-safety-mcp
Add to claude_desktop_config.json:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"swiss-food-safety": {
"command": "uvx",
"args": ["swiss-food-safety-mcp"]
}
}
}
Try it immediately in Claude Desktop:
"Which BLV food warnings are currently active?"
"Are there any notifiable animal diseases reported in Zurich canton this year?"
{
"mcpServers": {
"swiss-food-safety": {
"command": "uvx",
"args": ["swiss-food-safety-mcp"]
}
}
}
For use via claude.ai in the browser (e.g. on managed workstations without local software):
# Loopback only (default) — safe for local testing:
swiss-food-safety-mcp --http
# Server runs on 127.0.0.1:8002
# External exposure (e.g. behind the Render TLS proxy):
swiss-food-safety-mcp --http --host 0.0.0.0
⚠️ The HTTP transport binds to
127.0.0.1by default. Pass--host 0.0.0.0only when external exposure is intended. SetBLV_MCP_ALLOWED_ORIGINS(comma-separated, no wildcard) to permit browser clients; it defaults tohttps://claude.ai.
Render.com (recommended):
swiss-food-safety-mcp --http --host 0.0.0.0https://your-app.onrender.com/mcpDocker:
docker build -t swiss-food-safety-mcp .
docker run -p 8002:8002 swiss-food-safety-mcp
# or, with explicit CPU/memory limits:
docker compose up
The image is a non-root, multi-stage build; the container already binds
0.0.0.0 and includes a healthcheck. docker-compose.yml additionally caps
CPU and memory.
💡 "stdio for the developer laptop, Streamable HTTP for the browser."
🔧 Configuration — every runtime setting is overridable via
BLV_MCP_*environment variables (BLV_MCP_HTTP_HOST,BLV_MCP_HTTP_PORT,BLV_MCP_ALLOWED_ORIGINS,BLV_MCP_TIMEOUT,BLV_MCP_OTEL_ENDPOINT, …). Outbound requests are restricted to Swiss federal hosts (*.admin.ch,opendata.swiss). Optional OpenTelemetry tracing: install withpip install swiss-food-safety-mcp[otel]and setBLV_MCP_OTEL_ENDPOINT.
| Tool | Description | Data Source |
|---|---|---|
blv_get_public_warnings | Current food recalls & health warnings | news.admin.ch RSS |
blv_list_datasets | Browse all 28 BLV open datasets | opendata.swiss CKAN |
blv_get_dataset_info | Dataset details & resource URLs | opendata.swiss CKAN |
blv_search_animal_diseases | Notifiable animal diseases since 1991 | LINDAS SPARQL (/query) |
blv_get_animal_health_stats | Annual animal health statistics | opendata.swiss CSV/JSON |
blv_get_food_control_results | Cantonal food inspection results | opendata.swiss CSV |
blv_get_antibiotic_usage_vet | Veterinary antibiotic usage (ISABV) | opendata.swiss CSV |
blv_get_avian_influenza | Wild bird avian influenza surveillance | opendata.swiss CSV |
blv_get_nutrition_data_children | menuCH-Kids: questionnaire tallies (not nutrient intake) | opendata.swiss CSV |
blv_search_pesticide_products | Swiss approved pesticide register | opendata.swiss XML |
blv_get_meat_inspection_stats | Slaughterhouse inspection statistics | opendata.swiss CSV/JSON |
| Query | Tool |
|---|---|
| "Which BLV food warnings are currently active?" | blv_get_public_warnings |
| "Are there animal diseases in Zurich canton in 2024?" | blv_search_animal_diseases |
| "What is the avian influenza situation in Switzerland 2024?" | blv_get_avian_influenza |
| "What do Swiss children actually eat?" | blv_get_nutrition_data_children |
| "Which copper-based pesticides are approved in Switzerland?" | blv_search_pesticide_products |
┌─────────────────┐ ┌─────────────────────────────┐ ┌──────────────────────────────┐
│ Claude / AI │────▶│ Swiss Food Safety MCP │────▶│ Swiss Federal Open Data │
│ (MCP Host) │◀────│ (MCP Server) │◀────│ │
└─────────────────┘ │ │ │ opendata.swiss (CKAN/CSV) │
│ 11 Tools · No Auth │ │ lindas.admin.ch (SPARQL) │
│ Stdio | Streamable HTTP │ │ news.admin.ch (RSS/XML) │
└─────────────────────────────┘ └──────────────────────────────┘
| Combination | Use Case |
|---|---|
swiss-food-safety-mcp + zurich-opendata-mcp | Geo-mapped animal disease risk near school locations |
swiss-food-safety-mcp + fedlex-mcp | Link recalls to food law (Lebensmittelgesetz) |
swiss-food-safety-mcp + swiss-statistics-mcp | Nutrition data × socioeconomics by school district |
swiss-food-safety-mcp + global-education-mcp | Swiss children's nutrition vs. OECD benchmarks |
swiss-food-safety-mcp/
├── src/
│ └── swiss_food_safety_mcp/
│ ├── __init__.py # Package metadata
│ └── server.py # All tools, resources, prompts
├── tests/
│ ├── __init__.py
│ └── test_server.py # Unit tests (no live API calls)
├── .github/
│ └── workflows/
│ └── ci.yml # Python 3.11–3.13 matrix
├── pyproject.toml # hatchling build, uv-compatible
├── CHANGELOG.md
├── CONTRIBUTING.md # Contribution guide (English)
├── CONTRIBUTING.de.md # Contribution guide (German)
├── SECURITY.md # Security policy (English)
├── SECURITY.de.md # Security policy (German)
├── LICENSE # MIT
├── README.md # This file (English)
└── README.de.md # German version
| Source | Description | Format |
|---|---|---|
| opendata.swiss/BLV | 28 open datasets | CSV, JSON, Parquet, SPARQL, XML |
| lindas.admin.ch/sparql | Swiss linked data SPARQL endpoint | RDF/SPARQL |
| news.admin.ch RSS | BLV public warnings & recalls | RSS/XML |
| blv.admin.ch | BLV website (DE/FR/IT/EN) | HTML |
All data is open government data (OGD) under Creative Commons with attribution requirement.
DATENQUELLEN in server.py). A keyword search takes the first hit and therefore falls back silently onto something plausible — that is how blv_get_animal_health_stats came to return antibiotics data, and how blv_get_food_control_results came to return a code list out of a dataset whose 26 resources include 18 of them. scripts/record_fixtures.py re-measures the pinned pairs on every run; a renamed dataset now fails loudly.Geschlecht, Sprachregion, Altersgruppe, Frage, Antwort, Anzahl). The docstring previously promised nutrient intake against dietary recommendations and offered "Energie", "Zucker", "Eisen" as filter examples — those matched nothing and returned an empty list. Adult food-consumption data exists as a separate dataset that this server does not cover.format: CSV. With the endpoint corrected the fallback is also unnecessary — and a fallback that hides a broken query is worse than none.limit and filtering parameters conservatively. The server enforces a 30-second timeout per request.This server is Phase 1 — read-only (see ROADMAP.md): all
11 tools are read-only queries with no write surface.
Run it as a single instance. The Streamable HTTP transport keeps
per-session state, so horizontal scaling would require Mcp-Session-Id sticky
routing at the load balancer plus a shared session store — neither is
implemented, by design, for a server of this scope. A single Render instance
(or one container) is the supported deployment; docker-compose.yml sets
explicit CPU/memory limits for self-hosting.
# Unit + contract tests (no network) — this is what CI runs
PYTHONPATH=src pytest tests/ -m "not live"
# All tests including live API checks
PYTHONPATH=src pytest tests/
# Re-measure which dataset and resource each tool hits
PYTHONPATH=src python scripts/record_fixtures.py
54 tests — 53 offline, 1 live.
A hand-written mock encodes its author's assumption and therefore cannot refute it: production code and fixture come from the same head, the same hour, the same reading of the docs. Where both are wrong, both are wrong together — and the suite stays green.
This repo had it in pure form. Every mocked CKAN resource was named
"name": "CSV". On opendata.swiss the same field reads Food establishments 2025 or Food establishments codelist administrative measures — and that
difference alone decided whether a tool returned inspection results or a code
legend. The mocks could not express the distinction, so no test could fail on
it.
What is recorded is therefore the selection: for each tool, the pinned
dataset slug, the resource that was hit, and that file's header line. The
header is the object of the exercise — it separates data from a legend, and it
shows whether the BOM and the delimiter were handled. PROVENANCE.md names the
source, the date, the selection rule and the SHA-256 for each file.
Two of the recorded measurements are controls: an invented path under
lindas.admin.ch (POST 404, so the 404 on /sparql is real) and an invented
class in the fsvo namespace (0 instances, so the previously queried foag
class genuinely does not exist). Without them each measurement would only show
what we received. The recorder aborts if a control stops discriminating, if a
pinned resource disappears, if a header line is empty or starts with a BOM, or
if one of the findings is superseded.
This server speaks spec 2026-07-28 natively. It runs fastmcp 4.x, which
pins mcp 2.x, and that SDK serves two protocol eras over the same server
object:
| Era | Revision | How a client reaches it |
|---|---|---|
| modern | 2026-07-28 | server/discover, metadata in _meta.io.modelcontextprotocol/* |
| handshake (legacy) | 2025-11-25 | the classic initialize exchange |
A current client gets 2026-07-28; one that has not moved yet falls back to
the handshake and still gets every tool. Neither revision is chosen by this
server — the SDK negotiates, and the pin records which pair that negotiation is
allowed to produce.
The modern era is not just a higher number. It has no initialize handshake
and no InitializeResult: Client.initialize() raises there, and the server
metadata comes from server/discover instead. A check that still measured
initialize_result.protocolVersion would therefore be testing only the legacy
half while reporting a pass for both.
tests/test_protocol_version.py measures each era over its own path rather
than comparing constants to one another: it pins both revisions against
LATEST_MODERN_VERSION / LATEST_HANDSHAKE_VERSION, connects a real client in
each mode, asserts the structural absence of InitializeResult in the modern
one, and checks that both eras list the same tools. test_das_sdk_fuehrt_weiterhin_zwei_aeren
guards the other direction — a downgrade back to mcp 1.x would otherwise
reduce half of those assertions to an import error nobody reads.
The transport allow-list moved with the SDK: Mcp-Method, Mcp-Name and
MCP-Protocol-Version are the routing headers spec 2026-07-28 mirrors into
HTTP, and mcp.shared.inbound now reads them, so CORS lists them. Mcp-Param-*
is deliberately still absent — the spec mints those only for parameters a tool
marks with x-mcp-header, and no tool here does.
See CHANGELOG.md
See CONTRIBUTING.md
See SECURITY.md for the security policy and how to report a vulnerability.
MIT License — see LICENSE
Hayal Oezkan · github.com/malkreide
Run via 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):
{
"mcpServers": {
"swiss-food-safety-mcp": {
"command": "uvx",
"args": [
"swiss-food-safety-mcp"
]
}
}
}
maxi-moss/medical-rag-mcp-test