@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 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 response = await doFetch(options.url, {
175
- body: JSON.stringify({
176
- id: nextId++,
177
- jsonrpc: "2.0",
178
- method,
179
- ...params === undefined ? {} : { params }
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 }),
@@ -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;
@@ -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;
@@ -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.3.0",
15
- "@absolutejs/manifest": "^0.2.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.7.0"
61
+ "version": "0.9.0"
62
62
  }