lobbywatch-mcp
This MCP server lets AI models query Swiss federal parliamentarians' lobby connections, conflicts of interest, industry/commission affiliations, transparency scores, and lobby groups.
Look up a parliamentarian by fuzzy name or ID and retrieve full profile, commissions, and all interessenbindungen, with near-miss suggestions.
List and filter a parliamentarian's interessenbindungen by main occupation or active status.
Search parliamentarians by industry/organisation substring and optional commission filter.
Fetch a lobby group from the live dataIF API with connected organisations and parliamentarians.
Rank parliamentarians by number of interessenbindungen or hauptberuflich mandates, filtered by party or commission.
Get compensation-transparency distribution across all parliamentarians or a commission subset.
Refresh the weekly JSON dump and check dump cache status, age, and record count.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@lobbywatch-mcpWhich Swiss MPs have conflicts with pharma companies?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
ποΈ lobbywatch-mcp
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.
Part of the Swiss Public Data MCP Portfolio β 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 β
Demo
Related MCP server: parlament-mcp
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 (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
dataIFREST API is used only where it returns reliable data (lobby groups, search).Stale-cache fallback, never silent. If the dump cannot be refreshed, a cached copy up to 7 days old answers instead, and every dump-backed response then carries a
staleblock with its download time, age and the reason the refresh failed. The source is retried every 15 minutes;lobbywatch_refresh_dumpnever falls back.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 β
stdiofor Claude Desktop,streamable-http/ssefor 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):
pip install lobbywatch-mcpFrom source:
git clone https://github.com/malkreide/lobbywatch-mcp.git
cd lobbywatch-mcp
pip install -e ".[dev]"Usage
Standalone
lobbywatch-mcpThis starts the server in stdio mode. For HTTP:
LOBBYWATCH_MCP_TRANSPORT=http LOBBYWATCH_MCP_PORT=8000 lobbywatch-mcpContainer
A hardened multi-stage Dockerfile ships with the repo (non-root,
read-only-rootfs compatible). See docs/deployment.md
and deploy/docker-compose.example.yml
for resource limits, sticky-LB guidance and egress hardening.
docker build -t lobbywatch-mcp:0.2.0 .
docker run --rm -p 127.0.0.1:8000:8000 lobbywatch-mcp:0.2.0Claude Desktop
Add to your claude_desktop_config.json:
{
"mcpServers": {
"lobbywatch": {
"command": "uvx",
"args": ["lobbywatch-mcp"]
}
}
}A full example is provided in 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 |
| Full profile + all conflicts of interest | Dump |
| Filtered mandate list | Dump |
| Cross-filter by industry and commission | Dump |
| Lobby group with connected MPs and organisations | Live dataIF |
| Top-N by criterion | Dump |
| Distribution of compensation transparency labels | Dump |
| Cache control | Dump |
Configuration
All behaviour is controlled via environment variables:
Variable | Default | Purpose |
|
| Transport ( |
|
| HTTP bind host (set to |
|
| HTTP bind port |
|
| Dump cache location |
|
| Cache time-to-live in seconds |
|
| Oldest cached dump that may answer when the refresh fails, in seconds; |
|
| HTTP timeout in seconds |
| (unset) | Comma-separated origin allow-list for HTTP/SSE; when set, exposes |
|
|
|
|
|
|
|
| Set to |
| (unset) | OTLP/HTTP collector endpoint (e.g. |
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. Every response envelope includes the attribution string. Downstream users must:
Credit Lobbywatch.ch as the data source.
Share derivative datasets under the same CC BY-SA 4.0 terms.
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/...dataIFREST 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.
Security
See 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 |
|
| What today's clients speak. The server answers with the revision asked for, or with the |
Per-request envelope |
| A request carrying the |
Both revisions are pinned in
tests/test_protocol_version.py and asserted
against the installed SDK, so a Dependabot bump of mcp cannot move either one
silently. The constants alone are the weaker form, so
tests/test_spec_2026_07_28.py measures both
eras through the assembled ASGI app: server/discover without a session, an
initialize asking for a future revision capped at 2025-11-25, and every
tool called over a 2026-07-28 connection with SDK deprecations raised as
errors.
Native to 2026-07-28. lobbywatch_refresh_dump reports its progress as
notifications/progress, which both eras deliver to any client that sends a
progressToken. It used to log via notifications/message; logging is
deprecated in 2026-07-28 (SEP-2577) and only delivered on requests that opt
in with a log level, so modern clients silently received nothing. serverInfo
now carries the package version β under 2026-07-28 it is stamped into the
_meta of every response.
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 together.
Changelog
See CHANGELOG.md.
License
MIT License β see LICENSE. Data CC BY-SA 4.0 β see NOTICE.md.
Author
malkreide Β· GitHub
Part of the Swiss Public Data MCP Portfolio.
Installation
Run via 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):
{
"mcpServers": {
"lobbywatch-mcp": {
"command": "uvx",
"args": [
"lobbywatch-mcp"
]
}
}
}Available Tools
8 toolslobbywatch_dump_statusARead-onlyIdempotent
Return current dump cache status without forcing a refresh.
Use cases: - "How fresh is the cached data right now?" - "When was the dump last loaded?"
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| stale | No | Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it. |
| loaded | Yes | |
| source | No | |
| cached_at | No | |
| provenance | Yes | Which endpoint / dump the payload came from |
| age_seconds | No | |
| record_count | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and openWorldHint=false, so the safety profile is covered. The description adds only the 'without forcing a refresh' constraint, which is useful but modest given annotations carry the main behavioral burden.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded one-line purpose followed by two tightly scoped use-case bullets; every line earns its place with zero filler or restatement of the title.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no explanation, and annotations fully cover the read-only/idempotent safety profile. For a zero-parameter status read, nothing an agent needs to invoke it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes zero parameters, so there is nothing to document and no ambiguity for an agent to resolve. Baseline of 4 applies for a no-argument tool.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (Return) and resource (dump cache status), and the qualifier 'without forcing a refresh' distinguishes it from the mutation-oriented sibling lobbywatch_refresh_dump. It stops short of naming that sibling explicitly, but the scope is unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides concrete use cases ('How fresh is the cached data?' / 'When was the dump last loaded?') and an implicit when-not via 'without forcing a refresh'. Clear usage context, though it never names lobbywatch_refresh_dump as the alternative for forcing a reload.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lobbywatch_get_lobbygruppeARead-onlyIdempotent
Fetch a lobby group (interessengruppe) from the live Lobbywatch dataIF, including its connected organisations and parliamentarians.
Uses the live REST API since this endpoint returns fresh data.
Use cases: - "Look up 'economiesuisse' and list connected MPs" - "Who's affiliated with the lobby group #42?" - "Show me all parliamentarians linked to the pharma lobby"
| Name | Required | Description | Default |
|---|---|---|---|
| name_or_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| source | No | |
| provenance | Yes | Which endpoint / dump the payload came from |
| lobbygruppe | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already indicate read-only and idempotent behavior. The description adds transparency by noting the live REST API and fresh data, plus the inclusion of connected entities. No contradictions, but more detail on rate limits or auth could further enhance.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise with a clear purpose statement followed by illustrative use cases. Every sentence adds value, and the structure is front-loaded.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description adequately covers what the tool does, including its return content (connected organisations and parliamentarians). With an output schema available, further detail on return format is unnecessary.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema has no parameter descriptions (0% coverage), but the description compensates by showing examples of using a name or ID, clarifying the parameter's purpose and format.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool fetches a lobby group including connected organisations and parliamentarians. It is specific and distinguishes from sibling tools that focus on individual parliamentarians, rankings, or transparency quotes.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides example use cases that illustrate common queries, giving clear context for when to use the tool. However, it lacks explicit guidance on when not to use it or alternatives among siblings.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lobbywatch_get_parlamentarierARead-onlyIdempotent
Look up a Swiss federal parliamentarian and return their full profile, including all declared/researched interessenbindungen.
On a fuzzy miss, the response surfaces near-miss candidates so the LLM can prompt the user with "did you mean�" suggestions instead of treating the empty result as authoritative (audit ARCH-003).
Args: name_or_id: Either the numeric Lobbywatch ID (as string) or a name. Name matching is fuzzy β partial last names work.
Use cases: - "Show me Anna Mustermann's full lobbying profile" - "What conflicts of interest does parliamentarian #42 declare?" - "Look up Wehrli β give me everything you have"
| Name | Required | Description | Default |
|---|---|---|---|
| name_or_id | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| stale | No | Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it. |
| source | No | |
| provenance | Yes | Which endpoint / dump the payload came from |
| suggestions | No | Near-miss fuzzy candidates when the lookup failed (ARCH-003) |
| parlamentarier | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations cover the safety profile (readOnly, idempotent, closed-world), so the bar is lower, and the description adds non-obvious behavior: fuzzy miss returns near-miss candidates rather than an authoritative empty result. Useful and specific, though nothing is said about profile size, rate limits, or error behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Purpose and fuzzy-miss caveat are front-loaded, followed by clearly labeled Args and Use cases blocks. Slightly verbose with the use-case list and audit reference, but every section is scannable and none of it is filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
A single-parameter lookup with an output schema present, so return-field detail is not required, and the description still explains the fuzzy-miss return behavior. Complete enough for correct invocation; only marginal gaps remain around input sanitization and failure modes.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With schema description coverage at 0%, the description carries the burden and does so: it explains name_or_id accepts either a numeric Lobbywatch ID as a string or a name, and that name matching is fuzzy with partial last names supported. That is substantive semantics beyond the bare string type in the schema, though it omits edge cases like max length behavior.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (look up) and resource (Swiss federal parliamentarian) plus what it returns (full profile including interessenbindungen). Clearly distinct from siblings like lobbywatch_search_parlamentarier_nach_branche and lobbywatch_list_interessenbindungen.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides three concrete use cases that make the intended invocation context clear, and explains the fuzzy-miss path. It does not, however, explicitly name alternatives or state when not to use it (e.g., vs. the search-by-branche sibling), so it stops short of full routing guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lobbywatch_get_rankingARead-onlyIdempotent
Rank parliamentarians by a criterion.
Args: kriterium: One of "anzahl_interessenbindungen", "anzahl_hauptberuflich". kommission: Optional commission abbreviation filter (e.g. "WBK-N"). partei: Optional party filter (e.g. "SP", "SVP", "Mitte"). limit: Top-N to return (1β100).
Use cases: - "Top 10 SP MPs by total mandate count" - "Which Mitte-Fraktion members have the most full-time mandates?" - "Rank WBK-N by IB count β who's most involved?"
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| partei | No | ||
| kriterium | No | anzahl_interessenbindungen | |
| kommission | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| stale | No | Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it. |
| source | No | |
| eintraege | No | |
| kriterium | Yes | |
| provenance | Yes | Which endpoint / dump the payload came from |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true and openWorldHint=false, so the safety profile is covered. The description adds that kriterium is restricted to two values and that filters can be combined, but says nothing about ordering direction, tie-breaking, or behavior of defaults beyond the schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The one-line purpose is front-loaded, followed by a compact Arg list and three terse examples. Every element earns its place, though the labeled sections add slight overhead relative to the amount of content.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, return values need not be described, and all four parameters plus the two enum values receive coverage. Combined with annotations covering idempotency and safety, an agent has enough to invoke it correctly; only ordering/ranking semantics remain unstated.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must carry the load, and it largely does: it names each of the four parameters, gives examples for kommission ('WBK-N') and partei ('SP', 'SVP', 'Mitte'), and the use cases disambiguate the opaque enum values (e.g. 'night-most full-time mandates' for anzahl_hauptberuflich). It still does not explain the full semantics of anzahl_interessenbindungen beyond 'IB count'.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Rank parliamentarians by a criterion'), which is clearly distinct from sibling tools like get_parlamentarier or list_interessenbindungen. It does not explicitly name or contrast against those siblings, so it falls short of a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The 'Use cases' block gives three concrete scenarios that show when the tool applies (ranking by mandate count, filtering by party, filtering by commission), which is clear context. It does not state when NOT to use it or name an alternative sibling, so no exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lobbywatch_get_transparenzquoteARead-onlyIdempotent
Aggregate the verguetungstransparenz_beurteilung values across all parliamentarians (or a commission subset) and return the distribution.
Useful to answer: 'How transparent is the education commission on compensation disclosure?'
Use cases: - "How transparent is the FK-N on compensation disclosure?" - "Distribution of transparency labels across the whole parliament" - "Compare WBK-N transparency vs the council average"
| Name | Required | Description | Default |
|---|---|---|---|
| kommission | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| scope | Yes | |
| stale | No | Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it. |
| total | Yes | |
| source | No | |
| provenance | Yes | Which endpoint / dump the payload came from |
| nach_bewertung | No | |
| quote_ausreichend | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, and openWorldHint=false, so the safety profile is covered. The description adds real behavioral context beyond that: it states what underlying field is aggregated and that omitting the commission defaults to the whole parliament. No permission or rate-limit detail is given, but none is needed for a local-dump read.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The core operation is front-loaded in the first sentence and the example questions are tucked below it. The bulleted use cases partly restate the 'Useful to answer' line, so there is slight redundancy, but nothing is bloated for a tool of this complexity.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so the description needn't explain the return shape, and it correctly focuses on intent and scope. Combined with the annotations it gives an agent enough to invoke the tool correctly; only the commission-identifier format is left to inference.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
There is one parameter with 0% schema description coverage, so the description has to carry the load. It implies 'kommission' accepts a commission identifier and the examples (FK-N, WBK-N) hint at the accepted format, which is genuinely useful. However, it never states the format or matching rules explicitly and doesn't mention the maxLength constraint, so compensation is only partial.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description gives a specific verb and resource: it aggregates verguetungstransparenz_beurteilung values and returns the distribution, with scope stated as 'all parliamentarians (or a commission subset)'. That is far more informative than the opaque German tool name. It does not, however, explicitly differentiate itself from siblings such as lobbywatch_get_ranking, so it stops short of a 5.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage context is clear and concrete: 'Useful to answer: How transparent is the education commission...' plus three example questions covering whole-parliament, single-commission, and comparison scenarios. There is no explicit when-not guidance or named alternative (e.g. get_ranking), so it does not reach 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lobbywatch_list_interessenbindungenARead-onlyIdempotent
Return the list of interessenbindungen (conflicts of interest) for one parliamentarian, optionally restricted to full-time or currently-active mandates.
On a fuzzy miss, near-miss candidates are returned in
suggestions (audit ARCH-003).
Args: name_or_id: ID or name (fuzzy). nur_hauptberuflich: If True, only main-occupation mandates. nur_aktiv: If True, drop mandates with an end date (bis) set.
Use cases: - "Which active mandates does Jositsch hold today?" - "List Anna Mustermann's full-time mandates only" - "Give me every IB ever declared by parliamentarian #1"
| Name | Required | Description | Default |
|---|---|---|---|
| nur_aktiv | No | ||
| name_or_id | Yes | ||
| nur_hauptberuflich | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | |
| stale | No | Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it. |
| source | No | |
| provenance | Yes | Which endpoint / dump the payload came from |
| suggestions | No | Near-miss fuzzy candidates when the lookup failed (ARCH-003) |
| parlamentarier_id | Yes | |
| interessenbindungen | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent and non-open-world semantics, so the safety profile is covered. The description adds genuine behavioral context beyond that: fuzzy-name misses return near-miss candidates in a 'suggestions' field (audit ARCH-003), and nur_aktiv drops mandates with an end date set. It does not mention rate limits or result ordering, but for a read-only list tool this is solid.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Purpose and the fuzzy-miss caveat are front-loaded, then args, then use cases. Every section earns its place, though the use-case block repeats filter semantics already stated in Args, adding mild redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be explained, and the description covers the mutation-free behavior, the three parameters and the fuzzy-match edge case. Missing only the default of nur_aktiv and any note on result scope/ordering for a complete picture.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must carry the parameter meaning, and it does: name_or_id is 'ID or name (fuzzy)', nur_hauptberuflich means only main-occupation mandates, nur_aktiv drops mandates with an end date set. The one gap is not noting that nur_aktiv defaults to true in the schema, which could surprise an agent expecting all mandates.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Return the list of interessenbindungen'), glosses the German term as conflicts of interest, and scopes it to 'one parliamentarian' with optional filters. This is clearly distinguishable from siblings like get_parlamentarier or get_lobbygruppe without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Three concrete use cases ('Which active mandates does Jositsch hold today?', 'full-time mandates only', 'every IB ever declared by #1') make the intended usage unambiguous and show how the filter flags combine. It stops short of naming an alternative tool or stating when not to use it, so it misses the top tier.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lobbywatch_refresh_dumpAIdempotent
Force re-download of the weekly Lobbywatch dump. Returns the new cache status.
Reports progress via the MCP Context (audit SDK-003) so long-running downloads (~17 MB compressed) surface useful feedback to the calling LLM and operator.
Use cases: - "Force a fresh download β the data looks stale" - "I just heard about a new declaration β refresh and re-check"
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| stale | No | Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it. |
| loaded | Yes | |
| source | No | |
| cached_at | No | |
| provenance | Yes | Which endpoint / dump the payload came from |
| age_seconds | No | |
| record_count | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly=false, destructive=false, idempotent=true, and openWorld=true, so the safety profile is covered. The description adds genuinely useful context beyond that: progress reporting via MCP Context, the ~17 MB compressed payload size, and that it is long-running β helping an agent anticipate a slow call.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The core action and its return are front-loaded in the first two sentences, with operational detail and use cases following. Nothing is padded; the quoted examples are compact and aid routing.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With an output schema present, the description need not explain return values, and it correctly defers to it while noting 'returns the new cache status'. It covers behavior, size, and progress feedback; only the expected wall-clock duration or blocking behavior is left unstated.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes zero parameters, so there is nothing for the description to disambiguate; the empty schema is self-explanatory. Baseline 4 applies for a parameterless tool.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Force re-download of the weekly Lobbywatch dump') and adds the scoping detail that it is weekly and a full re-download. It does not explicitly differentiate from the sibling lobbywatch_dump_status, so an agent must infer that this mutates the cache while the sibling only reports it.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The two quoted use cases ('data looks stale', 'new declaration β refresh and re-check') give clear triggering conditions. However, it names no alternatives (e.g. check lobbywatch_dump_status first to see if a refresh is even needed) and no when-not-to-use guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
lobbywatch_search_parlamentarier_nach_brancheARead-onlyIdempotent
Find parliamentarians with interessenbindungen matching a search term against the linked organisation's name, alias, and (when available) branche field. Optional commission filter.
Note: the Lobbywatch "essential" dump does not embed a branche taxonomy on each organisation β branche is referenced by id and resolved via the separate interessengruppe table. Version 0.1 therefore performs a substring match against organisation names and the branche field when present. Version 0.2 will add full cross-reference resolution.
Args:
branche_query: Substring match (case-insensitive) against
organisation.anzeige_name, organisation.name,
organisation.branche (where present), e.g. "Verlag",
"Pharma", "Bank", "Krankenkasse", "Bildung".
kommission: Commission abbreviation to restrict the result to,
e.g. "WBK-N" for the education commission of the National Council.
limit: Max number of {parlamentarier, ib} pairs returned (1β200).
Use cases: - "Which WBK-N members hold mandates in the publishing industry?" - "Cross-filter Pharma Γ FK-N to surface health-policy lobbyists" - "List every parliamentarian with a 'Krankenkasse' connection"
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| kommission | No | ||
| branche_query | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| count | Yes | |
| query | Yes | |
| stale | No | Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it. |
| source | No | |
| treffer | No | List of {parlamentarier, interessenbindung} pairs matching the branche |
| provenance | Yes | Which endpoint / dump the payload came from |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint and openWorldHint=false, yet the description still adds real behavioral context: the match is a case-insensitive substring over organisation names and the branche field where present, and the current 0.1 implementation cannot resolve branche ids via interessengruppe. That limitation disclosure materially affects result interpretation.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The Args and Use cases sections are well front-loaded and earn their place, but the paragraph about version 0.1 vs. a planned 0.2 adds roadmap narrative that is tangential to calling the tool today. The definition is longer than it needs to be for three parameters.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be described, and the description still notes the pair shape ({parlamentarier, ib}). Combined with the disclosed substring/version limitation and full parameter coverage, the agent has what it needs; the undefined 'ib' abbreviation and unstated default are minor gaps.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With 0% schema description coverage the description carries the burden and largely succeeds: branche_query is documented with its matching fields and examples ('Verlag', 'Pharma'), kommission with a concrete abbreviation ('WBK-N'), and limit as the max number of pairs returned. It omits the limit default (25) and maxLength constraints, so it stops short of a 5.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description gives a specific verb+resource: find parliamentarians whose interessenbindungen match a term across an organisation's name/alias/branche. It is clearly a search over mandate links rather than a plain parliamentarian lookup, but it never names a sibling (e.g., lobbywatch_list_interessenbindungen or lobbywatch_get_parlamentarier) to route the agent between them.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Three concrete use cases establish the intended context (e.g., 'Which WBK-N members hold mandates in the publishing industry?'), and the kommission/branche combination is explained. However, there is no explicit when-not or alternative-tool guidance, so the agent must infer boundaries with the sibling list/search tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
7 tool updates
v0.3.8- Changed
lobbywatch_dump_status2 fields changed- added
Output schema / $defsAdded value: +{ + "StaleDump": { + "description": "The weekly dump could not be refreshed; an older cached copy answered.", + "properties": { + "age_seconds": { + "description": "Age of the answering copy", + "title": "Age Seconds", + "type": "integer" + }, + "cached_at": { + "description": "When the answering copy was downloaded (UTC, ISO 8601)", + "title": "Cached At", + "type": "string" + }, + "reason": { + "description": "Why the refresh failed", + "title": "Reason", + "type": "string" + } + }, + "required": [ + "cached_at", + "age_seconds", + "reason" + ], + "title": "StaleDump", + "type": "object" + } +} - added
Output schema / properties / staleAdded value: +{ + "anyOf": [ + { + "$ref": "#/$defs/StaleDump" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it." +}
- Changed
lobbywatch_get_parlamentarier2 fields changed- added
Output schema / $defs / StaleDumpAdded value: +{ + "description": "The weekly dump could not be refreshed; an older cached copy answered.", + "properties": { + "age_seconds": { + "description": "Age of the answering copy", + "title": "Age Seconds", + "type": "integer" + }, + "cached_at": { + "description": "When the answering copy was downloaded (UTC, ISO 8601)", + "title": "Cached At", + "type": "string" + }, + "reason": { + "description": "Why the refresh failed", + "title": "Reason", + "type": "string" + } + }, + "required": [ + "cached_at", + "age_seconds", + "reason" + ], + "title": "StaleDump", + "type": "object" +} - added
Output schema / properties / staleAdded value: +{ + "anyOf": [ + { + "$ref": "#/$defs/StaleDump" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it." +}
- Changed
lobbywatch_get_ranking2 fields changed- added
Output schema / $defs / StaleDumpAdded value: +{ + "description": "The weekly dump could not be refreshed; an older cached copy answered.", + "properties": { + "age_seconds": { + "description": "Age of the answering copy", + "title": "Age Seconds", + "type": "integer" + }, + "cached_at": { + "description": "When the answering copy was downloaded (UTC, ISO 8601)", + "title": "Cached At", + "type": "string" + }, + "reason": { + "description": "Why the refresh failed", + "title": "Reason", + "type": "string" + } + }, + "required": [ + "cached_at", + "age_seconds", + "reason" + ], + "title": "StaleDump", + "type": "object" +} - added
Output schema / properties / staleAdded value: +{ + "anyOf": [ + { + "$ref": "#/$defs/StaleDump" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it." +}
- Changed
lobbywatch_get_transparenzquote2 fields changed- added
Output schema / $defsAdded value: +{ + "StaleDump": { + "description": "The weekly dump could not be refreshed; an older cached copy answered.", + "properties": { + "age_seconds": { + "description": "Age of the answering copy", + "title": "Age Seconds", + "type": "integer" + }, + "cached_at": { + "description": "When the answering copy was downloaded (UTC, ISO 8601)", + "title": "Cached At", + "type": "string" + }, + "reason": { + "description": "Why the refresh failed", + "title": "Reason", + "type": "string" + } + }, + "required": [ + "cached_at", + "age_seconds", + "reason" + ], + "title": "StaleDump", + "type": "object" + } +} - added
Output schema / properties / staleAdded value: +{ + "anyOf": [ + { + "$ref": "#/$defs/StaleDump" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it." +}
- Changed
lobbywatch_list_interessenbindungen2 fields changed- added
Output schema / $defs / StaleDumpAdded value: +{ + "description": "The weekly dump could not be refreshed; an older cached copy answered.", + "properties": { + "age_seconds": { + "description": "Age of the answering copy", + "title": "Age Seconds", + "type": "integer" + }, + "cached_at": { + "description": "When the answering copy was downloaded (UTC, ISO 8601)", + "title": "Cached At", + "type": "string" + }, + "reason": { + "description": "Why the refresh failed", + "title": "Reason", + "type": "string" + } + }, + "required": [ + "cached_at", + "age_seconds", + "reason" + ], + "title": "StaleDump", + "type": "object" +} - added
Output schema / properties / staleAdded value: +{ + "anyOf": [ + { + "$ref": "#/$defs/StaleDump" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it." +}
- Changed
lobbywatch_refresh_dump2 fields changed- added
Output schema / $defsAdded value: +{ + "StaleDump": { + "description": "The weekly dump could not be refreshed; an older cached copy answered.", + "properties": { + "age_seconds": { + "description": "Age of the answering copy", + "title": "Age Seconds", + "type": "integer" + }, + "cached_at": { + "description": "When the answering copy was downloaded (UTC, ISO 8601)", + "title": "Cached At", + "type": "string" + }, + "reason": { + "description": "Why the refresh failed", + "title": "Reason", + "type": "string" + } + }, + "required": [ + "cached_at", + "age_seconds", + "reason" + ], + "title": "StaleDump", + "type": "object" + } +} - added
Output schema / properties / staleAdded value: +{ + "anyOf": [ + { + "$ref": "#/$defs/StaleDump" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it." +}
- Changed
lobbywatch_search_parlamentarier_nach_branche2 fields changed- added
Output schema / $defs / StaleDumpAdded value: +{ + "description": "The weekly dump could not be refreshed; an older cached copy answered.", + "properties": { + "age_seconds": { + "description": "Age of the answering copy", + "title": "Age Seconds", + "type": "integer" + }, + "cached_at": { + "description": "When the answering copy was downloaded (UTC, ISO 8601)", + "title": "Cached At", + "type": "string" + }, + "reason": { + "description": "Why the refresh failed", + "title": "Reason", + "type": "string" + } + }, + "required": [ + "cached_at", + "age_seconds", + "reason" + ], + "title": "StaleDump", + "type": "object" +} - added
Output schema / properties / staleAdded value: +{ + "anyOf": [ + { + "$ref": "#/$defs/StaleDump" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Set only when the dump could not be refreshed and an older cached copy answered instead. The data may miss changes made since `stale.cached_at` β say so when relaying it." +}
8 tool updates
v0.3.1- First observed
lobbywatch_dump_status - First observed
lobbywatch_get_lobbygruppe - First observed
lobbywatch_get_parlamentarier - First observed
lobbywatch_get_ranking - First observed
lobbywatch_get_transparenzquote - First observed
lobbywatch_list_interessenbindungen - First observed
lobbywatch_refresh_dump - First observed
lobbywatch_search_parlamentarier_nach_branche
TDQS
Scored across 8 tools
Most tools target clearly distinct actions (lookup, industry search, ranking, transparency, lobby group, cache ops). However, get_parlamentarier returns the full profile including interessenbindungen, which overlaps with list_interessenbindungen; the descriptions clarify that the latter adds filtering, so the boundary is workable but not perfectly sharp.
All tools share the lobbywatch_ prefix and use snake_case with a mostly verb_noun pattern (get_parlamentarier, list_interessenbindungen, get_ranking, refresh_dump). dump_status is a minor noun-only deviation but still fully consistent in style.
Eight tools is well-scoped for a domain-specific lobbying data server. Each tool earns its place, including the two cache-management tools that are justified by the dump-backed architecture.
The surface covers lookup, IB listing, industry/commission cross-filtering, rankings, transparency aggregates, lobby groups, and cache lifecycle. Minor gaps exist: no direct organisation or commission listing, and branche search is only a substring match pending v0.2, but core workflows are covered.
Maintenance
Related MCP Connectors
An MCP server that provides congressional transcripts
MCP server unifying ERPs, CRMs, APIs and knowledge base for Claude, ChatGPT and Gemini.
- UnifAPIOAuthcom.unifapi
Hosted MCP server for live public-data APIs and Skills for AI agents.
Related MCP Servers
- AlicenseAqualityAmaintenanceMCP server connecting AI models to Swiss Federal Food Safety and Veterinary Office open data, enabling queries about food recalls, animal disease surveillance, food control results, and more.1139 PyPIMIT
- AlicenseAqualityAmaintenanceAn MCP server connecting AI models to the Swiss Federal Parliament via the Curia Vista OData API, enabling queries of motions, votes, members, sessions, and debate transcripts without authentication.742 PyPI2MIT
- AlicenseBqualityDmaintenanceMCP server giving AI agents access to US political finance data: campaign contributions, PAC spending, lobbying records, and voting behavior.61MIT
- AlicenseAqualityAmaintenanceAn MCP server for Swiss Federal Railways (SBB) open data, enabling AI models to query real-time disruptions, passenger frequencies, station details, and more without an API key.931 PyPI2MIT