{
  "markdown": "> 🇨🇭 **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)**\n\n# register-mcp\n\n![Version](https://img.shields.io/badge/version-0.6.1-blue)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)\n[![MCP](https://img.shields.io/badge/MCP-Model%20Context%20Protocol-purple)](https://modelcontextprotocol.io/)\n[![No Auth Required](https://img.shields.io/badge/auth-none%20required-brightgreen)](https://github.com/malkreide/register-mcp)\n![CI](https://github.com/malkreide/register-mcp/actions/workflows/ci.yml/badge.svg)\n\n> MCP Server for the Swiss Federal Commercial Register (Zefix/Handelsregister), with a **company-UID join** to the official gazettes (SHAB + cantonal Amtsblätter)\n\n[🇩🇪 Deutsche Version](README.de.md)\n\n---\n\n## Overview\n\n`register-mcp` provides AI-native access to **two** Swiss federal data sources, joined on the UID, all without authentication:\n\n| Source | Data | API |\n|--------|------|-----|\n| **Zefix (Handelsregister)** | Swiss companies, legal forms, registered-office data | ZefixREST v1 |\n| **Amtsblattportal** | Everything published **about a specific company** (by its UID): HR mutations, calls to creditors, bankruptcy | amtsblattportal.ch v1 |\n\nThe two sources share one key — the **UID**. The value is in the join: **Zefix tells you whether a company exists; the gazette tells you what has been published about it.**\n\nThe gazette access here is deliberately **company-scoped only** — keyed on a company UID or a specific publication id. There is **no free-text / person-name gazette search** in this server; that would be a profiling tool over the gazette's person-data rubrics (bankruptcy, debt-collection, inheritance). Broad Amtsblatt platform search (procurement, cantonal notices, full-text) is proposed as a separate `amtsblatt-mcp` — see [`docs/amtsblatt-mcp-proposal.md`](docs/amtsblatt-mcp-proposal.md) and the **Data Protection & Scope** section below.\n\nDesigned for Swiss public administration use cases: vendor verification, contract partner due diligence, and supplier onboarding — all via natural language queries.\n\n**Anchor demo query:** *\"Before we sign a framework agreement with Lehrmittelverlag Zürich AG: is the company active in the commercial register, what is its UID and stated purpose — and, via that UID, what has the official gazette published about it (HR mutations, calls to creditors, any bankruptcy)?\"*\n\nThat single question walks the whole tool chain across both sources:\n\n```\nzefix_search_company  →  zefix_verify_company  →  gazette_company_publications(uid=…)  →  gazette_get_publication(id=…)\n```\n\n---\n\n## Features\n\n- 🏛️ **9 tools** across two sources — company search & verification (Zefix) + the company-scoped gazette join (SHAB/cantonal)\n- 🔗 **`gazette_company_publications`** — the UID join: everything published about a company\n- 🛡️ **Data-protection-safe by construction** — the only gazette entry points are UID- or id-scoped; no person-name search entry exists (see *Data Protection & Scope*)\n- 🔍 **`zefix_verify_company`** — quick active/dissolved status check\n- 🌐 **Bilingual output** (Markdown / JSON) with per-source attribution + `provenance`\n- 🔓 **No API key required** — open data from zefix.admin.ch and amtsblattportal.ch\n- ☁️ **Dual transport** — stdio (Claude Desktop) + SSE (cloud)\n\n---\n\n## Prerequisites\n\n- Python 3.11+\n- [uv](https://github.com/astral-sh/uv) (recommended) or pip\n\n---\n\n## Installation\n\n```bash\n# Clone the repository\ngit clone https://github.com/malkreide/register-mcp.git\ncd register-mcp\n\n# Install\npip install -e .\n# or with uv:\nuv pip install -e .\n```\n\nOr with `uvx` (no permanent installation):\n\n```bash\nuvx register-mcp\n```\n\n---\n\n## Quickstart\n\n```bash\n# stdio (for Claude Desktop)\npython -m register_mcp.server\n\n# SSE (cloud deployment) — MCP_API_KEY is REQUIRED\nMCP_API_KEY=$(openssl rand -hex 32) MCP_TRANSPORT=sse PORT=8000 \\\n  python -m register_mcp.server\n```\n\n### SSE / Cloud Deployment\n\nWhen running with `MCP_TRANSPORT=sse`, the server enforces:\n\n- **Bearer-token auth** — set `MCP_API_KEY` to a secret string. Clients must send\n  `Authorization: Bearer <key>` on every request. Missing or wrong → HTTP 401.\n  The server refuses to start without `MCP_API_KEY` set.\n- **Rate limiting** — sliding window per bearer-token hash. Defaults: 60 req / 60 s.\n  Tunable via `MCP_RATE_LIMIT` and `MCP_RATE_WINDOW`. Exceeding the limit returns\n  HTTP 429 with `Retry-After`.\n- **Structured JSON logging** — every tool call emits one line to stderr with\n  `tool`, `status`, `latency_ms`. Auth failures and rate-limit events are logged\n  at WARNING level. Configure verbosity with `LOG_LEVEL` (default `INFO`).\n- **Reference-data cache** — Zefix legal-forms are cached for 24h\n  (`LEGAL_FORMS_TTL` seconds) to avoid an extra upstream call per tool invocation.\n- **Egress allow-list** — outbound HTTP is restricted to `www.zefix.admin.ch`\n  and `amtsblattportal.ch` via an `httpx` request hook that also fires on\n  redirects. A `Location` header pointing elsewhere raises `EgressDenied` and is\n  never followed. Override with `MCP_ALLOWED_HOSTS=host1,host2` (comma-separated,\n  lower-case).\n\n  > ⚠️ **Upgrade note (0.2.x → 0.3.0):** `amtsblattportal.ch` was added to the\n  > **default** allow-list when the gazette tools shipped. If your deployment\n  > **pins** `MCP_ALLOWED_HOSTS`, that value overrides the default entirely —\n  > add `amtsblattportal.ch` to it, or every `gazette_*` call will raise\n  > `EgressDenied`.\n- **Optional OpenTelemetry tracing** — install with `pip install register-mcp[otel]`\n  and set `OTEL_EXPORTER_OTLP_ENDPOINT` (e.g. `http://otel-collector:4318/v1/traces`).\n  Without the extra or without the env var the server stays silent — no hard\n  dependency on the OTel SDK.\n\nFor multi-instance deployments, place a real gateway (Cloudflare, Railway internal\nnetworking, an API-Gateway with Redis-backed rate limiting) in front of the\nin-memory limiter, which is per-process by design.\n\n### Container deployment\n\nA minimal multi-stage `Dockerfile` ships with the repo. The image runs as a\nnon-root `mcp` user; dependencies are resolved from `uv.lock` (`uv sync\n--frozen`), so the build is reproducible.\n\n```bash\ndocker build -t register-mcp:local .\n\ndocker run --rm -p 8000:8000 \\\n  -e MCP_TRANSPORT=sse \\\n  -e MCP_API_KEY=\"$(openssl rand -hex 32)\" \\\n  register-mcp:local\n```\n\nFor local iteration there is a `compose.yaml` with `read_only`, `cap_drop: ALL`\nand `no-new-privileges`:\n\n```bash\nMCP_API_KEY=$(openssl rand -hex 32) docker compose up --build\n```\n\nSee [SECURITY.md](SECURITY.md) for hardening notes (egress restriction, key\nrotation, SIEM forwarding).\n\nTry it immediately in Claude Desktop:\n\n> *\"Is Lehrmittelverlag Zürich AG active in the commercial register?\"*\n> *\"Look up the company with UID CHE-108.954.978\"*\n> *\"List all Swiss legal forms\"*\n\n---\n\n## Configuration\n\n### Claude Desktop\n\nEdit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\\Claude\\claude_desktop_config.json` (Windows):\n\n```json\n{\n  \"mcpServers\": {\n    \"register\": {\n      \"command\": \"python\",\n      \"args\": [\"-m\", \"register_mcp.server\"]\n    }\n  }\n}\n```\n\nOr with `uvx`:\n\n```json\n{\n  \"mcpServers\": {\n    \"register\": {\n      \"command\": \"uvx\",\n      \"args\": [\"register-mcp\"]\n    }\n  }\n}\n```\n\n**Config file locations:**\n- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`\n- Windows: `%APPDATA%\\Claude\\claude_desktop_config.json`\n\n### Cloud Deployment (SSE for browser access)\n\nFor use via **claude.ai in the browser** (e.g. on managed workstations without local software):\n\n**Render.com (recommended):**\n1. Push/fork the repository to GitHub\n2. On [render.com](https://render.com): New Web Service → connect GitHub repo\n3. Set start command: `python -m register_mcp.server --http --port 8000`\n4. In claude.ai under Settings → MCP Servers, add: `https://your-app.onrender.com/sse`\n\n> 💡 *\"stdio for the developer laptop, SSE for the browser.\"*\n\n---\n\n## Available Tools\n\n**Zefix — commercial register (6):**\n\n| Tool | Description |\n|------|-------------|\n| `zefix_search_companies` | Search companies by name, canton, legal form |\n| `zefix_get_company` | Full company profile by internal EHRAID |\n| `zefix_get_company_by_uid` | Company lookup by UID (CHE-xxx.xxx.xxx) |\n| `zefix_verify_company` | Quick active/dissolved status check |\n| `zefix_list_legal_forms` | All Swiss legal forms with IDs |\n| `zefix_list_municipalities` | Swiss municipalities with BFS IDs |\n\n**Amtsblattportal — the company-scoped gazette join (3):**\n\n| Tool | Description |\n|------|-------------|\n| `gazette_company_publications` | **The UID join.** All gazette publications for a company **UID**, newest first, optional (validated) rubric/time filters |\n| `gazette_get_publication` | Single publication incl. XML full text, defensively parsed (by publication id) |\n| `gazette_source_status` | Reachability of both sources + cache ages (rubrics, legal forms) |\n\nThe prefix is `gazette_`, not `shab_`, because the source covers SHAB **and** the cantonal gazettes. Every entry point is UID- or id-scoped — see **Data Protection & Scope**. Broad, non-company gazette search (procurement, cantonal full-text) is scoped to the separate [`amtsblatt-mcp`](docs/amtsblatt-mcp-proposal.md).\n\n### Example Use Cases\n\n| Query | Tool |\n|-------|------|\n| *\"Is Lehrmittelverlag Zürich AG active?\"* | `zefix_verify_company` |\n| *\"Look up CHE-108.954.978\"* | `zefix_get_company_by_uid` |\n| *\"Find companies named Migros in canton ZH\"* | `zefix_search_companies` |\n| *\"What has been published about CHE-116.115.052?\"* | `gazette_company_publications` |\n| *\"Show the full official text of that HR deletion notice\"* | `gazette_get_publication` |\n| *\"Are both data sources reachable right now?\"* | `gazette_source_status` |\n\n---\n\n## Architecture\n\n```\n                                                          ┌──────────────────────────────┐\n                                                    ┌────▶│  Zefix (Handelsregister)     │\n                                                    │     │  www.zefix.admin.ch          │\n┌─────────────────┐     ┌──────────────────────────┴─┐   │  ZefixREST/api/v1            │\n│   Claude / AI   │────▶│       register-mcp           │   └──────────────────────────────┘\n│   (MCP Host)    │◀────│       (MCP Server)           │   ┌──────────────────────────────┐\n└─────────────────┘     │  9 Tools (zefix_ + gazette_) ├──▶│  Amtsblattportal             │\n                        │  Stdio | SSE                 │   │  amtsblattportal.ch/api/v1   │\n                        │  Egress allow-list           │   │  SHAB + cantonal gazettes    │\n                        │  No authentication required  │   └──────────────────────────────┘\n                        └──────────────────────────────┘\n                              join key: UID (CHE-XXX.XXX.XXX)\n```\n\n### Data Source Characteristics\n\n| Source | Protocol | Coverage | Auth |\n|--------|----------|----------|------|\n| Zefix | REST/JSON | Swiss companies, legal forms, registered offices | None |\n| Amtsblattportal | REST/JSON (list) + XML (full text) | SHAB + cantonal gazettes, 2.79M publications | None |\n| ZefixPublicREST (planned) | REST/JSON | Signatories, capital, full history | Basic Auth (free) |\n| UID Register (planned) | SOAP | MwSt, NOGA codes, cross-validation | Public (20 req/min) |\n\n### The UID join — Zefix ↔ Amtsblatt\n\nThe two sources share exactly one key: the **UID** (`CHE-XXX.XXX.XXX`). That is\nwhat turns them from two data sets into one workflow.\n\n```\nzefix_get_company_by_uid(uid)        # Zefix: does the company exist? status, purpose, legal form\n        │  UID\n        ▼\ngazette_company_publications(uid)    # Gazette: everything published about it (HR, KK, SB, LS, …)\n        │  publication id\n        ▼\ngazette_get_publication(id)          # Full official text from the per-rubric XML\n```\n\nTwo properties of the source shape this path (both verified in\n[`docs/probe-shab.md`](docs/probe-shab.md)):\n\n- The **bulk list carries no company UID** (`meta.uid` is `null`). The company\n  UID lives only in the **single-publication fetch** — `meta.uid` in the single\n  JSON, or `<uid>` in the XML (which also carries the full text). So the join\n  runs *list → per-hit single fetch → match against the Zefix UID*.\n- `gazette_company_publications` filters the corpus by `uids=<UID>` directly, so\n  in practice you get the company's publications in one call without walking\n  every record.\n\n### Procurement lives in the separate `amtsblatt-mcp`\n\nPublic procurement (Submissionen) is **not** a federal SHAB rubric and is **not**\ncovered by this server. It exists only as a **cantonal** `OB-<canton>` rubric,\nonly a few cantons publish it in this portal, and most — including **Zürich** —\nroute tenders through **[simap.ch](https://www.simap.ch/)**, a separate platform.\nProcurement, cantonal notices, and broad full-text search are scoped to the\nproposed [`amtsblatt-mcp`](docs/amtsblatt-mcp-proposal.md) server, which applies\na fail-closed **green-rubric allow-list**. See that proposal for the full\n`OB-*` coverage map and the rubric traffic-light table.\n\n> **`SB` ≠ Submissionen.** `SB` is *Schuldbetreibungen* (debt collection), a\n> person-data-heavy rubric this server never exposes as a search entry.\n\n---\n\n## Data Protection & Scope\n\nThis section is **not** a footnote — it is the reason the server is shaped the\nway it is.\n\nThe Amtsblattportal systematically publishes rubrics containing personal data of\n**natural** persons: bankruptcies (`KK`), debt-collection (`SB`), calls to\ncreditors (`LS`/`SR`), inheritance/estate calls (`ES`, `TE-*`), and building\napplications with owner names. Those publications are public — but making them\n*systematically queryable by name* through an AI agent is a repurposing the\npublication never intended, and under the revised Swiss Federal Act on Data\nProtection (**revDSG**) a \"show me every debt-collection entry for person X\" tool\nis a profiling instrument. Deliberate design choices follow:\n\n- **No person-based search entry.** No tool takes a natural person's name, birth\n  date or address. The only gazette entry points are keyed on a **company UID**\n  (`gazette_company_publications`) or an opaque **publication id**\n  (`gazette_get_publication`). A firm's own bankruptcy *is* returned via its UID —\n  that is corporate data about a legal person, not name-based profiling.\n- **No free-text gazette search here.** `keyword` and `cantons` are not even on\n  the internal query-parameter allow-list, so no future code change can smuggle a\n  corpus-wide keyword search in. Broad search lives in `amtsblatt-mcp` behind a\n  fail-closed green allow-list (procurement, HR, official notices only).\n- **No persistence of publication content.** The server is a pass-through; only\n  the rubric taxonomy and Zefix legal-forms list are cached in memory (24 h).\n  Official publications carry statutory deletion periods — a store that outlived\n  them would actively undermine those periods.\n- **Fail closed.** Rubric codes are validated against the live taxonomy before\n  any call; an unknown code is refused, not silently widened.\n\nThe broad-platform counterpart, its green/yellow/red rubric classification and\nits fail-closed design are specified in\n[`docs/amtsblatt-mcp-proposal.md`](docs/amtsblatt-mcp-proposal.md).\n\n---\n\n## Architecture decision\n\n**ARCH A — live-API-only**, consistent with the existing Zefix integration\n(decided 2026-07-18).\n\nThe Amtsblattportal is queried live on every call. All endpoints respond in\n0.2–2.0 s, and the use case — targeted company and topic research — does not\nneed a local bulk copy. A bulk dump would mean mirroring 2.79M records, with an\nongoing sync burden and staleness risk, for no benefit to the join-on-UID\nworkflow. The taxonomy (`/rubrics`) and the Zefix legal-forms list are the only\ndata cached, each for 24h in memory, because they change at most a few times a\nyear and every filtered call needs them.\n\n---\n\n## Phased Implementation\n\n| Phase | API | Auth | Status |\n|-------|-----|------|--------|\n| **Phase 1** | `ZefixREST/api/v1` | None | **Current** |\n| **Phase 2** | `ZefixPublicREST/api/v1` | Basic Auth (free, email zefix@bj.admin.ch) | Planned |\n| **Phase 3** | UID-Register SOAP | Public (20 req/min) | Planned |\n\nPhase 2 will add: signatory details, share capital, full historical entries.\nPhase 3 will add: MwSt status, NOGA industry codes, cross-register validation.\n\n---\n\n## Project Structure\n\n```\nregister-mcp/\n├── src/register_mcp/\n│   ├── __init__.py              # Package\n│   └── server.py                # 9 tools (Zefix + company-scoped gazette join)\n├── tests/\n│   ├── test_server.py           # Zefix unit + integration tests (mocked HTTP)\n│   ├── test_gazette.py          # Gazette tools + the three quirks (mocked HTTP)\n│   └── test_egress.py           # Egress allow-list\n├── docs/\n│   ├── probe-shab.md            # Phase-1 live probe of amtsblattportal.ch\n│   ├── amtsblatt-mcp-proposal.md# Spec for the separate broad-platform server\n│   └── demo/                    # vhs demo script + standalone CLI demo\n├── .github/workflows/ci.yml     # GitHub Actions (Python 3.11/3.12/3.13)\n├── pyproject.toml\n├── CHANGELOG.md\n├── CONTRIBUTING.md\n├── LICENSE\n├── README.md                    # This file (English)\n└── README.de.md                 # German version\n```\n\n---\n\n## Known Limitations\n\n- Search by canton without a name filter may return API errors (Zefix API limitation)\n- Phase 1 Zefix API may be rate-limited under heavy load; retry after a short delay\n- ZefixPublicREST (new API) requires registration: email zefix@bj.admin.ch\n\n### Amtsblattportal — verified behaviour (live-checked 2026-07-18)\n\n| Call | HTTP | Status | Records | Note |\n|---|---|---|---|---|\n| `/publications?publicationStates=PUBLISHED` | 200 | OK | 2,790,323 | baseline (full corpus) — never queried unfiltered |\n| `?uids=CHE-116.115.052` | 200 | **OK** | 4 | **the join — core (and only) gazette entry** |\n| `?uids=…&rubrics=HR` | 200 | OK | – | optional, validated rubric narrowing on the join |\n| `/publications/{id}/xml` | 200 | OK | – | full text, rubric-specific schema |\n| `/rubrics` | 200 | OK | – | taxonomy (for code validation) |\n| `?rubrics=ZZZZ` (invalid) | **200** | **Silent Empty** | 0, `total: null` | Quirk 2 |\n| `?uid=…` (wrong param name) | **200** | **Silent Ignore** | **2,790,323** | Quirk 1 |\n\n> Free-text (`keyword`) and broad `cantons` search are **not** performed by this\n> server — those probe results live in [`docs/probe-shab.md`](docs/probe-shab.md)\n> and inform the separate `amtsblatt-mcp`.\n\n### Zefix — verified behaviour (live-checked 2026-08-15)\n\nFound by the weekly live suite, not by the unit tests — which stayed green\nthroughout.\n\n| Call to `firm/search.json` | HTTP | Result |\n|---|---|---|\n| `{\"name\": \"Migros\", …}` | 200 | 35 hits |\n| a name with no hits | **404** | NORESULT envelope — *not* an empty 200 |\n| `{\"uid\": \"109741634\", …}` | **400** | Bad Request — there is no `uid` field |\n| `{\"name\": \"CHE-999.999.999\", \"searchType\": \"CONTAINS\"}` | 200 | **«CHEMAM - 999»**, UID CHE-113.593.998 |\n| a dissolved firm without `activeOnly: false` | 404 | NORESULT — as if it never existed |\n\n**Three shapes, one shipped bug each:**\n\n- **No hits answer with HTTP 404**, carrying the NORESULT envelope. Every call\n  therefore goes through `_zefix_post_search`; a raw `raise_for_status()` makes\n  the friendly branch unreachable. That is how `zefix_verify_company` shipped\n  answering *\"Eintrag nicht gefunden. Bitte EHRAID oder UID prüfen\"* to a **name**\n  search, where neither an EHRAID nor a UID was in play. A fixture that puts the\n  NORESULT body into a 200 makes exactly that dead branch look green.\n- **A hit list is not an answer.** UID lookup searches the *name* field with\n  `searchType: CONTAINS`, so `CHE-999.999.999` returns a real company under a UID\n  that is not its own. Defence: exact digit match or nothing — no `firms[0]`\n  fallback. The former fallback produced a complete, plausible, formatted record\n  about somebody else, indistinguishable from a correct answer.\n- **Without `activeOnly: false`, \"dissolved\" looks like \"never existed\".**\n  Zefix returns only active entries by default; `zefix_verify_company` sets the\n  flag deliberately. A firm with no UID comes back as a **string of blanks**\n  (`uid: \"            \"`, `uidFormatted: null`), not as `null`.\n\n**Three quirks are defended in code** (details in the [CHANGELOG](CHANGELOG.md)\nunder *Known findings*):\n\n- **Quirk 1 — Silent Ignore (critical).** Unknown query parameters are dropped\n  silently and return the full 2.79M corpus with HTTP 200. Defence: query\n  strings are built exclusively from an `ALLOWED_GAZETTE_PARAMS` allow-list, and\n  every filtered response is plausibility-checked — a `total` above 2,000,000 is\n  rejected as *\"filter ignored by upstream — result not trustworthy\"*.\n- **Quirk 2 — Silent Empty.** An invalid rubric code returns HTTP 200 with an\n  empty result. Defence: the `/rubrics` taxonomy is cached 24h and every code is\n  validated **before** any call, failing with the five closest valid codes.\n- **Quirk 3 — Two-step fetch.** The JSON list carries only `meta`; the content\n  lives only in the per-rubric namespaced XML. Defence: namespace-agnostic\n  defensive parsing (`meta` + `publicationText` mandatory, HR `company` when\n  present, everything else in `additional_fields`).\n\n---\n\n## Safety & Limits\n\n### Rate Limits\n\n| API | Limit | Notes |\n|-----|-------|-------|\n| ZefixREST (Phase 1) | Not officially documented | Throttling possible under heavy load — retry after 1–2 s |\n| ZefixPublicREST (Phase 2) | Not officially documented | Requires prior registration (free) |\n| UID-Register SOAP (Phase 3) | **20 req/min** | Hard limit, publicly documented |\n\n### Data Privacy\n\n- **Read-only access** — all tools carry `readOnlyHint: True`; the server performs no write, delete, or mutation operations against any API\n- **No person-based search entry** — no tool accepts a natural person's name, birth date or address; gazette access is UID- or publication-id-scoped only (see **Data Protection & Scope**). This is a deliberate revDSG-driven design choice, not an accident of the API\n- **No persistence of publication content** — the server is a stateless pass-through; only the rubric taxonomy and Zefix legal-forms list are cached in memory (24 h), never publication bodies, so statutory deletion periods are respected\n- **Public register data only** — the Zefix Handelsregister is a public federal register (HRegV); gazette data returned is likewise legally public, retrieved per company UID\n- **No personal tracking** — the server does not transmit user identity, query history, or session data to the upstream sources\n\n### Terms of Service & Data Sources\n\n- **Zefix API ToS:** Usage of the Zefix REST API is governed by the [zefix.admin.ch terms of use](https://www.zefix.admin.ch). The data is published under the [Open Government Data (OGD) Switzerland](https://opendata.swiss/) principles.\n- **SHAB:** Swiss Official Gazette of Commerce — published by the Federal Chancellery (BK). Public by law.\n- **Institutional use:** This server is designed for read-only queries in public administration workflows. Not suitable for mass harvesting or automated surveillance use cases.\n\n### Security\n\n- No credentials are stored or transmitted (Phase 1)\n- Phase 2 credentials (`ZEFIX_USER`, `ZEFIX_PASSWORD`) are passed via environment variables only — never hardcoded\n- All HTTP calls use HTTPS exclusively\n- Tool inputs are validated via Pydantic v2 before any API call is made\n\n---\n\n## Demo\n\n![register-mcp demo](assets/demo.png)\n\n> 📽️ *Terminal GIF coming soon — see [`docs/demo/`](docs/demo/) to generate it locally with [vhs](https://github.com/charmbracelet/vhs)*\n\n**Example interaction:**\n\n```\nUser:  \"Is Lehrmittelverlag Zürich AG active in the commercial register?\"\n\n→ Tool: zefix_verify_company(name=\"Lehrmittelverlag Zürich AG\")\n\nClaude: ✅ Lehrmittelverlag Zürich AG is ACTIVE in the Handelsregister.\n        UID: CHE-404.020.972 | Canton: ZH | Legal form: AG\n        Last SHAB mutation: 2023-07-27\n```\n\n[→ More use cases by audience →](EXAMPLES.md)\n\nTo generate the demo GIF locally:\n\n```bash\n# Install vhs (macOS/Linux)\nbrew install vhs        # macOS\n# or: go install github.com/charmbracelet/vhs@latest\n\n# Generate\nvhs docs/demo/demo.tape\n# → outputs docs/demo/demo.gif\n```\n\n---\n\n## MCP Protocol Version\n\nThis server speaks **two protocol eras** over the same endpoint. The client's\nfirst request on a connection decides which one applies; a later claim from the\nother era is refused.\n\n| Era | Revision | Who reaches it |\n|---|---|---|\n| `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. |\n| Per-request envelope | **`2026-07-28`** | A request carrying the `2026-07-28` `_meta` envelope opens a modern connection. |\n\nBoth revisions are pinned in\n[`tests/test_protocol_version.py`](tests/test_protocol_version.py) and asserted\nagainst the installed SDK, so a Dependabot bump of `mcp` cannot move either one\nsilently. This server builds no ASGI app to send an `initialize` through, so\nthe gate asserts the SDK constants rather than a measured response — the\nweaker form, named rather than left unsaid.\n\nNote that the SDK's `LATEST_PROTOCOL_VERSION` is an alias for the **modern**\nera, not for the handshake era — pinning against it alone would leave the era\nthat current clients actually negotiate free to drift.\n\n**Update policy.** When the gate fails, do not edit the constant blindly: read\nthe spec changelog between the two revisions, verify the server still behaves,\nthen move the constant, this section, `README.de.md` and\n[`CHANGELOG.md`](CHANGELOG.md) together.\n\n---\n\n## Testing\n\n```bash\n# Unit tests (no API key required)\nPYTHONPATH=src pytest tests/ -m \"not live\"\n\n# Integration tests (live API calls)\npytest tests/ -m \"live\"\n\n# Re-record the fixtures from the live sources (writes tests/fixtures/PROVENANCE.md)\npython scripts/record_fixtures.py\n```\n\nThe unit-test payloads are **recorded, not invented**. Source, retrieval date,\nselection rule, **redaction** and SHA-256 per file are in\n[`tests/fixtures/PROVENANCE.md`](tests/fixtures/PROVENANCE.md).\n\nTwo things are stated there rather than papered over. **Personal data:** the\ngazette carries debt-collection notices and Zefix carries the full SHAB text\nnaming registered persons with their place of residence — the recorded payloads\nkeep the shape and redact those values, with the complete list of redacted\nfields alongside. **Zefix needs no credentials:** until 2026-08-08 this\nrepository recorded no Zefix fixtures because the recording script measured\nHTTP 401. The measurement was right about the wrong address — the script asked\n`ZefixPublicREST`, while the server speaks to `ZefixREST`, which answers with no\nauthentication at all.\n\n### The live suite\n\n`ci.yml` runs `-m \"not live\"`: a foreign 503 must not redden a stranger's pull\nrequest, because a suite that does gets switched off, and a switched-off suite\nchecks nothing. The exclusion has a safety net —\n[`.github/workflows/live-tests.yml`](.github/workflows/live-tests.yml) runs\nweekly (`cron: \"31 5 * * 1\"`) plus `workflow_dispatch`.\n\nThe verdict is read from the JUnit XML rather than the exit code, by\n[`scripts/classify_live_run.py`](scripts/classify_live_run.py), because a live\nrun has three answers and not two:\n\n| State | Meaning | Issue |\n|---|---|---|\n| `clear` | the suite ran and was green | closes an open one |\n| `finding` | the suite ran and something fell | opens or updates one |\n| `unknown` | the suite did **not** run — failed install, timeout, renamed marker, everything skipped | left untouched |\n\n`tests - skipped == 0` is `unknown`, not `clear`: pytest exits 0 when every test\nwas skipped, and a job that books that as green closes an issue on a comparison\nthat never happened.\n\nOne caveat when editing that workflow: the pull-request checks do **not** cover\nit — it has no `push` or `pull_request` trigger, so a green PR says nothing about\nit. Verify changes with a manual `workflow_dispatch` run on the branch *before*\nmerging.\n\n---\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md)\n\n---\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md)\n\n---\n\n## Security\n\nSee [SECURITY.md](SECURITY.md) ([Deutsch](SECURITY.de.md)) for the security\nposture and how to report a vulnerability.\n\n---\n\n## License\n\nMIT License — see [LICENSE](LICENSE)\n\n---\n\n## Author\n\nHayal Oezkan · [malkreide](https://github.com/malkreide)\n\n---\n\n## Credits & Related Projects\n\n- **Zefix:** [zefix.admin.ch](https://www.zefix.admin.ch/) — Swiss Federal Commercial Register (BJ/FOJ)\n- **Amtsblattportal:** [amtsblattportal.ch](https://amtsblattportal.ch/) — SHAB and cantonal gazettes (SECO / Swiss Confederation)\n- **Protocol:** [Model Context Protocol](https://modelcontextprotocol.io/) — Anthropic / Linux Foundation\n- **Related:** [fedlex-mcp](https://github.com/malkreide/fedlex-mcp) — Commercial register ordinance (HRegV)\n- **Related:** [zurich-opendata-mcp](https://github.com/malkreide/zurich-opendata-mcp) — Company seat + geodata\n- **Related:** [swiss-statistics-mcp](https://github.com/malkreide/swiss-statistics-mcp) — Industry statistics by NOGA\n- **Related:** [swiss-snb-mcp](https://github.com/malkreide/swiss-snb-mcp) — Economic indicators\n- **Portfolio:** [Swiss Public Data MCP Portfolio](https://github.com/malkreide)\n\n<!-- mcp-name: io.github.malkreide/register-mcp -->\n\n<!-- BEGIN GENERATED: install -->\n## Installation\n\nRun 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`):\n\n```json\n{\n  \"mcpServers\": {\n    \"register-mcp\": {\n      \"command\": \"uvx\",\n      \"args\": [\n        \"register-mcp\"\n      ]\n    }\n  }\n}\n```\n<!-- END GENERATED: install -->\n",
  "bytes": 30206,
  "sha": "135aaefdc711c0b0b6764d4238dff8e0c0f9bd6781b4386dacb4ea83ffe6a4f1",
  "repo_slug": "malkreide/register-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_malkreide_register_mcp_3498ec5d/readme"
}