MCP Gateways, Registries and Conformance
Route through a stateless gateway, admit registry versions, catch drift, test the wire and gate releases.
- Tasks and commands
- Annotated code
- Diagrams
- Defaults and numbers
- Troubleshooting
- Questions and tips
Registry admission, drift, rollback#
controller = RegistryAdmissionController(clock=lambda: next(ticks))active_meta = {OFFICIAL_META_KEY: {"status": "active"}}controller.admit(sample_record("1.1.0"), active_meta, namespace_for_domain("example.com"),gives com.example evidence_for("1.1.0"), sample_live("1.1.0"))admit 1.0.0 the same waydrift = controller.check_live("com.example/inventory", "1.1.0", sample_live("1.1.0", True))extra tool: quarantinedcontroller.observe_registry_status("com.example/inventory", "1.1.0", "deprecated")controller.rollback("com.example/inventory", "1.0.0", "unexpected delete tool")controller.ledger.verify()False after any editnamespace_matches("com.exampleevil/inventory", "com.example")FalseConformance harness#
validate_result({"tools": []}, "legacy")infers completevalidate_result({"tools": []}, "modern")raises ProtocolViolationselect_era({"kind": "timeout"}, "fallback")fails closedselect_era({"kind": "jsonrpc_error", "code": -32021}, "fallback")moderncases = [run_transcript(case) for case in transcript_suite()]15 casescompare_sdk_view(raw_result, {"tools": []})droppedFields futureHintinspect_proxy(exchange)finds a 400 turned into 500redact(value)before hashing or uploadevaluate_health(healthy_window())samples, error rate, p95ReleaseGate(signers).evaluate(cases, [sdk_report], [proxy_report], healthy_window(), rollback)promote or rollbackGateway errors#
| Symptom | Cause | Fix |
|---|---|---|
401 -32001 | Bearer is not known | Authenticate the principal |
403 -32003 | Tool is not in RBAC for the user | Grant the role or keep the deny |
429 -32029 | TokenBucket is empty | Wait for refill |
409 -32010 | Live descriptor differs from the pin | Review, then re-approve the pin |
400 -32020 | Header and body disagree | Match Mcp-Method and Mcp-Name |
400 -32022 | Matched version is not supported | Send a version in supported |
406 on subscriptions/listen | Accept has no text/event-stream | Accept SSE |
| 405 | GET or DELETE on the endpoint | Send each message as POST |
Release failures#
| Symptom | Cause | Fix |
|---|---|---|
Client goes legacy after -32021 | Recognized modern error read as legacy | Never fall back after a modern error |
SDK shows resultType the wire lacked | SDK synthesizes the discriminator | Validate the raw body |
| Origin 400 arrives as 500 | Proxy collapses the error | Compare ingress, origin and egress |
| Canary has zero errors and no traffic | No minimum sample count | Enforce sampleCount above 0 |
| New tool appears after approval | Runtime drift | Quarantine the route |
| Rollback target was never admitted | Route and approval state disconnected | Refuse, run a new admission |
Gateway request path#
Run and verify#
| Command | What it does |
|---|---|
python3 code/ | Runs the demo from the lesson folder |
python3 -m unittest discover code/ | Runs the gateway tests (lesson 17) |
python3 -m unittest discover -s code/ | Runs the registry and conformance suites |
python3 -q | Opens a shell in code/ for the labs |
from main import * | Loads the lab functions in that shell |
Gateway tasks in code#
| Task | Code |
|---|---|
| Build a request and headers | make_request(method, |
| Send it as a principal | gateway.handle("bearer-alice", |
| Check the wire first | validate_wire(body, |
| Pin one descriptor | descriptor_digest(tool) |
| Rate limit a principal | TokenBucket().consume() |
| Admit a registry record | registry_record_is_admissible |
| Scope check in the capstone | gateway_call(token, |
Seven admission controls#
Defaults and numbers#
| Setting | Default | Note |
|---|---|---|
Gateway discover ttlMs | 30_000 | cacheScope private |
Gateway tools/list ttlMs | 10_000 | Private because RBAC filters it |
TokenBucket capacity | 5 | refill_per_second 1.0 |
maxErrorRate | 0.01 | Health gate default |
maxP95Ms | 500 | Health gate default |
MODERN_VERSION | 2026-07-28 | Needs resultType |
LEGACY_VERSION | 2025-11-25 | Only by allowlist and valid initialize |
| Registry schema | 2025-12-11 | Independent of the protocol version |
Rules#
- Never pass the outer gateway token to a backend.
- Do not copy outer client capabilities to a backend. The gateway is its client.
- Key rate limits by principal, issuer, resource, tool, cost class and window.
- Preserve
requestStatebyte for byte. Re-authorize every MRTR retry. - Route
tasks/get,tasks/update,tasks/cancelbyparams.taskIdinMcp-Name. - Run admission before a server appears in
tools/list. - Redact before serialization, hashing, logs or upload.
Questions#
- Does registry presence approve a server?
- No. Admission policy decides.
- Can
serverInfoprove the publisher? - No. It is diagnostic data only.
- Is an unknown field an unknown result?
- No. Keep the field. Reject an unknown
resultType. - Does rollback edit the old record?
- No. It changes the active route.
- Does a green capstone run prove the wire?
- No. It validates the simulation only.