@nitrostack/core 1.0.11 → 1.0.13

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 (55) hide show
  1. package/dist/auth/middleware.d.ts.map +1 -1
  2. package/dist/auth/middleware.js +17 -0
  3. package/dist/auth/middleware.js.map +1 -1
  4. package/dist/auth/token-validation.d.ts.map +1 -1
  5. package/dist/auth/token-validation.js +12 -2
  6. package/dist/auth/token-validation.js.map +1 -1
  7. package/dist/core/app-decorator.d.ts +6 -0
  8. package/dist/core/app-decorator.d.ts.map +1 -1
  9. package/dist/core/app-decorator.js +5 -3
  10. package/dist/core/app-decorator.js.map +1 -1
  11. package/dist/core/builders.d.ts.map +1 -1
  12. package/dist/core/builders.js +14 -4
  13. package/dist/core/builders.js.map +1 -1
  14. package/dist/core/decorators.d.ts +30 -0
  15. package/dist/core/decorators.d.ts.map +1 -1
  16. package/dist/core/decorators.js +42 -4
  17. package/dist/core/decorators.js.map +1 -1
  18. package/dist/core/di/container.d.ts +18 -0
  19. package/dist/core/di/container.d.ts.map +1 -1
  20. package/dist/core/di/container.js +31 -0
  21. package/dist/core/di/container.js.map +1 -1
  22. package/dist/core/di/injectable.decorator.d.ts.map +1 -1
  23. package/dist/core/di/injectable.decorator.js +2 -1
  24. package/dist/core/di/injectable.decorator.js.map +1 -1
  25. package/dist/core/events/event.decorator.d.ts.map +1 -1
  26. package/dist/core/events/event.decorator.js +2 -1
  27. package/dist/core/events/event.decorator.js.map +1 -1
  28. package/dist/core/index.d.ts +4 -2
  29. package/dist/core/index.d.ts.map +1 -1
  30. package/dist/core/index.js +3 -1
  31. package/dist/core/index.js.map +1 -1
  32. package/dist/core/lifecycle.d.ts +44 -0
  33. package/dist/core/lifecycle.d.ts.map +1 -0
  34. package/dist/core/lifecycle.js +62 -0
  35. package/dist/core/lifecycle.js.map +1 -0
  36. package/dist/core/oauth-module.d.ts +76 -5
  37. package/dist/core/oauth-module.d.ts.map +1 -1
  38. package/dist/core/oauth-module.js +343 -83
  39. package/dist/core/oauth-module.js.map +1 -1
  40. package/dist/core/pipes/pipe.decorator.d.ts.map +1 -1
  41. package/dist/core/pipes/pipe.decorator.js +2 -1
  42. package/dist/core/pipes/pipe.decorator.js.map +1 -1
  43. package/dist/core/prompt.d.ts +0 -3
  44. package/dist/core/prompt.d.ts.map +1 -1
  45. package/dist/core/prompt.js +28 -1
  46. package/dist/core/prompt.js.map +1 -1
  47. package/dist/core/server.d.ts +39 -26
  48. package/dist/core/server.d.ts.map +1 -1
  49. package/dist/core/server.js +222 -102
  50. package/dist/core/server.js.map +1 -1
  51. package/dist/core/transports/streamable-http.d.ts +64 -61
  52. package/dist/core/transports/streamable-http.d.ts.map +1 -1
  53. package/dist/core/transports/streamable-http.js +1591 -823
  54. package/dist/core/transports/streamable-http.js.map +1 -1
  55. package/package.json +5 -7
@@ -2,6 +2,32 @@ import { ValidationError } from './errors.js';
2
2
  /**
3
3
  * Prompt class provides a clean abstraction for defining and executing prompts
4
4
  */
5
+ // Enforces the PromptMessage contract (see types.ts): role in
6
+ // user|assistant|system and string content. Structured content blocks are not
7
+ // supported here by design; supporting them requires changing the PromptMessage
8
+ // type and the server-side MCP mapping together.
9
+ function validateMessageFormat(msg) {
10
+ if (!msg || typeof msg !== 'object') {
11
+ throw new ValidationError('Invalid prompt message format: message must be an object');
12
+ }
13
+ if (!msg.role || !['user', 'assistant', 'system'].includes(msg.role)) {
14
+ throw new ValidationError(`Invalid prompt message role: '${msg.role}'. Must be 'user', 'assistant', or 'system'`);
15
+ }
16
+ if (typeof msg.content !== 'string') {
17
+ throw new ValidationError('Invalid prompt message content: content must be a string');
18
+ }
19
+ return {
20
+ role: msg.role,
21
+ content: msg.content,
22
+ };
23
+ }
24
+ function normalizePromptResponse(result) {
25
+ if (result === null || result === undefined) {
26
+ return [];
27
+ }
28
+ const arrayResult = Array.isArray(result) ? result : [result];
29
+ return arrayResult.map(validateMessageFormat);
30
+ }
5
31
  export class Prompt {
6
32
  definition;
7
33
  constructor(definition) {
@@ -39,7 +65,8 @@ export class Prompt {
39
65
  this.validateArguments(args);
40
66
  context.logger.info(`Executing prompt: ${this.name}`, { args: args });
41
67
  try {
42
- const messages = await this.definition.handler(args, context);
68
+ const messagesResult = await this.definition.handler(args, context);
69
+ const messages = normalizePromptResponse(messagesResult);
43
70
  context.logger.info(`Prompt executed successfully: ${this.name}`, {
44
71
  messageCount: messages.length,
45
72
  });
@@ -1 +1 @@
1
- {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/core/prompt.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAY9C;;GAEG;AACH,MAAM,OAAO,MAAM;IACT,UAAU,CAAmB;IAErC,YAAY,UAA4B;QACtC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,IAAI,IAAI;QACN,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;IAC9B,CAAC;IAED;;OAEG;IACH,IAAI,KAAK;QACP,OAAO,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;IAC/B,CAAC;IAED;;OAEG;IACH,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;IACrC,CAAC;IAED;;OAEG;IACH,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,IAAI,EAAE,CAAC;IACzC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO,CAAC,IAAyC,EAAE,OAAyB;QAChF,8BAA8B;QAC9B,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAE7B,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,qBAAqB,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,IAA6B,EAAE,CAAC,CAAC;QAE/F,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YAE9D,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,iCAAiC,IAAI,CAAC,IAAI,EAAE,EAAE;gBAChE,YAAY,EAAE,QAAQ,CAAC,MAAM;aAC9B,CAAC,CAAC;YAEH,OAAO,QAAQ,CAAC;QAClB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAChI,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED;;OAEG;IACK,iBAAiB,CAAC,IAAyC;QACjE,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS;YAAE,OAAO;QAEvC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE,CAAC;YAC5C,IAAI,GAAG,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;gBACxC,MAAM,IAAI,eAAe,CACvB,8BAA8B,GAAG,CAAC,IAAI,iBAAiB,IAAI,CAAC,IAAI,GAAG,CACpE,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACH,WAAW;QACT,MAAM,MAAM,GAAc;YACxB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC;QAEF,IAAI,IAAI,CAAC,KAAK;YAAE,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QAE1C,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,UAA4B;IACvD,OAAO,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC;AAChC,CAAC"}
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/core/prompt.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
- import type { Express } from 'express';
1
+ import { Server as McpServer } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import type { Express, Response } from 'express';
2
3
  import { Tool } from './tool.js';
3
4
  import { Resource, ResourceTemplate } from './resource.js';
4
5
  import { Prompt } from './prompt.js';
@@ -10,8 +11,9 @@ import { TaskManager } from './task.js';
10
11
  interface HttpTransport {
11
12
  start(): Promise<void>;
12
13
  close(): Promise<void>;
13
- send(message: JsonRpcResponse): Promise<void>;
14
- onmessage?: (message: JsonRpcRequest) => Promise<void>;
14
+ /** Provide the factory used to build a configured MCP server per /mcp session. */
15
+ setMcpServerFactory?(factory: () => McpServer): void;
16
+ setLegacySseHandler?(handler: (req: unknown, res: Response) => Promise<void>): void;
15
17
  setToolsCallback?(callback: () => Promise<unknown[]>): void;
16
18
  setServerConfig?(config: {
17
19
  name: string;
@@ -21,28 +23,6 @@ interface HttpTransport {
21
23
  /** StreamableHttpTransport exposes the Express app for extra routes (legacy SDK SSE). */
22
24
  getApp?(): Express;
23
25
  }
24
- /**
25
- * JSON-RPC request structure
26
- */
27
- interface JsonRpcRequest {
28
- jsonrpc: '2.0';
29
- id?: string | number | null;
30
- method?: string;
31
- params?: Record<string, unknown>;
32
- }
33
- /**
34
- * JSON-RPC response structure
35
- */
36
- interface JsonRpcResponse {
37
- jsonrpc: '2.0';
38
- id: string | number | null;
39
- result?: unknown;
40
- error?: {
41
- code: number;
42
- message: string;
43
- data?: unknown;
44
- };
45
- }
46
26
  /**
47
27
  * NitroStackServer - Main server class
48
28
  */
@@ -68,13 +48,25 @@ export declare class NitroStackServer {
68
48
  private readonly legacySdkSseSessions;
69
49
  /** Task manager for MCP Tasks support */
70
50
  private taskManager;
51
+ /** Registered OS signal handlers for graceful shutdown (see enableShutdownHooks) */
52
+ private _shutdownSignalHandlers;
53
+ /** Guards stop() so double signals / repeated calls don't re-run teardown */
54
+ private _stopping?;
71
55
  constructor(config?: McpServerConfig);
72
56
  /** Shared MCP server constructor options (main + per legacy SSE session) */
73
57
  private static readonly mcpServerOptions;
58
+ /**
59
+ * Build a fresh MCP server instance wired with all of this application's
60
+ * tool/resource/prompt handlers. Used for transports that require their own
61
+ * server instance (legacy SSE sessions, official Streamable HTTP sessions),
62
+ * since a single SDK server can only be connected to one transport at a time.
63
+ */
64
+ createConfiguredMcpServer(): McpServer;
74
65
  /**
75
66
  * SDK-compatible legacy HTTP+SSE: GET /sse (SSEServerTransport) and POST /mcp/messages?sessionId=.
76
67
  * Streamable HTTP stays on GET/POST /mcp.
77
68
  */
69
+ private startLegacySdkSseSession;
78
70
  private attachLegacySdkSseRoutes;
79
71
  private attachLegacySdkSseIfNeeded;
80
72
  /**
@@ -174,10 +166,31 @@ export declare class NitroStackServer {
174
166
  * Get the TaskManager instance (useful for advanced use cases)
175
167
  */
176
168
  getTaskManager(): TaskManager;
169
+ /**
170
+ * Register OS signal handlers so the server shuts down gracefully, running the
171
+ * NestJS-style shutdown lifecycle hooks (beforeApplicationShutdown /
172
+ * onApplicationShutdown) with the received signal.
173
+ *
174
+ * Opt-in (like NestJS `enableShutdownHooks`) to avoid attaching process
175
+ * listeners for consumers that manage their own shutdown. Safe to call more
176
+ * than once; handlers are only attached once per signal.
177
+ *
178
+ * @param signals - Signals to listen for (default: SIGTERM, SIGINT)
179
+ */
180
+ enableShutdownHooks(signals?: NodeJS.Signals[]): this;
181
+ /**
182
+ * Remove any process signal handlers registered via enableShutdownHooks().
183
+ */
184
+ private removeShutdownHooks;
177
185
  /**
178
186
  * Stop the server
187
+ *
188
+ * Idempotent: concurrent or repeated calls (e.g. from multiple OS signals)
189
+ * share the same in-flight teardown instead of re-running lifecycle hooks or
190
+ * double-closing transports.
179
191
  */
180
- stop(): Promise<void>;
192
+ stop(signal?: string): Promise<void>;
193
+ private doStop;
181
194
  /**
182
195
  * Get the HTTP transport (for modules that need to register endpoints)
183
196
  */
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/core/server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/core/server.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,20 @@ import { isModule, getModuleMetadata } from './module.js';
12
12
  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
+ import { triggerLifecycleHook } from './lifecycle.js';
16
+ /** Optional Streamable HTTP session limits from environment variables. */
17
+ function getStreamableHttpEnvOptions() {
18
+ const maxSessions = process.env.MCP_MAX_SESSIONS
19
+ ? parseInt(process.env.MCP_MAX_SESSIONS, 10)
20
+ : undefined;
21
+ const sessionTimeout = process.env.MCP_SESSION_TIMEOUT_MS
22
+ ? parseInt(process.env.MCP_SESSION_TIMEOUT_MS, 10)
23
+ : undefined;
24
+ return {
25
+ ...(maxSessions !== undefined && !Number.isNaN(maxSessions) ? { maxSessions } : {}),
26
+ ...(sessionTimeout !== undefined && !Number.isNaN(sessionTimeout) ? { sessionTimeout } : {}),
27
+ };
28
+ }
15
29
  /**
16
30
  * NitroStackServer - Main server class
17
31
  */
@@ -42,12 +56,22 @@ export class NitroStackServer {
42
56
  legacySdkSseSessions = new Map();
43
57
  /** Task manager for MCP Tasks support */
44
58
  taskManager;
59
+ /** Registered OS signal handlers for graceful shutdown (see enableShutdownHooks) */
60
+ _shutdownSignalHandlers = [];
61
+ /** Guards stop() so double signals / repeated calls don't re-run teardown */
62
+ _stopping;
45
63
  constructor(config) {
46
64
  // Default config if not provided (e.g., when instantiated by DI container)
47
65
  this.config = config || {
48
66
  name: 'nitrostack-server',
49
67
  version: '1.0.0',
50
68
  };
69
+ // Register itself in DI container so modules can inject the server.
70
+ // NOTE: DIContainer is a process-wide singleton, so this assumes a single
71
+ // NitroStackServer instance per process. Multiple instances would overwrite
72
+ // each other's registration here.
73
+ DIContainer.getInstance().registerValue(NitroStackServer, this);
74
+ DIContainer.getInstance().registerValue('NitroStackServer', this);
51
75
  this.logger = createLogger({
52
76
  level: this.config.logging?.level || 'info',
53
77
  file: this.config.logging?.file,
@@ -90,10 +114,59 @@ export class NitroStackServer {
90
114
  },
91
115
  },
92
116
  };
117
+ /**
118
+ * Build a fresh MCP server instance wired with all of this application's
119
+ * tool/resource/prompt handlers. Used for transports that require their own
120
+ * server instance (legacy SSE sessions, official Streamable HTTP sessions),
121
+ * since a single SDK server can only be connected to one transport at a time.
122
+ */
123
+ createConfiguredMcpServer() {
124
+ const mcp = new McpServer({
125
+ name: this.config.name,
126
+ version: this.config.version,
127
+ }, NitroStackServer.mcpServerOptions);
128
+ this.setupHandlersOn(mcp);
129
+ return mcp;
130
+ }
93
131
  /**
94
132
  * SDK-compatible legacy HTTP+SSE: GET /sse (SSEServerTransport) and POST /mcp/messages?sessionId=.
95
133
  * Streamable HTTP stays on GET/POST /mcp.
96
134
  */
135
+ async startLegacySdkSseSession(res, messagesPath) {
136
+ try {
137
+ const sessionMcp = this.createConfiguredMcpServer();
138
+ const transport = new SSEServerTransport(messagesPath, res);
139
+ const sessionId = transport.sessionId;
140
+ this.legacySdkSseSessions.set(sessionId, { server: sessionMcp, transport });
141
+ let closing = false;
142
+ transport.onclose = async () => {
143
+ if (closing) {
144
+ return;
145
+ }
146
+ closing = true;
147
+ this.legacySdkSseSessions.delete(sessionId);
148
+ try {
149
+ await sessionMcp.close();
150
+ }
151
+ catch {
152
+ // ignore
153
+ }
154
+ };
155
+ transport.onerror = (err) => {
156
+ this.logger.error('Legacy SDK SSE transport error', {
157
+ error: err instanceof Error ? err.message : String(err),
158
+ });
159
+ };
160
+ await sessionMcp.connect(transport);
161
+ }
162
+ catch (error) {
163
+ const message = error instanceof Error ? error.message : String(error);
164
+ this.logger.error('Failed to start legacy SDK SSE session', { error: message });
165
+ if (!res.headersSent) {
166
+ res.status(500).end('Failed to establish SSE connection');
167
+ }
168
+ }
169
+ }
97
170
  attachLegacySdkSseRoutes(app) {
98
171
  if (this._legacySseRoutesAttached) {
99
172
  return;
@@ -101,44 +174,8 @@ export class NitroStackServer {
101
174
  this._legacySseRoutesAttached = true;
102
175
  const LEGACY_SSE_PATH = '/sse';
103
176
  const LEGACY_MESSAGES_PATH = '/mcp/messages';
104
- app.get(LEGACY_SSE_PATH, async (req, res) => {
105
- try {
106
- const sessionMcp = new McpServer({
107
- name: this.config.name,
108
- version: this.config.version,
109
- }, NitroStackServer.mcpServerOptions);
110
- this.setupHandlersOn(sessionMcp);
111
- const transport = new SSEServerTransport(LEGACY_MESSAGES_PATH, res);
112
- const sessionId = transport.sessionId;
113
- this.legacySdkSseSessions.set(sessionId, { server: sessionMcp, transport });
114
- let closing = false;
115
- transport.onclose = async () => {
116
- if (closing) {
117
- return;
118
- }
119
- closing = true;
120
- this.legacySdkSseSessions.delete(sessionId);
121
- try {
122
- await sessionMcp.close();
123
- }
124
- catch {
125
- // ignore
126
- }
127
- };
128
- transport.onerror = (err) => {
129
- this.logger.error('Legacy SDK SSE transport error', {
130
- error: err instanceof Error ? err.message : String(err),
131
- });
132
- };
133
- await sessionMcp.connect(transport);
134
- }
135
- catch (error) {
136
- const message = error instanceof Error ? error.message : String(error);
137
- this.logger.error('Failed to start legacy SDK SSE session', { error: message });
138
- if (!res.headersSent) {
139
- res.status(500).end('Failed to establish SSE connection');
140
- }
141
- }
177
+ app.get(LEGACY_SSE_PATH, async (_req, res) => {
178
+ await this.startLegacySdkSseSession(res, LEGACY_MESSAGES_PATH);
142
179
  });
143
180
  app.post(LEGACY_MESSAGES_PATH, async (req, res) => {
144
181
  const sessionId = typeof req.query.sessionId === 'string' ? req.query.sessionId : undefined;
@@ -168,6 +205,12 @@ export class NitroStackServer {
168
205
  if (typeof transport.getApp === 'function') {
169
206
  this.attachLegacySdkSseRoutes(transport.getApp());
170
207
  }
208
+ // Cursor opens GET /mcp without mcp-session-id; fall back to legacy SSE on that path.
209
+ if (typeof transport.setLegacySseHandler === 'function') {
210
+ transport.setLegacySseHandler(async (_req, res) => {
211
+ await this.startLegacySdkSseSession(res, '/mcp/messages');
212
+ });
213
+ }
171
214
  }
172
215
  /**
173
216
  * Add a tool to the server
@@ -376,10 +419,32 @@ export class NitroStackServer {
376
419
  throw new Error(`Failed to get metadata for module ${moduleClass.name}`);
377
420
  }
378
421
  this.logger.info(`Registering module: ${metadata.name}`);
422
+ // Register providers in DI so declared-but-uninjected providers are still
423
+ // instantiated during start() (via instantiateAll) and run lifecycle hooks,
424
+ // matching the McpApplicationFactory path.
425
+ const providers = metadata.providers || [];
426
+ for (const provider of providers) {
427
+ if (typeof provider === 'function') {
428
+ DIContainer.getInstance().register(provider);
429
+ }
430
+ else if (provider && typeof provider === 'object' && 'provide' in provider) {
431
+ const token = provider.provide;
432
+ if (provider.useValue !== undefined) {
433
+ DIContainer.getInstance().registerValue(token, provider.useValue);
434
+ }
435
+ else if (provider.useClass) {
436
+ DIContainer.getInstance().register(token, provider.useClass);
437
+ }
438
+ }
439
+ }
379
440
  // Process all controllers in the module
380
441
  const controllers = metadata.controllers || [];
381
442
  for (const controller of controllers) {
382
443
  this.logger.info(` Processing controller: ${controller.name}`);
444
+ // Register the controller in DI so it is instantiated during start()
445
+ // (via instantiateAll) and participates in lifecycle hooks, matching the
446
+ // McpApplicationFactory path.
447
+ DIContainer.getInstance().register(controller);
383
448
  // Build all tools, resources, and prompts from controller
384
449
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
385
450
  const { tools, resources, prompts } = buildController(controller);
@@ -702,7 +767,7 @@ export class NitroStackServer {
702
767
  text: JSON.stringify(content, null, 2),
703
768
  };
704
769
  }
705
- const contentsMeta = buildResourceReadContentsMeta(resource.getWidgetReadMeta());
770
+ const contentsMeta = buildResourceReadContentsMeta(resource.getWidgetReadMeta?.());
706
771
  if (contentsMeta) {
707
772
  responseContent._meta = contentsMeta;
708
773
  }
@@ -814,27 +879,34 @@ export class NitroStackServer {
814
879
  const transportType = explicitTransport || (isDevelopment ? 'stdio' : 'dual');
815
880
  this._transportType = transportType;
816
881
  this.logger.debug(`NitroStackServer.start(): NODE_ENV=${process.env.NODE_ENV}, MCP_TRANSPORT_TYPE=${explicitTransport}, transportType=${transportType}`);
817
- // Call onModuleInit for all modules
882
+ // Resolve all modules so they (and their dependencies) are initialized in the DI container
818
883
  for (const moduleClass of this.modules) {
819
- const moduleInstance = DIContainer.getInstance().resolve(moduleClass);
820
- if (moduleInstance.onModuleInit) {
821
- await moduleInstance.onModuleInit();
822
- }
884
+ DIContainer.getInstance().resolve(moduleClass);
823
885
  }
824
- // If HTTP transport is needed (dual mode), set it up BEFORE calling module.start()
825
- // This allows modules like OAuthModule to register endpoints on the HTTP server
826
- if (transportType === 'dual') {
886
+ // Eagerly instantiate all registered providers/controllers so their lifecycle
887
+ // hooks fire even when they aren't injected anywhere (NestJS-style singletons).
888
+ DIContainer.getInstance().instantiateAll(this.logger);
889
+ // Call onModuleInit for currently resolved instances (NestJS: after DI resolution)
890
+ const initializedInstances = new Set(DIContainer.getInstance().getInstances());
891
+ await triggerLifecycleHook([...initializedInstances], 'onModuleInit');
892
+ // If HTTP transport is needed (dual or http mode), set it up BEFORE calling module.start()
893
+ // This allows modules like OAuthModule to register endpoints/middleware on the HTTP server
894
+ if (transportType === 'dual' || transportType === 'http') {
827
895
  const port = parseInt(process.env.PORT || '3000');
828
896
  const host = process.env.HOST || 'localhost';
829
- // Create and start HTTP transport first
897
+ // Create HTTP transport first (do not start listening yet)
830
898
  const { StreamableHttpTransport } = await import('./transports/streamable-http.js');
831
899
  const httpTransport = new StreamableHttpTransport({
832
900
  port: port,
833
901
  host: host,
834
902
  endpoint: '/mcp',
835
- enableSessions: false, // Disable sessions for dual mode
903
+ enableSessions: transportType === 'http', // Sessions ONLY in pure http mode
836
904
  enableCors: process.env.ENABLE_CORS !== 'false',
905
+ ...getStreamableHttpEnvOptions(),
837
906
  });
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());
838
910
  // Set up tools callback and server config for documentation page
839
911
  httpTransport.setToolsCallback(async () => {
840
912
  const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
@@ -845,13 +917,11 @@ export class NitroStackServer {
845
917
  version: this.config.version,
846
918
  description: this.config.description,
847
919
  });
848
- this.attachLegacySdkSseIfNeeded(httpTransport);
849
- await httpTransport.start();
850
920
  // Store HTTP transport reference BEFORE modules start
851
- // This allows OAuthModule to register discovery endpoints
921
+ // This allows OAuthModule to register discovery endpoints and middleware
852
922
  this._httpTransport = httpTransport;
853
923
  }
854
- // Call start for all modules (e.g., OAuthModule to register discovery endpoints)
924
+ // Call start for all modules (e.g., OAuthModule to register discovery endpoints and middleware)
855
925
  // Now _httpTransport is available for OAuthModule to use
856
926
  for (const moduleClass of this.modules) {
857
927
  const moduleInstance = DIContainer.getInstance().resolve(moduleClass);
@@ -859,7 +929,20 @@ export class NitroStackServer {
859
929
  await moduleInstance.start();
860
930
  }
861
931
  }
862
- // Now complete the transport setup using the determined transportType
932
+ // Init any instances registered during module.start() that missed the first pass
933
+ const allInstances = DIContainer.getInstance().getInstances();
934
+ const lateInstances = allInstances.filter((instance) => !initializedInstances.has(instance));
935
+ if (lateInstances.length > 0) {
936
+ await triggerLifecycleHook(lateInstances, 'onModuleInit');
937
+ }
938
+ // NestJS: onApplicationBootstrap runs after init, before the app accepts connections
939
+ await triggerLifecycleHook(DIContainer.getInstance().getInstances(), 'onApplicationBootstrap');
940
+ // Start HTTP listener and attach legacy SSE routes ONLY AFTER dynamic modules have finished starting
941
+ if ((transportType === 'dual' || transportType === 'http') && this._httpTransport) {
942
+ this.attachLegacySdkSseIfNeeded(this._httpTransport);
943
+ await this._httpTransport.start();
944
+ }
945
+ // Now complete the transport setup (connect MCP server) using the determined transportType
863
946
  const port = parseInt(process.env.PORT || '3000');
864
947
  const host = process.env.HOST || 'localhost';
865
948
  await this.startWithTransport(transportType, {
@@ -888,57 +971,27 @@ export class NitroStackServer {
888
971
  // DUAL transport: STDIO + HTTP SSE
889
972
  // STDIO: For direct MCP connections (dev tools, Claude Desktop)
890
973
  // HTTP SSE: For web-based clients and multiple concurrent connections
891
- // 1. Start HTTP SSE transport (reuse if already created by start())
974
+ // 1. Ensure the HTTP host is running (normally created by start()).
975
+ // The host owns /mcp and delegates protocol handling to the official SDK
976
+ // Streamable HTTP transport via the MCP server factory (one server per
977
+ // session), so no manual message forwarding is required here.
892
978
  let httpTransport = this._httpTransport;
893
979
  if (!httpTransport) {
894
980
  const { StreamableHttpTransport } = await import('./transports/streamable-http.js');
895
- httpTransport = new StreamableHttpTransport({
981
+ const transport = new StreamableHttpTransport({
896
982
  port: transportOptions?.port || 3000,
897
983
  host: transportOptions?.host || 'localhost',
898
984
  endpoint: transportOptions?.endpoint || '/mcp',
899
- enableSessions: false, // Disable sessions for simpler backward compat
900
985
  enableCors: transportOptions?.enableCors !== false, // Enable CORS by default for web clients
986
+ ...getStreamableHttpEnvOptions(),
901
987
  });
902
- this.attachLegacySdkSseIfNeeded(httpTransport);
903
- await httpTransport.start();
988
+ transport.setMcpServerFactory(() => this.createConfiguredMcpServer());
989
+ this.attachLegacySdkSseIfNeeded(transport);
990
+ await transport.start();
991
+ httpTransport = transport;
904
992
  this._httpTransport = httpTransport;
905
993
  }
906
- // Wire up HTTP transport to handle MCP messages
907
- // Since we can't connect to two transports, manually forward HTTP messages
908
- const transport = httpTransport;
909
- transport.onmessage = async (message) => {
910
- // Handle the message through the MCP server's internal handler
911
- try {
912
- // Access internal handlers - this is necessary for dual mode
913
- const mcpServerInternal = this.mcpServer;
914
- const handlers = mcpServerInternal._requestHandlers;
915
- if (handlers && message && message.method && message.id !== undefined) {
916
- const handler = handlers.get(message.method);
917
- if (handler) {
918
- const result = await handler(message);
919
- // Send response back through HTTP transport
920
- await transport.send({
921
- jsonrpc: '2.0',
922
- id: message.id,
923
- result,
924
- });
925
- }
926
- }
927
- }
928
- catch (error) {
929
- const err = error;
930
- // Send error response
931
- await transport.send({
932
- jsonrpc: '2.0',
933
- id: message.id ?? null,
934
- error: {
935
- code: -32603,
936
- message: err.message || 'Internal error',
937
- },
938
- });
939
- }
940
- };
941
- // 2. Connect MCP server via STDIO for direct connections
994
+ // 2. Connect the primary MCP server via STDIO for direct connections.
942
995
  const stdioTransport = new StdioServerTransport();
943
996
  await this.mcpServer.connect(stdioTransport);
944
997
  this.logger.info(`${this.config.name} started successfully (DUAL MODE)`);
@@ -957,9 +1010,11 @@ export class NitroStackServer {
957
1010
  port: transportOptions?.port || 3000,
958
1011
  host: transportOptions?.host || 'localhost',
959
1012
  endpoint: transportOptions?.endpoint || '/mcp',
960
- enableSessions: true,
961
1013
  enableCors: transportOptions?.enableCors || false,
1014
+ ...getStreamableHttpEnvOptions(),
962
1015
  });
1016
+ // Delegate /mcp protocol handling to the official SDK transport.
1017
+ transport.setMcpServerFactory(() => this.createConfiguredMcpServer());
963
1018
  // Set up tools callback and server config for documentation page
964
1019
  transport.setToolsCallback(async () => {
965
1020
  const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
@@ -971,13 +1026,13 @@ export class NitroStackServer {
971
1026
  description: this.config.description,
972
1027
  });
973
1028
  this.attachLegacySdkSseIfNeeded(transport);
974
- // Start HTTP server first
1029
+ // Start HTTP server
975
1030
  await transport.start();
976
1031
  httpTransport = transport;
977
1032
  this._httpTransport = httpTransport;
978
1033
  }
979
- // Then connect MCP server
980
- await this.mcpServer.connect(httpTransport);
1034
+ // The HTTP host owns /mcp and manages its own per-session MCP servers via
1035
+ // the factory; no direct mcpServer.connect() is needed here.
981
1036
  this.logger.info(`${this.config.name} started successfully (HTTP SSE transport)`);
982
1037
  this.logger.info(`✨ Mode: ${getAppMode().toUpperCase()} (via NITROSTACK_APP_MODE)`);
983
1038
  this.logger.info(`🌐 Streamable HTTP: http://${transportOptions?.host || 'localhost'}:${transportOptions?.port || 3000}${transportOptions?.endpoint || '/mcp'}`);
@@ -1098,16 +1153,77 @@ export class NitroStackServer {
1098
1153
  getTaskManager() {
1099
1154
  return this.taskManager;
1100
1155
  }
1156
+ /**
1157
+ * Register OS signal handlers so the server shuts down gracefully, running the
1158
+ * NestJS-style shutdown lifecycle hooks (beforeApplicationShutdown /
1159
+ * onApplicationShutdown) with the received signal.
1160
+ *
1161
+ * Opt-in (like NestJS `enableShutdownHooks`) to avoid attaching process
1162
+ * listeners for consumers that manage their own shutdown. Safe to call more
1163
+ * than once; handlers are only attached once per signal.
1164
+ *
1165
+ * @param signals - Signals to listen for (default: SIGTERM, SIGINT)
1166
+ */
1167
+ enableShutdownHooks(signals = ['SIGTERM', 'SIGINT']) {
1168
+ for (const signal of signals) {
1169
+ if (this._shutdownSignalHandlers.some((h) => h.signal === signal)) {
1170
+ continue;
1171
+ }
1172
+ const handler = () => {
1173
+ // stop() is idempotent, so repeated signals are safe.
1174
+ this.stop(signal).catch((error) => {
1175
+ const errorMessage = error instanceof Error ? error.message : String(error);
1176
+ this.logger.error(`Error during ${signal} shutdown`, { error: errorMessage });
1177
+ });
1178
+ };
1179
+ process.on(signal, handler);
1180
+ this._shutdownSignalHandlers.push({ signal, handler });
1181
+ }
1182
+ return this;
1183
+ }
1184
+ /**
1185
+ * Remove any process signal handlers registered via enableShutdownHooks().
1186
+ */
1187
+ removeShutdownHooks() {
1188
+ for (const { signal, handler } of this._shutdownSignalHandlers) {
1189
+ process.removeListener(signal, handler);
1190
+ }
1191
+ this._shutdownSignalHandlers = [];
1192
+ }
1101
1193
  /**
1102
1194
  * Stop the server
1195
+ *
1196
+ * Idempotent: concurrent or repeated calls (e.g. from multiple OS signals)
1197
+ * share the same in-flight teardown instead of re-running lifecycle hooks or
1198
+ * double-closing transports.
1103
1199
  */
1104
- async stop() {
1200
+ async stop(signal) {
1201
+ if (!this._stopping) {
1202
+ this._stopping = this.doStop(signal);
1203
+ }
1204
+ return this._stopping;
1205
+ }
1206
+ async doStop(signal) {
1207
+ // Detach signal handlers first so nothing re-enters teardown mid-shutdown.
1208
+ this.removeShutdownHooks();
1209
+ const instances = DIContainer.getInstance().getInstances();
1105
1210
  try {
1106
- // Call stop for all modules
1211
+ // Call onModuleDestroy for all resolved instances
1212
+ await triggerLifecycleHook(instances, 'onModuleDestroy', { safe: true, logger: this.logger });
1213
+ // Call beforeApplicationShutdown for all resolved instances
1214
+ await triggerLifecycleHook(instances, 'beforeApplicationShutdown', { safe: true, logger: this.logger }, signal);
1215
+ // Call stop for all modules. A single failing module must not prevent the
1216
+ // remaining modules (or transport teardown) from stopping.
1107
1217
  for (const moduleClass of this.modules) {
1108
1218
  const moduleInstance = DIContainer.getInstance().resolve(moduleClass);
1109
1219
  if (moduleInstance.stop) {
1110
- await moduleInstance.stop();
1220
+ try {
1221
+ await moduleInstance.stop();
1222
+ }
1223
+ catch (error) {
1224
+ const errorMessage = error instanceof Error ? error.message : String(error);
1225
+ this.logger.error(`Error stopping module ${moduleClass.name}`, { error: errorMessage });
1226
+ }
1111
1227
  }
1112
1228
  }
1113
1229
  // Destroy task manager (stops cleanup interval)
@@ -1142,6 +1258,10 @@ export class NitroStackServer {
1142
1258
  this.logger.error('Error stopping server', { error: errorMessage });
1143
1259
  throw error;
1144
1260
  }
1261
+ finally {
1262
+ // NestJS: always run after connection close attempts, even if close threw
1263
+ await triggerLifecycleHook(instances, 'onApplicationShutdown', { safe: true, logger: this.logger }, signal);
1264
+ }
1145
1265
  }
1146
1266
  /**
1147
1267
  * Get the HTTP transport (for modules that need to register endpoints)