@mgcrea/mcp-ios-simulator 0.3.1 → 0.4.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/cli.js +2 -3
- package/dist/{config-D0JgmT_U.js → config-HmqayUrt.js} +140 -88
- package/dist/config-HmqayUrt.js.map +1 -0
- package/dist/index.d.ts +48 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/package.json +13 -13
- package/dist/config-D0JgmT_U.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { N as BUILD_INFO, a as createServer, n as loadConfig, t as defaultConfigPath } from "./config-
|
|
2
|
+
import { N as BUILD_INFO, a as createServer, n as loadConfig, t as defaultConfigPath } from "./config-HmqayUrt.js";
|
|
3
3
|
import { ZodError } from "zod";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
5
|
-
|
|
5
|
+
// src/cli.ts
|
|
6
6
|
const stderrLogger = {
|
|
7
7
|
debug: (...args) => {
|
|
8
8
|
if (process.env.IOS_SIMULATOR_DEBUG) console.error("[ios-simulator-mcp]", ...args);
|
|
@@ -41,7 +41,6 @@ main().catch((err) => {
|
|
|
41
41
|
console.error(`[ios-simulator-mcp] fatal: ${describeFatal(err)}`);
|
|
42
42
|
process.exit(1);
|
|
43
43
|
});
|
|
44
|
-
//#endregion
|
|
45
44
|
export {};
|
|
46
45
|
|
|
47
46
|
//# sourceMappingURL=cli.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { CommandError, CommandTimeoutError, DEFAULT_CRASH_KINDS, IosError, IosError as IosError$1, ToolchainError, WdaClient, WdaError, WdaUnavailableError, assertNoShellMetachars, crashKind, crashProcessName, createArgs, createExec, readLogFile, registerInputTools, registerScreenTools, summarizeCrashReport, wrap } from "@mgcrea/mcp-ios-core";
|
|
5
5
|
import { mkdir, mkdtemp, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -7,7 +7,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
-
|
|
10
|
+
// src/build-info.ts
|
|
11
11
|
const readPackageJson = () => {
|
|
12
12
|
try {
|
|
13
13
|
const pkgUrl = new URL("../package.json", import.meta.url);
|
|
@@ -23,11 +23,10 @@ const pkg = readPackageJson();
|
|
|
23
23
|
const BUILD_INFO = {
|
|
24
24
|
name: pkg.name,
|
|
25
25
|
version: pkg.version,
|
|
26
|
-
gitCommit: "
|
|
27
|
-
gitCommitDate: "2026-10-
|
|
26
|
+
gitCommit: "5584c7c",
|
|
27
|
+
gitCommitDate: "2026-10-29T22:11:35+01:00"
|
|
28
28
|
};
|
|
29
|
-
|
|
30
|
-
//#region src/client/errors.ts
|
|
29
|
+
// src/client/errors.ts
|
|
31
30
|
/** Raising the budget, or noticing the simulator died mid-call. */
|
|
32
31
|
const TIMEOUT_REMEDY = "Raise IOS_SIMULATOR_TIMEOUT_MS if this is a slow operation (a boot or a large `install` genuinely takes tens of seconds), or check the simulator is still booted.";
|
|
33
32
|
const TOOLCHAIN_REMEDY = "Install Xcode and run `xcode-select --install`, or point IOS_SIMULATOR_XCRUN_PATH at a different xcrun.";
|
|
@@ -99,8 +98,7 @@ var WritesDisabledError = class extends IosError {
|
|
|
99
98
|
};
|
|
100
99
|
/** A shut-down simulator captures nothing, and that is the usual cause here. */
|
|
101
100
|
const EMPTY_SCREENSHOT_REMEDY = "The simulator produced an empty capture. Check it is actually booted with ios_simulator_diagnostics, and that its runtime is installed.";
|
|
102
|
-
|
|
103
|
-
//#region src/client/display.ts
|
|
101
|
+
// src/client/display.ts
|
|
104
102
|
/**
|
|
105
103
|
* Screen geometry for a simulator, which has no `devicectl device info displays`.
|
|
106
104
|
*
|
|
@@ -191,10 +189,11 @@ const pngDimensions = (png) => {
|
|
|
191
189
|
* screen, so mapping it would be a lie.
|
|
192
190
|
*/
|
|
193
191
|
const toDisplayInfo = (profile, capture) => {
|
|
194
|
-
const scale = profile.mainScreenScale
|
|
192
|
+
const scale = profile.mainScreenScale;
|
|
195
193
|
const pixelWidth = capture?.width ?? profile.mainScreenWidth;
|
|
196
194
|
const pixelHeight = capture?.height ?? profile.mainScreenHeight;
|
|
197
195
|
if (!pixelWidth || !pixelHeight) throw new IosError$1("Could not determine the simulator's screen geometry.", { remedy: "The device type's profile.plist could not be read and no screenshot was available. Run ios_simulator_diagnostics — an unavailable runtime is the usual cause." });
|
|
196
|
+
if (!scale) throw new IosError$1("Could not determine the simulator's screen scale.", { remedy: "The device type's profile.plist could not be read, so points cannot be told from pixels. Run ios_simulator_diagnostics — an unavailable runtime is the usual cause." });
|
|
198
197
|
const landscape = profile.mainScreenWidth !== void 0 && profile.mainScreenHeight !== void 0 ? pixelWidth > pixelHeight && profile.mainScreenWidth < profile.mainScreenHeight : pixelWidth > pixelHeight;
|
|
199
198
|
return {
|
|
200
199
|
pixelWidth,
|
|
@@ -205,8 +204,7 @@ const toDisplayInfo = (profile, capture) => {
|
|
|
205
204
|
orientation: capture ? landscape ? "landscape" : "portrait" : "unknown"
|
|
206
205
|
};
|
|
207
206
|
};
|
|
208
|
-
|
|
209
|
-
//#region src/client/logs.ts
|
|
207
|
+
// src/client/logs.ts
|
|
210
208
|
const defaultProcessAlive = (pid) => {
|
|
211
209
|
try {
|
|
212
210
|
process.kill(pid, 0);
|
|
@@ -251,8 +249,7 @@ var LaunchCaptures = class {
|
|
|
251
249
|
};
|
|
252
250
|
/** True when `path` exists on this Mac. */
|
|
253
251
|
const exists = async (path) => stat(path).then(() => true, () => false);
|
|
254
|
-
|
|
255
|
-
//#region src/client/shape.ts
|
|
252
|
+
// src/client/shape.ts
|
|
256
253
|
/**
|
|
257
254
|
* Flatten the runtime-keyed map `list devices` returns.
|
|
258
255
|
*
|
|
@@ -307,8 +304,7 @@ const toListRow = (sim) => ({
|
|
|
307
304
|
...sim.available ? {} : { available: false },
|
|
308
305
|
...sim.unavailableReason ? { unavailableReason: sim.unavailableReason } : {}
|
|
309
306
|
});
|
|
310
|
-
|
|
311
|
-
//#region src/client/simctl.ts
|
|
307
|
+
// src/client/simctl.ts
|
|
312
308
|
/**
|
|
313
309
|
* Pull the human sentence out of a CoreSimulator error envelope.
|
|
314
310
|
*
|
|
@@ -358,6 +354,12 @@ var Simctl = class {
|
|
|
358
354
|
exec;
|
|
359
355
|
/** Device types change only when Xcode is updated; re-reading is pure cost. */
|
|
360
356
|
deviceTypes;
|
|
357
|
+
/**
|
|
358
|
+
* Read once, like device types: both change only when Xcode or a runtime is
|
|
359
|
+
* installed, and every tool call resolves a simulator. The cost is that a
|
|
360
|
+
* runtime added or deleted while this server runs is seen after a restart.
|
|
361
|
+
*/
|
|
362
|
+
runtimes;
|
|
361
363
|
constructor(opts) {
|
|
362
364
|
this.opts = opts;
|
|
363
365
|
this.exec = opts.exec ?? createExec({
|
|
@@ -367,9 +369,9 @@ var Simctl = class {
|
|
|
367
369
|
});
|
|
368
370
|
}
|
|
369
371
|
/** Raw stdout, for the subcommands that answer in prose. */
|
|
370
|
-
async run(args) {
|
|
372
|
+
async run(args, timeoutMs = this.opts.timeoutMs) {
|
|
371
373
|
this.opts.logger?.debug?.("simctl", ...args);
|
|
372
|
-
const { stdout } = await this.exec(this.opts.xcrunPath, ["simctl", ...args],
|
|
374
|
+
const { stdout } = await this.exec(this.opts.xcrunPath, ["simctl", ...args], timeoutMs);
|
|
373
375
|
return stdout;
|
|
374
376
|
}
|
|
375
377
|
/**
|
|
@@ -426,7 +428,8 @@ var Simctl = class {
|
|
|
426
428
|
return (await this.runJson(["list", "devices"])).devices ?? {};
|
|
427
429
|
}
|
|
428
430
|
async listRuntimes() {
|
|
429
|
-
|
|
431
|
+
this.runtimes ??= (await this.runJson(["list", "runtimes"])).runtimes ?? [];
|
|
432
|
+
return this.runtimes;
|
|
430
433
|
}
|
|
431
434
|
async listDeviceTypes() {
|
|
432
435
|
this.deviceTypes ??= (await this.runJson(["list", "devicetypes"])).devicetypes ?? [];
|
|
@@ -436,6 +439,16 @@ var Simctl = class {
|
|
|
436
439
|
assertNotBulkTarget(udid);
|
|
437
440
|
await this.run(["boot", udid]);
|
|
438
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Wait until a booting simulator has finished, or `timeoutMs` passes (a
|
|
444
|
+
* `CommandTimeoutError`). Not the state in `list`: measured on an iOS 27.0
|
|
445
|
+
* runtime, that reads "Booted" 0.4s after `boot` returns, while this returns
|
|
446
|
+
* 17s later, once the system is up.
|
|
447
|
+
*/
|
|
448
|
+
async bootstatus(udid, timeoutMs) {
|
|
449
|
+
assertNotBulkTarget(udid);
|
|
450
|
+
await this.run(["bootstatus", udid], timeoutMs);
|
|
451
|
+
}
|
|
439
452
|
async shutdown(udid) {
|
|
440
453
|
assertNotBulkTarget(udid);
|
|
441
454
|
await this.run(["shutdown", udid]);
|
|
@@ -702,8 +715,7 @@ var Simctl = class {
|
|
|
702
715
|
}
|
|
703
716
|
}
|
|
704
717
|
};
|
|
705
|
-
|
|
706
|
-
//#region src/client/simulator.ts
|
|
718
|
+
// src/client/simulator.ts
|
|
707
719
|
/**
|
|
708
720
|
* What to say when WebDriverAgent fails, on a simulator.
|
|
709
721
|
*
|
|
@@ -767,6 +779,13 @@ var SimulatorClient = class {
|
|
|
767
779
|
return simulators;
|
|
768
780
|
}
|
|
769
781
|
/**
|
|
782
|
+
* Drop the cached list. Called after a tool changes a simulator's state, so
|
|
783
|
+
* the next call within the cache window does not act on the state before it.
|
|
784
|
+
*/
|
|
785
|
+
forgetSimulators() {
|
|
786
|
+
this.cache = void 0;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
770
789
|
* Resolve a hint to exactly one simulator, or refuse.
|
|
771
790
|
*
|
|
772
791
|
* The device server's rule — never guess between two — transfers, but
|
|
@@ -835,7 +854,7 @@ var SimulatorClient = class {
|
|
|
835
854
|
exec: this.exec,
|
|
836
855
|
timeoutMs: this.execTimeoutMs
|
|
837
856
|
}) : {};
|
|
838
|
-
if (id) this.profiles.set(id, profile);
|
|
857
|
+
if (id && profile.mainScreenScale !== void 0) this.profiles.set(id, profile);
|
|
839
858
|
}
|
|
840
859
|
const capture = capturePng ? pngDimensions(Buffer.from(capturePng, "base64")) : void 0;
|
|
841
860
|
return toDisplayInfo(profile, capture);
|
|
@@ -894,6 +913,10 @@ var SimulatorClient = class {
|
|
|
894
913
|
wdaUrl() {
|
|
895
914
|
return this.opts.wdaUrl ?? `http://127.0.0.1:${this.opts.wdaPort}`;
|
|
896
915
|
}
|
|
916
|
+
/** The simulator `resolveTarget` falls back to when no hint is given. */
|
|
917
|
+
get defaultSimulatorId() {
|
|
918
|
+
return this.opts.defaultSimulatorId;
|
|
919
|
+
}
|
|
897
920
|
get wdaPort() {
|
|
898
921
|
return this.opts.wdaPort;
|
|
899
922
|
}
|
|
@@ -912,8 +935,7 @@ var SimulatorClient = class {
|
|
|
912
935
|
return client;
|
|
913
936
|
}
|
|
914
937
|
};
|
|
915
|
-
|
|
916
|
-
//#region src/tools/util.ts
|
|
938
|
+
// src/tools/util.ts
|
|
917
939
|
/**
|
|
918
940
|
* How the shared tools name themselves and each other in this server.
|
|
919
941
|
*
|
|
@@ -944,8 +966,7 @@ const { bundleIdArg, confirmArg, detailArg, screenshotArg, settleArg, xArg, yArg
|
|
|
944
966
|
* worse than either alone.
|
|
945
967
|
*/
|
|
946
968
|
const deviceArg = args.targetArg;
|
|
947
|
-
|
|
948
|
-
//#region src/tools/apps.ts
|
|
969
|
+
// src/tools/apps.ts
|
|
949
970
|
/** `io.mgcrea.Canopy: 12345` is the whole of what `simctl launch` prints. */
|
|
950
971
|
const parsePid = (output) => {
|
|
951
972
|
const pid = /:\s*(\d+)\s*$/.exec(output.trim())?.[1];
|
|
@@ -1130,8 +1151,7 @@ const registerAppTools = (server, client, launchArgs, outputDir) => {
|
|
|
1130
1151
|
return { opened: url };
|
|
1131
1152
|
}));
|
|
1132
1153
|
};
|
|
1133
|
-
|
|
1134
|
-
//#region src/tools/diagnostics.ts
|
|
1154
|
+
// src/tools/diagnostics.ts
|
|
1135
1155
|
/**
|
|
1136
1156
|
* Which simulator owns the WebDriverAgent port.
|
|
1137
1157
|
*
|
|
@@ -1219,7 +1239,7 @@ const diagnose = async (client, ctx, deviceHint) => {
|
|
|
1219
1239
|
try {
|
|
1220
1240
|
target = await client.resolveTarget(deviceHint);
|
|
1221
1241
|
} catch (err) {
|
|
1222
|
-
if (booted.length > 0) {
|
|
1242
|
+
if (booted.length > 0 || (deviceHint ?? client.defaultSimulatorId) !== void 0) {
|
|
1223
1243
|
problems.push(err instanceof Error ? err.message : String(err));
|
|
1224
1244
|
const remedy = err.remedy;
|
|
1225
1245
|
if (remedy) nextSteps.push(remedy);
|
|
@@ -1289,8 +1309,7 @@ const registerDiagnosticsTools = (server, client, ctx) => {
|
|
|
1289
1309
|
text: JSON.stringify(await diagnose(client, ctx))
|
|
1290
1310
|
}] }));
|
|
1291
1311
|
};
|
|
1292
|
-
|
|
1293
|
-
//#region src/tools/environment.ts
|
|
1312
|
+
// src/tools/environment.ts
|
|
1294
1313
|
/** simctl's own list, verbatim — a value it does not accept is not worth guessing at. */
|
|
1295
1314
|
const PRIVACY_SERVICES = [
|
|
1296
1315
|
"all",
|
|
@@ -1366,6 +1385,26 @@ const registerEnvironmentTools = (server, client) => {
|
|
|
1366
1385
|
}
|
|
1367
1386
|
}, async ({ device, appearance, content_size, increase_contrast, status_bar, clear_status_bar, location, clear_location, permission }) => wrap(async () => {
|
|
1368
1387
|
const target = client.requireBooted(await client.resolveTarget(device));
|
|
1388
|
+
const statusBarFlags = status_bar ? [
|
|
1389
|
+
...status_bar.time ? ["--time", status_bar.time] : [],
|
|
1390
|
+
...status_bar.wifi_bars !== void 0 ? [
|
|
1391
|
+
"--wifiMode",
|
|
1392
|
+
"active",
|
|
1393
|
+
"--wifiBars",
|
|
1394
|
+
String(status_bar.wifi_bars)
|
|
1395
|
+
] : [],
|
|
1396
|
+
...status_bar.cellular_bars !== void 0 ? [
|
|
1397
|
+
"--cellularMode",
|
|
1398
|
+
"active",
|
|
1399
|
+
"--cellularBars",
|
|
1400
|
+
String(status_bar.cellular_bars)
|
|
1401
|
+
] : [],
|
|
1402
|
+
...status_bar.operator_name !== void 0 ? ["--operatorName", status_bar.operator_name] : [],
|
|
1403
|
+
...status_bar.battery_level !== void 0 ? ["--batteryLevel", String(status_bar.battery_level)] : [],
|
|
1404
|
+
...status_bar.battery_state ? ["--batteryState", status_bar.battery_state] : []
|
|
1405
|
+
] : [];
|
|
1406
|
+
if (status_bar && statusBarFlags.length === 0) throw new IosError$1("`status_bar` was given with no fields to override.", { remedy: "Set at least one field, or use `clear_status_bar` to drop the overrides." });
|
|
1407
|
+
if (permission && permission.action !== "reset" && !permission.bundle_id) throw new IosError$1(`\`${permission.action}\` needs a bundle_id.`, { remedy: "Pass the app's bundle id, or use `reset`, which does not need one." });
|
|
1369
1408
|
const applied = [];
|
|
1370
1409
|
if (appearance) {
|
|
1371
1410
|
await client.simctl.uiSet(target.id, "appearance", appearance);
|
|
@@ -1384,26 +1423,7 @@ const registerEnvironmentTools = (server, client) => {
|
|
|
1384
1423
|
applied.push("status_bar cleared");
|
|
1385
1424
|
}
|
|
1386
1425
|
if (status_bar) {
|
|
1387
|
-
|
|
1388
|
-
...status_bar.time ? ["--time", status_bar.time] : [],
|
|
1389
|
-
...status_bar.wifi_bars !== void 0 ? [
|
|
1390
|
-
"--wifiMode",
|
|
1391
|
-
"active",
|
|
1392
|
-
"--wifiBars",
|
|
1393
|
-
String(status_bar.wifi_bars)
|
|
1394
|
-
] : [],
|
|
1395
|
-
...status_bar.cellular_bars !== void 0 ? [
|
|
1396
|
-
"--cellularMode",
|
|
1397
|
-
"active",
|
|
1398
|
-
"--cellularBars",
|
|
1399
|
-
String(status_bar.cellular_bars)
|
|
1400
|
-
] : [],
|
|
1401
|
-
...status_bar.operator_name !== void 0 ? ["--operatorName", status_bar.operator_name] : [],
|
|
1402
|
-
...status_bar.battery_level !== void 0 ? ["--batteryLevel", String(status_bar.battery_level)] : [],
|
|
1403
|
-
...status_bar.battery_state ? ["--batteryState", status_bar.battery_state] : []
|
|
1404
|
-
];
|
|
1405
|
-
if (flags.length === 0) throw new IosError$1("`status_bar` was given with no fields to override.", { remedy: "Set at least one field, or use `clear_status_bar` to drop the overrides." });
|
|
1406
|
-
await client.simctl.statusBarOverride(target.id, flags);
|
|
1426
|
+
await client.simctl.statusBarOverride(target.id, statusBarFlags);
|
|
1407
1427
|
applied.push("status_bar overridden");
|
|
1408
1428
|
}
|
|
1409
1429
|
if (clear_location) {
|
|
@@ -1415,17 +1435,21 @@ const registerEnvironmentTools = (server, client) => {
|
|
|
1415
1435
|
applied.push(`location=${location.latitude},${location.longitude}`);
|
|
1416
1436
|
}
|
|
1417
1437
|
if (permission) {
|
|
1418
|
-
if (permission.action !== "reset" && !permission.bundle_id) throw new IosError$1(`\`${permission.action}\` needs a bundle_id.`, { remedy: "Pass the app's bundle id, or use `reset`, which does not need one." });
|
|
1419
1438
|
await client.simctl.privacy(target.id, permission.action, permission.service, permission.bundle_id);
|
|
1420
1439
|
applied.push(`permission ${permission.action} ${permission.service}`);
|
|
1421
1440
|
}
|
|
1422
1441
|
if (applied.length === 0) throw new IosError$1("Nothing to do — no field was given.", { remedy: "Set at least one of appearance, content_size, status_bar, location, permission." });
|
|
1442
|
+
const [appearanceNow, contentSize, statusBar] = await Promise.all([
|
|
1443
|
+
client.simctl.uiGet(target.id, "appearance"),
|
|
1444
|
+
client.simctl.uiGet(target.id, "content_size"),
|
|
1445
|
+
client.simctl.statusBarList(target.id)
|
|
1446
|
+
]);
|
|
1423
1447
|
return {
|
|
1424
1448
|
applied,
|
|
1425
1449
|
state: {
|
|
1426
|
-
appearance:
|
|
1427
|
-
contentSize
|
|
1428
|
-
statusBar
|
|
1450
|
+
appearance: appearanceNow,
|
|
1451
|
+
contentSize,
|
|
1452
|
+
statusBar
|
|
1429
1453
|
}
|
|
1430
1454
|
};
|
|
1431
1455
|
}));
|
|
@@ -1475,11 +1499,7 @@ const registerEnvironmentTools = (server, client) => {
|
|
|
1475
1499
|
};
|
|
1476
1500
|
}));
|
|
1477
1501
|
};
|
|
1478
|
-
|
|
1479
|
-
//#region src/tools/lifecycle.ts
|
|
1480
|
-
/** Booting is slow and visible; this is how long `power` waits to see it land. */
|
|
1481
|
-
const BOOT_POLL_MS = 500;
|
|
1482
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1502
|
+
// src/tools/lifecycle.ts
|
|
1483
1503
|
const registerListTool = (server, client) => {
|
|
1484
1504
|
server.registerTool("ios_simulator_list", {
|
|
1485
1505
|
title: "iOS Simulator: List",
|
|
@@ -1524,13 +1544,14 @@ const registerLifecycleTools = (server, client) => {
|
|
|
1524
1544
|
}, async ({ device, state, open_window, wait_ms }) => wrap(async () => {
|
|
1525
1545
|
const target = await client.resolveTarget(device);
|
|
1526
1546
|
if (state === "shutdown") {
|
|
1527
|
-
if (target.state
|
|
1547
|
+
if (target.state === "Shutdown") return {
|
|
1528
1548
|
udid: target.id,
|
|
1529
1549
|
name: target.name,
|
|
1530
1550
|
state: "Shutdown",
|
|
1531
1551
|
alreadyThere: true
|
|
1532
1552
|
};
|
|
1533
1553
|
await client.simctl.shutdown(target.id);
|
|
1554
|
+
client.forgetSimulators();
|
|
1534
1555
|
return {
|
|
1535
1556
|
udid: target.id,
|
|
1536
1557
|
name: target.name,
|
|
@@ -1538,7 +1559,10 @@ const registerLifecycleTools = (server, client) => {
|
|
|
1538
1559
|
};
|
|
1539
1560
|
}
|
|
1540
1561
|
const already = target.state === "Booted";
|
|
1541
|
-
if (!already)
|
|
1562
|
+
if (!already) {
|
|
1563
|
+
await client.simctl.boot(target.id);
|
|
1564
|
+
client.forgetSimulators();
|
|
1565
|
+
}
|
|
1542
1566
|
let windowWarning;
|
|
1543
1567
|
let windowNote;
|
|
1544
1568
|
if (open_window) await client.simctl.openApp(client.openPath).then((app) => {
|
|
@@ -1546,20 +1570,22 @@ const registerLifecycleTools = (server, client) => {
|
|
|
1546
1570
|
}).catch((err) => {
|
|
1547
1571
|
windowWarning = `Booted, but no window could be opened: ${err instanceof Error ? err.message : String(err)}`;
|
|
1548
1572
|
});
|
|
1549
|
-
|
|
1550
|
-
let
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1573
|
+
let ready = already;
|
|
1574
|
+
let waitWarning = `Not waited for, since wait_ms is 0: it may still be starting up.`;
|
|
1575
|
+
if (!already && wait_ms > 0) try {
|
|
1576
|
+
await client.simctl.bootstatus(target.id, wait_ms);
|
|
1577
|
+
ready = true;
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
waitWarning = err instanceof CommandTimeoutError ? `Not finished booting after ${wait_ms}ms. Raise wait_ms, or check Xcode.` : `Booted, but could not confirm it finished: ${err instanceof Error ? err.message : String(err)}`;
|
|
1555
1580
|
}
|
|
1581
|
+
const observed = Object.values(await client.simctl.listDevices()).flat().find((sim) => sim.udid === target.id)?.state ?? target.state;
|
|
1556
1582
|
return {
|
|
1557
1583
|
udid: target.id,
|
|
1558
1584
|
name: target.name,
|
|
1559
1585
|
state: observed,
|
|
1560
1586
|
...already ? { alreadyThere: true } : {},
|
|
1561
1587
|
...windowNote ? { note: windowNote } : {},
|
|
1562
|
-
...
|
|
1588
|
+
...ready ? windowWarning ? { warning: windowWarning } : {} : { warning: waitWarning }
|
|
1563
1589
|
};
|
|
1564
1590
|
}));
|
|
1565
1591
|
server.registerTool("ios_simulator_erase", {
|
|
@@ -1580,6 +1606,7 @@ const registerLifecycleTools = (server, client) => {
|
|
|
1580
1606
|
if (wasBooted) await client.simctl.shutdown(target.id);
|
|
1581
1607
|
await client.simctl.erase(target.id);
|
|
1582
1608
|
if (wasBooted) await client.simctl.boot(target.id);
|
|
1609
|
+
client.forgetSimulators();
|
|
1583
1610
|
return {
|
|
1584
1611
|
udid: target.id,
|
|
1585
1612
|
name: target.name,
|
|
@@ -1588,8 +1615,7 @@ const registerLifecycleTools = (server, client) => {
|
|
|
1588
1615
|
};
|
|
1589
1616
|
}));
|
|
1590
1617
|
};
|
|
1591
|
-
|
|
1592
|
-
//#region src/client/crash.ts
|
|
1618
|
+
// src/client/crash.ts
|
|
1593
1619
|
/**
|
|
1594
1620
|
* Simulator crash reports, out of this Mac's own DiagnosticReports.
|
|
1595
1621
|
*
|
|
@@ -1660,6 +1686,13 @@ const listIps = async (dir) => {
|
|
|
1660
1686
|
return [...top.filter((name) => name.endsWith(".ips")), ...retired.filter((name) => name.endsWith(".ips")).map((name) => `Retired/${name}`)];
|
|
1661
1687
|
};
|
|
1662
1688
|
/**
|
|
1689
|
+
* What each report's head said, by path; `null` for one that is not from a
|
|
1690
|
+
* simulator. A written `.ips` never changes and its name carries a timestamp,
|
|
1691
|
+
* so after the first list only new reports cost a read. A Mac with Xcode
|
|
1692
|
+
* holds hundreds of them, and every list used to open each one.
|
|
1693
|
+
*/
|
|
1694
|
+
const scanned = /* @__PURE__ */ new Map();
|
|
1695
|
+
/**
|
|
1663
1696
|
* Every simulator report in `dir`, newest first. Reads the first 8 KB of each
|
|
1664
1697
|
* `.ips`, never the whole file, except to find the UDID in the rare report
|
|
1665
1698
|
* whose `coalitionName` did not fit in that.
|
|
@@ -1670,12 +1703,18 @@ const listSimulatorCrashReports = async (dir) => {
|
|
|
1670
1703
|
for (let i = 0; i < names.length; i += CONCURRENCY) {
|
|
1671
1704
|
const batch = await Promise.all(names.slice(i, i + CONCURRENCY).map(async (name) => {
|
|
1672
1705
|
const path = join(dir, name);
|
|
1706
|
+
const known = scanned.get(path);
|
|
1707
|
+
if (known !== void 0) return known ?? void 0;
|
|
1673
1708
|
const head = await readHead(path).catch(() => "");
|
|
1674
1709
|
const header = parseHeader(head);
|
|
1675
|
-
if (!header
|
|
1710
|
+
if (!header) return void 0;
|
|
1711
|
+
if (!SIMULATOR_PLATFORMS.has(Number(header.platform))) {
|
|
1712
|
+
scanned.set(path, null);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1676
1715
|
let udid = COALITION.exec(head)?.[1];
|
|
1677
1716
|
if (!udid) udid = COALITION.exec(await readFile(path, "utf8").catch(() => ""))?.[1];
|
|
1678
|
-
|
|
1717
|
+
const row = {
|
|
1679
1718
|
name,
|
|
1680
1719
|
process: header.app_name || header.name || crashProcessName(name),
|
|
1681
1720
|
bundleId: header.bundleID,
|
|
@@ -1683,6 +1722,8 @@ const listSimulatorCrashReports = async (dir) => {
|
|
|
1683
1722
|
timestamp: header.timestamp,
|
|
1684
1723
|
udid: udid?.toUpperCase()
|
|
1685
1724
|
};
|
|
1725
|
+
scanned.set(path, row);
|
|
1726
|
+
return row;
|
|
1686
1727
|
}));
|
|
1687
1728
|
for (const row of batch) if (row) rows.push(row);
|
|
1688
1729
|
}
|
|
@@ -1707,8 +1748,7 @@ const isSimulatorReport = (text) => {
|
|
|
1707
1748
|
};
|
|
1708
1749
|
/** The simulator a full report came from, when its body says. */
|
|
1709
1750
|
const reportUdid = (text) => COALITION.exec(text)?.[1]?.toUpperCase();
|
|
1710
|
-
|
|
1711
|
-
//#region src/tools/logs.ts
|
|
1751
|
+
// src/tools/logs.ts
|
|
1712
1752
|
/** UDID → name, for labelling reports. Best effort: a Mac with no Xcode still has reports. */
|
|
1713
1753
|
const simulatorNames = async (client) => {
|
|
1714
1754
|
try {
|
|
@@ -1738,8 +1778,8 @@ const registerCrashLogTools = (server, client, ctx) => {
|
|
|
1738
1778
|
const target = device ? await client.resolveTarget(device) : void 0;
|
|
1739
1779
|
const wanted = kinds ?? DEFAULT_CRASH_KINDS;
|
|
1740
1780
|
const needle = process?.toLowerCase();
|
|
1741
|
-
const
|
|
1742
|
-
const
|
|
1781
|
+
const [all, names] = await Promise.all([listSimulatorCrashReports(dir), target ? void 0 : simulatorNames(client)]);
|
|
1782
|
+
const matching = all.filter((report) => (!target || report.udid === target.id.toUpperCase()) && (wanted.includes("all") || wanted.includes(report.kind)) && (!needle || report.process.toLowerCase().includes(needle) || (report.bundleId?.toLowerCase().includes(needle) ?? false)));
|
|
1743
1783
|
return {
|
|
1744
1784
|
...target ? {
|
|
1745
1785
|
simulator: target.name,
|
|
@@ -1829,8 +1869,7 @@ const registerLogReadTools = (server, client) => {
|
|
|
1829
1869
|
};
|
|
1830
1870
|
}));
|
|
1831
1871
|
};
|
|
1832
|
-
|
|
1833
|
-
//#region src/tools/runner.ts
|
|
1872
|
+
// src/tools/runner.ts
|
|
1834
1873
|
const defaultSpawnRunner = async (command, args, { env, logPath }) => {
|
|
1835
1874
|
await mkdir(dirname(logPath), { recursive: true });
|
|
1836
1875
|
const log = await open(logPath, "a");
|
|
@@ -1852,9 +1891,22 @@ const defaultSpawnRunner = async (command, args, { env, logPath }) => {
|
|
|
1852
1891
|
};
|
|
1853
1892
|
const scriptPath = () => {
|
|
1854
1893
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1855
|
-
for (const candidate of [join(here, "..", "scripts", "wda.sh"), join(here, "..", "..", "scripts", "wda.sh")]) return candidate;
|
|
1894
|
+
for (const candidate of [join(here, "..", "scripts", "wda.sh"), join(here, "..", "..", "scripts", "wda.sh")]) if (existsSync(candidate)) return candidate;
|
|
1856
1895
|
throw new IosError$1("Could not find `scripts/wda.sh` beside this module.");
|
|
1857
1896
|
};
|
|
1897
|
+
/**
|
|
1898
|
+
* Whether `wda.sh run` has anything to run: the same directory and glob it
|
|
1899
|
+
* looks in. Without a build it exits with "no build found" into a log nobody
|
|
1900
|
+
* reads, while a detached spawn has already been reported as started.
|
|
1901
|
+
*/
|
|
1902
|
+
const runnerBuilt = async () => {
|
|
1903
|
+
const wdaDir = process.env["WDA_DIR"] || join(homedir(), ".cache", "mcp-ios-simulator", "WebDriverAgent");
|
|
1904
|
+
const products = join(wdaDir, ".build", "Build", "Products");
|
|
1905
|
+
return {
|
|
1906
|
+
built: (await readdir(products).catch(() => [])).some((name) => name.startsWith("WebDriverAgentRunner_iphonesimulator") && name.endsWith(".xctestrun")),
|
|
1907
|
+
products
|
|
1908
|
+
};
|
|
1909
|
+
};
|
|
1858
1910
|
const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpawnRunner) => {
|
|
1859
1911
|
server.registerTool("ios_simulator_restart_wda", {
|
|
1860
1912
|
title: "iOS Simulator: Restart WebDriverAgent",
|
|
@@ -1871,12 +1923,16 @@ const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpa
|
|
|
1871
1923
|
}, async ({ device, port }) => wrap(async () => {
|
|
1872
1924
|
const target = client.requireBooted(await client.resolveTarget(device));
|
|
1873
1925
|
const usePort = port ?? client.wdaPort;
|
|
1926
|
+
const { built, products } = await runnerBuilt();
|
|
1927
|
+
if (!built) throw new IosError$1(`No WebDriverAgent build in ${products}, so there is nothing to start.`, { remedy: "Build it once with `scripts/wda.sh setup` (or `npx -p @mgcrea/mcp-ios-simulator ios-simulator-wda setup` from an npm install), then call ios_simulator_restart_wda again." });
|
|
1874
1928
|
const logPath = join(outputDir, "wda.log");
|
|
1875
1929
|
const pid = await spawnRunner("/bin/bash", [scriptPath(), "run"], {
|
|
1876
1930
|
env: {
|
|
1877
1931
|
IOS_SIMULATOR_ID: target.id,
|
|
1878
1932
|
IOS_SIMULATOR_WDA_PORT: String(usePort),
|
|
1879
|
-
USE_PORT: String(usePort)
|
|
1933
|
+
USE_PORT: String(usePort),
|
|
1934
|
+
NODE: process.execPath,
|
|
1935
|
+
PATH: `${dirname(process.execPath)}:${process.env["PATH"] ?? ""}`
|
|
1880
1936
|
},
|
|
1881
1937
|
logPath
|
|
1882
1938
|
});
|
|
@@ -1891,8 +1947,7 @@ const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpa
|
|
|
1891
1947
|
};
|
|
1892
1948
|
}));
|
|
1893
1949
|
};
|
|
1894
|
-
|
|
1895
|
-
//#region src/tools/index.ts
|
|
1950
|
+
// src/tools/index.ts
|
|
1896
1951
|
/**
|
|
1897
1952
|
* All capability decisions in one place, so "why can't I call that" is answered
|
|
1898
1953
|
* by one file.
|
|
@@ -1943,8 +1998,7 @@ const registerTools = (server, client, ctx, spawnRunner) => {
|
|
|
1943
1998
|
registerEnvironmentTools(server, client);
|
|
1944
1999
|
registerRunnerTools(server, client, ctx.config.outputDir, spawnRunner);
|
|
1945
2000
|
};
|
|
1946
|
-
|
|
1947
|
-
//#region src/server.ts
|
|
2001
|
+
// src/server.ts
|
|
1948
2002
|
const SERVER_NAME = BUILD_INFO.name;
|
|
1949
2003
|
const SERVER_VERSION = BUILD_INFO.version;
|
|
1950
2004
|
/**
|
|
@@ -1985,8 +2039,7 @@ const createServer = (opts) => {
|
|
|
1985
2039
|
client
|
|
1986
2040
|
};
|
|
1987
2041
|
};
|
|
1988
|
-
|
|
1989
|
-
//#region src/config.ts
|
|
2042
|
+
// src/config.ts
|
|
1990
2043
|
/**
|
|
1991
2044
|
* There are no credentials here, and unlike the device server there is barely
|
|
1992
2045
|
* any setup either. Two lanes reach a simulator and both are local:
|
|
@@ -2112,7 +2165,6 @@ const loadConfig = (env = process.env, configPath = defaultConfigPath(env)) => {
|
|
|
2112
2165
|
openPath: pick(trimmed(env.IOS_SIMULATOR_OPEN_PATH), "openPath")
|
|
2113
2166
|
});
|
|
2114
2167
|
};
|
|
2115
|
-
//#endregion
|
|
2116
2168
|
export { WritesDisabledError as A, SimulatorNotBootedError as C, ToolchainError as D, TOOLCHAIN_REMEDY as E, wdaUnavailableRemedy as M, BUILD_INFO as N, WdaError as O, START_RUNNER_TOOL_REMEDY as S, TIMEOUT_REMEDY as T, CommandTimeoutError as _, createServer as a, RuntimeUnavailableError as b, assertNotBulkTarget as c, summarizeDevices as d, toListRow as f, CommandError as g, toDisplayInfo as h, SERVER_VERSION as i, startRunnerRemedy as j, WdaUnavailableError as k, parseSimctlError as l, readScreenProfile as m, loadConfig as n, SimulatorClient as o, pngDimensions as p, SERVER_NAME as r, Simctl as s, defaultConfigPath as t, summarizeApps as u, EMPTY_SCREENSHOT_REMEDY as v, SimulatorNotFoundError as w, START_RUNNER_SCRIPT_REMEDY as x, IosError$1 as y };
|
|
2117
2169
|
|
|
2118
|
-
//# sourceMappingURL=config-
|
|
2170
|
+
//# sourceMappingURL=config-HmqayUrt.js.map
|