mobile-debug-mcp 0.24.5 → 0.24.7
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/.github/workflows/ci.yml +1 -3
- package/README.md +7 -0
- package/dist/manage/android.js +9 -5
- package/dist/manage/index.js +37 -23
- package/dist/manage/ios.js +12 -15
- package/dist/observe/index.js +2 -2
- package/dist/server/common.js +46 -0
- package/dist/server/tool-handlers.js +120 -33
- package/dist/server-core.js +1 -1
- package/dist/utils/android/utils.js +17 -5
- package/dist/utils/cli/idb/check-idb.js +1 -1
- package/docs/CHANGELOG.md +16 -8
- package/docs/tools/observe.md +2 -2
- package/eslint.config.js +2 -47
- package/package.json +7 -6
- package/src/manage/android.ts +22 -11
- package/src/manage/index.ts +37 -16
- package/src/manage/ios.ts +28 -15
- package/src/observe/index.ts +2 -2
- package/src/server/common.ts +50 -0
- package/src/server/tool-handlers.ts +136 -32
- package/src/server-core.ts +1 -1
- package/src/utils/android/utils.ts +18 -7
- package/src/utils/cli/idb/check-idb.ts +1 -1
- package/test/device/automated/observe/capture_screenshot.android.smoke.ts +1 -1
- package/test/device/automated/observe/capture_screenshot.ios.smoke.ts +1 -1
- package/test/device/automated/observe/get_logs.android.smoke.ts +1 -1
- package/test/device/automated/observe/get_logs.ios.smoke.ts +1 -1
- package/test/device/automated/observe/get_ui_tree.android.smoke.ts +1 -1
- package/test/device/automated/observe/get_ui_tree.ios.smoke.ts +1 -1
- package/test/device/manual/interact/app_lifecycle.manual.ts +3 -3
- package/test/device/manual/observe/capture_screenshot.manual.ts +2 -2
- package/test/device/manual/observe/get_logs.manual.ts +2 -2
- package/test/device/manual/observe/get_ui_tree.manual.ts +2 -2
- package/test/device/manual/observe/logstream.manual.ts +1 -1
- package/test/device/manual/observe/screen_fingerprint.manual.ts +2 -2
- package/test/unit/manage/scoped_env.test.ts +137 -0
- package/test/unit/server/capture_screenshot.test.ts +17 -0
- package/test/unit/server/common.test.ts +18 -0
- package/test/unit/server/contract.test.ts +3 -0
- package/test/unit/server/get_logs.test.ts +17 -0
- package/test/unit/server/get_network_activity.test.ts +17 -0
- package/test/unit/server/get_ui_tree.test.ts +17 -0
- package/test/unit/server/response_shapes.test.ts +18 -0
- package/test/unit/server/start_log_stream.test.ts +37 -0
- package/.eslintignore +0 -5
- package/.eslintrc.cjs +0 -18
- package/eslint.config.cjs +0 -36
|
@@ -57,23 +57,36 @@ export function ensureAdbAvailable() {
|
|
|
57
57
|
return { adbCmd: adb, ok: false, error: String(err) };
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
function mergeEnv(overrides) {
|
|
61
|
+
const env = {};
|
|
62
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
63
|
+
if (typeof value === 'string')
|
|
64
|
+
env[key] = value;
|
|
65
|
+
}
|
|
66
|
+
for (const [key, value] of Object.entries(overrides || {})) {
|
|
67
|
+
if (typeof value === 'string')
|
|
68
|
+
env[key] = value;
|
|
69
|
+
}
|
|
70
|
+
return env;
|
|
71
|
+
}
|
|
60
72
|
/**
|
|
61
73
|
* Prepare Gradle execution options for building an Android project.
|
|
62
74
|
* Returns execCmd (wrapper or gradle), base gradleArgs array, and spawn options including env.
|
|
63
75
|
*/
|
|
64
|
-
export async function prepareGradle(projectPath) {
|
|
76
|
+
export async function prepareGradle(projectPath, envOverrides = {}) {
|
|
77
|
+
const env = mergeEnv(envOverrides);
|
|
65
78
|
const gradlewPath = path.join(projectPath, 'gradlew');
|
|
66
79
|
const gradleCmd = existsSync(gradlewPath) ? './gradlew' : 'gradle';
|
|
67
80
|
const execCmd = existsSync(gradlewPath) ? gradlewPath : gradleCmd;
|
|
68
81
|
// Start with a default task; callers may append/override via env flags
|
|
69
|
-
const gradleArgs = [
|
|
82
|
+
const gradleArgs = [env.MCP_GRADLE_TASK || 'assembleDebug'];
|
|
70
83
|
// Respect generic MCP_BUILD_JOBS and Android-specific MCP_GRADLE_WORKERS
|
|
71
|
-
const workers =
|
|
84
|
+
const workers = env.MCP_GRADLE_WORKERS || env.MCP_BUILD_JOBS;
|
|
72
85
|
if (workers) {
|
|
73
86
|
gradleArgs.push(`--max-workers=${workers}`);
|
|
74
87
|
}
|
|
75
88
|
// Respect gradle cache env: default enabled; set MCP_GRADLE_CACHE=0 to disable
|
|
76
|
-
if (
|
|
89
|
+
if (env.MCP_GRADLE_CACHE === '0') {
|
|
77
90
|
gradleArgs.push('-Dorg.gradle.caching=false');
|
|
78
91
|
}
|
|
79
92
|
const detectedJavaHome = await detectJavaHome().catch(() => undefined);
|
|
@@ -85,7 +98,6 @@ export async function prepareGradle(projectPath) {
|
|
|
85
98
|
catch {
|
|
86
99
|
gradleCheck = { gradleJavaHome: undefined, gradleValid: false, filesChecked: [], issues: [] };
|
|
87
100
|
}
|
|
88
|
-
const env = Object.assign({}, process.env);
|
|
89
101
|
// Ensure child processes can find Android platform-tools (adb, etc.) by
|
|
90
102
|
// prepending the platform-tools directory to PATH for spawned processes.
|
|
91
103
|
const adbPath = resolveAdbCmd();
|
|
@@ -24,7 +24,7 @@ async function runInstaller() {
|
|
|
24
24
|
// prefer invoking the TS script via npx/tsx to ensure environment
|
|
25
25
|
const runner = which('npx') ? 'npx' : which('tsx') ? 'tsx' : null;
|
|
26
26
|
if (runner) {
|
|
27
|
-
const args = runner === 'npx' ? ['tsx', './src/cli/idb/install-idb.ts'] : ['./src/cli/idb/install-idb.ts'];
|
|
27
|
+
const args = runner === 'npx' ? ['tsx', './src/utils/cli/idb/install-idb.ts'] : ['./src/utils/cli/idb/install-idb.ts'];
|
|
28
28
|
const res = spawnSync(runner, args, { stdio: 'inherit' });
|
|
29
29
|
return typeof res.status === 'number' ? res.status === 0 : false;
|
|
30
30
|
}
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.24.7]
|
|
6
|
+
- Aligned runtime metadata with the published package version.
|
|
7
|
+
- Fixed stale CLI helper paths in npm scripts and the `idb` healthcheck helper.
|
|
8
|
+
- Simplified ESLint configuration by keeping the flat config and removing legacy config files.
|
|
9
|
+
- Updated CI to use the current automated device test runner.
|
|
10
|
+
- Tightened server handler argument parsing and added contract coverage for version and required-argument error responses.
|
|
11
|
+
- Scoped temporary build environment overrides to the duration of each build helper call and added regression coverage for env restoration.
|
|
12
|
+
|
|
5
13
|
## [0.24.5]
|
|
6
14
|
- Improved snapshots
|
|
7
15
|
|
|
@@ -24,7 +32,7 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
|
|
|
24
32
|
|
|
25
33
|
## [0.23.0]
|
|
26
34
|
- Added network monitoring
|
|
27
|
-
- Added
|
|
35
|
+
- Added action-outcome classification tooling for backend-driven flows without visible UI changes.
|
|
28
36
|
|
|
29
37
|
## [0.22.0]
|
|
30
38
|
- Added a portable `test-authoring` skill package and documented the repository's vendor-neutral skill format
|
|
@@ -35,14 +43,14 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
|
|
|
35
43
|
- Fixed incorrect timeout
|
|
36
44
|
|
|
37
45
|
## [0.21.4]
|
|
38
|
-
-
|
|
39
|
-
-
|
|
46
|
+
- Updated `wait_for_ui` with better contract and observability
|
|
47
|
+
- Updated `get_logs` to return more useful structured output
|
|
40
48
|
|
|
41
49
|
## [0.21.3]
|
|
42
50
|
- Added structured logs
|
|
43
51
|
|
|
44
52
|
## [0.21.2]
|
|
45
|
-
- Fixed screenshots not working
|
|
53
|
+
- Fixed screenshots not working and improved the tool output
|
|
46
54
|
|
|
47
55
|
## [0.21.1]
|
|
48
56
|
- Removed wait_for_element and renamed observe_until to wait_for_ui (obsolete references removed)
|
|
@@ -51,7 +59,7 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
|
|
|
51
59
|
- Added `wait_for_ui` as a tool for agents to wait for things like API requests
|
|
52
60
|
|
|
53
61
|
## [0.20.1]
|
|
54
|
-
-
|
|
62
|
+
- Fixed Gradle home handling for Android
|
|
55
63
|
|
|
56
64
|
## [0.20.0]
|
|
57
65
|
- Added `get_system_status` tool and refactored system health checks into `src/system`.
|
|
@@ -60,8 +68,8 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
|
|
|
60
68
|
|
|
61
69
|
|
|
62
70
|
## [0.19.2]
|
|
63
|
-
- Added healthcheck
|
|
64
|
-
- Added skills
|
|
71
|
+
- Added healthcheck improvements
|
|
72
|
+
- Added reusable agent skills
|
|
65
73
|
|
|
66
74
|
## [0.19.1]
|
|
67
75
|
|
|
@@ -110,7 +118,7 @@ All notable changes to the **Mobile Debug MCP** project will be documented in th
|
|
|
110
118
|
## [0.12.1]
|
|
111
119
|
- Improve iOS build/install reliability: project auto-scan, explicit simulator destination, configurable watchdog timeout (MCP_XCODEBUILD_TIMEOUT) and retries (MCP_XCODEBUILD_RETRIES), and DerivedData fallback for locating .app artifacts.
|
|
112
120
|
- Make install_app capable of building iOS projects before installing so agents can autonomously fix, build, install and validate apps.
|
|
113
|
-
- Migrate CLI scripts into typed
|
|
121
|
+
- Migrate CLI scripts into typed source modules and update npm scripts; fix ESM import paths and lint issues.
|
|
114
122
|
- Add preflight checks and idb resolution helpers (getIdbCmd, isIDBInstalled) and add idb_companion health checks.
|
|
115
123
|
- Capture build stdout/stderr into build-results/ for easier diagnostics and surfaced suggestions when KMP frameworks are missing.
|
|
116
124
|
- Add device test runner under test/device and gate device-dependent tests behind RUN_DEVICE_TESTS.
|
package/docs/tools/observe.md
CHANGED
|
@@ -147,7 +147,7 @@ Response (example):
|
|
|
147
147
|
"fingerprint": "abc123",
|
|
148
148
|
"screenshot": "<base64 PNG string>",
|
|
149
149
|
"ui_tree": { ... },
|
|
150
|
-
"logs": [ { "timestamp":
|
|
150
|
+
"logs": [ { "timestamp": "2024-03-09T12:00:00.000Z", "level": "ERROR", "tag": "CheckoutViewModel", "pid": 1234, "message": "NullPointerException at CheckoutViewModel" } ]
|
|
151
151
|
},
|
|
152
152
|
"semantic": {
|
|
153
153
|
"screen": "Checkout",
|
|
@@ -203,5 +203,5 @@ Start a background adb logcat stream and retrieve parsed NDJSON entries.
|
|
|
203
203
|
read_log_stream response example:
|
|
204
204
|
|
|
205
205
|
```json
|
|
206
|
-
{ "entries": [ { "timestamp": "2026-03-20T...Z", "level": "
|
|
206
|
+
{ "entries": [ { "timestamp": "2026-03-20T...Z", "level": "ERROR", "tag": "AppTag", "pid": 1234, "message": "FATAL EXCEPTION" } ], "crash_summary": { "crash_detected": true } }
|
|
207
207
|
```
|
package/eslint.config.js
CHANGED
|
@@ -3,7 +3,6 @@ import tsPlugin from '@typescript-eslint/eslint-plugin'
|
|
|
3
3
|
import unusedImports from 'eslint-plugin-unused-imports'
|
|
4
4
|
|
|
5
5
|
export default [
|
|
6
|
-
// Files/directories to ignore
|
|
7
6
|
{
|
|
8
7
|
ignores: [
|
|
9
8
|
'dist/',
|
|
@@ -11,55 +10,11 @@ export default [
|
|
|
11
10
|
'.git/',
|
|
12
11
|
'.vscode/',
|
|
13
12
|
'coverage/',
|
|
14
|
-
'.env'
|
|
13
|
+
'.env'
|
|
15
14
|
]
|
|
16
15
|
},
|
|
17
|
-
// Apply rules to JS/TS source
|
|
18
16
|
{
|
|
19
|
-
files: ['src/**/*.ts', 'src/**/*.js'],
|
|
20
|
-
languageOptions: {
|
|
21
|
-
parser: tsParser,
|
|
22
|
-
parserOptions: {
|
|
23
|
-
ecmaVersion: 2020,
|
|
24
|
-
sourceType: 'module',
|
|
25
|
-
project: './tsconfig.json'
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
plugins: {
|
|
29
|
-
'@typescript-eslint': tsPlugin,
|
|
30
|
-
'unused-imports': unusedImports
|
|
31
|
-
},
|
|
32
|
-
rules: {
|
|
33
|
-
// Use plugin to error on unused imports and provide autofix where possible
|
|
34
|
-
'unused-imports/no-unused-imports': 'error',
|
|
35
|
-
'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
|
36
|
-
// Disable the default TS rule to avoid duplicate warnings
|
|
37
|
-
'@typescript-eslint/no-unused-vars': 'off'
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
// Apply lighter rules to test files (no project reference to avoid TS project parsing)
|
|
41
|
-
{
|
|
42
|
-
files: ['test/**/*.ts', 'test/**/*.js'],
|
|
43
|
-
languageOptions: {
|
|
44
|
-
parser: tsParser,
|
|
45
|
-
parserOptions: {
|
|
46
|
-
ecmaVersion: 2020,
|
|
47
|
-
sourceType: 'module'
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
plugins: {
|
|
51
|
-
'@typescript-eslint': tsPlugin,
|
|
52
|
-
'unused-imports': unusedImports
|
|
53
|
-
},
|
|
54
|
-
rules: {
|
|
55
|
-
'unused-imports/no-unused-imports': 'error',
|
|
56
|
-
'unused-imports/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
|
|
57
|
-
'@typescript-eslint/no-unused-vars': 'off'
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
// Apply rules to CLI tooling
|
|
61
|
-
{
|
|
62
|
-
files: ['src/cli/**/*.ts', 'src/cli/**/*.js'],
|
|
17
|
+
files: ['src/**/*.ts', 'src/**/*.js', 'test/**/*.ts', 'test/**/*.js'],
|
|
63
18
|
languageOptions: {
|
|
64
19
|
parser: tsParser,
|
|
65
20
|
parserOptions: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-debug-mcp",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.7",
|
|
4
4
|
"description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,15 +10,16 @@
|
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"start": "node ./dist/server.js",
|
|
12
12
|
"prepare": "npm run build",
|
|
13
|
-
"healthcheck": "tsx ./src/cli/idb/check-idb.ts",
|
|
14
|
-
"install-idb": "tsx ./src/cli/idb/install-idb.ts",
|
|
15
|
-
"preflight-ios": "tsx ./src/cli/ios/preflight-ios.ts",
|
|
13
|
+
"healthcheck": "tsx ./src/utils/cli/idb/check-idb.ts",
|
|
14
|
+
"install-idb": "tsx ./src/utils/cli/idb/install-idb.ts",
|
|
15
|
+
"preflight-ios": "tsx ./src/utils/cli/ios/preflight-ios.ts",
|
|
16
16
|
"test:unit": "tsx test/unit/index.ts",
|
|
17
17
|
"test:integration": "npm run test:device",
|
|
18
18
|
"test:device": "npm run build && tsx test/device/index.ts",
|
|
19
19
|
"test": "npm run test:unit",
|
|
20
|
-
"lint": "eslint
|
|
21
|
-
"lint:fix": "eslint
|
|
20
|
+
"lint": "eslint src test --quiet",
|
|
21
|
+
"lint:fix": "eslint src test --fix",
|
|
22
|
+
"verify": "npm run lint && npm run build && npm run test:unit"
|
|
22
23
|
},
|
|
23
24
|
|
|
24
25
|
"engines": {
|
package/src/manage/android.ts
CHANGED
|
@@ -2,22 +2,33 @@ import { promises as fs } from 'fs'
|
|
|
2
2
|
import { spawn } from 'child_process'
|
|
3
3
|
import path from 'path'
|
|
4
4
|
import { existsSync } from 'fs'
|
|
5
|
-
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } from '../utils/android/utils.js'
|
|
5
|
+
import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk, prepareGradle } from '../utils/android/utils.js'
|
|
6
6
|
import { execAdbWithDiagnostics } from '../utils/diagnostics.js'
|
|
7
7
|
import { detectJavaHome } from '../utils/java.js'
|
|
8
8
|
import { AndroidObserve } from '../observe/android.js'
|
|
9
9
|
import { InstallAppResponse, StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from '../types.js'
|
|
10
10
|
|
|
11
|
+
type BuildEnv = Record<string, string | undefined>
|
|
12
|
+
|
|
13
|
+
export interface AndroidBuildOptions {
|
|
14
|
+
variant?: string
|
|
15
|
+
env?: BuildEnv
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
export class AndroidManage {
|
|
12
19
|
private isTestOnlyInstallFailure(output: string | undefined): boolean {
|
|
13
20
|
return typeof output === 'string' && output.includes('INSTALL_FAILED_TEST_ONLY')
|
|
14
21
|
}
|
|
15
22
|
|
|
16
|
-
async build(projectPath: string,
|
|
17
|
-
|
|
23
|
+
async build(projectPath: string, optionsOrVariant?: string | AndroidBuildOptions): Promise<{ artifactPath: string, output?: string } | { error: string }> {
|
|
24
|
+
const options: AndroidBuildOptions = typeof optionsOrVariant === 'string' ? { variant: optionsOrVariant } : (optionsOrVariant || {})
|
|
18
25
|
try {
|
|
26
|
+
const env = {
|
|
27
|
+
...(options.env || {}),
|
|
28
|
+
...(options.variant ? { MCP_GRADLE_TASK: options.variant } : {})
|
|
29
|
+
}
|
|
19
30
|
// Always use the shared prepareGradle utility for consistent env/setup
|
|
20
|
-
const { execCmd, gradleArgs, spawnOpts } = await
|
|
31
|
+
const { execCmd, gradleArgs, spawnOpts } = await prepareGradle(projectPath, env)
|
|
21
32
|
await new Promise<void>((resolve, reject) => {
|
|
22
33
|
const proc = spawn(execCmd, gradleArgs, spawnOpts)
|
|
23
34
|
let stderr = ''
|
|
@@ -43,13 +54,13 @@ export class AndroidManage {
|
|
|
43
54
|
|
|
44
55
|
let apkToInstall: string = apkPath
|
|
45
56
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
const stat = await fs.stat(apkPath).catch(() => null)
|
|
58
|
+
if (stat && stat.isDirectory()) {
|
|
59
|
+
const detectedJavaHome = await detectJavaHome().catch(() => undefined)
|
|
60
|
+
const env = { ...process.env }
|
|
61
|
+
if (detectedJavaHome) {
|
|
62
|
+
if (env.JAVA_HOME !== detectedJavaHome) {
|
|
63
|
+
env.JAVA_HOME = detectedJavaHome
|
|
53
64
|
env.PATH = `${path.join(detectedJavaHome, 'bin')}${path.delimiter}${env.PATH || ''}`
|
|
54
65
|
console.debug('[android-run] Overriding JAVA_HOME with detected path:', detectedJavaHome)
|
|
55
66
|
}
|
package/src/manage/index.ts
CHANGED
|
@@ -60,25 +60,37 @@ export async function detectProjectPlatform(projectPath: string): Promise<'ios'|
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
type BuildEnv = Record<string, string | undefined>
|
|
64
|
+
|
|
65
|
+
function mergeDefinedEnv(...parts: Array<BuildEnv | undefined>): BuildEnv {
|
|
66
|
+
const merged: BuildEnv = {}
|
|
67
|
+
for (const part of parts) {
|
|
68
|
+
if (!part) continue
|
|
69
|
+
for (const [key, value] of Object.entries(part)) {
|
|
70
|
+
if (typeof value === 'undefined') continue
|
|
71
|
+
merged[key] = value
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return merged
|
|
75
|
+
}
|
|
76
|
+
|
|
63
77
|
export class ToolsManage {
|
|
64
78
|
static async build_android({ projectPath, gradleTask, maxWorkers, gradleCache, forceClean }: { projectPath: string, gradleTask?: string, maxWorkers?: number, gradleCache?: boolean, forceClean?: boolean }) {
|
|
65
79
|
const android = new AndroidManage()
|
|
66
|
-
// prepare gradle options via environment hints
|
|
67
|
-
if (typeof maxWorkers === 'number') process.env.MCP_GRADLE_WORKERS = String(maxWorkers)
|
|
68
|
-
if (typeof gradleCache === 'boolean') process.env.MCP_GRADLE_CACHE = gradleCache ? '1' : '0'
|
|
69
|
-
if (forceClean) process.env.MCP_FORCE_CLEAN_ANDROID = '1'
|
|
70
80
|
const task = gradleTask || 'assembleDebug'
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
return await (android as any).build(projectPath, {
|
|
82
|
+
variant: task,
|
|
83
|
+
env: mergeDefinedEnv({
|
|
84
|
+
MCP_GRADLE_TASK: task,
|
|
85
|
+
MCP_GRADLE_WORKERS: typeof maxWorkers === 'number' ? String(maxWorkers) : undefined,
|
|
86
|
+
MCP_GRADLE_CACHE: typeof gradleCache === 'boolean' ? (gradleCache ? '1' : '0') : undefined,
|
|
87
|
+
MCP_FORCE_CLEAN_ANDROID: forceClean ? '1' : undefined
|
|
88
|
+
})
|
|
89
|
+
})
|
|
73
90
|
}
|
|
74
91
|
|
|
75
92
|
static async build_ios({ projectPath, workspace: _workspace, project: _project, scheme: _scheme, destinationUDID, derivedDataPath, buildJobs, forceClean }: { projectPath: string, workspace?: string, project?: string, scheme?: string, destinationUDID?: string, derivedDataPath?: string, buildJobs?: number, forceClean?: boolean }) {
|
|
76
93
|
const ios = new iOSManage()
|
|
77
|
-
// Use provided options rather than env-only; still set env fallbacks for downstream tools
|
|
78
|
-
if (derivedDataPath) process.env.MCP_DERIVED_DATA = derivedDataPath
|
|
79
|
-
if (typeof buildJobs === 'number') process.env.MCP_BUILD_JOBS = String(buildJobs)
|
|
80
|
-
if (forceClean) process.env.MCP_FORCE_CLEAN_IOS = '1'
|
|
81
|
-
if (destinationUDID) process.env.MCP_XCODE_DESTINATION_UDID = destinationUDID
|
|
82
94
|
|
|
83
95
|
const opts: any = {}
|
|
84
96
|
if (_workspace) opts.workspace = _workspace
|
|
@@ -86,12 +98,21 @@ export class ToolsManage {
|
|
|
86
98
|
if (_scheme) opts.scheme = _scheme
|
|
87
99
|
if (destinationUDID) opts.destinationUDID = destinationUDID
|
|
88
100
|
if (derivedDataPath) opts.derivedDataPath = derivedDataPath
|
|
89
|
-
if (
|
|
101
|
+
if (typeof buildJobs === 'number') opts.buildJobs = buildJobs
|
|
102
|
+
if (typeof forceClean === 'boolean') opts.forceClean = forceClean
|
|
90
103
|
// prefer explicit xcodebuild path from env
|
|
91
104
|
if (process.env.XCODEBUILD_PATH) opts.xcodeCmd = process.env.XCODEBUILD_PATH
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
return await (ios as any).build(projectPath, {
|
|
107
|
+
...opts,
|
|
108
|
+
env: mergeDefinedEnv({
|
|
109
|
+
MCP_DERIVED_DATA: derivedDataPath,
|
|
110
|
+
MCP_XCODE_JOBS: typeof buildJobs === 'number' ? String(buildJobs) : undefined,
|
|
111
|
+
MCP_FORCE_CLEAN: typeof forceClean === 'boolean' ? (forceClean ? '1' : '0') : undefined,
|
|
112
|
+
MCP_XCODE_DESTINATION_UDID: destinationUDID,
|
|
113
|
+
XCODEBUILD_PATH: process.env.XCODEBUILD_PATH
|
|
114
|
+
})
|
|
115
|
+
})
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
static async build_flutter({ projectPath, platform, buildMode, maxWorkers: _maxWorkers, forceClean: _forceClean }: { projectPath: string, platform?: 'android'|'ios', buildMode?: 'debug'|'release'|'profile', maxWorkers?: number, forceClean?: boolean }) {
|
|
@@ -178,11 +199,11 @@ export class ToolsManage {
|
|
|
178
199
|
const chosen = platform || 'android'
|
|
179
200
|
if (chosen === 'android') {
|
|
180
201
|
const android = new AndroidManage()
|
|
181
|
-
const artifact = await (android as any).build(projectPath, variant)
|
|
202
|
+
const artifact = await (android as any).build(projectPath, { variant })
|
|
182
203
|
return artifact
|
|
183
204
|
} else {
|
|
184
205
|
const ios = new iOSManage()
|
|
185
|
-
const artifact = await (ios as any).build(projectPath, variant)
|
|
206
|
+
const artifact = await (ios as any).build(projectPath, { variant })
|
|
186
207
|
return artifact
|
|
187
208
|
}
|
|
188
209
|
}
|
package/src/manage/ios.ts
CHANGED
|
@@ -5,12 +5,25 @@ import { execCommand, execCommandWithDiagnostics, getIOSDeviceMetadata, validate
|
|
|
5
5
|
import { iOSObserve } from "../observe/ios.js"
|
|
6
6
|
import path from "path"
|
|
7
7
|
|
|
8
|
+
type BuildEnv = Record<string, string | undefined>
|
|
9
|
+
|
|
10
|
+
export interface IOSBuildOptions {
|
|
11
|
+
workspace?: string
|
|
12
|
+
project?: string
|
|
13
|
+
scheme?: string
|
|
14
|
+
destinationUDID?: string
|
|
15
|
+
derivedDataPath?: string
|
|
16
|
+
buildJobs?: number
|
|
17
|
+
forceClean?: boolean
|
|
18
|
+
xcodeCmd?: string
|
|
19
|
+
env?: BuildEnv
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
export class iOSManage {
|
|
9
|
-
async build(projectPath: string, optsOrVariant?: string |
|
|
23
|
+
async build(projectPath: string, optsOrVariant?: string | IOSBuildOptions): Promise<{ artifactPath: string, output?: string } | { error: string, diagnostics?: any }> {
|
|
10
24
|
// Support legacy variant string as second arg
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
else opts = optsOrVariant || {}
|
|
25
|
+
const opts: IOSBuildOptions = typeof optsOrVariant === 'string' ? {} : (optsOrVariant || {})
|
|
26
|
+
const env = { ...process.env, ...(opts.env || {}) }
|
|
14
27
|
|
|
15
28
|
try {
|
|
16
29
|
// Look for an Xcode workspace or project at the provided path. If not present, scan subdirectories (limited depth)
|
|
@@ -65,7 +78,7 @@ export class iOSManage {
|
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
// Determine destination: prefer explicit option, then env var, otherwise use booted simulator UDID
|
|
68
|
-
let destinationUDID = opts.destinationUDID ||
|
|
81
|
+
let destinationUDID = opts.destinationUDID || env.MCP_XCODE_DESTINATION_UDID || env.MCP_XCODE_DESTINATION || ''
|
|
69
82
|
if (!destinationUDID) {
|
|
70
83
|
try {
|
|
71
84
|
const meta = await getIOSDeviceMetadata('booted')
|
|
@@ -74,7 +87,7 @@ export class iOSManage {
|
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
// Determine xcode command early so it can be used when detecting schemes
|
|
77
|
-
const xcodeCmd = opts.xcodeCmd ||
|
|
90
|
+
const xcodeCmd = opts.xcodeCmd || env.XCODEBUILD_PATH || 'xcodebuild'
|
|
78
91
|
|
|
79
92
|
// Determine available schemes by querying xcodebuild -list rather than guessing
|
|
80
93
|
async function detectScheme(xcodeCmdInner: string, workspacePath?: string, projectPathFull?: string, cwd?: string): Promise<string | null> {
|
|
@@ -98,11 +111,11 @@ export class iOSManage {
|
|
|
98
111
|
let chosenScheme: string | null = opts.scheme || null
|
|
99
112
|
|
|
100
113
|
// Derived data and result bundle (agent-configurable)
|
|
101
|
-
const derivedDataPath = opts.derivedDataPath ||
|
|
114
|
+
const derivedDataPath = opts.derivedDataPath || env.MCP_DERIVED_DATA || path.join(projectRootDir, 'build', 'DerivedData')
|
|
102
115
|
// Use unique result bundle path by default to avoid collisions
|
|
103
|
-
const resultBundlePath =
|
|
104
|
-
const xcodeJobs = parseInt(
|
|
105
|
-
const forceClean = opts.forceClean
|
|
116
|
+
const resultBundlePath = env.MCP_XCODE_RESULTBUNDLE_PATH || path.join(projectRootDir, 'build', 'xcresults', `ResultBundle-${Date.now()}-${Math.random().toString(36).slice(2)}.xcresult`)
|
|
117
|
+
const xcodeJobs = typeof opts.buildJobs === 'number' ? opts.buildJobs : (parseInt(env.MCP_XCODE_JOBS || '', 10) || 4)
|
|
118
|
+
const forceClean = typeof opts.forceClean === 'boolean' ? opts.forceClean : env.MCP_FORCE_CLEAN === '1'
|
|
106
119
|
|
|
107
120
|
// ensure result dirs exist
|
|
108
121
|
await fs.mkdir(path.dirname(resultBundlePath), { recursive: true }).catch(() => {})
|
|
@@ -147,8 +160,8 @@ export class iOSManage {
|
|
|
147
160
|
await fs.mkdir(resultsDir, { recursive: true }).catch(() => {})
|
|
148
161
|
|
|
149
162
|
|
|
150
|
-
const XCODEBUILD_TIMEOUT = parseInt(
|
|
151
|
-
const MAX_RETRIES = parseInt(
|
|
163
|
+
const XCODEBUILD_TIMEOUT = parseInt(env.MCP_XCODEBUILD_TIMEOUT || '', 10) || 180000 // default 3 minutes
|
|
164
|
+
const MAX_RETRIES = parseInt(env.MCP_XCODEBUILD_RETRIES || '', 10) || 1
|
|
152
165
|
|
|
153
166
|
const tries = MAX_RETRIES + 1
|
|
154
167
|
let lastStdout = ''
|
|
@@ -157,8 +170,8 @@ export class iOSManage {
|
|
|
157
170
|
|
|
158
171
|
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
159
172
|
// Run xcodebuild with a watchdog
|
|
160
|
-
|
|
161
|
-
const proc = spawn(xcodeCmd, buildArgs, { cwd: projectRootDir })
|
|
173
|
+
const res = await new Promise<{ code: number | null, stdout: string, stderr: string, killedByWatchdog?: boolean }>((resolve) => {
|
|
174
|
+
const proc = spawn(xcodeCmd, buildArgs, { cwd: projectRootDir, env })
|
|
162
175
|
let stdout = ''
|
|
163
176
|
let stderr = ''
|
|
164
177
|
|
|
@@ -215,7 +228,7 @@ export class iOSManage {
|
|
|
215
228
|
if (lastErr) {
|
|
216
229
|
// Include diagnostics and result bundle path when available; provide structured info useful for agents
|
|
217
230
|
const invokedCommand = `${xcodeCmd} ${buildArgs.map(a => a.includes(' ') ? `"${a}"` : a).join(' ')}`
|
|
218
|
-
|
|
231
|
+
const envSnapshot = { PATH: env.PATH }
|
|
219
232
|
return { error: `xcodebuild failed: ${lastErr.message}. See build-results for logs.`, output: `stdout:\n${lastStdout}\nstderr:\n${lastStderr}`, diagnostics: { exitCode: (lastErr as any).code || null, invokedCommand, cwd: projectRootDir, envSnapshot } }
|
|
220
233
|
}
|
|
221
234
|
|
package/src/observe/index.ts
CHANGED
|
@@ -95,7 +95,7 @@ function deriveSnapshotSemantic(raw: CaptureDebugSnapshotRawResponse): SnapshotS
|
|
|
95
95
|
|
|
96
96
|
const hasErrorLogs = raw.logs.some((entry) => /error|fatal exception|exception|failed/i.test(entry.message))
|
|
97
97
|
const hasLoadingSignals = texts.some((text) => /loading|please wait|spinner|progress/i.test(text))
|
|
98
|
-
const hasPrimaryText = texts.some((text) => /sign in|log in|
|
|
98
|
+
const hasPrimaryText = texts.some((text) => /sign in|log in|login|home|checkout|settings|menu|profile|search/i.test(text))
|
|
99
99
|
const hasScreenshot = typeof raw.screenshot === 'string' && raw.screenshot.length > 0
|
|
100
100
|
const hasUiTree = !!tree && Array.isArray(tree.elements)
|
|
101
101
|
|
|
@@ -137,7 +137,7 @@ function deriveSnapshotSemantic(raw: CaptureDebugSnapshotRawResponse): SnapshotS
|
|
|
137
137
|
signals,
|
|
138
138
|
actions_available: actionables.length > 0 ? actionables.slice(0, 10) : null,
|
|
139
139
|
confidence,
|
|
140
|
-
warnings
|
|
140
|
+
warnings
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
package/src/server/common.ts
CHANGED
|
@@ -20,6 +20,56 @@ export type ToolCallResult = Awaited<ReturnType<typeof wrapResponse>> | {
|
|
|
20
20
|
}
|
|
21
21
|
export type ToolHandler = (args: ToolCallArgs) => Promise<ToolCallResult>
|
|
22
22
|
|
|
23
|
+
export function getStringArg(args: ToolCallArgs, key: string): string | undefined {
|
|
24
|
+
const value = args[key]
|
|
25
|
+
return typeof value === 'string' ? value : undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function requireStringArg(args: ToolCallArgs, key: string): string {
|
|
29
|
+
const value = getStringArg(args, key)
|
|
30
|
+
if (value === undefined) throw new Error(`Missing or invalid string argument: ${key}`)
|
|
31
|
+
return value
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getNumberArg(args: ToolCallArgs, key: string): number | undefined {
|
|
35
|
+
const value = args[key]
|
|
36
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function requireNumberArg(args: ToolCallArgs, key: string): number {
|
|
40
|
+
const value = getNumberArg(args, key)
|
|
41
|
+
if (value === undefined) throw new Error(`Missing or invalid number argument: ${key}`)
|
|
42
|
+
return value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getBooleanArg(args: ToolCallArgs, key: string): boolean | undefined {
|
|
46
|
+
const value = args[key]
|
|
47
|
+
return typeof value === 'boolean' ? value : undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function requireBooleanArg(args: ToolCallArgs, key: string): boolean {
|
|
51
|
+
const value = getBooleanArg(args, key)
|
|
52
|
+
if (value === undefined) throw new Error(`Missing or invalid boolean argument: ${key}`)
|
|
53
|
+
return value
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getObjectArg<T extends Record<string, unknown>>(args: ToolCallArgs, key: string): T | undefined {
|
|
57
|
+
const value = args[key]
|
|
58
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined
|
|
59
|
+
return value as T
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function requireObjectArg<T extends Record<string, unknown>>(args: ToolCallArgs, key: string): T {
|
|
63
|
+
const value = getObjectArg<T>(args, key)
|
|
64
|
+
if (value === undefined) throw new Error(`Missing or invalid object argument: ${key}`)
|
|
65
|
+
return value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getArrayArg<T>(args: ToolCallArgs, key: string): T[] | undefined {
|
|
69
|
+
const value = args[key]
|
|
70
|
+
return Array.isArray(value) ? value as T[] : undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
23
73
|
let actionSequence = 0
|
|
24
74
|
|
|
25
75
|
export function nextActionId(actionType: string, timestamp: number) {
|