@webpieces/mcp-server 0.4.839 → 0.4.841
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 +30 -6
- package/package.json +6 -6
- package/src/McpApiBinding.d.ts +2 -2
- package/src/McpApiBinding.js +7 -3
- package/src/McpApiBinding.js.map +1 -1
- package/src/McpApiDispatcher.d.ts +3 -7
- package/src/McpApiDispatcher.js +10 -39
- package/src/McpApiDispatcher.js.map +1 -1
- package/src/McpAuth.d.ts +9 -12
- package/src/McpAuth.js +10 -10
- package/src/McpAuth.js.map +1 -1
- package/src/McpAuthorityCompileAssertions.d.ts +4 -0
- package/src/McpAuthorityCompileAssertions.js +11 -2
- package/src/McpAuthorityCompileAssertions.js.map +1 -1
- package/src/McpToolPolicy.d.ts +14 -0
- package/src/McpToolPolicy.js +39 -0
- package/src/McpToolPolicy.js.map +1 -0
- package/src/McpToolRegistry.d.ts +8 -3
- package/src/McpToolRegistry.js +27 -8
- package/src/McpToolRegistry.js.map +1 -1
- package/src/WpMcpErrorTranslator.d.ts +1 -1
- package/src/WpMcpErrorTranslator.js +1 -1
- package/src/WpMcpErrorTranslator.js.map +1 -1
- package/src/WpMcpServer.d.ts +9 -8
- package/src/WpMcpServer.js +58 -57
- package/src/WpMcpServer.js.map +1 -1
- package/src/__tests__/WpMcpServerTestFixtures.js +2 -2
- package/src/__tests__/WpMcpServerTestFixtures.js.map +1 -1
package/README.md
CHANGED
|
@@ -177,8 +177,8 @@ one request and causes the official handler to select request-scoped SSE; closin
|
|
|
177
177
|
aborts the same signal. The context key has no HTTP header and is never propagated as caller input.
|
|
178
178
|
|
|
179
179
|
For a local binding the API method must combine `@WpMcpAuthJwt` with `@WpAuthJwt`. Before every tool
|
|
180
|
-
call the bridge asks the application `JwtHook` to mint a distinct short-lived endpoint JWT and invokes
|
|
181
|
-
`ApiFactory.
|
|
180
|
+
authorized, schema-valid call the bridge asks the application `JwtHook` to mint a distinct short-lived endpoint JWT and invokes
|
|
181
|
+
`ApiFactory.createInvocationApiClient`, preserving `JwtHook`, `LogApiFilter`, and `AuthFilter`. Literal MCP-token
|
|
182
182
|
passthrough is rejected. For a remote binding the endpoint must use `@WpAuthOidc`; the generated Node
|
|
183
183
|
client supplies its service credential and propagates only trusted delegated context established by
|
|
184
184
|
the MCP access-token authority. Neither an external MCP bearer nor a browser session JWT is sent to
|
|
@@ -197,24 +197,48 @@ rejected token must be thrown as `ApiUnauthorizedError` (answered 401 + `WWW-Aut
|
|
|
197
197
|
other throw from the authority is an implementation failure (500). Endpoint JWTs are capped at one
|
|
198
198
|
hour and MCP access tokens at 30 days.
|
|
199
199
|
|
|
200
|
+
## Principal, policy and request context
|
|
201
|
+
|
|
202
|
+
`VerifiedMcpCredential.caller` is the application's `AuthenticatedCaller` resolved from current account
|
|
203
|
+
facts. `principalRoles` is derived from that caller, a public hard cut from the old listing-role field.
|
|
204
|
+
Use the same application mapping for this caller and the endpoint JWT mint payload. Security metadata
|
|
205
|
+
(issuer, exact resource, scopes, issuance/expiry and account validation time) remains on the credential.
|
|
206
|
+
|
|
207
|
+
The framework publishes the caller's trusted entries and `llm` surface immediately after verification,
|
|
208
|
+
then reconciles inbound trusted headers using the same operation as ordinary HTTP. Contradictory or
|
|
209
|
+
unvouched identity/surface headers are rejected. `JwtHook.authorizeJwt` governs the request's authorized
|
|
210
|
+
tool view before SDK processing; only explicit `ApiForbiddenError` denials hide tools. Policy/datastore
|
|
211
|
+
failures propagate to the safe ingress boundary. Documentation, synchronous SDK schema lookup and call
|
|
212
|
+
name resolution all use that view. The dispatcher independently checks the same policy before input
|
|
213
|
+
validation and bridge issuance. A hidden tool is indistinguishable from an unknown name and triggers no
|
|
214
|
+
schema validation, JWT issuance or binding invocation.
|
|
215
|
+
|
|
216
|
+
Each HTTP POST has one transport-owned `RequestContext.run`, including malformed-body failures. Local
|
|
217
|
+
invocation opens no detached/nested scope and retains the original transport request, request/action IDs,
|
|
218
|
+
custom context and downstream response-header accumulation. Its immutable `InvocationAuthentication`
|
|
219
|
+
is carried on that invocation's proxy/MethodMeta, never on a mutable ambient credential slot. Normal
|
|
220
|
+
endpoint JWT parsing and policy still run, and parsed user/roles/trusted entries must match the ingress
|
|
221
|
+
principal. GUI proxies continue reading the transport bearer. Remote bindings still use explicit OIDC
|
|
222
|
+
clients and propagate the verified principal's trusted entries; another HTTP process has its own scope.
|
|
223
|
+
|
|
200
224
|
## Error boundary
|
|
201
225
|
|
|
202
226
|
`WpMcpErrorTranslator` is the one place a failure becomes an MCP reply, mirroring
|
|
203
227
|
`WebpiecesDefaultErrorTranslator`: an error in, the exact wire shape out. Each entry point has exactly one catch
|
|
204
|
-
that
|
|
228
|
+
that delegates to it. HTTP ingress uses one translating wrapper and all SDK callbacks use one shared catch implementation with explicit protocol/tool stage; the dispatcher and invokers have none. Classification is
|
|
205
229
|
the shared `ApiErrorBoundary.encode` rule (a non-`ApiError`, or a caller-local `ApiConnectionError`,
|
|
206
230
|
publishes as kind `implementation`). The boundary never substitutes an object for the thrown error,
|
|
207
231
|
so an app's `McpErrorTranslator` can `instanceof` its own error classes.
|
|
208
232
|
|
|
209
233
|
There is ONE method per BOUNDARY, named for the boundary it guards. They cannot be one shape — the
|
|
210
|
-
MCP spec fixes each
|
|
234
|
+
MCP spec fixes each. The shared SDK catch selects protocol versus tool rendering by operation/stage:
|
|
211
235
|
|
|
212
236
|
| Boundary | Method | Reply |
|
|
213
237
|
|---|---|---|
|
|
214
238
|
| before the SDK: bearer, `Origin`, malformed body | `toBearerBoundaryResponse(error): HttpResponseDto<McpHttpErrorBody>` | HTTP 401 + `WWW-Authenticate` / 403 / 400, anything else 500 |
|
|
215
239
|
| `tools/list` | `toListError(error): never` | throws JSON-RPC `-32602` (bad request) or `-32603` `Internal Error` |
|
|
216
|
-
| `tools/call`, tool found: bad arguments,
|
|
217
|
-
| `tools/call`, unknown tool name | `unknownTool(name)` | JSON-RPC `-32602` `Unknown tool: <name>` |
|
|
240
|
+
| `tools/call`, tool found: bad arguments, endpoint policy denial, JWT/OIDC mint failure, any local or remote failure, output-schema or serialization failure | `toToolCallResult(error): CallToolResult` (delegates to `McpRegistry.getErrorTranslator().toWire`) | `isError: true` result |
|
|
241
|
+
| `tools/call`, unknown or hidden tool name (including a dispatcher policy recheck denial) | `unknownTool(name)` | JSON-RPC `-32602` `Unknown tool: <name>` |
|
|
218
242
|
|
|
219
243
|
The pre-SDK boundary produces a VALUE like every other API in the framework and `WpMcpServer` writes
|
|
220
244
|
it through the same `ExpressResponseWriter` the ordinary HTTP path uses — it does not hand-roll
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/mcp-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.841",
|
|
4
4
|
"description": "Secure MCP tool generation from annotated Webpieces API contracts",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -24,15 +24,15 @@
|
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@modelcontextprotocol/node": "2.0.0",
|
|
26
26
|
"@modelcontextprotocol/server": "2.0.0",
|
|
27
|
-
"@webpieces/core-context": "0.4.
|
|
28
|
-
"@webpieces/core-util": "0.4.
|
|
29
|
-
"@webpieces/http-routing": "0.4.
|
|
30
|
-
"@webpieces/http-server": "0.4.
|
|
27
|
+
"@webpieces/core-context": "0.4.841",
|
|
28
|
+
"@webpieces/core-util": "0.4.841",
|
|
29
|
+
"@webpieces/http-routing": "0.4.841",
|
|
30
|
+
"@webpieces/http-server": "0.4.841",
|
|
31
31
|
"express": "5.1.0",
|
|
32
32
|
"reflect-metadata": "0.2.2",
|
|
33
33
|
"tslib": "2.8.1"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@webpieces/http-client-node": "0.4.
|
|
36
|
+
"@webpieces/http-client-node": "0.4.841"
|
|
37
37
|
}
|
|
38
38
|
}
|
package/src/McpApiBinding.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DtoValue } from '@webpieces/core-util';
|
|
2
|
-
import { ApiFactory, ClassType } from '@webpieces/http-routing';
|
|
2
|
+
import { ApiFactory, ClassType, InvocationAuthentication } from '@webpieces/http-routing';
|
|
3
3
|
export type McpBindingTopology = 'local' | 'remote';
|
|
4
4
|
/** Explicit contract-to-invoker binding; MCP never discovers implementations from a router. */
|
|
5
5
|
export declare class McpApiBinding<TApi extends object = object> {
|
|
@@ -9,6 +9,6 @@ export declare class McpApiBinding<TApi extends object = object> {
|
|
|
9
9
|
private constructor();
|
|
10
10
|
static local<TApi extends object>(api: ClassType<TApi>, apiFactory: ApiFactory): McpApiBinding<TApi>;
|
|
11
11
|
static remote<TApi extends object>(api: ClassType<TApi>, provider: () => TApi): McpApiBinding<TApi>;
|
|
12
|
-
invoke(methodName: string, request: DtoValue): Promise<DtoValue>;
|
|
12
|
+
invoke(methodName: string, request: DtoValue, authentication?: InvocationAuthentication): Promise<DtoValue>;
|
|
13
13
|
validateMethod(methodName: string): void;
|
|
14
14
|
}
|
package/src/McpApiBinding.js
CHANGED
|
@@ -14,14 +14,18 @@ class McpApiBinding {
|
|
|
14
14
|
}
|
|
15
15
|
// webpieces-disable no-function-outside-class -- explicit public factory mirrors the binding API
|
|
16
16
|
static local(api, apiFactory) {
|
|
17
|
-
return new McpApiBinding(api, 'local', () =>
|
|
17
|
+
return new McpApiBinding(api, 'local', (authentication) => {
|
|
18
|
+
if (!authentication)
|
|
19
|
+
throw new Error('Local MCP invocation requires explicit authentication.');
|
|
20
|
+
return apiFactory.createInvocationApiClient(api, authentication);
|
|
21
|
+
});
|
|
18
22
|
}
|
|
19
23
|
// webpieces-disable no-function-outside-class -- explicit public factory mirrors the binding API
|
|
20
24
|
static remote(api, provider) {
|
|
21
25
|
return new McpApiBinding(api, 'remote', provider);
|
|
22
26
|
}
|
|
23
|
-
invoke(methodName, request) {
|
|
24
|
-
const client = this.provider();
|
|
27
|
+
invoke(methodName, request, authentication) {
|
|
28
|
+
const client = this.provider(authentication);
|
|
25
29
|
const method = client[methodName];
|
|
26
30
|
if (typeof method !== 'function') {
|
|
27
31
|
throw new Error(`MCP binding provider for ${this.api.name} has no method '${methodName}'.`);
|
package/src/McpApiBinding.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpApiBinding.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiBinding.ts"],"names":[],"mappings":";;;AAAA,oDAA6D;
|
|
1
|
+
{"version":3,"file":"McpApiBinding.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiBinding.ts"],"names":[],"mappings":";;;AAAA,oDAA6D;AAU7D,+FAA+F;AAC/F,MAAa,aAAa;IAEF;IACA;IACC;IAHrB,YACoB,GAAoB,EACpB,QAA4B,EAC3B,QAA6D;QAF9D,QAAG,GAAH,GAAG,CAAiB;QACpB,aAAQ,GAAR,QAAQ,CAAoB;QAC3B,aAAQ,GAAR,QAAQ,CAAqD;IAC/E,CAAC;IAEJ,iGAAiG;IACjG,MAAM,CAAC,KAAK,CACR,GAAoB,EACpB,UAAsB;QAEtB,OAAO,IAAI,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,cAAc,EAAE,EAAE;YACtD,IAAI,CAAC,cAAc;gBACf,MAAM,IAAI,KAAK,CAAC,wDAAwD,CAAC,CAAC;YAC9E,OAAO,UAAU,CAAC,yBAAyB,CAAC,GAAY,EAAE,cAAc,CAAC,CAAC;QAC9E,CAAC,CAAC,CAAC;IACP,CAAC;IAED,iGAAiG;IACjG,MAAM,CAAC,MAAM,CACT,GAAoB,EACpB,QAAoB;QAEpB,OAAO,IAAI,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACtD,CAAC;IAED,MAAM,CACF,UAAkB,EAClB,OAAiB,EACjB,cAAyC;QAEzC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAmB,CAAC;QAC/D,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;QAClC,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,IAAI,KAAK,CACX,4BAA4B,IAAI,CAAC,GAAG,CAAC,IAAI,mBAAmB,UAAU,IAAI,CAC7E,CAAC;QACN,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAsB,CAAC;IAC7D,CAAC;IAED,cAAc,CAAC,UAAkB;QAC7B,MAAM,IAAI,GAAG,IAAA,uBAAW,EAAC,IAAI,CAAC,GAAG,EAAE,UAAU,CAAC,EAAE,IAAI,CAAC;QACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC;QAC5D,IAAI,IAAI,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CACX,OAAO,IAAI,CAAC,QAAQ,YAAY,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,UAAU,YAAY;gBACnE,IAAI,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,iBAAiB;gBACpE,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,cAAc,GAAG,CACrD,CAAC;QACN,CAAC;IACL,CAAC;CACJ;AArDD,sCAqDC","sourcesContent":["import { DtoValue, getAuthMeta } from '@webpieces/core-util';\nimport {\n ApiClientProxy,\n ApiFactory,\n ClassType,\n InvocationAuthentication,\n} from '@webpieces/http-routing';\n\nexport type McpBindingTopology = 'local' | 'remote';\n\n/** Explicit contract-to-invoker binding; MCP never discovers implementations from a router. */\nexport class McpApiBinding<TApi extends object = object> {\n private constructor(\n public readonly api: ClassType<TApi>,\n public readonly topology: McpBindingTopology,\n private readonly provider: (authentication?: InvocationAuthentication) => TApi,\n ) {}\n\n // webpieces-disable no-function-outside-class -- explicit public factory mirrors the binding API\n static local<TApi extends object>(\n api: ClassType<TApi>,\n apiFactory: ApiFactory,\n ): McpApiBinding<TApi> {\n return new McpApiBinding(api, 'local', (authentication) => {\n if (!authentication)\n throw new Error('Local MCP invocation requires explicit authentication.');\n return apiFactory.createInvocationApiClient(api as never, authentication);\n });\n }\n\n // webpieces-disable no-function-outside-class -- explicit public factory mirrors the binding API\n static remote<TApi extends object>(\n api: ClassType<TApi>,\n provider: () => TApi,\n ): McpApiBinding<TApi> {\n return new McpApiBinding(api, 'remote', provider);\n }\n\n invoke(\n methodName: string,\n request: DtoValue,\n authentication?: InvocationAuthentication,\n ): Promise<DtoValue> {\n const client = this.provider(authentication) as ApiClientProxy;\n const method = client[methodName];\n if (typeof method !== 'function') {\n throw new Error(\n `MCP binding provider for ${this.api.name} has no method '${methodName}'.`,\n );\n }\n return method.call(client, request) as Promise<DtoValue>;\n }\n\n validateMethod(methodName: string): void {\n const auth = getAuthMeta(this.api, methodName)?.mode;\n const expected = this.topology === 'local' ? 'jwt' : 'oidc';\n if (auth?.kind !== expected) {\n throw new Error(\n `MCP ${this.topology} binding ${this.api.name}.${methodName} requires ` +\n `@${expected === 'jwt' ? 'WpAuthJwt' : 'WpAuthOidc'}, but declares ` +\n `${auth ? `'${auth.kind}'` : 'no HTTP auth'}.`,\n );\n }\n }\n}\n"]}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { DtoValue } from '@webpieces/core-util';
|
|
2
|
+
import { JwtHook } from '@webpieces/http-routing';
|
|
2
3
|
import { RegisteredMcpTool } from './McpToolRegistry';
|
|
3
4
|
import { VerifiedMcpCredential } from './McpAuth';
|
|
4
5
|
import { McpInvocationContext } from './McpInvocationContext';
|
|
5
|
-
/**
|
|
6
|
-
* Executes one tool call through the explicit local or remote Webpieces client, never a controller
|
|
7
|
-
* reference. It has NO try/catch: every failure (bad arguments, MCP authorization, anything the local
|
|
8
|
-
* proxy or the remote generated client throws) propagates as a typed error to the single tools/call
|
|
9
|
-
* boundary in `WpMcpServer`, so local and remote bindings are translated identically.
|
|
10
|
-
*/
|
|
6
|
+
/** Authorize, validate, then issue a local credential and invoke the ordinary filters. No scope or catch. */
|
|
11
7
|
export declare class McpApiDispatcher {
|
|
12
8
|
private readonly schemas;
|
|
13
|
-
call(tool: RegisteredMcpTool, requestDto: DtoValue, credential: VerifiedMcpCredential, invocation: McpInvocationContext,
|
|
9
|
+
call<T>(tool: RegisteredMcpTool, requestDto: DtoValue, credential: VerifiedMcpCredential, invocation: McpInvocationContext, policy: JwtHook<T>, mintEndpointToken: () => Promise<string>): Promise<DtoValue>;
|
|
14
10
|
}
|
package/src/McpApiDispatcher.js
CHANGED
|
@@ -3,52 +3,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.McpApiDispatcher = void 0;
|
|
4
4
|
const core_util_1 = require("@webpieces/core-util");
|
|
5
5
|
const core_context_1 = require("@webpieces/core-context");
|
|
6
|
+
const http_routing_1 = require("@webpieces/http-routing");
|
|
7
|
+
const McpToolPolicy_1 = require("./McpToolPolicy");
|
|
6
8
|
const McpInvocationContext_1 = require("./McpInvocationContext");
|
|
7
|
-
/**
|
|
8
|
-
* Executes one tool call through the explicit local or remote Webpieces client, never a controller
|
|
9
|
-
* reference. It has NO try/catch: every failure (bad arguments, MCP authorization, anything the local
|
|
10
|
-
* proxy or the remote generated client throws) propagates as a typed error to the single tools/call
|
|
11
|
-
* boundary in `WpMcpServer`, so local and remote bindings are translated identically.
|
|
12
|
-
*/
|
|
9
|
+
/** Authorize, validate, then issue a local credential and invoke the ordinary filters. No scope or catch. */
|
|
13
10
|
class McpApiDispatcher {
|
|
14
11
|
schemas = new core_util_1.ApiJsonSchemaValidator();
|
|
15
|
-
async call(tool, requestDto, credential, invocation,
|
|
12
|
+
async call(tool, requestDto, credential, invocation, policy, mintEndpointToken) {
|
|
13
|
+
await new McpToolPolicy_1.McpToolPolicy(policy).require(credential.caller, tool.mcpAuth.requirement, tool.name);
|
|
16
14
|
const inputFailure = this.schemas.validate(tool.inputSchema, requestDto);
|
|
17
15
|
if (inputFailure) {
|
|
18
16
|
throw new core_util_1.ApiBadRequestError(`MCP arguments for ${tool.name} did not match its published input schema: ${inputFailure.message}`, inputFailure.field, inputFailure.message);
|
|
19
17
|
}
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const parentRequestId = core_context_1.RequestContext.getUntrusted(core_util_1.WebpiecesCoreHeaders.REQUEST_ID);
|
|
26
|
-
return core_context_1.RequestContext.runDetachedScope(async () => {
|
|
27
|
-
if (parentRequestId) {
|
|
28
|
-
core_context_1.RequestContext.putUntrusted(core_util_1.WebpiecesCoreHeaders.REQUEST_ID, parentRequestId);
|
|
29
|
-
}
|
|
30
|
-
const headers = new Map();
|
|
31
|
-
if (tool.binding.topology === 'local') {
|
|
32
|
-
if (!endpointBearerToken) {
|
|
33
|
-
throw new core_util_1.ApiImplementationError(`Local MCP tool ${tool.name} has no endpoint JWT.`);
|
|
34
|
-
}
|
|
35
|
-
headers.set('authorization', [`Bearer ${endpointBearerToken}`]);
|
|
36
|
-
}
|
|
37
|
-
const request = new core_context_1.HttpRequest('POST', `/__webpieces/mcp/${tool.name}`, headers);
|
|
38
|
-
new core_context_1.RequestContextHeaders().fillFromRequest(request);
|
|
39
|
-
core_context_1.RequestContext.putTrusted(McpInvocationContext_1.MCP_INVOCATION_CONTEXT, invocation);
|
|
40
|
-
// THIS hop is the edge, and its caller is a model. Stamped here rather than in
|
|
41
|
-
// `AuthFilter` because the local binding's HTTP auth mode describes the bridge's own
|
|
42
|
-
// endpoint JWT, not the LLM on the other end of it — `@WpMcpAuthJwt` is separate
|
|
43
|
-
// metadata and never reaches an `AuthMode`. `AuthFilter` only ever SETS a surface when
|
|
44
|
-
// none arrived, so this one survives the filter chain the invocation runs through.
|
|
45
|
-
core_context_1.RequestContext.putTrusted(core_util_1.WebpiecesCoreHeaders.SURFACE, 'llm');
|
|
46
|
-
if (tool.binding.topology === 'remote') {
|
|
47
|
-
for (const tuple of credential.trustedContext)
|
|
48
|
-
core_context_1.RequestContext.putTrusted(tuple.key, tuple.value);
|
|
49
|
-
}
|
|
50
|
-
return tool.binding.invoke(tool.methodName, requestDto);
|
|
51
|
-
});
|
|
18
|
+
const authentication = tool.binding.topology === 'local'
|
|
19
|
+
? new http_routing_1.InvocationAuthentication(await mintEndpointToken(), credential.caller)
|
|
20
|
+
: undefined;
|
|
21
|
+
core_context_1.RequestContext.putTrusted(McpInvocationContext_1.MCP_INVOCATION_CONTEXT, invocation);
|
|
22
|
+
return tool.binding.invoke(tool.methodName, requestDto, authentication);
|
|
52
23
|
}
|
|
53
24
|
}
|
|
54
25
|
exports.McpApiDispatcher = McpApiDispatcher;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpApiDispatcher.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiDispatcher.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"McpApiDispatcher.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiDispatcher.ts"],"names":[],"mappings":";;;AAAA,oDAA4F;AAC5F,0DAAyD;AACzD,0DAA4E;AAC5E,mDAAgD;AAGhD,iEAAsF;AAEtF,6GAA6G;AAC7G,MAAa,gBAAgB;IACR,OAAO,GAAG,IAAI,kCAAsB,EAAE,CAAC;IAExD,KAAK,CAAC,IAAI,CACN,IAAuB,EACvB,UAAoB,EACpB,UAAiC,EACjC,UAAgC,EAChC,MAAkB,EAClB,iBAAwC;QAExC,MAAM,IAAI,6BAAa,CAAC,MAAM,CAAC,CAAC,OAAO,CACnC,UAAU,CAAC,MAAM,EACjB,IAAI,CAAC,OAAO,CAAC,WAAW,EACxB,IAAI,CAAC,IAAI,CACZ,CAAC;QACF,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QACzE,IAAI,YAAY,EAAE,CAAC;YACf,MAAM,IAAI,8BAAkB,CACxB,qBAAqB,IAAI,CAAC,IAAI,8CAA8C,YAAY,CAAC,OAAO,EAAE,EAClG,YAAY,CAAC,KAAK,EAClB,YAAY,CAAC,OAAO,CACvB,CAAC;QACN,CAAC;QACD,MAAM,cAAc,GAChB,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO;YAC7B,CAAC,CAAC,IAAI,uCAAwB,CAAC,MAAM,iBAAiB,EAAE,EAAE,UAAU,CAAC,MAAM,CAAC;YAC5E,CAAC,CAAC,SAAS,CAAC;QACpB,6BAAc,CAAC,UAAU,CAAC,6CAAsB,EAAE,UAAU,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;IAC5E,CAAC;CACJ;AA/BD,4CA+BC","sourcesContent":["import { ApiBadRequestError, ApiJsonSchemaValidator, DtoValue } from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\nimport { InvocationAuthentication, JwtHook } from '@webpieces/http-routing';\nimport { McpToolPolicy } from './McpToolPolicy';\nimport { RegisteredMcpTool } from './McpToolRegistry';\nimport { VerifiedMcpCredential } from './McpAuth';\nimport { MCP_INVOCATION_CONTEXT, McpInvocationContext } from './McpInvocationContext';\n\n/** Authorize, validate, then issue a local credential and invoke the ordinary filters. No scope or catch. */\nexport class McpApiDispatcher {\n private readonly schemas = new ApiJsonSchemaValidator();\n\n async call<T>(\n tool: RegisteredMcpTool,\n requestDto: DtoValue,\n credential: VerifiedMcpCredential,\n invocation: McpInvocationContext,\n policy: JwtHook<T>,\n mintEndpointToken: () => Promise<string>,\n ): Promise<DtoValue> {\n await new McpToolPolicy(policy).require(\n credential.caller,\n tool.mcpAuth.requirement,\n tool.name,\n );\n const inputFailure = this.schemas.validate(tool.inputSchema, requestDto);\n if (inputFailure) {\n throw new ApiBadRequestError(\n `MCP arguments for ${tool.name} did not match its published input schema: ${inputFailure.message}`,\n inputFailure.field,\n inputFailure.message,\n );\n }\n const authentication =\n tool.binding.topology === 'local'\n ? new InvocationAuthentication(await mintEndpointToken(), credential.caller)\n : undefined;\n RequestContext.putTrusted(MCP_INVOCATION_CONTEXT, invocation);\n return tool.binding.invoke(tool.methodName, requestDto, authentication);\n }\n}\n"]}
|
package/src/McpAuth.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { JwtHook } from '@webpieces/http-routing';
|
|
2
|
-
import { ContextTuple } from '@webpieces/core-util';
|
|
1
|
+
import { AuthenticatedCaller, JwtHook } from '@webpieces/http-routing';
|
|
3
2
|
export declare const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS: number;
|
|
4
3
|
export declare const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS: number;
|
|
5
4
|
export declare const MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS: number;
|
|
@@ -12,7 +11,7 @@ export declare class MintedMcpAccessToken {
|
|
|
12
11
|
constructor(token: string, resource: string, issuedAtEpochSeconds: number, expiresAtEpochSeconds: number);
|
|
13
12
|
}
|
|
14
13
|
/**
|
|
15
|
-
* Fresh, authoritative security facts resolved at the MCP resource boundary. Implementations must
|
|
14
|
+
* Fresh, authoritative security facts and an explicit application-owned caller mapping resolved at the MCP resource boundary. Implementations must
|
|
16
15
|
* verify issuer, exact resource/audience, expiry, token type/version, key/algorithm policy, and
|
|
17
16
|
* current account state. Opaque access tokens are fully supported.
|
|
18
17
|
*/
|
|
@@ -25,17 +24,15 @@ export declare class VerifiedMcpCredential {
|
|
|
25
24
|
readonly scopes: readonly string[];
|
|
26
25
|
/** When current enabled/revoked state and roles were read from the authoritative source. */
|
|
27
26
|
readonly accountValidatedAtEpochSeconds: number;
|
|
28
|
-
/**
|
|
29
|
-
readonly
|
|
30
|
-
/** Trusted delegated identity derived only by the access-token authority. */
|
|
31
|
-
readonly trustedContext: readonly ContextTuple[];
|
|
27
|
+
/** Application mapping of freshly verified account facts, shared with endpoint JWT issuance. */
|
|
28
|
+
readonly caller: AuthenticatedCaller;
|
|
32
29
|
constructor(subject: string, issuer: string, resource: string, issuedAtEpochSeconds: number, expiresAtEpochSeconds: number, scopes: readonly string[],
|
|
33
30
|
/** When current enabled/revoked state and roles were read from the authoritative source. */
|
|
34
31
|
accountValidatedAtEpochSeconds: number,
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
|
|
32
|
+
/** Application mapping of freshly verified account facts, shared with endpoint JWT issuance. */
|
|
33
|
+
caller: AuthenticatedCaller);
|
|
34
|
+
/** The canonical application's roles; there is no independent listing-role snapshot. */
|
|
35
|
+
get principalRoles(): readonly string[];
|
|
39
36
|
}
|
|
40
37
|
/**
|
|
41
38
|
* Application-owned paired mint/verify authority for resource-bound MCP access tokens.
|
|
@@ -127,7 +124,7 @@ export declare class WpMcpServerConfig<TGrant, TMintRequest> {
|
|
|
127
124
|
setResource(resource: string): this;
|
|
128
125
|
/** REQUIRED. Application-owned paired mint/verify authority for MCP access tokens. */
|
|
129
126
|
setAccessTokenAuthority(authority: McpAccessTokenAuthority<TGrant>): this;
|
|
130
|
-
/** REQUIRED. The
|
|
127
|
+
/** REQUIRED. The application policy for MCP tools and short-lived endpoint JWT issuer. */
|
|
131
128
|
setEndpointJwtAuthority(authority: JwtHook<TMintRequest>): this;
|
|
132
129
|
/** REQUIRED. Maps a verified MCP identity plus tool identity to that JwtHook's mint request. */
|
|
133
130
|
setEndpointMintRequest(factory: McpEndpointMintRequestFactory<TMintRequest>): this;
|
package/src/McpAuth.js
CHANGED
|
@@ -31,7 +31,7 @@ class MintedMcpAccessToken {
|
|
|
31
31
|
}
|
|
32
32
|
exports.MintedMcpAccessToken = MintedMcpAccessToken;
|
|
33
33
|
/**
|
|
34
|
-
* Fresh, authoritative security facts resolved at the MCP resource boundary. Implementations must
|
|
34
|
+
* Fresh, authoritative security facts and an explicit application-owned caller mapping resolved at the MCP resource boundary. Implementations must
|
|
35
35
|
* verify issuer, exact resource/audience, expiry, token type/version, key/algorithm policy, and
|
|
36
36
|
* current account state. Opaque access tokens are fully supported.
|
|
37
37
|
*/
|
|
@@ -43,15 +43,12 @@ class VerifiedMcpCredential {
|
|
|
43
43
|
expiresAtEpochSeconds;
|
|
44
44
|
scopes;
|
|
45
45
|
accountValidatedAtEpochSeconds;
|
|
46
|
-
|
|
47
|
-
trustedContext;
|
|
46
|
+
caller;
|
|
48
47
|
constructor(subject, issuer, resource, issuedAtEpochSeconds, expiresAtEpochSeconds, scopes,
|
|
49
48
|
/** When current enabled/revoked state and roles were read from the authoritative source. */
|
|
50
49
|
accountValidatedAtEpochSeconds,
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
/** Trusted delegated identity derived only by the access-token authority. */
|
|
54
|
-
trustedContext = []) {
|
|
50
|
+
/** Application mapping of freshly verified account facts, shared with endpoint JWT issuance. */
|
|
51
|
+
caller) {
|
|
55
52
|
this.subject = subject;
|
|
56
53
|
this.issuer = issuer;
|
|
57
54
|
this.resource = resource;
|
|
@@ -59,8 +56,11 @@ class VerifiedMcpCredential {
|
|
|
59
56
|
this.expiresAtEpochSeconds = expiresAtEpochSeconds;
|
|
60
57
|
this.scopes = scopes;
|
|
61
58
|
this.accountValidatedAtEpochSeconds = accountValidatedAtEpochSeconds;
|
|
62
|
-
this.
|
|
63
|
-
|
|
59
|
+
this.caller = caller;
|
|
60
|
+
}
|
|
61
|
+
/** The canonical application's roles; there is no independent listing-role snapshot. */
|
|
62
|
+
get principalRoles() {
|
|
63
|
+
return this.caller.roles;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
exports.VerifiedMcpCredential = VerifiedMcpCredential;
|
|
@@ -165,7 +165,7 @@ class WpMcpServerConfig {
|
|
|
165
165
|
this.accessTokenAuthorityValue = this.requirePresent(authority, 'setAccessTokenAuthority');
|
|
166
166
|
return this;
|
|
167
167
|
}
|
|
168
|
-
/** REQUIRED. The
|
|
168
|
+
/** REQUIRED. The application policy for MCP tools and short-lived endpoint JWT issuer. */
|
|
169
169
|
setEndpointJwtAuthority(authority) {
|
|
170
170
|
this.endpointJwtAuthorityValue = this.requirePresent(authority, 'setEndpointJwtAuthority');
|
|
171
171
|
return this;
|
package/src/McpAuth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpAuth.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuth.ts"],"names":[],"mappings":"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","sourcesContent":["import { JwtHook } from '@webpieces/http-routing';\nimport { ContextTuple } from '@webpieces/core-util';\n\nexport const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;\nexport const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = 60 * 60;\nexport const MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS = 60 * 60;\n\n/** Framework-normalized access-token result. The token representation remains application-owned. */\nexport class MintedMcpAccessToken {\n constructor(\n public readonly token: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n ) {\n if (token.trim() === '' || resource.trim() === '') {\n throw new Error('Minted MCP access token and resource must be non-empty.');\n }\n if (!Number.isFinite(issuedAtEpochSeconds) || !Number.isFinite(expiresAtEpochSeconds)) {\n throw new Error(\n 'Minted MCP access token timestamps must be finite epoch-second values.',\n );\n }\n if (expiresAtEpochSeconds <= issuedAtEpochSeconds) {\n throw new Error('MCP access token expiry must be after issuance.');\n }\n if (expiresAtEpochSeconds - issuedAtEpochSeconds > MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS) {\n throw new Error('MCP access token lifetime must not exceed 30 days.');\n }\n }\n}\n\n/**\n * Fresh, authoritative security facts resolved at the MCP resource boundary. Implementations must\n * verify issuer, exact resource/audience, expiry, token type/version, key/algorithm policy, and\n * current account state. Opaque access tokens are fully supported.\n */\nexport class VerifiedMcpCredential {\n constructor(\n public readonly subject: string,\n public readonly issuer: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n public readonly scopes: readonly string[],\n /** When current enabled/revoked state and roles were read from the authoritative source. */\n public readonly accountValidatedAtEpochSeconds: number,\n /** Advisory tools/list filtering only. Endpoint authorization always runs again. */\n public readonly listingRoles: readonly string[] = [],\n /** Trusted delegated identity derived only by the access-token authority. */\n public readonly trustedContext: readonly ContextTuple[] = [],\n ) {}\n}\n\n/**\n * Application-owned paired mint/verify authority for resource-bound MCP access tokens.\n * `verifyAccessToken` must throw `ApiUnauthorizedError` for a token it rejects (the bind boundary\n * answers 401 + `WWW-Authenticate`); any other throw is treated as an implementation failure (500).\n */\nexport interface McpAccessTokenAuthority<TGrant> {\n mintAccessToken(grant: TGrant): Promise<MintedMcpAccessToken>;\n verifyAccessToken(\n accessToken: string,\n expectedResource: string,\n ): Promise<VerifiedMcpCredential>;\n}\n\n/** Minimal tool identity supplied when the app creates its own endpoint-JWT mint request. */\nexport class McpEndpointDescriptor {\n constructor(\n public readonly toolName: string,\n public readonly apiClassName: string,\n public readonly methodName: string,\n ) {}\n}\n\n/** Application mapping from verified MCP identity to its unconstrained JwtHook mint request. */\nexport type McpEndpointMintRequestFactory<TMintRequest> = (\n credential: VerifiedMcpCredential,\n endpoint: McpEndpointDescriptor,\n) => TMintRequest;\n\n/** RFC 9728-style metadata exposed by the MCP protected resource. */\nexport class McpProtectedResourceMetadata {\n readonly resource: string;\n readonly authorization_servers: readonly string[];\n readonly bearer_methods_supported = ['header'];\n readonly scopes_supported?: readonly string[];\n\n constructor(\n resource: string,\n authorizationServers: readonly string[],\n scopesSupported?: readonly string[],\n ) {\n this.resource = resource;\n this.authorization_servers = authorizationServers;\n this.scopes_supported = scopesSupported;\n }\n}\n\n/**\n * Node MCP server configuration, built with fluent setters: every name sits next to its own value, so\n * no two settings can be swapped, and a future setting is an additive setter rather than a breaking\n * signature change. Pass the same concrete application authority to both\n * `setAccessTokenAuthority(...)` and `setEndpointJwtAuthority(...)` when it implements both security\n * seams.\n *\n * SINGLE CANONICAL ORIGIN: one server serves exactly one protected resource. {@link setResource} is\n * fixed for the lifetime of the process, is the audience of every access token, and is never derived\n * from the request `Host` — see that setter for why a host-derived audience is a confused deputy.\n *\n * Each setter validates its own value immediately and names itself in the failure. Setters cannot\n * make a field impossible to forget the way a positional constructor could, so\n * {@link WpMcpServerConfig.validate} runs from `WpMcpServer.bind(...)` and fails at startup listing\n * EVERY missing required setter at once. A misconfiguration is therefore a boot failure naming the\n * setter, instead of a first-request 401 + `WWW-Authenticate` — which is the OAuth discovery signal,\n * so the client's correct response is to re-authenticate and fail again, forever.\n *\n * ```ts\n * const config = new WpMcpServerConfig<MyGrant, MyMintRequest>()\n * .setName('my-server')\n * .setVersion('1.0.0')\n * .setResource('https://api.example.com/mcp')\n * .setAccessTokenAuthority(authority)\n * .setEndpointJwtAuthority(jwtHook)\n * .setEndpointMintRequest((credential) => new MyMintRequest(credential.subject))\n * .setAuthorizationServers(['https://login.example.com'])\n * .setRequiredScopes(['tools']);\n *\n * // the tools/call error translator is a PROCESS-GLOBAL, not a member of this config:\n * McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());\n * ```\n */\nexport class WpMcpServerConfig<TGrant, TMintRequest> {\n private nameValue?: string;\n private versionValue?: string;\n private resourceValue?: string;\n private accessTokenAuthorityValue?: McpAccessTokenAuthority<TGrant>;\n private endpointJwtAuthorityValue?: JwtHook<TMintRequest>;\n private endpointMintRequestValue?: McpEndpointMintRequestFactory<TMintRequest>;\n private authorizationServersValue?: readonly string[];\n private requiredScopesValue?: readonly string[];\n private maxAccountValidationAgeSecondsValue = MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS;\n private maxEndpointJwtLifetimeSecondsValue = MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS;\n\n /** REQUIRED. The MCP server name reported by `initialize`. */\n setName(name: string): this {\n this.nameValue = this.requireText(name, 'setName');\n return this;\n }\n\n /** REQUIRED. The MCP server version; the registry revision is appended to it at bind time. */\n setVersion(version: string): this {\n this.versionValue = this.requireText(version, 'setVersion');\n return this;\n }\n\n /**\n * REQUIRED. The ONE canonical protected-resource URI, which must be an absolute URL: it is the\n * exact audience every access token is checked against, and {@link resourceMetadataUrl} is\n * derived from it for the 401 challenge, so a non-URL here is always a mistake — including a\n * name or version landed on it.\n *\n * It is fixed for the lifetime of the process and must NEVER be derived from the request `Host`\n * header: the boundary compares a token's audience against this value, so a host-derived\n * resource compares two values both taken from caller-controlled input — the confused-deputy\n * shape this check exists to prevent. Its path must equal `McpBindOptions.endpointPath`, which\n * `WpMcpServer.bind(...)` verifies. A second hostname is a second deployment, not a second\n * audience.\n */\n setResource(resource: string): this {\n this.resourceValue = this.requireUrl(resource, 'setResource');\n return this;\n }\n\n /** REQUIRED. Application-owned paired mint/verify authority for MCP access tokens. */\n setAccessTokenAuthority(authority: McpAccessTokenAuthority<TGrant>): this {\n this.accessTokenAuthorityValue = this.requirePresent(authority, 'setAccessTokenAuthority');\n return this;\n }\n\n /** REQUIRED. The `JwtHook` minting the short-lived per-endpoint JWT for local bindings. */\n setEndpointJwtAuthority(authority: JwtHook<TMintRequest>): this {\n this.endpointJwtAuthorityValue = this.requirePresent(authority, 'setEndpointJwtAuthority');\n return this;\n }\n\n /** REQUIRED. Maps a verified MCP identity plus tool identity to that JwtHook's mint request. */\n setEndpointMintRequest(factory: McpEndpointMintRequestFactory<TMintRequest>): this {\n if (typeof factory !== 'function') {\n throw new Error('WpMcpServerConfig.setEndpointMintRequest(...) requires a function.');\n }\n this.endpointMintRequestValue = factory;\n return this;\n }\n\n /**\n * REQUIRED. The trusted token issuers. Every entry must be an absolute URL, because each is\n * compared against a token's `issuer` claim and published as an `authorization_servers` entry —\n * which is what makes a scope list landed here fail at boot rather than on the first request.\n */\n setAuthorizationServers(authorizationServers: readonly string[]): this {\n const servers = this.requireArray(authorizationServers, 'setAuthorizationServers');\n if (servers.length === 0) {\n throw new Error(\n 'WpMcpServerConfig.setAuthorizationServers(...) requires at least one issuer URL.',\n );\n }\n for (const server of servers) this.requireUrl(server, 'setAuthorizationServers');\n this.authorizationServersValue = servers;\n return this;\n }\n\n /**\n * REQUIRED, and empty only by saying so explicitly. Scopes every access token must carry before\n * any tool is dispatched.\n */\n setRequiredScopes(requiredScopes: readonly string[]): this {\n const scopes = this.requireArray(requiredScopes, 'setRequiredScopes');\n for (const scope of scopes) this.requireText(scope, 'setRequiredScopes');\n this.requiredScopesValue = scopes;\n return this;\n }\n\n /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */\n setMaxAccountValidationAgeSeconds(seconds: number): this {\n this.maxAccountValidationAgeSecondsValue = this.requireSeconds(\n seconds,\n MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS,\n 'setMaxAccountValidationAgeSeconds',\n );\n return this;\n }\n\n /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */\n setMaxEndpointJwtLifetimeSeconds(seconds: number): this {\n this.maxEndpointJwtLifetimeSecondsValue = this.requireSeconds(\n seconds,\n MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS,\n 'setMaxEndpointJwtLifetimeSeconds',\n );\n return this;\n }\n\n /**\n * Called by `WpMcpServer.bind(...)`. Throws naming EVERY missing required setter at once, so a\n * half-built config is repaired in one pass rather than one boot failure at a time.\n */\n validate(): void {\n const missing: string[] = [];\n if (this.nameValue === undefined) missing.push('setName(...)');\n if (this.versionValue === undefined) missing.push('setVersion(...)');\n if (this.resourceValue === undefined) missing.push('setResource(...)');\n if (this.accessTokenAuthorityValue === undefined) {\n missing.push('setAccessTokenAuthority(...)');\n }\n if (this.endpointJwtAuthorityValue === undefined) {\n missing.push('setEndpointJwtAuthority(...)');\n }\n if (this.endpointMintRequestValue === undefined) {\n missing.push('setEndpointMintRequest(...)');\n }\n if (this.authorizationServersValue === undefined) {\n missing.push('setAuthorizationServers(...)');\n }\n if (this.requiredScopesValue === undefined) missing.push('setRequiredScopes(...)');\n if (missing.length > 0) {\n throw new Error(`WpMcpServerConfig is missing ${missing.join(', ')}`);\n }\n }\n\n get name(): string {\n return this.read(this.nameValue, 'setName');\n }\n\n get version(): string {\n return this.read(this.versionValue, 'setVersion');\n }\n\n get resource(): string {\n return this.read(this.resourceValue, 'setResource');\n }\n\n get accessTokenAuthority(): McpAccessTokenAuthority<TGrant> {\n return this.read(this.accessTokenAuthorityValue, 'setAccessTokenAuthority');\n }\n\n get endpointJwtAuthority(): JwtHook<TMintRequest> {\n return this.read(this.endpointJwtAuthorityValue, 'setEndpointJwtAuthority');\n }\n\n get endpointMintRequest(): McpEndpointMintRequestFactory<TMintRequest> {\n return this.read(this.endpointMintRequestValue, 'setEndpointMintRequest');\n }\n\n get authorizationServers(): readonly string[] {\n return this.read(this.authorizationServersValue, 'setAuthorizationServers');\n }\n\n get requiredScopes(): readonly string[] {\n return this.read(this.requiredScopesValue, 'setRequiredScopes');\n }\n\n get maxAccountValidationAgeSeconds(): number {\n return this.maxAccountValidationAgeSecondsValue;\n }\n\n get maxEndpointJwtLifetimeSeconds(): number {\n return this.maxEndpointJwtLifetimeSecondsValue;\n }\n\n /**\n * RFC 9728 §3.1: the URL of this resource's protected-resource METADATA DOCUMENT, derived from\n * {@link resource} by inserting `/.well-known/oauth-protected-resource` between origin and path\n * (`https://host/mcp` → `https://host/.well-known/oauth-protected-resource/mcp`).\n *\n * This — never the resource identifier itself — is what RFC 9728 §5.1 defines `resource_metadata`\n * to be, so it is the value webpieces puts in the 401 `WWW-Authenticate` challenge. webpieces\n * mounts no `.well-known` route; publish {@link protectedResourceMetadata} at this URL.\n */\n get resourceMetadataUrl(): string {\n const url = new URL(this.resource);\n const path = url.pathname === '/' ? '' : url.pathname;\n return `${url.origin}/.well-known/oauth-protected-resource${path}`;\n }\n\n protectedResourceMetadata(): McpProtectedResourceMetadata {\n return new McpProtectedResourceMetadata(\n this.resource,\n this.authorizationServers,\n this.requiredScopes,\n );\n }\n\n private requireText(value: string, setter: string): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a non-empty string.`);\n }\n return value;\n }\n\n private requireUrl(value: string, setter: string): string {\n this.requireText(value, setter);\n if (!URL.canParse(value)) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires an absolute URL, got '${value}'.`,\n );\n }\n return value;\n }\n\n private requireArray(value: readonly string[], setter: string): readonly string[] {\n if (!Array.isArray(value)) {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires an array of strings.`);\n }\n return value;\n }\n\n private requireSeconds(value: number, ceiling: number, setter: string): number {\n if (!Number.isFinite(value) || value <= 0 || value > ceiling) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires 1..${ceiling} seconds, got ${String(value)}.`,\n );\n }\n return value;\n }\n\n private requirePresent<T>(value: T, setter: string): T {\n if (value === undefined || value === null) {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a value.`);\n }\n return value;\n }\n\n private read<T>(value: T | undefined, setter: string): T {\n if (value === undefined) {\n throw new Error(`WpMcpServerConfig is missing ${setter}(...)`);\n }\n return value;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"McpAuth.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuth.ts"],"names":[],"mappings":";;;AAEa,QAAA,qCAAqC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAC1D,QAAA,sCAAsC,GAAG,EAAE,GAAG,EAAE,CAAC;AACjD,QAAA,qCAAqC,GAAG,EAAE,GAAG,EAAE,CAAC;AAE7D,oGAAoG;AACpG,MAAa,oBAAoB;IAET;IACA;IACA;IACA;IAJpB,YACoB,KAAa,EACb,QAAgB,EAChB,oBAA4B,EAC5B,qBAA6B;QAH7B,UAAK,GAAL,KAAK,CAAQ;QACb,aAAQ,GAAR,QAAQ,CAAQ;QAChB,yBAAoB,GAApB,oBAAoB,CAAQ;QAC5B,0BAAqB,GAArB,qBAAqB,CAAQ;QAE7C,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAChD,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;QAC/E,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;YACpF,MAAM,IAAI,KAAK,CACX,wEAAwE,CAC3E,CAAC;QACN,CAAC;QACD,IAAI,qBAAqB,IAAI,oBAAoB,EAAE,CAAC;YAChD,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,qBAAqB,GAAG,oBAAoB,GAAG,6CAAqC,EAAE,CAAC;YACvF,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;CACJ;AAtBD,oDAsBC;AAED;;;;GAIG;AACH,MAAa,qBAAqB;IAEV;IACA;IACA;IACA;IACA;IACA;IAEA;IAEA;IAVpB,YACoB,OAAe,EACf,MAAc,EACd,QAAgB,EAChB,oBAA4B,EAC5B,qBAA6B,EAC7B,MAAyB;IACzC,4FAA4F;IAC5E,8BAAsC;IACtD,gGAAgG;IAChF,MAA2B;QAT3B,YAAO,GAAP,OAAO,CAAQ;QACf,WAAM,GAAN,MAAM,CAAQ;QACd,aAAQ,GAAR,QAAQ,CAAQ;QAChB,yBAAoB,GAApB,oBAAoB,CAAQ;QAC5B,0BAAqB,GAArB,qBAAqB,CAAQ;QAC7B,WAAM,GAAN,MAAM,CAAmB;QAEzB,mCAA8B,GAA9B,8BAA8B,CAAQ;QAEtC,WAAM,GAAN,MAAM,CAAqB;IAC5C,CAAC;IAEJ,wFAAwF;IACxF,IAAI,cAAc;QACd,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IAC7B,CAAC;CACJ;AAlBD,sDAkBC;AAeD,6FAA6F;AAC7F,MAAa,qBAAqB;IAEV;IACA;IACA;IAHpB,YACoB,QAAgB,EAChB,YAAoB,EACpB,UAAkB;QAFlB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,iBAAY,GAAZ,YAAY,CAAQ;QACpB,eAAU,GAAV,UAAU,CAAQ;IACnC,CAAC;CACP;AAND,sDAMC;AAQD,qEAAqE;AACrE,MAAa,4BAA4B;IAC5B,QAAQ,CAAS;IACjB,qBAAqB,CAAoB;IACzC,wBAAwB,GAAG,CAAC,QAAQ,CAAC,CAAC;IACtC,gBAAgB,CAAqB;IAE9C,YACI,QAAgB,EAChB,oBAAuC,EACvC,eAAmC;QAEnC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,qBAAqB,GAAG,oBAAoB,CAAC;QAClD,IAAI,CAAC,gBAAgB,GAAG,eAAe,CAAC;IAC5C,CAAC;CACJ;AAfD,oEAeC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAa,iBAAiB;IAClB,SAAS,CAAU;IACnB,YAAY,CAAU;IACtB,aAAa,CAAU;IACvB,yBAAyB,CAAmC;IAC5D,yBAAyB,CAAyB;IAClD,wBAAwB,CAA+C;IACvE,yBAAyB,CAAqB;IAC9C,mBAAmB,CAAqB;IACxC,mCAAmC,GAAG,8CAAsC,CAAC;IAC7E,kCAAkC,GAAG,6CAAqC,CAAC;IAEnF,8DAA8D;IAC9D,OAAO,CAAC,IAAY;QAChB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,8FAA8F;IAC9F,UAAU,CAAC,OAAe;QACtB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;QAC5D,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,QAAgB;QACxB,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;QAC9D,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,sFAAsF;IACtF,uBAAuB,CAAC,SAA0C;QAC9D,IAAI,CAAC,yBAAyB,GAAG,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;QAC3F,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,0FAA0F;IAC1F,uBAAuB,CAAC,SAAgC;QACpD,IAAI,CAAC,yBAAyB,GAAG,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,yBAAyB,CAAC,CAAC;QAC3F,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,gGAAgG;IAChG,sBAAsB,CAAC,OAAoD;QACvE,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;YAChC,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;QAC1F,CAAC;QACD,IAAI,CAAC,wBAAwB,GAAG,OAAO,CAAC;QACxC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACH,uBAAuB,CAAC,oBAAuC;QAC3D,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,oBAAoB,EAAE,yBAAyB,CAAC,CAAC;QACnF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACX,kFAAkF,CACrF,CAAC;QACN,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,OAAO;YAAE,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAAC;QACjF,IAAI,CAAC,yBAAyB,GAAG,OAAO,CAAC;QACzC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,iBAAiB,CAAC,cAAiC;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,mBAAmB,CAAC,CAAC;QACtE,KAAK,MAAM,KAAK,IAAI,MAAM;YAAE,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC;QACzE,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC;QAClC,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,gEAAgE;IAChE,iCAAiC,CAAC,OAAe;QAC7C,IAAI,CAAC,mCAAmC,GAAG,IAAI,CAAC,cAAc,CAC1D,OAAO,EACP,8CAAsC,EACtC,mCAAmC,CACtC,CAAC;QACF,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,gEAAgE;IAChE,gCAAgC,CAAC,OAAe;QAC5C,IAAI,CAAC,kCAAkC,GAAG,IAAI,CAAC,cAAc,CACzD,OAAO,EACP,6CAAqC,EACrC,kCAAkC,CACrC,CAAC;QACF,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;OAGG;IACH,QAAQ;QACJ,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC/D,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QACrE,IAAI,IAAI,CAAC,aAAa,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QACvE,IAAI,IAAI,CAAC,yBAAyB,KAAK,SAAS,EAAE,CAAC;YAC/C,OAAO,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,IAAI,CAAC,yBAAyB,KAAK,SAAS,EAAE,CAAC;YAC/C,OAAO,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,IAAI,CAAC,wBAAwB,KAAK,SAAS,EAAE,CAAC;YAC9C,OAAO,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,IAAI,CAAC,yBAAyB,KAAK,SAAS,EAAE,CAAC;YAC/C,OAAO,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,IAAI,CAAC,mBAAmB,KAAK,SAAS;YAAE,OAAO,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;QACnF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,gCAAgC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;IAED,IAAI,IAAI;QACJ,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAChD,CAAC;IAED,IAAI,OAAO;QACP,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;IACtD,CAAC;IAED,IAAI,QAAQ;QACR,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;IACxD,CAAC;IAED,IAAI,oBAAoB;QACpB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,yBAAyB,EAAE,yBAAyB,CAAC,CAAC;IAChF,CAAC;IAED,IAAI,oBAAoB;QACpB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,yBAAyB,EAAE,yBAAyB,CAAC,CAAC;IAChF,CAAC;IAED,IAAI,mBAAmB;QACnB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,wBAAwB,EAAE,wBAAwB,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,oBAAoB;QACpB,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,yBAAyB,EAAE,yBAAyB,CAAC,CAAC;IAChF,CAAC;IAED,IAAI,cAAc;QACd,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,mBAAmB,CAAC,CAAC;IACpE,CAAC;IAED,IAAI,8BAA8B;QAC9B,OAAO,IAAI,CAAC,mCAAmC,CAAC;IACpD,CAAC;IAED,IAAI,6BAA6B;QAC7B,OAAO,IAAI,CAAC,kCAAkC,CAAC;IACnD,CAAC;IAED;;;;;;;;OAQG;IACH,IAAI,mBAAmB;QACnB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;QACtD,OAAO,GAAG,GAAG,CAAC,MAAM,wCAAwC,IAAI,EAAE,CAAC;IACvE,CAAC;IAED,yBAAyB;QACrB,OAAO,IAAI,4BAA4B,CACnC,IAAI,CAAC,QAAQ,EACb,IAAI,CAAC,oBAAoB,EACzB,IAAI,CAAC,cAAc,CACtB,CAAC;IACN,CAAC;IAEO,WAAW,CAAC,KAAa,EAAE,MAAc;QAC7C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,qBAAqB,MAAM,oCAAoC,CAAC,CAAC;QACrF,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,UAAU,CAAC,KAAa,EAAE,MAAc;QAC5C,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAChC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACX,qBAAqB,MAAM,wCAAwC,KAAK,IAAI,CAC/E,CAAC;QACN,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,YAAY,CAAC,KAAwB,EAAE,MAAc;QACzD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,qBAAqB,MAAM,qCAAqC,CAAC,CAAC;QACtF,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,cAAc,CAAC,KAAa,EAAE,OAAe,EAAE,MAAc;QACjE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,GAAG,OAAO,EAAE,CAAC;YAC3D,MAAM,IAAI,KAAK,CACX,qBAAqB,MAAM,qBAAqB,OAAO,iBAAiB,MAAM,CAAC,KAAK,CAAC,GAAG,CAC3F,CAAC;QACN,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,cAAc,CAAI,KAAQ,EAAE,MAAc;QAC9C,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,KAAK,CAAC,qBAAqB,MAAM,yBAAyB,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAEO,IAAI,CAAI,KAAoB,EAAE,MAAc;QAChD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,gCAAgC,MAAM,OAAO,CAAC,CAAC;QACnE,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;CACJ;AAvPD,8CAuPC","sourcesContent":["import { AuthenticatedCaller, JwtHook } from '@webpieces/http-routing';\n\nexport const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;\nexport const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = 60 * 60;\nexport const MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS = 60 * 60;\n\n/** Framework-normalized access-token result. The token representation remains application-owned. */\nexport class MintedMcpAccessToken {\n constructor(\n public readonly token: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n ) {\n if (token.trim() === '' || resource.trim() === '') {\n throw new Error('Minted MCP access token and resource must be non-empty.');\n }\n if (!Number.isFinite(issuedAtEpochSeconds) || !Number.isFinite(expiresAtEpochSeconds)) {\n throw new Error(\n 'Minted MCP access token timestamps must be finite epoch-second values.',\n );\n }\n if (expiresAtEpochSeconds <= issuedAtEpochSeconds) {\n throw new Error('MCP access token expiry must be after issuance.');\n }\n if (expiresAtEpochSeconds - issuedAtEpochSeconds > MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS) {\n throw new Error('MCP access token lifetime must not exceed 30 days.');\n }\n }\n}\n\n/**\n * Fresh, authoritative security facts and an explicit application-owned caller mapping resolved at the MCP resource boundary. Implementations must\n * verify issuer, exact resource/audience, expiry, token type/version, key/algorithm policy, and\n * current account state. Opaque access tokens are fully supported.\n */\nexport class VerifiedMcpCredential {\n constructor(\n public readonly subject: string,\n public readonly issuer: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n public readonly scopes: readonly string[],\n /** When current enabled/revoked state and roles were read from the authoritative source. */\n public readonly accountValidatedAtEpochSeconds: number,\n /** Application mapping of freshly verified account facts, shared with endpoint JWT issuance. */\n public readonly caller: AuthenticatedCaller,\n ) {}\n\n /** The canonical application's roles; there is no independent listing-role snapshot. */\n get principalRoles(): readonly string[] {\n return this.caller.roles;\n }\n}\n\n/**\n * Application-owned paired mint/verify authority for resource-bound MCP access tokens.\n * `verifyAccessToken` must throw `ApiUnauthorizedError` for a token it rejects (the bind boundary\n * answers 401 + `WWW-Authenticate`); any other throw is treated as an implementation failure (500).\n */\nexport interface McpAccessTokenAuthority<TGrant> {\n mintAccessToken(grant: TGrant): Promise<MintedMcpAccessToken>;\n verifyAccessToken(\n accessToken: string,\n expectedResource: string,\n ): Promise<VerifiedMcpCredential>;\n}\n\n/** Minimal tool identity supplied when the app creates its own endpoint-JWT mint request. */\nexport class McpEndpointDescriptor {\n constructor(\n public readonly toolName: string,\n public readonly apiClassName: string,\n public readonly methodName: string,\n ) {}\n}\n\n/** Application mapping from verified MCP identity to its unconstrained JwtHook mint request. */\nexport type McpEndpointMintRequestFactory<TMintRequest> = (\n credential: VerifiedMcpCredential,\n endpoint: McpEndpointDescriptor,\n) => TMintRequest;\n\n/** RFC 9728-style metadata exposed by the MCP protected resource. */\nexport class McpProtectedResourceMetadata {\n readonly resource: string;\n readonly authorization_servers: readonly string[];\n readonly bearer_methods_supported = ['header'];\n readonly scopes_supported?: readonly string[];\n\n constructor(\n resource: string,\n authorizationServers: readonly string[],\n scopesSupported?: readonly string[],\n ) {\n this.resource = resource;\n this.authorization_servers = authorizationServers;\n this.scopes_supported = scopesSupported;\n }\n}\n\n/**\n * Node MCP server configuration, built with fluent setters: every name sits next to its own value, so\n * no two settings can be swapped, and a future setting is an additive setter rather than a breaking\n * signature change. Pass the same concrete application authority to both\n * `setAccessTokenAuthority(...)` and `setEndpointJwtAuthority(...)` when it implements both security\n * seams.\n *\n * SINGLE CANONICAL ORIGIN: one server serves exactly one protected resource. {@link setResource} is\n * fixed for the lifetime of the process, is the audience of every access token, and is never derived\n * from the request `Host` — see that setter for why a host-derived audience is a confused deputy.\n *\n * Each setter validates its own value immediately and names itself in the failure. Setters cannot\n * make a field impossible to forget the way a positional constructor could, so\n * {@link WpMcpServerConfig.validate} runs from `WpMcpServer.bind(...)` and fails at startup listing\n * EVERY missing required setter at once. A misconfiguration is therefore a boot failure naming the\n * setter, instead of a first-request 401 + `WWW-Authenticate` — which is the OAuth discovery signal,\n * so the client's correct response is to re-authenticate and fail again, forever.\n *\n * ```ts\n * const config = new WpMcpServerConfig<MyGrant, MyMintRequest>()\n * .setName('my-server')\n * .setVersion('1.0.0')\n * .setResource('https://api.example.com/mcp')\n * .setAccessTokenAuthority(authority)\n * .setEndpointJwtAuthority(jwtHook)\n * .setEndpointMintRequest((credential) => new MyMintRequest(credential.subject))\n * .setAuthorizationServers(['https://login.example.com'])\n * .setRequiredScopes(['tools']);\n *\n * // the tools/call error translator is a PROCESS-GLOBAL, not a member of this config:\n * McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());\n * ```\n */\nexport class WpMcpServerConfig<TGrant, TMintRequest> {\n private nameValue?: string;\n private versionValue?: string;\n private resourceValue?: string;\n private accessTokenAuthorityValue?: McpAccessTokenAuthority<TGrant>;\n private endpointJwtAuthorityValue?: JwtHook<TMintRequest>;\n private endpointMintRequestValue?: McpEndpointMintRequestFactory<TMintRequest>;\n private authorizationServersValue?: readonly string[];\n private requiredScopesValue?: readonly string[];\n private maxAccountValidationAgeSecondsValue = MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS;\n private maxEndpointJwtLifetimeSecondsValue = MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS;\n\n /** REQUIRED. The MCP server name reported by `initialize`. */\n setName(name: string): this {\n this.nameValue = this.requireText(name, 'setName');\n return this;\n }\n\n /** REQUIRED. The MCP server version; the registry revision is appended to it at bind time. */\n setVersion(version: string): this {\n this.versionValue = this.requireText(version, 'setVersion');\n return this;\n }\n\n /**\n * REQUIRED. The ONE canonical protected-resource URI, which must be an absolute URL: it is the\n * exact audience every access token is checked against, and {@link resourceMetadataUrl} is\n * derived from it for the 401 challenge, so a non-URL here is always a mistake — including a\n * name or version landed on it.\n *\n * It is fixed for the lifetime of the process and must NEVER be derived from the request `Host`\n * header: the boundary compares a token's audience against this value, so a host-derived\n * resource compares two values both taken from caller-controlled input — the confused-deputy\n * shape this check exists to prevent. Its path must equal `McpBindOptions.endpointPath`, which\n * `WpMcpServer.bind(...)` verifies. A second hostname is a second deployment, not a second\n * audience.\n */\n setResource(resource: string): this {\n this.resourceValue = this.requireUrl(resource, 'setResource');\n return this;\n }\n\n /** REQUIRED. Application-owned paired mint/verify authority for MCP access tokens. */\n setAccessTokenAuthority(authority: McpAccessTokenAuthority<TGrant>): this {\n this.accessTokenAuthorityValue = this.requirePresent(authority, 'setAccessTokenAuthority');\n return this;\n }\n\n /** REQUIRED. The application policy for MCP tools and short-lived endpoint JWT issuer. */\n setEndpointJwtAuthority(authority: JwtHook<TMintRequest>): this {\n this.endpointJwtAuthorityValue = this.requirePresent(authority, 'setEndpointJwtAuthority');\n return this;\n }\n\n /** REQUIRED. Maps a verified MCP identity plus tool identity to that JwtHook's mint request. */\n setEndpointMintRequest(factory: McpEndpointMintRequestFactory<TMintRequest>): this {\n if (typeof factory !== 'function') {\n throw new Error('WpMcpServerConfig.setEndpointMintRequest(...) requires a function.');\n }\n this.endpointMintRequestValue = factory;\n return this;\n }\n\n /**\n * REQUIRED. The trusted token issuers. Every entry must be an absolute URL, because each is\n * compared against a token's `issuer` claim and published as an `authorization_servers` entry —\n * which is what makes a scope list landed here fail at boot rather than on the first request.\n */\n setAuthorizationServers(authorizationServers: readonly string[]): this {\n const servers = this.requireArray(authorizationServers, 'setAuthorizationServers');\n if (servers.length === 0) {\n throw new Error(\n 'WpMcpServerConfig.setAuthorizationServers(...) requires at least one issuer URL.',\n );\n }\n for (const server of servers) this.requireUrl(server, 'setAuthorizationServers');\n this.authorizationServersValue = servers;\n return this;\n }\n\n /**\n * REQUIRED, and empty only by saying so explicitly. Scopes every access token must carry before\n * any tool is dispatched.\n */\n setRequiredScopes(requiredScopes: readonly string[]): this {\n const scopes = this.requireArray(requiredScopes, 'setRequiredScopes');\n for (const scope of scopes) this.requireText(scope, 'setRequiredScopes');\n this.requiredScopesValue = scopes;\n return this;\n }\n\n /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */\n setMaxAccountValidationAgeSeconds(seconds: number): this {\n this.maxAccountValidationAgeSecondsValue = this.requireSeconds(\n seconds,\n MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS,\n 'setMaxAccountValidationAgeSeconds',\n );\n return this;\n }\n\n /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */\n setMaxEndpointJwtLifetimeSeconds(seconds: number): this {\n this.maxEndpointJwtLifetimeSecondsValue = this.requireSeconds(\n seconds,\n MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS,\n 'setMaxEndpointJwtLifetimeSeconds',\n );\n return this;\n }\n\n /**\n * Called by `WpMcpServer.bind(...)`. Throws naming EVERY missing required setter at once, so a\n * half-built config is repaired in one pass rather than one boot failure at a time.\n */\n validate(): void {\n const missing: string[] = [];\n if (this.nameValue === undefined) missing.push('setName(...)');\n if (this.versionValue === undefined) missing.push('setVersion(...)');\n if (this.resourceValue === undefined) missing.push('setResource(...)');\n if (this.accessTokenAuthorityValue === undefined) {\n missing.push('setAccessTokenAuthority(...)');\n }\n if (this.endpointJwtAuthorityValue === undefined) {\n missing.push('setEndpointJwtAuthority(...)');\n }\n if (this.endpointMintRequestValue === undefined) {\n missing.push('setEndpointMintRequest(...)');\n }\n if (this.authorizationServersValue === undefined) {\n missing.push('setAuthorizationServers(...)');\n }\n if (this.requiredScopesValue === undefined) missing.push('setRequiredScopes(...)');\n if (missing.length > 0) {\n throw new Error(`WpMcpServerConfig is missing ${missing.join(', ')}`);\n }\n }\n\n get name(): string {\n return this.read(this.nameValue, 'setName');\n }\n\n get version(): string {\n return this.read(this.versionValue, 'setVersion');\n }\n\n get resource(): string {\n return this.read(this.resourceValue, 'setResource');\n }\n\n get accessTokenAuthority(): McpAccessTokenAuthority<TGrant> {\n return this.read(this.accessTokenAuthorityValue, 'setAccessTokenAuthority');\n }\n\n get endpointJwtAuthority(): JwtHook<TMintRequest> {\n return this.read(this.endpointJwtAuthorityValue, 'setEndpointJwtAuthority');\n }\n\n get endpointMintRequest(): McpEndpointMintRequestFactory<TMintRequest> {\n return this.read(this.endpointMintRequestValue, 'setEndpointMintRequest');\n }\n\n get authorizationServers(): readonly string[] {\n return this.read(this.authorizationServersValue, 'setAuthorizationServers');\n }\n\n get requiredScopes(): readonly string[] {\n return this.read(this.requiredScopesValue, 'setRequiredScopes');\n }\n\n get maxAccountValidationAgeSeconds(): number {\n return this.maxAccountValidationAgeSecondsValue;\n }\n\n get maxEndpointJwtLifetimeSeconds(): number {\n return this.maxEndpointJwtLifetimeSecondsValue;\n }\n\n /**\n * RFC 9728 §3.1: the URL of this resource's protected-resource METADATA DOCUMENT, derived from\n * {@link resource} by inserting `/.well-known/oauth-protected-resource` between origin and path\n * (`https://host/mcp` → `https://host/.well-known/oauth-protected-resource/mcp`).\n *\n * This — never the resource identifier itself — is what RFC 9728 §5.1 defines `resource_metadata`\n * to be, so it is the value webpieces puts in the 401 `WWW-Authenticate` challenge. webpieces\n * mounts no `.well-known` route; publish {@link protectedResourceMetadata} at this URL.\n */\n get resourceMetadataUrl(): string {\n const url = new URL(this.resource);\n const path = url.pathname === '/' ? '' : url.pathname;\n return `${url.origin}/.well-known/oauth-protected-resource${path}`;\n }\n\n protectedResourceMetadata(): McpProtectedResourceMetadata {\n return new McpProtectedResourceMetadata(\n this.resource,\n this.authorizationServers,\n this.requiredScopes,\n );\n }\n\n private requireText(value: string, setter: string): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a non-empty string.`);\n }\n return value;\n }\n\n private requireUrl(value: string, setter: string): string {\n this.requireText(value, setter);\n if (!URL.canParse(value)) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires an absolute URL, got '${value}'.`,\n );\n }\n return value;\n }\n\n private requireArray(value: readonly string[], setter: string): readonly string[] {\n if (!Array.isArray(value)) {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires an array of strings.`);\n }\n return value;\n }\n\n private requireSeconds(value: number, ceiling: number, setter: string): number {\n if (!Number.isFinite(value) || value <= 0 || value > ceiling) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires 1..${ceiling} seconds, got ${String(value)}.`,\n );\n }\n return value;\n }\n\n private requirePresent<T>(value: T, setter: string): T {\n if (value === undefined || value === null) {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a value.`);\n }\n return value;\n }\n\n private read<T>(value: T | undefined, setter: string): T {\n if (value === undefined) {\n throw new Error(`WpMcpServerConfig is missing ${setter}(...)`);\n }\n return value;\n }\n}\n"]}
|
|
@@ -16,4 +16,8 @@ export declare class McpAuthorityCompileAssertions extends JwtHook<ExampleJwtMin
|
|
|
16
16
|
mintAccessToken(grant: ExampleMcpGrant): Promise<MintedMcpAccessToken>;
|
|
17
17
|
verifyAccessToken(_accessToken: string, expectedResource: string): Promise<VerifiedMcpCredential>;
|
|
18
18
|
}
|
|
19
|
+
/** Compiled hard-cut assertions; Vitest does not type-check specifications. */
|
|
20
|
+
export declare class McpPrincipalCompileAssertions {
|
|
21
|
+
roles(credential: VerifiedMcpCredential): readonly string[];
|
|
22
|
+
}
|
|
19
23
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.McpAuthorityCompileAssertions = void 0;
|
|
3
|
+
exports.McpPrincipalCompileAssertions = exports.McpAuthorityCompileAssertions = void 0;
|
|
4
4
|
const http_routing_1 = require("@webpieces/http-routing");
|
|
5
5
|
const McpAuth_1 = require("./McpAuth");
|
|
6
6
|
class ExampleJwtMintRequest {
|
|
@@ -31,8 +31,17 @@ class McpAuthorityCompileAssertions extends http_routing_1.JwtHook {
|
|
|
31
31
|
}
|
|
32
32
|
async verifyAccessToken(_accessToken, expectedResource) {
|
|
33
33
|
const now = Math.floor(Date.now() / 1000);
|
|
34
|
-
return new McpAuth_1.VerifiedMcpCredential('example-user', 'https://issuer.example.test', expectedResource, now, now + 60, ['tools'], now);
|
|
34
|
+
return new McpAuth_1.VerifiedMcpCredential('example-user', 'https://issuer.example.test', expectedResource, now, now + 60, ['tools'], now, new http_routing_1.AuthenticatedCaller('example-user'));
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
exports.McpAuthorityCompileAssertions = McpAuthorityCompileAssertions;
|
|
38
|
+
/** Compiled hard-cut assertions; Vitest does not type-check specifications. */
|
|
39
|
+
class McpPrincipalCompileAssertions {
|
|
40
|
+
roles(credential) {
|
|
41
|
+
// @ts-expect-error listingRoles was replaced by principalRoles derived from the canonical caller
|
|
42
|
+
credential.listingRoles;
|
|
43
|
+
return credential.principalRoles;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.McpPrincipalCompileAssertions = McpPrincipalCompileAssertions;
|
|
38
47
|
//# sourceMappingURL=McpAuthorityCompileAssertions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"McpAuthorityCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuthorityCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,0DAAkF;AAClF,uCAAiG;AAEjG,MAAM,qBAAqB;IACK;IAA5B,YAA4B,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;CAClD;AAED,MAAM,eAAe;IAEG;IACA;IAFpB,YACoB,OAAe,EACf,QAAgB;QADhB,YAAO,GAAP,OAAO,CAAQ;QACf,aAAQ,GAAR,QAAQ,CAAQ;IACjC,CAAC;CACP;AAED,uGAAuG;AACvG,MAAa,6BACT,SAAQ,sBAA8B;IAG7B,KAAK,CAAC,IAAI,CAAC,OAA8B;QAC9C,OAAO,IAAI,wBAAS,CAAC,YAAY,OAAO,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;IAChF,CAAC;IAEQ,KAAK,CAAC,QAAQ,CAAC,KAAa;QACjC,OAAO,IAAI,kCAAmB,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,KAAsB;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,OAAO,IAAI,8BAAoB,CAAC,OAAO,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED,KAAK,CAAC,iBAAiB,CACnB,YAAoB,EACpB,gBAAwB;QAExB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,OAAO,IAAI,+BAAqB,CAC5B,cAAc,EACd,6BAA6B,EAC7B,gBAAgB,EAChB,GAAG,EACH,GAAG,GAAG,EAAE,EACR,CAAC,OAAO,CAAC,EACT,GAAG,
|
|
1
|
+
{"version":3,"file":"McpAuthorityCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuthorityCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,0DAAkF;AAClF,uCAAiG;AAEjG,MAAM,qBAAqB;IACK;IAA5B,YAA4B,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;CAClD;AAED,MAAM,eAAe;IAEG;IACA;IAFpB,YACoB,OAAe,EACf,QAAgB;QADhB,YAAO,GAAP,OAAO,CAAQ;QACf,aAAQ,GAAR,QAAQ,CAAQ;IACjC,CAAC;CACP;AAED,uGAAuG;AACvG,MAAa,6BACT,SAAQ,sBAA8B;IAG7B,KAAK,CAAC,IAAI,CAAC,OAA8B;QAC9C,OAAO,IAAI,wBAAS,CAAC,YAAY,OAAO,CAAC,OAAO,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC;IAChF,CAAC;IAEQ,KAAK,CAAC,QAAQ,CAAC,KAAa;QACjC,OAAO,IAAI,kCAAmB,CAAC,KAAK,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,KAAsB;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,OAAO,IAAI,8BAAoB,CAAC,OAAO,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,GAAG,EAAE,CAAC,CAAC;IAC3F,CAAC;IAED,KAAK,CAAC,iBAAiB,CACnB,YAAoB,EACpB,gBAAwB;QAExB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,OAAO,IAAI,+BAAqB,CAC5B,cAAc,EACd,6BAA6B,EAC7B,gBAAgB,EAChB,GAAG,EACH,GAAG,GAAG,EAAE,EACR,CAAC,OAAO,CAAC,EACT,GAAG,EACH,IAAI,kCAAmB,CAAC,cAAc,CAAC,CAC1C,CAAC;IACN,CAAC;CACJ;AAjCD,sEAiCC;AAED,+EAA+E;AAC/E,MAAa,6BAA6B;IACtC,KAAK,CAAC,UAAiC;QACnC,iGAAiG;QACjG,UAAU,CAAC,YAAY,CAAC;QACxB,OAAO,UAAU,CAAC,cAAc,CAAC;IACrC,CAAC;CACJ;AAND,sEAMC","sourcesContent":["import { AuthenticatedCaller, JwtHook, MintedJwt } from '@webpieces/http-routing';\nimport { McpAccessTokenAuthority, MintedMcpAccessToken, VerifiedMcpCredential } from './McpAuth';\n\nclass ExampleJwtMintRequest {\n constructor(public readonly subject: string) {}\n}\n\nclass ExampleMcpGrant {\n constructor(\n public readonly subject: string,\n public readonly resource: string,\n ) {}\n}\n\n/** Compile assertion: one application authority can own both endpoint JWT and MCP token lifecycles. */\nexport class McpAuthorityCompileAssertions\n extends JwtHook<ExampleJwtMintRequest>\n implements McpAccessTokenAuthority<ExampleMcpGrant>\n{\n override async mint(request: ExampleJwtMintRequest): Promise<MintedJwt> {\n return new MintedJwt(`endpoint:${request.subject}`, Date.now() / 1000 + 60);\n }\n\n override async parseJwt(token: string): Promise<AuthenticatedCaller> {\n return new AuthenticatedCaller(token);\n }\n\n async mintAccessToken(grant: ExampleMcpGrant): Promise<MintedMcpAccessToken> {\n const now = Math.floor(Date.now() / 1000);\n return new MintedMcpAccessToken(`mcp:${grant.subject}`, grant.resource, now, now + 60);\n }\n\n async verifyAccessToken(\n _accessToken: string,\n expectedResource: string,\n ): Promise<VerifiedMcpCredential> {\n const now = Math.floor(Date.now() / 1000);\n return new VerifiedMcpCredential(\n 'example-user',\n 'https://issuer.example.test',\n expectedResource,\n now,\n now + 60,\n ['tools'],\n now,\n new AuthenticatedCaller('example-user'),\n );\n }\n}\n\n/** Compiled hard-cut assertions; Vitest does not type-check specifications. */\nexport class McpPrincipalCompileAssertions {\n roles(credential: VerifiedMcpCredential): readonly string[] {\n // @ts-expect-error listingRoles was replaced by principalRoles derived from the canonical caller\n credential.listingRoles;\n return credential.principalRoles;\n }\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { JwtRequirement } from '@webpieces/core-util';
|
|
2
|
+
import { AuthenticatedCaller, JwtHook } from '@webpieces/http-routing';
|
|
3
|
+
/** Internal denial marker; only the shared SDK boundary converts it to unknown-tool protocol shape. */
|
|
4
|
+
export declare class McpToolDeniedError extends Error {
|
|
5
|
+
readonly toolName: string;
|
|
6
|
+
constructor(toolName: string);
|
|
7
|
+
}
|
|
8
|
+
/** Application policy is the only predicate used for both projection and dispatch. */
|
|
9
|
+
export declare class McpToolPolicy<T> {
|
|
10
|
+
private readonly policy;
|
|
11
|
+
constructor(policy: JwtHook<T>);
|
|
12
|
+
permits(caller: AuthenticatedCaller, requirement: JwtRequirement): Promise<boolean>;
|
|
13
|
+
require(caller: AuthenticatedCaller, requirement: JwtRequirement, name: string): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpToolPolicy = exports.McpToolDeniedError = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
/** Internal denial marker; only the shared SDK boundary converts it to unknown-tool protocol shape. */
|
|
6
|
+
class McpToolDeniedError extends Error {
|
|
7
|
+
toolName;
|
|
8
|
+
constructor(toolName) {
|
|
9
|
+
super('MCP tool authorization denied');
|
|
10
|
+
this.toolName = toolName;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.McpToolDeniedError = McpToolDeniedError;
|
|
14
|
+
/** Application policy is the only predicate used for both projection and dispatch. */
|
|
15
|
+
class McpToolPolicy {
|
|
16
|
+
policy;
|
|
17
|
+
constructor(policy) {
|
|
18
|
+
this.policy = policy;
|
|
19
|
+
}
|
|
20
|
+
async permits(caller, requirement) {
|
|
21
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- only explicit policy denials are a negative predicate; other failures propagate
|
|
22
|
+
try {
|
|
23
|
+
await this.policy.authorizeJwt(caller, requirement);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const error = (0, core_util_1.toError)(err);
|
|
28
|
+
if (error instanceof core_util_1.ApiForbiddenError)
|
|
29
|
+
return false;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async require(caller, requirement, name) {
|
|
34
|
+
if (!(await this.permits(caller, requirement)))
|
|
35
|
+
throw new McpToolDeniedError(name);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.McpToolPolicy = McpToolPolicy;
|
|
39
|
+
//# sourceMappingURL=McpToolPolicy.js.map
|