io.github.malkreide/register-mcp
Zefix commercial register with a company-UID join to amtsblattportal.ch (SHAB + cantonal gazettes)
Open source Open in the app JSON README (API)
About
Zefix commercial register with a company-UID join to amtsblattportal.ch (SHAB + cantonal gazettes)
Details
- Kind
- MCP servers
- Topic
- No topic detected
- Publisher
- malkreide
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.6.1
- Last push
- 2026-08-29T18:53:19Z
- Repository state
- ativo
- Language
- Python
- License
- MIT
- Added
- 2026-08-29 04:00:27
- Updated
- 2026-08-29 04:00:27
- Origin id
io.github.malkreide/register-mcp
README
> π¨π **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)**
# register-mcp

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

> MCP Server for the Swiss Federal Commercial Register (Zefix/Handelsregister), with a **company-UID join** to the official gazettes (SHAB + cantonal AmtsblΓ€tter)
[π©πͺ Deutsche Version](README.de.md)
---
## Overview
`register-mcp` provides AI-native access to **two** Swiss federal data sources, joined on the UID, all without authentication:
| Source | Data | API |
|--------|------|-----|
| **Zefix (Handelsregister)** | Swiss companies, legal forms, registered-office data | ZefixREST v1 |
| **Amtsblattportal** | Everything published **about a specific company** (by its UID): HR mutations, calls to creditors, bankruptcy | amtsblattportal.ch v1 |
The 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.**
The 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.
Designed for Swiss public administration use cases: vendor verification, contract partner due diligence, and supplier onboarding β all via natural language queries.
**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)?"*
That single question walks the whole tool chain across both sources:
```
zefix_search_company β zefix_verify_company β gazette_company_publications(uid=β¦) β gazette_get_publication(id=β¦)
```
---
## Features
- ποΈ **9 tools** across two sources β company search & verification (Zefix) + the company-scoped gazette join (SHAB/cantonal)
- π **`gazette_company_publications`** β the UID join: everything published about a company
- π‘οΈ **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*)
- π **`zefix_verify_company`** β quick active/dissolved status check
- π **Bilingual output** (Markdown / JSON) with per-source attribution + `provenance`
- π **No API key required** β open data from zefix.admin.ch and amtsblattportal.ch
- βοΈ **Dual transport** β stdio (Claude Desktop) + SSE (cloud)
---
## Prerequisites
- Python 3.11+
- [uv](https://github.com/astral-sh/uv) (recommended) or pip
---
## Installation
```bash
# Clone the repository
git clone https://github.com/malkreide/register-mcp.git
cd register-mcp
# Install
pip install -e .
# or with uv:
uv pip install -e .
```
Or with `uvx` (no permanent installation):
```bash
uvx register-mcp
```
---
## Quickstart
```bash
# stdio (for Claude Desktop)
python -m register_mcp.server
# SSE (cloud deployment) β MCP_API_KEY is REQUIRED
MCP_API_KEY=$(openssl rand -hex 32) MCP_TRANSPORT=sse PORT=8000 \
python -m register_mcp.server
```
### SSE / Cloud Deployment
When running with `MCP_TRANSPORT=sse`, the server enforces:
- **Bearer-token auth** β set `MCP_API_KEY` to a secret string. Clients must send
`Authorization: Bearer <key>` on every request. Missing or wrong β HTTP 401.
The server refuses to start without `MCP_API_KEY` set.
- **Rate limiting** β sliding window per bearer-token hash. Defaults: 60 req / 60 s.
Tunable via `MCP_RATE_LIMIT` and `MCP_RATE_WINDOW`. Exceeding the limit returns
HTTP 429 with `Retry-After`.
- **Structured JSON logging** β every tool call emits one line to stderr with
`tool`, `status`, `latency_ms`. Auth failures and rate-limit events are logged
at WARNING level. Configure verbosity with `LOG_LEVEL` (default `INFO`).
- **Reference-data cache** β Zefix legal-forms are cached for 24h
(`LEGAL_FORMS_TTL` seconds) to avoid an extra upstream call per tool invocation.
- **Egress allow-list** β outbound HTTP is restricted to `www.zefix.admin.ch`
and `amtsblattportal.ch` via an `httpx` request hook that also fires on
redirects. A `Location` header pointing elsewhere raises `EgressDenied` and is
never followed. Override with `MCP_ALLOWED_HOSTS=host1,host2` (comma-separated,
lower-case).
> β οΈ **Upgrade note (0.2.x β 0.3.0):** `amtsblattportal.ch` was added to the
> **default** allow-list when the gazette tools shipped. If your deployment
> **pins** `MCP_ALLOWED_HOSTS`, that value overrides the default entirely β
> add `amtsblattportal.ch` to it, or every `gazette_*` call will raise
> `EgressDenied`.
- **Optional OpenTelemetry tracing** β install with `pip install register-mcp[otel]`
and set `OTEL_EXPORTER_OTLP_ENDPOINT` (e.g. `http://otel-collector:4318/v1/traces`).
Without the extra or without the env var the server stays silent β no hard
dependency on the OTel SDK.
For multi-instance deployments, place a real gateway (Cloudflare, Railway internal
networking, an API-Gateway with Redis-backed rate limiting) in front of the
in-memory limiter, which is per-process by design.
### Container deployment
A minimal multi-stage `Dockerfile` ships with the repo. The image runs as a
non-root `mcp` user; dependencies are resolved from `uv.lock` (`uv sync
--frozen`), so the build is reproducible.
```bash
docker build -t register-mcp:local .
docker run --rm -p 8000:8000 \
-e MCP_TRANSPORT=sse \
-e MCP_API_KEY="$(openssl rand -hex 32)" \
register-mcp:local
```
For local iteration there is a `compose.yaml` with `read_only`, `cap_drop: ALL`
and `no-new-privileges`:
```bash
MCP_API_KEY=$(openssl rand -hex 32) docker compose up --build
```
See [SECURITY.md](SECURITY.md) for hardening notes (egress restriction, key
rotation, SIEM forwarding).
Try it immediately in Claude Desktop:
> *"Is Lehrmittelverlag ZΓΌrich AG active in the commercial register?"*
> *"Look up the company with UID CHE-108.954.978"*
> *"List all Swiss legal forms"*
---
## Configuration
### Claude Desktop
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
```json
{
"mcpServers": {
"register": {
"command": "python",
"args": ["-m", "register_mcp.server"]
}
}
}
```
Or with `uvx`:
```json
{
"mcpServers": {
"register": {
"command": "uvx",
"args": ["register-mcp"]
}
}
}
```
**Config file locations:**
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
### Cloud Deployment (SSE for browser access)
For use via **claude.ai in the browser** (e.g. on managed workstations without local software):
**Render.com (recommended):**
1. Push/fork the repository to GitHub
2. On [render.com](https://render.com): New Web Service β connect GitHub repo
3. Set start command: `python -m register_mcp.server --http --port 8000`
4. In claude.ai under Settings β MCP Servers, add: `https://your-app.onrender.com/sse`
> π‘ *"stdio for the developer laptop, SSE for the browser."*
---
## Available Tools
**Zefix β commercial register (6):**
| Tool | Description |
|------|-------------|
| `zefix_search_companies` | Search companies by name, canton, legal form |
| `zefix_get_company` | Full company profile by internal EHRAID |
| `zefix_get_company_by_uid` | Company lookup by UID (CHE-xxx.xxx.xxx) |
| `zefix_verify_company` | Quick active/dissolved status check |
| `zefix_list_legal_forms` | All Swiss legal forms with IDs |
| `zefix_list_municipalities` | Swiss municipalities with BFS IDs |
**Amtsblattportal β the company-scoped gazette join (3):**
| Tool | Description |
|------|-------------|
| `gazette_company_publications` | **The UID join.** All gazette publications for a company **UID**, newest first, optional (validated) rubric/time filters |
| `gazette_get_publication` | Single publication incl. XML full text, defensively parsed (by publication id) |
| `gazette_source_status` | Reachability of both sources + cache ages (rubrics, legal forms) |
The 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).
### Example Use Cases
| Query | Tool |
|-------|------|
| *"Is Lehrmittelverlag ZΓΌrich AG active?"* | `zefix_verify_company` |
| *"Look up CHE-108.954.978"* | `zefix_get_company_by_uid` |
| *"Find companies named Migros in canton ZH"* | `zefix_search_companies` |
| *"What has been published about CHE-116.115.052?"* | `gazette_company_publications` |
| *"Show the full official text of that HR deletion notice"* | `gazette_get_publication` |
| *"Are both data sources reachable right now?"* | `gazette_source_status` |
---
## Architecture
```
ββββββββββββββββββββββββββββββββ
ββββββΆβ Zefix (Handelsregister) β
β β www.zefix.admin.ch β
βββββββββββββββββββ ββββββββββββββββββββββββββββ΄ββ β ZefixREST/api/v1 β
β Claude / AI ββββββΆβ register-mcp β ββββββββββββββββββββββββββββββββ
β (MCP Host) βββββββ (MCP Server) β ββββββββββββββββββββββββββββββββ
βββββββββββββββββββ β 9 Tools (zefix_ + gazette_) ββββΆβ Amtsblattportal β
β Stdio | SSE β β amtsblattportal.ch/api/v1 β
β Egress allow-list β β SHAB + cantonal gazettes β
β No authentication required β ββββββββββββββββββββββββββββββββ
ββββββββββββββββββββββββββββββββ
join key: UID (CHE-XXX.XXX.XXX)
```
### Data Source Characteristics
| Source | Protocol | Coverage | Auth |
|--------|----------|----------|------|
| Zefix | REST/JSON | Swiss companies, legal forms, registered offices | None |
| Amtsblattportal | REST/JSON (list) + XML (full text) | SHAB + cantonal gazettes, 2.79M publications | None |
| ZefixPublicREST (planned) | REST/JSON | Signatories, capital, full history | Basic Auth (free) |
| UID Register (planned) | SOAP | MwSt, NOGA codes, cross-validation | Public (20 req/min) |
### The UID join β Zefix β Amtsblatt
The two sources share exactly one key: the **UID** (`CHE-XXX.XXX.XXX`). That is
what turns them from two data sets into one workflow.
```
zefix_get_company_by_uid(uid) # Zefix: does the company exist? status, purpose, legal form
β UID
βΌ
gazette_company_publications(uid) # Gazette: everything published about it (HR, KK, SB, LS, β¦)
β publication id
βΌ
gazette_get_publication(id) # Full official text from the per-rubric XML
```
Two properties of the source shape this path (both verified in
[`docs/probe-shab.md`](docs/probe-shab.md)):
- The **bulk list carries no company UID** (`meta.uid` is `null`). The company
UID lives only in the **single-publication fetch** β `meta.uid` in the single
JSON, or `<uid>` in the XML (which also carries the full text). So the join
runs *list β per-hit single fetch β match against the Zefix UID*.
- `gazette_company_publications` filters the corpus by `uids=<UID>` directly, so
in practice you get the company's publications in one call without walking
every record.
### Procurement lives in the separate `amtsblatt-mcp`
Public procurement (Submissionen) is **not** a federal SHAB rubric and is **not**
covered by this server. It exists only as a **cantonal** `OB-<canton>` rubric,
only a few cantons publish it in this portal, and most β including **ZΓΌrich** β
route tenders through **[simap.ch](https://www.simap.ch/)**, a separate platform.
Procurement, cantonal notices, and broad full-text search are scoped to the
proposed [`amtsblatt-mcp`](docs/amtsblatt-mcp-proposal.md) server, which applies
a fail-closed **green-rubric allow-list**. See that proposal for the full
`OB-*` coverage map and the rubric traffic-light table.
> **`SB` β Submissionen.** `SB` is *Schuldbetreibungen* (debt collection), a
> person-data-heavy rubric this server never exposes as a search entry.
---
## Data Protection & Scope
This section is **not** a footnote β it is the reason the server is shaped the
way it is.
The Amtsblattportal systematically publishes rubrics containing personal data of
**natural** persons: bankruptcies (`KK`), debt-collection (`SB`), calls to
creditors (`LS`/`SR`), inheritance/estate calls (`ES`, `TE-*`), and building
applications with owner names. Those publications are public β but making them
*systematically queryable by name* through an AI agent is a repurposing the
publication never intended, and under the revised Swiss Federal Act on Data
Protection (**revDSG**) a "show me every debt-collection entry for person X" tool
is a profiling instrument. Deliberate design choices follow:
- **No person-based search entry.** No tool takes a natural person's name, birth
date or address. The only gazette entry points are keyed on a **company UID**
(`gazette_company_publications`) or an opaque **publication id**
(`gazette_get_publication`). A firm's own bankruptcy *is* returned via its UID β
that is corporate data about a legal person, not name-based profiling.
- **No free-text gazette search here.** `keyword` and `cantons` are not even on
the internal query-parameter allow-list, so no future code change can smuggle a
corpus-wide keyword search in. Broad search lives in `amtsblatt-mcp` behind a
fail-closed green allow-list (procurement, HR, official notices only).
- **No persistence of publication content.** The server is a pass-through; only
the rubric taxonomy and Zefix legal-forms list are cached in memory (24 h).
Official publications carry statutory deletion periods β a store that outlived
them would actively undermine those periods.
- **Fail closed.** Rubric codes are validated against the live taxonomy before
any call; an unknown code is refused, not silently widened.
The broad-platform counterpart, its green/yellow/red rubric classification and
its fail-closed design are specified in
[`docs/amtsblatt-mcp-proposal.md`](docs/amtsblatt-mcp-proposal.md).
---
## Architecture decision
**ARCH A β live-API-only**, consistent with the existing Zefix integration
(decided 2026-07-18).
The Amtsblattportal is queried live on every call. All endpoints respond in
0.2β2.0 s, and the use case β targeted company and topic research β does not
need a local bulk copy. A bulk dump would mean mirroring 2.79M records, with an
ongoing sync burden and staleness risk, for no benefit to the join-on-UID
workflow. The taxonomy (`/rubrics`) and the Zefix legal-forms list are the only
data cached, each for 24h in memory, because they change at most a few times a
year and every filtered call needs them.
---
## Phased Implementation
| Phase | API | Auth | Status |
|-------|-----|------|--------|
| **Phase 1** | `ZefixREST/api/v1` | None | **Current** |
| **Phase 2** | `ZefixPublicREST/api/v1` | Basic Auth (free, email zefix@bj.admin.ch) | Planned |
| **Phase 3** | UID-Register SOAP | Public (20 req/min) | Planned |
Phase 2 will add: signatory details, share capital, full historical entries.
Phase 3 will add: MwSt status, NOGA industry codes, cross-register validation.
---
## Project Structure
```
register-mcp/
βββ src/register_mcp/
β βββ __init__.py # Package
β βββ server.py # 9 tools (Zefix + company-scoped gazette join)
βββ tests/
β βββ test_server.py # Zefix unit + integration tests (mocked HTTP)
β βββ test_gazette.py # Gazette tools + the three quirks (mocked HTTP)
β βββ test_egress.py # Egress allow-list
βββ docs/
β βββ probe-shab.md # Phase-1 live probe of amtsblattportal.ch
β βββ amtsblatt-mcp-proposal.md# Spec for the separate broad-platform server
β βββ demo/ # vhs demo script + standalone CLI demo
βββ .github/workflows/ci.yml # GitHub Actions (Python 3.11/3.12/3.13)
βββ pyproject.toml
βββ CHANGELOG.md
βββ CONTRIBUTING.md
βββ LICENSE
βββ README.md # This file (English)
βββ README.de.md # German version
```
---
## Known Limitations
- Search by canton without a name filter may return API errors (Zefix API limitation)
- Phase 1 Zefix API may be rate-limited under heavy load; retry after a short delay
- ZefixPublicREST (new API) requires registration: email zefix@bj.admin.ch
### Amtsblattportal β verified behaviour (live-checked 2026-07-18)
| Call | HTTP | Status | Records | Note |
|---|---|---|---|---|
| `/publications?publicationStates=PUBLISHED` | 200 | OK | 2,790,323 | baseline (full corpus) β never queried unfiltered |
| `?uids=CHE-116.115.052` | 200 | **OK** | 4 | **the join β core (and only) gazette entry** |
| `?uids=β¦&rubrics=HR` | 200 | OK | β | optional, validated rubric narrowing on the join |
| `/publications/{id}/xml` | 200 | OK | β | full text, rubric-specific schema |
| `/rubrics` | 200 | OK | β | taxonomy (for code validation) |
| `?rubrics=ZZZZ` (invalid) | **200** | **Silent Empty** | 0, `total: null` | Quirk 2 |
| `?uid=β¦` (wrong param name) | **200** | **Silent Ignore** | **2,790,323** | Quirk 1 |
> Free-text (`keyword`) and broad `cantons` search are **not** performed by this
> server β those probe results live in [`docs/probe-shab.md`](docs/probe-shab.md)
> and inform the separate `amtsblatt-mcp`.
### Zefix β verified behaviour (live-checked 2026-08-15)
Found by the weekly live suite, not by the unit tests β which stayed green
throughout.
| Call to `firm/search.json` | HTTP | Result |
|---|---|---|
| `{"name": "Migros", β¦}` | 200 | 35 hits |
| a name with no hits | **404** | NORESULT envelope β *not* an empty 200 |
| `{"uid": "109741634", β¦}` | **400** | Bad Request β there is no `uid` field |
| `{"name": "CHE-999.999.999", "searchType": "CONTAINS"}` | 200 | **Β«CHEMAM - 999Β»**, UID CHE-113.593.998 |
| a dissolved firm without `activeOnly: false` | 404 | NORESULT β as if it never existed |
**Three shapes, one shipped bug each:**
- **No hits answer with HTTP 404**, carrying the NORESULT envelope. Every call
therefore goes through `_zefix_post_search`; a raw `raise_for_status()` makes
the friendly branch unreachable. That is how `zefix_verify_company` shipped
answering *"Eintrag nicht gefunden. Bitte EHRAID oder UID prΓΌfen"* to a **name**
search, where neither an EHRAID nor a UID was in play. A fixture that puts the
NORESULT body into a 200 makes exactly that dead branch look green.
- **A hit list is not an answer.** UID lookup searches the *name* field with
`searchType: CONTAINS`, so `CHE-999.999.999` returns a real company under a UID
that is not its own. Defence: exact digit match or nothing β no `firms[0]`
fallback. The former fallback produced a complete, plausible, formatted record
about somebody else, indistinguishable from a correct answer.
- **Without `activeOnly: false`, "dissolved" looks like "never existed".**
Zefix returns only active entries by default; `zefix_verify_company` sets the
flag deliberately. A firm with no UID comes back as a **string of blanks**
(`uid: " "`, `uidFormatted: null`), not as `null`.
**Three quirks are defended in code** (details in the [CHANGELOG](CHANGELOG.md)
under *Known findings*):
- **Quirk 1 β Silent Ignore (critical).** Unknown query parameters are dropped
silently and return the full 2.79M corpus with HTTP 200. Defence: query
strings are built exclusively from an `ALLOWED_GAZETTE_PARAMS` allow-list, and
every filtered response is plausibility-checked β a `total` above 2,000,000 is
rejected as *"filter ignored by upstream β result not trustworthy"*.
- **Quirk 2 β Silent Empty.** An invalid rubric code returns HTTP 200 with an
empty result. Defence: the `/rubrics` taxonomy is cached 24h and every code is
validated **before** any call, failing with the five closest valid codes.
- **Quirk 3 β Two-step fetch.** The JSON list carries only `meta`; the content
lives only in the per-rubric namespaced XML. Defence: namespace-agnostic
defensive parsing (`meta` + `publicationText` mandatory, HR `company` when
present, everything else in `additional_fields`).
---
## Safety & Limits
### Rate Limits
| API | Limit | Notes |
|-----|-------|-------|
| ZefixREST (Phase 1) | Not officially documented | Throttling possible under heavy load β retry after 1β2 s |
| ZefixPublicREST (Phase 2) | Not officially documented | Requires prior registration (free) |
| UID-Register SOAP (Phase 3) | **20 req/min** | Hard limit, publicly documented |
### Data Privacy
- **Read-only access** β all tools carry `readOnlyHint: True`; the server performs no write, delete, or mutation operations against any API
- **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
- **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
- **Public register data only** β the Zefix Handelsregister is a public federal register (HRegV); gazette data returned is likewise legally public, retrieved per company UID
- **No personal tracking** β the server does not transmit user identity, query history, or session data to the upstream sources
### Terms of Service & Data Sources
- **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.
- **SHAB:** Swiss Official Gazette of Commerce β published by the Federal Chancellery (BK). Public by law.
- **Institutional use:** This server is designed for read-only queries in public administration workflows. Not suitable for mass harvesting or automated surveillance use cases.
### Security
- No credentials are stored or transmitted (Phase 1)
- Phase 2 credentials (`ZEFIX_USER`, `ZEFIX_PASSWORD`) are passed via environment variables only β never hardcoded
- All HTTP calls use HTTPS exclusively
- Tool inputs are validated via Pydantic v2 before any API call is made
---
## Demo

> π½οΈ *Terminal GIF coming soon β see [`docs/demo/`](docs/demo/) to generate it locally with [vhs](https://github.com/charmbracelet/vhs)*
**Example interaction:**
```
User: "Is Lehrmittelverlag ZΓΌrich AG active in the commercial register?"
β Tool: zefix_verify_company(name="Lehrmittelverlag ZΓΌrich AG")
Claude: β
Lehrmittelverlag ZΓΌrich AG is ACTIVE in the Handelsregister.
UID: CHE-404.020.972 | Canton: ZH | Legal form: AG
Last SHAB mutation: 2023-07-27
```
[β More use cases by audience β](EXAMPLES.md)
To generate the demo GIF locally:
```bash
# Install vhs (macOS/Linux)
brew install vhs # macOS
# or: go install github.com/charmbracelet/vhs@latest
# Generate
vhs docs/demo/demo.tape
# β outputs docs/demo/demo.gif
```
---
## MCP Protocol Version
This server speaks **two protocol eras** over the same endpoint. The client's
first request on a connection decides which one applies; a later claim from the
other era is refused.
| Era | Revision | Who reaches it |
|---|---|---|
| `initialize` handshake | `2024-11-05` β¦ **`2025-11-25`** | What today's clients speak. The server answers with the revision asked for, or with the `2025-11-25` ceiling when the request asks for something newer. |
| Per-request envelope | **`2026-07-28`** | A request carrying the `2026-07-28` `_meta` envelope opens a modern connection. |
Both revisions are pinned in
[`tests/test_protocol_version.py`](tests/test_protocol_version.py) and asserted
against the installed SDK, so a Dependabot bump of `mcp` cannot move either one
silently. This server builds no ASGI app to send an `initialize` through, so
the gate asserts the SDK constants rather than a measured response β the
weaker form, named rather than left unsaid.
Note that the SDK's `LATEST_PROTOCOL_VERSION` is an alias for the **modern**
era, not for the handshake era β pinning against it alone would leave the era
that current clients actually negotiate free to drift.
**Update policy.** When the gate fails, do not edit the constant blindly: read
the spec changelog between the two revisions, verify the server still behaves,
then move the constant, this section, `README.de.md` and
[`CHANGELOG.md`](CHANGELOG.md) together.
---
## Testing
```bash
# Unit tests (no API key required)
PYTHONPATH=src pytest tests/ -m "not live"
# Integration tests (live API calls)
pytest tests/ -m "live"
# Re-record the fixtures from the live sources (writes tests/fixtures/PROVENANCE.md)
python scripts/record_fixtures.py
```
The unit-test payloads are **recorded, not invented**. Source, retrieval date,
selection rule, **redaction** and SHA-256 per file are in
[`tests/fixtures/PROVENANCE.md`](tests/fixtures/PROVENANCE.md).
Two things are stated there rather than papered over. **Personal data:** the
gazette carries debt-collection notices and Zefix carries the full SHAB text
naming registered persons with their place of residence β the recorded payloads
keep the shape and redact those values, with the complete list of redacted
fields alongside. **Zefix needs no credentials:** until 2026-08-08 this
repository recorded no Zefix fixtures because the recording script measured
HTTP 401. The measurement was right about the wrong address β the script asked
`ZefixPublicREST`, while the server speaks to `ZefixREST`, which answers with no
authentication at all.
### The live suite
`ci.yml` runs `-m "not live"`: a foreign 503 must not redden a stranger's pull
request, because a suite that does gets switched off, and a switched-off suite
checks nothing. The exclusion has a safety net β
[`.github/workflows/live-tests.yml`](.github/workflows/live-tests.yml) runs
weekly (`cron: "31 5 * * 1"`) plus `workflow_dispatch`.
The verdict is read from the JUnit XML rather than the exit code, by
[`scripts/classify_live_run.py`](scripts/classify_live_run.py), because a live
run has three answers and not two:
| State | Meaning | Issue |
|---|---|---|
| `clear` | the suite ran and was green | closes an open one |
| `finding` | the suite ran and something fell | opens or updates one |
| `unknown` | the suite did **not** run β failed install, timeout, renamed marker, everything skipped | left untouched |
`tests - skipped == 0` is `unknown`, not `clear`: pytest exits 0 when every test
was skipped, and a job that books that as green closes an issue on a comparison
that never happened.
One caveat when editing that workflow: the pull-request checks do **not** cover
it β it has no `push` or `pull_request` trigger, so a green PR says nothing about
it. Verify changes with a manual `workflow_dispatch` run on the branch *before*
merging.
---
## Changelog
See [CHANGELOG.md](CHANGELOG.md)
---
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)
---
## Security
See [SECURITY.md](SECURITY.md) ([Deutsch](SECURITY.de.md)) for the security
posture and how to report a vulnerability.
---
## License
MIT License β see [LICENSE](LICENSE)
---
## Author
Hayal Oezkan Β· [malkreide](https://github.com/malkreide)
---
## Credits & Related Projects
- **Zefix:** [zefix.admin.ch](https://www.zefix.admin.ch/) β Swiss Federal Commercial Register (BJ/FOJ)
- **Amtsblattportal:** [amtsblattportal.ch](https://amtsblattportal.ch/) β SHAB and cantonal gazettes (SECO / Swiss Confederation)
- **Protocol:** [Model Context Protocol](https://modelcontextprotocol.io/) β Anthropic / Linux Foundation
- **Related:** [fedlex-mcp](https://github.com/malkreide/fedlex-mcp) β Commercial register ordinance (HRegV)
- **Related:** [zurich-opendata-mcp](https://github.com/malkreide/zurich-opendata-mcp) β Company seat + geodata
- **Related:** [swiss-statistics-mcp](https://github.com/malkreide/swiss-statistics-mcp) β Industry statistics by NOGA
- **Related:** [swiss-snb-mcp](https://github.com/malkreide/swiss-snb-mcp) β Economic indicators
- **Portfolio:** [Swiss Public Data MCP Portfolio](https://github.com/malkreide)
<!-- mcp-name: io.github.malkreide/register-mcp -->
<!-- BEGIN GENERATED: install -->
## Installation
Run via [`uv`](https://docs.astral.sh/uv/)'s `uvx` β no clone or manual install needed. Add to your MCP client config (`mcpServers` for Claude Desktop, Cursor and Windsurf; use a top-level `servers` key for VS Code in `.vscode/mcp.json`):
```json
{
"mcpServers": {
"register-mcp": {
"command": "uvx",
"args": [
"register-mcp"
]
}
}
}
```
<!-- END GENERATED: install -->