@nitrostack/core 1.0.15 → 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 +48 -5
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +305 -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 +23 -0
- package/dist/core/transports/streamable-http.d.ts.map +1 -1
- package/dist/core/transports/streamable-http.js +66 -10
- 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
package/dist/core/server.js
CHANGED
|
@@ -13,6 +13,9 @@ import { buildController } from './builders.js';
|
|
|
13
13
|
import { DIContainer } from './di/container.js';
|
|
14
14
|
import { TaskManager, TaskContext, TaskNotFoundError, TaskAlreadyTerminalError, TaskAugmentationRequiredError, } from './task.js';
|
|
15
15
|
import { triggerLifecycleHook } from './lifecycle.js';
|
|
16
|
+
import { resolveProtocolEra, needsModernEngine, protocolVersionForEra, } from './protocol/version.js';
|
|
17
|
+
import { isInputRequired } from './protocol/features/mrtr.js';
|
|
18
|
+
import { extractBearerToken } from '../auth/token-validation.js';
|
|
16
19
|
/** Optional Streamable HTTP session limits from environment variables. */
|
|
17
20
|
function getStreamableHttpEnvOptions() {
|
|
18
21
|
const maxSessions = process.env.MCP_MAX_SESSIONS
|
|
@@ -48,6 +51,8 @@ export class NitroStackServer {
|
|
|
48
51
|
pendingComponentRegistrations = [];
|
|
49
52
|
/** Transport type used by the server */
|
|
50
53
|
_transportType;
|
|
54
|
+
/** Transport options configured during application creation */
|
|
55
|
+
_transportOptions;
|
|
51
56
|
/** HTTP transport instance (when using http or dual mode) */
|
|
52
57
|
_httpTransport;
|
|
53
58
|
/** SDK legacy HTTP+SSE routes (GET /sse + POST /mcp/messages) attached once per Express app */
|
|
@@ -60,6 +65,13 @@ export class NitroStackServer {
|
|
|
60
65
|
_shutdownSignalHandlers = [];
|
|
61
66
|
/** Guards stop() so double signals / repeated calls don't re-run teardown */
|
|
62
67
|
_stopping;
|
|
68
|
+
/**
|
|
69
|
+
* Resolved MCP protocol era. `legacy` (default) keeps the current 2025-era
|
|
70
|
+
* sessionful path unchanged; `modern` / `auto` engage the 2026-07-28 adapter.
|
|
71
|
+
*/
|
|
72
|
+
protocolEra;
|
|
73
|
+
/** Lazily constructed modern (2026-07-28) adapter (only on modern/auto). */
|
|
74
|
+
modernAdapter;
|
|
63
75
|
constructor(config) {
|
|
64
76
|
// Default config if not provided (e.g., when instantiated by DI container)
|
|
65
77
|
this.config = config || {
|
|
@@ -81,17 +93,74 @@ export class NitroStackServer {
|
|
|
81
93
|
// Initialize task manager for MCP Tasks support
|
|
82
94
|
this.taskManager = new TaskManager({
|
|
83
95
|
logger: this.logger,
|
|
96
|
+
store: this.config.taskStore ?? this.config.tasks?.store,
|
|
97
|
+
defaultTtl: this.config.tasks?.defaultTtl,
|
|
98
|
+
defaultPollInterval: this.config.tasks?.defaultPollInterval,
|
|
84
99
|
onStatusChange: (taskData) => {
|
|
85
100
|
// Send notifications/tasks/status when task status changes
|
|
86
101
|
this.sendTaskStatusNotification(taskData);
|
|
87
102
|
},
|
|
88
103
|
});
|
|
104
|
+
// Resolve the protocol era. Env NITRO_MCP_PROTOCOL_VERSION wins over the
|
|
105
|
+
// optional config.protocolVersion; unset ⇒ 'auto' (dual modern/legacy support).
|
|
106
|
+
this.protocolEra = resolveProtocolEra(this.config.protocolVersion);
|
|
107
|
+
if (this.protocolEra !== 'legacy') {
|
|
108
|
+
this.logger.info(`MCP protocol era: ${this.protocolEra} (${protocolVersionForEra(this.protocolEra)})`);
|
|
109
|
+
}
|
|
89
110
|
this.mcpServer = new McpServer({
|
|
90
111
|
name: this.config.name,
|
|
91
112
|
version: this.config.version,
|
|
92
113
|
}, NitroStackServer.mcpServerOptions);
|
|
93
114
|
this.setupHandlersOn(this.mcpServer);
|
|
94
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Build the read-only registry view the modern protocol adapter consumes.
|
|
118
|
+
* Captures `this` so the adapter never imports NitroStack internals.
|
|
119
|
+
*/
|
|
120
|
+
buildProtocolRegistry() {
|
|
121
|
+
return {
|
|
122
|
+
config: this.config,
|
|
123
|
+
logger: this.logger,
|
|
124
|
+
getTools: () => this.tools,
|
|
125
|
+
getResources: () => this.resources,
|
|
126
|
+
getResourceTemplates: () => this.resourceTemplates,
|
|
127
|
+
getTemplateResources: () => this.templateResources,
|
|
128
|
+
getPrompts: () => this.prompts,
|
|
129
|
+
getTaskManager: () => this.taskManager,
|
|
130
|
+
createExecutionContext: (options) => this.createContext(options),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Lazily create the modern (2026-07-28) protocol adapter. `auto` uses the v2
|
|
135
|
+
* stateless legacy fallback so a single endpoint serves both eras for
|
|
136
|
+
* validation; `modern` rejects legacy-classified traffic.
|
|
137
|
+
*/
|
|
138
|
+
async getModernAdapter() {
|
|
139
|
+
if (!this.modernAdapter) {
|
|
140
|
+
const { ModernProtocolAdapter } = await import('./protocol/modern-v2.adapter.js');
|
|
141
|
+
this.modernAdapter = new ModernProtocolAdapter(this.buildProtocolRegistry(), {
|
|
142
|
+
legacyMode: this.protocolEra === 'auto' ? 'stateless' : 'reject',
|
|
143
|
+
taskManager: this.taskManager,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return this.modernAdapter;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Bind an HTTP transport's /mcp endpoint to the correct protocol engine:
|
|
150
|
+
* the modern stateless handler on modern/auto, or the legacy per-session
|
|
151
|
+
* SDK factory otherwise.
|
|
152
|
+
*/
|
|
153
|
+
async configureTransportForProtocol(transport) {
|
|
154
|
+
if (needsModernEngine(this.protocolEra) && transport.setModernHandler) {
|
|
155
|
+
const adapter = await this.getModernAdapter();
|
|
156
|
+
const nodeHandler = await adapter.createNodeHandler();
|
|
157
|
+
transport.setModernHandler(nodeHandler);
|
|
158
|
+
transport.setProtocolVersionLabel?.(protocolVersionForEra(this.protocolEra));
|
|
159
|
+
}
|
|
160
|
+
else if (transport.setMcpServerFactory) {
|
|
161
|
+
transport.setMcpServerFactory((sessionContext) => this.createConfiguredMcpServer(sessionContext));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
95
164
|
/** Shared MCP server constructor options (main + per legacy SSE session) */
|
|
96
165
|
static mcpServerOptions = {
|
|
97
166
|
capabilities: {
|
|
@@ -192,13 +261,11 @@ export class NitroStackServer {
|
|
|
192
261
|
res.status(404).send('Unknown session');
|
|
193
262
|
return;
|
|
194
263
|
}
|
|
264
|
+
const authHeader = req.get('authorization');
|
|
265
|
+
if (authHeader) {
|
|
266
|
+
session.sessionContext.authHeader = authHeader;
|
|
267
|
+
}
|
|
195
268
|
try {
|
|
196
|
-
// Refresh the session auth header on every message POST so tool
|
|
197
|
-
// handlers see the caller's current credential.
|
|
198
|
-
const authHeader = req.get('authorization');
|
|
199
|
-
if (authHeader) {
|
|
200
|
-
session.sessionContext.authHeader = authHeader;
|
|
201
|
-
}
|
|
202
269
|
await session.transport.handlePostMessage(req, res, req.body);
|
|
203
270
|
}
|
|
204
271
|
catch (error) {
|
|
@@ -217,9 +284,8 @@ export class NitroStackServer {
|
|
|
217
284
|
}
|
|
218
285
|
// Cursor opens GET /mcp without mcp-session-id; fall back to legacy SSE on that path.
|
|
219
286
|
if (typeof transport.setLegacySseHandler === 'function') {
|
|
220
|
-
transport.setLegacySseHandler(async (
|
|
221
|
-
|
|
222
|
-
await this.startLegacySdkSseSession(res, '/mcp/messages', authHeader);
|
|
287
|
+
transport.setLegacySseHandler(async (_req, res) => {
|
|
288
|
+
await this.startLegacySdkSseSession(res, '/mcp/messages');
|
|
223
289
|
});
|
|
224
290
|
}
|
|
225
291
|
}
|
|
@@ -255,37 +321,33 @@ export class NitroStackServer {
|
|
|
255
321
|
mimeType: getWidgetMimeType(),
|
|
256
322
|
handler: async (uri, context) => {
|
|
257
323
|
context.logger.info(`Serving component: ${uri}`);
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
type: 'text',
|
|
280
|
-
data: html
|
|
281
|
-
};
|
|
282
|
-
}
|
|
324
|
+
// Serve the bundled HTML file if available
|
|
325
|
+
try {
|
|
326
|
+
const widgetId = component.id;
|
|
327
|
+
const cleanId = widgetId.replace(/^next-/, '');
|
|
328
|
+
const fs = await import('fs');
|
|
329
|
+
const path = await import('path');
|
|
330
|
+
const possiblePaths = [
|
|
331
|
+
path.join(process.cwd(), 'src/widgets/out', `${cleanId}.html`),
|
|
332
|
+
path.join(process.cwd(), 'src/widgets/out', `${widgetId}.html`),
|
|
333
|
+
path.join(process.cwd(), 'dist/widgets/out', `${cleanId}.html`),
|
|
334
|
+
path.join(process.cwd(), 'dist/widgets/out', `${widgetId}.html`),
|
|
335
|
+
path.join(process.cwd(), 'widgets/out', `${cleanId}.html`),
|
|
336
|
+
path.join(process.cwd(), 'widgets/out', `${widgetId}.html`),
|
|
337
|
+
];
|
|
338
|
+
for (const p of possiblePaths) {
|
|
339
|
+
if (fs.existsSync(p)) {
|
|
340
|
+
const html = fs.readFileSync(p, 'utf-8');
|
|
341
|
+
return {
|
|
342
|
+
type: 'text',
|
|
343
|
+
data: html,
|
|
344
|
+
};
|
|
283
345
|
}
|
|
284
|
-
context.logger.warn(`Bundled widget not found for ${widgetId}, falling back to default bundle`);
|
|
285
|
-
}
|
|
286
|
-
catch (error) {
|
|
287
|
-
context.logger.error(`Error serving bundled widget: ${error}`);
|
|
288
346
|
}
|
|
347
|
+
context.logger.warn(`Bundled widget not found for ${widgetId} (${cleanId}), falling back to default bundle`);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
context.logger.error(`Error serving bundled widget: ${error}`);
|
|
289
351
|
}
|
|
290
352
|
return {
|
|
291
353
|
type: 'text',
|
|
@@ -342,6 +404,7 @@ export class NitroStackServer {
|
|
|
342
404
|
* Notify clients that the list of resources has changed
|
|
343
405
|
*/
|
|
344
406
|
notifyResourcesListChanged() {
|
|
407
|
+
this.modernAdapter?.notifyResourcesListChanged();
|
|
345
408
|
try {
|
|
346
409
|
// Send notification through the MCP server
|
|
347
410
|
const mcpServerWithNotification = this.mcpServer;
|
|
@@ -359,6 +422,7 @@ export class NitroStackServer {
|
|
|
359
422
|
* Notify clients that the list of prompts has changed
|
|
360
423
|
*/
|
|
361
424
|
notifyPromptsListChanged() {
|
|
425
|
+
this.modernAdapter?.notifyPromptsListChanged();
|
|
362
426
|
try {
|
|
363
427
|
const mcpServerWithNotification = this.mcpServer;
|
|
364
428
|
if (mcpServerWithNotification.notification) {
|
|
@@ -375,6 +439,7 @@ export class NitroStackServer {
|
|
|
375
439
|
* Notify clients that the list of tools has changed
|
|
376
440
|
*/
|
|
377
441
|
notifyToolsListChanged() {
|
|
442
|
+
this.modernAdapter?.notifyToolsListChanged();
|
|
378
443
|
try {
|
|
379
444
|
const mcpServerWithNotification = this.mcpServer;
|
|
380
445
|
if (mcpServerWithNotification.notification) {
|
|
@@ -391,6 +456,7 @@ export class NitroStackServer {
|
|
|
391
456
|
* Notify subscribers that a resource has been updated
|
|
392
457
|
*/
|
|
393
458
|
notifyResourceUpdated(uri) {
|
|
459
|
+
this.modernAdapter?.notifyResourceUpdated(uri);
|
|
394
460
|
try {
|
|
395
461
|
const resource = this.resources.get(uri);
|
|
396
462
|
if (!resource || !resource.hasSubscribers())
|
|
@@ -488,11 +554,45 @@ export class NitroStackServer {
|
|
|
488
554
|
* Create execution context
|
|
489
555
|
*/
|
|
490
556
|
createContext(options) {
|
|
557
|
+
const metadata = options?.metadata || {};
|
|
558
|
+
let auth = options?.extra?.auth;
|
|
559
|
+
if (!auth) {
|
|
560
|
+
const authHeader = (metadata.authorization || metadata.Authorization);
|
|
561
|
+
const metaToken = (metadata._oauth || metadata.token || metadata.jwtToken || metadata._meta?.jwtToken || metadata._meta?.token);
|
|
562
|
+
const token = extractBearerToken(authHeader) || (typeof metaToken === 'string' ? extractBearerToken(metaToken) || metaToken : null);
|
|
563
|
+
if (token) {
|
|
564
|
+
let subject = 'authenticated-user';
|
|
565
|
+
let tokenPayload = undefined;
|
|
566
|
+
try {
|
|
567
|
+
const parts = token.split('.');
|
|
568
|
+
if (parts.length === 3) {
|
|
569
|
+
tokenPayload = JSON.parse(Buffer.from(parts[1], 'base64').toString('utf8'));
|
|
570
|
+
if (tokenPayload?.sub) {
|
|
571
|
+
subject = tokenPayload.sub;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
catch {
|
|
576
|
+
// ignore parse errors
|
|
577
|
+
}
|
|
578
|
+
auth = {
|
|
579
|
+
authenticated: true,
|
|
580
|
+
subject,
|
|
581
|
+
clientId: tokenPayload?.client_id || tokenPayload?.azp,
|
|
582
|
+
scopes: typeof tokenPayload?.scope === 'string' ? tokenPayload.scope.split(' ') : [],
|
|
583
|
+
tokenInfo: tokenPayload,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
}
|
|
491
587
|
return {
|
|
492
588
|
logger: this.logger,
|
|
493
589
|
requestId: uuidv4(),
|
|
494
590
|
toolName: options?.toolName,
|
|
495
|
-
metadata
|
|
591
|
+
metadata,
|
|
592
|
+
auth,
|
|
593
|
+
// Additive 2026-07-28 fields (protocolVersion, requestState, inputResponses,
|
|
594
|
+
// trace, clientInfo, clientCapabilities, auth) supplied by the modern adapter.
|
|
595
|
+
...(options?.extra || {}),
|
|
496
596
|
};
|
|
497
597
|
}
|
|
498
598
|
/**
|
|
@@ -542,8 +642,15 @@ export class NitroStackServer {
|
|
|
542
642
|
};
|
|
543
643
|
// Bridge the transport-captured HTTP Authorization header so OAuth
|
|
544
644
|
// guards can authenticate remote HTTP/SSE clients. Explicit _meta wins.
|
|
545
|
-
|
|
546
|
-
|
|
645
|
+
const rawAuth = combinedMeta['authorization'] || combinedMeta['Authorization'] || sessionContext?.authHeader;
|
|
646
|
+
if (rawAuth) {
|
|
647
|
+
combinedMeta['authorization'] = rawAuth;
|
|
648
|
+
combinedMeta['Authorization'] = rawAuth;
|
|
649
|
+
}
|
|
650
|
+
const rawToken = combinedMeta['token'] || combinedMeta['_oauth'] || combinedMeta['jwtToken'] || combinedMeta['_meta']?.jwtToken || combinedMeta['_meta']?.token;
|
|
651
|
+
if (rawToken) {
|
|
652
|
+
combinedMeta['token'] = rawToken;
|
|
653
|
+
combinedMeta['_oauth'] = rawToken;
|
|
547
654
|
}
|
|
548
655
|
const context = this.createContext({
|
|
549
656
|
metadata: combinedMeta,
|
|
@@ -553,14 +660,19 @@ export class NitroStackServer {
|
|
|
553
660
|
// Task-augmented path: create task, run async, return immediately
|
|
554
661
|
// ----------------------------------------------------------------
|
|
555
662
|
if (isTaskAugmented) {
|
|
556
|
-
const
|
|
663
|
+
const accessContext = sessionContext ? {
|
|
664
|
+
sessionId: sessionContext.sessionId,
|
|
665
|
+
userId: sessionContext.userId,
|
|
666
|
+
tenantId: sessionContext.tenantId,
|
|
667
|
+
} : undefined;
|
|
668
|
+
const taskData = this.taskManager.createTask(taskParam, name, accessContext);
|
|
557
669
|
const taskId = taskData.taskId;
|
|
558
670
|
// Attach a TaskContext to the execution context so handlers can
|
|
559
671
|
// report progress and check for cancellation
|
|
560
672
|
const taskContext = new TaskContext(this.taskManager, taskId);
|
|
561
673
|
context.task = taskContext;
|
|
562
674
|
// Execute the tool asynchronously (fire-and-forget)
|
|
563
|
-
this.runTaskAsync(tool,
|
|
675
|
+
this.runTaskAsync(tool, toolArgs, context, taskId, name);
|
|
564
676
|
// Return CreateTaskResult immediately
|
|
565
677
|
return {
|
|
566
678
|
task: taskData,
|
|
@@ -570,8 +682,22 @@ export class NitroStackServer {
|
|
|
570
682
|
// Normal synchronous path
|
|
571
683
|
// ----------------------------------------------------------------
|
|
572
684
|
try {
|
|
573
|
-
// Pass
|
|
574
|
-
const result = await tool.execute(
|
|
685
|
+
// Pass sanitized toolArgs to tool (protocol _meta is preserved in context.metadata)
|
|
686
|
+
const result = await tool.execute(toolArgs, context);
|
|
687
|
+
// MRTR (SEP-2322) is a 2026-07-28 feature. On the legacy path there is
|
|
688
|
+
// no client round-trip mechanism, so surface it as a clear error rather
|
|
689
|
+
// than leaking the marker's serialized shape.
|
|
690
|
+
if (isInputRequired(result)) {
|
|
691
|
+
return {
|
|
692
|
+
content: [
|
|
693
|
+
{
|
|
694
|
+
type: 'text',
|
|
695
|
+
text: 'This tool requires multi-round-trip input (MCP 2026-07-28). Set NITRO_MCP_PROTOCOL_VERSION=2026-07-28 to enable it.',
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
isError: true,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
575
701
|
this.stats.toolCalls++;
|
|
576
702
|
// Check if tool has a UI component
|
|
577
703
|
const response = {
|
|
@@ -640,7 +766,12 @@ export class NitroStackServer {
|
|
|
640
766
|
throw { code: -32602, message: 'Invalid params: taskId is required' };
|
|
641
767
|
}
|
|
642
768
|
try {
|
|
643
|
-
|
|
769
|
+
const accessContext = sessionContext ? {
|
|
770
|
+
sessionId: sessionContext.sessionId,
|
|
771
|
+
userId: sessionContext.userId,
|
|
772
|
+
tenantId: sessionContext.tenantId,
|
|
773
|
+
} : undefined;
|
|
774
|
+
return this.taskManager.getTask(taskId, accessContext);
|
|
644
775
|
}
|
|
645
776
|
catch (err) {
|
|
646
777
|
if (err instanceof TaskNotFoundError) {
|
|
@@ -660,7 +791,12 @@ export class NitroStackServer {
|
|
|
660
791
|
throw { code: -32602, message: 'Invalid params: taskId is required' };
|
|
661
792
|
}
|
|
662
793
|
try {
|
|
663
|
-
const
|
|
794
|
+
const accessContext = sessionContext ? {
|
|
795
|
+
sessionId: sessionContext.sessionId,
|
|
796
|
+
userId: sessionContext.userId,
|
|
797
|
+
tenantId: sessionContext.tenantId,
|
|
798
|
+
} : undefined;
|
|
799
|
+
const { result, error } = await this.taskManager.getResult(taskId, accessContext);
|
|
664
800
|
if (error) {
|
|
665
801
|
// Re-throw the original error so the client gets the JSON-RPC error
|
|
666
802
|
throw error;
|
|
@@ -691,9 +827,14 @@ export class NitroStackServer {
|
|
|
691
827
|
// Lists tasks with cursor-based pagination.
|
|
692
828
|
// ----------------------------------------------------------------
|
|
693
829
|
this.registerCustomHandler(mcp, 'tasks/list', async (params) => {
|
|
694
|
-
const { cursor } = (params || {});
|
|
830
|
+
const { cursor, limit } = (params || {});
|
|
695
831
|
try {
|
|
696
|
-
|
|
832
|
+
const accessContext = sessionContext ? {
|
|
833
|
+
sessionId: sessionContext.sessionId,
|
|
834
|
+
userId: sessionContext.userId,
|
|
835
|
+
tenantId: sessionContext.tenantId,
|
|
836
|
+
} : undefined;
|
|
837
|
+
return this.taskManager.listTasks(cursor, limit ?? 50, accessContext);
|
|
697
838
|
}
|
|
698
839
|
catch (err) {
|
|
699
840
|
if (err instanceof TaskNotFoundError) {
|
|
@@ -713,7 +854,12 @@ export class NitroStackServer {
|
|
|
713
854
|
throw { code: -32602, message: 'Invalid params: taskId is required' };
|
|
714
855
|
}
|
|
715
856
|
try {
|
|
716
|
-
|
|
857
|
+
const accessContext = sessionContext ? {
|
|
858
|
+
sessionId: sessionContext.sessionId,
|
|
859
|
+
userId: sessionContext.userId,
|
|
860
|
+
tenantId: sessionContext.tenantId,
|
|
861
|
+
} : undefined;
|
|
862
|
+
return this.taskManager.cancelTask(taskId, accessContext);
|
|
717
863
|
}
|
|
718
864
|
catch (err) {
|
|
719
865
|
if (err instanceof TaskNotFoundError) {
|
|
@@ -889,15 +1035,24 @@ export class NitroStackServer {
|
|
|
889
1035
|
* - NODE_ENV=development or unset → stdio mode
|
|
890
1036
|
* - NODE_ENV=production → dual mode (STDIO + HTTP)
|
|
891
1037
|
*/
|
|
892
|
-
|
|
1038
|
+
/**
|
|
1039
|
+
* Start the server.
|
|
1040
|
+
*
|
|
1041
|
+
* Transport determination precedence:
|
|
1042
|
+
* 1. Explicit `options.transport` passed to `start({ transport: 'stdio' })`
|
|
1043
|
+
* 2. `MCP_TRANSPORT_TYPE` environment variable
|
|
1044
|
+
* 3. Configured `this._transportType` (from `@McpApp` / `McpApplicationFactory`)
|
|
1045
|
+
* 4. Default: `NODE_ENV=development` or unset → 'stdio', `production` → 'dual'
|
|
1046
|
+
*/
|
|
1047
|
+
async start(options) {
|
|
893
1048
|
// Check for explicit transport type override
|
|
894
|
-
const explicitTransport = process.env.MCP_TRANSPORT_TYPE;
|
|
1049
|
+
const explicitTransport = options?.transport || process.env.MCP_TRANSPORT_TYPE;
|
|
895
1050
|
// Determine if we're in development mode
|
|
896
1051
|
// On Windows, NODE_ENV might not be passed correctly, so we're more lenient
|
|
897
1052
|
const nodeEnv = process.env.NODE_ENV?.toLowerCase();
|
|
898
1053
|
const isDevelopment = nodeEnv === 'development' || nodeEnv === 'dev' || !nodeEnv;
|
|
899
|
-
// Use explicit transport if set, otherwise
|
|
900
|
-
const transportType = explicitTransport || (isDevelopment ? 'stdio' : 'dual');
|
|
1054
|
+
// Use explicit transport if set, otherwise check configured instance transport, then fallback based on NODE_ENV
|
|
1055
|
+
const transportType = explicitTransport || this._transportType || (isDevelopment ? 'stdio' : 'dual');
|
|
901
1056
|
this._transportType = transportType;
|
|
902
1057
|
this.logger.debug(`NitroStackServer.start(): NODE_ENV=${process.env.NODE_ENV}, MCP_TRANSPORT_TYPE=${explicitTransport}, transportType=${transportType}`);
|
|
903
1058
|
// Resolve all modules so they (and their dependencies) are initialized in the DI container
|
|
@@ -910,24 +1065,26 @@ export class NitroStackServer {
|
|
|
910
1065
|
// Call onModuleInit for currently resolved instances (NestJS: after DI resolution)
|
|
911
1066
|
const initializedInstances = new Set(DIContainer.getInstance().getInstances());
|
|
912
1067
|
await triggerLifecycleHook([...initializedInstances], 'onModuleInit');
|
|
1068
|
+
// Resolve port/host/endpoint/cors
|
|
1069
|
+
const port = options?.port ?? (this._transportOptions?.port !== undefined ? this._transportOptions.port : parseInt(process.env.PORT || '3000'));
|
|
1070
|
+
const host = options?.host || this._transportOptions?.host || process.env.HOST || 'localhost';
|
|
1071
|
+
const endpoint = options?.endpoint || this._transportOptions?.endpoint || '/mcp';
|
|
1072
|
+
const enableCors = options?.enableCors ?? (this._transportOptions?.enableCors !== undefined ? this._transportOptions.enableCors : process.env.ENABLE_CORS !== 'false');
|
|
913
1073
|
// If HTTP transport is needed (dual or http mode), set it up BEFORE calling module.start()
|
|
914
1074
|
// This allows modules like OAuthModule to register endpoints/middleware on the HTTP server
|
|
915
1075
|
if (transportType === 'dual' || transportType === 'http') {
|
|
916
|
-
const port = parseInt(process.env.PORT || '3000');
|
|
917
|
-
const host = process.env.HOST || 'localhost';
|
|
918
1076
|
// Create HTTP transport first (do not start listening yet)
|
|
919
1077
|
const { StreamableHttpTransport } = await import('./transports/streamable-http.js');
|
|
920
1078
|
const httpTransport = new StreamableHttpTransport({
|
|
921
|
-
port
|
|
922
|
-
host
|
|
923
|
-
endpoint
|
|
1079
|
+
port,
|
|
1080
|
+
host,
|
|
1081
|
+
endpoint,
|
|
924
1082
|
enableSessions: transportType === 'http', // Sessions ONLY in pure http mode
|
|
925
|
-
enableCors
|
|
1083
|
+
enableCors,
|
|
926
1084
|
...getStreamableHttpEnvOptions(),
|
|
927
1085
|
});
|
|
928
|
-
// Delegate /mcp protocol handling to the
|
|
929
|
-
|
|
930
|
-
httpTransport.setMcpServerFactory((sessionContext) => this.createConfiguredMcpServer(sessionContext));
|
|
1086
|
+
// Delegate /mcp protocol handling to the correct protocol engine.
|
|
1087
|
+
await this.configureTransportForProtocol(httpTransport);
|
|
931
1088
|
// Set up tools callback and server config for documentation page
|
|
932
1089
|
httpTransport.setToolsCallback(async () => {
|
|
933
1090
|
const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
|
|
@@ -964,13 +1121,11 @@ export class NitroStackServer {
|
|
|
964
1121
|
await this._httpTransport.start();
|
|
965
1122
|
}
|
|
966
1123
|
// Now complete the transport setup (connect MCP server) using the determined transportType
|
|
967
|
-
const port = parseInt(process.env.PORT || '3000');
|
|
968
|
-
const host = process.env.HOST || 'localhost';
|
|
969
1124
|
await this.startWithTransport(transportType, {
|
|
970
1125
|
port,
|
|
971
1126
|
host,
|
|
972
|
-
endpoint
|
|
973
|
-
enableCors
|
|
1127
|
+
endpoint,
|
|
1128
|
+
enableCors,
|
|
974
1129
|
});
|
|
975
1130
|
}
|
|
976
1131
|
/**
|
|
@@ -1006,15 +1161,20 @@ export class NitroStackServer {
|
|
|
1006
1161
|
enableCors: transportOptions?.enableCors !== false, // Enable CORS by default for web clients
|
|
1007
1162
|
...getStreamableHttpEnvOptions(),
|
|
1008
1163
|
});
|
|
1009
|
-
|
|
1164
|
+
await this.configureTransportForProtocol(transport);
|
|
1010
1165
|
this.attachLegacySdkSseIfNeeded(transport);
|
|
1011
1166
|
await transport.start();
|
|
1012
1167
|
httpTransport = transport;
|
|
1013
1168
|
this._httpTransport = httpTransport;
|
|
1014
1169
|
}
|
|
1015
1170
|
// 2. Connect the primary MCP server via STDIO for direct connections.
|
|
1016
|
-
|
|
1017
|
-
|
|
1171
|
+
if (needsModernEngine(this.protocolEra)) {
|
|
1172
|
+
await (await this.getModernAdapter()).serveStdio();
|
|
1173
|
+
}
|
|
1174
|
+
else {
|
|
1175
|
+
const stdioTransport = new StdioServerTransport();
|
|
1176
|
+
await this.mcpServer.connect(stdioTransport);
|
|
1177
|
+
}
|
|
1018
1178
|
this.logger.info(`${this.config.name} started successfully (DUAL MODE)`);
|
|
1019
1179
|
this.logger.info(`✨ Mode: ${getAppMode().toUpperCase()} (via NITROSTACK_APP_MODE)`);
|
|
1020
1180
|
this.logger.info(`📡 STDIO: Ready for direct MCP connections`);
|
|
@@ -1034,8 +1194,8 @@ export class NitroStackServer {
|
|
|
1034
1194
|
enableCors: transportOptions?.enableCors || false,
|
|
1035
1195
|
...getStreamableHttpEnvOptions(),
|
|
1036
1196
|
});
|
|
1037
|
-
// Delegate /mcp protocol handling to the
|
|
1038
|
-
|
|
1197
|
+
// Delegate /mcp protocol handling to the correct protocol engine.
|
|
1198
|
+
await this.configureTransportForProtocol(transport);
|
|
1039
1199
|
// Set up tools callback and server config for documentation page
|
|
1040
1200
|
transport.setToolsCallback(async () => {
|
|
1041
1201
|
const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
|
|
@@ -1061,8 +1221,13 @@ export class NitroStackServer {
|
|
|
1061
1221
|
}
|
|
1062
1222
|
else {
|
|
1063
1223
|
// STDIO-only transport (default)
|
|
1064
|
-
|
|
1065
|
-
|
|
1224
|
+
if (needsModernEngine(this.protocolEra)) {
|
|
1225
|
+
await (await this.getModernAdapter()).serveStdio();
|
|
1226
|
+
}
|
|
1227
|
+
else {
|
|
1228
|
+
const transport = new StdioServerTransport();
|
|
1229
|
+
await this.mcpServer.connect(transport);
|
|
1230
|
+
}
|
|
1066
1231
|
this.logger.info(`${this.config.name} started successfully (STDIO transport)`);
|
|
1067
1232
|
this.logger.info(`✨ Mode: ${getAppMode().toUpperCase()} (via NITROSTACK_APP_MODE)`);
|
|
1068
1233
|
}
|
|
@@ -1080,7 +1245,9 @@ export class NitroStackServer {
|
|
|
1080
1245
|
*/
|
|
1081
1246
|
async runTaskAsync(tool, args, context, taskId, toolName) {
|
|
1082
1247
|
try {
|
|
1083
|
-
const
|
|
1248
|
+
const argsRecord = (args || {});
|
|
1249
|
+
const { _meta: _, ...toolArgs } = argsRecord;
|
|
1250
|
+
const result = await tool.execute(toolArgs, context);
|
|
1084
1251
|
this.stats.toolCalls++;
|
|
1085
1252
|
const response = {
|
|
1086
1253
|
content: [
|
|
@@ -1112,23 +1279,25 @@ export class NitroStackServer {
|
|
|
1112
1279
|
}
|
|
1113
1280
|
}
|
|
1114
1281
|
}
|
|
1115
|
-
this.taskManager.
|
|
1282
|
+
if (this.taskManager.hasTask(taskId)) {
|
|
1283
|
+
this.taskManager.completeTask(taskId, response);
|
|
1284
|
+
}
|
|
1116
1285
|
}
|
|
1117
1286
|
catch (error) {
|
|
1118
1287
|
this.stats.errors++;
|
|
1119
1288
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1120
1289
|
context.logger.error(`Task tool execution failed: ${toolName}`, { error: errorMessage, taskId });
|
|
1121
|
-
// Check if
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1290
|
+
// Check if task exists and is not cancelled
|
|
1291
|
+
if (this.taskManager.hasTask(taskId)) {
|
|
1292
|
+
const taskData = this.taskManager.getTask(taskId);
|
|
1293
|
+
if (taskData?.status === 'cancelled') {
|
|
1294
|
+
return; // Already cancelled — don't overwrite status
|
|
1295
|
+
}
|
|
1296
|
+
const formattedError = error instanceof ValidationError || error instanceof ToolExecutionError
|
|
1297
|
+
? error
|
|
1298
|
+
: new ToolExecutionError(toolName, error);
|
|
1299
|
+
this.taskManager.failTask(taskId, { code: -32603, message: formattedError.message }, formattedError.message);
|
|
1127
1300
|
}
|
|
1128
|
-
const formattedError = error instanceof ValidationError || error instanceof ToolExecutionError
|
|
1129
|
-
? error
|
|
1130
|
-
: new ToolExecutionError(toolName, error);
|
|
1131
|
-
this.taskManager.failTask(taskId, { code: -32603, message: formattedError.message }, formattedError.message);
|
|
1132
1301
|
}
|
|
1133
1302
|
}
|
|
1134
1303
|
/**
|
|
@@ -1153,14 +1322,39 @@ export class NitroStackServer {
|
|
|
1153
1322
|
*/
|
|
1154
1323
|
sendTaskStatusNotification(taskData) {
|
|
1155
1324
|
try {
|
|
1325
|
+
// 1. If task is bound to a legacy SSE session, deliver notification to that session's McpServer
|
|
1326
|
+
if (taskData.sessionId && this.legacySdkSseSessions.has(taskData.sessionId)) {
|
|
1327
|
+
const session = this.legacySdkSseSessions.get(taskData.sessionId);
|
|
1328
|
+
const sessionMcp = session?.server;
|
|
1329
|
+
if (sessionMcp?.notification) {
|
|
1330
|
+
sessionMcp.notification({
|
|
1331
|
+
method: 'notifications/tasks/status',
|
|
1332
|
+
params: taskData,
|
|
1333
|
+
}).catch((err) => this.logger.debug('Failed to send task status notification to session SSE', {
|
|
1334
|
+
sessionId: taskData.sessionId,
|
|
1335
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1336
|
+
}));
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
// 2. If modern protocol adapter is running, notify via modern adapter
|
|
1341
|
+
if (this.modernAdapter) {
|
|
1342
|
+
this.modernAdapter.notifyTaskStatus?.(taskData);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
// 3. Fallback to global mcpServer (used by STDIO transport)
|
|
1156
1346
|
const mcpServerWithNotification = this.mcpServer;
|
|
1157
|
-
if (mcpServerWithNotification
|
|
1347
|
+
if (mcpServerWithNotification?.notification) {
|
|
1158
1348
|
mcpServerWithNotification.notification({
|
|
1159
1349
|
method: 'notifications/tasks/status',
|
|
1160
1350
|
params: taskData,
|
|
1161
|
-
}).catch(err =>
|
|
1162
|
-
|
|
1163
|
-
|
|
1351
|
+
}).catch((err) => {
|
|
1352
|
+
// In stateless HTTP mode, global mcpServer is not connected to a single persistent client
|
|
1353
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1354
|
+
if (!errMsg.includes('Not connected')) {
|
|
1355
|
+
this.logger.error('Failed to send task status notification', { error: errMsg });
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1164
1358
|
}
|
|
1165
1359
|
}
|
|
1166
1360
|
catch (error) {
|
|
@@ -1174,6 +1368,22 @@ export class NitroStackServer {
|
|
|
1174
1368
|
getTaskManager() {
|
|
1175
1369
|
return this.taskManager;
|
|
1176
1370
|
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Configure a custom TaskStore (e.g., Redis, SQL, DynamoDB) for distributed task state
|
|
1373
|
+
*/
|
|
1374
|
+
setTaskStore(store) {
|
|
1375
|
+
this.taskManager.destroy();
|
|
1376
|
+
this.taskManager = new TaskManager({
|
|
1377
|
+
logger: this.logger,
|
|
1378
|
+
store,
|
|
1379
|
+
defaultTtl: this.config.tasks?.defaultTtl,
|
|
1380
|
+
defaultPollInterval: this.config.tasks?.defaultPollInterval,
|
|
1381
|
+
onStatusChange: (taskData) => {
|
|
1382
|
+
this.sendTaskStatusNotification(taskData);
|
|
1383
|
+
},
|
|
1384
|
+
});
|
|
1385
|
+
return this;
|
|
1386
|
+
}
|
|
1177
1387
|
/**
|
|
1178
1388
|
* Register OS signal handlers so the server shuts down gracefully, running the
|
|
1179
1389
|
* NestJS-style shutdown lifecycle hooks (beforeApplicationShutdown /
|
|
@@ -1249,6 +1459,11 @@ export class NitroStackServer {
|
|
|
1249
1459
|
}
|
|
1250
1460
|
// Destroy task manager (stops cleanup interval)
|
|
1251
1461
|
this.taskManager.destroy();
|
|
1462
|
+
// Close the modern protocol adapter (aborts in-flight modern exchanges).
|
|
1463
|
+
if (this.modernAdapter) {
|
|
1464
|
+
await this.modernAdapter.close();
|
|
1465
|
+
this.modernAdapter = undefined;
|
|
1466
|
+
}
|
|
1252
1467
|
for (const { server: sessionMcp, transport: legacyTransport } of this.legacySdkSseSessions.values()) {
|
|
1253
1468
|
try {
|
|
1254
1469
|
await legacyTransport.close();
|