AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Amtsblatt Mcp

mcp-malkreide-amtsblatt-mcp · by malkreide

MCP server for Swiss official gazettes (SHAB + cantonal) — procurement and notices, no person data

No reviews yet
0 installs
20 views
0.0% view→install

Install

$ agentstack add mcp-malkreide-amtsblatt-mcp

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-malkreide-amtsblatt-mcp)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Amtsblatt Mcp? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

> 🇨🇭 Part of the Swiss Public Data MCP Portfolio

📰 amtsblatt-mcp

[](https://opensource.org/licenses/MIT) [](https://www.python.org/downloads/) [](https://modelcontextprotocol.io/) [](https://github.com/malkreide/amtsblatt-mcp)

> MCP server for amtsblattportal.ch — the Swiss official gazette portal > (SHAB + 27 cantonal gazettes). Public procurement and official notices, > person-data rubrics excluded by design.

[🇩🇪 Deutsche Version](README.de.md)

Overview

The Amtsblattportal publishes roughly 2.79 million official notices: public procurement, cantonal and communal announcements, enactments, spatial planning — and also bankruptcies, debt collection, inheritance calls and civil-status records naming natural persons.

This server exposes only the first group. Rubrics carrying systematic natural-person data are not queryable, and no tool accepts a person's name, birth date or address. That is a deliberate data-protection decision, explained in [Data Protection & Scope](#data-protection--scope).

Anchor demo query: "Which public tenders did canton Ticino publish this month?"

Demo

gazette_search_procurement(canton="TI", only_language=True, language="it")gazette_get_publication(id=…)

For procurement in any other canton — including Zürich, Bern and Basel-Stadt — use swiss-procurement-mcp; see [Boundary with swiss-procurement-mcp](#boundary-with-swiss-procurement-mcp).

Features

  • Fail-closed green allow-list — 49 released rubrics out of 152; everything

else is blocked by default, including rubrics the upstream adds later

  • Explanatory refusals — a blocked rubric returns why, never a silent

empty result and never a workaround hint

  • Procurement-aware — knows that only AR and TI still publish tenders here,

that BS wound down during 2024 and BL/VS are historical archives, that OB-ZG was never filled after the simap switch, and that ZH routes everything through simap.ch — so it explains instead of returning nothing. Activity is [measured, not read off the rubric label](docs/procurement-coverage.md)

  • Deadline arithmetic in Europe/Zurich, the legally relevant timezone
  • Honest multilingual counts — the portal publishes one record per language

with a different publication number each; identical editions are collapsed, translated ones are reported via language_mix rather than guessed at, and only_language=True gives a single-language view

  • Defensive XML parsing — the schema is per-sub-rubric; no rubric-specific

path is hard-coded, and entity-escaped HTML bodies are unescaped and stripped

  • Egress allow-list, retry with backoff, structured JSON logging
  • Markdown or JSON output with per-response attribution + provenance

Prerequisites

  • Python 3.11+
  • No API key. The read API of amtsblattportal.ch is freely accessible.

Installation

pip install amtsblatt-mcp
# or, without installing:
uvx amtsblatt-mcp

From source:

git clone https://github.com/malkreide/amtsblatt-mcp
cd amtsblatt-mcp
pip install -e ".[dev]"

Configuration

Claude Desktop

{
  "mcpServers": {
    "amtsblatt": {
      "command": "uvx",
      "args": ["amtsblatt-mcp"]
    }
  }
}

Cloud deployment (streamable-http)

export MCP_TRANSPORT=streamable-http
export MCP_API_KEY="$(openssl rand -hex 32)"   # mandatory — fails loud if unset
export PORT=8000
amtsblatt-mcp

The endpoint is /mcp.

> Migrating from SSE. Until 0.18.0 this server spoke SSE only, on > /sse + /messages. MCP spec 2026-07-28 reclassifies HTTP+SSE as > deprecated with a twelve-month removal window and removes protocol-level > sessions, so streamable-http is now the default. MCP_TRANSPORT=sse still > works and still carries the full bearer-auth, rate-limit and CORS stack — it > logs a warning at startup naming the deadline. Update the client URL when > you switch: the path change is the part that breaks silently.

| Variable | Default | Purpose | |---|---|---| | MCP_TRANSPORT | stdio | stdio, streamable-http (alias http), or the deprecated sse | | MCP_HOST | 127.0.0.1 | HTTP bind address. Defaults to loopback; set 0.0.0.0 to expose on all interfaces (the Docker image does this deliberately). | | MCP_STATELESS | (unset) | 1 runs streamable-http with no session tracking at all. Removes session hijacking and session affinity as questions rather than answering them (SEC-009, SCALE-002). Opt-in, because a stateless server cannot resume an interrupted stream or push server-initiated notifications. Ignored on sse, which has no stateless mode. | | MCP_CORS_ORIGINS | (unset) | Comma-separated origins allowed to call the endpoint from a browser. Unset means no cross-origin browser access at all — stdio and non-browser clients are unaffected. Mcp-Session-Id is exposed and accepted for the listed origins, so a browser client can hold a session. * is honoured but logs a warning and disables credentials, because browsers reject a wildcard origin together with credentials. | | MCP_API_KEY | — | Bearer token; required on every HTTP transport | | MCP_RATE_LIMIT / MCP_RATE_WINDOW | 60 / 60 | Sliding-window rate limit | | RUBRICS_TTL | 86400 | Taxonomy cache TTL (seconds) | | LOG_LEVEL | INFO | DEBUG \| INFO \| WARNING \| ERROR. Structured JSON, one object per line, always on stderr — stdout carries the MCP protocol on a stdio transport. |

Logging

Built on structlog. Every event emitted during a tool call carries that call's correlation_id, bound via contextvars — so a retry or an egress denial logged deep in the HTTP path can be joined to the request that caused it, without threading context through every signature.

| Level | Emitted when | |---|---| | DEBUG | a tool call was entered (tool_call_started) — tells you whether a hung call ever started | | INFO | a tool call finished cleanly, with latency | | WARNING | upstream retry, egress denied, auth failure, rate limit | | ERROR | a tool call raised |

Records carry the exception type only — never its message and never an upstream response body.

{"event":"tool_call_started","tool":"gazette_search_procurement","correlation_id":"23221af26ae640c7","level":"debug","timestamp":"2026-07-27T22:20:07.494276Z"}
{"status":"ok","latency_ms":312,"event":"tool_call","tool":"gazette_search_procurement","correlation_id":"23221af26ae640c7","level":"info","timestamp":"2026-07-27T22:20:07.806Z"}

Ship these to your SIEM and alert on auth_failed, rate_limited, egress_denied, green_gate_violation and blocked_publication_requested — the last two mean something tried to reach a rubric this server does not serve.

Available Tools

| Tool | Signature | Notes | |---|---|---| | gazette_search_publications | (keyword?, rubric?, sub_rubric?, canton?, date_start?, date_end?, limit=20, page=0, language='de', only_language=False) | Green rubrics enforced. Without rubric, all green rubrics are injected — a keyword-only query can never reach a blocked one. | | gazette_search_detailed | same filters + top_n=3 | Aggregated. Search and full text for the top top_n hits in one call, fetched in parallel. Same green gate on every expanded document; blocked ones are withheld and counted, never rendered. | | gazette_search_procurement | (keyword?, canton?, date_start?, date_end?, include_inactive=False, limit=20, page=0, language='de', only_language=False) | OB-* rubrics plus the gazette-native sub-rubrics AR-VS40, AR-OW40, BA-SH40. A canton with neither gets a simap.ch explainer and no HTTP call. No CPV — the source has none. | | gazette_get_publication | (id, response_format='markdown') | Full official text from XML. Re-checks the rubric after fetching; content from a blocked rubric is discarded. | | gazette_list_rubrics | (language='de', rubric_class='green', response_format='markdown') | rubric_class='all' shows the full taxonomy with traffic-light classes and reasons — listed ≠ queryable. | | gazette_source_status | (response_format='markdown') | Reachability, latency, cache age, scope metrics. |

All tools are readOnlyHint=True.

Example use cases

| Question | Tool chain | |---|---| | Tenders in Ticino this quarter | gazette_search_procurement(canton="TI", only_language=True, language="it") | | Procurement simap.ch does not have | gazette_search_procurement(canton="VS") — 150 Valais awards, none on simap | | Tenders in any other canton | → use swiss-procurement-mcp | | What is even queryable here? | gazette_list_rubrics() | | Why can't I search bankruptcies? | gazette_list_rubrics(rubric_class="all") | | Zoning changes in Zurich | gazette_search_publications(rubric="RP-ZH") | | Full text of a notice | gazette_get_publication(id="fbf0ff9e-…") | | Everything published about one company | → use register-mcp |

Data Protection & Scope

The Amtsblattportal systematically publishes personal data of natural persons. Those publications are public — but making them systematically queryable by name through an AI agent is a repurposing the publication never intended, and a profiling instrument under the revised Swiss FADP (revDSG).

Four rules follow, and they are enforced in code, not in documentation:

  1. Allow-list, never block-list. Not explicitly green ⇒ not queryable.

New upstream rubrics are closed by default.

  1. No person-based search entry in any tool signature.
  2. No persistence. Publications have statutory deletion periods; a cache

outliving them would actively undermine them. Only the taxonomy is cached.

  1. Blocked ⇒ explained. Never a silent empty result, never a hint at

circumvention.

What is excluded

🔴 Konkurse (KK), Schuldbetreibungen (SB), Schuldenrufe (LS, SR), Nachlass (NA), Erbschaft/Testament/Ableben (ES, TE-*, VA-*), Familie & Zivilstand (FZ-*, BV-*, BU-*), gerichtliche Vorladungen (UV, GB-*, GE-*, SJ-BE), Baugesuche (BP-*), Grundbuch (GR-*), Meldungskatalog GR (AA-GR).

🟡 Deferred: Steuerwesen, Anzeigen, Bewilligungen, Bildungs- und Kirchenwesen and the general catch-all rubrics.

The full audit trail — including three documented extensions to the source specification — is in [docs/rubric-classification.md](docs/rubric-classification.md).

How much each decision covers is measured, not estimated. [docs/coverage-matrix.md](docs/coverage-matrix.md) enumerates the source's own rubric axis and marks this server's reach into it: 84.2 % of 2 804 063 publications are reachable, 12.6 % blocked by decision, 3.3 % still unclassified. The insolvency group alone is 321 704 publications — present in the source, out of reach here on purpose. Without that figure, "out of scope" and "not in the source" read the same in a review, and this repository made exactly that mistake once (see ARCH-003 in [SECURITY.md](SECURITY.md)).

The boundary with register-mcp

For publications about a specific company, use register-mcp. It keeps full rubric access — including a firm's own bankruptcy — but only ever keyed on a company UID. A firm's insolvency is corporate data, not natural-person profiling, and UID scoping makes name-based enumeration impossible.

amtsblatt-mcp has the opposite shape: broad search, narrow rubrics. It does not expose the upstream uids parameter at all.

Boundary with swiss-procurement-mcp

simap.ch is the primary source for Swiss public procurement — all 26 cantons plus the Confederation, with CPV and BKP codes, awards and publication history. Use swiss-procurement-mcp for procurement questions.

amtsblattportal.ch is the primary source for official notices — commercial register, spatial planning, enactments, cantonal and communal announcements. That is what this server is for; procurement is 6 of its 49 released rubrics.

Procurement here is largely a second publication of the same tenders, and that is now measured rather than assumed. A publication's XML carries ` when it originates on simap.ch, which joins the two corpora exactly. Over the full 2026 OB-TI corpus, **503 of 546 records (92.1%) carry one**; three of the six OB-* rubrics say as much in their own labels (OB-BL` — "über Simap importiert (I N A K T I V)").

The exception is small and sharply bounded: AR-VS40 (Valais, 150 awards), AR-OW40 (Obwalden, 7), BA-SH40 (Schaffhausen, 2) and the Ticino sub-rubric OB-TI65 ("Avvisi di gara non CIAP") carry no simap reference at all. That is the one part of this portal's procurement coverage swiss-procurement-mcp cannot reach, and gazette_search_procurement serves it for cantons VS, OW and SH even though they have no active OB-* rubric. Numbers and method in [docs/simap-overlap.md](docs/simap-overlap.md).

The two servers stay separate on purpose: different sources, different reuse terms, and a fail-closed rubric gate that only means something while it covers every tool in the server. See [docs/procurement-coverage.md](docs/procurement-coverage.md) for the numbers.

Maturity & phase

Phase 1 — read-only. All six tools are read-only; there is no write path and none is planned. See [ROADMAP.md](ROADMAP.md) for the phase-specific backlog, what is deliberately not planned, and what a phase transition would require.

The scope restriction that matters most here is not the phase but the green allow-list — rubrics carrying systematic natural-person data are not queryable, enforced in code and re-checked after every fetch. That does not change with phase. See [Data Protection & Scope](#data-protection--scope).

SDK and dependency updates arrive as [Dependabot](.github/dependabot.yml) PRs, so a breaking protocol or SDK change is reviewed deliberately rather than drifting in silently.


Architecture

   Claude / MCP client
            │
      amtsblatt-mcp
            │
   ┌────────┴────────┐
   │  green gate     │  ← rubrics.py: fail-closed allow-list
   └────────┬────────┘     (checked at the tool AND at the query builder)
            │
   ┌────────┴────────┐
   │  param allow-   │  ← Silent Ignore guard
   │  list + quirks  │  ← Silent Empty guard (taxonomy validation)
   └────────┬────────┘  ← plausibility guard (corpus-size check)
            │
   ┌────────┴────────┐
   │ egress allow-   │
   │ list (httpx)    │
   └────────┬────────┘
            │
  amtsblattportal.ch/api/v1
   /publications · /publications/{id}/xml · /rubrics · /tenants

Architecture A (live-API-only). The endpoints answer stably without authentication, so no bulk dump is maintained.

Verified upstream quirks (live-checked 2026-07-20)

| Quirk | Behaviour | Defence | |---|---|---| | Silent Ignore | An unknown parameter name returns HTTP 200 and the full corpus. canton=ZH (singular typo) silently drops the filter. | Query params built exclusively from ALLOWED_GAZETTE_PARAMS; plausibility guard rejects results > 2 000 000. | | Silent Empty | An unknown rubric value returns HTTP 200 with total: 0 — indistinguishable from a real no-hit. | Every code validated against the taxonomy before the call. | | Metadata only | The list endpoint and GET /publications/{id} both return content: null. | Full text only via /publications/{id}/xml. | | Sorting ignored | pageRequest.sortOrders is accepted with 200 but has no effect; sortOrders comes back []. | Sorted client-side. | | Missing publicationStates | Returns 401, not 400 — it does not mean cre

Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.