mobile-debug-mcp 0.24.0 → 0.24.1

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.
@@ -6,6 +6,9 @@ import { execAdb, spawnAdb, getAndroidDeviceMetadata, getDeviceInfo, findApk } f
6
6
  import { execAdbWithDiagnostics } from '../utils/diagnostics.js';
7
7
  import { detectJavaHome } from '../utils/java.js';
8
8
  export class AndroidManage {
9
+ isTestOnlyInstallFailure(output) {
10
+ return typeof output === 'string' && output.includes('INSTALL_FAILED_TEST_ONLY');
11
+ }
9
12
  async build(projectPath, _variant) {
10
13
  void _variant;
11
14
  try {
@@ -93,6 +96,13 @@ export class AndroidManage {
93
96
  if (res.code === 0) {
94
97
  return { device: deviceInfo, installed: true, output: res.stdout };
95
98
  }
99
+ const installOutput = `${res.stdout}\n${res.stderr}`.trim();
100
+ if (this.isTestOnlyInstallFailure(installOutput)) {
101
+ const retryRes = await spawnAdb(['install', '-r', '-t', apkToInstall], deviceId);
102
+ if (retryRes.code === 0) {
103
+ return { device: deviceInfo, installed: true, output: retryRes.stdout };
104
+ }
105
+ }
96
106
  }
97
107
  catch (e) {
98
108
  console.debug('[android-run] adb install failed, attempting push+pm fallback:', e instanceof Error ? e.message : String(e));
@@ -100,12 +110,25 @@ export class AndroidManage {
100
110
  const basename = path.basename(apkToInstall);
101
111
  const remotePath = `/data/local/tmp/${basename}`;
102
112
  await execAdb(['push', apkToInstall, remotePath], deviceId);
103
- const pmOut = await execAdb(['shell', 'pm', 'install', '-r', remotePath], deviceId);
113
+ let finalPmRes = await spawnAdb(['shell', 'pm', 'install', '-r', remotePath], deviceId);
104
114
  try {
105
- await execAdb(['shell', 'rm', remotePath], deviceId);
115
+ if (finalPmRes.code === 0) {
116
+ return { device: deviceInfo, installed: true, output: finalPmRes.stdout };
117
+ }
118
+ if (this.isTestOnlyInstallFailure(`${finalPmRes.stdout}\n${finalPmRes.stderr}`)) {
119
+ finalPmRes = await spawnAdb(['shell', 'pm', 'install', '-r', '-t', remotePath], deviceId);
120
+ if (finalPmRes.code === 0) {
121
+ return { device: deviceInfo, installed: true, output: finalPmRes.stdout };
122
+ }
123
+ }
124
+ throw new Error(finalPmRes.stderr || finalPmRes.stdout || 'pm install failed');
125
+ }
126
+ finally {
127
+ try {
128
+ await execAdb(['shell', 'rm', remotePath], deviceId);
129
+ }
130
+ catch { }
106
131
  }
107
- catch { }
108
- return { device: deviceInfo, installed: true, output: pmOut };
109
132
  }
110
133
  catch (e) {
111
134
  // gather diagnostics for attempted adb operations
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  All notable changes to the **Mobile Debug MCP** project will be documented in this file.
4
4
 
5
+ ## [0.24.1]
6
+ - Fixed Android install issue
7
+
5
8
  ## [0.24.0]
6
9
  - Improved execution loop
7
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,10 @@ import { detectJavaHome } from '../utils/java.js'
8
8
  import { InstallAppResponse, StartAppResponse, TerminateAppResponse, RestartAppResponse, ResetAppDataResponse } from '../types.js'
9
9
 
10
10
  export class AndroidManage {
11
+ private isTestOnlyInstallFailure(output: string | undefined): boolean {
12
+ return typeof output === 'string' && output.includes('INSTALL_FAILED_TEST_ONLY')
13
+ }
14
+
11
15
  async build(projectPath: string, _variant?: string): Promise<{ artifactPath: string, output?: string } | { error: string }> {
12
16
  void _variant
13
17
  try {
@@ -92,6 +96,14 @@ export class AndroidManage {
92
96
  if (res.code === 0) {
93
97
  return { device: deviceInfo, installed: true, output: res.stdout }
94
98
  }
99
+
100
+ const installOutput = `${res.stdout}\n${res.stderr}`.trim()
101
+ if (this.isTestOnlyInstallFailure(installOutput)) {
102
+ const retryRes = await spawnAdb(['install', '-r', '-t', apkToInstall], deviceId)
103
+ if (retryRes.code === 0) {
104
+ return { device: deviceInfo, installed: true, output: retryRes.stdout }
105
+ }
106
+ }
95
107
  } catch (e) {
96
108
  console.debug('[android-run] adb install failed, attempting push+pm fallback:', e instanceof Error ? e.message : String(e))
97
109
  }
@@ -99,9 +111,21 @@ export class AndroidManage {
99
111
  const basename = path.basename(apkToInstall)
100
112
  const remotePath = `/data/local/tmp/${basename}`
101
113
  await execAdb(['push', apkToInstall, remotePath], deviceId)
102
- const pmOut = await execAdb(['shell', 'pm', 'install', '-r', remotePath], deviceId)
103
- try { await execAdb(['shell', 'rm', remotePath], deviceId) } catch {}
104
- return { device: deviceInfo, installed: true, output: pmOut }
114
+ let finalPmRes = await spawnAdb(['shell', 'pm', 'install', '-r', remotePath], deviceId)
115
+ try {
116
+ if (finalPmRes.code === 0) {
117
+ return { device: deviceInfo, installed: true, output: finalPmRes.stdout }
118
+ }
119
+ if (this.isTestOnlyInstallFailure(`${finalPmRes.stdout}\n${finalPmRes.stderr}`)) {
120
+ finalPmRes = await spawnAdb(['shell', 'pm', 'install', '-r', '-t', remotePath], deviceId)
121
+ if (finalPmRes.code === 0) {
122
+ return { device: deviceInfo, installed: true, output: finalPmRes.stdout }
123
+ }
124
+ }
125
+ throw new Error(finalPmRes.stderr || finalPmRes.stdout || 'pm install failed')
126
+ } finally {
127
+ try { await execAdb(['shell', 'rm', remotePath], deviceId) } catch {}
128
+ }
105
129
  } catch (e) {
106
130
  // gather diagnostics for attempted adb operations
107
131
  const basename = path.basename(apkToInstall)
@@ -69,8 +69,101 @@ exit 0
69
69
  assert.ok(res2.output && typeof res2.output === 'string', 'Project dir install succeeded with output')
70
70
  }
71
71
 
72
+ const testOnlyAdbPath = path.join(binDir, 'adb-test-only')
73
+ const testOnlyAdbScript = `#!/bin/sh
74
+ if [ "$1" = "-s" ]; then
75
+ shift 2
76
+ fi
77
+
78
+ if [ "$1" = "shell" ] && [ "$2" = "getprop" ]; then
79
+ case "$3" in
80
+ ro.build.version.release) echo '16' ;;
81
+ ro.product.model) echo 'sdk_gphone64_arm64' ;;
82
+ ro.kernel.qemu) echo '1' ;;
83
+ esac
84
+ exit 0
85
+ fi
86
+
87
+ if [ "$1" = "install" ]; then
88
+ if [ "$2" = "-r" ] && [ "$3" = "-t" ]; then
89
+ echo 'Performing Streamed Install'
90
+ echo 'Success'
91
+ exit 0
92
+ fi
93
+ echo 'Performing Streamed Install'
94
+ echo 'adb: failed to install test.apk: Failure [INSTALL_FAILED_TEST_ONLY: Failed to install test-only apk. Did you forget to add -t?]' 1>&2
95
+ exit 1
96
+ fi
97
+
98
+ echo 'Success'
99
+ exit 0
100
+ `
101
+ await fs.writeFile(testOnlyAdbPath, testOnlyAdbScript, { mode: 0o755 })
102
+ process.env.ADB_PATH = testOnlyAdbPath
103
+
104
+ const { dir: d2, file: testOnlyApk } = await makeTempFile('.apk')
105
+ const testOnlyRes = await ai.installApp(testOnlyApk, 'emulator-5554')
106
+ console.log('testOnlyRes', testOnlyRes)
107
+ assert.strictEqual(testOnlyRes.installed, true, 'Test-only APK should retry with -t and install successfully')
108
+
109
+ const cleanupLog = path.join(binDir, 'pm-cleanup.log')
110
+ const pmFallbackAdbPath = path.join(binDir, 'adb-pm-fallback')
111
+ const pmFallbackAdbScript = `#!/bin/sh
112
+ if [ "$1" = "-s" ]; then
113
+ shift 2
114
+ fi
115
+
116
+ if [ "$1" = "shell" ] && [ "$2" = "getprop" ]; then
117
+ case "$3" in
118
+ ro.build.version.release) echo '16' ;;
119
+ ro.product.model) echo 'sdk_gphone64_arm64' ;;
120
+ ro.kernel.qemu) echo '1' ;;
121
+ esac
122
+ exit 0
123
+ fi
124
+
125
+ if [ "$1" = "install" ]; then
126
+ echo 'adb install failed' 1>&2
127
+ exit 1
128
+ fi
129
+
130
+ if [ "$1" = "push" ]; then
131
+ echo 'pushed'
132
+ exit 0
133
+ fi
134
+
135
+ if [ "$1" = "shell" ] && [ "$2" = "pm" ] && [ "$3" = "install" ]; then
136
+ if [ "$4" = "-r" ] && [ "$5" = "-t" ]; then
137
+ echo 'Failure [INSTALL_FAILED_VERSION_DOWNGRADE]'
138
+ exit 1
139
+ fi
140
+ echo 'Failure [INSTALL_FAILED_TEST_ONLY: Failed to install test-only apk. Did you forget to add -t?]'
141
+ exit 1
142
+ fi
143
+
144
+ if [ "$1" = "shell" ] && [ "$2" = "rm" ]; then
145
+ echo cleanup >> "${cleanupLog}"
146
+ exit 0
147
+ fi
148
+
149
+ echo 'unexpected args:' "$@" 1>&2
150
+ exit 1
151
+ `
152
+ await fs.writeFile(pmFallbackAdbPath, pmFallbackAdbScript, { mode: 0o755 })
153
+ process.env.ADB_PATH = pmFallbackAdbPath
154
+
155
+ const { dir: d3, file: pmFallbackApk } = await makeTempFile('.apk')
156
+ const pmFallbackRes = await ai.installApp(pmFallbackApk, 'emulator-5554')
157
+ console.log('pmFallbackRes', pmFallbackRes)
158
+ assert.strictEqual(pmFallbackRes.installed, false, 'Failed pm fallback should surface as install failure')
159
+ assert.match(pmFallbackRes.error || '', /INSTALL_FAILED_VERSION_DOWNGRADE/, 'Final pm retry failure should be reported')
160
+ const cleanupCount = (await fs.readFile(cleanupLog, 'utf8')).trim().split('\n').filter(Boolean).length
161
+ assert.strictEqual(cleanupCount, 1, 'pm fallback cleanup should run once')
162
+
72
163
  // cleanup
73
164
  await fs.rm(d1, { recursive: true, force: true }).catch(() => {})
165
+ await fs.rm(d2, { recursive: true, force: true }).catch(() => {})
166
+ await fs.rm(d3, { recursive: true, force: true }).catch(() => {})
74
167
  await fs.rm(dirGradle, { recursive: true, force: true }).catch(() => {})
75
168
 
76
169
  // restore PATH and ADB_PATH
@@ -87,4 +180,4 @@ exit 0
87
180
  }
88
181
  }
89
182
 
90
- run().catch((e) => { console.error(e); process.exit(1) })
183
+ run().catch((e) => { console.error(e); process.exit(1) })