@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.
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +17 -0
- package/dist/auth/middleware.js.map +1 -1
- package/dist/auth/token-validation.d.ts.map +1 -1
- package/dist/auth/token-validation.js +12 -2
- package/dist/auth/token-validation.js.map +1 -1
- package/dist/core/app-decorator.d.ts +6 -0
- package/dist/core/app-decorator.d.ts.map +1 -1
- package/dist/core/app-decorator.js +5 -3
- package/dist/core/app-decorator.js.map +1 -1
- package/dist/core/builders.d.ts.map +1 -1
- package/dist/core/builders.js +14 -4
- package/dist/core/builders.js.map +1 -1
- package/dist/core/decorators.d.ts +30 -0
- package/dist/core/decorators.d.ts.map +1 -1
- package/dist/core/decorators.js +42 -4
- package/dist/core/decorators.js.map +1 -1
- package/dist/core/di/container.d.ts +18 -0
- package/dist/core/di/container.d.ts.map +1 -1
- package/dist/core/di/container.js +31 -0
- package/dist/core/di/container.js.map +1 -1
- package/dist/core/di/injectable.decorator.d.ts.map +1 -1
- package/dist/core/di/injectable.decorator.js +2 -1
- package/dist/core/di/injectable.decorator.js.map +1 -1
- package/dist/core/events/event.decorator.d.ts.map +1 -1
- package/dist/core/events/event.decorator.js +2 -1
- package/dist/core/events/event.decorator.js.map +1 -1
- package/dist/core/index.d.ts +4 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/lifecycle.d.ts +44 -0
- package/dist/core/lifecycle.d.ts.map +1 -0
- package/dist/core/lifecycle.js +62 -0
- package/dist/core/lifecycle.js.map +1 -0
- package/dist/core/oauth-module.d.ts +76 -5
- package/dist/core/oauth-module.d.ts.map +1 -1
- package/dist/core/oauth-module.js +343 -83
- package/dist/core/oauth-module.js.map +1 -1
- package/dist/core/pipes/pipe.decorator.d.ts.map +1 -1
- package/dist/core/pipes/pipe.decorator.js +2 -1
- package/dist/core/pipes/pipe.decorator.js.map +1 -1
- package/dist/core/prompt.d.ts +0 -3
- package/dist/core/prompt.d.ts.map +1 -1
- package/dist/core/prompt.js +28 -1
- package/dist/core/prompt.js.map +1 -1
- package/dist/core/server.d.ts +39 -26
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +222 -102
- package/dist/core/server.js.map +1 -1
- package/dist/core/transports/streamable-http.d.ts +64 -61
- package/dist/core/transports/streamable-http.d.ts.map +1 -1
- package/dist/core/transports/streamable-http.js +1591 -823
- package/dist/core/transports/streamable-http.js.map +1 -1
- package/package.json +5 -7
package/dist/core/prompt.js
CHANGED
|
@@ -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
|
|
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
|
});
|
package/dist/core/prompt.js.map
CHANGED
|
@@ -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,
|
|
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,8DAA8D;AAC9D,8EAA8E;AAC9E,gFAAgF;AAChF,iDAAiD;AACjD,SAAS,qBAAqB,CAAC,GAAQ;IACrC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,eAAe,CAAC,0DAA0D,CAAC,CAAC;IACxF,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,eAAe,CAAC,iCAAiC,GAAG,CAAC,IAAI,6CAA6C,CAAC,CAAC;IACpH,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,eAAe,CAAC,0DAA0D,CAAC,CAAC;IACxF,CAAC;IACD,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,OAAO,EAAE,GAAG,CAAC,OAAO;KACrB,CAAC;AACJ,CAAC;AAED,SAAS,uBAAuB,CAAC,MAAe;IAC9C,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC5C,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,MAAM,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC9D,OAAO,WAAW,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;AAChD,CAAC;AAED,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,cAAc,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACpE,MAAM,QAAQ,GAAG,uBAAuB,CAAC,cAAc,CAAC,CAAC;YAEzD,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"}
|
package/dist/core/server.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
14
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/core/server.ts"],"names":[],"mappings":"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"}
|
package/dist/core/server.js
CHANGED
|
@@ -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 (
|
|
105
|
-
|
|
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
|
-
//
|
|
882
|
+
// Resolve all modules so they (and their dependencies) are initialized in the DI container
|
|
818
883
|
for (const moduleClass of this.modules) {
|
|
819
|
-
|
|
820
|
-
if (moduleInstance.onModuleInit) {
|
|
821
|
-
await moduleInstance.onModuleInit();
|
|
822
|
-
}
|
|
884
|
+
DIContainer.getInstance().resolve(moduleClass);
|
|
823
885
|
}
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
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.
|
|
903
|
-
|
|
988
|
+
transport.setMcpServerFactory(() => this.createConfiguredMcpServer());
|
|
989
|
+
this.attachLegacySdkSseIfNeeded(transport);
|
|
990
|
+
await transport.start();
|
|
991
|
+
httpTransport = transport;
|
|
904
992
|
this._httpTransport = httpTransport;
|
|
905
993
|
}
|
|
906
|
-
//
|
|
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
|
|
1029
|
+
// Start HTTP server
|
|
975
1030
|
await transport.start();
|
|
976
1031
|
httpTransport = transport;
|
|
977
1032
|
this._httpTransport = httpTransport;
|
|
978
1033
|
}
|
|
979
|
-
//
|
|
980
|
-
|
|
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
|
|
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
|
-
|
|
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)
|