@webpieces/mcp-server 0.4.772 → 0.4.774

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.
Files changed (42) hide show
  1. package/README.md +114 -8
  2. package/package.json +11 -5
  3. package/src/McpApiBinding.d.ts +14 -0
  4. package/src/McpApiBinding.js +42 -0
  5. package/src/McpApiBinding.js.map +1 -0
  6. package/src/McpApiDispatcher.d.ts +10 -19
  7. package/src/McpApiDispatcher.js +35 -44
  8. package/src/McpApiDispatcher.js.map +1 -1
  9. package/src/McpAuth.d.ts +11 -2
  10. package/src/McpAuth.js +5 -1
  11. package/src/McpAuth.js.map +1 -1
  12. package/src/McpAuthorityCompileAssertions.js.map +1 -1
  13. package/src/McpBindOptions.d.ts +17 -0
  14. package/src/McpBindOptions.js +36 -0
  15. package/src/McpBindOptions.js.map +1 -0
  16. package/src/McpDeployment.d.ts +11 -0
  17. package/src/McpDeployment.js +32 -0
  18. package/src/McpDeployment.js.map +1 -0
  19. package/src/McpInvocationContext.d.ts +14 -0
  20. package/src/McpInvocationContext.js +25 -0
  21. package/src/McpInvocationContext.js.map +1 -0
  22. package/src/McpToolRegistry.d.ts +7 -4
  23. package/src/McpToolRegistry.js +20 -16
  24. package/src/McpToolRegistry.js.map +1 -1
  25. package/src/WpMcpErrorTranslator.d.ts +69 -0
  26. package/src/WpMcpErrorTranslator.js +168 -0
  27. package/src/WpMcpErrorTranslator.js.map +1 -0
  28. package/src/WpMcpServer.d.ts +55 -24
  29. package/src/WpMcpServer.js +331 -154
  30. package/src/WpMcpServer.js.map +1 -1
  31. package/src/__tests__/McpHttpTestHarness.d.ts +40 -0
  32. package/src/__tests__/McpHttpTestHarness.js +158 -0
  33. package/src/__tests__/McpHttpTestHarness.js.map +1 -0
  34. package/src/__tests__/McpRemoteFixtures.d.ts +93 -0
  35. package/src/__tests__/McpRemoteFixtures.js +409 -0
  36. package/src/__tests__/McpRemoteFixtures.js.map +1 -0
  37. package/src/__tests__/WpMcpServerTestFixtures.d.ts +60 -0
  38. package/src/__tests__/WpMcpServerTestFixtures.js +269 -0
  39. package/src/__tests__/WpMcpServerTestFixtures.js.map +1 -0
  40. package/src/index.d.ts +10 -3
  41. package/src/index.js +18 -4
  42. package/src/index.js.map +1 -1
package/README.md CHANGED
@@ -4,8 +4,9 @@ Publishes explicitly annotated Webpieces RPC endpoints as MCP tools. The bridge
4
4
  output JSON Schema from DTO metadata, invokes the normal Webpieces filter/controller path, and turns
5
5
  exceptions into safe model-visible MCP results.
6
6
 
7
- `@WpMcpTool` is an opt-in. Existing endpoint authentication remains authoritative on every call.
8
- The MCP adapter never accepts trusted context values from tool arguments or headers.
7
+ `@WpMcpTool` is an opt-in and every tool must also declare `@WpMcpAuthJwt`. MCP user
8
+ authorization and endpoint transport authentication are deliberately separate. The MCP adapter
9
+ never accepts trusted context values from tool arguments or unverified headers.
9
10
 
10
11
  ```ts
11
12
  @WpDto()
@@ -22,6 +23,7 @@ class FindOrderResponse {
22
23
 
23
24
  @ApiPath('/orders')
24
25
  abstract class OrdersApi {
26
+ @WpMcpAuthJwt({ allRolesAllowed: true })
25
27
  @WpAuthJwt({ allRolesAllowed: true })
26
28
  @Endpoint('/find', 'rpc')
27
29
  @WpResponseDto(() => FindOrderResponse)
@@ -42,12 +44,116 @@ The `@WpMcpTool.description` becomes the tool description returned by `tools/lis
42
44
  response classes generate `inputSchema` and `outputSchema`; `@WpDtoField` supplies property
43
45
  descriptions and the facts TypeScript erases, such as optionality and array element types.
44
46
 
45
- Applications construct `WpMcpServer` with their built `ApiFactory`, the API classes they want scanned,
46
- a paired `McpAccessTokenAuthority`, and their application `JwtHook`. The authority verifies issuer,
47
- signature/token state, expiry, scopes, the exact resource URI, and current account state on every MCP
48
- operation. Before every tool call the bridge asks the same application `JwtHook` to mint a distinct,
49
- short-lived endpoint JWT, then invokes the ordinary `AuthFilter`. Literal MCP-token passthrough is
50
- rejected, endpoint JWTs are capped at one hour, and MCP access tokens are capped at 30 days.
47
+ ### Typed maps
48
+
49
+ A `Record<string, V>` reflects as plain `Object`, so its value type must be declared — the same reason
50
+ `arrayItems` exists for arrays. Use `WpDtoMapFieldOptions(description, required, mapValues)`, where
51
+ `mapValues` is `'string' | 'number' | 'integer' | 'boolean'` or a `@WpDto` class:
52
+
53
+ ```ts
54
+ @WpDto()
55
+ class PassageSentenceItem {
56
+ @WpDtoField(new WpDtoFieldOptions('Sentence text', true))
57
+ text!: string;
58
+
59
+ @WpDtoField(new WpDtoMapFieldOptions('ISO 639-1 -> sentence', false, 'string'))
60
+ translations?: Record<string, string>;
61
+ }
62
+
63
+ @WpDto()
64
+ class PassageResponse {
65
+ @WpDtoField(new WpDtoMapFieldOptions('Sentences by locale', true, PassageSentenceItem))
66
+ sentencesByLocale!: Record<string, PassageSentenceItem>;
67
+ }
68
+ ```
69
+
70
+ The field emits `{ type: 'object', additionalProperties: <value schema> }`, and validation checks every
71
+ value (`$.translations.es must be string`). An `Object`-typed field without map options — an interface or
72
+ an undeclared `Record` — fails at startup, because an interface can never carry `@WpDto`.
73
+
74
+ A map is still a closed schema: no key can carry an unspecified value. Check closure with
75
+ `new DtoSchemaBuilder().isClosedSchema(schema)`, not `schema.additionalProperties === false`, which wrongly
76
+ rejects typed maps.
77
+
78
+ Applications supply the endpoint path, explicit bindings, and deployment topology. There is no
79
+ framework-owned `/mcp` path:
80
+
81
+ ```ts
82
+ const mcp = new WpMcpServer(config);
83
+ mcp.bind(expressApp, new McpBindOptions(
84
+ connectorConfig.endpointPath,
85
+ [
86
+ McpApiBinding.local(OrdersApi, apiFactory),
87
+ McpApiBinding.remote(RemoteOrdersApi, () => remoteOrdersClient),
88
+ ],
89
+ McpDeployment.singleProcess(),
90
+ ));
91
+ ```
92
+
93
+ The official MCP v2 server and Node adapter own MCP 2026-07-28 JSON-RPC validation, JSON versus
94
+ request-scoped SSE responses, backpressure, cancellation, keepalives, and subscriptions. Webpieces
95
+ owns the Express path, root `RequestContext`, authentication boundary, schema-derived tool registry,
96
+ and dispatch through the normal API proxy.
97
+
98
+ During a tool invocation, `RequestContext.getTrusted(MCP_INVOCATION_CONTEXT)` exposes the verified
99
+ JSON-RPC ID, tool/user/role facts, the official SDK cancellation `AbortSignal`, and (when the client
100
+ supplied `_meta.progressToken`) a `reportProgress(...)` callback. Progress remains related to that
101
+ one request and causes the official handler to select request-scoped SSE; closing that response
102
+ aborts the same signal. The context key has no HTTP header and is never propagated as caller input.
103
+
104
+ For a local binding the API method must combine `@WpMcpAuthJwt` with `@WpAuthJwt`. Before every tool
105
+ call the bridge asks the application `JwtHook` to mint a distinct short-lived endpoint JWT and invokes
106
+ `ApiFactory.createApiClient`, preserving `JwtHook`, `LogApiFilter`, and `AuthFilter`. Literal MCP-token
107
+ passthrough is rejected. For a remote binding the endpoint must use `@WpAuthOidc`; the generated Node
108
+ client supplies its service credential and propagates only trusted delegated context established by
109
+ the MCP access-token authority. Neither an external MCP bearer nor a browser session JWT is sent to
110
+ the downstream endpoint.
111
+
112
+ `McpDeployment.singleProcess()` uses an in-memory invalidation bus and makes that limit explicit.
113
+ Distributed deployments must use `McpDeployment.distributed(sharedBus)`. List responses have a bounded
114
+ TTL, registry metadata contains a surface-derived revision, `toolsChanged()` publishes a
115
+ level-triggered invalidation, and `close()` drains active response streams. A reconnecting client must
116
+ re-listen and refresh its authoritative lists; notifications are not a durable or replayable event log.
117
+
118
+ The authority verifies issuer, signature/token state, expiry, scopes, the exact resource URI, and
119
+ current account state. MCP 2026-07-28 has no sessions, so the bearer is verified exactly once per POST
120
+ at the `bind(...)` HTTP boundary, before the SDK is involved; tool handlers never re-verify it. A
121
+ rejected token must be thrown as `ApiUnauthorizedError` (answered 401 + `WWW-Authenticate`); any
122
+ other throw from the authority is an implementation failure (500). Endpoint JWTs are capped at one
123
+ hour and MCP access tokens at 30 days.
124
+
125
+ ## Error boundary
126
+
127
+ `WpMcpErrorTranslator` is the one place a failure becomes an MCP reply, mirroring
128
+ `ApiErrorHttpMapper`. Each entry point has exactly one catch that only delegates to it; the dispatcher
129
+ and the local/remote invokers have none. Normalization is the shared `ApiErrorBoundary` rule (a
130
+ non-`ApiError`, or an `ApiConnectionError`, becomes `ApiImplementationError`), and each failure is
131
+ logged once at the same per-kind level as HTTP, with `requestId`, JSON-RPC id and tool name.
132
+
133
+ | Where | Reply |
134
+ |---|---|
135
+ | `tools/call`, tool found: bad arguments, `@WpMcpAuthJwt` denial, JWT/OIDC mint failure, any local or remote failure, output-schema or serialization failure | `isError: true` result |
136
+ | `tools/call`, unknown tool name | JSON-RPC `-32602` `Unknown tool: <name>` |
137
+ | `tools/list` | JSON-RPC `-32602` (bad request) or `-32603` `Internal Error` |
138
+ | before the SDK: bearer, `Origin`, malformed body | HTTP 401 + `WWW-Authenticate` / 403 / 400, anything else 500 |
139
+
140
+ `subscriptions/listen` is served entirely by the SDK's listen router, so Webpieces has no handler there.
141
+
142
+ What the model reads inside an `isError` result: an `ApiEndUserError` message verbatim plus
143
+ `errorCode` (it survives the remote hop byte-for-byte); an `ApiBadRequestError`'s `callerMessage` and
144
+ `field`, never its operator `message`; for an implementation failure, generic text naming the tool and
145
+ requestId and saying it is a bug in the tool, not in the arguments; for every other kind, that kind's
146
+ generic message plus `retryAfterSeconds` where present.
147
+
148
+ Every reply carries the requestId so a user can quote it: `_meta["webpieces/requestId"]` on every
149
+ `tools/call` result (success or `isError`), `requestId` in the `isError` payload, and
150
+ `error.data.requestId` on JSON-RPC and HTTP-boundary errors.
151
+
152
+ A remote binding's generated Node client turns a dependency's 4xx into the gateway's own
153
+ `ApiImplementationError` (see `NodeProxyClient.adaptDownstreamFailure`). A gateway that wants the model
154
+ to see the peer's typed 4xx instead registers an `ErrorTranslators` via
155
+ `ClientRegistry.setErrorTranslators(...)` whose `fromWire` relays decoded Webpieces payloads; with it,
156
+ local and remote bindings produce identical tool results.
51
157
 
52
158
  The verifier's `accountValidatedAtEpochSeconds` must represent an authoritative enabled/revoked and
53
159
  role/scope read. The bridge enforces a maximum one-hour decision age; per-request reads are preferred so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/mcp-server",
3
- "version": "0.4.772",
3
+ "version": "0.4.774",
4
4
  "description": "Secure MCP tool generation from annotated Webpieces API contracts",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -22,11 +22,17 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@modelcontextprotocol/sdk": "1.29.0",
26
- "@webpieces/core-context": "0.4.772",
27
- "@webpieces/core-util": "0.4.772",
28
- "@webpieces/http-routing": "0.4.772",
25
+ "@modelcontextprotocol/node": "2.0.0",
26
+ "@modelcontextprotocol/server": "2.0.0",
27
+ "@webpieces/core-context": "0.4.774",
28
+ "@webpieces/core-util": "0.4.774",
29
+ "@webpieces/http-routing": "0.4.774",
30
+ "express": "5.1.0",
29
31
  "reflect-metadata": "0.2.2",
30
32
  "tslib": "2.8.1"
33
+ },
34
+ "devDependencies": {
35
+ "@webpieces/http-client-node": "0.4.774",
36
+ "@webpieces/http-server": "0.4.774"
31
37
  }
32
38
  }
@@ -0,0 +1,14 @@
1
+ import { DtoValue } from '@webpieces/core-util';
2
+ import { ApiFactory, ClassType } from '@webpieces/http-routing';
3
+ export type McpBindingTopology = 'local' | 'remote';
4
+ /** Explicit contract-to-invoker binding; MCP never discovers implementations from a router. */
5
+ export declare class McpApiBinding<TApi extends object = object> {
6
+ readonly api: ClassType<TApi>;
7
+ readonly topology: McpBindingTopology;
8
+ private readonly provider;
9
+ private constructor();
10
+ static local<TApi extends object>(api: ClassType<TApi>, apiFactory: ApiFactory): McpApiBinding<TApi>;
11
+ static remote<TApi extends object>(api: ClassType<TApi>, provider: () => TApi): McpApiBinding<TApi>;
12
+ invoke(methodName: string, request: DtoValue): Promise<DtoValue>;
13
+ validateMethod(methodName: string): void;
14
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.McpApiBinding = void 0;
4
+ const core_util_1 = require("@webpieces/core-util");
5
+ /** Explicit contract-to-invoker binding; MCP never discovers implementations from a router. */
6
+ class McpApiBinding {
7
+ api;
8
+ topology;
9
+ provider;
10
+ constructor(api, topology, provider) {
11
+ this.api = api;
12
+ this.topology = topology;
13
+ this.provider = provider;
14
+ }
15
+ // webpieces-disable no-function-outside-class -- explicit public factory mirrors the binding API
16
+ static local(api, apiFactory) {
17
+ return new McpApiBinding(api, 'local', () => apiFactory.createApiClient(api));
18
+ }
19
+ // webpieces-disable no-function-outside-class -- explicit public factory mirrors the binding API
20
+ static remote(api, provider) {
21
+ return new McpApiBinding(api, 'remote', provider);
22
+ }
23
+ invoke(methodName, request) {
24
+ const client = this.provider();
25
+ const method = client[methodName];
26
+ if (typeof method !== 'function') {
27
+ throw new Error(`MCP binding provider for ${this.api.name} has no method '${methodName}'.`);
28
+ }
29
+ return method.call(client, request);
30
+ }
31
+ validateMethod(methodName) {
32
+ const auth = (0, core_util_1.getAuthMeta)(this.api, methodName)?.mode;
33
+ const expected = this.topology === 'local' ? 'jwt' : 'oidc';
34
+ if (auth?.kind !== expected) {
35
+ throw new Error(`MCP ${this.topology} binding ${this.api.name}.${methodName} requires ` +
36
+ `@${expected === 'jwt' ? 'WpAuthJwt' : 'WpAuthOidc'}, but declares ` +
37
+ `${auth ? `'${auth.kind}'` : 'no HTTP auth'}.`);
38
+ }
39
+ }
40
+ }
41
+ exports.McpApiBinding = McpApiBinding;
42
+ //# sourceMappingURL=McpApiBinding.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpApiBinding.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiBinding.ts"],"names":[],"mappings":";;;AAAA,oDAA6D;AAK7D,+FAA+F;AAC/F,MAAa,aAAa;IAEF;IACA;IACC;IAHrB,YACoB,GAAoB,EACpB,QAA4B,EAC3B,QAAoB;QAFrB,QAAG,GAAH,GAAG,CAAiB;QACpB,aAAQ,GAAR,QAAQ,CAAoB;QAC3B,aAAQ,GAAR,QAAQ,CAAY;IACtC,CAAC;IAEJ,iGAAiG;IACjG,MAAM,CAAC,KAAK,CACR,GAAoB,EACpB,UAAsB;QAEtB,OAAO,IAAI,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC,GAAY,CAAC,CAAC,CAAC;IAC3F,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,CAAC,UAAkB,EAAE,OAAiB;QACxC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAoB,CAAC;QACjD,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;AA7CD,sCA6CC","sourcesContent":["import { DtoValue, getAuthMeta } from '@webpieces/core-util';\nimport { ApiClientProxy, ApiFactory, ClassType } 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: () => 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', () => apiFactory.createApiClient(api as never));\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(methodName: string, request: DtoValue): Promise<DtoValue> {\n const client = this.provider() 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,23 +1,14 @@
1
- import { ApiErrorPayload, DtoValue } from '@webpieces/core-util';
2
- import { ApiFactory } from '@webpieces/http-routing';
1
+ import { DtoValue } from '@webpieces/core-util';
3
2
  import { RegisteredMcpTool } from './McpToolRegistry';
4
- export declare class McpDispatchSuccess {
5
- readonly value: DtoValue;
6
- readonly requestId: string;
7
- readonly success = true;
8
- constructor(value: DtoValue, requestId: string);
9
- }
10
- export declare class McpDispatchFailure {
11
- readonly error: ApiErrorPayload;
12
- readonly requestId: string;
13
- readonly success = false;
14
- constructor(error: ApiErrorPayload, requestId: string);
15
- }
16
- export type McpDispatchResult = McpDispatchSuccess | McpDispatchFailure;
17
- /** Executes one tool through the same ApiFactory proxy/filter/controller path as HTTP. */
3
+ import { VerifiedMcpCredential } from './McpAuth';
4
+ 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
+ */
18
11
  export declare class McpApiDispatcher {
19
- private readonly apiFactory;
20
12
  private readonly schemaBuilder;
21
- constructor(apiFactory: ApiFactory);
22
- call(tool: RegisteredMcpTool, requestDto: DtoValue, endpointBearerToken: string): Promise<McpDispatchResult>;
13
+ call(tool: RegisteredMcpTool, requestDto: DtoValue, credential: VerifiedMcpCredential, invocation: McpInvocationContext, endpointBearerToken?: string): Promise<DtoValue>;
23
14
  }
@@ -1,56 +1,47 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.McpApiDispatcher = exports.McpDispatchFailure = exports.McpDispatchSuccess = void 0;
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
- class McpDispatchSuccess {
7
- value;
8
- requestId;
9
- success = true;
10
- constructor(value, requestId) {
11
- this.value = value;
12
- this.requestId = requestId;
13
- }
14
- }
15
- exports.McpDispatchSuccess = McpDispatchSuccess;
16
- class McpDispatchFailure {
17
- error;
18
- requestId;
19
- success = false;
20
- constructor(error, requestId) {
21
- this.error = error;
22
- this.requestId = requestId;
23
- }
24
- }
25
- exports.McpDispatchFailure = McpDispatchFailure;
26
- /** Executes one tool through the same ApiFactory proxy/filter/controller path as HTTP. */
6
+ 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
+ */
27
13
  class McpApiDispatcher {
28
- apiFactory;
29
14
  schemaBuilder = new core_util_1.DtoSchemaBuilder();
30
- constructor(apiFactory) {
31
- this.apiFactory = apiFactory;
32
- }
33
- async call(tool, requestDto, endpointBearerToken) {
34
- const headers = new Map();
35
- headers.set('authorization', [`Bearer ${endpointBearerToken}`]);
36
- const request = new core_context_1.HttpRequest('POST', `/__webpieces/mcp/${tool.name}`, headers);
37
- return core_context_1.RequestContext.run(async () => {
38
- new core_context_1.RequestContextHeaders().fillFromRequest(request);
39
- const requestId = core_context_1.RequestContext.getUntrusted(core_util_1.WebpiecesCoreHeaders.REQUEST_ID) ?? 'missing-request-id';
40
- const inputFailure = this.schemaBuilder.validate(tool.requestClass, requestDto);
41
- if (inputFailure) {
42
- return new McpDispatchFailure(core_util_1.ApiErrorCodec.encode(new core_util_1.ApiBadRequestError('MCP request DTO did not match its declared schema.', undefined, inputFailure.message)), requestId);
15
+ async call(tool, requestDto, credential, invocation, endpointBearerToken) {
16
+ const inputFailure = this.schemaBuilder.validate(tool.requestClass, requestDto);
17
+ if (inputFailure) {
18
+ throw new core_util_1.ApiBadRequestError(`MCP arguments for ${tool.name} did not match ${tool.requestClass.name}: ${inputFailure.message}`, inputFailure.field, inputFailure.message);
19
+ }
20
+ const required = (0, core_util_1.rolesRequired)(tool.mcpAuth.requirement);
21
+ if (required.length > 0 &&
22
+ !required.some((role) => credential.listingRoles.includes(role))) {
23
+ throw new core_util_1.ApiForbiddenError(`MCP principal ${credential.subject} lacks a role required by @WpMcpAuthJwt on ${tool.name}.`);
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);
43
29
  }
44
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary sanitizes endpoint throws
45
- try {
46
- const client = this.apiFactory.createApiClient(tool.apiClass);
47
- const value = (await client[tool.methodName](requestDto));
48
- return new McpDispatchSuccess(value, requestId);
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}`]);
49
36
  }
50
- catch (err) {
51
- const error = (0, core_util_1.toError)(err);
52
- return new McpDispatchFailure(core_util_1.ApiErrorCodec.encode(error), requestId);
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
+ if (tool.binding.topology === 'remote') {
41
+ for (const tuple of credential.trustedContext)
42
+ core_context_1.RequestContext.putTrusted(tuple.key, tuple.value);
53
43
  }
44
+ return tool.binding.invoke(tool.methodName, requestDto);
54
45
  });
55
46
  }
56
47
  }
@@ -1 +1 @@
1
- {"version":3,"file":"McpApiDispatcher.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiDispatcher.ts"],"names":[],"mappings":";;;AAAA,oDAQ8B;AAC9B,0DAA6F;AAI7F,MAAa,kBAAkB;IAEC;IAAiC;IADpD,OAAO,GAAG,IAAI,CAAC;IACxB,YAA4B,KAAe,EAAkB,SAAiB;QAAlD,UAAK,GAAL,KAAK,CAAU;QAAkB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;CACrF;AAHD,gDAGC;AAED,MAAa,kBAAkB;IAEC;IAAwC;IAD3D,OAAO,GAAG,KAAK,CAAC;IACzB,YAA4B,KAAsB,EAAkB,SAAiB;QAAzD,UAAK,GAAL,KAAK,CAAiB;QAAkB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;CAC5F;AAHD,gDAGC;AAID,0FAA0F;AAC1F,MAAa,gBAAgB;IAGI;IAFZ,aAAa,GAAG,IAAI,4BAAgB,EAAE,CAAC;IAExD,YAA6B,UAAsB;QAAtB,eAAU,GAAV,UAAU,CAAY;IAAG,CAAC;IAEvD,KAAK,CAAC,IAAI,CACN,IAAuB,EACvB,UAAoB,EACpB,mBAA2B;QAE3B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;QAC5C,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,UAAU,mBAAmB,EAAE,CAAC,CAAC,CAAC;QAChE,MAAM,OAAO,GAAG,IAAI,0BAAW,CAAC,MAAM,EAAE,oBAAoB,IAAI,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;QAClF,OAAO,6BAAc,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE;YACjC,IAAI,oCAAqB,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YACrD,MAAM,SAAS,GACX,6BAAc,CAAC,YAAY,CAAC,gCAAoB,CAAC,UAAU,CAAC,IAAI,oBAAoB,CAAC;YACzF,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;YAChF,IAAI,YAAY,EAAE,CAAC;gBACf,OAAO,IAAI,kBAAkB,CACzB,yBAAa,CAAC,MAAM,CAChB,IAAI,8BAAkB,CAClB,oDAAoD,EACpD,SAAS,EACT,YAAY,CAAC,OAAO,CACvB,CACJ,EACD,SAAS,CACZ,CAAC;YACN,CAAC;YACD,8GAA8G;YAC9G,IAAI,CAAC;gBACD,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,eAAe,CAAiB,IAAI,CAAC,QAAiB,CAAC,CAAC;gBACvF,MAAM,KAAK,GAAG,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,UAAU,CAAC,CAAa,CAAC;gBACtE,OAAO,IAAI,kBAAkB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YACpD,CAAC;YAAC,OAAO,GAAY,EAAE,CAAC;gBACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;gBAC3B,OAAO,IAAI,kBAAkB,CAAC,yBAAa,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,CAAC;YAC1E,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AAzCD,4CAyCC","sourcesContent":["import {\n ApiBadRequestError,\n ApiErrorCodec,\n ApiErrorPayload,\n DtoSchemaBuilder,\n DtoValue,\n toError,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { HttpRequest, RequestContext, RequestContextHeaders } from '@webpieces/core-context';\nimport { ApiFactory, ApiClientProxy } from '@webpieces/http-routing';\nimport { RegisteredMcpTool } from './McpToolRegistry';\n\nexport class McpDispatchSuccess {\n readonly success = true;\n constructor(public readonly value: DtoValue, public readonly requestId: string) {}\n}\n\nexport class McpDispatchFailure {\n readonly success = false;\n constructor(public readonly error: ApiErrorPayload, public readonly requestId: string) {}\n}\n\nexport type McpDispatchResult = McpDispatchSuccess | McpDispatchFailure;\n\n/** Executes one tool through the same ApiFactory proxy/filter/controller path as HTTP. */\nexport class McpApiDispatcher {\n private readonly schemaBuilder = new DtoSchemaBuilder();\n\n constructor(private readonly apiFactory: ApiFactory) {}\n\n async call(\n tool: RegisteredMcpTool,\n requestDto: DtoValue,\n endpointBearerToken: string,\n ): Promise<McpDispatchResult> {\n const headers = new Map<string, string[]>();\n headers.set('authorization', [`Bearer ${endpointBearerToken}`]);\n const request = new HttpRequest('POST', `/__webpieces/mcp/${tool.name}`, headers);\n return RequestContext.run(async () => {\n new RequestContextHeaders().fillFromRequest(request);\n const requestId =\n RequestContext.getUntrusted(WebpiecesCoreHeaders.REQUEST_ID) ?? 'missing-request-id';\n const inputFailure = this.schemaBuilder.validate(tool.requestClass, requestDto);\n if (inputFailure) {\n return new McpDispatchFailure(\n ApiErrorCodec.encode(\n new ApiBadRequestError(\n 'MCP request DTO did not match its declared schema.',\n undefined,\n inputFailure.message,\n ),\n ),\n requestId,\n );\n }\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary sanitizes endpoint throws\n try {\n const client = this.apiFactory.createApiClient<ApiClientProxy>(tool.apiClass as never);\n const value = (await client[tool.methodName](requestDto)) as DtoValue;\n return new McpDispatchSuccess(value, requestId);\n } catch (err: unknown) {\n const error = toError(err);\n return new McpDispatchFailure(ApiErrorCodec.encode(error), requestId);\n }\n });\n }\n}\n"]}
1
+ {"version":3,"file":"McpApiDispatcher.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiDispatcher.ts"],"names":[],"mappings":";;;AAAA,oDAQ8B;AAC9B,0DAA6F;AAG7F,iEAAsF;AAEtF;;;;;GAKG;AACH,MAAa,gBAAgB;IACR,aAAa,GAAG,IAAI,4BAAgB,EAAE,CAAC;IAExD,KAAK,CAAC,IAAI,CACN,IAAuB,EACvB,UAAoB,EACpB,UAAiC,EACjC,UAAgC,EAChC,mBAA4B;QAE5B,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QAChF,IAAI,YAAY,EAAE,CAAC;YACf,MAAM,IAAI,8BAAkB,CACxB,qBAAqB,IAAI,CAAC,IAAI,kBAAkB,IAAI,CAAC,YAAY,CAAC,IAAI,KAAK,YAAY,CAAC,OAAO,EAAE,EACjG,YAAY,CAAC,KAAK,EAClB,YAAY,CAAC,OAAO,CACvB,CAAC;QACN,CAAC;QACD,MAAM,QAAQ,GAAG,IAAA,yBAAa,EAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACzD,IACI,QAAQ,CAAC,MAAM,GAAG,CAAC;YACnB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAC1E,CAAC;YACC,MAAM,IAAI,6BAAiB,CACvB,iBAAiB,UAAU,CAAC,OAAO,8CAA8C,IAAI,CAAC,IAAI,GAAG,CAChG,CAAC;QACN,CAAC;QACD,MAAM,eAAe,GAAG,6BAAc,CAAC,YAAY,CAAC,gCAAoB,CAAC,UAAU,CAAC,CAAC;QACrF,OAAO,6BAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,EAAE;YAC9C,IAAI,eAAe,EAAE,CAAC;gBAClB,6BAAc,CAAC,YAAY,CAAC,gCAAoB,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;YAClF,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;YAC5C,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;gBACpC,IAAI,CAAC,mBAAmB,EAAE,CAAC;oBACvB,MAAM,IAAI,kCAAsB,CAC5B,kBAAkB,IAAI,CAAC,IAAI,uBAAuB,CACrD,CAAC;gBACN,CAAC;gBACD,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,UAAU,mBAAmB,EAAE,CAAC,CAAC,CAAC;YACpE,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,0BAAW,CAAC,MAAM,EAAE,oBAAoB,IAAI,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;YAClF,IAAI,oCAAqB,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YACrD,6BAAc,CAAC,UAAU,CAAC,6CAAsB,EAAE,UAAU,CAAC,CAAC;YAC9D,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACrC,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,cAAc;oBACzC,6BAAc,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;YAC1D,CAAC;YACD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC5D,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AAnDD,4CAmDC","sourcesContent":["import {\n ApiBadRequestError,\n ApiForbiddenError,\n ApiImplementationError,\n DtoSchemaBuilder,\n DtoValue,\n rolesRequired,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { HttpRequest, RequestContext, RequestContextHeaders } from '@webpieces/core-context';\nimport { RegisteredMcpTool } from './McpToolRegistry';\nimport { VerifiedMcpCredential } from './McpAuth';\nimport { MCP_INVOCATION_CONTEXT, McpInvocationContext } from './McpInvocationContext';\n\n/**\n * Executes one tool call through the explicit local or remote Webpieces client, never a controller\n * reference. It has NO try/catch: every failure (bad arguments, MCP authorization, anything the local\n * proxy or the remote generated client throws) propagates as a typed error to the single tools/call\n * boundary in `WpMcpServer`, so local and remote bindings are translated identically.\n */\nexport class McpApiDispatcher {\n private readonly schemaBuilder = new DtoSchemaBuilder();\n\n async call(\n tool: RegisteredMcpTool,\n requestDto: DtoValue,\n credential: VerifiedMcpCredential,\n invocation: McpInvocationContext,\n endpointBearerToken?: string,\n ): Promise<DtoValue> {\n const inputFailure = this.schemaBuilder.validate(tool.requestClass, requestDto);\n if (inputFailure) {\n throw new ApiBadRequestError(\n `MCP arguments for ${tool.name} did not match ${tool.requestClass.name}: ${inputFailure.message}`,\n inputFailure.field,\n inputFailure.message,\n );\n }\n const required = rolesRequired(tool.mcpAuth.requirement);\n if (\n required.length > 0 &&\n !required.some((role: string) => credential.listingRoles.includes(role))\n ) {\n throw new ApiForbiddenError(\n `MCP principal ${credential.subject} lacks a role required by @WpMcpAuthJwt on ${tool.name}.`,\n );\n }\n const parentRequestId = RequestContext.getUntrusted(WebpiecesCoreHeaders.REQUEST_ID);\n return RequestContext.runDetachedScope(async () => {\n if (parentRequestId) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID, parentRequestId);\n }\n const headers = new Map<string, string[]>();\n if (tool.binding.topology === 'local') {\n if (!endpointBearerToken) {\n throw new ApiImplementationError(\n `Local MCP tool ${tool.name} has no endpoint JWT.`,\n );\n }\n headers.set('authorization', [`Bearer ${endpointBearerToken}`]);\n }\n const request = new HttpRequest('POST', `/__webpieces/mcp/${tool.name}`, headers);\n new RequestContextHeaders().fillFromRequest(request);\n RequestContext.putTrusted(MCP_INVOCATION_CONTEXT, invocation);\n if (tool.binding.topology === 'remote') {\n for (const tuple of credential.trustedContext)\n RequestContext.putTrusted(tuple.key, tuple.value);\n }\n return tool.binding.invoke(tool.methodName, requestDto);\n });\n }\n}\n"]}
package/src/McpAuth.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { JwtHook } from '@webpieces/http-routing';
2
+ import { ContextTuple } from '@webpieces/core-util';
2
3
  export declare const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS: number;
3
4
  export declare const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS: number;
4
5
  export declare const MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS: number;
@@ -26,13 +27,21 @@ export declare class VerifiedMcpCredential {
26
27
  readonly accountValidatedAtEpochSeconds: number;
27
28
  /** Advisory tools/list filtering only. Endpoint authorization always runs again. */
28
29
  readonly listingRoles: readonly string[];
30
+ /** Trusted delegated identity derived only by the access-token authority. */
31
+ readonly trustedContext: readonly ContextTuple[];
29
32
  constructor(subject: string, issuer: string, resource: string, issuedAtEpochSeconds: number, expiresAtEpochSeconds: number, scopes: readonly string[],
30
33
  /** When current enabled/revoked state and roles were read from the authoritative source. */
31
34
  accountValidatedAtEpochSeconds: number,
32
35
  /** Advisory tools/list filtering only. Endpoint authorization always runs again. */
33
- listingRoles?: readonly string[]);
36
+ listingRoles?: readonly string[],
37
+ /** Trusted delegated identity derived only by the access-token authority. */
38
+ trustedContext?: readonly ContextTuple[]);
34
39
  }
35
- /** Application-owned paired mint/verify authority for resource-bound MCP access tokens. */
40
+ /**
41
+ * Application-owned paired mint/verify authority for resource-bound MCP access tokens.
42
+ * `verifyAccessToken` must throw `ApiUnauthorizedError` for a token it rejects (the bind boundary
43
+ * answers 401 + `WWW-Authenticate`); any other throw is treated as an implementation failure (500).
44
+ */
36
45
  export interface McpAccessTokenAuthority<TGrant> {
37
46
  mintAccessToken(grant: TGrant): Promise<MintedMcpAccessToken>;
38
47
  verifyAccessToken(accessToken: string, expectedResource: string): Promise<VerifiedMcpCredential>;
package/src/McpAuth.js CHANGED
@@ -44,11 +44,14 @@ class VerifiedMcpCredential {
44
44
  scopes;
45
45
  accountValidatedAtEpochSeconds;
46
46
  listingRoles;
47
+ trustedContext;
47
48
  constructor(subject, issuer, resource, issuedAtEpochSeconds, expiresAtEpochSeconds, scopes,
48
49
  /** When current enabled/revoked state and roles were read from the authoritative source. */
49
50
  accountValidatedAtEpochSeconds,
50
51
  /** Advisory tools/list filtering only. Endpoint authorization always runs again. */
51
- listingRoles = []) {
52
+ listingRoles = [],
53
+ /** Trusted delegated identity derived only by the access-token authority. */
54
+ trustedContext = []) {
52
55
  this.subject = subject;
53
56
  this.issuer = issuer;
54
57
  this.resource = resource;
@@ -57,6 +60,7 @@ class VerifiedMcpCredential {
57
60
  this.scopes = scopes;
58
61
  this.accountValidatedAtEpochSeconds = accountValidatedAtEpochSeconds;
59
62
  this.listingRoles = listingRoles;
63
+ this.trustedContext = trustedContext;
60
64
  }
61
65
  }
62
66
  exports.VerifiedMcpCredential = VerifiedMcpCredential;
@@ -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';\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('Minted MCP access token timestamps must be finite epoch-second values.');\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 ) {}\n}\n\n/** Application-owned paired mint/verify authority for resource-bound MCP access tokens. */\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. Pass the same concrete application authority as both\n * accessTokenAuthority and endpointJwtAuthority when it implements both security seams.\n */\nexport class WpMcpServerConfig<TGrant, TMintRequest> {\n constructor(\n public readonly name: string,\n public readonly version: string,\n public readonly resource: string,\n public readonly accessTokenAuthority: McpAccessTokenAuthority<TGrant>,\n public readonly endpointJwtAuthority: JwtHook<TMintRequest>,\n public readonly endpointMintRequest: McpEndpointMintRequestFactory<TMintRequest>,\n public readonly authorizationServers: readonly string[],\n public readonly requiredScopes: readonly string[],\n public readonly maxAccountValidationAgeSeconds = MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS,\n public readonly maxEndpointJwtLifetimeSeconds = MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS,\n ) {\n if (name.trim() === '' || version.trim() === '' || resource.trim() === '') {\n throw new Error('MCP name, version, and resource must be non-empty.');\n }\n if (\n maxAccountValidationAgeSeconds <= 0 ||\n maxAccountValidationAgeSeconds > MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS\n ) {\n throw new Error('MCP account validation cache ceiling must be between 1 second and 1 hour.');\n }\n if (\n maxEndpointJwtLifetimeSeconds <= 0 ||\n maxEndpointJwtLifetimeSeconds > MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS\n ) {\n throw new Error('MCP endpoint JWT lifetime must be between 1 second and 1 hour.');\n }\n }\n\n protectedResourceMetadata(): McpProtectedResourceMetadata {\n return new McpProtectedResourceMetadata(\n this.resource,\n this.authorizationServers,\n this.requiredScopes,\n );\n }\n}\n"]}
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. Pass the same concrete application authority as both\n * accessTokenAuthority and endpointJwtAuthority when it implements both security seams.\n */\nexport class WpMcpServerConfig<TGrant, TMintRequest> {\n constructor(\n public readonly name: string,\n public readonly version: string,\n public readonly resource: string,\n public readonly accessTokenAuthority: McpAccessTokenAuthority<TGrant>,\n public readonly endpointJwtAuthority: JwtHook<TMintRequest>,\n public readonly endpointMintRequest: McpEndpointMintRequestFactory<TMintRequest>,\n public readonly authorizationServers: readonly string[],\n public readonly requiredScopes: readonly string[],\n public readonly maxAccountValidationAgeSeconds = MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS,\n public readonly maxEndpointJwtLifetimeSeconds = MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS,\n ) {\n if (name.trim() === '' || version.trim() === '' || resource.trim() === '') {\n throw new Error('MCP name, version, and resource must be non-empty.');\n }\n if (\n maxAccountValidationAgeSeconds <= 0 ||\n maxAccountValidationAgeSeconds > MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS\n ) {\n throw new Error(\n 'MCP account validation cache ceiling must be between 1 second and 1 hour.',\n );\n }\n if (\n maxEndpointJwtLifetimeSeconds <= 0 ||\n maxEndpointJwtLifetimeSeconds > MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS\n ) {\n throw new Error('MCP endpoint JWT lifetime must be between 1 second and 1 hour.');\n }\n }\n\n protectedResourceMetadata(): McpProtectedResourceMetadata {\n return new McpProtectedResourceMetadata(\n this.resource,\n this.authorizationServers,\n this.requiredScopes,\n );\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"McpAuthorityCompileAssertions.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuthorityCompileAssertions.ts"],"names":[],"mappings":";;;AAAA,0DAIiC;AACjC,uCAImB;AAEnB,MAAM,qBAAqB;IACK;IAA5B,YAA4B,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;CAClD;AAED,MAAM,eAAe;IACW;IAAiC;IAA7D,YAA4B,OAAe,EAAkB,QAAgB;QAAjD,YAAO,GAAP,OAAO,CAAQ;QAAkB,aAAQ,GAAR,QAAQ,CAAQ;IAAG,CAAC;CACpF;AAED,uGAAuG;AACvG,MAAa,6BAA8B,SAAQ,sBAA8B;IAGpE,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,CACN,CAAC;IACN,CAAC;CACJ;AA/BD,sEA+BC","sourcesContent":["import {\n AuthenticatedCaller,\n JwtHook,\n MintedJwt,\n} from '@webpieces/http-routing';\nimport {\n McpAccessTokenAuthority,\n MintedMcpAccessToken,\n VerifiedMcpCredential,\n} from './McpAuth';\n\nclass ExampleJwtMintRequest {\n constructor(public readonly subject: string) {}\n}\n\nclass ExampleMcpGrant {\n constructor(public readonly subject: string, public readonly resource: string) {}\n}\n\n/** Compile assertion: one application authority can own both endpoint JWT and MCP token lifecycles. */\nexport class McpAuthorityCompileAssertions 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 );\n }\n}\n"]}
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,CACN,CAAC;IACN,CAAC;CACJ;AAhCD,sEAgCC","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 );\n }\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import { McpApiBinding } from './McpApiBinding';
2
+ import { McpDeployment } from './McpDeployment';
3
+ export declare class McpBindOptions {
4
+ /** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */
5
+ readonly endpointPath: string;
6
+ readonly bindings: readonly McpApiBinding[];
7
+ readonly deployment: McpDeployment;
8
+ /** Requests without Origin remain valid for non-browser MCP clients. */
9
+ readonly allowedOrigins: readonly string[];
10
+ readonly maxSubscriptions: number;
11
+ readonly keepAliveMs: number;
12
+ constructor(
13
+ /** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */
14
+ endpointPath: string, bindings: readonly McpApiBinding[], deployment: McpDeployment,
15
+ /** Requests without Origin remain valid for non-browser MCP clients. */
16
+ allowedOrigins?: readonly string[], maxSubscriptions?: number, keepAliveMs?: number);
17
+ }
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.McpBindOptions = void 0;
4
+ class McpBindOptions {
5
+ endpointPath;
6
+ bindings;
7
+ deployment;
8
+ allowedOrigins;
9
+ maxSubscriptions;
10
+ keepAliveMs;
11
+ constructor(
12
+ /** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */
13
+ endpointPath, bindings, deployment,
14
+ /** Requests without Origin remain valid for non-browser MCP clients. */
15
+ allowedOrigins = [], maxSubscriptions = 1024, keepAliveMs = 15_000) {
16
+ this.endpointPath = endpointPath;
17
+ this.bindings = bindings;
18
+ this.deployment = deployment;
19
+ this.allowedOrigins = allowedOrigins;
20
+ this.maxSubscriptions = maxSubscriptions;
21
+ this.keepAliveMs = keepAliveMs;
22
+ if (!endpointPath.startsWith('/') || endpointPath.includes('?')) {
23
+ throw new Error('MCP endpointPath must be an absolute path without a query string.');
24
+ }
25
+ if (bindings.length === 0)
26
+ throw new Error('MCP binding requires at least one API contract.');
27
+ if (!Number.isInteger(maxSubscriptions) || maxSubscriptions <= 0) {
28
+ throw new Error('MCP maxSubscriptions must be a positive integer.');
29
+ }
30
+ if (!Number.isFinite(keepAliveMs) || keepAliveMs < 0) {
31
+ throw new Error('MCP keepAliveMs must be zero or a positive number.');
32
+ }
33
+ }
34
+ }
35
+ exports.McpBindOptions = McpBindOptions;
36
+ //# sourceMappingURL=McpBindOptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpBindOptions.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpBindOptions.ts"],"names":[],"mappings":";;;AAGA,MAAa,cAAc;IAGH;IACA;IACA;IAEA;IACA;IACA;IARpB;IACI,oFAAoF;IACpE,YAAoB,EACpB,QAAkC,EAClC,UAAyB;IACzC,wEAAwE;IACxD,iBAAoC,EAAE,EACtC,mBAAmB,IAAI,EACvB,cAAc,MAAM;QANpB,iBAAY,GAAZ,YAAY,CAAQ;QACpB,aAAQ,GAAR,QAAQ,CAA0B;QAClC,eAAU,GAAV,UAAU,CAAe;QAEzB,mBAAc,GAAd,cAAc,CAAwB;QACtC,qBAAgB,GAAhB,gBAAgB,CAAO;QACvB,gBAAW,GAAX,WAAW,CAAS;QAEpC,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9D,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;QACzF,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YACrB,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;QACvE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,IAAI,gBAAgB,IAAI,CAAC,EAAE,CAAC;YAC/D,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QACxE,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACnD,MAAM,IAAI,KAAK,CAAC,oDAAoD,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC;CACJ;AAvBD,wCAuBC","sourcesContent":["import { McpApiBinding } from './McpApiBinding';\nimport { McpDeployment } from './McpDeployment';\n\nexport class McpBindOptions {\n constructor(\n /** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */\n public readonly endpointPath: string,\n public readonly bindings: readonly McpApiBinding[],\n public readonly deployment: McpDeployment,\n /** Requests without Origin remain valid for non-browser MCP clients. */\n public readonly allowedOrigins: readonly string[] = [],\n public readonly maxSubscriptions = 1024,\n public readonly keepAliveMs = 15_000,\n ) {\n if (!endpointPath.startsWith('/') || endpointPath.includes('?')) {\n throw new Error('MCP endpointPath must be an absolute path without a query string.');\n }\n if (bindings.length === 0)\n throw new Error('MCP binding requires at least one API contract.');\n if (!Number.isInteger(maxSubscriptions) || maxSubscriptions <= 0) {\n throw new Error('MCP maxSubscriptions must be a positive integer.');\n }\n if (!Number.isFinite(keepAliveMs) || keepAliveMs < 0) {\n throw new Error('MCP keepAliveMs must be zero or a positive number.');\n }\n }\n}\n"]}
@@ -0,0 +1,11 @@
1
+ import { ServerEventBus } from '@modelcontextprotocol/server';
2
+ export type McpDeploymentMode = 'single-process' | 'distributed';
3
+ /** Makes process topology and bounded list-cache behavior explicit at construction. */
4
+ export declare class McpDeployment {
5
+ readonly mode: McpDeploymentMode;
6
+ readonly bus: ServerEventBus;
7
+ readonly ttlMs: number;
8
+ private constructor();
9
+ static singleProcess(ttlMs?: number): McpDeployment;
10
+ static distributed(bus: ServerEventBus, ttlMs?: number): McpDeployment;
11
+ }