@aiscene/shared 8.0.2 → 8.0.3
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/cli/cli-args.mjs +95 -0
- package/dist/es/cli/cli-error.mjs +24 -0
- package/dist/es/cli/cli-runner.mjs +10 -40
- package/dist/es/cli/index.mjs +4 -2
- package/dist/es/constants/example-code.mjs +2 -2
- package/dist/es/constants/index.mjs +6 -1
- package/dist/es/env/model-config-manager.mjs +3 -0
- package/dist/es/env/parse-model-config.mjs +6 -1
- package/dist/es/env/types.mjs +2 -1
- package/dist/es/env/utils.mjs +1 -9
- package/dist/es/extractor/index.mjs +2 -2
- package/dist/es/img/index.mjs +2 -2
- package/dist/es/img/info.mjs +18 -21
- package/dist/es/key-alias-utils.mjs +19 -0
- package/dist/es/mcp/base-server.mjs +10 -9
- package/dist/es/mcp/base-tools.mjs +72 -4
- package/dist/es/mcp/cli-report-session.mjs +78 -0
- package/dist/es/mcp/error-formatter.mjs +19 -0
- package/dist/es/mcp/index.mjs +2 -0
- package/dist/es/mcp/init-arg-utils.mjs +38 -0
- package/dist/es/mcp/inject-report-html-plugin.mjs +1 -1
- package/dist/es/mcp/tool-generator.mjs +65 -17
- package/dist/es/node/fs.mjs +1 -1
- package/dist/lib/cli/cli-args.js +138 -0
- package/dist/lib/cli/cli-error.js +61 -0
- package/dist/lib/cli/cli-runner.js +19 -46
- package/dist/lib/cli/index.js +8 -3
- package/dist/lib/constants/example-code.js +2 -2
- package/dist/lib/constants/index.js +23 -3
- package/dist/lib/env/model-config-manager.js +3 -0
- package/dist/lib/env/parse-model-config.js +6 -1
- package/dist/lib/env/types.js +4 -0
- package/dist/lib/env/utils.js +0 -11
- package/dist/lib/extractor/index.js +3 -0
- package/dist/lib/img/index.js +3 -0
- package/dist/lib/img/info.js +21 -21
- package/dist/lib/key-alias-utils.js +62 -0
- package/dist/lib/mcp/base-server.js +11 -10
- package/dist/lib/mcp/base-tools.js +74 -6
- package/dist/lib/mcp/cli-report-session.js +121 -0
- package/dist/lib/mcp/error-formatter.js +53 -0
- package/dist/lib/mcp/index.js +24 -10
- package/dist/lib/mcp/init-arg-utils.js +78 -0
- package/dist/lib/mcp/inject-report-html-plugin.js +1 -1
- package/dist/lib/mcp/tool-generator.js +74 -26
- package/dist/lib/node/fs.js +1 -1
- package/dist/types/cli/cli-args.d.ts +8 -0
- package/dist/types/cli/cli-error.d.ts +5 -0
- package/dist/types/cli/cli-runner.d.ts +4 -7
- package/dist/types/cli/index.d.ts +3 -1
- package/dist/types/constants/example-code.d.ts +1 -1
- package/dist/types/constants/index.d.ts +5 -0
- package/dist/types/env/types.d.ts +10 -2
- package/dist/types/env/utils.d.ts +0 -31
- package/dist/types/extractor/index.d.ts +1 -1
- package/dist/types/img/index.d.ts +1 -1
- package/dist/types/img/info.d.ts +5 -0
- package/dist/types/key-alias-utils.d.ts +9 -0
- package/dist/types/mcp/base-tools.d.ts +74 -5
- package/dist/types/mcp/cli-report-session.d.ts +12 -0
- package/dist/types/mcp/error-formatter.d.ts +12 -0
- package/dist/types/mcp/index.d.ts +2 -0
- package/dist/types/mcp/init-arg-utils.d.ts +13 -0
- package/dist/types/mcp/inject-report-html-plugin.d.ts +2 -2
- package/dist/types/mcp/launcher-helper.d.ts +3 -3
- package/dist/types/mcp/tool-generator.d.ts +3 -3
- package/dist/types/mcp/types.d.ts +9 -0
- package/package.json +1 -1
- package/src/cli/cli-args.ts +173 -0
- package/src/cli/cli-error.ts +24 -0
- package/src/cli/cli-runner.ts +37 -56
- package/src/cli/index.ts +3 -7
- package/src/constants/example-code.ts +2 -2
- package/src/constants/index.ts +10 -0
- package/src/env/model-config-manager.ts +3 -0
- package/src/env/parse-model-config.ts +19 -1
- package/src/env/types.ts +11 -2
- package/src/env/utils.ts +0 -50
- package/src/extractor/index.ts +1 -1
- package/src/img/index.ts +2 -0
- package/src/img/info.ts +36 -45
- package/src/key-alias-utils.ts +23 -0
- package/src/mcp/base-server.ts +10 -12
- package/src/mcp/base-tools.ts +218 -11
- package/src/mcp/cli-report-session.ts +130 -0
- package/src/mcp/error-formatter.ts +52 -0
- package/src/mcp/index.ts +2 -0
- package/src/mcp/init-arg-utils.ts +105 -0
- package/src/mcp/inject-report-html-plugin.ts +3 -3
- package/src/mcp/launcher-helper.ts +3 -3
- package/src/mcp/tool-generator.ts +98 -20
- package/src/mcp/types.ts +16 -1
|
@@ -24,10 +24,26 @@ import {
|
|
|
24
24
|
} from './types';
|
|
25
25
|
|
|
26
26
|
import { getDebug } from '../logger';
|
|
27
|
+
|
|
27
28
|
import { assert } from '../utils';
|
|
28
29
|
import { maskConfig, parseJson } from './helper';
|
|
29
30
|
import { initDebugConfig } from './init-debug';
|
|
30
31
|
|
|
32
|
+
declare const __VERSION__: string | undefined;
|
|
33
|
+
|
|
34
|
+
const MODEL_CONFIG_DOC_URL = 'https://midscenejs.com/model-common-config.html';
|
|
35
|
+
|
|
36
|
+
const getCurrentVersion = (): string => {
|
|
37
|
+
if (typeof __VERSION__ !== 'undefined' && __VERSION__) {
|
|
38
|
+
return __VERSION__;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return 'unknown';
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getInvalidModelFamilyMessage = (modelFamily: TModelFamily): string =>
|
|
45
|
+
`Invalid MIDSCENE_MODEL_FAMILY value: ${modelFamily}. Current version v${getCurrentVersion()} accepts the following model families: ${MODEL_FAMILY_VALUES.join(', ')}. You can also visit ${MODEL_CONFIG_DOC_URL} for the latest configuration information.`;
|
|
46
|
+
|
|
31
47
|
type TModelConfigKeys =
|
|
32
48
|
| typeof INSIGHT_MODEL_CONFIG_KEYS
|
|
33
49
|
| typeof PLANNING_MODEL_CONFIG_KEYS
|
|
@@ -70,7 +86,7 @@ export const getUITarsModelVersion = (
|
|
|
70
86
|
*/
|
|
71
87
|
export const validateModelFamily = (modelFamily?: TModelFamily): void => {
|
|
72
88
|
if (modelFamily && !MODEL_FAMILY_VALUES.includes(modelFamily as any)) {
|
|
73
|
-
throw new Error(
|
|
89
|
+
throw new Error(getInvalidModelFamilyMessage(modelFamily));
|
|
74
90
|
}
|
|
75
91
|
};
|
|
76
92
|
|
|
@@ -228,6 +244,7 @@ export const parseOpenaiSdkConfig = ({
|
|
|
228
244
|
modelName: modelName!,
|
|
229
245
|
modelDescription,
|
|
230
246
|
intent: '-' as any,
|
|
247
|
+
slot: '-' as any,
|
|
231
248
|
timeout: provider[keys.timeout]
|
|
232
249
|
? Number(provider[keys.timeout])
|
|
233
250
|
: undefined,
|
|
@@ -292,6 +309,7 @@ export const decideModelConfigFromIntentConfig = (
|
|
|
292
309
|
useLegacyLogic: intent === 'default',
|
|
293
310
|
});
|
|
294
311
|
finalResult.intent = intent;
|
|
312
|
+
finalResult.slot = intent;
|
|
295
313
|
|
|
296
314
|
debugLog(
|
|
297
315
|
'decideModelConfig result by agent.modelConfig() with intent',
|
package/src/env/types.ts
CHANGED
|
@@ -69,6 +69,8 @@ export const MIDSCENE_ANDROID_IME_STRATEGY = 'MIDSCENE_ANDROID_IME_STRATEGY';
|
|
|
69
69
|
|
|
70
70
|
export const MIDSCENE_IOS_DEVICE_UDID = 'MIDSCENE_IOS_DEVICE_UDID';
|
|
71
71
|
export const MIDSCENE_IOS_SIMULATOR_UDID = 'MIDSCENE_IOS_SIMULATOR_UDID';
|
|
72
|
+
export const MIDSCENE_IOS_DEVICE_CLASS_OVERRIDE =
|
|
73
|
+
'MIDSCENE_IOS_DEVICE_CLASS_OVERRIDE';
|
|
72
74
|
|
|
73
75
|
export const MIDSCENE_CACHE = 'MIDSCENE_CACHE';
|
|
74
76
|
export const MIDSCENE_USE_VLM_UI_TARS = 'MIDSCENE_USE_VLM_UI_TARS';
|
|
@@ -425,7 +427,7 @@ export enum UITarsModelVersion {
|
|
|
425
427
|
|
|
426
428
|
/**
|
|
427
429
|
* Callback to create custom OpenAI client instance
|
|
428
|
-
* @param config - Resolved model configuration including apiKey, baseURL, modelName, intent, etc.
|
|
430
|
+
* @param config - Resolved model configuration including apiKey, baseURL, modelName, intent, slot, etc.
|
|
429
431
|
* @returns OpenAI client instance (can be wrapped with langsmith, langfuse, etc.)
|
|
430
432
|
*
|
|
431
433
|
* Note: Wrapper functions like langsmith's wrapOpenAI() return the same OpenAI instance
|
|
@@ -519,9 +521,16 @@ export interface IModelConfig {
|
|
|
519
521
|
uiTarsModelVersion?: UITarsModelVersion;
|
|
520
522
|
modelDescription: string;
|
|
521
523
|
/**
|
|
522
|
-
*
|
|
524
|
+
* The semantic intent this config is requested for.
|
|
525
|
+
* For example, getModelConfig('planning') always returns intent === 'planning'.
|
|
523
526
|
*/
|
|
524
527
|
intent: TIntent;
|
|
528
|
+
/**
|
|
529
|
+
* The model-config slot this config was resolved from.
|
|
530
|
+
* For example, getModelConfig('planning') may resolve from slot === 'default'
|
|
531
|
+
* when MIDSCENE_PLANNING_MODEL_NAME is not configured.
|
|
532
|
+
*/
|
|
533
|
+
slot: TIntent;
|
|
525
534
|
/**
|
|
526
535
|
* Custom OpenAI client factory function
|
|
527
536
|
*
|
package/src/env/utils.ts
CHANGED
|
@@ -13,56 +13,6 @@ export const globalConfigManager = new GlobalConfigManager();
|
|
|
13
13
|
globalConfigManager.registerModelConfigManager(globalModelConfigManager);
|
|
14
14
|
globalModelConfigManager.registerGlobalConfigManager(globalConfigManager);
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Interface for devices that support getTimestamp method.
|
|
18
|
-
* This is a minimal interface to avoid circular dependencies with @midscene/core.
|
|
19
|
-
*/
|
|
20
|
-
export interface DeviceWithTimestamp {
|
|
21
|
-
getTimestamp?: () => Promise<number>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Get the current timestamp, optionally from the target device.
|
|
26
|
-
*
|
|
27
|
-
* When useDeviceTimestamp is enabled and a device with getTimestamp is provided,
|
|
28
|
-
* this function will return the device's time. Otherwise, it returns the system time.
|
|
29
|
-
*
|
|
30
|
-
* This is useful when:
|
|
31
|
-
* - Testing on devices with different time zones
|
|
32
|
-
* - Debugging time-sensitive features
|
|
33
|
-
* - The system clock and device clock are not synchronized
|
|
34
|
-
*
|
|
35
|
-
* @param device Optional device interface that supports getTimestamp
|
|
36
|
-
* @param useDeviceTimestamp Whether to use device timestamp (from agent config)
|
|
37
|
-
* @returns Timestamp in milliseconds
|
|
38
|
-
*
|
|
39
|
-
* @example
|
|
40
|
-
* // Without device - always returns system time
|
|
41
|
-
* const systemTime = await getCurrentTime();
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* // With device and config enabled - returns device time
|
|
45
|
-
* const deviceTime = await getCurrentTime(androidDevice, true);
|
|
46
|
-
*/
|
|
47
|
-
export async function getCurrentTime(
|
|
48
|
-
device?: DeviceWithTimestamp,
|
|
49
|
-
useDeviceTimestamp?: boolean,
|
|
50
|
-
): Promise<number> {
|
|
51
|
-
if (useDeviceTimestamp && device?.getTimestamp) {
|
|
52
|
-
try {
|
|
53
|
-
return await device.getTimestamp();
|
|
54
|
-
} catch (error) {
|
|
55
|
-
// Fall back to system time if device time retrieval fails
|
|
56
|
-
console.warn(
|
|
57
|
-
`Failed to get device time, falling back to system time: ${error}`,
|
|
58
|
-
);
|
|
59
|
-
return Date.now();
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return Date.now();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
16
|
export const getPreferredLanguage = () => {
|
|
67
17
|
const prefer = globalConfigManager.getEnvConfigValue(
|
|
68
18
|
MIDSCENE_PREFERRED_LANGUAGE,
|
package/src/extractor/index.ts
CHANGED
package/src/img/index.ts
CHANGED
package/src/img/info.ts
CHANGED
|
@@ -14,52 +14,16 @@ export interface ImageInfo extends Size {}
|
|
|
14
14
|
export async function imageInfoOfBase64(
|
|
15
15
|
imageBase64: string,
|
|
16
16
|
): Promise<ImageInfo> {
|
|
17
|
-
|
|
18
|
-
if (!imageBase64 || typeof imageBase64 !== 'string') {
|
|
19
|
-
throw new Error('Invalid image base64: data is empty or not a string');
|
|
20
|
-
}
|
|
21
|
-
|
|
17
|
+
const { PhotonImage } = await getPhoton();
|
|
22
18
|
const base64Data = imageBase64.replace(/^data:image\/\w+;base64,/, '');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64Data)) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
'Invalid image base64: contains invalid characters',
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Check minimum length for a valid image (at least a few bytes)
|
|
38
|
-
if (base64Data.length < 4) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
`Invalid image base64: data too short (${base64Data.length} characters)`,
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const { PhotonImage } = await getPhoton();
|
|
46
|
-
// Support both sync (Photon) and async (Canvas fallback) versions
|
|
47
|
-
const result = PhotonImage.new_from_base64(base64Data);
|
|
48
|
-
const image = result instanceof Promise ? await result : result;
|
|
49
|
-
const width = image.get_width();
|
|
50
|
-
const height = image.get_height();
|
|
51
|
-
image.free();
|
|
52
|
-
assert(width && height, 'Invalid image: cannot get width or height');
|
|
53
|
-
return { width, height };
|
|
54
|
-
} catch (error) {
|
|
55
|
-
// Provide more helpful error message for Photon failures
|
|
56
|
-
if (error instanceof Error && error.message.includes('unreachable')) {
|
|
57
|
-
throw new Error(
|
|
58
|
-
`Failed to decode image: invalid or corrupted image data. Original error: ${error.message}`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
19
|
+
// Support both sync (Photon) and async (Canvas fallback) versions
|
|
20
|
+
const result = PhotonImage.new_from_base64(base64Data);
|
|
21
|
+
const image = result instanceof Promise ? await result : result;
|
|
22
|
+
const width = image.get_width();
|
|
23
|
+
const height = image.get_height();
|
|
24
|
+
image.free();
|
|
25
|
+
assert(width && height, 'Invalid image: cannot get width or height');
|
|
26
|
+
return { width, height };
|
|
63
27
|
}
|
|
64
28
|
|
|
65
29
|
/**
|
|
@@ -109,3 +73,30 @@ export function isValidJPEGImageBuffer(buffer: Buffer): boolean {
|
|
|
109
73
|
export function isValidImageBuffer(buffer: Buffer): boolean {
|
|
110
74
|
return isValidPNGImageBuffer(buffer) || isValidJPEGImageBuffer(buffer);
|
|
111
75
|
}
|
|
76
|
+
|
|
77
|
+
export interface ValidateScreenshotBufferOptions {
|
|
78
|
+
label: string;
|
|
79
|
+
minBufferSize?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function validateScreenshotBuffer(
|
|
83
|
+
screenshotBuffer: Buffer | undefined,
|
|
84
|
+
{ label, minBufferSize = 0 }: ValidateScreenshotBufferOptions,
|
|
85
|
+
): asserts screenshotBuffer is Buffer {
|
|
86
|
+
const bufferSize = screenshotBuffer?.length ?? 0;
|
|
87
|
+
if (!screenshotBuffer || bufferSize === 0) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`${label} validation failed: buffer size ${bufferSize} bytes`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!isValidImageBuffer(screenshotBuffer)) {
|
|
94
|
+
throw new Error(`${label} buffer has invalid image format`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (minBufferSize > 0 && bufferSize < minBufferSize) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`${label} validation failed: buffer size ${bufferSize} bytes (minimum: ${minBufferSize})`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal-only helpers for CLI/MCP argument key aliasing.
|
|
3
|
+
* Not re-exported from the package entry point — keep consumers within
|
|
4
|
+
* `cli/` and `mcp/`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export function kebabToCamel(str: string): string {
|
|
8
|
+
return str.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function camelToKebab(str: string): string {
|
|
12
|
+
return str
|
|
13
|
+
.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
|
|
14
|
+
.replace(/^-/, '');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getKeyAliases(key: string): string[] {
|
|
18
|
+
return [...new Set([key, kebabToCamel(key), camelToKebab(key)])];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
package/src/mcp/base-server.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import type { ParseArgsConfig } from 'node:util';
|
|
3
|
+
import { setIsMcp } from '@aiscene/shared/utils';
|
|
3
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
6
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
@@ -8,7 +9,7 @@ import express, {
|
|
|
8
9
|
type Request,
|
|
9
10
|
type Response,
|
|
10
11
|
} from 'express';
|
|
11
|
-
import {
|
|
12
|
+
import { getErrorMessage } from './error-formatter';
|
|
12
13
|
import type { IMidsceneTools } from './types';
|
|
13
14
|
|
|
14
15
|
export interface BaseMCPServerConfig {
|
|
@@ -119,7 +120,7 @@ export abstract class BaseMCPServer {
|
|
|
119
120
|
try {
|
|
120
121
|
await this.toolsManager.initTools();
|
|
121
122
|
} catch (error: unknown) {
|
|
122
|
-
const message =
|
|
123
|
+
const message = getErrorMessage(error);
|
|
123
124
|
console.error(`Failed to initialize tools: ${message}`);
|
|
124
125
|
console.error('Tools will be initialized on first use');
|
|
125
126
|
}
|
|
@@ -160,7 +161,7 @@ export abstract class BaseMCPServer {
|
|
|
160
161
|
try {
|
|
161
162
|
await this.mcpServer.connect(transport);
|
|
162
163
|
} catch (error: unknown) {
|
|
163
|
-
const message =
|
|
164
|
+
const message = getErrorMessage(error);
|
|
164
165
|
console.error(`Failed to connect MCP stdio transport: ${message}`);
|
|
165
166
|
throw new Error(`Failed to initialize MCP stdio transport: ${message}`);
|
|
166
167
|
}
|
|
@@ -282,7 +283,7 @@ export abstract class BaseMCPServer {
|
|
|
282
283
|
.json({ error: 'Invalid session or GET without session' });
|
|
283
284
|
}
|
|
284
285
|
} catch (error: unknown) {
|
|
285
|
-
const message =
|
|
286
|
+
const message = getErrorMessage(error);
|
|
286
287
|
const duration = Date.now() - startTime;
|
|
287
288
|
console.error(
|
|
288
289
|
`[${new Date().toISOString()}] [${requestId}] MCP request error after ${duration}ms: ${message}`,
|
|
@@ -336,8 +337,7 @@ export abstract class BaseMCPServer {
|
|
|
336
337
|
try {
|
|
337
338
|
await session.transport.close();
|
|
338
339
|
} catch (error: unknown) {
|
|
339
|
-
const message =
|
|
340
|
-
error instanceof Error ? error.message : String(error);
|
|
340
|
+
const message = getErrorMessage(error);
|
|
341
341
|
console.error(
|
|
342
342
|
`Failed to close session ${session.transport.sessionId}: ${message}`,
|
|
343
343
|
);
|
|
@@ -390,7 +390,7 @@ export abstract class BaseMCPServer {
|
|
|
390
390
|
try {
|
|
391
391
|
await this.mcpServer.connect(transport);
|
|
392
392
|
} catch (error: unknown) {
|
|
393
|
-
const message =
|
|
393
|
+
const message = getErrorMessage(error);
|
|
394
394
|
console.error(
|
|
395
395
|
`[${new Date().toISOString()}] Failed to connect MCP transport: ${message}`,
|
|
396
396
|
);
|
|
@@ -425,8 +425,7 @@ export abstract class BaseMCPServer {
|
|
|
425
425
|
`[${new Date().toISOString()}] Session ${sid} cleaned up due to inactivity (remaining: ${sessions.size})`,
|
|
426
426
|
);
|
|
427
427
|
} catch (error: unknown) {
|
|
428
|
-
const message =
|
|
429
|
-
error instanceof Error ? error.message : String(error);
|
|
428
|
+
const message = getErrorMessage(error);
|
|
430
429
|
console.error(
|
|
431
430
|
`[${new Date().toISOString()}] Failed to close session ${sid} during cleanup: ${message}`,
|
|
432
431
|
);
|
|
@@ -455,8 +454,7 @@ export abstract class BaseMCPServer {
|
|
|
455
454
|
try {
|
|
456
455
|
session.transport.close();
|
|
457
456
|
} catch (error: unknown) {
|
|
458
|
-
const message =
|
|
459
|
-
error instanceof Error ? error.message : String(error);
|
|
457
|
+
const message = getErrorMessage(error);
|
|
460
458
|
console.error(`Error closing session during shutdown: ${message}`);
|
|
461
459
|
}
|
|
462
460
|
}
|
|
@@ -475,7 +473,7 @@ export abstract class BaseMCPServer {
|
|
|
475
473
|
this.performCleanup().finally(() => process.exit(1));
|
|
476
474
|
}, 5000);
|
|
477
475
|
} catch (error: unknown) {
|
|
478
|
-
const message =
|
|
476
|
+
const message = getErrorMessage(error);
|
|
479
477
|
console.error(`Error closing HTTP server: ${message}`);
|
|
480
478
|
this.performCleanup().finally(() => process.exit(1));
|
|
481
479
|
}
|
package/src/mcp/base-tools.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
|
+
import { parseBase64 } from '@aiscene/shared/img';
|
|
2
|
+
import { getDebug } from '@aiscene/shared/logger';
|
|
1
3
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
+
import type { z } from 'zod';
|
|
5
|
+
import { camelToKebab, getKeyAliases } from '../key-alias-utils';
|
|
6
|
+
import {
|
|
7
|
+
type CliReportSession,
|
|
8
|
+
generateCliReportSession,
|
|
9
|
+
readCliReportSession,
|
|
10
|
+
writeCliReportSession,
|
|
11
|
+
} from './cli-report-session';
|
|
12
|
+
import {
|
|
13
|
+
createNamespacedInitArgSchema,
|
|
14
|
+
extractNamespacedArgs,
|
|
15
|
+
sanitizeNamespacedArgs,
|
|
16
|
+
} from './init-arg-utils';
|
|
4
17
|
import {
|
|
5
18
|
generateCommonTools,
|
|
6
19
|
generateToolsFromActionSpace,
|
|
@@ -10,22 +23,68 @@ import type {
|
|
|
10
23
|
BaseAgent,
|
|
11
24
|
BaseDevice,
|
|
12
25
|
IMidsceneTools,
|
|
26
|
+
ToolCliMetadata,
|
|
13
27
|
ToolDefinition,
|
|
28
|
+
ToolSchema,
|
|
14
29
|
} from './types';
|
|
15
30
|
|
|
16
31
|
const debug = getDebug('mcp:base-tools');
|
|
17
32
|
|
|
18
33
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
34
|
+
* Declarative description of a platform's agent init args.
|
|
35
|
+
* Collapses the `extractAgentInitParam` / `sanitizeToolArgs` /
|
|
36
|
+
* `getAgentInitArgSchema` trio into a single data declaration.
|
|
37
|
+
*/
|
|
38
|
+
export interface InitArgSpec<TInitParam> {
|
|
39
|
+
/** Arg namespace, e.g. `android`, `ios`. */
|
|
40
|
+
namespace: string;
|
|
41
|
+
/** Zod shape describing the init args. Field names drive the MCP schema. */
|
|
42
|
+
shape: Record<string, z.ZodTypeAny>;
|
|
43
|
+
/**
|
|
44
|
+
* Optional CLI presentation hints. These affect `--help` output for
|
|
45
|
+
* single-platform CLIs but do not alter MCP/YAML protocol keys.
|
|
46
|
+
*/
|
|
47
|
+
cli?: {
|
|
48
|
+
/** Prefer bare `--device-id`-style options in platform CLI help output. */
|
|
49
|
+
preferBareKeys?: boolean;
|
|
50
|
+
/** Override the displayed option name for specific init arg fields. */
|
|
51
|
+
preferredNames?: Record<string, string>;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Adapt extracted namespaced args into the concrete `TInitParam` passed to
|
|
55
|
+
* `ensureAgent`. Defaults to returning the raw extracted record.
|
|
56
|
+
*/
|
|
57
|
+
adapt?: (
|
|
58
|
+
extracted: Record<string, unknown> | undefined,
|
|
59
|
+
) => TInitParam | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Base class for platform-specific MCP tools.
|
|
64
|
+
* @typeParam TAgent - Platform-specific agent type.
|
|
65
|
+
* @typeParam TInitParam - Platform-specific init parameter consumed by
|
|
66
|
+
* `ensureAgent`. Defaults to `undefined` for platforms that take no args.
|
|
21
67
|
*/
|
|
22
|
-
export abstract class BaseMidsceneTools<
|
|
23
|
-
|
|
68
|
+
export abstract class BaseMidsceneTools<
|
|
69
|
+
TAgent extends BaseAgent = BaseAgent,
|
|
70
|
+
TInitParam = unknown,
|
|
71
|
+
> implements IMidsceneTools
|
|
24
72
|
{
|
|
25
73
|
protected mcpServer?: McpServer;
|
|
26
74
|
protected agent?: TAgent;
|
|
27
75
|
protected toolDefinitions: ToolDefinition[] = [];
|
|
28
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Declarative init-arg spec. Subclasses that accept CLI/MCP init args should
|
|
79
|
+
* set this once and get `extractAgentInitParam` / `sanitizeToolArgs` /
|
|
80
|
+
* `getAgentInitArgSchema` auto-implemented.
|
|
81
|
+
*
|
|
82
|
+
* Declared with `declare` so that TS doesn't emit an `Object.defineProperty`
|
|
83
|
+
* for this field on the base constructor, which would otherwise overwrite
|
|
84
|
+
* a subclass field initializer under `useDefineForClassFields`.
|
|
85
|
+
*/
|
|
86
|
+
protected declare readonly initArgSpec?: InitArgSpec<TInitParam>;
|
|
87
|
+
|
|
29
88
|
/**
|
|
30
89
|
* Ensure agent is initialized and ready for use.
|
|
31
90
|
* Must be implemented by subclasses to create platform-specific agent.
|
|
@@ -33,7 +92,102 @@ export abstract class BaseMidsceneTools<TAgent extends BaseAgent = BaseAgent>
|
|
|
33
92
|
* @returns Promise resolving to initialized agent instance
|
|
34
93
|
* @throws Error if agent initialization fails
|
|
35
94
|
*/
|
|
36
|
-
protected abstract ensureAgent(initParam?:
|
|
95
|
+
protected abstract ensureAgent(initParam?: TInitParam): Promise<TAgent>;
|
|
96
|
+
|
|
97
|
+
private getInitArgKeys(): readonly string[] {
|
|
98
|
+
return this.initArgSpec ? Object.keys(this.initArgSpec.shape) : [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extract a platform-specific agent init parameter from CLI/MCP tool args.
|
|
103
|
+
*/
|
|
104
|
+
protected extractAgentInitParam(
|
|
105
|
+
args: Record<string, unknown>,
|
|
106
|
+
): TInitParam | undefined {
|
|
107
|
+
if (!this.initArgSpec) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const extracted = extractNamespacedArgs(
|
|
111
|
+
args,
|
|
112
|
+
this.initArgSpec.namespace,
|
|
113
|
+
this.getInitArgKeys(),
|
|
114
|
+
);
|
|
115
|
+
if (this.initArgSpec.adapt) {
|
|
116
|
+
return this.initArgSpec.adapt(extracted);
|
|
117
|
+
}
|
|
118
|
+
return extracted as TInitParam | undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Remove platform-specific init args before dispatching a tool payload to the action itself.
|
|
123
|
+
*/
|
|
124
|
+
protected sanitizeToolArgs(
|
|
125
|
+
args: Record<string, unknown>,
|
|
126
|
+
): Record<string, unknown> {
|
|
127
|
+
if (!this.initArgSpec) {
|
|
128
|
+
return args;
|
|
129
|
+
}
|
|
130
|
+
return sanitizeNamespacedArgs(
|
|
131
|
+
args,
|
|
132
|
+
this.initArgSpec.namespace,
|
|
133
|
+
this.getInitArgKeys(),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Expose platform-specific init args on action/common tool schemas.
|
|
139
|
+
*/
|
|
140
|
+
protected getAgentInitArgSchema(): ToolSchema {
|
|
141
|
+
if (!this.initArgSpec) {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
return createNamespacedInitArgSchema(
|
|
145
|
+
this.initArgSpec.namespace,
|
|
146
|
+
this.initArgSpec.shape,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Expose CLI-only metadata for platform init args so single-platform help can
|
|
152
|
+
* show ergonomic bare flags while the underlying schema stays namespaced.
|
|
153
|
+
* When `preferBareKeys` is enabled, single-platform CLIs only accept the
|
|
154
|
+
* bare spellings; namespaced dotted spellings remain available through the
|
|
155
|
+
* MCP/YAML schema instead of the platform CLI surface.
|
|
156
|
+
*/
|
|
157
|
+
protected getAgentInitArgCliMetadata(): ToolCliMetadata | undefined {
|
|
158
|
+
if (!this.initArgSpec?.cli) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const options = Object.fromEntries(
|
|
163
|
+
this.getInitArgKeys().map((key) => {
|
|
164
|
+
const canonicalKey = `${this.initArgSpec!.namespace}.${key}`;
|
|
165
|
+
const preferredName =
|
|
166
|
+
this.initArgSpec!.cli?.preferredNames?.[key] ??
|
|
167
|
+
(this.initArgSpec!.cli?.preferBareKeys
|
|
168
|
+
? camelToKebab(key)
|
|
169
|
+
: canonicalKey);
|
|
170
|
+
|
|
171
|
+
const acceptedNames = new Set<string>([
|
|
172
|
+
preferredName,
|
|
173
|
+
...(this.initArgSpec!.cli?.preferBareKeys
|
|
174
|
+
? getKeyAliases(key)
|
|
175
|
+
: getKeyAliases(canonicalKey)),
|
|
176
|
+
]);
|
|
177
|
+
acceptedNames.delete(preferredName);
|
|
178
|
+
|
|
179
|
+
return [
|
|
180
|
+
canonicalKey,
|
|
181
|
+
{
|
|
182
|
+
preferredName,
|
|
183
|
+
aliases: [...acceptedNames],
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return { options };
|
|
190
|
+
}
|
|
37
191
|
|
|
38
192
|
/**
|
|
39
193
|
* Optional: prepare platform-specific tools (e.g., device connection)
|
|
@@ -42,6 +196,52 @@ export abstract class BaseMidsceneTools<TAgent extends BaseAgent = BaseAgent>
|
|
|
42
196
|
return [];
|
|
43
197
|
}
|
|
44
198
|
|
|
199
|
+
protected getCliReportSessionName(): string | undefined {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
protected createNewCliReportSession(
|
|
204
|
+
targetIdentity?: string,
|
|
205
|
+
): CliReportSession | undefined {
|
|
206
|
+
const sessionName = this.getCliReportSessionName();
|
|
207
|
+
if (!sessionName) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
return generateCliReportSession(sessionName, targetIdentity);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
protected commitCliReportSession(session?: CliReportSession): void {
|
|
214
|
+
if (session) {
|
|
215
|
+
writeCliReportSession(session);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
protected readCliReportFileName(): string | undefined {
|
|
220
|
+
const sessionName = this.getCliReportSessionName();
|
|
221
|
+
if (!sessionName) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
return readCliReportSession(sessionName)?.reportFileName;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
protected readCliReportAgentOptions():
|
|
228
|
+
| {
|
|
229
|
+
reportFileName: string;
|
|
230
|
+
reportAttributes: Record<string, string>;
|
|
231
|
+
}
|
|
232
|
+
| undefined {
|
|
233
|
+
const reportFileName = this.readCliReportFileName();
|
|
234
|
+
if (!reportFileName) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
reportFileName,
|
|
239
|
+
reportAttributes: {
|
|
240
|
+
'data-group-id': reportFileName,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
45
245
|
/**
|
|
46
246
|
* Must be implemented by subclasses to create a temporary device instance
|
|
47
247
|
* This allows getting real actionSpace without connecting to device
|
|
@@ -83,13 +283,20 @@ export abstract class BaseMidsceneTools<TAgent extends BaseAgent = BaseAgent>
|
|
|
83
283
|
}
|
|
84
284
|
|
|
85
285
|
// 3. Generate tools from action space (core innovation)
|
|
86
|
-
const actionTools = generateToolsFromActionSpace(
|
|
87
|
-
|
|
286
|
+
const actionTools = generateToolsFromActionSpace(
|
|
287
|
+
actionSpace,
|
|
288
|
+
(args = {}) => this.ensureAgent(this.extractAgentInitParam(args)),
|
|
289
|
+
(args = {}) => this.sanitizeToolArgs(args),
|
|
290
|
+
this.getAgentInitArgSchema(),
|
|
291
|
+
this.getAgentInitArgCliMetadata(),
|
|
88
292
|
);
|
|
89
293
|
|
|
90
294
|
// 4. Add common tools (screenshot, waitFor)
|
|
91
|
-
const commonTools = generateCommonTools(
|
|
92
|
-
|
|
295
|
+
const commonTools = generateCommonTools(
|
|
296
|
+
(args = {}) => this.ensureAgent(this.extractAgentInitParam(args)),
|
|
297
|
+
this.getAgentInitArgSchema(),
|
|
298
|
+
this.getAgentInitArgCliMetadata(),
|
|
299
|
+
);
|
|
93
300
|
this.toolDefinitions.push(...actionTools, ...commonTools);
|
|
94
301
|
|
|
95
302
|
debug('Total tools prepared:', this.toolDefinitions.length);
|