@nitrostack/core 1.0.10 → 1.0.12

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.
@@ -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) {
@@ -40,7 +66,7 @@ export class Prompt {
40
66
  context.logger.info(`Executing prompt: ${this.name}`, { args: args });
41
67
  try {
42
68
  const messagesResult = await this.definition.handler(args, context);
43
- const messages = Array.isArray(messagesResult) ? messagesResult : [messagesResult];
69
+ const messages = normalizePromptResponse(messagesResult);
44
70
  context.logger.info(`Prompt executed successfully: ${this.name}`, {
45
71
  messageCount: messages.length,
46
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,cAAc,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;YACpE,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC;YAEnF,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":"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"}
@@ -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
  */
@@ -71,10 +51,18 @@ export declare class NitroStackServer {
71
51
  constructor(config?: McpServerConfig);
72
52
  /** Shared MCP server constructor options (main + per legacy SSE session) */
73
53
  private static readonly mcpServerOptions;
54
+ /**
55
+ * Build a fresh MCP server instance wired with all of this application's
56
+ * tool/resource/prompt handlers. Used for transports that require their own
57
+ * server instance (legacy SSE sessions, official Streamable HTTP sessions),
58
+ * since a single SDK server can only be connected to one transport at a time.
59
+ */
60
+ createConfiguredMcpServer(): McpServer;
74
61
  /**
75
62
  * SDK-compatible legacy HTTP+SSE: GET /sse (SSEServerTransport) and POST /mcp/messages?sessionId=.
76
63
  * Streamable HTTP stays on GET/POST /mcp.
77
64
  */
65
+ private startLegacySdkSseSession;
78
66
  private attachLegacySdkSseRoutes;
79
67
  private attachLegacySdkSseIfNeeded;
80
68
  /**
@@ -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,19 @@ 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
+ /** Optional Streamable HTTP session limits from environment variables. */
16
+ function getStreamableHttpEnvOptions() {
17
+ const maxSessions = process.env.MCP_MAX_SESSIONS
18
+ ? parseInt(process.env.MCP_MAX_SESSIONS, 10)
19
+ : undefined;
20
+ const sessionTimeout = process.env.MCP_SESSION_TIMEOUT_MS
21
+ ? parseInt(process.env.MCP_SESSION_TIMEOUT_MS, 10)
22
+ : undefined;
23
+ return {
24
+ ...(maxSessions !== undefined && !Number.isNaN(maxSessions) ? { maxSessions } : {}),
25
+ ...(sessionTimeout !== undefined && !Number.isNaN(sessionTimeout) ? { sessionTimeout } : {}),
26
+ };
27
+ }
15
28
  /**
16
29
  * NitroStackServer - Main server class
17
30
  */
@@ -48,6 +61,12 @@ export class NitroStackServer {
48
61
  name: 'nitrostack-server',
49
62
  version: '1.0.0',
50
63
  };
64
+ // Register itself in DI container so modules can inject the server.
65
+ // NOTE: DIContainer is a process-wide singleton, so this assumes a single
66
+ // NitroStackServer instance per process. Multiple instances would overwrite
67
+ // each other's registration here.
68
+ DIContainer.getInstance().registerValue(NitroStackServer, this);
69
+ DIContainer.getInstance().registerValue('NitroStackServer', this);
51
70
  this.logger = createLogger({
52
71
  level: this.config.logging?.level || 'info',
53
72
  file: this.config.logging?.file,
@@ -90,10 +109,59 @@ export class NitroStackServer {
90
109
  },
91
110
  },
92
111
  };
112
+ /**
113
+ * Build a fresh MCP server instance wired with all of this application's
114
+ * tool/resource/prompt handlers. Used for transports that require their own
115
+ * server instance (legacy SSE sessions, official Streamable HTTP sessions),
116
+ * since a single SDK server can only be connected to one transport at a time.
117
+ */
118
+ createConfiguredMcpServer() {
119
+ const mcp = new McpServer({
120
+ name: this.config.name,
121
+ version: this.config.version,
122
+ }, NitroStackServer.mcpServerOptions);
123
+ this.setupHandlersOn(mcp);
124
+ return mcp;
125
+ }
93
126
  /**
94
127
  * SDK-compatible legacy HTTP+SSE: GET /sse (SSEServerTransport) and POST /mcp/messages?sessionId=.
95
128
  * Streamable HTTP stays on GET/POST /mcp.
96
129
  */
130
+ async startLegacySdkSseSession(res, messagesPath) {
131
+ try {
132
+ const sessionMcp = this.createConfiguredMcpServer();
133
+ const transport = new SSEServerTransport(messagesPath, res);
134
+ const sessionId = transport.sessionId;
135
+ this.legacySdkSseSessions.set(sessionId, { server: sessionMcp, transport });
136
+ let closing = false;
137
+ transport.onclose = async () => {
138
+ if (closing) {
139
+ return;
140
+ }
141
+ closing = true;
142
+ this.legacySdkSseSessions.delete(sessionId);
143
+ try {
144
+ await sessionMcp.close();
145
+ }
146
+ catch {
147
+ // ignore
148
+ }
149
+ };
150
+ transport.onerror = (err) => {
151
+ this.logger.error('Legacy SDK SSE transport error', {
152
+ error: err instanceof Error ? err.message : String(err),
153
+ });
154
+ };
155
+ await sessionMcp.connect(transport);
156
+ }
157
+ catch (error) {
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ this.logger.error('Failed to start legacy SDK SSE session', { error: message });
160
+ if (!res.headersSent) {
161
+ res.status(500).end('Failed to establish SSE connection');
162
+ }
163
+ }
164
+ }
97
165
  attachLegacySdkSseRoutes(app) {
98
166
  if (this._legacySseRoutesAttached) {
99
167
  return;
@@ -101,44 +169,8 @@ export class NitroStackServer {
101
169
  this._legacySseRoutesAttached = true;
102
170
  const LEGACY_SSE_PATH = '/sse';
103
171
  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
- }
172
+ app.get(LEGACY_SSE_PATH, async (_req, res) => {
173
+ await this.startLegacySdkSseSession(res, LEGACY_MESSAGES_PATH);
142
174
  });
143
175
  app.post(LEGACY_MESSAGES_PATH, async (req, res) => {
144
176
  const sessionId = typeof req.query.sessionId === 'string' ? req.query.sessionId : undefined;
@@ -168,6 +200,12 @@ export class NitroStackServer {
168
200
  if (typeof transport.getApp === 'function') {
169
201
  this.attachLegacySdkSseRoutes(transport.getApp());
170
202
  }
203
+ // Cursor opens GET /mcp without mcp-session-id; fall back to legacy SSE on that path.
204
+ if (typeof transport.setLegacySseHandler === 'function') {
205
+ transport.setLegacySseHandler(async (_req, res) => {
206
+ await this.startLegacySdkSseSession(res, '/mcp/messages');
207
+ });
208
+ }
171
209
  }
172
210
  /**
173
211
  * Add a tool to the server
@@ -702,7 +740,7 @@ export class NitroStackServer {
702
740
  text: JSON.stringify(content, null, 2),
703
741
  };
704
742
  }
705
- const contentsMeta = buildResourceReadContentsMeta(resource.getWidgetReadMeta());
743
+ const contentsMeta = buildResourceReadContentsMeta(resource.getWidgetReadMeta?.());
706
744
  if (contentsMeta) {
707
745
  responseContent._meta = contentsMeta;
708
746
  }
@@ -810,8 +848,8 @@ export class NitroStackServer {
810
848
  // On Windows, NODE_ENV might not be passed correctly, so we're more lenient
811
849
  const nodeEnv = process.env.NODE_ENV?.toLowerCase();
812
850
  const isDevelopment = nodeEnv === 'development' || nodeEnv === 'dev' || !nodeEnv;
813
- // Use explicit transport if set, respect decorator setting, otherwise default to dual mode
814
- const transportType = explicitTransport || this._transportType || 'dual';
851
+ // Use explicit transport if set, otherwise infer from NODE_ENV
852
+ const transportType = explicitTransport || (isDevelopment ? 'stdio' : 'dual');
815
853
  this._transportType = transportType;
816
854
  this.logger.debug(`NitroStackServer.start(): NODE_ENV=${process.env.NODE_ENV}, MCP_TRANSPORT_TYPE=${explicitTransport}, transportType=${transportType}`);
817
855
  // Call onModuleInit for all modules
@@ -821,20 +859,24 @@ export class NitroStackServer {
821
859
  await moduleInstance.onModuleInit();
822
860
  }
823
861
  }
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') {
862
+ // If HTTP transport is needed (dual or http mode), set it up BEFORE calling module.start()
863
+ // This allows modules like OAuthModule to register endpoints/middleware on the HTTP server
864
+ if (transportType === 'dual' || transportType === 'http') {
827
865
  const port = parseInt(process.env.PORT || '3000');
828
866
  const host = process.env.HOST || 'localhost';
829
- // Create and start HTTP transport first
867
+ // Create HTTP transport first (do not start listening yet)
830
868
  const { StreamableHttpTransport } = await import('./transports/streamable-http.js');
831
869
  const httpTransport = new StreamableHttpTransport({
832
870
  port: port,
833
871
  host: host,
834
872
  endpoint: '/mcp',
835
- enableSessions: false, // Disable sessions for dual mode
873
+ enableSessions: transportType === 'http', // Sessions ONLY in pure http mode
836
874
  enableCors: process.env.ENABLE_CORS !== 'false',
875
+ ...getStreamableHttpEnvOptions(),
837
876
  });
877
+ // Delegate /mcp protocol handling to the official SDK transport: each
878
+ // session gets its own configured MCP server built via this factory.
879
+ httpTransport.setMcpServerFactory(() => this.createConfiguredMcpServer());
838
880
  // Set up tools callback and server config for documentation page
839
881
  httpTransport.setToolsCallback(async () => {
840
882
  const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
@@ -845,13 +887,11 @@ export class NitroStackServer {
845
887
  version: this.config.version,
846
888
  description: this.config.description,
847
889
  });
848
- this.attachLegacySdkSseIfNeeded(httpTransport);
849
- await httpTransport.start();
850
890
  // Store HTTP transport reference BEFORE modules start
851
- // This allows OAuthModule to register discovery endpoints
891
+ // This allows OAuthModule to register discovery endpoints and middleware
852
892
  this._httpTransport = httpTransport;
853
893
  }
854
- // Call start for all modules (e.g., OAuthModule to register discovery endpoints)
894
+ // Call start for all modules (e.g., OAuthModule to register discovery endpoints and middleware)
855
895
  // Now _httpTransport is available for OAuthModule to use
856
896
  for (const moduleClass of this.modules) {
857
897
  const moduleInstance = DIContainer.getInstance().resolve(moduleClass);
@@ -859,7 +899,12 @@ export class NitroStackServer {
859
899
  await moduleInstance.start();
860
900
  }
861
901
  }
862
- // Now complete the transport setup using the determined transportType
902
+ // Start HTTP listener and attach legacy SSE routes ONLY AFTER dynamic modules have finished starting
903
+ if ((transportType === 'dual' || transportType === 'http') && this._httpTransport) {
904
+ this.attachLegacySdkSseIfNeeded(this._httpTransport);
905
+ await this._httpTransport.start();
906
+ }
907
+ // Now complete the transport setup (connect MCP server) using the determined transportType
863
908
  const port = parseInt(process.env.PORT || '3000');
864
909
  const host = process.env.HOST || 'localhost';
865
910
  await this.startWithTransport(transportType, {
@@ -888,57 +933,27 @@ export class NitroStackServer {
888
933
  // DUAL transport: STDIO + HTTP SSE
889
934
  // STDIO: For direct MCP connections (dev tools, Claude Desktop)
890
935
  // HTTP SSE: For web-based clients and multiple concurrent connections
891
- // 1. Start HTTP SSE transport (reuse if already created by start())
936
+ // 1. Ensure the HTTP host is running (normally created by start()).
937
+ // The host owns /mcp and delegates protocol handling to the official SDK
938
+ // Streamable HTTP transport via the MCP server factory (one server per
939
+ // session), so no manual message forwarding is required here.
892
940
  let httpTransport = this._httpTransport;
893
941
  if (!httpTransport) {
894
942
  const { StreamableHttpTransport } = await import('./transports/streamable-http.js');
895
- httpTransport = new StreamableHttpTransport({
943
+ const transport = new StreamableHttpTransport({
896
944
  port: transportOptions?.port || 3000,
897
945
  host: transportOptions?.host || 'localhost',
898
946
  endpoint: transportOptions?.endpoint || '/mcp',
899
- enableSessions: false, // Disable sessions for simpler backward compat
900
947
  enableCors: transportOptions?.enableCors !== false, // Enable CORS by default for web clients
948
+ ...getStreamableHttpEnvOptions(),
901
949
  });
902
- this.attachLegacySdkSseIfNeeded(httpTransport);
903
- await httpTransport.start();
950
+ transport.setMcpServerFactory(() => this.createConfiguredMcpServer());
951
+ this.attachLegacySdkSseIfNeeded(transport);
952
+ await transport.start();
953
+ httpTransport = transport;
904
954
  this._httpTransport = httpTransport;
905
955
  }
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
956
+ // 2. Connect the primary MCP server via STDIO for direct connections.
942
957
  const stdioTransport = new StdioServerTransport();
943
958
  await this.mcpServer.connect(stdioTransport);
944
959
  this.logger.info(`${this.config.name} started successfully (DUAL MODE)`);
@@ -957,9 +972,11 @@ export class NitroStackServer {
957
972
  port: transportOptions?.port || 3000,
958
973
  host: transportOptions?.host || 'localhost',
959
974
  endpoint: transportOptions?.endpoint || '/mcp',
960
- enableSessions: true,
961
975
  enableCors: transportOptions?.enableCors || false,
976
+ ...getStreamableHttpEnvOptions(),
962
977
  });
978
+ // Delegate /mcp protocol handling to the official SDK transport.
979
+ transport.setMcpServerFactory(() => this.createConfiguredMcpServer());
963
980
  // Set up tools callback and server config for documentation page
964
981
  transport.setToolsCallback(async () => {
965
982
  const tools = await Promise.all(Array.from(this.tools.values()).map((tool) => tool.toMcpTool()));
@@ -971,13 +988,13 @@ export class NitroStackServer {
971
988
  description: this.config.description,
972
989
  });
973
990
  this.attachLegacySdkSseIfNeeded(transport);
974
- // Start HTTP server first
991
+ // Start HTTP server
975
992
  await transport.start();
976
993
  httpTransport = transport;
977
994
  this._httpTransport = httpTransport;
978
995
  }
979
- // Then connect MCP server
980
- await this.mcpServer.connect(httpTransport);
996
+ // The HTTP host owns /mcp and manages its own per-session MCP servers via
997
+ // the factory; no direct mcpServer.connect() is needed here.
981
998
  this.logger.info(`${this.config.name} started successfully (HTTP SSE transport)`);
982
999
  this.logger.info(`✨ Mode: ${getAppMode().toUpperCase()} (via NITROSTACK_APP_MODE)`);
983
1000
  this.logger.info(`🌐 Streamable HTTP: http://${transportOptions?.host || 'localhost'}:${transportOptions?.port || 3000}${transportOptions?.endpoint || '/mcp'}`);