mobile-debug-mcp 0.12.7 → 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.
- package/dist/ios/manage.js +5 -106
- package/dist/ios/utils.js +0 -49
- package/dist/server.js +40 -5
- package/docs/CHANGELOG.md +3 -0
- package/package.json +1 -1
- package/src/ios/manage.ts +6 -93
- package/src/ios/utils.ts +0 -47
- package/src/server.ts +42 -5
- package/src/types.ts +0 -1
package/dist/ios/manage.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,15 +389,50 @@ 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 caller provided platform and appId.
|
|
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
|
|
398
|
+
try {
|
|
399
|
+
const diag = require('./utils/diagnostics.js');
|
|
400
|
+
if (diag && diag.appendDiagnosticFile) {
|
|
401
|
+
diag.appendDiagnosticFile('bad_requests.log', payload);
|
|
402
|
+
logged = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
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
|
+
}
|
|
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 });
|
|
430
|
+
}
|
|
392
431
|
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
|
|
394
432
|
const response = {
|
|
395
433
|
device: res.device,
|
|
396
434
|
appStarted: res.appStarted,
|
|
397
|
-
launchTimeMs: res.launchTimeMs
|
|
398
|
-
error: res.error,
|
|
399
|
-
diagnostics: res.diagnostics,
|
|
400
|
-
instrumentation: res.instrumentation
|
|
435
|
+
launchTimeMs: res.launchTimeMs
|
|
401
436
|
};
|
|
402
437
|
return wrapResponse(response);
|
|
403
438
|
}
|
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
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
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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,15 +412,52 @@ 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 caller provided platform and appId.
|
|
416
|
+
if (!platform || !appId) {
|
|
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
|
+
}
|
|
431
|
+
|
|
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
|
+
}
|
|
441
|
+
}
|
|
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 })
|
|
454
|
+
}
|
|
455
|
+
|
|
415
456
|
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
|
|
417
457
|
const response: StartAppResponse = {
|
|
418
458
|
device: res.device,
|
|
419
459
|
appStarted: res.appStarted,
|
|
420
|
-
launchTimeMs: res.launchTimeMs
|
|
421
|
-
error: (res as any).error,
|
|
422
|
-
diagnostics: (res as any).diagnostics,
|
|
423
|
-
instrumentation: (res as any).instrumentation
|
|
460
|
+
launchTimeMs: res.launchTimeMs
|
|
424
461
|
}
|
|
425
462
|
return wrapResponse(response)
|
|
426
463
|
}
|