Termdat Mcp

io.github.malkreidePublished by GitHub account malkreidev0.2.0Updated Oct 24, 2026

MCP server for TERMDAT — the terminology database of the Swiss Federal Administration

Overview

AI-generated overview

Lets an assistant look up officially validated Swiss administrative designations and their DE/FR/IT/EN equivalents from the TERMDAT terminology database.

What you can do
Wraps the public TERMDAT v2 API of the Swiss Federal Chancellery in seven read-only tools: search_terms, translate_term, check_terms, get_entries, list_collections, list_classifications and api_status (R25, R71-R79). Responses carry official designations in German, French, Italian and English with source reference and validation status (R26). A 24-hour cache covers the roughly 140 collections and 23 classifications, and searches are live API calls with retry and explicit truncation reporting (R28, R29, R93).
When to use it
Use it when an assistant needs the official name of a Swiss authority, department or legal act, or its equivalent in another national language, and when checking terminology consistency in administrative texts (R9, R27). It is a certified name-plate archive, not a subject dictionary: it is strong on authority naming, official titles and abbreviations and largely silent on domain vocabulary (R12, R22, R23).
Requirements
Local process over stdio, run with uvx termdat-mcp (or pip). Needs Python 3.10+ and uv/uvx, plus network access to api.termdat.bk.admin.ch; no API key is needed (R37, R38, R39). Optional environment variables configure transport, host, port, CORS origins, allowed hosts, log level and vocabulary cache TTL (R46-R60).
Before you install
Read-only: every tool is annotated readOnlyHint true and destructiveHint false, and the server never writes to TERMDAT (R97). No credentials are handled; the API is unauthenticated and no secrets are stored or forwarded (R98). Egress is restricted to api.termdat.bk.admin.ch (R102). Reuse and republication of TERMDAT content require naming the source www.termdat.ch and informing the Federal Chancellery's Terminology Section of purpose and manner beforehand (R101, R128); running the server is said

Installation

In SourceWeft

  1. Open Termdat Mcp in the dashboard and add it to a workspace.
  2. Enable the server for the chats that should use its tools.

Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.

Other MCP clients

Follow the launch instructions in the repository.

README

🇨🇭 Part of the Swiss Public Data MCP Portfolio — open-source MCP servers connecting AI agents to Swiss public and open data. This is a private project. It is independent of any employer or institutional affiliation.

🏷️ termdat-mcp

[Version] [CI] [License: MIT] [Python 3.10+] [MCP] [Auth: none] [Portfolio]

Official, validated Swiss administrative designations across DE / FR / IT / EN — with source references and validation status.

🇩🇪 Deutsche Version

Overview

MCP server for TERMDAT, the terminology database of the Swiss Federal Administration, maintained by the Federal Chancellery. It gives an AI agent the officially validated designations of Swiss authorities, departments and legal acts across DE / FR / IT / EN — with source references and validation status.

Discovered through i14y-mcp, which catalogues TERMDAT as data service ff0c37eb-2f7c-4ff6-996e-d22b77bf52fc.

What this is — and what it is not. TERMDAT is not a subject dictionary. It is a certified name-plate archive: it will not tell you what «Sonderpädagogik» means, but it will tell you the official name of the authority responsible for it, and what that authority is called in French.

Measured coverage (live, 2026-07-19, German search over the Terminus field):

Search termHits
Departement20
Bildung13
Verordnung8
Schule5
Behörde4
Sonderpädagogik3
Volksschule · Lehrperson · Schulleitung · Unterricht · Kindergarten0

The thirteen «Bildung» hits are organisational names — Bildungsdirektion, Erziehungsdepartement, Departement für Volkswirtschaft und Bildung — not pedagogical concepts. Plan accordingly: this server is strong for authority naming, official titles and abbreviations, and largely silent on domain vocabulary.

Features

  • Seven read-only tools over the official TERMDAT public v2 API.
  • Official designations across DE / FR / IT / EN, with source reference and validation status on every response.
  • Communication QA: check up to 25 terms in one call against validated designations.
  • Vocabulary cache (24 h TTL) for the 140 collections and 23 classifications, with stale-serve fallback.
  • Retry with exponential backoff (2/4/8 s); explicit MaxEntryCount to avoid silent truncation.
  • Dual transport: stdio (local) and Streamable HTTP (cloud). Both serve MCP protocol revision 2026-07-28.
  • No authentication required — public, unauthenticated API (No-Auth-First).

🎯 Anchor demo query

«What are the official French and Italian names of the education directorates of the German-speaking cantons?»

Resolved with list_classifications → search_terms → translate_term.

Demo

[Demo: Claude using search_terms and translate_term]

Prerequisites

  • Python 3.10+
  • uv / uvx (recommended) or pip
  • Network access to api.termdat.bk.admin.ch — no API key needed

Installation

bash
uvx termdat-mcp

Claude Desktop (claude_desktop_config.json):

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

Quickstart

bash
# Run locally over stdio (default transport)uvx termdat-mcp
# From a checkout, without installingPYTHONPATH=src python -m termdat_mcp

Configuration

All configuration is via environment variables. Defaults are safe for local use.

VariableDefaultPurpose
TERMDAT_MCP_TRANSPORTstdioTransport: stdio (local) or streamable-http / http (cloud). sse is accepted as a deprecated alias and now serves Streamable HTTP on /mcp — it warns on startup
HOST127.0.0.1Bind host (HTTP transport only). Loopback by default; set HOST=0.0.0.0 only inside a container
PORT8000Bind port (HTTP transport only)
TERMDAT_MCP_CORS_ORIGINS[]HTTP only: explicit allowed browser origins (default-deny; never a wildcard in production). Comma-separated, e.g. https://a.example,https://b.example
TERMDAT_MCP_ALLOWED_HOSTS[]HTTP only: inbound Host allow-list. Comma-separated, e.g. mcp.example.ch,mcp.example.ch:443. Needed for a non-loopback bind — without it the Host header is not checked at all
TERMDAT_MCP_LOG_LEVELINFOstructlog level (JSON to stderr)
TERMDAT_MCP_VOCAB_TTL86400Vocabulary cache TTL in seconds

Configuration is loaded once into a typed Settings object (pydantic-settings).

List variables. TERMDAT_MCP_CORS_ORIGINS and TERMDAT_MCP_ALLOWED_HOSTS take a comma-separated list — the recommended spelling, and the one the rest of the Swiss Public Data MCP portfolio uses:

bash
TERMDAT_MCP_ALLOWED_HOSTS="mcp.example.ch,mcp.example.ch:443"

A JSON list (["mcp.example.ch"]) stays valid. Surrounding whitespace and empty entries are dropped in both forms, so a trailing comma is harmless. A single value needs no brackets.

TERMDAT_MCP_ALLOWED_HOSTS matters in the cloud. The SDK leaves DNS-rebinding protection off while no allow-list is configured. Bind to 0.0.0.0 in a container without this variable and the Host header is never validated — the server cannot derive its own public name from the bind address, and a guessed list would reject every real request with HTTP 421. It warns on startup when it has to fall back to that state.

Cloud (Render / Railway):

bash
TERMDAT_MCP_TRANSPORT=streamable-http PORT=8000 termdat-mcp   # exposes /mcp

Available Tools

ToolPurpose
search_termsSearch TERMDAT with field flags, collection and classification filters
translate_termOfficial equivalent of an administrative term in another national language
check_termsCommunication QA: check up to 25 terms against validated designations
get_entriesFetch known entries by numeric ID
list_collectionsThe ~140 terminology collections (filter values)
list_classificationsThe 23 subject classifications, e.g. BILD = education
api_statusAvailability; never returns silently empty

All tools are annotated readOnlyHint: true, destructiveHint: false.

MCP primitives. This server uses only the Tools primitive. TERMDAT answers are live queries with no stable resource hierarchy to expose as Resources, and there are no server-authored Prompts. The seven tools are small and closely related, so they live in a single server.py rather than a tools/ package.

Architecture

┌─────────────────┐  stdio / HTTP    ┌──────────────────────────┐│  MCP host       │ ───────────────► │  termdat-mcp             ││  (Claude, IDE)  │ ◄─────────────── │                          │└─────────────────┘                  │  vocabulary cache (24 h) │                                     │  140 collections         │                                     │   23 classifications     │                                     └────────────┬─────────────┘                                                  │ httpx + retry (2/4/8 s)                                                  ▼                              https://api.termdat.bk.admin.ch/v2                              ├── /Search          (SearchTerm + InLanguageCode)                              ├── /Entry           (EntryIds)                              ├── /Collection      (140 values)                              └── /Classification  ( 23 values, incl. BILD)

Architecture decision

This server uses Architecture A (live API only), with caching limited to the two controlled vocabularies.

Rationale (verified live on 2026-07-19):

  • The API publishes a complete OpenAPI 3.0.4 specification at /swagger/v2/swagger.json and declares no security schemes — unauthenticated access, No-Auth-First satisfied.
  • Server-side search works properly, including 11 field flags and filters by collection and classification. There is no reason to mirror the database locally, and no bulk dump is offered.
  • /Collection (140 entries) and /Classification (23 entries) change rarely and are needed to make filter arguments legible to an agent, so they are cached with a 24-hour TTL and a stale-serve fallback.

Consequences:

  • Every search is a live call; provenance is live_api except for vocabulary lookups.
  • Validation errors arrive as clean RFC 9110 payloads and are surfaced rather than swallowed.

Project Structure

termdat-mcp/├── src/termdat_mcp/│   ├── __init__.py│   ├── __main__.py       # entry point; dual transport (stdio / Streamable HTTP)│   ├── client.py         # httpx client, retry, vocabulary cache│   ├── models.py         # Pydantic models│   └── server.py         # MCP tool definitions├── tests/│   ├── test_client.py    # offline, respx-mocked│   └── test_live.py       # hits the real TERMDAT API├── README.md├── README.de.md├── CHANGELOG.md├── LICENSE└── pyproject.toml

Safety & Limits

  • Read-only. Every tool is annotated readOnlyHint: true, destructiveHint: false; the server never writes to TERMDAT.
  • No credentials handled. The API is unauthenticated; the server stores and forwards no secrets.
  • No silent empties. api_status and error paths surface failures instead of returning an empty result that looks complete.
  • Truncation is explicit. MaxEntryCount is always sent and truncated is reported (see Known Limitations).
  • Terms of use are stated, not guessed. Every response repeats them in source: reuse and republication require the source www.termdat.ch to be named, and the Federal Chancellery's Terminology Section to be informed of purpose and manner beforehand (statement of 2026-08-21).
  • Egress allow-list. Requests can only reach api.termdat.bk.admin.ch (HTTPS), enforced before every call by a frozen ALLOWED_HOSTS set — no user input can redirect egress. See docs/network-egress.md.
  • Loopback by default. The HTTP transport binds to 127.0.0.1; 0.0.0.0 is an explicit container opt-in that warns on stderr. It also sets default-deny CORS, exposing only Mcp-Session-Id.
  • Errors are masked. Upstream/internal error detail is logged to stderr (structlog JSON) and never returned to the model.
  • Accepted risks (ADRs): DNS pinning (ADR 0001) and stateful load balancing (ADR 0002) are deliberately deferred — low risk for a single-instance, single-host, no-auth server.
  • Container. A hardened, non-root Dockerfile is provided for hosted deployments.

Known Limitations

  • Administrative scope only. See the coverage table above. check_terms returns not_found, never «incorrect», precisely because absence from TERMDAT is not evidence of error.
  • MaxEntryCount has a silent default of ~25. Omitting it looks like a complete result set. This server always sends the parameter explicitly and reports truncated.
  • CollectionIds / ClassificationIds have a silent default of VARIA. An ID-less /v2/Search covers one of 23 subject areas — the residual one — and reports the truncated result as a normal empty answer. This server sends the full classification set unless you narrow it explicitly. See issue #11.
  • SearchTerm is Lucene, and matching is on whole words. «Quellensteuer» does not match «Quellensteuerverordnung»; «Quellensteuer*» does. *, ? and ~ are available — on an empty result, retry with a wildcard before concluding the term is absent.
  • Field.* flags default to true where unsent. Terminus, Name, Abbreviation and Phraseology are on unless explicitly disabled, so a partial flag set can only widen a search. This server sends all eleven flags explicitly, which is what makes fields able to narrow.
  • Multilingual variants are opt-in. Without OutLanguageCode, entries return German designations only. translate_term sets it for you.
  • The public API exposes less than the website — deliberately. Not a scope setting, and not a defect: asked directly, the Federal Chancellery's Terminology Section confirmed on 2026-08-21 that the API covers only part of the TERMDAT records, that the selection follows the needs of the federal administration's translators, and that no fuller coverage is planned. Measured beforehand: for «Quellensteuer» the website lists 12 distinct entries and the API returns 7 at maximum recall (every language, all 11 fields, infix wildcard, all classifications and collections); the overlap is one entry, 447912. Fetching the missing IDs directly via /v2/Entry returns HTTP 200 with an empty body — they are not served at all, so no query can reach them. Consequence: absence from this server means absence from the API, not from TERMDAT, and there is nothing to work around. Verified 2026-07-30 with the entry IDs supplied by @dfch in issue #11.
  • The I14Y catalogue record carries license: null — the terms are elsewhere. Reuse and republication of TERMDAT content are permitted only with the source www.termdat.ch named, and the Terminology Section of the Federal Chancellery informed of purpose and manner beforehand ([email protected]; statement of 2026-08-21). Running this server is covered by the enquiry that produced that statement; your own downstream republication needs its own notice. Every response repeats the terms in source.
  • Entry-level language coverage varies. Not every entry exists in all four languages; translate_term omits entries without a target-language variant rather than inventing one.

Live probe findings (2026-07-19)

EndpointHTTPStatusNote
/swagger/v2/swagger.json200✅OpenAPI 3.0.4, 132 KB, securitySchemes: []
/v2/Search200✅requires SearchTerm, InLanguageCode, ReturnType
/v2/Entry200✅requires EntryIds, InLanguageCode
/v2/Collection200✅140 values
/v2/Classification200✅23 values, incl. BILD (education)
/v2/ (root)404❌no index; the I14Y record points here
InLanguageCode=deu / de-CH400❌only two-letter ISO codes, case-insensitive

Probe note: a correction worth recording. An earlier probe concluded that OutLanguageCode filters the result set, because adding it appeared to drop all hits. It does not. Two variables had been changed at once — the parameter and the search term — and the term itself («Volksschule») genuinely has zero hits. Verified afterwards across four broad terms: result counts are identical with and without OutLanguageCode; the parameter is purely additive. A regression test (test_out_language_is_additive_not_filtering) now guards this.

Rule of thumb: change one variable per probe call, or the API will confess to a crime it did not commit.

Live probe findings (2026-07-27) — search scope

Reported in issue #11: «Quellensteuer» returned nothing while the TERMDAT website returned twelve hits. Three independent causes, in descending order of effect. Entry counts, InLanguageCode=DE:

QueryID-less (=VARIA)all 23 classifications+ free-text fields+ * wildcard
Quellensteuer0136
Pensionskasse142227

The first column is what this server sent before the fix. The VARIA default is the dominant term: it hid FINANZWESEN, RECHT and twenty other subject areas behind an answer that looked like a confident zero.

Probe note. The failure mode worth recording is not the count — it is that an under-scoped search is indistinguishable from a genuine absence. In the reported session the model read the empty result together with this server's own «absence usually means out of scope» caveat and invented a plausible explanation for a term that was in the database all along. A tool that narrows silently will be believed silently. Hence hint on empty results, and a caveat that now tells the model to retry rather than to conclude.

Project Phase

This server is in Phase 1 (read-only). All tools are annotated readOnlyHint: true / destructiveHint: false and only ever query the public TERMDAT v2 API — there are no write, send, or filesystem capabilities.

PhaseScopeStatus
1 — Read-onlySearch, translate and check administrative designations✅ current
2 — Write-capable(none planned)—
3 — Multi-agent(none planned)—

A transition to a later phase would require a re-audit and human-in-the-loop controls before any write-capable tool is added.

MCP Protocol Version

This server speaks two protocol eras over the same endpoint, on both transports. The client's first request on a connection decides which one applies; a later claim from the other era is refused.

EraRevisionWho reaches it
initialize handshake2024-11-05 … 2025-11-25What 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 envelope2026-07-28A request carrying the 2026-07-28 _meta envelope opens a modern connection.

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.

That pin alone once hid a real gap. Until 18.09.2026 the network transport was the SDK's SSE app, which has no branch into the modern era at all — over HTTP, 2026-07-28 was unreachable, while the constants said otherwise and stdio served it fine. Since the switch to Streamable HTTP on /mcp, both eras are also measured: tests/test_streamable_http.py sends real requests of both kinds through the assembled ASGI stack and checks the negotiated revision, the mandatory resultType, and the -32022 rejection of an unknown modern revision.

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.

Testing

bash
PYTHONPATH=src pytest tests/ -m "not live"   # offline, respx-mockedPYTHONPATH=src pytest tests/ -m live         # hits the real APIpython scripts/check_ruff_pin.pyruff check src/ tests/ scripts/ruff format --check src/ tests/ scripts/python scripts/check_version_sync.py

Changelog

See CHANGELOG.md.

Contributing

Issues and pull requests are welcome. Please keep tools read-only, run ruff check and the offline test suite before submitting, and add a CHANGELOG.md entry under [Unreleased] for user-facing changes.

Maintainers: see PUBLISHING.md for the step-by-step PyPI release process (Trusted Publishing via GitHub Release).

Security

See SECURITY.md for the security posture, hardening controls, and how to report a vulnerability.

License

MIT for this server — see LICENSE. TERMDAT content remains subject to the Federal Chancellery's terms: name the source www.termdat.ch and inform the Terminology Section beforehand of purpose and manner of any reuse or republication (statement of 2026-08-21).

Author

Hayal Oezkan · github.com/malkreide

Credits & Related Projects

Source: README.md at commit 59394ba

Tools

0
SourceWeft has not started this package in its sandbox yet, so its tools are not known.

Version history

2
  1. v0.2.0LatestSep 19, 2026
  2. v0.1.3Sep 16, 2026

Score

50% of the applicable checks met

How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.

  • Reached in the last 30 days25 pts

    Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.

  • Auth declared10 pts

    Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.

  • Tools listed15 pts

    Answer tools/list without a credential, or ship a package that starts in a sandbox without one.

  • DescriptionRequired15 pts
  • Logo10 pts
  • Documentation15 pts
  • License10 pts