@absolutejs/mcp 0.7.0 → 0.9.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 +33 -0
- package/dist/index.js +314 -10
- package/dist/manifest.js +8 -0
- package/dist/src/client.d.ts +5 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/oauth.d.ts +98 -0
- package/dist/src/types.d.ts +3 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @absolutejs/mcp
|
|
2
2
|
|
|
3
|
+
MCP tool discovery preserves the OpenID AuthZEN COAZ `coaz` marker and
|
|
4
|
+
`x-coaz-mapping` JSON Schema extension end to end. Use `@absolutejs/policy` to
|
|
5
|
+
validate and evaluate the mapping before dispatching an authorized tool call.
|
|
6
|
+
|
|
3
7
|
Serve a remote [Model Context Protocol](https://modelcontextprotocol.io) endpoint
|
|
4
8
|
— streamable HTTP, stateless — from a tool/prompt/resource registry. You supply
|
|
5
9
|
**which** tools to expose and **how** to authorize a request into a caller; the
|
|
@@ -309,6 +313,35 @@ app.use(mcpServer({ path: "/mcp" /* member */ })).use(
|
|
|
309
313
|
|
|
310
314
|
Only one endpoint per app should set `serveRootMetadata` (the un-suffixed alias).
|
|
311
315
|
|
|
316
|
+
## OAuth-native MCP client
|
|
317
|
+
|
|
318
|
+
`createMcpOAuthProvider` handles the current MCP authorization flow without
|
|
319
|
+
coupling to an identity vendor: RFC 9728 protected-resource discovery, OAuth or
|
|
320
|
+
OIDC authorization-server discovery, Client ID Metadata Document identifiers,
|
|
321
|
+
PKCE S256, resource indicators, refresh rotation, incremental scope challenges,
|
|
322
|
+
and optional DPoP proofs. The host owns the user interaction and token store.
|
|
323
|
+
|
|
324
|
+
```ts
|
|
325
|
+
const authorization = createMcpOAuthProvider({
|
|
326
|
+
endpoint: "https://tools.example/mcp",
|
|
327
|
+
clientId: "https://my-agent.example/oauth-client.json",
|
|
328
|
+
redirectUri: "https://my-agent.example/oauth/callback",
|
|
329
|
+
fetch: egress.fetch,
|
|
330
|
+
store: durableTokenStore,
|
|
331
|
+
onAuthorize: showConsentAndWaitForCallback,
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const client = createMcpClient({
|
|
335
|
+
url: "https://tools.example/mcp",
|
|
336
|
+
authorization,
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The client retries a 401 only once and only after the authorization provider
|
|
341
|
+
reports success. Metadata fetches require HTTPS, reject redirects, enforce byte
|
|
342
|
+
limits, verify issuer/resource identity, and use the injected fetch so production
|
|
343
|
+
deployments can route discovery through `@absolutejs/egress`.
|
|
344
|
+
|
|
312
345
|
## License
|
|
313
346
|
|
|
314
347
|
Business Source License 1.1 — see [LICENSE](./LICENSE). Converts to Apache 2.0
|
package/dist/index.js
CHANGED
|
@@ -131,11 +131,13 @@ var createMcpClient = (options) => {
|
|
|
131
131
|
let protocolVersion = options.protocolVersion ?? DEFAULT_PROTOCOL;
|
|
132
132
|
let sessionId = null;
|
|
133
133
|
let nextId = 1;
|
|
134
|
+
const authorizationHeaders = (method) => options.authorization?.headers({ method, url: options.url }) ?? {};
|
|
134
135
|
const respond = async (id, result) => {
|
|
135
136
|
const headers = {
|
|
136
137
|
"content-type": "application/json",
|
|
137
138
|
"mcp-protocol-version": protocolVersion,
|
|
138
|
-
...options.headers
|
|
139
|
+
...options.headers,
|
|
140
|
+
...await authorizationHeaders("POST")
|
|
139
141
|
};
|
|
140
142
|
if (sessionId !== null)
|
|
141
143
|
headers["mcp-session-id"] = sessionId;
|
|
@@ -167,21 +169,38 @@ var createMcpClient = (options) => {
|
|
|
167
169
|
accept: "application/json, text/event-stream",
|
|
168
170
|
"content-type": "application/json",
|
|
169
171
|
"mcp-protocol-version": protocolVersion,
|
|
170
|
-
...options.headers
|
|
172
|
+
...options.headers,
|
|
173
|
+
...await authorizationHeaders("POST")
|
|
171
174
|
};
|
|
172
175
|
if (sessionId !== null)
|
|
173
176
|
headers["mcp-session-id"] = sessionId;
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
const requestBody = JSON.stringify({
|
|
178
|
+
id: nextId++,
|
|
179
|
+
jsonrpc: "2.0",
|
|
180
|
+
method,
|
|
181
|
+
...params === undefined ? {} : { params }
|
|
182
|
+
});
|
|
183
|
+
let response = await doFetch(options.url, {
|
|
184
|
+
body: requestBody,
|
|
181
185
|
headers,
|
|
182
186
|
method: "POST",
|
|
183
187
|
signal: controller.signal
|
|
184
188
|
});
|
|
189
|
+
if (response.status === 401 && options.authorization?.onUnauthorized) {
|
|
190
|
+
const retry = await options.authorization.onUnauthorized({
|
|
191
|
+
method: "POST",
|
|
192
|
+
response,
|
|
193
|
+
url: options.url
|
|
194
|
+
});
|
|
195
|
+
if (retry) {
|
|
196
|
+
response = await doFetch(options.url, {
|
|
197
|
+
body: requestBody,
|
|
198
|
+
headers: { ...headers, ...await authorizationHeaders("POST") },
|
|
199
|
+
method: "POST",
|
|
200
|
+
signal: controller.signal
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
185
204
|
const captured = response.headers.get("mcp-session-id");
|
|
186
205
|
if (captured)
|
|
187
206
|
sessionId = captured;
|
|
@@ -209,7 +228,8 @@ var createMcpClient = (options) => {
|
|
|
209
228
|
const headers = {
|
|
210
229
|
"content-type": "application/json",
|
|
211
230
|
"mcp-protocol-version": protocolVersion,
|
|
212
|
-
...options.headers
|
|
231
|
+
...options.headers,
|
|
232
|
+
...await authorizationHeaders("POST")
|
|
213
233
|
};
|
|
214
234
|
if (sessionId !== null)
|
|
215
235
|
headers["mcp-session-id"] = sessionId;
|
|
@@ -245,6 +265,7 @@ var createMcpClient = (options) => {
|
|
|
245
265
|
const tools = isRecord(result) && Array.isArray(result.tools) ? result.tools : [];
|
|
246
266
|
collected.push(...tools.filter(isRecord).map((tool) => ({
|
|
247
267
|
annotations: isRecord(tool.annotations) ? tool.annotations : undefined,
|
|
268
|
+
coaz: typeof tool.coaz === "boolean" ? tool.coaz : undefined,
|
|
248
269
|
description: typeof tool.description === "string" ? tool.description : undefined,
|
|
249
270
|
inputSchema: isRecord(tool.inputSchema) ? tool.inputSchema : undefined,
|
|
250
271
|
name: typeof tool.name === "string" ? tool.name : "",
|
|
@@ -285,6 +306,283 @@ var createMcpClient = (options) => {
|
|
|
285
306
|
};
|
|
286
307
|
return { callTool, initialize, listResources, listTools, ping, readResource };
|
|
287
308
|
};
|
|
309
|
+
// src/oauth.ts
|
|
310
|
+
var splitChallenges = (value) => {
|
|
311
|
+
const entries = [];
|
|
312
|
+
let quoted = false;
|
|
313
|
+
let start = 0;
|
|
314
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
315
|
+
const character = value[index];
|
|
316
|
+
if (character === '"' && value[index - 1] !== "\\")
|
|
317
|
+
quoted = !quoted;
|
|
318
|
+
if (character === "," && !quoted) {
|
|
319
|
+
entries.push(value.slice(start, index).trim());
|
|
320
|
+
start = index + 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
entries.push(value.slice(start).trim());
|
|
324
|
+
return entries;
|
|
325
|
+
};
|
|
326
|
+
var parseMcpAuthorizationChallenge = (value) => {
|
|
327
|
+
if (!value)
|
|
328
|
+
return;
|
|
329
|
+
const firstSpace = value.indexOf(" ");
|
|
330
|
+
const scheme = firstSpace < 0 ? value : value.slice(0, firstSpace);
|
|
331
|
+
if (scheme.toLowerCase() !== "bearer" && scheme.toLowerCase() !== "dpop")
|
|
332
|
+
return;
|
|
333
|
+
const parameters = new Map;
|
|
334
|
+
for (const entry of splitChallenges(firstSpace < 0 ? "" : value.slice(firstSpace + 1))) {
|
|
335
|
+
const separator = entry.indexOf("=");
|
|
336
|
+
if (separator < 1)
|
|
337
|
+
continue;
|
|
338
|
+
const key = entry.slice(0, separator).trim().toLowerCase();
|
|
339
|
+
const raw = entry.slice(separator + 1).trim();
|
|
340
|
+
parameters.set(key, raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1).replaceAll("\\\"", '"') : raw);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
scheme,
|
|
344
|
+
resourceMetadataUrl: parameters.get("resource_metadata"),
|
|
345
|
+
scopes: parameters.get("scope")?.split(" ").filter(Boolean) ?? [],
|
|
346
|
+
error: parameters.get("error")
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
var endpointMetadataPath = (endpoint) => `/.well-known/oauth-protected-resource${endpoint.pathname === "/" ? "" : endpoint.pathname}`;
|
|
350
|
+
var fetchJson = async (url, fetcher, maxBytes) => {
|
|
351
|
+
const target = new URL(url);
|
|
352
|
+
if (target.protocol !== "https:")
|
|
353
|
+
throw new Error("OAuth metadata requires HTTPS");
|
|
354
|
+
const response = await fetcher(target, {
|
|
355
|
+
headers: { accept: "application/json" },
|
|
356
|
+
redirect: "error"
|
|
357
|
+
});
|
|
358
|
+
if (!response.ok)
|
|
359
|
+
throw new Error(`OAuth metadata discovery failed with ${response.status}`);
|
|
360
|
+
const declared = Number(response.headers.get("content-length") ?? "0");
|
|
361
|
+
if (declared > maxBytes)
|
|
362
|
+
throw new Error("OAuth metadata exceeds byte limit");
|
|
363
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
364
|
+
if (bytes.byteLength > maxBytes)
|
|
365
|
+
throw new Error("OAuth metadata exceeds byte limit");
|
|
366
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
367
|
+
};
|
|
368
|
+
var discoverMcpAuthorization = async ({
|
|
369
|
+
endpoint,
|
|
370
|
+
fetch: fetcher,
|
|
371
|
+
resourceMetadataUrl,
|
|
372
|
+
maxMetadataBytes = 64 * 1024
|
|
373
|
+
}) => {
|
|
374
|
+
const target = new URL(endpoint);
|
|
375
|
+
const resourceUrl = resourceMetadataUrl ?? new URL(endpointMetadataPath(target), target.origin).toString();
|
|
376
|
+
const resource = await fetchJson(resourceUrl, fetcher, maxMetadataBytes);
|
|
377
|
+
if (resource.resource !== endpoint)
|
|
378
|
+
throw new Error("Protected resource metadata has the wrong resource identifier");
|
|
379
|
+
const issuer = resource.authorization_servers?.[0];
|
|
380
|
+
if (!issuer)
|
|
381
|
+
throw new Error("Protected resource metadata has no authorization server");
|
|
382
|
+
const issuerUrl = new URL(issuer);
|
|
383
|
+
if (issuerUrl.protocol !== "https:")
|
|
384
|
+
throw new Error("Authorization server issuer requires HTTPS");
|
|
385
|
+
const candidates = [
|
|
386
|
+
new URL("/.well-known/oauth-authorization-server", issuerUrl).toString(),
|
|
387
|
+
new URL("/.well-known/openid-configuration", issuerUrl).toString()
|
|
388
|
+
];
|
|
389
|
+
let authorizationServer;
|
|
390
|
+
for (const candidate of candidates) {
|
|
391
|
+
try {
|
|
392
|
+
const metadata = await fetchJson(candidate, fetcher, maxMetadataBytes);
|
|
393
|
+
if (metadata.issuer === issuer) {
|
|
394
|
+
authorizationServer = metadata;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
} catch {}
|
|
398
|
+
}
|
|
399
|
+
if (!authorizationServer)
|
|
400
|
+
throw new Error("Authorization server discovery failed");
|
|
401
|
+
return { resource, authorizationServer, resourceMetadataUrl: resourceUrl };
|
|
402
|
+
};
|
|
403
|
+
var random = (bytes = 32) => Buffer.from(crypto.getRandomValues(new Uint8Array(bytes))).toString("base64url");
|
|
404
|
+
var challengeFor = async (verifier) => Buffer.from(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))).toString("base64url");
|
|
405
|
+
var createMcpAuthorizationRequest = async ({
|
|
406
|
+
discovery,
|
|
407
|
+
clientId,
|
|
408
|
+
redirectUri,
|
|
409
|
+
scopes
|
|
410
|
+
}) => {
|
|
411
|
+
if (!discovery.authorizationServer.code_challenge_methods_supported?.includes("S256"))
|
|
412
|
+
throw new Error("Authorization server does not advertise PKCE S256");
|
|
413
|
+
const codeVerifier = random(48);
|
|
414
|
+
const state = random(24);
|
|
415
|
+
const url = new URL(discovery.authorizationServer.authorization_endpoint);
|
|
416
|
+
url.searchParams.set("response_type", "code");
|
|
417
|
+
url.searchParams.set("client_id", clientId);
|
|
418
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
419
|
+
url.searchParams.set("code_challenge", await challengeFor(codeVerifier));
|
|
420
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
421
|
+
url.searchParams.set("resource", discovery.resource.resource);
|
|
422
|
+
url.searchParams.set("state", state);
|
|
423
|
+
if (scopes.length)
|
|
424
|
+
url.searchParams.set("scope", scopes.join(" "));
|
|
425
|
+
return { authorizationUrl: url.toString(), codeVerifier, state };
|
|
426
|
+
};
|
|
427
|
+
var parseTokens = (value, resource, now) => {
|
|
428
|
+
if (!value || typeof value !== "object")
|
|
429
|
+
throw new Error("Malformed OAuth token response");
|
|
430
|
+
const body = value;
|
|
431
|
+
if (typeof body.access_token !== "string")
|
|
432
|
+
throw new Error("OAuth response has no access token");
|
|
433
|
+
return {
|
|
434
|
+
accessToken: body.access_token,
|
|
435
|
+
tokenType: body.token_type === "DPoP" ? "DPoP" : "Bearer",
|
|
436
|
+
...typeof body.expires_in === "number" ? { expiresAt: now + body.expires_in * 1000 } : {},
|
|
437
|
+
...typeof body.refresh_token === "string" ? { refreshToken: body.refresh_token } : {},
|
|
438
|
+
scopes: typeof body.scope === "string" ? body.scope.split(" ").filter(Boolean) : [],
|
|
439
|
+
resource
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
var tokenRequest = async ({
|
|
443
|
+
endpoint,
|
|
444
|
+
fetch: fetcher,
|
|
445
|
+
params,
|
|
446
|
+
dpopProof,
|
|
447
|
+
now,
|
|
448
|
+
resource
|
|
449
|
+
}) => {
|
|
450
|
+
const response = await fetcher(endpoint, {
|
|
451
|
+
method: "POST",
|
|
452
|
+
redirect: "error",
|
|
453
|
+
headers: {
|
|
454
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
455
|
+
...dpopProof ? { dpop: dpopProof } : {}
|
|
456
|
+
},
|
|
457
|
+
body: params.toString()
|
|
458
|
+
});
|
|
459
|
+
if (!response.ok)
|
|
460
|
+
throw new Error(`OAuth token exchange failed with ${response.status}`);
|
|
461
|
+
return parseTokens(await response.json(), resource, now);
|
|
462
|
+
};
|
|
463
|
+
var createMemoryMcpOAuthTokenStore = () => {
|
|
464
|
+
const tokens = new Map;
|
|
465
|
+
return {
|
|
466
|
+
load: async (resource) => tokens.get(resource),
|
|
467
|
+
save: async (value) => {
|
|
468
|
+
tokens.set(value.resource, structuredClone(value));
|
|
469
|
+
},
|
|
470
|
+
remove: async (resource) => {
|
|
471
|
+
tokens.delete(resource);
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
};
|
|
475
|
+
var createMcpOAuthProvider = (options) => {
|
|
476
|
+
const now = options.now ?? Date.now;
|
|
477
|
+
let discovery;
|
|
478
|
+
const ensureDiscovery = async (metadataUrl) => discovery ??= await discoverMcpAuthorization({
|
|
479
|
+
endpoint: options.endpoint,
|
|
480
|
+
fetch: options.fetch,
|
|
481
|
+
resourceMetadataUrl: metadataUrl,
|
|
482
|
+
maxMetadataBytes: options.maxMetadataBytes
|
|
483
|
+
});
|
|
484
|
+
const refresh = async (tokens) => {
|
|
485
|
+
if (!tokens.refreshToken)
|
|
486
|
+
return false;
|
|
487
|
+
const found = await ensureDiscovery();
|
|
488
|
+
const params = new URLSearchParams({
|
|
489
|
+
grant_type: "refresh_token",
|
|
490
|
+
refresh_token: tokens.refreshToken,
|
|
491
|
+
client_id: options.clientId,
|
|
492
|
+
resource: found.resource.resource
|
|
493
|
+
});
|
|
494
|
+
if (tokens.scopes.length)
|
|
495
|
+
params.set("scope", tokens.scopes.join(" "));
|
|
496
|
+
const proof = await options.createDpopProof?.({
|
|
497
|
+
method: "POST",
|
|
498
|
+
url: found.authorizationServer.token_endpoint
|
|
499
|
+
});
|
|
500
|
+
const next = await tokenRequest({
|
|
501
|
+
endpoint: found.authorizationServer.token_endpoint,
|
|
502
|
+
fetch: options.fetch,
|
|
503
|
+
params,
|
|
504
|
+
dpopProof: proof,
|
|
505
|
+
now: now(),
|
|
506
|
+
resource: found.resource.resource
|
|
507
|
+
});
|
|
508
|
+
await options.store.save({
|
|
509
|
+
...next,
|
|
510
|
+
refreshToken: next.refreshToken ?? tokens.refreshToken
|
|
511
|
+
});
|
|
512
|
+
return true;
|
|
513
|
+
};
|
|
514
|
+
return {
|
|
515
|
+
headers: async ({ method, url }) => {
|
|
516
|
+
let tokens = await options.store.load(options.endpoint);
|
|
517
|
+
if (!tokens)
|
|
518
|
+
return {};
|
|
519
|
+
if (tokens.expiresAt !== undefined && tokens.expiresAt <= now() + 5000) {
|
|
520
|
+
if (!await refresh(tokens))
|
|
521
|
+
return {};
|
|
522
|
+
tokens = await options.store.load(options.endpoint);
|
|
523
|
+
if (!tokens)
|
|
524
|
+
return {};
|
|
525
|
+
}
|
|
526
|
+
const headers = {
|
|
527
|
+
authorization: `${tokens.tokenType} ${tokens.accessToken}`
|
|
528
|
+
};
|
|
529
|
+
const proof = await options.createDpopProof?.({
|
|
530
|
+
accessToken: tokens.accessToken,
|
|
531
|
+
method,
|
|
532
|
+
url
|
|
533
|
+
});
|
|
534
|
+
if (proof)
|
|
535
|
+
headers.dpop = proof;
|
|
536
|
+
return headers;
|
|
537
|
+
},
|
|
538
|
+
onUnauthorized: async ({ response }) => {
|
|
539
|
+
const challenge = parseMcpAuthorizationChallenge(response.headers.get("www-authenticate"));
|
|
540
|
+
const found = await ensureDiscovery(challenge?.resourceMetadataUrl);
|
|
541
|
+
const existing = await options.store.load(found.resource.resource);
|
|
542
|
+
if (existing?.refreshToken && await refresh(existing))
|
|
543
|
+
return true;
|
|
544
|
+
const scopes = [
|
|
545
|
+
...new Set([...options.scopes ?? [], ...challenge?.scopes ?? []])
|
|
546
|
+
];
|
|
547
|
+
const request = await createMcpAuthorizationRequest({
|
|
548
|
+
discovery: found,
|
|
549
|
+
clientId: options.clientId,
|
|
550
|
+
redirectUri: options.redirectUri,
|
|
551
|
+
scopes
|
|
552
|
+
});
|
|
553
|
+
const result = await options.onAuthorize({
|
|
554
|
+
authorizationUrl: request.authorizationUrl,
|
|
555
|
+
state: request.state,
|
|
556
|
+
scopes,
|
|
557
|
+
resource: found.resource.resource
|
|
558
|
+
});
|
|
559
|
+
if (result.state !== request.state)
|
|
560
|
+
throw new Error("OAuth state mismatch");
|
|
561
|
+
const params = new URLSearchParams({
|
|
562
|
+
grant_type: "authorization_code",
|
|
563
|
+
code: result.code,
|
|
564
|
+
client_id: options.clientId,
|
|
565
|
+
redirect_uri: options.redirectUri,
|
|
566
|
+
code_verifier: request.codeVerifier,
|
|
567
|
+
resource: found.resource.resource
|
|
568
|
+
});
|
|
569
|
+
const proof = await options.createDpopProof?.({
|
|
570
|
+
method: "POST",
|
|
571
|
+
url: found.authorizationServer.token_endpoint
|
|
572
|
+
});
|
|
573
|
+
const tokens = await tokenRequest({
|
|
574
|
+
endpoint: found.authorizationServer.token_endpoint,
|
|
575
|
+
fetch: options.fetch,
|
|
576
|
+
params,
|
|
577
|
+
dpopProof: proof,
|
|
578
|
+
now: now(),
|
|
579
|
+
resource: found.resource.resource
|
|
580
|
+
});
|
|
581
|
+
await options.store.save(tokens);
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
};
|
|
288
586
|
// node_modules/@absolutejs/agency/dist/authzen.js
|
|
289
587
|
var createCoazActionInput = ({
|
|
290
588
|
actor,
|
|
@@ -466,6 +764,7 @@ var toolsList = async (config, caller, scopes, id, params) => {
|
|
|
466
764
|
const tools = await config.tools({ caller, meta: {} });
|
|
467
765
|
const visible = Object.entries(tools).filter(([, tool]) => scopeAllows(tool, scopes) && agencyAllows(config, tool, scopes)).map(([name, tool]) => ({
|
|
468
766
|
annotations: tool.annotations,
|
|
767
|
+
...tool.coaz === undefined ? {} : { coaz: tool.coaz },
|
|
469
768
|
description: tool.description,
|
|
470
769
|
inputSchema: tool.inputSchema,
|
|
471
770
|
name,
|
|
@@ -1257,17 +1556,22 @@ export {
|
|
|
1257
1556
|
verifyBearer,
|
|
1258
1557
|
publicMcpTask,
|
|
1259
1558
|
protectedResourceMetadata,
|
|
1559
|
+
parseMcpAuthorizationChallenge,
|
|
1260
1560
|
metadataPathFor,
|
|
1261
1561
|
mcpServer,
|
|
1262
1562
|
mcpPostgresSchemaSql,
|
|
1263
1563
|
feedbackTools,
|
|
1264
1564
|
dispatchMcp,
|
|
1565
|
+
discoverMcpAuthorization,
|
|
1265
1566
|
createSessionRegistry,
|
|
1266
1567
|
createPostgresMcpTaskStore,
|
|
1267
1568
|
createPostgresMcpSessionStore,
|
|
1268
1569
|
createMemoryMcpTaskStore,
|
|
1570
|
+
createMemoryMcpOAuthTokenStore,
|
|
1571
|
+
createMcpOAuthProvider,
|
|
1269
1572
|
createMcpHandler,
|
|
1270
1573
|
createMcpClient,
|
|
1574
|
+
createMcpAuthorizationRequest,
|
|
1271
1575
|
McpClientError,
|
|
1272
1576
|
FEEDBACK_INSTRUCTIONS
|
|
1273
1577
|
};
|
package/dist/manifest.js
CHANGED
|
@@ -5916,6 +5916,14 @@ var serializedTool = Type.Object({
|
|
|
5916
5916
|
});
|
|
5917
5917
|
var manifestSchema = Type.Object({
|
|
5918
5918
|
contract: Type.Union([Type.Literal(1), Type.Literal(2)]),
|
|
5919
|
+
discovery: Type.Optional(Type.Object({
|
|
5920
|
+
audiences: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
5921
|
+
certificationUrl: Type.Optional(Type.String()),
|
|
5922
|
+
intents: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
5923
|
+
keywords: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
5924
|
+
protocols: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
5925
|
+
url: Type.Optional(Type.String())
|
|
5926
|
+
})),
|
|
5919
5927
|
identity: Type.Object({
|
|
5920
5928
|
accent: Type.Optional(Type.String({ pattern: "^#[0-9a-fA-F]{3,8}$" })),
|
|
5921
5929
|
category: Type.String({ minLength: 1 }),
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpElicitationRequest, McpElicitResult, McpToolAnnotations, McpToolResult } from "./types";
|
|
2
|
+
import type { McpAuthorizationProvider } from "./oauth";
|
|
2
3
|
export declare class McpClientError extends Error {
|
|
3
4
|
readonly code: number | undefined;
|
|
4
5
|
readonly status: number | undefined;
|
|
@@ -8,6 +9,9 @@ export declare class McpClientError extends Error {
|
|
|
8
9
|
});
|
|
9
10
|
}
|
|
10
11
|
export type McpClientOptions = {
|
|
12
|
+
/** Dynamic OAuth/DPoP provider. It may answer a 401 by completing discovery,
|
|
13
|
+
* incremental authorization, or refresh; the request is retried once. */
|
|
14
|
+
authorization?: McpAuthorizationProvider;
|
|
11
15
|
clientInfo?: {
|
|
12
16
|
name: string;
|
|
13
17
|
version: string;
|
|
@@ -30,6 +34,7 @@ export type McpClientOptions = {
|
|
|
30
34
|
};
|
|
31
35
|
export type McpRemoteTool = {
|
|
32
36
|
annotations?: McpToolAnnotations;
|
|
37
|
+
coaz?: boolean;
|
|
33
38
|
description?: string;
|
|
34
39
|
inputSchema?: Record<string, unknown>;
|
|
35
40
|
name: string;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
*/
|
|
33
33
|
export { verifyBearer, type BearerResult, type BearerVerifier, type VerifiedJwt, type VerifyBearerConfig, } from "./auth";
|
|
34
34
|
export { createMcpClient, McpClientError, type McpClient, type McpClientOptions, type McpInitializeResult, type McpRemoteTool, } from "./client";
|
|
35
|
+
export * from "./oauth";
|
|
35
36
|
export { dispatchMcp, type McpDispatchContext } from "./dispatch";
|
|
36
37
|
export { FEEDBACK_INSTRUCTIONS, feedbackTools, type McpFeedbackRating, type McpFeedbackReport, type McpFeedbackStore, type McpProblemReport, } from "./feedback";
|
|
37
38
|
export { createMcpHandler } from "./handler";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export type McpProtectedResourceMetadata = {
|
|
2
|
+
resource: string;
|
|
3
|
+
authorization_servers: string[];
|
|
4
|
+
scopes_supported?: string[];
|
|
5
|
+
bearer_methods_supported?: string[];
|
|
6
|
+
resource_name?: string;
|
|
7
|
+
resource_documentation?: string;
|
|
8
|
+
};
|
|
9
|
+
export type McpAuthorizationServerMetadata = {
|
|
10
|
+
issuer: string;
|
|
11
|
+
authorization_endpoint: string;
|
|
12
|
+
token_endpoint: string;
|
|
13
|
+
registration_endpoint?: string;
|
|
14
|
+
scopes_supported?: string[];
|
|
15
|
+
code_challenge_methods_supported?: string[];
|
|
16
|
+
dpop_signing_alg_values_supported?: string[];
|
|
17
|
+
client_id_metadata_document_supported?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type McpOAuthTokens = {
|
|
20
|
+
accessToken: string;
|
|
21
|
+
tokenType: "Bearer" | "DPoP";
|
|
22
|
+
expiresAt?: number;
|
|
23
|
+
refreshToken?: string;
|
|
24
|
+
scopes: string[];
|
|
25
|
+
resource: string;
|
|
26
|
+
};
|
|
27
|
+
export type McpOAuthTokenStore = {
|
|
28
|
+
load(resource: string): Promise<McpOAuthTokens | undefined>;
|
|
29
|
+
save(tokens: McpOAuthTokens): Promise<void>;
|
|
30
|
+
remove(resource: string): Promise<void>;
|
|
31
|
+
};
|
|
32
|
+
export type McpAuthorizationChallenge = {
|
|
33
|
+
scheme: string;
|
|
34
|
+
resourceMetadataUrl?: string;
|
|
35
|
+
scopes: string[];
|
|
36
|
+
error?: string;
|
|
37
|
+
};
|
|
38
|
+
export type McpOAuthDiscovery = {
|
|
39
|
+
resource: McpProtectedResourceMetadata;
|
|
40
|
+
authorizationServer: McpAuthorizationServerMetadata;
|
|
41
|
+
resourceMetadataUrl: string;
|
|
42
|
+
};
|
|
43
|
+
export type McpAuthorizationProvider = {
|
|
44
|
+
headers(context: {
|
|
45
|
+
method: string;
|
|
46
|
+
url: string;
|
|
47
|
+
}): Promise<Record<string, string>> | Record<string, string>;
|
|
48
|
+
onUnauthorized?(context: {
|
|
49
|
+
method: string;
|
|
50
|
+
response: Response;
|
|
51
|
+
url: string;
|
|
52
|
+
}): Promise<boolean> | boolean;
|
|
53
|
+
};
|
|
54
|
+
export type McpOAuthInteractiveRequest = {
|
|
55
|
+
authorizationUrl: string;
|
|
56
|
+
state: string;
|
|
57
|
+
scopes: readonly string[];
|
|
58
|
+
resource: string;
|
|
59
|
+
};
|
|
60
|
+
export type McpOAuthOptions = {
|
|
61
|
+
clientId: string;
|
|
62
|
+
redirectUri: string;
|
|
63
|
+
store: McpOAuthTokenStore;
|
|
64
|
+
fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
65
|
+
onAuthorize(request: McpOAuthInteractiveRequest): Promise<{
|
|
66
|
+
code: string;
|
|
67
|
+
state: string;
|
|
68
|
+
}>;
|
|
69
|
+
scopes?: string[];
|
|
70
|
+
createDpopProof?: (input: {
|
|
71
|
+
accessToken?: string;
|
|
72
|
+
method: string;
|
|
73
|
+
url: string;
|
|
74
|
+
}) => Promise<string>;
|
|
75
|
+
now?: () => number;
|
|
76
|
+
maxMetadataBytes?: number;
|
|
77
|
+
};
|
|
78
|
+
export declare const parseMcpAuthorizationChallenge: (value: string | null) => McpAuthorizationChallenge | undefined;
|
|
79
|
+
export declare const discoverMcpAuthorization: ({ endpoint, fetch: fetcher, resourceMetadataUrl, maxMetadataBytes, }: {
|
|
80
|
+
endpoint: string;
|
|
81
|
+
fetch: McpOAuthOptions["fetch"];
|
|
82
|
+
resourceMetadataUrl?: string;
|
|
83
|
+
maxMetadataBytes?: number;
|
|
84
|
+
}) => Promise<McpOAuthDiscovery>;
|
|
85
|
+
export declare const createMcpAuthorizationRequest: ({ discovery, clientId, redirectUri, scopes, }: {
|
|
86
|
+
discovery: McpOAuthDiscovery;
|
|
87
|
+
clientId: string;
|
|
88
|
+
redirectUri: string;
|
|
89
|
+
scopes: readonly string[];
|
|
90
|
+
}) => Promise<{
|
|
91
|
+
authorizationUrl: string;
|
|
92
|
+
codeVerifier: string;
|
|
93
|
+
state: string;
|
|
94
|
+
}>;
|
|
95
|
+
export declare const createMemoryMcpOAuthTokenStore: () => McpOAuthTokenStore;
|
|
96
|
+
export declare const createMcpOAuthProvider: (options: McpOAuthOptions & {
|
|
97
|
+
endpoint: string;
|
|
98
|
+
}) => McpAuthorizationProvider;
|
package/dist/src/types.d.ts
CHANGED
|
@@ -110,6 +110,9 @@ export type McpToolCallContext = {
|
|
|
110
110
|
/** One callable tool. `inputSchema` is a JSON Schema object. */
|
|
111
111
|
export type McpTool = {
|
|
112
112
|
annotations?: McpToolAnnotations;
|
|
113
|
+
/** OpenID AuthZEN COAZ opt-in marker. When true, inputSchema MUST carry an
|
|
114
|
+
* `x-coaz-mapping`; hosts should evaluate it before invoking the handler. */
|
|
115
|
+
coaz?: boolean;
|
|
113
116
|
/** Enforceable semantic effects from manifest contract 2. A tool carrying
|
|
114
117
|
* this is hidden unless the server configures `agency`. */
|
|
115
118
|
authorization?: ToolAuthorization;
|
package/package.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"elysia": ">=1.1.0"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@absolutejs/agency": "^0.
|
|
15
|
-
"@absolutejs/manifest": "^0.
|
|
14
|
+
"@absolutejs/agency": "^0.4.0",
|
|
15
|
+
"@absolutejs/manifest": "^0.3.0",
|
|
16
16
|
"@sinclair/typebox": "^0.34.0"
|
|
17
17
|
},
|
|
18
18
|
"license": "BUSL-1.1",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"typecheck": "tsc --noEmit --project tsconfig.json"
|
|
59
59
|
},
|
|
60
60
|
"types": "./dist/src/index.d.ts",
|
|
61
|
-
"version": "0.
|
|
61
|
+
"version": "0.9.0"
|
|
62
62
|
}
|