mcp-trellis 0.2.0 → 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.
- package/README.md +41 -561
- package/dist/adapters/node.d.ts +15 -6
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +56 -29
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/origins.d.ts.map +1 -1
- package/dist/adapters/origins.js +1 -4
- package/dist/adapters/origins.js.map +1 -1
- package/dist/app.d.ts +27 -4
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +31 -18
- package/dist/app.js.map +1 -1
- package/dist/auth/bearer.d.ts.map +1 -1
- package/dist/auth/bearer.js +6 -1
- package/dist/auth/bearer.js.map +1 -1
- package/dist/body.d.ts +48 -0
- package/dist/body.d.ts.map +1 -0
- package/dist/body.js +145 -0
- package/dist/body.js.map +1 -0
- package/dist/clients.d.ts.map +1 -1
- package/dist/clients.js +2 -5
- package/dist/clients.js.map +1 -1
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +58 -18
- package/dist/dispatch.js.map +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js.map +1 -1
- package/dist/jsonrpc.d.ts +1 -0
- package/dist/jsonrpc.d.ts.map +1 -1
- package/dist/jsonrpc.js +1 -0
- package/dist/jsonrpc.js.map +1 -1
- package/dist/methods.d.ts +38 -15
- package/dist/methods.d.ts.map +1 -1
- package/dist/methods.js +38 -22
- package/dist/methods.js.map +1 -1
- package/dist/oauth/authorize.d.ts.map +1 -1
- package/dist/oauth/authorize.js +100 -68
- package/dist/oauth/authorize.js.map +1 -1
- package/dist/oauth/base64url.d.ts +1 -1
- package/dist/oauth/base64url.d.ts.map +1 -1
- package/dist/oauth/base64url.js +1 -4
- package/dist/oauth/base64url.js.map +1 -1
- package/dist/oauth/clientauth.d.ts +7 -2
- package/dist/oauth/clientauth.d.ts.map +1 -1
- package/dist/oauth/clientauth.js +59 -16
- package/dist/oauth/clientauth.js.map +1 -1
- package/dist/oauth/codes.d.ts +4 -4
- package/dist/oauth/codes.d.ts.map +1 -1
- package/dist/oauth/codes.js +43 -51
- package/dist/oauth/codes.js.map +1 -1
- package/dist/oauth/consent.d.ts +65 -0
- package/dist/oauth/consent.d.ts.map +1 -0
- package/dist/oauth/consent.js +173 -0
- package/dist/oauth/consent.js.map +1 -0
- package/dist/oauth/constants.d.ts +4 -0
- package/dist/oauth/constants.d.ts.map +1 -1
- package/dist/oauth/constants.js +4 -0
- package/dist/oauth/constants.js.map +1 -1
- package/dist/oauth/metadata.d.ts +6 -6
- package/dist/oauth/metadata.d.ts.map +1 -1
- package/dist/oauth/metadata.js.map +1 -1
- package/dist/oauth/pkce.d.ts.map +1 -1
- package/dist/oauth/pkce.js +1 -1
- package/dist/oauth/pkce.js.map +1 -1
- package/dist/oauth/redirect.d.ts +6 -1
- package/dist/oauth/redirect.d.ts.map +1 -1
- package/dist/oauth/redirect.js +23 -10
- package/dist/oauth/redirect.js.map +1 -1
- package/dist/oauth/reqbody.d.ts +10 -0
- package/dist/oauth/reqbody.d.ts.map +1 -0
- package/dist/oauth/reqbody.js +29 -0
- package/dist/oauth/reqbody.js.map +1 -0
- package/dist/oauth/resource.d.ts +30 -0
- package/dist/oauth/resource.d.ts.map +1 -1
- package/dist/oauth/resource.js +38 -8
- package/dist/oauth/resource.js.map +1 -1
- package/dist/oauth/revoke.d.ts.map +1 -1
- package/dist/oauth/revoke.js +15 -3
- package/dist/oauth/revoke.js.map +1 -1
- package/dist/oauth/router.d.ts +14 -11
- package/dist/oauth/router.d.ts.map +1 -1
- package/dist/oauth/router.js +62 -22
- package/dist/oauth/router.js.map +1 -1
- package/dist/oauth/scope.d.ts +11 -2
- package/dist/oauth/scope.d.ts.map +1 -1
- package/dist/oauth/scope.js +18 -7
- package/dist/oauth/scope.js.map +1 -1
- package/dist/oauth/sealed.d.ts +17 -0
- package/dist/oauth/sealed.d.ts.map +1 -0
- package/dist/oauth/sealed.js +57 -0
- package/dist/oauth/sealed.js.map +1 -0
- package/dist/oauth/secrethash.d.ts +44 -0
- package/dist/oauth/secrethash.d.ts.map +1 -0
- package/dist/oauth/secrethash.js +66 -0
- package/dist/oauth/secrethash.js.map +1 -0
- package/dist/oauth/token.d.ts.map +1 -1
- package/dist/oauth/token.js +13 -34
- package/dist/oauth/token.js.map +1 -1
- package/dist/oauth/types.d.ts +106 -9
- package/dist/oauth/types.d.ts.map +1 -1
- package/dist/oauth/types.js +67 -4
- package/dist/oauth/types.js.map +1 -1
- package/dist/oauth/wellknown.d.ts.map +1 -1
- package/dist/oauth/wellknown.js +6 -4
- package/dist/oauth/wellknown.js.map +1 -1
- package/dist/protocol.d.ts +15 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +16 -6
- package/dist/protocol.js.map +1 -1
- package/dist/registry.d.ts +4 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +15 -7
- package/dist/registry.js.map +1 -1
- package/dist/server.d.ts +14 -10
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +8 -8
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts +11 -2
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +14 -13
- package/dist/tools.js.map +1 -1
- package/dist/validate.d.ts +16 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +46 -8
- package/dist/validate.js.map +1 -1
- package/docs/ROADMAP.md +295 -0
- package/docs/guide.md +417 -0
- package/docs/reference.md +178 -0
- package/docs/security.md +63 -0
- package/package.json +7 -3
package/docs/ROADMAP.md
ADDED
|
@@ -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.)
|