@webpieces/mcp-server 0.4.805 → 0.4.807

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 CHANGED
@@ -1,83 +1,91 @@
1
1
  # @webpieces/mcp-server
2
2
 
3
- Publishes explicitly annotated Webpieces RPC endpoints as MCP tools. The bridge generates input and
4
- output JSON Schema from DTO metadata, invokes the normal Webpieces filter/controller path, and turns
5
- exceptions into safe model-visible MCP results.
3
+ Publishes explicitly annotated Webpieces RPC endpoints as MCP tools. The bridge is HANDED the input
4
+ and output JSON Schema — the build extracts them from the contract source and writes `mcp-tools.json`
5
+ — invokes the normal Webpieces filter/controller path, and turns exceptions into safe model-visible
6
+ MCP results.
6
7
 
7
8
  `@WpMcpTool` is an opt-in and every tool must also declare `@WpMcpAuthJwt`. MCP user
8
9
  authorization and endpoint transport authentication are deliberately separate. The MCP adapter
9
10
  never accepts trusted context values from tool arguments or unverified headers.
10
11
 
11
12
  ```ts
12
- @WpDto()
13
- class FindOrderRequest {
14
- @WpDtoField(new WpDtoFieldOptions('Order identifier', true))
15
- orderId!: string;
13
+ interface FindOrderRequest {
14
+ /** Order identifier */
15
+ orderId: string;
16
16
  }
17
17
 
18
- @WpDto()
19
- class FindOrderResponse {
20
- @WpDtoField(new WpDtoFieldOptions('Current order state', true))
21
- state!: string;
18
+ interface FindOrderResponse {
19
+ /** Current order state */
20
+ state: string;
22
21
  }
23
22
 
24
23
  @ApiPath('/orders')
24
+ @ApiType(SVC_TO_SVC, MCP)
25
25
  abstract class OrdersApi {
26
+ /** Find one order owned by the signed-in user. */
26
27
  @WpMcpAuthJwt({ allRolesAllowed: true })
27
28
  @WpAuthJwt({ allRolesAllowed: true })
28
29
  @Endpoint(POST, '/find', READ, RPC)
29
- @WpResponseDto(() => FindOrderResponse)
30
- @WpMcpTool({
31
- name: 'orders_find',
32
- description: 'Find one order owned by the signed-in user.',
33
- openWorldHint: false,
34
- })
30
+ @WpMcpTool('orders_find')
35
31
  find(request: FindOrderRequest): Promise<FindOrderResponse> {
36
32
  throw new Error('contract only');
37
33
  }
38
34
  }
39
35
  ```
40
36
 
41
- The `@WpMcpTool.description` becomes the tool description returned by `tools/list`. The request and
42
- response classes generate `inputSchema` and `outputSchema`; `@WpDtoField` supplies property
43
- descriptions and the facts TypeScript erases, such as optionality and array element types.
37
+ ## Everything but the tool NAME comes from the source
44
38
 
45
- Every `@Endpoint` declares one enum-backed operation: `READ`, `WRITE_IDEMPOTENT`, or `WRITE`.
46
- This is independent of its required `GET` or `POST` argument: a GET may deliberately write and a
47
- POST may be a read. MCP derives
48
- `readOnlyHint`, `idempotentHint`, and `destructiveHint` from that one declaration; `@WpMcpTool`
49
- owns only the stable name, description, and `openWorldHint`.
39
+ `@WpMcpTool` takes one argument, the stable protocol name, because that is the only fact the source
40
+ cannot state. Everything else is read off the contract by `@webpieces/api-doc-model` at BUILD time:
50
41
 
51
- ### Typed maps
42
+ | fact | source |
43
+ |---|---|
44
+ | `description` | the method's JSDoc body, or its `@mcp` tag — the same words the partner reads in OpenAPI |
45
+ | `inputSchema` / `outputSchema` | the declared request and response types, with each field's JSDoc as its description |
46
+ | `readOnlyHint` / `destructiveHint` / `idempotentHint` | `@Endpoint`'s `operation` — `READ`, `WRITE_IDEMPOTENT` or `WRITE` |
47
+ | `openWorldHint` | `@Endpoint`'s `openWorld` option |
48
+ | `x-mcp-header` | the field's `@mcpHeader <token>` JSDoc tag |
52
49
 
53
- A `Record<string, V>` reflects as plain `Object`, so its value type must be declared — the same reason
54
- `arrayItems` exists for arrays. Use `WpDtoMapFieldOptions(description, required, mapValues)`, where
55
- `mapValues` is `'string' | 'number' | 'integer' | 'boolean'` or a `@WpDto` class:
50
+ Every `@Endpoint` declares one enum-backed operation, independent of its required `GET` or `POST`
51
+ argument: a GET may deliberately write and a POST may be a read. A tool therefore cannot claim to be
52
+ read-only while its endpoint declares `WRITE` — the contradiction is unrepresentable.
56
53
 
57
- ```ts
58
- @WpDto()
59
- class PassageSentenceItem {
60
- @WpDtoField(new WpDtoFieldOptions('Sentence text', true))
61
- text!: string;
54
+ ## Booting: the server is constructed with the generated catalog
62
55
 
63
- @WpDtoField(new WpDtoMapFieldOptions('ISO 639-1 -> sentence', false, 'string'))
64
- translations?: Record<string, string>;
65
- }
56
+ ```bash
57
+ wp-openapi --manifest openapi.manifest.json --out dist
58
+ ```
66
59
 
67
- @WpDto()
68
- class PassageResponse {
69
- @WpDtoField(new WpDtoMapFieldOptions('Sentences by locale', true, PassageSentenceItem))
70
- sentencesByLocale!: Record<string, PassageSentenceItem>;
71
- }
60
+ writes `mcp-tools.json` beside the OpenAPI documents. Hand it to `McpBindOptions`:
61
+
62
+ ```ts
63
+ const catalog = McpToolCatalog.fromJsonText(fs.readFileSync('dist/mcp-tools.json', 'utf8'));
64
+ server.bind(app, new McpBindOptions(path, bindings, catalog, McpDeployment.singleProcess()));
72
65
  ```
73
66
 
74
- The field emits `{ type: 'object', additionalProperties: <value schema> }`, and validation checks every
75
- value (`$.translations.es must be string`). An `Object`-typed field without map options — an interface or
76
- an undeclared `Record` — fails at startup, because an interface can never carry `@WpDto`.
67
+ `McpToolRegistry` FAILS FAST at boot when a registered `@WpMcpTool` is absent from the catalog: a tool
68
+ the build never saw is a tool whose schema nobody checked. Tool arguments and structured output are
69
+ validated against those same schemas by `ApiJsonSchemaValidator`, so the shape an agent is shown and
70
+ the shape the server accepts are the same bytes rather than two derivations of one contract.
71
+
72
+ Before #984 the schemas were instead built at boot from `@WpDtoField` reflect-metadata. That spelling,
73
+ `@WpDto`, `WpDtoFieldOptions`, `WpDtoMapFieldOptions`, `WpMcpHeader`, `@WpResponseDto` and
74
+ `DtoSchemaBuilder` are all DELETED — see the migration note in `responsibilities.md`.
75
+
76
+ ### Typed maps
77
+
78
+ A `Record<string, V>` is read straight off the declared type and emits
79
+ `{ type: 'object', additionalProperties: <value schema> }`. Validation checks every value
80
+ (`$.translations.es must be a string`). A map is still a CLOSED schema: no key can carry an unspecified
81
+ value, and `ApiJsonSchema.isClosed(schema)` answers that for both forms — use it rather than
82
+ `schema.additionalProperties === false`, which wrongly rejects typed maps.
83
+
84
+ ### Nullable
77
85
 
78
- A map is still a closed schema: no key can carry an unspecified value. Check closure with
79
- `new DtoSchemaBuilder().isClosedSchema(schema)`, not `schema.additionalProperties === false`, which wrongly
80
- rejects typed maps.
86
+ `externalId: string | null` publishes as `type: ["string", "null"]`, which is a different wire document
87
+ from omitting the key. OPTIONAL (`externalId?: string`) is the absence of the field from `required`.
88
+ The two are kept apart.
81
89
 
82
90
  ## Configuration
83
91
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/mcp-server",
3
- "version": "0.4.805",
3
+ "version": "0.4.807",
4
4
  "description": "Secure MCP tool generation from annotated Webpieces API contracts",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -24,15 +24,15 @@
24
24
  "dependencies": {
25
25
  "@modelcontextprotocol/node": "2.0.0",
26
26
  "@modelcontextprotocol/server": "2.0.0",
27
- "@webpieces/core-context": "0.4.805",
28
- "@webpieces/core-util": "0.4.805",
29
- "@webpieces/http-routing": "0.4.805",
30
- "@webpieces/http-server": "0.4.805",
27
+ "@webpieces/core-context": "0.4.807",
28
+ "@webpieces/core-util": "0.4.807",
29
+ "@webpieces/http-routing": "0.4.807",
30
+ "@webpieces/http-server": "0.4.807",
31
31
  "express": "5.1.0",
32
32
  "reflect-metadata": "0.2.2",
33
33
  "tslib": "2.8.1"
34
34
  },
35
35
  "devDependencies": {
36
- "@webpieces/http-client-node": "0.4.805"
36
+ "@webpieces/http-client-node": "0.4.807"
37
37
  }
38
38
  }
@@ -9,6 +9,6 @@ import { McpInvocationContext } from './McpInvocationContext';
9
9
  * boundary in `WpMcpServer`, so local and remote bindings are translated identically.
10
10
  */
11
11
  export declare class McpApiDispatcher {
12
- private readonly schemaBuilder;
12
+ private readonly schemas;
13
13
  call(tool: RegisteredMcpTool, requestDto: DtoValue, credential: VerifiedMcpCredential, invocation: McpInvocationContext, endpointBearerToken?: string): Promise<DtoValue>;
14
14
  }
@@ -11,11 +11,11 @@ const McpInvocationContext_1 = require("./McpInvocationContext");
11
11
  * boundary in `WpMcpServer`, so local and remote bindings are translated identically.
12
12
  */
13
13
  class McpApiDispatcher {
14
- schemaBuilder = new core_util_1.DtoSchemaBuilder();
14
+ schemas = new core_util_1.ApiJsonSchemaValidator();
15
15
  async call(tool, requestDto, credential, invocation, endpointBearerToken) {
16
- const inputFailure = this.schemaBuilder.validate(tool.requestClass, requestDto);
16
+ const inputFailure = this.schemas.validate(tool.inputSchema, requestDto);
17
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);
18
+ throw new core_util_1.ApiBadRequestError(`MCP arguments for ${tool.name} did not match its published input schema: ${inputFailure.message}`, inputFailure.field, inputFailure.message);
19
19
  }
20
20
  const required = (0, core_util_1.rolesRequired)(tool.mcpAuth.requirement);
21
21
  if (required.length > 0 &&
@@ -1 +1 @@
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,+EAA+E;YAC/E,qFAAqF;YACrF,iFAAiF;YACjF,uFAAuF;YACvF,mFAAmF;YACnF,6BAAc,CAAC,UAAU,CAAC,gCAAoB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC/D,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;AAzDD,4CAyDC","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 // THIS hop is the edge, and its caller is a model. Stamped here rather than in\n // `AuthFilter` because the local binding's HTTP auth mode describes the bridge's own\n // endpoint JWT, not the LLM on the other end of it — `@WpMcpAuthJwt` is separate\n // metadata and never reaches an `AuthMode`. `AuthFilter` only ever SETS a surface when\n // none arrived, so this one survives the filter chain the invocation runs through.\n RequestContext.putTrusted(WebpiecesCoreHeaders.SURFACE, 'llm');\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"]}
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,OAAO,GAAG,IAAI,kCAAsB,EAAE,CAAC;IAExD,KAAK,CAAC,IAAI,CACN,IAAuB,EACvB,UAAoB,EACpB,UAAiC,EACjC,UAAgC,EAChC,mBAA4B;QAE5B,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QACzE,IAAI,YAAY,EAAE,CAAC;YACf,MAAM,IAAI,8BAAkB,CACxB,qBAAqB,IAAI,CAAC,IAAI,8CAA8C,YAAY,CAAC,OAAO,EAAE,EAClG,YAAY,CAAC,KAAK,EAClB,YAAY,CAAC,OAAO,CACvB,CAAC;QACN,CAAC;QACD,MAAM,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,+EAA+E;YAC/E,qFAAqF;YACrF,iFAAiF;YACjF,uFAAuF;YACvF,mFAAmF;YACnF,6BAAc,CAAC,UAAU,CAAC,gCAAoB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC/D,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;AAzDD,4CAyDC","sourcesContent":["import {\n ApiBadRequestError,\n ApiForbiddenError,\n ApiImplementationError,\n ApiJsonSchemaValidator,\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 schemas = new ApiJsonSchemaValidator();\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.schemas.validate(tool.inputSchema, requestDto);\n if (inputFailure) {\n throw new ApiBadRequestError(\n `MCP arguments for ${tool.name} did not match its published input schema: ${inputFailure.message}`,\n inputFailure.field,\n inputFailure.message,\n );\n }\n const 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 // THIS hop is the edge, and its caller is a model. Stamped here rather than in\n // `AuthFilter` because the local binding's HTTP auth mode describes the bridge's own\n // endpoint JWT, not the LLM on the other end of it — `@WpMcpAuthJwt` is separate\n // metadata and never reaches an `AuthMode`. `AuthFilter` only ever SETS a surface when\n // none arrived, so this one survives the filter chain the invocation runs through.\n RequestContext.putTrusted(WebpiecesCoreHeaders.SURFACE, 'llm');\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"]}
@@ -1,9 +1,16 @@
1
+ import { McpToolCatalog } from '@webpieces/core-util';
1
2
  import { McpApiBinding } from './McpApiBinding';
2
3
  import { McpDeployment } from './McpDeployment';
3
4
  export declare class McpBindOptions {
4
5
  /** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */
5
6
  readonly endpointPath: string;
6
7
  readonly bindings: readonly McpApiBinding[];
8
+ /**
9
+ * The tools this build published — `mcp-tools.json`, written by `wp-openapi` and read with
10
+ * `McpToolCatalog.fromJsonText`. It is REQUIRED because it is the only source of a tool's
11
+ * documentation and schemas; there is no reflect-metadata fallback to fall back to.
12
+ */
13
+ readonly toolCatalog: McpToolCatalog;
7
14
  readonly deployment: McpDeployment;
8
15
  /** Requests without Origin remain valid for non-browser MCP clients. */
9
16
  readonly allowedOrigins: readonly string[];
@@ -11,7 +18,13 @@ export declare class McpBindOptions {
11
18
  readonly keepAliveMs: number;
12
19
  constructor(
13
20
  /** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */
14
- endpointPath: string, bindings: readonly McpApiBinding[], deployment: McpDeployment,
21
+ endpointPath: string, bindings: readonly McpApiBinding[],
22
+ /**
23
+ * The tools this build published — `mcp-tools.json`, written by `wp-openapi` and read with
24
+ * `McpToolCatalog.fromJsonText`. It is REQUIRED because it is the only source of a tool's
25
+ * documentation and schemas; there is no reflect-metadata fallback to fall back to.
26
+ */
27
+ toolCatalog: McpToolCatalog, deployment: McpDeployment,
15
28
  /** Requests without Origin remain valid for non-browser MCP clients. */
16
29
  allowedOrigins?: readonly string[], maxSubscriptions?: number, keepAliveMs?: number);
17
30
  }
@@ -4,17 +4,25 @@ exports.McpBindOptions = void 0;
4
4
  class McpBindOptions {
5
5
  endpointPath;
6
6
  bindings;
7
+ toolCatalog;
7
8
  deployment;
8
9
  allowedOrigins;
9
10
  maxSubscriptions;
10
11
  keepAliveMs;
11
12
  constructor(
12
13
  /** Supplied by the application/discovery config; Webpieces never assumes `/mcp`. */
13
- endpointPath, bindings, deployment,
14
+ endpointPath, bindings,
15
+ /**
16
+ * The tools this build published — `mcp-tools.json`, written by `wp-openapi` and read with
17
+ * `McpToolCatalog.fromJsonText`. It is REQUIRED because it is the only source of a tool's
18
+ * documentation and schemas; there is no reflect-metadata fallback to fall back to.
19
+ */
20
+ toolCatalog, deployment,
14
21
  /** Requests without Origin remain valid for non-browser MCP clients. */
15
22
  allowedOrigins = [], maxSubscriptions = 1024, keepAliveMs = 15_000) {
16
23
  this.endpointPath = endpointPath;
17
24
  this.bindings = bindings;
25
+ this.toolCatalog = toolCatalog;
18
26
  this.deployment = deployment;
19
27
  this.allowedOrigins = allowedOrigins;
20
28
  this.maxSubscriptions = maxSubscriptions;
@@ -1 +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"]}
1
+ {"version":3,"file":"McpBindOptions.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpBindOptions.ts"],"names":[],"mappings":";;;AAIA,MAAa,cAAc;IAGH;IACA;IAMA;IACA;IAEA;IACA;IACA;IAdpB;IACI,oFAAoF;IACpE,YAAoB,EACpB,QAAkC;IAClD;;;;OAIG;IACa,WAA2B,EAC3B,UAAyB;IACzC,wEAAwE;IACxD,iBAAoC,EAAE,EACtC,mBAAmB,IAAI,EACvB,cAAc,MAAM;QAZpB,iBAAY,GAAZ,YAAY,CAAQ;QACpB,aAAQ,GAAR,QAAQ,CAA0B;QAMlC,gBAAW,GAAX,WAAW,CAAgB;QAC3B,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;AA7BD,wCA6BC","sourcesContent":["import { McpToolCatalog } from '@webpieces/core-util';\nimport { 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 /**\n * The tools this build published — `mcp-tools.json`, written by `wp-openapi` and read with\n * `McpToolCatalog.fromJsonText`. It is REQUIRED because it is the only source of a tool's\n * documentation and schemas; there is no reflect-metadata fallback to fall back to.\n */\n public readonly toolCatalog: McpToolCatalog,\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"]}
@@ -1,4 +1,4 @@
1
- import { ApiJsonSchema, AuthMeta, DtoClass, DtoSchemaBuilder, EndpointOperation, WpMcpJwtAuthMetadata, WpMcpToolHints } from '@webpieces/core-util';
1
+ import { ApiJsonSchema, AuthMeta, EndpointOperation, McpToolCatalog, WpMcpJwtAuthMetadata, WpMcpToolHints } from '@webpieces/core-util';
2
2
  import { ClassType } from '@webpieces/http-routing';
3
3
  import { McpApiBinding } from './McpApiBinding';
4
4
  /** Fully resolved contract metadata for one MCP tool. */
@@ -12,19 +12,34 @@ export declare class RegisteredMcpTool {
12
12
  readonly authMeta: AuthMeta;
13
13
  readonly mcpAuth: WpMcpJwtAuthMetadata;
14
14
  readonly binding: McpApiBinding;
15
- readonly requestClass: DtoClass;
16
- readonly responseClass: DtoClass;
17
15
  readonly inputSchema: ApiJsonSchema;
18
16
  readonly outputSchema: ApiJsonSchema;
19
- constructor(apiClass: ClassType, methodName: string, name: string, description: string, annotations: WpMcpToolHints, operation: EndpointOperation, authMeta: AuthMeta, mcpAuth: WpMcpJwtAuthMetadata, binding: McpApiBinding, requestClass: DtoClass, responseClass: DtoClass, inputSchema: ApiJsonSchema, outputSchema: ApiJsonSchema);
17
+ constructor(apiClass: ClassType, methodName: string, name: string, description: string, annotations: WpMcpToolHints, operation: EndpointOperation, authMeta: AuthMeta, mcpAuth: WpMcpJwtAuthMetadata, binding: McpApiBinding, inputSchema: ApiJsonSchema, outputSchema: ApiJsonSchema);
20
18
  /** Listing is advisory; MCP authorization is rechecked before the endpoint auth boundary. */
21
19
  isVisibleTo(listingRoles: readonly string[]): boolean;
22
20
  }
23
- /** Fail-fast registry built entirely from API/decorator metadata. */
21
+ /**
22
+ * Fail-fast registry: the CONTRACT says which methods are tools and how they are authorized, and the
23
+ * build's generated catalog says what each tool's documentation and schemas are.
24
+ *
25
+ * ## Why the schemas come from the catalog and not from here
26
+ *
27
+ * They used to be built at boot by `DtoSchemaBuilder` walking reflect-metadata. That made the
28
+ * document a partner reads and the schema a server accepts two independent derivations of one
29
+ * contract, free to disagree; and it could not see a field's declared TYPE at all, so every fact
30
+ * about a field had to be restated in a `@WpDtoField` argument. #983 measured the compiler against
31
+ * it and found the compiler reproduces all of it, so #984 deleted the decorator and this registry
32
+ * now READS the same `mcp-tools.json` the documents were rendered from.
33
+ *
34
+ * A registered `@WpMcpTool` the catalog does not name is a HARD FAILURE, not a fallback: a tool the
35
+ * build never saw is a tool whose schema nobody checked.
36
+ */
24
37
  export declare class McpToolRegistry {
38
+ private readonly catalog;
25
39
  readonly tools: readonly RegisteredMcpTool[];
26
- readonly schemaBuilder: DtoSchemaBuilder;
27
- constructor(bindings: readonly McpApiBinding[]);
40
+ constructor(bindings: readonly McpApiBinding[], catalog: McpToolCatalog);
28
41
  find(name: string): RegisteredMcpTool | undefined;
29
42
  private resolve;
43
+ /** The generated entry for this tool, or a refusal naming what the build DID publish. */
44
+ private published;
30
45
  }
@@ -13,11 +13,9 @@ class RegisteredMcpTool {
13
13
  authMeta;
14
14
  mcpAuth;
15
15
  binding;
16
- requestClass;
17
- responseClass;
18
16
  inputSchema;
19
17
  outputSchema;
20
- constructor(apiClass, methodName, name, description, annotations, operation, authMeta, mcpAuth, binding, requestClass, responseClass, inputSchema, outputSchema) {
18
+ constructor(apiClass, methodName, name, description, annotations, operation, authMeta, mcpAuth, binding, inputSchema, outputSchema) {
21
19
  this.apiClass = apiClass;
22
20
  this.methodName = methodName;
23
21
  this.name = name;
@@ -27,8 +25,6 @@ class RegisteredMcpTool {
27
25
  this.authMeta = authMeta;
28
26
  this.mcpAuth = mcpAuth;
29
27
  this.binding = binding;
30
- this.requestClass = requestClass;
31
- this.responseClass = responseClass;
32
28
  this.inputSchema = inputSchema;
33
29
  this.outputSchema = outputSchema;
34
30
  }
@@ -39,15 +35,35 @@ class RegisteredMcpTool {
39
35
  }
40
36
  }
41
37
  exports.RegisteredMcpTool = RegisteredMcpTool;
42
- /** Fail-fast registry built entirely from API/decorator metadata. */
38
+ /**
39
+ * Fail-fast registry: the CONTRACT says which methods are tools and how they are authorized, and the
40
+ * build's generated catalog says what each tool's documentation and schemas are.
41
+ *
42
+ * ## Why the schemas come from the catalog and not from here
43
+ *
44
+ * They used to be built at boot by `DtoSchemaBuilder` walking reflect-metadata. That made the
45
+ * document a partner reads and the schema a server accepts two independent derivations of one
46
+ * contract, free to disagree; and it could not see a field's declared TYPE at all, so every fact
47
+ * about a field had to be restated in a `@WpDtoField` argument. #983 measured the compiler against
48
+ * it and found the compiler reproduces all of it, so #984 deleted the decorator and this registry
49
+ * now READS the same `mcp-tools.json` the documents were rendered from.
50
+ *
51
+ * A registered `@WpMcpTool` the catalog does not name is a HARD FAILURE, not a fallback: a tool the
52
+ * build never saw is a tool whose schema nobody checked.
53
+ */
43
54
  class McpToolRegistry {
55
+ catalog;
44
56
  tools;
45
- schemaBuilder = new core_util_1.DtoSchemaBuilder();
46
- constructor(bindings) {
57
+ constructor(bindings, catalog) {
58
+ this.catalog = catalog;
47
59
  const registered = [];
48
60
  const names = new Set();
49
61
  for (const binding of bindings) {
50
62
  const apiClass = binding.api;
63
+ // MCP membership is declared twice — on @ApiType and on the methods — and this is the
64
+ // one call that makes the two disagree impossible. It existed before #984 and nothing
65
+ // reached it, so a contract could carry tools nobody published to agents.
66
+ (0, core_util_1.assertApiTypeMatchesMcpTools)(apiClass);
51
67
  for (const metadata of (0, core_util_1.getWpMcpTools)(apiClass)) {
52
68
  if (names.has(metadata.name)) {
53
69
  throw new Error(`Duplicate @WpMcpTool name '${metadata.name}'. Tool names must be globally unique.`);
@@ -77,10 +93,21 @@ class McpToolRegistry {
77
93
  throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} must declare @WpMcpAuthJwt(...).`);
78
94
  }
79
95
  binding.validateMethod(metadata.methodName);
80
- const requestClass = this.schemaBuilder.requestClassOf(apiClass, metadata.methodName);
81
- const responseClass = this.schemaBuilder.responseClassOf(apiClass, metadata.methodName);
82
- const operation = (0, core_util_1.getEndpointOperation)(apiClass, metadata.methodName);
83
- return new RegisteredMcpTool(apiClass, metadata.methodName, metadata.name, metadata.description, (0, core_util_1.mcpHintsForOperation)(operation, metadata.openWorldHint), operation, authMeta, mcpAuth, binding, requestClass, responseClass, this.schemaBuilder.build(requestClass), this.schemaBuilder.build(responseClass));
96
+ const published = this.published(apiClass, metadata);
97
+ return new RegisteredMcpTool(apiClass, metadata.methodName, metadata.name, published.description, published.hints, (0, core_util_1.getEndpointOperation)(apiClass, metadata.methodName), authMeta, mcpAuth, binding, published.inputSchema, published.outputSchema);
98
+ }
99
+ /** The generated entry for this tool, or a refusal naming what the build DID publish. */
100
+ published(apiClass, metadata) {
101
+ const published = this.catalog.find(metadata.name);
102
+ if (published === undefined) {
103
+ throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} publishes '${metadata.name}', ` +
104
+ `which the generated MCP tool catalog does not contain. It has: ` +
105
+ `${this.catalog.names().join(', ') || '(nothing)'}. Regenerate it with ` +
106
+ '`wp-openapi --manifest <manifest> --out <dir>` and make sure the manifest ' +
107
+ 'names this contract — a tool the build never saw is a tool whose schema ' +
108
+ 'nobody checked.');
109
+ }
110
+ return published;
84
111
  }
85
112
  }
86
113
  exports.McpToolRegistry = McpToolRegistry;
@@ -1 +1 @@
1
- {"version":3,"file":"McpToolRegistry.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ApiJsonSchema,\n AuthMeta,\n DtoClass,\n DtoSchemaBuilder,\n getAuthMeta,\n getEndpointKind,\n getEndpointOperation,\n getEndpoints,\n getWpMcpTools,\n getWpMcpAuthJwt,\n rolesRequired,\n mcpHintsForOperation,\n EndpointOperation,\n WpMcpJwtAuthMetadata,\n WpMcpToolHints,\n WpMcpToolMetadata,\n} from '@webpieces/core-util';\nimport { ClassType } from '@webpieces/http-routing';\nimport { McpApiBinding } from './McpApiBinding';\n\n/** Fully resolved contract metadata for one MCP tool. */\nexport class RegisteredMcpTool {\n constructor(\n public readonly apiClass: ClassType,\n public readonly methodName: string,\n public readonly name: string,\n public readonly description: string,\n public readonly annotations: WpMcpToolHints,\n public readonly operation: EndpointOperation,\n public readonly authMeta: AuthMeta,\n public readonly mcpAuth: WpMcpJwtAuthMetadata,\n public readonly binding: McpApiBinding,\n public readonly requestClass: DtoClass,\n public readonly responseClass: DtoClass,\n public readonly inputSchema: ApiJsonSchema,\n public readonly outputSchema: ApiJsonSchema,\n ) {}\n\n /** Listing is advisory; MCP authorization is rechecked before the endpoint auth boundary. */\n isVisibleTo(listingRoles: readonly string[]): boolean {\n const required = rolesRequired(this.mcpAuth.requirement);\n return (\n required.length === 0 || required.some((role: string) => listingRoles.includes(role))\n );\n }\n}\n\n/** Fail-fast registry built entirely from API/decorator metadata. */\nexport class McpToolRegistry {\n readonly tools: readonly RegisteredMcpTool[];\n readonly schemaBuilder = new DtoSchemaBuilder();\n\n constructor(bindings: readonly McpApiBinding[]) {\n const registered: RegisteredMcpTool[] = [];\n const names = new Set<string>();\n for (const binding of bindings) {\n const apiClass = binding.api;\n for (const metadata of getWpMcpTools(apiClass)) {\n if (names.has(metadata.name)) {\n throw new Error(\n `Duplicate @WpMcpTool name '${metadata.name}'. Tool names must be globally unique.`,\n );\n }\n names.add(metadata.name);\n registered.push(this.resolve(apiClass, metadata, binding));\n }\n }\n this.tools = registered;\n }\n\n find(name: string): RegisteredMcpTool | undefined {\n return this.tools.find((tool: RegisteredMcpTool) => tool.name === name);\n }\n\n private resolve(\n apiClass: ClassType,\n metadata: WpMcpToolMetadata,\n binding: McpApiBinding,\n ): RegisteredMcpTool {\n const endpoints = getEndpoints(apiClass) ?? {};\n if (!endpoints[metadata.methodName]) {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must also be an @Endpoint.`,\n );\n }\n if (getEndpointKind(apiClass, metadata.methodName) !== 'rpc') {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must be an RPC endpoint.`,\n );\n }\n const authMeta = getAuthMeta(apiClass, metadata.methodName);\n if (!authMeta)\n throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} has no HTTP auth.`);\n const mcpAuth = getWpMcpAuthJwt(apiClass, metadata.methodName);\n if (!mcpAuth) {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must declare @WpMcpAuthJwt(...).`,\n );\n }\n binding.validateMethod(metadata.methodName);\n const requestClass = this.schemaBuilder.requestClassOf(apiClass, metadata.methodName);\n const responseClass = this.schemaBuilder.responseClassOf(apiClass, metadata.methodName);\n const operation = getEndpointOperation(apiClass, metadata.methodName);\n return new RegisteredMcpTool(\n apiClass,\n metadata.methodName,\n metadata.name,\n metadata.description,\n mcpHintsForOperation(operation, metadata.openWorldHint),\n operation,\n authMeta,\n mcpAuth,\n binding,\n requestClass,\n responseClass,\n this.schemaBuilder.build(requestClass),\n this.schemaBuilder.build(responseClass),\n );\n }\n}\n"]}
1
+ {"version":3,"file":"McpToolRegistry.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolRegistry.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ApiJsonSchema,\n AuthMeta,\n assertApiTypeMatchesMcpTools,\n getAuthMeta,\n getEndpointKind,\n getEndpointOperation,\n getEndpoints,\n getWpMcpTools,\n getWpMcpAuthJwt,\n rolesRequired,\n EndpointOperation,\n McpToolCatalog,\n McpToolDefinition,\n WpMcpJwtAuthMetadata,\n WpMcpToolHints,\n WpMcpToolMetadata,\n} from '@webpieces/core-util';\nimport { ClassType } from '@webpieces/http-routing';\nimport { McpApiBinding } from './McpApiBinding';\n\n/** Fully resolved contract metadata for one MCP tool. */\nexport class RegisteredMcpTool {\n constructor(\n public readonly apiClass: ClassType,\n public readonly methodName: string,\n public readonly name: string,\n public readonly description: string,\n public readonly annotations: WpMcpToolHints,\n public readonly operation: EndpointOperation,\n public readonly authMeta: AuthMeta,\n public readonly mcpAuth: WpMcpJwtAuthMetadata,\n public readonly binding: McpApiBinding,\n public readonly inputSchema: ApiJsonSchema,\n public readonly outputSchema: ApiJsonSchema,\n ) {}\n\n /** Listing is advisory; MCP authorization is rechecked before the endpoint auth boundary. */\n isVisibleTo(listingRoles: readonly string[]): boolean {\n const required = rolesRequired(this.mcpAuth.requirement);\n return (\n required.length === 0 || required.some((role: string) => listingRoles.includes(role))\n );\n }\n}\n\n/**\n * Fail-fast registry: the CONTRACT says which methods are tools and how they are authorized, and the\n * build's generated catalog says what each tool's documentation and schemas are.\n *\n * ## Why the schemas come from the catalog and not from here\n *\n * They used to be built at boot by `DtoSchemaBuilder` walking reflect-metadata. That made the\n * document a partner reads and the schema a server accepts two independent derivations of one\n * contract, free to disagree; and it could not see a field's declared TYPE at all, so every fact\n * about a field had to be restated in a `@WpDtoField` argument. #983 measured the compiler against\n * it and found the compiler reproduces all of it, so #984 deleted the decorator and this registry\n * now READS the same `mcp-tools.json` the documents were rendered from.\n *\n * A registered `@WpMcpTool` the catalog does not name is a HARD FAILURE, not a fallback: a tool the\n * build never saw is a tool whose schema nobody checked.\n */\nexport class McpToolRegistry {\n readonly tools: readonly RegisteredMcpTool[];\n\n constructor(\n bindings: readonly McpApiBinding[],\n private readonly catalog: McpToolCatalog,\n ) {\n const registered: RegisteredMcpTool[] = [];\n const names = new Set<string>();\n for (const binding of bindings) {\n const apiClass = binding.api;\n // MCP membership is declared twice — on @ApiType and on the methods — and this is the\n // one call that makes the two disagree impossible. It existed before #984 and nothing\n // reached it, so a contract could carry tools nobody published to agents.\n assertApiTypeMatchesMcpTools(apiClass);\n for (const metadata of getWpMcpTools(apiClass)) {\n if (names.has(metadata.name)) {\n throw new Error(\n `Duplicate @WpMcpTool name '${metadata.name}'. Tool names must be globally unique.`,\n );\n }\n names.add(metadata.name);\n registered.push(this.resolve(apiClass, metadata, binding));\n }\n }\n this.tools = registered;\n }\n\n find(name: string): RegisteredMcpTool | undefined {\n return this.tools.find((tool: RegisteredMcpTool) => tool.name === name);\n }\n\n private resolve(\n apiClass: ClassType,\n metadata: WpMcpToolMetadata,\n binding: McpApiBinding,\n ): RegisteredMcpTool {\n const endpoints = getEndpoints(apiClass) ?? {};\n if (!endpoints[metadata.methodName]) {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must also be an @Endpoint.`,\n );\n }\n if (getEndpointKind(apiClass, metadata.methodName) !== 'rpc') {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must be an RPC endpoint.`,\n );\n }\n const authMeta = getAuthMeta(apiClass, metadata.methodName);\n if (!authMeta)\n throw new Error(`@WpMcpTool ${apiClass.name}.${metadata.methodName} has no HTTP auth.`);\n const mcpAuth = getWpMcpAuthJwt(apiClass, metadata.methodName);\n if (!mcpAuth) {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} must declare @WpMcpAuthJwt(...).`,\n );\n }\n binding.validateMethod(metadata.methodName);\n const published = this.published(apiClass, metadata);\n return new RegisteredMcpTool(\n apiClass,\n metadata.methodName,\n metadata.name,\n published.description,\n published.hints,\n getEndpointOperation(apiClass, metadata.methodName),\n authMeta,\n mcpAuth,\n binding,\n published.inputSchema,\n published.outputSchema,\n );\n }\n\n /** The generated entry for this tool, or a refusal naming what the build DID publish. */\n private published(apiClass: ClassType, metadata: WpMcpToolMetadata): McpToolDefinition {\n const published = this.catalog.find(metadata.name);\n if (published === undefined) {\n throw new Error(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} publishes '${metadata.name}', ` +\n `which the generated MCP tool catalog does not contain. It has: ` +\n `${this.catalog.names().join(', ') || '(nothing)'}. Regenerate it with ` +\n '`wp-openapi --manifest <manifest> --out <dir>` and make sure the manifest ' +\n 'names this contract — a tool the build never saw is a tool whose schema ' +\n 'nobody checked.',\n );\n }\n return published;\n }\n}\n"]}
@@ -19,6 +19,7 @@ import { McpBindOptions } from './McpBindOptions';
19
19
  export declare class WpMcpServer<TGrant, TMintRequest> {
20
20
  private readonly config;
21
21
  private readonly dispatcher;
22
+ private readonly outputSchemas;
22
23
  /**
23
24
  * These edges have NO filter chain above them — `LogApiFilter` never sees a rejected bearer, a
24
25
  * bad `Origin`, a body express could not parse, or a `tools/list` the SDK renders itself.
@@ -74,6 +74,7 @@ class WpSdkMcpServer extends server_1.McpServer {
74
74
  class WpMcpServer {
75
75
  config;
76
76
  dispatcher = new McpApiDispatcher_1.McpApiDispatcher();
77
+ outputSchemas = new core_util_1.ApiJsonSchemaValidator();
77
78
  /**
78
79
  * These edges have NO filter chain above them — `LogApiFilter` never sees a rejected bearer, a
79
80
  * bad `Origin`, a body express could not parse, or a `tools/list` the SDK renders itself.
@@ -122,7 +123,7 @@ class WpMcpServer {
122
123
  `'${options.endpointPath}'; the canonical resource URI ` +
123
124
  `'${this.config.resource}' and the bound route are the same endpoint.`);
124
125
  }
125
- this.registry = new McpToolRegistry_1.McpToolRegistry(options.bindings);
126
+ this.registry = new McpToolRegistry_1.McpToolRegistry(options.bindings, options.toolCatalog);
126
127
  this.revision = this.calculateRegistryRevision(this.registry);
127
128
  this.handler = this.createHandler(options, 'auto');
128
129
  this.streamingHandler = this.createHandler(options, 'sse');
@@ -345,7 +346,7 @@ class WpMcpServer {
345
346
  const invocation = new McpInvocationContext_1.McpInvocationContext(sdkContext.mcpReq.id, tool.name, credential.subject, credential.listingRoles, AbortSignal.any([sdkContext.mcpReq.signal, authentication.disconnectSignal]), this.progressReporter(sdkContext));
346
347
  const args = (request.params.arguments ?? {});
347
348
  const value = await this.dispatcher.call(tool, args, credential, invocation, endpointJwt?.token);
348
- const outputFailure = this.requireRegistry().schemaBuilder.validate(tool.responseClass, value);
349
+ const outputFailure = this.outputSchemas.validate(tool.outputSchema, value);
349
350
  if (outputFailure) {
350
351
  throw new core_util_1.ApiImplementationError(`MCP output schema violation for ${tool.apiClass.name}.${tool.methodName}: ${outputFailure.message}`);
351
352
  }