mobile-debug-mcp 0.12.8 → 0.13.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.
@@ -291,117 +291,16 @@ export class iOSManage {
291
291
  }
292
292
  async startApp(bundleId, deviceId = "booted") {
293
293
  validateBundleId(bundleId);
294
- // Prepare instrumentation object upfront so it can be returned to callers
295
- const instrumentation = { ts: new Date().toISOString(), action: 'startApp', cmd: 'xcrun', args: ['simctl', 'launch', deviceId, bundleId], cwd: process.cwd(), env: { PATH: process.env.PATH, XCRUN_PATH: process.env.XCRUN_PATH } };
296
294
  try {
297
- // Instrumentation: persist and emit to stderr for server logs
298
- try {
299
- await fs.appendFile('/tmp/mcp_startapp_instrument.log', JSON.stringify(instrumentation) + '\n');
300
- }
301
- catch (e) { }
302
- try {
303
- console.error('MCP-STARTAPP-EXEC', JSON.stringify(instrumentation));
304
- }
305
- catch (e) { }
306
- }
307
- catch { }
308
- // Attempt to launch
309
- let launchResult = null;
310
- try {
311
- launchResult = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId);
312
- }
313
- catch (launchErr) {
314
- // Collect diagnostics when simctl launch fails
315
- const launchDiag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId);
295
+ const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId);
316
296
  const device = await getIOSDeviceMetadata(deviceId);
317
- const post = await this.collectPostLaunchDiagnostics(bundleId, deviceId);
318
- return { device, appStarted: false, launchTimeMs: 0, error: launchErr instanceof Error ? launchErr.message : String(launchErr), diagnostics: { launchDiag, post }, instrumentation };
319
- }
320
- // Basic success — but verify RunningBoard/installcoordination didn't mark it as placeholder
321
- const device = await getIOSDeviceMetadata(deviceId);
322
- // short wait to let system settle
323
- await new Promise(r => setTimeout(r, 1000));
324
- let appinfo = '';
325
- try {
326
- const ai = await execCommand(['simctl', 'appinfo', deviceId, bundleId], deviceId);
327
- appinfo = ai.output || '';
328
- }
329
- catch { }
330
- // capture recent runningboard/installcoordination logs
331
- const logDiag = execCommandWithDiagnostics(['simctl', 'spawn', deviceId, 'log', 'show', '--style', 'syslog', '--predicate', `(process == "${bundleId}" ) OR eventMessage CONTAINS "installcoordinationd" OR eventMessage CONTAINS "runningboard"`, '--last', '1m'], deviceId);
332
- const placeholderDetected = (appinfo && /isPlaceholder[:=]?\s*Y/i.test(appinfo)) || (logDiag && ((logDiag.runResult && ((logDiag.runResult.stdout || '').includes('isPlaceholder')) || (logDiag.runResult.stderr || '').includes('isPlaceholder'))));
333
- if (placeholderDetected) {
334
- const post = await this.collectPostLaunchDiagnostics(bundleId, deviceId, appinfo);
335
- return { device, appStarted: false, launchTimeMs: 0, diagnostics: { appinfo, logDiag, post }, instrumentation };
336
- }
337
- return { device, appStarted: !!(launchResult && launchResult.output), launchTimeMs: 1000, instrumentation };
338
- }
339
- appExecutableName(bundleId) {
340
- // Best-effort executable name: prefer last component of bundleId
341
- try {
342
- const candidate = bundleId.split('.').pop();
343
- return candidate || bundleId;
344
- }
345
- catch {
346
- return bundleId;
347
- }
348
- }
349
- // Collect bundle- and system-level diagnostics after a failed or placeholder launch
350
- async collectPostLaunchDiagnostics(bundleId, deviceId = "booted", appinfo) {
351
- const diagnostics = { ts: new Date().toISOString(), bundleId, deviceId };
352
- // gather simctl appinfo (if not provided)
353
- try {
354
- diagnostics.appinfo = appinfo || ((await execCommand(['simctl', 'appinfo', deviceId, bundleId], deviceId)).output || '');
355
- }
356
- catch (e) {
357
- diagnostics.appinfoError = String(e);
358
- }
359
- // attempt to discover bundle path from appinfo
360
- let bundlePath = null;
361
- if (diagnostics.appinfo) {
362
- const m = diagnostics.appinfo.match(/Path\s*=\s*"?([\S]+)"?/) || diagnostics.appinfo.match(/Container: (\/\S+)/);
363
- if (m)
364
- bundlePath = m[1];
365
- }
366
- // lipo / file / otool / codesign / xattr
367
- if (bundlePath) {
368
- diagnostics.bundlePath = bundlePath;
369
- const execs = [
370
- { name: 'file', cmd: ['file', bundlePath + '/' + this.appExecutableName(bundleId)] },
371
- { name: 'lipo', cmd: ['lipo', '-info', bundlePath + '/' + this.appExecutableName(bundleId)] },
372
- { name: 'otool-L', cmd: ['otool', '-L', bundlePath + '/' + this.appExecutableName(bundleId)] },
373
- { name: 'otool-load', cmd: ['otool', '-l', bundlePath + '/' + this.appExecutableName(bundleId)] },
374
- { name: 'plutil', cmd: ['plutil', '-p', bundlePath + '/Info.plist'] },
375
- { name: 'codesign', cmd: ['codesign', '-dvvv', bundlePath] },
376
- { name: 'xattr', cmd: ['xattr', '-l', bundlePath] },
377
- { name: 'ls', cmd: ['ls', '-la', bundlePath] },
378
- ];
379
- diagnostics.bundle = {};
380
- for (const e of execs) {
381
- try {
382
- const r = execCommandWithDiagnostics(e.cmd, deviceId);
383
- diagnostics.bundle[e.name] = r && r.runResult ? { stdout: r.runResult.stdout, stderr: r.runResult.stderr, code: r.runResult.exitCode } : { error: 'no-result' };
384
- }
385
- catch (err) {
386
- diagnostics.bundle[e.name] = { error: String(err) };
387
- }
388
- }
389
- }
390
- // collect recent system logs and a screenshot
391
- try {
392
- diagnostics.recentLogs = execCommandWithDiagnostics(['simctl', 'spawn', deviceId, 'log', 'show', '--style', 'syslog', '--predicate', `eventMessage CONTAINS "installcoordinationd" OR eventMessage CONTAINS "runningboard"`, '--last', '5m'], deviceId);
393
- }
394
- catch (e) {
395
- diagnostics.recentLogsError = String(e);
396
- }
397
- try {
398
- const shot = await execCommandWithDiagnostics(['simctl', 'io', deviceId, 'screenshot', '--type', 'png', '/tmp/mcp_post_launch_screenshot.png'], deviceId);
399
- diagnostics.screenshot = { created: true, path: '/tmp/mcp_post_launch_screenshot.png', result: shot && shot.runResult };
297
+ return { device, appStarted: !!result.output, launchTimeMs: 1000 };
400
298
  }
401
299
  catch (e) {
402
- diagnostics.screenshotError = String(e);
300
+ const diag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId);
301
+ const device = await getIOSDeviceMetadata(deviceId);
302
+ return { device, appStarted: false, launchTimeMs: 0, error: e instanceof Error ? e.message : String(e), diagnostics: diag };
403
303
  }
404
- return diagnostics;
405
304
  }
406
305
  async terminateApp(bundleId, deviceId = "booted") {
407
306
  validateBundleId(bundleId);
package/dist/ios/utils.js CHANGED
@@ -96,40 +96,7 @@ export function validateBundleId(bundleId) {
96
96
  }
97
97
  export function execCommand(args, deviceId = "booted") {
98
98
  return new Promise((resolve, reject) => {
99
- // Instrumentation: append a JSON line with timestamp, command, args, cwd and selected env vars
100
- try {
101
- const mcpEnv = {};
102
- for (const k of Object.keys(process.env || {})) {
103
- if (k.startsWith('MCP_'))
104
- mcpEnv[k] = process.env[k];
105
- }
106
- const instrument = {
107
- timestamp: new Date().toISOString(),
108
- command: getXcrunCmd(),
109
- args,
110
- cwd: process.cwd(),
111
- env: {
112
- PATH: process.env.PATH,
113
- XCRUN_PATH: process.env.XCRUN_PATH,
114
- ...mcpEnv
115
- }
116
- };
117
- try {
118
- require('fs').appendFileSync('/tmp/mcp_exec_instrument.log', JSON.stringify(instrument) + '\n');
119
- }
120
- catch (e) { }
121
- }
122
- catch (e) {
123
- // swallow instrumentation errors to avoid changing behavior
124
- }
125
99
  // Use spawn for better stream control and consistency with Android implementation
126
- // Instrument: emit a JSON line to stderr so the MCP server stderr/stdout capture can record the exact command and env
127
- try {
128
- const instLine = JSON.stringify({ ts: new Date().toISOString(), cmd: getXcrunCmd(), args, cwd: process.cwd(), PATH: process.env.PATH });
129
- // Use stderr so it appears in server logs reliably
130
- console.error('MCP-INSTRUMENT-EXEC', instLine);
131
- }
132
- catch (e) { }
133
100
  const child = spawn(getXcrunCmd(), args);
134
101
  let stdout = '';
135
102
  let stderr = '';
@@ -143,17 +110,6 @@ export function execCommand(args, deviceId = "booted") {
143
110
  stderr += data.toString();
144
111
  });
145
112
  }
146
- // Additional instrumentation: write pid and env snapshot when child starts
147
- try {
148
- const pidInfo = { ts: new Date().toISOString(), childPid: (child.pid || null), invoked: getXcrunCmd(), args };
149
- try {
150
- require('fs').appendFileSync('/tmp/mcp_exec_instrument.log', JSON.stringify(pidInfo) + '\n');
151
- }
152
- catch (e) { }
153
- }
154
- catch (e) {
155
- // ignore
156
- }
157
113
  const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000; // env (ms) or default 30s
158
114
  const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000; // env (ms) or default 60s
159
115
  const timeoutMs = args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT; // choose appropriate timeout
@@ -177,11 +133,6 @@ export function execCommand(args, deviceId = "booted") {
177
133
  });
178
134
  }
179
135
  export function execCommandWithDiagnostics(args, deviceId = "booted") {
180
- try {
181
- const syncInst = { ts: new Date().toISOString(), cmd: getXcrunCmd(), args, cwd: process.cwd() };
182
- require('fs').appendFileSync('/tmp/mcp_exec_instrument_sync.log', JSON.stringify(syncInst) + '\n');
183
- }
184
- catch (e) { }
185
136
  // Run synchronously to capture stdout/stderr and exitCode reliably for diagnostics
186
137
  const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000;
187
138
  const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000;
package/dist/server.js CHANGED
@@ -389,37 +389,52 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
389
389
  try {
390
390
  if (name === "start_app") {
391
391
  const { platform, appId, deviceId } = args;
392
- // Defensive validation: ensure required args are present and log malformed requests
392
+ // Defensive validation: ensure caller provided platform and appId.
393
393
  if (!platform || !appId) {
394
+ const msg = 'Both platform and appId parameters are required (platform: ios|android, appId: bundle id or package name).';
395
+ const payload = { ts: new Date().toISOString(), tool: 'start_app', args };
396
+ let logged = false;
397
+ // Prefer the diagnostics module when available
394
398
  try {
395
- require('fs').appendFileSync('/tmp/mcp_bad_requests.log', JSON.stringify({ ts: new Date().toISOString(), tool: 'start_app', args }) + '\n');
399
+ const diag = require('./utils/diagnostics.js');
400
+ if (diag && diag.appendDiagnosticFile) {
401
+ diag.appendDiagnosticFile('bad_requests.log', payload);
402
+ logged = true;
403
+ }
396
404
  }
397
- catch (e) { }
398
- const deviceFallback = { platform: platform || 'ios', id: deviceId || 'unknown', osVersion: '', model: '', simulator: true };
399
- const response = { device: deviceFallback, appStarted: false, launchTimeMs: 0, error: 'Missing required argument: platform and/or appId', diagnostics: { receivedArgs: args } };
400
- return wrapResponse(response);
401
- }
402
- try {
403
- const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId));
404
- // Preserve diagnostics and instrumentation from platform managers so agents receive full context
405
- const response = {
406
- device: res.device,
407
- appStarted: res.appStarted,
408
- launchTimeMs: res.launchTimeMs,
409
- error: res.error,
410
- diagnostics: res.diagnostics,
411
- instrumentation: res.instrumentation
412
- };
413
- return wrapResponse(response);
414
- }
415
- catch (err) {
416
- try {
417
- require('fs').appendFileSync('/tmp/mcp_bad_requests.log', JSON.stringify({ ts: new Date().toISOString(), tool: 'start_app', args, error: err && err.message ? err.message : String(err) }) + '\n');
405
+ catch (err) {
406
+ console.error('Diagnostics append failed:', String(err));
407
+ }
408
+ // Fallback to /tmp file (synchronous) and report failures rather than swallowing
409
+ if (!logged) {
410
+ try {
411
+ const fs = require('fs');
412
+ fs.appendFileSync('/tmp/mcp_bad_requests.log', JSON.stringify(payload) + '\n');
413
+ logged = true;
414
+ }
415
+ catch (err) {
416
+ console.error('Failed to write bad request to /tmp/mcp_bad_requests.log:', String(err));
417
+ }
418
418
  }
419
- catch (e) { }
420
- const deviceFallback = { platform: platform || 'ios', id: deviceId || 'unknown', osVersion: '', model: '', simulator: true };
421
- return wrapResponse({ device: deviceFallback, appStarted: false, launchTimeMs: 0, error: err instanceof Error ? err.message : String(err), diagnostics: { receivedArgs: args } });
419
+ // Final fallback: emit payload to stderr so it's visible in server logs
420
+ if (!logged) {
421
+ try {
422
+ console.error('Bad request (start_app) payload:', JSON.stringify(payload));
423
+ }
424
+ catch (err) {
425
+ // Last resort: still log the failure
426
+ console.error('Failed to emit bad request payload to stderr:', String(err));
427
+ }
428
+ }
429
+ return wrapResponse({ error: msg });
422
430
  }
431
+ const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId));
432
+ const response = {
433
+ device: res.device,
434
+ appStarted: res.appStarted,
435
+ launchTimeMs: res.launchTimeMs
436
+ };
437
+ return wrapResponse(response);
423
438
  }
424
439
  if (name === "terminate_app") {
425
440
  const { platform, appId, deviceId } = args;
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.13.0]
6
+ - Fixed a crash in the `start_app` tool by adding validation to ensure `appId` and `platform` are provided.
7
+
5
8
  ## [0.12.4]
6
9
  - Made projectType and platform mandatory
7
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.12.8",
3
+ "version": "0.13.0",
4
4
  "description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
5
5
  "type": "module",
6
6
  "bin": {
package/src/ios/manage.ts CHANGED
@@ -298,102 +298,15 @@ export class iOSManage {
298
298
 
299
299
  async startApp(bundleId: string, deviceId: string = "booted"): Promise<StartAppResponse> {
300
300
  validateBundleId(bundleId)
301
- // Prepare instrumentation object upfront so it can be returned to callers
302
- const instrumentation = { ts: new Date().toISOString(), action: 'startApp', cmd: 'xcrun', args: ['simctl','launch', deviceId, bundleId], cwd: process.cwd(), env: { PATH: process.env.PATH, XCRUN_PATH: process.env.XCRUN_PATH } }
303
-
304
- try {
305
- // Instrumentation: persist and emit to stderr for server logs
306
- try { await fs.appendFile('/tmp/mcp_startapp_instrument.log', JSON.stringify(instrumentation) + '\n') } catch (e) {}
307
- try { console.error('MCP-STARTAPP-EXEC', JSON.stringify(instrumentation)) } catch (e) {}
308
- } catch {}
309
-
310
- // Attempt to launch
311
- let launchResult: any = null
312
301
  try {
313
- launchResult = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId)
314
- } catch (launchErr:any) {
315
- // Collect diagnostics when simctl launch fails
316
- const launchDiag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId)
302
+ const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId)
317
303
  const device = await getIOSDeviceMetadata(deviceId)
318
- const post = await this.collectPostLaunchDiagnostics(bundleId, deviceId)
319
- return { device, appStarted: false, launchTimeMs: 0, error: launchErr instanceof Error ? launchErr.message : String(launchErr), diagnostics: { launchDiag, post }, instrumentation } as any
320
- }
321
-
322
- // Basic success but verify RunningBoard/installcoordination didn't mark it as placeholder
323
- const device = await getIOSDeviceMetadata(deviceId)
324
- // short wait to let system settle
325
- await new Promise(r => setTimeout(r, 1000))
326
-
327
- let appinfo = ''
328
- try {
329
- const ai = await execCommand(['simctl', 'appinfo', deviceId, bundleId], deviceId)
330
- appinfo = ai.output || ''
331
- } catch {}
332
-
333
- // capture recent runningboard/installcoordination logs
334
- const logDiag = execCommandWithDiagnostics(['simctl','spawn',deviceId,'log','show','--style','syslog','--predicate',`(process == "${bundleId}" ) OR eventMessage CONTAINS "installcoordinationd" OR eventMessage CONTAINS "runningboard"`, '--last', '1m'], deviceId)
335
-
336
- const placeholderDetected = (appinfo && /isPlaceholder[:=]?\s*Y/i.test(appinfo)) || (logDiag && ((logDiag.runResult && ((logDiag.runResult.stdout || '').includes('isPlaceholder')) || (logDiag.runResult.stderr || '').includes('isPlaceholder'))))
337
-
338
- if (placeholderDetected) {
339
- const post = await this.collectPostLaunchDiagnostics(bundleId, deviceId, appinfo)
340
- return { device, appStarted: false, launchTimeMs: 0, diagnostics: { appinfo, logDiag, post }, instrumentation } as any
341
- }
342
-
343
- return { device, appStarted: !!(launchResult && launchResult.output), launchTimeMs: 1000, instrumentation }
344
- }
345
-
346
- appExecutableName(bundleId: string) {
347
- // Best-effort executable name: prefer last component of bundleId
348
- try { const candidate = bundleId.split('.').pop(); return candidate || bundleId }
349
- catch { return bundleId }
350
- }
351
-
352
- // Collect bundle- and system-level diagnostics after a failed or placeholder launch
353
- async collectPostLaunchDiagnostics(bundleId: string, deviceId: string = "booted", appinfo?: string) {
354
- const diagnostics: any = { ts: new Date().toISOString(), bundleId, deviceId }
355
-
356
- // gather simctl appinfo (if not provided)
357
- try { diagnostics.appinfo = appinfo || ((await execCommand(['simctl','appinfo', deviceId, bundleId], deviceId)).output || '') } catch (e) { diagnostics.appinfoError = String(e) }
358
-
359
- // attempt to discover bundle path from appinfo
360
- let bundlePath: string | null = null
361
- if (diagnostics.appinfo) {
362
- const m = diagnostics.appinfo.match(/Path\s*=\s*"?([\S]+)"?/) || diagnostics.appinfo.match(/Container: (\/\S+)/)
363
- if (m) bundlePath = m[1]
364
- }
365
-
366
- // lipo / file / otool / codesign / xattr
367
- if (bundlePath) {
368
- diagnostics.bundlePath = bundlePath
369
- const execs = [
370
- { name: 'file', cmd: ['file', bundlePath + '/' + this.appExecutableName(bundleId)] },
371
- { name: 'lipo', cmd: ['lipo', '-info', bundlePath + '/' + this.appExecutableName(bundleId)] },
372
- { name: 'otool-L', cmd: ['otool', '-L', bundlePath + '/' + this.appExecutableName(bundleId)] },
373
- { name: 'otool-load', cmd: ['otool', '-l', bundlePath + '/' + this.appExecutableName(bundleId)] },
374
- { name: 'plutil', cmd: ['plutil', '-p', bundlePath + '/Info.plist'] },
375
- { name: 'codesign', cmd: ['codesign', '-dvvv', bundlePath] },
376
- { name: 'xattr', cmd: ['xattr', '-l', bundlePath] },
377
- { name: 'ls', cmd: ['ls', '-la', bundlePath] },
378
- ]
379
-
380
- diagnostics.bundle = {}
381
- for (const e of execs) {
382
- try {
383
- const r = execCommandWithDiagnostics(e.cmd, deviceId)
384
- diagnostics.bundle[e.name] = r && r.runResult ? { stdout: r.runResult.stdout, stderr: r.runResult.stderr, code: r.runResult.exitCode } : { error: 'no-result' }
385
- } catch (err) { diagnostics.bundle[e.name] = { error: String(err) } }
386
- }
304
+ return { device, appStarted: !!result.output, launchTimeMs: 1000 }
305
+ } catch (e:any) {
306
+ const diag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId)
307
+ const device = await getIOSDeviceMetadata(deviceId)
308
+ return { device, appStarted: false, launchTimeMs: 0, error: e instanceof Error ? e.message : String(e), diagnostics: diag } as any
387
309
  }
388
-
389
- // collect recent system logs and a screenshot
390
- try { diagnostics.recentLogs = execCommandWithDiagnostics(['simctl','spawn',deviceId,'log','show','--style','syslog','--predicate',`eventMessage CONTAINS "installcoordinationd" OR eventMessage CONTAINS "runningboard"`, '--last', '5m'], deviceId) } catch (e) { diagnostics.recentLogsError = String(e) }
391
- try {
392
- const shot = await execCommandWithDiagnostics(['simctl','io', deviceId, 'screenshot', '--type', 'png', '/tmp/mcp_post_launch_screenshot.png'], deviceId)
393
- diagnostics.screenshot = { created: true, path: '/tmp/mcp_post_launch_screenshot.png', result: shot && shot.runResult }
394
- } catch (e) { diagnostics.screenshotError = String(e) }
395
-
396
- return diagnostics
397
310
  }
398
311
 
399
312
  async terminateApp(bundleId: string, deviceId: string = "booted"): Promise<TerminateAppResponse> {
package/src/ios/utils.ts CHANGED
@@ -88,41 +88,7 @@ export function validateBundleId(bundleId: string) {
88
88
 
89
89
  export function execCommand(args: string[], deviceId: string = "booted"): Promise<IOSResult> {
90
90
  return new Promise((resolve, reject) => {
91
- // Instrumentation: append a JSON line with timestamp, command, args, cwd and selected env vars
92
- try {
93
- const mcpEnv: Record<string,string|undefined> = {}
94
- for (const k of Object.keys(process.env || {})) {
95
- if (k.startsWith('MCP_')) mcpEnv[k] = process.env[k]
96
- }
97
-
98
- const instrument = {
99
- timestamp: new Date().toISOString(),
100
- command: getXcrunCmd(),
101
- args,
102
- cwd: process.cwd(),
103
- env: {
104
- PATH: process.env.PATH,
105
- XCRUN_PATH: process.env.XCRUN_PATH,
106
- ...mcpEnv
107
- }
108
- }
109
-
110
- try {
111
- require('fs').appendFileSync('/tmp/mcp_exec_instrument.log', JSON.stringify(instrument) + '\n')
112
- } catch (e) {}
113
-
114
- } catch (e) {
115
- // swallow instrumentation errors to avoid changing behavior
116
- }
117
-
118
91
  // Use spawn for better stream control and consistency with Android implementation
119
- // Instrument: emit a JSON line to stderr so the MCP server stderr/stdout capture can record the exact command and env
120
- try {
121
- const instLine = JSON.stringify({ ts: new Date().toISOString(), cmd: getXcrunCmd(), args, cwd: process.cwd(), PATH: process.env.PATH })
122
- // Use stderr so it appears in server logs reliably
123
- console.error('MCP-INSTRUMENT-EXEC', instLine)
124
- } catch (e) {}
125
-
126
92
  const child = spawn(getXcrunCmd(), args)
127
93
 
128
94
  let stdout = ''
@@ -140,14 +106,6 @@ export function execCommand(args: string[], deviceId: string = "booted"): Promis
140
106
  })
141
107
  }
142
108
 
143
- // Additional instrumentation: write pid and env snapshot when child starts
144
- try {
145
- const pidInfo = { ts: new Date().toISOString(), childPid: (child.pid || null), invoked: getXcrunCmd(), args }
146
- try { require('fs').appendFileSync('/tmp/mcp_exec_instrument.log', JSON.stringify(pidInfo) + '\n') } catch (e) {}
147
- } catch (e) {
148
- // ignore
149
- }
150
-
151
109
  const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000 // env (ms) or default 30s
152
110
  const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000 // env (ms) or default 60s
153
111
  const timeoutMs = args.includes('log') ? DEFAULT_XCRUN_LOG_TIMEOUT : DEFAULT_XCRUN_CMD_TIMEOUT // choose appropriate timeout
@@ -173,11 +131,6 @@ export function execCommand(args: string[], deviceId: string = "booted"): Promis
173
131
  }
174
132
 
175
133
  export function execCommandWithDiagnostics(args: string[], deviceId: string = "booted") {
176
- try {
177
- const syncInst = { ts: new Date().toISOString(), cmd: getXcrunCmd(), args, cwd: process.cwd() }
178
- require('fs').appendFileSync('/tmp/mcp_exec_instrument_sync.log', JSON.stringify(syncInst) + '\n')
179
- } catch (e) {}
180
-
181
134
  // Run synchronously to capture stdout/stderr and exitCode reliably for diagnostics
182
135
  const DEFAULT_XCRUN_LOG_TIMEOUT = parseInt(process.env.MCP_XCRUN_LOG_TIMEOUT || '', 10) || 30000
183
136
  const DEFAULT_XCRUN_CMD_TIMEOUT = parseInt(process.env.MCP_XCRUN_TIMEOUT || '', 10) || 60000
package/src/server.ts CHANGED
@@ -412,31 +412,54 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
412
412
  try {
413
413
  if (name === "start_app") {
414
414
  const { platform, appId, deviceId } = args as any
415
- // Defensive validation: ensure required args are present and log malformed requests
415
+ // Defensive validation: ensure caller provided platform and appId.
416
416
  if (!platform || !appId) {
417
- try { require('fs').appendFileSync('/tmp/mcp_bad_requests.log', JSON.stringify({ ts: new Date().toISOString(), tool: 'start_app', args }) + '\n') } catch (e) {}
418
- const deviceFallback: any = { platform: platform || 'ios', id: deviceId || 'unknown', osVersion: '', model: '', simulator: true }
419
- const response: StartAppResponse = { device: deviceFallback, appStarted: false, launchTimeMs: 0, error: 'Missing required argument: platform and/or appId', diagnostics: { receivedArgs: args } }
420
- return wrapResponse(response)
421
- }
417
+ const msg = 'Both platform and appId parameters are required (platform: ios|android, appId: bundle id or package name).'
418
+ const payload = { ts: new Date().toISOString(), tool: 'start_app', args }
419
+ let logged = false
420
+
421
+ // Prefer the diagnostics module when available
422
+ try {
423
+ const diag = require('./utils/diagnostics.js')
424
+ if (diag && diag.appendDiagnosticFile) {
425
+ diag.appendDiagnosticFile('bad_requests.log', payload)
426
+ logged = true
427
+ }
428
+ } catch (err) {
429
+ console.error('Diagnostics append failed:', String(err))
430
+ }
422
431
 
423
- try {
424
- const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId))
425
- // Preserve diagnostics and instrumentation from platform managers so agents receive full context
426
- const response: StartAppResponse = {
427
- device: res.device,
428
- appStarted: res.appStarted,
429
- launchTimeMs: res.launchTimeMs,
430
- error: (res as any).error,
431
- diagnostics: (res as any).diagnostics,
432
- instrumentation: (res as any).instrumentation
432
+ // Fallback to /tmp file (synchronous) and report failures rather than swallowing
433
+ if (!logged) {
434
+ try {
435
+ const fs = require('fs')
436
+ fs.appendFileSync('/tmp/mcp_bad_requests.log', JSON.stringify(payload) + '\n')
437
+ logged = true
438
+ } catch (err) {
439
+ console.error('Failed to write bad request to /tmp/mcp_bad_requests.log:', String(err))
440
+ }
433
441
  }
434
- return wrapResponse(response)
435
- } catch (err:any) {
436
- try { require('fs').appendFileSync('/tmp/mcp_bad_requests.log', JSON.stringify({ ts: new Date().toISOString(), tool: 'start_app', args, error: err && err.message ? err.message : String(err) }) + '\n') } catch (e) {}
437
- const deviceFallback: any = { platform: platform || 'ios', id: deviceId || 'unknown', osVersion: '', model: '', simulator: true }
438
- return wrapResponse({ device: deviceFallback, appStarted: false, launchTimeMs: 0, error: err instanceof Error ? err.message : String(err), diagnostics: { receivedArgs: args } })
442
+
443
+ // Final fallback: emit payload to stderr so it's visible in server logs
444
+ if (!logged) {
445
+ try {
446
+ console.error('Bad request (start_app) payload:', JSON.stringify(payload))
447
+ } catch (err) {
448
+ // Last resort: still log the failure
449
+ console.error('Failed to emit bad request payload to stderr:', String(err))
450
+ }
451
+ }
452
+
453
+ return wrapResponse({ error: msg })
439
454
  }
455
+
456
+ const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId))
457
+ const response: StartAppResponse = {
458
+ device: res.device,
459
+ appStarted: res.appStarted,
460
+ launchTimeMs: res.launchTimeMs
461
+ }
462
+ return wrapResponse(response)
440
463
  }
441
464
 
442
465
  if (name === "terminate_app") {
package/src/types.ts CHANGED
@@ -12,7 +12,6 @@ export interface StartAppResponse {
12
12
  launchTimeMs: number;
13
13
  error?: string;
14
14
  diagnostics?: any;
15
- instrumentation?: any;
16
15
  }
17
16
 
18
17
  export interface TerminateAppResponse {