@webpieces/mcp-server 0.4.788 → 0.4.789
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 +50 -22
- package/package.json +6 -6
- package/src/McpAuth.d.ts +3 -2
- package/src/McpAuth.js +5 -4
- package/src/McpAuth.js.map +1 -1
- package/src/WpMcpErrorTranslator.d.ts +107 -59
- package/src/WpMcpErrorTranslator.js +149 -98
- package/src/WpMcpErrorTranslator.js.map +1 -1
- package/src/WpMcpServer.d.ts +27 -7
- package/src/WpMcpServer.js +77 -28
- package/src/WpMcpServer.js.map +1 -1
- package/src/__tests__/McpRemoteFixtures.d.ts +13 -6
- package/src/__tests__/McpRemoteFixtures.js +29 -18
- package/src/__tests__/McpRemoteFixtures.js.map +1 -1
- package/src/__tests__/WpMcpServerTestFixtures.js +1 -1
- package/src/__tests__/WpMcpServerTestFixtures.js.map +1 -1
- package/src/index.d.ts +1 -1
- package/src/index.js +3 -2
- package/src/index.js.map +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WpMcpErrorTranslator = exports.McpHttpErrorDetail = exports.McpHttpErrorBody = exports.ModelVisibleToolError = exports.
|
|
3
|
+
exports.WpMcpErrorTranslator = exports.McpDefaultToolCallRenderer = exports.McpHttpErrorDetail = exports.McpHttpErrorBody = exports.ModelVisibleToolError = exports.McpCorrelation = exports.McpErrorData = exports.MCP_REQUEST_ID_META_KEY = void 0;
|
|
4
4
|
const server_1 = require("@modelcontextprotocol/server");
|
|
5
5
|
const core_util_1 = require("@webpieces/core-util");
|
|
6
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
6
7
|
const log = core_util_1.LogManager.getLogger('WpMcpErrorTranslator');
|
|
7
8
|
/** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */
|
|
8
9
|
exports.MCP_REQUEST_ID_META_KEY = 'webpieces/requestId';
|
|
@@ -14,24 +15,43 @@ class McpErrorData {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
exports.McpErrorData = McpErrorData;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The two per-request facts a renderer needs that are NOT the error: the JSON-RPC `id` it must echo,
|
|
20
|
+
* and the tool being called. They are stamped into the request scope by `WpMcpServer` rather than
|
|
21
|
+
* threaded through every renderer signature, because `requestId` already travels that way
|
|
22
|
+
* ({@link WebpiecesCoreHeaders.REQUEST_ID}) and a second, parallel correlation parameter was a COPY
|
|
23
|
+
* of the context — which is what the deleted `McpFailureScope` was.
|
|
24
|
+
*
|
|
25
|
+
* Context-only and never logged: the logging backends read `RequestContext` for correlation on every
|
|
26
|
+
* record already, and this object exists purely so the renderers can be one-argument functions.
|
|
27
|
+
*/
|
|
28
|
+
class McpCorrelation {
|
|
20
29
|
jsonRpcId;
|
|
21
|
-
method;
|
|
22
30
|
toolName;
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
// webpieces-disable no-any-unknown -- ContextKey value types are per-key; this one is context-only
|
|
32
|
+
static KEY = core_util_1.ContextKey.untrusted('webpieces-mcp-correlation', undefined, false,
|
|
33
|
+
/*isLogged*/ false);
|
|
34
|
+
constructor(jsonRpcId, toolName) {
|
|
25
35
|
this.jsonRpcId = jsonRpcId;
|
|
26
|
-
this.method = method;
|
|
27
36
|
this.toolName = toolName;
|
|
28
37
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
38
|
+
/** Stamp what this request knows so far; `WpMcpServer` re-stamps once the tool name is known. */
|
|
39
|
+
// webpieces-disable no-function-outside-class -- static accessor trio for one context key
|
|
40
|
+
static stamp(correlation) {
|
|
41
|
+
core_context_1.RequestContext.putUntrusted(McpCorrelation.KEY, correlation);
|
|
42
|
+
}
|
|
43
|
+
/** A renderer reached before anything stamped (a failure with no parseable body) still renders. */
|
|
44
|
+
// webpieces-disable no-function-outside-class -- static accessor trio for one context key
|
|
45
|
+
static current() {
|
|
46
|
+
return core_context_1.RequestContext.getUntrusted(McpCorrelation.KEY) ?? new McpCorrelation(null);
|
|
47
|
+
}
|
|
48
|
+
/** The webpieces requestId of the request being served, as every MCP reply reports it. */
|
|
49
|
+
// webpieces-disable no-function-outside-class -- static accessor trio for one context key
|
|
50
|
+
static requestId() {
|
|
51
|
+
return core_context_1.RequestContext.getUntrusted(core_util_1.WebpiecesCoreHeaders.REQUEST_ID) ?? 'missing-request-id';
|
|
32
52
|
}
|
|
33
53
|
}
|
|
34
|
-
exports.
|
|
54
|
+
exports.McpCorrelation = McpCorrelation;
|
|
35
55
|
/** The only error shape rendered into model-visible MCP tool content. */
|
|
36
56
|
class ModelVisibleToolError {
|
|
37
57
|
kind;
|
|
@@ -77,108 +97,153 @@ class McpHttpErrorDetail {
|
|
|
77
97
|
}
|
|
78
98
|
exports.McpHttpErrorDetail = McpHttpErrorDetail;
|
|
79
99
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
100
|
+
* webpieces' DEFAULT `tools/call` rendering, as a public class so an app's own
|
|
101
|
+
* {@link McpErrorTranslators} can DECLINE an error by delegating to it — the MCP twin of
|
|
102
|
+
* `ApiErrorHttpMapper.toResponse` 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
|
+
class McpDefaultToolCallRenderer {
|
|
108
|
+
boundary = new core_util_1.ApiErrorBoundary();
|
|
109
|
+
toToolCallResult(error) {
|
|
110
|
+
const requestId = McpCorrelation.requestId();
|
|
111
|
+
const visible = this.modelVisible(this.boundary.encode(error), requestId);
|
|
112
|
+
const text = JSON.stringify(visible);
|
|
113
|
+
return {
|
|
114
|
+
content: [{ type: 'text', text }],
|
|
115
|
+
isError: true,
|
|
116
|
+
_meta: { [exports.MCP_REQUEST_ID_META_KEY]: requestId },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
modelVisible(payload, requestId) {
|
|
120
|
+
switch (payload.kind) {
|
|
121
|
+
case 'end-user':
|
|
122
|
+
return new ModelVisibleToolError(payload.kind, payload.message, requestId, undefined, undefined, payload.errorCode);
|
|
123
|
+
case 'bad-request':
|
|
124
|
+
return new ModelVisibleToolError(payload.kind, payload.callerMessage ?? payload.message, requestId, payload.field, payload.callerMessage);
|
|
125
|
+
case 'coded':
|
|
126
|
+
return new ModelVisibleToolError(payload.kind, payload.message, requestId, undefined, undefined, payload.errorCode, undefined, payload.statusCode);
|
|
127
|
+
case 'implementation':
|
|
128
|
+
return new ModelVisibleToolError(payload.kind, `Internal error in tool ${McpCorrelation.current().toolName ?? 'unknown'} ` +
|
|
129
|
+
`(requestId ${requestId}). This is a bug in the tool, not in your ` +
|
|
130
|
+
'arguments; retrying with different arguments will not help.', requestId);
|
|
131
|
+
default:
|
|
132
|
+
return new ModelVisibleToolError(payload.kind, payload.message, requestId, undefined, undefined, undefined, payload.retryAfterSeconds);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.McpDefaultToolCallRenderer = McpDefaultToolCallRenderer;
|
|
137
|
+
/**
|
|
138
|
+
* The ONE place every MCP failure becomes a reply, mirroring `ApiErrorHttpMapper` for HTTP: an error
|
|
139
|
+
* in, the exact wire shape out. There is one method per BOUNDARY, and the MCP spec is what makes
|
|
140
|
+
* them three rather than one:
|
|
141
|
+
*
|
|
142
|
+
* | Boundary | Who writes HTTP | Failure shape |
|
|
143
|
+
* |--------------------------------|-----------------|-------------------------------------|
|
|
144
|
+
* | pre-SDK bearer / Origin / body | **webpieces** | `HttpResponseDto<McpHttpErrorBody>` |
|
|
145
|
+
* | `tools/list` | the SDK | `throw ProtocolError` |
|
|
146
|
+
* | `tools/call` | the SDK | `CallToolResult` with `isError` |
|
|
147
|
+
*
|
|
148
|
+
* Classification and published text are the shared {@link ApiErrorBoundary} rules. Operator logging
|
|
149
|
+
* is NOT here: `LogApiCall` wraps each of those boundaries in `WpMcpServer`, so every failure gets
|
|
150
|
+
* exactly one `[API-server-resp-*]` line with the request and the identity on it.
|
|
85
151
|
*/
|
|
86
152
|
class WpMcpErrorTranslator {
|
|
153
|
+
challenge;
|
|
87
154
|
appTranslators;
|
|
88
|
-
boundary = new core_util_1.ApiErrorBoundary(
|
|
89
|
-
|
|
90
|
-
|
|
155
|
+
boundary = new core_util_1.ApiErrorBoundary();
|
|
156
|
+
defaultToolCall = new McpDefaultToolCallRenderer();
|
|
157
|
+
/**
|
|
158
|
+
* @param challenge - the `WWW-Authenticate` value for a 401, read lazily because
|
|
159
|
+
* `WpMcpServerConfig` is only fully validated at `bind(...)` time.
|
|
160
|
+
* @param appTranslators - the app's `tools/call` seam; `undefined` means webpieces renders all.
|
|
161
|
+
*/
|
|
162
|
+
constructor(challenge, appTranslators) {
|
|
163
|
+
this.challenge = challenge;
|
|
91
164
|
this.appTranslators = appTranslators;
|
|
92
165
|
}
|
|
93
166
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
167
|
+
* Boundary #1: everything webpieces itself rejects before the SDK is involved — bearer
|
|
168
|
+
* verification, `Origin`, a malformed body. This is 401/403/400/500 today and any status
|
|
169
|
+
* tomorrow, so it produces a VALUE like every other API in the framework and `WpMcpServer` hands
|
|
170
|
+
* it to the shared express writer. The BODY stays JSON-RPC shaped because an MCP client expects
|
|
171
|
+
* that; `HttpResponseDto` is generic, and its header LIST carries `WWW-Authenticate`.
|
|
96
172
|
*/
|
|
97
|
-
|
|
98
|
-
this.boundary.
|
|
99
|
-
if (res.headersSent) {
|
|
100
|
-
res.end();
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const payload = this.boundary.encode(thrown);
|
|
173
|
+
toBearerBoundaryResponse(error) {
|
|
174
|
+
const payload = this.boundary.encode(error);
|
|
104
175
|
const generic = payload.message;
|
|
105
176
|
switch (payload.kind) {
|
|
106
177
|
case 'unauthorized':
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
178
|
+
return this.httpError(401, -32_000, generic, [
|
|
179
|
+
new core_util_1.HttpHeader('WWW-Authenticate', this.challenge()),
|
|
180
|
+
]);
|
|
110
181
|
case 'forbidden':
|
|
111
|
-
this.
|
|
112
|
-
return;
|
|
182
|
+
return this.httpError(403, -32_000, generic, []);
|
|
113
183
|
case 'bad-request':
|
|
114
|
-
this.
|
|
115
|
-
return;
|
|
184
|
+
return this.httpError(400, server_1.ProtocolErrorCode.ParseError, generic, []);
|
|
116
185
|
default:
|
|
117
|
-
this.
|
|
186
|
+
return this.httpError(500, server_1.ProtocolErrorCode.InternalError, 'Internal Error', []);
|
|
118
187
|
}
|
|
119
188
|
}
|
|
120
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
189
|
+
/**
|
|
190
|
+
* Boundary #2: `tools/list` has no tool-result channel, so a failure IS a JSON-RPC error. It
|
|
191
|
+
* THROWS rather than returning a value the caller must remember to throw, and is typed `never`
|
|
192
|
+
* so the compiler knows the call is terminal.
|
|
193
|
+
*
|
|
194
|
+
* This is LOAD-BEARING for disclosure, not defence in depth. Measured on
|
|
195
|
+
* `@modelcontextprotocol/server` 2.0.0 and pinned by `McpSdkForeignThrow.spec.ts`: the SDK wraps
|
|
196
|
+
* a foreign (non-`ProtocolError`) throw in `-32603` but copies its `message` onto the wire
|
|
197
|
+
* VERBATIM. So a `tools/list` handler that lets any error escape un-translated publishes that
|
|
198
|
+
* error's operator text to the caller.
|
|
199
|
+
*/
|
|
200
|
+
toListError(error) {
|
|
201
|
+
const payload = this.boundary.encode(error);
|
|
202
|
+
const requestId = McpCorrelation.requestId();
|
|
128
203
|
if (payload.kind === 'bad-request') {
|
|
129
|
-
|
|
204
|
+
throw new server_1.ProtocolError(server_1.ProtocolErrorCode.InvalidParams, payload.callerMessage ?? payload.message, new McpErrorData(requestId));
|
|
130
205
|
}
|
|
131
206
|
const message = payload.kind === 'end-user' ? payload.message : 'Internal Error';
|
|
132
|
-
|
|
207
|
+
throw new server_1.ProtocolError(server_1.ProtocolErrorCode.InternalError, message, new McpErrorData(requestId));
|
|
133
208
|
}
|
|
134
209
|
/** tools/call with a name no binding registered: a JSON-RPC protocol error per MCP 2026-07-28. */
|
|
135
|
-
unknownTool(toolName
|
|
136
|
-
|
|
137
|
-
|
|
210
|
+
unknownTool(toolName) {
|
|
211
|
+
const requestId = McpCorrelation.requestId();
|
|
212
|
+
log.info(`Unknown MCP tool: ${toolName} requestId=${requestId}`);
|
|
213
|
+
return new server_1.ProtocolError(server_1.ProtocolErrorCode.InvalidParams, `Unknown tool: ${toolName}`, new McpErrorData(requestId));
|
|
138
214
|
}
|
|
139
215
|
/**
|
|
140
|
-
*
|
|
216
|
+
* Boundary #3: every tools/call failure after the tool is found — an `isError: true` result the
|
|
217
|
+
* model sees.
|
|
141
218
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* whoever renders the reply.
|
|
219
|
+
* A registered {@link McpErrorTranslators} REPLACES the default and declines by delegating to
|
|
220
|
+
* {@link McpDefaultToolCallRenderer}, so "no translator registered" and "a translator that does
|
|
221
|
+
* not claim this error" produce byte-identical results.
|
|
146
222
|
*/
|
|
147
|
-
|
|
148
|
-
this.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const visible = this.modelVisible(this.boundary.encode(thrown), scope);
|
|
153
|
-
const text = JSON.stringify(visible);
|
|
154
|
-
return {
|
|
155
|
-
content: [{ type: 'text', text }],
|
|
156
|
-
isError: true,
|
|
157
|
-
_meta: this.resultMeta(scope.requestId),
|
|
158
|
-
};
|
|
223
|
+
toToolCallResult(failure) {
|
|
224
|
+
const renderer = this.appTranslators;
|
|
225
|
+
if (!renderer)
|
|
226
|
+
return this.defaultToolCall.toToolCallResult(failure);
|
|
227
|
+
return this.withDefaultMeta(this.appOrDefault(renderer, failure));
|
|
159
228
|
}
|
|
160
229
|
/** The `_meta` every tools/call result carries so a user can quote its requestId. */
|
|
161
230
|
resultMeta(requestId) {
|
|
162
231
|
return { [exports.MCP_REQUEST_ID_META_KEY]: requestId };
|
|
163
232
|
}
|
|
164
233
|
/**
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* carrying the requestId.
|
|
234
|
+
* An app translator that THROWS must not replace the failure being reported: its own bug is
|
|
235
|
+
* logged here and the ORIGINAL error still renders through the webpieces default, so the model
|
|
236
|
+
* still gets a reply carrying the requestId.
|
|
169
237
|
*/
|
|
170
|
-
|
|
171
|
-
if (!this.appTranslators)
|
|
172
|
-
return undefined;
|
|
238
|
+
appOrDefault(renderer, failure) {
|
|
173
239
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an app translator's own bug must not replace the failure it was asked to render
|
|
174
240
|
try {
|
|
175
|
-
return
|
|
241
|
+
return renderer.toToolCallResult(failure);
|
|
176
242
|
}
|
|
177
243
|
catch (err) {
|
|
178
244
|
const error = (0, core_util_1.toError)(err);
|
|
179
|
-
|
|
180
|
-
this.
|
|
181
|
-
return undefined;
|
|
245
|
+
log.error('Application McpErrorTranslators.toToolCallResult threw; rendering the webpieces default.', error);
|
|
246
|
+
return this.defaultToolCall.toToolCallResult(failure);
|
|
182
247
|
}
|
|
183
248
|
}
|
|
184
249
|
/**
|
|
@@ -186,29 +251,15 @@ class WpMcpErrorTranslator {
|
|
|
186
251
|
* carries the requestId" promise a DEFAULT rather than a restriction: an app that set `_meta`
|
|
187
252
|
* keeps it byte-for-byte.
|
|
188
253
|
*/
|
|
189
|
-
withDefaultMeta(result
|
|
254
|
+
withDefaultMeta(result) {
|
|
190
255
|
if (result._meta !== undefined)
|
|
191
256
|
return result;
|
|
192
|
-
return { ...result, _meta: this.resultMeta(requestId) };
|
|
193
|
-
}
|
|
194
|
-
modelVisible(payload, scope) {
|
|
195
|
-
switch (payload.kind) {
|
|
196
|
-
case 'end-user':
|
|
197
|
-
return new ModelVisibleToolError(payload.kind, payload.message, scope.requestId, undefined, undefined, payload.errorCode);
|
|
198
|
-
case 'bad-request':
|
|
199
|
-
return new ModelVisibleToolError(payload.kind, payload.callerMessage ?? payload.message, scope.requestId, payload.field, payload.callerMessage);
|
|
200
|
-
case 'coded':
|
|
201
|
-
return new ModelVisibleToolError(payload.kind, payload.message, scope.requestId, undefined, undefined, payload.errorCode, undefined, payload.statusCode);
|
|
202
|
-
case 'implementation':
|
|
203
|
-
return new ModelVisibleToolError(payload.kind, `Internal error in tool ${scope.toolName ?? 'unknown'} (requestId ${scope.requestId}). ` +
|
|
204
|
-
'This is a bug in the tool, not in your arguments; retrying with different ' +
|
|
205
|
-
'arguments will not help.', scope.requestId);
|
|
206
|
-
default:
|
|
207
|
-
return new ModelVisibleToolError(payload.kind, payload.message, scope.requestId, undefined, undefined, undefined, payload.retryAfterSeconds);
|
|
208
|
-
}
|
|
257
|
+
return { ...result, _meta: this.resultMeta(McpCorrelation.requestId()) };
|
|
209
258
|
}
|
|
210
|
-
|
|
211
|
-
|
|
259
|
+
httpError(status, code, message, headers) {
|
|
260
|
+
const correlation = McpCorrelation.current();
|
|
261
|
+
const requestId = McpCorrelation.requestId();
|
|
262
|
+
return new core_util_1.HttpResponseDto(new core_util_1.HttpResponseStatus(status, message), headers, new McpHttpErrorBody(new McpHttpErrorDetail(code, message, new McpErrorData(requestId)), correlation.jsonRpcId));
|
|
212
263
|
}
|
|
213
264
|
}
|
|
214
265
|
exports.WpMcpErrorTranslator = WpMcpErrorTranslator;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WpMcpErrorTranslator.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/WpMcpErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { CallToolResult, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';\nimport { Response } from 'express';\nimport {\n ApiBadRequestError,\n ApiErrorBoundary,\n ApiErrorPayload,\n ApiImplementationError,\n DtoValue,\n LogManager,\n toError,\n} from '@webpieces/core-util';\n\nconst log = LogManager.getLogger('WpMcpErrorTranslator');\n\n/** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */\nexport const MCP_REQUEST_ID_META_KEY = 'webpieces/requestId';\n\n/** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */\nexport class McpErrorData {\n constructor(public readonly requestId: string) {}\n}\n\n/** Correlation facts for one failure; rendered into the single operator log line. */\nexport class McpFailureScope {\n constructor(\n public readonly requestId: string,\n public readonly jsonRpcId: string | number | null,\n public readonly method: string,\n public readonly toolName?: string,\n ) {}\n\n describe(): string {\n const tool = this.toolName ? ` tool=${this.toolName}` : '';\n return `requestId=${this.requestId} jsonRpcId=${String(this.jsonRpcId)} method=${this.method}${tool}`;\n }\n}\n\n/**\n * The application's own `tools/call` error translation — the MCP twin of the `ErrorTranslators` that\n * `ExpressWrapper.handleError` consults through `ClientRegistry.tryTranslateToWire` on the HTTP path.\n * Register one with `WpMcpServerConfig.setErrorTranslator(...)`; it is NOT a global, because\n * `WpMcpServer` is constructed by app code and two servers may run in one process.\n *\n * An app owns its error taxonomy and owns how those errors should be explained to a model, so a\n * claimed error yields the ENTIRE `CallToolResult` — content, `structuredContent`, `isError`, the lot.\n *\n * Contract:\n * - `error` is the RAW thrown value: an app must be able to `instanceof` its own error classes.\n * `ApiErrorBoundary` never substitutes an object for the thrown error, so there is exactly one\n * error value on this path.\n * - return `undefined` for \"not mine\" — webpieces then renders its own default, unchanged.\n * - webpieces default-fills `_meta['webpieces/requestId']` only when the returned result has NO\n * `_meta`. An app that sets `_meta` owns it untouched.\n * - operator-detail logging has ALREADY happened when this is called, so claiming an error can never\n * silently kill observability.\n * - scope is `tools/call` ONLY. The pre-SDK HTTP boundary and `tools/list` stay framework-owned:\n * that boundary emits the `401 + WWW-Authenticate: Bearer resource_metadata=...` MCP clients\n * depend on for OAuth discovery, and an app rewriting it breaks connector onboarding.\n */\nexport interface McpErrorTranslators {\n toToolResult(error: Error, scope: McpFailureScope): CallToolResult | undefined;\n}\n\n/** The only error shape rendered into model-visible MCP tool content. */\nexport class ModelVisibleToolError {\n constructor(\n public readonly kind: string,\n public readonly message: string,\n public readonly requestId: string,\n public readonly field?: string,\n public readonly callerMessage?: string,\n public readonly errorCode?: string,\n public readonly retryAfterSeconds?: number,\n public readonly statusCode?: number,\n ) {}\n}\n\n/** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */\nexport class McpHttpErrorBody {\n readonly jsonrpc = '2.0';\n constructor(\n public readonly error: McpHttpErrorDetail,\n public readonly id: string | number | null,\n ) {}\n}\n\nexport class McpHttpErrorDetail {\n constructor(\n public readonly code: number,\n public readonly message: string,\n public readonly data: McpErrorData,\n ) {}\n}\n\n/**\n * The ONE place every MCP failure is mapped to a reply, mirroring `ApiErrorHttpMapper` for HTTP.\n * Each MCP entry point (the bind HTTP handler, tools/list, tools/call) has exactly one catch that\n * only delegates here. Classification and per-kind log levels are the shared `ApiErrorBoundary`\n * rules, and published text comes only from `ApiErrorBoundary.encode`: this class never puts an\n * error's own message on the wire unless it is an `ApiEndUserError`.\n */\nexport class WpMcpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary(log);\n\n /** `appTranslators` is the app's `tools/call` seam; `undefined` means webpieces renders all. */\n constructor(private readonly appTranslators?: McpErrorTranslators) {}\n\n /**\n * Pre-SDK failures: 401 + WWW-Authenticate, 403, 400 (-32700), anything else 500 (-32603).\n * A failure after response headers were sent can only end the stream.\n */\n toHttp(thrown: Error, res: Response, scope: McpFailureScope, wwwAuthenticate: string): void {\n this.boundary.logOperatorDetail(thrown, scope.describe());\n if (res.headersSent) {\n res.end();\n return;\n }\n const payload = this.boundary.encode(thrown);\n const generic = payload.message;\n switch (payload.kind) {\n case 'unauthorized':\n res.setHeader('WWW-Authenticate', wwwAuthenticate);\n this.writeHttp(res, 401, -32_000, generic, scope);\n return;\n case 'forbidden':\n this.writeHttp(res, 403, -32_000, generic, scope);\n return;\n case 'bad-request':\n this.writeHttp(res, 400, ProtocolErrorCode.ParseError, generic, scope);\n return;\n default:\n this.writeHttp(res, 500, ProtocolErrorCode.InternalError, 'Internal Error', scope);\n }\n }\n\n /** Express body-parser failures (malformed JSON, body too large) are caller input errors. */\n toHttpBodyFailure(cause: Error, res: Response, scope: McpFailureScope, wwwAuth: string): void {\n this.toHttp(\n new ApiBadRequestError(\n `MCP request body rejected: ${cause.message}`,\n undefined,\n undefined,\n cause,\n ),\n res,\n scope,\n wwwAuth,\n );\n }\n\n /** tools/list and other result-less methods: bad-request → -32602, anything else → -32603. */\n toProtocolError(thrown: Error, scope: McpFailureScope): ProtocolError {\n this.boundary.logOperatorDetail(thrown, scope.describe());\n const payload = this.boundary.encode(thrown);\n if (payload.kind === 'bad-request') {\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n payload.callerMessage ?? payload.message,\n new McpErrorData(scope.requestId),\n );\n }\n const message = payload.kind === 'end-user' ? payload.message : 'Internal Error';\n return new ProtocolError(\n ProtocolErrorCode.InternalError,\n message,\n new McpErrorData(scope.requestId),\n );\n }\n\n /** tools/call with a name no binding registered: a JSON-RPC protocol error per MCP 2026-07-28. */\n unknownTool(toolName: string, scope: McpFailureScope): ProtocolError {\n log.info(`Unknown MCP tool: ${scope.describe()}`);\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n `Unknown tool: ${toolName}`,\n new McpErrorData(scope.requestId),\n );\n }\n\n /**\n * Every tools/call failure after the tool is found: an `isError: true` result the model sees.\n *\n * The app's {@link McpErrorTranslators} gets FIRST REFUSAL — the same convention\n * `ExpressWrapper.handleError` applies on the HTTP path — and owns the entire result it claims.\n * Operator-detail logging runs BEFORE it, so exactly one operator line is written per failure\n * whoever renders the reply.\n */\n toToolResult(thrown: Error, scope: McpFailureScope): CallToolResult {\n this.boundary.logOperatorDetail(thrown, scope.describe());\n const claimed = this.appToolResult(thrown, scope);\n if (claimed) return this.withDefaultMeta(claimed, scope.requestId);\n const visible = this.modelVisible(this.boundary.encode(thrown), scope);\n const text = JSON.stringify(visible as DtoValue);\n return {\n content: [{ type: 'text', text }],\n isError: true,\n _meta: this.resultMeta(scope.requestId),\n };\n }\n\n /** The `_meta` every tools/call result carries so a user can quote its requestId. */\n resultMeta(requestId: string): Record<string, string> {\n return { [MCP_REQUEST_ID_META_KEY]: requestId };\n }\n\n /**\n * The app's first refusal. A translator that THROWS must not replace the failure being reported:\n * the throw is reported through the same boundary as its own implementation failure and the\n * ORIGINAL error still renders through the webpieces default, so the model still gets a reply\n * carrying the requestId.\n */\n private appToolResult(thrown: Error, scope: McpFailureScope): CallToolResult | undefined {\n if (!this.appTranslators) return undefined;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an app translator's own bug must not replace the failure it was asked to render\n try {\n return this.appTranslators.toToolResult(thrown, scope);\n } catch (err: unknown) {\n const error = toError(err);\n const failure = new ApiImplementationError(\n 'Application McpErrorTranslators.toToolResult threw; rendering the webpieces default.',\n error,\n );\n this.boundary.logOperatorDetail(failure, scope.describe());\n return undefined;\n }\n }\n\n /**\n * webpieces fills `_meta` only when the app left it off, keeping the README's \"every reply\n * carries the requestId\" promise a DEFAULT rather than a restriction: an app that set `_meta`\n * keeps it byte-for-byte.\n */\n private withDefaultMeta(result: CallToolResult, requestId: string): CallToolResult {\n if (result._meta !== undefined) return result;\n return { ...result, _meta: this.resultMeta(requestId) };\n }\n\n private modelVisible(payload: ApiErrorPayload, scope: McpFailureScope): ModelVisibleToolError {\n switch (payload.kind) {\n case 'end-user':\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n scope.requestId,\n undefined,\n undefined,\n payload.errorCode,\n );\n case 'bad-request':\n return new ModelVisibleToolError(\n payload.kind,\n payload.callerMessage ?? payload.message,\n scope.requestId,\n payload.field,\n payload.callerMessage,\n );\n case 'coded':\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n scope.requestId,\n undefined,\n undefined,\n payload.errorCode,\n undefined,\n payload.statusCode,\n );\n case 'implementation':\n return new ModelVisibleToolError(\n payload.kind,\n `Internal error in tool ${scope.toolName ?? 'unknown'} (requestId ${scope.requestId}). ` +\n 'This is a bug in the tool, not in your arguments; retrying with different ' +\n 'arguments will not help.',\n scope.requestId,\n );\n default:\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n scope.requestId,\n undefined,\n undefined,\n undefined,\n payload.retryAfterSeconds,\n );\n }\n }\n\n private writeHttp(\n res: Response,\n status: number,\n code: number,\n message: string,\n scope: McpFailureScope,\n ): void {\n res.status(status).json(\n new McpHttpErrorBody(\n new McpHttpErrorDetail(code, message, new McpErrorData(scope.requestId)),\n scope.jsonRpcId,\n ),\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"WpMcpErrorTranslator.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/WpMcpErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { CallToolResult, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';\nimport {\n ApiErrorBoundary,\n ApiErrorPayload,\n ContextKey,\n DtoValue,\n HttpHeader,\n HttpResponseDto,\n HttpResponseStatus,\n LogManager,\n toError,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\n\nconst log = LogManager.getLogger('WpMcpErrorTranslator');\n\n/** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */\nexport const MCP_REQUEST_ID_META_KEY = 'webpieces/requestId';\n\n/** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */\nexport class McpErrorData {\n constructor(public readonly requestId: string) {}\n}\n\n/**\n * The two per-request facts a renderer needs that are NOT the error: the JSON-RPC `id` it must echo,\n * and the tool being called. They are stamped into the request scope by `WpMcpServer` rather than\n * threaded through every renderer signature, because `requestId` already travels that way\n * ({@link WebpiecesCoreHeaders.REQUEST_ID}) and a second, parallel correlation parameter was a COPY\n * of the context — which is what the deleted `McpFailureScope` was.\n *\n * Context-only and never logged: the logging backends read `RequestContext` for correlation on every\n * record already, and this object exists purely so the renderers can be one-argument functions.\n */\nexport class McpCorrelation {\n // webpieces-disable no-any-unknown -- ContextKey value types are per-key; this one is context-only\n static readonly KEY = ContextKey.untrusted<McpCorrelation>(\n 'webpieces-mcp-correlation',\n undefined,\n false,\n /*isLogged*/ false,\n );\n\n constructor(\n public readonly jsonRpcId: string | number | null,\n public readonly toolName?: string,\n ) {}\n\n /** Stamp what this request knows so far; `WpMcpServer` re-stamps once the tool name is known. */\n // webpieces-disable no-function-outside-class -- static accessor trio for one context key\n static stamp(correlation: McpCorrelation): void {\n RequestContext.putUntrusted(McpCorrelation.KEY, correlation);\n }\n\n /** A renderer reached before anything stamped (a failure with no parseable body) still renders. */\n // webpieces-disable no-function-outside-class -- static accessor trio for one context key\n static current(): McpCorrelation {\n return RequestContext.getUntrusted(McpCorrelation.KEY) ?? new McpCorrelation(null);\n }\n\n /** The webpieces requestId of the request being served, as every MCP reply reports it. */\n // webpieces-disable no-function-outside-class -- static accessor trio for one context key\n static requestId(): string {\n return RequestContext.getUntrusted(WebpiecesCoreHeaders.REQUEST_ID) ?? 'missing-request-id';\n }\n}\n\n/** The only error shape rendered into model-visible MCP tool content. */\nexport class ModelVisibleToolError {\n constructor(\n public readonly kind: string,\n public readonly message: string,\n public readonly requestId: string,\n public readonly field?: string,\n public readonly callerMessage?: string,\n public readonly errorCode?: string,\n public readonly retryAfterSeconds?: number,\n public readonly statusCode?: number,\n ) {}\n}\n\n/** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */\nexport class McpHttpErrorBody {\n readonly jsonrpc = '2.0';\n constructor(\n public readonly error: McpHttpErrorDetail,\n public readonly id: string | number | null,\n ) {}\n}\n\nexport class McpHttpErrorDetail {\n constructor(\n public readonly code: number,\n public readonly message: string,\n public readonly data: McpErrorData,\n ) {}\n}\n\n/**\n * The application's own `tools/call` error translation — the MCP twin of the `ErrorTranslators` an\n * app registers on `ClientRegistry` for the HTTP path. Register one with\n * `WpMcpServerConfig.setErrorTranslator(...)`; it is NOT a global, because `WpMcpServer` is\n * constructed by app code and two servers may run in one process.\n *\n * An app owns its error taxonomy and owns how those errors should be explained to a model, so a\n * claimed error yields the ENTIRE `CallToolResult` — content, `structuredContent`, `isError`, the lot.\n *\n * Contract:\n * - `error` is the RAW thrown value: an app must be able to `instanceof` its own error classes.\n * `ApiErrorBoundary` never substitutes an object for the thrown error, so there is exactly one\n * error value on this path.\n * - There is no \"not mine\" return. A registered translator REPLACES the webpieces default and\n * declines by DELEGATING to it: `new McpDefaultToolCallRenderer().toToolCallResult(error)`.\n * - webpieces default-fills `_meta['webpieces/requestId']` only when the returned result has NO\n * `_meta`. An app that sets `_meta` owns it untouched.\n * - scope is `tools/call` ONLY. The pre-SDK HTTP boundary and `tools/list` stay framework-owned:\n * that boundary emits the `401 + WWW-Authenticate: Bearer resource_metadata=...` MCP clients\n * depend on for OAuth discovery, and an app rewriting it breaks connector onboarding.\n */\nexport interface McpErrorTranslators {\n toToolCallResult(error: Error): CallToolResult;\n}\n\n/**\n * webpieces' DEFAULT `tools/call` rendering, as a public class so an app's own\n * {@link McpErrorTranslators} can DECLINE an error by delegating to it — the MCP twin of\n * `ApiErrorHttpMapper.toResponse` on the HTTP side.\n *\n * Published text comes only from {@link ApiErrorBoundary.encode}: this never puts an error's own\n * message in front of a model unless it is an `ApiEndUserError`.\n */\nexport class McpDefaultToolCallRenderer implements McpErrorTranslators {\n private readonly boundary = new ApiErrorBoundary();\n\n toToolCallResult(error: Error): CallToolResult {\n const requestId = McpCorrelation.requestId();\n const visible = this.modelVisible(this.boundary.encode(error), requestId);\n const text = JSON.stringify(visible as DtoValue);\n return {\n content: [{ type: 'text', text }],\n isError: true,\n _meta: { [MCP_REQUEST_ID_META_KEY]: requestId },\n };\n }\n\n private modelVisible(payload: ApiErrorPayload, requestId: string): ModelVisibleToolError {\n switch (payload.kind) {\n case 'end-user':\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n requestId,\n undefined,\n undefined,\n payload.errorCode,\n );\n case 'bad-request':\n return new ModelVisibleToolError(\n payload.kind,\n payload.callerMessage ?? payload.message,\n requestId,\n payload.field,\n payload.callerMessage,\n );\n case 'coded':\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n requestId,\n undefined,\n undefined,\n payload.errorCode,\n undefined,\n payload.statusCode,\n );\n case 'implementation':\n return new ModelVisibleToolError(\n payload.kind,\n `Internal error in tool ${McpCorrelation.current().toolName ?? 'unknown'} ` +\n `(requestId ${requestId}). This is a bug in the tool, not in your ` +\n 'arguments; retrying with different arguments will not help.',\n requestId,\n );\n default:\n return new ModelVisibleToolError(\n payload.kind,\n payload.message,\n requestId,\n undefined,\n undefined,\n undefined,\n payload.retryAfterSeconds,\n );\n }\n }\n}\n\n/**\n * The ONE place every MCP failure becomes a reply, mirroring `ApiErrorHttpMapper` for HTTP: an error\n * in, the exact wire shape out. There is one method per BOUNDARY, and the MCP spec is what makes\n * them three rather than one:\n *\n * | Boundary | Who writes HTTP | Failure shape |\n * |--------------------------------|-----------------|-------------------------------------|\n * | pre-SDK bearer / Origin / body | **webpieces** | `HttpResponseDto<McpHttpErrorBody>` |\n * | `tools/list` | the SDK | `throw ProtocolError` |\n * | `tools/call` | the SDK | `CallToolResult` with `isError` |\n *\n * Classification and published text are the shared {@link ApiErrorBoundary} rules. Operator logging\n * is NOT here: `LogApiCall` wraps each of those boundaries in `WpMcpServer`, so every failure gets\n * exactly one `[API-server-resp-*]` line with the request and the identity on it.\n */\nexport class WpMcpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n private readonly defaultToolCall = new McpDefaultToolCallRenderer();\n\n /**\n * @param challenge - the `WWW-Authenticate` value for a 401, read lazily because\n * `WpMcpServerConfig` is only fully validated at `bind(...)` time.\n * @param appTranslators - the app's `tools/call` seam; `undefined` means webpieces renders all.\n */\n constructor(\n private readonly challenge: () => string,\n private readonly appTranslators?: McpErrorTranslators,\n ) {}\n\n /**\n * Boundary #1: everything webpieces itself rejects before the SDK is involved — bearer\n * verification, `Origin`, a malformed body. This is 401/403/400/500 today and any status\n * tomorrow, so it produces a VALUE like every other API in the framework and `WpMcpServer` hands\n * it to the shared express writer. The BODY stays JSON-RPC shaped because an MCP client expects\n * that; `HttpResponseDto` is generic, and its header LIST carries `WWW-Authenticate`.\n */\n toBearerBoundaryResponse(error: Error): HttpResponseDto<McpHttpErrorBody> {\n const payload = this.boundary.encode(error);\n const generic = payload.message;\n switch (payload.kind) {\n case 'unauthorized':\n return this.httpError(401, -32_000, generic, [\n new HttpHeader('WWW-Authenticate', this.challenge()),\n ]);\n case 'forbidden':\n return this.httpError(403, -32_000, generic, []);\n case 'bad-request':\n return this.httpError(400, ProtocolErrorCode.ParseError, generic, []);\n default:\n return this.httpError(500, ProtocolErrorCode.InternalError, 'Internal Error', []);\n }\n }\n\n /**\n * Boundary #2: `tools/list` has no tool-result channel, so a failure IS a JSON-RPC error. It\n * THROWS rather than returning a value the caller must remember to throw, and is typed `never`\n * so the compiler knows the call is terminal.\n *\n * This is LOAD-BEARING for disclosure, not defence in depth. Measured on\n * `@modelcontextprotocol/server` 2.0.0 and pinned by `McpSdkForeignThrow.spec.ts`: the SDK wraps\n * a foreign (non-`ProtocolError`) throw in `-32603` but copies its `message` onto the wire\n * VERBATIM. So a `tools/list` handler that lets any error escape un-translated publishes that\n * error's operator text to the caller.\n */\n toListError(error: Error): never {\n const payload = this.boundary.encode(error);\n const requestId = McpCorrelation.requestId();\n if (payload.kind === 'bad-request') {\n throw new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n payload.callerMessage ?? payload.message,\n new McpErrorData(requestId),\n );\n }\n const message = payload.kind === 'end-user' ? payload.message : 'Internal Error';\n throw new ProtocolError(\n ProtocolErrorCode.InternalError,\n message,\n new McpErrorData(requestId),\n );\n }\n\n /** tools/call with a name no binding registered: a JSON-RPC protocol error per MCP 2026-07-28. */\n unknownTool(toolName: string): ProtocolError {\n const requestId = McpCorrelation.requestId();\n log.info(`Unknown MCP tool: ${toolName} requestId=${requestId}`);\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n `Unknown tool: ${toolName}`,\n new McpErrorData(requestId),\n );\n }\n\n /**\n * Boundary #3: every tools/call failure after the tool is found — an `isError: true` result the\n * model sees.\n *\n * A registered {@link McpErrorTranslators} REPLACES the default and declines by delegating to\n * {@link McpDefaultToolCallRenderer}, so \"no translator registered\" and \"a translator that does\n * not claim this error\" produce byte-identical results.\n */\n toToolCallResult(failure: Error): CallToolResult {\n const renderer = this.appTranslators;\n if (!renderer) return this.defaultToolCall.toToolCallResult(failure);\n return this.withDefaultMeta(this.appOrDefault(renderer, failure));\n }\n\n /** The `_meta` every tools/call result carries so a user can quote its requestId. */\n resultMeta(requestId: string): Record<string, string> {\n return { [MCP_REQUEST_ID_META_KEY]: requestId };\n }\n\n /**\n * An app translator that THROWS must not replace the failure being reported: its own bug is\n * logged here and the ORIGINAL error still renders through the webpieces default, so the model\n * still gets a reply carrying the requestId.\n */\n private appOrDefault(renderer: McpErrorTranslators, failure: Error): CallToolResult {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an app translator's own bug must not replace the failure it was asked to render\n try {\n return renderer.toToolCallResult(failure);\n } catch (err: unknown) {\n const error = toError(err);\n log.error(\n 'Application McpErrorTranslators.toToolCallResult threw; rendering the webpieces default.',\n error,\n );\n return this.defaultToolCall.toToolCallResult(failure);\n }\n }\n\n /**\n * webpieces fills `_meta` only when the app left it off, keeping the README's \"every reply\n * carries the requestId\" promise a DEFAULT rather than a restriction: an app that set `_meta`\n * keeps it byte-for-byte.\n */\n private withDefaultMeta(result: CallToolResult): CallToolResult {\n if (result._meta !== undefined) return result;\n return { ...result, _meta: this.resultMeta(McpCorrelation.requestId()) };\n }\n\n private httpError(\n status: number,\n code: number,\n message: string,\n headers: HttpHeader[],\n ): HttpResponseDto<McpHttpErrorBody> {\n const correlation = McpCorrelation.current();\n const requestId = McpCorrelation.requestId();\n return new HttpResponseDto<McpHttpErrorBody>(\n new HttpResponseStatus(status, message),\n headers,\n new McpHttpErrorBody(\n new McpHttpErrorDetail(code, message, new McpErrorData(requestId)),\n correlation.jsonRpcId,\n ),\n );\n }\n}\n"]}
|
package/src/WpMcpServer.d.ts
CHANGED
|
@@ -5,14 +5,23 @@ import { McpBindOptions } from './McpBindOptions';
|
|
|
5
5
|
* Modern-only MCP 2026-07-28 adapter around the official SDK's request-scoped HTTP handler.
|
|
6
6
|
*
|
|
7
7
|
* Error boundary: every failure is mapped by the one `WpMcpErrorTranslator`, and each entry point
|
|
8
|
-
* has exactly one catch that only delegates to it: the bind HTTP handler
|
|
9
|
-
* (`
|
|
10
|
-
* the SDK's listen router, so Webpieces has no handler
|
|
11
|
-
* verified exactly once per POST, at the HTTP
|
|
8
|
+
* has exactly one catch that only delegates to it: the bind HTTP handler
|
|
9
|
+
* (`toBearerBoundaryResponse`), tools/list (`toListError`) and tools/call (`toToolCallResult`).
|
|
10
|
+
* `subscriptions/listen` is served entirely by the SDK's listen router, so Webpieces has no handler
|
|
11
|
+
* (and no catch) there. The external bearer is verified exactly once per POST, at the HTTP
|
|
12
|
+
* boundary, before the SDK is involved.
|
|
12
13
|
*/
|
|
13
14
|
export declare class WpMcpServer<TGrant, TMintRequest> {
|
|
14
15
|
private readonly config;
|
|
15
16
|
private readonly dispatcher;
|
|
17
|
+
/**
|
|
18
|
+
* These edges have NO filter chain above them — `LogApiFilter` never sees a rejected bearer, a
|
|
19
|
+
* bad `Origin`, a body express could not parse, or a `tools/list` the SDK renders itself.
|
|
20
|
+
* Wrapping each in `LogApiCall` is this repo's one line on logging, and it is why
|
|
21
|
+
* `ApiErrorBoundary` no longer writes a second, barer line of its own.
|
|
22
|
+
*/
|
|
23
|
+
private readonly logApiCall;
|
|
24
|
+
private readonly responseWriter;
|
|
16
25
|
/** tools/call gives the app's translators first refusal; every other reply stays framework-owned. */
|
|
17
26
|
private readonly translator;
|
|
18
27
|
private registry?;
|
|
@@ -46,6 +55,18 @@ export declare class WpMcpServer<TGrant, TMintRequest> {
|
|
|
46
55
|
private createHandler;
|
|
47
56
|
/** Entry point #1: the one catch for everything before and around the SDK exchange. */
|
|
48
57
|
private handleHttp;
|
|
58
|
+
/** Express hands body-parser failures (bad JSON, body too large) here, outside every filter. */
|
|
59
|
+
private handleBodyFailure;
|
|
60
|
+
/** A body express could not parse is a caller input error, reported through the same edge. */
|
|
61
|
+
private rejectBody;
|
|
62
|
+
/**
|
|
63
|
+
* The pre-SDK boundary owns the socket, so it produces a VALUE and writes it through the SAME
|
|
64
|
+
* express writer `ExpressWrapper` uses. A failure after headers were sent can only end the
|
|
65
|
+
* stream.
|
|
66
|
+
*/
|
|
67
|
+
private writeBoundaryError;
|
|
68
|
+
/** Run one un-filtered edge step under `LogApiCall`, so its failure gets exactly one log line. */
|
|
69
|
+
private logEdge;
|
|
49
70
|
private serveExpress;
|
|
50
71
|
/** Verifies the external bearer once for this POST. Handlers never re-verify it. */
|
|
51
72
|
private authenticate;
|
|
@@ -67,9 +88,8 @@ export declare class WpMcpServer<TGrant, TMintRequest> {
|
|
|
67
88
|
private authInfo;
|
|
68
89
|
private progressReporter;
|
|
69
90
|
private progressParams;
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
private httpScope;
|
|
91
|
+
/** What the raw POST body tells us before the SDK has parsed anything: the id, and the tool. */
|
|
92
|
+
private correlationOf;
|
|
73
93
|
private challenge;
|
|
74
94
|
private bodyObject;
|
|
75
95
|
private hasProgressToken;
|