@webpieces/mcp-server 0.4.990 → 0.4.991
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 +199 -42
- package/package.json +7 -6
- package/src/McpApiDispatcher.d.ts +6 -3
- package/src/McpApiDispatcher.js +6 -2
- package/src/McpApiDispatcher.js.map +1 -1
- package/src/McpAuth.d.ts +115 -56
- package/src/McpAuth.js +180 -111
- package/src/McpAuth.js.map +1 -1
- package/src/McpAuthorityCompileAssertions.d.ts +12 -2
- package/src/McpAuthorityCompileAssertions.js +32 -4
- package/src/McpAuthorityCompileAssertions.js.map +1 -1
- package/src/McpBindOptions.d.ts +5 -20
- package/src/McpBindOptions.js +5 -15
- package/src/McpBindOptions.js.map +1 -1
- package/src/McpClientSet.d.ts +35 -0
- package/src/McpClientSet.js +86 -0
- package/src/McpClientSet.js.map +1 -0
- package/src/McpDoorAuth.d.ts +117 -0
- package/src/McpDoorAuth.js +455 -0
- package/src/McpDoorAuth.js.map +1 -0
- package/src/McpToolCallRendering.d.ts +5 -4
- package/src/McpToolCallRendering.js.map +1 -1
- package/src/McpToolCatalog.d.ts +1 -1
- package/src/McpToolCatalog.js +1 -1
- package/src/McpToolCatalog.js.map +1 -1
- package/src/McpToolRegistry.js +8 -2
- package/src/McpToolRegistry.js.map +1 -1
- package/src/McpToolset.d.ts +115 -0
- package/src/McpToolset.js +323 -0
- package/src/McpToolset.js.map +1 -0
- package/src/WpMcpErrorTranslator.d.ts +5 -7
- package/src/WpMcpErrorTranslator.js +7 -13
- package/src/WpMcpErrorTranslator.js.map +1 -1
- package/src/WpMcpInMemory.d.ts +30 -0
- package/src/WpMcpInMemory.js +97 -0
- package/src/WpMcpInMemory.js.map +1 -0
- package/src/WpMcpRemote.d.ts +23 -0
- package/src/WpMcpRemote.js +89 -0
- package/src/WpMcpRemote.js.map +1 -0
- package/src/WpMcpServer.d.ts +45 -76
- package/src/WpMcpServer.js +101 -327
- package/src/WpMcpServer.js.map +1 -1
- package/src/__tests__/McpTransportFixtures.d.ts +107 -0
- package/src/__tests__/McpTransportFixtures.js +266 -0
- package/src/__tests__/McpTransportFixtures.js.map +1 -0
- package/src/index.d.ts +9 -2
- package/src/index.js +17 -1
- package/src/index.js.map +1 -1
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpToolset = exports.McpToolRequest = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const server_1 = require("@modelcontextprotocol/server");
|
|
6
|
+
const core_util_1 = require("@webpieces/core-util");
|
|
7
|
+
const core_context_1 = require("@webpieces/core-context");
|
|
8
|
+
const http_routing_1 = require("@webpieces/http-routing");
|
|
9
|
+
const McpApiDispatcher_1 = require("./McpApiDispatcher");
|
|
10
|
+
const McpProtocol_1 = require("./McpProtocol");
|
|
11
|
+
const McpToolCallRendering_1 = require("./McpToolCallRendering");
|
|
12
|
+
const McpToolPolicy_1 = require("./McpToolPolicy");
|
|
13
|
+
const McpToolRegistry_1 = require("./McpToolRegistry");
|
|
14
|
+
const WpMcpErrorTranslator_1 = require("./WpMcpErrorTranslator");
|
|
15
|
+
const log = core_util_1.LogManager.getLogger('McpToolset');
|
|
16
|
+
/**
|
|
17
|
+
* The request DTO the edge `LogApiCall` lines report. These boundaries run outside any API dispatch,
|
|
18
|
+
* so there is no contract DTO to log; the step name is the fact worth having.
|
|
19
|
+
*/
|
|
20
|
+
class McpEdgeRequest {
|
|
21
|
+
step;
|
|
22
|
+
constructor(step) {
|
|
23
|
+
this.step = step;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
class McpSdkOperation {
|
|
27
|
+
name;
|
|
28
|
+
toolStage = false;
|
|
29
|
+
constructor(name) {
|
|
30
|
+
this.name = name;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* `Tool.annotations` exactly as tools/list publishes it: the four hints PLUS the human-readable
|
|
35
|
+
* `title` (#1180). The Claude Connectors Directory checks `annotations.title`; clients on the
|
|
36
|
+
* 2025-06-18+ spec render the top-level `Tool.title`, so both carry the one title — the method's
|
|
37
|
+
* `@summary` JSDoc tag, read from the generated catalog (#1296).
|
|
38
|
+
*/
|
|
39
|
+
class McpPublishedToolAnnotations {
|
|
40
|
+
title;
|
|
41
|
+
readOnlyHint;
|
|
42
|
+
destructiveHint;
|
|
43
|
+
idempotentHint;
|
|
44
|
+
openWorldHint;
|
|
45
|
+
constructor(tool) {
|
|
46
|
+
this.title = tool.title;
|
|
47
|
+
this.readOnlyHint = tool.annotations.readOnlyHint;
|
|
48
|
+
this.destructiveHint = tool.annotations.destructiveHint;
|
|
49
|
+
this.idempotentHint = tool.annotations.idempotentHint;
|
|
50
|
+
this.openWorldHint = tool.annotations.openWorldHint;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What ONE MCP request may see and how it is cancelled, established by the transport before the SDK
|
|
55
|
+
* handler runs: the caller's authorized projection, and the signal that fires when the caller goes away.
|
|
56
|
+
*/
|
|
57
|
+
class McpToolRequest {
|
|
58
|
+
tools;
|
|
59
|
+
disconnectSignal;
|
|
60
|
+
constructor(tools, disconnectSignal) {
|
|
61
|
+
this.tools = tools;
|
|
62
|
+
this.disconnectSignal = disconnectSignal;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
exports.McpToolRequest = McpToolRequest;
|
|
66
|
+
/**
|
|
67
|
+
* The official SDK server with Webpieces-owned tools/list and tools/call handlers. It answers
|
|
68
|
+
* `toolInputSchemaJson` from the request-authorized view when the transport has one, so the SDK's
|
|
69
|
+
* pre-dispatch SEP-2243 `Mcp-Param-*` header validation still runs without registering SDK-side tool
|
|
70
|
+
* callbacks.
|
|
71
|
+
*/
|
|
72
|
+
class WpSdkMcpServer extends server_1.McpServer {
|
|
73
|
+
schemaSource;
|
|
74
|
+
constructor(info, options, schemaSource) {
|
|
75
|
+
super(info, options);
|
|
76
|
+
this.schemaSource = schemaSource;
|
|
77
|
+
}
|
|
78
|
+
// webpieces-disable no-any-unknown -- signature is fixed by the SDK's JSON Schema record type
|
|
79
|
+
toolInputSchemaJson(name) {
|
|
80
|
+
return this.schemaSource(name)?.inputSchema;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* McpToolset — the shared core of every MCP transport: WHICH tools are served, and everything that
|
|
85
|
+
* happens to a request once its caller is established.
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* const toolset = new McpToolset(
|
|
89
|
+
* bindings, // McpApiBinding.local / .remote
|
|
90
|
+
* McpToolCatalog.fromPackages(['@myorg/my-apis'], __dirname),
|
|
91
|
+
* router.authorizationService(),
|
|
92
|
+
* ['user'], // #1187 profiles: WHICH tools this toolset serves
|
|
93
|
+
* );
|
|
94
|
+
* const client = await WpMcpInMemory.connect(toolset); // in-process agent
|
|
95
|
+
* new WpMcpServer(config, toolset).bind(app, options); // HTTP
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* It owns the {@link McpToolRegistry}, the per-caller {@link AuthorizedMcpTools} projection, input and
|
|
99
|
+
* output schema validation, dispatch through `binding.invoke`, and the error translation. Its ONLY input
|
|
100
|
+
* about the caller is the CURRENT `RequestContext` — the trusted USER_ID / USER_ROLES / ORG_ID and a
|
|
101
|
+
* `VerifiedMachineCaller`, exactly what `AuthorizationService.authorize` reads. A transport's only job
|
|
102
|
+
* is to establish that context; `@WpAuthorization` on the contract is the single policy everywhere.
|
|
103
|
+
*
|
|
104
|
+
* Profiles are fixed per toolset: `/mcp/user` and `/mcp/admin` are two toolsets (two servers), so paths
|
|
105
|
+
* split by WHICH TOOLS, never by HOW THE CALLER AUTHENTICATES.
|
|
106
|
+
*
|
|
107
|
+
* Before tools/list and tools/call hand an IMPLEMENTATION-kind failure to the translator, they
|
|
108
|
+
* `log.error` it with its stack and the requestId ({@link logImplementationFailure}, #1055). The
|
|
109
|
+
* translator reduces such a failure to "Internal Error" for the caller, so that line is the only place
|
|
110
|
+
* its cause exists.
|
|
111
|
+
*/
|
|
112
|
+
class McpToolset {
|
|
113
|
+
authorizationService;
|
|
114
|
+
registry;
|
|
115
|
+
profiles;
|
|
116
|
+
/** A short hash of the served tool surface, appended to every server version and tool `_meta`. */
|
|
117
|
+
revision;
|
|
118
|
+
dispatcher = new McpApiDispatcher_1.McpApiDispatcher();
|
|
119
|
+
/** Classifies a failure exactly as the translator will render it, so the two cannot disagree. */
|
|
120
|
+
boundary = new core_util_1.ApiErrorBoundary();
|
|
121
|
+
outputSchemas = new core_util_1.ApiJsonSchemaValidator();
|
|
122
|
+
/**
|
|
123
|
+
* tools/list has NO filter chain above it — `LogApiFilter` never sees a list the SDK renders. Wrapping
|
|
124
|
+
* it in `LogApiCall` is this repo's one line on logging.
|
|
125
|
+
*/
|
|
126
|
+
logApiCall = new core_util_1.LogApiCallImpl(new core_context_1.RequestContextApiCallContext());
|
|
127
|
+
/** tools/call reads `McpRegistry`; tools/list replies stay framework-owned. See McpErrorTranslator. */
|
|
128
|
+
translator = new WpMcpErrorTranslator_1.WpMcpErrorTranslator();
|
|
129
|
+
constructor(bindings,
|
|
130
|
+
/**
|
|
131
|
+
* The tools the build published — ONE catalog per bound contract, each the
|
|
132
|
+
* `mcp-<ContractClass>-tools.json` `wp-openapi` wrote beside its library's OpenAPI documents,
|
|
133
|
+
* read with `McpToolCatalog.fromPackages([...], __dirname)`. The only source of a tool's
|
|
134
|
+
* documentation and schemas; `McpToolRegistry` refuses to build unless it holds exactly one
|
|
135
|
+
* catalog per bound contract.
|
|
136
|
+
*/
|
|
137
|
+
catalogs, authorizationService,
|
|
138
|
+
/** The union of `@WpMcpTool` profiles this toolset serves, at least one (`[Mcp.DEFAULT]` for untagged tools). */
|
|
139
|
+
profiles) {
|
|
140
|
+
this.authorizationService = authorizationService;
|
|
141
|
+
if (bindings.length === 0)
|
|
142
|
+
throw new Error('McpToolset requires at least one API contract binding.');
|
|
143
|
+
this.profiles = core_util_1.Mcp.profiles(profiles, 'McpToolset profiles');
|
|
144
|
+
this.registry = new McpToolRegistry_1.McpToolRegistry(bindings, catalogs, authorizationService);
|
|
145
|
+
this.revision = this.calculateRevision();
|
|
146
|
+
}
|
|
147
|
+
/** The tools the caller in the CURRENT RequestContext may see, sorted by name. */
|
|
148
|
+
async authorizedTools() {
|
|
149
|
+
return McpToolRegistry_1.AuthorizedMcpTools.create(this.registry, this.authorizationService, this.profiles);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A fresh SDK server whose tools/list and tools/call are this toolset's. `request` establishes, per
|
|
153
|
+
* MCP request, what the caller may see; `schemaView` answers the SDK's input-schema lookup (the HTTP
|
|
154
|
+
* transport passes the request's projection, the in-memory one the full registry).
|
|
155
|
+
*/
|
|
156
|
+
createSdkServer(info, options, request, schemaView) {
|
|
157
|
+
const server = new WpSdkMcpServer(info, options, schemaView);
|
|
158
|
+
server.server.removeRequestHandler('tools/list');
|
|
159
|
+
server.server.removeRequestHandler('tools/call');
|
|
160
|
+
server.server.setRequestHandler('tools/list', async (_request) => this.sdkBoundary(new McpSdkOperation('tools/list'), () => this.logEdge('tools/list', async () => this.listTools((await request()).tools))));
|
|
161
|
+
server.server.setRequestHandler('tools/call', async (call, sdkContext) => this.handleCallTool(server, call, request, sdkContext));
|
|
162
|
+
return server;
|
|
163
|
+
}
|
|
164
|
+
/** Run one un-filtered edge step under `LogApiCall`, so its failure gets exactly one log line. */
|
|
165
|
+
async logEdge(step, work) {
|
|
166
|
+
return this.logApiCall.execute(new core_util_1.ApiMethodInfo('server', 'McpEndpoint', step, McpProtocol_1.McpProtocol), new McpEdgeRequest(step), work);
|
|
167
|
+
}
|
|
168
|
+
/** ONE SDK callback catch implementation; the SDK intercepts foreign throws itself. */
|
|
169
|
+
async sdkBoundary(operation, work) {
|
|
170
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- shared SDK response boundary, preserving protocol versus tool envelopes
|
|
171
|
+
try {
|
|
172
|
+
return await work();
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const error = (0, core_util_1.toError)(err);
|
|
176
|
+
if (error instanceof McpToolPolicy_1.McpToolDeniedError)
|
|
177
|
+
throw this.translator.unknownTool(error.toolName);
|
|
178
|
+
if (!operation.toolStage && error instanceof server_1.ProtocolError)
|
|
179
|
+
throw error;
|
|
180
|
+
this.logImplementationFailure(operation.name, error);
|
|
181
|
+
if (operation.toolStage)
|
|
182
|
+
return this.translator.toToolCallResult(error);
|
|
183
|
+
this.translator.toListError(error);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
listTools(view) {
|
|
187
|
+
const tools = view.tools.map((tool) => this.toolDefinition(tool));
|
|
188
|
+
return { tools };
|
|
189
|
+
}
|
|
190
|
+
toolDefinition(tool) {
|
|
191
|
+
return {
|
|
192
|
+
name: tool.name,
|
|
193
|
+
title: tool.title,
|
|
194
|
+
description: tool.description,
|
|
195
|
+
inputSchema: tool.inputSchema,
|
|
196
|
+
outputSchema: tool.outputSchema,
|
|
197
|
+
annotations: new McpPublishedToolAnnotations(tool),
|
|
198
|
+
_meta: { webpiecesRegistryRevision: this.revision },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* SDK tool callback: an unknown or hidden tool is a JSON-RPC -32602; every failure after the tool
|
|
203
|
+
* is found is an `isError: true` result, identically for local and remote bindings.
|
|
204
|
+
*/
|
|
205
|
+
async handleCallTool(server, call, request, sdkContext) {
|
|
206
|
+
const name = call.params.name;
|
|
207
|
+
const operation = new McpSdkOperation(`tools/call ${name}`);
|
|
208
|
+
return this.sdkBoundary(operation, async () => {
|
|
209
|
+
McpToolCallRendering_1.McpCorrelation.stamp(new McpToolCallRendering_1.McpCorrelation(sdkContext.mcpReq.id, McpProtocol_1.McpProtocol, name));
|
|
210
|
+
const established = await request();
|
|
211
|
+
const tool = established.tools.find(name);
|
|
212
|
+
if (!tool)
|
|
213
|
+
throw this.translator.unknownTool(name);
|
|
214
|
+
McpToolCallRendering_1.McpCorrelation.stamp(new McpToolCallRendering_1.McpCorrelation(sdkContext.mcpReq.id, tool.apiClass, name, tool.operation));
|
|
215
|
+
operation.toolStage = true;
|
|
216
|
+
const result = await this.callTool(tool, call, established, sdkContext);
|
|
217
|
+
result._meta = this.translator.resultMeta(McpToolCallRendering_1.McpCorrelation.requestId());
|
|
218
|
+
return server.server.projectCallToolResult(result, tool.outputSchema);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* ONE `log.error` line, with the stack and the requestId, for a failure that is THIS server's bug
|
|
223
|
+
* — the `implementation` kind, which the translator reduces to "Internal Error" and a requestId
|
|
224
|
+
* for the caller (#1055).
|
|
225
|
+
*
|
|
226
|
+
* `LogApiCall` does not cover this. An MCP output-schema violation is raised AFTER the controller
|
|
227
|
+
* returned, so the only `LogApiCall` line for that call reads `resp-SUCCESS`; before this line
|
|
228
|
+
* existed, the requestId support was handed led to a log with no error in it. Where a
|
|
229
|
+
* `[API-*-resp-FAIL]` line does exist it carries the error's message, not its stack. So this line
|
|
230
|
+
* is not the bare duplicate `ApiErrorBoundary` stopped writing: it is the one that says WHERE.
|
|
231
|
+
*
|
|
232
|
+
* Every other kind (bad-request, end-user, dependency, ...) is a caller's or a downstream's
|
|
233
|
+
* failure, published to the caller with its own text, and `LogApiCall` already records it.
|
|
234
|
+
*/
|
|
235
|
+
logImplementationFailure(entryPoint, error) {
|
|
236
|
+
if (this.boundary.encode(error).kind !== 'implementation')
|
|
237
|
+
return;
|
|
238
|
+
log.error(`MCP ${entryPoint} failed with an implementation error requestId=${McpToolCallRendering_1.McpCorrelation.requestId()}`, error);
|
|
239
|
+
}
|
|
240
|
+
/** Everything a tool call does. No catch in here: failures propagate to handleCallTool. */
|
|
241
|
+
async callTool(tool, call, established, sdkContext) {
|
|
242
|
+
const caller = this.currentCaller();
|
|
243
|
+
const invocation = new core_util_1.McpInvocationContext(sdkContext.mcpReq.id, tool.name, caller.subject, caller.roles, AbortSignal.any([sdkContext.mcpReq.signal, established.disconnectSignal]), this.progressReporter(sdkContext));
|
|
244
|
+
const args = (call.params.arguments ?? {});
|
|
245
|
+
const value = await this.dispatcher.call(tool, args, invocation, this.authorizationService);
|
|
246
|
+
const outputFailure = this.outputSchemas.validate(tool.outputSchema, value);
|
|
247
|
+
if (outputFailure) {
|
|
248
|
+
throw new core_util_1.ApiImplementationError(`MCP output schema violation for ${tool.apiClass.name}.${tool.methodName}: ${outputFailure.message}`);
|
|
249
|
+
}
|
|
250
|
+
const structured = this.record(value);
|
|
251
|
+
return {
|
|
252
|
+
content: [{ type: 'text', text: JSON.stringify(structured) }],
|
|
253
|
+
structuredContent: structured,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** The trusted caller of the CURRENT RequestContext: a verified user, else a verified machine. */
|
|
257
|
+
currentCaller() {
|
|
258
|
+
const userId = core_context_1.RequestContext.getTrusted(core_util_1.WebpiecesCoreHeaders.USER_ID);
|
|
259
|
+
if (typeof userId === 'string' && userId.trim() !== '') {
|
|
260
|
+
return new McpCallerFacts(userId, http_routing_1.CanonicalUserRoles.read());
|
|
261
|
+
}
|
|
262
|
+
const machine = core_context_1.RequestContext.getTrusted(http_routing_1.VERIFIED_MACHINE_CALLER);
|
|
263
|
+
if (machine instanceof http_routing_1.VerifiedMachineCaller)
|
|
264
|
+
return new McpCallerFacts(machine.identity, []);
|
|
265
|
+
throw new core_util_1.ApiUnauthorizedError('MCP tool call has no trusted caller in its RequestContext.');
|
|
266
|
+
}
|
|
267
|
+
progressReporter(context) {
|
|
268
|
+
const token = context.mcpReq._meta?.progressToken;
|
|
269
|
+
if (token === undefined)
|
|
270
|
+
return undefined;
|
|
271
|
+
return async (progress, total, message) => {
|
|
272
|
+
await context.mcpReq.notify({
|
|
273
|
+
method: 'notifications/progress',
|
|
274
|
+
params: this.progressParams(token, progress, total, message),
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
progressParams(progressToken, progress, total, message) {
|
|
279
|
+
const params = { progressToken, progress };
|
|
280
|
+
if (total !== undefined)
|
|
281
|
+
params['total'] = total;
|
|
282
|
+
if (message !== undefined)
|
|
283
|
+
params['message'] = message;
|
|
284
|
+
return params;
|
|
285
|
+
}
|
|
286
|
+
calculateRevision() {
|
|
287
|
+
const surface = this.registry.tools.map((tool) => [
|
|
288
|
+
tool.name,
|
|
289
|
+
tool.title,
|
|
290
|
+
tool.description,
|
|
291
|
+
tool.inputSchema,
|
|
292
|
+
tool.outputSchema,
|
|
293
|
+
tool.annotations,
|
|
294
|
+
tool.profiles,
|
|
295
|
+
]);
|
|
296
|
+
return (0, node_crypto_1.createHash)('sha256')
|
|
297
|
+
.update(JSON.stringify([this.profiles, surface]))
|
|
298
|
+
.digest('hex')
|
|
299
|
+
.slice(0, 12);
|
|
300
|
+
}
|
|
301
|
+
record(value) {
|
|
302
|
+
const jsonValue = JSON.stringify(value);
|
|
303
|
+
if (jsonValue === undefined) {
|
|
304
|
+
throw new core_util_1.ApiImplementationError('MCP structured content is not JSON serializable.');
|
|
305
|
+
}
|
|
306
|
+
const parsed = JSON.parse(jsonValue);
|
|
307
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
308
|
+
return { value: parsed };
|
|
309
|
+
}
|
|
310
|
+
return parsed;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
exports.McpToolset = McpToolset;
|
|
314
|
+
/** Who the current RequestContext says is calling, as McpInvocationContext reports it. */
|
|
315
|
+
class McpCallerFacts {
|
|
316
|
+
subject;
|
|
317
|
+
roles;
|
|
318
|
+
constructor(subject, roles) {
|
|
319
|
+
this.subject = subject;
|
|
320
|
+
this.roles = roles;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
//# sourceMappingURL=McpToolset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"McpToolset.js","sourceRoot":"","sources":["../../../../../packages/http/mcp-server/src/McpToolset.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport {\n CallToolRequest,\n CallToolResult,\n Implementation,\n ListToolsResult,\n McpServer,\n ProgressToken,\n ProtocolError,\n ServerContext,\n ServerOptions,\n Tool,\n} from '@modelcontextprotocol/server';\nimport {\n ApiErrorBoundary,\n ApiImplementationError,\n ApiJsonSchemaValidator,\n ApiMethodInfo,\n ApiUnauthorizedError,\n DtoValue,\n LogApiCallImpl,\n LogManager,\n Mcp,\n McpInvocationContext,\n McpProgressReporter,\n McpToolProfiles,\n toError,\n WebpiecesCoreHeaders,\n} from '@webpieces/core-util';\nimport { RequestContext, RequestContextApiCallContext } from '@webpieces/core-context';\nimport {\n AuthorizationService,\n CanonicalUserRoles,\n VERIFIED_MACHINE_CALLER,\n VerifiedMachineCaller,\n} from '@webpieces/http-routing';\nimport { McpApiBinding } from './McpApiBinding';\nimport { McpApiDispatcher } from './McpApiDispatcher';\nimport { McpProtocol } from './McpProtocol';\nimport { McpToolCatalog } from './McpToolCatalog';\nimport { McpCorrelation } from './McpToolCallRendering';\nimport { McpToolDeniedError } from './McpToolPolicy';\nimport { AuthorizedMcpTools, McpToolRegistry, RegisteredMcpTool } from './McpToolRegistry';\nimport { WpMcpErrorTranslator } from './WpMcpErrorTranslator';\n\nconst log = LogManager.getLogger('McpToolset');\n\n/**\n * The request DTO the edge `LogApiCall` lines report. These boundaries run outside any API dispatch,\n * so there is no contract DTO to log; the step name is the fact worth having.\n */\nclass McpEdgeRequest {\n constructor(public readonly step: string) {}\n}\n\nclass McpSdkOperation {\n toolStage = false;\n constructor(public readonly name: string) {}\n}\n\n/**\n * `Tool.annotations` exactly as tools/list publishes it: the four hints PLUS the human-readable\n * `title` (#1180). The Claude Connectors Directory checks `annotations.title`; clients on the\n * 2025-06-18+ spec render the top-level `Tool.title`, so both carry the one title — the method's\n * `@summary` JSDoc tag, read from the generated catalog (#1296).\n */\nclass McpPublishedToolAnnotations {\n readonly title: string;\n readonly readOnlyHint: boolean;\n readonly destructiveHint: boolean;\n readonly idempotentHint: boolean;\n readonly openWorldHint: boolean;\n\n constructor(tool: RegisteredMcpTool) {\n this.title = tool.title;\n this.readOnlyHint = tool.annotations.readOnlyHint;\n this.destructiveHint = tool.annotations.destructiveHint;\n this.idempotentHint = tool.annotations.idempotentHint;\n this.openWorldHint = tool.annotations.openWorldHint;\n }\n}\n\n/**\n * What ONE MCP request may see and how it is cancelled, established by the transport before the SDK\n * handler runs: the caller's authorized projection, and the signal that fires when the caller goes away.\n */\nexport class McpToolRequest {\n constructor(\n public readonly tools: AuthorizedMcpTools,\n public readonly disconnectSignal: AbortSignal,\n ) {}\n}\n\n/**\n * The official SDK server with Webpieces-owned tools/list and tools/call handlers. It answers\n * `toolInputSchemaJson` from the request-authorized view when the transport has one, so the SDK's\n * pre-dispatch SEP-2243 `Mcp-Param-*` header validation still runs without registering SDK-side tool\n * callbacks.\n */\nclass WpSdkMcpServer extends McpServer {\n constructor(\n info: Implementation,\n options: ServerOptions,\n private readonly schemaSource: (name: string) => RegisteredMcpTool | undefined,\n ) {\n super(info, options);\n }\n\n // webpieces-disable no-any-unknown -- signature is fixed by the SDK's JSON Schema record type\n override toolInputSchemaJson(name: string): Record<string, unknown> | undefined {\n return this.schemaSource(name)?.inputSchema as Record<string, DtoValue> | undefined;\n }\n}\n\n/**\n * McpToolset — the shared core of every MCP transport: WHICH tools are served, and everything that\n * happens to a request once its caller is established.\n *\n * ```ts\n * const toolset = new McpToolset(\n * bindings, // McpApiBinding.local / .remote\n * McpToolCatalog.fromPackages(['@myorg/my-apis'], __dirname),\n * router.authorizationService(),\n * ['user'], // #1187 profiles: WHICH tools this toolset serves\n * );\n * const client = await WpMcpInMemory.connect(toolset); // in-process agent\n * new WpMcpServer(config, toolset).bind(app, options); // HTTP\n * ```\n *\n * It owns the {@link McpToolRegistry}, the per-caller {@link AuthorizedMcpTools} projection, input and\n * output schema validation, dispatch through `binding.invoke`, and the error translation. Its ONLY input\n * about the caller is the CURRENT `RequestContext` — the trusted USER_ID / USER_ROLES / ORG_ID and a\n * `VerifiedMachineCaller`, exactly what `AuthorizationService.authorize` reads. A transport's only job\n * is to establish that context; `@WpAuthorization` on the contract is the single policy everywhere.\n *\n * Profiles are fixed per toolset: `/mcp/user` and `/mcp/admin` are two toolsets (two servers), so paths\n * split by WHICH TOOLS, never by HOW THE CALLER AUTHENTICATES.\n *\n * Before tools/list and tools/call hand an IMPLEMENTATION-kind failure to the translator, they\n * `log.error` it with its stack and the requestId ({@link logImplementationFailure}, #1055). The\n * translator reduces such a failure to \"Internal Error\" for the caller, so that line is the only place\n * its cause exists.\n */\nexport class McpToolset {\n readonly registry: McpToolRegistry;\n readonly profiles: readonly string[];\n /** A short hash of the served tool surface, appended to every server version and tool `_meta`. */\n readonly revision: string;\n private readonly dispatcher = new McpApiDispatcher();\n /** Classifies a failure exactly as the translator will render it, so the two cannot disagree. */\n private readonly boundary = new ApiErrorBoundary();\n private readonly outputSchemas = new ApiJsonSchemaValidator();\n /**\n * tools/list has NO filter chain above it — `LogApiFilter` never sees a list the SDK renders. Wrapping\n * it in `LogApiCall` is this repo's one line on logging.\n */\n private readonly logApiCall = new LogApiCallImpl(new RequestContextApiCallContext());\n /** tools/call reads `McpRegistry`; tools/list replies stay framework-owned. See McpErrorTranslator. */\n private readonly translator: WpMcpErrorTranslator = new WpMcpErrorTranslator();\n\n constructor(\n bindings: readonly McpApiBinding[],\n /**\n * The tools the build published — ONE catalog per bound contract, each the\n * `mcp-<ContractClass>-tools.json` `wp-openapi` wrote beside its library's OpenAPI documents,\n * read with `McpToolCatalog.fromPackages([...], __dirname)`. The only source of a tool's\n * documentation and schemas; `McpToolRegistry` refuses to build unless it holds exactly one\n * catalog per bound contract.\n */\n catalogs: readonly McpToolCatalog[],\n public readonly authorizationService: AuthorizationService,\n /** The union of `@WpMcpTool` profiles this toolset serves, at least one (`[Mcp.DEFAULT]` for untagged tools). */\n profiles: McpToolProfiles,\n ) {\n if (bindings.length === 0)\n throw new Error('McpToolset requires at least one API contract binding.');\n this.profiles = Mcp.profiles(profiles, 'McpToolset profiles');\n this.registry = new McpToolRegistry(bindings, catalogs, authorizationService);\n this.revision = this.calculateRevision();\n }\n\n /** The tools the caller in the CURRENT RequestContext may see, sorted by name. */\n async authorizedTools(): Promise<AuthorizedMcpTools> {\n return AuthorizedMcpTools.create(this.registry, this.authorizationService, this.profiles);\n }\n\n /**\n * A fresh SDK server whose tools/list and tools/call are this toolset's. `request` establishes, per\n * MCP request, what the caller may see; `schemaView` answers the SDK's input-schema lookup (the HTTP\n * transport passes the request's projection, the in-memory one the full registry).\n */\n createSdkServer(\n info: Implementation,\n options: ServerOptions,\n request: () => Promise<McpToolRequest>,\n schemaView: (name: string) => RegisteredMcpTool | undefined,\n ): McpServer {\n const server = new WpSdkMcpServer(info, options, schemaView);\n server.server.removeRequestHandler('tools/list');\n server.server.removeRequestHandler('tools/call');\n server.server.setRequestHandler(\n 'tools/list',\n async (_request: object): Promise<ListToolsResult> =>\n this.sdkBoundary(new McpSdkOperation('tools/list'), () =>\n this.logEdge('tools/list', async () => this.listTools((await request()).tools)),\n ) as Promise<ListToolsResult>,\n );\n server.server.setRequestHandler(\n 'tools/call',\n async (call: CallToolRequest, sdkContext: ServerContext): Promise<CallToolResult> =>\n this.handleCallTool(server, call, request, sdkContext),\n );\n return server;\n }\n\n /** Run one un-filtered edge step under `LogApiCall`, so its failure gets exactly one log line. */\n async logEdge<R>(step: string, work: () => Promise<R>): Promise<R> {\n return this.logApiCall.execute(\n new ApiMethodInfo('server', 'McpEndpoint', step, McpProtocol),\n new McpEdgeRequest(step),\n work,\n );\n }\n\n /** ONE SDK callback catch implementation; the SDK intercepts foreign throws itself. */\n private async sdkBoundary<R>(\n operation: McpSdkOperation,\n work: () => Promise<R>,\n ): Promise<R | CallToolResult> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- shared SDK response boundary, preserving protocol versus tool envelopes\n try {\n return await work();\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof McpToolDeniedError)\n throw this.translator.unknownTool(error.toolName);\n if (!operation.toolStage && error instanceof ProtocolError) throw error;\n this.logImplementationFailure(operation.name, error);\n if (operation.toolStage) return this.translator.toToolCallResult(error);\n this.translator.toListError(error);\n }\n }\n\n private listTools(view: AuthorizedMcpTools): ListToolsResult {\n const tools: Tool[] = view.tools.map((tool: RegisteredMcpTool) =>\n this.toolDefinition(tool),\n );\n return { tools };\n }\n\n private toolDefinition(tool: RegisteredMcpTool): Tool {\n return {\n name: tool.name,\n title: tool.title,\n description: tool.description,\n inputSchema: tool.inputSchema as Tool['inputSchema'],\n outputSchema: tool.outputSchema as Tool['outputSchema'],\n annotations: new McpPublishedToolAnnotations(tool),\n _meta: { webpiecesRegistryRevision: this.revision },\n };\n }\n\n /**\n * SDK tool callback: an unknown or hidden tool is a JSON-RPC -32602; every failure after the tool\n * is found is an `isError: true` result, identically for local and remote bindings.\n */\n private async handleCallTool(\n server: McpServer,\n call: CallToolRequest,\n request: () => Promise<McpToolRequest>,\n sdkContext: ServerContext,\n ): Promise<CallToolResult> {\n const name = call.params.name;\n const operation = new McpSdkOperation(`tools/call ${name}`);\n return this.sdkBoundary(operation, async () => {\n McpCorrelation.stamp(new McpCorrelation(sdkContext.mcpReq.id, McpProtocol, name));\n const established = await request();\n const tool = established.tools.find(name);\n if (!tool) throw this.translator.unknownTool(name);\n McpCorrelation.stamp(\n new McpCorrelation(sdkContext.mcpReq.id, tool.apiClass, name, tool.operation),\n );\n operation.toolStage = true;\n const result = await this.callTool(tool, call, established, sdkContext);\n result._meta = this.translator.resultMeta(McpCorrelation.requestId());\n return server.server.projectCallToolResult(\n result,\n tool.outputSchema as Record<string, DtoValue>,\n );\n }) as Promise<CallToolResult>;\n }\n\n /**\n * ONE `log.error` line, with the stack and the requestId, for a failure that is THIS server's bug\n * — the `implementation` kind, which the translator reduces to \"Internal Error\" and a requestId\n * for the caller (#1055).\n *\n * `LogApiCall` does not cover this. An MCP output-schema violation is raised AFTER the controller\n * returned, so the only `LogApiCall` line for that call reads `resp-SUCCESS`; before this line\n * existed, the requestId support was handed led to a log with no error in it. Where a\n * `[API-*-resp-FAIL]` line does exist it carries the error's message, not its stack. So this line\n * is not the bare duplicate `ApiErrorBoundary` stopped writing: it is the one that says WHERE.\n *\n * Every other kind (bad-request, end-user, dependency, ...) is a caller's or a downstream's\n * failure, published to the caller with its own text, and `LogApiCall` already records it.\n */\n private logImplementationFailure(entryPoint: string, error: Error): void {\n if (this.boundary.encode(error).kind !== 'implementation') return;\n log.error(\n `MCP ${entryPoint} failed with an implementation error requestId=${McpCorrelation.requestId()}`,\n error,\n );\n }\n\n /** Everything a tool call does. No catch in here: failures propagate to handleCallTool. */\n private async callTool(\n tool: RegisteredMcpTool,\n call: CallToolRequest,\n established: McpToolRequest,\n sdkContext: ServerContext,\n ): Promise<CallToolResult> {\n const caller = this.currentCaller();\n const invocation = new McpInvocationContext(\n sdkContext.mcpReq.id,\n tool.name,\n caller.subject,\n caller.roles,\n AbortSignal.any([sdkContext.mcpReq.signal, established.disconnectSignal]),\n this.progressReporter(sdkContext),\n );\n const args = (call.params.arguments ?? {}) as DtoValue;\n const value = await this.dispatcher.call(tool, args, invocation, this.authorizationService);\n const outputFailure = this.outputSchemas.validate(tool.outputSchema, value);\n if (outputFailure) {\n throw new ApiImplementationError(\n `MCP output schema violation for ${tool.apiClass.name}.${tool.methodName}: ${outputFailure.message}`,\n );\n }\n const structured = this.record(value);\n return {\n content: [{ type: 'text', text: JSON.stringify(structured) }],\n structuredContent: structured,\n };\n }\n\n /** The trusted caller of the CURRENT RequestContext: a verified user, else a verified machine. */\n private currentCaller(): McpCallerFacts {\n const userId = RequestContext.getTrusted(WebpiecesCoreHeaders.USER_ID);\n if (typeof userId === 'string' && userId.trim() !== '') {\n return new McpCallerFacts(userId, CanonicalUserRoles.read());\n }\n const machine = RequestContext.getTrusted(VERIFIED_MACHINE_CALLER);\n if (machine instanceof VerifiedMachineCaller)\n return new McpCallerFacts(machine.identity, []);\n throw new ApiUnauthorizedError(\n 'MCP tool call has no trusted caller in its RequestContext.',\n );\n }\n\n private progressReporter(context: ServerContext): McpProgressReporter | undefined {\n const token = context.mcpReq._meta?.progressToken;\n if (token === undefined) return undefined;\n return async (progress: number, total?: number, message?: string): Promise<void> => {\n await context.mcpReq.notify({\n method: 'notifications/progress',\n params: this.progressParams(token, progress, total, message),\n });\n };\n }\n\n private progressParams(\n progressToken: ProgressToken,\n progress: number,\n total?: number,\n message?: string,\n ): Record<string, DtoValue> {\n const params: Record<string, DtoValue> = { progressToken, progress };\n if (total !== undefined) params['total'] = total;\n if (message !== undefined) params['message'] = message;\n return params;\n }\n\n private calculateRevision(): string {\n const surface = this.registry.tools.map((tool: RegisteredMcpTool) => [\n tool.name,\n tool.title,\n tool.description,\n tool.inputSchema,\n tool.outputSchema,\n tool.annotations,\n tool.profiles,\n ]);\n return createHash('sha256')\n .update(JSON.stringify([this.profiles, surface]))\n .digest('hex')\n .slice(0, 12);\n }\n\n private record(value: DtoValue): Record<string, DtoValue> {\n const jsonValue = JSON.stringify(value);\n if (jsonValue === undefined) {\n throw new ApiImplementationError('MCP structured content is not JSON serializable.');\n }\n const parsed = JSON.parse(jsonValue) as DtoValue;\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { value: parsed };\n }\n return parsed as Record<string, DtoValue>;\n }\n}\n\n/** Who the current RequestContext says is calling, as McpInvocationContext reports it. */\nclass McpCallerFacts {\n constructor(\n public readonly subject: string,\n public readonly roles: readonly string[],\n ) {}\n}\n"]}
|
|
@@ -21,22 +21,20 @@ import { McpHttpErrorBody } from './McpToolCallRendering';
|
|
|
21
21
|
* "Internal Error" and nothing else records WHERE it broke (#1055).
|
|
22
22
|
*/
|
|
23
23
|
export declare class WpMcpErrorTranslator {
|
|
24
|
-
private readonly challenge;
|
|
25
24
|
private readonly boundary;
|
|
26
25
|
private readonly defaultToolCall;
|
|
27
|
-
/**
|
|
28
|
-
* @param challenge - the `WWW-Authenticate` value for a 401, read lazily because
|
|
29
|
-
* `WpMcpServerConfig` is only fully validated at `bind(...)` time.
|
|
30
|
-
*/
|
|
31
|
-
constructor(challenge: () => string);
|
|
32
26
|
/**
|
|
33
27
|
* Boundary #1: everything webpieces itself rejects before the SDK is involved — bearer
|
|
34
28
|
* verification, `Origin`, a malformed body. This is 401/403/400/500 today and any status
|
|
35
29
|
* tomorrow, so it produces a VALUE like every other API in the framework and `WpMcpServer` hands
|
|
36
30
|
* it to the shared express writer. The BODY stays JSON-RPC shaped because an MCP client expects
|
|
37
31
|
* that — except for an {@link OAuthProtocolError}, which is answered in the RFC 6749/6750 shape; `HttpResponseDto` is generic, and its header LIST carries `WWW-Authenticate`.
|
|
32
|
+
*
|
|
33
|
+
* `challenge` is the `mcpOAuth(...)` discovery value, or `undefined` when this request is not an
|
|
34
|
+
* OAuth one: ONLY the OAuth mode emits `WWW-Authenticate`, so a rejected `jwt()`, OIDC or api-key
|
|
35
|
+
* credential is a plain 401 with no hint of which mode came closest.
|
|
38
36
|
*/
|
|
39
|
-
toBearerBoundaryResponse(error: Error): HttpResponseDto<McpHttpErrorBody> | HttpResponseDto<OAuthErrorBody>;
|
|
37
|
+
toBearerBoundaryResponse(error: Error, challenge: string | undefined): HttpResponseDto<McpHttpErrorBody> | HttpResponseDto<OAuthErrorBody>;
|
|
40
38
|
/**
|
|
41
39
|
* Boundary #2: `tools/list` has no tool-result channel, so a failure IS a JSON-RPC error. It
|
|
42
40
|
* THROWS rather than returning a value the caller must remember to throw, and is typed `never`
|
|
@@ -26,37 +26,31 @@ const log = core_util_1.LogManager.getLogger('WpMcpErrorTranslator');
|
|
|
26
26
|
* "Internal Error" and nothing else records WHERE it broke (#1055).
|
|
27
27
|
*/
|
|
28
28
|
class WpMcpErrorTranslator {
|
|
29
|
-
challenge;
|
|
30
29
|
boundary = new core_util_1.ApiErrorBoundary();
|
|
31
30
|
defaultToolCall = new McpToolCallRendering_1.McpDefaultToolCallRenderer();
|
|
32
|
-
/**
|
|
33
|
-
* @param challenge - the `WWW-Authenticate` value for a 401, read lazily because
|
|
34
|
-
* `WpMcpServerConfig` is only fully validated at `bind(...)` time.
|
|
35
|
-
*/
|
|
36
|
-
constructor(challenge) {
|
|
37
|
-
this.challenge = challenge;
|
|
38
|
-
}
|
|
39
31
|
/**
|
|
40
32
|
* Boundary #1: everything webpieces itself rejects before the SDK is involved — bearer
|
|
41
33
|
* verification, `Origin`, a malformed body. This is 401/403/400/500 today and any status
|
|
42
34
|
* tomorrow, so it produces a VALUE like every other API in the framework and `WpMcpServer` hands
|
|
43
35
|
* it to the shared express writer. The BODY stays JSON-RPC shaped because an MCP client expects
|
|
44
36
|
* that — except for an {@link OAuthProtocolError}, which is answered in the RFC 6749/6750 shape; `HttpResponseDto` is generic, and its header LIST carries `WWW-Authenticate`.
|
|
37
|
+
*
|
|
38
|
+
* `challenge` is the `mcpOAuth(...)` discovery value, or `undefined` when this request is not an
|
|
39
|
+
* OAuth one: ONLY the OAuth mode emits `WWW-Authenticate`, so a rejected `jwt()`, OIDC or api-key
|
|
40
|
+
* credential is a plain 401 with no hint of which mode came closest.
|
|
45
41
|
*/
|
|
46
|
-
toBearerBoundaryResponse(error) {
|
|
42
|
+
toBearerBoundaryResponse(error, challenge) {
|
|
47
43
|
// An authorization service that refuses the bearer with an RFC 6750 code (invalid_token,
|
|
48
44
|
// insufficient_scope) is answered in the RFC shape, and a 401 without its own challenge
|
|
49
45
|
// gets this server's, so the client still finds `resource_metadata` (#1176).
|
|
50
46
|
if (error instanceof core_util_1.OAuthProtocolError) {
|
|
51
|
-
return core_util_1.OAuthErrorResponse.toResponse(error,
|
|
47
|
+
return core_util_1.OAuthErrorResponse.toResponse(error, challenge);
|
|
52
48
|
}
|
|
53
49
|
const payload = this.boundary.encode(error);
|
|
54
50
|
const generic = payload.message;
|
|
55
51
|
switch (payload.kind) {
|
|
56
52
|
case 'unauthorized':
|
|
57
|
-
return this.httpError(401, -32_000, generic, [
|
|
58
|
-
new core_util_1.HttpHeader('WWW-Authenticate', this.challenge()),
|
|
59
|
-
]);
|
|
53
|
+
return this.httpError(401, -32_000, generic, challenge === undefined ? [] : [new core_util_1.HttpHeader('WWW-Authenticate', challenge)]);
|
|
60
54
|
case 'forbidden':
|
|
61
55
|
return this.httpError(403, -32_000, generic, []);
|
|
62
56
|
case 'bad-request':
|
|
@@ -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 HttpHeader,\n HttpResponseDto,\n HttpResponseStatus,\n LogManager,\n OAuthErrorBody,\n OAuthErrorResponse,\n OAuthProtocolError,\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 * their renderings distinct at the two interception seams (HTTP ingress and shared SDK callbacks):\n *\n * | Boundary | Who writes HTTP | Failure shape |\n * |--------------------------------|-----------------|-------------------------------------|\n * | pre-SDK bearer / Origin / body | **webpieces** | `HttpResponseDto<McpHttpErrorBody>` |\n * | ... an `OAuthProtocolError` | **webpieces** | `HttpResponseDto<OAuthErrorBody>` |\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 — and\n * `WpMcpServer` adds one `log.error` with the stack and requestId for an IMPLEMENTATION-kind failure\n * at tools/list and tools/call before handing it here, because this class renders that kind as\n * \"Internal Error\" and nothing else records WHERE it broke (#1055).\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 — except for an {@link OAuthProtocolError}, which is answered in the RFC 6749/6750 shape; `HttpResponseDto` is generic, and its header LIST carries `WWW-Authenticate`.\n */\n toBearerBoundaryResponse(\n error: Error,\n ): HttpResponseDto<McpHttpErrorBody> | HttpResponseDto<OAuthErrorBody> {\n // An authorization service that refuses the bearer with an RFC 6750 code (invalid_token,\n // insufficient_scope) is answered in the RFC shape, and a 401 without its own challenge\n // gets this server's, so the client still finds `resource_metadata` (#1176).\n if (error instanceof OAuthProtocolError) {\n return OAuthErrorResponse.toResponse(error, this.challenge());\n }\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"]}
|
|
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 OAuthErrorBody,\n OAuthErrorResponse,\n OAuthProtocolError,\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 * their renderings distinct at the two interception seams (HTTP ingress and shared SDK callbacks):\n *\n * | Boundary | Who writes HTTP | Failure shape |\n * |--------------------------------|-----------------|-------------------------------------|\n * | pre-SDK bearer / Origin / body | **webpieces** | `HttpResponseDto<McpHttpErrorBody>` |\n * | ... an `OAuthProtocolError` | **webpieces** | `HttpResponseDto<OAuthErrorBody>` |\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 — and\n * `WpMcpServer` adds one `log.error` with the stack and requestId for an IMPLEMENTATION-kind failure\n * at tools/list and tools/call before handing it here, because this class renders that kind as\n * \"Internal Error\" and nothing else records WHERE it broke (#1055).\n */\nexport class WpMcpErrorTranslator {\n private readonly boundary = new ApiErrorBoundary();\n private readonly defaultToolCall = new McpDefaultToolCallRenderer();\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 — except for an {@link OAuthProtocolError}, which is answered in the RFC 6749/6750 shape; `HttpResponseDto` is generic, and its header LIST carries `WWW-Authenticate`.\n *\n * `challenge` is the `mcpOAuth(...)` discovery value, or `undefined` when this request is not an\n * OAuth one: ONLY the OAuth mode emits `WWW-Authenticate`, so a rejected `jwt()`, OIDC or api-key\n * credential is a plain 401 with no hint of which mode came closest.\n */\n toBearerBoundaryResponse(\n error: Error,\n challenge: string | undefined,\n ): HttpResponseDto<McpHttpErrorBody> | HttpResponseDto<OAuthErrorBody> {\n // An authorization service that refuses the bearer with an RFC 6750 code (invalid_token,\n // insufficient_scope) is answered in the RFC shape, and a 401 without its own challenge\n // gets this server's, so the client still finds `resource_metadata` (#1176).\n if (error instanceof OAuthProtocolError) {\n return OAuthErrorResponse.toResponse(error, challenge);\n }\n const payload = this.boundary.encode(error);\n const generic = payload.message;\n switch (payload.kind) {\n case 'unauthorized':\n return this.httpError(\n 401,\n -32_000,\n generic,\n challenge === undefined ? [] : [new HttpHeader('WWW-Authenticate', 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"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/client';
|
|
2
|
+
import { McpToolset } from './McpToolset';
|
|
3
|
+
/**
|
|
4
|
+
* The in-memory transport over an {@link McpToolset}: an official MCP SDK `Client` wired to the
|
|
5
|
+
* toolset's server through the SDK's linked `InMemoryTransport` pair, for an agent that runs its model
|
|
6
|
+
* loop in-process and already holds a verified caller in its `RequestContext`.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* const client: Client = await WpMcpInMemory.connect(toolset); // once per process
|
|
10
|
+
* // ...inside a request whose caller AuthFilter already verified:
|
|
11
|
+
* const { tools } = await client.listTools(); // only what THIS caller may see
|
|
12
|
+
* await client.callTool({ name: 'stores_pause', arguments: { storeId } });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* NO CREDENTIAL. The caller is whoever is trusted in the `RequestContext` of the code calling
|
|
16
|
+
* `client.listTools()` / `client.callTool()`, READ PER REQUEST and NEVER captured at connect. Apps cache
|
|
17
|
+
* one client per process, so a connect-time capture would run user B's call as user A. The SDK's
|
|
18
|
+
* in-memory transport delivers each message synchronously inside the caller's own `send`, so the
|
|
19
|
+
* toolset's handlers run in the caller's async context; a call made outside any `RequestContext` is
|
|
20
|
+
* refused rather than run as nobody.
|
|
21
|
+
*
|
|
22
|
+
* The caller's scope is the one the tool runs in: a local binding's `InvocationAuthentication` needs
|
|
23
|
+
* that scope to have completed verified ingress (it did, if an endpoint's `AuthFilter` admitted the
|
|
24
|
+
* request). That is also why `McpToolset` refuses a SERVICE_ONLY tool: the caller's own service hop
|
|
25
|
+
* would satisfy it for every model behind it. No tools/list cache hint is sent, so one shared client
|
|
26
|
+
* never serves user A's list to user B.
|
|
27
|
+
*/
|
|
28
|
+
export declare class WpMcpInMemory {
|
|
29
|
+
static connect(toolset: McpToolset): Promise<Client>;
|
|
30
|
+
}
|