> ๐Ÿ‡จ๐Ÿ‡ญ **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)** # ๐Ÿ“ก bakom-mcp [![Version](https://img.shields.io/badge/version-3.0.0-blue)](https://github.com/malkreide/bakom-mcp/releases) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![MCP](https://img.shields.io/badge/MCP-Model%20Context%20Protocol-purple)](https://modelcontextprotocol.io/) [![Auth](https://img.shields.io/badge/auth-none%20required-brightgreen)](https://www.bakom.admin.ch/bakom/en/home/digital-switzerland-and-internet/open-data.html) ![CI](https://github.com/malkreide/bakom-mcp/actions/workflows/ci.yml/badge.svg) > MCP server for BAKOM open data โ€“ broadband, mobile coverage, media and Swiss telecom statistics. [๐Ÿ‡ฉ๐Ÿ‡ช Deutsche Version](README.de.md)

Demo: Claude queries fibre and 5G coverage via MCP tool call

--- ## Overview **bakom-mcp** connects AI assistants like Claude to the Swiss Federal Office of Communications (BAKOM) open data infrastructure. It enables natural-language queries about broadband availability, 5G/4G coverage, mobile antenna locations, BAKOM datasets on radio and television, and telecommunications statistics โ€” all without API keys. All data is published as Open Government Data (OGD) on opendata.swiss / geo.admin.ch under **CC BY 4.0** โ€” see the [Data Licence](#data-licence) section below for attribution requirements. **Anchor demo query:** *"Which school buildings in district 7 do not yet have fibre optic connectivity?"* > `bakom_multi_standort_konnektivitaet` delivers the comparison table automatically. [โ†’ More use cases by audience โ†’](EXAMPLES.md) --- ## Scope ### What this server does โœ“ Read-only queries against three public BAKOM/Confederation APIs: - `api3.geo.admin.ch` / `wms.geo.admin.ch` (broadband, mobile coverage, antennas) - `ckan.opendata.swiss` (telecom statistics, dataset metadata) โœ“ Returns aggregated, anonymous data โ€” no personal data, no household-level identification. โœ“ Bound to Swiss WGS84 coordinates (lat 45.8โ€“47.9, lon 5.9โ€“10.6) via Pydantic input validation. โœ“ Egress is locked to a [code-layer allow-list](src/bakom_mcp/server.py) of the six known data-source hosts. ### What this server does not โœ— Send data anywhere (read-only, no write tools). โœ— Access the local filesystem (no path-traversal surface). โœ— Use authentication tokens (none required โ€” all sources are public OGD). โœ— Cache or persist user inputs across calls. โœ— Execute shell commands or arbitrary code (no `subprocess`/`os.system`/`eval`). --- ## Features - ๐Ÿ“ถ **Broadband availability** โ€“ Fixed-line coverage at 30/100/300/500/1000 Mbit/s (250ร—250m grid) - ๐Ÿ”Œ **Fibre status** โ€“ FTTB/FTTH availability per location - ๐Ÿ“ **Multi-location comparison** โ€“ Connectivity check for up to 20 locations simultaneously - ๐Ÿ“ฑ **Mobile coverage** โ€“ 5G/4G/3G outdoor coverage (100ร—100m grid) - ๐Ÿ“ก **Antenna search** โ€“ Mobile and broadcast transmitters within a configurable radius - ๐Ÿ“บ **Radio & TV datasets** โ€“ Full-text search across BAKOM's radio and television datasets on opendata.swiss - ๐Ÿ—ž๏ธ **Media landscape** โ€“ BAKOM media structure reports and datasets - ๐Ÿ“Š **Telecom statistics** โ€“ Fixed-line, mobile, broadband market data via opendata.swiss - ๐Ÿ—‚๏ธ **Broadband Atlas catalogue** โ€“ All BAKOM dataset layers with direct API links - ๐Ÿ”“ **No authentication required** โ€“ All data is Open Government Data (OGD) --- ## Prerequisites - Python 3.11+ - `uv` or `pip` for installation - Internet connection (live APIs: geo.admin.ch, opendata.swiss) --- ## Installation ```bash # Recommended: uvx (no permanent installation required) uvx bakom-mcp # Or install with pip pip install bakom-mcp # Development install git clone https://github.com/malkreide/bakom-mcp cd bakom-mcp pip install -e ".[dev]" ``` --- ## Quickstart ### Claude Desktop Add to `claude_desktop_config.json`: ```json { "mcpServers": { "bakom": { "command": "uvx", "args": ["bakom-mcp"] } } } ``` **Config file locations:** - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` ### Cloud / HTTP Transport ```bash python -m bakom_mcp.server --http # Server running at http://localhost:8050/mcp ``` Configuration via environment variables (see [`.env.example`](.env.example)): | Variable | Default | Purpose | |---|---|---| | `BAKOM_MCP_HOST` | `127.0.0.1` | Bind address. Set to `0.0.0.0` only on trusted networks (warning logged). | | `BAKOM_MCP_PORT` | `8050` | TCP port. | | `BAKOM_MCP_CORS_ORIGINS` | _(empty)_ | Comma-separated allowed origins for browser clients. Empty = CORS disabled. | ### Docker A hardened container image is provided. Suitable for cloud deployments behind a reverse proxy (Caddy, Traefik, nginx). ```bash # Build & run via compose (recommended) docker compose up --build # Or via docker run docker build -t bakom-mcp:latest . docker run --rm \ --read-only \ --cap-drop=ALL \ --security-opt=no-new-privileges \ --tmpfs /tmp:rw,size=16M \ -p 127.0.0.1:8050:8050 \ bakom-mcp:latest ``` The image runs as **non-root** (UID 10001), uses a **read-only filesystem**, drops **all Linux capabilities** and refuses **privilege escalation**. Resource limits are configured in [`docker-compose.yml`](docker-compose.yml) (256 MB memory, 0.5 CPU, 64 PIDs). The default port mapping binds to `127.0.0.1` only โ€” for public exposure, terminate TLS and CORS at a reverse proxy. ### Cursor / VS Code / LibreChat ```json { "bakom": { "command": "uvx", "args": ["bakom-mcp"], "transport": "stdio" } } ``` > ๐Ÿ’ก *"stdio for the developer laptop, HTTP/SSE for the browser."* --- ## Available Tools (12) ### Broadband & Connectivity | Tool | Description | |------|-------------| | `bakom_broadband_coverage` | Fixed-line coverage at a coordinate (30โ€“1000 Mbit/s) | | `bakom_glasfaser_verfuegbarkeit` | FTTB/FTTH fibre availability | | `bakom_multi_standort_konnektivitaet` | Connectivity comparison for up to 20 locations | ### Mobile & Transmitters | Tool | Description | |------|-------------| | `bakom_mobilfunk_abdeckung` | 5G/4G/3G outdoor coverage | | `bakom_sendeanlagen_suche` | Mobile antennas within a configurable radius | | `bakom_frequenzdaten` | Radio/TV transmitter sites near a location | ### Media & RTV | Tool | Description | |------|-------------| | `bakom_rtv_suche` | Search BAKOM radio/TV datasets on opendata.swiss | | `bakom_medienstruktur_info` | Swiss media landscape datasets | | `bakom_aktuell` | Recently updated BAKOM datasets on a topic (catalogue, not press releases) | | `bakom_medien_statistik` | Market shares, reach and programme structure from the BAKOM cubes on LINDAS | ### Statistics & Catalogue | Tool | Description | |------|-------------| | `bakom_telekomstatistik_uebersicht` | Telecom statistics from opendata.swiss | | `bakom_breitbandatlas_datensaetze` | Full catalogue of Broadband Atlas layers | | `bakom_check_api_status` | ๐Ÿ” Health check for all configured data sources | --- ## Example Prompts ``` What is the broadband situation at Schulhaus Leutschenbach (47.4148, 8.5654)? Compare 5G and fibre coverage for these three school buildings: [coordinates] Which radio stations are licensed in canton Zurich? Show me the current BAKOM position on 5G frequency allocation. List all Broadband Atlas datasets available via geo.admin.ch. ``` --- ## Safety & Limits | Aspect | Details | |--------|---------| | **Access** | Read-only (`readOnlyHint: true`) โ€” the server cannot modify or delete any data | | **Personal data** | No personal data โ€” all sources are aggregated, public open data | | **Rate limits** | Built-in per-query caps (max 50 antennas, max 20 locations, max 50 RTV results) | | **Timeout** | 20 seconds per API call | | **Authentication** | No API keys required โ€” all 3 APIs are publicly accessible | | **Licences** | All data under CC0 / open licences (Open Government Data) | | **Terms of Service** | Subject to ToS of the respective data sources: [geo.admin.ch](https://www.geo.admin.ch/en/general-terms-and-conditions-fsdi), [opendata.swiss](https://opendata.swiss/en/terms-of-use) | --- ## Data Sources | Source | Data | Authentication | |--------|------|----------------| | [geo.admin.ch](https://api3.geo.admin.ch) | Broadband Atlas, mobile coverage, antenna locations | None | | [opendata.swiss](https://opendata.swiss) | BAKOM datasets, telecom statistics | None | | [lindas.admin.ch](https://lindas.admin.ch/query) | BAKOM media statistics cubes (SPARQL) | None | All data is published under open licences (CC0 / OGD). ### Architecture decision โ€” media statistics `bakom_medien_statistik` uses **Architecture A (live API only)**, verified live on 2026-08-13: - The SPARQL endpoint is `https://lindas.admin.ch/query`; the `/sparql` path documented elsewhere returns 404. - The OFCOM cubes live in the named graph `https://lindas.admin.ch/ofcom/cube`. Without an explicit `FROM`, a query hits the default graph and sees 2010 cubes from every federal office instead of 540. - A bulk download exists for only 137 of 540 cubes, so a dump-first architecture would cover a quarter of the data. - An unknown graph answers HTTP 200 with zero rows โ€” a silent empty, not an error. Requests carry retry with exponential backoff; egress-policy violations are not retried. **Scope, stated in the tool description because the README does not reach the model:** the cubes cover the programmes that were *surveyed*, not the full inventory. LINDAS's own count cube reports 199+39+17 radio programmes for 2020, while the `Programm` dimension across all cubes exposes 128 labels โ€” some of which are the same station spelled differently (`Energy BE` / `Energy Bern`), plus `Durchschnitt`, which is an aggregate rather than a station. This tool is a statistics source, not a broadcaster register; licensed broadcasters are listed in the [RTV database](https://rtvdb.ofcomnet.ch/de), which offers no machine-readable interface. --- ## Synergies with the MCP Portfolio **bakom-mcp** can be combined with other servers in the portfolio for multi-dimensional queries: ``` zurich-opendata-mcp โ†’ school building addresses + bakom-mcp โ†’ fibre and 5G status = "Digital equity" dashboard for all school districts ``` Further combinations: - `srgssr-mcp` + `bakom-mcp` โ†’ Media coverage and broadcast network - `swiss-statistics-mcp` + `bakom-mcp` โ†’ Telecom market development - `fedlex-mcp` + `bakom-mcp` โ†’ Regulatory context (RTVG, FMG) --- ## Project Structure ``` bakom-mcp/ โ”œโ”€โ”€ src/bakom_mcp/ โ”‚ โ”œโ”€โ”€ __init__.py # Package โ”‚ โ”œโ”€โ”€ server.py # MCP server (12 tools, 2 resources) โ”‚ โ””โ”€โ”€ py.typed # PEP 561 type marker โ”œโ”€โ”€ tests/ โ”‚ โ””โ”€โ”€ test_integration.py # Integration tests (live APIs) โ”œโ”€โ”€ assets/ โ”‚ โ””โ”€โ”€ demo.svg # Demo flow diagram โ”œโ”€โ”€ .github/workflows/ โ”‚ โ”œโ”€โ”€ ci.yml # CI: lint, syntax, import, tests โ”‚ โ””โ”€โ”€ publish.yml # PyPI publish on release โ”œโ”€โ”€ .gitignore โ”œโ”€โ”€ pyproject.toml # Build config (hatchling) โ”œโ”€โ”€ 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 License โ”œโ”€โ”€ README.md # This file (English) โ””โ”€โ”€ README.de.md # German version ``` --- ## MCP Protocol Version This server speaks **two protocol eras** over the same endpoint. The client's first request on a connection decides which one applies; a later claim from the other era is refused. | Era | Revision | Who reaches it | |---|---|---| | `initialize` handshake | `2024-11-05` โ€ฆ **`2025-11-25`** | What today's clients speak. The server answers with the revision asked for, or with the `2025-11-25` ceiling when the request asks for something newer. | | Per-request envelope | **`2026-07-28`** | A request carrying the `2026-07-28` `_meta` envelope opens a modern connection. | Both revisions are pinned in [`tests/test_protocol_version.py`](tests/test_protocol_version.py) and asserted against the installed SDK, so a Dependabot bump of `mcp` cannot move either one silently. The handshake ceiling is measured against a live `initialize` through the assembled ASGI stack, not read off a constant name. Note that the SDK's `LATEST_PROTOCOL_VERSION` is an alias for the **modern** era, not for the handshake era โ€” pinning against it alone would leave the era that current clients actually negotiate free to drift. **Update policy.** When the gate fails, do not edit the constant blindly: read the spec changelog between the two revisions, verify the server still behaves, then move the constant, this section, `README.de.md` and [`CHANGELOG.md`](CHANGELOG.md) together. --- ## Testing ```bash # Unit tests (no network required) PYTHONPATH=src pytest tests/ -m "not live" # Integration tests (live APIs, internet required) PYTHONPATH=src pytest tests/ -m "live" ``` --- ## Changelog See [CHANGELOG.md](CHANGELOG.md) --- ## Contributing Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) ([๐Ÿ‡ฉ๐Ÿ‡ช Deutsch](CONTRIBUTING.de.md)) for guidelines on reporting bugs, suggesting features, and submitting pull requests. --- ## Security To report a vulnerability, see the [Security Policy](SECURITY.md) ([๐Ÿ‡ฉ๐Ÿ‡ช Deutsch](SECURITY.de.md)). Please report security issues privately via GitHub Security Advisories rather than public issues. --- ## Software Licence MIT License โ€” see [LICENSE](LICENSE). ## Data Licence The BAKOM open data delivered through this server is published under **[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.de)**. When using or redistributing tool outputs, attribute the source as: > *Source: Federal Office of Communications (BAKOM) via opendata.swiss / geo.admin.ch ยท Licence: CC BY 4.0* Tool outputs already include this footer automatically. The Markdown response format ends with the attribution line; downstream applications that consume the JSON format should propagate the source/licence metadata to their end users. --- ## Author Hayal Oezkan ยท [github.com/malkreide](https://github.com/malkreide) --- ## Credits & Related Projects - **Data:** [BAKOM Open Data](https://www.bakom.admin.ch/bakom/en/home/digital-switzerland-and-internet/open-data.html) โ€“ Federal Office of Communications (OFCOM/BAKOM) - **Geodata:** [geo.admin.ch](https://api3.geo.admin.ch) โ€“ swisstopo / Federal Geodata Infrastructure - **Protocol:** [Model Context Protocol](https://modelcontextprotocol.io/) โ€“ Anthropic / Linux Foundation - **Related:** [zurich-opendata-mcp](https://github.com/malkreide/zurich-opendata-mcp) โ€“ MCP server for Zurich city open data - **Related:** [swiss-transport-mcp](https://github.com/malkreide/swiss-transport-mcp) โ€“ Swiss public transport MCP server - **Portfolio:** [Swiss Public Data MCP Portfolio](https://github.com/malkreide) --- *Part of the Swiss Open Data MCP portfolio โ€” public data deserves public interfaces.* ## Installation Run via [`uv`](https://docs.astral.sh/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`): ```json { "mcpServers": { "bakom-mcp": { "command": "uvx", "args": [ "bakom-mcp" ] } } } ```