MCP server for Swiss official gazettes (SHAB + cantonal) — procurement and notices, no person data
- ✓Open-source license (MIT)
- ✓Actively maintained (<30d)
- ✓Clear description
- ✓Topics declared
- ✓Documented (README)
claude mcp add amtsblatt-mcp -- uvx amtsblatt-mcp{
"mcpServers": {
"amtsblatt-mcp": {
"command": "uvx",
"args": ["amtsblatt-mcp"]
}
}
}MCP Servers overview
> 🇨🇭 **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)**
# 📰 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`](https://github.com/malkreide/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
```bash
pip install amtsblatt-mcp
# or, without installing:
uvx amtsblatt-mcp
```
From source:
```bash
git clone https://github.com/malkreide/amtsblatt-mcp
cd amtsblatt-mcp
pip install -e ".[dev]"
```
## Configuration
### Claude Desktop
```json
{
"mcpServers": {
"amtsblatt": {
"command": "uvx",
"args": ["amtsblatt-mcp"]
}
}
}
```
### Cloud deployment (streamable-http)
```bash
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` | _(on)_ | streamable-http runs with no session tracking at all — the native mode of spec `2026-07-28`, which has no protocol-level sessions. Removes session hijacking and session affinity as questions rather than answering them (`SEC-009`, `SCALE-002`). `0` / `false` / `no` restores sessions for handshake-era clients; any other value keeps the default. 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](https://www.structlog.org/). 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.
```json
{"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`](https://github.com/malkreide/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`](https://github.com/malkreide/register-mcp) |
## Data Protection & Scope
The Amtsblattportal systematically publishes personal data of **natural**
persons. Those publications are public — but making them *systematically
queryable What people ask about amtsblatt-mcp
What is malkreide/amtsblatt-mcp?
+
malkreide/amtsblatt-mcp is mcp servers for the Claude AI ecosystem. MCP server for Swiss official gazettes (SHAB + cantonal) — procurement and notices, no person data It has 0 GitHub stars and its last recorded update is dated 2026-09-27.
How do I install amtsblatt-mcp?
+
You can install amtsblatt-mcp by cloning the repository (https://github.com/malkreide/amtsblatt-mcp) or following the README instructions on GitHub. ClaudeWave also provides quick install blocks on this page.
Is malkreide/amtsblatt-mcp safe to use?
+
Our security agent has analyzed malkreide/amtsblatt-mcp and assigned a Trust Score of 95/100 (tier: Verified). See the full breakdown of passed checks and flags on this page.
Who maintains malkreide/amtsblatt-mcp?
+
malkreide/amtsblatt-mcp is maintained by malkreide. The last recorded GitHub activity is dated 2026-09-27, with 0 open issues.
Are there alternatives to amtsblatt-mcp?
+
Yes. On ClaudeWave you can browse similar mcp servers at /categories/mcp, sorted by popularity or recent activity.
Deploy amtsblatt-mcp to your cloud
Ship this repo to production in minutes. Each platform spins up its own environment with editable env vars.
Maintain this repo? Add a badge to your README
Drop the badge into your GitHub README to show it's tracked on ClaudeWave. Each badge links back to this page and reflects the live Trust Score.
[](https://claudewave.com/repo/malkreide-amtsblatt-mcp)<a href="https://claudewave.com/repo/malkreide-amtsblatt-mcp"><img src="https://claudewave.com/api/badge/malkreide-amtsblatt-mcp" alt="Featured on ClaudeWave: malkreide/amtsblatt-mcp" width="320" height="64" /></a>More MCP Servers
Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.
User-friendly AI Interface (Supports Ollama, OpenAI API, ...)
An open-source AI agent that brings the power of Gemini directly into your terminal.
Real-time global intelligence dashboard. AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface
🕷️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl! Don't be shy, join here: https://discord.gg/EMgGbDceNQ and follow here for daily tips and tricks: https://x.com/Scrapling_dev
The fastest path to AI-powered full stack observability, even for lean teams.