@webpieces/mcp-server 0.4.773 → 0.4.775
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 +83 -8
- package/package.json +11 -5
- package/src/McpApiBinding.d.ts +14 -0
- package/src/McpApiBinding.js +42 -0
- package/src/McpApiBinding.js.map +1 -0
- package/src/McpApiDispatcher.d.ts +10 -19
- package/src/McpApiDispatcher.js +35 -44
- package/src/McpApiDispatcher.js.map +1 -1
- package/src/McpAuth.d.ts +11 -2
- package/src/McpAuth.js +5 -1
- package/src/McpAuth.js.map +1 -1
- package/src/McpAuthorityCompileAssertions.js.map +1 -1
- package/src/McpBindOptions.d.ts +17 -0
- package/src/McpBindOptions.js +36 -0
- package/src/McpBindOptions.js.map +1 -0
- package/src/McpDeployment.d.ts +11 -0
- package/src/McpDeployment.js +32 -0
- package/src/McpDeployment.js.map +1 -0
- package/src/McpInvocationContext.d.ts +14 -0
- package/src/McpInvocationContext.js +25 -0
- package/src/McpInvocationContext.js.map +1 -0
- package/src/McpToolRegistry.d.ts +7 -4
- package/src/McpToolRegistry.js +20 -16
- package/src/McpToolRegistry.js.map +1 -1
- package/src/WpMcpErrorTranslator.d.ts +69 -0
- package/src/WpMcpErrorTranslator.js +168 -0
- package/src/WpMcpErrorTranslator.js.map +1 -0
- package/src/WpMcpServer.d.ts +55 -24
- package/src/WpMcpServer.js +331 -154
- package/src/WpMcpServer.js.map +1 -1
- package/src/__tests__/McpHttpTestHarness.d.ts +40 -0
- package/src/__tests__/McpHttpTestHarness.js +158 -0
- package/src/__tests__/McpHttpTestHarness.js.map +1 -0
- package/src/__tests__/McpRemoteFixtures.d.ts +93 -0
- package/src/__tests__/McpRemoteFixtures.js +409 -0
- package/src/__tests__/McpRemoteFixtures.js.map +1 -0
- package/src/__tests__/WpMcpServerTestFixtures.d.ts +60 -0
- package/src/__tests__/WpMcpServerTestFixtures.js +269 -0
- package/src/__tests__/WpMcpServerTestFixtures.js.map +1 -0
- package/src/index.d.ts +10 -3
- package/src/index.js +18 -4
- 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
|
|
8
|
-
|
|
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)
|
|
@@ -73,12 +75,85 @@ A map is still a closed schema: no key can carry an unspecified value. Check clo
|
|
|
73
75
|
`new DtoSchemaBuilder().isClosedSchema(schema)`, not `schema.additionalProperties === false`, which wrongly
|
|
74
76
|
rejects typed maps.
|
|
75
77
|
|
|
76
|
-
Applications
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
82
157
|
|
|
83
158
|
The verifier's `accountValidatedAtEpochSeconds` must represent an authoritative enabled/revoked and
|
|
84
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.
|
|
3
|
+
"version": "0.4.775",
|
|
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/
|
|
26
|
-
"@
|
|
27
|
-
"@webpieces/core-
|
|
28
|
-
"@webpieces/
|
|
25
|
+
"@modelcontextprotocol/node": "2.0.0",
|
|
26
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
27
|
+
"@webpieces/core-context": "0.4.775",
|
|
28
|
+
"@webpieces/core-util": "0.4.775",
|
|
29
|
+
"@webpieces/http-routing": "0.4.775",
|
|
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.775",
|
|
36
|
+
"@webpieces/http-server": "0.4.775"
|
|
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 {
|
|
2
|
-
import { ApiFactory } from '@webpieces/http-routing';
|
|
1
|
+
import { DtoValue } from '@webpieces/core-util';
|
|
3
2
|
import { RegisteredMcpTool } from './McpToolRegistry';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/McpApiDispatcher.js
CHANGED
|
@@ -1,56 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.McpApiDispatcher =
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
new
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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;
|
|
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
|
-
/**
|
|
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;
|
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":";;;
|
|
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,
|
|
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
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpDeployment = void 0;
|
|
4
|
+
const server_1 = require("@modelcontextprotocol/server");
|
|
5
|
+
/** Makes process topology and bounded list-cache behavior explicit at construction. */
|
|
6
|
+
class McpDeployment {
|
|
7
|
+
mode;
|
|
8
|
+
bus;
|
|
9
|
+
ttlMs;
|
|
10
|
+
constructor(mode, bus, ttlMs) {
|
|
11
|
+
this.mode = mode;
|
|
12
|
+
this.bus = bus;
|
|
13
|
+
this.ttlMs = ttlMs;
|
|
14
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0)
|
|
15
|
+
throw new Error('MCP list cache ttlMs must be positive.');
|
|
16
|
+
}
|
|
17
|
+
// webpieces-disable no-function-outside-class -- explicit deployment factory
|
|
18
|
+
static singleProcess(ttlMs = 30_000) {
|
|
19
|
+
return new McpDeployment('single-process', new server_1.InMemoryServerEventBus(), ttlMs);
|
|
20
|
+
}
|
|
21
|
+
// webpieces-disable no-function-outside-class -- explicit deployment factory
|
|
22
|
+
static distributed(bus, ttlMs = 30_000) {
|
|
23
|
+
if (!bus)
|
|
24
|
+
throw new Error('Distributed MCP deployment requires a shared ServerEventBus.');
|
|
25
|
+
if (bus instanceof server_1.InMemoryServerEventBus) {
|
|
26
|
+
throw new Error('Distributed MCP deployment cannot use InMemoryServerEventBus; supply a cross-instance bus.');
|
|
27
|
+
}
|
|
28
|
+
return new McpDeployment('distributed', bus, ttlMs);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.McpDeployment = McpDeployment;
|
|
32
|
+
//# sourceMappingURL=McpDeployment.js.map
|