Repository navigation
[0.2.0] — 2026-09-19
⚠️ Breaking fuer gehostete Deployments. Der Netz-Endpunkt ist/mcp
(Streamable HTTP) statt/sse; der alte Pfad antwortet mit 404. Die
EinstellungTERMDAT_MCP_TRANSPORTbleibt unveraendert gueltig —ssegilt
als Alias, bedient/mcpund warnt beim Start. Clients umstellen.stdio-Nutzung (Claude Desktop,
uvx termdat-mcp) ist nicht betroffen.
Minor statt Patch, obwohl das Schema 0.x fuehrt: Der Endpunktwechsel bricht
bestehende Netz-Clients, und eine Patch-Nummer haette das verschwiegen.
Geaendert
-
Der Netz-Transport ist Streamable HTTP unter
/mcp, nicht mehr SSE unter
/sse. Gemessen am 18.09.2026 gegenmcp2.2.0: Einetools/list-Anfrage
mit dem2026-07-28-Envelope bekam gegen die bisherige App/sse→ 405,
/mcp→ 404,/messages/→ 400; gegenstreamable_http_app()antwortet
dieselbe Anfrage mit 200. Die Weiche in die moderne Protokoll-Aera steht in
StreamableHTTPSessionManager._handle_request;mcp/server/sse.pyerwaehnt
sie nirgends. Der Server erreichte ueber das Netz also nur die Handshake-Aera
— die Spec-Revision, die beide READMEs seit Monaten als bedient auswiesen,
war fuer jeden HTTP-Client unerreichbar.Unbemerkt blieb das aus zwei Gruenden, die einander deckten: stdio bedient
die moderne Aera tatsaechlich (ebenfalls nachgemessen,python -m termdat_mcp
antwortet auf denselben Envelope), und die Suite pinnte die Revision nur
gegen SDK-Konstanten. Eine Konstante, die2026-07-28sagt, sagt nichts
darueber, ob eine Anfrage dort ankommt. -
TERMDAT_MCP_TRANSPORT=ssebleibt gueltig, bedient aber/mcpund warnt
beim Start — auf stderr und im Log. Den Wert abzulehnen waere sauberer und
zugleich der Bruch, der jedes bestehende Deployment beim naechsten Start
anhaelt. Deployments muessen ihre Clients auf/mcpumstellen; der alte
Pfad antwortet mit 404. DasDockerfilesetzt jetztstreamable-http. -
CORS gibt frei, was der Endpunkt tatsaechlich annimmt.
DELETE(die
ausdrueckliche Beendigung einer Session) undLast-Event-ID(Wiederaufnahme
eines abgerissenen Streams) fehlten in der Freigabeliste — unter SSE gab es
beides nicht.tests/test_cors.pyliest die Methodenliste jetzt aus dem
Allow-Header einer echten 405-Antwort des SDK statt aus der eigenen
Konstante: Der parametrierte Test daneben zieht seine Faelle aus derselben
Liste und haette das Streichen vonDELETEmitgestrichen.
Hinzugefuegt
-
tests/test_streamable_http.py— beide Protokoll-Aeren, gemessen statt
gepinnt. Echte HTTP-Anfragen durch den zusammengebauten ASGI-Stack: der
Handshake antwortet fuer jede der vier Revisionen2024-11-05…2025-11-25
mit der angefragten und deckelt eine zu neue bei2025-11-25; der Envelope
oeffnet die moderne Aera, deren Antwort den Pflicht-resultTypeund die
Cache-Hinweise traegt; eine unbekannte moderne Revision wird mit-32022
samtsupported/requestedabgewiesen, ein Envelope-loser Aufruf mit
-32602, ein Header, der dem Body widerspricht, mit-32020.Gegenprobe gefahren: Mit
mcp.sse_app()an derselben Stelle fallen 14 der 15
Zusicherungen. Gruen bleibt allein die, die die Deprecation-Warnung prueft —
die haengt nicht am Transport.
Behoben
-
Die Cache-Hinweise nach SEP-2549 erreichten ueber das Netz keine Leitung.
server.pysetztCACHE_HINTSseit laengerem, undtests/test_cache_hints.py
belegt sie ueber eine In-Process-ClientSession. Ueber HTTP kamen sie
trotzdem nirgends an: Die Handshake-Aera siebtttlMsundcacheScopeaus
dem Resultat (ihr Schema kennt die Felder nicht, gemessen: die
tools/list-Antwort traegt aussertoolskein einziges Feld), und die
moderne Aera, in der die Felder stehen, war unerreichbar. Seit dem Wechsel
antwortettools/listmitttlMs: 300000undcacheScope: "public". -
Der Server behauptete, die Nutzungsbedingungen seien unbekannt. Das Feld
sourcejeder Antwort sagte «The I14Y catalogue record carries no explicit
licence statement — clarify terms with the Federal Chancellery before
republishing». Der Katalogeintrag fuehrt tatsaechlichlicense: null, die
Bedingungen gibt es trotzdem: Auf Anfrage hat die Sektion Terminologie der
Bundeskanzlei am 21.08.2026 geantwortet, dass Weiterverwendung und
Weiterveroeffentlichung nur unter Angabe der Quellewww.termdat.chzulaessig
sind und die Sektion vorgaengig ueber Zweck und Art zu informieren ist. Eine
Bedingung, die niemand nennt, kann niemand erfuellen — der Hinweis schob die
Klaerung jedem Einzelnen zu, statt sie einmal einzuholen.ATTRIBUTIONnennt
jetzt beide Auflagen; die Auskunft steht im Wortlaut in
docs/bk-auskunft-2026-08-21.md. -
Die Erklaerung der Abdeckungsluecke war eine Vermutung, formuliert wie ein
Befund. READMEs und Tool-Beschreibung leiteten aus einem einzigen
ausgelieferten Entwurfseintrag (1557) ab, «der Suchindex deckt validierte
Eintraege ab». Die Bundeskanzlei nennt ein anderes Kriterium: Die Auswahl
orientiert sich am Bedarf der Uebersetzerinnen und Uebersetzer der
Bundesverwaltung, die Teilabdeckung ist beabsichtigt und eine vollstaendigere
Abdeckung ist nicht geplant. Damit faellt auch der Satz weg, das sei «eher ein
Fall fuer eine Rueckfrage als fuer einen Workaround» — die Rueckfrage ist
beantwortet, einen Workaround gibt es nicht. -
check_termssetzte «nicht gefunden» mit «nicht in TERMDAT» gleich. Der
caveatsagte woertlich «'not_found' means 'not in TERMDAT'», und derselbe
Kurzschluss stand imhintauf Leermengen («absent from TERMDAT»). Gemessen
ist nur die API, und die ist erklaertermassen ein Ausschnitt: Beide Stellen
sagen jetzt «not in the public API» und verweisen aufwww.termdat.ch. -
Tests fuer diese Texte.
tests/test_terms_of_use.pyprueft, dass die
Quellenangabewww.termdat.chund die Meldepflicht nennt, dass sie in einer
echten Antwort ankommt und dass wedercaveatnochhinteine Leermenge
wieder zu einer Aussage ueber TERMDAT machen. Die Texte sind hier das Produkt
— das Modell sieht nie die Auskunft, nur diese Felder. -
Browser-Clients scheiterten am Preflight. Spec
2026-07-28routet eine
Anfrage überMcp-Method,Mcp-NameundMcp-Protocol-Version; die
CORS-Freigabeliste nannte keinen davon, dafür mitMcp-Session-Idden
Session-Header, der für sich genommen keine Anfrage routet. Ein Browser darf
einen nicht safelisteten Header nicht senden, wenn der Server ihn nicht
nennt: die Anfrage starb vor dem ersten MCP-Byte, während stdio und Python,
für die kein Preflight gilt, weiterliefen. Deshalb war nichts rot.
Hinzugefügt
-
build_http_app(), herausgezogen aus_run_sse. Solange Aufbau und
uvicorn.runin derselben Funktion standen, liess sich die CORS-Schicht nur
lesen, nicht ausprobieren — und eine gelesene Freigabeliste kann vollständig
aussehen und trotzdem nie an der Middleware ankommen._run_sseruft die
neue Funktion auf; am Verhalten ändert sich nichts. -
Frischehinweise auf
tools/listundserver/discover(SEP-2549, Spec
2026-07-28):ttlMs300000,cacheScopepublic. Das SDK setzt sonst
«sofort veraltet, nie geteilt» und lässt damit jeden Client bei jeder
Verbindung neu auflisten — für eine Liste, die beim Import feststeht. -
Die Dokumentation nannte ein schmaleres Lint-Gate als die CI faehrt.
README.md,README.de.md,CONTRIBUTING.mdundCONTRIBUTING.de.md
empfahlenruff check src tests. Die CI faehrt fuenf Gates, darunter
scripts/im Lint-Pfad,ruff format --checkund die beiden Pruefskripte.
Wer der Dokumentation folgte, bekam gruen und danach eine rote CI, deren
Ursache nicht im Diff stand. Alle vier Stellen nennen jetzt die Gates
woertlich ausci.yml. -
Die READMEs nannten einen
mcp-Pin, den es nicht mehr gibt. Dort stand
>=1.2.0;pyproject.tomlfordert seit dem 2.x-Bruch>=2.0.0,<3. Genau
diese Untergrenze ist der Grund, warum ein frischer Install ueberhaupt
laeuft — als>=1.2.0gelesen wirkt sie wie eine unverbindliche Notiz. -
Die Pruefsummen im Fixture-Nachweis waren Zierde.
PROVENANCE.mdfuehrt
je Datei einen SHA-256 — um genau einen Fall zu fangen: eine Aufzeichnung,
die nach dem Lauf von Hand nachgebessert wurde. Eine korrigierte Antwort ist
wieder eine erfundene, und von aussen ist ihr das nicht anzusehen.
Nachgerechnet hat sie kein Test.test_die_pruefsumme_im_nachweis_stimmt
tut es jetzt, ueber die Bytes auf der Platte statt ueber den Loader — genau
die hat der Recorder gehasht. -
detail=Falseliefert Treffer ohne jede Benennung — und sagte es nicht.
Der Parameter hängt am Werkzeugtermdat_search, war im Docstring aber mit
keinem Wort erklärt. Er setztReturnType=Summary; die Quelle lässt dann
languageDetailsweg, undflatten_entrymacht darausvariants: []. Die
Treffer behalten ID, URL, Status und Klassifikation — aber keine einzige
Benennung. Für einen Terminologie-Server ist das ein Eintrag ohne Begriff,
und ein Modell, das den Parameter zum Token-Sparen setzt, bekommt Treffer, aus
denen es nichts lesen kann.Der Docstring sagt es jetzt, mitsamt dem, wofür der Modus taugt (zählen,
nach Klassifikation filtern) und wofür nicht. -
Der Fixture-Dispatcher ordnete nach Pfad zu.
/Searchbekam immer
search_detail.json— auch für eine Summary-Abfrage. Der Unterschied, um den
es hier geht, war damit im Test unsichtbar: eine Abfrageform ohne
Aufzeichnung sah aus wie eine mit. Zugeordnet wird jetzt nachReturnType,
und ein unbekannter Wert fällt laut auf, statt still eine fremde Aufzeichnung
zu bekommen. -
Protokoll-Gate: beide Spec-Aeren gepinnt und geprueft
(tests/test_protocol_version.py).mcp2.x bedient zwei Aeren ueber
denselben Server — deninitialize-Handshake, der bei2025-11-25
deckelt, und den Pro-Request-Envelope, der2026-07-28erreicht.
LATEST_PROTOCOL_VERSIONist ein Alias auf die moderne Aera; wer nur
dagegen pinnt, laesst genau die Aera frei wandern, die heutige Clients
aushandeln. Beide sind jetzt einzeln gepinnt, ein Dependabot-Bump von
mcpkann keine davon still verschieben.Ohne gemessenen Teil: dieser Server baut keine ASGI-App, durch die sich ein
initializeschicken liesse. Das Gate haengt deshalb an den SDK-Konstanten —
die schwaechere Form, im Docstring benannt statt verschwiegen.Beide READMEs beschreiben die Aeren; ein Test haelt jede Sprache einzeln
dagegen — im Portfolio sind EN und DE desselben Repos schon dreimal
auseinandergelaufen, weil nur eine Fassung nachgezogen wurde. -
Mcp-Session-Idist weiterhin freigegeben — und das steht jetzt in einem
Test statt in einem Satz. Der Docstring vontests/test_cors.pynannte den
Header die Spur einer Mechanik, die2026-07-28abgeschafft habe. Das stimmt
nicht:mcp2.x bedient beide Protokoll-Aeren, die Session gehoert zur
Handshake-Aera, und der Server gibt den Header nicht ohne Grund auch in
expose_headersfrei.Nachgemessen statt aus Spec-Text geschlossen:
MCP_SESSION_ID_HEADERsteht
unveraendert inmcp/server/streamable_http.py, und ein echterinitialize
durch den zusammengebauten ASGI-Stack bekommt eine Session-ID im
Antwort-Header zurueck.test_der_session_header_ist_weiterhin_freigegebenhaelt beides fest. Die
Gegenprobe zeigt, dass es die Luecke wirklich gab: nimmt man den Header aus
der Freigabeliste, faellt genau dieser eine Test, und die sieben bestehenden
bleiben gruen.
Hinzugefügt
-
SessionStart-Hook, der einen veralteten Klon meldet.
.claude/hooks/session-start-stale-clone.shsagt beim Sessionstart, wie
viele Commits der ausgecheckte Stand hinterorigin/<default-branch>liegt;
bei 0 schweigt er. Ein veralteter Klon hat am 3.8.2026 zweimal eine rote CI
erzeugt, deren Ursache nicht im Diff stand — es fehlten jeweils genau die
Commits, die das Gate einführten, an dem der Branch scheiterte. Der
Default-Branch wird übergit ls-remote --symrefermittelt, nicht alsmain
angenommen: drei Server im Portfolio heissen ihnmaster.Wichtiger als die Meldung ist, dass der Hook die Session niemals
blockiert — ein Hook, der bei Netzproblemen die Arbeit anhält, wird nach dem
zweiten Mal abgeschaltet und schützt danach gar nichts. Kein Netz, kein
Remote, detached HEAD, kein Git-Repo: alles endet still mitexit 0, jede
Netzoperation unter einer Zeitgrenze von 5 Sekunden.
tests/test_session_start_hook.pyfährt das Skript gegen echte
Wegwerf-Repos und weist die Zeitgrenze an einem künstlich hängendengit
nach — beide Zweige, mittimeout-Binary und ohne. Die Gegenprobe über
diesen Zweig fand einen echten Fehler: ohne Job-Control landete der
Hintergrundjob in der Prozessgruppe des Hooks, der Kill traf nurgit
selbst, und ein Enkelprozess hielt die stdout-Pipe die vollen 30 Sekunden
offen. -
search_summary.json— die zweite Abfrageform von/Search, echt
aufgezeichnet.test_die_beiden_suchformen_sind_wirklich_verschiedenhält
fest, dass sie sich in genau dem Feld unterscheiden, aus dem die Benennungen
kommen; ohne diesen Nachweis belegte die zweite Datei nichts.
test_eine_summary_suche_liefert_treffer_ohne_jede_benennunghält den Stand
fest und fällt, wenn die Quelle im Summary doch Benennungen liefert — dann
gehört der Warnsatz im Docstring gestrichen.
Changed
- Der Backoff-Schlaf wird ueber einen Modul-Alias gepatcht, nicht ueber
asyncio.sleep. Die Tests nullten die Wartezeit mit
monkeypatch.setattr(<modul>.asyncio, "sleep", ...). Das liest sich lokal,
ersetztsleepaber auf dem geteilten Modulobjekt — fuer httpx, respx,
pytest-asyncio und jeden anderen Importeur im Prozess. Das Modul legt die
Naht jetzt als_sleep = asyncio.sleepoffen; gepatcht wird diese.
test_der_retry_geht_ueber_den_aliashaelt sie: umgeht der Retry den Alias,
faellt der Test in Sekundenbruchteilen. Ohne ihn fiel gar nichts — die Suite
wurde nur ein Vielfaches langsamer, und eine laengere Laufzeit ist kein
Signal, das jemand liest.
Behoben
-
Der 20-Sekunden-Deckel war keine Grenze. Gedeckelt wurde vor dem
Jittern, also wurde ein aufMAX_DELAY_Sgedeckelter Wert anschliessend mit
bis zu 1.5 multipliziert: exponentielle Wartezeiten bis 30 s,
Retry-After-Wartezeiten bis 25 s. Neu wird nach dem Jittern gedeckelt. -
Das Gesamtbudget war nicht garantiert.
httpxwendet sein Timeout pro
Operation an, und das Read-Timeout beginnt mit jedem Chunk von vorn — eine
langsam troepfelnde Antwort konnte das Budget ueberdauern, ohne dass ein
einzelner Read ablief. Neu liegt eineasyncio.wait_for-Deadline um den
Request. (asyncio.timeoutlaese sich besser, kam aber erst in 3.11; dieses
Paket unterstuetzt weiterhin 3.10.)Beide Befunde stammen aus einem Codex-Review an
parlament-mcp#35. Der Test
zur Deadline musste die echteasyncio.sleepfesthalten, bevor die
autouse-_no_sleep-Fixture ausconftest.pysie ersetzt — sonst waere auch
dieser Test gruen gewesen, ohne etwas zu pruefen.
Added
-
Aufgezeichnete Fixtures, eine je externem Endpunkt, mit Nachweis.
tests/fixtures/haelt jetzt echte TERMDAT-Antworten fuer alle vier
Endpunkte, die dieser Server aufruft —/Search,/Entry,/Classification
und/Collection—, aufgezeichnet vonscripts/record_fixtures.py. Herkunft,
Datum, Auswahlregel und SHA-256 stehen je Datei in
tests/fixtures/PROVENANCE.md, wie im uebrigen Portfolio.
tests/test_recorded_fixtures.pyspielt sie durch den echten Client.Handgeschriebene Stubs kodieren die Annahme ihres Autors und koennen sie nicht
widerlegen: ini14y-mcpblieb genau deshalb die ganze Suite gruen, waehrend
drei Tools produktiv leere Titel lieferten. Fehlerpfade bleiben
handgeschrieben, die lassen sich nicht auf Zuruf aufzeichnen.Gegenprobe, jede Zusicherung einzeln neutralisiert: Aufnahmedatum entfernt ->
Datums-Check faellt; Fixture ohne PROVENANCE-Eintrag -> Vollstaendigkeits-Check
faellt; Aufzeichnung geloescht -> Abdeckungs-Waechter faellt;languageIsoCode
umbenannt -> der Mapper-Test faellt. -
Retry-Afterwird gelesen und schlaegt die eigene Backoff-Kurve (ARCH-014).
Bei 429 und 503 sagt TERMDAT im Header, wann es wieder mag — als Sekundenzahl
oder HTTP-Datum; beide Formen kommen vor, beide werden gelesen
(RFC 9110 §10.2.3). Wer stattdessen weiter seine Kurve faehrt, ignoriert eine
ausdrueckliche Angabe. Ein unbrauchbarer Header fuehrt zurueck auf die Kurve
statt zum Absturz — auf dem Fehlerpfad darf eine kaputte Kopfzeile nicht das
Letzte sein, woran der Client stirbt. -
Backoff ist gestreut (Jitter).
2**attemptwar deterministisch: Faellt
die API aus, waehrend mehrere Clients sie abfragen, retryen alle im
Gleichtakt, und die Last kommt als Welle zurueck — genau wenn die API sich
erholt. Exponentielle Wartezeiten landen jetzt in[0.5x, 1.5x]. Auf einem
Retry-Afterist die Streuung einseitig ([1.0x, 1.25x]): spaeter ist
hoeflich, frueher waere die Missachtung derselben Angabe, die man gerade
gelesen hat. -
Deckel von 20 s auf jede einzelne Wartezeit — gegen die unbegrenzt
wachsende Leiter und gegen einRetry-After, das die API senden darf, das man
aber nicht absitzen muss. -
Gesamtbudget von 25 s ueber den ganzen Aufruf (ARCH-014). Eine Anzahl
Versuche ist keine Grenze: Vier Versuche a 60 s Timeout plus Backoff sind
ueber vier Minuten, und die Zahl4sagt das nirgends. Entscheidender ist,
dass die massgebliche Grenze gar nicht uns gehoert — der Aufrufer hat sein
eigenes Timeout, und jenseits davon hoert niemand mehr zu: Die Arbeit laeuft
weiter, die Last landet bei TERMDAT, das Ergebnis geht ins Leere.Der Anker ist gemessen, nicht geschaetzt: Das Python-MCP-SDK setzt
MCP_DEFAULT_TIMEOUT = 30.0fuer allgemeine Operationen
(mcp/shared/_httpx_utils.py). 25 s lassen Luft fuer MCP-Framing,
Antwort-Parsing und die Tool-Schicht. Ein Test haelt die Beziehung fest und
schlaegt an, wenn das SDK seinen Default senkt.Geprueft wird vor jedem Versuch: Eine Wartezeit, die das Budget ueberdauern
wuerde, wird nicht mehr angetreten, und das Timeout eines einzelnen Versuchs
ist auf die verbleibende Zeit geklemmt. Log-Event und Meldung nennen neu,
welche Grenze gegriffen hat — «all 4 attempts used» und «25s budget
spent» verlangen verschiedene Antworten.Das Client-Timeout faellt von 60 s auf 25 s: Ein Wert oberhalb des Budgets
haette nur noch behauptet, was er nicht mehr gewaehrt.Die Abwaegung ist bewusst: Ein langsamer erster Versuch kann jetzt das Budget
aufbrauchen und laesst dann keinen Retry mehr zu. Genau das ist die
beabsichtigte Antwort — ein Retry, der nach dem Aufgeben des Aufrufers fertig
wird, bringt niemandem etwas und kostet TERMDAT eine Anfrage.