{
  "markdown": "> 🇨🇭 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.\n> This is a private project. It is independent of any employer or institutional affiliation.\n\n# 🏛️ swiss-efv-mcp\n\n[![Version](https://img.shields.io/badge/version-0.3.2-blue.svg)](CHANGELOG.md)\n[![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)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)\n[![MCP](https://img.shields.io/badge/MCP-Model_Context_Protocol-8A2BE2.svg)](https://modelcontextprotocol.io/)\n[![Auth: none](https://img.shields.io/badge/auth-none-brightgreen.svg)](#architecture-decision)\n[![Portfolio](https://img.shields.io/badge/portfolio-swiss--public--data--mcp-informational)](https://github.com/malkreide/swiss-public-data-mcp)\n\n> MCP server for Swiss federal finances (EFV): budget, debt, forecasts and spending by task and institution.\n\n[🇩🇪 Deutsche Version](README.de.md)\n\n## Overview\n\nThis server closes the fiscal gap in the portfolio's Economics & Finance cluster.\n`swiss-snb-mcp` already covers monetary policy; `swiss-efv-mcp` adds the **state\nbudget** — federal revenue, expenditure, balance, debt ratios (with forecasts to\n2029), a hierarchical budget drill-down, and spending by department. Data comes\nfrom the Eidgenössische Finanzverwaltung (EFV) via opendata.swiss (OGD Schweiz).\n\n## Features\n\n- Five read-only tools over the curated EFV FS/GFS dump files.\n- Headline series 1990–2029 per household (bund, ktn, gdn, staat, sv) and model\n  (FS / GFS); every point carries `is_projection` so actuals and plan/forecast\n  years are unambiguous.\n- Hierarchical federal-budget drill-down and spending by department / unit.\n- 24 h TTL in-memory cache with stale-serve fallback; retry with exponential\n  backoff (2/4/8 s); `dump_status` never returns empty silently.\n- Dual transport: `stdio` (local) and SSE (cloud).\n- No authentication required — public open-government data (No-Auth-First).\n\n## 🎯 Anchor Demo Query\n\n> *\"How has the federal balance developed since the SNB rate turnaround in 2022 —\n> and which task areas absorbed the growth in spending?\"*\n\n```\nfiscal_headline(variable=\"saldo\", household=\"bund\", year_from=2021)\nfiscal_budget_breakdown(topic=\"Ausgaben nach Aufgabengebiet\", level=2)\n```\n\nCross-read with `swiss-snb-mcp`, this connects the interest-rate cycle to the\nfederal deficit — something neither server can answer alone.\n\n### Demo\n\n![Demo: Claude using fiscal_headline and fiscal_budget_breakdown](docs/assets/demo.svg)\n\n## Prerequisites\n\n- Python 3.11+\n- [`uv` / `uvx`](https://docs.astral.sh/uv/) (recommended) or `pip`\n- Network access to `data.finance.admin.ch` and `efv.admin.ch` — no API key needed\n\n## Installation\n\n```bash\nuvx swiss-efv-mcp            # zero-install run (once published to PyPI)\n# or\npip install swiss-efv-mcp\n```\n\nClaude Desktop (`claude_desktop_config.json`):\n\n```json\n{\n  \"mcpServers\": {\n    \"swiss-efv\": {\n      \"command\": \"uvx\",\n      \"args\": [\"swiss-efv-mcp\"]\n    }\n  }\n}\n```\n\n## Quickstart\n\n```bash\n# Run locally over stdio (default transport)\nuvx swiss-efv-mcp\n\n# From a checkout, without installing\nPYTHONPATH=src python -m swiss_efv_mcp\n```\n\n## Configuration\n\nAll configuration is loaded once into a typed `Settings` object\n(`pydantic-settings`). The legacy unprefixed names below keep working; the\ncanonical names use the `EFV_MCP_` prefix. Defaults are safe for local use.\n\n| Variable    | Default     | Purpose                                                                    |\n|-------------|-------------|----------------------------------------------------------------------------|\n| `TRANSPORT` | `stdio`     | Transport: `stdio` (Claude Desktop) or `sse` / `streamable-http` (cloud)   |\n| `HOST`      | `127.0.0.1` | Bind host (SSE only). Loopback by default; set `0.0.0.0` **only** in a container |\n| `PORT`      | `8000`      | Bind port (SSE only)                                                        |\n| `EFV_MCP_LOG_LEVEL`    | `INFO` | structlog level (JSON to stderr)                                  |\n| `EFV_MCP_CORS_ORIGINS` | `[]`   | SSE only: explicit allowed browser origins (default-deny; comma-separated or JSON) |\n| `EFV_MCP_OTEL_ENABLED` | `false`| Enable OpenTelemetry tracing (requires the `otel` extra); standard `OTEL_*` env vars configure export |\n\nCloud (Render / Railway):\n\n```bash\nTRANSPORT=sse PORT=8000 swiss-efv-mcp   # exposes /sse\n```\n\n## Available Tools\n\n| Tool | Purpose |\n|---|---|\n| `fiscal_headline` | Revenue / expenditure / balance / debt ratios over 1990–2029, per household and model; every point flags `is_projection` |\n| `fiscal_budget_breakdown` | Hierarchical federal budget by topic (Ausgaben nach Art / nach Aufgabengebiet, Einnahmen, Bilanz, …) |\n| `fiscal_by_institution` | Spending per department / administrative unit since 2007 (Personalausgaben, Informatik, external services, FTE) |\n| `fiscal_list_dimensions` | Discover valid parameter values — call this first to build correct arguments |\n| `fiscal_status` | Cache freshness and upstream health per dataset; never returns empty silently |\n| `dump_status` | **Deprecated** alias of `fiscal_status` (kept for backward compatibility; removed in a future minor) |\n\nAll tools are **read-only**: each is annotated `readOnlyHint: true`,\n`destructiveHint: false`, only issues HTTP GETs against the EFV dump files, and\nhas no write, send, or filesystem capability.\n\n**MCP primitives.** This server uses only the **Tools** primitive. The EFV data\nare sliced live from cached dumps with no stable resource hierarchy to expose as\n*Resources*, and there are no server-authored *Prompts*. The five tools are small\nand closely related, so they live in a single `server.py` rather than a `tools/`\npackage.\n\n## Architecture\n\n```\n                      ┌──────────────────────────────┐\n   Claude / Agent ──▶ │  swiss-efv-mcp (FastMCP)      │\n                      │  5 tools · Pydantic v2 env.   │\n                      └───────────────┬──────────────┘\n                                      │ fetch + retry + TTL cache\n              ┌───────────────────────┴───────────────────────┐\n              ▼                                               ▼\n   data.finance.admin.ch                          efv.admin.ch/dam\n   fs_dashboard/main_extern.csv                   bundeshaushalt_de.csv\n   (headline, 1990–2029)                          institutionen_de.csv\n```\n\n## Architecture decision\n\nThis server uses **Architecture C (Dump-first)**.\n\nRationale (verified live on 2026-07-24):\n- The EFV FS/GFS dashboard has **no filtered query API**; it serves static CSV\n  dumps that its front-end filters in the browser.\n- Three curated files are small enough to fetch-and-cache whole (516 KB / 5 MB /\n  1 MB). They cover the headline aggregates, the hierarchical budget and the\n  by-institution view — i.e. the answerable questions.\n- The full detail cubes (`standardauswertung.csv` 157 MB, `fir_art_funk.csv`\n  1.23 GB) are **out of scope for v0.1.0**; loading them per request is not\n  viable. A future Phase 2 would pre-process them into SQLite/Parquet.\n\nConsequences:\n- Files are cached in memory with a 24 h TTL; stale cache is preferred over an\n  empty response when upstream is down.\n- Retry with exponential backoff on all HTTP; `dump_status` always returns a\n  readable state.\n\n## Project Structure\n\n```\nswiss-efv-mcp/\n├── src/swiss_efv_mcp/\n│   ├── __init__.py\n│   ├── __main__.py        # entry point; dual transport (stdio / SSE+CORS)\n│   ├── client.py          # dump-first data layer: egress allow-list, retry, UA, TTL cache\n│   ├── logging_config.py  # structlog JSON to stderr\n│   ├── models.py          # Pydantic v2 envelopes (source + provenance)\n│   ├── server.py          # 5 FastMCP tools (annotated) + testable *_impl functions\n│   └── settings.py        # typed pydantic-settings config\n├── tests/                 # respx mock tests + hardening tests + @pytest.mark.live\n├── docs/                  # network-egress.md + accepted-risk ADRs\n├── audits/                # MCP best-practice audit runs (findings, report, summary)\n├── README.md · README.de.md · CHANGELOG.md · SECURITY.md · CONTRIBUTING.md\n├── Dockerfile · server.json · LICENSE\n└── pyproject.toml\n```\n\n## Safety & Limits\n\n- **Read-only.** Every tool is annotated `readOnlyHint: true`, only issues HTTP\n  GETs against the EFV dump files, and has no write, send, or filesystem capability.\n- **Egress allow-list.** An immutable `ALLOWED_HOSTS` frozenset + `assert_host_allowed()`\n  is enforced before every request (HTTPS-only, two fixed EFV hosts). URLs are\n  hardcoded constants; no user input builds a URL. See [`docs/network-egress.md`](docs/network-egress.md).\n- **TLS on.** httpx certificate verification is on by default and never disabled.\n- **No credentials.** The endpoints are public OGD; no API keys or secrets are\n  stored or forwarded. A browser `User-Agent` is injected because the endpoints\n  `403` the default httpx/curl UA (see Known limitations) — do not remove it.\n- **Error masking.** `mask_error_details=True` plus client-side masking keep raw\n  upstream/internal detail out of tool results; full detail goes only to the\n  structlog stderr log.\n- **Input bounds.** Tool arguments carry explicit Pydantic constraints (year\n  `1900–2100`, `level 1–8`, string `max_length`).\n- **Graceful degradation.** Retry with exponential backoff (2/4/8 s); a stale\n  cache is served over an empty response; `dump_status` always returns a readable\n  state and never a silent empty.\n- **Loopback + default-deny CORS.** SSE binds to `HOST`, default `127.0.0.1`; set\n  `HOST=0.0.0.0` **only** inside a container (the provided [`Dockerfile`](Dockerfile) does).\n  Browser origins must be listed explicitly via `EFV_MCP_CORS_ORIGINS`.\n- **Audited.** Reviewed against the portfolio MCP best-practice catalogue\n  (44 applicable checks) — see [`audits/`](audits/) and [`SECURITY.md`](SECURITY.md).\n  Accepted risks are documented as ADRs under [`docs/adr/`](docs/adr/).\n- **Not authoritative.** Figures are not official; consult the EFV originals for\n  official use.\n\n## Known limitations\n\nLive-probe findings (2026-07-24), also in `CHANGELOG.md → Known findings`:\n\n| Finding | Impact |\n|---|---|\n| Endpoints return **HTTP 403 without a browser User-Agent** | UA is injected by the client; do not remove it |\n| 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 |\n| `NA` appears as a literal string in `hh`/`model`/`source` | cleaned to `None` centrally |\n| \"Forward-looking\" is **not one label**: Bund uses \"Budget/financial plans\", `staat` uses \"Forecasts\" | abstracted via `is_projection` |\n| **Accounting-model break at 2022/2023** (\"bis 2022\" vs \"ab 2023\" topics) | series has a seam; a `note` flags affected topics |\n| Detail cubes (157 MB / 1.23 GB) not served | Phase 2; use the curated files for now |\n\n## Project Phase\n\nThis server is in **Phase 1 (read-only)**. Every tool only ever fetches the\npublic EFV dump files — there are no write, send, or filesystem capabilities.\n\n| Phase | Scope | Status |\n|---|---|---|\n| **1 — Read-only** | Headline series, budget breakdown, spending by institution | ✅ current |\n| 2 — Detail cubes | Pre-process the 157 MB / 1.23 GB cubes to SQLite/Parquet | planned |\n| 3 — Multi-agent | (none planned) | — |\n\nA transition to a later phase would require a re-audit before any write-capable\ntool is added.\n\n## MCP Protocol Version\n\nThe protocol version is negotiated at the `initialize` handshake by\n[FastMCP](https://pypi.org/project/fastmcp/) (pinned `fastmcp>=3.4` in\n`pyproject.toml`), which builds on the `mcp` Python SDK. The baseline this server\nis built and audited against is **`2025-11-25`**, pinned as `MCP_PROTOCOL_VERSION`\nin `server.py`; a regression test asserts the negotiated version still equals it,\nso a protocol-changing SDK bump fails CI loudly (ARCH-012). Dependencies are kept\ncurrent via monthly Dependabot PRs (`.github/dependabot.yml`); protocol-relevant\nbumps are noted in [`CHANGELOG.md`](CHANGELOG.md).\n\n## Testing\n\n```bash\nPYTHONPATH=src pytest tests/ -m \"not live\"   # offline, respx-mocked\nPYTHONPATH=src pytest tests/ -m live         # hits the real EFV endpoints\nPYTHONPATH=src ruff check src tests\n```\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md).\n\n## Contributing\n\nIssues and pull requests are welcome. Please keep tools read-only, run\n`ruff check` and the offline test suite before submitting, and add a\n`CHANGELOG.md` entry under `[Unreleased]` for user-facing changes. See\n[CONTRIBUTING.md](CONTRIBUTING.md).\n\nMaintainers: see [PUBLISHING.md](PUBLISHING.md) for the step-by-step PyPI release\nprocess (Trusted Publishing via GitHub Release).\n\n## Security\n\nSee [SECURITY.md](SECURITY.md) for the security posture, hardening controls, and\nhow to report a vulnerability.\n\n## License\n\nMIT for this server — see [LICENSE](LICENSE). The EFV data remain subject to the\nOGD Schweiz terms (freely usable, with attribution).\n\n## Author\n\n**Hayal Oezkan** · [github.com/malkreide](https://github.com/malkreide)\n\n## Credits & Related Projects\n\n- Data: **Eidgenössische Finanzverwaltung EFV** via opendata.swiss (OGD Schweiz, freely usable)\n- Companion: [`swiss-snb-mcp`](https://github.com/malkreide) (monetary policy) — the fiscal/monetary pair\n- Portfolio index: [swiss-public-data-mcp](https://github.com/malkreide/swiss-public-data-mcp)\n\n> Disclaimer: private project, independent of any employer or institution. No warranty; figures are not authoritative — consult the EFV originals for official use.\n\n<!-- mcp-name: io.github.malkreide/swiss-efv-mcp -->\n",
  "bytes": 13935,
  "sha": "95e3359c1309b1339a321a964fa25c5b1dec17579135089d9313280e8675e459",
  "repo_slug": "malkreide/swiss-efv-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_malkreide_swiss_efv_mcp_a8667243/readme"
}