mobile-debug-mcp 0.20.0 → 0.20.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.
package/README.md CHANGED
@@ -2,7 +2,10 @@
2
2
 
3
3
  A minimal, secure MCP server for AI-assisted mobile development. Build, install, and inspect Android/iOS apps from an MCP-compatible client.
4
4
 
5
- > **Note:** iOS support is limited currently, Please use with caution and report any issues.
5
+ > **Note:**
6
+ > * iOS only tested on simulator.
7
+ > * Flutter iOS projects not fetching logs
8
+ > * React native not tested
6
9
 
7
10
  ## Requirements
8
11
 
@@ -28,10 +31,9 @@ You will need to add ADB_PATH for Android and XCRUN_PATH and IDB_PATH for iOS.
28
31
 
29
32
  ## Usage
30
33
 
31
- Example:
32
- After a crash tell the agent the following:
33
-
34
- I have a crash on the app, can you diagnose it, fix and validate using the mcp tools available
34
+ Examples:
35
+ * I have a crash on the app, can you diagnose it, fix and validate using the mcp tools available
36
+ * Add a button, hook into the repository and confirm API request successful
35
37
 
36
38
  ## Docs
37
39
 
@@ -64,10 +64,13 @@ export class AndroidManage {
64
64
  const spawnOpts = { cwd: apkPath, env };
65
65
  if (useWrapper) {
66
66
  await fs.chmod(wrapperPath, 0o755).catch(() => { });
67
+ // Run wrapper directly to avoid shell splitting of args
68
+ spawnOpts.shell = false;
69
+ }
70
+ else {
71
+ // Execute gradle directly without a shell so paths with spaces are preserved
67
72
  spawnOpts.shell = false;
68
73
  }
69
- else
70
- spawnOpts.shell = true;
71
74
  const proc = spawn(execCmd, gradleArgs, spawnOpts);
72
75
  let stderr = '';
73
76
  await new Promise((resolve, reject) => {
@@ -0,0 +1,101 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ function readPropertiesFile(p) {
5
+ try {
6
+ const txt = readFileSync(p, { encoding: 'utf8' });
7
+ const lines = String(txt).split(/\r?\n/);
8
+ const out = {};
9
+ for (const l of lines) {
10
+ const trimmed = l.trim();
11
+ if (!trimmed || trimmed.startsWith('#'))
12
+ continue;
13
+ const idx = trimmed.indexOf('=');
14
+ if (idx === -1)
15
+ continue;
16
+ const k = trimmed.substring(0, idx).trim();
17
+ const v = trimmed.substring(idx + 1).trim();
18
+ out[k] = v;
19
+ }
20
+ return out;
21
+ }
22
+ catch (e) {
23
+ return {};
24
+ }
25
+ }
26
+ function javaBinExists(p) {
27
+ if (!p)
28
+ return false;
29
+ try {
30
+ const javaPath = path.join(p, 'bin', 'java');
31
+ if (existsSync(javaPath))
32
+ return true;
33
+ const alt = path.join(p, 'Contents', 'Home', 'bin', 'java');
34
+ if (existsSync(alt))
35
+ return true;
36
+ return false;
37
+ }
38
+ catch (e) {
39
+ return false;
40
+ }
41
+ }
42
+ export async function checkGradle() {
43
+ const issues = [];
44
+ const filesChecked = [];
45
+ const suggestedFixes = [];
46
+ let gradleJavaHome;
47
+ // 1) explicit env
48
+ if (process.env.GRADLE_JAVA_HOME) {
49
+ gradleJavaHome = process.env.GRADLE_JAVA_HOME;
50
+ if (!javaBinExists(gradleJavaHome)) {
51
+ issues.push(`GRADLE_JAVA_HOME is set to '${gradleJavaHome}' but no java binary was found there`);
52
+ suggestedFixes.push('Unset GRADLE_JAVA_HOME or point it to a valid JDK (e.g., /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home)');
53
+ }
54
+ }
55
+ // 2) user gradle.properties
56
+ const gradleUserHome = process.env.GRADLE_USER_HOME || path.join(os.homedir(), '.gradle');
57
+ const userProps = path.join(gradleUserHome, 'gradle.properties');
58
+ filesChecked.push(userProps);
59
+ try {
60
+ const props = readPropertiesFile(userProps);
61
+ if (props['org.gradle.java.home']) {
62
+ const p = props['org.gradle.java.home'];
63
+ gradleJavaHome = gradleJavaHome || p;
64
+ if (!javaBinExists(p)) {
65
+ issues.push(`org.gradle.java.home in ${userProps} points to '${p}' which does not look like a valid JDK`);
66
+ suggestedFixes.push(`Edit ${userProps} to remove or correct org.gradle.java.home`);
67
+ }
68
+ }
69
+ }
70
+ catch (e) { /* ignore */ }
71
+ // 3) system gradle.properties
72
+ const systemProps = '/etc/gradle/gradle.properties';
73
+ filesChecked.push(systemProps);
74
+ try {
75
+ const props = readPropertiesFile(systemProps);
76
+ if (props['org.gradle.java.home']) {
77
+ const p = props['org.gradle.java.home'];
78
+ gradleJavaHome = gradleJavaHome || p;
79
+ if (!javaBinExists(p)) {
80
+ issues.push(`org.gradle.java.home in ${systemProps} points to '${p}' which does not look like a valid JDK`);
81
+ suggestedFixes.push(`Edit ${systemProps} to remove or correct org.gradle.java.home`);
82
+ }
83
+ }
84
+ }
85
+ catch (e) { /* ignore */ }
86
+ // 4) GRADLE_HOME fallback
87
+ if (!gradleJavaHome && process.env.GRADLE_HOME) {
88
+ filesChecked.push(process.env.GRADLE_HOME);
89
+ if (javaBinExists(process.env.GRADLE_HOME)) {
90
+ gradleJavaHome = process.env.GRADLE_HOME;
91
+ }
92
+ }
93
+ const gradleValid = !!gradleJavaHome && javaBinExists(gradleJavaHome);
94
+ if (!gradleJavaHome) {
95
+ // no explicit gradle java home detected — not an issue
96
+ }
97
+ else if (!gradleValid) {
98
+ issues.push(`Detected org.gradle.java.home = '${gradleJavaHome}' but it is invalid`);
99
+ }
100
+ return { gradleJavaHome, gradleValid, filesChecked, issues, suggestedFixes };
101
+ }
@@ -1,10 +1,12 @@
1
1
  import { checkAndroid } from './android.js';
2
2
  import { checkIOS } from './ios.js';
3
+ import { checkGradle } from './gradle.js';
3
4
  export async function getSystemStatus() {
4
5
  try {
5
6
  const android = await checkAndroid();
6
7
  const ios = await checkIOS();
7
- const issues = [...android.issues, ...ios.issues];
8
+ const gradle = await checkGradle();
9
+ const issues = [...android.issues, ...ios.issues, ...(gradle.issues || [])];
8
10
  const success = issues.length === 0;
9
11
  return {
10
12
  success,
@@ -17,7 +19,11 @@ export async function getSystemStatus() {
17
19
  issues,
18
20
  appInstalled: android.appInstalled,
19
21
  iosAvailable: ios.iosAvailable,
20
- iosDevices: ios.iosDevices
22
+ iosDevices: ios.iosDevices,
23
+ gradleJavaHome: gradle.gradleJavaHome,
24
+ gradleValid: gradle.gradleValid,
25
+ gradleFilesChecked: gradle.filesChecked,
26
+ gradleSuggestedFixes: gradle.suggestedFixes
21
27
  };
22
28
  }
23
29
  catch (e) {
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { detectJavaHome } from '../java.js';
4
4
  import { execCmd } from '../exec.js';
5
5
  import { spawnSync } from 'child_process';
6
+ import { checkGradle } from '../../system/gradle.js';
6
7
  function findInPath(cmd) {
7
8
  try {
8
9
  // prefer command -v for POSIX
@@ -76,6 +77,14 @@ export async function prepareGradle(projectPath) {
76
77
  gradleArgs.push('-Dorg.gradle.caching=false');
77
78
  }
78
79
  const detectedJavaHome = await detectJavaHome().catch(() => undefined);
80
+ // Check for problematic org.gradle.java.home entries (env or properties) and avoid passing invalid values to Gradle
81
+ let gradleCheck;
82
+ try {
83
+ gradleCheck = await checkGradle();
84
+ }
85
+ catch (e) {
86
+ gradleCheck = { gradleJavaHome: undefined, gradleValid: false, filesChecked: [], issues: [] };
87
+ }
79
88
  const env = Object.assign({}, process.env);
80
89
  // Ensure child processes can find Android platform-tools (adb, etc.) by
81
90
  // prepending the platform-tools directory to PATH for spawned processes.
@@ -90,6 +99,7 @@ export async function prepareGradle(projectPath) {
90
99
  console.debug(`[prepareGradle] error resolving adbPath: ${String(e)}`);
91
100
  }
92
101
  const pathParts = [];
102
+ // Prefer a detected (validated) Java home from the system/IDE
93
103
  if (detectedJavaHome) {
94
104
  if (env.JAVA_HOME !== detectedJavaHome) {
95
105
  env.JAVA_HOME = detectedJavaHome;
@@ -100,6 +110,26 @@ export async function prepareGradle(projectPath) {
100
110
  gradleArgs.push('--no-daemon');
101
111
  env.GRADLE_JAVA_HOME = detectedJavaHome;
102
112
  }
113
+ else if (gradleCheck && gradleCheck.gradleJavaHome) {
114
+ // There's an org.gradle.java.home configured somewhere (env or gradle.properties)
115
+ if (gradleCheck.gradleValid) {
116
+ const p = gradleCheck.gradleJavaHome;
117
+ const javaBin = path.join(p, 'bin');
118
+ if (!env.PATH || !env.PATH.includes(javaBin))
119
+ pathParts.push(javaBin);
120
+ gradleArgs.push(`-Dorg.gradle.java.home=${p}`);
121
+ gradleArgs.push('--no-daemon');
122
+ env.GRADLE_JAVA_HOME = p;
123
+ }
124
+ else {
125
+ // Invalid gradle java home detected: avoid passing it to Gradle and remove from spawn env
126
+ console.debug(`[prepareGradle] Invalid org.gradle.java.home detected (${gradleCheck.gradleJavaHome}); removing from spawn env to avoid Gradle error.`);
127
+ try {
128
+ delete env.GRADLE_JAVA_HOME;
129
+ }
130
+ catch (e) { }
131
+ }
132
+ }
103
133
  if (platformToolsDir) {
104
134
  // Prepend platform-tools so gradle and child tools find adb without modifying global env
105
135
  if (!env.PATH || !env.PATH.includes(platformToolsDir)) {
@@ -138,10 +168,12 @@ export async function prepareGradle(projectPath) {
138
168
  catch (e) {
139
169
  console.debug('[prepareGradle] chmod failed for gradlew:', String(e));
140
170
  }
171
+ // Execute the wrapper directly without a shell to avoid shell tokenization of args (spaces in paths)
141
172
  spawnOpts.shell = false;
142
173
  }
143
174
  else {
144
- spawnOpts.shell = true;
175
+ // Prefer executing gradle directly without invoking a shell to preserve argument boundaries
176
+ spawnOpts.shell = false;
145
177
  }
146
178
  return { execCmd, gradleArgs, spawnOpts };
147
179
  }
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.20.1]
6
+ - Fixes gradle home issue for android
7
+
5
8
  ## [0.20.0]
6
9
  - Added `get_system_status` tool and refactored system health checks into `src/system`.
7
10
  - Provides a fast environment healthcheck (ADB availability/version, connected devices, log access, Android env vars, and basic iOS xcrun/simulator checks).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.20.0",
3
+ "version": "0.20.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": {
@@ -64,8 +64,12 @@ export class AndroidManage {
64
64
  const spawnOpts: any = { cwd: apkPath, env }
65
65
  if (useWrapper) {
66
66
  await fs.chmod(wrapperPath, 0o755).catch(() => {})
67
+ // Run wrapper directly to avoid shell splitting of args
67
68
  spawnOpts.shell = false
68
- } else spawnOpts.shell = true
69
+ } else {
70
+ // Execute gradle directly without a shell so paths with spaces are preserved
71
+ spawnOpts.shell = false
72
+ }
69
73
 
70
74
  const proc = spawn(execCmd, gradleArgs, spawnOpts)
71
75
  let stderr = ''
@@ -0,0 +1,98 @@
1
+ import { readFileSync, existsSync } from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+
5
+ function readPropertiesFile(p: string): Record<string,string> {
6
+ try {
7
+ const txt = readFileSync(p, { encoding: 'utf8' })
8
+ const lines = String(txt).split(/\r?\n/)
9
+ const out: Record<string,string> = {}
10
+ for (const l of lines) {
11
+ const trimmed = l.trim()
12
+ if (!trimmed || trimmed.startsWith('#')) continue
13
+ const idx = trimmed.indexOf('=')
14
+ if (idx === -1) continue
15
+ const k = trimmed.substring(0, idx).trim()
16
+ const v = trimmed.substring(idx+1).trim()
17
+ out[k] = v
18
+ }
19
+ return out
20
+ } catch (e: unknown) {
21
+ return {}
22
+ }
23
+ }
24
+
25
+ function javaBinExists(p?: string): boolean {
26
+ if (!p) return false
27
+ try {
28
+ const javaPath = path.join(p, 'bin', 'java')
29
+ if (existsSync(javaPath)) return true
30
+ const alt = path.join(p, 'Contents', 'Home', 'bin', 'java')
31
+ if (existsSync(alt)) return true
32
+ return false
33
+ } catch (e: unknown) { return false }
34
+ }
35
+
36
+ export async function checkGradle(): Promise<{ gradleJavaHome?: string; gradleValid: boolean; filesChecked: string[]; issues: string[]; suggestedFixes?: string[] }> {
37
+ const issues: string[] = []
38
+ const filesChecked: string[] = []
39
+ const suggestedFixes: string[] = []
40
+ let gradleJavaHome: string | undefined
41
+
42
+ // 1) explicit env
43
+ if (process.env.GRADLE_JAVA_HOME) {
44
+ gradleJavaHome = process.env.GRADLE_JAVA_HOME
45
+ if (!javaBinExists(gradleJavaHome)) {
46
+ issues.push(`GRADLE_JAVA_HOME is set to '${gradleJavaHome}' but no java binary was found there`)
47
+ suggestedFixes.push('Unset GRADLE_JAVA_HOME or point it to a valid JDK (e.g., /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home)')
48
+ }
49
+ }
50
+
51
+ // 2) user gradle.properties
52
+ const gradleUserHome = process.env.GRADLE_USER_HOME || path.join(os.homedir(), '.gradle')
53
+ const userProps = path.join(gradleUserHome, 'gradle.properties')
54
+ filesChecked.push(userProps)
55
+ try {
56
+ const props = readPropertiesFile(userProps)
57
+ if (props['org.gradle.java.home']) {
58
+ const p = props['org.gradle.java.home']
59
+ gradleJavaHome = gradleJavaHome || p
60
+ if (!javaBinExists(p)) {
61
+ issues.push(`org.gradle.java.home in ${userProps} points to '${p}' which does not look like a valid JDK`)
62
+ suggestedFixes.push(`Edit ${userProps} to remove or correct org.gradle.java.home`)
63
+ }
64
+ }
65
+ } catch (e: unknown) { /* ignore */ }
66
+
67
+ // 3) system gradle.properties
68
+ const systemProps = '/etc/gradle/gradle.properties'
69
+ filesChecked.push(systemProps)
70
+ try {
71
+ const props = readPropertiesFile(systemProps)
72
+ if (props['org.gradle.java.home']) {
73
+ const p = props['org.gradle.java.home']
74
+ gradleJavaHome = gradleJavaHome || p
75
+ if (!javaBinExists(p)) {
76
+ issues.push(`org.gradle.java.home in ${systemProps} points to '${p}' which does not look like a valid JDK`)
77
+ suggestedFixes.push(`Edit ${systemProps} to remove or correct org.gradle.java.home`)
78
+ }
79
+ }
80
+ } catch (e: unknown) { /* ignore */ }
81
+
82
+ // 4) GRADLE_HOME fallback
83
+ if (!gradleJavaHome && process.env.GRADLE_HOME) {
84
+ filesChecked.push(process.env.GRADLE_HOME)
85
+ if (javaBinExists(process.env.GRADLE_HOME)) {
86
+ gradleJavaHome = process.env.GRADLE_HOME
87
+ }
88
+ }
89
+
90
+ const gradleValid = !!gradleJavaHome && javaBinExists(gradleJavaHome)
91
+ if (!gradleJavaHome) {
92
+ // no explicit gradle java home detected — not an issue
93
+ } else if (!gradleValid) {
94
+ issues.push(`Detected org.gradle.java.home = '${gradleJavaHome}' but it is invalid`)
95
+ }
96
+
97
+ return { gradleJavaHome, gradleValid, filesChecked, issues, suggestedFixes }
98
+ }
@@ -1,11 +1,13 @@
1
1
  import { checkAndroid } from './android.js'
2
2
  import { checkIOS } from './ios.js'
3
+ import { checkGradle } from './gradle.js'
3
4
 
4
5
  export async function getSystemStatus() {
5
6
  try {
6
7
  const android = await checkAndroid()
7
8
  const ios = await checkIOS()
8
- const issues = [...android.issues, ...ios.issues]
9
+ const gradle = await checkGradle()
10
+ const issues = [...android.issues, ...ios.issues, ...(gradle.issues || [])]
9
11
 
10
12
  const success = issues.length === 0
11
13
  return {
@@ -19,7 +21,11 @@ export async function getSystemStatus() {
19
21
  issues,
20
22
  appInstalled: android.appInstalled,
21
23
  iosAvailable: ios.iosAvailable,
22
- iosDevices: ios.iosDevices
24
+ iosDevices: ios.iosDevices,
25
+ gradleJavaHome: gradle.gradleJavaHome,
26
+ gradleValid: gradle.gradleValid,
27
+ gradleFilesChecked: gradle.filesChecked,
28
+ gradleSuggestedFixes: gradle.suggestedFixes
23
29
  }
24
30
  } catch (e: unknown) {
25
31
  return { success: false, issues: ['Internal error: ' + (e instanceof Error ? e.message : String(e))] }
@@ -4,6 +4,7 @@ import path from 'path'
4
4
  import { detectJavaHome } from '../java.js'
5
5
  import { execCmd } from '../exec.js'
6
6
  import { spawnSync } from 'child_process'
7
+ import { checkGradle } from '../../system/gradle.js'
7
8
 
8
9
  function findInPath(cmd: string): string | null {
9
10
  try {
@@ -73,6 +74,14 @@ export async function prepareGradle(projectPath: string): Promise<{ execCmd: str
73
74
  }
74
75
 
75
76
  const detectedJavaHome = await detectJavaHome().catch(() => undefined)
77
+ // Check for problematic org.gradle.java.home entries (env or properties) and avoid passing invalid values to Gradle
78
+ let gradleCheck
79
+ try {
80
+ gradleCheck = await checkGradle()
81
+ } catch (e: unknown) {
82
+ gradleCheck = { gradleJavaHome: undefined, gradleValid: false, filesChecked: [], issues: [] }
83
+ }
84
+
76
85
  const env = Object.assign({}, process.env)
77
86
 
78
87
  // Ensure child processes can find Android platform-tools (adb, etc.) by
@@ -86,6 +95,7 @@ export async function prepareGradle(projectPath: string): Promise<{ execCmd: str
86
95
  } catch (e: unknown) { console.debug(`[prepareGradle] error resolving adbPath: ${String(e)}`) }
87
96
 
88
97
  const pathParts: string[] = []
98
+ // Prefer a detected (validated) Java home from the system/IDE
89
99
  if (detectedJavaHome) {
90
100
  if (env.JAVA_HOME !== detectedJavaHome) {
91
101
  env.JAVA_HOME = detectedJavaHome
@@ -95,6 +105,20 @@ export async function prepareGradle(projectPath: string): Promise<{ execCmd: str
95
105
  gradleArgs.push(`-Dorg.gradle.java.home=${detectedJavaHome}`)
96
106
  gradleArgs.push('--no-daemon')
97
107
  env.GRADLE_JAVA_HOME = detectedJavaHome
108
+ } else if (gradleCheck && gradleCheck.gradleJavaHome) {
109
+ // There's an org.gradle.java.home configured somewhere (env or gradle.properties)
110
+ if (gradleCheck.gradleValid) {
111
+ const p = gradleCheck.gradleJavaHome as string
112
+ const javaBin = path.join(p, 'bin')
113
+ if (!env.PATH || !env.PATH.includes(javaBin)) pathParts.push(javaBin)
114
+ gradleArgs.push(`-Dorg.gradle.java.home=${p}`)
115
+ gradleArgs.push('--no-daemon')
116
+ env.GRADLE_JAVA_HOME = p
117
+ } else {
118
+ // Invalid gradle java home detected: avoid passing it to Gradle and remove from spawn env
119
+ console.debug(`[prepareGradle] Invalid org.gradle.java.home detected (${gradleCheck.gradleJavaHome}); removing from spawn env to avoid Gradle error.`)
120
+ try { delete env.GRADLE_JAVA_HOME } catch (e: unknown) { }
121
+ }
98
122
  }
99
123
 
100
124
  if (platformToolsDir) {
@@ -126,9 +150,11 @@ export async function prepareGradle(projectPath: string): Promise<{ execCmd: str
126
150
  const spawnOpts: any = { cwd: projectPath, env }
127
151
  if (useWrapper) {
128
152
  try { await fsPromises.chmod(gradlewPath, 0o755) } catch (e: unknown) { console.debug('[prepareGradle] chmod failed for gradlew:', String(e)) }
153
+ // Execute the wrapper directly without a shell to avoid shell tokenization of args (spaces in paths)
129
154
  spawnOpts.shell = false
130
155
  } else {
131
- spawnOpts.shell = true
156
+ // Prefer executing gradle directly without invoking a shell to preserve argument boundaries
157
+ spawnOpts.shell = false
132
158
  }
133
159
 
134
160
  return { execCmd, gradleArgs, spawnOpts }