@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.
Files changed (111) hide show
  1. package/dist/auth/cimd.d.ts +90 -0
  2. package/dist/auth/cimd.d.ts.map +1 -0
  3. package/dist/auth/cimd.js +344 -0
  4. package/dist/auth/cimd.js.map +1 -0
  5. package/dist/auth/client.d.ts +21 -0
  6. package/dist/auth/client.d.ts.map +1 -1
  7. package/dist/auth/client.js +48 -0
  8. package/dist/auth/client.js.map +1 -1
  9. package/dist/auth/index.d.ts +2 -1
  10. package/dist/auth/index.d.ts.map +1 -1
  11. package/dist/auth/index.js +3 -1
  12. package/dist/auth/index.js.map +1 -1
  13. package/dist/auth/middleware.d.ts.map +1 -1
  14. package/dist/auth/middleware.js +15 -3
  15. package/dist/auth/middleware.js.map +1 -1
  16. package/dist/auth/server-metadata.d.ts +5 -0
  17. package/dist/auth/server-metadata.d.ts.map +1 -1
  18. package/dist/auth/server-metadata.js +7 -1
  19. package/dist/auth/server-metadata.js.map +1 -1
  20. package/dist/auth/token-store.d.ts +12 -1
  21. package/dist/auth/token-store.d.ts.map +1 -1
  22. package/dist/auth/token-store.js +23 -1
  23. package/dist/auth/token-store.js.map +1 -1
  24. package/dist/auth/token-validation.d.ts +1 -1
  25. package/dist/auth/token-validation.d.ts.map +1 -1
  26. package/dist/auth/token-validation.js +20 -6
  27. package/dist/auth/token-validation.js.map +1 -1
  28. package/dist/auth/types.d.ts +17 -0
  29. package/dist/auth/types.d.ts.map +1 -1
  30. package/dist/core/app-decorator.d.ts +10 -0
  31. package/dist/core/app-decorator.d.ts.map +1 -1
  32. package/dist/core/app-decorator.js +25 -11
  33. package/dist/core/app-decorator.js.map +1 -1
  34. package/dist/core/builders.d.ts.map +1 -1
  35. package/dist/core/builders.js +5 -0
  36. package/dist/core/builders.js.map +1 -1
  37. package/dist/core/errors.d.ts +6 -0
  38. package/dist/core/errors.d.ts.map +1 -1
  39. package/dist/core/errors.js +9 -0
  40. package/dist/core/errors.js.map +1 -1
  41. package/dist/core/guards/oauth.guard.d.ts +13 -6
  42. package/dist/core/guards/oauth.guard.d.ts.map +1 -1
  43. package/dist/core/guards/oauth.guard.js +90 -1
  44. package/dist/core/guards/oauth.guard.js.map +1 -1
  45. package/dist/core/index.d.ts +7 -0
  46. package/dist/core/index.d.ts.map +1 -1
  47. package/dist/core/index.js +7 -0
  48. package/dist/core/index.js.map +1 -1
  49. package/dist/core/oauth-module.d.ts +5 -0
  50. package/dist/core/oauth-module.d.ts.map +1 -1
  51. package/dist/core/oauth-module.js +15 -6
  52. package/dist/core/oauth-module.js.map +1 -1
  53. package/dist/core/protocol/adapter.d.ts +97 -0
  54. package/dist/core/protocol/adapter.d.ts.map +1 -0
  55. package/dist/core/protocol/adapter.js +21 -0
  56. package/dist/core/protocol/adapter.js.map +1 -0
  57. package/dist/core/protocol/features/cache-hints.d.ts +64 -0
  58. package/dist/core/protocol/features/cache-hints.d.ts.map +1 -0
  59. package/dist/core/protocol/features/cache-hints.js +56 -0
  60. package/dist/core/protocol/features/cache-hints.js.map +1 -0
  61. package/dist/core/protocol/features/errors.d.ts +34 -0
  62. package/dist/core/protocol/features/errors.d.ts.map +1 -0
  63. package/dist/core/protocol/features/errors.js +48 -0
  64. package/dist/core/protocol/features/errors.js.map +1 -0
  65. package/dist/core/protocol/features/extensions.d.ts +39 -0
  66. package/dist/core/protocol/features/extensions.d.ts.map +1 -0
  67. package/dist/core/protocol/features/extensions.js +42 -0
  68. package/dist/core/protocol/features/extensions.js.map +1 -0
  69. package/dist/core/protocol/features/mrtr.d.ts +75 -0
  70. package/dist/core/protocol/features/mrtr.d.ts.map +1 -0
  71. package/dist/core/protocol/features/mrtr.js +66 -0
  72. package/dist/core/protocol/features/mrtr.js.map +1 -0
  73. package/dist/core/protocol/features/schema.d.ts +39 -0
  74. package/dist/core/protocol/features/schema.d.ts.map +1 -0
  75. package/dist/core/protocol/features/schema.js +96 -0
  76. package/dist/core/protocol/features/schema.js.map +1 -0
  77. package/dist/core/protocol/features/trace-context.d.ts +31 -0
  78. package/dist/core/protocol/features/trace-context.d.ts.map +1 -0
  79. package/dist/core/protocol/features/trace-context.js +45 -0
  80. package/dist/core/protocol/features/trace-context.js.map +1 -0
  81. package/dist/core/protocol/modern-v2.adapter.d.ts +92 -0
  82. package/dist/core/protocol/modern-v2.adapter.d.ts.map +1 -0
  83. package/dist/core/protocol/modern-v2.adapter.js +939 -0
  84. package/dist/core/protocol/modern-v2.adapter.js.map +1 -0
  85. package/dist/core/protocol/version.d.ts +72 -0
  86. package/dist/core/protocol/version.d.ts.map +1 -0
  87. package/dist/core/protocol/version.js +99 -0
  88. package/dist/core/protocol/version.js.map +1 -0
  89. package/dist/core/resource.d.ts +15 -0
  90. package/dist/core/resource.d.ts.map +1 -1
  91. package/dist/core/resource.js +13 -0
  92. package/dist/core/resource.js.map +1 -1
  93. package/dist/core/server.d.ts +50 -6
  94. package/dist/core/server.d.ts.map +1 -1
  95. package/dist/core/server.js +326 -90
  96. package/dist/core/server.js.map +1 -1
  97. package/dist/core/task.d.ts +126 -40
  98. package/dist/core/task.d.ts.map +1 -1
  99. package/dist/core/task.js +166 -56
  100. package/dist/core/task.js.map +1 -1
  101. package/dist/core/tool.d.ts +20 -0
  102. package/dist/core/tool.d.ts.map +1 -1
  103. package/dist/core/tool.js +16 -2
  104. package/dist/core/tool.js.map +1 -1
  105. package/dist/core/transports/streamable-http.d.ts +35 -1
  106. package/dist/core/transports/streamable-http.d.ts.map +1 -1
  107. package/dist/core/transports/streamable-http.js +77 -12
  108. package/dist/core/transports/streamable-http.js.map +1 -1
  109. package/dist/core/types.d.ts +86 -0
  110. package/dist/core/types.d.ts.map +1 -1
  111. package/package.json +5 -2
@@ -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 sessionMcp = this.createConfiguredMcpServer();
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 (_req, res) => {
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
- // In production, serve the bundled HTML file if available
248
- if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'prod') {
249
- try {
250
- // Check if we have a bundled file for this component
251
- // The component ID usually matches the widget output name
252
- const widgetId = component.id;
253
- // We need to find where the widgets are located relative to the running server
254
- // In production, we expect them in src/widgets/out or dist/widgets/out
255
- // Try to find the bundled file
256
- const fs = await import('fs');
257
- const path = await import('path');
258
- // Possible locations for bundled widgets
259
- const possiblePaths = [
260
- path.join(process.cwd(), 'src/widgets/out', `${widgetId}.html`),
261
- path.join(process.cwd(), 'dist/widgets/out', `${widgetId}.html`),
262
- path.join(process.cwd(), 'widgets/out', `${widgetId}.html`)
263
- ];
264
- for (const p of possiblePaths) {
265
- if (fs.existsSync(p)) {
266
- const html = fs.readFileSync(p, 'utf-8');
267
- return {
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: options?.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 args if present and add to context metadata
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: _meta,
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 taskData = this.taskManager.createTask(taskParam, name);
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, args, context, taskId, name);
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 original args (including _meta) to tool
553
- const result = await tool.execute(args, context);
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
- return this.taskManager.getTask(taskId);
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 { result, error } = await this.taskManager.getResult(taskId);
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
- return this.taskManager.listTasks(cursor);
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
- return this.taskManager.cancelTask(taskId);
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
- async start() {
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 infer from NODE_ENV
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: port,
901
- host: host,
902
- endpoint: '/mcp',
1079
+ port,
1080
+ host,
1081
+ endpoint,
903
1082
  enableSessions: transportType === 'http', // Sessions ONLY in pure http mode
904
- enableCors: process.env.ENABLE_CORS !== 'false',
1083
+ enableCors,
905
1084
  ...getStreamableHttpEnvOptions(),
906
1085
  });
907
- // Delegate /mcp protocol handling to the official SDK transport: each
908
- // session gets its own configured MCP server built via this factory.
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: '/mcp',
952
- enableCors: process.env.ENABLE_CORS !== 'false',
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
- transport.setMcpServerFactory(() => this.createConfiguredMcpServer());
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
- const stdioTransport = new StdioServerTransport();
996
- await this.mcpServer.connect(stdioTransport);
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 official SDK transport.
1017
- transport.setMcpServerFactory(() => this.createConfiguredMcpServer());
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
- const transport = new StdioServerTransport();
1044
- await this.mcpServer.connect(transport);
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 result = await tool.execute(args, context);
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.completeTask(taskId, response);
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 due to cancellation
1101
- const taskData = this.taskManager.hasTask(taskId)
1102
- ? this.taskManager.getTask(taskId)
1103
- : null;
1104
- if (taskData?.status === 'cancelled') {
1105
- return; // Already cancelled — don't overwrite status
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.notification) {
1347
+ if (mcpServerWithNotification?.notification) {
1137
1348
  mcpServerWithNotification.notification({
1138
1349
  method: 'notifications/tasks/status',
1139
1350
  params: taskData,
1140
- }).catch(err => this.logger.error('Failed to send task status notification', {
1141
- error: err instanceof Error ? err.message : String(err),
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();