@godscene/shared 1.7.11

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 (236) hide show
  1. package/README.md +9 -0
  2. package/dist/es/baseDB.mjs +109 -0
  3. package/dist/es/cli/cli-args.mjs +95 -0
  4. package/dist/es/cli/cli-error.mjs +24 -0
  5. package/dist/es/cli/cli-runner.mjs +122 -0
  6. package/dist/es/cli/index.mjs +4 -0
  7. package/dist/es/common.mjs +37 -0
  8. package/dist/es/constants/example-code.mjs +227 -0
  9. package/dist/es/constants/index.mjs +124 -0
  10. package/dist/es/env/basic.mjs +6 -0
  11. package/dist/es/env/constants.mjs +110 -0
  12. package/dist/es/env/global-config-manager.mjs +94 -0
  13. package/dist/es/env/helper.mjs +43 -0
  14. package/dist/es/env/index.mjs +5 -0
  15. package/dist/es/env/init-debug.mjs +18 -0
  16. package/dist/es/env/model-config-manager.mjs +79 -0
  17. package/dist/es/env/parse-model-config.mjs +165 -0
  18. package/dist/es/env/types.mjs +232 -0
  19. package/dist/es/env/utils.mjs +18 -0
  20. package/dist/es/extractor/constants.mjs +2 -0
  21. package/dist/es/extractor/cs_postmessage.mjs +61 -0
  22. package/dist/es/extractor/customLocator.mjs +641 -0
  23. package/dist/es/extractor/debug.mjs +6 -0
  24. package/dist/es/extractor/dom-util.mjs +96 -0
  25. package/dist/es/extractor/index.mjs +5 -0
  26. package/dist/es/extractor/locator.mjs +250 -0
  27. package/dist/es/extractor/tree.mjs +78 -0
  28. package/dist/es/extractor/util.mjs +245 -0
  29. package/dist/es/extractor/web-extractor.mjs +393 -0
  30. package/dist/es/img/box-select.mjs +824 -0
  31. package/dist/es/img/canvas-fallback.mjs +238 -0
  32. package/dist/es/img/get-photon.mjs +45 -0
  33. package/dist/es/img/get-sharp.mjs +11 -0
  34. package/dist/es/img/index.mjs +4 -0
  35. package/dist/es/img/info.mjs +35 -0
  36. package/dist/es/img/transform.mjs +275 -0
  37. package/dist/es/index.mjs +2 -0
  38. package/dist/es/key-alias-utils.mjs +19 -0
  39. package/dist/es/logger.mjs +64 -0
  40. package/dist/es/mcp/base-server.mjs +282 -0
  41. package/dist/es/mcp/base-tools.mjs +159 -0
  42. package/dist/es/mcp/chrome-path.mjs +35 -0
  43. package/dist/es/mcp/cli-report-session.mjs +78 -0
  44. package/dist/es/mcp/error-formatter.mjs +19 -0
  45. package/dist/es/mcp/index.mjs +9 -0
  46. package/dist/es/mcp/init-arg-utils.mjs +38 -0
  47. package/dist/es/mcp/inject-report-html-plugin.mjs +53 -0
  48. package/dist/es/mcp/launcher-helper.mjs +52 -0
  49. package/dist/es/mcp/tool-generator.mjs +419 -0
  50. package/dist/es/mcp/types.mjs +3 -0
  51. package/dist/es/node/fs.mjs +44 -0
  52. package/dist/es/node/index.mjs +2 -0
  53. package/dist/es/node/port.mjs +24 -0
  54. package/dist/es/polyfills/async-hooks.mjs +2 -0
  55. package/dist/es/polyfills/index.mjs +1 -0
  56. package/dist/es/types/index.mjs +3 -0
  57. package/dist/es/us-keyboard-layout.mjs +1414 -0
  58. package/dist/es/us-keyboard-layout.mjs.LICENSE.txt +5 -0
  59. package/dist/es/utils.mjs +72 -0
  60. package/dist/es/zod-schema-utils.mjs +54 -0
  61. package/dist/lib/baseDB.js +149 -0
  62. package/dist/lib/cli/cli-args.js +138 -0
  63. package/dist/lib/cli/cli-error.js +61 -0
  64. package/dist/lib/cli/cli-runner.js +181 -0
  65. package/dist/lib/cli/index.js +53 -0
  66. package/dist/lib/common.js +93 -0
  67. package/dist/lib/constants/example-code.js +264 -0
  68. package/dist/lib/constants/index.js +221 -0
  69. package/dist/lib/env/basic.js +40 -0
  70. package/dist/lib/env/constants.js +153 -0
  71. package/dist/lib/env/global-config-manager.js +128 -0
  72. package/dist/lib/env/helper.js +80 -0
  73. package/dist/lib/env/index.js +90 -0
  74. package/dist/lib/env/init-debug.js +52 -0
  75. package/dist/lib/env/model-config-manager.js +113 -0
  76. package/dist/lib/env/parse-model-config.js +211 -0
  77. package/dist/lib/env/types.js +572 -0
  78. package/dist/lib/env/utils.js +61 -0
  79. package/dist/lib/extractor/constants.js +42 -0
  80. package/dist/lib/extractor/cs_postmessage.js +98 -0
  81. package/dist/lib/extractor/customLocator.js +693 -0
  82. package/dist/lib/extractor/debug.js +12 -0
  83. package/dist/lib/extractor/dom-util.js +157 -0
  84. package/dist/lib/extractor/index.js +87 -0
  85. package/dist/lib/extractor/locator.js +296 -0
  86. package/dist/lib/extractor/tree.js +124 -0
  87. package/dist/lib/extractor/util.js +336 -0
  88. package/dist/lib/extractor/web-extractor.js +442 -0
  89. package/dist/lib/img/box-select.js +875 -0
  90. package/dist/lib/img/canvas-fallback.js +305 -0
  91. package/dist/lib/img/get-photon.js +82 -0
  92. package/dist/lib/img/get-sharp.js +45 -0
  93. package/dist/lib/img/index.js +95 -0
  94. package/dist/lib/img/info.js +92 -0
  95. package/dist/lib/img/transform.js +364 -0
  96. package/dist/lib/index.js +36 -0
  97. package/dist/lib/key-alias-utils.js +62 -0
  98. package/dist/lib/logger.js +114 -0
  99. package/dist/lib/mcp/base-server.js +332 -0
  100. package/dist/lib/mcp/base-tools.js +193 -0
  101. package/dist/lib/mcp/chrome-path.js +72 -0
  102. package/dist/lib/mcp/cli-report-session.js +121 -0
  103. package/dist/lib/mcp/error-formatter.js +53 -0
  104. package/dist/lib/mcp/index.js +114 -0
  105. package/dist/lib/mcp/init-arg-utils.js +78 -0
  106. package/dist/lib/mcp/inject-report-html-plugin.js +98 -0
  107. package/dist/lib/mcp/launcher-helper.js +86 -0
  108. package/dist/lib/mcp/tool-generator.js +456 -0
  109. package/dist/lib/mcp/types.js +40 -0
  110. package/dist/lib/node/fs.js +97 -0
  111. package/dist/lib/node/index.js +65 -0
  112. package/dist/lib/node/port.js +61 -0
  113. package/dist/lib/polyfills/async-hooks.js +36 -0
  114. package/dist/lib/polyfills/index.js +58 -0
  115. package/dist/lib/types/index.js +37 -0
  116. package/dist/lib/us-keyboard-layout.js +1457 -0
  117. package/dist/lib/us-keyboard-layout.js.LICENSE.txt +5 -0
  118. package/dist/lib/utils.js +148 -0
  119. package/dist/lib/zod-schema-utils.js +97 -0
  120. package/dist/types/baseDB.d.ts +25 -0
  121. package/dist/types/cli/cli-args.d.ts +8 -0
  122. package/dist/types/cli/cli-error.d.ts +5 -0
  123. package/dist/types/cli/cli-runner.d.ts +19 -0
  124. package/dist/types/cli/index.d.ts +4 -0
  125. package/dist/types/common.d.ts +12 -0
  126. package/dist/types/constants/example-code.d.ts +2 -0
  127. package/dist/types/constants/index.d.ts +61 -0
  128. package/dist/types/env/basic.d.ts +6 -0
  129. package/dist/types/env/constants.d.ts +50 -0
  130. package/dist/types/env/global-config-manager.d.ts +32 -0
  131. package/dist/types/env/helper.d.ts +4 -0
  132. package/dist/types/env/index.d.ts +4 -0
  133. package/dist/types/env/init-debug.d.ts +1 -0
  134. package/dist/types/env/model-config-manager.d.ts +25 -0
  135. package/dist/types/env/parse-model-config.d.ts +31 -0
  136. package/dist/types/env/types.d.ts +339 -0
  137. package/dist/types/env/utils.d.ts +7 -0
  138. package/dist/types/extractor/constants.d.ts +1 -0
  139. package/dist/types/extractor/cs_postmessage.d.ts +2 -0
  140. package/dist/types/extractor/customLocator.d.ts +69 -0
  141. package/dist/types/extractor/debug.d.ts +1 -0
  142. package/dist/types/extractor/dom-util.d.ts +57 -0
  143. package/dist/types/extractor/index.d.ts +33 -0
  144. package/dist/types/extractor/locator.d.ts +9 -0
  145. package/dist/types/extractor/tree.d.ts +6 -0
  146. package/dist/types/extractor/util.d.ts +47 -0
  147. package/dist/types/extractor/web-extractor.d.ts +24 -0
  148. package/dist/types/img/box-select.d.ts +26 -0
  149. package/dist/types/img/canvas-fallback.d.ts +105 -0
  150. package/dist/types/img/get-photon.d.ts +19 -0
  151. package/dist/types/img/get-sharp.d.ts +3 -0
  152. package/dist/types/img/index.d.ts +3 -0
  153. package/dist/types/img/info.d.ts +34 -0
  154. package/dist/types/img/transform.d.ts +98 -0
  155. package/dist/types/index.d.ts +2 -0
  156. package/dist/types/key-alias-utils.d.ts +9 -0
  157. package/dist/types/logger.d.ts +5 -0
  158. package/dist/types/mcp/base-server.d.ts +93 -0
  159. package/dist/types/mcp/base-tools.d.ts +148 -0
  160. package/dist/types/mcp/chrome-path.d.ts +2 -0
  161. package/dist/types/mcp/cli-report-session.d.ts +12 -0
  162. package/dist/types/mcp/error-formatter.d.ts +12 -0
  163. package/dist/types/mcp/index.d.ts +9 -0
  164. package/dist/types/mcp/init-arg-utils.d.ts +13 -0
  165. package/dist/types/mcp/inject-report-html-plugin.d.ts +18 -0
  166. package/dist/types/mcp/launcher-helper.d.ts +94 -0
  167. package/dist/types/mcp/tool-generator.d.ts +10 -0
  168. package/dist/types/mcp/types.d.ts +113 -0
  169. package/dist/types/node/fs.d.ts +15 -0
  170. package/dist/types/node/index.d.ts +2 -0
  171. package/dist/types/node/port.d.ts +8 -0
  172. package/dist/types/polyfills/async-hooks.d.ts +6 -0
  173. package/dist/types/polyfills/index.d.ts +4 -0
  174. package/dist/types/types/index.d.ts +36 -0
  175. package/dist/types/us-keyboard-layout.d.ts +32 -0
  176. package/dist/types/utils.d.ts +34 -0
  177. package/dist/types/zod-schema-utils.d.ts +23 -0
  178. package/package.json +125 -0
  179. package/src/baseDB.ts +158 -0
  180. package/src/cli/cli-args.ts +173 -0
  181. package/src/cli/cli-error.ts +24 -0
  182. package/src/cli/cli-runner.ts +230 -0
  183. package/src/cli/index.ts +4 -0
  184. package/src/common.ts +67 -0
  185. package/src/constants/example-code.ts +227 -0
  186. package/src/constants/index.ts +139 -0
  187. package/src/env/basic.ts +12 -0
  188. package/src/env/constants.ts +303 -0
  189. package/src/env/global-config-manager.ts +191 -0
  190. package/src/env/helper.ts +58 -0
  191. package/src/env/index.ts +4 -0
  192. package/src/env/init-debug.ts +34 -0
  193. package/src/env/model-config-manager.ts +149 -0
  194. package/src/env/parse-model-config.ts +357 -0
  195. package/src/env/types.ts +583 -0
  196. package/src/env/utils.ts +39 -0
  197. package/src/extractor/constants.ts +5 -0
  198. package/src/extractor/cs_postmessage.ts +136 -0
  199. package/src/extractor/customLocator.ts +1245 -0
  200. package/src/extractor/debug.ts +10 -0
  201. package/src/extractor/dom-util.ts +231 -0
  202. package/src/extractor/index.ts +50 -0
  203. package/src/extractor/locator.ts +469 -0
  204. package/src/extractor/tree.ts +179 -0
  205. package/src/extractor/util.ts +482 -0
  206. package/src/extractor/web-extractor.ts +617 -0
  207. package/src/img/box-select.ts +588 -0
  208. package/src/img/canvas-fallback.ts +393 -0
  209. package/src/img/get-photon.ts +108 -0
  210. package/src/img/get-sharp.ts +18 -0
  211. package/src/img/index.ts +27 -0
  212. package/src/img/info.ts +102 -0
  213. package/src/img/transform.ts +553 -0
  214. package/src/index.ts +1 -0
  215. package/src/key-alias-utils.ts +23 -0
  216. package/src/logger.ts +96 -0
  217. package/src/mcp/base-server.ts +500 -0
  218. package/src/mcp/base-tools.ts +391 -0
  219. package/src/mcp/chrome-path.ts +48 -0
  220. package/src/mcp/cli-report-session.ts +130 -0
  221. package/src/mcp/error-formatter.ts +52 -0
  222. package/src/mcp/index.ts +9 -0
  223. package/src/mcp/init-arg-utils.ts +105 -0
  224. package/src/mcp/inject-report-html-plugin.ts +119 -0
  225. package/src/mcp/launcher-helper.ts +200 -0
  226. package/src/mcp/tool-generator.ts +658 -0
  227. package/src/mcp/types.ts +131 -0
  228. package/src/node/fs.ts +84 -0
  229. package/src/node/index.ts +2 -0
  230. package/src/node/port.ts +37 -0
  231. package/src/polyfills/async-hooks.ts +6 -0
  232. package/src/polyfills/index.ts +4 -0
  233. package/src/types/index.ts +54 -0
  234. package/src/us-keyboard-layout.ts +723 -0
  235. package/src/utils.ts +149 -0
  236. package/src/zod-schema-utils.ts +133 -0
@@ -0,0 +1,500 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { ParseArgsConfig } from 'node:util';
3
+ import { setIsMcp } from '@godscene/shared/utils';
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
+ import express, {
8
+ type Application,
9
+ type Request,
10
+ type Response,
11
+ } from 'express';
12
+ import { getErrorMessage } from './error-formatter';
13
+ import type { IMidsceneTools } from './types';
14
+
15
+ export interface BaseMCPServerConfig {
16
+ name: string;
17
+ version: string;
18
+ description: string;
19
+ }
20
+
21
+ export interface HttpLaunchOptions {
22
+ port: number;
23
+ host?: string;
24
+ }
25
+
26
+ export interface LaunchMCPServerResult {
27
+ /**
28
+ * The MCP server port (for HTTP mode)
29
+ */
30
+ port?: number;
31
+
32
+ /**
33
+ * The server host (for HTTP mode)
34
+ */
35
+ host?: string;
36
+
37
+ /**
38
+ * Function to gracefully shutdown the MCP server
39
+ */
40
+ close: () => Promise<void>;
41
+ }
42
+
43
+ interface SessionData {
44
+ transport: StreamableHTTPServerTransport;
45
+ createdAt: Date;
46
+ lastAccessedAt: Date;
47
+ }
48
+
49
+ /**
50
+ * CLI argument configuration for MCP servers
51
+ */
52
+ export const CLI_ARGS_CONFIG: ParseArgsConfig['options'] = {
53
+ mode: { type: 'string', default: 'stdio' },
54
+ port: { type: 'string', default: '3000' },
55
+ host: { type: 'string', default: 'localhost' },
56
+ };
57
+
58
+ export interface CLIArgs {
59
+ mode?: string;
60
+ port?: string;
61
+ host?: string;
62
+ }
63
+
64
+ /**
65
+ * Launch an MCP server based on CLI arguments
66
+ * Shared helper to reduce duplication across platform CLI entry points
67
+ */
68
+ export function launchMCPServer(
69
+ server: BaseMCPServer,
70
+ args: CLIArgs,
71
+ ): Promise<LaunchMCPServerResult> {
72
+ if (args.mode === 'http') {
73
+ return server.launchHttp({
74
+ port: Number.parseInt(args.port || '3000', 10),
75
+ host: args.host || 'localhost',
76
+ });
77
+ }
78
+ return server.launch();
79
+ }
80
+
81
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
82
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
83
+ const MAX_SESSIONS = 100; // Maximum concurrent sessions to prevent DoS
84
+
85
+ /**
86
+ * Base MCP Server class with programmatic launch() API
87
+ * Each platform extends this to provide their own tools manager
88
+ */
89
+ export abstract class BaseMCPServer {
90
+ protected mcpServer: McpServer;
91
+ protected toolsManager?: IMidsceneTools;
92
+ protected config: BaseMCPServerConfig;
93
+ protected providedToolsManager?: IMidsceneTools;
94
+
95
+ constructor(config: BaseMCPServerConfig, toolsManager?: IMidsceneTools) {
96
+ this.config = config;
97
+ this.mcpServer = new McpServer({
98
+ name: config.name,
99
+ version: config.version,
100
+ description: config.description,
101
+ });
102
+ this.providedToolsManager = toolsManager;
103
+ }
104
+
105
+ /**
106
+ * Platform-specific: create tools manager instance
107
+ * This is only called if no tools manager was provided in constructor
108
+ */
109
+ protected abstract createToolsManager(): IMidsceneTools;
110
+
111
+ /**
112
+ * Initialize tools manager and attach to MCP server
113
+ */
114
+ private async initializeToolsManager(): Promise<void> {
115
+ setIsMcp(true);
116
+
117
+ // Use provided tools manager if available, otherwise create new one
118
+ this.toolsManager = this.providedToolsManager || this.createToolsManager();
119
+
120
+ try {
121
+ await this.toolsManager.initTools();
122
+ } catch (error: unknown) {
123
+ const message = getErrorMessage(error);
124
+ console.error(`Failed to initialize tools: ${message}`);
125
+ console.error('Tools will be initialized on first use');
126
+ }
127
+
128
+ this.toolsManager.attachToServer(this.mcpServer);
129
+ }
130
+
131
+ /**
132
+ * Perform cleanup on shutdown
133
+ */
134
+ private async performCleanup(): Promise<void> {
135
+ console.error(`${this.config.name} closing...`);
136
+ this.mcpServer.close();
137
+ await this.toolsManager?.destroy?.().catch(console.error);
138
+ }
139
+
140
+ /**
141
+ * Initialize and launch the MCP server with stdio transport
142
+ */
143
+ public async launch(): Promise<LaunchMCPServerResult> {
144
+ // Hijack stdout-based console methods to stderr for stdio mode
145
+ // This prevents them from breaking MCP JSON-RPC protocol on stdout
146
+ // Note: console.warn and console.error already output to stderr
147
+ console.log = (...args: unknown[]) => {
148
+ console.error('[LOG]', ...args);
149
+ };
150
+ console.info = (...args: unknown[]) => {
151
+ console.error('[INFO]', ...args);
152
+ };
153
+ console.debug = (...args: unknown[]) => {
154
+ console.error('[DEBUG]', ...args);
155
+ };
156
+
157
+ await this.initializeToolsManager();
158
+
159
+ const transport = new StdioServerTransport();
160
+
161
+ try {
162
+ await this.mcpServer.connect(transport);
163
+ } catch (error: unknown) {
164
+ const message = getErrorMessage(error);
165
+ console.error(`Failed to connect MCP stdio transport: ${message}`);
166
+ throw new Error(`Failed to initialize MCP stdio transport: ${message}`);
167
+ }
168
+
169
+ // Setup signal handlers for graceful shutdown
170
+ let isShuttingDown = false;
171
+ const cleanup = () => {
172
+ if (isShuttingDown) return;
173
+ isShuttingDown = true;
174
+ console.error(`${this.config.name} shutting down...`);
175
+ this.performCleanup().finally(() => process.exit(0));
176
+ };
177
+
178
+ // Setup process-level error handlers to prevent crashes
179
+ process.on('uncaughtException', (error: Error & { code?: string }) => {
180
+ // Exit on pipe errors — parent process is gone
181
+ if (error.code === 'EPIPE' || error.code === 'ERR_STREAM_DESTROYED') {
182
+ cleanup();
183
+ return;
184
+ }
185
+ console.error(`[${this.config.name}] Uncaught Exception:`, error);
186
+ console.error('Stack:', error.stack);
187
+ });
188
+
189
+ process.on('unhandledRejection', (reason: unknown) => {
190
+ console.error(`[${this.config.name}] Unhandled Rejection:`, reason);
191
+ if (reason instanceof Error) {
192
+ console.error('Stack:', reason.stack);
193
+ }
194
+ });
195
+
196
+ // Exit when stdin closes (parent process gone) or reaches EOF
197
+ process.stdin.on('close', cleanup);
198
+ process.stdin.on('end', cleanup);
199
+
200
+ // Exit when stdout/stderr pipe breaks (parent process gone)
201
+ process.stdout.on('error', cleanup);
202
+
203
+ process.once('SIGINT', cleanup);
204
+ process.once('SIGTERM', cleanup);
205
+ process.once('SIGHUP', cleanup);
206
+
207
+ return {
208
+ close: async () => {
209
+ this.performCleanup();
210
+ },
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Launch MCP server with HTTP transport
216
+ * Supports stateful sessions for web applications and service integration
217
+ */
218
+ public async launchHttp(
219
+ options: HttpLaunchOptions,
220
+ ): Promise<LaunchMCPServerResult> {
221
+ // Validate port number
222
+ if (
223
+ !Number.isInteger(options.port) ||
224
+ options.port < 1 ||
225
+ options.port > 65535
226
+ ) {
227
+ throw new Error(
228
+ `Invalid port number: ${options.port}. Port must be between 1 and 65535.`,
229
+ );
230
+ }
231
+
232
+ await this.initializeToolsManager();
233
+
234
+ const app: Application = express();
235
+
236
+ // Add JSON body parser with size limit
237
+ app.use(express.json({ limit: '10mb' }));
238
+
239
+ const sessions = new Map<string, SessionData>();
240
+
241
+ app.all('/mcp', async (req: Request, res: Response) => {
242
+ const startTime = Date.now();
243
+ const requestId = randomUUID().substring(0, 8);
244
+
245
+ try {
246
+ const rawSessionId = req.headers['mcp-session-id'];
247
+ const sessionId = Array.isArray(rawSessionId)
248
+ ? rawSessionId[0]
249
+ : rawSessionId;
250
+ let session = sessionId ? sessions.get(sessionId) : undefined;
251
+
252
+ if (!session && req.method === 'POST') {
253
+ // Check session limit to prevent DoS
254
+ if (sessions.size >= MAX_SESSIONS) {
255
+ console.error(
256
+ `[${new Date().toISOString()}] [${requestId}] Session limit reached: ${sessions.size}/${MAX_SESSIONS}`,
257
+ );
258
+ res.status(503).json({
259
+ error: 'Too many active sessions',
260
+ message: 'Server is at maximum capacity. Please try again later.',
261
+ });
262
+ return;
263
+ }
264
+ session = await this.createHttpSession(sessions);
265
+ console.log(
266
+ `[${new Date().toISOString()}] [${requestId}] New session created: ${session.transport.sessionId}`,
267
+ );
268
+ }
269
+
270
+ if (session) {
271
+ session.lastAccessedAt = new Date();
272
+ await session.transport.handleRequest(req, res, req.body);
273
+ const duration = Date.now() - startTime;
274
+ console.log(
275
+ `[${new Date().toISOString()}] [${requestId}] Request completed in ${duration}ms`,
276
+ );
277
+ } else {
278
+ console.error(
279
+ `[${new Date().toISOString()}] [${requestId}] Invalid session or GET without session`,
280
+ );
281
+ res
282
+ .status(400)
283
+ .json({ error: 'Invalid session or GET without session' });
284
+ }
285
+ } catch (error: unknown) {
286
+ const message = getErrorMessage(error);
287
+ const duration = Date.now() - startTime;
288
+ console.error(
289
+ `[${new Date().toISOString()}] [${requestId}] MCP request error after ${duration}ms: ${message}`,
290
+ );
291
+ if (!res.headersSent) {
292
+ res.status(500).json({
293
+ error: 'Internal server error',
294
+ message: 'Failed to process MCP request',
295
+ });
296
+ }
297
+ }
298
+ });
299
+
300
+ const host = options.host || 'localhost';
301
+
302
+ // Create server with error handling
303
+ const server = app
304
+ .listen(options.port, host, () => {
305
+ console.log(
306
+ `${this.config.name} HTTP server listening on http://${host}:${options.port}/mcp`,
307
+ );
308
+ })
309
+ .on('error', (error: NodeJS.ErrnoException) => {
310
+ if (error.code === 'EADDRINUSE') {
311
+ console.error(
312
+ `ERROR: Port ${options.port} is already in use.\nPlease try a different port: --port=<number>\nExample: --mode=http --port=${options.port + 1}`,
313
+ );
314
+ } else if (error.code === 'EACCES') {
315
+ console.error(
316
+ `ERROR: Permission denied to bind to port ${options.port}.\nPorts below 1024 require root/admin privileges.\nPlease use a port above 1024 or run with elevated privileges.`,
317
+ );
318
+ } else {
319
+ console.error(
320
+ `ERROR: Failed to start HTTP server on ${host}:${options.port}\n` +
321
+ `Reason: ${error.message}\n` +
322
+ `Code: ${error.code || 'unknown'}`,
323
+ );
324
+ }
325
+ process.exit(1);
326
+ });
327
+
328
+ const cleanupInterval = this.startSessionCleanup(sessions);
329
+ this.setupHttpShutdownHandlers(server, sessions, cleanupInterval);
330
+
331
+ return {
332
+ port: options.port,
333
+ host,
334
+ close: async () => {
335
+ clearInterval(cleanupInterval);
336
+ for (const session of sessions.values()) {
337
+ try {
338
+ await session.transport.close();
339
+ } catch (error: unknown) {
340
+ const message = getErrorMessage(error);
341
+ console.error(
342
+ `Failed to close session ${session.transport.sessionId}: ${message}`,
343
+ );
344
+ }
345
+ }
346
+ sessions.clear();
347
+
348
+ return new Promise<void>((resolve) => {
349
+ server.close(async (err) => {
350
+ if (err) {
351
+ console.error('Error closing HTTP server:', err);
352
+ }
353
+ await this.performCleanup();
354
+ resolve();
355
+ });
356
+ });
357
+ },
358
+ };
359
+ }
360
+
361
+ /**
362
+ * Create a new HTTP session with transport
363
+ */
364
+ private async createHttpSession(
365
+ sessions: Map<string, SessionData>,
366
+ ): Promise<SessionData> {
367
+ const transport = new StreamableHTTPServerTransport({
368
+ sessionIdGenerator: () => randomUUID(),
369
+ onsessioninitialized: (sid: string) => {
370
+ sessions.set(sid, {
371
+ transport,
372
+ createdAt: new Date(),
373
+ lastAccessedAt: new Date(),
374
+ });
375
+ console.log(
376
+ `[${new Date().toISOString()}] Session ${sid} initialized (total: ${sessions.size})`,
377
+ );
378
+ },
379
+ });
380
+
381
+ transport.onclose = () => {
382
+ if (transport.sessionId) {
383
+ sessions.delete(transport.sessionId);
384
+ console.log(
385
+ `[${new Date().toISOString()}] Session ${transport.sessionId} closed (remaining: ${sessions.size})`,
386
+ );
387
+ }
388
+ };
389
+
390
+ try {
391
+ await this.mcpServer.connect(transport);
392
+ } catch (error: unknown) {
393
+ const message = getErrorMessage(error);
394
+ console.error(
395
+ `[${new Date().toISOString()}] Failed to connect MCP transport: ${message}`,
396
+ );
397
+ // Clean up the failed transport
398
+ if (transport.sessionId) {
399
+ sessions.delete(transport.sessionId);
400
+ }
401
+ throw new Error(`Failed to initialize MCP session: ${message}`);
402
+ }
403
+
404
+ return {
405
+ transport,
406
+ createdAt: new Date(),
407
+ lastAccessedAt: new Date(),
408
+ };
409
+ }
410
+
411
+ /**
412
+ * Start periodic session cleanup for inactive sessions
413
+ */
414
+ private startSessionCleanup(
415
+ sessions: Map<string, SessionData>,
416
+ ): ReturnType<typeof setInterval> {
417
+ return setInterval(() => {
418
+ const now = Date.now();
419
+ for (const [sid, session] of sessions) {
420
+ if (now - session.lastAccessedAt.getTime() > SESSION_TIMEOUT_MS) {
421
+ try {
422
+ session.transport.close();
423
+ sessions.delete(sid);
424
+ console.log(
425
+ `[${new Date().toISOString()}] Session ${sid} cleaned up due to inactivity (remaining: ${sessions.size})`,
426
+ );
427
+ } catch (error: unknown) {
428
+ const message = getErrorMessage(error);
429
+ console.error(
430
+ `[${new Date().toISOString()}] Failed to close session ${sid} during cleanup: ${message}`,
431
+ );
432
+ // Still delete from map to prevent retry loops
433
+ sessions.delete(sid);
434
+ }
435
+ }
436
+ }
437
+ }, CLEANUP_INTERVAL_MS);
438
+ }
439
+
440
+ /**
441
+ * Setup shutdown handlers for HTTP server
442
+ */
443
+ private setupHttpShutdownHandlers(
444
+ server: ReturnType<Application['listen']>,
445
+ sessions: Map<string, SessionData>,
446
+ cleanupInterval: ReturnType<typeof setInterval>,
447
+ ): void {
448
+ const cleanup = () => {
449
+ console.error(`${this.config.name} shutting down...`);
450
+ clearInterval(cleanupInterval);
451
+
452
+ // Close all sessions with error handling
453
+ for (const session of sessions.values()) {
454
+ try {
455
+ session.transport.close();
456
+ } catch (error: unknown) {
457
+ const message = getErrorMessage(error);
458
+ console.error(`Error closing session during shutdown: ${message}`);
459
+ }
460
+ }
461
+ sessions.clear();
462
+
463
+ // Close HTTP server gracefully
464
+ try {
465
+ server.close(() => {
466
+ // Server closed callback - all connections finished
467
+ this.performCleanup().finally(() => process.exit(0));
468
+ });
469
+
470
+ // Set a timeout in case server.close() hangs
471
+ setTimeout(() => {
472
+ console.error('Forcefully shutting down after timeout');
473
+ this.performCleanup().finally(() => process.exit(1));
474
+ }, 5000);
475
+ } catch (error: unknown) {
476
+ const message = getErrorMessage(error);
477
+ console.error(`Error closing HTTP server: ${message}`);
478
+ this.performCleanup().finally(() => process.exit(1));
479
+ }
480
+ };
481
+
482
+ // Use once() to prevent multiple registrations
483
+ process.once('SIGINT', cleanup);
484
+ process.once('SIGTERM', cleanup);
485
+ }
486
+
487
+ /**
488
+ * Get the underlying MCP server instance
489
+ */
490
+ public getServer(): McpServer {
491
+ return this.mcpServer;
492
+ }
493
+
494
+ /**
495
+ * Get the tools manager instance
496
+ */
497
+ public getToolsManager(): IMidsceneTools | undefined {
498
+ return this.toolsManager;
499
+ }
500
+ }