@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
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: {
|
|
@@ -120,24 +189,28 @@ export class NitroStackServer {
|
|
|
120
189
|
* server instance (legacy SSE sessions, official Streamable HTTP sessions),
|
|
121
190
|
* since a single SDK server can only be connected to one transport at a time.
|
|
122
191
|
*/
|
|
123
|
-
createConfiguredMcpServer() {
|
|
192
|
+
createConfiguredMcpServer(sessionContext) {
|
|
124
193
|
const mcp = new McpServer({
|
|
125
194
|
name: this.config.name,
|
|
126
195
|
version: this.config.version,
|
|
127
196
|
}, NitroStackServer.mcpServerOptions);
|
|
128
|
-
this.setupHandlersOn(mcp);
|
|
197
|
+
this.setupHandlersOn(mcp, sessionContext);
|
|
129
198
|
return mcp;
|
|
130
199
|
}
|
|
131
200
|
/**
|
|
132
201
|
* SDK-compatible legacy HTTP+SSE: GET /sse (SSEServerTransport) and POST /mcp/messages?sessionId=.
|
|
133
202
|
* Streamable HTTP stays on GET/POST /mcp.
|
|
134
203
|
*/
|
|
135
|
-
async startLegacySdkSseSession(res, messagesPath) {
|
|
204
|
+
async startLegacySdkSseSession(res, messagesPath, authHeader) {
|
|
136
205
|
try {
|
|
137
|
-
const
|
|
206
|
+
const sessionContext = {};
|
|
207
|
+
if (authHeader) {
|
|
208
|
+
sessionContext.authHeader = authHeader;
|
|
209
|
+
}
|
|
210
|
+
const sessionMcp = this.createConfiguredMcpServer(sessionContext);
|
|
138
211
|
const transport = new SSEServerTransport(messagesPath, res);
|
|
139
212
|
const sessionId = transport.sessionId;
|
|
140
|
-
this.legacySdkSseSessions.set(sessionId, { server: sessionMcp, transport });
|
|
213
|
+
this.legacySdkSseSessions.set(sessionId, { server: sessionMcp, transport, sessionContext });
|
|
141
214
|
let closing = false;
|
|
142
215
|
transport.onclose = async () => {
|
|
143
216
|
if (closing) {
|
|
@@ -174,8 +247,8 @@ export class NitroStackServer {
|
|
|
174
247
|
this._legacySseRoutesAttached = true;
|
|
175
248
|
const LEGACY_SSE_PATH = '/sse';
|
|
176
249
|
const LEGACY_MESSAGES_PATH = '/mcp/messages';
|
|
177
|
-
app.get(LEGACY_SSE_PATH, async (
|
|
178
|
-
await this.startLegacySdkSseSession(res, LEGACY_MESSAGES_PATH);
|
|
250
|
+
app.get(LEGACY_SSE_PATH, async (req, res) => {
|
|
251
|
+
await this.startLegacySdkSseSession(res, LEGACY_MESSAGES_PATH, req.get('authorization'));
|
|
179
252
|
});
|
|
180
253
|
app.post(LEGACY_MESSAGES_PATH, async (req, res) => {
|
|
181
254
|
const sessionId = typeof req.query.sessionId === 'string' ? req.query.sessionId : undefined;
|
|
@@ -188,6 +261,10 @@ export class NitroStackServer {
|
|
|
188
261
|
res.status(404).send('Unknown session');
|
|
189
262
|
return;
|
|
190
263
|
}
|
|
264
|
+
const authHeader = req.get('authorization');
|
|
265
|
+
if (authHeader) {
|
|
266
|
+
session.sessionContext.authHeader = authHeader;
|
|
267
|
+
}
|
|
191
268
|
try {
|
|
192
269
|
await session.transport.handlePostMessage(req, res, req.body);
|
|
193
270
|
}
|
|
@@ -244,37 +321,33 @@ export class NitroStackServer {
|
|
|
244
321
|
mimeType: getWidgetMimeType(),
|
|
245
322
|
handler: async (uri, context) => {
|
|
246
323
|
context.logger.info(`Serving component: ${uri}`);
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
type: 'text',
|
|
269
|
-
data: html
|
|
270
|
-
};
|
|
271
|
-
}
|
|
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
|
+
};
|
|
272
345
|
}
|
|
273
|
-
context.logger.warn(`Bundled widget not found for ${widgetId}, falling back to default bundle`);
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
context.logger.error(`Error serving bundled widget: ${error}`);
|
|
277
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}`);
|
|
278
351
|
}
|
|
279
352
|
return {
|
|
280
353
|
type: 'text',
|
|
@@ -331,6 +404,7 @@ export class NitroStackServer {
|
|
|
331
404
|
* Notify clients that the list of resources has changed
|
|
332
405
|
*/
|
|
333
406
|
notifyResourcesListChanged() {
|
|
407
|
+
this.modernAdapter?.notifyResourcesListChanged();
|
|
334
408
|
try {
|
|
335
409
|
// Send notification through the MCP server
|
|
336
410
|
const mcpServerWithNotification = this.mcpServer;
|
|
@@ -348,6 +422,7 @@ export class NitroStackServer {
|
|
|
348
422
|
* Notify clients that the list of prompts has changed
|
|
349
423
|
*/
|
|
350
424
|
notifyPromptsListChanged() {
|
|
425
|
+
this.modernAdapter?.notifyPromptsListChanged();
|
|
351
426
|
try {
|
|
352
427
|
const mcpServerWithNotification = this.mcpServer;
|
|
353
428
|
if (mcpServerWithNotification.notification) {
|
|
@@ -364,6 +439,7 @@ export class NitroStackServer {
|
|
|
364
439
|
* Notify clients that the list of tools has changed
|
|
365
440
|
*/
|
|
366
441
|
notifyToolsListChanged() {
|
|
442
|
+
this.modernAdapter?.notifyToolsListChanged();
|
|
367
443
|
try {
|
|
368
444
|
const mcpServerWithNotification = this.mcpServer;
|
|
369
445
|
if (mcpServerWithNotification.notification) {
|
|
@@ -380,6 +456,7 @@ export class NitroStackServer {
|
|
|
380
456
|
* Notify subscribers that a resource has been updated
|
|
381
457
|
*/
|
|
382
458
|
notifyResourceUpdated(uri) {
|
|
459
|
+
this.modernAdapter?.notifyResourceUpdated(uri);
|
|
383
460
|
try {
|
|
384
461
|
const resource = this.resources.get(uri);
|
|
385
462
|
if (!resource || !resource.hasSubscribers())
|
|
@@ -477,17 +554,51 @@ export class NitroStackServer {
|
|
|
477
554
|
* Create execution context
|
|
478
555
|
*/
|
|
479
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
|
+
}
|
|
480
587
|
return {
|
|
481
588
|
logger: this.logger,
|
|
482
589
|
requestId: uuidv4(),
|
|
483
590
|
toolName: options?.toolName,
|
|
484
|
-
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 || {}),
|
|
485
596
|
};
|
|
486
597
|
}
|
|
487
598
|
/**
|
|
488
599
|
* Register MCP protocol handlers on the given server instance (main or per legacy SSE session).
|
|
489
600
|
*/
|
|
490
|
-
setupHandlersOn(mcp) {
|
|
601
|
+
setupHandlersOn(mcp, sessionContext) {
|
|
491
602
|
// List tools
|
|
492
603
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
493
604
|
this.logger.debug('Listing tools');
|
|
@@ -521,25 +632,47 @@ export class NitroStackServer {
|
|
|
521
632
|
if (!isTaskAugmented && tool.taskSupport === 'required') {
|
|
522
633
|
throw new TaskAugmentationRequiredError();
|
|
523
634
|
}
|
|
524
|
-
// Extract _meta from
|
|
635
|
+
// Extract _meta from request params (MCP spec) and from arguments
|
|
636
|
+
// (legacy Studio clients); params._meta takes precedence.
|
|
525
637
|
const argsRecord = (args || {});
|
|
526
|
-
const { _meta, ...toolArgs } = argsRecord;
|
|
638
|
+
const { _meta: metaFromArgs, ...toolArgs } = argsRecord;
|
|
639
|
+
const combinedMeta = {
|
|
640
|
+
...metaFromArgs,
|
|
641
|
+
...requestParams._meta,
|
|
642
|
+
};
|
|
643
|
+
// Bridge the transport-captured HTTP Authorization header so OAuth
|
|
644
|
+
// guards can authenticate remote HTTP/SSE clients. Explicit _meta wins.
|
|
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;
|
|
654
|
+
}
|
|
527
655
|
const context = this.createContext({
|
|
528
|
-
metadata:
|
|
656
|
+
metadata: combinedMeta,
|
|
529
657
|
toolName: name
|
|
530
658
|
});
|
|
531
659
|
// ----------------------------------------------------------------
|
|
532
660
|
// Task-augmented path: create task, run async, return immediately
|
|
533
661
|
// ----------------------------------------------------------------
|
|
534
662
|
if (isTaskAugmented) {
|
|
535
|
-
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);
|
|
536
669
|
const taskId = taskData.taskId;
|
|
537
670
|
// Attach a TaskContext to the execution context so handlers can
|
|
538
671
|
// report progress and check for cancellation
|
|
539
672
|
const taskContext = new TaskContext(this.taskManager, taskId);
|
|
540
673
|
context.task = taskContext;
|
|
541
674
|
// Execute the tool asynchronously (fire-and-forget)
|
|
542
|
-
this.runTaskAsync(tool,
|
|
675
|
+
this.runTaskAsync(tool, toolArgs, context, taskId, name);
|
|
543
676
|
// Return CreateTaskResult immediately
|
|
544
677
|
return {
|
|
545
678
|
task: taskData,
|
|
@@ -549,8 +682,22 @@ export class NitroStackServer {
|
|
|
549
682
|
// Normal synchronous path
|
|
550
683
|
// ----------------------------------------------------------------
|
|
551
684
|
try {
|
|
552
|
-
// Pass
|
|
553
|
-
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
|
+
}
|
|
554
701
|
this.stats.toolCalls++;
|
|
555
702
|
// Check if tool has a UI component
|
|
556
703
|
const response = {
|
|
@@ -619,7 +766,12 @@ export class NitroStackServer {
|
|
|
619
766
|
throw { code: -32602, message: 'Invalid params: taskId is required' };
|
|
620
767
|
}
|
|
621
768
|
try {
|
|
622
|
-
|
|
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);
|
|
623
775
|
}
|
|
624
776
|
catch (err) {
|
|
625
777
|
if (err instanceof TaskNotFoundError) {
|
|
@@ -639,7 +791,12 @@ export class NitroStackServer {
|
|
|
639
791
|
throw { code: -32602, message: 'Invalid params: taskId is required' };
|
|
640
792
|
}
|
|
641
793
|
try {
|
|
642
|
-
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);
|
|
643
800
|
if (error) {
|
|
644
801
|
// Re-throw the original error so the client gets the JSON-RPC error
|
|
645
802
|
throw error;
|
|
@@ -670,9 +827,14 @@ export class NitroStackServer {
|
|
|
670
827
|
// Lists tasks with cursor-based pagination.
|
|
671
828
|
// ----------------------------------------------------------------
|
|
672
829
|
this.registerCustomHandler(mcp, 'tasks/list', async (params) => {
|
|
673
|
-
const { cursor } = (params || {});
|
|
830
|
+
const { cursor, limit } = (params || {});
|
|
674
831
|
try {
|
|
675
|
-
|
|
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);
|
|
676
838
|
}
|
|
677
839
|
catch (err) {
|
|
678
840
|
if (err instanceof TaskNotFoundError) {
|
|
@@ -692,7 +854,12 @@ export class NitroStackServer {
|
|
|
692
854
|
throw { code: -32602, message: 'Invalid params: taskId is required' };
|
|
693
855
|
}
|
|
694
856
|
try {
|
|
695
|
-
|
|
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);
|
|
696
863
|
}
|
|
697
864
|
catch (err) {
|
|
698
865
|
if (err instanceof TaskNotFoundError) {
|
|
@@ -868,15 +1035,24 @@ export class NitroStackServer {
|
|
|
868
1035
|
* - NODE_ENV=development or unset → stdio mode
|
|
869
1036
|
* - NODE_ENV=production → dual mode (STDIO + HTTP)
|
|
870
1037
|
*/
|
|
871
|
-
|
|
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) {
|
|
872
1048
|
// Check for explicit transport type override
|
|
873
|
-
const explicitTransport = process.env.MCP_TRANSPORT_TYPE;
|
|
1049
|
+
const explicitTransport = options?.transport || process.env.MCP_TRANSPORT_TYPE;
|
|
874
1050
|
// Determine if we're in development mode
|
|
875
1051
|
// On Windows, NODE_ENV might not be passed correctly, so we're more lenient
|
|
876
1052
|
const nodeEnv = process.env.NODE_ENV?.toLowerCase();
|
|
877
1053
|
const isDevelopment = nodeEnv === 'development' || nodeEnv === 'dev' || !nodeEnv;
|
|
878
|
-
// Use explicit transport if set, otherwise
|
|
879
|
-
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');
|
|
880
1056
|
this._transportType = transportType;
|
|
881
1057
|
this.logger.debug(`NitroStackServer.start(): NODE_ENV=${process.env.NODE_ENV}, MCP_TRANSPORT_TYPE=${explicitTransport}, transportType=${transportType}`);
|
|
882
1058
|
// Resolve all modules so they (and their dependencies) are initialized in the DI container
|
|
@@ -889,24 +1065,26 @@ export class NitroStackServer {
|
|
|
889
1065
|
// Call onModuleInit for currently resolved instances (NestJS: after DI resolution)
|
|
890
1066
|
const initializedInstances = new Set(DIContainer.getInstance().getInstances());
|
|
891
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');
|
|
892
1073
|
// If HTTP transport is needed (dual or http mode), set it up BEFORE calling module.start()
|
|
893
1074
|
// This allows modules like OAuthModule to register endpoints/middleware on the HTTP server
|
|
894
1075
|
if (transportType === 'dual' || transportType === 'http') {
|
|
895
|
-
const port = parseInt(process.env.PORT || '3000');
|
|
896
|
-
const host = process.env.HOST || 'localhost';
|
|
897
1076
|
// Create HTTP transport first (do not start listening yet)
|
|
898
1077
|
const { StreamableHttpTransport } = await import('./transports/streamable-http.js');
|
|
899
1078
|
const httpTransport = new StreamableHttpTransport({
|
|
900
|
-
port
|
|
901
|
-
host
|
|
902
|
-
endpoint
|
|
1079
|
+
port,
|
|
1080
|
+
host,
|
|
1081
|
+
endpoint,
|
|
903
1082
|
enableSessions: transportType === 'http', // Sessions ONLY in pure http mode
|
|
904
|
-
enableCors
|
|
1083
|
+
enableCors,
|
|
905
1084
|
...getStreamableHttpEnvOptions(),
|
|
906
1085
|
});
|
|
907
|
-
// Delegate /mcp protocol handling to the
|
|
908
|
-
|
|
909
|
-
httpTransport.setMcpServerFactory(() => this.createConfiguredMcpServer());
|
|
1086
|
+
// Delegate /mcp protocol handling to the correct protocol engine.
|
|
1087
|
+
await this.configureTransportForProtocol(httpTransport);
|
|
910
1088
|
// Set up tools callback and server config for documentation page
|
|
911
1089
|
httpTransport.setToolsCallback(async () => {
|
|
912
1090
|
const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
|
|
@@ -943,13 +1121,11 @@ export class NitroStackServer {
|
|
|
943
1121
|
await this._httpTransport.start();
|
|
944
1122
|
}
|
|
945
1123
|
// Now complete the transport setup (connect MCP server) using the determined transportType
|
|
946
|
-
const port = parseInt(process.env.PORT || '3000');
|
|
947
|
-
const host = process.env.HOST || 'localhost';
|
|
948
1124
|
await this.startWithTransport(transportType, {
|
|
949
1125
|
port,
|
|
950
1126
|
host,
|
|
951
|
-
endpoint
|
|
952
|
-
enableCors
|
|
1127
|
+
endpoint,
|
|
1128
|
+
enableCors,
|
|
953
1129
|
});
|
|
954
1130
|
}
|
|
955
1131
|
/**
|
|
@@ -985,15 +1161,20 @@ export class NitroStackServer {
|
|
|
985
1161
|
enableCors: transportOptions?.enableCors !== false, // Enable CORS by default for web clients
|
|
986
1162
|
...getStreamableHttpEnvOptions(),
|
|
987
1163
|
});
|
|
988
|
-
|
|
1164
|
+
await this.configureTransportForProtocol(transport);
|
|
989
1165
|
this.attachLegacySdkSseIfNeeded(transport);
|
|
990
1166
|
await transport.start();
|
|
991
1167
|
httpTransport = transport;
|
|
992
1168
|
this._httpTransport = httpTransport;
|
|
993
1169
|
}
|
|
994
1170
|
// 2. Connect the primary MCP server via STDIO for direct connections.
|
|
995
|
-
|
|
996
|
-
|
|
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
|
+
}
|
|
997
1178
|
this.logger.info(`${this.config.name} started successfully (DUAL MODE)`);
|
|
998
1179
|
this.logger.info(`✨ Mode: ${getAppMode().toUpperCase()} (via NITROSTACK_APP_MODE)`);
|
|
999
1180
|
this.logger.info(`📡 STDIO: Ready for direct MCP connections`);
|
|
@@ -1013,8 +1194,8 @@ export class NitroStackServer {
|
|
|
1013
1194
|
enableCors: transportOptions?.enableCors || false,
|
|
1014
1195
|
...getStreamableHttpEnvOptions(),
|
|
1015
1196
|
});
|
|
1016
|
-
// Delegate /mcp protocol handling to the
|
|
1017
|
-
|
|
1197
|
+
// Delegate /mcp protocol handling to the correct protocol engine.
|
|
1198
|
+
await this.configureTransportForProtocol(transport);
|
|
1018
1199
|
// Set up tools callback and server config for documentation page
|
|
1019
1200
|
transport.setToolsCallback(async () => {
|
|
1020
1201
|
const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
|
|
@@ -1040,8 +1221,13 @@ export class NitroStackServer {
|
|
|
1040
1221
|
}
|
|
1041
1222
|
else {
|
|
1042
1223
|
// STDIO-only transport (default)
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
+
}
|
|
1045
1231
|
this.logger.info(`${this.config.name} started successfully (STDIO transport)`);
|
|
1046
1232
|
this.logger.info(`✨ Mode: ${getAppMode().toUpperCase()} (via NITROSTACK_APP_MODE)`);
|
|
1047
1233
|
}
|
|
@@ -1059,7 +1245,9 @@ export class NitroStackServer {
|
|
|
1059
1245
|
*/
|
|
1060
1246
|
async runTaskAsync(tool, args, context, taskId, toolName) {
|
|
1061
1247
|
try {
|
|
1062
|
-
const
|
|
1248
|
+
const argsRecord = (args || {});
|
|
1249
|
+
const { _meta: _, ...toolArgs } = argsRecord;
|
|
1250
|
+
const result = await tool.execute(toolArgs, context);
|
|
1063
1251
|
this.stats.toolCalls++;
|
|
1064
1252
|
const response = {
|
|
1065
1253
|
content: [
|
|
@@ -1091,23 +1279,25 @@ export class NitroStackServer {
|
|
|
1091
1279
|
}
|
|
1092
1280
|
}
|
|
1093
1281
|
}
|
|
1094
|
-
this.taskManager.
|
|
1282
|
+
if (this.taskManager.hasTask(taskId)) {
|
|
1283
|
+
this.taskManager.completeTask(taskId, response);
|
|
1284
|
+
}
|
|
1095
1285
|
}
|
|
1096
1286
|
catch (error) {
|
|
1097
1287
|
this.stats.errors++;
|
|
1098
1288
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1099
1289
|
context.logger.error(`Task tool execution failed: ${toolName}`, { error: errorMessage, taskId });
|
|
1100
|
-
// Check if
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
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);
|
|
1106
1300
|
}
|
|
1107
|
-
const formattedError = error instanceof ValidationError || error instanceof ToolExecutionError
|
|
1108
|
-
? error
|
|
1109
|
-
: new ToolExecutionError(toolName, error);
|
|
1110
|
-
this.taskManager.failTask(taskId, { code: -32603, message: formattedError.message }, formattedError.message);
|
|
1111
1301
|
}
|
|
1112
1302
|
}
|
|
1113
1303
|
/**
|
|
@@ -1132,14 +1322,39 @@ export class NitroStackServer {
|
|
|
1132
1322
|
*/
|
|
1133
1323
|
sendTaskStatusNotification(taskData) {
|
|
1134
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)
|
|
1135
1346
|
const mcpServerWithNotification = this.mcpServer;
|
|
1136
|
-
if (mcpServerWithNotification
|
|
1347
|
+
if (mcpServerWithNotification?.notification) {
|
|
1137
1348
|
mcpServerWithNotification.notification({
|
|
1138
1349
|
method: 'notifications/tasks/status',
|
|
1139
1350
|
params: taskData,
|
|
1140
|
-
}).catch(err =>
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
+
});
|
|
1143
1358
|
}
|
|
1144
1359
|
}
|
|
1145
1360
|
catch (error) {
|
|
@@ -1153,6 +1368,22 @@ export class NitroStackServer {
|
|
|
1153
1368
|
getTaskManager() {
|
|
1154
1369
|
return this.taskManager;
|
|
1155
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
|
+
}
|
|
1156
1387
|
/**
|
|
1157
1388
|
* Register OS signal handlers so the server shuts down gracefully, running the
|
|
1158
1389
|
* NestJS-style shutdown lifecycle hooks (beforeApplicationShutdown /
|
|
@@ -1228,6 +1459,11 @@ export class NitroStackServer {
|
|
|
1228
1459
|
}
|
|
1229
1460
|
// Destroy task manager (stops cleanup interval)
|
|
1230
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
|
+
}
|
|
1231
1467
|
for (const { server: sessionMcp, transport: legacyTransport } of this.legacySdkSseSessions.values()) {
|
|
1232
1468
|
try {
|
|
1233
1469
|
await legacyTransport.close();
|