@webpieces/mcp-server 0.4.839 → 0.4.841

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpToolPolicy.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolPolicy.ts"],"names":[],"mappings":";;;AAAA,oDAAkF;AAGlF,uGAAuG;AACvG,MAAa,kBAAmB,SAAQ,KAAK;IACb;IAA5B,YAA4B,QAAgB;QACxC,KAAK,CAAC,+BAA+B,CAAC,CAAC;QADf,aAAQ,GAAR,QAAQ,CAAQ;IAE5C,CAAC;CACJ;AAJD,gDAIC;AAED,sFAAsF;AACtF,MAAa,aAAa;IACO;IAA7B,YAA6B,MAAkB;QAAlB,WAAM,GAAN,MAAM,CAAY;IAAG,CAAC;IAEnD,KAAK,CAAC,OAAO,CAAC,MAA2B,EAAE,WAA2B;QAClE,iJAAiJ;QACjJ,IAAI,CAAC;YACD,MAAM,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;YACpD,OAAO,IAAI,CAAC;QAChB,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,IAAI,KAAK,YAAY,6BAAiB;gBAAE,OAAO,KAAK,CAAC;YACrD,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;IAED,KAAK,CAAC,OAAO,CACT,MAA2B,EAC3B,WAA2B,EAC3B,IAAY;QAEZ,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;YAAE,MAAM,IAAI,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACvF,CAAC;CACJ;AAtBD,sCAsBC","sourcesContent":["import { ApiForbiddenError, JwtRequirement, toError } from '@webpieces/core-util';\nimport { AuthenticatedCaller, JwtHook } from '@webpieces/http-routing';\n\n/** Internal denial marker; only the shared SDK boundary converts it to unknown-tool protocol shape. */\nexport class McpToolDeniedError extends Error {\n constructor(public readonly toolName: string) {\n super('MCP tool authorization denied');\n }\n}\n\n/** Application policy is the only predicate used for both projection and dispatch. */\nexport class McpToolPolicy<T> {\n constructor(private readonly policy: JwtHook<T>) {}\n\n async permits(caller: AuthenticatedCaller, requirement: JwtRequirement): Promise<boolean> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- only explicit policy denials are a negative predicate; other failures propagate\n try {\n await this.policy.authorizeJwt(caller, requirement);\n return true;\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof ApiForbiddenError) return false;\n throw error;\n }\n }\n\n async require(\n caller: AuthenticatedCaller,\n requirement: JwtRequirement,\n name: string,\n ): Promise<void> {\n if (!(await this.permits(caller, requirement))) throw new McpToolDeniedError(name);\n }\n}\n"]}
@@ -1,5 +1,5 @@
1
1
  import { ApiJsonSchema, AuthMeta, EndpointOperation, WpMcpJwtAuthMetadata, WpMcpToolHints } from '@webpieces/core-util';
2
- import { ClassType } from '@webpieces/http-routing';
2
+ import { AuthenticatedCaller, ClassType, JwtHook } from '@webpieces/http-routing';
3
3
  import { McpApiBinding } from './McpApiBinding';
4
4
  import { McpToolCatalog } from './McpToolCatalog';
5
5
  /** Fully resolved contract metadata for one MCP tool. */
@@ -16,8 +16,6 @@ export declare class RegisteredMcpTool {
16
16
  readonly inputSchema: ApiJsonSchema;
17
17
  readonly outputSchema: ApiJsonSchema;
18
18
  constructor(apiClass: ClassType, methodName: string, name: string, description: string, annotations: WpMcpToolHints, operation: EndpointOperation, authMeta: AuthMeta, mcpAuth: WpMcpJwtAuthMetadata, binding: McpApiBinding, inputSchema: ApiJsonSchema, outputSchema: ApiJsonSchema);
19
- /** Listing is advisory; MCP authorization is rechecked before the endpoint auth boundary. */
20
- isVisibleTo(listingRoles: readonly string[]): boolean;
21
19
  }
22
20
  /**
23
21
  * Fail-fast registry: the CONTRACT says which methods are tools and how they are authorized, and the
@@ -57,3 +55,10 @@ export declare class McpToolRegistry {
57
55
  /** The generated entry for this tool, or a recorded refusal naming what the build DID publish. */
58
56
  private published;
59
57
  }
58
+ /** Request-specific authorized projection, shared by documentation, schema lookup and name lookup. */
59
+ export declare class AuthorizedMcpTools {
60
+ readonly tools: readonly RegisteredMcpTool[];
61
+ constructor(tools: readonly RegisteredMcpTool[]);
62
+ find(name: string): RegisteredMcpTool | undefined;
63
+ static create<T>(registry: McpToolRegistry, caller: AuthenticatedCaller, policy: JwtHook<T>): Promise<AuthorizedMcpTools>;
64
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.McpToolRegistry = exports.RegisteredMcpTool = void 0;
3
+ exports.AuthorizedMcpTools = exports.McpToolRegistry = exports.RegisteredMcpTool = void 0;
4
+ const McpToolPolicy_1 = require("./McpToolPolicy");
4
5
  const core_util_1 = require("@webpieces/core-util");
5
6
  /** Fully resolved contract metadata for one MCP tool. */
6
7
  class RegisteredMcpTool {
@@ -28,11 +29,6 @@ class RegisteredMcpTool {
28
29
  this.inputSchema = inputSchema;
29
30
  this.outputSchema = outputSchema;
30
31
  }
31
- /** Listing is advisory; MCP authorization is rechecked before the endpoint auth boundary. */
32
- isVisibleTo(listingRoles) {
33
- const required = (0, core_util_1.rolesRequired)(this.mcpAuth.requirement);
34
- return (required.length === 0 || required.some((role) => listingRoles.includes(role)));
35
- }
36
32
  }
37
33
  exports.RegisteredMcpTool = RegisteredMcpTool;
38
34
  /**
@@ -166,7 +162,7 @@ class McpCatalogPairing {
166
162
  `wrote no ${core_util_1.McpToolCatalogFile.fileNameFor(apiClass.name)} for it. Delete its ` +
167
163
  'McpApiBinding — it publishes nothing.'
168
164
  : `The server binds ${apiClass.name}, and no ${core_util_1.McpToolCatalogFile.fileNameFor(apiClass.name)} ` +
169
- "was handed to it. Add the api library that declares it to McpToolCatalog.fromPackages([...]) " +
165
+ 'was handed to it. Add the api library that declares it to McpToolCatalog.fromPackages([...]) ' +
170
166
  'and rebuild that library (its openapi-generate target writes the file).');
171
167
  return undefined;
172
168
  }
@@ -185,7 +181,9 @@ class McpCatalogPairing {
185
181
  }
186
182
  if (this.problems.length === 0)
187
183
  return;
188
- const directories = [...new Set(this.catalogs.map((catalog) => catalog.directory))];
184
+ const directories = [
185
+ ...new Set(this.catalogs.map((catalog) => catalog.directory)),
186
+ ];
189
187
  throw new core_util_1.McpToolCatalogError('The MCP tool catalogs do not match the bound contracts:\n' +
190
188
  this.problems.map((text) => ` - ${text}`).join('\n') +
191
189
  '\nDirectories searched for mcp-<ContractClass>-tools.json:\n' +
@@ -195,4 +193,25 @@ class McpCatalogPairing {
195
193
  '\n', 'Each bound contract needs exactly the one catalog its api library build writes, and nothing else.');
196
194
  }
197
195
  }
196
+ /** Request-specific authorized projection, shared by documentation, schema lookup and name lookup. */
197
+ class AuthorizedMcpTools {
198
+ tools;
199
+ constructor(tools) {
200
+ this.tools = tools;
201
+ }
202
+ find(name) {
203
+ return this.tools.find((tool) => tool.name === name);
204
+ }
205
+ // webpieces-disable no-function-outside-class -- asynchronous projection factory
206
+ static async create(registry, caller, policy) {
207
+ const tools = [];
208
+ const authorization = new McpToolPolicy_1.McpToolPolicy(policy);
209
+ for (const tool of registry.tools) {
210
+ if (await authorization.permits(caller, tool.mcpAuth.requirement))
211
+ tools.push(tool);
212
+ }
213
+ return new AuthorizedMcpTools(tools);
214
+ }
215
+ }
216
+ exports.AuthorizedMcpTools = AuthorizedMcpTools;
198
217
  //# sourceMappingURL=McpToolRegistry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"McpToolRegistry.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolRegistry.ts"],"names":[],"mappings":";;;AAAA,oDAkB8B;AAK9B,yDAAyD;AACzD,MAAa,iBAAiB;IAEN;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAXpB,YACoB,QAAmB,EACnB,UAAkB,EAClB,IAAY,EACZ,WAAmB,EACnB,WAA2B,EAC3B,SAA4B,EAC5B,QAAkB,EAClB,OAA6B,EAC7B,OAAsB,EACtB,WAA0B,EAC1B,YAA2B;QAV3B,aAAQ,GAAR,QAAQ,CAAW;QACnB,eAAU,GAAV,UAAU,CAAQ;QAClB,SAAI,GAAJ,IAAI,CAAQ;QACZ,gBAAW,GAAX,WAAW,CAAQ;QACnB,gBAAW,GAAX,WAAW,CAAgB;QAC3B,cAAS,GAAT,SAAS,CAAmB;QAC5B,aAAQ,GAAR,QAAQ,CAAU;QAClB,YAAO,GAAP,OAAO,CAAsB;QAC7B,YAAO,GAAP,OAAO,CAAe;QACtB,gBAAW,GAAX,WAAW,CAAe;QAC1B,iBAAY,GAAZ,YAAY,CAAe;IAC5C,CAAC;IAEJ,6FAA6F;IAC7F,WAAW,CAAC,YAA+B;QACvC,MAAM,QAAQ,GAAG,IAAA,yBAAa,EAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACzD,OAAO,CACH,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CACxF,CAAC;IACN,CAAC;CACJ;AAtBD,8CAsBC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAa,eAAe;IACf,KAAK,CAA+B;IAE7C,YAAY,QAAkC,EAAE,QAAmC;QAC/E,MAAM,OAAO,GAAG,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,UAAU,GAAwB,EAAE,CAAC;QAC3C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;QAChC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC;YAC7B,sFAAsF;YACtF,sFAAsF;YACtF,0EAA0E;YAC1E,IAAA,wCAA4B,EAAC,QAAQ,CAAC,CAAC;YACvC,MAAM,QAAQ,GAAG,IAAA,yBAAa,EAAC,QAAQ,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;YACzD,KAAK,MAAM,QAAQ,IAAI,QAAQ,EAAE,CAAC;gBAC9B,IAAI,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC3B,OAAO,CAAC,OAAO,CACX,8BAA8B,QAAQ,CAAC,IAAI,eAAe,QAAQ,CAAC,IAAI,UAAU;wBAC7E,2EAA2E,CAClF,CAAC;oBACF,SAAS;gBACb,CAAC;gBACD,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBACzB,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;gBACzE,IAAI,IAAI,KAAK,SAAS;oBAAE,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClD,CAAC;QACL,CAAC;QACD,OAAO,CAAC,cAAc,EAAE,CAAC;QACzB,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC;IAC5B,CAAC;IAED,IAAI,CAAC,IAAY;QACb,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAuB,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;IAC5E,CAAC;IAED;;;OAGG;IACK,OAAO,CACX,QAAmB,EACnB,QAA2B,EAC3B,OAAsB,EACtB,OAAmC,EACnC,OAA0B;QAE1B,MAAM,SAAS,GAAG,IAAA,wBAAY,EAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC/C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACX,cAAc,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,UAAU,6BAA6B,CAClF,CAAC;QACN,CAAC;QACD,IAAI,IAAA,2BAAe,EAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,KAAK,KAAK,EAAE,CAAC;YAC3D,MAAM,IAAI,KAAK,CACX,cAAc,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,UAAU,2BAA2B,CAChF,CAAC;QACN,CAAC;QACD,MAAM,QAAQ,GAAG,IAAA,uBAAW,EAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC5D,IAAI,CAAC,QAAQ;YACT,MAAM,IAAI,KAAK,CAAC,cAAc,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,UAAU,oBAAoB,CAAC,CAAC;QAC5F,MAAM,OAAO,GAAG,IAAA,2BAAe,EAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC/D,IAAI,CAAC,OAAO,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CACX,cAAc,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,UAAU,mCAAmC,CACxF,CAAC;QACN,CAAC;QACD,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;QAC5C,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;QACvE,IAAI,SAAS,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC9C,OAAO,IAAI,iBAAiB,CACxB,QAAQ,EACR,QAAQ,CAAC,UAAU,EACnB,QAAQ,CAAC,IAAI,EACb,SAAS,CAAC,WAAW,EACrB,SAAS,CAAC,KAAK,EACf,IAAA,gCAAoB,EAAC,QAAQ,EAAE,QAAQ,CAAC,UAAU,CAAC,EACnD,QAAQ,EACR,OAAO,EACP,OAAO,EACP,SAAS,CAAC,WAAW,EACrB,SAAS,CAAC,YAAY,CACzB,CAAC;IACN,CAAC;IAED,kGAAkG;IAC1F,SAAS,CACb,QAAmB,EACnB,QAA2B,EAC3B,OAAuB,EACvB,OAA0B;QAE1B,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,CAAC,OAAO,CACX,cAAc,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,UAAU,eAAe,QAAQ,CAAC,IAAI,WAAW;gBACrF,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC,SAAS,8BAA8B;gBAC5E,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,WAAW,6CAA6C;gBACzF,4FAA4F,CACnG,CAAC;QACN,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;CACJ;AAxGD,0CAwGC;AAED;;;GAGG;AACH,MAAM,iBAAiB;IAKU;IAJZ,UAAU,GAAG,IAAI,GAAG,EAA0B,CAAC;IAC/C,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAC5B,QAAQ,GAAa,EAAE,CAAC;IAEzC,YAA6B,QAAmC;QAAnC,aAAQ,GAAR,QAAQ,CAA2B;QAC5D,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;YAC1D,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBACxB,IAAI,CAAC,OAAO,CACR,6BAA6B,OAAO,CAAC,YAAY,KAAK,OAAO,CAAC,SAAS,OAAO;oBAC1E,GAAG,OAAO,CAAC,SAAS,8DAA8D;oBAClF,yCAAyC,CAChD,CAAC;gBACF,SAAS;YACb,CAAC;YACD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QACvD,CAAC;IACL,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,QAAmB,EAAE,aAAqB;QAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,OAAO,CAAC;QAC1C,IAAI,CAAC,OAAO,CACR,aAAa,KAAK,CAAC;YACf,CAAC,CAAC,oBAAoB,QAAQ,CAAC,IAAI,sDAAsD;gBACnF,YAAY,8BAAkB,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,sBAAsB;gBAC/E,uCAAuC;YAC7C,CAAC,CAAC,oBAAoB,QAAQ,CAAC,IAAI,YAAY,8BAAkB,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG;gBACzF,+FAA+F;gBAC/F,yEAAyE,CACtF,CAAC;QACF,OAAO,SAAS,CAAC;IACrB,CAAC;IAED,OAAO,CAAC,IAAY;QAChB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,0FAA0F;IAC1F,cAAc;QACV,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;YAC7C,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,YAAY,CAAC;gBAAE,SAAS;YACrD,IAAI,CAAC,OAAO,CACR,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,KAAK,OAAO,CAAC,SAAS,uBAAuB,OAAO,CAAC,YAAY,IAAI;gBACzF,oEAAoE,OAAO,CAAC,YAAY,SAAS;gBACjG,yDAAyD;gBACzD,0EAA0E,OAAO,CAAC,YAAY,KAAK,CAC1G,CAAC;QACN,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACvC,MAAM,WAAW,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAuB,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QACpG,MAAM,IAAI,+BAAmB,CACzB,2DAA2D;YACvD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;YAC7D,8DAA8D;YAC9D,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC;gBACrB,CAAC,CAAC,kCAAkC;gBACpC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC9D,IAAI,EACR,mGAAmG,CACtG,CAAC;IACN,CAAC;CACJ","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 McpToolCatalogError,\n McpToolCatalogFile,\n McpToolDefinition,\n WpMcpJwtAuthMetadata,\n WpMcpToolHints,\n WpMcpToolMetadata,\n} from '@webpieces/core-util';\nimport { ClassType } from '@webpieces/http-routing';\nimport { McpApiBinding } from './McpApiBinding';\nimport { McpToolCatalog } from './McpToolCatalog';\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 OF THAT CONTRACT 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 catalogs the documents were rendered beside.\n *\n * ## One catalog per bound contract, exactly (#1021)\n *\n * `wp-openapi` writes one `mcp-<ContractClass>-tools.json` per contract, and this registry pairs every\n * `McpApiBinding` with the file of its own contract. Three mismatches refuse the boot, all reported at\n * once, each naming every directory the catalogs were read from:\n *\n * - a bound contract with no catalog — its tools were never given a schema by the build;\n * - a catalog whose contract is not bound — the server was handed tools it does not serve;\n * - a tool name declared twice — the protocol namespace is flat.\n *\n * A registered `@WpMcpTool` the catalog does not name is likewise a HARD FAILURE, not a fallback: a\n * tool the build never saw is a tool whose schema nobody checked.\n */\nexport class McpToolRegistry {\n readonly tools: readonly RegisteredMcpTool[];\n\n constructor(bindings: readonly McpApiBinding[], catalogs: readonly McpToolCatalog[]) {\n const pairing = new McpCatalogPairing(catalogs);\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 const declared = getWpMcpTools(apiClass);\n const catalog = pairing.claim(apiClass, declared.length);\n for (const metadata of declared) {\n if (names.has(metadata.name)) {\n pairing.problem(\n `Duplicate @WpMcpTool name '${metadata.name}' (again on ${apiClass.name}). Tool ` +\n 'names are the protocol identity and must be globally unique — rename one.',\n );\n continue;\n }\n names.add(metadata.name);\n const tool = this.resolve(apiClass, metadata, binding, catalog, pairing);\n if (tool !== undefined) registered.push(tool);\n }\n }\n pairing.assertComplete();\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 /**\n * The contract checks THROW at once — they are about the contract source, not the build. A tool\n * missing from its catalog is RECORDED on `pairing`, so one boot names every catalog mismatch.\n */\n private resolve(\n apiClass: ClassType,\n metadata: WpMcpToolMetadata,\n binding: McpApiBinding,\n catalog: McpToolCatalog | undefined,\n pairing: McpCatalogPairing,\n ): RegisteredMcpTool | undefined {\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 if (catalog === undefined) return undefined;\n const published = this.published(apiClass, metadata, catalog, pairing);\n if (published === undefined) return undefined;\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 recorded refusal naming what the build DID publish. */\n private published(\n apiClass: ClassType,\n metadata: WpMcpToolMetadata,\n catalog: McpToolCatalog,\n pairing: McpCatalogPairing,\n ): McpToolDefinition | undefined {\n const published = catalog.find(metadata.name);\n if (published === undefined) {\n pairing.problem(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} publishes '${metadata.name}', which ` +\n `${catalog.file.fileName} (${catalog.directory}) does not contain. It has: ` +\n `${catalog.names().join(', ') || '(nothing)'}. The catalog is older than the contract — ` +\n 'rebuild the api library; a tool the build never saw is a tool whose schema nobody checked.',\n );\n }\n return published;\n }\n}\n\n/**\n * Pairs each bound contract with the catalog of exactly that contract, and collects every mismatch so\n * the boot refusal lists them all at once instead of one per restart.\n */\nclass McpCatalogPairing {\n private readonly byContract = new Map<string, McpToolCatalog>();\n private readonly claimed = new Set<string>();\n private readonly problems: string[] = [];\n\n constructor(private readonly catalogs: readonly McpToolCatalog[]) {\n for (const catalog of catalogs) {\n const earlier = this.byContract.get(catalog.contractName);\n if (earlier !== undefined) {\n this.problem(\n `Two MCP tool catalogs for ${catalog.contractName}: ${earlier.directory} and ` +\n `${catalog.directory}. Name each api library in fromPackages(...) once, and bind ` +\n 'each contract from exactly one of them.',\n );\n continue;\n }\n this.byContract.set(catalog.contractName, catalog);\n }\n }\n\n /** The catalog of `apiClass`, recording a problem when there is none. */\n claim(apiClass: ClassType, declaredTools: number): McpToolCatalog | undefined {\n const catalog = this.byContract.get(apiClass.name);\n this.claimed.add(apiClass.name);\n if (catalog !== undefined) return catalog;\n this.problem(\n declaredTools === 0\n ? `The server binds ${apiClass.name}, which declares no @WpMcpTool method, so the build ` +\n `wrote no ${McpToolCatalogFile.fileNameFor(apiClass.name)} for it. Delete its ` +\n 'McpApiBinding — it publishes nothing.'\n : `The server binds ${apiClass.name}, and no ${McpToolCatalogFile.fileNameFor(apiClass.name)} ` +\n \"was handed to it. Add the api library that declares it to McpToolCatalog.fromPackages([...]) \" +\n 'and rebuild that library (its openapi-generate target writes the file).',\n );\n return undefined;\n }\n\n problem(text: string): void {\n this.problems.push(text);\n }\n\n /** Refuse the boot on any mismatch, naming every problem and every directory searched. */\n assertComplete(): void {\n for (const catalog of this.byContract.values()) {\n if (this.claimed.has(catalog.contractName)) continue;\n this.problem(\n `${catalog.file.fileName} (${catalog.directory}) is the catalog of ${catalog.contractName}, ` +\n `which the server does not bind. Bind it with McpApiBinding.local(${catalog.contractName}, ...) ` +\n 'or .remote(...), or leave it out of toolCatalogs, e.g. ' +\n `catalogs.filter((catalog: McpToolCatalog) => catalog.contractName !== '${catalog.contractName}').`,\n );\n }\n if (this.problems.length === 0) return;\n const directories = [...new Set(this.catalogs.map((catalog: McpToolCatalog) => catalog.directory))];\n throw new McpToolCatalogError(\n 'The MCP tool catalogs do not match the bound contracts:\\n' +\n this.problems.map((text: string) => ` - ${text}`).join('\\n') +\n '\\nDirectories searched for mcp-<ContractClass>-tools.json:\\n' +\n (directories.length === 0\n ? ' (none — toolCatalogs is empty)'\n : directories.map((dir: string) => ` ${dir}`).join('\\n')) +\n '\\n',\n 'Each bound contract needs exactly the one catalog its api library build writes, and nothing else.',\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 { McpToolPolicy } from './McpToolPolicy';\nimport {\n ApiJsonSchema,\n AuthMeta,\n assertApiTypeMatchesMcpTools,\n getAuthMeta,\n getEndpointKind,\n getEndpointOperation,\n getEndpoints,\n getWpMcpTools,\n getWpMcpAuthJwt,\n EndpointOperation,\n McpToolCatalogError,\n McpToolCatalogFile,\n McpToolDefinition,\n WpMcpJwtAuthMetadata,\n WpMcpToolHints,\n WpMcpToolMetadata,\n} from '@webpieces/core-util';\nimport { AuthenticatedCaller, ClassType, JwtHook } from '@webpieces/http-routing';\nimport { McpApiBinding } from './McpApiBinding';\nimport { McpToolCatalog } from './McpToolCatalog';\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\n/**\n * Fail-fast registry: the CONTRACT says which methods are tools and how they are authorized, and the\n * build's generated catalog OF THAT CONTRACT 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 catalogs the documents were rendered beside.\n *\n * ## One catalog per bound contract, exactly (#1021)\n *\n * `wp-openapi` writes one `mcp-<ContractClass>-tools.json` per contract, and this registry pairs every\n * `McpApiBinding` with the file of its own contract. Three mismatches refuse the boot, all reported at\n * once, each naming every directory the catalogs were read from:\n *\n * - a bound contract with no catalog — its tools were never given a schema by the build;\n * - a catalog whose contract is not bound — the server was handed tools it does not serve;\n * - a tool name declared twice — the protocol namespace is flat.\n *\n * A registered `@WpMcpTool` the catalog does not name is likewise a HARD FAILURE, not a fallback: a\n * tool the build never saw is a tool whose schema nobody checked.\n */\nexport class McpToolRegistry {\n readonly tools: readonly RegisteredMcpTool[];\n\n constructor(bindings: readonly McpApiBinding[], catalogs: readonly McpToolCatalog[]) {\n const pairing = new McpCatalogPairing(catalogs);\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 const declared = getWpMcpTools(apiClass);\n const catalog = pairing.claim(apiClass, declared.length);\n for (const metadata of declared) {\n if (names.has(metadata.name)) {\n pairing.problem(\n `Duplicate @WpMcpTool name '${metadata.name}' (again on ${apiClass.name}). Tool ` +\n 'names are the protocol identity and must be globally unique — rename one.',\n );\n continue;\n }\n names.add(metadata.name);\n const tool = this.resolve(apiClass, metadata, binding, catalog, pairing);\n if (tool !== undefined) registered.push(tool);\n }\n }\n pairing.assertComplete();\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 /**\n * The contract checks THROW at once — they are about the contract source, not the build. A tool\n * missing from its catalog is RECORDED on `pairing`, so one boot names every catalog mismatch.\n */\n private resolve(\n apiClass: ClassType,\n metadata: WpMcpToolMetadata,\n binding: McpApiBinding,\n catalog: McpToolCatalog | undefined,\n pairing: McpCatalogPairing,\n ): RegisteredMcpTool | undefined {\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 if (catalog === undefined) return undefined;\n const published = this.published(apiClass, metadata, catalog, pairing);\n if (published === undefined) return undefined;\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 recorded refusal naming what the build DID publish. */\n private published(\n apiClass: ClassType,\n metadata: WpMcpToolMetadata,\n catalog: McpToolCatalog,\n pairing: McpCatalogPairing,\n ): McpToolDefinition | undefined {\n const published = catalog.find(metadata.name);\n if (published === undefined) {\n pairing.problem(\n `@WpMcpTool ${apiClass.name}.${metadata.methodName} publishes '${metadata.name}', which ` +\n `${catalog.file.fileName} (${catalog.directory}) does not contain. It has: ` +\n `${catalog.names().join(', ') || '(nothing)'}. The catalog is older than the contract — ` +\n 'rebuild the api library; a tool the build never saw is a tool whose schema nobody checked.',\n );\n }\n return published;\n }\n}\n\n/**\n * Pairs each bound contract with the catalog of exactly that contract, and collects every mismatch so\n * the boot refusal lists them all at once instead of one per restart.\n */\nclass McpCatalogPairing {\n private readonly byContract = new Map<string, McpToolCatalog>();\n private readonly claimed = new Set<string>();\n private readonly problems: string[] = [];\n\n constructor(private readonly catalogs: readonly McpToolCatalog[]) {\n for (const catalog of catalogs) {\n const earlier = this.byContract.get(catalog.contractName);\n if (earlier !== undefined) {\n this.problem(\n `Two MCP tool catalogs for ${catalog.contractName}: ${earlier.directory} and ` +\n `${catalog.directory}. Name each api library in fromPackages(...) once, and bind ` +\n 'each contract from exactly one of them.',\n );\n continue;\n }\n this.byContract.set(catalog.contractName, catalog);\n }\n }\n\n /** The catalog of `apiClass`, recording a problem when there is none. */\n claim(apiClass: ClassType, declaredTools: number): McpToolCatalog | undefined {\n const catalog = this.byContract.get(apiClass.name);\n this.claimed.add(apiClass.name);\n if (catalog !== undefined) return catalog;\n this.problem(\n declaredTools === 0\n ? `The server binds ${apiClass.name}, which declares no @WpMcpTool method, so the build ` +\n `wrote no ${McpToolCatalogFile.fileNameFor(apiClass.name)} for it. Delete its ` +\n 'McpApiBinding — it publishes nothing.'\n : `The server binds ${apiClass.name}, and no ${McpToolCatalogFile.fileNameFor(apiClass.name)} ` +\n 'was handed to it. Add the api library that declares it to McpToolCatalog.fromPackages([...]) ' +\n 'and rebuild that library (its openapi-generate target writes the file).',\n );\n return undefined;\n }\n\n problem(text: string): void {\n this.problems.push(text);\n }\n\n /** Refuse the boot on any mismatch, naming every problem and every directory searched. */\n assertComplete(): void {\n for (const catalog of this.byContract.values()) {\n if (this.claimed.has(catalog.contractName)) continue;\n this.problem(\n `${catalog.file.fileName} (${catalog.directory}) is the catalog of ${catalog.contractName}, ` +\n `which the server does not bind. Bind it with McpApiBinding.local(${catalog.contractName}, ...) ` +\n 'or .remote(...), or leave it out of toolCatalogs, e.g. ' +\n `catalogs.filter((catalog: McpToolCatalog) => catalog.contractName !== '${catalog.contractName}').`,\n );\n }\n if (this.problems.length === 0) return;\n const directories = [\n ...new Set(this.catalogs.map((catalog: McpToolCatalog) => catalog.directory)),\n ];\n throw new McpToolCatalogError(\n 'The MCP tool catalogs do not match the bound contracts:\\n' +\n this.problems.map((text: string) => ` - ${text}`).join('\\n') +\n '\\nDirectories searched for mcp-<ContractClass>-tools.json:\\n' +\n (directories.length === 0\n ? ' (none — toolCatalogs is empty)'\n : directories.map((dir: string) => ` ${dir}`).join('\\n')) +\n '\\n',\n 'Each bound contract needs exactly the one catalog its api library build writes, and nothing else.',\n );\n }\n}\n\n/** Request-specific authorized projection, shared by documentation, schema lookup and name lookup. */\nexport class AuthorizedMcpTools {\n constructor(public readonly tools: readonly RegisteredMcpTool[]) {}\n\n find(name: string): RegisteredMcpTool | undefined {\n return this.tools.find((tool: RegisteredMcpTool) => tool.name === name);\n }\n\n // webpieces-disable no-function-outside-class -- asynchronous projection factory\n static async create<T>(\n registry: McpToolRegistry,\n caller: AuthenticatedCaller,\n policy: JwtHook<T>,\n ): Promise<AuthorizedMcpTools> {\n const tools: RegisteredMcpTool[] = [];\n const authorization = new McpToolPolicy(policy);\n for (const tool of registry.tools) {\n if (await authorization.permits(caller, tool.mcpAuth.requirement)) tools.push(tool);\n }\n return new AuthorizedMcpTools(tools);\n }\n}\n"]}
@@ -4,7 +4,7 @@ import { McpHttpErrorBody } from './McpToolCallRendering';
4
4
  /**
5
5
  * The ONE place every MCP failure becomes a reply, mirroring `WebpiecesDefaultErrorTranslator` for HTTP: an error
6
6
  * in, the exact wire shape out. There is one method per BOUNDARY, and the MCP spec is what makes
7
- * them three rather than one:
7
+ * their renderings distinct at the two interception seams (HTTP ingress and shared SDK callbacks):
8
8
  *
9
9
  * | Boundary | Who writes HTTP | Failure shape |
10
10
  * |--------------------------------|-----------------|-------------------------------------|
@@ -9,7 +9,7 @@ const log = core_util_1.LogManager.getLogger('WpMcpErrorTranslator');
9
9
  /**
10
10
  * The ONE place every MCP failure becomes a reply, mirroring `WebpiecesDefaultErrorTranslator` for HTTP: an error
11
11
  * in, the exact wire shape out. There is one method per BOUNDARY, and the MCP spec is what makes
12
- * them three rather than one:
12
+ * their renderings distinct at the two interception seams (HTTP ingress and shared SDK callbacks):
13
13
  *
14
14
  * | Boundary | Who writes HTTP | Failure shape |
15
15
  * |--------------------------------|-----------------|-------------------------------------|
@@ -1 +1 @@
1
- {"version":3,"file":"WpMcpErrorTranslator.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/WpMcpErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { CallToolResult, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';\nimport {\n ApiErrorBoundary,\n HttpHeader,\n HttpResponseDto,\n HttpResponseStatus,\n LogManager,\n toError,\n} from '@webpieces/core-util';\nimport { McpRegistry } from './McpRegistry';\nimport {\n McpCorrelation,\n McpDefaultToolCallRenderer,\n McpErrorData,\n McpErrorTranslator,\n McpHttpErrorBody,\n McpHttpErrorDetail,\n MCP_REQUEST_ID_META_KEY,\n} from './McpToolCallRendering';\n\nconst log = LogManager.getLogger('WpMcpErrorTranslator');\n\n/**\n * The ONE place every MCP failure becomes a reply, mirroring `WebpiecesDefaultErrorTranslator` for HTTP: an error\n * in, the exact wire shape out. There is one method per BOUNDARY, and the MCP spec is what makes\n * them three rather than one:\n *\n * | Boundary | Who writes HTTP | Failure shape |\n * |--------------------------------|-----------------|-------------------------------------|\n * | pre-SDK bearer / Origin / body | **webpieces** | `HttpResponseDto<McpHttpErrorBody>` |\n * | `tools/list` | the SDK | `throw ProtocolError` |\n * | `tools/call` | the SDK | `CallToolResult` with `isError` |\n *\n * Classification and published text are the shared {@link ApiErrorBoundary} rules. Operator logging\n * is NOT here: `LogApiCall` wraps each of those boundaries in `WpMcpServer`, so every failure gets\n * exactly one `[API-server-resp-*]` line with the request and the identity on it — and\n * `WpMcpServer` adds one `log.error` with the stack and requestId for an IMPLEMENTATION-kind failure\n * at tools/list and tools/call before handing it here, because this class renders that kind as\n * \"Internal Error\" and nothing else records WHERE it broke (#1055).\n */\nexport class WpMcpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n private readonly defaultToolCall = new McpDefaultToolCallRenderer();\n\n /**\n * @param challenge - the `WWW-Authenticate` value for a 401, read lazily because\n * `WpMcpServerConfig` is only fully validated at `bind(...)` time.\n */\n constructor(private readonly challenge: () => string) {}\n\n /**\n * Boundary #1: everything webpieces itself rejects before the SDK is involved — bearer\n * verification, `Origin`, a malformed body. This is 401/403/400/500 today and any status\n * tomorrow, so it produces a VALUE like every other API in the framework and `WpMcpServer` hands\n * it to the shared express writer. The BODY stays JSON-RPC shaped because an MCP client expects\n * that; `HttpResponseDto` is generic, and its header LIST carries `WWW-Authenticate`.\n */\n toBearerBoundaryResponse(error: Error): HttpResponseDto<McpHttpErrorBody> {\n const payload = this.boundary.encode(error);\n const generic = payload.message;\n switch (payload.kind) {\n case 'unauthorized':\n return this.httpError(401, -32_000, generic, [\n new HttpHeader('WWW-Authenticate', this.challenge()),\n ]);\n case 'forbidden':\n return this.httpError(403, -32_000, generic, []);\n case 'bad-request':\n return this.httpError(400, ProtocolErrorCode.ParseError, generic, []);\n default:\n return this.httpError(500, ProtocolErrorCode.InternalError, 'Internal Error', []);\n }\n }\n\n /**\n * Boundary #2: `tools/list` has no tool-result channel, so a failure IS a JSON-RPC error. It\n * THROWS rather than returning a value the caller must remember to throw, and is typed `never`\n * so the compiler knows the call is terminal.\n *\n * This is LOAD-BEARING for disclosure, not defence in depth. Measured on\n * `@modelcontextprotocol/server` 2.0.0 and pinned by `McpSdkForeignThrow.spec.ts`: the SDK wraps\n * a foreign (non-`ProtocolError`) throw in `-32603` but copies its `message` onto the wire\n * VERBATIM. So a `tools/list` handler that lets any error escape un-translated publishes that\n * error's operator text to the caller.\n */\n toListError(error: Error): never {\n const payload = this.boundary.encode(error);\n const requestId = McpCorrelation.requestId();\n if (payload.kind === 'bad-request') {\n throw new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n payload.callerMessage ?? payload.message,\n new McpErrorData(requestId),\n );\n }\n const message = payload.kind === 'end-user' ? payload.message : 'Internal Error';\n throw new ProtocolError(\n ProtocolErrorCode.InternalError,\n message,\n new McpErrorData(requestId),\n );\n }\n\n /** tools/call with a name no binding registered: a JSON-RPC protocol error in either MCP era. */\n unknownTool(toolName: string): ProtocolError {\n const requestId = McpCorrelation.requestId();\n log.info(`Unknown MCP tool: ${toolName} requestId=${requestId}`);\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n `Unknown tool: ${toolName}`,\n new McpErrorData(requestId),\n );\n }\n\n /**\n * Boundary #3: every tools/call failure after the tool is found — an `isError: true` result the\n * model sees.\n *\n * ONE unconditional line: {@link McpRegistry.getErrorTranslator} is never undefined, so there is\n * no \"was one registered\" branch. A registered {@link McpErrorTranslator} REPLACES the default and\n * declines by delegating to {@link McpDefaultToolCallRenderer}, so \"no translator registered\" and\n * \"a translator that does not claim this error\" produce byte-identical results.\n */\n toToolCallResult(failure: Error): CallToolResult {\n return this.withDefaultMeta(this.appOrDefault(McpRegistry.getErrorTranslator(), failure));\n }\n\n /** The `_meta` every tools/call result carries so a user can quote its requestId. */\n resultMeta(requestId: string): Record<string, string> {\n return { [MCP_REQUEST_ID_META_KEY]: requestId };\n }\n\n /**\n * An app translator that THROWS must not replace the failure being reported: its own bug is\n * logged here and the ORIGINAL error still renders through the webpieces default, so the model\n * still gets a reply carrying the requestId.\n *\n * This is BUG CONTAINMENT, not a \"did anyone register one\" branch — the same shape as\n * `ClientErrorTranslator.throwIfFailure` on the HTTP side.\n */\n private appOrDefault(renderer: McpErrorTranslator, failure: Error): CallToolResult {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an app translator's own bug must not replace the failure it was asked to render\n try {\n return renderer.toWire(failure);\n } catch (err: unknown) {\n const error = toError(err);\n log.error(\n 'Application McpErrorTranslator.toWire threw; rendering the webpieces default.',\n error,\n );\n return this.defaultToolCall.toWire(failure);\n }\n }\n\n /**\n * webpieces fills `_meta` only when the app left it off, keeping the README's \"every reply\n * carries the requestId\" promise a DEFAULT rather than a restriction: an app that set `_meta`\n * keeps it byte-for-byte.\n */\n private withDefaultMeta(result: CallToolResult): CallToolResult {\n if (result._meta !== undefined) return result;\n return { ...result, _meta: this.resultMeta(McpCorrelation.requestId()) };\n }\n\n private httpError(\n status: number,\n code: number,\n message: string,\n headers: HttpHeader[],\n ): HttpResponseDto<McpHttpErrorBody> {\n const correlation = McpCorrelation.current();\n const requestId = McpCorrelation.requestId();\n return new HttpResponseDto<McpHttpErrorBody>(\n new HttpResponseStatus(status, message),\n headers,\n new McpHttpErrorBody(\n new McpHttpErrorDetail(code, message, new McpErrorData(requestId)),\n correlation.jsonRpcId,\n ),\n );\n }\n}\n"]}
1
+ {"version":3,"file":"WpMcpErrorTranslator.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/WpMcpErrorTranslator.ts"],"names":[],"mappings":";;;AAAA,yDAAgG;AAChG,oDAO8B;AAC9B,+CAA4C;AAC5C,iEAQgC;AAEhC,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAa,oBAAoB;IAQA;IAPZ,QAAQ,GAAG,IAAI,4BAAgB,EAAE,CAAC;IAClC,eAAe,GAAG,IAAI,iDAA0B,EAAE,CAAC;IAEpE;;;OAGG;IACH,YAA6B,SAAuB;QAAvB,cAAS,GAAT,SAAS,CAAc;IAAG,CAAC;IAExD;;;;;;OAMG;IACH,wBAAwB,CAAC,KAAY;QACjC,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,cAAc;gBACf,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE;oBACzC,IAAI,sBAAU,CAAC,kBAAkB,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;iBACvD,CAAC,CAAC;YACP,KAAK,WAAW;gBACZ,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;YACrD,KAAK,aAAa;gBACd,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,0BAAiB,CAAC,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC;YAC1E;gBACI,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,0BAAiB,CAAC,aAAa,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAC;QAC1F,CAAC;IACL,CAAC;IAED;;;;;;;;;;OAUG;IACH,WAAW,CAAC,KAAY;QACpB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC5C,MAAM,SAAS,GAAG,qCAAc,CAAC,SAAS,EAAE,CAAC;QAC7C,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YACjC,MAAM,IAAI,sBAAa,CACnB,0BAAiB,CAAC,aAAa,EAC/B,OAAO,CAAC,aAAa,IAAI,OAAO,CAAC,OAAO,EACxC,IAAI,mCAAY,CAAC,SAAS,CAAC,CAC9B,CAAC;QACN,CAAC;QACD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC;QACjF,MAAM,IAAI,sBAAa,CACnB,0BAAiB,CAAC,aAAa,EAC/B,OAAO,EACP,IAAI,mCAAY,CAAC,SAAS,CAAC,CAC9B,CAAC;IACN,CAAC;IAED,iGAAiG;IACjG,WAAW,CAAC,QAAgB;QACxB,MAAM,SAAS,GAAG,qCAAc,CAAC,SAAS,EAAE,CAAC;QAC7C,GAAG,CAAC,IAAI,CAAC,qBAAqB,QAAQ,cAAc,SAAS,EAAE,CAAC,CAAC;QACjE,OAAO,IAAI,sBAAa,CACpB,0BAAiB,CAAC,aAAa,EAC/B,iBAAiB,QAAQ,EAAE,EAC3B,IAAI,mCAAY,CAAC,SAAS,CAAC,CAC9B,CAAC;IACN,CAAC;IAED;;;;;;;;OAQG;IACH,gBAAgB,CAAC,OAAc;QAC3B,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,YAAY,CAAC,yBAAW,CAAC,kBAAkB,EAAE,EAAE,OAAO,CAAC,CAAC,CAAC;IAC9F,CAAC;IAED,qFAAqF;IACrF,UAAU,CAAC,SAAiB;QACxB,OAAO,EAAE,CAAC,8CAAuB,CAAC,EAAE,SAAS,EAAE,CAAC;IACpD,CAAC;IAED;;;;;;;OAOG;IACK,YAAY,CAAC,QAA4B,EAAE,OAAc;QAC7D,iJAAiJ;QACjJ,IAAI,CAAC;YACD,OAAO,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,GAAG,CAAC,KAAK,CACL,+EAA+E,EAC/E,KAAK,CACR,CAAC;YACF,OAAO,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAChD,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,eAAe,CAAC,MAAsB;QAC1C,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QAC9C,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,qCAAc,CAAC,SAAS,EAAE,CAAC,EAAE,CAAC;IAC7E,CAAC;IAEO,SAAS,CACb,MAAc,EACd,IAAY,EACZ,OAAe,EACf,OAAqB;QAErB,MAAM,WAAW,GAAG,qCAAc,CAAC,OAAO,EAAE,CAAC;QAC7C,MAAM,SAAS,GAAG,qCAAc,CAAC,SAAS,EAAE,CAAC;QAC7C,OAAO,IAAI,2BAAe,CACtB,IAAI,8BAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,EACvC,OAAO,EACP,IAAI,uCAAgB,CAChB,IAAI,yCAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,mCAAY,CAAC,SAAS,CAAC,CAAC,EAClE,WAAW,CAAC,SAAS,CACxB,CACJ,CAAC;IACN,CAAC;CACJ;AA7ID,oDA6IC","sourcesContent":["import { CallToolResult, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';\nimport {\n ApiErrorBoundary,\n HttpHeader,\n HttpResponseDto,\n HttpResponseStatus,\n LogManager,\n toError,\n} from '@webpieces/core-util';\nimport { McpRegistry } from './McpRegistry';\nimport {\n McpCorrelation,\n McpDefaultToolCallRenderer,\n McpErrorData,\n McpErrorTranslator,\n McpHttpErrorBody,\n McpHttpErrorDetail,\n MCP_REQUEST_ID_META_KEY,\n} from './McpToolCallRendering';\n\nconst log = LogManager.getLogger('WpMcpErrorTranslator');\n\n/**\n * The ONE place every MCP failure becomes a reply, mirroring `WebpiecesDefaultErrorTranslator` for HTTP: an error\n * in, the exact wire shape out. There is one method per BOUNDARY, and the MCP spec is what makes\n * their renderings distinct at the two interception seams (HTTP ingress and shared SDK callbacks):\n *\n * | Boundary | Who writes HTTP | Failure shape |\n * |--------------------------------|-----------------|-------------------------------------|\n * | pre-SDK bearer / Origin / body | **webpieces** | `HttpResponseDto<McpHttpErrorBody>` |\n * | `tools/list` | the SDK | `throw ProtocolError` |\n * | `tools/call` | the SDK | `CallToolResult` with `isError` |\n *\n * Classification and published text are the shared {@link ApiErrorBoundary} rules. Operator logging\n * is NOT here: `LogApiCall` wraps each of those boundaries in `WpMcpServer`, so every failure gets\n * exactly one `[API-server-resp-*]` line with the request and the identity on it — and\n * `WpMcpServer` adds one `log.error` with the stack and requestId for an IMPLEMENTATION-kind failure\n * at tools/list and tools/call before handing it here, because this class renders that kind as\n * \"Internal Error\" and nothing else records WHERE it broke (#1055).\n */\nexport class WpMcpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n private readonly defaultToolCall = new McpDefaultToolCallRenderer();\n\n /**\n * @param challenge - the `WWW-Authenticate` value for a 401, read lazily because\n * `WpMcpServerConfig` is only fully validated at `bind(...)` time.\n */\n constructor(private readonly challenge: () => string) {}\n\n /**\n * Boundary #1: everything webpieces itself rejects before the SDK is involved — bearer\n * verification, `Origin`, a malformed body. This is 401/403/400/500 today and any status\n * tomorrow, so it produces a VALUE like every other API in the framework and `WpMcpServer` hands\n * it to the shared express writer. The BODY stays JSON-RPC shaped because an MCP client expects\n * that; `HttpResponseDto` is generic, and its header LIST carries `WWW-Authenticate`.\n */\n toBearerBoundaryResponse(error: Error): HttpResponseDto<McpHttpErrorBody> {\n const payload = this.boundary.encode(error);\n const generic = payload.message;\n switch (payload.kind) {\n case 'unauthorized':\n return this.httpError(401, -32_000, generic, [\n new HttpHeader('WWW-Authenticate', this.challenge()),\n ]);\n case 'forbidden':\n return this.httpError(403, -32_000, generic, []);\n case 'bad-request':\n return this.httpError(400, ProtocolErrorCode.ParseError, generic, []);\n default:\n return this.httpError(500, ProtocolErrorCode.InternalError, 'Internal Error', []);\n }\n }\n\n /**\n * Boundary #2: `tools/list` has no tool-result channel, so a failure IS a JSON-RPC error. It\n * THROWS rather than returning a value the caller must remember to throw, and is typed `never`\n * so the compiler knows the call is terminal.\n *\n * This is LOAD-BEARING for disclosure, not defence in depth. Measured on\n * `@modelcontextprotocol/server` 2.0.0 and pinned by `McpSdkForeignThrow.spec.ts`: the SDK wraps\n * a foreign (non-`ProtocolError`) throw in `-32603` but copies its `message` onto the wire\n * VERBATIM. So a `tools/list` handler that lets any error escape un-translated publishes that\n * error's operator text to the caller.\n */\n toListError(error: Error): never {\n const payload = this.boundary.encode(error);\n const requestId = McpCorrelation.requestId();\n if (payload.kind === 'bad-request') {\n throw new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n payload.callerMessage ?? payload.message,\n new McpErrorData(requestId),\n );\n }\n const message = payload.kind === 'end-user' ? payload.message : 'Internal Error';\n throw new ProtocolError(\n ProtocolErrorCode.InternalError,\n message,\n new McpErrorData(requestId),\n );\n }\n\n /** tools/call with a name no binding registered: a JSON-RPC protocol error in either MCP era. */\n unknownTool(toolName: string): ProtocolError {\n const requestId = McpCorrelation.requestId();\n log.info(`Unknown MCP tool: ${toolName} requestId=${requestId}`);\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n `Unknown tool: ${toolName}`,\n new McpErrorData(requestId),\n );\n }\n\n /**\n * Boundary #3: every tools/call failure after the tool is found — an `isError: true` result the\n * model sees.\n *\n * ONE unconditional line: {@link McpRegistry.getErrorTranslator} is never undefined, so there is\n * no \"was one registered\" branch. A registered {@link McpErrorTranslator} REPLACES the default and\n * declines by delegating to {@link McpDefaultToolCallRenderer}, so \"no translator registered\" and\n * \"a translator that does not claim this error\" produce byte-identical results.\n */\n toToolCallResult(failure: Error): CallToolResult {\n return this.withDefaultMeta(this.appOrDefault(McpRegistry.getErrorTranslator(), failure));\n }\n\n /** The `_meta` every tools/call result carries so a user can quote its requestId. */\n resultMeta(requestId: string): Record<string, string> {\n return { [MCP_REQUEST_ID_META_KEY]: requestId };\n }\n\n /**\n * An app translator that THROWS must not replace the failure being reported: its own bug is\n * logged here and the ORIGINAL error still renders through the webpieces default, so the model\n * still gets a reply carrying the requestId.\n *\n * This is BUG CONTAINMENT, not a \"did anyone register one\" branch — the same shape as\n * `ClientErrorTranslator.throwIfFailure` on the HTTP side.\n */\n private appOrDefault(renderer: McpErrorTranslator, failure: Error): CallToolResult {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an app translator's own bug must not replace the failure it was asked to render\n try {\n return renderer.toWire(failure);\n } catch (err: unknown) {\n const error = toError(err);\n log.error(\n 'Application McpErrorTranslator.toWire threw; rendering the webpieces default.',\n error,\n );\n return this.defaultToolCall.toWire(failure);\n }\n }\n\n /**\n * webpieces fills `_meta` only when the app left it off, keeping the README's \"every reply\n * carries the requestId\" promise a DEFAULT rather than a restriction: an app that set `_meta`\n * keeps it byte-for-byte.\n */\n private withDefaultMeta(result: CallToolResult): CallToolResult {\n if (result._meta !== undefined) return result;\n return { ...result, _meta: this.resultMeta(McpCorrelation.requestId()) };\n }\n\n private httpError(\n status: number,\n code: number,\n message: string,\n headers: HttpHeader[],\n ): HttpResponseDto<McpHttpErrorBody> {\n const correlation = McpCorrelation.current();\n const requestId = McpCorrelation.requestId();\n return new HttpResponseDto<McpHttpErrorBody>(\n new HttpResponseStatus(status, message),\n headers,\n new McpHttpErrorBody(\n new McpHttpErrorDetail(code, message, new McpErrorData(requestId)),\n correlation.jsonRpcId,\n ),\n );\n }\n}\n"]}
@@ -9,9 +9,9 @@ import { McpBindOptions } from './McpBindOptions';
9
9
  * every shipping client unreachable (issue #969), and the MCP lifecycle spec is explicit that a
10
10
  * server answers with a revision it supports rather than erroring.
11
11
  *
12
- * Error boundary: every failure is mapped by the one `WpMcpErrorTranslator`, and each entry point
13
- * has exactly one catch that only delegates to it: the bind HTTP handler
14
- * (`toBearerBoundaryResponse`), tools/list (`toListError`) and tools/call (`toToolCallResult`).
12
+ * Error boundaries: HTTP ingress owns pre-SDK replies; one shared SDK callback wrapper owns
13
+ * protocol/list/name-resolution errors and authorized tool-result errors. The SDK writes callback
14
+ * replies, so it cannot safely delegate those failures back to HTTP ingress.
15
15
  * `subscriptions/listen` is served entirely by the SDK's listen router, so Webpieces has no handler
16
16
  * (and no catch) there. The external bearer is verified exactly once per POST, at the HTTP
17
17
  * boundary, before the SDK is involved.
@@ -23,6 +23,7 @@ import { McpBindOptions } from './McpBindOptions';
23
23
  */
24
24
  export declare class WpMcpServer<TGrant, TMintRequest> {
25
25
  private readonly config;
26
+ private readonly callerContext;
26
27
  private readonly dispatcher;
27
28
  /** Classifies a failure exactly as the translator will render it, so the two cannot disagree. */
28
29
  private readonly boundary;
@@ -66,10 +67,10 @@ export declare class WpMcpServer<TGrant, TMintRequest> {
66
67
  toolsChanged(): void;
67
68
  get registryRevision(): string | undefined;
68
69
  private createHandler;
69
- /** Entry point #1: the one catch for everything before and around the SDK exchange. */
70
+ /** Each HTTP entry uses the same full-request scope and ingress translation seam. */
70
71
  private handleHttp;
71
- /** Express hands body-parser failures (bad JSON, body too large) here, outside every filter. */
72
72
  private handleBodyFailure;
73
+ private ingress;
73
74
  /** A body express could not parse is a caller input error, reported through the same edge. */
74
75
  private rejectBody;
75
76
  /**
@@ -85,12 +86,12 @@ export declare class WpMcpServer<TGrant, TMintRequest> {
85
86
  private authenticate;
86
87
  private disconnectSignal;
87
88
  private buildSdkServer;
88
- /** Entry point #2: tools/list has no tool-result channel, so failures are JSON-RPC errors. */
89
- private handleListTools;
89
+ /** ONE SDK callback catch implementation; the SDK intercepts foreign throws itself. */
90
+ private sdkBoundary;
90
91
  private listTools;
91
92
  private toolDefinition;
92
93
  /**
93
- * Entry point #3: an unknown tool is a JSON-RPC -32602; every failure after the tool is found
94
+ * SDK tool callback: an unknown or hidden tool is a JSON-RPC -32602; every failure after the tool is found
94
95
  * is an `isError: true` result, identically for local and remote bindings.
95
96
  */
96
97
  private handleCallTool;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WpMcpServer = void 0;
4
+ const McpToolPolicy_1 = require("./McpToolPolicy");
4
5
  const node_crypto_1 = require("node:crypto");
5
6
  const server_1 = require("@modelcontextprotocol/server");
6
7
  const node_1 = require("@modelcontextprotocol/node");
@@ -8,6 +9,7 @@ const express_1 = require("express");
8
9
  const core_util_1 = require("@webpieces/core-util");
9
10
  const core_context_1 = require("@webpieces/core-context");
10
11
  const http_server_1 = require("@webpieces/http-server");
12
+ const http_routing_1 = require("@webpieces/http-routing");
11
13
  const McpAuth_1 = require("./McpAuth");
12
14
  const McpApiDispatcher_1 = require("./McpApiDispatcher");
13
15
  const McpInvocationContext_1 = require("./McpInvocationContext");
@@ -19,6 +21,13 @@ const log = core_util_1.LogManager.getLogger('WpMcpServer');
19
21
  * The request DTO the edge `LogApiCall` lines report. These boundaries run BEFORE (or outside) any
20
22
  * API dispatch, so there is no contract DTO to log; the step name is the fact worth having.
21
23
  */
24
+ class McpSdkOperation {
25
+ name;
26
+ toolStage = false;
27
+ constructor(name) {
28
+ this.name = name;
29
+ }
30
+ }
22
31
  class McpEdgeRequest {
23
32
  step;
24
33
  constructor(step) {
@@ -34,15 +43,17 @@ class McpPostAuthentication {
34
43
  accessToken;
35
44
  credential;
36
45
  disconnectSignal;
37
- constructor(accessToken, credential, disconnectSignal) {
46
+ tools;
47
+ constructor(accessToken, credential, disconnectSignal, tools) {
38
48
  this.accessToken = accessToken;
39
49
  this.credential = credential;
40
50
  this.disconnectSignal = disconnectSignal;
51
+ this.tools = tools;
41
52
  }
42
53
  }
43
54
  /**
44
55
  * The official SDK server with Webpieces-owned tools/list and tools/call handlers. It answers
45
- * `toolInputSchemaJson` from the Webpieces registry so the SDK's pre-dispatch SEP-2243
56
+ * `toolInputSchemaJson` from the request-authorized view so the SDK's pre-dispatch SEP-2243
46
57
  * `Mcp-Param-*` header validation still runs without registering SDK-side tool callbacks.
47
58
  */
48
59
  class WpSdkMcpServer extends server_1.McpServer {
@@ -64,9 +75,9 @@ class WpSdkMcpServer extends server_1.McpServer {
64
75
  * every shipping client unreachable (issue #969), and the MCP lifecycle spec is explicit that a
65
76
  * server answers with a revision it supports rather than erroring.
66
77
  *
67
- * Error boundary: every failure is mapped by the one `WpMcpErrorTranslator`, and each entry point
68
- * has exactly one catch that only delegates to it: the bind HTTP handler
69
- * (`toBearerBoundaryResponse`), tools/list (`toListError`) and tools/call (`toToolCallResult`).
78
+ * Error boundaries: HTTP ingress owns pre-SDK replies; one shared SDK callback wrapper owns
79
+ * protocol/list/name-resolution errors and authorized tool-result errors. The SDK writes callback
80
+ * replies, so it cannot safely delegate those failures back to HTTP ingress.
70
81
  * `subscriptions/listen` is served entirely by the SDK's listen router, so Webpieces has no handler
71
82
  * (and no catch) there. The external bearer is verified exactly once per POST, at the HTTP
72
83
  * boundary, before the SDK is involved.
@@ -78,6 +89,7 @@ class WpSdkMcpServer extends server_1.McpServer {
78
89
  */
79
90
  class WpMcpServer {
80
91
  config;
92
+ callerContext = new http_routing_1.AuthenticatedCallerContext();
81
93
  dispatcher = new McpApiDispatcher_1.McpApiDispatcher();
82
94
  /** Classifies a failure exactly as the translator will render it, so the two cannot disagree. */
83
95
  boundary = new core_util_1.ApiErrorBoundary();
@@ -181,30 +193,21 @@ class WpMcpServer {
181
193
  onerror: (error) => log.warn('MCP protocol request rejected', error),
182
194
  });
183
195
  }
184
- /** Entry point #1: the one catch for everything before and around the SDK exchange. */
196
+ /** Each HTTP entry uses the same full-request scope and ingress translation seam. */
185
197
  async handleHttp(req, res, options) {
186
198
  res.setHeader('X-Accel-Buffering', 'no');
187
- await core_context_1.RequestContext.run(async () => {
188
- new core_context_1.RequestContextHeaders().fillFromRequest(this.toHttpRequest(req));
189
- McpToolCallRendering_1.McpCorrelation.stamp(this.correlationOf(req));
190
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP HTTP entry point; delegates only to WpMcpErrorTranslator
191
- try {
192
- await this.logEdge('serve', () => this.serveExpress(req, res, options));
193
- }
194
- catch (err) {
195
- const error = (0, core_util_1.toError)(err);
196
- this.writeBoundaryError(res, error);
197
- }
198
- });
199
+ await this.ingress(req, res, 'serve', () => this.serveExpress(req, res, options));
199
200
  }
200
- /** Express hands body-parser failures (bad JSON, body too large) here, outside every filter. */
201
201
  async handleBodyFailure(cause, req, res) {
202
+ await this.ingress(req, res, 'body', () => this.rejectBody(cause));
203
+ }
204
+ async ingress(req, res, step, work) {
202
205
  await core_context_1.RequestContext.run(async () => {
203
- new core_context_1.RequestContextHeaders().fillFromRequest(this.toHttpRequest(req));
204
- McpToolCallRendering_1.McpCorrelation.stamp(this.correlationOf(req));
205
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP body entry point; delegates only to WpMcpErrorTranslator
206
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- HTTP response-owning ingress boundary, including request publication and correlation failures
206
207
  try {
207
- await this.logEdge('body', () => this.rejectBody(cause));
208
+ new core_context_1.RequestContextHeaders().fillFromRequest(this.toHttpRequest(req));
209
+ McpToolCallRendering_1.McpCorrelation.stamp(this.correlationOf(req));
210
+ await this.logEdge(step, work);
208
211
  }
209
212
  catch (err) {
210
213
  const error = (0, core_util_1.toError)(err);
@@ -255,7 +258,11 @@ class WpMcpServer {
255
258
  throw new core_util_1.ApiUnauthorizedError('MCP request has no bearer access token.');
256
259
  const credential = await this.config.accessTokenAuthority.verifyAccessToken(token, this.config.resource);
257
260
  this.validateCredential(credential);
258
- return new McpPostAuthentication(token, credential, this.disconnectSignal(req));
261
+ this.callerContext.publish(credential.caller);
262
+ core_context_1.RequestContext.putTrusted(core_util_1.WebpiecesCoreHeaders.SURFACE, 'llm');
263
+ this.callerContext.reconcileWireTrust(false);
264
+ const tools = await McpToolRegistry_1.AuthorizedMcpTools.create(this.requireRegistry(), credential.caller, this.config.endpointJwtAuthority);
265
+ return new McpPostAuthentication(token, credential, this.disconnectSignal(req), tools);
259
266
  }
260
267
  disconnectSignal(req) {
261
268
  const disconnected = new AbortController();
@@ -279,38 +286,39 @@ class WpMcpServer {
279
286
  if (!(authentication instanceof McpPostAuthentication)) {
280
287
  throw new core_util_1.ApiImplementationError('WpMcpServer serves only MCP requests authenticated by its bind boundary.');
281
288
  }
282
- const registry = this.requireRegistry();
283
289
  const server = new WpSdkMcpServer({ name: this.config.name, version: `${this.config.version}+${this.revision}` }, {
284
290
  capabilities: { tools: { listChanged: true } },
285
291
  cacheHints: {
286
292
  'tools/list': { ttlMs: options.deployment.ttlMs, cacheScope: 'private' },
287
293
  'server/discover': { ttlMs: options.deployment.ttlMs, cacheScope: 'private' },
288
294
  },
289
- }, registry);
295
+ }, authentication.tools);
290
296
  server.server.removeRequestHandler('tools/list');
291
297
  server.server.removeRequestHandler('tools/call');
292
- server.server.setRequestHandler('tools/list', async (_request) => this.handleListTools(authentication));
298
+ server.server.setRequestHandler('tools/list', async (_request) => this.sdkBoundary(new McpSdkOperation('tools/list'), () => this.logEdge('tools/list', async () => this.listTools(authentication.tools))));
293
299
  server.server.setRequestHandler('tools/call', async (request, sdkContext) => this.handleCallTool(server, request, authentication, sdkContext));
294
300
  return server;
295
301
  }
296
- /** Entry point #2: tools/list has no tool-result channel, so failures are JSON-RPC errors. */
297
- async handleListTools(authentication) {
298
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP tools/list entry point; delegates only to WpMcpErrorTranslator
302
+ /** ONE SDK callback catch implementation; the SDK intercepts foreign throws itself. */
303
+ async sdkBoundary(operation, work) {
304
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- shared SDK response boundary, preserving protocol versus tool envelopes
299
305
  try {
300
- return await this.logEdge('tools/list', async () => this.listTools(authentication.credential));
306
+ return await work();
301
307
  }
302
308
  catch (err) {
303
309
  const error = (0, core_util_1.toError)(err);
304
- this.logImplementationFailure('tools/list', error);
310
+ if (error instanceof McpToolPolicy_1.McpToolDeniedError)
311
+ throw this.translator.unknownTool(error.toolName);
312
+ if (!operation.toolStage && error instanceof server_1.ProtocolError)
313
+ throw error;
314
+ this.logImplementationFailure(operation.name, error);
315
+ if (operation.toolStage)
316
+ return this.translator.toToolCallResult(error);
305
317
  this.translator.toListError(error);
306
318
  }
307
319
  }
308
- listTools(credential) {
309
- const tools = [];
310
- for (const tool of this.requireRegistry().tools) {
311
- if (tool.isVisibleTo(credential.listingRoles))
312
- tools.push(this.toolDefinition(tool));
313
- }
320
+ listTools(view) {
321
+ const tools = view.tools.map((tool) => this.toolDefinition(tool));
314
322
  return { tools };
315
323
  }
316
324
  toolDefinition(tool) {
@@ -324,27 +332,23 @@ class WpMcpServer {
324
332
  };
325
333
  }
326
334
  /**
327
- * Entry point #3: an unknown tool is a JSON-RPC -32602; every failure after the tool is found
335
+ * SDK tool callback: an unknown or hidden tool is a JSON-RPC -32602; every failure after the tool is found
328
336
  * is an `isError: true` result, identically for local and remote bindings.
329
337
  */
330
338
  async handleCallTool(server, request, authentication, sdkContext) {
331
339
  const name = request.params.name;
332
- McpToolCallRendering_1.McpCorrelation.stamp(new McpToolCallRendering_1.McpCorrelation(sdkContext.mcpReq.id, name));
333
- const tool = this.requireRegistry().find(name);
334
- if (!tool)
335
- throw this.translator.unknownTool(name);
336
- McpToolCallRendering_1.McpCorrelation.stamp(new McpToolCallRendering_1.McpCorrelation(sdkContext.mcpReq.id, name, tool.operation));
337
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP tools/call entry point; delegates only to WpMcpErrorTranslator
338
- try {
340
+ const operation = new McpSdkOperation(`tools/call ${name}`);
341
+ return this.sdkBoundary(operation, async () => {
342
+ McpToolCallRendering_1.McpCorrelation.stamp(new McpToolCallRendering_1.McpCorrelation(sdkContext.mcpReq.id, name));
343
+ const tool = authentication.tools.find(name);
344
+ if (!tool)
345
+ throw this.translator.unknownTool(name);
346
+ McpToolCallRendering_1.McpCorrelation.stamp(new McpToolCallRendering_1.McpCorrelation(sdkContext.mcpReq.id, name, tool.operation));
347
+ operation.toolStage = true;
339
348
  const result = await this.callTool(tool, request, authentication, sdkContext);
340
349
  result._meta = this.translator.resultMeta(McpToolCallRendering_1.McpCorrelation.requestId());
341
350
  return server.server.projectCallToolResult(result, tool.outputSchema);
342
- }
343
- catch (err) {
344
- const error = (0, core_util_1.toError)(err);
345
- this.logImplementationFailure(`tools/call ${name}`, error);
346
- return this.translator.toToolCallResult(error);
347
- }
351
+ });
348
352
  }
349
353
  /**
350
354
  * ONE `log.error` line, with the stack and the requestId, for a failure that is THIS server's bug
@@ -368,12 +372,9 @@ class WpMcpServer {
368
372
  /** Everything a tool call does. No catch in here: failures propagate to handleCallTool. */
369
373
  async callTool(tool, request, authentication, sdkContext) {
370
374
  const credential = authentication.credential;
371
- const endpointJwt = tool.binding.topology === 'local'
372
- ? await this.mintEndpointJwt(tool, authentication)
373
- : undefined;
374
- 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));
375
+ const invocation = new McpInvocationContext_1.McpInvocationContext(sdkContext.mcpReq.id, tool.name, credential.subject, credential.principalRoles, AbortSignal.any([sdkContext.mcpReq.signal, authentication.disconnectSignal]), this.progressReporter(sdkContext));
375
376
  const args = (request.params.arguments ?? {});
376
- const value = await this.dispatcher.call(tool, args, credential, invocation, endpointJwt?.token);
377
+ const value = await this.dispatcher.call(tool, args, credential, invocation, this.config.endpointJwtAuthority, async () => (await this.mintEndpointJwt(tool, authentication)).token);
377
378
  const outputFailure = this.outputSchemas.validate(tool.outputSchema, value);
378
379
  if (outputFailure) {
379
380
  throw new core_util_1.ApiImplementationError(`MCP output schema violation for ${tool.apiClass.name}.${tool.methodName}: ${outputFailure.message}`);