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.
- package/README.md +98 -16
- package/dist/cli.d.ts +2 -3
- package/dist/cli.js +1060 -16
- package/dist/definitions-CZVk-j1C.d.ts +5 -0
- package/dist/definitions-CyIy4YSZ.js +132 -0
- package/dist/index.d.ts +12 -8
- package/dist/index.js +16 -7
- package/dist/{ladder-CUzOKudC.js → ladder-6TnD3hTJ.js} +3 -1
- package/dist/node.d.ts +3 -4
- package/dist/openapi.d.ts +10 -11
- package/dist/{permissions-module-DxCHuE-N.d.ts → permissions-module-TOpt20D4.d.ts} +5 -5
- package/dist/proxy.d.ts +16 -4
- package/dist/proxy.js +287 -34
- package/dist/screen-DxoujEpO.js +79 -0
- package/dist/strict-json-DLKOgsGE.js +100 -0
- package/dist/testing.d.ts +12 -7
- package/dist/testing.js +54 -62
- package/dist/{tools-BQE1O-7P.d.ts → tools-C1dZESYK.d.ts} +9 -2
- package/package.json +10 -12
|
@@ -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-
|
|
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
|
-
|
|
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,
|
|
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-
|
|
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
|
-
|
|
315
|
-
permissionForRoute: (kind, name) =>
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
return
|
|
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) =>
|
|
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 =
|
|
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-
|
|
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-
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
/**
|
|
14
|
-
|
|
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
|