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/guide.md
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# Guide
|
|
2
|
+
|
|
3
|
+
Practical deep dive: architecture, host recipes, ports, tools, and multi-tenant
|
|
4
|
+
connectors. For a working minimal server, start from the [README](../README.md).
|
|
5
|
+
API tables and exports live in [reference.md](reference.md). Security guarantees
|
|
6
|
+
and threat model: [security.md](security.md).
|
|
7
|
+
|
|
8
|
+
[Architecture](#how-it-fits-together) · [Clients](#clients) · [Recipes](#recipes) ·
|
|
9
|
+
[Compose the primitives](#advanced-compose-the-primitives) ·
|
|
10
|
+
[Ports](#ports--what-you-implement) · [Consent](#consent) ·
|
|
11
|
+
[Tool registry](#tool-registry) · [Multi-tenant](#multi-tenant-saas-connectors)
|
|
12
|
+
|
|
13
|
+
## How it fits together
|
|
14
|
+
|
|
15
|
+
What `createMcpApp` wires for you — two pieces, one deciding which requests go
|
|
16
|
+
to the other:
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
Typical first-connection path:
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
You only touch this routing directly if you skip `createMcpApp` — see
|
|
25
|
+
[Advanced: compose the primitives](#advanced-compose-the-primitives).
|
|
26
|
+
|
|
27
|
+
## Clients
|
|
28
|
+
|
|
29
|
+
Name the connector clients you serve; the AS is configured to match.
|
|
30
|
+
|
|
31
|
+
| Client | Registration | Token endpoint auth | Notes |
|
|
32
|
+
|--------|--------------|---------------------|-------|
|
|
33
|
+
| `claude` | Dynamic (DCR), public + PKCE | `none` | Claude Custom Connectors callback is allowlisted for you |
|
|
34
|
+
| `gemini` | **Pre-registered**, confidential | `client_secret_basic`, `client_secret_post` | Gemini Enterprise is registered with your org's IdP — supply `auth.clientStore` |
|
|
35
|
+
| `codex` | OAuth 2.1 per MCP auth spec, public + PKCE | `none` | ChatGPT / Codex share one connector contract |
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
createMcpApp({
|
|
39
|
+
clients: ["claude", "gemini"],
|
|
40
|
+
auth: {
|
|
41
|
+
// …
|
|
42
|
+
clientStore: {
|
|
43
|
+
get: async (clientId) => lookupClient(clientId),
|
|
44
|
+
verifySecret: async (clientId, presented) =>
|
|
45
|
+
checkSecret(clientId, presented),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Naming a pre-registered client without a `clientStore` throws at construction
|
|
52
|
+
rather than failing at the first token exchange. Secret storage and comparison
|
|
53
|
+
stay in your `clientStore` — the library never sees or persists credentials.
|
|
54
|
+
|
|
55
|
+
Naming only pre-registered clients (`clients: ["gemini"]`, no `claude` /
|
|
56
|
+
`codex`) sets `allowUnregisteredClients: false`: `/register` is unmounted
|
|
57
|
+
and dropped from AS metadata, unknown ids are rejected with
|
|
58
|
+
`unauthorized_client` at `authorize` and `invalid_client` at `token` and
|
|
59
|
+
`revoke`. Naming `claude` or `codex` alongside `gemini` reopens dynamic
|
|
60
|
+
registration.
|
|
61
|
+
|
|
62
|
+
## Recipes
|
|
63
|
+
|
|
64
|
+
### Cloudflare Worker, Next.js, Deno, Bun
|
|
65
|
+
|
|
66
|
+
`createMcpApp` returns a plain `fetch`, so the same object mounts everywhere:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// Worker
|
|
70
|
+
export default { fetch: (req: Request, env: Env) => buildApp(env).fetch(req) };
|
|
71
|
+
|
|
72
|
+
// Next.js App Router — app/[[...mcp]]/route.ts
|
|
73
|
+
export const GET = (req: Request) => app.fetch(req);
|
|
74
|
+
export const POST = (req: Request) => app.fetch(req);
|
|
75
|
+
export const OPTIONS = (req: Request) => app.fetch(req);
|
|
76
|
+
|
|
77
|
+
// Node / Express / Cloud Run
|
|
78
|
+
import { asNodeHandler } from "mcp-trellis/node";
|
|
79
|
+
export const mcp = asNodeHandler(app, { origin: process.env.MCP_PUBLIC_ORIGIN! });
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The Node bridge reads the request stream when `req.body` is absent, so raw
|
|
83
|
+
`http.createServer` works without a body-parser. Runnable recipe:
|
|
84
|
+
[`examples/http-server.ts`](../examples/http-server.ts).
|
|
85
|
+
|
|
86
|
+
## Advanced: compose the primitives
|
|
87
|
+
|
|
88
|
+
Skip `createMcpApp` when you want to own the routing. Same behavior, more wiring:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { createMcpHandler, createToolRegistry, parseBearer } from "mcp-trellis";
|
|
92
|
+
import { createOAuthRouter } from "mcp-trellis/oauth";
|
|
93
|
+
|
|
94
|
+
type Ctx = { userId: string };
|
|
95
|
+
type Env = { OAUTH_CODE_SECRET: string };
|
|
96
|
+
|
|
97
|
+
const registry = createToolRegistry<Ctx>([
|
|
98
|
+
{
|
|
99
|
+
name: "ping_db",
|
|
100
|
+
description: "Health check",
|
|
101
|
+
inputSchema: { type: "object", properties: {} },
|
|
102
|
+
scope: "read",
|
|
103
|
+
handler: async () => "ok",
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const handler = createMcpHandler<Ctx>({
|
|
108
|
+
registry,
|
|
109
|
+
serverInfo: { name: "my-app", version: "1.0.0" },
|
|
110
|
+
instructions: "Tools available to authenticated clients.",
|
|
111
|
+
wwwAuthenticate: (req) => ({
|
|
112
|
+
realm: "my-app",
|
|
113
|
+
resourceMetadataUrl: `${new URL(req.url).origin}/.well-known/oauth-protected-resource/mcp`,
|
|
114
|
+
}),
|
|
115
|
+
ports: {
|
|
116
|
+
authenticate: async (req) => {
|
|
117
|
+
const token = parseBearer(req.headers.get("authorization"));
|
|
118
|
+
if (!token) return null;
|
|
119
|
+
// Reject tokens whose aud ≠ this server's canonical resource.
|
|
120
|
+
return { id: "user-1", scopes: ["read"] };
|
|
121
|
+
},
|
|
122
|
+
context: async (_req, principal) => ({
|
|
123
|
+
userId: principal?.id ?? "",
|
|
124
|
+
}),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export default {
|
|
129
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
130
|
+
const oauth = createOAuthRouter({
|
|
131
|
+
ports: {
|
|
132
|
+
codeSecret: env.OAUTH_CODE_SECRET,
|
|
133
|
+
resolveUser: async () => ({ id: "user-1" }),
|
|
134
|
+
loginUrl: (req, next) =>
|
|
135
|
+
`${new URL(req.url).origin}/login?next=${encodeURIComponent(next)}`,
|
|
136
|
+
mintAccessToken: async ({ userId, resource }) => ({
|
|
137
|
+
// Embed `resource` as the token audience (RFC 8707).
|
|
138
|
+
accessToken: await issueUserToken(userId, { aud: resource }),
|
|
139
|
+
expiresIn: 3600,
|
|
140
|
+
scope: "mcp",
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const oauthResponse = await oauth.tryHandle(request);
|
|
146
|
+
if (oauthResponse) return oauthResponse;
|
|
147
|
+
if (new URL(request.url).pathname === "/mcp") return handler.fetch(request);
|
|
148
|
+
return new Response("Not found", { status: 404 });
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Next.js App Router
|
|
154
|
+
|
|
155
|
+
Delegate Web `Request` straight through — same handler as above:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
// app/mcp/route.ts
|
|
159
|
+
import { handler, oauth } from "@/lib/mcp"; // your shared setup
|
|
160
|
+
|
|
161
|
+
export async function GET(req: Request) {
|
|
162
|
+
const oauthResponse = await oauth.tryHandle(req);
|
|
163
|
+
if (oauthResponse) return oauthResponse;
|
|
164
|
+
return handler.fetch(req);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function POST(req: Request) {
|
|
168
|
+
const oauthResponse = await oauth.tryHandle(req);
|
|
169
|
+
if (oauthResponse) return oauthResponse;
|
|
170
|
+
return handler.fetch(req);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function OPTIONS() {
|
|
174
|
+
return handler.fetch(new Request("https://local/mcp", { method: "OPTIONS" }));
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Also mount `.well-known/*` and `/mcp/oauth/*` the same way (or route everything through one catch-all that calls `oauth.tryHandle` first).
|
|
179
|
+
|
|
180
|
+
### Node HTTP `(req, res)`
|
|
181
|
+
|
|
182
|
+
Express, Cloud Functions, Cloud Run, `http.createServer` — bridge with
|
|
183
|
+
`asNodeHandler`. Runnable recipe: [`examples/http-server.ts`](../examples/http-server.ts)
|
|
184
|
+
(`npx tsx examples/http-server.ts`).
|
|
185
|
+
|
|
186
|
+
## Ports — what you implement
|
|
187
|
+
|
|
188
|
+
The library stays protocol-shaped. Your app plugs in the seams:
|
|
189
|
+
|
|
190
|
+

|
|
191
|
+
|
|
192
|
+
### App (`createMcpApp`)
|
|
193
|
+
|
|
194
|
+
| Port | Role |
|
|
195
|
+
|------|------|
|
|
196
|
+
| `verifyToken(token, req)` | Decode a bearer and return `{ userId, scopes, audience, claims? }`, or `null`. **The library compares `audience` against this server's canonical resource** — you cannot forget the check. Optional `claims` are passed to `context` on `principal` |
|
|
197
|
+
| `resolveUser`, `loginUrl`, `mintAccessToken`, `codeSecret` | Same as the OAuth ports below |
|
|
198
|
+
| `clientStore?` | Pre-registered clients; required for confidential clients like Gemini |
|
|
199
|
+
| `refreshAccessToken?` | If set, metadata advertises `refresh_token` |
|
|
200
|
+
| `revokeToken?` | RFC 7009; presence mounts `/revoke` and advertises `revocation_endpoint` |
|
|
201
|
+
|
|
202
|
+
`consent?` (approval-page policy — see [Consent](#consent), below) and
|
|
203
|
+
`defaultScopes?` sit as top-level `McpAppOptions`, not under `auth` — they
|
|
204
|
+
are policy, not credentials, the same reasoning as `allowLoopback`.
|
|
205
|
+
| `context?`, `audit?` | Same as the MCP ports below; `context` defaults to an empty object. Receives `principal.claims` when `verifyToken` set them |
|
|
206
|
+
|
|
207
|
+
### MCP (`createMcpHandler`)
|
|
208
|
+
|
|
209
|
+
The lower-level handler. Here the audience check is **yours** — prefer
|
|
210
|
+
`createMcpApp` unless you need the control. See [security.md](security.md).
|
|
211
|
+
|
|
212
|
+
| Port | Role |
|
|
213
|
+
|------|------|
|
|
214
|
+
| `authenticate(req, method, tool?)` | Return `{ id, scopes, claims? }`, or `null` → 401 + `WWW-Authenticate`. **Must reject tokens whose audience is not this server's canonical resource** |
|
|
215
|
+
| `context(req, principal)` | Build per-request ctx for tools (DB, env, …). Use `principal?.claims` for tenant / plan / role without re-decoding the bearer |
|
|
216
|
+
| `audit?(entry)` | Opt-in **metrics hook**: pass any function to receive tool results **and** auth denials (bad token, missing scope, query-string token), plus the 500 path when a host port throws. `entry.method` is `""` for transport-level denials made before parsing. Protocol errors (malformed JSON, bad `jsonrpc`, unsupported protocol version, oversized body, batch) are **not** audited. Throwing from this port never fails the request, and neither does hanging — see `auditTimeoutMs` below. Omit it and the library stays silent |
|
|
217
|
+
|
|
218
|
+
`auditTimeoutMs` (default 1000ms) races `audit` against a timeout, so a
|
|
219
|
+
slow sink can't stall a response past that bound either.
|
|
220
|
+
|
|
221
|
+
Pass any function to get per-request metrics — do what you want with them
|
|
222
|
+
(DB, APM, admin UI). The library never writes to stdout or a store on its
|
|
223
|
+
own. `consoleAudit` is only a convenience for local / small deploys;
|
|
224
|
+
[`examples/audit-store.ts`](../examples/audit-store.ts) shows a ring buffer and
|
|
225
|
+
a DB-shaped sketch:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { createMcpApp, consoleAudit } from "mcp-trellis";
|
|
229
|
+
|
|
230
|
+
createMcpApp({ /* … */, audit: consoleAudit });
|
|
231
|
+
|
|
232
|
+
// Or your own sink:
|
|
233
|
+
createMcpApp({
|
|
234
|
+
/* … */,
|
|
235
|
+
audit: (entry) => {
|
|
236
|
+
// entry: { method, tool?, principalId?, ok, error?, durationMs }
|
|
237
|
+
void metrics.record(entry);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Failures (denials and tool exceptions) go to `console.error` when using
|
|
243
|
+
`consoleAudit`; everything else to `console.log`. Entries never carry raw
|
|
244
|
+
exceptions or stack traces, so console output is safe. For anything beyond
|
|
245
|
+
stdout, write your own `audit` function — that is the intended production path.
|
|
246
|
+
|
|
247
|
+
`wwwAuthenticate` can be a static object or `(req) => …` when the PRM URL depends on origin.
|
|
248
|
+
|
|
249
|
+
By default `initialize`, `ping`, and notifications are public. Override `publicMethods` if every method must require Bearer.
|
|
250
|
+
|
|
251
|
+
### OAuth (`createOAuthRouter`)
|
|
252
|
+
|
|
253
|
+
| Port | Role |
|
|
254
|
+
|------|------|
|
|
255
|
+
| `codeSecret` | HMAC secret for signed auth codes (string or `(req) => string`) |
|
|
256
|
+
| `resolveUser` | Consenting user, or `null` → redirect to `loginUrl` |
|
|
257
|
+
| `loginUrl` | Where unauthenticated authorize requests go |
|
|
258
|
+
| `mintAccessToken` | Issue a **user-bound**, **audience-bound** access token (`resource` is the RFC 8707 URI) — never a shared god token |
|
|
259
|
+
| `refreshAccessToken?` | If set, metadata advertises `refresh_token`; also receives `resource` |
|
|
260
|
+
| `revokeToken?` | If set, mounts `/revoke` and advertises `revocation_endpoint`. Well-formed authenticated revoke → 200 even if the token is unknown; `invalid_request` 400 and `invalid_client` 401 still apply |
|
|
261
|
+
| `codeStore?` | Shared single-use jti store for multi-instance (`consume(jti, expMs)`); pruning in-memory default otherwise. Also backs consent-ticket single-use — see below |
|
|
262
|
+
| `clientStore?` | Pre-registered clients: `get(clientId)` returns registered redirect URIs and auth method. Two ways to authenticate a confidential client's secret — **prefer `secretHash(clientId)`**: return a stored hash from `hashClientSecret`, and the library compares it with the same constant-time primitive it uses everywhere else; `verifySecret(clientId, presented)` is the fallback when you'd rather compare yourself. Credentials never enter the library either way |
|
|
263
|
+
| `audit?` | Opt-in **OAuth-side metrics hook** — mirrors the MCP-side one above. `/token`/`/revoke` collapse every client-auth failure to one generic `invalid_client` (an enumeration-oracle fix — see [security.md](security.md)), and a rejected `codeSecret` surfaces to the caller only as `server_error`. This is where you get the real reason back (`entry.reason`) without handing it to an unauthenticated caller. Throwing never fails the request |
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { hashClientSecret } from "mcp-trellis/oauth";
|
|
267
|
+
|
|
268
|
+
// Once, when you provision a confidential client (e.g. Gemini Enterprise):
|
|
269
|
+
const stored = await hashClientSecret(theSecretYouGenerated, codeSecretValue);
|
|
270
|
+
// Save `stored` — never the plaintext secret — and return it from
|
|
271
|
+
// clientStore.secretHash(clientId) at verify time.
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Auth codes carry `userId`, `resource`, and the granted `scope`. Clients **must** send `resource` on authorize and token (MCP MUST) — omitting it is a **breaking** requirement vs earlier 0.1.x drafts that ignored the parameter. Advertised grants come only from the handlers you configure. AS metadata sets `resource_parameter_supported: true`.
|
|
275
|
+
|
|
276
|
+
**Scope is negotiated:** `authorize` validates the requested `scope` against `scopes` (default `["mcp"]`) and rejects anything outside it with `invalid_scope`. An omitted `scope` grants `defaultScopes` — which defaults to the full advertised set **only when `scopes` has a single entry**. Once `scopes` names more than one, `createOAuthRouter` throws at construction unless you set `defaultScopes` explicitly: silently granting everything advertised to a client that asked for nothing is exactly the escalation least-privilege scoping exists to prevent. The auth code carries the grant, and `mintAccessToken` receives it — so `ToolDef.scope` and `principal.scopes` sit on a chain that actually reaches the OAuth layer.
|
|
277
|
+
|
|
278
|
+
**Client authentication:** unknown `client_id`s are public (PKCE only) unless `allowUnregisteredClients` is `false` — then DCR is unmounted, dropped from metadata, and unrecognized ids are rejected (`unauthorized_client` at authorize, `invalid_client` at token and revoke). `createMcpApp` derives the flag from `clients` via `hasDynamicClient`. Separately, `requireRegisteredClients` (default **true** since 1.0) keeps DCR mounted but rejects any `client_id` that isn't `clientStore`-resolved or a sealed id this server itself issued via `/register` — see [security.md](security.md). Set `requireRegisteredClients: false` only when you intentionally accept invented public ids.
|
|
279
|
+
|
|
280
|
+
### Consent
|
|
281
|
+
|
|
282
|
+
`/authorize` never issues a code directly, even for a resolved session — it
|
|
283
|
+
renders an approval interstitial and only issues the code once the user
|
|
284
|
+
POSTs an explicit approval to `/consent`. This closes a cross-site flow
|
|
285
|
+
where an attacker's page could otherwise walk a logged-in visitor through
|
|
286
|
+
`/authorize` with an attacker-chosen `client_id` and a loopback
|
|
287
|
+
`redirect_uri`, then catch the code on a local listener; PKCE alone does
|
|
288
|
+
not stop this, because it proves possession of the verifier, not the
|
|
289
|
+
identity of a legitimate client.
|
|
290
|
+
|
|
291
|
+
| Option (`consent`) | Default | Role |
|
|
292
|
+
|---|---|---|
|
|
293
|
+
| `render?(input)` | built-in interstitial | Render your own approval page — return any `Response`, or redirect to a route you own. `input.ticket` is the opaque value your form must echo back as `consent_ticket` |
|
|
294
|
+
| `preApprovedClientIds?` | `[]` | Skip the screen for these ids — **only** when `clientStore` actually resolves the id. A DCR or self-invented id is never trusted to pre-approve itself, no matter what's in this list |
|
|
295
|
+
|
|
296
|
+
The built-in interstitial ships `Content-Security-Policy`,
|
|
297
|
+
`X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`, and HTML-escapes
|
|
298
|
+
every reflected value (`client_id` and `redirect_uri` are attacker-
|
|
299
|
+
controlled input up to this point). If you supply `render`, you own that
|
|
300
|
+
hardening for your own markup.
|
|
301
|
+
|
|
302
|
+
Approval tickets are sealed (AES-GCM, single-use via the same store as
|
|
303
|
+
auth-code replay protection, 5-minute TTL) and re-checked against the
|
|
304
|
+
resolved user at redemption — approving as one user and redeeming as
|
|
305
|
+
another is rejected, and a ticket can never be presented at `/token` as an
|
|
306
|
+
authorization code, or vice versa.
|
|
307
|
+
|
|
308
|
+
## Tool registry
|
|
309
|
+
|
|
310
|
+
Schema and handler live in one place — no parallel "defs" and "handlers" lists to keep in sync:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
createToolRegistry(tools, { validateArgs: false }) // default: off, easy adoption
|
|
314
|
+
// onToolError?: (exc) => string — default redacts to "Tool execution failed"
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
- Handler may return a `string` or `{ content, isError? }`
|
|
318
|
+
- Unknown tools and thrown errors become `isError: true` (not transport errors)
|
|
319
|
+
- Thrown exceptions are **redacted** by default (`"Tool execution failed"`); pass `onToolError` to map them. An `onToolError` that itself throws or returns a non-string falls back to the redacted default
|
|
320
|
+
- Duplicate tool names throw at registry construction
|
|
321
|
+
- Turn `validateArgs: true` once clients send schema-valid args. The evaluated
|
|
322
|
+
subset is `type`, `enum`, `required`, `properties`, `items`, `minimum`,
|
|
323
|
+
`maximum`. Pure metadata (`description`, `title`, `$schema`, `$id`,
|
|
324
|
+
`$comment`, `default`, `examples`, `deprecated`, `readOnly`, `writeOnly`,
|
|
325
|
+
`format`) is allowed and never evaluated. Any other keyword is not
|
|
326
|
+
evaluated — and with `validateArgs: true` `createToolRegistry` throws at
|
|
327
|
+
construction so a schema like `pattern` or `additionalProperties` cannot
|
|
328
|
+
silently look enforced
|
|
329
|
+
|
|
330
|
+
### Wrapping an API as a tool
|
|
331
|
+
|
|
332
|
+
`ToolDef` stays the primitive. `defineTool` and `apiTool` are optional sugar for
|
|
333
|
+
the common case — turning "I have an API" into "I have an MCP tool" without
|
|
334
|
+
hand-rolling arg validation or fetch/error plumbing each time:
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { apiTool } from "mcp-trellis";
|
|
338
|
+
|
|
339
|
+
const getWeather = apiTool({
|
|
340
|
+
name: "get_weather",
|
|
341
|
+
description: "Current weather for a city",
|
|
342
|
+
inputSchema: {
|
|
343
|
+
type: "object",
|
|
344
|
+
properties: { city: { type: "string" } },
|
|
345
|
+
required: ["city"],
|
|
346
|
+
},
|
|
347
|
+
// any Standard Schema v1 validator (https://standardschema.dev)
|
|
348
|
+
input: citySchema,
|
|
349
|
+
request: (_ctx, { city }) =>
|
|
350
|
+
`https://api.example.com/weather?city=${encodeURIComponent(city)}`,
|
|
351
|
+
respond: async (res) => {
|
|
352
|
+
const data = await res.json();
|
|
353
|
+
return `${data.tempC}°C, ${data.condition}`;
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
- **`inputSchema` stays required** — it's what `tools/list` actually advertises to
|
|
359
|
+
clients, and there's no library-agnostic way to derive JSON Schema from an
|
|
360
|
+
arbitrary Standard Schema validator. Keep the two in sync.
|
|
361
|
+
- **`input`** (optional) parses and types `args` before your handler runs — any
|
|
362
|
+
[Standard Schema](https://standardschema.dev)-compliant validator works,
|
|
363
|
+
nothing library-specific. A validation failure returns `isError: true` with
|
|
364
|
+
the issues and never calls your handler.
|
|
365
|
+
- **`apiTool`** builds on `defineTool`: give it `request` (build the outgoing
|
|
366
|
+
call from typed args) and optionally `respond` (shape a 2xx response;
|
|
367
|
+
default is the body as text). Non-2xx becomes `isError: true` automatically
|
|
368
|
+
— **status only by default, the upstream body is never forwarded** (it
|
|
369
|
+
could contain internal detail, tokens, or SQL an upstream error page
|
|
370
|
+
echoes back). Pass `onError(res)` to shape it yourself, e.g. include a
|
|
371
|
+
redacted version of the body. `fetch` is overridable — testing, request
|
|
372
|
+
signing, a custom agent.
|
|
373
|
+
- `defineTool` alone (without `request`/`respond`) is just the typed-args
|
|
374
|
+
layer over a regular `ToolDef` — use it for anything, not only REST calls.
|
|
375
|
+
|
|
376
|
+
## Multi-tenant (SaaS connectors)
|
|
377
|
+
|
|
378
|
+
Resource-per-tenant already works with **zero library change**.
|
|
379
|
+
`canonicalResource(origin, resourcePath)` is derived per request, and the
|
|
380
|
+
audience check in `createMcpApp` rejects a token minted for another origin
|
|
381
|
+
before any tool runs. Deploy `acme.mcp.example.com` and
|
|
382
|
+
`globex.mcp.example.com` as separate Hosts and cross-tenant token reuse is a
|
|
383
|
+
protocol failure, not an application bug.
|
|
384
|
+
|
|
385
|
+
For the in-tool half, derive the tenant from the request Host (or from the
|
|
386
|
+
verified audience after the library check) in your `context` port — that is
|
|
387
|
+
the isolation key. Optional `claims` from `verifyToken` (plan, role, …) are
|
|
388
|
+
handy metadata on `principal`; they are **not** Host→tenant authorization.
|
|
389
|
+
A forged `claims.tenant_id` with a matching audience must not change which
|
|
390
|
+
tenant's data a tool can see.
|
|
391
|
+
|
|
392
|
+
**Host caveat (API requirement).** On Node, multi-tenant needs a Host-derived
|
|
393
|
+
origin (`trustProxy: true` without a fixed `origin`). That path **requires**
|
|
394
|
+
a non-empty `allowedOrigins` at `asNodeHandler` construction — omit it and
|
|
395
|
+
construction throws. Use exact origins and/or `*.example.com` wildcards for
|
|
396
|
+
**https** subdomains; the **resolved** origin must match or the request
|
|
397
|
+
returns **400** before any OAuth or MCP handling. Loud opt-out:
|
|
398
|
+
`allowedOrigins: ["*"]` (admits any Host — only behind a proxy you trust to
|
|
399
|
+
strip client `X-Forwarded-*`). A fixed `origin` absent from a non-empty list
|
|
400
|
+
fails at construction; with a fixed `origin` the allowlist is optional
|
|
401
|
+
defense-in-depth. Absolute-form request targets are rebased onto that
|
|
402
|
+
validated origin, so a spoofed URL line cannot override it. A fixed `origin`
|
|
403
|
+
is otherwise safe but collapses every tenant onto one resource, so it cannot
|
|
404
|
+
do subdomain-per-tenant.
|
|
405
|
+
|
|
406
|
+
On Cloudflare Workers (or any wildcard route), apply the equivalent
|
|
407
|
+
allowlist check on `new URL(req.url).origin` before calling `app.fetch`
|
|
408
|
+
(`isAllowedOrigin` from `mcp-trellis/node`, or the same module the Worker
|
|
409
|
+
example imports).
|
|
410
|
+
|
|
411
|
+
Typechecked sketch: [`examples/multi-tenant.ts`](../examples/multi-tenant.ts)
|
|
412
|
+
(needs a Host matching `*.mcp.example.com` — loopback alone is rejected).
|
|
413
|
+
Shared `codeStore` / revocation shapes: [`examples/stores.ts`](../examples/stores.ts)
|
|
414
|
+
(atomic `setIfAbsent` for auth codes; Workers KV alone is not enough).
|
|
415
|
+
`audit` metrics sink (ring + DB sketch): [`examples/audit-store.ts`](../examples/audit-store.ts).
|
|
416
|
+
Worker mount: [`examples/cloudflare-worker.ts`](../examples/cloudflare-worker.ts)
|
|
417
|
+
(typecheck-only in CI — no wrangler).
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Reference
|
|
2
|
+
|
|
3
|
+
Package surface, routes, MCP methods, status codes, options, and exports.
|
|
4
|
+
Ports and recipes: [guide.md](guide.md). Security: [security.md](security.md).
|
|
5
|
+
|
|
6
|
+
## Package surface
|
|
7
|
+
|
|
8
|
+
| Import | What you get |
|
|
9
|
+
|--------|----------------|
|
|
10
|
+
| `mcp-trellis` | `createMcpApp`, client profiles, MCP handler, tool registry, bearer helpers, HTTP utils |
|
|
11
|
+
| `mcp-trellis/oauth` | OAuth 2.1 AS router, PKCE, auth codes, client auth, metadata |
|
|
12
|
+
| `mcp-trellis/node` | `asNodeHandler` + `resolveOrigin` for Node `(req, res)` |
|
|
13
|
+
|
|
14
|
+
`createMcpApp` is the batteries-included layer. `createMcpHandler` and
|
|
15
|
+
`createOAuthRouter` remain exported and unchanged — reach for them when you want
|
|
16
|
+
to own the routing or mount the two halves separately. See
|
|
17
|
+
[compose the primitives](guide.md#advanced-compose-the-primitives).
|
|
18
|
+
|
|
19
|
+
## Default OAuth routes
|
|
20
|
+
|
|
21
|
+
With defaults `resourcePath: "/mcp"` and `oauthPath: "/mcp/oauth"`:
|
|
22
|
+
|
|
23
|
+
| Path | Purpose |
|
|
24
|
+
|------|---------|
|
|
25
|
+
| `/.well-known/oauth-protected-resource` (+ `/mcp`) | Protected resource metadata |
|
|
26
|
+
| `/.well-known/oauth-authorization-server` (+ `/mcp`) | Authorization server metadata |
|
|
27
|
+
| `/mcp/oauth/register` | Dynamic client registration (unmounted when DCR is off) |
|
|
28
|
+
| `/mcp/oauth/authorize` | Authorization endpoint — GET only. Renders a consent interstitial rather than redirecting directly; see `/consent` below and [security.md](security.md). Once `redirect_uri` is validated, every remaining error redirects to it with `?error=…` (RFC 6749 §4.1.2.1) rather than returning JSON; an invalid `redirect_uri` itself, and an oversized `state`, are the two exceptions and stay direct 400s |
|
|
29
|
+
| `/mcp/oauth/consent` | POST only. Approves or denies a consent ticket issued by `/authorize` and, on approval, issues the code and redirects to `redirect_uri` |
|
|
30
|
+
| `/mcp/oauth/token` | Token endpoint. JSON or form body |
|
|
31
|
+
| `/mcp/oauth/revoke` | Token revocation (RFC 7009; mounted only when `revokeToken` is set). JSON or form body |
|
|
32
|
+
|
|
33
|
+
## Supported MCP methods
|
|
34
|
+
|
|
35
|
+
| Method | Auth | Notes |
|
|
36
|
+
|--------|------|-------|
|
|
37
|
+
| `initialize` | public | Negotiates protocol version; returns `capabilities.tools` |
|
|
38
|
+
| `ping` | public | Empty result |
|
|
39
|
+
| `tools/list` | Bearer | Lists registry entries |
|
|
40
|
+
| `tools/call` | Bearer + scope | Runs the tool; missing scope → 401 |
|
|
41
|
+
| `notifications/*` | public | HTTP 202 empty body |
|
|
42
|
+
|
|
43
|
+
Anything else → JSON-RPC `-32601`. Capabilities advertise **tools only**.
|
|
44
|
+
|
|
45
|
+
## HTTP and JSON-RPC status codes
|
|
46
|
+
|
|
47
|
+
| HTTP | When |
|
|
48
|
+
|------|------|
|
|
49
|
+
| **202** | Notification (empty body) |
|
|
50
|
+
| **400** | Parse error, batch array, invalid request, `jsonrpc` not `"2.0"`, unsupported `MCP-Protocol-Version` |
|
|
51
|
+
| **401** | Missing/invalid Bearer; includes `WWW-Authenticate`; echoes the real request id, not `null` |
|
|
52
|
+
| **405** | Wrong HTTP verb on MCP |
|
|
53
|
+
| **413** | Request body over the cap — `/mcp` (1 MiB default), `/token`/`/revoke`/`/register` (64 KiB default) |
|
|
54
|
+
|
|
55
|
+
| JSON-RPC code | Constant |
|
|
56
|
+
|---------------|----------|
|
|
57
|
+
| `-32700` | `JSONRPC_PARSE_ERROR` |
|
|
58
|
+
| `-32600` | `JSONRPC_INVALID_REQUEST` (batches) |
|
|
59
|
+
| `-32601` | `JSONRPC_METHOD_NOT_FOUND` |
|
|
60
|
+
| `-32602` | `JSONRPC_INVALID_PARAMS` |
|
|
61
|
+
| `-32603` | `JSONRPC_INTERNAL_ERROR` |
|
|
62
|
+
| `-32001` | `JSONRPC_UNAUTHORIZED` |
|
|
63
|
+
|
|
64
|
+
## `createMcpHandler` options
|
|
65
|
+
|
|
66
|
+
| Option | Required | Description |
|
|
67
|
+
|--------|----------|-------------|
|
|
68
|
+
| `registry` | yes | From `createToolRegistry` |
|
|
69
|
+
| `ports` | yes | `authenticate`, `context`, optional `audit` — see [Ports](guide.md#ports--what-you-implement) |
|
|
70
|
+
| `serverInfo` | yes | `{ name, version }` |
|
|
71
|
+
| `wwwAuthenticate` | yes | Static or `(req) => …` for RFC 9728 PRM URL |
|
|
72
|
+
| `instructions` | no | Returned on `initialize` |
|
|
73
|
+
| `publicMethods` | no | Default: `initialize`, `ping`, notifications |
|
|
74
|
+
| `auditTimeoutMs` | no | Max time to wait for `ports.audit` before responding anyway. Default 1000ms — see [security.md](security.md) |
|
|
75
|
+
|
|
76
|
+
`ports.audit` fires on tool results and on auth denials (bad token, missing
|
|
77
|
+
scope, query-string token), plus the 500 path when a host port throws.
|
|
78
|
+
Malformed JSON, bad `jsonrpc`, unsupported `MCP-Protocol-Version`, oversized
|
|
79
|
+
bodies, and batch requests return JSON-RPC client errors without an audit
|
|
80
|
+
entry — those are protocol errors, not auth denials.
|
|
81
|
+
|
|
82
|
+
## `createOAuthRouter` options
|
|
83
|
+
|
|
84
|
+
| Option | Default | Description |
|
|
85
|
+
|--------|---------|-------------|
|
|
86
|
+
| `ports` | — | Required OAuth ports — see [Ports](guide.md#ports--what-you-implement) |
|
|
87
|
+
| `resourcePath` | `"/mcp"` | MCP resource path in PRM. Normalized once at construction (a trailing slash is stripped); construction throws if it starts with `/.well-known` or equals `oauthPath` |
|
|
88
|
+
| `oauthPath` | `"/mcp/oauth"` | Prefix for authorize / token / register / revoke |
|
|
89
|
+
| `realm` | — | Optional realm string |
|
|
90
|
+
| `scopes` | `["mcp"]` | Advertised in metadata, and the ceiling `authorize` validates against |
|
|
91
|
+
| `defaultScopes` | full `scopes` | Scopes granted when a client omits `scope` entirely. **Required at construction** once `scopes` has more than one entry — the router throws rather than silently granting everything advertised. Must be a subset of `scopes` |
|
|
92
|
+
| `tokenEndpointAuthMethods` | `["none"]` | Advertised client auth methods |
|
|
93
|
+
| `allowUnregisteredClients` | `true` | `false` requires `clientStore`, unmounts DCR, and rejects unknown `client_id`s (`unauthorized_client` / `invalid_client`) |
|
|
94
|
+
| `requireRegisteredClients` | `true` | Reject any `client_id` that isn't `clientStore`-resolved or self-sealed via this server's own `/register` (see [security.md](security.md)). DCR stays mounted, unlike `allowUnregisteredClients: false` — a client just can't invent an id out of thin air |
|
|
95
|
+
| `redirect` | see below | Redirect URI allowlist |
|
|
96
|
+
| `consent` | see below | Consent/approval policy for `/authorize` |
|
|
97
|
+
|
|
98
|
+
**`redirect` (`RedirectAllowlistOptions`):**
|
|
99
|
+
|
|
100
|
+
| Field | Default | Description |
|
|
101
|
+
|-------|---------|-------------|
|
|
102
|
+
| `extra` | `[]` | Exact-match redirect URIs |
|
|
103
|
+
| `allowLoopback` | `true` | Allow `http:` loopback — `127.0.0.1`, `[::1]` (any port). `localhost` and `https:` loopback are not accepted — see [security.md](security.md) |
|
|
104
|
+
| `allowClaude` | `true` | Allow the Claude.ai Custom Connector callback |
|
|
105
|
+
|
|
106
|
+
Pre-registered clients, and DCR-issued clients whose id was sealed by this
|
|
107
|
+
server's own `/register`, bypass this allowlist entirely — they are
|
|
108
|
+
validated against their own bound `redirectUris`.
|
|
109
|
+
|
|
110
|
+
**`consent` (`ConsentOptions`):**
|
|
111
|
+
|
|
112
|
+
| Field | Default | Description |
|
|
113
|
+
|-------|---------|-------------|
|
|
114
|
+
| `render` | built-in interstitial | Render the approval page yourself — return any `Response`, including a redirect to your own route. Receives a `ConsentRequest` with the validated `clientId`, `redirectUri`, `scope`, `resource`, `user`, and an opaque `ticket` to embed in your form's `consent_ticket` field |
|
|
115
|
+
| `preApprovedClientIds` | `[]` | Client ids that skip the interstitial and go straight to code issuance. Only honored when `clientStore` actually resolves the id — a DCR or self-invented id is never trusted to skip it |
|
|
116
|
+
|
|
117
|
+
## `createMcpApp` options
|
|
118
|
+
|
|
119
|
+
| Option | Default | Description |
|
|
120
|
+
|--------|---------|-------------|
|
|
121
|
+
| `serverInfo`, `tools`, `auth` | — | Required |
|
|
122
|
+
| `clients` | `["claude"]` | Connector profiles to serve |
|
|
123
|
+
| `resourcePath` | `"/mcp"` | MCP endpoint path |
|
|
124
|
+
| `oauthPath` | `` `${resourcePath}/oauth` `` | OAuth prefix |
|
|
125
|
+
| `scopes` | `["mcp"]` | Scopes this server grants |
|
|
126
|
+
| `defaultScopes` | full `scopes` | Same as `OAuthRouterOptions.defaultScopes` — required once `scopes` has more than one entry |
|
|
127
|
+
| `realm` | `serverInfo.name` | `WWW-Authenticate` realm |
|
|
128
|
+
| `extraRedirectUris` | `[]` | Callbacks beyond the client profiles |
|
|
129
|
+
| `allowLoopback` | `true` | Allow loopback redirects — see the narrowed `http:`-only, no-`localhost` behavior in [security.md](security.md) |
|
|
130
|
+
| `consent` | built-in interstitial | Same as `OAuthRouterOptions.consent` — override the approval page or pre-approve specific client ids |
|
|
131
|
+
| `requireRegisteredClients` | `true` | Same as `OAuthRouterOptions.requireRegisteredClients` — inventing a public `client_id` is rejected unless it comes from `clientStore` or this server's own `/register` |
|
|
132
|
+
| `instructions`, `validateArgs`, `onToolError`, `context`, `audit`, `auditTimeoutMs` | — | Passed through. `audit`/`auditTimeoutMs` are the MCP-side (tool-call) hook — the OAuth side has its own, separate `auth.audit` (see [Ports](guide.md#ports--what-you-implement)) |
|
|
133
|
+
|
|
134
|
+
## Exports
|
|
135
|
+
|
|
136
|
+
<details>
|
|
137
|
+
<summary><code>mcp-trellis</code></summary>
|
|
138
|
+
|
|
139
|
+
- `createMcpApp`, `createMcpHandler`, `createToolRegistry`
|
|
140
|
+
- `consoleAudit` — convenience `audit` sink that logs to the console (pass any function for metrics / DB / APM)
|
|
141
|
+
- `defineTool`, `apiTool` — typed, validated tool authoring on top of `ToolDef`
|
|
142
|
+
- `CLIENT_PROFILES`, `DEFAULT_CLIENTS`, `authMethodsFor`, `redirectUrisFor`, `preRegisteredClients`, `hasDynamicClient`
|
|
143
|
+
- `parseBearer`, `timingSafeEqual`, `matchesAny`, `wwwAuthenticateHeader`, `rejectQueryToken`
|
|
144
|
+
- `validateAgainstSchema`, `JSON_SCHEMA_TYPES`, `SUPPORTED_SCHEMA_KEYWORDS`, `IGNORED_SCHEMA_KEYWORDS`, `unsupportedKeywords`, `missingObjectType`
|
|
145
|
+
- `rpcResult`, `rpcError`, JSON-RPC error constants
|
|
146
|
+
- `pickProtocolVersion`, `PROTOCOL_VERSIONS`, `DEFAULT_PROTOCOL_VERSION`, `ASSUMED_HEADER_PROTOCOL_VERSION`
|
|
147
|
+
- `jsonResponse`, `emptyResponse`, `optionsResponse`, `corsHeaders`, `methodNotAllowed`
|
|
148
|
+
- Types: `McpApp`, `McpAppOptions`, `McpAppAuth`, `VerifiedToken`, `ClientName`, `ClientProfile`, `McpHandler`, `McpHandlerOptions`, `McpPorts`, `Principal`, `AuditEntry`, `ServerInfo`, `ToolDef`, `ToolHandler`, `ToolResult`, `ToolRegistry`, `JsonSchema`, `StandardSchemaV1`, `DefineToolOptions`, `ApiToolOptions`, `ApiRequest`, `ClientStore`, `RegisteredClient`, `CodeStore`, `MintAccessTokenInput`, `RefreshAccessTokenInput`, `RevokeTokenInput`, `MintedToken`, `OAuthUser`, …
|
|
149
|
+
|
|
150
|
+
</details>
|
|
151
|
+
|
|
152
|
+
<details>
|
|
153
|
+
<summary><code>mcp-trellis/oauth</code></summary>
|
|
154
|
+
|
|
155
|
+
- `createOAuthRouter`
|
|
156
|
+
- `authorizationServerMetadata`, `protectedResourceMetadata`, `mcpWwwAuthenticate`
|
|
157
|
+
- `canonicalResource`, `resourcesEqual`, `firstResourceError`, `resourceErrorInfo`, `DEFAULT_RESOURCE_PATH`
|
|
158
|
+
- `isAllowedRedirectUri`, `CLAUDE_CALLBACK`
|
|
159
|
+
- `issueAuthCode`, `consumeAuthCode`, `newClientId`
|
|
160
|
+
- `verifyPkceS256`, `sha256Base64Url`, `randomBase64Url`
|
|
161
|
+
- `readClientAuth`, `firstClientAuthError`, `unregisteredClientsAllowed`
|
|
162
|
+
- `parseScope`, `formatScope`, `requestedScopes`, `firstScopeError`, `scopeErrorInfo`
|
|
163
|
+
- `defaultScopes`, `registeredClientsRequired` — resolve the effective option value, same pattern as `unregisteredClientsAllowed`
|
|
164
|
+
- `hashClientSecret`, `verifyClientSecret` — for `ClientStore.secretHash` (hash once at registration time; the library verifies)
|
|
165
|
+
- `buildErrorRedirectUrl` — the RFC 6749 §4.1.2.1 helper `/authorize` uses internally, exported for hosts composing their own authorize flow via [compose the primitives](guide.md#advanced-compose-the-primitives)
|
|
166
|
+
- `normalizeConfiguredPath` — strips a trailing slash from a configured path (`resourcePath`, `oauthPath`); `createOAuthRouter` applies this to `resourcePath` internally, exported for hosts composing their own routing
|
|
167
|
+
- `GRANT_TYPES`, `OAUTH_ERRORS`, `DEFAULT_SCOPE`, `TOKEN_ENDPOINT_AUTH_METHODS`
|
|
168
|
+
- Types: `OAuthUser`, `MintedToken`, `OAuthPorts`, `OAuthAuditEntry`, `OAuthRouterOptions`, `OAuthErrorInfo`, `AuthCodeRecord`, `CodeStore`, `ClientStore`, `RegisteredClient`, `ClientAssertion`, `ClientAuth`, `TokenEndpointAuthMethod`, `MintAccessTokenInput`, `RefreshAccessTokenInput`, `RevokeTokenInput`, `ConsentOptions`, `ConsentRequest`
|
|
169
|
+
|
|
170
|
+
</details>
|
|
171
|
+
|
|
172
|
+
<details>
|
|
173
|
+
<summary><code>mcp-trellis/node</code></summary>
|
|
174
|
+
|
|
175
|
+
- `asNodeHandler`, `resolveOrigin`, `isAllowedOrigin`, `toWebRequest`, `sendWebResponse`, `readNodeBody`
|
|
176
|
+
- Types: `NodeRequestLike`, `NodeResponseLike`, `ToWebRequestOptions`, `ResolveOriginOptions`, `AsNodeHandlerOptions`, `OriginAllowlistOptions`
|
|
177
|
+
|
|
178
|
+
</details>
|