@nitrostack/core 1.0.14 → 1.0.16
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/dist/auth/cimd.d.ts +90 -0
- package/dist/auth/cimd.d.ts.map +1 -0
- package/dist/auth/cimd.js +344 -0
- package/dist/auth/cimd.js.map +1 -0
- package/dist/auth/client.d.ts +21 -0
- package/dist/auth/client.d.ts.map +1 -1
- package/dist/auth/client.js +48 -0
- package/dist/auth/client.js.map +1 -1
- package/dist/auth/index.d.ts +2 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +15 -3
- package/dist/auth/middleware.js.map +1 -1
- package/dist/auth/server-metadata.d.ts +5 -0
- package/dist/auth/server-metadata.d.ts.map +1 -1
- package/dist/auth/server-metadata.js +7 -1
- package/dist/auth/server-metadata.js.map +1 -1
- package/dist/auth/token-store.d.ts +12 -1
- package/dist/auth/token-store.d.ts.map +1 -1
- package/dist/auth/token-store.js +23 -1
- package/dist/auth/token-store.js.map +1 -1
- package/dist/auth/token-validation.d.ts +1 -1
- package/dist/auth/token-validation.d.ts.map +1 -1
- package/dist/auth/token-validation.js +20 -6
- package/dist/auth/token-validation.js.map +1 -1
- package/dist/auth/types.d.ts +17 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/core/app-decorator.d.ts +10 -0
- package/dist/core/app-decorator.d.ts.map +1 -1
- package/dist/core/app-decorator.js +25 -11
- package/dist/core/app-decorator.js.map +1 -1
- package/dist/core/builders.d.ts.map +1 -1
- package/dist/core/builders.js +5 -0
- package/dist/core/builders.js.map +1 -1
- package/dist/core/errors.d.ts +6 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +9 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/guards/oauth.guard.d.ts +13 -6
- package/dist/core/guards/oauth.guard.d.ts.map +1 -1
- package/dist/core/guards/oauth.guard.js +90 -1
- package/dist/core/guards/oauth.guard.js.map +1 -1
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/oauth-module.d.ts +5 -0
- package/dist/core/oauth-module.d.ts.map +1 -1
- package/dist/core/oauth-module.js +15 -6
- package/dist/core/oauth-module.js.map +1 -1
- package/dist/core/protocol/adapter.d.ts +97 -0
- package/dist/core/protocol/adapter.d.ts.map +1 -0
- package/dist/core/protocol/adapter.js +21 -0
- package/dist/core/protocol/adapter.js.map +1 -0
- package/dist/core/protocol/features/cache-hints.d.ts +64 -0
- package/dist/core/protocol/features/cache-hints.d.ts.map +1 -0
- package/dist/core/protocol/features/cache-hints.js +56 -0
- package/dist/core/protocol/features/cache-hints.js.map +1 -0
- package/dist/core/protocol/features/errors.d.ts +34 -0
- package/dist/core/protocol/features/errors.d.ts.map +1 -0
- package/dist/core/protocol/features/errors.js +48 -0
- package/dist/core/protocol/features/errors.js.map +1 -0
- package/dist/core/protocol/features/extensions.d.ts +39 -0
- package/dist/core/protocol/features/extensions.d.ts.map +1 -0
- package/dist/core/protocol/features/extensions.js +42 -0
- package/dist/core/protocol/features/extensions.js.map +1 -0
- package/dist/core/protocol/features/mrtr.d.ts +75 -0
- package/dist/core/protocol/features/mrtr.d.ts.map +1 -0
- package/dist/core/protocol/features/mrtr.js +66 -0
- package/dist/core/protocol/features/mrtr.js.map +1 -0
- package/dist/core/protocol/features/schema.d.ts +39 -0
- package/dist/core/protocol/features/schema.d.ts.map +1 -0
- package/dist/core/protocol/features/schema.js +96 -0
- package/dist/core/protocol/features/schema.js.map +1 -0
- package/dist/core/protocol/features/trace-context.d.ts +31 -0
- package/dist/core/protocol/features/trace-context.d.ts.map +1 -0
- package/dist/core/protocol/features/trace-context.js +45 -0
- package/dist/core/protocol/features/trace-context.js.map +1 -0
- package/dist/core/protocol/modern-v2.adapter.d.ts +92 -0
- package/dist/core/protocol/modern-v2.adapter.d.ts.map +1 -0
- package/dist/core/protocol/modern-v2.adapter.js +939 -0
- package/dist/core/protocol/modern-v2.adapter.js.map +1 -0
- package/dist/core/protocol/version.d.ts +72 -0
- package/dist/core/protocol/version.d.ts.map +1 -0
- package/dist/core/protocol/version.js +99 -0
- package/dist/core/protocol/version.js.map +1 -0
- package/dist/core/resource.d.ts +15 -0
- package/dist/core/resource.d.ts.map +1 -1
- package/dist/core/resource.js +13 -0
- package/dist/core/resource.js.map +1 -1
- package/dist/core/server.d.ts +50 -6
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +326 -90
- package/dist/core/server.js.map +1 -1
- package/dist/core/task.d.ts +126 -40
- package/dist/core/task.d.ts.map +1 -1
- package/dist/core/task.js +166 -56
- package/dist/core/task.js.map +1 -1
- package/dist/core/tool.d.ts +20 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +16 -2
- package/dist/core/tool.js.map +1 -1
- package/dist/core/transports/streamable-http.d.ts +35 -1
- package/dist/core/transports/streamable-http.d.ts.map +1 -1
- package/dist/core/transports/streamable-http.js +77 -12
- package/dist/core/transports/streamable-http.js.map +1 -1
- package/dist/core/types.d.ts +86 -0
- package/dist/core/types.d.ts.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modern (2026-07-28) protocol adapter.
|
|
3
|
+
*
|
|
4
|
+
* Binds the NitroStack registry to the official `@modelcontextprotocol/server`
|
|
5
|
+
* v2 engine:
|
|
6
|
+
*
|
|
7
|
+
* - HTTP: `createMcpHandler(() => buildServer())` wrapped with
|
|
8
|
+
* `toNodeHandler(...)` from `@modelcontextprotocol/node`, mounted on the
|
|
9
|
+
* Express app. Stateless per-request serving, `server/discover`, per-request
|
|
10
|
+
* `_meta` envelope, `Mcp-Method`/`Mcp-Name` headers, and cache hints are all
|
|
11
|
+
* provided by the SDK.
|
|
12
|
+
* - stdio: `serveStdio(() => buildServer())` from `@modelcontextprotocol/server/stdio`.
|
|
13
|
+
*
|
|
14
|
+
* The SDK stamps `_meta['io.modelcontextprotocol/serverInfo']` on every 2026
|
|
15
|
+
* response from the `Implementation` handed to `new McpServer(...)`.
|
|
16
|
+
*
|
|
17
|
+
* The v2 packages are loaded lazily (dynamic `import`) so a default (legacy)
|
|
18
|
+
* install never touches them and existing Jest suites never load v2. The SDK's
|
|
19
|
+
* types use minified re-exports, so this file types the SDK surface loosely at
|
|
20
|
+
* the boundary while keeping NitroStack's own objects strongly typed.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import { MODERN_PROTOCOL_VERSION } from './version.js';
|
|
25
|
+
import { resolveToolCacheHint, resolveResourceCacheHint } from './features/cache-hints.js';
|
|
26
|
+
import { convertToModernJsonSchema } from './features/schema.js';
|
|
27
|
+
import { extractTraceContext } from './features/trace-context.js';
|
|
28
|
+
import { buildExtensionsMap } from './features/extensions.js';
|
|
29
|
+
import { mapToJsonRpcError } from './features/errors.js';
|
|
30
|
+
import { isInputRequired } from './features/mrtr.js';
|
|
31
|
+
import { isMcpAppMode, isOpenAiMode } from '../app-mode.js';
|
|
32
|
+
import { TaskContext, TaskAugmentationRequiredError } from '../task.js';
|
|
33
|
+
/** Meta-key constants (SEP-2575 / SEP-414) used to read the request envelope. */
|
|
34
|
+
const META = {
|
|
35
|
+
PROTOCOL_VERSION: 'io.modelcontextprotocol/protocolVersion',
|
|
36
|
+
CLIENT_INFO: 'io.modelcontextprotocol/clientInfo',
|
|
37
|
+
CLIENT_CAPABILITIES: 'io.modelcontextprotocol/clientCapabilities',
|
|
38
|
+
};
|
|
39
|
+
export class ModernProtocolAdapter {
|
|
40
|
+
registry;
|
|
41
|
+
options;
|
|
42
|
+
era = 'modern';
|
|
43
|
+
handler;
|
|
44
|
+
stdioHandle;
|
|
45
|
+
serverSdkPromise;
|
|
46
|
+
taskManager;
|
|
47
|
+
constructor(registry, options = { legacyMode: 'reject' }) {
|
|
48
|
+
this.registry = registry;
|
|
49
|
+
this.options = options;
|
|
50
|
+
this.taskManager = options.taskManager;
|
|
51
|
+
}
|
|
52
|
+
loadServerSdk() {
|
|
53
|
+
if (!this.serverSdkPromise) {
|
|
54
|
+
this.serverSdkPromise = import('@modelcontextprotocol/server');
|
|
55
|
+
}
|
|
56
|
+
return this.serverSdkPromise;
|
|
57
|
+
}
|
|
58
|
+
// ==========================================================================
|
|
59
|
+
// Server construction (called per request by the SDK factory)
|
|
60
|
+
// ==========================================================================
|
|
61
|
+
async buildServer() {
|
|
62
|
+
const sdk = await this.loadServerSdk();
|
|
63
|
+
const config = this.registry.config;
|
|
64
|
+
const extensions = this.advertisedExtensions();
|
|
65
|
+
const serverOptions = {
|
|
66
|
+
cacheHints: this.buildServerCacheHints(),
|
|
67
|
+
};
|
|
68
|
+
if (Object.keys(extensions).length > 0) {
|
|
69
|
+
// Advertise the SEP-2133 extensions map on server/discover capabilities.
|
|
70
|
+
serverOptions.capabilities = { extensions };
|
|
71
|
+
}
|
|
72
|
+
const server = new sdk.McpServer({ name: config.name, version: config.version }, serverOptions);
|
|
73
|
+
await this.registerTools(server, sdk);
|
|
74
|
+
await this.registerResources(server, sdk);
|
|
75
|
+
await this.registerPrompts(server, sdk);
|
|
76
|
+
return server;
|
|
77
|
+
}
|
|
78
|
+
/** Server-level per-operation cache hints for list/discover results. */
|
|
79
|
+
buildServerCacheHints() {
|
|
80
|
+
// Conservative shared default for list surfaces; per-resource hints still win.
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
async registerTools(server, sdk) {
|
|
84
|
+
for (const tool of this.registry.getTools().values()) {
|
|
85
|
+
const inputSchema = await this.toModernSchema(tool.inputSchema, 'input', sdk);
|
|
86
|
+
const outputSchema = tool.outputSchema
|
|
87
|
+
? await this.toModernSchema(tool.outputSchema, 'output', sdk)
|
|
88
|
+
: undefined;
|
|
89
|
+
const config = {
|
|
90
|
+
description: tool.description,
|
|
91
|
+
inputSchema,
|
|
92
|
+
};
|
|
93
|
+
if (tool.title)
|
|
94
|
+
config.title = tool.title;
|
|
95
|
+
if (outputSchema)
|
|
96
|
+
config.outputSchema = outputSchema;
|
|
97
|
+
if (tool.annotations)
|
|
98
|
+
config.annotations = tool.annotations;
|
|
99
|
+
const meta = {};
|
|
100
|
+
const cacheHint = resolveToolCacheHint(tool);
|
|
101
|
+
if (cacheHint)
|
|
102
|
+
meta['io.modelcontextprotocol/cacheHint'] = cacheHint;
|
|
103
|
+
if (tool.hasComponent && tool.hasComponent()) {
|
|
104
|
+
const component = tool.getComponent();
|
|
105
|
+
const resourceUri = component.getResourceUri();
|
|
106
|
+
const componentMeta = component.getResourceMetadata();
|
|
107
|
+
meta['ui/template'] = resourceUri;
|
|
108
|
+
meta['openai/outputTemplate'] = resourceUri;
|
|
109
|
+
meta['ui'] = { resourceUri };
|
|
110
|
+
if (componentMeta) {
|
|
111
|
+
if (componentMeta['openai/widgetCSP'] !== undefined) {
|
|
112
|
+
meta['openai/widgetCSP'] = componentMeta['openai/widgetCSP'];
|
|
113
|
+
}
|
|
114
|
+
if (componentMeta['openai/widgetDescription'] !== undefined) {
|
|
115
|
+
meta['openai/widgetDescription'] = componentMeta['openai/widgetDescription'];
|
|
116
|
+
}
|
|
117
|
+
if (componentMeta['openai/widgetPrefersBorder'] !== undefined) {
|
|
118
|
+
meta['openai/widgetPrefersBorder'] = componentMeta['openai/widgetPrefersBorder'];
|
|
119
|
+
}
|
|
120
|
+
if (componentMeta['openai/widgetDomain'] !== undefined) {
|
|
121
|
+
meta['openai/widgetDomain'] = componentMeta['openai/widgetDomain'];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (tool.widget?.route || tool.outputTemplate) {
|
|
126
|
+
const route = tool.widget?.route || tool.outputTemplate;
|
|
127
|
+
const normalized = route?.startsWith('/') ? route : `/${route}`;
|
|
128
|
+
const resourceUri = `/widgets${normalized}`;
|
|
129
|
+
meta['ui/template'] = resourceUri;
|
|
130
|
+
meta['openai/outputTemplate'] = resourceUri;
|
|
131
|
+
meta['ui'] = { resourceUri };
|
|
132
|
+
}
|
|
133
|
+
if (tool.examples) {
|
|
134
|
+
meta['tool/examples'] = tool.examples;
|
|
135
|
+
}
|
|
136
|
+
if (tool.isInitial) {
|
|
137
|
+
meta['tool/initial'] = true;
|
|
138
|
+
}
|
|
139
|
+
if (Object.keys(meta).length > 0) {
|
|
140
|
+
config._meta = meta;
|
|
141
|
+
}
|
|
142
|
+
server.registerTool(tool.name, config, async (args, ctx) => this.runTool(tool, args, ctx, sdk));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async registerResources(server, sdk) {
|
|
146
|
+
// Static resources and template resources both flow through registerResource.
|
|
147
|
+
const templateResources = this.registry.getTemplateResources();
|
|
148
|
+
for (const resource of this.registry.getResources().values()) {
|
|
149
|
+
// A template resource is registered via its template URI, not the static map.
|
|
150
|
+
const isTemplate = resource.uri.includes('{') && resource.uri.includes('}');
|
|
151
|
+
if (isTemplate)
|
|
152
|
+
continue;
|
|
153
|
+
const cacheHint = resolveResourceCacheHint(resource);
|
|
154
|
+
const config = {
|
|
155
|
+
description: resource.description,
|
|
156
|
+
mimeType: resource.mimeType,
|
|
157
|
+
};
|
|
158
|
+
if (resource.title)
|
|
159
|
+
config.title = resource.title;
|
|
160
|
+
if (cacheHint)
|
|
161
|
+
config.cacheHint = cacheHint;
|
|
162
|
+
server.registerResource(resource.name, resource.uri, config, async (uri) => this.readResource(String(uri?.href ?? uri), resource, sdk));
|
|
163
|
+
}
|
|
164
|
+
// Template resources keyed by uri template.
|
|
165
|
+
for (const [uriTemplate, resource] of templateResources.entries()) {
|
|
166
|
+
const cacheHint = resolveResourceCacheHint(resource);
|
|
167
|
+
const config = {
|
|
168
|
+
description: resource.description,
|
|
169
|
+
mimeType: resource.mimeType,
|
|
170
|
+
};
|
|
171
|
+
if (resource.title)
|
|
172
|
+
config.title = resource.title;
|
|
173
|
+
if (cacheHint)
|
|
174
|
+
config.cacheHint = cacheHint;
|
|
175
|
+
try {
|
|
176
|
+
const ResourceTemplateCtor = sdk.ResourceTemplate;
|
|
177
|
+
const template = ResourceTemplateCtor
|
|
178
|
+
? new ResourceTemplateCtor(uriTemplate, { list: undefined })
|
|
179
|
+
: uriTemplate;
|
|
180
|
+
server.registerResource(resource.name, template, config, async (uri) => this.readResource(String(uri?.href ?? uri), resource, sdk));
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
this.registry.logger.warn('Failed to register modern resource template', {
|
|
184
|
+
uriTemplate,
|
|
185
|
+
error: err instanceof Error ? err.message : String(err),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Modern SDK v2 strictly validates URIs using `new URL(uri)`. To support custom
|
|
190
|
+
// or relative URI schemes such as `/widgets/*` used by NitroStudio and MCP Apps,
|
|
191
|
+
// attach a fallback resources/read handler on the underlying MCP server only when
|
|
192
|
+
// the server actually has resources registered (otherwise SDK throws capability error).
|
|
193
|
+
const rawResources = this.registry.getResources();
|
|
194
|
+
if ((rawResources.size > 0 || templateResources.size > 0) && server.server && typeof server.server.setRequestHandler === 'function') {
|
|
195
|
+
server.server.setRequestHandler('resources/read', async (request, ctx) => {
|
|
196
|
+
const reqUri = String(request?.params?.uri ?? '');
|
|
197
|
+
// 1. Check exact match in registered resources (including path-based URIs like /widgets/...)
|
|
198
|
+
const matchingResource = rawResources.get(reqUri);
|
|
199
|
+
if (matchingResource) {
|
|
200
|
+
const resResult = await this.readResource(reqUri, matchingResource, sdk);
|
|
201
|
+
const cacheHint = resolveResourceCacheHint(matchingResource);
|
|
202
|
+
if (cacheHint) {
|
|
203
|
+
return { ...resResult, cacheHint };
|
|
204
|
+
}
|
|
205
|
+
return resResult;
|
|
206
|
+
}
|
|
207
|
+
// 2. Try URL parsing for standard schemes (mcp://, ui://, http://)
|
|
208
|
+
let parsedUrl;
|
|
209
|
+
try {
|
|
210
|
+
parsedUrl = new URL(reqUri);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// If not parseable as standard URL, check if any resource matches
|
|
214
|
+
for (const [uri, res] of rawResources.entries()) {
|
|
215
|
+
if (uri === reqUri || uri.endsWith(reqUri) || reqUri.endsWith(uri)) {
|
|
216
|
+
return this.readResource(reqUri, res, sdk);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (parsedUrl) {
|
|
221
|
+
const registered = server._registeredResources?.[parsedUrl.toString()] ||
|
|
222
|
+
rawResources.get(parsedUrl.toString());
|
|
223
|
+
if (registered) {
|
|
224
|
+
if (typeof registered.readCallback === 'function') {
|
|
225
|
+
return registered.readCallback(parsedUrl, ctx);
|
|
226
|
+
}
|
|
227
|
+
return this.readResource(reqUri, registered, sdk);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// 3. Check template resources
|
|
231
|
+
if (server._registeredResourceTemplates) {
|
|
232
|
+
for (const template of Object.values(server._registeredResourceTemplates)) {
|
|
233
|
+
const variables = template.resourceTemplate?.uriTemplate?.match?.(reqUri);
|
|
234
|
+
if (variables) {
|
|
235
|
+
return template.readCallback(reqUri, variables, ctx);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
throw this.toSdkError(new Error(`Resource not found: ${reqUri}`), sdk);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async registerPrompts(server, sdk) {
|
|
244
|
+
for (const prompt of this.registry.getPrompts().values()) {
|
|
245
|
+
const args = prompt.arguments;
|
|
246
|
+
const config = { description: prompt.description };
|
|
247
|
+
if (prompt.title)
|
|
248
|
+
config.title = prompt.title;
|
|
249
|
+
if (args && args.length > 0) {
|
|
250
|
+
const properties = {};
|
|
251
|
+
const required = [];
|
|
252
|
+
for (const arg of args) {
|
|
253
|
+
properties[arg.name] = {
|
|
254
|
+
type: 'string',
|
|
255
|
+
description: arg.description,
|
|
256
|
+
};
|
|
257
|
+
if (arg.required) {
|
|
258
|
+
required.push(arg.name);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const rawSchema = {
|
|
262
|
+
type: 'object',
|
|
263
|
+
properties,
|
|
264
|
+
};
|
|
265
|
+
if (required.length > 0) {
|
|
266
|
+
rawSchema.required = required;
|
|
267
|
+
}
|
|
268
|
+
config.argsSchema = sdk.fromJsonSchema ? sdk.fromJsonSchema(rawSchema) : rawSchema;
|
|
269
|
+
server.registerPrompt(prompt.name, config, async (promptArgs, ctx) => {
|
|
270
|
+
const context = this.buildContext(ctx, { toolName: prompt.name });
|
|
271
|
+
const messages = await prompt.execute(promptArgs || {}, context);
|
|
272
|
+
return {
|
|
273
|
+
description: prompt.description,
|
|
274
|
+
messages: messages.map((m) => ({
|
|
275
|
+
role: m.role,
|
|
276
|
+
content: { type: 'text', text: m.content },
|
|
277
|
+
})),
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
server.registerPrompt(prompt.name, config, async (ctx) => {
|
|
283
|
+
const context = this.buildContext(ctx, { toolName: prompt.name });
|
|
284
|
+
const messages = await prompt.execute({}, context);
|
|
285
|
+
return {
|
|
286
|
+
description: prompt.description,
|
|
287
|
+
messages: messages.map((m) => ({
|
|
288
|
+
role: m.role,
|
|
289
|
+
content: { type: 'text', text: m.content },
|
|
290
|
+
})),
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// ==========================================================================
|
|
297
|
+
// Handlers
|
|
298
|
+
// ==========================================================================
|
|
299
|
+
async runTool(tool, args, ctx, sdk) {
|
|
300
|
+
const context = this.buildContext(ctx, { toolName: tool.name });
|
|
301
|
+
const isTaskAugmented = ctx?.task !== undefined || ctx?.mcpReq?.params?.task !== undefined;
|
|
302
|
+
// Enforce tool-level task support negotiation
|
|
303
|
+
if (tool.taskSupport === 'required' && !isTaskAugmented) {
|
|
304
|
+
throw this.toSdkError(new TaskAugmentationRequiredError(), sdk);
|
|
305
|
+
}
|
|
306
|
+
if (tool.taskSupport === 'forbidden' && isTaskAugmented) {
|
|
307
|
+
throw this.toSdkError({
|
|
308
|
+
code: -32601,
|
|
309
|
+
message: `Tool '${tool.name}' does not support task augmentation`,
|
|
310
|
+
}, sdk);
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const argsRecord = (args || {});
|
|
314
|
+
const { _meta: metaFromArgs, ...toolArgs } = argsRecord;
|
|
315
|
+
if (metaFromArgs && typeof metaFromArgs === 'object') {
|
|
316
|
+
context.metadata = context.metadata || {};
|
|
317
|
+
const argMeta = metaFromArgs;
|
|
318
|
+
const rawAuth = argMeta.authorization || argMeta.Authorization;
|
|
319
|
+
if (rawAuth) {
|
|
320
|
+
context.metadata.authorization = rawAuth;
|
|
321
|
+
context.metadata.Authorization = rawAuth;
|
|
322
|
+
}
|
|
323
|
+
const rawToken = argMeta.token || argMeta._oauth || argMeta.jwtToken;
|
|
324
|
+
if (rawToken) {
|
|
325
|
+
context.metadata.token = rawToken;
|
|
326
|
+
context.metadata._oauth = rawToken;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const result = await tool.execute(toolArgs, context);
|
|
330
|
+
// MRTR: a handler may pause and ask for more input.
|
|
331
|
+
if (isInputRequired(result)) {
|
|
332
|
+
return sdk.inputRequired({
|
|
333
|
+
inputRequests: result.inputRequests,
|
|
334
|
+
requestState: result.requestState,
|
|
335
|
+
message: result.message,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
const response = {
|
|
339
|
+
content: [
|
|
340
|
+
{
|
|
341
|
+
type: 'text',
|
|
342
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
};
|
|
346
|
+
if (tool.hasComponent()) {
|
|
347
|
+
const component = tool.getComponent();
|
|
348
|
+
response.structuredContent = (await component.transformData(result, context));
|
|
349
|
+
const widgetMeta = ((await component.getWidgetMeta(result, context)) || {});
|
|
350
|
+
response._meta = widgetMeta;
|
|
351
|
+
if (isMcpAppMode()) {
|
|
352
|
+
if (!widgetMeta.ui) {
|
|
353
|
+
widgetMeta.ui = { resourceUri: component.getResourceUri() };
|
|
354
|
+
}
|
|
355
|
+
else if (typeof widgetMeta.ui === 'object' && !('resourceUri' in widgetMeta.ui)) {
|
|
356
|
+
widgetMeta.ui.resourceUri = component.getResourceUri();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (isOpenAiMode() && !widgetMeta['openai/outputTemplate']) {
|
|
360
|
+
widgetMeta['openai/outputTemplate'] = component.getResourceUri();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return response;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
const mapped = mapToJsonRpcError(error);
|
|
367
|
+
context.logger.error(`Tool execution failed (modern): ${tool.name}`, { error: mapped.message });
|
|
368
|
+
// Tool failures are returned as isError results, not JSON-RPC errors.
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: 'text', text: `Error: ${mapped.message}` }],
|
|
371
|
+
isError: true,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Translate a NitroStack/handler error into an SDK error carrying the right
|
|
377
|
+
* JSON-RPC code for the modern era. Prefers the v2 SDK error classes (so the
|
|
378
|
+
* SDK serializes `error.code` correctly); falls back to a plain `Error` with
|
|
379
|
+
* a numeric `code` property the SDK also recognizes.
|
|
380
|
+
*/
|
|
381
|
+
toSdkError(error, sdk) {
|
|
382
|
+
const mapped = mapToJsonRpcError(error);
|
|
383
|
+
// -32602 is the SEP-2164 code for a missing resource and for invalid params;
|
|
384
|
+
// InvalidParamsError takes a message and serializes with the right code.
|
|
385
|
+
const Ctor = mapped.code === -32602
|
|
386
|
+
? sdk.InvalidParamsError || sdk.ResourceNotFoundError
|
|
387
|
+
: mapped.code === -32601
|
|
388
|
+
? sdk.MethodNotFoundError
|
|
389
|
+
: sdk.InternalError;
|
|
390
|
+
if (Ctor) {
|
|
391
|
+
try {
|
|
392
|
+
return new Ctor(mapped.message, mapped.data);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
/* fall through */
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const err = new Error(mapped.message);
|
|
399
|
+
err.code = mapped.code;
|
|
400
|
+
if (mapped.data !== undefined)
|
|
401
|
+
err.data = mapped.data;
|
|
402
|
+
return err;
|
|
403
|
+
}
|
|
404
|
+
async readResource(uri, resource, sdk) {
|
|
405
|
+
const context = this.registry.createExecutionContext({
|
|
406
|
+
extra: { protocolVersion: MODERN_PROTOCOL_VERSION },
|
|
407
|
+
});
|
|
408
|
+
let content;
|
|
409
|
+
try {
|
|
410
|
+
content = await resource.fetch(context, uri);
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
// SEP-2164: surface the correct JSON-RPC code on the modern path
|
|
414
|
+
// (ResourceNotFound → -32602 Invalid Params, not the 2025-era -32002).
|
|
415
|
+
throw this.toSdkError(error, sdk);
|
|
416
|
+
}
|
|
417
|
+
const mimeType = resource.mimeType || 'text/plain';
|
|
418
|
+
let entry;
|
|
419
|
+
switch (content.type) {
|
|
420
|
+
case 'text':
|
|
421
|
+
entry = { uri, mimeType, text: content.data };
|
|
422
|
+
break;
|
|
423
|
+
case 'binary':
|
|
424
|
+
entry = { uri, mimeType: resource.mimeType || 'application/octet-stream', blob: content.data.toString('base64') };
|
|
425
|
+
break;
|
|
426
|
+
case 'json':
|
|
427
|
+
entry = { uri, mimeType: resource.mimeType || 'application/json', text: JSON.stringify(content.data, null, 2) };
|
|
428
|
+
break;
|
|
429
|
+
default:
|
|
430
|
+
entry = { uri, mimeType: resource.mimeType || 'application/json', text: JSON.stringify(content, null, 2) };
|
|
431
|
+
}
|
|
432
|
+
const widgetMeta = resource.getWidgetReadMeta?.();
|
|
433
|
+
if (widgetMeta && Object.keys(widgetMeta).length > 0) {
|
|
434
|
+
entry._meta = widgetMeta;
|
|
435
|
+
}
|
|
436
|
+
return { contents: [entry] };
|
|
437
|
+
}
|
|
438
|
+
// ==========================================================================
|
|
439
|
+
// Context bridging (per-request envelope → ExecutionContext)
|
|
440
|
+
// ==========================================================================
|
|
441
|
+
buildContext(ctx, opts) {
|
|
442
|
+
const mcpReq = ctx?.mcpReq ?? {};
|
|
443
|
+
const meta = mcpReq._meta ?? {};
|
|
444
|
+
const envelope = mcpReq.envelope ?? {};
|
|
445
|
+
const readEnvelope = (bareKey, prefixedKey) => envelope[prefixedKey] ?? envelope[bareKey] ?? meta[prefixedKey] ?? meta[bareKey];
|
|
446
|
+
const protocolVersion = readEnvelope('protocolVersion', META.PROTOCOL_VERSION) ?? MODERN_PROTOCOL_VERSION;
|
|
447
|
+
const clientInfo = readEnvelope('clientInfo', META.CLIENT_INFO);
|
|
448
|
+
const clientCapabilities = readEnvelope('clientCapabilities', META.CLIENT_CAPABILITIES);
|
|
449
|
+
let requestState;
|
|
450
|
+
try {
|
|
451
|
+
requestState = typeof mcpReq.requestState === 'function' ? mcpReq.requestState() : mcpReq.requestState;
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
requestState = undefined;
|
|
455
|
+
}
|
|
456
|
+
const trace = extractTraceContext({ ...meta, ...envelope });
|
|
457
|
+
const inputResponses = mcpReq.inputResponses;
|
|
458
|
+
const authInfo = readEnvelope('auth', 'io.modelcontextprotocol/auth') ??
|
|
459
|
+
ctx?.http?.authInfo ??
|
|
460
|
+
mcpReq?.http?.authInfo ??
|
|
461
|
+
ctx?.authInfo ??
|
|
462
|
+
ctx?.auth;
|
|
463
|
+
const rawHeaders = {};
|
|
464
|
+
const reqHeaders = ctx?.http?.req?.headers ||
|
|
465
|
+
ctx?.http?.headers ||
|
|
466
|
+
ctx?.headers ||
|
|
467
|
+
mcpReq?.headers ||
|
|
468
|
+
ctx?.req?.headers;
|
|
469
|
+
if (reqHeaders) {
|
|
470
|
+
if (typeof reqHeaders.forEach === 'function') {
|
|
471
|
+
reqHeaders.forEach((val, key) => {
|
|
472
|
+
rawHeaders[key.toLowerCase()] = val;
|
|
473
|
+
rawHeaders[key] = val;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
else if (typeof reqHeaders.entries === 'function') {
|
|
477
|
+
for (const [k, v] of reqHeaders.entries()) {
|
|
478
|
+
rawHeaders[k.toLowerCase()] = v;
|
|
479
|
+
rawHeaders[k] = v;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else if (typeof reqHeaders === 'object') {
|
|
483
|
+
for (const [k, v] of Object.entries(reqHeaders)) {
|
|
484
|
+
rawHeaders[k.toLowerCase()] = String(v);
|
|
485
|
+
rawHeaders[k] = String(v);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (ctx?.http?.req?.headers?.get && typeof ctx.http.req.headers.get === 'function') {
|
|
490
|
+
const authHeader = ctx.http.req.headers.get('authorization') || ctx.http.req.headers.get('Authorization');
|
|
491
|
+
if (authHeader) {
|
|
492
|
+
rawHeaders.authorization = authHeader;
|
|
493
|
+
rawHeaders.Authorization = authHeader;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const rawAuth = rawHeaders.authorization ||
|
|
497
|
+
rawHeaders.Authorization ||
|
|
498
|
+
(authInfo?.token ? `Bearer ${authInfo.token}` : undefined) ||
|
|
499
|
+
meta?.authorization ||
|
|
500
|
+
meta?.Authorization;
|
|
501
|
+
let rawToken = authInfo?.token ||
|
|
502
|
+
meta?.token ||
|
|
503
|
+
meta?._oauth ||
|
|
504
|
+
meta?.jwtToken ||
|
|
505
|
+
meta?._meta?.jwtToken ||
|
|
506
|
+
meta?._meta?.token;
|
|
507
|
+
if (!rawToken && rawAuth && typeof rawAuth === 'string' && rawAuth.startsWith('Bearer ')) {
|
|
508
|
+
rawToken = rawAuth.substring(7).trim();
|
|
509
|
+
}
|
|
510
|
+
const metadata = {
|
|
511
|
+
...rawHeaders,
|
|
512
|
+
...meta,
|
|
513
|
+
};
|
|
514
|
+
if (rawAuth) {
|
|
515
|
+
metadata.authorization = rawAuth;
|
|
516
|
+
metadata.Authorization = rawAuth;
|
|
517
|
+
}
|
|
518
|
+
if (rawToken) {
|
|
519
|
+
metadata.token = rawToken;
|
|
520
|
+
metadata._oauth = rawToken;
|
|
521
|
+
metadata.jwtToken = rawToken;
|
|
522
|
+
}
|
|
523
|
+
return this.registry.createExecutionContext({
|
|
524
|
+
toolName: opts.toolName,
|
|
525
|
+
metadata,
|
|
526
|
+
extra: {
|
|
527
|
+
protocolVersion,
|
|
528
|
+
clientInfo,
|
|
529
|
+
clientCapabilities,
|
|
530
|
+
requestState,
|
|
531
|
+
inputResponses,
|
|
532
|
+
trace,
|
|
533
|
+
auth: authInfo ? this.mapAuthInfo(authInfo) : undefined,
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
mapAuthInfo(authInfo) {
|
|
538
|
+
const user = authInfo.user || authInfo.tokenPayload || authInfo;
|
|
539
|
+
return {
|
|
540
|
+
subject: authInfo.subject ?? user.sub ?? authInfo.clientId ?? authInfo.client_id ?? authInfo.extra?.sub,
|
|
541
|
+
clientId: authInfo.clientId ?? authInfo.client_id ?? user.client_id,
|
|
542
|
+
scopes: authInfo.scopes ?? (typeof authInfo.scope === 'string' ? authInfo.scope.split(' ') : authInfo.scope) ?? [],
|
|
543
|
+
claims: authInfo.claims ?? authInfo.extra ?? user,
|
|
544
|
+
tokenPayload: authInfo.tokenPayload ?? user,
|
|
545
|
+
exp: authInfo.exp ?? user.exp,
|
|
546
|
+
iat: authInfo.iat ?? user.iat,
|
|
547
|
+
iss: authInfo.iss ?? user.iss,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async toModernSchema(schema, root, sdk) {
|
|
551
|
+
// NitroStack ships Zod v3, which the v2 SDK cannot ingest directly (it only
|
|
552
|
+
// accepts Zod >= 4.2.0 or a `fromJsonSchema(...)`-wrapped JSON Schema). So
|
|
553
|
+
// always lift the schema (Zod or pre-built JSON) to JSON Schema 2020-12
|
|
554
|
+
// (SEP-2106, preserving composition/$defs) and wrap it with `fromJsonSchema`
|
|
555
|
+
// when available so the SDK validates against it.
|
|
556
|
+
const json = await convertToModernJsonSchema(schema, { root });
|
|
557
|
+
if (sdk.fromJsonSchema) {
|
|
558
|
+
try {
|
|
559
|
+
return sdk.fromJsonSchema(json);
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
/* fall through to raw json */
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return json;
|
|
566
|
+
}
|
|
567
|
+
// ==========================================================================
|
|
568
|
+
// Transport wiring
|
|
569
|
+
// ==========================================================================
|
|
570
|
+
/**
|
|
571
|
+
* Build the stateless per-request Node HTTP handler for the modern engine.
|
|
572
|
+
* Returned so an existing Express host (NitroStack's `StreamableHttpTransport`)
|
|
573
|
+
* can route its `/mcp` endpoint to it, keeping one Express app and one set of
|
|
574
|
+
* OAuth/docs routes.
|
|
575
|
+
*/
|
|
576
|
+
/**
|
|
577
|
+
* Build (once) the web-standard v2 handler `{ fetch, close, notify, bus }`
|
|
578
|
+
* returned by `createMcpHandler`. Idempotent: repeated calls reuse the same
|
|
579
|
+
* handler so a single notify bus backs `subscriptions/listen`.
|
|
580
|
+
*/
|
|
581
|
+
async getHttpHandler() {
|
|
582
|
+
if (!this.handler) {
|
|
583
|
+
const sdk = await this.loadServerSdk();
|
|
584
|
+
const rawHandler = sdk.createMcpHandler(() => this.buildServer(), {
|
|
585
|
+
legacy: this.options.legacyMode,
|
|
586
|
+
onerror: (error) => {
|
|
587
|
+
this.registry.logger.error('Modern MCP handler error', { error: error.message });
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
const rawFetch = rawHandler.fetch;
|
|
591
|
+
this.handler = {
|
|
592
|
+
...rawHandler,
|
|
593
|
+
fetch: async (request, requestOptions) => {
|
|
594
|
+
if (request.headers.get('mcp-method') === 'ping') {
|
|
595
|
+
try {
|
|
596
|
+
const clone = request.clone();
|
|
597
|
+
const json = (await clone.json());
|
|
598
|
+
return new Response(JSON.stringify({ jsonrpc: '2.0', id: json?.id ?? null, result: {} }), {
|
|
599
|
+
status: 200,
|
|
600
|
+
headers: { 'Content-Type': 'application/json' },
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
return new Response(JSON.stringify({ jsonrpc: '2.0', id: null, result: {} }), {
|
|
605
|
+
status: 200,
|
|
606
|
+
headers: { 'Content-Type': 'application/json' },
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Pre-dispatch wire interceptor for tasks methods and task-augmented tools/call
|
|
611
|
+
if (this.taskManager) {
|
|
612
|
+
try {
|
|
613
|
+
const clone = request.clone();
|
|
614
|
+
const body = (await clone.json());
|
|
615
|
+
const taskResponse = await this.handleTaskPreDispatch(body, request);
|
|
616
|
+
if (taskResponse) {
|
|
617
|
+
return new Response(JSON.stringify(taskResponse), {
|
|
618
|
+
status: 200,
|
|
619
|
+
headers: { 'Content-Type': 'application/json' },
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
// Not JSON or cannot intercept; let rawFetch handle
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return rawFetch(request, requestOptions);
|
|
628
|
+
},
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return this.handler;
|
|
632
|
+
}
|
|
633
|
+
extractAccessContext(req, parsedBody) {
|
|
634
|
+
const reqAny = req;
|
|
635
|
+
const auth = reqAny?.auth || reqAny?.user;
|
|
636
|
+
const userId = auth?.sub || auth?.userId || auth?.id;
|
|
637
|
+
const tenantId = auth?.tenantId || auth?.orgId;
|
|
638
|
+
const sessionId = reqAny?.headers?.['mcp-session-id'] || reqAny?.get?.('mcp-session-id');
|
|
639
|
+
if (!userId && !tenantId && !sessionId) {
|
|
640
|
+
return undefined;
|
|
641
|
+
}
|
|
642
|
+
return { userId, tenantId, sessionId };
|
|
643
|
+
}
|
|
644
|
+
async handleTaskPreDispatch(body, req) {
|
|
645
|
+
if (!this.taskManager || !body || typeof body !== 'object')
|
|
646
|
+
return null;
|
|
647
|
+
const { method, params, id } = body;
|
|
648
|
+
const accessContext = this.extractAccessContext(req, body);
|
|
649
|
+
// 1. tasks/get
|
|
650
|
+
if (method === 'tasks/get') {
|
|
651
|
+
const taskId = params?.taskId;
|
|
652
|
+
if (!taskId) {
|
|
653
|
+
return { jsonrpc: '2.0', id: id ?? null, error: { code: -32602, message: 'Invalid params: taskId is required' } };
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const entry = this.taskManager.getEntry(taskId, accessContext);
|
|
657
|
+
const resultPayload = { ...entry.data };
|
|
658
|
+
if (entry.data.status === 'completed' && entry.result !== undefined) {
|
|
659
|
+
resultPayload.result = entry.result;
|
|
660
|
+
}
|
|
661
|
+
if (entry.data.status === 'failed' && entry.error !== undefined) {
|
|
662
|
+
resultPayload.error = entry.error;
|
|
663
|
+
}
|
|
664
|
+
return { jsonrpc: '2.0', id: id ?? null, result: resultPayload };
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
return { jsonrpc: '2.0', id: id ?? null, error: { code: err.code || -32602, message: err.message || 'Task not found' } };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
// 2. tasks/cancel
|
|
671
|
+
if (method === 'tasks/cancel') {
|
|
672
|
+
const taskId = params?.taskId;
|
|
673
|
+
if (!taskId) {
|
|
674
|
+
return { jsonrpc: '2.0', id: id ?? null, error: { code: -32602, message: 'Invalid params: taskId is required' } };
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
const taskData = this.taskManager.cancelTask(taskId, accessContext);
|
|
678
|
+
return { jsonrpc: '2.0', id: id ?? null, result: taskData };
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
return { jsonrpc: '2.0', id: id ?? null, error: { code: err.code || -32602, message: err.message || 'Task not found' } };
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// 3. tasks/update
|
|
685
|
+
if (method === 'tasks/update') {
|
|
686
|
+
const taskId = params?.taskId;
|
|
687
|
+
if (!taskId) {
|
|
688
|
+
return { jsonrpc: '2.0', id: id ?? null, error: { code: -32602, message: 'Invalid params: taskId is required' } };
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const taskData = this.taskManager.updateStatus(taskId, params.status || 'working', params.statusMessage, accessContext);
|
|
692
|
+
return { jsonrpc: '2.0', id: id ?? null, result: taskData };
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
return { jsonrpc: '2.0', id: id ?? null, error: { code: err.code || -32602, message: err.message || 'Failed to update task' } };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
// 4. tasks/result (legacy 2025-06-18 only; rejected in modern 2026-07-28)
|
|
699
|
+
if (method === 'tasks/result') {
|
|
700
|
+
return {
|
|
701
|
+
jsonrpc: '2.0',
|
|
702
|
+
id: id ?? null,
|
|
703
|
+
error: {
|
|
704
|
+
code: -32601,
|
|
705
|
+
message: "Method 'tasks/result' is not supported in MCP 2026-07-28; use 'tasks/get' with embedded results.",
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
// 5. tasks/list (removed in modern 2026-07-28)
|
|
710
|
+
if (method === 'tasks/list') {
|
|
711
|
+
return {
|
|
712
|
+
jsonrpc: '2.0',
|
|
713
|
+
id: id ?? null,
|
|
714
|
+
error: {
|
|
715
|
+
code: -32601,
|
|
716
|
+
message: "Method 'tasks/list' is not supported in modern stateless MCP 2026-07-28.",
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
// 6. tools/call with task augmentation OR mandatory task support check
|
|
721
|
+
if (method === 'tools/call' && params) {
|
|
722
|
+
const toolName = params.name;
|
|
723
|
+
const tool = this.registry.getTools().get(toolName);
|
|
724
|
+
if (!tool)
|
|
725
|
+
return null; // Let standard flow handle tool not found
|
|
726
|
+
const isTaskAugmented = params.task !== undefined;
|
|
727
|
+
// Enforcement: if taskSupport === 'required' and not task-augmented
|
|
728
|
+
if (!isTaskAugmented && tool.taskSupport === 'required') {
|
|
729
|
+
return {
|
|
730
|
+
jsonrpc: '2.0',
|
|
731
|
+
id: id ?? null,
|
|
732
|
+
error: { code: -32600, message: `Task augmentation required for tools/call requests on tool '${toolName}'` },
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
// If task-augmented:
|
|
736
|
+
if (isTaskAugmented) {
|
|
737
|
+
if (tool.taskSupport === 'forbidden') {
|
|
738
|
+
return {
|
|
739
|
+
jsonrpc: '2.0',
|
|
740
|
+
id: id ?? null,
|
|
741
|
+
error: { code: -32601, message: `Tool '${toolName}' does not support task augmentation` },
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
const taskData = this.taskManager.createTask(params.task, toolName, accessContext);
|
|
745
|
+
const taskId = taskData.taskId;
|
|
746
|
+
const taskContext = new TaskContext(this.taskManager, taskId);
|
|
747
|
+
const executionContext = this.registry.createExecutionContext({
|
|
748
|
+
toolName,
|
|
749
|
+
extra: {
|
|
750
|
+
task: taskContext,
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
executionContext.task = taskContext;
|
|
754
|
+
const tm = this.taskManager;
|
|
755
|
+
// Run tool asynchronously in the background
|
|
756
|
+
Promise.resolve().then(async () => {
|
|
757
|
+
try {
|
|
758
|
+
const argsRecord = (params.arguments || {});
|
|
759
|
+
const { _meta: _, ...toolArgs } = argsRecord;
|
|
760
|
+
const toolResult = await tool.execute(toolArgs, executionContext);
|
|
761
|
+
if (tm.hasTask(taskId)) {
|
|
762
|
+
const current = tm.getTask(taskId);
|
|
763
|
+
if (current.status !== 'cancelled') {
|
|
764
|
+
tm.completeTask(taskId, toolResult, undefined, accessContext);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
if (tm.hasTask(taskId)) {
|
|
770
|
+
const current = tm.getTask(taskId);
|
|
771
|
+
if (current.status !== 'cancelled') {
|
|
772
|
+
tm.failTask(taskId, { code: err.code || -32603, message: err.message || String(err) }, undefined, accessContext);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
// Return CreateTaskResult immediately
|
|
778
|
+
return {
|
|
779
|
+
jsonrpc: '2.0',
|
|
780
|
+
id: id ?? null,
|
|
781
|
+
result: {
|
|
782
|
+
task: taskData,
|
|
783
|
+
resultType: 'task',
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
async createNodeHandler() {
|
|
791
|
+
const node = (await import('@modelcontextprotocol/node'));
|
|
792
|
+
const handler = await this.getHttpHandler();
|
|
793
|
+
const nodeHandler = node.toNodeHandler(handler);
|
|
794
|
+
return (req, res) => {
|
|
795
|
+
// Express bodyParser/json middleware may have already consumed the request
|
|
796
|
+
// stream and populated `req.body`. Pass `req.body` so `toWebRequest` uses
|
|
797
|
+
// the parsed body rather than reading an already-drained request stream.
|
|
798
|
+
const parsedBody = req.body !== undefined &&
|
|
799
|
+
req.body !== null &&
|
|
800
|
+
typeof req.body === 'object' &&
|
|
801
|
+
Object.keys(req.body).length > 0
|
|
802
|
+
? req.body
|
|
803
|
+
: undefined;
|
|
804
|
+
// Handle ping directly for studio heartbeat / health monitoring
|
|
805
|
+
if (parsedBody && parsedBody.method === 'ping') {
|
|
806
|
+
res.setHeader('Content-Type', 'application/json');
|
|
807
|
+
res.status(200).json({
|
|
808
|
+
jsonrpc: '2.0',
|
|
809
|
+
id: parsedBody.id ?? null,
|
|
810
|
+
result: {},
|
|
811
|
+
});
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
// Pre-dispatch wire interceptor for tasks methods and task-augmented tools/call
|
|
815
|
+
if (parsedBody && this.taskManager) {
|
|
816
|
+
const taskResponsePromise = this.handleTaskPreDispatch(parsedBody, req);
|
|
817
|
+
if (taskResponsePromise) {
|
|
818
|
+
Promise.resolve(taskResponsePromise).then((resp) => {
|
|
819
|
+
if (resp) {
|
|
820
|
+
res.setHeader('Content-Type', 'application/json');
|
|
821
|
+
res.status(200).json(resp);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
nodeHandler(req, res, parsedBody);
|
|
825
|
+
}
|
|
826
|
+
}).catch((err) => {
|
|
827
|
+
res.status(500).json({
|
|
828
|
+
jsonrpc: '2.0',
|
|
829
|
+
id: parsedBody.id ?? null,
|
|
830
|
+
error: { code: -32603, message: err instanceof Error ? err.message : String(err) },
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
Promise.resolve(nodeHandler(req, res, parsedBody)).catch((err) => {
|
|
837
|
+
this.registry.logger.error('Modern MCP request failed', {
|
|
838
|
+
error: err instanceof Error ? err.message : String(err),
|
|
839
|
+
});
|
|
840
|
+
if (!res.headersSent) {
|
|
841
|
+
res.status(500).json({
|
|
842
|
+
jsonrpc: '2.0',
|
|
843
|
+
error: { code: -32603, message: 'Internal error' },
|
|
844
|
+
id: null,
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
async attachHttp(app, options) {
|
|
851
|
+
const nodeHandler = await this.createNodeHandler();
|
|
852
|
+
const endpoint = options.endpoint || '/mcp';
|
|
853
|
+
if (options.enableCors !== false) {
|
|
854
|
+
app.use(endpoint, (req, res, next) => {
|
|
855
|
+
this.applyCorsHeaders(req, res);
|
|
856
|
+
if (req.method === 'OPTIONS') {
|
|
857
|
+
res.status(204).end();
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
next();
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
app.all(endpoint, nodeHandler);
|
|
864
|
+
this.registry.logger.info(`Modern MCP (${MODERN_PROTOCOL_VERSION}) mounted at ${endpoint}`);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* SEP-2243/SEP-2575 CORS: expose and allow the new required request headers
|
|
868
|
+
* (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`, and `Mcp-Param-*`).
|
|
869
|
+
*/
|
|
870
|
+
applyCorsHeaders(req, res) {
|
|
871
|
+
const origin = req.headers.origin;
|
|
872
|
+
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
873
|
+
res.setHeader('Vary', 'Origin');
|
|
874
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
875
|
+
res.setHeader('Access-Control-Allow-Headers', [
|
|
876
|
+
'Content-Type',
|
|
877
|
+
'Authorization',
|
|
878
|
+
'MCP-Protocol-Version',
|
|
879
|
+
'Mcp-Method',
|
|
880
|
+
'Mcp-Name',
|
|
881
|
+
'Mcp-Param-*',
|
|
882
|
+
'Last-Event-ID',
|
|
883
|
+
].join(', '));
|
|
884
|
+
res.setHeader('Access-Control-Expose-Headers', ['MCP-Protocol-Version', 'Mcp-Method', 'Mcp-Name'].join(', '));
|
|
885
|
+
}
|
|
886
|
+
async serveStdio() {
|
|
887
|
+
const stdio = (await import('@modelcontextprotocol/server/stdio'));
|
|
888
|
+
this.stdioHandle = stdio.serveStdio(() => this.buildServer());
|
|
889
|
+
this.registry.logger.info(`Modern MCP (${MODERN_PROTOCOL_VERSION}) serving over stdio`);
|
|
890
|
+
}
|
|
891
|
+
// ==========================================================================
|
|
892
|
+
// Notifications (subscriptions/listen bus)
|
|
893
|
+
// ==========================================================================
|
|
894
|
+
notifyToolsListChanged() {
|
|
895
|
+
this.handler?.notify?.toolsChanged?.();
|
|
896
|
+
}
|
|
897
|
+
notifyResourcesListChanged() {
|
|
898
|
+
this.handler?.notify?.resourcesChanged?.();
|
|
899
|
+
}
|
|
900
|
+
notifyPromptsListChanged() {
|
|
901
|
+
this.handler?.notify?.promptsChanged?.();
|
|
902
|
+
}
|
|
903
|
+
notifyResourceUpdated(uri) {
|
|
904
|
+
this.handler?.notify?.resourceUpdated?.(uri);
|
|
905
|
+
}
|
|
906
|
+
notifyTaskStatus(taskData) {
|
|
907
|
+
try {
|
|
908
|
+
this.handler?.notify?.custom?.('notifications/tasks/status', taskData);
|
|
909
|
+
this.handler?.bus?.emit?.('task_status', taskData);
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
/* ignore delivery error if no subscriber is active */
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
async close() {
|
|
916
|
+
try {
|
|
917
|
+
await this.handler?.close?.();
|
|
918
|
+
}
|
|
919
|
+
catch {
|
|
920
|
+
/* ignore */
|
|
921
|
+
}
|
|
922
|
+
try {
|
|
923
|
+
await this.stdioHandle?.close?.();
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
/* ignore */
|
|
927
|
+
}
|
|
928
|
+
this.handler = undefined;
|
|
929
|
+
this.stdioHandle = undefined;
|
|
930
|
+
}
|
|
931
|
+
/** The extensions map this adapter would advertise (for diagnostics/notes). */
|
|
932
|
+
advertisedExtensions() {
|
|
933
|
+
const tools = this.registry.getTools();
|
|
934
|
+
const hasTasks = Array.from(tools.values()).some((t) => t.taskSupport && t.taskSupport !== 'forbidden');
|
|
935
|
+
const hasApps = Array.from(tools.values()).some((t) => t.hasComponent());
|
|
936
|
+
return buildExtensionsMap({ hasTasks, hasApps, declared: this.registry.config.extensions });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
//# sourceMappingURL=modern-v2.adapter.js.map
|