mcp-authz 0.3.0 → 0.5.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.
@@ -0,0 +1,5 @@
1
+ import { Client } from "@modelcontextprotocol/client";
2
+ //#region src/definitions.d.ts
3
+ type Definition = Record<string, unknown>;
4
+ //#endregion
5
+ export { Definition as t };
@@ -0,0 +1,132 @@
1
+ //#region src/definitions.ts
2
+ /**
3
+ * Left out of a definition: servers stamp these per build, the model does not
4
+ * read them, and a record that churns teaches people to ignore its diff. Every
5
+ * other field is kept, so the next field the spec adds is covered by default.
6
+ */
7
+ const UNRECORDED = /* @__PURE__ */ new Set(["_meta", "icons"]);
8
+ /**
9
+ * Where a server's own `instructions` are recorded: beside its capabilities,
10
+ * because they reach the model the same way a description does, and a server
11
+ * that keeps every tool identical could otherwise change what it tells the
12
+ * model there instead.
13
+ */
14
+ const INSTRUCTIONS = "server:instructions";
15
+ /** The labels a record must cover and does not, so a gap fails loudly rather than passing unchecked. */
16
+ function missingDefinitions(labels, definitions) {
17
+ return [...labels].filter((label) => {
18
+ const definition = definitions.get(label);
19
+ return typeof definition !== "object" || definition === null || Array.isArray(definition);
20
+ });
21
+ }
22
+ /** The parts of a listed tool, prompt or resource that are held to the record. */
23
+ function definitionOf(item) {
24
+ return canonical(Object.fromEntries(Object.entries(item).filter(([field]) => !UNRECORDED.has(field))));
25
+ }
26
+ /** The fields that differ, compared regardless of key order. */
27
+ function changedFields(recorded, live) {
28
+ const fields = new Set([...Object.keys(recorded), ...Object.keys(live)].filter((f) => !UNRECORDED.has(f)));
29
+ const same = (a, b) => JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
30
+ return [...fields].sort().filter((field) => !same(recorded[field], live[field]));
31
+ }
32
+ /**
33
+ * Key order is an accident of how a value was built, so sort it away. Without
34
+ * this an SDK that emitted the same definition in a different order would churn
35
+ * every record, and teach people to ignore the diff.
36
+ */
37
+ function canonical(value) {
38
+ if (Array.isArray(value)) return value.map(canonical);
39
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, inner]) => [key, canonical(inner)]));
40
+ return value;
41
+ }
42
+ /**
43
+ * Characters a person reviewing the text cannot see but a model reads:
44
+ * zero-width characters, bidirectional overrides that reorder what is
45
+ * displayed, and the Unicode tag block, which can spell out a whole hidden
46
+ * sentence. Not U+200D, the joiner inside every family or flag emoji: flagging
47
+ * it would flag honest output until nobody reads the flag.
48
+ */
49
+ const INVISIBLE = /[\u200B\u200C\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF]|[\u{E0000}-\u{E007F}]/u;
50
+ /**
51
+ * Wording seen in published tool-poisoning attacks: text addressed to the model
52
+ * rather than describing the tool. A short list on purpose. It flags a
53
+ * definition for a closer read and decides nothing, and a long list would flag
54
+ * honest tools until nobody reads the flag.
55
+ */
56
+ const ADDRESSED_TO_MODEL = [
57
+ /ignore (all |any )?(previous|prior|above) (instructions|prompts)/i,
58
+ /<\/?(important|system|instructions?)>/i,
59
+ /do not (tell|inform|mention|alert|notify) the user/i,
60
+ /without (telling|informing|asking) the user/i,
61
+ /\bid_rsa\b|~\/\.ssh|\.aws\/credentials/i
62
+ ];
63
+ /**
64
+ * Text with each invisible character spelled out as `\u{200B}`, so a person
65
+ * reading a listing or a diff sees what the model will read.
66
+ */
67
+ function reveal(text) {
68
+ return text.replace(new RegExp(INVISIBLE.source, "gu"), (char) => {
69
+ return `\\u{${char.codePointAt(0).toString(16).toUpperCase()}}`;
70
+ });
71
+ }
72
+ /** Why a definition deserves a closer read before it is approved, if it does. */
73
+ function suspicious(definition) {
74
+ const text = JSON.stringify(definition);
75
+ const reasons = [];
76
+ if (INVISIBLE.test(text)) reasons.push("contains invisible characters");
77
+ if (ADDRESSED_TO_MODEL.some((pattern) => pattern.test(text))) reasons.push("contains text addressed to the model");
78
+ return reasons;
79
+ }
80
+ const LISTED = [
81
+ ["tools", ""],
82
+ ["prompts", "prompt:"],
83
+ ["resources", "resource:"],
84
+ ["resourceTemplates", "resource:"]
85
+ ];
86
+ /**
87
+ * Every listing that arrives on a client transport, as the server wrote it and
88
+ * keyed by the label `gate()` gives it. A record is compared against what the
89
+ * proxy and `wrap` read off the wire, so it is taken from the wire too, not from
90
+ * what the SDK parsed it into. Install after `client.connect`, which sets the
91
+ * handler this wraps.
92
+ */
93
+ function captureListings(transport) {
94
+ const listed = /* @__PURE__ */ new Map();
95
+ const deliver = transport.onmessage;
96
+ transport.onmessage = (message, ...rest) => {
97
+ const result = message.result;
98
+ for (const [field, prefix] of LISTED) {
99
+ const items = result?.[field];
100
+ if (!Array.isArray(items)) continue;
101
+ for (const item of items) listed.set(`${prefix}${String(item.name)}`, item);
102
+ }
103
+ deliver?.(message, ...rest);
104
+ };
105
+ return listed;
106
+ }
107
+ /**
108
+ * Connect a client and list everything the server advertises, returning each
109
+ * item as the server wrote it, keyed by label, with its instructions under
110
+ * `server:instructions`. One function for recording a catalogue and for
111
+ * checking it later, so the two cannot read a server differently.
112
+ *
113
+ * Only what the server says it has is asked for: the SDK answers an
114
+ * unadvertised list with a warning on stdout, which is the generated module
115
+ * when a caller redirects it. The SDK follows `nextCursor` itself.
116
+ */
117
+ async function listCatalogue(client, transport) {
118
+ const listed = captureListings(transport);
119
+ await client.connect(transport);
120
+ const instructions = client.getInstructions();
121
+ if (instructions !== void 0) listed.set(INSTRUCTIONS, { instructions });
122
+ const advertised = client.getServerCapabilities() ?? {};
123
+ await Promise.all([
124
+ advertised.tools && client.listTools(),
125
+ advertised.prompts && client.listPrompts(),
126
+ advertised.resources && client.listResources(),
127
+ advertised.resources && client.listResourceTemplates()
128
+ ]);
129
+ return listed;
130
+ }
131
+ //#endregion
132
+ export { listCatalogue as a, suspicious as c, definitionOf as i, captureListings as n, missingDefinitions as o, changedFields as r, reveal as s, INSTRUCTIONS as t };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { _ as Identity, a as PermissionCatalog, c as PolicySpec, d as createPrincipal, f as definePermissions, g as DenialReason, h as AccessDeniedError, i as MatchedRule, l as Principal, m as reconcile, n as Explanation, o as PermissionOf, p as definePolicy, r as Match, s as Policy, u as Rule } from "./policy-DuZbwrKf.js";
2
- import { a as AuditDeliveryFailure, c as AuditSink, d as PromptConfig, f as ResourceConfig, h as authz, i as ApprovalSink, l as Capability, m as ToolConfig, n as ApprovalRefusedError, o as AuditErrorSink, p as ServerOptions, r as ApprovalRequest, s as AuditEvent, t as ApprovalDecision, u as Definition } from "./tools-BQE1O-7P.js";
2
+ import { a as AuditDeliveryFailure, c as AuditSink, d as PromptConfig, f as ResourceConfig, h as authz, i as ApprovalSink, l as Capability, m as ToolConfig, n as ApprovalRefusedError, o as AuditErrorSink, p as ServerOptions, r as ApprovalRequest, s as AuditEvent, t as ApprovalDecision, u as Definition } from "./tools-C1dZESYK.js";
3
3
  import { a as AuthorizationDecisionSink, i as AuthorizationDecisionEvent, n as identityFromAuth, r as jwksVerifier, t as VerifierOptions } from "./verifier-DF6gUMQ6.js";
4
4
  import { a as scopesForCapability, i as decodeMcpNameHeader, n as ScopeRequirement, o as scopesFromMcpHeaders, r as ToolScopeMap, s as TrustedMcpRoute, t as CapabilityScopeMap } from "./ladder-D18eJ7tD.js";
5
5
  import { AuthInfo, McpServer, OAuthMetadata, OAuthTokenVerifier } from "@modelcontextprotocol/server";
@@ -8,7 +8,7 @@ type DiscoverOptions = {
8
8
  /** Swap in for tests, or to add a timeout or proxy. Defaults to global fetch. */
9
9
  fetch?: typeof globalThis.fetch;
10
10
  };
11
- declare function discoverOAuth(issuer: string, options?: DiscoverOptions): Promise<OAuthMetadata>;
11
+ export declare function discoverOAuth(issuer: string, options?: DiscoverOptions): Promise<OAuthMetadata>;
12
12
  //#endregion
13
13
  //#region src/gate.d.ts
14
14
  /**
@@ -60,7 +60,7 @@ type GateOptions = {
60
60
  };
61
61
  /** Bare name for a tool, `prompt:`/`resource:` prefixed for the rest. */
62
62
  type PermissionMap<P extends string> = Readonly<Record<string, P>> | ReadonlyMap<string, P>;
63
- declare function gate<P extends string>(server: McpServer, principal: Principal<P>, permissions: PermissionMap<P>, options?: GateOptions): McpServer;
63
+ export declare function gate<P extends string>(server: McpServer, principal: Principal<P>, permissions: PermissionMap<P>, options?: GateOptions): McpServer;
64
64
  //#endregion
65
65
  //#region src/handler.d.ts
66
66
  /**
@@ -161,7 +161,11 @@ type McpFetchOptions<TContext, P extends string = string> = {
161
161
  permissions?: ReadonlyMap<string, string>;
162
162
  /** Protocol route to permission, used to distinguish policy denial from scope step-up. */
163
163
  routePermissions?: ReadonlyMap<string, string>;
164
- routeNameFor?: (kind: 'tool' | 'prompt' | 'resource', name: string) => string | undefined;
164
+ /** Every registration a concrete request name reaches, so overlapping ones are all enforced. */
165
+ routesFor?: (kind: 'tool' | 'prompt' | 'resource', name: string) => readonly {
166
+ routeName: string;
167
+ permission: string;
168
+ }[];
165
169
  /** Resolve exact and templated protocol routes to their declared permission. */
166
170
  permissionForRoute?: (kind: 'tool' | 'prompt' | 'resource', name: string) => string | undefined;
167
171
  };
@@ -189,20 +193,20 @@ type McpFetchOptions<TContext, P extends string = string> = {
189
193
  * policy ran first and already refused anyone it grants nothing. Saying so here
190
194
  * saves every caller the same non-null assertion.
191
195
  */
192
- declare function createMcpFetch<TContext = Principal<string>, P extends string = string>(options: Omit<McpFetchOptions<TContext, P>, 'policy' | 'authorize' | 'resolve'> & {
196
+ export declare function createMcpFetch<TContext = Principal<string>, P extends string = string>(options: Omit<McpFetchOptions<TContext, P>, 'policy' | 'authorize' | 'resolve'> & {
193
197
  policy: Policy<P>;
194
198
  authorize?: never;
195
199
  resolve?: (identity: Identity, principal: Principal<P>) => Promise<TContext> | TContext;
196
200
  }): (request: Request) => Promise<Response>;
197
- declare function createMcpFetch<TContext = Principal<string>, P extends string = string>(options: Omit<McpFetchOptions<TContext, P>, 'policy' | 'authorize' | 'resolve'> & {
201
+ export declare function createMcpFetch<TContext = Principal<string>, P extends string = string>(options: Omit<McpFetchOptions<TContext, P>, 'policy' | 'authorize' | 'resolve'> & {
198
202
  policy?: never;
199
203
  authorize: (identity: Identity) => Promise<Principal<P>> | Principal<P>;
200
204
  resolve?: (identity: Identity, principal: Principal<P>) => Promise<TContext> | TContext;
201
205
  }): (request: Request) => Promise<Response>;
202
- declare function createMcpFetch<TContext = Principal<string>, P extends string = string>(options: Omit<McpFetchOptions<TContext, P>, 'policy' | 'authorize' | 'resolve'> & {
206
+ export declare function createMcpFetch<TContext = Principal<string>, P extends string = string>(options: Omit<McpFetchOptions<TContext, P>, 'policy' | 'authorize' | 'resolve'> & {
203
207
  policy?: never;
204
208
  authorize?: never;
205
209
  resolve: (identity: Identity, principal: undefined) => Promise<TContext> | TContext;
206
210
  }): (request: Request) => Promise<Response>;
207
211
  //#endregion
208
- export { AccessDeniedError, type ApprovalDecision, ApprovalRefusedError, type ApprovalRequest, type ApprovalSink, type AuditDeliveryFailure, type AuditErrorSink, type AuditEvent, type AuditSink, type AuthorizationDecisionEvent, type AuthorizationDecisionSink, type Capability, type CapabilityScopeMap, type Definition, type DenialReason, type DiscoverOptions, type Explanation, type GateOptions, type Identity, type Match, type MatchedRule, type McpFetchOptions, type PermissionCatalog, type PermissionMap, type PermissionOf, type Policy, type PolicySpec, type Principal, type PromptConfig, type ResourceConfig, type Rule, type ScopeRequirement, type ServerOptions, type ToolConfig, type ToolScopeMap, type TrustedMcpRoute, type VerifierOptions, authz, createMcpFetch, createPrincipal, decodeMcpNameHeader, definePermissions, definePolicy, discoverOAuth, gate, identityFromAuth, jwksVerifier, reconcile, scopesForCapability, scopesFromMcpHeaders };
212
+ export { AccessDeniedError, type ApprovalDecision, ApprovalRefusedError, type ApprovalRequest, type ApprovalSink, type AuditDeliveryFailure, type AuditErrorSink, type AuditEvent, type AuditSink, type AuthorizationDecisionEvent, type AuthorizationDecisionSink, type Capability, type CapabilityScopeMap, type Definition, type DenialReason, type DiscoverOptions, type Explanation, type GateOptions, type Identity, type Match, type MatchedRule, type McpFetchOptions, type PermissionCatalog, type PermissionMap, type PermissionOf, type Policy, type PolicySpec, type Principal, type PromptConfig, type ResourceConfig, type Rule, type ScopeRequirement, type ServerOptions, type ToolConfig, type ToolScopeMap, type TrustedMcpRoute, type VerifierOptions, authz, createPrincipal, decodeMcpNameHeader, definePermissions, definePolicy, identityFromAuth, jwksVerifier, reconcile, scopesForCapability, scopesFromMcpHeaders };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as policyDenied, i as emitDecision, n as jwksVerifier, o as principalLabel, r as verifierFor, s as AccessDeniedError, t as identityFromAuth } from "./verifier-D6VIAYuT.js";
2
- import { d as scopesForCapability, f as scopesFromMcpHeaders, i as runScopedGate, r as permissionForRoute, u as decodeMcpNameHeader } from "./ladder-CUzOKudC.js";
2
+ import { d as scopesForCapability, f as scopesFromMcpHeaders, i as runScopedGate, r as permissionForRoute, u as decodeMcpNameHeader } from "./ladder-6TnD3hTJ.js";
3
3
  import { i as reconcile, n as definePermissions, r as definePolicy, t as createPrincipal } from "./policy-BBp3Jq6G.js";
4
4
  import { McpServer, UriTemplate, createMcpHandler, getOAuthProtectedResourceMetadataUrl, oauthMetadataResponse, requireBearerAuth } from "@modelcontextprotocol/server";
5
5
  //#region src/discovery.ts
@@ -308,11 +308,15 @@ function buildServer(definitions, options, principalOf) {
308
308
  for (const definition of enabled) definition.register(server, principal, context, guards);
309
309
  return server;
310
310
  };
311
+ const routesFor = (kind, name) => definitions.filter((definition) => definition.kind === kind && (definition.routeMatches?.(name) ?? definition.routeName === name)).map((definition) => ({
312
+ routeName: definition.routeName,
313
+ permission: definition.permission
314
+ }));
311
315
  return Object.assign(factory, {
312
316
  permissions,
313
317
  routePermissions,
314
- routeNameFor: (kind, name) => definitions.find((definition) => definition.kind === kind && (definition.routeMatches?.(name) ?? definition.routeName === name))?.routeName,
315
- permissionForRoute: (kind, name) => definitions.find((definition) => definition.kind === kind && (definition.routeMatches?.(name) ?? definition.routeName === name))?.permission
318
+ routesFor,
319
+ permissionForRoute: (kind, name) => routesFor(kind, name)[0]?.permission
316
320
  });
317
321
  }
318
322
  /**
@@ -509,6 +513,7 @@ function createMcpFetch(options) {
509
513
  throw error;
510
514
  }
511
515
  const scoped = Boolean(scopeMap || scopesForRequest);
516
+ const resourceRoutes = (route) => route.method === "resources/read" && route.name ? createServer.routesFor?.("resource", route.name) ?? [] : [];
512
517
  const routed = Boolean(createServer.permissionForRoute ?? createServer.routePermissions);
513
518
  const gateResult = await runScopedGate({
514
519
  request,
@@ -520,12 +525,16 @@ function createMcpFetch(options) {
520
525
  routed,
521
526
  scopeMap,
522
527
  resolveCapabilityScopes: (route) => {
523
- if (route.method !== "resources/read" || !route.name || !scopeMap) return void 0;
524
- const registered = createServer.routeNameFor?.("resource", route.name);
525
- return registered === void 0 ? void 0 : scopesForCapability(route.method, registered, scopeMap, requiredScopes[0] ?? "mcp");
528
+ const matches = resourceRoutes(route);
529
+ if (matches.length === 0 || !scopeMap) return void 0;
530
+ return [...new Set(matches.flatMap((match) => scopesForCapability(route.method, match.routeName, scopeMap, requiredScopes[0] ?? "mcp")))];
526
531
  },
527
532
  scopesForRequest,
528
- resolvePermission: (route) => permissionForRoute(createServer.permissionForRoute, createServer.routePermissions, route),
533
+ resolvePermission: (route) => {
534
+ const matches = resourceRoutes(route);
535
+ if (matches.length === 0) return permissionForRoute(createServer.permissionForRoute, createServer.routePermissions, route);
536
+ return (matches.find((match) => principal && !principal.can(match.permission)) ?? matches[0])?.permission;
537
+ },
529
538
  onDecision: options.onDecision,
530
539
  ...emitter ? { emitter } : {},
531
540
  resourceMetadataUrl
@@ -1,4 +1,5 @@
1
1
  import { a as policyDenied, i as emitDecision, o as principalLabel, s as AccessDeniedError } from "./verifier-D6VIAYuT.js";
2
+ import { r as parseChecked, t as hasDuplicateKey } from "./strict-json-DLKOgsGE.js";
2
3
  import { OAuthError, OAuthErrorCode, bearerAuthChallengeResponse, classifyInboundRequest, isJsonContentType } from "@modelcontextprotocol/server";
3
4
  //#region src/scopes.ts
4
5
  const BASE64_SENTINEL = /^=\?base64\?([A-Za-z0-9+/]*(?:={0,2}))\?=$/;
@@ -355,10 +356,11 @@ async function preflightScopedRequest(request, maxBytes) {
355
356
  if (raw === void 0) return protocolError(413, -32e3, `Request body exceeds the ${maxBytes} byte limit.`);
356
357
  let body;
357
358
  try {
358
- body = JSON.parse(raw);
359
+ body = parseChecked(raw);
359
360
  } catch {
360
361
  return protocolError(400, -32700, "Parse error: the request body is not valid JSON");
361
362
  }
363
+ if (hasDuplicateKey(raw)) return protocolError(400, -32600, "Invalid Request: the body repeats a key, which parsers resolve differently.", void 0, requestId(body));
362
364
  const route = classifyScopedRequest(request, body);
363
365
  if (route.kind === "reject") return protocolError(route.httpStatus, route.code, route.message, route.data, route.id);
364
366
  if (route.kind !== "modern") {
package/dist/node.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Server } from "node:http";
2
2
  //#region src/node.d.ts
3
- type ListenMcpOptions = {
3
+ export type ListenMcpOptions = {
4
4
  port: number;
5
5
  /** Log label. Defaults to `mcp-authz`. */
6
6
  name?: string;
@@ -11,6 +11,5 @@ type ListenMcpOptions = {
11
11
  * Bind a `createMcpFetch` handler to every interface. Safe because the
12
12
  * bearer gate is the security boundary, not the bind address.
13
13
  */
14
- declare function listenMcp(fetch: (request: Request) => Promise<Response>, options: ListenMcpOptions): Promise<Server>;
15
- //#endregion
16
- export { ListenMcpOptions, listenMcp };
14
+ export declare function listenMcp(fetch: (request: Request) => Promise<Response>, options: ListenMcpOptions): Promise<Server>;
15
+ //#endregion
package/dist/openapi.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { _ as Identity, l as Principal, s as Policy } from "./policy-DuZbwrKf.js";
2
- import { c as AuditSink, o as AuditErrorSink } from "./tools-BQE1O-7P.js";
2
+ import { c as AuditSink, o as AuditErrorSink } from "./tools-C1dZESYK.js";
3
3
  import { a as AuthorizationDecisionSink, t as VerifierOptions } from "./verifier-DF6gUMQ6.js";
4
- import { t as PermissionMapRecord } from "./permissions-module-DxCHuE-N.js";
4
+ import { t as PermissionMapRecord } from "./permissions-module-TOpt20D4.js";
5
5
  import { AuthInfo, OAuthMetadata, OAuthTokenVerifier } from "@modelcontextprotocol/server";
6
6
  //#region src/openapi.d.ts
7
7
  /**
@@ -22,11 +22,11 @@ import { AuthInfo, OAuthMetadata, OAuthTokenVerifier } from "@modelcontextprotoc
22
22
  * rides along untouched, so a document with `components`, `webhooks` or a
23
23
  * vendor extension comes back out the way it went in.
24
24
  */
25
- type OpenApiDocument = {
25
+ export type OpenApiDocument = {
26
26
  paths?: Record<string, Record<string, unknown> | undefined>;
27
27
  [key: string]: unknown;
28
28
  };
29
- type OpenApiOperation = {
29
+ export type OpenApiOperation = {
30
30
  /** The document's own `operationId`, which is what the permission map is keyed by. */
31
31
  operationId: string;
32
32
  /** Lowercased HTTP method. */
@@ -34,7 +34,7 @@ type OpenApiOperation = {
34
34
  /** The templated path, e.g. `/cases/{id}`. */
35
35
  path: string;
36
36
  };
37
- type OperationRecord = PermissionMapRecord & {
37
+ export type OperationRecord = PermissionMapRecord & {
38
38
  /** Every operation the document describes, in document order. */
39
39
  operations: OpenApiOperation[];
40
40
  };
@@ -47,7 +47,7 @@ type OperationRecord = PermissionMapRecord & {
47
47
  * role grants and the boot refuses until somebody decides what each operation
48
48
  * costs.
49
49
  */
50
- declare function recordOperations(spec: OpenApiDocument): OperationRecord;
50
+ export declare function recordOperations(spec: OpenApiDocument): OperationRecord;
51
51
  /**
52
52
  * The document as this caller should see it: their operations, and nothing else.
53
53
  *
@@ -56,8 +56,8 @@ declare function recordOperations(spec: OpenApiDocument): OperationRecord;
56
56
  * means walking the `$ref` graph, and a schema nobody references costs a few
57
57
  * hundred tokens where a wrongly-pruned one breaks the document.
58
58
  */
59
- declare function filterSpec<P extends string>(spec: OpenApiDocument, principal: Principal<P>, permissions: Readonly<Record<string, string>>): OpenApiDocument;
60
- type OpenApiFetchOptions<P extends string = string> = {
59
+ export declare function filterSpec<P extends string>(spec: OpenApiDocument, principal: Principal<P>, permissions: Readonly<Record<string, string>>): OpenApiDocument;
60
+ export type OpenApiFetchOptions<P extends string = string> = {
61
61
  /** The document describing this API. Also the catalogue served to callers. */
62
62
  spec: OpenApiDocument;
63
63
  /** `operationId` to the permission it costs. Every operation needs an entry. */
@@ -107,6 +107,5 @@ type OpenApiFetchOptions<P extends string = string> = {
107
107
  * out of the spec (a health check, static files) belong outside this wrapper
108
108
  * rather than behind it.
109
109
  */
110
- declare function createOpenApiFetch<P extends string = string>(options: OpenApiFetchOptions<P>): (request: Request) => Promise<Response>;
111
- //#endregion
112
- export { OpenApiDocument, OpenApiFetchOptions, OpenApiOperation, OperationRecord, createOpenApiFetch, filterSpec, recordOperations };
110
+ export declare function createOpenApiFetch<P extends string = string>(options: OpenApiFetchOptions<P>): (request: Request) => Promise<Response>;
111
+ //#endregion
@@ -3,15 +3,15 @@
3
3
  * Rendering a permission map as source, shared by everything that records a
4
4
  * catalogue.
5
5
  *
6
- * Its own module because the MCP recorder needs an optional peer dependency to
7
- * talk to a server, and the OpenAPI one only needs a file it was handed. A
8
- * caller after the scaffold should not have to install a client to get it.
6
+ * Its own module because the MCP recorder loads a client to talk to a server,
7
+ * and the OpenAPI one only needs a file it was handed. A caller after the
8
+ * scaffold should not have to load a client to get it.
9
9
  */
10
10
  type PermissionMapRecord = {
11
11
  /** Every capability, sorted, labelled the way the gate labels it. */
12
12
  names: string[];
13
- /** A digest per capability, when the source can produce one. */
14
- fingerprints?: Record<string, string>;
13
+ /** What each capability says to the model, when the source can record it. */
14
+ definitions?: Record<string, unknown>;
15
15
  /** `resource:` labels to the URI or template each answers on. */
16
16
  resourceUris?: Record<string, string>;
17
17
  };
package/dist/proxy.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { _ as Identity, s as Policy } from "./policy-DuZbwrKf.js";
2
2
  import { a as AuthorizationDecisionSink, t as VerifierOptions } from "./verifier-DF6gUMQ6.js";
3
3
  import { t as CapabilityScopeMap } from "./ladder-D18eJ7tD.js";
4
+ import { t as Definition } from "./definitions-CZVk-j1C.js";
4
5
  import { AuthInfo, OAuthMetadata, OAuthTokenVerifier } from "@modelcontextprotocol/server";
5
6
  //#region src/upstream.d.ts
6
7
  type UpstreamConfig = {
@@ -11,7 +12,7 @@ type UpstreamConfig = {
11
12
  };
12
13
  //#endregion
13
14
  //#region src/proxy.d.ts
14
- type McpProxyOptions<P extends string = string> = {
15
+ export type McpProxyOptions<P extends string = string> = {
15
16
  /** This proxy's public URL, e.g. `https://mcp.acme.com/mcp`. */
16
17
  resourceServerUrl: URL;
17
18
  /** RFC 8414 metadata for the authorization server in front of the proxy. */
@@ -23,6 +24,18 @@ type McpProxyOptions<P extends string = string> = {
23
24
  upstream: UpstreamConfig;
24
25
  /** Flat permission map — same labels as `gate()` and `recordCapabilities`. */
25
26
  permissions: Readonly<Record<string, P>> | ReadonlyMap<string, P>;
27
+ /**
28
+ * What each priced capability said to the model when it was recorded: the
29
+ * `DEFINITIONS` that `mcp-authz record` writes beside `PERMISSIONS`.
30
+ *
31
+ * A permission prices a name, and the upstream decides what stands behind
32
+ * it. Without this, an upstream could keep an approved tool's name and
33
+ * rewrite its description to steer the model, or add an argument to carry
34
+ * data out, and every caller would be served the new one. With it, a
35
+ * capability whose definition has changed is left out of listings and its
36
+ * invocations are refused until someone re-records and approves the change.
37
+ */
38
+ definitions: Readonly<Record<string, Definition>>;
26
39
  /**
27
40
  * `resource:<label>` to the URI or URI template it answers on.
28
41
  *
@@ -41,6 +54,5 @@ type McpProxyOptions<P extends string = string> = {
41
54
  healthPath?: string;
42
55
  maxRequestBytes?: number;
43
56
  };
44
- declare function createMcpProxy<P extends string = string>(options: McpProxyOptions<P>): (request: Request) => Promise<Response>;
45
- //#endregion
46
- export { McpProxyOptions, createMcpProxy };
57
+ export declare function createMcpProxy<P extends string = string>(options: McpProxyOptions<P>): (request: Request) => Promise<Response>;
58
+ //#endregion