@webpieces/mcp-server 0.4.790 → 0.4.792
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -13
- package/package.json +6 -6
- package/src/McpApiDispatcher.js +6 -0
- package/src/McpApiDispatcher.js.map +1 -1
- package/src/McpAuth.d.ts +4 -12
- package/src/McpAuth.js +4 -20
- package/src/McpAuth.js.map +1 -1
- package/src/McpRegistry.d.ts +40 -0
- package/src/McpRegistry.js +54 -0
- package/src/McpRegistry.js.map +1 -0
- package/src/McpToolCallRendering.d.ts +111 -0
- package/src/McpToolCallRendering.js +135 -0
- package/src/McpToolCallRendering.js.map +1 -0
- package/src/WpMcpErrorTranslator.d.ts +11 -99
- package/src/WpMcpErrorTranslator.js +27 -157
- package/src/WpMcpErrorTranslator.js.map +1 -1
- package/src/WpMcpServer.d.ts +1 -1
- package/src/WpMcpServer.js +8 -7
- package/src/WpMcpServer.js.map +1 -1
- package/src/__tests__/McpRemoteFixtures.d.ts +4 -3
- package/src/__tests__/McpRemoteFixtures.js +11 -11
- package/src/__tests__/McpRemoteFixtures.js.map +1 -1
- package/src/index.d.ts +4 -2
- package/src/index.js +12 -8
- package/src/index.js.map +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpDefaultToolCallRenderer = exports.McpHttpErrorDetail = exports.McpHttpErrorBody = exports.ModelVisibleToolError = exports.McpCorrelation = exports.McpErrorData = exports.MCP_REQUEST_ID_META_KEY = void 0;
|
|
4
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
5
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
6
|
+
/** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */
|
|
7
|
+
exports.MCP_REQUEST_ID_META_KEY = 'webpieces/requestId';
|
|
8
|
+
/** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */
|
|
9
|
+
class McpErrorData {
|
|
10
|
+
requestId;
|
|
11
|
+
constructor(requestId) {
|
|
12
|
+
this.requestId = requestId;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.McpErrorData = McpErrorData;
|
|
16
|
+
/**
|
|
17
|
+
* The two per-request facts a renderer needs that are NOT the error: the JSON-RPC `id` it must echo,
|
|
18
|
+
* and the tool being called. They are stamped into the request scope by `WpMcpServer` rather than
|
|
19
|
+
* threaded through every renderer signature, because `requestId` already travels that way
|
|
20
|
+
* ({@link WebpiecesCoreHeaders.REQUEST_ID}) and a second, parallel correlation parameter was a COPY
|
|
21
|
+
* of the context — which is what the deleted `McpFailureScope` was.
|
|
22
|
+
*
|
|
23
|
+
* Context-only and never logged: the logging backends read `RequestContext` for correlation on every
|
|
24
|
+
* record already, and this object exists purely so the renderers can be one-argument functions.
|
|
25
|
+
*/
|
|
26
|
+
class McpCorrelation {
|
|
27
|
+
jsonRpcId;
|
|
28
|
+
toolName;
|
|
29
|
+
// webpieces-disable no-any-unknown -- ContextKey value types are per-key; this one is context-only
|
|
30
|
+
static KEY = core_util_1.ContextKey.untrusted('webpieces-mcp-correlation', undefined, false,
|
|
31
|
+
/*isLogged*/ false);
|
|
32
|
+
constructor(jsonRpcId, toolName) {
|
|
33
|
+
this.jsonRpcId = jsonRpcId;
|
|
34
|
+
this.toolName = toolName;
|
|
35
|
+
}
|
|
36
|
+
/** Stamp what this request knows so far; `WpMcpServer` re-stamps once the tool name is known. */
|
|
37
|
+
// webpieces-disable no-function-outside-class -- static accessor trio for one context key
|
|
38
|
+
static stamp(correlation) {
|
|
39
|
+
core_context_1.RequestContext.putUntrusted(McpCorrelation.KEY, correlation);
|
|
40
|
+
}
|
|
41
|
+
/** A renderer reached before anything stamped (a failure with no parseable body) still renders. */
|
|
42
|
+
// webpieces-disable no-function-outside-class -- static accessor trio for one context key
|
|
43
|
+
static current() {
|
|
44
|
+
return core_context_1.RequestContext.getUntrusted(McpCorrelation.KEY) ?? new McpCorrelation(null);
|
|
45
|
+
}
|
|
46
|
+
/** The webpieces requestId of the request being served, as every MCP reply reports it. */
|
|
47
|
+
// webpieces-disable no-function-outside-class -- static accessor trio for one context key
|
|
48
|
+
static requestId() {
|
|
49
|
+
return core_context_1.RequestContext.getUntrusted(core_util_1.WebpiecesCoreHeaders.REQUEST_ID) ?? 'missing-request-id';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.McpCorrelation = McpCorrelation;
|
|
53
|
+
/** The only error shape rendered into model-visible MCP tool content. */
|
|
54
|
+
class ModelVisibleToolError {
|
|
55
|
+
kind;
|
|
56
|
+
message;
|
|
57
|
+
requestId;
|
|
58
|
+
field;
|
|
59
|
+
callerMessage;
|
|
60
|
+
errorCode;
|
|
61
|
+
retryAfterSeconds;
|
|
62
|
+
statusCode;
|
|
63
|
+
constructor(kind, message, requestId, field, callerMessage, errorCode, retryAfterSeconds, statusCode) {
|
|
64
|
+
this.kind = kind;
|
|
65
|
+
this.message = message;
|
|
66
|
+
this.requestId = requestId;
|
|
67
|
+
this.field = field;
|
|
68
|
+
this.callerMessage = callerMessage;
|
|
69
|
+
this.errorCode = errorCode;
|
|
70
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
71
|
+
this.statusCode = statusCode;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.ModelVisibleToolError = ModelVisibleToolError;
|
|
75
|
+
/** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */
|
|
76
|
+
class McpHttpErrorBody {
|
|
77
|
+
error;
|
|
78
|
+
id;
|
|
79
|
+
jsonrpc = '2.0';
|
|
80
|
+
constructor(error, id) {
|
|
81
|
+
this.error = error;
|
|
82
|
+
this.id = id;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.McpHttpErrorBody = McpHttpErrorBody;
|
|
86
|
+
class McpHttpErrorDetail {
|
|
87
|
+
code;
|
|
88
|
+
message;
|
|
89
|
+
data;
|
|
90
|
+
constructor(code, message, data) {
|
|
91
|
+
this.code = code;
|
|
92
|
+
this.message = message;
|
|
93
|
+
this.data = data;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.McpHttpErrorDetail = McpHttpErrorDetail;
|
|
97
|
+
/**
|
|
98
|
+
* webpieces' DEFAULT `tools/call` rendering, and what {@link McpRegistry} holds until an app installs
|
|
99
|
+
* its own. Public so an app's own {@link McpErrorTranslator} can DECLINE an error by delegating to it
|
|
100
|
+
* — the MCP twin of `WebpiecesDefaultErrorTranslator.toWire` on the HTTP side.
|
|
101
|
+
*
|
|
102
|
+
* Published text comes only from {@link ApiErrorBoundary.encode}: this never puts an error's own
|
|
103
|
+
* message in front of a model unless it is an `ApiEndUserError`.
|
|
104
|
+
*/
|
|
105
|
+
class McpDefaultToolCallRenderer {
|
|
106
|
+
boundary = new core_util_1.ApiErrorBoundary();
|
|
107
|
+
toWire(error) {
|
|
108
|
+
const requestId = McpCorrelation.requestId();
|
|
109
|
+
const visible = this.modelVisible(this.boundary.encode(error), requestId);
|
|
110
|
+
const text = JSON.stringify(visible);
|
|
111
|
+
return {
|
|
112
|
+
content: [{ type: 'text', text }],
|
|
113
|
+
isError: true,
|
|
114
|
+
_meta: { [exports.MCP_REQUEST_ID_META_KEY]: requestId },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
modelVisible(payload, requestId) {
|
|
118
|
+
switch (payload.kind) {
|
|
119
|
+
case 'end-user':
|
|
120
|
+
return new ModelVisibleToolError(payload.kind, payload.message, requestId, undefined, undefined, payload.errorCode);
|
|
121
|
+
case 'bad-request':
|
|
122
|
+
return new ModelVisibleToolError(payload.kind, payload.callerMessage ?? payload.message, requestId, payload.field, payload.callerMessage);
|
|
123
|
+
case 'coded':
|
|
124
|
+
return new ModelVisibleToolError(payload.kind, payload.message, requestId, undefined, undefined, payload.errorCode, undefined, payload.statusCode);
|
|
125
|
+
case 'implementation':
|
|
126
|
+
return new ModelVisibleToolError(payload.kind, `Internal error in tool ${McpCorrelation.current().toolName ?? 'unknown'} ` +
|
|
127
|
+
`(requestId ${requestId}). This is a bug in the tool, not in your ` +
|
|
128
|
+
'arguments; retrying with different arguments will not help.', requestId);
|
|
129
|
+
default:
|
|
130
|
+
return new ModelVisibleToolError(payload.kind, payload.message, requestId, undefined, undefined, undefined, payload.retryAfterSeconds);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
exports.McpDefaultToolCallRenderer = McpDefaultToolCallRenderer;
|
|
135
|
+
//# sourceMappingURL=McpToolCallRendering.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpToolCallRendering.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolCallRendering.ts"],"names":[],"mappings":"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","sourcesContent":["import { CallToolResult } from '@modelcontextprotocol/server';\nimport {\n ApiErrorBoundary,\n ApiErrorPayload,\n ContextKey,\n DtoValue,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { RequestContext } from '@webpieces/core-context';\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 `ErrorTranslator` an app\n * registers on `ClientRegistry` for HTTP and the `IpcErrorTranslator` it registers on `IpcRegistry`.\n * Register one with {@link McpRegistry.setErrorTranslator}.\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 * # There is NO `fromWire`, and that is the protocol, not an oversight\n *\n * The HTTP and IPC translators have two halves because webpieces sits on BOTH ends of those wires: it\n * writes the response AND, in another process, reads one. **webpieces is never the MCP client.** The\n * client is Claude (or another model host); there is no return path for webpieces to translate, so\n * the pair is one-directional by nature. Do not \"restore the symmetry\" by adding a half with nothing\n * to call it.\n *\n * # The pre-SDK boundary does NOT share the HTTP `ErrorTranslator`, despite the identical type\n *\n * `WpMcpErrorTranslator.toBearerBoundaryResponse` returns an `HttpResponseDto`, exactly like\n * `ErrorTranslator.toWire` — and it must STILL not be routed through it. Two reasons, both hard:\n *\n * - its body must stay JSON-RPC shaped (`McpHttpErrorBody`) or an MCP client cannot parse the reply\n * at all, whereas an app's HTTP translator publishes the app's own REST envelope;\n * - it emits the `401 + WWW-Authenticate: Bearer resource_metadata=...` that MCP clients use for OAuth\n * discovery, so an app rewriting it breaks connector onboarding.\n *\n * Same TYPE, different OWNER. `tools/list` is framework-owned for the same reason.\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().toWire(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.\n */\nexport interface McpErrorTranslator {\n /** An error in, the whole `tools/call` reply out. Named for its HTTP and IPC twins. */\n toWire(error: Error): CallToolResult;\n}\n\n/**\n * webpieces' DEFAULT `tools/call` rendering, and what {@link McpRegistry} holds until an app installs\n * its own. Public so an app's own {@link McpErrorTranslator} can DECLINE an error by delegating to it\n * — the MCP twin of `WebpiecesDefaultErrorTranslator.toWire` 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 McpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n\n toWire(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"]}
|
|
@@ -1,98 +1,8 @@
|
|
|
1
1
|
import { CallToolResult, ProtocolError } from '@modelcontextprotocol/server';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
export declare const MCP_REQUEST_ID_META_KEY = "webpieces/requestId";
|
|
5
|
-
/** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */
|
|
6
|
-
export declare class McpErrorData {
|
|
7
|
-
readonly requestId: string;
|
|
8
|
-
constructor(requestId: string);
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* The two per-request facts a renderer needs that are NOT the error: the JSON-RPC `id` it must echo,
|
|
12
|
-
* and the tool being called. They are stamped into the request scope by `WpMcpServer` rather than
|
|
13
|
-
* threaded through every renderer signature, because `requestId` already travels that way
|
|
14
|
-
* ({@link WebpiecesCoreHeaders.REQUEST_ID}) and a second, parallel correlation parameter was a COPY
|
|
15
|
-
* of the context — which is what the deleted `McpFailureScope` was.
|
|
16
|
-
*
|
|
17
|
-
* Context-only and never logged: the logging backends read `RequestContext` for correlation on every
|
|
18
|
-
* record already, and this object exists purely so the renderers can be one-argument functions.
|
|
19
|
-
*/
|
|
20
|
-
export declare class McpCorrelation {
|
|
21
|
-
readonly jsonRpcId: string | number | null;
|
|
22
|
-
readonly toolName?: string | undefined;
|
|
23
|
-
static readonly KEY: ContextKey<McpCorrelation, "untrusted">;
|
|
24
|
-
constructor(jsonRpcId: string | number | null, toolName?: string | undefined);
|
|
25
|
-
/** Stamp what this request knows so far; `WpMcpServer` re-stamps once the tool name is known. */
|
|
26
|
-
static stamp(correlation: McpCorrelation): void;
|
|
27
|
-
/** A renderer reached before anything stamped (a failure with no parseable body) still renders. */
|
|
28
|
-
static current(): McpCorrelation;
|
|
29
|
-
/** The webpieces requestId of the request being served, as every MCP reply reports it. */
|
|
30
|
-
static requestId(): string;
|
|
31
|
-
}
|
|
32
|
-
/** The only error shape rendered into model-visible MCP tool content. */
|
|
33
|
-
export declare class ModelVisibleToolError {
|
|
34
|
-
readonly kind: string;
|
|
35
|
-
readonly message: string;
|
|
36
|
-
readonly requestId: string;
|
|
37
|
-
readonly field?: string | undefined;
|
|
38
|
-
readonly callerMessage?: string | undefined;
|
|
39
|
-
readonly errorCode?: string | undefined;
|
|
40
|
-
readonly retryAfterSeconds?: number | undefined;
|
|
41
|
-
readonly statusCode?: number | undefined;
|
|
42
|
-
constructor(kind: string, message: string, requestId: string, field?: string | undefined, callerMessage?: string | undefined, errorCode?: string | undefined, retryAfterSeconds?: number | undefined, statusCode?: number | undefined);
|
|
43
|
-
}
|
|
44
|
-
/** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */
|
|
45
|
-
export declare class McpHttpErrorBody {
|
|
46
|
-
readonly error: McpHttpErrorDetail;
|
|
47
|
-
readonly id: string | number | null;
|
|
48
|
-
readonly jsonrpc = "2.0";
|
|
49
|
-
constructor(error: McpHttpErrorDetail, id: string | number | null);
|
|
50
|
-
}
|
|
51
|
-
export declare class McpHttpErrorDetail {
|
|
52
|
-
readonly code: number;
|
|
53
|
-
readonly message: string;
|
|
54
|
-
readonly data: McpErrorData;
|
|
55
|
-
constructor(code: number, message: string, data: McpErrorData);
|
|
56
|
-
}
|
|
2
|
+
import { HttpResponseDto } from '@webpieces/core-util';
|
|
3
|
+
import { McpHttpErrorBody } from './McpToolCallRendering';
|
|
57
4
|
/**
|
|
58
|
-
* The
|
|
59
|
-
* app registers on `ClientRegistry` for the HTTP path. Register one with
|
|
60
|
-
* `WpMcpServerConfig.setErrorTranslator(...)`; it is NOT a global, because `WpMcpServer` is
|
|
61
|
-
* constructed by app code and two servers may run in one process.
|
|
62
|
-
*
|
|
63
|
-
* An app owns its error taxonomy and owns how those errors should be explained to a model, so a
|
|
64
|
-
* claimed error yields the ENTIRE `CallToolResult` — content, `structuredContent`, `isError`, the lot.
|
|
65
|
-
*
|
|
66
|
-
* Contract:
|
|
67
|
-
* - `error` is the RAW thrown value: an app must be able to `instanceof` its own error classes.
|
|
68
|
-
* `ApiErrorBoundary` never substitutes an object for the thrown error, so there is exactly one
|
|
69
|
-
* error value on this path.
|
|
70
|
-
* - There is no "not mine" return. A registered translator REPLACES the webpieces default and
|
|
71
|
-
* declines by DELEGATING to it: `new McpDefaultToolCallRenderer().toToolCallResult(error)`.
|
|
72
|
-
* - webpieces default-fills `_meta['webpieces/requestId']` only when the returned result has NO
|
|
73
|
-
* `_meta`. An app that sets `_meta` owns it untouched.
|
|
74
|
-
* - scope is `tools/call` ONLY. The pre-SDK HTTP boundary and `tools/list` stay framework-owned:
|
|
75
|
-
* that boundary emits the `401 + WWW-Authenticate: Bearer resource_metadata=...` MCP clients
|
|
76
|
-
* depend on for OAuth discovery, and an app rewriting it breaks connector onboarding.
|
|
77
|
-
*/
|
|
78
|
-
export interface McpErrorTranslators {
|
|
79
|
-
toToolCallResult(error: Error): CallToolResult;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* webpieces' DEFAULT `tools/call` rendering, as a public class so an app's own
|
|
83
|
-
* {@link McpErrorTranslators} can DECLINE an error by delegating to it — the MCP twin of
|
|
84
|
-
* `ApiErrorHttpMapper.toResponse` on the HTTP side.
|
|
85
|
-
*
|
|
86
|
-
* Published text comes only from {@link ApiErrorBoundary.encode}: this never puts an error's own
|
|
87
|
-
* message in front of a model unless it is an `ApiEndUserError`.
|
|
88
|
-
*/
|
|
89
|
-
export declare class McpDefaultToolCallRenderer implements McpErrorTranslators {
|
|
90
|
-
private readonly boundary;
|
|
91
|
-
toToolCallResult(error: Error): CallToolResult;
|
|
92
|
-
private modelVisible;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* The ONE place every MCP failure becomes a reply, mirroring `ApiErrorHttpMapper` for HTTP: an error
|
|
5
|
+
* The ONE place every MCP failure becomes a reply, mirroring `WebpiecesDefaultErrorTranslator` for HTTP: an error
|
|
96
6
|
* in, the exact wire shape out. There is one method per BOUNDARY, and the MCP spec is what makes
|
|
97
7
|
* them three rather than one:
|
|
98
8
|
*
|
|
@@ -108,15 +18,13 @@ export declare class McpDefaultToolCallRenderer implements McpErrorTranslators {
|
|
|
108
18
|
*/
|
|
109
19
|
export declare class WpMcpErrorTranslator {
|
|
110
20
|
private readonly challenge;
|
|
111
|
-
private readonly appTranslators?;
|
|
112
21
|
private readonly boundary;
|
|
113
22
|
private readonly defaultToolCall;
|
|
114
23
|
/**
|
|
115
24
|
* @param challenge - the `WWW-Authenticate` value for a 401, read lazily because
|
|
116
25
|
* `WpMcpServerConfig` is only fully validated at `bind(...)` time.
|
|
117
|
-
* @param appTranslators - the app's `tools/call` seam; `undefined` means webpieces renders all.
|
|
118
26
|
*/
|
|
119
|
-
constructor(challenge: () => string
|
|
27
|
+
constructor(challenge: () => string);
|
|
120
28
|
/**
|
|
121
29
|
* Boundary #1: everything webpieces itself rejects before the SDK is involved — bearer
|
|
122
30
|
* verification, `Origin`, a malformed body. This is 401/403/400/500 today and any status
|
|
@@ -143,9 +51,10 @@ export declare class WpMcpErrorTranslator {
|
|
|
143
51
|
* Boundary #3: every tools/call failure after the tool is found — an `isError: true` result the
|
|
144
52
|
* model sees.
|
|
145
53
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
54
|
+
* ONE unconditional line: {@link McpRegistry.getErrorTranslator} is never undefined, so there is
|
|
55
|
+
* no "was one registered" branch. A registered {@link McpErrorTranslator} REPLACES the default and
|
|
56
|
+
* declines by delegating to {@link McpDefaultToolCallRenderer}, so "no translator registered" and
|
|
57
|
+
* "a translator that does not claim this error" produce byte-identical results.
|
|
149
58
|
*/
|
|
150
59
|
toToolCallResult(failure: Error): CallToolResult;
|
|
151
60
|
/** The `_meta` every tools/call result carries so a user can quote its requestId. */
|
|
@@ -154,6 +63,9 @@ export declare class WpMcpErrorTranslator {
|
|
|
154
63
|
* An app translator that THROWS must not replace the failure being reported: its own bug is
|
|
155
64
|
* logged here and the ORIGINAL error still renders through the webpieces default, so the model
|
|
156
65
|
* still gets a reply carrying the requestId.
|
|
66
|
+
*
|
|
67
|
+
* This is BUG CONTAINMENT, not a "did anyone register one" branch — the same shape as
|
|
68
|
+
* `ClientErrorTranslator.throwIfFailure` on the HTTP side.
|
|
157
69
|
*/
|
|
158
70
|
private appOrDefault;
|
|
159
71
|
/**
|
|
@@ -1,141 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WpMcpErrorTranslator =
|
|
3
|
+
exports.WpMcpErrorTranslator = void 0;
|
|
4
4
|
const server_1 = require("@modelcontextprotocol/server");
|
|
5
5
|
const core_util_1 = require("@webpieces/core-util");
|
|
6
|
-
const
|
|
6
|
+
const McpRegistry_1 = require("./McpRegistry");
|
|
7
|
+
const McpToolCallRendering_1 = require("./McpToolCallRendering");
|
|
7
8
|
const log = core_util_1.LogManager.getLogger('WpMcpErrorTranslator');
|
|
8
|
-
/** `_meta` key carrying the Webpieces requestId on every tools/call result, success or failure. */
|
|
9
|
-
exports.MCP_REQUEST_ID_META_KEY = 'webpieces/requestId';
|
|
10
|
-
/** JSON-RPC error `data` so a user can quote the requestId of any protocol-level failure. */
|
|
11
|
-
class McpErrorData {
|
|
12
|
-
requestId;
|
|
13
|
-
constructor(requestId) {
|
|
14
|
-
this.requestId = requestId;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
exports.McpErrorData = McpErrorData;
|
|
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 {
|
|
29
|
-
jsonRpcId;
|
|
30
|
-
toolName;
|
|
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) {
|
|
35
|
-
this.jsonRpcId = jsonRpcId;
|
|
36
|
-
this.toolName = toolName;
|
|
37
|
-
}
|
|
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';
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
exports.McpCorrelation = McpCorrelation;
|
|
55
|
-
/** The only error shape rendered into model-visible MCP tool content. */
|
|
56
|
-
class ModelVisibleToolError {
|
|
57
|
-
kind;
|
|
58
|
-
message;
|
|
59
|
-
requestId;
|
|
60
|
-
field;
|
|
61
|
-
callerMessage;
|
|
62
|
-
errorCode;
|
|
63
|
-
retryAfterSeconds;
|
|
64
|
-
statusCode;
|
|
65
|
-
constructor(kind, message, requestId, field, callerMessage, errorCode, retryAfterSeconds, statusCode) {
|
|
66
|
-
this.kind = kind;
|
|
67
|
-
this.message = message;
|
|
68
|
-
this.requestId = requestId;
|
|
69
|
-
this.field = field;
|
|
70
|
-
this.callerMessage = callerMessage;
|
|
71
|
-
this.errorCode = errorCode;
|
|
72
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
73
|
-
this.statusCode = statusCode;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
exports.ModelVisibleToolError = ModelVisibleToolError;
|
|
77
|
-
/** JSON-RPC error body written by the HTTP boundary before the SDK is involved. */
|
|
78
|
-
class McpHttpErrorBody {
|
|
79
|
-
error;
|
|
80
|
-
id;
|
|
81
|
-
jsonrpc = '2.0';
|
|
82
|
-
constructor(error, id) {
|
|
83
|
-
this.error = error;
|
|
84
|
-
this.id = id;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
exports.McpHttpErrorBody = McpHttpErrorBody;
|
|
88
|
-
class McpHttpErrorDetail {
|
|
89
|
-
code;
|
|
90
|
-
message;
|
|
91
|
-
data;
|
|
92
|
-
constructor(code, message, data) {
|
|
93
|
-
this.code = code;
|
|
94
|
-
this.message = message;
|
|
95
|
-
this.data = data;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
exports.McpHttpErrorDetail = McpHttpErrorDetail;
|
|
99
|
-
/**
|
|
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
9
|
/**
|
|
138
|
-
* The ONE place every MCP failure becomes a reply, mirroring `
|
|
10
|
+
* The ONE place every MCP failure becomes a reply, mirroring `WebpiecesDefaultErrorTranslator` for HTTP: an error
|
|
139
11
|
* in, the exact wire shape out. There is one method per BOUNDARY, and the MCP spec is what makes
|
|
140
12
|
* them three rather than one:
|
|
141
13
|
*
|
|
@@ -151,17 +23,14 @@ exports.McpDefaultToolCallRenderer = McpDefaultToolCallRenderer;
|
|
|
151
23
|
*/
|
|
152
24
|
class WpMcpErrorTranslator {
|
|
153
25
|
challenge;
|
|
154
|
-
appTranslators;
|
|
155
26
|
boundary = new core_util_1.ApiErrorBoundary();
|
|
156
|
-
defaultToolCall = new McpDefaultToolCallRenderer();
|
|
27
|
+
defaultToolCall = new McpToolCallRendering_1.McpDefaultToolCallRenderer();
|
|
157
28
|
/**
|
|
158
29
|
* @param challenge - the `WWW-Authenticate` value for a 401, read lazily because
|
|
159
30
|
* `WpMcpServerConfig` is only fully validated at `bind(...)` time.
|
|
160
|
-
* @param appTranslators - the app's `tools/call` seam; `undefined` means webpieces renders all.
|
|
161
31
|
*/
|
|
162
|
-
constructor(challenge
|
|
32
|
+
constructor(challenge) {
|
|
163
33
|
this.challenge = challenge;
|
|
164
|
-
this.appTranslators = appTranslators;
|
|
165
34
|
}
|
|
166
35
|
/**
|
|
167
36
|
* Boundary #1: everything webpieces itself rejects before the SDK is involved — bearer
|
|
@@ -199,51 +68,52 @@ class WpMcpErrorTranslator {
|
|
|
199
68
|
*/
|
|
200
69
|
toListError(error) {
|
|
201
70
|
const payload = this.boundary.encode(error);
|
|
202
|
-
const requestId = McpCorrelation.requestId();
|
|
71
|
+
const requestId = McpToolCallRendering_1.McpCorrelation.requestId();
|
|
203
72
|
if (payload.kind === 'bad-request') {
|
|
204
|
-
throw new server_1.ProtocolError(server_1.ProtocolErrorCode.InvalidParams, payload.callerMessage ?? payload.message, new McpErrorData(requestId));
|
|
73
|
+
throw new server_1.ProtocolError(server_1.ProtocolErrorCode.InvalidParams, payload.callerMessage ?? payload.message, new McpToolCallRendering_1.McpErrorData(requestId));
|
|
205
74
|
}
|
|
206
75
|
const message = payload.kind === 'end-user' ? payload.message : 'Internal Error';
|
|
207
|
-
throw new server_1.ProtocolError(server_1.ProtocolErrorCode.InternalError, message, new McpErrorData(requestId));
|
|
76
|
+
throw new server_1.ProtocolError(server_1.ProtocolErrorCode.InternalError, message, new McpToolCallRendering_1.McpErrorData(requestId));
|
|
208
77
|
}
|
|
209
78
|
/** tools/call with a name no binding registered: a JSON-RPC protocol error in either MCP era. */
|
|
210
79
|
unknownTool(toolName) {
|
|
211
|
-
const requestId = McpCorrelation.requestId();
|
|
80
|
+
const requestId = McpToolCallRendering_1.McpCorrelation.requestId();
|
|
212
81
|
log.info(`Unknown MCP tool: ${toolName} requestId=${requestId}`);
|
|
213
|
-
return new server_1.ProtocolError(server_1.ProtocolErrorCode.InvalidParams, `Unknown tool: ${toolName}`, new McpErrorData(requestId));
|
|
82
|
+
return new server_1.ProtocolError(server_1.ProtocolErrorCode.InvalidParams, `Unknown tool: ${toolName}`, new McpToolCallRendering_1.McpErrorData(requestId));
|
|
214
83
|
}
|
|
215
84
|
/**
|
|
216
85
|
* Boundary #3: every tools/call failure after the tool is found — an `isError: true` result the
|
|
217
86
|
* model sees.
|
|
218
87
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
88
|
+
* ONE unconditional line: {@link McpRegistry.getErrorTranslator} is never undefined, so there is
|
|
89
|
+
* no "was one registered" branch. A registered {@link McpErrorTranslator} REPLACES the default and
|
|
90
|
+
* declines by delegating to {@link McpDefaultToolCallRenderer}, so "no translator registered" and
|
|
91
|
+
* "a translator that does not claim this error" produce byte-identical results.
|
|
222
92
|
*/
|
|
223
93
|
toToolCallResult(failure) {
|
|
224
|
-
|
|
225
|
-
if (!renderer)
|
|
226
|
-
return this.defaultToolCall.toToolCallResult(failure);
|
|
227
|
-
return this.withDefaultMeta(this.appOrDefault(renderer, failure));
|
|
94
|
+
return this.withDefaultMeta(this.appOrDefault(McpRegistry_1.McpRegistry.getErrorTranslator(), failure));
|
|
228
95
|
}
|
|
229
96
|
/** The `_meta` every tools/call result carries so a user can quote its requestId. */
|
|
230
97
|
resultMeta(requestId) {
|
|
231
|
-
return { [
|
|
98
|
+
return { [McpToolCallRendering_1.MCP_REQUEST_ID_META_KEY]: requestId };
|
|
232
99
|
}
|
|
233
100
|
/**
|
|
234
101
|
* An app translator that THROWS must not replace the failure being reported: its own bug is
|
|
235
102
|
* logged here and the ORIGINAL error still renders through the webpieces default, so the model
|
|
236
103
|
* still gets a reply carrying the requestId.
|
|
104
|
+
*
|
|
105
|
+
* This is BUG CONTAINMENT, not a "did anyone register one" branch — the same shape as
|
|
106
|
+
* `ClientErrorTranslator.throwIfFailure` on the HTTP side.
|
|
237
107
|
*/
|
|
238
108
|
appOrDefault(renderer, failure) {
|
|
239
109
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an app translator's own bug must not replace the failure it was asked to render
|
|
240
110
|
try {
|
|
241
|
-
return renderer.
|
|
111
|
+
return renderer.toWire(failure);
|
|
242
112
|
}
|
|
243
113
|
catch (err) {
|
|
244
114
|
const error = (0, core_util_1.toError)(err);
|
|
245
|
-
log.error('Application
|
|
246
|
-
return this.defaultToolCall.
|
|
115
|
+
log.error('Application McpErrorTranslator.toWire threw; rendering the webpieces default.', error);
|
|
116
|
+
return this.defaultToolCall.toWire(failure);
|
|
247
117
|
}
|
|
248
118
|
}
|
|
249
119
|
/**
|
|
@@ -254,12 +124,12 @@ class WpMcpErrorTranslator {
|
|
|
254
124
|
withDefaultMeta(result) {
|
|
255
125
|
if (result._meta !== undefined)
|
|
256
126
|
return result;
|
|
257
|
-
return { ...result, _meta: this.resultMeta(McpCorrelation.requestId()) };
|
|
127
|
+
return { ...result, _meta: this.resultMeta(McpToolCallRendering_1.McpCorrelation.requestId()) };
|
|
258
128
|
}
|
|
259
129
|
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));
|
|
130
|
+
const correlation = McpToolCallRendering_1.McpCorrelation.current();
|
|
131
|
+
const requestId = McpToolCallRendering_1.McpCorrelation.requestId();
|
|
132
|
+
return new core_util_1.HttpResponseDto(new core_util_1.HttpResponseStatus(status, message), headers, new McpToolCallRendering_1.McpHttpErrorBody(new McpToolCallRendering_1.McpHttpErrorDetail(code, message, new McpToolCallRendering_1.McpErrorData(requestId)), correlation.jsonRpcId));
|
|
263
133
|
}
|
|
264
134
|
}
|
|
265
135
|
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 {\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 in either MCP era. */\n unknownTool(toolName: string): ProtocolError {\n const requestId = McpCorrelation.requestId();\n log.info(`Unknown MCP tool: ${toolName} requestId=${requestId}`);\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n `Unknown tool: ${toolName}`,\n new McpErrorData(requestId),\n );\n }\n\n /**\n * Boundary #3: every tools/call failure after the tool is found — an `isError: true` result the\n * model sees.\n *\n * 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"]}
|
|
1
|
+
{"version":3,"file":"WpMcpErrorTranslator.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/WpMcpErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import { CallToolResult, ProtocolError, ProtocolErrorCode } from '@modelcontextprotocol/server';\nimport {\n ApiErrorBoundary,\n HttpHeader,\n HttpResponseDto,\n HttpResponseStatus,\n LogManager,\n toError,\n} from '@webpieces/core-util';\nimport { McpRegistry } from './McpRegistry';\nimport {\n McpCorrelation,\n McpDefaultToolCallRenderer,\n McpErrorData,\n McpErrorTranslator,\n McpHttpErrorBody,\n McpHttpErrorDetail,\n MCP_REQUEST_ID_META_KEY,\n} from './McpToolCallRendering';\n\nconst log = LogManager.getLogger('WpMcpErrorTranslator');\n\n/**\n * The ONE place every MCP failure becomes a reply, mirroring `WebpiecesDefaultErrorTranslator` for HTTP: an error\n * in, the exact wire shape out. There is one method per BOUNDARY, and the MCP spec is what makes\n * them three rather than one:\n *\n * | Boundary | Who writes HTTP | Failure shape |\n * |--------------------------------|-----------------|-------------------------------------|\n * | pre-SDK bearer / Origin / body | **webpieces** | `HttpResponseDto<McpHttpErrorBody>` |\n * | `tools/list` | the SDK | `throw ProtocolError` |\n * | `tools/call` | the SDK | `CallToolResult` with `isError` |\n *\n * Classification and published text are the shared {@link ApiErrorBoundary} rules. Operator logging\n * is NOT here: `LogApiCall` wraps each of those boundaries in `WpMcpServer`, so every failure gets\n * exactly one `[API-server-resp-*]` line with the request and the identity on it.\n */\nexport class WpMcpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n private readonly defaultToolCall = new McpDefaultToolCallRenderer();\n\n /**\n * @param challenge - the `WWW-Authenticate` value for a 401, read lazily because\n * `WpMcpServerConfig` is only fully validated at `bind(...)` time.\n */\n constructor(private readonly challenge: () => string) {}\n\n /**\n * Boundary #1: everything webpieces itself rejects before the SDK is involved — bearer\n * verification, `Origin`, a malformed body. This is 401/403/400/500 today and any status\n * tomorrow, so it produces a VALUE like every other API in the framework and `WpMcpServer` hands\n * it to the shared express writer. The BODY stays JSON-RPC shaped because an MCP client expects\n * that; `HttpResponseDto` is generic, and its header LIST carries `WWW-Authenticate`.\n */\n toBearerBoundaryResponse(error: Error): HttpResponseDto<McpHttpErrorBody> {\n const payload = this.boundary.encode(error);\n const generic = payload.message;\n switch (payload.kind) {\n case 'unauthorized':\n return this.httpError(401, -32_000, generic, [\n new HttpHeader('WWW-Authenticate', this.challenge()),\n ]);\n case 'forbidden':\n return this.httpError(403, -32_000, generic, []);\n case 'bad-request':\n return this.httpError(400, ProtocolErrorCode.ParseError, generic, []);\n default:\n return this.httpError(500, ProtocolErrorCode.InternalError, 'Internal Error', []);\n }\n }\n\n /**\n * Boundary #2: `tools/list` has no tool-result channel, so a failure IS a JSON-RPC error. It\n * THROWS rather than returning a value the caller must remember to throw, and is typed `never`\n * so the compiler knows the call is terminal.\n *\n * This is LOAD-BEARING for disclosure, not defence in depth. Measured on\n * `@modelcontextprotocol/server` 2.0.0 and pinned by `McpSdkForeignThrow.spec.ts`: the SDK wraps\n * a foreign (non-`ProtocolError`) throw in `-32603` but copies its `message` onto the wire\n * VERBATIM. So a `tools/list` handler that lets any error escape un-translated publishes that\n * error's operator text to the caller.\n */\n toListError(error: Error): never {\n const payload = this.boundary.encode(error);\n const requestId = McpCorrelation.requestId();\n if (payload.kind === 'bad-request') {\n throw new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n payload.callerMessage ?? payload.message,\n new McpErrorData(requestId),\n );\n }\n const message = payload.kind === 'end-user' ? payload.message : 'Internal Error';\n throw new ProtocolError(\n ProtocolErrorCode.InternalError,\n message,\n new McpErrorData(requestId),\n );\n }\n\n /** tools/call with a name no binding registered: a JSON-RPC protocol error in either MCP era. */\n unknownTool(toolName: string): ProtocolError {\n const requestId = McpCorrelation.requestId();\n log.info(`Unknown MCP tool: ${toolName} requestId=${requestId}`);\n return new ProtocolError(\n ProtocolErrorCode.InvalidParams,\n `Unknown tool: ${toolName}`,\n new McpErrorData(requestId),\n );\n }\n\n /**\n * Boundary #3: every tools/call failure after the tool is found — an `isError: true` result the\n * model sees.\n *\n * ONE unconditional line: {@link McpRegistry.getErrorTranslator} is never undefined, so there is\n * no \"was one registered\" branch. A registered {@link McpErrorTranslator} REPLACES the default and\n * declines by delegating to {@link McpDefaultToolCallRenderer}, so \"no translator registered\" and\n * \"a translator that does not claim this error\" produce byte-identical results.\n */\n toToolCallResult(failure: Error): CallToolResult {\n return this.withDefaultMeta(this.appOrDefault(McpRegistry.getErrorTranslator(), failure));\n }\n\n /** The `_meta` every tools/call result carries so a user can quote its requestId. */\n resultMeta(requestId: string): Record<string, string> {\n return { [MCP_REQUEST_ID_META_KEY]: requestId };\n }\n\n /**\n * An app translator that THROWS must not replace the failure being reported: its own bug is\n * logged here and the ORIGINAL error still renders through the webpieces default, so the model\n * still gets a reply carrying the requestId.\n *\n * This is BUG CONTAINMENT, not a \"did anyone register one\" branch — the same shape as\n * `ClientErrorTranslator.throwIfFailure` on the HTTP side.\n */\n private appOrDefault(renderer: McpErrorTranslator, failure: Error): CallToolResult {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- an app translator's own bug must not replace the failure it was asked to render\n try {\n return renderer.toWire(failure);\n } catch (err: unknown) {\n const error = toError(err);\n log.error(\n 'Application McpErrorTranslator.toWire threw; rendering the webpieces default.',\n error,\n );\n return this.defaultToolCall.toWire(failure);\n }\n }\n\n /**\n * webpieces fills `_meta` only when the app left it off, keeping the README's \"every reply\n * carries the requestId\" promise a DEFAULT rather than a restriction: an app that set `_meta`\n * keeps it byte-for-byte.\n */\n private withDefaultMeta(result: CallToolResult): CallToolResult {\n if (result._meta !== undefined) return result;\n return { ...result, _meta: this.resultMeta(McpCorrelation.requestId()) };\n }\n\n private httpError(\n status: number,\n code: number,\n message: string,\n headers: HttpHeader[],\n ): HttpResponseDto<McpHttpErrorBody> {\n const correlation = McpCorrelation.current();\n const requestId = McpCorrelation.requestId();\n return new HttpResponseDto<McpHttpErrorBody>(\n new HttpResponseStatus(status, message),\n headers,\n new McpHttpErrorBody(\n new McpHttpErrorDetail(code, message, new McpErrorData(requestId)),\n correlation.jsonRpcId,\n ),\n );\n }\n}\n"]}
|