@midscene/shared 1.0.1-beta-20251208031823.0 → 1.0.1-beta-20251208033501.0

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 (63) hide show
  1. package/dist/es/mcp/base-server.mjs +250 -0
  2. package/dist/es/mcp/base-tools.mjs +84 -0
  3. package/dist/es/mcp/index.mjs +4 -0
  4. package/dist/es/mcp/tool-generator.mjs +215 -0
  5. package/dist/es/mcp/types.mjs +3 -0
  6. package/dist/es/node/fs.mjs +1 -1
  7. package/dist/lib/baseDB.js +2 -2
  8. package/dist/lib/build/copy-static.js +2 -2
  9. package/dist/lib/build/rspack-config.js +2 -2
  10. package/dist/lib/common.js +2 -2
  11. package/dist/lib/constants/example-code.js +2 -2
  12. package/dist/lib/constants/index.js +2 -2
  13. package/dist/lib/env/basic.js +2 -2
  14. package/dist/lib/env/constants.js +2 -2
  15. package/dist/lib/env/global-config-manager.js +2 -2
  16. package/dist/lib/env/helper.js +2 -2
  17. package/dist/lib/env/index.js +6 -6
  18. package/dist/lib/env/init-debug.js +2 -2
  19. package/dist/lib/env/model-config-manager.js +2 -2
  20. package/dist/lib/env/parse-model-config.js +2 -2
  21. package/dist/lib/env/types.js +2 -2
  22. package/dist/lib/env/utils.js +2 -2
  23. package/dist/lib/extractor/constants.js +2 -2
  24. package/dist/lib/extractor/debug.js +1 -1
  25. package/dist/lib/extractor/dom-util.js +2 -2
  26. package/dist/lib/extractor/index.js +2 -2
  27. package/dist/lib/extractor/locator.js +2 -2
  28. package/dist/lib/extractor/tree.js +2 -2
  29. package/dist/lib/extractor/util.js +2 -2
  30. package/dist/lib/extractor/web-extractor.js +2 -2
  31. package/dist/lib/img/box-select.js +2 -2
  32. package/dist/lib/img/draw-box.js +2 -2
  33. package/dist/lib/img/get-jimp.js +2 -2
  34. package/dist/lib/img/get-photon.js +2 -2
  35. package/dist/lib/img/get-sharp.js +2 -2
  36. package/dist/lib/img/index.js +2 -2
  37. package/dist/lib/img/info.js +2 -2
  38. package/dist/lib/img/transform.js +2 -2
  39. package/dist/lib/index.js +2 -2
  40. package/dist/lib/logger.js +2 -2
  41. package/dist/lib/mcp/base-server.js +290 -0
  42. package/dist/lib/mcp/base-tools.js +118 -0
  43. package/dist/lib/mcp/index.js +79 -0
  44. package/dist/lib/mcp/tool-generator.js +252 -0
  45. package/dist/lib/mcp/types.js +40 -0
  46. package/dist/lib/node/fs.js +3 -3
  47. package/dist/lib/node/index.js +2 -2
  48. package/dist/lib/polyfills/async-hooks.js +2 -2
  49. package/dist/lib/polyfills/index.js +2 -2
  50. package/dist/lib/types/index.js +2 -2
  51. package/dist/lib/us-keyboard-layout.js +2 -2
  52. package/dist/lib/utils.js +2 -2
  53. package/dist/types/mcp/base-server.d.ts +77 -0
  54. package/dist/types/mcp/base-tools.d.ts +51 -0
  55. package/dist/types/mcp/index.d.ts +4 -0
  56. package/dist/types/mcp/tool-generator.d.ts +11 -0
  57. package/dist/types/mcp/types.d.ts +98 -0
  58. package/package.json +17 -3
  59. package/src/mcp/base-server.ts +432 -0
  60. package/src/mcp/base-tools.ts +190 -0
  61. package/src/mcp/index.ts +4 -0
  62. package/src/mcp/tool-generator.ts +311 -0
  63. package/src/mcp/types.ts +106 -0
@@ -0,0 +1,98 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { z } from 'zod';
3
+ /**
4
+ * Default timeout constants for app loading verification
5
+ */
6
+ export declare const defaultAppLoadingTimeoutMs = 10000;
7
+ export declare const defaultAppLoadingCheckIntervalMs = 2000;
8
+ /**
9
+ * Content item types for tool results (MCP compatible)
10
+ */
11
+ export type ToolResultContent = {
12
+ type: 'text';
13
+ text: string;
14
+ } | {
15
+ type: 'image';
16
+ data: string;
17
+ mimeType: string;
18
+ } | {
19
+ type: 'audio';
20
+ data: string;
21
+ mimeType: string;
22
+ } | {
23
+ type: 'resource';
24
+ resource: {
25
+ text: string;
26
+ uri: string;
27
+ mimeType?: string;
28
+ } | {
29
+ uri: string;
30
+ blob: string;
31
+ mimeType?: string;
32
+ };
33
+ };
34
+ /**
35
+ * Result type for tool execution (MCP compatible)
36
+ */
37
+ export interface ToolResult {
38
+ [x: string]: unknown;
39
+ content: ToolResultContent[];
40
+ isError?: boolean;
41
+ _meta?: Record<string, unknown>;
42
+ }
43
+ /**
44
+ * Tool handler function type
45
+ * Takes parsed arguments and returns a tool result
46
+ */
47
+ export type ToolHandler<T = Record<string, unknown>> = (args: T) => Promise<ToolResult>;
48
+ /**
49
+ * Tool schema type using Zod
50
+ */
51
+ export type ToolSchema = Record<string, z.ZodTypeAny>;
52
+ /**
53
+ * Tool definition for MCP server
54
+ */
55
+ export interface ToolDefinition<T = Record<string, unknown>> {
56
+ name: string;
57
+ description: string;
58
+ schema: ToolSchema;
59
+ handler: ToolHandler<T>;
60
+ autoDestroy?: boolean;
61
+ }
62
+ /**
63
+ * Action space item definition
64
+ */
65
+ export interface ActionSpaceItem {
66
+ name: string;
67
+ description?: string;
68
+ args?: Record<string, unknown>;
69
+ [key: string]: unknown;
70
+ }
71
+ /**
72
+ * Base agent interface
73
+ * Represents a platform-specific agent (Android, iOS, Web)
74
+ */
75
+ export interface BaseAgent {
76
+ getActionSpace(): Promise<ActionSpaceItem[]>;
77
+ destroy?(): Promise<void>;
78
+ page?: {
79
+ screenshotBase64(): Promise<string>;
80
+ };
81
+ aiAction?: (description: string, params?: Record<string, unknown>) => Promise<void>;
82
+ aiWaitFor?: (assertion: string, options: Record<string, unknown>) => Promise<void>;
83
+ }
84
+ /**
85
+ * Base device interface for temporary device instances
86
+ */
87
+ export interface BaseDevice {
88
+ actionSpace(): ActionSpaceItem[];
89
+ destroy?(): Promise<void>;
90
+ }
91
+ /**
92
+ * Interface for platform-specific MCP tools manager
93
+ */
94
+ export interface IMidsceneTools {
95
+ attachToServer(server: McpServer): void;
96
+ initTools(): Promise<void>;
97
+ closeBrowser?(): Promise<void>;
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@midscene/shared",
3
- "version": "1.0.1-beta-20251208031823.0",
3
+ "version": "1.0.1-beta-20251208033501.0",
4
4
  "repository": "https://github.com/web-infra-dev/midscene",
5
5
  "homepage": "https://midscenejs.com/",
6
6
  "types": "./dist/types/index.d.ts",
@@ -57,6 +57,16 @@
57
57
  "import": "./dist/es/common.mjs",
58
58
  "require": "./dist/lib/common.js"
59
59
  },
60
+ "./mcp": {
61
+ "types": "./dist/types/mcp/index.d.ts",
62
+ "import": "./dist/es/mcp/index.mjs",
63
+ "require": "./dist/lib/mcp/index.js"
64
+ },
65
+ "./logger": {
66
+ "types": "./dist/types/logger.d.ts",
67
+ "import": "./dist/es/logger.mjs",
68
+ "require": "./dist/lib/logger.js"
69
+ },
60
70
  "./*": {
61
71
  "types": "./dist/types/*.d.ts",
62
72
  "import": "./dist/es/*.mjs",
@@ -72,21 +82,25 @@
72
82
  "@silvia-odwyer/photon": "0.3.3",
73
83
  "@silvia-odwyer/photon-node": "0.3.3",
74
84
  "debug": "4.4.0",
85
+ "express": "^4.21.2",
75
86
  "jimp": "0.22.12",
76
87
  "js-sha256": "0.11.0",
77
88
  "sharp": "^0.34.3",
78
89
  "uuid": "11.1.0"
79
90
  },
80
91
  "devDependencies": {
81
- "@rslib/core": "^0.18.2",
92
+ "@rslib/core": "^0.18.3",
93
+ "@modelcontextprotocol/sdk": "1.10.2",
82
94
  "@types/debug": "4.1.12",
95
+ "@types/express": "^4.17.21",
83
96
  "@types/node": "^18.0.0",
84
97
  "@ui-tars/shared": "1.2.0",
85
98
  "dotenv": "^16.4.5",
86
99
  "openai": "6.3.0",
87
100
  "rimraf": "~3.0.2",
88
101
  "typescript": "^5.8.3",
89
- "vitest": "3.0.5"
102
+ "vitest": "3.0.5",
103
+ "zod": "3.24.3"
90
104
  },
91
105
  "sideEffects": [],
92
106
  "publishConfig": {
@@ -0,0 +1,432 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { ParseArgsConfig } from 'node:util';
3
+ import { setIsMcp } from '@midscene/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 type { Application, Request, Response } from 'express';
8
+ import type { IMidsceneTools } from './types';
9
+
10
+ export interface BaseMCPServerConfig {
11
+ name: string;
12
+ version: string;
13
+ description: string;
14
+ }
15
+
16
+ export interface HttpLaunchOptions {
17
+ port: number;
18
+ host?: string;
19
+ }
20
+
21
+ interface SessionData {
22
+ transport: StreamableHTTPServerTransport;
23
+ createdAt: Date;
24
+ lastAccessedAt: Date;
25
+ }
26
+
27
+ /**
28
+ * CLI argument configuration for MCP servers
29
+ */
30
+ export const CLI_ARGS_CONFIG: ParseArgsConfig['options'] = {
31
+ mode: { type: 'string', default: 'stdio' },
32
+ port: { type: 'string', default: '3000' },
33
+ host: { type: 'string', default: 'localhost' },
34
+ };
35
+
36
+ export interface CLIArgs {
37
+ mode?: string;
38
+ port?: string;
39
+ host?: string;
40
+ }
41
+
42
+ /**
43
+ * Launch an MCP server based on CLI arguments
44
+ * Shared helper to reduce duplication across platform CLI entry points
45
+ */
46
+ export function launchMCPServer(
47
+ server: BaseMCPServer,
48
+ args: CLIArgs,
49
+ ): Promise<void> {
50
+ if (args.mode === 'http') {
51
+ return server.launchHttp({
52
+ port: Number.parseInt(args.port || '3000', 10),
53
+ host: args.host || 'localhost',
54
+ });
55
+ }
56
+ return server.launch();
57
+ }
58
+
59
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
60
+ const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
61
+ const MAX_SESSIONS = 100; // Maximum concurrent sessions to prevent DoS
62
+
63
+ /**
64
+ * Base MCP Server class with programmatic launch() API
65
+ * Each platform extends this to provide their own tools manager
66
+ */
67
+ export abstract class BaseMCPServer {
68
+ protected mcpServer: McpServer;
69
+ protected toolsManager?: IMidsceneTools;
70
+ protected config: BaseMCPServerConfig;
71
+
72
+ constructor(config: BaseMCPServerConfig) {
73
+ this.config = config;
74
+ this.mcpServer = new McpServer({
75
+ name: config.name,
76
+ version: config.version,
77
+ description: config.description,
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Platform-specific: create tools manager instance
83
+ */
84
+ protected abstract createToolsManager(): IMidsceneTools;
85
+
86
+ /**
87
+ * Initialize tools manager and attach to MCP server
88
+ */
89
+ private async initializeToolsManager(): Promise<void> {
90
+ setIsMcp(true);
91
+ this.toolsManager = this.createToolsManager();
92
+
93
+ try {
94
+ await this.toolsManager.initTools();
95
+ } catch (error: unknown) {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ console.error(`Failed to initialize tools: ${message}`);
98
+ console.error('Tools will be initialized on first use');
99
+ }
100
+
101
+ this.toolsManager.attachToServer(this.mcpServer);
102
+ }
103
+
104
+ /**
105
+ * Perform cleanup on shutdown
106
+ */
107
+ private performCleanup(): void {
108
+ console.error(`${this.config.name} closing...`);
109
+ this.mcpServer.close();
110
+ this.toolsManager?.closeBrowser?.().catch(console.error);
111
+ }
112
+
113
+ /**
114
+ * Initialize and launch the MCP server with stdio transport
115
+ */
116
+ public async launch(): Promise<void> {
117
+ // Hijack stdout-based console methods to stderr for stdio mode
118
+ // This prevents them from breaking MCP JSON-RPC protocol on stdout
119
+ // Note: console.warn and console.error already output to stderr
120
+ console.log = (...args: unknown[]) => {
121
+ console.error('[LOG]', ...args);
122
+ };
123
+ console.info = (...args: unknown[]) => {
124
+ console.error('[INFO]', ...args);
125
+ };
126
+ console.debug = (...args: unknown[]) => {
127
+ console.error('[DEBUG]', ...args);
128
+ };
129
+
130
+ await this.initializeToolsManager();
131
+
132
+ const transport = new StdioServerTransport();
133
+
134
+ try {
135
+ await this.mcpServer.connect(transport);
136
+ } catch (error: unknown) {
137
+ const message = error instanceof Error ? error.message : String(error);
138
+ console.error(`Failed to connect MCP stdio transport: ${message}`);
139
+ throw new Error(`Failed to initialize MCP stdio transport: ${message}`);
140
+ }
141
+
142
+ // Setup process-level error handlers to prevent crashes
143
+ process.on('uncaughtException', (error: Error) => {
144
+ console.error(`[${this.config.name}] Uncaught Exception:`, error);
145
+ console.error('Stack:', error.stack);
146
+ // Don't exit - try to recover
147
+ });
148
+
149
+ process.on('unhandledRejection', (reason: unknown) => {
150
+ console.error(`[${this.config.name}] Unhandled Rejection:`, reason);
151
+ if (reason instanceof Error) {
152
+ console.error('Stack:', reason.stack);
153
+ }
154
+ // Don't exit - try to recover
155
+ });
156
+
157
+ // Setup cleanup handlers
158
+ process.stdin.on('close', () => this.performCleanup());
159
+
160
+ // Setup signal handlers for graceful shutdown
161
+ const cleanup = () => {
162
+ console.error(`${this.config.name} shutting down...`);
163
+ this.performCleanup();
164
+ process.exit(0);
165
+ };
166
+
167
+ process.once('SIGINT', cleanup);
168
+ process.once('SIGTERM', cleanup);
169
+ }
170
+
171
+ /**
172
+ * Launch MCP server with HTTP transport
173
+ * Supports stateful sessions for web applications and service integration
174
+ */
175
+ public async launchHttp(options: HttpLaunchOptions): Promise<void> {
176
+ // Validate port number
177
+ if (
178
+ !Number.isInteger(options.port) ||
179
+ options.port < 1 ||
180
+ options.port > 65535
181
+ ) {
182
+ throw new Error(
183
+ `Invalid port number: ${options.port}. Port must be between 1 and 65535.`,
184
+ );
185
+ }
186
+
187
+ await this.initializeToolsManager();
188
+
189
+ const express = await import('express');
190
+ const app: Application = express.default();
191
+
192
+ // Add JSON body parser with size limit
193
+ app.use(express.default.json({ limit: '10mb' }));
194
+
195
+ const sessions = new Map<string, SessionData>();
196
+
197
+ app.all('/mcp', async (req: Request, res: Response) => {
198
+ const startTime = Date.now();
199
+ const requestId = randomUUID().substring(0, 8);
200
+
201
+ try {
202
+ const rawSessionId = req.headers['mcp-session-id'];
203
+ const sessionId = Array.isArray(rawSessionId)
204
+ ? rawSessionId[0]
205
+ : rawSessionId;
206
+ let session = sessionId ? sessions.get(sessionId) : undefined;
207
+
208
+ if (!session && req.method === 'POST') {
209
+ // Check session limit to prevent DoS
210
+ if (sessions.size >= MAX_SESSIONS) {
211
+ console.error(
212
+ `[${new Date().toISOString()}] [${requestId}] Session limit reached: ${sessions.size}/${MAX_SESSIONS}`,
213
+ );
214
+ res.status(503).json({
215
+ error: 'Too many active sessions',
216
+ message: 'Server is at maximum capacity. Please try again later.',
217
+ });
218
+ return;
219
+ }
220
+ session = await this.createHttpSession(sessions);
221
+ console.log(
222
+ `[${new Date().toISOString()}] [${requestId}] New session created: ${session.transport.sessionId}`,
223
+ );
224
+ }
225
+
226
+ if (session) {
227
+ session.lastAccessedAt = new Date();
228
+ await session.transport.handleRequest(req, res, req.body);
229
+ const duration = Date.now() - startTime;
230
+ console.log(
231
+ `[${new Date().toISOString()}] [${requestId}] Request completed in ${duration}ms`,
232
+ );
233
+ } else {
234
+ console.error(
235
+ `[${new Date().toISOString()}] [${requestId}] Invalid session or GET without session`,
236
+ );
237
+ res
238
+ .status(400)
239
+ .json({ error: 'Invalid session or GET without session' });
240
+ }
241
+ } catch (error: unknown) {
242
+ const message = error instanceof Error ? error.message : String(error);
243
+ const duration = Date.now() - startTime;
244
+ console.error(
245
+ `[${new Date().toISOString()}] [${requestId}] MCP request error after ${duration}ms: ${message}`,
246
+ );
247
+ if (!res.headersSent) {
248
+ res.status(500).json({
249
+ error: 'Internal server error',
250
+ message: 'Failed to process MCP request',
251
+ });
252
+ }
253
+ }
254
+ });
255
+
256
+ const host = options.host || 'localhost';
257
+
258
+ // Create server with error handling
259
+ const server = app
260
+ .listen(options.port, host, () => {
261
+ console.log(
262
+ `${this.config.name} HTTP server listening on http://${host}:${options.port}/mcp`,
263
+ );
264
+ })
265
+ .on('error', (error: NodeJS.ErrnoException) => {
266
+ if (error.code === 'EADDRINUSE') {
267
+ console.error(
268
+ `ERROR: Port ${options.port} is already in use.\nPlease try a different port: --port=<number>\nExample: --mode=http --port=${options.port + 1}`,
269
+ );
270
+ } else if (error.code === 'EACCES') {
271
+ console.error(
272
+ `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.`,
273
+ );
274
+ } else {
275
+ console.error(
276
+ `ERROR: Failed to start HTTP server on ${host}:${options.port}\n` +
277
+ `Reason: ${error.message}\n` +
278
+ `Code: ${error.code || 'unknown'}`,
279
+ );
280
+ }
281
+ process.exit(1);
282
+ });
283
+
284
+ const cleanupInterval = this.startSessionCleanup(sessions);
285
+ this.setupHttpShutdownHandlers(server, sessions, cleanupInterval);
286
+ }
287
+
288
+ /**
289
+ * Create a new HTTP session with transport
290
+ */
291
+ private async createHttpSession(
292
+ sessions: Map<string, SessionData>,
293
+ ): Promise<SessionData> {
294
+ const transport = new StreamableHTTPServerTransport({
295
+ sessionIdGenerator: () => randomUUID(),
296
+ onsessioninitialized: (sid: string) => {
297
+ sessions.set(sid, {
298
+ transport,
299
+ createdAt: new Date(),
300
+ lastAccessedAt: new Date(),
301
+ });
302
+ console.log(
303
+ `[${new Date().toISOString()}] Session ${sid} initialized (total: ${sessions.size})`,
304
+ );
305
+ },
306
+ });
307
+
308
+ transport.onclose = () => {
309
+ if (transport.sessionId) {
310
+ sessions.delete(transport.sessionId);
311
+ console.log(
312
+ `[${new Date().toISOString()}] Session ${transport.sessionId} closed (remaining: ${sessions.size})`,
313
+ );
314
+ }
315
+ };
316
+
317
+ try {
318
+ await this.mcpServer.connect(transport);
319
+ } catch (error: unknown) {
320
+ const message = error instanceof Error ? error.message : String(error);
321
+ console.error(
322
+ `[${new Date().toISOString()}] Failed to connect MCP transport: ${message}`,
323
+ );
324
+ // Clean up the failed transport
325
+ if (transport.sessionId) {
326
+ sessions.delete(transport.sessionId);
327
+ }
328
+ throw new Error(`Failed to initialize MCP session: ${message}`);
329
+ }
330
+
331
+ return {
332
+ transport,
333
+ createdAt: new Date(),
334
+ lastAccessedAt: new Date(),
335
+ };
336
+ }
337
+
338
+ /**
339
+ * Start periodic session cleanup for inactive sessions
340
+ */
341
+ private startSessionCleanup(
342
+ sessions: Map<string, SessionData>,
343
+ ): ReturnType<typeof setInterval> {
344
+ return setInterval(() => {
345
+ const now = Date.now();
346
+ for (const [sid, session] of sessions) {
347
+ if (now - session.lastAccessedAt.getTime() > SESSION_TIMEOUT_MS) {
348
+ try {
349
+ session.transport.close();
350
+ sessions.delete(sid);
351
+ console.log(
352
+ `[${new Date().toISOString()}] Session ${sid} cleaned up due to inactivity (remaining: ${sessions.size})`,
353
+ );
354
+ } catch (error: unknown) {
355
+ const message =
356
+ error instanceof Error ? error.message : String(error);
357
+ console.error(
358
+ `[${new Date().toISOString()}] Failed to close session ${sid} during cleanup: ${message}`,
359
+ );
360
+ // Still delete from map to prevent retry loops
361
+ sessions.delete(sid);
362
+ }
363
+ }
364
+ }
365
+ }, CLEANUP_INTERVAL_MS);
366
+ }
367
+
368
+ /**
369
+ * Setup shutdown handlers for HTTP server
370
+ */
371
+ private setupHttpShutdownHandlers(
372
+ server: ReturnType<Application['listen']>,
373
+ sessions: Map<string, SessionData>,
374
+ cleanupInterval: ReturnType<typeof setInterval>,
375
+ ): void {
376
+ const cleanup = () => {
377
+ console.error(`${this.config.name} shutting down...`);
378
+ clearInterval(cleanupInterval);
379
+
380
+ // Close all sessions with error handling
381
+ for (const session of sessions.values()) {
382
+ try {
383
+ session.transport.close();
384
+ } catch (error: unknown) {
385
+ const message =
386
+ error instanceof Error ? error.message : String(error);
387
+ console.error(`Error closing session during shutdown: ${message}`);
388
+ }
389
+ }
390
+ sessions.clear();
391
+
392
+ // Close HTTP server gracefully
393
+ try {
394
+ server.close(() => {
395
+ // Server closed callback - all connections finished
396
+ this.performCleanup();
397
+ process.exit(0);
398
+ });
399
+
400
+ // Set a timeout in case server.close() hangs
401
+ setTimeout(() => {
402
+ console.error('Forcefully shutting down after timeout');
403
+ this.performCleanup();
404
+ process.exit(1);
405
+ }, 5000);
406
+ } catch (error: unknown) {
407
+ const message = error instanceof Error ? error.message : String(error);
408
+ console.error(`Error closing HTTP server: ${message}`);
409
+ this.performCleanup();
410
+ process.exit(1);
411
+ }
412
+ };
413
+
414
+ // Use once() to prevent multiple registrations
415
+ process.once('SIGINT', cleanup);
416
+ process.once('SIGTERM', cleanup);
417
+ }
418
+
419
+ /**
420
+ * Get the underlying MCP server instance
421
+ */
422
+ public getServer(): McpServer {
423
+ return this.mcpServer;
424
+ }
425
+
426
+ /**
427
+ * Get the tools manager instance
428
+ */
429
+ public getToolsManager(): IMidsceneTools | undefined {
430
+ return this.toolsManager;
431
+ }
432
+ }