@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.
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 +48 -5
  94. package/dist/core/server.d.ts.map +1 -1
  95. package/dist/core/server.js +305 -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 +23 -0
  106. package/dist/core/transports/streamable-http.d.ts.map +1 -1
  107. package/dist/core/transports/streamable-http.js +66 -10
  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: {
@@ -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 (req, res) => {
221
- const authHeader = req.get?.('authorization');
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
- // In production, serve the bundled HTML file if available
259
- if (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'prod') {
260
- try {
261
- // Check if we have a bundled file for this component
262
- // The component ID usually matches the widget output name
263
- const widgetId = component.id;
264
- // We need to find where the widgets are located relative to the running server
265
- // In production, we expect them in src/widgets/out or dist/widgets/out
266
- // Try to find the bundled file
267
- const fs = await import('fs');
268
- const path = await import('path');
269
- // Possible locations for bundled widgets
270
- const possiblePaths = [
271
- path.join(process.cwd(), 'src/widgets/out', `${widgetId}.html`),
272
- path.join(process.cwd(), 'dist/widgets/out', `${widgetId}.html`),
273
- path.join(process.cwd(), 'widgets/out', `${widgetId}.html`)
274
- ];
275
- for (const p of possiblePaths) {
276
- if (fs.existsSync(p)) {
277
- const html = fs.readFileSync(p, 'utf-8');
278
- return {
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: 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 || {}),
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
- if (sessionContext?.authHeader && combinedMeta['authorization'] === undefined) {
546
- combinedMeta['authorization'] = sessionContext.authHeader;
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 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);
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, args, context, taskId, name);
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 original args (including _meta) to tool
574
- 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
+ }
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
- 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);
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 { 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);
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
- 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);
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
- 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);
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
- 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) {
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 infer from NODE_ENV
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: port,
922
- host: host,
923
- endpoint: '/mcp',
1079
+ port,
1080
+ host,
1081
+ endpoint,
924
1082
  enableSessions: transportType === 'http', // Sessions ONLY in pure http mode
925
- enableCors: process.env.ENABLE_CORS !== 'false',
1083
+ enableCors,
926
1084
  ...getStreamableHttpEnvOptions(),
927
1085
  });
928
- // Delegate /mcp protocol handling to the official SDK transport: each
929
- // session gets its own configured MCP server built via this factory.
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: '/mcp',
973
- enableCors: process.env.ENABLE_CORS !== 'false',
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
- transport.setMcpServerFactory((sessionContext) => this.createConfiguredMcpServer(sessionContext));
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
- const stdioTransport = new StdioServerTransport();
1017
- 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
+ }
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 official SDK transport.
1038
- transport.setMcpServerFactory((sessionContext) => this.createConfiguredMcpServer(sessionContext));
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
- const transport = new StdioServerTransport();
1065
- 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
+ }
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 result = await tool.execute(args, context);
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.completeTask(taskId, response);
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 due to cancellation
1122
- const taskData = this.taskManager.hasTask(taskId)
1123
- ? this.taskManager.getTask(taskId)
1124
- : null;
1125
- if (taskData?.status === 'cancelled') {
1126
- 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);
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.notification) {
1347
+ if (mcpServerWithNotification?.notification) {
1158
1348
  mcpServerWithNotification.notification({
1159
1349
  method: 'notifications/tasks/status',
1160
1350
  params: taskData,
1161
- }).catch(err => this.logger.error('Failed to send task status notification', {
1162
- error: err instanceof Error ? err.message : String(err),
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();