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.
- package/dist/ios/manage.js +95 -5
- package/dist/server.js +5 -1
- package/package.json +1 -1
- package/src/ios/manage.ts +87 -6
- package/src/server.ts +5 -1
package/dist/ios/manage.js
CHANGED
|
@@ -303,15 +303,105 @@ export class iOSManage {
|
|
|
303
303
|
console.error('MCP-STARTAPP-EXEC', JSON.stringify(instrumentation));
|
|
304
304
|
}
|
|
305
305
|
catch (e) { }
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
}
|