> πŸ‡¨πŸ‡­ Part of the [**Swiss Public Data MCP Portfolio**](https://github.com/malkreide/swiss-public-data-mcp) β€” open-source MCP servers connecting AI agents to Swiss public and open data. > This is a private project. It is independent of any employer or institutional affiliation. # πŸ›οΈ swiss-efv-mcp [![Version](https://img.shields.io/badge/version-0.4.0-blue.svg)](CHANGELOG.md) [![CI](https://github.com/malkreide/swiss-efv-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/malkreide/swiss-efv-mcp/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/) [![MCP](https://img.shields.io/badge/MCP-Model_Context_Protocol-8A2BE2.svg)](https://modelcontextprotocol.io/) [![Auth: none](https://img.shields.io/badge/auth-none-brightgreen.svg)](#architecture-decision) [![Portfolio](https://img.shields.io/badge/portfolio-swiss--public--data--mcp-informational)](https://github.com/malkreide/swiss-public-data-mcp) > MCP server for Swiss federal finances (EFV): budget, debt, forecasts and spending by task and institution. [πŸ‡©πŸ‡ͺ Deutsche Version](README.de.md) ## Overview This server closes the fiscal gap in the portfolio's Economics & Finance cluster. `swiss-snb-mcp` already covers monetary policy; `swiss-efv-mcp` adds the **state budget** β€” federal revenue, expenditure, balance, debt ratios (with forecasts to 2029), a hierarchical budget drill-down, and spending by department. Data comes from the EidgenΓΆssische Finanzverwaltung (EFV) via opendata.swiss (OGD Schweiz). ## Features - Five read-only tools over the curated EFV FS/GFS dump files. - Headline series 1990–2029 per household (bund, ktn, gdn, staat, sv) and model (FS / GFS); every point carries `is_projection` so actuals and plan/forecast years are unambiguous. - Hierarchical federal-budget drill-down and spending by department / unit. - 24 h TTL in-memory cache with stale-serve fallback; retry with exponential backoff (2/4/8 s); `dump_status` never returns empty silently. - Dual transport: `stdio` (local) and SSE (cloud). - No authentication required β€” public open-government data (No-Auth-First). ## 🎯 Anchor Demo Query > *"How has the federal balance developed since the SNB rate turnaround in 2022 β€” > and which task areas absorbed the growth in spending?"* ``` fiscal_headline(variable="saldo", household="bund", year_from=2021) fiscal_budget_breakdown(topic="Ausgaben nach Aufgabengebiet", level=2) ``` Cross-read with `swiss-snb-mcp`, this connects the interest-rate cycle to the federal deficit β€” something neither server can answer alone. ### Demo ![Demo: Claude using fiscal_headline and fiscal_budget_breakdown](docs/assets/demo.svg) ## Prerequisites - Python 3.11+ - [`uv` / `uvx`](https://docs.astral.sh/uv/) (recommended) or `pip` - Network access to `data.finance.admin.ch` and `efv.admin.ch` β€” no API key needed ## Installation ```bash uvx swiss-efv-mcp # zero-install run (once published to PyPI) # or pip install swiss-efv-mcp ``` Claude Desktop (`claude_desktop_config.json`): ```json { "mcpServers": { "swiss-efv": { "command": "uvx", "args": ["swiss-efv-mcp"] } } } ``` ## Quickstart ```bash # Run locally over stdio (default transport) uvx swiss-efv-mcp # From a checkout, without installing PYTHONPATH=src python -m swiss_efv_mcp ``` ## Configuration All configuration is loaded once into a typed `Settings` object (`pydantic-settings`). The legacy unprefixed names below keep working; the canonical names use the `EFV_MCP_` prefix. Defaults are safe for local use. | Variable | Default | Purpose | |-------------|-------------|----------------------------------------------------------------------------| | `TRANSPORT` | `stdio` | Transport: `stdio` (Claude Desktop) or `sse` / `streamable-http` (cloud) | | `HOST` | `127.0.0.1` | Bind host (SSE only). Loopback by default; set `0.0.0.0` **only** in a container | | `PORT` | `8000` | Bind port (SSE only) | | `EFV_MCP_LOG_LEVEL` | `INFO` | structlog level (JSON to stderr) | | `EFV_MCP_CORS_ORIGINS` | `[]` | SSE only: explicit allowed browser origins (default-deny; comma-separated or JSON) | | `EFV_MCP_OTEL_ENABLED` | `false`| Enable OpenTelemetry tracing (requires the `otel` extra); standard `OTEL_*` env vars configure export | Cloud (Render / Railway): ```bash TRANSPORT=sse PORT=8000 swiss-efv-mcp # exposes /sse ``` ## Available Tools | Tool | Purpose | |---|---| | `fiscal_headline` | Revenue / expenditure / balance / debt ratios over 1990–2029, per household and model; every point flags `is_projection` | | `fiscal_budget_breakdown` | Hierarchical federal budget by topic (Ausgaben nach Art / nach Aufgabengebiet, Einnahmen, Bilanz, …) | | `fiscal_by_institution` | Spending per department / administrative unit since 2007 (Personalausgaben, Informatik, external services, FTE) | | `fiscal_list_dimensions` | Discover valid parameter values β€” call this first to build correct arguments | | `fiscal_status` | Cache freshness and upstream health per dataset; never returns empty silently | | `dump_status` | **Deprecated** alias of `fiscal_status` (kept for backward compatibility; removed in a future minor) | All tools are **read-only**: each is annotated `readOnlyHint: true`, `destructiveHint: false`, only issues HTTP GETs against the EFV dump files, and has no write, send, or filesystem capability. **MCP primitives.** This server uses only the **Tools** primitive. The EFV data are sliced live from cached dumps with no stable resource hierarchy to expose as *Resources*, and there are no server-authored *Prompts*. The five tools are small and closely related, so they live in a single `server.py` rather than a `tools/` package. ## Architecture ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Claude / Agent ──▢ β”‚ swiss-efv-mcp (FastMCP) β”‚ β”‚ 5 tools Β· Pydantic v2 env. β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ fetch + retry + TTL cache β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β–Ό β–Ό data.finance.admin.ch efv.admin.ch/dam fs_dashboard/main_extern.csv bundeshaushalt_de.csv (headline, 1990–2029) institutionen_de.csv ``` ## Architecture decision This server uses **Architecture C (Dump-first)**. Rationale (verified live on 2026-07-24): - The EFV FS/GFS dashboard has **no filtered query API**; it serves static CSV dumps that its front-end filters in the browser. - Three curated files are small enough to fetch-and-cache whole (516 KB / 5 MB / 1 MB). They cover the headline aggregates, the hierarchical budget and the by-institution view β€” i.e. the answerable questions. - The full detail cubes (`standardauswertung.csv` 157 MB, `fir_art_funk.csv` 1.23 GB) are **out of scope for v0.1.0**; loading them per request is not viable. A future Phase 2 would pre-process them into SQLite/Parquet. Consequences: - Files are cached in memory with a 24 h TTL; stale cache is preferred over an empty response when upstream is down. - Retry with exponential backoff on all HTTP; `dump_status` always returns a readable state. ## Project Structure ``` swiss-efv-mcp/ β”œβ”€β”€ src/swiss_efv_mcp/ β”‚ β”œβ”€β”€ __init__.py β”‚ β”œβ”€β”€ __main__.py # entry point; dual transport (stdio / SSE+CORS) β”‚ β”œβ”€β”€ client.py # dump-first data layer: egress allow-list, retry, UA, TTL cache β”‚ β”œβ”€β”€ logging_config.py # structlog JSON to stderr β”‚ β”œβ”€β”€ models.py # Pydantic v2 envelopes (source + provenance) β”‚ β”œβ”€β”€ server.py # 5 FastMCP tools (annotated) + testable *_impl functions β”‚ └── settings.py # typed pydantic-settings config β”œβ”€β”€ tests/ # respx mock tests + hardening tests + @pytest.mark.live β”œβ”€β”€ docs/ # network-egress.md + accepted-risk ADRs β”œβ”€β”€ audits/ # MCP best-practice audit runs (findings, report, summary) β”œβ”€β”€ README.md Β· README.de.md Β· CHANGELOG.md Β· SECURITY.md Β· CONTRIBUTING.md β”œβ”€β”€ Dockerfile Β· server.json Β· LICENSE └── pyproject.toml ``` ## Safety & Limits - **Read-only.** Every tool is annotated `readOnlyHint: true`, only issues HTTP GETs against the EFV dump files, and has no write, send, or filesystem capability. - **Egress allow-list.** An immutable `ALLOWED_HOSTS` frozenset + `assert_host_allowed()` is enforced before every request (HTTPS-only, two fixed EFV hosts). URLs are hardcoded constants; no user input builds a URL. See [`docs/network-egress.md`](docs/network-egress.md). - **TLS on.** httpx certificate verification is on by default and never disabled. - **No credentials.** The endpoints are public OGD; no API keys or secrets are stored or forwarded. A browser `User-Agent` is injected because the endpoints `403` the default httpx/curl UA (see Known limitations) β€” do not remove it. - **Error masking.** `mask_error_details=True` plus client-side masking keep raw upstream/internal detail out of tool results; full detail goes only to the structlog stderr log. - **Input bounds.** Tool arguments carry explicit Pydantic constraints (year `1900–2100`, `level 1–8`, string `max_length`). - **Graceful degradation.** Retry with exponential backoff (2/4/8 s); a stale cache is served over an empty response; `dump_status` always returns a readable state and never a silent empty. - **Loopback + default-deny CORS.** SSE binds to `HOST`, default `127.0.0.1`; set `HOST=0.0.0.0` **only** inside a container (the provided [`Dockerfile`](Dockerfile) does). Browser origins must be listed explicitly via `EFV_MCP_CORS_ORIGINS`. - **Audited.** Reviewed against the portfolio MCP best-practice catalogue (44 applicable checks) β€” see [`audits/`](audits/) and [`SECURITY.md`](SECURITY.md). Accepted risks are documented as ADRs under [`docs/adr/`](docs/adr/). - **Not authoritative.** Figures are not official; consult the EFV originals for official use. ## Known limitations Live-probe findings (2026-07-24), also in `CHANGELOG.md β†’ Known findings`: | Finding | Impact | |---|---| | Endpoints return **HTTP 403 without a browser User-Agent** | UA is injected by the client; do not remove it | | opendata.swiss "CSV" links for 2 datasets point to an **HTML landing page** | real files resolved to a DAM path (`/dam/de/sd-web/{id}/…`) whose opaque id may rotate on re-upload | | `NA` appears as a literal string in `hh`/`model`/`source` | cleaned to `None` centrally | | "Forward-looking" is **not one label**: Bund uses "Budget/financial plans", `staat` uses "Forecasts" | abstracted via `is_projection` | | **Accounting-model break at 2022/2023** ("bis 2022" vs "ab 2023" topics) | series has a seam; a `note` flags affected topics | | Detail cubes (157 MB / 1.23 GB) not served | Phase 2; use the curated files for now | ## Project Phase This server is in **Phase 1 (read-only)**. Every tool only ever fetches the public EFV dump files β€” there are no write, send, or filesystem capabilities. | Phase | Scope | Status | |---|---|---| | **1 β€” Read-only** | Headline series, budget breakdown, spending by institution | βœ… current | | 2 β€” Detail cubes | Pre-process the 157 MB / 1.23 GB cubes to SQLite/Parquet | planned | | 3 β€” Multi-agent | (none planned) | β€” | A transition to a later phase would require a re-audit before any write-capable tool is added. ## MCP Protocol Version This server is native to MCP spec **`2026-07-28`** and still serves the older handshake era, so both pins are stated β€” a single number would describe only half of what clients actually get. | Era | Revision | How a connection negotiates it | Pinned as | |---|---|---|---| | **modern** (default) | **`2026-07-28`** | `server/discover` + a per-request envelope; **no `initialize` handshake**, and `Client.initialize_result` is `None` | `MCP_MODERN_PROTOCOL_VERSION` | | handshake (legacy clients) | **`2025-11-25`** | the classic `initialize` handshake | `MCP_HANDSHAKE_PROTOCOL_VERSION` | Both constants live in `server.py` and are held against the `mcp` SDK's own `LATEST_MODERN_VERSION` / `LATEST_HANDSHAKE_VERSION` rather than against copied-out spec text, and both eras are exercised over a real connection β€” a protocol-changing SDK bump fails CI loudly instead of drifting silently (ARCH-012). The `2026-07-28` era carries consequences beyond the number: - **Routing headers.** Every modern request carries `Mcp-Protocol-Version`, `Mcp-Method` and (for `tools/call`) `Mcp-Name`. They are listed in the CORS allow-list in `__main__.py`; without them a browser client fails at the preflight and never reaches the server. `Mcp-Param-*` is deliberately absent β€” no tool schema here carries the `x-mcp-header` annotation that would make a client send one, and a test fails the day one does. - **Logging is deprecated (SEP-2577).** Tool handlers no longer send client-facing log notifications; per-call diagnostics go to the structlog stderr stream, honouring `EFV_MCP_LOG_LEVEL`. Progress reporting is unaffected and stays. - **`fastmcp>=4.0` is a floor, not cosmetics.** Only fastmcp 4 pulls in `mcp` 2.x, and only there does revision `2026-07-28` exist at all. Under fastmcp 3.x this server would speak `2025-11-25` at best. Dependencies are kept current via monthly Dependabot PRs (`.github/dependabot.yml`); protocol-relevant bumps are noted in [`CHANGELOG.md`](CHANGELOG.md). ## Testing ```bash PYTHONPATH=src pytest tests/ -m "not live" # offline, respx-mocked PYTHONPATH=src pytest tests/ -m live # hits the real EFV endpoints PYTHONPATH=src ruff check src tests ``` ## Changelog See [CHANGELOG.md](CHANGELOG.md). ## Contributing Issues and pull requests are welcome. Please keep tools read-only, run `ruff check` and the offline test suite before submitting, and add a `CHANGELOG.md` entry under `[Unreleased]` for user-facing changes. See [CONTRIBUTING.md](CONTRIBUTING.md). Maintainers: see [PUBLISHING.md](PUBLISHING.md) for the step-by-step PyPI release process (Trusted Publishing via GitHub Release). ## Security See [SECURITY.md](SECURITY.md) for the security posture, hardening controls, and how to report a vulnerability. ## License MIT for this server β€” see [LICENSE](LICENSE). The EFV data remain subject to the OGD Schweiz terms (freely usable, with attribution). ## Author **Hayal Oezkan** Β· [github.com/malkreide](https://github.com/malkreide) ## Credits & Related Projects - Data: **EidgenΓΆssische Finanzverwaltung EFV** via opendata.swiss (OGD Schweiz, freely usable) - Companion: [`swiss-snb-mcp`](https://github.com/malkreide) (monetary policy) β€” the fiscal/monetary pair - Portfolio index: [swiss-public-data-mcp](https://github.com/malkreide/swiss-public-data-mcp) > Disclaimer: private project, independent of any employer or institution. No warranty; figures are not authoritative β€” consult the EFV originals for official use.