@webpieces/mcp-server 0.4.789 → 0.4.791

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(...)`
@@ -120,15 +122,31 @@ mcp.bind(expressApp, new McpBindOptions(
120
122
  ));
121
123
  ```
122
124
 
123
- `bind(...)` registers POST at that path plus a 405 (`Allow: POST`) for every other method there — MCP
124
- 2026-07-28 has no session GET, so a probe gets a method error rather than a 404. It may be called only
125
+ `bind(...)` registers POST at that path plus a 405 (`Allow: POST`) for every other method there —
126
+ neither served era has a session GET here (2026-07-28 has none at all, and the 2025 leg is the SDK's
127
+ STATELESS fallback, which answers the 2025 session GET/DELETE with the same 405), so a probe gets a
128
+ method error rather than a 404. It may be called only
125
129
  once: one server serves exactly one canonical resource URI. `WpMcpServerConfig.resource` is that URI,
126
130
  fixed for the process lifetime and never derived from the request `Host` (a host-derived audience would
127
131
  make the boundary's own audience check circular — a confused deputy), and its path must equal
128
132
  `endpointPath`, which `bind(...)` verifies. A second hostname is a second deployment, not a second
129
133
  audience.
130
134
 
131
- The official MCP v2 server and Node adapter own MCP 2026-07-28 JSON-RPC validation, JSON versus
135
+ ## Protocol revisions: negotiated, never pinned
136
+
137
+ One endpoint serves BOTH MCP wire eras from one tool registry — modern (2026-07-28, per-request
138
+ `_meta` envelope) and legacy (the 2025-11-25 family, negotiated by `initialize`). The official SDK
139
+ classifies each request and answers it on the matching leg, calling the SAME server factory for both,
140
+ so the two eras can never drift apart. There is no configuration for this and no way to turn it off:
141
+ a revision an app can pin is a revision an app can pin itself out of reach with, which is exactly what
142
+ happened when this bridge was modern-only and every shipping client (all still on the 2025
143
+ generation) got `-32022 Unsupported protocol version` at `initialize`. Only a revision NO era knows is
144
+ refused, which is what the MCP lifecycle spec asks of a server.
145
+
146
+ The external bearer is verified identically on both legs, at the `bind(...)` HTTP boundary, before the
147
+ SDK is involved — the era is the SDK's decision, the authentication is not.
148
+
149
+ The official MCP v2 server and Node adapter own JSON-RPC validation for both eras, JSON versus
132
150
  request-scoped SSE responses, backpressure, cancellation, keepalives, and subscriptions. Webpieces
133
151
  owns the Express path, root `RequestContext`, authentication boundary, schema-derived tool registry,
134
152
  and dispatch through the normal API proxy.
@@ -154,7 +172,7 @@ level-triggered invalidation, and `close()` drains active response streams. A re
154
172
  re-listen and refresh its authoritative lists; notifications are not a durable or replayable event log.
155
173
 
156
174
  The authority verifies issuer, signature/token state, expiry, scopes, the exact resource URI, and
157
- current account state. MCP 2026-07-28 has no sessions, so the bearer is verified exactly once per POST
175
+ current account state. Neither served era keeps a session here, so the bearer is verified once per POST
158
176
  at the `bind(...)` HTTP boundary, before the SDK is involved; tool handlers never re-verify it. A
159
177
  rejected token must be thrown as `ApiUnauthorizedError` (answered 401 + `WWW-Authenticate`); any
160
178
  other throw from the authority is an implementation failure (500). Endpoint JWTs are capped at one
@@ -163,11 +181,11 @@ hour and MCP access tokens at 30 days.
163
181
  ## Error boundary
164
182
 
165
183
  `WpMcpErrorTranslator` is the one place a failure becomes an MCP reply, mirroring
166
- `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
167
185
  that only delegates to it; the dispatcher and the local/remote invokers have none. Classification is
168
186
  the shared `ApiErrorBoundary.encode` rule (a non-`ApiError`, or a caller-local `ApiConnectionError`,
169
187
  publishes as kind `implementation`). The boundary never substitutes an object for the thrown error,
170
- so an app's `McpErrorTranslators` can `instanceof` its own error classes.
188
+ so an app's `McpErrorTranslator` can `instanceof` its own error classes.
171
189
 
172
190
  There is ONE method per BOUNDARY, named for the boundary it guards. They cannot be one shape — the
173
191
  MCP spec fixes each — so the three-ness is made obvious rather than accidental:
@@ -176,7 +194,7 @@ MCP spec fixes each — so the three-ness is made obvious rather than accidental
176
194
  |---|---|---|
177
195
  | before the SDK: bearer, `Origin`, malformed body | `toBearerBoundaryResponse(error): HttpResponseDto<McpHttpErrorBody>` | HTTP 401 + `WWW-Authenticate` / 403 / 400, anything else 500 |
178
196
  | `tools/list` | `toListError(error): never` | throws JSON-RPC `-32602` (bad request) or `-32603` `Internal Error` |
179
- | `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 |
180
198
  | `tools/call`, unknown tool name | `unknownTool(name)` | JSON-RPC `-32602` `Unknown tool: <name>` |
181
199
 
182
200
  The pre-SDK boundary produces a VALUE like every other API in the framework and `WpMcpServer` writes
@@ -218,16 +236,18 @@ Every reply carries the requestId so a user can quote it: `_meta["webpieces/requ
218
236
  An app owns its error taxonomy and owns how those errors should be explained to a model, so a
219
237
  registered translator REPLACES the webpieces default for every `tools/call` failure — the same
220
238
  convention `ExpressWrapper.handleError` applies on the HTTP path. Register one with
221
- `WpMcpServerConfig.setErrorTranslator(...)`; it is a setter and not a global, because `WpMcpServer` is
222
- 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.)
223
243
 
224
244
  ```ts
225
- class LangMcpErrorTranslators implements McpErrorTranslators {
245
+ class LangMcpErrorTranslator implements McpErrorTranslator {
226
246
  private readonly fallback = new McpDefaultToolCallRenderer();
227
247
 
228
- toToolCallResult(error: Error): CallToolResult {
248
+ toWire(error: Error): CallToolResult {
229
249
  if (!(error instanceof LangPassageLockedError)) {
230
- return this.fallback.toToolCallResult(error); // not mine -> webpieces default
250
+ return this.fallback.toWire(error); // not mine -> webpieces default
231
251
  }
232
252
  return {
233
253
  content: [{ type: 'text', text: `Passage ${error.passageId} is locked.` }],
@@ -254,10 +274,16 @@ class LangMcpErrorTranslators implements McpErrorTranslators {
254
274
  extremely hard to debug.
255
275
 
256
276
  A remote binding's generated Node client turns a dependency's 4xx into the gateway's own
257
- `ApiImplementationError` (see `NodeProxyClient.adaptDownstreamFailure`). A gateway that wants the model
258
- to see the peer's typed 4xx instead registers an `ErrorTranslators` via
259
- `ClientRegistry.setErrorTranslators(...)` whose `fromWire` relays decoded Webpieces payloads; with it,
260
- 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.
261
287
 
262
288
  The verifier's `accountValidatedAtEpochSeconds` must represent an authoritative enabled/revoked and
263
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.789",
3
+ "version": "0.4.791",
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.789",
28
- "@webpieces/core-util": "0.4.789",
29
- "@webpieces/http-routing": "0.4.789",
30
- "@webpieces/http-server": "0.4.789",
27
+ "@webpieces/core-context": "0.4.791",
28
+ "@webpieces/core-util": "0.4.791",
29
+ "@webpieces/http-routing": "0.4.791",
30
+ "@webpieces/http-server": "0.4.791",
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.789"
36
+ "@webpieces/http-client-node": "0.4.791"
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"]}