io.github.malkreide/lobbywatch-mcp
Lobbywatch.ch transparency data on parliamentarians, interests, access badges
Open source Open in the app JSON README (API)
About
Lobbywatch.ch transparency data on parliamentarians, interests, access badges
Details
- Kind
- MCP servers
- Topic
- Government & public data
- Publisher
- malkreide
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.3.6
- Open pull requests
- 1
- Last push
- 2026-09-01T02:48:51Z
- Repository state
- ativo
- Language
- Python
- License
- MIT
- Added
- 2026-08-29 04:00:27
- Updated
- 2026-08-29 04:00:27
- Origin id
io.github.malkreide/lobbywatch-mcp
README
# ποΈ lobbywatch-mcp
[](https://github.com/malkreide/lobbywatch-mcp/actions/workflows/ci.yml)
[](https://badge.fury.io/py/lobbywatch-mcp)
[](https://www.python.org)
[](LICENSE)
[](https://creativecommons.org/licenses/by-sa/4.0/)
[](https://github.com/malkreide)
> An MCP server that connects AI models to **Lobbywatch.ch**, the largest lobby database of the Swiss Federal Parliament β conflicts of interest, lobby groups, access badges, and transparency scores.
[π©πͺ Deutsche Version](README.de.md)
> **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)** β connecting AI models to Swiss public data sources.
---
## π― Anchor Demo Query
> *"Welche Mitglieder der WBK-N haben Interessenbindungen zu Bildungsverlagen oder privaten BildungstrΓ€gern, und wie ist ihre Transparenz-Bewertung?"*
Which members of the National Council's Education Commission have declared conflicts of interest with educational publishers or private education providers, and how does their compensation transparency score compare?
[β More use cases by audience β](EXAMPLES.md)
### Demo

---
## Overview
Lobbywatch.ch maintains the largest public database on Swiss federal parliamentarians and their connections to lobby organisations: 245 parliamentarians, ~7'800 interessenbindungen (declared mandates), 139 lobby groups, 368 access-badge holders, updated weekly, licensed CC BY-SA 4.0.
`lobbywatch-mcp` exposes this data to Large Language Models via the Model Context Protocol. It is designed to be used alongside [`parlament-mcp`](https://github.com/malkreide/parlament-mcp) (the official Swiss Parliament's Curia Vista data): the pair makes it possible to ask *what* a parliamentarian did officially and *who* they are connected to β in a single conversation.
## Features
- **Dump-first, API-fallback architecture.** The weekly JSON dump is the primary source (stable, verified in production); the live `dataIF` REST API is used only where it returns reliable data (lobby groups, search).
- **Seven Phase 1 tools** β parliamentarian lookup, conflict-of-interest listing, branche search, lobby group fetch, rankings, transparency quota, cache control.
- **CC BY-SA 4.0 attribution** baked into every response via Pydantic envelopes.
- **Dual transport** β `stdio` for Claude Desktop, `streamable-http` / `sse` for cloud deployments.
- **Fuzzy name matching** via rapidfuzz for natural LLM input like "Jositsch" or "Wehrli".
- **No authentication required** (Phase 1 β No-Auth-First).
## Architecture
```
βββββββββββββββββββββββββββββββ
LLM client β LobbywatchClient β
(Claude Desktop, β β
Inspector, β¦) β βββββββββββββββββββββ β
β β β Dump cache β β cms.lobbywatch.ch
β MCP β β (24 h TTL, β β ββββββββββββββββββββ
βΌ stdio / β β ~80 MB resident)βββββββΌββββββΊβ weekly JSON β
βββββββββββ HTTP β βββββββββββββββββββββ β β export (~17 MB) β
β FastMCP βββββββΊβ β ββββββββββββββββββββ
β server β β βββββββββββββββββββββ β ββββββββββββββββββββ
βββββββββββ β β dataIF REST βββββββΌββββββΊβ /interface/v1/ β
β β (live fallback) β β β json/β¦ β
β βββββββββββββββββββββ β ββββββββββββββββββββ
βββββββββββββββββββββββββββββββ
```
Outbound HTTP runs through a single `httpx.AsyncClient` with `follow_redirects=False`, an SSRF guard that blocks RFC1918 / link-local / metadata IPs, and an httpx event hook that re-resolves on every request. The dump path is the primary source of truth for parliamentarian queries; `dataIF` is only used for lobby group lookups and the search endpoint.
## Prerequisites
- Python 3.11 or newer
- Internet access to download the weekly Lobbywatch JSON export (~17 MB zipped)
## Installation
From PyPI (after first release):
```bash
pip install lobbywatch-mcp
```
From source:
```bash
git clone https://github.com/malkreide/lobbywatch-mcp.git
cd lobbywatch-mcp
pip install -e ".[dev]"
```
## Usage
### Standalone
```bash
lobbywatch-mcp
```
This starts the server in `stdio` mode. For HTTP:
```bash
LOBBYWATCH_MCP_TRANSPORT=http LOBBYWATCH_MCP_PORT=8000 lobbywatch-mcp
```
### Container
A hardened multi-stage `Dockerfile` ships with the repo (non-root,
read-only-rootfs compatible). See [`docs/deployment.md`](docs/deployment.md)
and [`deploy/docker-compose.example.yml`](deploy/docker-compose.example.yml)
for resource limits, sticky-LB guidance and egress hardening.
```bash
docker build -t lobbywatch-mcp:0.2.0 .
docker run --rm -p 127.0.0.1:8000:8000 lobbywatch-mcp:0.2.0
```
### Claude Desktop
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"lobbywatch": {
"command": "uvx",
"args": ["lobbywatch-mcp"]
}
}
}
```
A full example is provided in [`claude_desktop_config.json`](claude_desktop_config.json).
### Example Queries
Once connected, try prompts such as:
- *"Give me the top 10 parliamentarians by number of interessenbindungen in the SP party."*
- *"Which WBK-N members have mandates in the publishing or education industry?"*
- *"Look up the lobby group 'economiesuisse' and list its connected parliamentarians."*
- *"What is the compensation-transparency score distribution for the finance commission (FK-N)?"*
## Tools
All tool names use the `lobbywatch_` namespace prefix (since 0.2.0) to
avoid collisions with sibling portfolio servers.
| Tool | Purpose | Source |
|---|---|---|
| `lobbywatch_get_parlamentarier(name_or_id)` | Full profile + all conflicts of interest | Dump |
| `lobbywatch_list_interessenbindungen(name_or_id, nur_hauptberuflich, nur_aktiv)` | Filtered mandate list | Dump |
| `lobbywatch_search_parlamentarier_nach_branche(branche_query, kommission, limit)` | Cross-filter by industry and commission | Dump |
| `lobbywatch_get_lobbygruppe(name_or_id)` | Lobby group with connected MPs and organisations | Live dataIF |
| `lobbywatch_get_ranking(kriterium, kommission, partei, limit)` | Top-N by criterion | Dump |
| `lobbywatch_get_transparenzquote(kommission)` | Distribution of compensation transparency labels | Dump |
| `lobbywatch_refresh_dump()` / `lobbywatch_dump_status()` | Cache control | Dump |
## Configuration
All behaviour is controlled via environment variables:
| Variable | Default | Purpose |
|---|---|---|
| `LOBBYWATCH_MCP_TRANSPORT` | `stdio` | Transport (`stdio`, `http`, `sse`) |
| `LOBBYWATCH_MCP_HOST` | `127.0.0.1` | HTTP bind host (set to `0.0.0.0` only behind an auth gateway) |
| `LOBBYWATCH_MCP_PORT` | `8000` | HTTP bind port |
| `LOBBYWATCH_MCP_CACHE_DIR` | `~/.cache/lobbywatch-mcp` | Dump cache location |
| `LOBBYWATCH_MCP_CACHE_TTL` | `86400` (24h) | Cache time-to-live in seconds |
| `LOBBYWATCH_MCP_HTTP_TIMEOUT` | `60` | HTTP timeout in seconds |
| `LOBBYWATCH_MCP_CORS_ORIGINS` | _(unset)_ | Comma-separated origin allow-list for HTTP/SSE; when set, exposes `Mcp-Session-Id` to browsers |
| `LOBBYWATCH_MCP_LOG_FORMAT` | `text` | `text` (stdlib formatter) or `json` (structured via structlog) |
| `LOBBYWATCH_MCP_LOG_LEVEL` | `INFO` | `DEBUG` / `INFO` / `WARNING` / `ERROR` |
| `LOBBYWATCH_MCP_OTEL_ENABLED` | `0` | Set to `1` to enable OpenTelemetry tracing (requires `pip install 'lobbywatch-mcp[obs]'`) |
| `LOBBYWATCH_MCP_OTEL_ENDPOINT` | _(unset)_ | OTLP/HTTP collector endpoint (e.g. `http://localhost:4318/v1/traces`) |
## Project Structure
```
lobbywatch-mcp/
βββ src/lobbywatch_mcp/
β βββ __init__.py
β βββ __main__.py # CLI + transport selection
β βββ config.py # URLs, cache paths, attribution
β βββ client.py # Dump download + dataIF client
β βββ models.py # Pydantic v2 response envelopes
β βββ server.py # FastMCP tool registrations
βββ tests/
β βββ conftest.py # Fixture parliamentarians
β βββ test_client.py # Respx-mocked unit tests
β βββ test_server.py # Tool integration tests
β βββ test_live.py # @pytest.mark.live β excluded from CI
βββ .github/workflows/
β βββ ci.yml # Test matrix + ruff
β βββ publish.yml # PyPI OIDC Trusted Publisher
βββ claude_desktop_config.json
βββ pyproject.toml
βββ ...
```
## Data License & Attribution
**The code** is released under the MIT License.
**The data** served through this MCP is Β© Lobbywatch.ch and licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). Every response envelope includes the attribution string. Downstream users must:
1. Credit Lobbywatch.ch as the data source.
2. Share derivative datasets under the same CC BY-SA 4.0 terms.
3. Understand that Lobbywatch is a **community-researched database** β not an official register. It is authoritative for transparency research but should not be confused with the Federal Parliament's own declarations.
## Known Limitations
- The upstream `/table/parlamentarier/...` `dataIF` REST endpoint currently returns empty result sets. The server works around this by using the weekly JSON dump instead.
- `zutrittsberechtigungen` (access badges) are not populated in the "essential" dump variant used here. A future release will add a dedicated tool using the non-essential dump.
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md).
## Security
See [SECURITY.md](SECURITY.md) for the security posture, accepted-risk
decisions, and how to report a vulnerability.
## 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. This server builds no ASGI app to send an `initialize` through, so
the gate asserts the SDK constants rather than a measured response β the
weaker form, named rather than left unsaid.
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.
## Changelog
See [CHANGELOG.md](CHANGELOG.md).
## License
MIT License β see [LICENSE](LICENSE). Data CC BY-SA 4.0 β see [NOTICE.md](NOTICE.md).
## Author
**malkreide** Β· [GitHub](https://github.com/malkreide)
---
*Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide).*
<!-- mcp-name: io.github.malkreide/lobbywatch-mcp -->
<!-- BEGIN GENERATED: install -->
## 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": {
"lobbywatch-mcp": {
"command": "uvx",
"args": [
"lobbywatch-mcp"
]
}
}
}
```
<!-- END GENERATED: install -->