mcp-trellis 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +41 -561
  2. package/dist/adapters/node.d.ts +14 -5
  3. package/dist/adapters/node.d.ts.map +1 -1
  4. package/dist/adapters/node.js +56 -29
  5. package/dist/adapters/node.js.map +1 -1
  6. package/dist/adapters/origins.d.ts.map +1 -1
  7. package/dist/adapters/origins.js +1 -4
  8. package/dist/adapters/origins.js.map +1 -1
  9. package/dist/app.d.ts +27 -4
  10. package/dist/app.d.ts.map +1 -1
  11. package/dist/app.js +31 -18
  12. package/dist/app.js.map +1 -1
  13. package/dist/auth/bearer.d.ts.map +1 -1
  14. package/dist/auth/bearer.js +6 -1
  15. package/dist/auth/bearer.js.map +1 -1
  16. package/dist/body.d.ts +48 -0
  17. package/dist/body.d.ts.map +1 -0
  18. package/dist/body.js +145 -0
  19. package/dist/body.js.map +1 -0
  20. package/dist/clients.d.ts.map +1 -1
  21. package/dist/clients.js +2 -5
  22. package/dist/clients.js.map +1 -1
  23. package/dist/dispatch.d.ts +1 -1
  24. package/dist/dispatch.d.ts.map +1 -1
  25. package/dist/dispatch.js +58 -18
  26. package/dist/dispatch.js.map +1 -1
  27. package/dist/http.d.ts.map +1 -1
  28. package/dist/http.js.map +1 -1
  29. package/dist/jsonrpc.d.ts +1 -0
  30. package/dist/jsonrpc.d.ts.map +1 -1
  31. package/dist/jsonrpc.js +1 -0
  32. package/dist/jsonrpc.js.map +1 -1
  33. package/dist/methods.d.ts +38 -15
  34. package/dist/methods.d.ts.map +1 -1
  35. package/dist/methods.js +38 -22
  36. package/dist/methods.js.map +1 -1
  37. package/dist/oauth/authorize.d.ts.map +1 -1
  38. package/dist/oauth/authorize.js +100 -68
  39. package/dist/oauth/authorize.js.map +1 -1
  40. package/dist/oauth/base64url.d.ts +1 -1
  41. package/dist/oauth/base64url.d.ts.map +1 -1
  42. package/dist/oauth/base64url.js +1 -4
  43. package/dist/oauth/base64url.js.map +1 -1
  44. package/dist/oauth/clientauth.d.ts +7 -2
  45. package/dist/oauth/clientauth.d.ts.map +1 -1
  46. package/dist/oauth/clientauth.js +59 -16
  47. package/dist/oauth/clientauth.js.map +1 -1
  48. package/dist/oauth/codes.d.ts +4 -4
  49. package/dist/oauth/codes.d.ts.map +1 -1
  50. package/dist/oauth/codes.js +43 -51
  51. package/dist/oauth/codes.js.map +1 -1
  52. package/dist/oauth/consent.d.ts +65 -0
  53. package/dist/oauth/consent.d.ts.map +1 -0
  54. package/dist/oauth/consent.js +173 -0
  55. package/dist/oauth/consent.js.map +1 -0
  56. package/dist/oauth/constants.d.ts +4 -0
  57. package/dist/oauth/constants.d.ts.map +1 -1
  58. package/dist/oauth/constants.js +4 -0
  59. package/dist/oauth/constants.js.map +1 -1
  60. package/dist/oauth/metadata.d.ts +6 -6
  61. package/dist/oauth/metadata.d.ts.map +1 -1
  62. package/dist/oauth/metadata.js.map +1 -1
  63. package/dist/oauth/pkce.d.ts.map +1 -1
  64. package/dist/oauth/pkce.js +1 -1
  65. package/dist/oauth/pkce.js.map +1 -1
  66. package/dist/oauth/redirect.d.ts +6 -1
  67. package/dist/oauth/redirect.d.ts.map +1 -1
  68. package/dist/oauth/redirect.js +23 -10
  69. package/dist/oauth/redirect.js.map +1 -1
  70. package/dist/oauth/reqbody.d.ts +10 -0
  71. package/dist/oauth/reqbody.d.ts.map +1 -0
  72. package/dist/oauth/reqbody.js +29 -0
  73. package/dist/oauth/reqbody.js.map +1 -0
  74. package/dist/oauth/resource.d.ts +30 -0
  75. package/dist/oauth/resource.d.ts.map +1 -1
  76. package/dist/oauth/resource.js +38 -8
  77. package/dist/oauth/resource.js.map +1 -1
  78. package/dist/oauth/revoke.d.ts.map +1 -1
  79. package/dist/oauth/revoke.js +15 -3
  80. package/dist/oauth/revoke.js.map +1 -1
  81. package/dist/oauth/router.d.ts +14 -11
  82. package/dist/oauth/router.d.ts.map +1 -1
  83. package/dist/oauth/router.js +62 -22
  84. package/dist/oauth/router.js.map +1 -1
  85. package/dist/oauth/scope.d.ts +11 -2
  86. package/dist/oauth/scope.d.ts.map +1 -1
  87. package/dist/oauth/scope.js +18 -7
  88. package/dist/oauth/scope.js.map +1 -1
  89. package/dist/oauth/sealed.d.ts +17 -0
  90. package/dist/oauth/sealed.d.ts.map +1 -0
  91. package/dist/oauth/sealed.js +57 -0
  92. package/dist/oauth/sealed.js.map +1 -0
  93. package/dist/oauth/secrethash.d.ts +44 -0
  94. package/dist/oauth/secrethash.d.ts.map +1 -0
  95. package/dist/oauth/secrethash.js +66 -0
  96. package/dist/oauth/secrethash.js.map +1 -0
  97. package/dist/oauth/token.d.ts.map +1 -1
  98. package/dist/oauth/token.js +13 -34
  99. package/dist/oauth/token.js.map +1 -1
  100. package/dist/oauth/types.d.ts +106 -9
  101. package/dist/oauth/types.d.ts.map +1 -1
  102. package/dist/oauth/types.js +67 -4
  103. package/dist/oauth/types.js.map +1 -1
  104. package/dist/oauth/wellknown.d.ts.map +1 -1
  105. package/dist/oauth/wellknown.js +6 -4
  106. package/dist/oauth/wellknown.js.map +1 -1
  107. package/dist/protocol.d.ts +15 -1
  108. package/dist/protocol.d.ts.map +1 -1
  109. package/dist/protocol.js +16 -6
  110. package/dist/protocol.js.map +1 -1
  111. package/dist/registry.d.ts +4 -4
  112. package/dist/registry.d.ts.map +1 -1
  113. package/dist/registry.js +15 -7
  114. package/dist/registry.js.map +1 -1
  115. package/dist/server.d.ts +14 -13
  116. package/dist/server.d.ts.map +1 -1
  117. package/dist/server.js +8 -8
  118. package/dist/server.js.map +1 -1
  119. package/dist/tools.d.ts +11 -2
  120. package/dist/tools.d.ts.map +1 -1
  121. package/dist/tools.js +14 -13
  122. package/dist/tools.js.map +1 -1
  123. package/dist/validate.d.ts +16 -1
  124. package/dist/validate.d.ts.map +1 -1
  125. package/dist/validate.js +46 -8
  126. package/dist/validate.js.map +1 -1
  127. package/docs/ROADMAP.md +295 -0
  128. package/docs/guide.md +417 -0
  129. package/docs/reference.md +178 -0
  130. package/docs/security.md +63 -0
  131. package/package.json +6 -3
@@ -0,0 +1,295 @@
1
+ # mcp-trellis — roadmap
2
+
3
+ Status: 2026-08-21. Written against MCP spec revision `2026-07-28`.
4
+
5
+ ## What this library is
6
+
7
+ > **The easy way to build a secure MCP server that works with Claude, Gemini,
8
+ > and Codex** — on any Web-standard runtime, with no database and no vendor.
9
+
10
+ The differentiating axis is **clients**, not runtimes. Runtime portability is
11
+ table stakes; the official SDK has it too. What nobody in TypeScript ships is
12
+ both halves — the MCP handler *and* a self-hosted OAuth authorization server —
13
+ in one package you can drop into a Next.js route or a Deno server.
14
+
15
+ | | Portable runtime | Self-hosted | AS included | Zero-dep |
16
+ |---|---|---|---|---|
17
+ | Official SDK v2 | yes | yes | **no** | no |
18
+ | `workers-oauth-provider` | Workers only | yes | yes | — |
19
+ | Auth0 / Authlete / Clerk / WorkOS | yes | **no** (SaaS) | yes | — |
20
+ | Better Auth | yes | yes | yes, but full stack + DB | no |
21
+ | **mcp-trellis** | yes | yes | yes | yes |
22
+
23
+ The official SDK explicitly does not provide an authorization server — it offers
24
+ SDK-level opt-ins and expects you to bring the infrastructure. That gap is the
25
+ product.
26
+
27
+ ### The rule that decides design questions
28
+
29
+ **If a user can forget to do something and the result is a security hole, it
30
+ goes in the code, not the README.** Audience validation moved into
31
+ `createMcpApp` for exactly this reason.
32
+
33
+ ### The acceptance test
34
+
35
+ From `npm install` to a connected client: **under 15 lines and one secret.**
36
+ Anything that forces the user to route requests, read an RFC, or install a
37
+ second package fails.
38
+
39
+ ---
40
+
41
+ ## Shipped
42
+
43
+ **Phase 0 — trust.** Query-string tokens now cover RFC 6750's `access_token`;
44
+ audit fires on every denial including transport-level ones, and a throwing audit
45
+ port can no longer fail a request; tool exceptions are redacted by default;
46
+ a relative `loginUrl` resolves instead of throwing; explicit `id: null` returns
47
+ a proper JSON-RPC error.
48
+
49
+ **Phase 1 — the app layer and the Gemini unblock.**
50
+
51
+ - `createMcpApp` — one call mounts tools, the AS, both discovery documents, and
52
+ the routing between them. `createMcpHandler` / `createOAuthRouter` stay
53
+ exported for anyone who wants to compose the pieces.
54
+ - **Audience validation is enforced by the library.** `verifyToken` returns the
55
+ token's audience; `createMcpApp` compares it against `canonicalResource` and
56
+ rejects mismatches before any tool runs.
57
+ - **Client profiles** (`claude` / `gemini` / `codex`) compose the redirect
58
+ allowlist and the advertised token-endpoint auth methods. Client-specific
59
+ knowledge is the asset, so it lives in the library.
60
+ - **Confidential clients and pre-registration** via a `clientStore` port —
61
+ `client_secret_basic` and `client_secret_post`. This is what Gemini Enterprise
62
+ needs, and it was previously impossible: the AS advertised `["none"]` only.
63
+ Secret storage and comparison stay in the port; credentials never enter the
64
+ library.
65
+ - **Naming only confidential clients is enforced, not just advertised.**
66
+ The first cut of this let `clients: ["gemini"]` configure the AS's metadata
67
+ while leaving DCR and the loopback allowlist wide open — a self-registered
68
+ public client could still complete a normal PKCE flow and get a token,
69
+ quietly defeating the "confidential clients only" intent. Fixed:
70
+ `allowUnregisteredClients: false` (derived automatically by `createMcpApp`
71
+ when no configured client registers dynamically) unmounts `/register`, drops
72
+ it from AS metadata, and rejects any `client_id` the `clientStore` doesn't
73
+ resolve — `unauthorized_client` at `authorize`, `invalid_client` at `token`.
74
+ Public type break: `AuthorizationServerMetadata.registration_endpoint` is
75
+ now optional (`string | undefined`), omitted when DCR is off.
76
+ - **Scope threaded end-to-end.** `authorize` validates the requested scope
77
+ against the advertised set, the auth code carries the grant, and
78
+ `mintAccessToken` receives it. `ToolDef.scope` finally sits on a chain that
79
+ reaches the OAuth layer.
80
+ - Node adapter drains the request stream when `req.body` is absent, so raw
81
+ `http.createServer` works without a body-parser.
82
+ - `[::1]` added to the loopback redirect allowlist.
83
+ - **`defineTool` / `apiTool`.** The library was easy to run but not easy to
84
+ *author tools for* — every tool hand-wrote its own JSON Schema disconnected
85
+ from its handler's types, and every API-backed tool re-invented fetch/error
86
+ plumbing. `defineTool` adds optional Standard Schema v1 parsing in front of
87
+ parsing in front of a handler — typed args, validation failures short-circuit
88
+ before the handler runs. `apiTool` builds on it: give it `request` (+
89
+ optional `respond`) and get a working REST-call tool, non-2xx handled as
90
+ `isError` automatically. `inputSchema` (JSON Schema, for `tools/list`) stays
91
+ required and separate — no library-agnostic way exists to derive it from an
92
+ arbitrary Standard Schema validator, so the two must be kept in sync by hand.
93
+ - **No host port can crash `fetch`.** Nothing guarded `authenticate`,
94
+ `context`, `resolveUser`, `mintAccessToken`, `clientStore`, etc. — any of
95
+ them throwing turned `.fetch()` into a rejected promise instead of a
96
+ `Response`, with the fallout depending on the runtime (a generic error page
97
+ on Workers, a hung socket with no response ever sent on raw
98
+ `http.createServer`). `createMcpHandler`, `createOAuthRouter`,
99
+ `createMcpApp`, and `asNodeHandler` now each catch at their own boundary and
100
+ return a real `500` instead.
101
+
102
+ - **RFC 7009 token revocation.** The library never stores access or refresh
103
+ tokens, so revocation is a host port — `revokeToken` — advertised and mounted
104
+ only when implemented (same honest-advertisement pattern as refresh).
105
+ Well-formed authenticated `POST /revoke` returns 200 even if the token is
106
+ unknown (`invalid_request` 400 and `invalid_client` 401 still apply).
107
+ Stolen-token invalidation lives in the host store that `verifyToken` and
108
+ `refreshAccessToken` consult.
109
+
110
+ - **Claims on `Principal`.** Optional `claims` from `verifyToken` /
111
+ `VerifiedToken` flow into `context` (plan/role metadata — not the
112
+ Host→tenant isolation key). Host-origin allowlisting (`allowedOrigins` on
113
+ `asNodeHandler`) is **required** when origin is Host-derived.
114
+
115
+ - **`validateArgs` fail-fast.** With `validateArgs: true`, unsupported JSON
116
+ Schema keywords throw at `createToolRegistry` construction instead of
117
+ silently looking enforced. Default (off) is unchanged.
118
+
119
+ **Phase 2 — the authorize endpoint (0.2.2 / 0.3.0).**
120
+
121
+ - **Consent step at `/authorize`.** A resolved session no longer issues a
122
+ code and redirects on its own; it renders an approval interstitial (built
123
+ in, or your own via `consent.render`) and only issues a code after an
124
+ explicit `POST ${oauthPath}/consent` approval. Closes a cross-site flow
125
+ where an attacker-controlled page could otherwise walk a logged-in user
126
+ through `/authorize` with an attacker-chosen `client_id`/`code_challenge`
127
+ and a loopback `redirect_uri`, catching the code with a local listener —
128
+ PKCE alone does not defend against this, it proves possession of the
129
+ verifier, not the identity of a legitimate client. Approval tickets are
130
+ AES-GCM sealed (`sealed.ts`), single-use, and re-checked against the
131
+ resolved user at redemption.
132
+ - **DCR `client_id` is bound to its own `redirect_uris`**, zero storage — the
133
+ id issued by `/register` is a sealed, self-verifying assertion of the list
134
+ it registered. Stops a substituted `redirect_uri` on a legitimate
135
+ connector's id (not impersonation via self-registration — the consent step
136
+ above is what stops that). `requireRegisteredClients` (default `true`
137
+ since 1.0) is the stricter default, and a direct stepping stone to CIMD below.
138
+ - **`codeSecret` is validated** — minimum 32 characters, and the literal
139
+ values this package's own docs/examples publish are denylisted by name.
140
+ It is the single key that can forge an auth code, consent ticket, or DCR
141
+ client assertion for any userId/scope/resource, and had no validation at
142
+ all before this.
143
+ - **Loopback redirect allowlist narrowed** — `localhost` and `https:`
144
+ loopback are no longer accepted (RFC 8252 §8.3); a `redirect_uri` carrying
145
+ a fragment or embedded credentials is rejected for every predicate, not
146
+ only loopback.
147
+ - **`defaultScopes` required for multi-scope servers.** `createOAuthRouter`
148
+ now throws at construction when `scopes` has more than one entry and
149
+ `defaultScopes` is unset, instead of silently granting the full advertised
150
+ set to a client that asked for nothing.
151
+ - **`timingSafeEqual("", "")` now returns `false`**, not `true` — a public
152
+ export, so a host comparing a presented secret against an unset stored one
153
+ no longer authenticates.
154
+ - **Request body size limits** on `/mcp`, `/token`, `/revoke`, and
155
+ `/register` — previously unbounded on every one of them.
156
+
157
+ **Phase 3 — credentials and correctness (0.4.0).**
158
+
159
+ - **Auth codes sealed (AES-GCM), not signed-only.** v1 (HMAC) codes were
160
+ readable by anyone who saw one; v2 codes are encrypted too. Soft
161
+ migration in 0.4.0 / 0.5.0 (read both, write v2); **1.0.0 drops v1 reading**.
162
+ - **`ClientStore.secretHash`**, preferred over `verifySecret`: return a
163
+ stored hash from the new `hashClientSecret` and the library does the
164
+ comparison with the same constant-time primitive it uses everywhere
165
+ else. Keyed by `codeSecret` — no second secret to manage.
166
+ - **Client-auth enumeration oracle closed.** `/token` and `/revoke`
167
+ collapsed five distinguishable error messages into one; a locked-down
168
+ server also pays a fixed-cost dummy comparison for an unknown
169
+ `client_id`, so response timing can't distinguish "unknown" from
170
+ "known, wrong secret" either. Unaffected: public-client traffic, and
171
+ `/authorize`'s deliberately-still-descriptive `unauthorized_client`.
172
+ - **`/revoke` accepts JSON**, matching `/token` — previously form-only,
173
+ so a JSON revoke silently failed while the caller believed it worked.
174
+ - **`apiTool` no longer forwards the upstream error body by default** —
175
+ status only; opt in via the new `onError`.
176
+ - **`missingObjectType`** — a schema using `properties`/`required`
177
+ without `type: "object"` now throws at construction under
178
+ `validateArgs: true`, and is enforced at runtime by dispatching on the
179
+ value's actual shape rather than trusting a schema author remembered to
180
+ declare `type`.
181
+ - **Union `type` arrays** (`type: ["string", "null"]`) now evaluate
182
+ correctly — previously always failed.
183
+ - **`/authorize` errors redirect to the client's callback** (RFC 6749
184
+ §4.1.2.1) once `redirect_uri` is validated, instead of a bare JSON body
185
+ the connector never parses — this was silently connector-breaking.
186
+
187
+ **Phase 4 — protocol correctness and observability (0.5.0).**
188
+
189
+ - **`jsonrpc` validated on every `/mcp` request** — `-32600` on anything
190
+ but exactly `"2.0"`, including a missing field. Previously accepted
191
+ silently.
192
+ - **`MCP-Protocol-Version` header read and validated**, per the
193
+ 2025-06-18 spec — unsupported → 400, missing → assumed `2025-03-26`
194
+ (the spec's own compatibility default), not checked on `initialize`
195
+ (which is what negotiates the version). Previously only appeared in the
196
+ CORS allow-list, never actually read.
197
+ - **`initialize` defaults to the newest supported protocol version**
198
+ (`2025-06-18`) for a missing/unrecognized request, not the oldest.
199
+ - **401 responses echo the real request id** instead of hardcoding `null`.
200
+ - **`/mcp/` routes identically to `/mcp`.** `canonicalResource` normalizes
201
+ its `resourcePath` argument internally, so route matching, `/authorize`,
202
+ and `/token` all agree — previously only some call sites normalized a
203
+ trailing slash and others read a raw, unnormalized value.
204
+ - **`createOAuthRouter` refuses to construct** if `oauthPath` would equal
205
+ `resourcePath` (they'd shadow each other) or if `resourcePath` starts
206
+ with `/.well-known` (reserved for discovery documents).
207
+ - **`auditTimeoutMs`** (default 1000ms) — a hanging `ports.audit` can no
208
+ longer stall a response indefinitely; it already couldn't fail one.
209
+ - **`OAuthPorts.audit` / `McpAppAuth.audit`** — the OAuth-side twin of the
210
+ MCP audit hook. Sees the real reason behind a collapsed `invalid_client`
211
+ or a rejected `codeSecret`, which the caller never does.
212
+
213
+ ---
214
+
215
+ ## Next
216
+
217
+ ### 1. CIMD — Client ID Metadata Documents
218
+
219
+ DCR is deprecated as of `2026-07-28`; CIMD is the recommended replacement and
220
+ the direction Claude and Codex are heading. The client publishes JSON at an
221
+ HTTPS URL and uses that URL as its `client_id`; the AS resolves it on demand.
222
+
223
+ AS-side requirements: detect URL-formatted `client_id`; fetch it; **MUST**
224
+ validate the document's `client_id` equals the URL exactly; **MUST** validate
225
+ the request's `redirect_uri` against the document's `redirect_uris`; **SHOULD**
226
+ cache respecting HTTP cache headers; advertise
227
+ `client_id_metadata_document_supported: true`.
228
+
229
+ This slots cleanly into the existing shape — pure `fetch` plus validation, with
230
+ caching as a port. It also completes the picture: the spec defines exactly three
231
+ registration mechanisms, and they map one-to-one onto the three clients.
232
+
233
+ ```
234
+ CIMD → Claude, Codex (public + PKCE)
235
+ pre-registration → Gemini Enterprise (confidential) ← shipped
236
+ DCR → legacy fallback ← shipped
237
+ ```
238
+
239
+ The security work is the real work: an AS that fetches attacker-supplied URLs is
240
+ an SSRF engine unless it blocks private/loopback/link-local ranges and DNS
241
+ rebinding, caps redirects, response size, and timeout, and refuses non-HTTPS.
242
+ Cloudflare's implementation is a useful reference for what "done right" means
243
+ (5 KB cap, 10 s timeout, public clients only).
244
+
245
+ Note: CIMD is not unclaimed. Authlete, Auth0, Clerk, and `workers-oauth-provider`
246
+ all ship it. The claim here is narrower and still true — none of them is a
247
+ portable, self-hosted, zero-dependency library.
248
+
249
+ ### 2. RFC 9207 — `iss` in the authorization response
250
+
251
+ Add `iss` to the authorize redirect and
252
+ `authorization_response_iss_parameter_supported` to AS metadata. Small, and a
253
+ spec-level SHOULD.
254
+
255
+ ### 3. Protocol currency — `2026-07-28`
256
+
257
+ Deferred deliberately: Claude, Gemini, and Codex all work against current
258
+ revisions today, and the official SDK already covers dual-era for anyone who
259
+ needs it now. But a batteries-included server that speaks an outdated revision
260
+ is worse than useless, so this becomes mandatory once real clients move.
261
+
262
+ Work: remove `initialize` / `ping`, add `server/discover` (MUST), carry protocol
263
+ version and client capabilities in `_meta` per request, `resultType` on every
264
+ result, `ttlMs` / `cacheScope` on `tools/list`, `Mcp-Method` / `Mcp-Name`
265
+ headers, deterministic `tools/list` ordering. Add `2025-11-25` and `2026-07-28`
266
+ to the version table and stop defaulting to the oldest revision.
267
+
268
+ ---
269
+
270
+ ## Non-goals
271
+
272
+ Backed by the spec, not just by our own scoping:
273
+
274
+ - **No sampling / roots / logging** — deprecated in `2026-07-28`
275
+ - **No sessions** — removed from the protocol
276
+ - **No SSE resumability** — removed from the transport
277
+ - **No HTTP+SSE transport** — deprecated; Gemini Enterprise refuses it outright
278
+ - **No login UI, token store, or IdP** — the ports stay the product. The
279
+ `/authorize` consent/approval screen is shipped (built in, overridable),
280
+ since it's a security control determined by protocol data the library
281
+ already holds, not by your IdP or branding; login itself stays yours
282
+ - **No resources / prompts** — a real limitation; revisit on demand
283
+ - **No DPoP** — sender-constrained tokens; connectors use bearer + PKCE
284
+ - **No PAR (RFC 9126)** — pushed authorization requests
285
+ - **No `client_credentials`** — connectors are user-delegated; this AS is authorization_code (+ optional refresh)
286
+
287
+ Deferred until asked: MRTR (`input_required`), `subscriptions/listen`,
288
+ `io.modelcontextprotocol/tasks`.
289
+
290
+ ## Known gaps
291
+
292
+ Tracked, not scheduled: `pruneMemory` scans the whole jti map on every code
293
+ consume; `mcpWwwAuthenticate` and `wwwAuthenticateHeader` build the same
294
+ header twice. (Unsupported JSON Schema keywords under `validateArgs: true`
295
+ now throw at `createToolRegistry` construction — no longer a silent gap.)