@midscene/shared 1.0.1-beta-20251208075922.0 → 1.0.1-beta-20251208112226.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/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/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/package.json +3 -17
- package/dist/es/mcp/base-server.mjs +0 -250
- package/dist/es/mcp/base-tools.mjs +0 -84
- package/dist/es/mcp/index.mjs +0 -5
- package/dist/es/mcp/inject-report-html-plugin.mjs +0 -53
- package/dist/es/mcp/tool-generator.mjs +0 -215
- package/dist/es/mcp/types.mjs +0 -3
- package/dist/lib/mcp/base-server.js +0 -290
- package/dist/lib/mcp/base-tools.js +0 -118
- package/dist/lib/mcp/index.js +0 -86
- package/dist/lib/mcp/inject-report-html-plugin.js +0 -98
- package/dist/lib/mcp/tool-generator.js +0 -252
- package/dist/lib/mcp/types.js +0 -40
- package/dist/types/mcp/base-server.d.ts +0 -77
- package/dist/types/mcp/base-tools.d.ts +0 -55
- package/dist/types/mcp/index.d.ts +0 -5
- package/dist/types/mcp/inject-report-html-plugin.d.ts +0 -18
- package/dist/types/mcp/tool-generator.d.ts +0 -11
- package/dist/types/mcp/types.d.ts +0 -100
- package/src/mcp/base-server.ts +0 -432
- package/src/mcp/base-tools.ts +0 -196
- package/src/mcp/index.ts +0 -5
- package/src/mcp/inject-report-html-plugin.ts +0 -119
- package/src/mcp/tool-generator.ts +0 -311
- package/src/mcp/types.ts +0 -108
package/src/mcp/base-server.ts
DELETED
|
@@ -1,432 +0,0 @@
|
|
|
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
|
-
}
|
package/src/mcp/base-tools.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { parseBase64 } from '@midscene/shared/img';
|
|
2
|
-
import { getDebug } from '@midscene/shared/logger';
|
|
3
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
-
import {
|
|
5
|
-
generateCommonTools,
|
|
6
|
-
generateToolsFromActionSpace,
|
|
7
|
-
} from './tool-generator';
|
|
8
|
-
import type {
|
|
9
|
-
ActionSpaceItem,
|
|
10
|
-
BaseAgent,
|
|
11
|
-
BaseDevice,
|
|
12
|
-
IMidsceneTools,
|
|
13
|
-
ToolDefinition,
|
|
14
|
-
} from './types';
|
|
15
|
-
|
|
16
|
-
const debug = getDebug('mcp:base-tools');
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Base class for platform-specific MCP tools
|
|
20
|
-
* Generic type TAgent allows subclasses to use their specific agent types
|
|
21
|
-
*/
|
|
22
|
-
export abstract class BaseMidsceneTools<TAgent extends BaseAgent = BaseAgent>
|
|
23
|
-
implements IMidsceneTools
|
|
24
|
-
{
|
|
25
|
-
protected mcpServer?: McpServer;
|
|
26
|
-
protected agent?: TAgent;
|
|
27
|
-
protected toolDefinitions: ToolDefinition[] = [];
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Ensure agent is initialized and ready for use.
|
|
31
|
-
* Must be implemented by subclasses to create platform-specific agent.
|
|
32
|
-
* @param initParam Optional initialization parameter (platform-specific, e.g., URL, device ID)
|
|
33
|
-
* @returns Promise resolving to initialized agent instance
|
|
34
|
-
* @throws Error if agent initialization fails
|
|
35
|
-
*/
|
|
36
|
-
protected abstract ensureAgent(initParam?: string): Promise<TAgent>;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Optional: prepare platform-specific tools (e.g., device connection)
|
|
40
|
-
*/
|
|
41
|
-
protected preparePlatformTools(): ToolDefinition[] {
|
|
42
|
-
return [];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Must be implemented by subclasses to create a temporary device instance
|
|
47
|
-
* This allows getting real actionSpace without connecting to device
|
|
48
|
-
*/
|
|
49
|
-
protected abstract createTemporaryDevice(): BaseDevice;
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Initialize all tools by querying actionSpace
|
|
53
|
-
* Uses two-layer fallback strategy:
|
|
54
|
-
* 1. Try to get actionSpace from connected agent (if available)
|
|
55
|
-
* 2. Create temporary device instance to read actionSpace (always succeeds)
|
|
56
|
-
*/
|
|
57
|
-
public async initTools(): Promise<void> {
|
|
58
|
-
this.toolDefinitions = [];
|
|
59
|
-
|
|
60
|
-
// 1. Add platform-specific tools first (device connection, etc.)
|
|
61
|
-
// These don't require an agent and should always be available
|
|
62
|
-
const platformTools = this.preparePlatformTools();
|
|
63
|
-
this.toolDefinitions.push(...platformTools);
|
|
64
|
-
|
|
65
|
-
// 2. Try to get agent and its action space (two-layer fallback)
|
|
66
|
-
let actionSpace: ActionSpaceItem[];
|
|
67
|
-
try {
|
|
68
|
-
// Layer 1: Try to use connected agent
|
|
69
|
-
const agent = await this.ensureAgent();
|
|
70
|
-
actionSpace = await agent.getActionSpace();
|
|
71
|
-
debug(
|
|
72
|
-
'Action space from connected agent:',
|
|
73
|
-
actionSpace.map((a) => a.name).join(', '),
|
|
74
|
-
);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
// Layer 2: Create temporary device instance to read actionSpace
|
|
77
|
-
// This is expected behavior for bridge mode without URL or unconnected devices
|
|
78
|
-
const errorMessage =
|
|
79
|
-
error instanceof Error ? error.message : String(error);
|
|
80
|
-
if (
|
|
81
|
-
errorMessage.includes('requires a URL') ||
|
|
82
|
-
errorMessage.includes('web_connect')
|
|
83
|
-
) {
|
|
84
|
-
debug(
|
|
85
|
-
'Bridge mode detected - agent will be initialized on first web_connect call',
|
|
86
|
-
);
|
|
87
|
-
} else {
|
|
88
|
-
debug(
|
|
89
|
-
'Agent not available yet, using temporary device for action space',
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
const tempDevice = this.createTemporaryDevice();
|
|
93
|
-
actionSpace = tempDevice.actionSpace();
|
|
94
|
-
debug(
|
|
95
|
-
'Action space from temporary device:',
|
|
96
|
-
actionSpace.map((a) => a.name).join(', '),
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
// Destroy temporary instance using optional chaining
|
|
100
|
-
await tempDevice.destroy?.();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 3. Generate tools from action space (core innovation)
|
|
104
|
-
const actionTools = generateToolsFromActionSpace(actionSpace, () =>
|
|
105
|
-
this.ensureAgent(),
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
// 4. Add common tools (screenshot, waitFor)
|
|
109
|
-
const commonTools = generateCommonTools(() => this.ensureAgent());
|
|
110
|
-
|
|
111
|
-
this.toolDefinitions.push(...actionTools, ...commonTools);
|
|
112
|
-
|
|
113
|
-
debug('Total tools prepared:', this.toolDefinitions.length);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Attach to MCP server and register all tools
|
|
118
|
-
*/
|
|
119
|
-
public attachToServer(server: McpServer): void {
|
|
120
|
-
this.mcpServer = server;
|
|
121
|
-
|
|
122
|
-
if (this.toolDefinitions.length === 0) {
|
|
123
|
-
debug('Warning: No tools to register. Tools may be initialized lazily.');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
for (const toolDef of this.toolDefinitions) {
|
|
127
|
-
if (toolDef.autoDestroy) {
|
|
128
|
-
this.toolWithAutoDestroy(
|
|
129
|
-
toolDef.name,
|
|
130
|
-
toolDef.description,
|
|
131
|
-
toolDef.schema,
|
|
132
|
-
toolDef.handler,
|
|
133
|
-
);
|
|
134
|
-
} else {
|
|
135
|
-
this.mcpServer.tool(
|
|
136
|
-
toolDef.name,
|
|
137
|
-
toolDef.description,
|
|
138
|
-
toolDef.schema,
|
|
139
|
-
toolDef.handler,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
debug('Registered', this.toolDefinitions.length, 'tools');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Wrapper for auto-destroy behavior
|
|
149
|
-
*/
|
|
150
|
-
private toolWithAutoDestroy(
|
|
151
|
-
name: string,
|
|
152
|
-
description: string,
|
|
153
|
-
schema: any,
|
|
154
|
-
handler: (...args: any[]) => Promise<any>,
|
|
155
|
-
): void {
|
|
156
|
-
if (!this.mcpServer) {
|
|
157
|
-
throw new Error('MCP server not attached');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
this.mcpServer.tool(name, description, schema, async (...args: any[]) => {
|
|
161
|
-
try {
|
|
162
|
-
return await handler(...args);
|
|
163
|
-
} finally {
|
|
164
|
-
if (!process.env.MIDSCENE_MCP_DISABLE_AGENT_AUTO_DESTROY) {
|
|
165
|
-
try {
|
|
166
|
-
await this.agent?.destroy?.();
|
|
167
|
-
} catch (error) {
|
|
168
|
-
debug('Failed to destroy agent during cleanup:', error);
|
|
169
|
-
}
|
|
170
|
-
this.agent = undefined;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Cleanup method - destroy agent and release resources
|
|
178
|
-
*/
|
|
179
|
-
public async closeBrowser(): Promise<void> {
|
|
180
|
-
await this.agent?.destroy?.();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Helper: Convert base64 screenshot to image content array
|
|
185
|
-
*/
|
|
186
|
-
protected buildScreenshotContent(screenshot: string) {
|
|
187
|
-
const { mimeType, body } = parseBase64(screenshot);
|
|
188
|
-
return [
|
|
189
|
-
{
|
|
190
|
-
type: 'image' as const,
|
|
191
|
-
data: body,
|
|
192
|
-
mimeType,
|
|
193
|
-
},
|
|
194
|
-
];
|
|
195
|
-
}
|
|
196
|
-
}
|