mobile-debug-mcp 0.4.0 → 0.5.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.
@@ -0,0 +1,33 @@
1
+ # Copilot Instructions for mobile-debug-mcp
2
+
3
+ ## Build and Run
4
+ - **Build**: `npm run build` (runs `tsc` to compile TypeScript to `dist/`)
5
+ - **Start**: `npm start` (runs the compiled server at `dist/server.js`)
6
+ - **Dev Workflow**: modify `src/*.ts` -> `npm run build` -> `npm start` to test changes.
7
+
8
+ ## Architecture
9
+ - **Core**: `src/server.ts` implements the MCP server using `@modelcontextprotocol/sdk`. It handles tool registration and execution.
10
+ - **Platform Modules**:
11
+ - `src/android.ts`: Encapsulates `adb` commands for Android device interaction.
12
+ - `src/ios.ts`: Encapsulates `xcrun simctl` commands for iOS simulator interaction.
13
+ - **Types**: `src/types.ts` defines shared interfaces for device info and tool responses.
14
+
15
+ ## Key Conventions
16
+
17
+ ### Tool Implementation
18
+ - **Response Format**: Tools typically return a list of content blocks.
19
+ - `start_app`: Returns a single text block containing JSON (via `wrapResponse`).
20
+ - `terminate_app`: Returns a single text block containing JSON (via `wrapResponse`).
21
+ - `restart_app`: Returns a single text block containing JSON (via `wrapResponse`).
22
+ - `reset_app_data`: Returns a single text block containing JSON (via `wrapResponse`).
23
+ - `get_logs`: Returns a text block (JSON metadata) AND a text block (raw logs).
24
+ - `capture_screenshot`: Returns a text block (JSON metadata) AND an image block (base64 PNG).
25
+ - **Metadata**: Always include a `device` object (platform, id, model, etc.) in the JSON response part.
26
+
27
+ ### External Tools
28
+ - **Android**: Uses `process.env.ADB_PATH` or defaults to `adb`.
29
+ - **iOS**: Uses `process.env.XCRUN_PATH` or defaults to `xcrun`. Assumes a booted simulator.
30
+ - **Execution**: Uses `child_process.exec` for running shell commands.
31
+
32
+ ### Error Handling
33
+ - Tools should catch execution errors and return a user-friendly error message in a `text` content block, rather than crashing the server.
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Mobile Debug MCP
2
2
 
3
- **Mobile Debug MCP** is a minimal MCP server for AI-assisted mobile development. It allows you to **launch Android or iOS apps** and **read their logs** from an MCP-compatible AI client.
3
+ **Mobile Debug MCP** is a minimal, secure MCP server for AI-assisted mobile development. It allows you to **launch Android or iOS apps**, **read their logs**, and **inspect UI** from an MCP-compatible AI client.
4
+
5
+ This server is designed with security in mind, using strict argument handling to prevent shell injection, and reliability, with robust process management to avoid hanging operations.
6
+
7
+ > **Note:** iOS support is currently an untested Work In Progress (WIP). Please use with caution and report any issues.
4
8
 
5
9
  ---
6
10
 
@@ -9,6 +13,9 @@
9
13
  - Launch Android apps via package name.
10
14
  - Launch iOS apps via bundle ID on a booted simulator.
11
15
  - Fetch recent logs from Android or iOS apps.
16
+ - Terminate and restart apps.
17
+ - Clear app data for fresh installs.
18
+ - Capture screenshots.
12
19
  - Cross-platform support (Android + iOS).
13
20
  - Minimal, focused design for fast debugging loops.
14
21
 
@@ -25,7 +32,9 @@
25
32
 
26
33
  ## Installation
27
34
 
28
- Clone the repo and build:
35
+ You can install and use **Mobile Debug MCP** in one of two ways:
36
+
37
+ ### 1. Clone the repository for local development
29
38
 
30
39
  ```bash
31
40
  git clone https://github.com/YOUR_USERNAME/mobile-debug-mcp.git
@@ -34,74 +43,194 @@ npm install
34
43
  npm run build
35
44
  ```
36
45
 
37
- Alternatively, you can publish to npm and install globally:
46
+ This option is suitable if you want to modify or contribute to the code.
47
+
48
+ ### 2. Install via npm for standard use
38
49
 
39
50
  ```bash
40
51
  npm install -g mobile-debug-mcp
41
52
  ```
42
53
 
43
- ---
54
+ This option installs the package globally for easy use without cloning the repo.
44
55
 
45
- ## MCP Server Configuration
56
+ ---
46
57
 
47
- Example WebUI MCP config:
58
+ ## Testing
59
+ 33.
60
+ 34. The repository includes a smoke test script to verify end-to-end functionality on real devices or simulators.
61
+ 35.
62
+ 36. ```bash
63
+ 37. # Run smoke test for Android
64
+ 38. npx tsx smoke-test.ts android com.example.package
65
+ 39.
66
+ 40. # Run smoke test for iOS
67
+ 41. npx tsx smoke-test.ts ios com.example.bundleid
68
+ 42. ```
69
+ 43.
70
+ 44. The smoke test performs the following sequence:
71
+ 45. 1. Starts the app
72
+ 46. 2. Captures a screenshot
73
+ 47. 3. Fetches logs
74
+ 48. 4. Terminates the app
75
+ 49.
76
+ 50. ---
77
+ 51.
78
+ 52. ## MCP Server Configuration
79
+
80
+ Example WebUI MCP config using `npx --yes` and environment variables:
48
81
 
49
82
  ```json
50
83
  {
51
84
  "mcpServers": {
52
85
  "mobile-debug": {
53
- "command": "node",
54
- "args": ["/full/path/to/mobile-debug-mcp/dist/server.js"]
86
+ "command": "npx",
87
+ "args": [
88
+ "--yes",
89
+ "mobile-debug-mcp",
90
+ "server"
91
+ ],
92
+ "env": {
93
+ "ADB_PATH": "/path/to/adb",
94
+ "XCRUN_PATH": "/usr/bin/xcrun"
95
+ }
55
96
  }
56
97
  }
57
98
  }
58
99
  ```
59
100
 
60
- > Make sure to replace `/full/path/to/` with your actual project path.
101
+ > Make sure to set `ADB_PATH` (Android) and `XCRUN_PATH` (iOS) if the tools are not in your system PATH.
61
102
 
62
103
  ---
63
104
 
64
105
  ## Tools
65
106
 
107
+ All tools accept a JSON input payload and return a structured JSON response. **Every response includes a `device` object** (with information about the selected device/simulator used for the operation), plus the tool-specific output.
108
+
66
109
  ### start_app
67
110
  Launch a mobile app.
68
111
 
69
112
  **Input:**
113
+ ```json
114
+ {
115
+ "platform": "android" | "ios",
116
+ "appId": "com.example.app", // Android package or iOS bundle ID (Required)
117
+ "deviceId": "emulator-5554" // Optional: target specific device/simulator
118
+ }
119
+ ```
70
120
 
121
+ **Response:**
122
+ ```json
123
+ {
124
+ "device": { /* device info */ },
125
+ "appStarted": true,
126
+ "launchTimeMs": 123
127
+ }
128
+ ```
129
+
130
+ ### get_logs
131
+ Fetch recent logs from the app or device.
132
+
133
+ **Input:**
71
134
  ```json
72
135
  {
73
136
  "platform": "android" | "ios",
74
- "id": "com.example.app" // Android package or iOS bundle ID
137
+ "appId": "com.example.app", // Optional: filter logs by app
138
+ "deviceId": "emulator-5554", // Optional: target specific device
139
+ "lines": 200 // Optional: number of lines (Android only)
140
+ }
141
+ ```
142
+
143
+ **Response:**
144
+ Returns two content blocks:
145
+ 1. JSON metadata:
146
+ ```json
147
+ {
148
+ "device": { /* device info */ },
149
+ "result": { "lines": 50, "crashLines": [...] }
75
150
  }
76
151
  ```
152
+ 2. Plain text log output.
77
153
 
78
- **Example:**
154
+ ### capture_screenshot
155
+ Capture a screenshot of the current device screen.
79
156
 
157
+ **Input:**
80
158
  ```json
81
159
  {
82
- "platform": "android",
83
- "id": "com.modul8.app"
160
+ "platform": "android" | "ios",
161
+ "deviceId": "emulator-5554" // Optional: target specific device
84
162
  }
85
163
  ```
86
164
 
87
- ### get_logs
88
- Fetch recent logs from the app.
165
+ **Response:**
166
+ Returns two content blocks:
167
+ 1. JSON metadata:
168
+ ```json
169
+ {
170
+ "device": { /* device info */ },
171
+ "result": { "resolution": { "width": 1080, "height": 1920 } }
172
+ }
173
+ ```
174
+ 2. Image content (image/png) containing the raw PNG data.
175
+
176
+ ### terminate_app
177
+ Terminate a running app.
89
178
 
90
179
  **Input:**
180
+ ```json
181
+ {
182
+ "platform": "android" | "ios",
183
+ "appId": "com.example.app", // Android package or iOS bundle ID (Required)
184
+ "deviceId": "emulator-5554" // Optional
185
+ }
186
+ ```
187
+
188
+ **Response:**
189
+ ```json
190
+ {
191
+ "device": { /* device info */ },
192
+ "appTerminated": true
193
+ }
194
+ ```
91
195
 
196
+ ### restart_app
197
+ Restart an app (terminate then launch).
198
+
199
+ **Input:**
92
200
  ```json
93
201
  {
94
202
  "platform": "android" | "ios",
95
- "lines": 200 // optional, Android only
203
+ "appId": "com.example.app", // Android package or iOS bundle ID (Required)
204
+ "deviceId": "emulator-5554" // Optional
205
+ }
206
+ ```
207
+
208
+ **Response:**
209
+ ```json
210
+ {
211
+ "device": { /* device info */ },
212
+ "appRestarted": true,
213
+ "launchTimeMs": 123
96
214
  }
97
215
  ```
98
216
 
99
- **Example:**
217
+ ### reset_app_data
218
+ Clear app storage (reset to fresh install state).
219
+
220
+ **Input:**
221
+ ```json
222
+ {
223
+ "platform": "android" | "ios",
224
+ "appId": "com.example.app", // Android package or iOS bundle ID (Required)
225
+ "deviceId": "emulator-5554" // Optional
226
+ }
227
+ ```
100
228
 
229
+ **Response:**
101
230
  ```json
102
231
  {
103
- "platform": "android",
104
- "lines": 200
232
+ "device": { /* device info */ },
233
+ "dataCleared": true
105
234
  }
106
235
  ```
107
236
 
@@ -112,15 +241,17 @@ Fetch recent logs from the app.
112
241
  1. Ensure Android device or iOS simulator is running.
113
242
  2. Use `start_app` to launch the app.
114
243
  3. Use `get_logs` to read the latest logs.
115
- 4. Repeat for debugging loops.
244
+ 4. Use `capture_screenshot` to visually inspect the app if needed.
245
+ 5. Use `reset_app_data` to clear state if debugging fresh install scenarios.
246
+ 6. Use `restart_app` to quickly reboot the app during development cycles.
116
247
 
117
248
  ---
118
249
 
119
250
  ## Notes
120
251
 
121
- - Ensure `adb` and `xcrun` are in your PATH.
122
- - For iOS, the simulator must be booted before using `start_app` or `get_logs`.
123
- - You may want to clear Android logs before launching for cleaner output: `adb logcat -c`
252
+ - Ensure `adb` and `xcrun` are in your PATH or set `ADB_PATH` / `XCRUN_PATH` accordingly.
253
+ - For iOS, the simulator must be booted before using tools.
254
+ - The `capture_screenshot` tool returns a multi-block response: a JSON text block with metadata, followed by an image block containing the base64-encoded PNG data.
124
255
 
125
256
  ---
126
257
 
package/dist/android.js CHANGED
@@ -1,44 +1,186 @@
1
- import { exec } from "child_process";
1
+ import { spawn } from "child_process";
2
2
  const ADB = process.env.ADB_PATH || "adb";
3
- export function startAndroidApp(pkg) {
4
- return new Promise((resolve, reject) => {
5
- exec(`${ADB} shell monkey -p ${pkg} -c android.intent.category.LAUNCHER 1`, (err, stdout, stderr) => {
6
- if (err)
7
- reject(stderr);
8
- else
9
- resolve(stdout);
10
- });
11
- });
3
+ // Helper to construct ADB args with optional device ID
4
+ function getAdbArgs(args, deviceId) {
5
+ if (deviceId) {
6
+ return ['-s', deviceId, ...args];
7
+ }
8
+ return args;
12
9
  }
13
- export function getAndroidLogs(pkg, lines = 200) {
10
+ function execAdb(args, deviceId, options = {}) {
11
+ const adbArgs = getAdbArgs(args, deviceId);
14
12
  return new Promise((resolve, reject) => {
15
- exec(`${ADB} shell pidof -s ${pkg}`, (pidErr, pidStdout, pidStderr) => {
16
- if (pidErr || !pidStdout.trim()) {
17
- reject(pidStderr || "App process not running");
18
- return;
19
- }
20
- const pid = pidStdout.trim();
21
- exec(`${ADB} logcat -d --pid=${pid} -t ${lines} -v threadtime`, (err, stdout, stderr) => {
22
- if (err)
23
- reject(stderr || err.message);
24
- else
25
- resolve(stdout);
13
+ // Use spawn instead of execFile for better stream control and to avoid potential buffering hangs
14
+ const child = spawn(ADB, adbArgs, options);
15
+ let stdout = '';
16
+ let stderr = '';
17
+ if (child.stdout) {
18
+ child.stdout.on('data', (data) => {
19
+ stdout += data.toString();
20
+ });
21
+ }
22
+ if (child.stderr) {
23
+ child.stderr.on('data', (data) => {
24
+ stderr += data.toString();
26
25
  });
26
+ }
27
+ const timeoutMs = args.includes('logcat') ? 10000 : 2000; // Shorter timeout for metadata queries
28
+ const timeout = setTimeout(() => {
29
+ child.kill();
30
+ reject(new Error(`ADB command timed out after ${timeoutMs}ms: ${args.join(' ')}`));
31
+ }, timeoutMs);
32
+ child.on('close', (code) => {
33
+ clearTimeout(timeout);
34
+ if (code !== 0) {
35
+ // If there's an actual error (non-zero exit code), reject
36
+ reject(new Error(stderr.trim() || `Command failed with code ${code}`));
37
+ }
38
+ else {
39
+ // If exit code is 0, resolve with stdout
40
+ resolve(stdout.trim());
41
+ }
42
+ });
43
+ child.on('error', (err) => {
44
+ clearTimeout(timeout);
45
+ reject(err);
27
46
  });
28
47
  });
29
48
  }
30
- export async function getAndroidCrash(pkg, lines = 200) {
49
+ function getDeviceInfo(deviceId, metadata = {}) {
50
+ return {
51
+ platform: 'android',
52
+ id: deviceId || 'default',
53
+ osVersion: metadata.osVersion || '',
54
+ model: metadata.model || '',
55
+ simulator: metadata.simulator || false
56
+ };
57
+ }
58
+ export async function getAndroidDeviceMetadata(appId, deviceId) {
31
59
  try {
32
- const logs = await getAndroidLogs(pkg, lines);
33
- const crashLines = logs
34
- .split('\n')
35
- .filter(line => line.includes('FATAL EXCEPTION'));
36
- if (crashLines.length === 0) {
37
- return "No crashes found.";
60
+ // Run these in parallel to avoid sequential timeouts
61
+ const [osVersion, model, simOutput] = await Promise.all([
62
+ execAdb(['shell', 'getprop', 'ro.build.version.release'], deviceId).catch(() => ''),
63
+ execAdb(['shell', 'getprop', 'ro.product.model'], deviceId).catch(() => ''),
64
+ execAdb(['shell', 'getprop', 'ro.kernel.qemu'], deviceId).catch(() => '0')
65
+ ]);
66
+ const simulator = simOutput === '1';
67
+ return { platform: 'android', id: deviceId || 'default', osVersion, model, simulator };
68
+ }
69
+ catch (e) {
70
+ return { platform: 'android', id: deviceId || 'default', osVersion: '', model: '', simulator: false };
71
+ }
72
+ }
73
+ export async function startAndroidApp(appId, deviceId) {
74
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId);
75
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
76
+ await execAdb(['shell', 'monkey', '-p', appId, '-c', 'android.intent.category.LAUNCHER', '1'], deviceId);
77
+ return { device: deviceInfo, appStarted: true, launchTimeMs: 1000 };
78
+ }
79
+ export async function terminateAndroidApp(appId, deviceId) {
80
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId);
81
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
82
+ await execAdb(['shell', 'am', 'force-stop', appId], deviceId);
83
+ return { device: deviceInfo, appTerminated: true };
84
+ }
85
+ export async function restartAndroidApp(appId, deviceId) {
86
+ await terminateAndroidApp(appId, deviceId);
87
+ const startResult = await startAndroidApp(appId, deviceId);
88
+ return {
89
+ device: startResult.device,
90
+ appRestarted: startResult.appStarted,
91
+ launchTimeMs: startResult.launchTimeMs
92
+ };
93
+ }
94
+ export async function resetAndroidAppData(appId, deviceId) {
95
+ const metadata = await getAndroidDeviceMetadata(appId, deviceId);
96
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
97
+ const output = await execAdb(['shell', 'pm', 'clear', appId], deviceId);
98
+ return { device: deviceInfo, dataCleared: output === 'Success' };
99
+ }
100
+ export async function getAndroidLogs(appId, lines = 200, deviceId) {
101
+ const metadata = await getAndroidDeviceMetadata(appId || "", deviceId);
102
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
103
+ try {
104
+ // We'll skip PID lookup for now to avoid potential hangs with 'pidof' on some emulators
105
+ // and rely on robust string matching against the log line.
106
+ // Get logs
107
+ const stdout = await execAdb(['logcat', '-d', '-t', lines.toString(), '-v', 'threadtime'], deviceId);
108
+ const allLogs = stdout.split('\n');
109
+ let filteredLogs = allLogs;
110
+ if (appId) {
111
+ // Filter by checking if the line contains the appId string.
112
+ const matchingLogs = allLogs.filter(line => line.includes(appId));
113
+ if (matchingLogs.length > 0) {
114
+ filteredLogs = matchingLogs;
115
+ }
116
+ else {
117
+ // Fallback: if no logs match the appId, return the raw logs (last N lines)
118
+ // This matches the behavior of the "working" version provided by the user,
119
+ // ensuring they at least see system activity if the app is silent or crashing early.
120
+ filteredLogs = allLogs;
121
+ }
38
122
  }
39
- return crashLines.join('\n');
123
+ return { device: deviceInfo, logs: filteredLogs, logCount: filteredLogs.length };
40
124
  }
41
- catch (error) {
42
- return `Error retrieving crash logs: ${error}`;
125
+ catch (e) {
126
+ console.error("Error fetching logs:", e);
127
+ return { device: deviceInfo, logs: [], logCount: 0 };
43
128
  }
44
129
  }
130
+ export async function captureAndroidScreen(deviceId) {
131
+ const metadata = await getAndroidDeviceMetadata("", deviceId);
132
+ const deviceInfo = getDeviceInfo(deviceId || 'default', metadata);
133
+ return new Promise((resolve, reject) => {
134
+ const adbArgs = getAdbArgs(['exec-out', 'screencap', '-p'], deviceId);
135
+ // Using spawn for screencap as well to ensure consistent process handling
136
+ const child = spawn(ADB, adbArgs);
137
+ const chunks = [];
138
+ let stderr = '';
139
+ child.stdout.on('data', (chunk) => {
140
+ chunks.push(Buffer.from(chunk));
141
+ });
142
+ child.stderr.on('data', (data) => {
143
+ stderr += data.toString();
144
+ });
145
+ const timeout = setTimeout(() => {
146
+ child.kill();
147
+ reject(new Error(`ADB screencap timed out after 10s`));
148
+ }, 10000);
149
+ child.on('close', (code) => {
150
+ clearTimeout(timeout);
151
+ if (code !== 0) {
152
+ reject(new Error(stderr.trim() || `Screencap failed with code ${code}`));
153
+ return;
154
+ }
155
+ const screenshotBuffer = Buffer.concat(chunks);
156
+ const screenshotBase64 = screenshotBuffer.toString('base64');
157
+ // Get resolution
158
+ execAdb(['shell', 'wm', 'size'], deviceId)
159
+ .then(sizeStdout => {
160
+ let width = 0;
161
+ let height = 0;
162
+ const match = sizeStdout.match(/Physical size: (\d+)x(\d+)/);
163
+ if (match) {
164
+ width = parseInt(match[1], 10);
165
+ height = parseInt(match[2], 10);
166
+ }
167
+ resolve({
168
+ device: deviceInfo,
169
+ screenshot: screenshotBase64,
170
+ resolution: { width, height }
171
+ });
172
+ })
173
+ .catch(() => {
174
+ resolve({
175
+ device: deviceInfo,
176
+ screenshot: screenshotBase64,
177
+ resolution: { width: 0, height: 0 }
178
+ });
179
+ });
180
+ });
181
+ child.on('error', (err) => {
182
+ clearTimeout(timeout);
183
+ reject(err);
184
+ });
185
+ });
186
+ }