mobile-debug-mcp 0.12.5 → 0.12.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.
@@ -303,15 +303,105 @@ export class iOSManage {
303
303
  console.error('MCP-STARTAPP-EXEC', JSON.stringify(instrumentation));
304
304
  }
305
305
  catch (e) { }
306
- const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId);
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);
307
316
  const device = await getIOSDeviceMetadata(deviceId);
308
- return { device, appStarted: !!result.output, launchTimeMs: 1000, instrumentation };
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 || '');
309
355
  }
310
356
  catch (e) {
311
- const diag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId);
312
- const device = await getIOSDeviceMetadata(deviceId);
313
- return { device, appStarted: false, launchTimeMs: 0, error: e instanceof Error ? e.message : String(e), diagnostics: diag, instrumentation };
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 };
400
+ }
401
+ catch (e) {
402
+ diagnostics.screenshotError = String(e);
314
403
  }
404
+ return diagnostics;
315
405
  }
316
406
  async terminateApp(bundleId, deviceId = "booted") {
317
407
  validateBundleId(bundleId);
package/dist/server.js CHANGED
@@ -390,10 +390,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
390
390
  if (name === "start_app") {
391
391
  const { platform, appId, deviceId } = args;
392
392
  const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId));
393
+ // Preserve diagnostics and instrumentation from platform managers so agents receive full context
393
394
  const response = {
394
395
  device: res.device,
395
396
  appStarted: res.appStarted,
396
- launchTimeMs: res.launchTimeMs
397
+ launchTimeMs: res.launchTimeMs,
398
+ error: res.error,
399
+ diagnostics: res.diagnostics,
400
+ instrumentation: res.instrumentation
397
401
  };
398
402
  return wrapResponse(response);
399
403
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.12.5",
3
+ "version": "0.12.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": {
package/src/ios/manage.ts CHANGED
@@ -300,19 +300,100 @@ export class iOSManage {
300
300
  validateBundleId(bundleId)
301
301
  // Prepare instrumentation object upfront so it can be returned to callers
302
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
+
303
304
  try {
304
305
  // Instrumentation: persist and emit to stderr for server logs
305
306
  try { await fs.appendFile('/tmp/mcp_startapp_instrument.log', JSON.stringify(instrumentation) + '\n') } catch (e) {}
306
307
  try { console.error('MCP-STARTAPP-EXEC', JSON.stringify(instrumentation)) } catch (e) {}
308
+ } catch {}
307
309
 
308
- const result = await execCommand(['simctl', 'launch', deviceId, bundleId], deviceId)
309
- const device = await getIOSDeviceMetadata(deviceId)
310
- return { device, appStarted: !!result.output, launchTimeMs: 1000, instrumentation }
311
- } catch (e:any) {
312
- const diag = execCommandWithDiagnostics(['simctl', 'launch', deviceId, bundleId], deviceId)
310
+ // Attempt to launch
311
+ let launchResult: any = null
312
+ 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)
313
317
  const device = await getIOSDeviceMetadata(deviceId)
314
- return { device, appStarted: false, launchTimeMs: 0, error: e instanceof Error ? e.message : String(e), diagnostics: diag, instrumentation } as any
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
315
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
+ }
387
+ }
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
316
397
  }
317
398
 
318
399
  async terminateApp(bundleId: string, deviceId: string = "booted"): Promise<TerminateAppResponse> {
package/src/server.ts CHANGED
@@ -413,10 +413,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
413
413
  if (name === "start_app") {
414
414
  const { platform, appId, deviceId } = args as any
415
415
  const res = await (platform === 'android' ? new AndroidManage().startApp(appId, deviceId) : new iOSManage().startApp(appId, deviceId))
416
+ // Preserve diagnostics and instrumentation from platform managers so agents receive full context
416
417
  const response: StartAppResponse = {
417
418
  device: res.device,
418
419
  appStarted: res.appStarted,
419
- launchTimeMs: res.launchTimeMs
420
+ launchTimeMs: res.launchTimeMs,
421
+ error: (res as any).error,
422
+ diagnostics: (res as any).diagnostics,
423
+ instrumentation: (res as any).instrumentation
420
424
  }
421
425
  return wrapResponse(response)
422
426
  }