@webpieces/mcp-server 0.4.785 → 0.4.787

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
@@ -164,9 +164,11 @@ hour and MCP access tokens at 30 days.
164
164
 
165
165
  `WpMcpErrorTranslator` is the one place a failure becomes an MCP reply, mirroring
166
166
  `ApiErrorHttpMapper`. Each entry point has exactly one catch that only delegates to it; the dispatcher
167
- and the local/remote invokers have none. Normalization is the shared `ApiErrorBoundary` rule (a
168
- non-`ApiError`, or an `ApiConnectionError`, becomes `ApiImplementationError`), and each failure is
169
- logged once at the same per-kind level as HTTP, with `requestId`, JSON-RPC id and tool name.
167
+ and the local/remote invokers have none. Classification is the shared `ApiErrorBoundary` rule (a
168
+ non-`ApiError`, or a caller-local `ApiConnectionError`, publishes as kind `implementation`), and each
169
+ failure is logged once at the same per-kind level as HTTP, with `requestId`, JSON-RPC id and tool
170
+ name. The boundary never substitutes an object for the thrown error, so the operator line names the
171
+ class that actually failed and an app's `McpErrorTranslators` can `instanceof` its own error classes.
170
172
 
171
173
  | Where | Reply |
172
174
  |---|---|
@@ -209,7 +211,8 @@ class LangMcpErrorTranslators implements McpErrorTranslators {
209
211
  }
210
212
  ```
211
213
 
212
- - `error` is the RAW thrown value, NOT normalized, so `instanceof` on the app's own classes works.
214
+ - `error` is the thrown value itself — webpieces never substitutes another object for it, so
215
+ `instanceof` on the app's own classes works.
213
216
  - Returning `undefined` means "not mine" and the webpieces default renders, unchanged.
214
217
  - A claimed error owns the ENTIRE `CallToolResult` — content, `structuredContent`, `isError`.
215
218
  - webpieces default-fills `_meta["webpieces/requestId"]` only when the returned result has no
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/mcp-server",
3
- "version": "0.4.785",
3
+ "version": "0.4.787",
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.785",
28
- "@webpieces/core-util": "0.4.785",
29
- "@webpieces/http-routing": "0.4.785",
27
+ "@webpieces/core-context": "0.4.787",
28
+ "@webpieces/core-util": "0.4.787",
29
+ "@webpieces/http-routing": "0.4.787",
30
30
  "express": "5.1.0",
31
31
  "reflect-metadata": "0.2.2",
32
32
  "tslib": "2.8.1"
33
33
  },
34
34
  "devDependencies": {
35
- "@webpieces/http-client-node": "0.4.785",
36
- "@webpieces/http-server": "0.4.785"
35
+ "@webpieces/http-client-node": "0.4.787",
36
+ "@webpieces/http-server": "0.4.787"
37
37
  }
38
38
  }
@@ -26,9 +26,9 @@ export declare class McpFailureScope {
26
26
  * claimed error yields the ENTIRE `CallToolResult` — content, `structuredContent`, `isError`, the lot.
27
27
  *
28
28
  * Contract:
29
- * - `error` is the RAW thrown value, NOT normalized: an app must be able to `instanceof` its own
30
- * error classes, which `ApiErrorBoundary.normalize` would have collapsed to
31
- * `ApiImplementationError`.
29
+ * - `error` is the RAW thrown value: an app must be able to `instanceof` its own error classes.
30
+ * `ApiErrorBoundary` never substitutes an object for the thrown error, so there is exactly one
31
+ * error value on this path.
32
32
  * - return `undefined` for "not mine" — webpieces then renders its own default, unchanged.
33
33
  * - webpieces default-fills `_meta['webpieces/requestId']` only when the returned result has NO
34
34
  * `_meta`. An app that sets `_meta` owns it untouched.
@@ -69,8 +69,8 @@ export declare class McpHttpErrorDetail {
69
69
  /**
70
70
  * The ONE place every MCP failure is mapped to a reply, mirroring `ApiErrorHttpMapper` for HTTP.
71
71
  * Each MCP entry point (the bind HTTP handler, tools/list, tools/call) has exactly one catch that
72
- * only delegates here. Normalization and per-kind log levels are the shared `ApiErrorBoundary`
73
- * rules, and published text comes only from `ApiErrorCodec.encode`: this class never puts an
72
+ * only delegates here. Classification and per-kind log levels are the shared `ApiErrorBoundary`
73
+ * rules, and published text comes only from `ApiErrorBoundary.encode`: this class never puts an
74
74
  * error's own message on the wire unless it is an `ApiEndUserError`.
75
75
  */
76
76
  export declare class WpMcpErrorTranslator {
@@ -79,8 +79,8 @@ exports.McpHttpErrorDetail = McpHttpErrorDetail;
79
79
  /**
80
80
  * The ONE place every MCP failure is mapped to a reply, mirroring `ApiErrorHttpMapper` for HTTP.
81
81
  * Each MCP entry point (the bind HTTP handler, tools/list, tools/call) has exactly one catch that
82
- * only delegates here. Normalization and per-kind log levels are the shared `ApiErrorBoundary`
83
- * rules, and published text comes only from `ApiErrorCodec.encode`: this class never puts an
82
+ * only delegates here. Classification and per-kind log levels are the shared `ApiErrorBoundary`
83
+ * rules, and published text comes only from `ApiErrorBoundary.encode`: this class never puts an
84
84
  * error's own message on the wire unless it is an `ApiEndUserError`.
85
85
  */
86
86
  class WpMcpErrorTranslator {
@@ -95,14 +95,14 @@ class WpMcpErrorTranslator {
95
95
  * A failure after response headers were sent can only end the stream.
96
96
  */
97
97
  toHttp(thrown, res, scope, wwwAuthenticate) {
98
- const error = this.boundary.normalize(thrown);
99
- this.boundary.logOperatorDetail(error, scope.describe());
98
+ this.boundary.logOperatorDetail(thrown, scope.describe());
100
99
  if (res.headersSent) {
101
100
  res.end();
102
101
  return;
103
102
  }
104
- const generic = core_util_1.ApiErrorCodec.encode(error).message;
105
- switch (error.kind) {
103
+ const payload = this.boundary.encode(thrown);
104
+ const generic = payload.message;
105
+ switch (payload.kind) {
106
106
  case 'unauthorized':
107
107
  res.setHeader('WWW-Authenticate', wwwAuthenticate);
108
108
  this.writeHttp(res, 401, -32_000, generic, scope);
@@ -123,13 +123,12 @@ class WpMcpErrorTranslator {
123
123
  }
124
124
  /** tools/list and other result-less methods: bad-request → -32602, anything else → -32603. */
125
125
  toProtocolError(thrown, scope) {
126
- const error = this.boundary.normalize(thrown);
127
- this.boundary.logOperatorDetail(error, scope.describe());
128
- const payload = core_util_1.ApiErrorCodec.encode(error);
129
- if (error.kind === 'bad-request') {
126
+ this.boundary.logOperatorDetail(thrown, scope.describe());
127
+ const payload = this.boundary.encode(thrown);
128
+ if (payload.kind === 'bad-request') {
130
129
  return new server_1.ProtocolError(server_1.ProtocolErrorCode.InvalidParams, payload.callerMessage ?? payload.message, new McpErrorData(scope.requestId));
131
130
  }
132
- const message = error.kind === 'end-user' ? payload.message : 'Internal Error';
131
+ const message = payload.kind === 'end-user' ? payload.message : 'Internal Error';
133
132
  return new server_1.ProtocolError(server_1.ProtocolErrorCode.InternalError, message, new McpErrorData(scope.requestId));
134
133
  }
135
134
  /** tools/call with a name no binding registered: a JSON-RPC protocol error per MCP 2026-07-28. */
@@ -146,12 +145,11 @@ class WpMcpErrorTranslator {
146
145
  * whoever renders the reply.
147
146
  */
148
147
  toToolResult(thrown, scope) {
149
- const error = this.boundary.normalize(thrown);
150
- this.boundary.logOperatorDetail(error, scope.describe());
148
+ this.boundary.logOperatorDetail(thrown, scope.describe());
151
149
  const claimed = this.appToolResult(thrown, scope);
152
150
  if (claimed)
153
151
  return this.withDefaultMeta(claimed, scope.requestId);
154
- const visible = this.modelVisible(core_util_1.ApiErrorCodec.encode(error), scope);
152
+ const visible = this.modelVisible(this.boundary.encode(thrown), scope);
155
153
  const text = JSON.stringify(visible);
156
154
  return {
157
155
  content: [{ type: 'text', text }],
@@ -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 { Response } from 'express';\nimport {\n ApiBadRequestError,\n ApiErrorBoundary,\n ApiErrorCodec,\n ApiErrorPayload,\n ApiImplementationError,\n DtoValue,\n LogManager,\n toError,\n} from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('WpMcpErrorTranslator');\n\n/** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */\nexport const MCP_REQUEST_ID_META_KEY = 'webpieces/requestId';\n\n/** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */\nexport class McpErrorData {\n constructor(public readonly requestId: string) {}\n}\n\n/** Correlation facts for one failure; rendered into the single operator log line. */\nexport class McpFailureScope {\n constructor(\n public readonly requestId: string,\n public readonly jsonRpcId: string | number | null,\n public readonly method: string,\n public readonly toolName?: string,\n ) {}\n\n describe(): string {\n const tool = this.toolName ? ` tool=${this.toolName}` : '';\n return `requestId=${this.requestId} jsonRpcId=${String(this.jsonRpcId)} method=${this.method}${tool}`;\n }\n}\n\n/**\n * The application's own `tools/call` error translation — the MCP twin of the `ErrorTranslators` that\n * `ExpressWrapper.handleError` consults through `ClientRegistry.tryTranslateToWire` on the HTTP path.\n * Register one with `WpMcpServerConfig.setErrorTranslator(...)`; it is NOT a global, because\n * `WpMcpServer` is constructed by app code and two servers may run in one process.\n *\n * An app owns its error taxonomy and owns how those errors should be explained to a model, so a\n * claimed error yields the ENTIRE `CallToolResult` — content, `structuredContent`, `isError`, the lot.\n *\n * Contract:\n * - `error` is the RAW thrown value, NOT normalized: an app must be able to `instanceof` its own\n * error classes, which `ApiErrorBoundary.normalize` would have collapsed to\n * `ApiImplementationError`.\n * - return `undefined` for \"not mine\" — webpieces then renders its own default, unchanged.\n * - webpieces default-fills `_meta['webpieces/requestId']` only when the returned result has NO\n * `_meta`. An app that sets `_meta` owns it untouched.\n * - operator-detail logging has ALREADY happened when this is called, so claiming an error can never\n * silently kill observability.\n * - scope is `tools/call` ONLY. The pre-SDK HTTP boundary and `tools/list` stay framework-owned:\n * that boundary emits the `401 + WWW-Authenticate: Bearer resource_metadata=...` MCP clients\n * depend on for OAuth discovery, and an app rewriting it breaks connector onboarding.\n */\nexport interface McpErrorTranslators {\n toToolResult(error: Error, scope: McpFailureScope): CallToolResult | undefined;\n}\n\n/** The only error shape rendered into model-visible MCP tool content. */\nexport class ModelVisibleToolError {\n constructor(\n public readonly kind: string,\n public readonly message: string,\n public readonly requestId: string,\n public readonly field?: string,\n public readonly callerMessage?: string,\n public readonly errorCode?: string,\n public readonly retryAfterSeconds?: number,\n public readonly statusCode?: number,\n ) {}\n}\n\n/** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */\nexport class McpHttpErrorBody {\n readonly jsonrpc = '2.0';\n constructor(\n public readonly error: McpHttpErrorDetail,\n public readonly id: string | number | null,\n ) {}\n}\n\nexport class McpHttpErrorDetail {\n constructor(\n public readonly code: number,\n public readonly message: string,\n public readonly data: McpErrorData,\n ) {}\n}\n\n/**\n * The ONE place every MCP failure is mapped to a reply, mirroring `ApiErrorHttpMapper` for HTTP.\n * Each MCP entry point (the bind HTTP handler, tools/list, tools/call) has exactly one catch that\n * only delegates here. Normalization and per-kind log levels are the shared `ApiErrorBoundary`\n * rules, and published text comes only from `ApiErrorCodec.encode`: this class never puts an\n * error's own message on the wire unless it is an `ApiEndUserError`.\n */\nexport class WpMcpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary(log);\n\n /** `appTranslators` is the app's `tools/call` seam; `undefined` means webpieces renders all. */\n constructor(private readonly appTranslators?: McpErrorTranslators) {}\n\n /**\n * Pre-SDK failures: 401 + WWW-Authenticate, 403, 400 (-32700), anything else 500 (-32603).\n * A failure after response headers were sent can only end the stream.\n */\n toHttp(thrown: Error, res: Response, scope: McpFailureScope, wwwAuthenticate: string): void {\n const error = this.boundary.normalize(thrown);\n this.boundary.logOperatorDetail(error, scope.describe());\n if (res.headersSent) {\n res.end();\n return;\n }\n const generic = ApiErrorCodec.encode(error).message;\n switch (error.kind) {\n case 'unauthorized':\n res.setHeader('WWW-Authenticate', wwwAuthenticate);\n this.writeHttp(res, 401, -32_000, generic, scope);\n return;\n case 'forbidden':\n this.writeHttp(res, 403, -32_000, generic, scope);\n return;\n case 'bad-request':\n this.writeHttp(res, 400, ProtocolErrorCode.ParseError, generic, scope);\n return;\n default:\n this.writeHttp(res, 500, ProtocolErrorCode.InternalError, 'Internal Error', scope);\n }\n }\n\n /** Express body-parser failures (malformed JSON, body too large) are caller input errors. */\n toHttpBodyFailure(cause: Error, res: Response, scope: McpFailureScope, wwwAuth: string): void {\n this.toHttp(\n new ApiBadRequestError(\n `MCP request body rejected: ${cause.message}`,\n undefined,\n undefined,\n cause,\n ),\n res,\n scope,\n wwwAuth,\n );\n }\n\n /** tools/list and other result-less methods: bad-request → -32602, anything else → -32603. */\n toProtocolError(thrown: Error, scope: McpFailureScope): ProtocolError {\n const error = this.boundary.normalize(thrown);\n this.boundary.logOperatorDetail(error, scope.describe());\n const payload = ApiErrorCodec.encode(error);\n if (error.kind === 'bad-request') {\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n payload.callerMessage ?? payload.message,\n new McpErrorData(scope.requestId),\n );\n }\n const message = error.kind === 'end-user' ? payload.message : 'Internal Error';\n return new ProtocolError(\n ProtocolErrorCode.InternalError,\n message,\n new McpErrorData(scope.requestId),\n );\n }\n\n /** tools/call with a name no binding registered: a JSON-RPC protocol error per MCP 2026-07-28. */\n unknownTool(toolName: string, scope: McpFailureScope): ProtocolError {\n log.info(`Unknown MCP tool: ${scope.describe()}`);\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n `Unknown tool: ${toolName}`,\n new McpErrorData(scope.requestId),\n );\n }\n\n /**\n * Every tools/call failure after the tool is found: an `isError: true` result the model sees.\n *\n * The app's {@link McpErrorTranslators} gets FIRST REFUSAL — the same convention\n * `ExpressWrapper.handleError` applies on the HTTP path — and owns the entire result it claims.\n * Operator-detail logging runs BEFORE it, so exactly one operator line is written per failure\n * whoever renders the reply.\n */\n toToolResult(thrown: Error, scope: McpFailureScope): CallToolResult {\n const error = this.boundary.normalize(thrown);\n this.boundary.logOperatorDetail(error, scope.describe());\n const claimed = this.appToolResult(thrown, scope);\n if (claimed) return this.withDefaultMeta(claimed, scope.requestId);\n const visible = this.modelVisible(ApiErrorCodec.encode(error), scope);\n const text = JSON.stringify(visible as DtoValue);\n return {\n content: [{ type: 'text', text }],\n isError: true,\n _meta: this.resultMeta(scope.requestId),\n };\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 * The app's first refusal. A translator that THROWS must not replace the failure being reported:\n * the throw is reported through the same boundary as its own implementation failure and the\n * ORIGINAL error still renders through the webpieces default, so the model still gets a reply\n * carrying the requestId.\n */\n private appToolResult(thrown: Error, scope: McpFailureScope): CallToolResult | undefined {\n if (!this.appTranslators) return undefined;\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 this.appTranslators.toToolResult(thrown, scope);\n } catch (err: unknown) {\n const error = toError(err);\n const failure = new ApiImplementationError(\n 'Application McpErrorTranslators.toToolResult threw; rendering the webpieces default.',\n error,\n );\n this.boundary.logOperatorDetail(failure, scope.describe());\n return undefined;\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, requestId: string): CallToolResult {\n if (result._meta !== undefined) return result;\n return { ...result, _meta: this.resultMeta(requestId) };\n }\n\n private modelVisible(payload: ApiErrorPayload, scope: McpFailureScope): ModelVisibleToolError {\n switch (payload.kind) {\n case 'end-user':\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n scope.requestId,\n undefined,\n undefined,\n payload.errorCode,\n );\n case 'bad-request':\n return new ModelVisibleToolError(\n payload.kind,\n payload.callerMessage ?? payload.message,\n scope.requestId,\n payload.field,\n payload.callerMessage,\n );\n case 'coded':\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n scope.requestId,\n undefined,\n undefined,\n payload.errorCode,\n undefined,\n payload.statusCode,\n );\n case 'implementation':\n return new ModelVisibleToolError(\n payload.kind,\n `Internal error in tool ${scope.toolName ?? 'unknown'} (requestId ${scope.requestId}). ` +\n 'This is a bug in the tool, not in your arguments; retrying with different ' +\n 'arguments will not help.',\n scope.requestId,\n );\n default:\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n scope.requestId,\n undefined,\n undefined,\n undefined,\n payload.retryAfterSeconds,\n );\n }\n }\n\n private writeHttp(\n res: Response,\n status: number,\n code: number,\n message: string,\n scope: McpFailureScope,\n ): void {\n res.status(status).json(\n new McpHttpErrorBody(\n new McpHttpErrorDetail(code, message, new McpErrorData(scope.requestId)),\n scope.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;AAEhG,oDAQ8B;AAE9B,MAAM,GAAG,GAAG,sBAAU,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;AAEzD,mGAAmG;AACtF,QAAA,uBAAuB,GAAG,qBAAqB,CAAC;AAE7D,6FAA6F;AAC7F,MAAa,YAAY;IACO;IAA5B,YAA4B,SAAiB;QAAjB,cAAS,GAAT,SAAS,CAAQ;IAAG,CAAC;CACpD;AAFD,oCAEC;AAED,qFAAqF;AACrF,MAAa,eAAe;IAEJ;IACA;IACA;IACA;IAJpB,YACoB,SAAiB,EACjB,SAAiC,EACjC,MAAc,EACd,QAAiB;QAHjB,cAAS,GAAT,SAAS,CAAQ;QACjB,cAAS,GAAT,SAAS,CAAwB;QACjC,WAAM,GAAN,MAAM,CAAQ;QACd,aAAQ,GAAR,QAAQ,CAAS;IAClC,CAAC;IAEJ,QAAQ;QACJ,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,OAAO,aAAa,IAAI,CAAC,SAAS,cAAc,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,IAAI,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;IAC1G,CAAC;CACJ;AAZD,0CAYC;AA4BD,yEAAyE;AACzE,MAAa,qBAAqB;IAEV;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IARpB,YACoB,IAAY,EACZ,OAAe,EACf,SAAiB,EACjB,KAAc,EACd,aAAsB,EACtB,SAAkB,EAClB,iBAA0B,EAC1B,UAAmB;QAPnB,SAAI,GAAJ,IAAI,CAAQ;QACZ,YAAO,GAAP,OAAO,CAAQ;QACf,cAAS,GAAT,SAAS,CAAQ;QACjB,UAAK,GAAL,KAAK,CAAS;QACd,kBAAa,GAAb,aAAa,CAAS;QACtB,cAAS,GAAT,SAAS,CAAS;QAClB,sBAAiB,GAAjB,iBAAiB,CAAS;QAC1B,eAAU,GAAV,UAAU,CAAS;IACpC,CAAC;CACP;AAXD,sDAWC;AAED,mFAAmF;AACnF,MAAa,gBAAgB;IAGL;IACA;IAHX,OAAO,GAAG,KAAK,CAAC;IACzB,YACoB,KAAyB,EACzB,EAA0B;QAD1B,UAAK,GAAL,KAAK,CAAoB;QACzB,OAAE,GAAF,EAAE,CAAwB;IAC3C,CAAC;CACP;AAND,4CAMC;AAED,MAAa,kBAAkB;IAEP;IACA;IACA;IAHpB,YACoB,IAAY,EACZ,OAAe,EACf,IAAkB;QAFlB,SAAI,GAAJ,IAAI,CAAQ;QACZ,YAAO,GAAP,OAAO,CAAQ;QACf,SAAI,GAAJ,IAAI,CAAc;IACnC,CAAC;CACP;AAND,gDAMC;AAED;;;;;;GAMG;AACH,MAAa,oBAAoB;IAIA;IAHZ,QAAQ,GAAG,IAAI,4BAAgB,CAAC,GAAG,CAAC,CAAC;IAEtD,gGAAgG;IAChG,YAA6B,cAAoC;QAApC,mBAAc,GAAd,cAAc,CAAsB;IAAG,CAAC;IAErE;;;OAGG;IACH,MAAM,CAAC,MAAa,EAAE,GAAa,EAAE,KAAsB,EAAE,eAAuB;QAChF,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC1D,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;YAClB,GAAG,CAAC,GAAG,EAAE,CAAC;YACV,OAAO;QACX,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,cAAc;gBACf,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAAE,eAAe,CAAC,CAAC;gBACnD,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;gBAClD,OAAO;YACX,KAAK,WAAW;gBACZ,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;gBAClD,OAAO;YACX,KAAK,aAAa;gBACd,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,0BAAiB,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;gBACvE,OAAO;YACX;gBACI,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,0BAAiB,CAAC,aAAa,EAAE,gBAAgB,EAAE,KAAK,CAAC,CAAC;QAC3F,CAAC;IACL,CAAC;IAED,6FAA6F;IAC7F,iBAAiB,CAAC,KAAY,EAAE,GAAa,EAAE,KAAsB,EAAE,OAAe;QAClF,IAAI,CAAC,MAAM,CACP,IAAI,8BAAkB,CAClB,8BAA8B,KAAK,CAAC,OAAO,EAAE,EAC7C,SAAS,EACT,SAAS,EACT,KAAK,CACR,EACD,GAAG,EACH,KAAK,EACL,OAAO,CACV,CAAC;IACN,CAAC;IAED,8FAA8F;IAC9F,eAAe,CAAC,MAAa,EAAE,KAAsB;QACjD,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,OAAO,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;YACjC,OAAO,IAAI,sBAAa,CACpB,0BAAiB,CAAC,aAAa,EAC/B,OAAO,CAAC,aAAa,IAAI,OAAO,CAAC,OAAO,EACxC,IAAI,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,CACpC,CAAC;QACN,CAAC;QACD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC;QACjF,OAAO,IAAI,sBAAa,CACpB,0BAAiB,CAAC,aAAa,EAC/B,OAAO,EACP,IAAI,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,CACpC,CAAC;IACN,CAAC;IAED,kGAAkG;IAClG,WAAW,CAAC,QAAgB,EAAE,KAAsB;QAChD,GAAG,CAAC,IAAI,CAAC,qBAAqB,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAClD,OAAO,IAAI,sBAAa,CACpB,0BAAiB,CAAC,aAAa,EAC/B,iBAAiB,QAAQ,EAAE,EAC3B,IAAI,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,CACpC,CAAC;IACN,CAAC;IAED;;;;;;;OAOG;IACH,YAAY,CAAC,MAAa,EAAE,KAAsB;QAC9C,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAClD,IAAI,OAAO;YAAE,OAAO,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;QACnE,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;QACvE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,OAAmB,CAAC,CAAC;QACjD,OAAO;YACH,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;YACjC,OAAO,EAAE,IAAI;YACb,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,SAAS,CAAC;SAC1C,CAAC;IACN,CAAC;IAED,qFAAqF;IACrF,UAAU,CAAC,SAAiB;QACxB,OAAO,EAAE,CAAC,+BAAuB,CAAC,EAAE,SAAS,EAAE,CAAC;IACpD,CAAC;IAED;;;;;OAKG;IACK,aAAa,CAAC,MAAa,EAAE,KAAsB;QACvD,IAAI,CAAC,IAAI,CAAC,cAAc;YAAE,OAAO,SAAS,CAAC;QAC3C,iJAAiJ;QACjJ,IAAI,CAAC;YACD,OAAO,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC3D,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,mBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,MAAM,OAAO,GAAG,IAAI,kCAAsB,CACtC,sFAAsF,EACtF,KAAK,CACR,CAAC;YACF,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;YAC3D,OAAO,SAAS,CAAC;QACrB,CAAC;IACL,CAAC;IAED;;;;OAIG;IACK,eAAe,CAAC,MAAsB,EAAE,SAAiB;QAC7D,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QAC9C,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;IAC5D,CAAC;IAEO,YAAY,CAAC,OAAwB,EAAE,KAAsB;QACjE,QAAQ,OAAO,CAAC,IAAI,EAAE,CAAC;YACnB,KAAK,UAAU;gBACX,OAAO,IAAI,qBAAqB,CAC5B,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,OAAO,EACf,KAAK,CAAC,SAAS,EACf,SAAS,EACT,SAAS,EACT,OAAO,CAAC,SAAS,CACpB,CAAC;YACN,KAAK,aAAa;gBACd,OAAO,IAAI,qBAAqB,CAC5B,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,aAAa,IAAI,OAAO,CAAC,OAAO,EACxC,KAAK,CAAC,SAAS,EACf,OAAO,CAAC,KAAK,EACb,OAAO,CAAC,aAAa,CACxB,CAAC;YACN,KAAK,OAAO;gBACR,OAAO,IAAI,qBAAqB,CAC5B,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,OAAO,EACf,KAAK,CAAC,SAAS,EACf,SAAS,EACT,SAAS,EACT,OAAO,CAAC,SAAS,EACjB,SAAS,EACT,OAAO,CAAC,UAAU,CACrB,CAAC;YACN,KAAK,gBAAgB;gBACjB,OAAO,IAAI,qBAAqB,CAC5B,OAAO,CAAC,IAAI,EACZ,0BAA0B,KAAK,CAAC,QAAQ,IAAI,SAAS,eAAe,KAAK,CAAC,SAAS,KAAK;oBACpF,4EAA4E;oBAC5E,0BAA0B,EAC9B,KAAK,CAAC,SAAS,CAClB,CAAC;YACN;gBACI,OAAO,IAAI,qBAAqB,CAC5B,OAAO,CAAC,IAAI,EACZ,OAAO,CAAC,OAAO,EACf,KAAK,CAAC,SAAS,EACf,SAAS,EACT,SAAS,EACT,SAAS,EACT,OAAO,CAAC,iBAAiB,CAC5B,CAAC;QACV,CAAC;IACL,CAAC;IAEO,SAAS,CACb,GAAa,EACb,MAAc,EACd,IAAY,EACZ,OAAe,EACf,KAAsB;QAEtB,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CACnB,IAAI,gBAAgB,CAChB,IAAI,kBAAkB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EACxE,KAAK,CAAC,SAAS,CAClB,CACJ,CAAC;IACN,CAAC;CACJ;AAzMD,oDAyMC","sourcesContent":["import { CallToolResult, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';\nimport { Response } from 'express';\nimport {\n ApiBadRequestError,\n ApiErrorBoundary,\n ApiErrorPayload,\n ApiImplementationError,\n DtoValue,\n LogManager,\n toError,\n} from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('WpMcpErrorTranslator');\n\n/** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */\nexport const MCP_REQUEST_ID_META_KEY = 'webpieces/requestId';\n\n/** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */\nexport class McpErrorData {\n constructor(public readonly requestId: string) {}\n}\n\n/** Correlation facts for one failure; rendered into the single operator log line. */\nexport class McpFailureScope {\n constructor(\n public readonly requestId: string,\n public readonly jsonRpcId: string | number | null,\n public readonly method: string,\n public readonly toolName?: string,\n ) {}\n\n describe(): string {\n const tool = this.toolName ? ` tool=${this.toolName}` : '';\n return `requestId=${this.requestId} jsonRpcId=${String(this.jsonRpcId)} method=${this.method}${tool}`;\n }\n}\n\n/**\n * The application's own `tools/call` error translation — the MCP twin of the `ErrorTranslators` that\n * `ExpressWrapper.handleError` consults through `ClientRegistry.tryTranslateToWire` on the HTTP path.\n * Register one with `WpMcpServerConfig.setErrorTranslator(...)`; it is NOT a global, because\n * `WpMcpServer` is constructed by app code and two servers may run in one process.\n *\n * An app owns its error taxonomy and owns how those errors should be explained to a model, so a\n * claimed error yields the ENTIRE `CallToolResult` — content, `structuredContent`, `isError`, the lot.\n *\n * Contract:\n * - `error` is the RAW thrown value: an app must be able to `instanceof` its own error classes.\n * `ApiErrorBoundary` never substitutes an object for the thrown error, so there is exactly one\n * error value on this path.\n * - return `undefined` for \"not mine\" — webpieces then renders its own default, unchanged.\n * - webpieces default-fills `_meta['webpieces/requestId']` only when the returned result has NO\n * `_meta`. An app that sets `_meta` owns it untouched.\n * - operator-detail logging has ALREADY happened when this is called, so claiming an error can never\n * silently kill observability.\n * - scope is `tools/call` ONLY. The pre-SDK HTTP boundary and `tools/list` stay framework-owned:\n * that boundary emits the `401 + WWW-Authenticate: Bearer resource_metadata=...` MCP clients\n * depend on for OAuth discovery, and an app rewriting it breaks connector onboarding.\n */\nexport interface McpErrorTranslators {\n toToolResult(error: Error, scope: McpFailureScope): CallToolResult | undefined;\n}\n\n/** The only error shape rendered into model-visible MCP tool content. */\nexport class ModelVisibleToolError {\n constructor(\n public readonly kind: string,\n public readonly message: string,\n public readonly requestId: string,\n public readonly field?: string,\n public readonly callerMessage?: string,\n public readonly errorCode?: string,\n public readonly retryAfterSeconds?: number,\n public readonly statusCode?: number,\n ) {}\n}\n\n/** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */\nexport class McpHttpErrorBody {\n readonly jsonrpc = '2.0';\n constructor(\n public readonly error: McpHttpErrorDetail,\n public readonly id: string | number | null,\n ) {}\n}\n\nexport class McpHttpErrorDetail {\n constructor(\n public readonly code: number,\n public readonly message: string,\n public readonly data: McpErrorData,\n ) {}\n}\n\n/**\n * The ONE place every MCP failure is mapped to a reply, mirroring `ApiErrorHttpMapper` for HTTP.\n * Each MCP entry point (the bind HTTP handler, tools/list, tools/call) has exactly one catch that\n * only delegates here. Classification and per-kind log levels are the shared `ApiErrorBoundary`\n * rules, and published text comes only from `ApiErrorBoundary.encode`: this class never puts an\n * error's own message on the wire unless it is an `ApiEndUserError`.\n */\nexport class WpMcpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary(log);\n\n /** `appTranslators` is the app's `tools/call` seam; `undefined` means webpieces renders all. */\n constructor(private readonly appTranslators?: McpErrorTranslators) {}\n\n /**\n * Pre-SDK failures: 401 + WWW-Authenticate, 403, 400 (-32700), anything else 500 (-32603).\n * A failure after response headers were sent can only end the stream.\n */\n toHttp(thrown: Error, res: Response, scope: McpFailureScope, wwwAuthenticate: string): void {\n this.boundary.logOperatorDetail(thrown, scope.describe());\n if (res.headersSent) {\n res.end();\n return;\n }\n const payload = this.boundary.encode(thrown);\n const generic = payload.message;\n switch (payload.kind) {\n case 'unauthorized':\n res.setHeader('WWW-Authenticate', wwwAuthenticate);\n this.writeHttp(res, 401, -32_000, generic, scope);\n return;\n case 'forbidden':\n this.writeHttp(res, 403, -32_000, generic, scope);\n return;\n case 'bad-request':\n this.writeHttp(res, 400, ProtocolErrorCode.ParseError, generic, scope);\n return;\n default:\n this.writeHttp(res, 500, ProtocolErrorCode.InternalError, 'Internal Error', scope);\n }\n }\n\n /** Express body-parser failures (malformed JSON, body too large) are caller input errors. */\n toHttpBodyFailure(cause: Error, res: Response, scope: McpFailureScope, wwwAuth: string): void {\n this.toHttp(\n new ApiBadRequestError(\n `MCP request body rejected: ${cause.message}`,\n undefined,\n undefined,\n cause,\n ),\n res,\n scope,\n wwwAuth,\n );\n }\n\n /** tools/list and other result-less methods: bad-request → -32602, anything else → -32603. */\n toProtocolError(thrown: Error, scope: McpFailureScope): ProtocolError {\n this.boundary.logOperatorDetail(thrown, scope.describe());\n const payload = this.boundary.encode(thrown);\n if (payload.kind === 'bad-request') {\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n payload.callerMessage ?? payload.message,\n new McpErrorData(scope.requestId),\n );\n }\n const message = payload.kind === 'end-user' ? payload.message : 'Internal Error';\n return new ProtocolError(\n ProtocolErrorCode.InternalError,\n message,\n new McpErrorData(scope.requestId),\n );\n }\n\n /** tools/call with a name no binding registered: a JSON-RPC protocol error per MCP 2026-07-28. */\n unknownTool(toolName: string, scope: McpFailureScope): ProtocolError {\n log.info(`Unknown MCP tool: ${scope.describe()}`);\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n `Unknown tool: ${toolName}`,\n new McpErrorData(scope.requestId),\n );\n }\n\n /**\n * Every tools/call failure after the tool is found: an `isError: true` result the model sees.\n *\n * The app's {@link McpErrorTranslators} gets FIRST REFUSAL — the same convention\n * `ExpressWrapper.handleError` applies on the HTTP path — and owns the entire result it claims.\n * Operator-detail logging runs BEFORE it, so exactly one operator line is written per failure\n * whoever renders the reply.\n */\n toToolResult(thrown: Error, scope: McpFailureScope): CallToolResult {\n this.boundary.logOperatorDetail(thrown, scope.describe());\n const claimed = this.appToolResult(thrown, scope);\n if (claimed) return this.withDefaultMeta(claimed, scope.requestId);\n const visible = this.modelVisible(this.boundary.encode(thrown), scope);\n const text = JSON.stringify(visible as DtoValue);\n return {\n content: [{ type: 'text', text }],\n isError: true,\n _meta: this.resultMeta(scope.requestId),\n };\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 * The app's first refusal. A translator that THROWS must not replace the failure being reported:\n * the throw is reported through the same boundary as its own implementation failure and the\n * ORIGINAL error still renders through the webpieces default, so the model still gets a reply\n * carrying the requestId.\n */\n private appToolResult(thrown: Error, scope: McpFailureScope): CallToolResult | undefined {\n if (!this.appTranslators) return undefined;\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 this.appTranslators.toToolResult(thrown, scope);\n } catch (err: unknown) {\n const error = toError(err);\n const failure = new ApiImplementationError(\n 'Application McpErrorTranslators.toToolResult threw; rendering the webpieces default.',\n error,\n );\n this.boundary.logOperatorDetail(failure, scope.describe());\n return undefined;\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, requestId: string): CallToolResult {\n if (result._meta !== undefined) return result;\n return { ...result, _meta: this.resultMeta(requestId) };\n }\n\n private modelVisible(payload: ApiErrorPayload, scope: McpFailureScope): ModelVisibleToolError {\n switch (payload.kind) {\n case 'end-user':\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n scope.requestId,\n undefined,\n undefined,\n payload.errorCode,\n );\n case 'bad-request':\n return new ModelVisibleToolError(\n payload.kind,\n payload.callerMessage ?? payload.message,\n scope.requestId,\n payload.field,\n payload.callerMessage,\n );\n case 'coded':\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n scope.requestId,\n undefined,\n undefined,\n payload.errorCode,\n undefined,\n payload.statusCode,\n );\n case 'implementation':\n return new ModelVisibleToolError(\n payload.kind,\n `Internal error in tool ${scope.toolName ?? 'unknown'} (requestId ${scope.requestId}). ` +\n 'This is a bug in the tool, not in your arguments; retrying with different ' +\n 'arguments will not help.',\n scope.requestId,\n );\n default:\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n scope.requestId,\n undefined,\n undefined,\n undefined,\n payload.retryAfterSeconds,\n );\n }\n }\n\n private writeHttp(\n res: Response,\n status: number,\n code: number,\n message: string,\n scope: McpFailureScope,\n ): void {\n res.status(status).json(\n new McpHttpErrorBody(\n new McpHttpErrorDetail(code, message, new McpErrorData(scope.requestId)),\n scope.jsonRpcId,\n ),\n );\n }\n}\n"]}
@@ -119,6 +119,11 @@ let SearchController = class SearchController extends SearchApi {
119
119
  async search(request) {
120
120
  if (request.query === 'internal')
121
121
  throw new Error('database password appeared here');
122
+ // A DOWNSTREAM call of ours failed. From the model's seat that is OUR bug, never a kind of
123
+ // its own on the wire — see #959 and ApiErrorBoundary.apiOutcome.
124
+ if (request.query === 'downstream') {
125
+ throw new core_util_1.ApiConnectionError('ECONNREFUSED private-host:8443');
126
+ }
122
127
  if (request.query === 'human')
123
128
  throw new core_util_1.ApiEndUserError('Safe human message', 'SAFE');
124
129
  if (request.query === 'malformed') {
@@ -1 +1 @@
1
- {"version":3,"file":"WpMcpServerTestFixtures.js","sourceRoot":"","sources":["../../../../../../packages/http/mcp-server/src/__tests__/WpMcpServerTestFixtures.ts"],"names":[],"mappings":";;;;;AAAA,4BAA0B;AAC1B,yCAAuC;AACvC,oDAoB8B;AAC9B,0DAAyD;AACzD,0DAAkF;AAClF,wCAA4E;AAC5E,kEAAiE;AAEpD,QAAA,OAAO,GAAG,sBAAU,CAAC,OAAO,CACrC,eAAe,EACf,iCAAiC,CACpC,CAAC;AACW,QAAA,aAAa,GAAG,gBAAgB,CAAC;AACjC,QAAA,cAAc,GAAG,YAAY,CAAC;AAGpC,IAAM,aAAa,GAAnB,MAAM,aAAa;IAatB,KAAK,CAAU;CAClB,CAAA;AAdY,sCAAa;AAatB;IAZC,IAAA,sBAAU,EACP,IAAI,6BAAiB,CACjB,aAAa,EACb,IAAI,EACJ,SAAS,EACT,KAAK,EACL,SAAS,EACT,SAAS,EACT,SAAS,EACT,IAAI,uBAAW,CAAC,OAAO,CAAC,CAC3B,CACJ;;4CACc;wBAbN,aAAa;IADzB,IAAA,iBAAK,GAAE;GACK,aAAa,CAczB;AAGM,IAAM,cAAc,GAApB,MAAM,cAAc;IAEvB,MAAM,CAAU;IAGhB,MAAM,CAAU;CACnB,CAAA;AANY,wCAAc;AAEvB;IADC,IAAA,sBAAU,EAAC,IAAI,6BAAiB,CAAC,oBAAoB,EAAE,IAAI,CAAC,CAAC;;8CAC9C;AAGhB;IADC,IAAA,sBAAU,EAAC,IAAI,6BAAiB,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;;8CACzC;yBALP,cAAc;IAD1B,IAAA,iBAAK,GAAE;GACK,cAAc,CAM1B;AAGM,IAAe,SAAS,GAAxB,MAAe,SAAS;IAY3B,MAAM,CAAC,QAAuB;QAC1B,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACrC,CAAC;IAcD,KAAK,CAAC,QAAuB;QACzB,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACrC,CAAC;CACJ,CAAA;AA/BqB,8BAAS;AAY3B;IAXC,IAAA,wBAAY,EAAC,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC;IACvC,IAAA,qBAAS,EAAC,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC;IACpC,IAAA,oBAAQ,EAAC,SAAS,EAAE,KAAK,CAAC;IAC1B,IAAA,yBAAa,EAAC,GAAG,EAAE,CAAC,cAAc,CAAC;IACnC,IAAA,qBAAS,EAAC;QACP,IAAI,EAAE,gBAAgB;QACtB,WAAW,EAAE,iDAAiD;QAC9D,YAAY,EAAE,IAAI;QAClB,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,KAAK;KACvB,CAAC;;6CACe,aAAa;;uCAE7B;AAcD;IAZC,IAAA,wBAAY,EAAC,EAAE,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAClC,IAAA,qBAAS,EAAC,EAAE,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC/B,IAAA,oBAAQ,EAAC,QAAQ,EAAE,KAAK,CAAC;IACzB,IAAA,yBAAa,EAAC,GAAG,EAAE,CAAC,cAAc,CAAC;IACnC,IAAA,qBAAS,EAAC;QACP,IAAI,EAAE,cAAc;QACpB,WAAW,EAAE,wBAAwB;QACrC,YAAY,EAAE,KAAK;QACnB,eAAe,EAAE,KAAK;QACtB,cAAc,EAAE,KAAK;QACrB,aAAa,EAAE,KAAK;KACvB,CAAC;;6CACc,aAAa;;sCAE5B;oBA9BiB,SAAS;IAD9B,IAAA,mBAAO,EAAC,WAAW,CAAC;GACC,SAAS,CA+B9B;AAGM,IAAe,eAAe,GAA9B,MAAe,eAAe;IAajC,MAAM,CAAC,QAAuB;QAC1B,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACrC,CAAC;CACJ,CAAA;AAhBqB,0CAAe;AAajC;IAZC,IAAA,wBAAY,EAAC,EAAE,eAAe,EAAE,IAAI,EAAE,CAAC;IACvC,IAAA,sBAAU,GAAE;IACZ,IAAA,oBAAQ,EAAC,SAAS,EAAE,KAAK,CAAC;IAC1B,IAAA,yBAAa,EAAC,GAAG,EAAE,CAAC,cAAc,CAAC;IACnC,IAAA,qBAAS,EAAC;QACP,IAAI,EAAE,eAAe;QACrB,WAAW,EAAE,0BAA0B;QACvC,YAAY,EAAE,KAAK;QACnB,eAAe,EAAE,KAAK;QACtB,cAAc,EAAE,KAAK;QACrB,aAAa,EAAE,KAAK;KACvB,CAAC;;6CACe,aAAa;;6CAE7B;0BAfiB,eAAe;IADpC,IAAA,mBAAO,EAAC,cAAc,CAAC;GACF,eAAe,CAgBpC;AAGM,IAAM,gBAAgB,GAAtB,MAAM,gBAAiB,SAAQ,SAAS;;IAC3C,MAAM,CAAC,eAAe,CAAc;IACpC,MAAM,CAAC,eAAe,CAAc;IACpC,gBAAgB,GAAG,CAAC,CAAC;IAEZ,KAAK,CAAC,MAAM,CAAC,OAAsB;QACxC,IAAI,OAAO,CAAC,KAAK,KAAK,UAAU;YAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACrF,IAAI,OAAO,CAAC,KAAK,KAAK,OAAO;YAAE,MAAM,IAAI,2BAAe,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC;QACvF,IAAI,OAAO,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;YAChC,MAAM,IAAI,8BAAkB,CACxB,yCAAyC,EACzC,SAAS,EACT,oBAAoB,CACvB,CAAC;QACN,CAAC;QACD,MAAM,UAAU,GAAG,6BAAc,CAAC,UAAU,CAAC,6CAAsB,CAAC,CAAC;QACrE,IAAI,OAAO,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YAC/B,MAAM,UAAU,EAAE,cAAc,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC;QACxD,CAAC;QACD,IAAI,OAAO,CAAC,KAAK,KAAK,MAAM,IAAI,UAAU,EAAE,CAAC;YACzC,MAAM,UAAU,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC;YACnD,kBAAgB,CAAC,eAAe,EAAE,EAAE,CAAC;YACrC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAmB,EAAE,EAAE;gBAC5C,MAAM,MAAM,GAAG,GAAS,EAAE;oBACtB,kBAAgB,CAAC,eAAe,EAAE,EAAE,CAAC;oBACrC,OAAO,EAAE,CAAC;gBACd,CAAC,CAAC;gBACF,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO;oBAAE,MAAM,EAAE,CAAC;;oBACnC,UAAU,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;YAC7E,CAAC,CAAC,CAAC;QACP,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,6BAAc,CAAC,UAAU,CAAC,eAAO,CAAC,IAAI,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;IAC9F,CAAC;IAEQ,KAAK,CAAC,KAAK,CAAC,OAAsB;QACvC,IAAI,CAAC,gBAAgB,IAAI,CAAC,CAAC;QAC3B,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;CACJ,CAAA;AAtCY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,sBAAU,GAAE;GACA,gBAAgB,CAsC5B;AAED,MAAa,kBAAmB,SAAQ,eAAe;IACnD,iBAAiB,CAAU;IAElB,KAAK,CAAC,MAAM,CAAC,OAAsB;QACxC,IAAI,CAAC,iBAAiB,GAAG,6BAAc,CAAC,UAAU,EAAE,EAAE,SAAS,CAAC,eAAe,CAAC,CAAC;QACjF,OAAO,EAAE,MAAM,EAAE,6BAAc,CAAC,UAAU,CAAC,eAAO,CAAC,IAAI,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;IAC9F,CAAC;CACJ;AAPD,gDAOC;AAED,MAAa,WAAY,SAAQ,sBAAe;IAC5C,YAAY,GAAa,EAAE,CAAC;IAC5B,eAAe,GAAG,EAAE,CAAC;IACrB,QAAQ,CAAS;IAER,KAAK,CAAC,IAAI,CAAC,OAAe;QAC/B,IAAI,IAAI,CAAC,QAAQ;YAAE,MAAM,IAAI,CAAC,QAAQ,CAAC;QACvC,MAAM,KAAK,GACP,OAAO,KAAK,aAAa;YACrB,CAAC,CAAC,iBAAiB;YACnB,CAAC,CAAC,GAAG,OAAO,aAAa,IAAI,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAChE,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,OAAO,IAAI,wBAAS,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,eAAe,CAAC,CAAC;IACtF,CAAC;IAEQ,KAAK,CAAC,QAAQ,CAAC,KAAa;QACjC,IAAI,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACtC,OAAO,IAAI,kCAAmB,CAC1B,SAAS,EACT,CAAC,OAAO,CAAC,EACT,CAAC,IAAI,wBAAY,CAAC,eAAO,EAAE,SAAS,CAAC,CAAC,CACzC,CAAC;QACN,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,gBAAgB,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QACnF,OAAO,IAAI,kCAAmB,CAAC,QAAQ,EAAE,EAAE,EAAE,CAAC,IAAI,wBAAY,CAAC,eAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IACxF,CAAC;CACJ;AA1BD,kCA0BC;AAED,MAAa,kBAAkB;IAC3B,iBAAiB,GAAG,CAAC,CAAC;IACtB,KAAK,GAAsB,EAAE,CAAC;IAE9B,KAAK,CAAC,eAAe,CAAC,MAAc;QAChC,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,KAAa,EAAE,QAAgB;QACnD,IAAI,CAAC,iBAAiB,IAAI,CAAC,CAAC;QAC5B,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,IAAI,gCAAoB,CAAC,eAAe,CAAC,CAAC;QACzE,IAAI,KAAK,KAAK,eAAe;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QACzE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC1C,MAAM,cAAc,GAChB,KAAK,KAAK,gBAAgB,CAAC,CAAC,CAAC,8BAA8B,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC3E,MAAM,OAAO,GAAG,KAAK,KAAK,iBAAiB,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC;QACrE,MAAM,MAAM,GACR,KAAK,KAAK,cAAc,CAAC,CAAC,CAAC,0BAA0B,CAAC,CAAC,CAAC,4BAA4B,CAAC;QACzF,MAAM,SAAS,GAAG,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC;QAC3D,MAAM,MAAM,GAAG,KAAK,KAAK,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,KAAK,KAAK,eAAe,CAAC,CAAC,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC;QAClE,MAAM,UAAU,GAAG,IAAI,+BAAqB,CACxC,OAAO,EACP,MAAM,EACN,cAAc,EACd,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,EACpC,SAAS,EACT,MAAM,EACN,WAAW,EACX,IAAI,CAAC,KAAK,EACV,CAAC,IAAI,wBAAY,CAAC,eAAO,EAAE,GAAG,OAAO,YAAY,CAAC,CAAC,CACtD,CAAC;QACF,IAAI,KAAK,KAAK,UAAU,EAAE,CAAC;YACvB,MAAM,CAAC,cAAc,CAAC,UAAU,EAAE,cAAc,EAAE;gBAC9C,GAAG,EAAE,GAAsB,EAAE;oBACzB,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;gBAC9C,CAAC;aACJ,CAAC,CAAC;QACP,CAAC;QACD,OAAO,UAAU,CAAC;IACtB,CAAC;CACJ;AAzCD,gDAyCC;AAED,MAAa,eAAe;IAEJ;IACA;IACA;IACA;IAJpB,YACoB,MAAc,EACd,KAAe,EACf,OAAe,EACf,KAAa;QAHb,WAAM,GAAN,MAAM,CAAQ;QACd,UAAK,GAAL,KAAK,CAAU;QACf,YAAO,GAAP,OAAO,CAAQ;QACf,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;IAEJ,gGAAgG;IAChG,QAAQ;QACJ,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE,OAAO,IAAI,EAAE,IAAI,IAAI,CAAC,KAAK,EAAE,KAAK,IAAI,EAAE,EAAE,CAAC;IACrF,CAAC;CACJ;AAZD,0CAYC;AAED,MAAM,eAAe;IAEI;IACA;IAFrB,YACqB,IAAY,EACZ,KAAwB;QADxB,SAAI,GAAJ,IAAI,CAAQ;QACZ,UAAK,GAAL,KAAK,CAAmB;IAC1C,CAAC;IAEJ,KAAK,CAAC,OAAe,EAAE,GAAW;QAC9B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3E,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,GAAW;QAC9B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3E,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,GAAW;QAC7B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,GAAW;QAC7B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1E,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,GAAW;QAC9B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3E,CAAC;CACJ;AAED,6FAA6F;AAC7F,MAAa,sBAAsB;IACtB,KAAK,GAAsB,EAAE,CAAC;IAEvC,SAAS,CAAC,IAAY;QAClB,OAAO,IAAI,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;IAED,UAAU,CAAC,IAAY;QACnB,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAqB,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IACxF,CAAC;CACJ;AAVD,wDAUC","sourcesContent":["import 'reflect-metadata';\nimport { injectable } from 'inversify';\nimport {\n ApiBadRequestError,\n ApiEndUserError,\n ApiPath,\n ApiUnauthorizedError,\n Logger,\n LoggerFactory,\n LogLevel,\n ContextKey,\n ContextTuple,\n Endpoint,\n WpAuthJwt,\n WpAuthOidc,\n WpDto,\n WpDtoField,\n WpDtoFieldOptions,\n WpMcpAuthJwt,\n WpMcpHeader,\n WpMcpTool,\n WpResponseDto,\n} from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\nimport { AuthenticatedCaller, JwtHook, MintedJwt } from '@webpieces/http-routing';\nimport { McpAccessTokenAuthority, VerifiedMcpCredential } from '../McpAuth';\nimport { MCP_INVOCATION_CONTEXT } from '../McpInvocationContext';\n\nexport const USER_ID = ContextKey.trusted<string>(\n 'mcpSpecUserId',\n 'proven by a credential verifier',\n);\nexport const ENDPOINT_PATH = '/app-owned/mcp';\nexport const MODERN_VERSION = '2026-07-28';\n\n@WpDto()\nexport class SearchRequest {\n @WpDtoField(\n new WpDtoFieldOptions(\n 'Search text',\n true,\n undefined,\n false,\n undefined,\n undefined,\n undefined,\n new WpMcpHeader('query'),\n ),\n )\n query!: string;\n}\n\n@WpDto()\nexport class SearchResponse {\n @WpDtoField(new WpDtoFieldOptions('Authenticated user', true))\n userId!: string;\n\n @WpDtoField(new WpDtoFieldOptions('Search result', true))\n result!: string;\n}\n\n@ApiPath('/mcp-spec')\nexport abstract class SearchApi {\n @WpMcpAuthJwt({ allRolesAllowed: true })\n @WpAuthJwt({ allRolesAllowed: true })\n @Endpoint('/search', 'rpc')\n @WpResponseDto(() => SearchResponse)\n @WpMcpTool({\n name: 'account_search',\n description: 'Search records owned by the authenticated user.',\n readOnlyHint: true,\n idempotentHint: true,\n openWorldHint: false,\n })\n search(_request: SearchRequest): Promise<SearchResponse> {\n throw new Error('contract only');\n }\n\n @WpMcpAuthJwt({ roles: ['admin'] })\n @WpAuthJwt({ roles: ['admin'] })\n @Endpoint('/admin', 'rpc')\n @WpResponseDto(() => SearchResponse)\n @WpMcpTool({\n name: 'admin_search',\n description: 'Administrative search.',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n })\n admin(_request: SearchRequest): Promise<SearchResponse> {\n throw new Error('contract only');\n }\n}\n\n@ApiPath('/remote-spec')\nexport abstract class RemoteSearchApi {\n @WpMcpAuthJwt({ allRolesAllowed: true })\n @WpAuthOidc()\n @Endpoint('/search', 'rpc')\n @WpResponseDto(() => SearchResponse)\n @WpMcpTool({\n name: 'remote_search',\n description: 'Search a remote binding.',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n })\n search(_request: SearchRequest): Promise<SearchResponse> {\n throw new Error('contract only');\n }\n}\n\n@injectable()\nexport class SearchController extends SearchApi {\n static waitStartedHook?: () => void;\n static waitAbortedHook?: () => void;\n adminInvocations = 0;\n\n override async search(request: SearchRequest): Promise<SearchResponse> {\n if (request.query === 'internal') throw new Error('database password appeared here');\n if (request.query === 'human') throw new ApiEndUserError('Safe human message', 'SAFE');\n if (request.query === 'malformed') {\n throw new ApiBadRequestError(\n 'operator-only: column tenant_7 rejected',\n '$.query',\n 'query is malformed',\n );\n }\n const invocation = RequestContext.getTrusted(MCP_INVOCATION_CONTEXT);\n if (request.query === 'progress') {\n await invocation?.reportProgress?.(1, 2, 'halfway');\n }\n if (request.query === 'wait' && invocation) {\n await invocation.reportProgress?.(0, 1, 'waiting');\n SearchController.waitStartedHook?.();\n await new Promise<void>((resolve: () => void) => {\n const finish = (): void => {\n SearchController.waitAbortedHook?.();\n resolve();\n };\n if (invocation.signal.aborted) finish();\n else invocation.signal.addEventListener('abort', finish, { once: true });\n });\n }\n return { userId: RequestContext.getTrusted(USER_ID) ?? 'missing', result: request.query };\n }\n\n override async admin(request: SearchRequest): Promise<SearchResponse> {\n this.adminInvocations += 1;\n return this.search(request);\n }\n}\n\nexport class RemoteSearchClient extends RemoteSearchApi {\n seenAuthorization?: string;\n\n override async search(request: SearchRequest): Promise<SearchResponse> {\n this.seenAuthorization = RequestContext.getRequest()?.getHeader('authorization');\n return { userId: RequestContext.getTrusted(USER_ID) ?? 'missing', result: request.query };\n }\n}\n\nexport class TestJwtHook extends JwtHook<string> {\n mintedTokens: string[] = [];\n lifetimeSeconds = 60;\n failWith?: Error;\n\n override async mint(subject: string): Promise<MintedJwt> {\n if (this.failWith) throw this.failWith;\n const token =\n subject === 'passthrough'\n ? 'mcp-passthrough'\n : `${subject}-endpoint-${this.mintedTokens.length + 1}`;\n this.mintedTokens.push(token);\n return new MintedJwt(token, Math.floor(Date.now() / 1000) + this.lifetimeSeconds);\n }\n\n override async parseJwt(token: string): Promise<AuthenticatedCaller> {\n if (token.startsWith('admin-endpoint-')) {\n return new AuthenticatedCaller(\n 'admin-7',\n ['admin'],\n [new ContextTuple(USER_ID, 'admin-7')],\n );\n }\n if (!token.startsWith('user-endpoint-')) throw new Error('invalid endpoint token');\n return new AuthenticatedCaller('user-7', [], [new ContextTuple(USER_ID, 'user-7')]);\n }\n}\n\nexport class TestTokenAuthority implements McpAccessTokenAuthority<string> {\n verificationCount = 0;\n roles: readonly string[] = [];\n\n async mintAccessToken(_grant: string): Promise<never> {\n throw new Error('not used');\n }\n\n async verifyAccessToken(token: string, resource: string): Promise<VerifiedMcpCredential> {\n this.verificationCount += 1;\n if (token === 'invalid') throw new ApiUnauthorizedError('invalid token');\n if (token === 'authority-bug') throw new Error('SECRET-internal-detail');\n const now = Math.floor(Date.now() / 1000);\n const actualResource =\n token === 'wrong-resource' ? 'https://attacker.example/mcp' : resource;\n const subject = token === 'mcp-passthrough' ? 'passthrough' : 'user';\n const issuer =\n token === 'wrong-issuer' ? 'https://attacker.example' : 'https://login.example.test';\n const expiresAt = token === 'expired' ? now - 1 : now + 60;\n const scopes = token === 'missing-scope' ? [] : ['tools'];\n const validatedAt = token === 'stale-account' ? now - 3_601 : now;\n const credential = new VerifiedMcpCredential(\n subject,\n issuer,\n actualResource,\n token === 'expired' ? now - 61 : now,\n expiresAt,\n scopes,\n validatedAt,\n this.roles,\n [new ContextTuple(USER_ID, `${subject}-delegated`)],\n );\n if (token === 'list-bug') {\n Object.defineProperty(credential, 'listingRoles', {\n get: (): readonly string[] => {\n throw new Error('SECRET-internal-detail');\n },\n });\n }\n return credential;\n }\n}\n\nexport class RecordedLogLine {\n constructor(\n public readonly logger: string,\n public readonly level: LogLevel,\n public readonly message: string,\n public readonly error?: Error,\n ) {}\n\n /** The rendered line an operator would see, including an attached error's message and stack. */\n rendered(): string {\n return `${this.message} ${this.error?.message ?? ''} ${this.error?.stack ?? ''}`;\n }\n}\n\nclass RecordingLogger implements Logger {\n constructor(\n private readonly name: string,\n private readonly lines: RecordedLogLine[],\n ) {}\n\n trace(message: string, err?: Error): void {\n this.lines.push(new RecordedLogLine(this.name, 'trace', message, err));\n }\n\n debug(message: string, err?: Error): void {\n this.lines.push(new RecordedLogLine(this.name, 'debug', message, err));\n }\n\n info(message: string, err?: Error): void {\n this.lines.push(new RecordedLogLine(this.name, 'info', message, err));\n }\n\n warn(message: string, err?: Error): void {\n this.lines.push(new RecordedLogLine(this.name, 'warn', message, err));\n }\n\n error(message: string, err?: Error): void {\n this.lines.push(new RecordedLogLine(this.name, 'error', message, err));\n }\n}\n\n/** Captures every log line in-process so tests can count and level-check operator output. */\nexport class RecordingLoggerFactory implements LoggerFactory {\n readonly lines: RecordedLogLine[] = [];\n\n getLogger(name: string): Logger {\n return new RecordingLogger(name, this.lines);\n }\n\n containing(text: string): RecordedLogLine[] {\n return this.lines.filter((line: RecordedLogLine) => line.rendered().includes(text));\n }\n}\n"]}
1
+ {"version":3,"file":"WpMcpServerTestFixtures.js","sourceRoot":"","sources":["../../../../../../packages/http/mcp-server/src/__tests__/WpMcpServerTestFixtures.ts"],"names":[],"mappings":"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","sourcesContent":["import 'reflect-metadata';\nimport { injectable } from 'inversify';\nimport {\n ApiBadRequestError,\n ApiConnectionError,\n ApiEndUserError,\n ApiPath,\n ApiUnauthorizedError,\n Logger,\n LoggerFactory,\n LogLevel,\n ContextKey,\n ContextTuple,\n Endpoint,\n WpAuthJwt,\n WpAuthOidc,\n WpDto,\n WpDtoField,\n WpDtoFieldOptions,\n WpMcpAuthJwt,\n WpMcpHeader,\n WpMcpTool,\n WpResponseDto,\n} from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\nimport { AuthenticatedCaller, JwtHook, MintedJwt } from '@webpieces/http-routing';\nimport { McpAccessTokenAuthority, VerifiedMcpCredential } from '../McpAuth';\nimport { MCP_INVOCATION_CONTEXT } from '../McpInvocationContext';\n\nexport const USER_ID = ContextKey.trusted<string>(\n 'mcpSpecUserId',\n 'proven by a credential verifier',\n);\nexport const ENDPOINT_PATH = '/app-owned/mcp';\nexport const MODERN_VERSION = '2026-07-28';\n\n@WpDto()\nexport class SearchRequest {\n @WpDtoField(\n new WpDtoFieldOptions(\n 'Search text',\n true,\n undefined,\n false,\n undefined,\n undefined,\n undefined,\n new WpMcpHeader('query'),\n ),\n )\n query!: string;\n}\n\n@WpDto()\nexport class SearchResponse {\n @WpDtoField(new WpDtoFieldOptions('Authenticated user', true))\n userId!: string;\n\n @WpDtoField(new WpDtoFieldOptions('Search result', true))\n result!: string;\n}\n\n@ApiPath('/mcp-spec')\nexport abstract class SearchApi {\n @WpMcpAuthJwt({ allRolesAllowed: true })\n @WpAuthJwt({ allRolesAllowed: true })\n @Endpoint('/search', 'rpc')\n @WpResponseDto(() => SearchResponse)\n @WpMcpTool({\n name: 'account_search',\n description: 'Search records owned by the authenticated user.',\n readOnlyHint: true,\n idempotentHint: true,\n openWorldHint: false,\n })\n search(_request: SearchRequest): Promise<SearchResponse> {\n throw new Error('contract only');\n }\n\n @WpMcpAuthJwt({ roles: ['admin'] })\n @WpAuthJwt({ roles: ['admin'] })\n @Endpoint('/admin', 'rpc')\n @WpResponseDto(() => SearchResponse)\n @WpMcpTool({\n name: 'admin_search',\n description: 'Administrative search.',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n })\n admin(_request: SearchRequest): Promise<SearchResponse> {\n throw new Error('contract only');\n }\n}\n\n@ApiPath('/remote-spec')\nexport abstract class RemoteSearchApi {\n @WpMcpAuthJwt({ allRolesAllowed: true })\n @WpAuthOidc()\n @Endpoint('/search', 'rpc')\n @WpResponseDto(() => SearchResponse)\n @WpMcpTool({\n name: 'remote_search',\n description: 'Search a remote binding.',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n })\n search(_request: SearchRequest): Promise<SearchResponse> {\n throw new Error('contract only');\n }\n}\n\n@injectable()\nexport class SearchController extends SearchApi {\n static waitStartedHook?: () => void;\n static waitAbortedHook?: () => void;\n adminInvocations = 0;\n\n override async search(request: SearchRequest): Promise<SearchResponse> {\n if (request.query === 'internal') throw new Error('database password appeared here');\n // A DOWNSTREAM call of ours failed. From the model's seat that is OUR bug, never a kind of\n // its own on the wire — see #959 and ApiErrorBoundary.apiOutcome.\n if (request.query === 'downstream') {\n throw new ApiConnectionError('ECONNREFUSED private-host:8443');\n }\n if (request.query === 'human') throw new ApiEndUserError('Safe human message', 'SAFE');\n if (request.query === 'malformed') {\n throw new ApiBadRequestError(\n 'operator-only: column tenant_7 rejected',\n '$.query',\n 'query is malformed',\n );\n }\n const invocation = RequestContext.getTrusted(MCP_INVOCATION_CONTEXT);\n if (request.query === 'progress') {\n await invocation?.reportProgress?.(1, 2, 'halfway');\n }\n if (request.query === 'wait' && invocation) {\n await invocation.reportProgress?.(0, 1, 'waiting');\n SearchController.waitStartedHook?.();\n await new Promise<void>((resolve: () => void) => {\n const finish = (): void => {\n SearchController.waitAbortedHook?.();\n resolve();\n };\n if (invocation.signal.aborted) finish();\n else invocation.signal.addEventListener('abort', finish, { once: true });\n });\n }\n return { userId: RequestContext.getTrusted(USER_ID) ?? 'missing', result: request.query };\n }\n\n override async admin(request: SearchRequest): Promise<SearchResponse> {\n this.adminInvocations += 1;\n return this.search(request);\n }\n}\n\nexport class RemoteSearchClient extends RemoteSearchApi {\n seenAuthorization?: string;\n\n override async search(request: SearchRequest): Promise<SearchResponse> {\n this.seenAuthorization = RequestContext.getRequest()?.getHeader('authorization');\n return { userId: RequestContext.getTrusted(USER_ID) ?? 'missing', result: request.query };\n }\n}\n\nexport class TestJwtHook extends JwtHook<string> {\n mintedTokens: string[] = [];\n lifetimeSeconds = 60;\n failWith?: Error;\n\n override async mint(subject: string): Promise<MintedJwt> {\n if (this.failWith) throw this.failWith;\n const token =\n subject === 'passthrough'\n ? 'mcp-passthrough'\n : `${subject}-endpoint-${this.mintedTokens.length + 1}`;\n this.mintedTokens.push(token);\n return new MintedJwt(token, Math.floor(Date.now() / 1000) + this.lifetimeSeconds);\n }\n\n override async parseJwt(token: string): Promise<AuthenticatedCaller> {\n if (token.startsWith('admin-endpoint-')) {\n return new AuthenticatedCaller(\n 'admin-7',\n ['admin'],\n [new ContextTuple(USER_ID, 'admin-7')],\n );\n }\n if (!token.startsWith('user-endpoint-')) throw new Error('invalid endpoint token');\n return new AuthenticatedCaller('user-7', [], [new ContextTuple(USER_ID, 'user-7')]);\n }\n}\n\nexport class TestTokenAuthority implements McpAccessTokenAuthority<string> {\n verificationCount = 0;\n roles: readonly string[] = [];\n\n async mintAccessToken(_grant: string): Promise<never> {\n throw new Error('not used');\n }\n\n async verifyAccessToken(token: string, resource: string): Promise<VerifiedMcpCredential> {\n this.verificationCount += 1;\n if (token === 'invalid') throw new ApiUnauthorizedError('invalid token');\n if (token === 'authority-bug') throw new Error('SECRET-internal-detail');\n const now = Math.floor(Date.now() / 1000);\n const actualResource =\n token === 'wrong-resource' ? 'https://attacker.example/mcp' : resource;\n const subject = token === 'mcp-passthrough' ? 'passthrough' : 'user';\n const issuer =\n token === 'wrong-issuer' ? 'https://attacker.example' : 'https://login.example.test';\n const expiresAt = token === 'expired' ? now - 1 : now + 60;\n const scopes = token === 'missing-scope' ? [] : ['tools'];\n const validatedAt = token === 'stale-account' ? now - 3_601 : now;\n const credential = new VerifiedMcpCredential(\n subject,\n issuer,\n actualResource,\n token === 'expired' ? now - 61 : now,\n expiresAt,\n scopes,\n validatedAt,\n this.roles,\n [new ContextTuple(USER_ID, `${subject}-delegated`)],\n );\n if (token === 'list-bug') {\n Object.defineProperty(credential, 'listingRoles', {\n get: (): readonly string[] => {\n throw new Error('SECRET-internal-detail');\n },\n });\n }\n return credential;\n }\n}\n\nexport class RecordedLogLine {\n constructor(\n public readonly logger: string,\n public readonly level: LogLevel,\n public readonly message: string,\n public readonly error?: Error,\n ) {}\n\n /** The rendered line an operator would see, including an attached error's message and stack. */\n rendered(): string {\n return `${this.message} ${this.error?.message ?? ''} ${this.error?.stack ?? ''}`;\n }\n}\n\nclass RecordingLogger implements Logger {\n constructor(\n private readonly name: string,\n private readonly lines: RecordedLogLine[],\n ) {}\n\n trace(message: string, err?: Error): void {\n this.lines.push(new RecordedLogLine(this.name, 'trace', message, err));\n }\n\n debug(message: string, err?: Error): void {\n this.lines.push(new RecordedLogLine(this.name, 'debug', message, err));\n }\n\n info(message: string, err?: Error): void {\n this.lines.push(new RecordedLogLine(this.name, 'info', message, err));\n }\n\n warn(message: string, err?: Error): void {\n this.lines.push(new RecordedLogLine(this.name, 'warn', message, err));\n }\n\n error(message: string, err?: Error): void {\n this.lines.push(new RecordedLogLine(this.name, 'error', message, err));\n }\n}\n\n/** Captures every log line in-process so tests can count and level-check operator output. */\nexport class RecordingLoggerFactory implements LoggerFactory {\n readonly lines: RecordedLogLine[] = [];\n\n getLogger(name: string): Logger {\n return new RecordingLogger(name, this.lines);\n }\n\n containing(text: string): RecordedLogLine[] {\n return this.lines.filter((line: RecordedLogLine) => line.rendered().includes(text));\n }\n}\n"]}