@mgcrea/mcp-ios-simulator 0.3.1 → 0.4.1
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-SdCcoXC3.js} +178 -97
- package/dist/config-SdCcoXC3.js.map +1 -0
- package/dist/index.d.ts +59 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -2
- package/package.json +13 -13
- package/scripts/wda.sh +2 -2
- 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-SdCcoXC3.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,13 +1,13 @@
|
|
|
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
|
-
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";
|
|
4
|
+
import { CommandError, CommandError as CommandError$1, 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";
|
|
6
6
|
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-
|
|
26
|
+
gitCommit: "6789bb6",
|
|
27
|
+
gitCommitDate: "2026-11-04T10:23:19+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
|
*
|
|
@@ -353,11 +349,23 @@ const remedyFor = (stderr) => {
|
|
|
353
349
|
const assertNotBulkTarget = (udid) => {
|
|
354
350
|
if (udid === "all" || udid === "unavailable" || udid === "booted") throw new IosError$1(`Refusing to pass "${udid}" to simctl as a device.`, { remedy: "simctl reads these as *every* matching simulator, so `erase all` would wipe the machine. Resolve to one concrete UDID with ios_simulator_list first." });
|
|
355
351
|
};
|
|
352
|
+
/**
|
|
353
|
+
* `io … screenshot` refusing to pick between a multi-display device's screens.
|
|
354
|
+
* Matched on simctl's own words rather than exit 22 alone, which is just
|
|
355
|
+
* EINVAL and covers any malformed argument.
|
|
356
|
+
*/
|
|
357
|
+
const isAmbiguousDisplay = (err) => err instanceof CommandError && /Cannot choose between \d+ displays/.test(`${String(err.details ?? "")}\n${err.message}`);
|
|
356
358
|
var Simctl = class {
|
|
357
359
|
opts;
|
|
358
360
|
exec;
|
|
359
361
|
/** Device types change only when Xcode is updated; re-reading is pure cost. */
|
|
360
362
|
deviceTypes;
|
|
363
|
+
/**
|
|
364
|
+
* Read once, like device types: both change only when Xcode or a runtime is
|
|
365
|
+
* installed, and every tool call resolves a simulator. The cost is that a
|
|
366
|
+
* runtime added or deleted while this server runs is seen after a restart.
|
|
367
|
+
*/
|
|
368
|
+
runtimes;
|
|
361
369
|
constructor(opts) {
|
|
362
370
|
this.opts = opts;
|
|
363
371
|
this.exec = opts.exec ?? createExec({
|
|
@@ -367,9 +375,9 @@ var Simctl = class {
|
|
|
367
375
|
});
|
|
368
376
|
}
|
|
369
377
|
/** Raw stdout, for the subcommands that answer in prose. */
|
|
370
|
-
async run(args) {
|
|
378
|
+
async run(args, timeoutMs = this.opts.timeoutMs) {
|
|
371
379
|
this.opts.logger?.debug?.("simctl", ...args);
|
|
372
|
-
const { stdout } = await this.exec(this.opts.xcrunPath, ["simctl", ...args],
|
|
380
|
+
const { stdout } = await this.exec(this.opts.xcrunPath, ["simctl", ...args], timeoutMs);
|
|
373
381
|
return stdout;
|
|
374
382
|
}
|
|
375
383
|
/**
|
|
@@ -426,7 +434,8 @@ var Simctl = class {
|
|
|
426
434
|
return (await this.runJson(["list", "devices"])).devices ?? {};
|
|
427
435
|
}
|
|
428
436
|
async listRuntimes() {
|
|
429
|
-
|
|
437
|
+
this.runtimes ??= (await this.runJson(["list", "runtimes"])).runtimes ?? [];
|
|
438
|
+
return this.runtimes;
|
|
430
439
|
}
|
|
431
440
|
async listDeviceTypes() {
|
|
432
441
|
this.deviceTypes ??= (await this.runJson(["list", "devicetypes"])).devicetypes ?? [];
|
|
@@ -436,6 +445,16 @@ var Simctl = class {
|
|
|
436
445
|
assertNotBulkTarget(udid);
|
|
437
446
|
await this.run(["boot", udid]);
|
|
438
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* Wait until a booting simulator has finished, or `timeoutMs` passes (a
|
|
450
|
+
* `CommandTimeoutError`). Not the state in `list`: measured on an iOS 27.0
|
|
451
|
+
* runtime, that reads "Booted" 0.4s after `boot` returns, while this returns
|
|
452
|
+
* 17s later, once the system is up.
|
|
453
|
+
*/
|
|
454
|
+
async bootstatus(udid, timeoutMs) {
|
|
455
|
+
assertNotBulkTarget(udid);
|
|
456
|
+
await this.run(["bootstatus", udid], timeoutMs);
|
|
457
|
+
}
|
|
439
458
|
async shutdown(udid) {
|
|
440
459
|
assertNotBulkTarget(udid);
|
|
441
460
|
await this.run(["shutdown", udid]);
|
|
@@ -580,19 +599,42 @@ var Simctl = class {
|
|
|
580
599
|
*
|
|
581
600
|
* A successful capture also writes a `Note: No display specified…` line to
|
|
582
601
|
* stderr, so non-empty stderr is not a failure here.
|
|
602
|
+
*
|
|
603
|
+
* A device with two displays — the iPhone Duo, iOS 27.1 runtime — makes the
|
|
604
|
+
* bare call fail outright: exit 22, "Cannot choose between 2 displays.
|
|
605
|
+
* Specify one with --display". Only then is it retried on `--display=primary`,
|
|
606
|
+
* so a single-display simulator and Xcode 26 never see the flag. Primary
|
|
607
|
+
* rather than whichever is lit, because it is the display WebDriverAgent
|
|
608
|
+
* addresses when no `currentDisplayId` is set: a capture of the other one
|
|
609
|
+
* would put every tap read off it in the wrong coordinate space. On an open
|
|
610
|
+
* Duo that display is black (appium/appium-xcuitest-driver#3012); driving
|
|
611
|
+
* the inner one means selecting it in WDA too, which nothing here does yet.
|
|
612
|
+
* The `internal` alias the help text offers fails on the Duo with exit 22.
|
|
583
613
|
*/
|
|
584
614
|
async screenshotPng(udid) {
|
|
585
615
|
assertNotBulkTarget(udid);
|
|
586
616
|
const dir = await mkdtemp(join(tmpdir(), "simshot-"));
|
|
587
617
|
const path = join(dir, "shot.png");
|
|
588
618
|
try {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
619
|
+
try {
|
|
620
|
+
await this.run([
|
|
621
|
+
"io",
|
|
622
|
+
udid,
|
|
623
|
+
"screenshot",
|
|
624
|
+
"--type=png",
|
|
625
|
+
path
|
|
626
|
+
]);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
if (!isAmbiguousDisplay(err)) throw err;
|
|
629
|
+
await this.run([
|
|
630
|
+
"io",
|
|
631
|
+
udid,
|
|
632
|
+
"screenshot",
|
|
633
|
+
"--type=png",
|
|
634
|
+
"--display=primary",
|
|
635
|
+
path
|
|
636
|
+
]);
|
|
637
|
+
}
|
|
596
638
|
return (await readFile(path)).toString("base64");
|
|
597
639
|
} finally {
|
|
598
640
|
await rm(dir, {
|
|
@@ -702,8 +744,7 @@ var Simctl = class {
|
|
|
702
744
|
}
|
|
703
745
|
}
|
|
704
746
|
};
|
|
705
|
-
|
|
706
|
-
//#region src/client/simulator.ts
|
|
747
|
+
// src/client/simulator.ts
|
|
707
748
|
/**
|
|
708
749
|
* What to say when WebDriverAgent fails, on a simulator.
|
|
709
750
|
*
|
|
@@ -767,6 +808,13 @@ var SimulatorClient = class {
|
|
|
767
808
|
return simulators;
|
|
768
809
|
}
|
|
769
810
|
/**
|
|
811
|
+
* Drop the cached list. Called after a tool changes a simulator's state, so
|
|
812
|
+
* the next call within the cache window does not act on the state before it.
|
|
813
|
+
*/
|
|
814
|
+
forgetSimulators() {
|
|
815
|
+
this.cache = void 0;
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
770
818
|
* Resolve a hint to exactly one simulator, or refuse.
|
|
771
819
|
*
|
|
772
820
|
* The device server's rule — never guess between two — transfers, but
|
|
@@ -835,7 +883,7 @@ var SimulatorClient = class {
|
|
|
835
883
|
exec: this.exec,
|
|
836
884
|
timeoutMs: this.execTimeoutMs
|
|
837
885
|
}) : {};
|
|
838
|
-
if (id) this.profiles.set(id, profile);
|
|
886
|
+
if (id && profile.mainScreenScale !== void 0) this.profiles.set(id, profile);
|
|
839
887
|
}
|
|
840
888
|
const capture = capturePng ? pngDimensions(Buffer.from(capturePng, "base64")) : void 0;
|
|
841
889
|
return toDisplayInfo(profile, capture);
|
|
@@ -894,6 +942,10 @@ var SimulatorClient = class {
|
|
|
894
942
|
wdaUrl() {
|
|
895
943
|
return this.opts.wdaUrl ?? `http://127.0.0.1:${this.opts.wdaPort}`;
|
|
896
944
|
}
|
|
945
|
+
/** The simulator `resolveTarget` falls back to when no hint is given. */
|
|
946
|
+
get defaultSimulatorId() {
|
|
947
|
+
return this.opts.defaultSimulatorId;
|
|
948
|
+
}
|
|
897
949
|
get wdaPort() {
|
|
898
950
|
return this.opts.wdaPort;
|
|
899
951
|
}
|
|
@@ -912,8 +964,7 @@ var SimulatorClient = class {
|
|
|
912
964
|
return client;
|
|
913
965
|
}
|
|
914
966
|
};
|
|
915
|
-
|
|
916
|
-
//#region src/tools/util.ts
|
|
967
|
+
// src/tools/util.ts
|
|
917
968
|
/**
|
|
918
969
|
* How the shared tools name themselves and each other in this server.
|
|
919
970
|
*
|
|
@@ -944,8 +995,7 @@ const { bundleIdArg, confirmArg, detailArg, screenshotArg, settleArg, xArg, yArg
|
|
|
944
995
|
* worse than either alone.
|
|
945
996
|
*/
|
|
946
997
|
const deviceArg = args.targetArg;
|
|
947
|
-
|
|
948
|
-
//#region src/tools/apps.ts
|
|
998
|
+
// src/tools/apps.ts
|
|
949
999
|
/** `io.mgcrea.Canopy: 12345` is the whole of what `simctl launch` prints. */
|
|
950
1000
|
const parsePid = (output) => {
|
|
951
1001
|
const pid = /:\s*(\d+)\s*$/.exec(output.trim())?.[1];
|
|
@@ -1130,8 +1180,7 @@ const registerAppTools = (server, client, launchArgs, outputDir) => {
|
|
|
1130
1180
|
return { opened: url };
|
|
1131
1181
|
}));
|
|
1132
1182
|
};
|
|
1133
|
-
|
|
1134
|
-
//#region src/tools/diagnostics.ts
|
|
1183
|
+
// src/tools/diagnostics.ts
|
|
1135
1184
|
/**
|
|
1136
1185
|
* Which simulator owns the WebDriverAgent port.
|
|
1137
1186
|
*
|
|
@@ -1219,7 +1268,7 @@ const diagnose = async (client, ctx, deviceHint) => {
|
|
|
1219
1268
|
try {
|
|
1220
1269
|
target = await client.resolveTarget(deviceHint);
|
|
1221
1270
|
} catch (err) {
|
|
1222
|
-
if (booted.length > 0) {
|
|
1271
|
+
if (booted.length > 0 || (deviceHint ?? client.defaultSimulatorId) !== void 0) {
|
|
1223
1272
|
problems.push(err instanceof Error ? err.message : String(err));
|
|
1224
1273
|
const remedy = err.remedy;
|
|
1225
1274
|
if (remedy) nextSteps.push(remedy);
|
|
@@ -1289,8 +1338,7 @@ const registerDiagnosticsTools = (server, client, ctx) => {
|
|
|
1289
1338
|
text: JSON.stringify(await diagnose(client, ctx))
|
|
1290
1339
|
}] }));
|
|
1291
1340
|
};
|
|
1292
|
-
|
|
1293
|
-
//#region src/tools/environment.ts
|
|
1341
|
+
// src/tools/environment.ts
|
|
1294
1342
|
/** simctl's own list, verbatim — a value it does not accept is not worth guessing at. */
|
|
1295
1343
|
const PRIVACY_SERVICES = [
|
|
1296
1344
|
"all",
|
|
@@ -1366,6 +1414,26 @@ const registerEnvironmentTools = (server, client) => {
|
|
|
1366
1414
|
}
|
|
1367
1415
|
}, async ({ device, appearance, content_size, increase_contrast, status_bar, clear_status_bar, location, clear_location, permission }) => wrap(async () => {
|
|
1368
1416
|
const target = client.requireBooted(await client.resolveTarget(device));
|
|
1417
|
+
const statusBarFlags = status_bar ? [
|
|
1418
|
+
...status_bar.time ? ["--time", status_bar.time] : [],
|
|
1419
|
+
...status_bar.wifi_bars !== void 0 ? [
|
|
1420
|
+
"--wifiMode",
|
|
1421
|
+
"active",
|
|
1422
|
+
"--wifiBars",
|
|
1423
|
+
String(status_bar.wifi_bars)
|
|
1424
|
+
] : [],
|
|
1425
|
+
...status_bar.cellular_bars !== void 0 ? [
|
|
1426
|
+
"--cellularMode",
|
|
1427
|
+
"active",
|
|
1428
|
+
"--cellularBars",
|
|
1429
|
+
String(status_bar.cellular_bars)
|
|
1430
|
+
] : [],
|
|
1431
|
+
...status_bar.operator_name !== void 0 ? ["--operatorName", status_bar.operator_name] : [],
|
|
1432
|
+
...status_bar.battery_level !== void 0 ? ["--batteryLevel", String(status_bar.battery_level)] : [],
|
|
1433
|
+
...status_bar.battery_state ? ["--batteryState", status_bar.battery_state] : []
|
|
1434
|
+
] : [];
|
|
1435
|
+
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." });
|
|
1436
|
+
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
1437
|
const applied = [];
|
|
1370
1438
|
if (appearance) {
|
|
1371
1439
|
await client.simctl.uiSet(target.id, "appearance", appearance);
|
|
@@ -1384,26 +1452,7 @@ const registerEnvironmentTools = (server, client) => {
|
|
|
1384
1452
|
applied.push("status_bar cleared");
|
|
1385
1453
|
}
|
|
1386
1454
|
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);
|
|
1455
|
+
await client.simctl.statusBarOverride(target.id, statusBarFlags);
|
|
1407
1456
|
applied.push("status_bar overridden");
|
|
1408
1457
|
}
|
|
1409
1458
|
if (clear_location) {
|
|
@@ -1415,17 +1464,21 @@ const registerEnvironmentTools = (server, client) => {
|
|
|
1415
1464
|
applied.push(`location=${location.latitude},${location.longitude}`);
|
|
1416
1465
|
}
|
|
1417
1466
|
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
1467
|
await client.simctl.privacy(target.id, permission.action, permission.service, permission.bundle_id);
|
|
1420
1468
|
applied.push(`permission ${permission.action} ${permission.service}`);
|
|
1421
1469
|
}
|
|
1422
1470
|
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." });
|
|
1471
|
+
const [appearanceNow, contentSize, statusBar] = await Promise.all([
|
|
1472
|
+
client.simctl.uiGet(target.id, "appearance"),
|
|
1473
|
+
client.simctl.uiGet(target.id, "content_size"),
|
|
1474
|
+
client.simctl.statusBarList(target.id)
|
|
1475
|
+
]);
|
|
1423
1476
|
return {
|
|
1424
1477
|
applied,
|
|
1425
1478
|
state: {
|
|
1426
|
-
appearance:
|
|
1427
|
-
contentSize
|
|
1428
|
-
statusBar
|
|
1479
|
+
appearance: appearanceNow,
|
|
1480
|
+
contentSize,
|
|
1481
|
+
statusBar
|
|
1429
1482
|
}
|
|
1430
1483
|
};
|
|
1431
1484
|
}));
|
|
@@ -1475,11 +1528,7 @@ const registerEnvironmentTools = (server, client) => {
|
|
|
1475
1528
|
};
|
|
1476
1529
|
}));
|
|
1477
1530
|
};
|
|
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));
|
|
1531
|
+
// src/tools/lifecycle.ts
|
|
1483
1532
|
const registerListTool = (server, client) => {
|
|
1484
1533
|
server.registerTool("ios_simulator_list", {
|
|
1485
1534
|
title: "iOS Simulator: List",
|
|
@@ -1524,13 +1573,14 @@ const registerLifecycleTools = (server, client) => {
|
|
|
1524
1573
|
}, async ({ device, state, open_window, wait_ms }) => wrap(async () => {
|
|
1525
1574
|
const target = await client.resolveTarget(device);
|
|
1526
1575
|
if (state === "shutdown") {
|
|
1527
|
-
if (target.state
|
|
1576
|
+
if (target.state === "Shutdown") return {
|
|
1528
1577
|
udid: target.id,
|
|
1529
1578
|
name: target.name,
|
|
1530
1579
|
state: "Shutdown",
|
|
1531
1580
|
alreadyThere: true
|
|
1532
1581
|
};
|
|
1533
1582
|
await client.simctl.shutdown(target.id);
|
|
1583
|
+
client.forgetSimulators();
|
|
1534
1584
|
return {
|
|
1535
1585
|
udid: target.id,
|
|
1536
1586
|
name: target.name,
|
|
@@ -1538,7 +1588,10 @@ const registerLifecycleTools = (server, client) => {
|
|
|
1538
1588
|
};
|
|
1539
1589
|
}
|
|
1540
1590
|
const already = target.state === "Booted";
|
|
1541
|
-
if (!already)
|
|
1591
|
+
if (!already) {
|
|
1592
|
+
await client.simctl.boot(target.id);
|
|
1593
|
+
client.forgetSimulators();
|
|
1594
|
+
}
|
|
1542
1595
|
let windowWarning;
|
|
1543
1596
|
let windowNote;
|
|
1544
1597
|
if (open_window) await client.simctl.openApp(client.openPath).then((app) => {
|
|
@@ -1546,20 +1599,22 @@ const registerLifecycleTools = (server, client) => {
|
|
|
1546
1599
|
}).catch((err) => {
|
|
1547
1600
|
windowWarning = `Booted, but no window could be opened: ${err instanceof Error ? err.message : String(err)}`;
|
|
1548
1601
|
});
|
|
1549
|
-
|
|
1550
|
-
let
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1602
|
+
let ready = already;
|
|
1603
|
+
let waitWarning = `Not waited for, since wait_ms is 0: it may still be starting up.`;
|
|
1604
|
+
if (!already && wait_ms > 0) try {
|
|
1605
|
+
await client.simctl.bootstatus(target.id, wait_ms);
|
|
1606
|
+
ready = true;
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
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
1609
|
}
|
|
1610
|
+
const observed = Object.values(await client.simctl.listDevices()).flat().find((sim) => sim.udid === target.id)?.state ?? target.state;
|
|
1556
1611
|
return {
|
|
1557
1612
|
udid: target.id,
|
|
1558
1613
|
name: target.name,
|
|
1559
1614
|
state: observed,
|
|
1560
1615
|
...already ? { alreadyThere: true } : {},
|
|
1561
1616
|
...windowNote ? { note: windowNote } : {},
|
|
1562
|
-
...
|
|
1617
|
+
...ready ? windowWarning ? { warning: windowWarning } : {} : { warning: waitWarning }
|
|
1563
1618
|
};
|
|
1564
1619
|
}));
|
|
1565
1620
|
server.registerTool("ios_simulator_erase", {
|
|
@@ -1580,6 +1635,7 @@ const registerLifecycleTools = (server, client) => {
|
|
|
1580
1635
|
if (wasBooted) await client.simctl.shutdown(target.id);
|
|
1581
1636
|
await client.simctl.erase(target.id);
|
|
1582
1637
|
if (wasBooted) await client.simctl.boot(target.id);
|
|
1638
|
+
client.forgetSimulators();
|
|
1583
1639
|
return {
|
|
1584
1640
|
udid: target.id,
|
|
1585
1641
|
name: target.name,
|
|
@@ -1588,8 +1644,7 @@ const registerLifecycleTools = (server, client) => {
|
|
|
1588
1644
|
};
|
|
1589
1645
|
}));
|
|
1590
1646
|
};
|
|
1591
|
-
|
|
1592
|
-
//#region src/client/crash.ts
|
|
1647
|
+
// src/client/crash.ts
|
|
1593
1648
|
/**
|
|
1594
1649
|
* Simulator crash reports, out of this Mac's own DiagnosticReports.
|
|
1595
1650
|
*
|
|
@@ -1660,6 +1715,13 @@ const listIps = async (dir) => {
|
|
|
1660
1715
|
return [...top.filter((name) => name.endsWith(".ips")), ...retired.filter((name) => name.endsWith(".ips")).map((name) => `Retired/${name}`)];
|
|
1661
1716
|
};
|
|
1662
1717
|
/**
|
|
1718
|
+
* What each report's head said, by path; `null` for one that is not from a
|
|
1719
|
+
* simulator. A written `.ips` never changes and its name carries a timestamp,
|
|
1720
|
+
* so after the first list only new reports cost a read. A Mac with Xcode
|
|
1721
|
+
* holds hundreds of them, and every list used to open each one.
|
|
1722
|
+
*/
|
|
1723
|
+
const scanned = /* @__PURE__ */ new Map();
|
|
1724
|
+
/**
|
|
1663
1725
|
* Every simulator report in `dir`, newest first. Reads the first 8 KB of each
|
|
1664
1726
|
* `.ips`, never the whole file, except to find the UDID in the rare report
|
|
1665
1727
|
* whose `coalitionName` did not fit in that.
|
|
@@ -1670,12 +1732,18 @@ const listSimulatorCrashReports = async (dir) => {
|
|
|
1670
1732
|
for (let i = 0; i < names.length; i += CONCURRENCY) {
|
|
1671
1733
|
const batch = await Promise.all(names.slice(i, i + CONCURRENCY).map(async (name) => {
|
|
1672
1734
|
const path = join(dir, name);
|
|
1735
|
+
const known = scanned.get(path);
|
|
1736
|
+
if (known !== void 0) return known ?? void 0;
|
|
1673
1737
|
const head = await readHead(path).catch(() => "");
|
|
1674
1738
|
const header = parseHeader(head);
|
|
1675
|
-
if (!header
|
|
1739
|
+
if (!header) return void 0;
|
|
1740
|
+
if (!SIMULATOR_PLATFORMS.has(Number(header.platform))) {
|
|
1741
|
+
scanned.set(path, null);
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1676
1744
|
let udid = COALITION.exec(head)?.[1];
|
|
1677
1745
|
if (!udid) udid = COALITION.exec(await readFile(path, "utf8").catch(() => ""))?.[1];
|
|
1678
|
-
|
|
1746
|
+
const row = {
|
|
1679
1747
|
name,
|
|
1680
1748
|
process: header.app_name || header.name || crashProcessName(name),
|
|
1681
1749
|
bundleId: header.bundleID,
|
|
@@ -1683,6 +1751,8 @@ const listSimulatorCrashReports = async (dir) => {
|
|
|
1683
1751
|
timestamp: header.timestamp,
|
|
1684
1752
|
udid: udid?.toUpperCase()
|
|
1685
1753
|
};
|
|
1754
|
+
scanned.set(path, row);
|
|
1755
|
+
return row;
|
|
1686
1756
|
}));
|
|
1687
1757
|
for (const row of batch) if (row) rows.push(row);
|
|
1688
1758
|
}
|
|
@@ -1707,8 +1777,7 @@ const isSimulatorReport = (text) => {
|
|
|
1707
1777
|
};
|
|
1708
1778
|
/** The simulator a full report came from, when its body says. */
|
|
1709
1779
|
const reportUdid = (text) => COALITION.exec(text)?.[1]?.toUpperCase();
|
|
1710
|
-
|
|
1711
|
-
//#region src/tools/logs.ts
|
|
1780
|
+
// src/tools/logs.ts
|
|
1712
1781
|
/** UDID → name, for labelling reports. Best effort: a Mac with no Xcode still has reports. */
|
|
1713
1782
|
const simulatorNames = async (client) => {
|
|
1714
1783
|
try {
|
|
@@ -1738,8 +1807,8 @@ const registerCrashLogTools = (server, client, ctx) => {
|
|
|
1738
1807
|
const target = device ? await client.resolveTarget(device) : void 0;
|
|
1739
1808
|
const wanted = kinds ?? DEFAULT_CRASH_KINDS;
|
|
1740
1809
|
const needle = process?.toLowerCase();
|
|
1741
|
-
const
|
|
1742
|
-
const
|
|
1810
|
+
const [all, names] = await Promise.all([listSimulatorCrashReports(dir), target ? void 0 : simulatorNames(client)]);
|
|
1811
|
+
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
1812
|
return {
|
|
1744
1813
|
...target ? {
|
|
1745
1814
|
simulator: target.name,
|
|
@@ -1829,8 +1898,7 @@ const registerLogReadTools = (server, client) => {
|
|
|
1829
1898
|
};
|
|
1830
1899
|
}));
|
|
1831
1900
|
};
|
|
1832
|
-
|
|
1833
|
-
//#region src/tools/runner.ts
|
|
1901
|
+
// src/tools/runner.ts
|
|
1834
1902
|
const defaultSpawnRunner = async (command, args, { env, logPath }) => {
|
|
1835
1903
|
await mkdir(dirname(logPath), { recursive: true });
|
|
1836
1904
|
const log = await open(logPath, "a");
|
|
@@ -1852,9 +1920,22 @@ const defaultSpawnRunner = async (command, args, { env, logPath }) => {
|
|
|
1852
1920
|
};
|
|
1853
1921
|
const scriptPath = () => {
|
|
1854
1922
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1855
|
-
for (const candidate of [join(here, "..", "scripts", "wda.sh"), join(here, "..", "..", "scripts", "wda.sh")]) return candidate;
|
|
1923
|
+
for (const candidate of [join(here, "..", "scripts", "wda.sh"), join(here, "..", "..", "scripts", "wda.sh")]) if (existsSync(candidate)) return candidate;
|
|
1856
1924
|
throw new IosError$1("Could not find `scripts/wda.sh` beside this module.");
|
|
1857
1925
|
};
|
|
1926
|
+
/**
|
|
1927
|
+
* Whether `wda.sh run` has anything to run: the same directory and glob it
|
|
1928
|
+
* looks in. Without a build it exits with "no build found" into a log nobody
|
|
1929
|
+
* reads, while a detached spawn has already been reported as started.
|
|
1930
|
+
*/
|
|
1931
|
+
const runnerBuilt = async () => {
|
|
1932
|
+
const wdaDir = process.env["WDA_DIR"] || join(homedir(), ".cache", "mcp-ios-simulator", "WebDriverAgent");
|
|
1933
|
+
const products = join(wdaDir, ".build", "Build", "Products");
|
|
1934
|
+
return {
|
|
1935
|
+
built: (await readdir(products).catch(() => [])).some((name) => name.startsWith("WebDriverAgentRunner_iphonesimulator") && name.endsWith(".xctestrun")),
|
|
1936
|
+
products
|
|
1937
|
+
};
|
|
1938
|
+
};
|
|
1858
1939
|
const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpawnRunner) => {
|
|
1859
1940
|
server.registerTool("ios_simulator_restart_wda", {
|
|
1860
1941
|
title: "iOS Simulator: Restart WebDriverAgent",
|
|
@@ -1871,12 +1952,16 @@ const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpa
|
|
|
1871
1952
|
}, async ({ device, port }) => wrap(async () => {
|
|
1872
1953
|
const target = client.requireBooted(await client.resolveTarget(device));
|
|
1873
1954
|
const usePort = port ?? client.wdaPort;
|
|
1955
|
+
const { built, products } = await runnerBuilt();
|
|
1956
|
+
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
1957
|
const logPath = join(outputDir, "wda.log");
|
|
1875
1958
|
const pid = await spawnRunner("/bin/bash", [scriptPath(), "run"], {
|
|
1876
1959
|
env: {
|
|
1877
1960
|
IOS_SIMULATOR_ID: target.id,
|
|
1878
1961
|
IOS_SIMULATOR_WDA_PORT: String(usePort),
|
|
1879
|
-
USE_PORT: String(usePort)
|
|
1962
|
+
USE_PORT: String(usePort),
|
|
1963
|
+
NODE: process.execPath,
|
|
1964
|
+
PATH: `${dirname(process.execPath)}:${process.env["PATH"] ?? ""}`
|
|
1880
1965
|
},
|
|
1881
1966
|
logPath
|
|
1882
1967
|
});
|
|
@@ -1891,8 +1976,7 @@ const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpa
|
|
|
1891
1976
|
};
|
|
1892
1977
|
}));
|
|
1893
1978
|
};
|
|
1894
|
-
|
|
1895
|
-
//#region src/tools/index.ts
|
|
1979
|
+
// src/tools/index.ts
|
|
1896
1980
|
/**
|
|
1897
1981
|
* All capability decisions in one place, so "why can't I call that" is answered
|
|
1898
1982
|
* by one file.
|
|
@@ -1943,8 +2027,7 @@ const registerTools = (server, client, ctx, spawnRunner) => {
|
|
|
1943
2027
|
registerEnvironmentTools(server, client);
|
|
1944
2028
|
registerRunnerTools(server, client, ctx.config.outputDir, spawnRunner);
|
|
1945
2029
|
};
|
|
1946
|
-
|
|
1947
|
-
//#region src/server.ts
|
|
2030
|
+
// src/server.ts
|
|
1948
2031
|
const SERVER_NAME = BUILD_INFO.name;
|
|
1949
2032
|
const SERVER_VERSION = BUILD_INFO.version;
|
|
1950
2033
|
/**
|
|
@@ -1985,8 +2068,7 @@ const createServer = (opts) => {
|
|
|
1985
2068
|
client
|
|
1986
2069
|
};
|
|
1987
2070
|
};
|
|
1988
|
-
|
|
1989
|
-
//#region src/config.ts
|
|
2071
|
+
// src/config.ts
|
|
1990
2072
|
/**
|
|
1991
2073
|
* There are no credentials here, and unlike the device server there is barely
|
|
1992
2074
|
* any setup either. Two lanes reach a simulator and both are local:
|
|
@@ -2112,7 +2194,6 @@ const loadConfig = (env = process.env, configPath = defaultConfigPath(env)) => {
|
|
|
2112
2194
|
openPath: pick(trimmed(env.IOS_SIMULATOR_OPEN_PATH), "openPath")
|
|
2113
2195
|
});
|
|
2114
2196
|
};
|
|
2115
|
-
|
|
2116
|
-
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 };
|
|
2197
|
+
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$1 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
2198
|
|
|
2118
|
-
//# sourceMappingURL=config-
|
|
2199
|
+
//# sourceMappingURL=config-SdCcoXC3.js.map
|