@webpieces/mcp-server 0.4.790 → 0.4.792

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
@@ -91,8 +91,10 @@ const config = new WpMcpServerConfig<MyGrant, MyMintRequest>()
91
91
  .setEndpointMintRequest((credential) => new MyMintRequest(credential.subject))
92
92
  .setAuthorizationServers(['https://login.example.com'])
93
93
  .setRequiredScopes(['tools'])
94
- .setErrorTranslator(new MyMcpErrorTranslators())
95
94
  .setMaxAccountValidationAgeSeconds(15 * 60);
95
+
96
+ // The tools/call error translator is a PROCESS-GLOBAL, not a member of this config:
97
+ McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());
96
98
  ```
97
99
 
98
100
  Each setter validates its own value immediately and names itself in the failure: `setResource(...)`
@@ -179,11 +181,11 @@ hour and MCP access tokens at 30 days.
179
181
  ## Error boundary
180
182
 
181
183
  `WpMcpErrorTranslator` is the one place a failure becomes an MCP reply, mirroring
182
- `ApiErrorHttpMapper`: an error in, the exact wire shape out. Each entry point has exactly one catch
184
+ `WebpiecesDefaultErrorTranslator`: an error in, the exact wire shape out. Each entry point has exactly one catch
183
185
  that only delegates to it; the dispatcher and the local/remote invokers have none. Classification is
184
186
  the shared `ApiErrorBoundary.encode` rule (a non-`ApiError`, or a caller-local `ApiConnectionError`,
185
187
  publishes as kind `implementation`). The boundary never substitutes an object for the thrown error,
186
- so an app's `McpErrorTranslators` can `instanceof` its own error classes.
188
+ so an app's `McpErrorTranslator` can `instanceof` its own error classes.
187
189
 
188
190
  There is ONE method per BOUNDARY, named for the boundary it guards. They cannot be one shape — the
189
191
  MCP spec fixes each — so the three-ness is made obvious rather than accidental:
@@ -192,7 +194,7 @@ MCP spec fixes each — so the three-ness is made obvious rather than accidental
192
194
  |---|---|---|
193
195
  | before the SDK: bearer, `Origin`, malformed body | `toBearerBoundaryResponse(error): HttpResponseDto<McpHttpErrorBody>` | HTTP 401 + `WWW-Authenticate` / 403 / 400, anything else 500 |
194
196
  | `tools/list` | `toListError(error): never` | throws JSON-RPC `-32602` (bad request) or `-32603` `Internal Error` |
195
- | `tools/call`, tool found: bad arguments, `@WpMcpAuthJwt` denial, JWT/OIDC mint failure, any local or remote failure, output-schema or serialization failure | `toToolCallResult(error): CallToolResult` | `isError: true` result |
197
+ | `tools/call`, tool found: bad arguments, `@WpMcpAuthJwt` denial, JWT/OIDC mint failure, any local or remote failure, output-schema or serialization failure | `toToolCallResult(error): CallToolResult` (delegates to `McpRegistry.getErrorTranslator().toWire`) | `isError: true` result |
196
198
  | `tools/call`, unknown tool name | `unknownTool(name)` | JSON-RPC `-32602` `Unknown tool: <name>` |
197
199
 
198
200
  The pre-SDK boundary produces a VALUE like every other API in the framework and `WpMcpServer` writes
@@ -234,16 +236,18 @@ Every reply carries the requestId so a user can quote it: `_meta["webpieces/requ
234
236
  An app owns its error taxonomy and owns how those errors should be explained to a model, so a
235
237
  registered translator REPLACES the webpieces default for every `tools/call` failure — the same
236
238
  convention `ExpressWrapper.handleError` applies on the HTTP path. Register one with
237
- `WpMcpServerConfig.setErrorTranslator(...)`; it is a setter and not a global, because `WpMcpServer` is
238
- constructed by app code and two servers may run in one process.
239
+ `McpRegistry.setErrorTranslator(...)`: ONE process-global per protocol, mirroring `ClientRegistry`
240
+ for HTTP and `IpcRegistry` for IPC, so every call site is one unconditional line. (Two MCP servers in
241
+ one process therefore share one translator, and registration order matters rather than being fixed at
242
+ construction — the trade taken deliberately in issue #968 to make all three protocols read alike.)
239
243
 
240
244
  ```ts
241
- class LangMcpErrorTranslators implements McpErrorTranslators {
245
+ class LangMcpErrorTranslator implements McpErrorTranslator {
242
246
  private readonly fallback = new McpDefaultToolCallRenderer();
243
247
 
244
- toToolCallResult(error: Error): CallToolResult {
248
+ toWire(error: Error): CallToolResult {
245
249
  if (!(error instanceof LangPassageLockedError)) {
246
- return this.fallback.toToolCallResult(error); // not mine -> webpieces default
250
+ return this.fallback.toWire(error); // not mine -> webpieces default
247
251
  }
248
252
  return {
249
253
  content: [{ type: 'text', text: `Passage ${error.passageId} is locked.` }],
@@ -270,10 +274,16 @@ class LangMcpErrorTranslators implements McpErrorTranslators {
270
274
  extremely hard to debug.
271
275
 
272
276
  A remote binding's generated Node client turns a dependency's 4xx into the gateway's own
273
- `ApiImplementationError` (see `NodeProxyClient.adaptDownstreamFailure`). A gateway that wants the model
274
- to see the peer's typed 4xx instead registers an `ErrorTranslators` via
275
- `ClientRegistry.setErrorTranslators(...)` whose `fromWire` relays decoded Webpieces payloads; with it,
276
- local and remote bindings produce identical tool results.
277
+ `ApiImplementationError`, and a 5xx into an `ApiDependencyError` (the uniform rule in core-util's
278
+ `ReceivedApiErrorRule`). A gateway that wants the model to see the peer's typed failure instead
279
+ registers an `ErrorTranslator` via `ClientRegistry.setErrorTranslator(...)` whose `fromWire` THROWS
280
+ the decoded Webpieces payload; with it, local and remote bindings produce identical tool results.
281
+
282
+ ### Why there is no `fromWire` on `McpErrorTranslator`
283
+
284
+ The HTTP and IPC translators have two halves because webpieces sits on both ends of those wires.
285
+ **webpieces is never the MCP client** — Claude is — so there is no return path for webpieces to
286
+ translate. The pair is one-directional by nature, not by omission.
277
287
 
278
288
  The verifier's `accountValidatedAtEpochSeconds` must represent an authoritative enabled/revoked and
279
289
  role/scope read. The bridge enforces a maximum one-hour decision age; per-request reads are preferred so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/mcp-server",
3
- "version": "0.4.790",
3
+ "version": "0.4.792",
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.790",
28
- "@webpieces/core-util": "0.4.790",
29
- "@webpieces/http-routing": "0.4.790",
30
- "@webpieces/http-server": "0.4.790",
27
+ "@webpieces/core-context": "0.4.792",
28
+ "@webpieces/core-util": "0.4.792",
29
+ "@webpieces/http-routing": "0.4.792",
30
+ "@webpieces/http-server": "0.4.792",
31
31
  "express": "5.1.0",
32
32
  "reflect-metadata": "0.2.2",
33
33
  "tslib": "2.8.1"
34
34
  },
35
35
  "devDependencies": {
36
- "@webpieces/http-client-node": "0.4.790"
36
+ "@webpieces/http-client-node": "0.4.792"
37
37
  }
38
38
  }
@@ -37,6 +37,12 @@ class McpApiDispatcher {
37
37
  const request = new core_context_1.HttpRequest('POST', `/__webpieces/mcp/${tool.name}`, headers);
38
38
  new core_context_1.RequestContextHeaders().fillFromRequest(request);
39
39
  core_context_1.RequestContext.putTrusted(McpInvocationContext_1.MCP_INVOCATION_CONTEXT, invocation);
40
+ // THIS hop is the edge, and its caller is a model. Stamped here rather than in
41
+ // `AuthFilter` because the local binding's HTTP auth mode describes the bridge's own
42
+ // endpoint JWT, not the LLM on the other end of it — `@WpMcpAuthJwt` is separate
43
+ // metadata and never reaches an `AuthMode`. `AuthFilter` only ever SETS a surface when
44
+ // none arrived, so this one survives the filter chain the invocation runs through.
45
+ core_context_1.RequestContext.putTrusted(core_util_1.WebpiecesCoreHeaders.SURFACE, 'llm');
40
46
  if (tool.binding.topology === 'remote') {
41
47
  for (const tuple of credential.trustedContext)
42
48
  core_context_1.RequestContext.putTrusted(tuple.key, tuple.value);
@@ -1 +1 @@
1
- {"version":3,"file":"McpApiDispatcher.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiDispatcher.ts"],"names":[],"mappings":";;;AAAA,oDAQ8B;AAC9B,0DAA6F;AAG7F,iEAAsF;AAEtF;;;;;GAKG;AACH,MAAa,gBAAgB;IACR,aAAa,GAAG,IAAI,4BAAgB,EAAE,CAAC;IAExD,KAAK,CAAC,IAAI,CACN,IAAuB,EACvB,UAAoB,EACpB,UAAiC,EACjC,UAAgC,EAChC,mBAA4B;QAE5B,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QAChF,IAAI,YAAY,EAAE,CAAC;YACf,MAAM,IAAI,8BAAkB,CACxB,qBAAqB,IAAI,CAAC,IAAI,kBAAkB,IAAI,CAAC,YAAY,CAAC,IAAI,KAAK,YAAY,CAAC,OAAO,EAAE,EACjG,YAAY,CAAC,KAAK,EAClB,YAAY,CAAC,OAAO,CACvB,CAAC;QACN,CAAC;QACD,MAAM,QAAQ,GAAG,IAAA,yBAAa,EAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACzD,IACI,QAAQ,CAAC,MAAM,GAAG,CAAC;YACnB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAC1E,CAAC;YACC,MAAM,IAAI,6BAAiB,CACvB,iBAAiB,UAAU,CAAC,OAAO,8CAA8C,IAAI,CAAC,IAAI,GAAG,CAChG,CAAC;QACN,CAAC;QACD,MAAM,eAAe,GAAG,6BAAc,CAAC,YAAY,CAAC,gCAAoB,CAAC,UAAU,CAAC,CAAC;QACrF,OAAO,6BAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,EAAE;YAC9C,IAAI,eAAe,EAAE,CAAC;gBAClB,6BAAc,CAAC,YAAY,CAAC,gCAAoB,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;YAClF,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;YAC5C,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;gBACpC,IAAI,CAAC,mBAAmB,EAAE,CAAC;oBACvB,MAAM,IAAI,kCAAsB,CAC5B,kBAAkB,IAAI,CAAC,IAAI,uBAAuB,CACrD,CAAC;gBACN,CAAC;gBACD,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,UAAU,mBAAmB,EAAE,CAAC,CAAC,CAAC;YACpE,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,0BAAW,CAAC,MAAM,EAAE,oBAAoB,IAAI,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;YAClF,IAAI,oCAAqB,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YACrD,6BAAc,CAAC,UAAU,CAAC,6CAAsB,EAAE,UAAU,CAAC,CAAC;YAC9D,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACrC,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,cAAc;oBACzC,6BAAc,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;YAC1D,CAAC;YACD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC5D,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AAnDD,4CAmDC","sourcesContent":["import {\n ApiBadRequestError,\n ApiForbiddenError,\n ApiImplementationError,\n DtoSchemaBuilder,\n DtoValue,\n rolesRequired,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { HttpRequest, RequestContext, RequestContextHeaders } from '@webpieces/core-context';\nimport { RegisteredMcpTool } from './McpToolRegistry';\nimport { VerifiedMcpCredential } from './McpAuth';\nimport { MCP_INVOCATION_CONTEXT, McpInvocationContext } from './McpInvocationContext';\n\n/**\n * Executes one tool call through the explicit local or remote Webpieces client, never a controller\n * reference. It has NO try/catch: every failure (bad arguments, MCP authorization, anything the local\n * proxy or the remote generated client throws) propagates as a typed error to the single tools/call\n * boundary in `WpMcpServer`, so local and remote bindings are translated identically.\n */\nexport class McpApiDispatcher {\n private readonly schemaBuilder = new DtoSchemaBuilder();\n\n async call(\n tool: RegisteredMcpTool,\n requestDto: DtoValue,\n credential: VerifiedMcpCredential,\n invocation: McpInvocationContext,\n endpointBearerToken?: string,\n ): Promise<DtoValue> {\n const inputFailure = this.schemaBuilder.validate(tool.requestClass, requestDto);\n if (inputFailure) {\n throw new ApiBadRequestError(\n `MCP arguments for ${tool.name} did not match ${tool.requestClass.name}: ${inputFailure.message}`,\n inputFailure.field,\n inputFailure.message,\n );\n }\n const required = rolesRequired(tool.mcpAuth.requirement);\n if (\n required.length > 0 &&\n !required.some((role: string) => credential.listingRoles.includes(role))\n ) {\n throw new ApiForbiddenError(\n `MCP principal ${credential.subject} lacks a role required by @WpMcpAuthJwt on ${tool.name}.`,\n );\n }\n const parentRequestId = RequestContext.getUntrusted(WebpiecesCoreHeaders.REQUEST_ID);\n return RequestContext.runDetachedScope(async () => {\n if (parentRequestId) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID, parentRequestId);\n }\n const headers = new Map<string, string[]>();\n if (tool.binding.topology === 'local') {\n if (!endpointBearerToken) {\n throw new ApiImplementationError(\n `Local MCP tool ${tool.name} has no endpoint JWT.`,\n );\n }\n headers.set('authorization', [`Bearer ${endpointBearerToken}`]);\n }\n const request = new HttpRequest('POST', `/__webpieces/mcp/${tool.name}`, headers);\n new RequestContextHeaders().fillFromRequest(request);\n RequestContext.putTrusted(MCP_INVOCATION_CONTEXT, invocation);\n if (tool.binding.topology === 'remote') {\n for (const tuple of credential.trustedContext)\n RequestContext.putTrusted(tuple.key, tuple.value);\n }\n return tool.binding.invoke(tool.methodName, requestDto);\n });\n }\n}\n"]}
1
+ {"version":3,"file":"McpApiDispatcher.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpApiDispatcher.ts"],"names":[],"mappings":";;;AAAA,oDAQ8B;AAC9B,0DAA6F;AAG7F,iEAAsF;AAEtF;;;;;GAKG;AACH,MAAa,gBAAgB;IACR,aAAa,GAAG,IAAI,4BAAgB,EAAE,CAAC;IAExD,KAAK,CAAC,IAAI,CACN,IAAuB,EACvB,UAAoB,EACpB,UAAiC,EACjC,UAAgC,EAChC,mBAA4B;QAE5B,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC;QAChF,IAAI,YAAY,EAAE,CAAC;YACf,MAAM,IAAI,8BAAkB,CACxB,qBAAqB,IAAI,CAAC,IAAI,kBAAkB,IAAI,CAAC,YAAY,CAAC,IAAI,KAAK,YAAY,CAAC,OAAO,EAAE,EACjG,YAAY,CAAC,KAAK,EAClB,YAAY,CAAC,OAAO,CACvB,CAAC;QACN,CAAC;QACD,MAAM,QAAQ,GAAG,IAAA,yBAAa,EAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACzD,IACI,QAAQ,CAAC,MAAM,GAAG,CAAC;YACnB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAC1E,CAAC;YACC,MAAM,IAAI,6BAAiB,CACvB,iBAAiB,UAAU,CAAC,OAAO,8CAA8C,IAAI,CAAC,IAAI,GAAG,CAChG,CAAC;QACN,CAAC;QACD,MAAM,eAAe,GAAG,6BAAc,CAAC,YAAY,CAAC,gCAAoB,CAAC,UAAU,CAAC,CAAC;QACrF,OAAO,6BAAc,CAAC,gBAAgB,CAAC,KAAK,IAAI,EAAE;YAC9C,IAAI,eAAe,EAAE,CAAC;gBAClB,6BAAc,CAAC,YAAY,CAAC,gCAAoB,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;YAClF,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,GAAG,EAAoB,CAAC;YAC5C,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;gBACpC,IAAI,CAAC,mBAAmB,EAAE,CAAC;oBACvB,MAAM,IAAI,kCAAsB,CAC5B,kBAAkB,IAAI,CAAC,IAAI,uBAAuB,CACrD,CAAC;gBACN,CAAC;gBACD,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,CAAC,UAAU,mBAAmB,EAAE,CAAC,CAAC,CAAC;YACpE,CAAC;YACD,MAAM,OAAO,GAAG,IAAI,0BAAW,CAAC,MAAM,EAAE,oBAAoB,IAAI,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;YAClF,IAAI,oCAAqB,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YACrD,6BAAc,CAAC,UAAU,CAAC,6CAAsB,EAAE,UAAU,CAAC,CAAC;YAC9D,+EAA+E;YAC/E,qFAAqF;YACrF,iFAAiF;YACjF,uFAAuF;YACvF,mFAAmF;YACnF,6BAAc,CAAC,UAAU,CAAC,gCAAoB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAC/D,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACrC,KAAK,MAAM,KAAK,IAAI,UAAU,CAAC,cAAc;oBACzC,6BAAc,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;YAC1D,CAAC;YACD,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;QAC5D,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AAzDD,4CAyDC","sourcesContent":["import {\n ApiBadRequestError,\n ApiForbiddenError,\n ApiImplementationError,\n DtoSchemaBuilder,\n DtoValue,\n rolesRequired,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { HttpRequest, RequestContext, RequestContextHeaders } from '@webpieces/core-context';\nimport { RegisteredMcpTool } from './McpToolRegistry';\nimport { VerifiedMcpCredential } from './McpAuth';\nimport { MCP_INVOCATION_CONTEXT, McpInvocationContext } from './McpInvocationContext';\n\n/**\n * Executes one tool call through the explicit local or remote Webpieces client, never a controller\n * reference. It has NO try/catch: every failure (bad arguments, MCP authorization, anything the local\n * proxy or the remote generated client throws) propagates as a typed error to the single tools/call\n * boundary in `WpMcpServer`, so local and remote bindings are translated identically.\n */\nexport class McpApiDispatcher {\n private readonly schemaBuilder = new DtoSchemaBuilder();\n\n async call(\n tool: RegisteredMcpTool,\n requestDto: DtoValue,\n credential: VerifiedMcpCredential,\n invocation: McpInvocationContext,\n endpointBearerToken?: string,\n ): Promise<DtoValue> {\n const inputFailure = this.schemaBuilder.validate(tool.requestClass, requestDto);\n if (inputFailure) {\n throw new ApiBadRequestError(\n `MCP arguments for ${tool.name} did not match ${tool.requestClass.name}: ${inputFailure.message}`,\n inputFailure.field,\n inputFailure.message,\n );\n }\n const required = rolesRequired(tool.mcpAuth.requirement);\n if (\n required.length > 0 &&\n !required.some((role: string) => credential.listingRoles.includes(role))\n ) {\n throw new ApiForbiddenError(\n `MCP principal ${credential.subject} lacks a role required by @WpMcpAuthJwt on ${tool.name}.`,\n );\n }\n const parentRequestId = RequestContext.getUntrusted(WebpiecesCoreHeaders.REQUEST_ID);\n return RequestContext.runDetachedScope(async () => {\n if (parentRequestId) {\n RequestContext.putUntrusted(WebpiecesCoreHeaders.REQUEST_ID, parentRequestId);\n }\n const headers = new Map<string, string[]>();\n if (tool.binding.topology === 'local') {\n if (!endpointBearerToken) {\n throw new ApiImplementationError(\n `Local MCP tool ${tool.name} has no endpoint JWT.`,\n );\n }\n headers.set('authorization', [`Bearer ${endpointBearerToken}`]);\n }\n const request = new HttpRequest('POST', `/__webpieces/mcp/${tool.name}`, headers);\n new RequestContextHeaders().fillFromRequest(request);\n RequestContext.putTrusted(MCP_INVOCATION_CONTEXT, invocation);\n // THIS hop is the edge, and its caller is a model. Stamped here rather than in\n // `AuthFilter` because the local binding's HTTP auth mode describes the bridge's own\n // endpoint JWT, not the LLM on the other end of it — `@WpMcpAuthJwt` is separate\n // metadata and never reaches an `AuthMode`. `AuthFilter` only ever SETS a surface when\n // none arrived, so this one survives the filter chain the invocation runs through.\n RequestContext.putTrusted(WebpiecesCoreHeaders.SURFACE, 'llm');\n if (tool.binding.topology === 'remote') {\n for (const tuple of credential.trustedContext)\n RequestContext.putTrusted(tuple.key, tuple.value);\n }\n return tool.binding.invoke(tool.methodName, requestDto);\n });\n }\n}\n"]}
package/src/McpAuth.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { JwtHook } from '@webpieces/http-routing';
2
2
  import { ContextTuple } from '@webpieces/core-util';
3
- import { McpErrorTranslators } from './WpMcpErrorTranslator';
4
3
  export declare const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS: number;
5
4
  export declare const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS: number;
6
5
  export declare const MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS: number;
@@ -91,8 +90,10 @@ export declare class McpProtectedResourceMetadata {
91
90
  * .setEndpointJwtAuthority(jwtHook)
92
91
  * .setEndpointMintRequest((credential) => new MyMintRequest(credential.subject))
93
92
  * .setAuthorizationServers(['https://login.example.com'])
94
- * .setRequiredScopes(['tools'])
95
- * .setErrorTranslator(new MyMcpErrorTranslators());
93
+ * .setRequiredScopes(['tools']);
94
+ *
95
+ * // the tools/call error translator is a PROCESS-GLOBAL, not a member of this config:
96
+ * McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());
96
97
  * ```
97
98
  */
98
99
  export declare class WpMcpServerConfig<TGrant, TMintRequest> {
@@ -104,7 +105,6 @@ export declare class WpMcpServerConfig<TGrant, TMintRequest> {
104
105
  private endpointMintRequestValue?;
105
106
  private authorizationServersValue?;
106
107
  private requiredScopesValue?;
107
- private errorTranslatorValue?;
108
108
  private maxAccountValidationAgeSecondsValue;
109
109
  private maxEndpointJwtLifetimeSecondsValue;
110
110
  /** REQUIRED. The MCP server name reported by `initialize`. */
@@ -142,12 +142,6 @@ export declare class WpMcpServerConfig<TGrant, TMintRequest> {
142
142
  * any tool is dispatched.
143
143
  */
144
144
  setRequiredScopes(requiredScopes: readonly string[]): this;
145
- /**
146
- * OPTIONAL. The application's `tools/call` error translator — it REPLACES the webpieces default
147
- * for every tool failure, owning the ENTIRE result, and declines by delegating to
148
- * `new McpDefaultToolCallRenderer().toToolCallResult(error)`. See {@link McpErrorTranslators}.
149
- */
150
- setErrorTranslator(translators: McpErrorTranslators): this;
151
145
  /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */
152
146
  setMaxAccountValidationAgeSeconds(seconds: number): this;
153
147
  /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */
@@ -165,8 +159,6 @@ export declare class WpMcpServerConfig<TGrant, TMintRequest> {
165
159
  get endpointMintRequest(): McpEndpointMintRequestFactory<TMintRequest>;
166
160
  get authorizationServers(): readonly string[];
167
161
  get requiredScopes(): readonly string[];
168
- /** `undefined` when the app registered none: webpieces then renders every tool failure. */
169
- get errorTranslator(): McpErrorTranslators | undefined;
170
162
  get maxAccountValidationAgeSeconds(): number;
171
163
  get maxEndpointJwtLifetimeSeconds(): number;
172
164
  /**
package/src/McpAuth.js CHANGED
@@ -116,8 +116,10 @@ exports.McpProtectedResourceMetadata = McpProtectedResourceMetadata;
116
116
  * .setEndpointJwtAuthority(jwtHook)
117
117
  * .setEndpointMintRequest((credential) => new MyMintRequest(credential.subject))
118
118
  * .setAuthorizationServers(['https://login.example.com'])
119
- * .setRequiredScopes(['tools'])
120
- * .setErrorTranslator(new MyMcpErrorTranslators());
119
+ * .setRequiredScopes(['tools']);
120
+ *
121
+ * // the tools/call error translator is a PROCESS-GLOBAL, not a member of this config:
122
+ * McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());
121
123
  * ```
122
124
  */
123
125
  class WpMcpServerConfig {
@@ -129,7 +131,6 @@ class WpMcpServerConfig {
129
131
  endpointMintRequestValue;
130
132
  authorizationServersValue;
131
133
  requiredScopesValue;
132
- errorTranslatorValue;
133
134
  maxAccountValidationAgeSecondsValue = exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS;
134
135
  maxEndpointJwtLifetimeSecondsValue = exports.MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS;
135
136
  /** REQUIRED. The MCP server name reported by `initialize`. */
@@ -203,19 +204,6 @@ class WpMcpServerConfig {
203
204
  this.requiredScopesValue = scopes;
204
205
  return this;
205
206
  }
206
- /**
207
- * OPTIONAL. The application's `tools/call` error translator — it REPLACES the webpieces default
208
- * for every tool failure, owning the ENTIRE result, and declines by delegating to
209
- * `new McpDefaultToolCallRenderer().toToolCallResult(error)`. See {@link McpErrorTranslators}.
210
- */
211
- setErrorTranslator(translators) {
212
- const value = this.requirePresent(translators, 'setErrorTranslator');
213
- if (typeof value.toToolCallResult !== 'function') {
214
- throw new Error('WpMcpServerConfig.setErrorTranslator(...) requires a toToolCallResult(error) method.');
215
- }
216
- this.errorTranslatorValue = value;
217
- return this;
218
- }
219
207
  /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */
220
208
  setMaxAccountValidationAgeSeconds(seconds) {
221
209
  this.maxAccountValidationAgeSecondsValue = this.requireSeconds(seconds, exports.MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS, 'setMaxAccountValidationAgeSeconds');
@@ -280,10 +268,6 @@ class WpMcpServerConfig {
280
268
  get requiredScopes() {
281
269
  return this.read(this.requiredScopesValue, 'setRequiredScopes');
282
270
  }
283
- /** `undefined` when the app registered none: webpieces then renders every tool failure. */
284
- get errorTranslator() {
285
- return this.errorTranslatorValue;
286
- }
287
271
  get maxAccountValidationAgeSeconds() {
288
272
  return this.maxAccountValidationAgeSecondsValue;
289
273
  }
@@ -1 +1 @@
1
- {"version":3,"file":"McpAuth.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuth.ts"],"names":[],"mappings":"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","sourcesContent":["import { JwtHook } from '@webpieces/http-routing';\nimport { ContextTuple } from '@webpieces/core-util';\nimport { McpErrorTranslators } from './WpMcpErrorTranslator';\n\nexport const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;\nexport const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = 60 * 60;\nexport const MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS = 60 * 60;\n\n/** Framework-normalized access-token result. The token representation remains application-owned. */\nexport class MintedMcpAccessToken {\n constructor(\n public readonly token: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n ) {\n if (token.trim() === '' || resource.trim() === '') {\n throw new Error('Minted MCP access token and resource must be non-empty.');\n }\n if (!Number.isFinite(issuedAtEpochSeconds) || !Number.isFinite(expiresAtEpochSeconds)) {\n throw new Error(\n 'Minted MCP access token timestamps must be finite epoch-second values.',\n );\n }\n if (expiresAtEpochSeconds <= issuedAtEpochSeconds) {\n throw new Error('MCP access token expiry must be after issuance.');\n }\n if (expiresAtEpochSeconds - issuedAtEpochSeconds > MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS) {\n throw new Error('MCP access token lifetime must not exceed 30 days.');\n }\n }\n}\n\n/**\n * Fresh, authoritative security facts resolved at the MCP resource boundary. Implementations must\n * verify issuer, exact resource/audience, expiry, token type/version, key/algorithm policy, and\n * current account state. Opaque access tokens are fully supported.\n */\nexport class VerifiedMcpCredential {\n constructor(\n public readonly subject: string,\n public readonly issuer: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n public readonly scopes: readonly string[],\n /** When current enabled/revoked state and roles were read from the authoritative source. */\n public readonly accountValidatedAtEpochSeconds: number,\n /** Advisory tools/list filtering only. Endpoint authorization always runs again. */\n public readonly listingRoles: readonly string[] = [],\n /** Trusted delegated identity derived only by the access-token authority. */\n public readonly trustedContext: readonly ContextTuple[] = [],\n ) {}\n}\n\n/**\n * Application-owned paired mint/verify authority for resource-bound MCP access tokens.\n * `verifyAccessToken` must throw `ApiUnauthorizedError` for a token it rejects (the bind boundary\n * answers 401 + `WWW-Authenticate`); any other throw is treated as an implementation failure (500).\n */\nexport interface McpAccessTokenAuthority<TGrant> {\n mintAccessToken(grant: TGrant): Promise<MintedMcpAccessToken>;\n verifyAccessToken(\n accessToken: string,\n expectedResource: string,\n ): Promise<VerifiedMcpCredential>;\n}\n\n/** Minimal tool identity supplied when the app creates its own endpoint-JWT mint request. */\nexport class McpEndpointDescriptor {\n constructor(\n public readonly toolName: string,\n public readonly apiClassName: string,\n public readonly methodName: string,\n ) {}\n}\n\n/** Application mapping from verified MCP identity to its unconstrained JwtHook mint request. */\nexport type McpEndpointMintRequestFactory<TMintRequest> = (\n credential: VerifiedMcpCredential,\n endpoint: McpEndpointDescriptor,\n) => TMintRequest;\n\n/** RFC 9728-style metadata exposed by the MCP protected resource. */\nexport class McpProtectedResourceMetadata {\n readonly resource: string;\n readonly authorization_servers: readonly string[];\n readonly bearer_methods_supported = ['header'];\n readonly scopes_supported?: readonly string[];\n\n constructor(\n resource: string,\n authorizationServers: readonly string[],\n scopesSupported?: readonly string[],\n ) {\n this.resource = resource;\n this.authorization_servers = authorizationServers;\n this.scopes_supported = scopesSupported;\n }\n}\n\n/**\n * Node MCP server configuration, built with fluent setters: every name sits next to its own value, so\n * no two settings can be swapped, and a future setting is an additive setter rather than a breaking\n * signature change. Pass the same concrete application authority to both\n * `setAccessTokenAuthority(...)` and `setEndpointJwtAuthority(...)` when it implements both security\n * seams.\n *\n * SINGLE CANONICAL ORIGIN: one server serves exactly one protected resource. {@link setResource} is\n * fixed for the lifetime of the process, is the audience of every access token, and is never derived\n * from the request `Host` — see that setter for why a host-derived audience is a confused deputy.\n *\n * Each setter validates its own value immediately and names itself in the failure. Setters cannot\n * make a field impossible to forget the way a positional constructor could, so\n * {@link WpMcpServerConfig.validate} runs from `WpMcpServer.bind(...)` and fails at startup listing\n * EVERY missing required setter at once. A misconfiguration is therefore a boot failure naming the\n * setter, instead of a first-request 401 + `WWW-Authenticate` — which is the OAuth discovery signal,\n * so the client's correct response is to re-authenticate and fail again, forever.\n *\n * ```ts\n * const config = new WpMcpServerConfig<MyGrant, MyMintRequest>()\n * .setName('my-server')\n * .setVersion('1.0.0')\n * .setResource('https://api.example.com/mcp')\n * .setAccessTokenAuthority(authority)\n * .setEndpointJwtAuthority(jwtHook)\n * .setEndpointMintRequest((credential) => new MyMintRequest(credential.subject))\n * .setAuthorizationServers(['https://login.example.com'])\n * .setRequiredScopes(['tools'])\n * .setErrorTranslator(new MyMcpErrorTranslators());\n * ```\n */\nexport class WpMcpServerConfig<TGrant, TMintRequest> {\n private nameValue?: string;\n private versionValue?: string;\n private resourceValue?: string;\n private accessTokenAuthorityValue?: McpAccessTokenAuthority<TGrant>;\n private endpointJwtAuthorityValue?: JwtHook<TMintRequest>;\n private endpointMintRequestValue?: McpEndpointMintRequestFactory<TMintRequest>;\n private authorizationServersValue?: readonly string[];\n private requiredScopesValue?: readonly string[];\n private errorTranslatorValue?: McpErrorTranslators;\n private maxAccountValidationAgeSecondsValue = MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS;\n private maxEndpointJwtLifetimeSecondsValue = MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS;\n\n /** REQUIRED. The MCP server name reported by `initialize`. */\n setName(name: string): this {\n this.nameValue = this.requireText(name, 'setName');\n return this;\n }\n\n /** REQUIRED. The MCP server version; the registry revision is appended to it at bind time. */\n setVersion(version: string): this {\n this.versionValue = this.requireText(version, 'setVersion');\n return this;\n }\n\n /**\n * REQUIRED. The ONE canonical protected-resource URI, which must be an absolute URL: it is the\n * exact audience every access token is checked against, and {@link resourceMetadataUrl} is\n * derived from it for the 401 challenge, so a non-URL here is always a mistake — including a\n * name or version landed on it.\n *\n * It is fixed for the lifetime of the process and must NEVER be derived from the request `Host`\n * header: the boundary compares a token's audience against this value, so a host-derived\n * resource compares two values both taken from caller-controlled input — the confused-deputy\n * shape this check exists to prevent. Its path must equal `McpBindOptions.endpointPath`, which\n * `WpMcpServer.bind(...)` verifies. A second hostname is a second deployment, not a second\n * audience.\n */\n setResource(resource: string): this {\n this.resourceValue = this.requireUrl(resource, 'setResource');\n return this;\n }\n\n /** REQUIRED. Application-owned paired mint/verify authority for MCP access tokens. */\n setAccessTokenAuthority(authority: McpAccessTokenAuthority<TGrant>): this {\n this.accessTokenAuthorityValue = this.requirePresent(authority, 'setAccessTokenAuthority');\n return this;\n }\n\n /** REQUIRED. The `JwtHook` minting the short-lived per-endpoint JWT for local bindings. */\n setEndpointJwtAuthority(authority: JwtHook<TMintRequest>): this {\n this.endpointJwtAuthorityValue = this.requirePresent(authority, 'setEndpointJwtAuthority');\n return this;\n }\n\n /** REQUIRED. Maps a verified MCP identity plus tool identity to that JwtHook's mint request. */\n setEndpointMintRequest(factory: McpEndpointMintRequestFactory<TMintRequest>): this {\n if (typeof factory !== 'function') {\n throw new Error('WpMcpServerConfig.setEndpointMintRequest(...) requires a function.');\n }\n this.endpointMintRequestValue = factory;\n return this;\n }\n\n /**\n * REQUIRED. The trusted token issuers. Every entry must be an absolute URL, because each is\n * compared against a token's `issuer` claim and published as an `authorization_servers` entry —\n * which is what makes a scope list landed here fail at boot rather than on the first request.\n */\n setAuthorizationServers(authorizationServers: readonly string[]): this {\n const servers = this.requireArray(authorizationServers, 'setAuthorizationServers');\n if (servers.length === 0) {\n throw new Error(\n 'WpMcpServerConfig.setAuthorizationServers(...) requires at least one issuer URL.',\n );\n }\n for (const server of servers) this.requireUrl(server, 'setAuthorizationServers');\n this.authorizationServersValue = servers;\n return this;\n }\n\n /**\n * REQUIRED, and empty only by saying so explicitly. Scopes every access token must carry before\n * any tool is dispatched.\n */\n setRequiredScopes(requiredScopes: readonly string[]): this {\n const scopes = this.requireArray(requiredScopes, 'setRequiredScopes');\n for (const scope of scopes) this.requireText(scope, 'setRequiredScopes');\n this.requiredScopesValue = scopes;\n return this;\n }\n\n /**\n * OPTIONAL. The application's `tools/call` error translator — it REPLACES the webpieces default\n * for every tool failure, owning the ENTIRE result, and declines by delegating to\n * `new McpDefaultToolCallRenderer().toToolCallResult(error)`. See {@link McpErrorTranslators}.\n */\n setErrorTranslator(translators: McpErrorTranslators): this {\n const value = this.requirePresent(translators, 'setErrorTranslator');\n if (typeof value.toToolCallResult !== 'function') {\n throw new Error(\n 'WpMcpServerConfig.setErrorTranslator(...) requires a toToolCallResult(error) method.',\n );\n }\n this.errorTranslatorValue = value;\n return this;\n }\n\n /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */\n setMaxAccountValidationAgeSeconds(seconds: number): this {\n this.maxAccountValidationAgeSecondsValue = this.requireSeconds(\n seconds,\n MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS,\n 'setMaxAccountValidationAgeSeconds',\n );\n return this;\n }\n\n /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */\n setMaxEndpointJwtLifetimeSeconds(seconds: number): this {\n this.maxEndpointJwtLifetimeSecondsValue = this.requireSeconds(\n seconds,\n MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS,\n 'setMaxEndpointJwtLifetimeSeconds',\n );\n return this;\n }\n\n /**\n * Called by `WpMcpServer.bind(...)`. Throws naming EVERY missing required setter at once, so a\n * half-built config is repaired in one pass rather than one boot failure at a time.\n */\n validate(): void {\n const missing: string[] = [];\n if (this.nameValue === undefined) missing.push('setName(...)');\n if (this.versionValue === undefined) missing.push('setVersion(...)');\n if (this.resourceValue === undefined) missing.push('setResource(...)');\n if (this.accessTokenAuthorityValue === undefined) {\n missing.push('setAccessTokenAuthority(...)');\n }\n if (this.endpointJwtAuthorityValue === undefined) {\n missing.push('setEndpointJwtAuthority(...)');\n }\n if (this.endpointMintRequestValue === undefined) {\n missing.push('setEndpointMintRequest(...)');\n }\n if (this.authorizationServersValue === undefined) {\n missing.push('setAuthorizationServers(...)');\n }\n if (this.requiredScopesValue === undefined) missing.push('setRequiredScopes(...)');\n if (missing.length > 0) {\n throw new Error(`WpMcpServerConfig is missing ${missing.join(', ')}`);\n }\n }\n\n get name(): string {\n return this.read(this.nameValue, 'setName');\n }\n\n get version(): string {\n return this.read(this.versionValue, 'setVersion');\n }\n\n get resource(): string {\n return this.read(this.resourceValue, 'setResource');\n }\n\n get accessTokenAuthority(): McpAccessTokenAuthority<TGrant> {\n return this.read(this.accessTokenAuthorityValue, 'setAccessTokenAuthority');\n }\n\n get endpointJwtAuthority(): JwtHook<TMintRequest> {\n return this.read(this.endpointJwtAuthorityValue, 'setEndpointJwtAuthority');\n }\n\n get endpointMintRequest(): McpEndpointMintRequestFactory<TMintRequest> {\n return this.read(this.endpointMintRequestValue, 'setEndpointMintRequest');\n }\n\n get authorizationServers(): readonly string[] {\n return this.read(this.authorizationServersValue, 'setAuthorizationServers');\n }\n\n get requiredScopes(): readonly string[] {\n return this.read(this.requiredScopesValue, 'setRequiredScopes');\n }\n\n /** `undefined` when the app registered none: webpieces then renders every tool failure. */\n get errorTranslator(): McpErrorTranslators | undefined {\n return this.errorTranslatorValue;\n }\n\n get maxAccountValidationAgeSeconds(): number {\n return this.maxAccountValidationAgeSecondsValue;\n }\n\n get maxEndpointJwtLifetimeSeconds(): number {\n return this.maxEndpointJwtLifetimeSecondsValue;\n }\n\n /**\n * RFC 9728 §3.1: the URL of this resource's protected-resource METADATA DOCUMENT, derived from\n * {@link resource} by inserting `/.well-known/oauth-protected-resource` between origin and path\n * (`https://host/mcp` → `https://host/.well-known/oauth-protected-resource/mcp`).\n *\n * This — never the resource identifier itself — is what RFC 9728 §5.1 defines `resource_metadata`\n * to be, so it is the value webpieces puts in the 401 `WWW-Authenticate` challenge. webpieces\n * mounts no `.well-known` route; publish {@link protectedResourceMetadata} at this URL.\n */\n get resourceMetadataUrl(): string {\n const url = new URL(this.resource);\n const path = url.pathname === '/' ? '' : url.pathname;\n return `${url.origin}/.well-known/oauth-protected-resource${path}`;\n }\n\n protectedResourceMetadata(): McpProtectedResourceMetadata {\n return new McpProtectedResourceMetadata(\n this.resource,\n this.authorizationServers,\n this.requiredScopes,\n );\n }\n\n private requireText(value: string, setter: string): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a non-empty string.`);\n }\n return value;\n }\n\n private requireUrl(value: string, setter: string): string {\n this.requireText(value, setter);\n if (!URL.canParse(value)) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires an absolute URL, got '${value}'.`,\n );\n }\n return value;\n }\n\n private requireArray(value: readonly string[], setter: string): readonly string[] {\n if (!Array.isArray(value)) {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires an array of strings.`);\n }\n return value;\n }\n\n private requireSeconds(value: number, ceiling: number, setter: string): number {\n if (!Number.isFinite(value) || value <= 0 || value > ceiling) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires 1..${ceiling} seconds, got ${String(value)}.`,\n );\n }\n return value;\n }\n\n private requirePresent<T>(value: T, setter: string): T {\n if (value === undefined || value === null) {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a value.`);\n }\n return value;\n }\n\n private read<T>(value: T | undefined, setter: string): T {\n if (value === undefined) {\n throw new Error(`WpMcpServerConfig is missing ${setter}(...)`);\n }\n return value;\n }\n}\n"]}
1
+ {"version":3,"file":"McpAuth.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpAuth.ts"],"names":[],"mappings":"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","sourcesContent":["import { JwtHook } from '@webpieces/http-routing';\nimport { ContextTuple } from '@webpieces/core-util';\n\nexport const MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS = 30 * 24 * 60 * 60;\nexport const MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS = 60 * 60;\nexport const MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS = 60 * 60;\n\n/** Framework-normalized access-token result. The token representation remains application-owned. */\nexport class MintedMcpAccessToken {\n constructor(\n public readonly token: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n ) {\n if (token.trim() === '' || resource.trim() === '') {\n throw new Error('Minted MCP access token and resource must be non-empty.');\n }\n if (!Number.isFinite(issuedAtEpochSeconds) || !Number.isFinite(expiresAtEpochSeconds)) {\n throw new Error(\n 'Minted MCP access token timestamps must be finite epoch-second values.',\n );\n }\n if (expiresAtEpochSeconds <= issuedAtEpochSeconds) {\n throw new Error('MCP access token expiry must be after issuance.');\n }\n if (expiresAtEpochSeconds - issuedAtEpochSeconds > MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS) {\n throw new Error('MCP access token lifetime must not exceed 30 days.');\n }\n }\n}\n\n/**\n * Fresh, authoritative security facts resolved at the MCP resource boundary. Implementations must\n * verify issuer, exact resource/audience, expiry, token type/version, key/algorithm policy, and\n * current account state. Opaque access tokens are fully supported.\n */\nexport class VerifiedMcpCredential {\n constructor(\n public readonly subject: string,\n public readonly issuer: string,\n public readonly resource: string,\n public readonly issuedAtEpochSeconds: number,\n public readonly expiresAtEpochSeconds: number,\n public readonly scopes: readonly string[],\n /** When current enabled/revoked state and roles were read from the authoritative source. */\n public readonly accountValidatedAtEpochSeconds: number,\n /** Advisory tools/list filtering only. Endpoint authorization always runs again. */\n public readonly listingRoles: readonly string[] = [],\n /** Trusted delegated identity derived only by the access-token authority. */\n public readonly trustedContext: readonly ContextTuple[] = [],\n ) {}\n}\n\n/**\n * Application-owned paired mint/verify authority for resource-bound MCP access tokens.\n * `verifyAccessToken` must throw `ApiUnauthorizedError` for a token it rejects (the bind boundary\n * answers 401 + `WWW-Authenticate`); any other throw is treated as an implementation failure (500).\n */\nexport interface McpAccessTokenAuthority<TGrant> {\n mintAccessToken(grant: TGrant): Promise<MintedMcpAccessToken>;\n verifyAccessToken(\n accessToken: string,\n expectedResource: string,\n ): Promise<VerifiedMcpCredential>;\n}\n\n/** Minimal tool identity supplied when the app creates its own endpoint-JWT mint request. */\nexport class McpEndpointDescriptor {\n constructor(\n public readonly toolName: string,\n public readonly apiClassName: string,\n public readonly methodName: string,\n ) {}\n}\n\n/** Application mapping from verified MCP identity to its unconstrained JwtHook mint request. */\nexport type McpEndpointMintRequestFactory<TMintRequest> = (\n credential: VerifiedMcpCredential,\n endpoint: McpEndpointDescriptor,\n) => TMintRequest;\n\n/** RFC 9728-style metadata exposed by the MCP protected resource. */\nexport class McpProtectedResourceMetadata {\n readonly resource: string;\n readonly authorization_servers: readonly string[];\n readonly bearer_methods_supported = ['header'];\n readonly scopes_supported?: readonly string[];\n\n constructor(\n resource: string,\n authorizationServers: readonly string[],\n scopesSupported?: readonly string[],\n ) {\n this.resource = resource;\n this.authorization_servers = authorizationServers;\n this.scopes_supported = scopesSupported;\n }\n}\n\n/**\n * Node MCP server configuration, built with fluent setters: every name sits next to its own value, so\n * no two settings can be swapped, and a future setting is an additive setter rather than a breaking\n * signature change. Pass the same concrete application authority to both\n * `setAccessTokenAuthority(...)` and `setEndpointJwtAuthority(...)` when it implements both security\n * seams.\n *\n * SINGLE CANONICAL ORIGIN: one server serves exactly one protected resource. {@link setResource} is\n * fixed for the lifetime of the process, is the audience of every access token, and is never derived\n * from the request `Host` — see that setter for why a host-derived audience is a confused deputy.\n *\n * Each setter validates its own value immediately and names itself in the failure. Setters cannot\n * make a field impossible to forget the way a positional constructor could, so\n * {@link WpMcpServerConfig.validate} runs from `WpMcpServer.bind(...)` and fails at startup listing\n * EVERY missing required setter at once. A misconfiguration is therefore a boot failure naming the\n * setter, instead of a first-request 401 + `WWW-Authenticate` — which is the OAuth discovery signal,\n * so the client's correct response is to re-authenticate and fail again, forever.\n *\n * ```ts\n * const config = new WpMcpServerConfig<MyGrant, MyMintRequest>()\n * .setName('my-server')\n * .setVersion('1.0.0')\n * .setResource('https://api.example.com/mcp')\n * .setAccessTokenAuthority(authority)\n * .setEndpointJwtAuthority(jwtHook)\n * .setEndpointMintRequest((credential) => new MyMintRequest(credential.subject))\n * .setAuthorizationServers(['https://login.example.com'])\n * .setRequiredScopes(['tools']);\n *\n * // the tools/call error translator is a PROCESS-GLOBAL, not a member of this config:\n * McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());\n * ```\n */\nexport class WpMcpServerConfig<TGrant, TMintRequest> {\n private nameValue?: string;\n private versionValue?: string;\n private resourceValue?: string;\n private accessTokenAuthorityValue?: McpAccessTokenAuthority<TGrant>;\n private endpointJwtAuthorityValue?: JwtHook<TMintRequest>;\n private endpointMintRequestValue?: McpEndpointMintRequestFactory<TMintRequest>;\n private authorizationServersValue?: readonly string[];\n private requiredScopesValue?: readonly string[];\n private maxAccountValidationAgeSecondsValue = MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS;\n private maxEndpointJwtLifetimeSecondsValue = MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS;\n\n /** REQUIRED. The MCP server name reported by `initialize`. */\n setName(name: string): this {\n this.nameValue = this.requireText(name, 'setName');\n return this;\n }\n\n /** REQUIRED. The MCP server version; the registry revision is appended to it at bind time. */\n setVersion(version: string): this {\n this.versionValue = this.requireText(version, 'setVersion');\n return this;\n }\n\n /**\n * REQUIRED. The ONE canonical protected-resource URI, which must be an absolute URL: it is the\n * exact audience every access token is checked against, and {@link resourceMetadataUrl} is\n * derived from it for the 401 challenge, so a non-URL here is always a mistake — including a\n * name or version landed on it.\n *\n * It is fixed for the lifetime of the process and must NEVER be derived from the request `Host`\n * header: the boundary compares a token's audience against this value, so a host-derived\n * resource compares two values both taken from caller-controlled input — the confused-deputy\n * shape this check exists to prevent. Its path must equal `McpBindOptions.endpointPath`, which\n * `WpMcpServer.bind(...)` verifies. A second hostname is a second deployment, not a second\n * audience.\n */\n setResource(resource: string): this {\n this.resourceValue = this.requireUrl(resource, 'setResource');\n return this;\n }\n\n /** REQUIRED. Application-owned paired mint/verify authority for MCP access tokens. */\n setAccessTokenAuthority(authority: McpAccessTokenAuthority<TGrant>): this {\n this.accessTokenAuthorityValue = this.requirePresent(authority, 'setAccessTokenAuthority');\n return this;\n }\n\n /** REQUIRED. The `JwtHook` minting the short-lived per-endpoint JWT for local bindings. */\n setEndpointJwtAuthority(authority: JwtHook<TMintRequest>): this {\n this.endpointJwtAuthorityValue = this.requirePresent(authority, 'setEndpointJwtAuthority');\n return this;\n }\n\n /** REQUIRED. Maps a verified MCP identity plus tool identity to that JwtHook's mint request. */\n setEndpointMintRequest(factory: McpEndpointMintRequestFactory<TMintRequest>): this {\n if (typeof factory !== 'function') {\n throw new Error('WpMcpServerConfig.setEndpointMintRequest(...) requires a function.');\n }\n this.endpointMintRequestValue = factory;\n return this;\n }\n\n /**\n * REQUIRED. The trusted token issuers. Every entry must be an absolute URL, because each is\n * compared against a token's `issuer` claim and published as an `authorization_servers` entry —\n * which is what makes a scope list landed here fail at boot rather than on the first request.\n */\n setAuthorizationServers(authorizationServers: readonly string[]): this {\n const servers = this.requireArray(authorizationServers, 'setAuthorizationServers');\n if (servers.length === 0) {\n throw new Error(\n 'WpMcpServerConfig.setAuthorizationServers(...) requires at least one issuer URL.',\n );\n }\n for (const server of servers) this.requireUrl(server, 'setAuthorizationServers');\n this.authorizationServersValue = servers;\n return this;\n }\n\n /**\n * REQUIRED, and empty only by saying so explicitly. Scopes every access token must carry before\n * any tool is dispatched.\n */\n setRequiredScopes(requiredScopes: readonly string[]): this {\n const scopes = this.requireArray(requiredScopes, 'setRequiredScopes');\n for (const scope of scopes) this.requireText(scope, 'setRequiredScopes');\n this.requiredScopesValue = scopes;\n return this;\n }\n\n /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */\n setMaxAccountValidationAgeSeconds(seconds: number): this {\n this.maxAccountValidationAgeSecondsValue = this.requireSeconds(\n seconds,\n MAX_MCP_ACCOUNT_VALIDATION_AGE_SECONDS,\n 'setMaxAccountValidationAgeSeconds',\n );\n return this;\n }\n\n /** OPTIONAL ceiling, 1 second to 1 hour. Defaults to 1 hour. */\n setMaxEndpointJwtLifetimeSeconds(seconds: number): this {\n this.maxEndpointJwtLifetimeSecondsValue = this.requireSeconds(\n seconds,\n MAX_MCP_ENDPOINT_JWT_LIFETIME_SECONDS,\n 'setMaxEndpointJwtLifetimeSeconds',\n );\n return this;\n }\n\n /**\n * Called by `WpMcpServer.bind(...)`. Throws naming EVERY missing required setter at once, so a\n * half-built config is repaired in one pass rather than one boot failure at a time.\n */\n validate(): void {\n const missing: string[] = [];\n if (this.nameValue === undefined) missing.push('setName(...)');\n if (this.versionValue === undefined) missing.push('setVersion(...)');\n if (this.resourceValue === undefined) missing.push('setResource(...)');\n if (this.accessTokenAuthorityValue === undefined) {\n missing.push('setAccessTokenAuthority(...)');\n }\n if (this.endpointJwtAuthorityValue === undefined) {\n missing.push('setEndpointJwtAuthority(...)');\n }\n if (this.endpointMintRequestValue === undefined) {\n missing.push('setEndpointMintRequest(...)');\n }\n if (this.authorizationServersValue === undefined) {\n missing.push('setAuthorizationServers(...)');\n }\n if (this.requiredScopesValue === undefined) missing.push('setRequiredScopes(...)');\n if (missing.length > 0) {\n throw new Error(`WpMcpServerConfig is missing ${missing.join(', ')}`);\n }\n }\n\n get name(): string {\n return this.read(this.nameValue, 'setName');\n }\n\n get version(): string {\n return this.read(this.versionValue, 'setVersion');\n }\n\n get resource(): string {\n return this.read(this.resourceValue, 'setResource');\n }\n\n get accessTokenAuthority(): McpAccessTokenAuthority<TGrant> {\n return this.read(this.accessTokenAuthorityValue, 'setAccessTokenAuthority');\n }\n\n get endpointJwtAuthority(): JwtHook<TMintRequest> {\n return this.read(this.endpointJwtAuthorityValue, 'setEndpointJwtAuthority');\n }\n\n get endpointMintRequest(): McpEndpointMintRequestFactory<TMintRequest> {\n return this.read(this.endpointMintRequestValue, 'setEndpointMintRequest');\n }\n\n get authorizationServers(): readonly string[] {\n return this.read(this.authorizationServersValue, 'setAuthorizationServers');\n }\n\n get requiredScopes(): readonly string[] {\n return this.read(this.requiredScopesValue, 'setRequiredScopes');\n }\n\n get maxAccountValidationAgeSeconds(): number {\n return this.maxAccountValidationAgeSecondsValue;\n }\n\n get maxEndpointJwtLifetimeSeconds(): number {\n return this.maxEndpointJwtLifetimeSecondsValue;\n }\n\n /**\n * RFC 9728 §3.1: the URL of this resource's protected-resource METADATA DOCUMENT, derived from\n * {@link resource} by inserting `/.well-known/oauth-protected-resource` between origin and path\n * (`https://host/mcp` → `https://host/.well-known/oauth-protected-resource/mcp`).\n *\n * This — never the resource identifier itself — is what RFC 9728 §5.1 defines `resource_metadata`\n * to be, so it is the value webpieces puts in the 401 `WWW-Authenticate` challenge. webpieces\n * mounts no `.well-known` route; publish {@link protectedResourceMetadata} at this URL.\n */\n get resourceMetadataUrl(): string {\n const url = new URL(this.resource);\n const path = url.pathname === '/' ? '' : url.pathname;\n return `${url.origin}/.well-known/oauth-protected-resource${path}`;\n }\n\n protectedResourceMetadata(): McpProtectedResourceMetadata {\n return new McpProtectedResourceMetadata(\n this.resource,\n this.authorizationServers,\n this.requiredScopes,\n );\n }\n\n private requireText(value: string, setter: string): string {\n if (typeof value !== 'string' || value.trim() === '') {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a non-empty string.`);\n }\n return value;\n }\n\n private requireUrl(value: string, setter: string): string {\n this.requireText(value, setter);\n if (!URL.canParse(value)) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires an absolute URL, got '${value}'.`,\n );\n }\n return value;\n }\n\n private requireArray(value: readonly string[], setter: string): readonly string[] {\n if (!Array.isArray(value)) {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires an array of strings.`);\n }\n return value;\n }\n\n private requireSeconds(value: number, ceiling: number, setter: string): number {\n if (!Number.isFinite(value) || value <= 0 || value > ceiling) {\n throw new Error(\n `WpMcpServerConfig.${setter}(...) requires 1..${ceiling} seconds, got ${String(value)}.`,\n );\n }\n return value;\n }\n\n private requirePresent<T>(value: T, setter: string): T {\n if (value === undefined || value === null) {\n throw new Error(`WpMcpServerConfig.${setter}(...) requires a value.`);\n }\n return value;\n }\n\n private read<T>(value: T | undefined, setter: string): T {\n if (value === undefined) {\n throw new Error(`WpMcpServerConfig is missing ${setter}(...)`);\n }\n return value;\n }\n}\n"]}
@@ -0,0 +1,40 @@
1
+ import { McpErrorTranslator } from './McpToolCallRendering';
2
+ /**
3
+ * McpRegistry - the process-global home of the ONE {@link McpErrorTranslator}, mirroring
4
+ * `ClientRegistry` for HTTP and `IpcRegistry` for IPC.
5
+ *
6
+ * ```ts
7
+ * // startup, ONCE per process
8
+ * McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());
9
+ * ```
10
+ *
11
+ * # This REVERSES the constructor-argument decision of #953, deliberately
12
+ *
13
+ * `WpMcpServerConfig.setErrorTranslator(...)` used to carry this, on the argument that `WpMcpServer`
14
+ * is constructed by app code and two servers may run in one process. The trade is now taken the other
15
+ * way, with the repo owner's explicit agreement: two MCP servers in one process share ONE translator,
16
+ * and registration order matters rather than being fixed at construction.
17
+ *
18
+ * What buys that back is uniformity. HTTP, MCP and IPC now have the identical shape — one registry per
19
+ * protocol, a non-optional `getErrorTranslator()`, a webpieces default that is always installed, and
20
+ * therefore exactly one unconditional line at every call site. The "did anyone register one" branch
21
+ * that the optional config member forced on `WpMcpErrorTranslator` is gone, and an app reads one
22
+ * pattern instead of three.
23
+ */
24
+ export declare class McpRegistry {
25
+ /**
26
+ * THE translator. NEVER undefined — it starts life holding a {@link McpDefaultToolCallRenderer},
27
+ * and {@link McpRegistry.setErrorTranslator} REPLACES it.
28
+ */
29
+ private static errorTranslator;
30
+ /**
31
+ * Install the app's {@link McpErrorTranslator} — it REPLACES the webpieces default for every
32
+ * `tools/call` failure, owning the ENTIRE result, and declines by delegating to
33
+ * `new McpDefaultToolCallRenderer().toWire(error)`.
34
+ */
35
+ static setErrorTranslator(translator: McpErrorTranslator): void;
36
+ /** THE {@link McpErrorTranslator} for this process — never `undefined`, so no caller branches. */
37
+ static getErrorTranslator(): McpErrorTranslator;
38
+ /** Put the process-global back to the webpieces default. For TESTS, so specs cannot leak. */
39
+ static resetForTests(): void;
40
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.McpRegistry = void 0;
4
+ const McpToolCallRendering_1 = require("./McpToolCallRendering");
5
+ /**
6
+ * McpRegistry - the process-global home of the ONE {@link McpErrorTranslator}, mirroring
7
+ * `ClientRegistry` for HTTP and `IpcRegistry` for IPC.
8
+ *
9
+ * ```ts
10
+ * // startup, ONCE per process
11
+ * McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());
12
+ * ```
13
+ *
14
+ * # This REVERSES the constructor-argument decision of #953, deliberately
15
+ *
16
+ * `WpMcpServerConfig.setErrorTranslator(...)` used to carry this, on the argument that `WpMcpServer`
17
+ * is constructed by app code and two servers may run in one process. The trade is now taken the other
18
+ * way, with the repo owner's explicit agreement: two MCP servers in one process share ONE translator,
19
+ * and registration order matters rather than being fixed at construction.
20
+ *
21
+ * What buys that back is uniformity. HTTP, MCP and IPC now have the identical shape — one registry per
22
+ * protocol, a non-optional `getErrorTranslator()`, a webpieces default that is always installed, and
23
+ * therefore exactly one unconditional line at every call site. The "did anyone register one" branch
24
+ * that the optional config member forced on `WpMcpErrorTranslator` is gone, and an app reads one
25
+ * pattern instead of three.
26
+ */
27
+ class McpRegistry {
28
+ /**
29
+ * THE translator. NEVER undefined — it starts life holding a {@link McpDefaultToolCallRenderer},
30
+ * and {@link McpRegistry.setErrorTranslator} REPLACES it.
31
+ */
32
+ static errorTranslator = new McpToolCallRendering_1.McpDefaultToolCallRenderer();
33
+ /**
34
+ * Install the app's {@link McpErrorTranslator} — it REPLACES the webpieces default for every
35
+ * `tools/call` failure, owning the ENTIRE result, and declines by delegating to
36
+ * `new McpDefaultToolCallRenderer().toWire(error)`.
37
+ */
38
+ // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected
39
+ static setErrorTranslator(translator) {
40
+ McpRegistry.errorTranslator = translator;
41
+ }
42
+ /** THE {@link McpErrorTranslator} for this process — never `undefined`, so no caller branches. */
43
+ // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected
44
+ static getErrorTranslator() {
45
+ return McpRegistry.errorTranslator;
46
+ }
47
+ /** Put the process-global back to the webpieces default. For TESTS, so specs cannot leak. */
48
+ // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected
49
+ static resetForTests() {
50
+ McpRegistry.errorTranslator = new McpToolCallRendering_1.McpDefaultToolCallRenderer();
51
+ }
52
+ }
53
+ exports.McpRegistry = McpRegistry;
54
+ //# sourceMappingURL=McpRegistry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpRegistry.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpRegistry.ts"],"names":[],"mappings":";;;AAAA,iEAAwF;AAExF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAa,WAAW;IACpB;;;OAGG;IACK,MAAM,CAAC,eAAe,GAAuB,IAAI,iDAA0B,EAAE,CAAC;IAEtF;;;;OAIG;IACH,4JAA4J;IAC5J,MAAM,CAAC,kBAAkB,CAAC,UAA8B;QACpD,WAAW,CAAC,eAAe,GAAG,UAAU,CAAC;IAC7C,CAAC;IAED,kGAAkG;IAClG,4JAA4J;IAC5J,MAAM,CAAC,kBAAkB;QACrB,OAAO,WAAW,CAAC,eAAe,CAAC;IACvC,CAAC;IAED,6FAA6F;IAC7F,4JAA4J;IAC5J,MAAM,CAAC,aAAa;QAChB,WAAW,CAAC,eAAe,GAAG,IAAI,iDAA0B,EAAE,CAAC;IACnE,CAAC;;AA3BL,kCA4BC","sourcesContent":["import { McpDefaultToolCallRenderer, McpErrorTranslator } from './McpToolCallRendering';\n\n/**\n * McpRegistry - the process-global home of the ONE {@link McpErrorTranslator}, mirroring\n * `ClientRegistry` for HTTP and `IpcRegistry` for IPC.\n *\n * ```ts\n * // startup, ONCE per process\n * McpRegistry.setErrorTranslator(new MyMcpErrorTranslator());\n * ```\n *\n * # This REVERSES the constructor-argument decision of #953, deliberately\n *\n * `WpMcpServerConfig.setErrorTranslator(...)` used to carry this, on the argument that `WpMcpServer`\n * is constructed by app code and two servers may run in one process. The trade is now taken the other\n * way, with the repo owner's explicit agreement: two MCP servers in one process share ONE translator,\n * and registration order matters rather than being fixed at construction.\n *\n * What buys that back is uniformity. HTTP, MCP and IPC now have the identical shape — one registry per\n * protocol, a non-optional `getErrorTranslator()`, a webpieces default that is always installed, and\n * therefore exactly one unconditional line at every call site. The \"did anyone register one\" branch\n * that the optional config member forced on `WpMcpErrorTranslator` is gone, and an app reads one\n * pattern instead of three.\n */\nexport class McpRegistry {\n /**\n * THE translator. NEVER undefined — it starts life holding a {@link McpDefaultToolCallRenderer},\n * and {@link McpRegistry.setErrorTranslator} REPLACES it.\n */\n private static errorTranslator: McpErrorTranslator = new McpDefaultToolCallRenderer();\n\n /**\n * Install the app's {@link McpErrorTranslator} — it REPLACES the webpieces default for every\n * `tools/call` failure, owning the ENTIRE result, and declines by delegating to\n * `new McpDefaultToolCallRenderer().toWire(error)`.\n */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static setErrorTranslator(translator: McpErrorTranslator): void {\n McpRegistry.errorTranslator = translator;\n }\n\n /** THE {@link McpErrorTranslator} for this process — never `undefined`, so no caller branches. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static getErrorTranslator(): McpErrorTranslator {\n return McpRegistry.errorTranslator;\n }\n\n /** Put the process-global back to the webpieces default. For TESTS, so specs cannot leak. */\n // webpieces-disable no-function-outside-class -- static global singleton (like HeaderRegistry/ClientRegistry); populated once at startup, never DI-injected\n static resetForTests(): void {\n McpRegistry.errorTranslator = new McpDefaultToolCallRenderer();\n }\n}\n"]}
@@ -0,0 +1,111 @@
1
+ import { CallToolResult } from '@modelcontextprotocol/server';
2
+ import { ContextKey } from '@webpieces/core-util';
3
+ /** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */
4
+ export declare const MCP_REQUEST_ID_META_KEY = "webpieces/requestId";
5
+ /** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */
6
+ export declare class McpErrorData {
7
+ readonly requestId: string;
8
+ constructor(requestId: string);
9
+ }
10
+ /**
11
+ * The two per-request facts a renderer needs that are NOT the error: the JSON-RPC `id` it must echo,
12
+ * and the tool being called. They are stamped into the request scope by `WpMcpServer` rather than
13
+ * threaded through every renderer signature, because `requestId` already travels that way
14
+ * ({@link WebpiecesCoreHeaders.REQUEST_ID}) and a second, parallel correlation parameter was a COPY
15
+ * of the context — which is what the deleted `McpFailureScope` was.
16
+ *
17
+ * Context-only and never logged: the logging backends read `RequestContext` for correlation on every
18
+ * record already, and this object exists purely so the renderers can be one-argument functions.
19
+ */
20
+ export declare class McpCorrelation {
21
+ readonly jsonRpcId: string | number | null;
22
+ readonly toolName?: string | undefined;
23
+ static readonly KEY: ContextKey<McpCorrelation, "untrusted">;
24
+ constructor(jsonRpcId: string | number | null, toolName?: string | undefined);
25
+ /** Stamp what this request knows so far; `WpMcpServer` re-stamps once the tool name is known. */
26
+ static stamp(correlation: McpCorrelation): void;
27
+ /** A renderer reached before anything stamped (a failure with no parseable body) still renders. */
28
+ static current(): McpCorrelation;
29
+ /** The webpieces requestId of the request being served, as every MCP reply reports it. */
30
+ static requestId(): string;
31
+ }
32
+ /** The only error shape rendered into model-visible MCP tool content. */
33
+ export declare class ModelVisibleToolError {
34
+ readonly kind: string;
35
+ readonly message: string;
36
+ readonly requestId: string;
37
+ readonly field?: string | undefined;
38
+ readonly callerMessage?: string | undefined;
39
+ readonly errorCode?: string | undefined;
40
+ readonly retryAfterSeconds?: number | undefined;
41
+ readonly statusCode?: number | undefined;
42
+ constructor(kind: string, message: string, requestId: string, field?: string | undefined, callerMessage?: string | undefined, errorCode?: string | undefined, retryAfterSeconds?: number | undefined, statusCode?: number | undefined);
43
+ }
44
+ /** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */
45
+ export declare class McpHttpErrorBody {
46
+ readonly error: McpHttpErrorDetail;
47
+ readonly id: string | number | null;
48
+ readonly jsonrpc = "2.0";
49
+ constructor(error: McpHttpErrorDetail, id: string | number | null);
50
+ }
51
+ export declare class McpHttpErrorDetail {
52
+ readonly code: number;
53
+ readonly message: string;
54
+ readonly data: McpErrorData;
55
+ constructor(code: number, message: string, data: McpErrorData);
56
+ }
57
+ /**
58
+ * The application's own `tools/call` error translation — the MCP twin of the `ErrorTranslator` an app
59
+ * registers on `ClientRegistry` for HTTP and the `IpcErrorTranslator` it registers on `IpcRegistry`.
60
+ * Register one with {@link McpRegistry.setErrorTranslator}.
61
+ *
62
+ * An app owns its error taxonomy and owns how those errors should be explained to a model, so a
63
+ * claimed error yields the ENTIRE `CallToolResult` — content, `structuredContent`, `isError`, the lot.
64
+ *
65
+ * # There is NO `fromWire`, and that is the protocol, not an oversight
66
+ *
67
+ * The HTTP and IPC translators have two halves because webpieces sits on BOTH ends of those wires: it
68
+ * writes the response AND, in another process, reads one. **webpieces is never the MCP client.** The
69
+ * client is Claude (or another model host); there is no return path for webpieces to translate, so
70
+ * the pair is one-directional by nature. Do not "restore the symmetry" by adding a half with nothing
71
+ * to call it.
72
+ *
73
+ * # The pre-SDK boundary does NOT share the HTTP `ErrorTranslator`, despite the identical type
74
+ *
75
+ * `WpMcpErrorTranslator.toBearerBoundaryResponse` returns an `HttpResponseDto`, exactly like
76
+ * `ErrorTranslator.toWire` — and it must STILL not be routed through it. Two reasons, both hard:
77
+ *
78
+ * - its body must stay JSON-RPC shaped (`McpHttpErrorBody`) or an MCP client cannot parse the reply
79
+ * at all, whereas an app's HTTP translator publishes the app's own REST envelope;
80
+ * - it emits the `401 + WWW-Authenticate: Bearer resource_metadata=...` that MCP clients use for OAuth
81
+ * discovery, so an app rewriting it breaks connector onboarding.
82
+ *
83
+ * Same TYPE, different OWNER. `tools/list` is framework-owned for the same reason.
84
+ *
85
+ * Contract:
86
+ * - `error` is the RAW thrown value: an app must be able to `instanceof` its own error classes.
87
+ * `ApiErrorBoundary` never substitutes an object for the thrown error, so there is exactly one
88
+ * error value on this path.
89
+ * - There is no "not mine" return. A registered translator REPLACES the webpieces default and
90
+ * declines by DELEGATING to it: `new McpDefaultToolCallRenderer().toWire(error)`.
91
+ * - webpieces default-fills `_meta['webpieces/requestId']` only when the returned result has NO
92
+ * `_meta`. An app that sets `_meta` owns it untouched.
93
+ * - scope is `tools/call` ONLY.
94
+ */
95
+ export interface McpErrorTranslator {
96
+ /** An error in, the whole `tools/call` reply out. Named for its HTTP and IPC twins. */
97
+ toWire(error: Error): CallToolResult;
98
+ }
99
+ /**
100
+ * webpieces' DEFAULT `tools/call` rendering, and what {@link McpRegistry} holds until an app installs
101
+ * its own. Public so an app's own {@link McpErrorTranslator} can DECLINE an error by delegating to it
102
+ * — the MCP twin of `WebpiecesDefaultErrorTranslator.toWire` on the HTTP side.
103
+ *
104
+ * Published text comes only from {@link ApiErrorBoundary.encode}: this never puts an error's own
105
+ * message in front of a model unless it is an `ApiEndUserError`.
106
+ */
107
+ export declare class McpDefaultToolCallRenderer implements McpErrorTranslator {
108
+ private readonly boundary;
109
+ toWire(error: Error): CallToolResult;
110
+ private modelVisible;
111
+ }