@midscene/shared 1.0.1-beta-20251208031823.0 → 1.0.1-beta-20251208031856.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.
- package/dist/es/mcp/base-server.mjs +241 -0
- package/dist/es/mcp/base-tools.mjs +84 -0
- package/dist/es/mcp/index.mjs +4 -0
- package/dist/es/mcp/tool-generator.mjs +215 -0
- package/dist/es/mcp/types.mjs +3 -0
- package/dist/es/node/fs.mjs +1 -1
- package/dist/lib/baseDB.js +2 -2
- package/dist/lib/build/copy-static.js +2 -2
- package/dist/lib/build/rspack-config.js +2 -2
- package/dist/lib/common.js +2 -2
- package/dist/lib/constants/example-code.js +2 -2
- package/dist/lib/constants/index.js +2 -2
- package/dist/lib/env/basic.js +2 -2
- package/dist/lib/env/constants.js +2 -2
- package/dist/lib/env/global-config-manager.js +2 -2
- package/dist/lib/env/helper.js +2 -2
- package/dist/lib/env/index.js +6 -6
- package/dist/lib/env/init-debug.js +2 -2
- package/dist/lib/env/model-config-manager.js +2 -2
- package/dist/lib/env/parse-model-config.js +2 -2
- package/dist/lib/env/types.js +2 -2
- package/dist/lib/env/utils.js +2 -2
- package/dist/lib/extractor/constants.js +2 -2
- package/dist/lib/extractor/debug.js +1 -1
- package/dist/lib/extractor/dom-util.js +2 -2
- package/dist/lib/extractor/index.js +2 -2
- package/dist/lib/extractor/locator.js +2 -2
- package/dist/lib/extractor/tree.js +2 -2
- package/dist/lib/extractor/util.js +2 -2
- package/dist/lib/extractor/web-extractor.js +2 -2
- package/dist/lib/img/box-select.js +2 -2
- package/dist/lib/img/draw-box.js +2 -2
- package/dist/lib/img/get-jimp.js +2 -2
- package/dist/lib/img/get-photon.js +2 -2
- package/dist/lib/img/get-sharp.js +2 -2
- package/dist/lib/img/index.js +2 -2
- package/dist/lib/img/info.js +2 -2
- package/dist/lib/img/transform.js +2 -2
- package/dist/lib/index.js +2 -2
- package/dist/lib/logger.js +2 -2
- package/dist/lib/mcp/base-server.js +281 -0
- package/dist/lib/mcp/base-tools.js +118 -0
- package/dist/lib/mcp/index.js +79 -0
- package/dist/lib/mcp/tool-generator.js +252 -0
- package/dist/lib/mcp/types.js +40 -0
- package/dist/lib/node/fs.js +3 -3
- package/dist/lib/node/index.js +2 -2
- package/dist/lib/polyfills/async-hooks.js +2 -2
- package/dist/lib/polyfills/index.js +2 -2
- package/dist/lib/types/index.js +2 -2
- package/dist/lib/us-keyboard-layout.js +2 -2
- package/dist/lib/utils.js +2 -2
- package/dist/types/mcp/base-server.d.ts +77 -0
- package/dist/types/mcp/base-tools.d.ts +51 -0
- package/dist/types/mcp/index.d.ts +4 -0
- package/dist/types/mcp/tool-generator.d.ts +11 -0
- package/dist/types/mcp/types.d.ts +98 -0
- package/package.json +17 -3
- package/src/mcp/base-server.ts +419 -0
- package/src/mcp/base-tools.ts +190 -0
- package/src/mcp/index.ts +4 -0
- package/src/mcp/tool-generator.ts +311 -0
- 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-
|
|
3
|
+
"version": "1.0.1-beta-20251208031856.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.
|
|
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,419 @@
|
|
|
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
|
+
await this.initializeToolsManager();
|
|
118
|
+
|
|
119
|
+
const transport = new StdioServerTransport();
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
await this.mcpServer.connect(transport);
|
|
123
|
+
} catch (error: unknown) {
|
|
124
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
125
|
+
console.error(`Failed to connect MCP stdio transport: ${message}`);
|
|
126
|
+
throw new Error(`Failed to initialize MCP stdio transport: ${message}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Setup process-level error handlers to prevent crashes
|
|
130
|
+
process.on('uncaughtException', (error: Error) => {
|
|
131
|
+
console.error(`[${this.config.name}] Uncaught Exception:`, error);
|
|
132
|
+
console.error('Stack:', error.stack);
|
|
133
|
+
// Don't exit - try to recover
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
process.on('unhandledRejection', (reason: unknown) => {
|
|
137
|
+
console.error(`[${this.config.name}] Unhandled Rejection:`, reason);
|
|
138
|
+
if (reason instanceof Error) {
|
|
139
|
+
console.error('Stack:', reason.stack);
|
|
140
|
+
}
|
|
141
|
+
// Don't exit - try to recover
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Setup cleanup handlers
|
|
145
|
+
process.stdin.on('close', () => this.performCleanup());
|
|
146
|
+
|
|
147
|
+
// Setup signal handlers for graceful shutdown
|
|
148
|
+
const cleanup = () => {
|
|
149
|
+
console.error(`${this.config.name} shutting down...`);
|
|
150
|
+
this.performCleanup();
|
|
151
|
+
process.exit(0);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
process.once('SIGINT', cleanup);
|
|
155
|
+
process.once('SIGTERM', cleanup);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Launch MCP server with HTTP transport
|
|
160
|
+
* Supports stateful sessions for web applications and service integration
|
|
161
|
+
*/
|
|
162
|
+
public async launchHttp(options: HttpLaunchOptions): Promise<void> {
|
|
163
|
+
// Validate port number
|
|
164
|
+
if (
|
|
165
|
+
!Number.isInteger(options.port) ||
|
|
166
|
+
options.port < 1 ||
|
|
167
|
+
options.port > 65535
|
|
168
|
+
) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Invalid port number: ${options.port}. Port must be between 1 and 65535.`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await this.initializeToolsManager();
|
|
175
|
+
|
|
176
|
+
const express = await import('express');
|
|
177
|
+
const app: Application = express.default();
|
|
178
|
+
|
|
179
|
+
// Add JSON body parser with size limit
|
|
180
|
+
app.use(express.default.json({ limit: '10mb' }));
|
|
181
|
+
|
|
182
|
+
const sessions = new Map<string, SessionData>();
|
|
183
|
+
|
|
184
|
+
app.all('/mcp', async (req: Request, res: Response) => {
|
|
185
|
+
const startTime = Date.now();
|
|
186
|
+
const requestId = randomUUID().substring(0, 8);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const rawSessionId = req.headers['mcp-session-id'];
|
|
190
|
+
const sessionId = Array.isArray(rawSessionId)
|
|
191
|
+
? rawSessionId[0]
|
|
192
|
+
: rawSessionId;
|
|
193
|
+
let session = sessionId ? sessions.get(sessionId) : undefined;
|
|
194
|
+
|
|
195
|
+
if (!session && req.method === 'POST') {
|
|
196
|
+
// Check session limit to prevent DoS
|
|
197
|
+
if (sessions.size >= MAX_SESSIONS) {
|
|
198
|
+
console.error(
|
|
199
|
+
`[${new Date().toISOString()}] [${requestId}] Session limit reached: ${sessions.size}/${MAX_SESSIONS}`,
|
|
200
|
+
);
|
|
201
|
+
res.status(503).json({
|
|
202
|
+
error: 'Too many active sessions',
|
|
203
|
+
message: 'Server is at maximum capacity. Please try again later.',
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
session = await this.createHttpSession(sessions);
|
|
208
|
+
console.log(
|
|
209
|
+
`[${new Date().toISOString()}] [${requestId}] New session created: ${session.transport.sessionId}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (session) {
|
|
214
|
+
session.lastAccessedAt = new Date();
|
|
215
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
216
|
+
const duration = Date.now() - startTime;
|
|
217
|
+
console.log(
|
|
218
|
+
`[${new Date().toISOString()}] [${requestId}] Request completed in ${duration}ms`,
|
|
219
|
+
);
|
|
220
|
+
} else {
|
|
221
|
+
console.error(
|
|
222
|
+
`[${new Date().toISOString()}] [${requestId}] Invalid session or GET without session`,
|
|
223
|
+
);
|
|
224
|
+
res
|
|
225
|
+
.status(400)
|
|
226
|
+
.json({ error: 'Invalid session or GET without session' });
|
|
227
|
+
}
|
|
228
|
+
} catch (error: unknown) {
|
|
229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
const duration = Date.now() - startTime;
|
|
231
|
+
console.error(
|
|
232
|
+
`[${new Date().toISOString()}] [${requestId}] MCP request error after ${duration}ms: ${message}`,
|
|
233
|
+
);
|
|
234
|
+
if (!res.headersSent) {
|
|
235
|
+
res.status(500).json({
|
|
236
|
+
error: 'Internal server error',
|
|
237
|
+
message: 'Failed to process MCP request',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const host = options.host || 'localhost';
|
|
244
|
+
|
|
245
|
+
// Create server with error handling
|
|
246
|
+
const server = app
|
|
247
|
+
.listen(options.port, host, () => {
|
|
248
|
+
console.log(
|
|
249
|
+
`${this.config.name} HTTP server listening on http://${host}:${options.port}/mcp`,
|
|
250
|
+
);
|
|
251
|
+
})
|
|
252
|
+
.on('error', (error: NodeJS.ErrnoException) => {
|
|
253
|
+
if (error.code === 'EADDRINUSE') {
|
|
254
|
+
console.error(
|
|
255
|
+
`ERROR: Port ${options.port} is already in use.\nPlease try a different port: --port=<number>\nExample: --mode=http --port=${options.port + 1}`,
|
|
256
|
+
);
|
|
257
|
+
} else if (error.code === 'EACCES') {
|
|
258
|
+
console.error(
|
|
259
|
+
`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.`,
|
|
260
|
+
);
|
|
261
|
+
} else {
|
|
262
|
+
console.error(
|
|
263
|
+
`ERROR: Failed to start HTTP server on ${host}:${options.port}\n` +
|
|
264
|
+
`Reason: ${error.message}\n` +
|
|
265
|
+
`Code: ${error.code || 'unknown'}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
process.exit(1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const cleanupInterval = this.startSessionCleanup(sessions);
|
|
272
|
+
this.setupHttpShutdownHandlers(server, sessions, cleanupInterval);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Create a new HTTP session with transport
|
|
277
|
+
*/
|
|
278
|
+
private async createHttpSession(
|
|
279
|
+
sessions: Map<string, SessionData>,
|
|
280
|
+
): Promise<SessionData> {
|
|
281
|
+
const transport = new StreamableHTTPServerTransport({
|
|
282
|
+
sessionIdGenerator: () => randomUUID(),
|
|
283
|
+
onsessioninitialized: (sid: string) => {
|
|
284
|
+
sessions.set(sid, {
|
|
285
|
+
transport,
|
|
286
|
+
createdAt: new Date(),
|
|
287
|
+
lastAccessedAt: new Date(),
|
|
288
|
+
});
|
|
289
|
+
console.log(
|
|
290
|
+
`[${new Date().toISOString()}] Session ${sid} initialized (total: ${sessions.size})`,
|
|
291
|
+
);
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
transport.onclose = () => {
|
|
296
|
+
if (transport.sessionId) {
|
|
297
|
+
sessions.delete(transport.sessionId);
|
|
298
|
+
console.log(
|
|
299
|
+
`[${new Date().toISOString()}] Session ${transport.sessionId} closed (remaining: ${sessions.size})`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
await this.mcpServer.connect(transport);
|
|
306
|
+
} catch (error: unknown) {
|
|
307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
+
console.error(
|
|
309
|
+
`[${new Date().toISOString()}] Failed to connect MCP transport: ${message}`,
|
|
310
|
+
);
|
|
311
|
+
// Clean up the failed transport
|
|
312
|
+
if (transport.sessionId) {
|
|
313
|
+
sessions.delete(transport.sessionId);
|
|
314
|
+
}
|
|
315
|
+
throw new Error(`Failed to initialize MCP session: ${message}`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
transport,
|
|
320
|
+
createdAt: new Date(),
|
|
321
|
+
lastAccessedAt: new Date(),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Start periodic session cleanup for inactive sessions
|
|
327
|
+
*/
|
|
328
|
+
private startSessionCleanup(
|
|
329
|
+
sessions: Map<string, SessionData>,
|
|
330
|
+
): ReturnType<typeof setInterval> {
|
|
331
|
+
return setInterval(() => {
|
|
332
|
+
const now = Date.now();
|
|
333
|
+
for (const [sid, session] of sessions) {
|
|
334
|
+
if (now - session.lastAccessedAt.getTime() > SESSION_TIMEOUT_MS) {
|
|
335
|
+
try {
|
|
336
|
+
session.transport.close();
|
|
337
|
+
sessions.delete(sid);
|
|
338
|
+
console.log(
|
|
339
|
+
`[${new Date().toISOString()}] Session ${sid} cleaned up due to inactivity (remaining: ${sessions.size})`,
|
|
340
|
+
);
|
|
341
|
+
} catch (error: unknown) {
|
|
342
|
+
const message =
|
|
343
|
+
error instanceof Error ? error.message : String(error);
|
|
344
|
+
console.error(
|
|
345
|
+
`[${new Date().toISOString()}] Failed to close session ${sid} during cleanup: ${message}`,
|
|
346
|
+
);
|
|
347
|
+
// Still delete from map to prevent retry loops
|
|
348
|
+
sessions.delete(sid);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}, CLEANUP_INTERVAL_MS);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Setup shutdown handlers for HTTP server
|
|
357
|
+
*/
|
|
358
|
+
private setupHttpShutdownHandlers(
|
|
359
|
+
server: ReturnType<Application['listen']>,
|
|
360
|
+
sessions: Map<string, SessionData>,
|
|
361
|
+
cleanupInterval: ReturnType<typeof setInterval>,
|
|
362
|
+
): void {
|
|
363
|
+
const cleanup = () => {
|
|
364
|
+
console.error(`${this.config.name} shutting down...`);
|
|
365
|
+
clearInterval(cleanupInterval);
|
|
366
|
+
|
|
367
|
+
// Close all sessions with error handling
|
|
368
|
+
for (const session of sessions.values()) {
|
|
369
|
+
try {
|
|
370
|
+
session.transport.close();
|
|
371
|
+
} catch (error: unknown) {
|
|
372
|
+
const message =
|
|
373
|
+
error instanceof Error ? error.message : String(error);
|
|
374
|
+
console.error(`Error closing session during shutdown: ${message}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
sessions.clear();
|
|
378
|
+
|
|
379
|
+
// Close HTTP server gracefully
|
|
380
|
+
try {
|
|
381
|
+
server.close(() => {
|
|
382
|
+
// Server closed callback - all connections finished
|
|
383
|
+
this.performCleanup();
|
|
384
|
+
process.exit(0);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Set a timeout in case server.close() hangs
|
|
388
|
+
setTimeout(() => {
|
|
389
|
+
console.error('Forcefully shutting down after timeout');
|
|
390
|
+
this.performCleanup();
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}, 5000);
|
|
393
|
+
} catch (error: unknown) {
|
|
394
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
395
|
+
console.error(`Error closing HTTP server: ${message}`);
|
|
396
|
+
this.performCleanup();
|
|
397
|
+
process.exit(1);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// Use once() to prevent multiple registrations
|
|
402
|
+
process.once('SIGINT', cleanup);
|
|
403
|
+
process.once('SIGTERM', cleanup);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Get the underlying MCP server instance
|
|
408
|
+
*/
|
|
409
|
+
public getServer(): McpServer {
|
|
410
|
+
return this.mcpServer;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Get the tools manager instance
|
|
415
|
+
*/
|
|
416
|
+
public getToolsManager(): IMidsceneTools | undefined {
|
|
417
|
+
return this.toolsManager;
|
|
418
|
+
}
|
|
419
|
+
}
|