mcp-authz 0.1.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,386 @@
1
+ import { a as Policy, c as Rule, d as definePolicy, f as reconcile, h as Identity, i as PermissionOf, l as createPrincipal, m as DenialReason, n as Match, o as PolicySpec, p as AccessDeniedError, r as PermissionCatalog, s as Principal, u as definePermissions } from "./policy-CnQj53Hq.js";
2
+ import { AuthInfo, CallToolResult, GetPromptResult, Icon, Implementation, InboundClassificationOutcome, McpServer, OAuthMetadata, OAuthTokenVerifier, ReadResourceResult, ResourceMetadata, ResourceTemplate, ServerOptions as ServerOptions$1, StandardSchemaV1, StandardSchemaWithJSON, ToolAnnotations } from "@modelcontextprotocol/server";
3
+ //#region src/discovery.d.ts
4
+ type DiscoverOptions = {
5
+ /** Swap in for tests, or to add a timeout or proxy. Defaults to global fetch. */
6
+ fetch?: typeof globalThis.fetch;
7
+ };
8
+ declare function discoverOAuth(issuer: string, options?: DiscoverOptions): Promise<OAuthMetadata>;
9
+ //#endregion
10
+ //#region src/tools.d.ts
11
+ /**
12
+ * Tools, prompts and resources that carry the permission they require.
13
+ *
14
+ * Declaring it here rather than in a separate rules file is the whole point: it
15
+ * is checked against the policy by the compiler, reconciled against the policy
16
+ * at boot, and it cannot drift when somebody renames the tool.
17
+ *
18
+ * MCP exposes three things a caller can reach, so gating only tools leaves two
19
+ * doors open. All three register the same way: check the permission, skip the
20
+ * registration when the caller lacks it, audit what they do reach.
21
+ */
22
+ /** Which of the three a caller reached. */
23
+ type Capability = 'tool' | 'prompt' | 'resource';
24
+ /** What actually happened, for the audit log the downstream API cannot write. */
25
+ type AuditEvent = {
26
+ issuer: string;
27
+ sub: string;
28
+ email?: string;
29
+ kind: Capability;
30
+ /** The registered name, e.g. `update_case`. */
31
+ name: string;
32
+ permission: string;
33
+ /** Whatever the definition's own `audit` said the call touched, e.g. `case:C1234`. */
34
+ resource?: string;
35
+ decision: 'allow';
36
+ phase: 'attempt' | 'success' | 'failure';
37
+ at: string;
38
+ durationMs?: number;
39
+ error?: string;
40
+ };
41
+ type AuditSink = (event: AuditEvent) => unknown | Promise<unknown>;
42
+ type InferArgs<S> = S extends StandardSchemaV1<unknown, infer Output> ? Output : undefined;
43
+ /** Shared by all three: what it costs, and what the call touched. */
44
+ type Gated<P extends string, A> = {
45
+ /** Required to reach it. Typed to the policy's permissions, so a typo is a build error. */
46
+ permission: P;
47
+ /**
48
+ * Name the thing the call touched, for the audit event. The library cannot
49
+ * know that `{ caseId: 'C1234' }` means a case; the application always does.
50
+ */
51
+ audit?: (args: A) => string | undefined;
52
+ };
53
+ type ToolConfig<P extends string, S> = Gated<P, InferArgs<S>> & {
54
+ title?: string;
55
+ description?: string;
56
+ inputSchema?: S;
57
+ outputSchema?: StandardSchemaWithJSON;
58
+ annotations?: ToolAnnotations;
59
+ icons?: Icon[];
60
+ };
61
+ type PromptConfig<P extends string, S> = Gated<P, InferArgs<S>> & {
62
+ title?: string;
63
+ description?: string;
64
+ argsSchema?: S;
65
+ icons?: Icon[];
66
+ };
67
+ type ResourceConfig<P extends string> = Gated<P, URL> & ResourceMetadata & {
68
+ /** The URI clients read, or a template for a family of them. */
69
+ uri: string | ResourceTemplate;
70
+ };
71
+ /**
72
+ * Erased once built, so a server registers every capability the same way
73
+ * whatever it accepts.
74
+ */
75
+ type Definition<P extends string, C = Principal<P>> = {
76
+ /**
77
+ * Unique key for the boot-time check. Bare for a tool, prefixed otherwise, so
78
+ * a prompt sharing a tool's name stays a separate entry rather than shadowing
79
+ * it.
80
+ */
81
+ label: string;
82
+ kind: Capability;
83
+ /** Exact protocol name used to refuse an unpermitted direct invocation before scope step-up. */
84
+ routeName?: string;
85
+ routeMatches?: (name: string) => boolean;
86
+ permission: P;
87
+ register: (server: McpServer, principal: Principal<P>, context: C, onAudit?: AuditSink) => void;
88
+ };
89
+ type ServerOptions = Implementation & {
90
+ /** Called on every permitted invocation. The one record tying a person to an action. */
91
+ onAudit?: AuditSink;
92
+ /** Pass-through SDK options; declared gated capabilities are merged in. */
93
+ mcp?: ServerOptions$1;
94
+ };
95
+ /**
96
+ * A per-request server factory over a fixed set of capabilities, plus the
97
+ * label-to-permission map that `createMcpFetch` reconciles against the policy
98
+ * at boot.
99
+ */
100
+ type ServerFactory<C> = ((context: C) => McpServer) & {
101
+ permissions: ReadonlyMap<string, string>;
102
+ routePermissions: ReadonlyMap<string, string>;
103
+ permissionForRoute: (kind: Capability, name: string) => string | undefined;
104
+ };
105
+ /**
106
+ * Everything bound to one policy: `permission` accepts only what that policy can
107
+ * grant, in all four places, from one call.
108
+ *
109
+ * The policy is a type carrier here and is never invoked. Authorization happens
110
+ * once per request when the principal is resolved, not once per definition.
111
+ *
112
+ * ```ts
113
+ * const { tool, prompt, resource, server } = authz(policy);
114
+ * ```
115
+ */
116
+ declare function bindAuthz<P extends string, C, H>(_permissions: Policy<P> | PermissionCatalog<P>, principalOf: (context: C) => Principal<P>, handlerContext: (context: C, principal: Principal<P>) => H): {
117
+ tool: <S extends StandardSchemaWithJSON | undefined = undefined>(name: string, config: ToolConfig<P, S>, handler: (args: InferArgs<S>, context: H) => CallToolResult | Promise<CallToolResult>) => Definition<P, C>;
118
+ prompt: <S extends StandardSchemaWithJSON | undefined = undefined>(name: string, config: PromptConfig<P, S>, handler: (args: InferArgs<S>, context: H) => GetPromptResult | Promise<GetPromptResult>) => Definition<P, C>;
119
+ resource: (name: string, config: ResourceConfig<P>, handler: (uri: URL, context: H) => ReadResourceResult | Promise<ReadResourceResult>) => Definition<P, C>;
120
+ server: (definitions: readonly Definition<P, C>[], options: ServerOptions) => ServerFactory<C>;
121
+ };
122
+ declare function authz<P extends string>(policy: Policy<P> | PermissionCatalog<P>): ReturnType<typeof bindAuthz<P, Principal<P>, {
123
+ principal: Principal<P>;
124
+ }>>;
125
+ declare function authz<P extends string, C>(policy: Policy<P> | PermissionCatalog<P>, options: {
126
+ principal: (context: C) => Principal<P>;
127
+ }): ReturnType<typeof bindAuthz<P, C, C>>;
128
+ //#endregion
129
+ //#region src/gate.d.ts
130
+ /**
131
+ * Gate a server somebody else builds.
132
+ *
133
+ * `authz(policy)` is for tools you write. This is for the ones you already
134
+ * have: an MCP server from another package, or one you are not ready to change,
135
+ * whose tools were never declared with a permission.
136
+ *
137
+ * It cannot read a built server's tool list — the SDK keeps that private, and
138
+ * no library can filter what it never saw. So this wraps the server *before*
139
+ * registration and gates each call as it happens, which needs one hook from
140
+ * whoever builds it:
141
+ *
142
+ * ```ts
143
+ * // in their builder
144
+ * const server = options.wrap ? options.wrap(new McpServer(info, opts)) : new McpServer(info, opts);
145
+ *
146
+ * // in your connector
147
+ * buildServer(config, { wrap: (server) => gate(server, principal, PERMISSIONS) })
148
+ * ```
149
+ *
150
+ * Unpermitted registrations are registered and immediately disabled rather than
151
+ * skipped, so the caller still gets the handle its own code expects, and the
152
+ * SDK does the list-filtering and the refusal.
153
+ */
154
+ type GateOptions = {
155
+ /** Same event as `authz`, for tools this package did not define. */
156
+ onAudit?: AuditSink;
157
+ /**
158
+ * Name the thing a call touched, keyed by the same label as `permissions`.
159
+ * Their tool, your domain knowledge.
160
+ */
161
+ audit?: Readonly<Record<string, (args: never) => string | undefined>>;
162
+ };
163
+ /** Bare name for a tool, `prompt:`/`resource:` prefixed for the rest. */
164
+ type PermissionMap<P extends string> = Readonly<Record<string, P>> | ReadonlyMap<string, P>;
165
+ declare function gate<P extends string>(server: McpServer, principal: Principal<P>, permissions: PermissionMap<P>, options?: GateOptions): McpServer;
166
+ //#endregion
167
+ //#region src/routing.d.ts
168
+ type TrustedMcpRoute = {
169
+ kind: 'modern';
170
+ body: unknown;
171
+ outcome: Extract<InboundClassificationOutcome, {
172
+ kind: 'modern';
173
+ }>;
174
+ method: string;
175
+ name?: string;
176
+ };
177
+ //#endregion
178
+ //#region src/scopes.d.ts
179
+ /**
180
+ * Helpers for deriving required OAuth scopes from Streamable HTTP headers
181
+ * (SEP-2243). Use with `createMcpFetch({ scopesForRequest })`.
182
+ */
183
+ type ScopeRequirement = string | readonly string[];
184
+ type ToolScopeMap = Readonly<Record<string, ScopeRequirement>>;
185
+ type CapabilityScopeMap = ToolScopeMap;
186
+ /**
187
+ * Tools use their bare name (or `tool:name`); prompts use `prompt:name`; and
188
+ * resources use `resource:<uri>`. Everything else needs only the baseline.
189
+ */
190
+ declare function scopesFromMcpHeaders(request: Request, toolScopes: ToolScopeMap, baseline?: string): string[];
191
+ /** Select scopes from an already validated MCP method/name pair. */
192
+ declare function scopesForCapability(method: string | undefined, name: string | undefined, capabilityScopes: CapabilityScopeMap, baseline?: string): string[];
193
+ /** Decode SEP-2243's optional Base64 sentinel without accepting non-canonical input. */
194
+ declare function decodeMcpNameHeader(value: string): string | undefined;
195
+ //#endregion
196
+ //#region src/verifier.d.ts
197
+ /**
198
+ * Token verification, the one thing the SDK deliberately leaves to you.
199
+ *
200
+ * `authInfo` is strictly pass-through in the SDK: it is never derived from
201
+ * request headers, and no token is checked unless we check it. So this file is
202
+ * the security boundary of the whole server.
203
+ *
204
+ * The authorization server is something that already exists (WorkOS, Stytch,
205
+ * Auth0) doing dynamic client registration / CIMD and Google Workspace login.
206
+ * Google cannot play that role itself: it has no DCR/CIMD, and it will not
207
+ * mint a token whose audience is this server.
208
+ */
209
+ type VerifierOptions = {
210
+ /** The AS issuer, e.g. `https://auth.acme.com`. Must match the token's `iss`. */
211
+ issuer: string;
212
+ /** Where the AS publishes its signing keys. */
213
+ jwksUri: string;
214
+ /**
215
+ * This server's public URL. A token minted for a different resource is
216
+ * refused even when its signature is valid (RFC 8707).
217
+ */
218
+ resource: URL;
219
+ /**
220
+ * Restrict to one Google Workspace domain, checked against the `hd` claim
221
+ * the AS passes through. Omit to accept any domain the AS admits.
222
+ */
223
+ allowedDomain?: string;
224
+ /** Claim carrying the verified email. Auth0 and WorkOS both use `email`. */
225
+ emailClaim?: string;
226
+ /** Claim proving the email was verified. Defaults to `email_verified`. */
227
+ emailVerifiedClaim?: string;
228
+ /** Require an explicit `true` verified-email claim. Defaults to true. */
229
+ requireEmailVerified?: boolean;
230
+ };
231
+ /**
232
+ * A JWKS-backed verifier. Keys are fetched once and cached by `jose`, which
233
+ * also handles rotation, so a key roll at the AS does not need a redeploy.
234
+ */
235
+ declare function jwksVerifier(options: VerifierOptions): OAuthTokenVerifier & {
236
+ identityOf: (auth: AuthInfo) => Identity;
237
+ };
238
+ /** Default identity mapper for custom verifiers using `AuthInfo.extra`. */
239
+ declare function identityFromAuth(auth: AuthInfo): Identity;
240
+ //#endregion
241
+ //#region src/handler.d.ts
242
+ type AuthorizationDecisionEvent = {
243
+ issuer: string;
244
+ sub: string;
245
+ email?: string;
246
+ decision: 'allow' | 'deny';
247
+ roles: readonly string[];
248
+ permissions: readonly string[];
249
+ reason?: string;
250
+ at: string;
251
+ };
252
+ type AuthorizationDecisionSink = (event: AuthorizationDecisionEvent) => unknown | Promise<unknown>;
253
+ /**
254
+ * An MCP Streamable HTTP resource server (2026-07-28) with
255
+ * each person's own Google login in front of a per-request server factory.
256
+ *
257
+ * Three parties, and it matters which does what:
258
+ *
259
+ * Claude runs the OAuth flow and presents a bearer token
260
+ * your AS registers Claude (CIMD preferred; DCR deprecated), logs
261
+ * the human in with Google, and mints a token bound to
262
+ * THIS server's URL
263
+ * this file verifies that token, maps the identity to context, and
264
+ * builds a server for that request
265
+ *
266
+ * We are a resource server and nothing more.
267
+ */
268
+ type McpFetchOptions<TContext, P extends string = string> = {
269
+ /** This server's public URL, e.g. `https://mcp.acme.com/mcp`. */
270
+ resourceServerUrl: URL;
271
+ /** RFC 8414 metadata for the authorization server in front of us. */
272
+ oauthMetadata: OAuthMetadata;
273
+ /**
274
+ * Token verification. The SDK verifies no tokens of its own, so this is the
275
+ * security boundary, but every field has a sound default.
276
+ *
277
+ * `issuer` and `resource` come from `oauthMetadata.issuer` and
278
+ * `resourceServerUrl`, which is what they have to be: a verifier trusting a
279
+ * different issuer, or checking the audience against a URL this server does
280
+ * not answer on, refuses every valid token and says only "invalid token".
281
+ * `jwksUri` comes from `oauthMetadata.jwks_uri` when discovery supplied it,
282
+ * so this whole block is optional.
283
+ */
284
+ verifier?: Partial<VerifierOptions>;
285
+ /**
286
+ * Bring any SDK-compatible verifier, including RFC 7662 introspection for
287
+ * opaque tokens. Mutually exclusive with the built-in `verifier` options.
288
+ */
289
+ tokenVerifier?: OAuthTokenVerifier;
290
+ /** Map a custom verifier's AuthInfo into the identity consumed by policy. */
291
+ identityFromAuth?: (auth: AuthInfo) => Identity;
292
+ /**
293
+ * Baseline scopes every request must carry. Keep it to one small scope
294
+ * (`mcp` by default); use `scopesForRequest` for per-tool step-up.
295
+ */
296
+ requiredScopes?: string[];
297
+ /** All scopes advertised in protected-resource metadata. */
298
+ supportedScopes?: string[];
299
+ /** Declarative per-tool step-up. Mutually exclusive with `scopesForRequest`. */
300
+ toolScopes?: ToolScopeMap;
301
+ /** Tools, prompts and resource URIs mapped to scope requirements. */
302
+ capabilityScopes?: CapabilityScopeMap;
303
+ /**
304
+ * Map a request to the scopes it needs. The second argument is the trusted,
305
+ * body-validated and Base64-decoded MCP route; use it instead of reading raw
306
+ * `Mcp-Method` / `Mcp-Name` values yourself. Returning more than the token
307
+ * holds produces HTTP 403 with actionable `insufficient_scope` step-up.
308
+ *
309
+ * Omit to demand only `requiredScopes` on every call.
310
+ */
311
+ scopesForRequest?: (request: Request, route: TrustedMcpRoute) => string[];
312
+ /**
313
+ * Who may connect and what they may do. Supply this and the per-request
314
+ * context is the `Principal`; a caller matching no rule is refused.
315
+ *
316
+ * Paired with a `toolServer`, the tools' permissions are reconciled against
317
+ * the policy at startup, so a tool no role can reach fails the boot rather
318
+ * than sitting there looking live.
319
+ */
320
+ policy?: Policy<P>;
321
+ /**
322
+ * Async alternative to `policy`, for roles and permissions stored in an IdP,
323
+ * database or policy service. Return a principal with `createPrincipal`.
324
+ */
325
+ authorize?: (identity: Identity) => Promise<Principal<P>> | Principal<P>;
326
+ /** Awaited access-decision sink. A rejection fails closed. */
327
+ onDecision?: AuthorizationDecisionSink;
328
+ /**
329
+ * After auth: map the verified identity to backend context, or throw
330
+ * `AccessDeniedError` for an actionable 403. Omit to use `policy` alone.
331
+ *
332
+ * Supply both when the context needs more than the principal — a tenant, a
333
+ * connection, a downstream credential. The policy still runs first and still
334
+ * refuses anyone it grants nothing, so this enriches the context rather than
335
+ * taking over the decision, and the boot-time reconciliation stays honest.
336
+ */
337
+ resolve?: (identity: Identity, principal: Principal<P> | undefined) => Promise<TContext> | TContext;
338
+ /** Per-request MCP server factory. `toolServer` builds one from a tool set. */
339
+ createServer: ((context: TContext) => McpServer) & {
340
+ /** Tool name to required permission, when the factory knows its own tools. */
341
+ permissions?: ReadonlyMap<string, string>;
342
+ /** Protocol route to permission, used to distinguish policy denial from scope step-up. */
343
+ routePermissions?: ReadonlyMap<string, string>;
344
+ /** Resolve exact and templated protocol routes to their declared permission. */
345
+ permissionForRoute?: (kind: 'tool' | 'prompt' | 'resource', name: string) => string | undefined;
346
+ };
347
+ /**
348
+ * The same map, when your own factory wraps a `toolServer` and hides it —
349
+ * which a richer `TContext` than the principal forces you to do. Pass
350
+ * `toolServer(...).permissions` here and the boot-time check still runs.
351
+ */
352
+ permissions?: ReadonlyMap<string, string>;
353
+ /** Key under `authInfo.extra` for the resolved context. */
354
+ contextExtraKey?: string;
355
+ /** Health check path. Defaults to `/health`. */
356
+ healthPath?: string;
357
+ /** Reject 2025-era requests by default; opt in only when legacy clients are required. */
358
+ legacy?: 'reject' | 'stateless';
359
+ /**
360
+ * Cap on a body read before authentication, which only the declarative scope
361
+ * path does. Defaults to 1 MiB; over it is a 413.
362
+ */
363
+ maxRequestBytes?: number;
364
+ };
365
+ /**
366
+ * With a `policy`, the principal reaching `resolve` is never undefined: the
367
+ * policy ran first and already refused anyone it grants nothing. Saying so here
368
+ * saves every caller the same non-null assertion.
369
+ */
370
+ declare function createMcpFetch<TContext = Principal<string>, P extends string = string>(options: Omit<McpFetchOptions<TContext, P>, 'policy' | 'authorize' | 'resolve'> & {
371
+ policy: Policy<P>;
372
+ authorize?: never;
373
+ resolve?: (identity: Identity, principal: Principal<P>) => Promise<TContext> | TContext;
374
+ }): (request: Request) => Promise<Response>;
375
+ declare function createMcpFetch<TContext = Principal<string>, P extends string = string>(options: Omit<McpFetchOptions<TContext, P>, 'policy' | 'authorize' | 'resolve'> & {
376
+ policy?: never;
377
+ authorize: (identity: Identity) => Promise<Principal<P>> | Principal<P>;
378
+ resolve?: (identity: Identity, principal: Principal<P>) => Promise<TContext> | TContext;
379
+ }): (request: Request) => Promise<Response>;
380
+ declare function createMcpFetch<TContext = Principal<string>, P extends string = string>(options: Omit<McpFetchOptions<TContext, P>, 'policy' | 'authorize' | 'resolve'> & {
381
+ policy?: never;
382
+ authorize?: never;
383
+ resolve: (identity: Identity, principal: undefined) => Promise<TContext> | TContext;
384
+ }): (request: Request) => Promise<Response>;
385
+ //#endregion
386
+ export { AccessDeniedError, type AuditEvent, type AuditSink, type AuthorizationDecisionEvent, type AuthorizationDecisionSink, type Capability, type CapabilityScopeMap, type Definition, type DenialReason, type DiscoverOptions, type GateOptions, type Identity, type Match, 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 };