swiss-efv-mcp
> π¨π 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
[](CHANGELOG.md)
[](https://github.com/malkreide/swiss-efv-mcp/actions/workflows/ci.yml)
[](LICENSE)
[](https://www.python.org/)
[](https://modelcontextprotocol.io/)
[](#architecture-decision)
[](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

## 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
The protocol version is negotiated at the `initialize` handshake by
[FastMCP](https://pypi.org/project/fastmcp/) (pinned `fastmcp>=3.4` in
`pyproject.toml`), which builds on the `mcp` Python SDK. The baseline this server
is built and audited against is **`2025-11-25`**, pinned as `MCP_PROTOCOL_VERSION`
in `server.py`; a regression test asserts the negotiated version still equals it,
so a protocol-changing SDK bump fails CI loudly (ARCH-012). 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.
<!-- mcp-name: io.github.malkreide/swiss-efv-mcp -->
TDQS
Scored across 6 tools
The active fiscal_* tools have clearly distinct purposes: headline time series, budget breakdowns, institutional spending, dimension discovery, and status reporting. However, `dump_status` is a deprecated alias for `fiscal_status`, which could cause confusion if not noticed, and `fiscal_headline` vs `fiscal_budget_breakdown` might occasionally overlap for users seeking spending data.
All active tools follow the consistent `fiscal_` prefix followed by a descriptive noun, e.g., `fiscal_headline`, `fiscal_budget_breakdown`. The only deviation is the deprecated `dump_status`, which breaks the pattern and is explicitly noted as an alias for backward compatibility.
Six tools is a well-scoped number for a fiscal data server. Each tool serves a distinct retrieval or operational function, and the deprecated alias is the only slight redundancy but does not weigh down the count.
The tool set covers the main needs for exploring Swiss fiscal data: aggregate trends, budget breakdowns, departmental spending, dimension validation, and data freshness. Minor gaps exist, such as no direct comparative tool across multiple variables or a search-by-keyword function, but the core use cases are well covered.