@webpieces/mcp-server 0.4.773 → 0.4.775

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.
Files changed (42) hide show
  1. package/README.md +83 -8
  2. package/package.json +11 -5
  3. package/src/McpApiBinding.d.ts +14 -0
  4. package/src/McpApiBinding.js +42 -0
  5. package/src/McpApiBinding.js.map +1 -0
  6. package/src/McpApiDispatcher.d.ts +10 -19
  7. package/src/McpApiDispatcher.js +35 -44
  8. package/src/McpApiDispatcher.js.map +1 -1
  9. package/src/McpAuth.d.ts +11 -2
  10. package/src/McpAuth.js +5 -1
  11. package/src/McpAuth.js.map +1 -1
  12. package/src/McpAuthorityCompileAssertions.js.map +1 -1
  13. package/src/McpBindOptions.d.ts +17 -0
  14. package/src/McpBindOptions.js +36 -0
  15. package/src/McpBindOptions.js.map +1 -0
  16. package/src/McpDeployment.d.ts +11 -0
  17. package/src/McpDeployment.js +32 -0
  18. package/src/McpDeployment.js.map +1 -0
  19. package/src/McpInvocationContext.d.ts +14 -0
  20. package/src/McpInvocationContext.js +25 -0
  21. package/src/McpInvocationContext.js.map +1 -0
  22. package/src/McpToolRegistry.d.ts +7 -4
  23. package/src/McpToolRegistry.js +20 -16
  24. package/src/McpToolRegistry.js.map +1 -1
  25. package/src/WpMcpErrorTranslator.d.ts +69 -0
  26. package/src/WpMcpErrorTranslator.js +168 -0
  27. package/src/WpMcpErrorTranslator.js.map +1 -0
  28. package/src/WpMcpServer.d.ts +55 -24
  29. package/src/WpMcpServer.js +331 -154
  30. package/src/WpMcpServer.js.map +1 -1
  31. package/src/__tests__/McpHttpTestHarness.d.ts +40 -0
  32. package/src/__tests__/McpHttpTestHarness.js +158 -0
  33. package/src/__tests__/McpHttpTestHarness.js.map +1 -0
  34. package/src/__tests__/McpRemoteFixtures.d.ts +93 -0
  35. package/src/__tests__/McpRemoteFixtures.js +409 -0
  36. package/src/__tests__/McpRemoteFixtures.js.map +1 -0
  37. package/src/__tests__/WpMcpServerTestFixtures.d.ts +60 -0
  38. package/src/__tests__/WpMcpServerTestFixtures.js +269 -0
  39. package/src/__tests__/WpMcpServerTestFixtures.js.map +1 -0
  40. package/src/index.d.ts +10 -3
  41. package/src/index.js +18 -4
  42. package/src/index.js.map +1 -1
@@ -1,212 +1,389 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WpMcpServer = exports.ModelVisibleToolError = void 0;
4
- const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
- const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
3
+ exports.WpMcpServer = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const server_1 = require("@modelcontextprotocol/server");
6
+ const node_1 = require("@modelcontextprotocol/node");
7
+ const express_1 = require("express");
6
8
  const core_util_1 = require("@webpieces/core-util");
9
+ const core_context_1 = require("@webpieces/core-context");
7
10
  const McpAuth_1 = require("./McpAuth");
8
11
  const McpApiDispatcher_1 = require("./McpApiDispatcher");
12
+ const McpInvocationContext_1 = require("./McpInvocationContext");
9
13
  const McpToolRegistry_1 = require("./McpToolRegistry");
14
+ const WpMcpErrorTranslator_1 = require("./WpMcpErrorTranslator");
10
15
  const log = core_util_1.LogManager.getLogger('WpMcpServer');
11
- /** The only error shape rendered into model-visible MCP content. */
12
- class ModelVisibleToolError {
13
- kind;
14
- message;
15
- requestId;
16
- field;
17
- callerMessage;
18
- errorCode;
19
- retryAfterSeconds;
20
- constructor(kind, message, requestId, field, callerMessage, errorCode, retryAfterSeconds) {
21
- this.kind = kind;
22
- this.message = message;
23
- this.requestId = requestId;
24
- this.field = field;
25
- this.callerMessage = callerMessage;
26
- this.errorCode = errorCode;
27
- this.retryAfterSeconds = retryAfterSeconds;
16
+ /** Facts established once per POST at the bind boundary and handed to the SDK handlers. */
17
+ class McpPostAuthentication {
18
+ accessToken;
19
+ credential;
20
+ disconnectSignal;
21
+ constructor(accessToken, credential, disconnectSignal) {
22
+ this.accessToken = accessToken;
23
+ this.credential = credential;
24
+ this.disconnectSignal = disconnectSignal;
28
25
  }
29
26
  }
30
- exports.ModelVisibleToolError = ModelVisibleToolError;
31
27
  /**
32
- * Node-only MCP adapter. Build one Server per authenticated transport/session; every tool call still
33
- * re-enters the endpoint's ordinary AuthFilter, so tools/list visibility never grants access.
28
+ * The official SDK server with Webpieces-owned tools/list and tools/call handlers. It answers
29
+ * `toolInputSchemaJson` from the Webpieces registry so the SDK's pre-dispatch SEP-2243
30
+ * `Mcp-Param-*` header validation still runs without registering SDK-side tool callbacks.
31
+ */
32
+ class WpSdkMcpServer extends server_1.McpServer {
33
+ registry;
34
+ constructor(info, options, registry) {
35
+ super(info, options);
36
+ this.registry = registry;
37
+ }
38
+ // webpieces-disable no-any-unknown -- signature is fixed by the SDK's JSON Schema record type
39
+ toolInputSchemaJson(name) {
40
+ return this.registry.find(name)?.inputSchema;
41
+ }
42
+ }
43
+ /**
44
+ * Modern-only MCP 2026-07-28 adapter around the official SDK's request-scoped HTTP handler.
45
+ *
46
+ * Error boundary: every failure is mapped by the one `WpMcpErrorTranslator`, and each entry point
47
+ * has exactly one catch that only delegates to it: the bind HTTP handler (`toHttp`), tools/list
48
+ * (`toProtocolError`) and tools/call (`toToolResult`). `subscriptions/listen` is served entirely by
49
+ * the SDK's listen router, so Webpieces has no handler (and no catch) there. The external bearer is
50
+ * verified exactly once per POST, at the HTTP boundary, before the SDK is involved.
34
51
  */
35
52
  class WpMcpServer {
36
53
  config;
54
+ dispatcher = new McpApiDispatcher_1.McpApiDispatcher();
55
+ translator = new WpMcpErrorTranslator_1.WpMcpErrorTranslator();
37
56
  registry;
38
- dispatcher;
39
- constructor(config, apiFactory, apiClasses) {
57
+ handler;
58
+ streamingHandler;
59
+ nodeHandler;
60
+ streamingNodeHandler;
61
+ revision;
62
+ constructor(config) {
40
63
  this.config = config;
41
- this.registry = new McpToolRegistry_1.McpToolRegistry(apiClasses);
42
- this.dispatcher = new McpApiDispatcher_1.McpApiDispatcher(apiFactory);
43
64
  }
44
- async build(accessToken) {
45
- await this.verifyCredential(accessToken);
46
- return this.buildForAccessToken(accessToken);
65
+ bind(app, options) {
66
+ if (this.handler)
67
+ throw new Error('WpMcpServer.bind(...) may be called only once.');
68
+ this.registry = new McpToolRegistry_1.McpToolRegistry(options.bindings);
69
+ this.revision = this.calculateRegistryRevision(this.registry);
70
+ this.handler = this.createHandler(options, 'auto');
71
+ this.streamingHandler = this.createHandler(options, 'sse');
72
+ this.nodeHandler = (0, node_1.toNodeHandler)(this.handler, {
73
+ onerror: (error) => log.error('MCP Node transport failed', error),
74
+ });
75
+ this.streamingNodeHandler = (0, node_1.toNodeHandler)(this.streamingHandler, {
76
+ onerror: (error) => log.error('MCP streaming Node transport failed', error),
77
+ });
78
+ app.post(options.endpointPath, (0, express_1.json)(), async (req, res) => this.handleHttp(req, res, options),
79
+ // Express routes body-parser failures (bad JSON, body too large) to an error handler.
80
+ (err, req, res, _next) => core_context_1.RequestContext.run(() => {
81
+ new core_context_1.RequestContextHeaders().fillFromRequest(this.toHttpRequest(req));
82
+ this.translator.toHttpBodyFailure(err, res, this.httpScope(req), this.challenge());
83
+ }));
47
84
  }
48
85
  protectedResourceMetadata() {
49
86
  return this.config.protectedResourceMetadata();
50
87
  }
51
- buildForAccessToken(accessToken) {
52
- const server = new index_js_1.Server(
53
- // webpieces-disable no-anonymous-object-literals -- external MCP SDK request structure
54
- { name: this.config.name, version: this.config.version },
55
- // webpieces-disable no-anonymous-object-literals -- external MCP SDK capability structure
56
- { capabilities: { tools: {} } });
57
- server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
58
- const credential = await this.verifyCredential(accessToken);
59
- return {
60
- tools: this.registry.tools
61
- .filter((tool) => tool.isVisibleTo(credential.listingRoles))
62
- .map((tool) => ({
63
- name: tool.name,
64
- description: tool.description,
65
- inputSchema: tool.inputSchema,
66
- outputSchema: tool.outputSchema,
67
- annotations: tool.annotations,
68
- })),
69
- };
88
+ /** Gracefully closes active listen/request streams; clients must reconnect and refresh lists. */
89
+ async close() {
90
+ await Promise.all([this.handler?.close(), this.streamingHandler?.close()]);
91
+ }
92
+ /** Level-triggered invalidation; the authoritative list is always fetched again by clients. */
93
+ toolsChanged() {
94
+ this.handler?.notify.toolsChanged();
95
+ }
96
+ get registryRevision() {
97
+ return this.revision;
98
+ }
99
+ createHandler(options, responseMode) {
100
+ return (0, server_1.createMcpHandler)((context) => this.buildSdkServer(context, options), {
101
+ legacy: 'reject',
102
+ responseMode,
103
+ bus: options.deployment.bus,
104
+ maxSubscriptions: options.maxSubscriptions,
105
+ keepAliveMs: options.keepAliveMs,
106
+ onerror: (error) => log.warn('MCP protocol request rejected', error),
70
107
  });
71
- server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
72
- const tool = this.registry.find(request.params.name);
73
- if (!tool) {
74
- throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
108
+ }
109
+ /** Entry point #1: the one catch for everything before and around the SDK exchange. */
110
+ async handleHttp(req, res, options) {
111
+ res.setHeader('X-Accel-Buffering', 'no');
112
+ await core_context_1.RequestContext.run(async () => {
113
+ new core_context_1.RequestContextHeaders().fillFromRequest(this.toHttpRequest(req));
114
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP HTTP entry point; delegates only to WpMcpErrorTranslator
115
+ try {
116
+ await this.serveExpress(req, res, options);
117
+ }
118
+ catch (err) {
119
+ const error = (0, core_util_1.toError)(err);
120
+ this.translator.toHttp(error, res, this.httpScope(req), this.challenge());
75
121
  }
76
- return this.call(tool, request.params.arguments ?? {}, accessToken);
77
122
  });
123
+ }
124
+ async serveExpress(req, res, options) {
125
+ const origin = req.header('origin');
126
+ if (origin && !options.allowedOrigins.includes(origin)) {
127
+ throw new core_util_1.ApiForbiddenError(`MCP request Origin '${origin}' is not allowed.`);
128
+ }
129
+ const authentication = await this.authenticate(req);
130
+ const request = req;
131
+ request.auth = this.authInfo(authentication);
132
+ const serve = this.hasProgressToken(req.body)
133
+ ? this.streamingNodeHandler
134
+ : this.nodeHandler;
135
+ if (!serve)
136
+ throw new core_util_1.ApiImplementationError('MCP Node handlers are not initialized.');
137
+ await serve(request, res, req.body);
138
+ }
139
+ /** Verifies the external bearer once for this POST. Handlers never re-verify it. */
140
+ async authenticate(req) {
141
+ const match = req.header('authorization')?.match(/^Bearer ([^\s]+)$/i);
142
+ const token = match?.[1];
143
+ if (!token)
144
+ throw new core_util_1.ApiUnauthorizedError('MCP request has no bearer access token.');
145
+ const credential = await this.config.accessTokenAuthority.verifyAccessToken(token, this.config.resource);
146
+ this.validateCredential(credential);
147
+ return new McpPostAuthentication(token, credential, this.disconnectSignal(req));
148
+ }
149
+ disconnectSignal(req) {
150
+ const disconnected = new AbortController();
151
+ const res = req.res;
152
+ const cancel = () => disconnected.abort();
153
+ req.once('aborted', cancel);
154
+ req.socket.once('close', cancel);
155
+ res?.once('close', cancel);
156
+ res?.once('finish', () => {
157
+ req.off('aborted', cancel);
158
+ req.socket.off('close', cancel);
159
+ res.off('close', cancel);
160
+ });
161
+ return disconnected.signal;
162
+ }
163
+ buildSdkServer(context, options) {
164
+ const authentication = context.authInfo?.extra?.['webpiecesAuthentication'];
165
+ if (context.era !== 'modern' || !(authentication instanceof McpPostAuthentication)) {
166
+ throw new core_util_1.ApiImplementationError('WpMcpServer serves only MCP 2026-07-28 requests authenticated by its bind boundary.');
167
+ }
168
+ const registry = this.requireRegistry();
169
+ const server = new WpSdkMcpServer({ name: this.config.name, version: `${this.config.version}+${this.revision}` }, {
170
+ capabilities: { tools: { listChanged: true } },
171
+ cacheHints: {
172
+ 'tools/list': { ttlMs: options.deployment.ttlMs, cacheScope: 'private' },
173
+ 'server/discover': { ttlMs: options.deployment.ttlMs, cacheScope: 'private' },
174
+ },
175
+ }, registry);
176
+ server.server.removeRequestHandler('tools/list');
177
+ server.server.removeRequestHandler('tools/call');
178
+ server.server.setRequestHandler('tools/list', async (_request, sdkContext) => this.handleListTools(authentication, sdkContext));
179
+ server.server.setRequestHandler('tools/call', async (request, sdkContext) => this.handleCallTool(server, request, authentication, sdkContext));
78
180
  return server;
79
181
  }
80
- async verifyCredential(accessToken) {
81
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- security boundary logs owner detail and exposes only a generic authentication failure
182
+ /** Entry point #2: tools/list has no tool-result channel, so failures are JSON-RPC errors. */
183
+ async handleListTools(authentication, sdkContext) {
184
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP tools/list entry point; delegates only to WpMcpErrorTranslator
82
185
  try {
83
- const credential = await this.config.accessTokenAuthority.verifyAccessToken(accessToken, this.config.resource);
84
- this.validateCredential(credential);
85
- return credential;
186
+ return this.listTools(authentication.credential);
86
187
  }
87
188
  catch (err) {
88
189
  const error = (0, core_util_1.toError)(err);
89
- log.warn('MCP access-token verification rejected a request', error);
90
- throw new core_util_1.ApiUnauthorizedError('MCP access token rejected.', undefined, error);
190
+ throw this.translator.toProtocolError(error, this.sdkScope(sdkContext, 'tools/list'));
91
191
  }
92
192
  }
93
- validateCredential(credential) {
94
- const now = Math.floor(Date.now() / 1000);
95
- if (credential.subject.trim() === '') {
96
- throw new Error('MCP access token has no subject.');
97
- }
98
- if (!Number.isFinite(credential.issuedAtEpochSeconds) ||
99
- !Number.isFinite(credential.expiresAtEpochSeconds) ||
100
- !Number.isFinite(credential.accountValidatedAtEpochSeconds)) {
101
- throw new Error('MCP access token security timestamps must be finite.');
102
- }
103
- if (credential.resource !== this.config.resource) {
104
- throw new Error('MCP access token was not issued for this protected resource.');
105
- }
106
- if (!this.config.authorizationServers.includes(credential.issuer)) {
107
- throw new Error('MCP access token issuer is not trusted by this protected resource.');
108
- }
109
- if (credential.issuedAtEpochSeconds > now) {
110
- throw new Error('MCP access token was issued in the future.');
111
- }
112
- if (credential.expiresAtEpochSeconds <= now) {
113
- throw new Error('MCP access token has expired.');
114
- }
115
- if (credential.expiresAtEpochSeconds - credential.issuedAtEpochSeconds >
116
- McpAuth_1.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS) {
117
- throw new Error('MCP access token lifetime exceeds 30 days.');
118
- }
119
- for (const scope of this.config.requiredScopes) {
120
- if (!credential.scopes.includes(scope)) {
121
- throw new Error(`MCP access token is missing required scope '${scope}'.`);
122
- }
123
- }
124
- if (credential.accountValidatedAtEpochSeconds > now ||
125
- now - credential.accountValidatedAtEpochSeconds > this.config.maxAccountValidationAgeSeconds) {
126
- throw new Error('MCP account authorization state is not fresh enough for dispatch.');
193
+ listTools(credential) {
194
+ const tools = [];
195
+ for (const tool of this.requireRegistry().tools) {
196
+ if (tool.isVisibleTo(credential.listingRoles))
197
+ tools.push(this.toolDefinition(tool));
127
198
  }
199
+ return { tools };
128
200
  }
129
- async call(tool, args, accessToken) {
130
- let credential;
131
- // webpieces-disable no-unmanaged-exceptions -- MCP transport boundary normalizes authentication failures before they reach the model
132
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP transport boundary
133
- try {
134
- credential = await this.verifyCredential(accessToken);
135
- }
136
- catch (err) {
137
- //const error = toError(err);
138
- throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Unauthorized');
139
- }
140
- let endpointJwt;
141
- // webpieces-disable no-unmanaged-exceptions -- MCP transport boundary logs mint failures and exposes only a generic protocol error
142
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP transport boundary
201
+ toolDefinition(tool) {
202
+ return {
203
+ name: tool.name,
204
+ description: tool.description,
205
+ inputSchema: tool.inputSchema,
206
+ outputSchema: tool.outputSchema,
207
+ annotations: tool.annotations,
208
+ _meta: { webpiecesRegistryRevision: this.revision },
209
+ };
210
+ }
211
+ /**
212
+ * Entry point #3: an unknown tool is a JSON-RPC -32602; every failure after the tool is found
213
+ * is an `isError: true` result, identically for local and remote bindings.
214
+ */
215
+ async handleCallTool(server, request, authentication, sdkContext) {
216
+ const name = request.params.name;
217
+ const scope = this.sdkScope(sdkContext, 'tools/call', name);
218
+ const tool = this.requireRegistry().find(name);
219
+ if (!tool)
220
+ throw this.translator.unknownTool(name, scope);
221
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- MCP tools/call entry point; delegates only to WpMcpErrorTranslator
143
222
  try {
144
- const descriptor = new McpAuth_1.McpEndpointDescriptor(tool.name, tool.apiClass.name, tool.methodName);
145
- const mintRequest = this.config.endpointMintRequest(credential, descriptor);
146
- endpointJwt = await this.config.endpointJwtAuthority.mint(mintRequest);
147
- this.validateEndpointJwt(endpointJwt, accessToken);
223
+ const result = await this.callTool(tool, request, authentication, sdkContext);
224
+ result._meta = this.translator.resultMeta(scope.requestId);
225
+ return server.server.projectCallToolResult(result, tool.outputSchema);
148
226
  }
149
227
  catch (err) {
150
228
  const error = (0, core_util_1.toError)(err);
151
- log.error(`MCP endpoint credential mint failed for ${tool.apiClass.name}.${tool.methodName}`, error);
152
- throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, 'Internal Error');
229
+ return this.translator.toToolResult(error, scope);
153
230
  }
154
- const result = await this.dispatcher.call(tool, args ?? {}, endpointJwt.token);
155
- if (!result.success)
156
- return this.apiError(result.error, result.requestId);
157
- const outputFailure = this.registry.schemaBuilder.validate(tool.responseClass, result.value);
231
+ }
232
+ /** Everything a tool call does. No catch in here: failures propagate to handleCallTool. */
233
+ async callTool(tool, request, authentication, sdkContext) {
234
+ const credential = authentication.credential;
235
+ const endpointJwt = tool.binding.topology === 'local'
236
+ ? await this.mintEndpointJwt(tool, authentication)
237
+ : undefined;
238
+ const invocation = new McpInvocationContext_1.McpInvocationContext(sdkContext.mcpReq.id, tool.name, credential.subject, credential.listingRoles, AbortSignal.any([sdkContext.mcpReq.signal, authentication.disconnectSignal]), this.progressReporter(sdkContext));
239
+ const args = (request.params.arguments ?? {});
240
+ const value = await this.dispatcher.call(tool, args, credential, invocation, endpointJwt?.token);
241
+ const outputFailure = this.requireRegistry().schemaBuilder.validate(tool.responseClass, value);
158
242
  if (outputFailure) {
159
- log.error(`MCP output schema violation for ${tool.apiClass.name}.${tool.methodName}: ${outputFailure.message}`);
160
- return this.errorResult(new ModelVisibleToolError('implementation', 'Internal Error', result.requestId));
161
- }
162
- let structured;
163
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- transport boundary sanitizes serialization failures
164
- try {
165
- structured = this.record(result.value);
166
- }
167
- catch (err) {
168
- const error = (0, core_util_1.toError)(err);
169
- log.error(`MCP response serialization failed for ${tool.apiClass.name}.${tool.methodName}`, error);
170
- return this.errorResult(new ModelVisibleToolError('implementation', 'Internal Error', result.requestId));
243
+ throw new core_util_1.ApiImplementationError(`MCP output schema violation for ${tool.apiClass.name}.${tool.methodName}: ${outputFailure.message}`);
171
244
  }
245
+ const structured = this.record(value);
172
246
  return {
173
247
  content: [{ type: 'text', text: JSON.stringify(structured) }],
174
248
  structuredContent: structured,
175
249
  };
176
250
  }
177
- validateEndpointJwt(endpointJwt, accessToken) {
251
+ async mintEndpointJwt(tool, authentication) {
252
+ const descriptor = new McpAuth_1.McpEndpointDescriptor(tool.name, tool.apiClass.name, tool.methodName);
253
+ const minted = await this.config.endpointJwtAuthority.mint(this.config.endpointMintRequest(authentication.credential, descriptor));
178
254
  const now = Math.floor(Date.now() / 1000);
179
- if (endpointJwt.token === accessToken) {
180
- throw new Error('MCP access-token passthrough to endpoint dispatch is forbidden.');
181
- }
182
- if (endpointJwt.expiresAtEpochSeconds <= now) {
183
- throw new Error('MCP endpoint JWT has already expired.');
255
+ if (minted.token === authentication.accessToken) {
256
+ throw new core_util_1.ApiImplementationError(`MCP access-token passthrough is forbidden (endpoint JWT for ${tool.name}).`);
184
257
  }
185
- if (endpointJwt.expiresAtEpochSeconds - now >
186
- this.config.maxEndpointJwtLifetimeSeconds) {
187
- throw new Error('MCP endpoint JWT lifetime exceeds the configured one-hour ceiling.');
258
+ if (minted.expiresAtEpochSeconds <= now ||
259
+ minted.expiresAtEpochSeconds - now > this.config.maxEndpointJwtLifetimeSeconds) {
260
+ throw new core_util_1.ApiImplementationError(`MCP endpoint JWT lifetime is invalid for ${tool.name}.`);
188
261
  }
262
+ return minted;
189
263
  }
190
- apiError(payload, requestId) {
191
- return this.errorResult(new ModelVisibleToolError(payload.kind, payload.message, requestId, payload.field, payload.callerMessage, payload.errorCode, payload.retryAfterSeconds));
264
+ validateCredential(credential) {
265
+ const now = Math.floor(Date.now() / 1000);
266
+ const reject = (reason) => {
267
+ throw new core_util_1.ApiUnauthorizedError(`MCP access token rejected: ${reason}`);
268
+ };
269
+ if (credential.subject.trim() === '')
270
+ reject('no subject');
271
+ const stamps = [
272
+ credential.issuedAtEpochSeconds,
273
+ credential.expiresAtEpochSeconds,
274
+ credential.accountValidatedAtEpochSeconds,
275
+ ];
276
+ if (!stamps.every(Number.isFinite))
277
+ reject('security timestamps must be finite');
278
+ if (credential.resource !== this.config.resource)
279
+ reject('resource mismatch');
280
+ if (!this.config.authorizationServers.includes(credential.issuer)) {
281
+ reject('issuer is not trusted');
282
+ }
283
+ if (credential.issuedAtEpochSeconds > now || credential.expiresAtEpochSeconds <= now) {
284
+ reject('outside its valid time window');
285
+ }
286
+ const lifetime = credential.expiresAtEpochSeconds - credential.issuedAtEpochSeconds;
287
+ if (lifetime > McpAuth_1.MAX_MCP_ACCESS_TOKEN_LIFETIME_SECONDS)
288
+ reject('lifetime exceeds 30 days');
289
+ for (const scope of this.config.requiredScopes) {
290
+ if (!credential.scopes.includes(scope))
291
+ reject(`missing required scope '${scope}'`);
292
+ }
293
+ const validatedAge = now - credential.accountValidatedAtEpochSeconds;
294
+ if (credential.accountValidatedAtEpochSeconds > now ||
295
+ validatedAge > this.config.maxAccountValidationAgeSeconds) {
296
+ reject('account authorization state is not fresh enough for dispatch');
297
+ }
192
298
  }
193
- errorResult(error) {
194
- const structured = this.record(error);
299
+ authInfo(authentication) {
300
+ const credential = authentication.credential;
195
301
  return {
196
- content: [{ type: 'text', text: JSON.stringify(structured) }],
197
- // `outputSchema` describes the endpoint's success DTO. MCP clients validate
198
- // structuredContent against it even when isError is true, so error details must stay
199
- // in text content until the protocol supports a separate error schema.
200
- isError: true,
302
+ token: authentication.accessToken,
303
+ clientId: credential.subject,
304
+ scopes: [...credential.scopes],
305
+ expiresAt: credential.expiresAtEpochSeconds,
306
+ resource: new URL(credential.resource),
307
+ extra: { webpiecesAuthentication: authentication },
201
308
  };
202
309
  }
310
+ progressReporter(context) {
311
+ const token = context.mcpReq._meta?.progressToken;
312
+ if (token === undefined)
313
+ return undefined;
314
+ return async (progress, total, message) => {
315
+ await context.mcpReq.notify({
316
+ method: 'notifications/progress',
317
+ params: this.progressParams(token, progress, total, message),
318
+ });
319
+ };
320
+ }
321
+ progressParams(progressToken, progress, total, message) {
322
+ const params = { progressToken, progress };
323
+ if (total !== undefined)
324
+ params['total'] = total;
325
+ if (message !== undefined)
326
+ params['message'] = message;
327
+ return params;
328
+ }
329
+ requestId() {
330
+ return core_context_1.RequestContext.getUntrusted(core_util_1.WebpiecesCoreHeaders.REQUEST_ID) ?? 'missing-request-id';
331
+ }
332
+ sdkScope(context, method, toolName) {
333
+ return new WpMcpErrorTranslator_1.McpFailureScope(this.requestId(), context.mcpReq.id, method, toolName);
334
+ }
335
+ httpScope(req) {
336
+ const body = this.bodyObject(req.body);
337
+ const id = body?.['id'];
338
+ const method = body?.['method'];
339
+ const params = this.bodyObject(body?.['params']);
340
+ const name = params?.['name'];
341
+ return new WpMcpErrorTranslator_1.McpFailureScope(this.requestId(), typeof id === 'string' || typeof id === 'number' ? id : null, typeof method === 'string' ? method : 'unknown', typeof name === 'string' ? name : undefined);
342
+ }
343
+ challenge() {
344
+ return `Bearer resource_metadata="${this.config.resource}"`;
345
+ }
346
+ bodyObject(value) {
347
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
348
+ return undefined;
349
+ return value;
350
+ }
351
+ hasProgressToken(body) {
352
+ const params = this.bodyObject(this.bodyObject(body)?.['params']);
353
+ return this.bodyObject(params?.['_meta'])?.['progressToken'] !== undefined;
354
+ }
355
+ toHttpRequest(req) {
356
+ const headers = new Map();
357
+ for (const [name, value] of Object.entries(req.headers)) {
358
+ if (typeof value === 'string')
359
+ headers.set(name, [value]);
360
+ else if (Array.isArray(value))
361
+ headers.set(name, value);
362
+ }
363
+ return new core_context_1.HttpRequest(req.method, req.originalUrl || req.path, headers);
364
+ }
365
+ calculateRegistryRevision(registry) {
366
+ const surface = registry.tools.map((tool) => [
367
+ tool.name,
368
+ tool.description,
369
+ tool.inputSchema,
370
+ tool.outputSchema,
371
+ tool.annotations,
372
+ ]);
373
+ return (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(surface)).digest('hex').slice(0, 12);
374
+ }
375
+ requireRegistry() {
376
+ if (!this.registry)
377
+ throw new Error('Call WpMcpServer.bind(...) before serving requests.');
378
+ return this.registry;
379
+ }
203
380
  record(value) {
204
- const json = JSON.stringify(value);
205
- if (json === undefined)
206
- throw new Error('MCP structured content is not JSON serializable.');
207
- const parsed = JSON.parse(json);
381
+ const jsonValue = JSON.stringify(value);
382
+ if (jsonValue === undefined) {
383
+ throw new core_util_1.ApiImplementationError('MCP structured content is not JSON serializable.');
384
+ }
385
+ const parsed = JSON.parse(jsonValue);
208
386
  if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
209
- // webpieces-disable no-anonymous-object-literals -- external MCP structured-content wrapper
210
387
  return { value: parsed };
211
388
  }
212
389
  return parsed;