@mgcrea/mcp-ios-simulator 0.4.1 → 0.4.2
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/README.md +12 -4
- package/dist/cli.js +1 -1
- package/dist/{config-SdCcoXC3.js → config-BA3aCrX9.js} +255 -73
- package/dist/config-BA3aCrX9.js.map +1 -0
- package/dist/index.d.ts +78 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/config-SdCcoXC3.js.map +0 -1
package/README.md
CHANGED
|
@@ -95,8 +95,8 @@ failure.
|
|
|
95
95
|
| `IOS_SIMULATOR_ID` | the only booted simulator | UDID, name, or `booted`. Two booted and no value is an error that names them. |
|
|
96
96
|
| `IOS_SIMULATOR_ALLOW_WRITES` | **on** | `0` drops the seventeen driving tools from `tools/list`. |
|
|
97
97
|
| `IOS_SIMULATOR_LAUNCH_ARGS` | none | Launch arguments applied when `launch` passes none of its own. |
|
|
98
|
-
| `IOS_SIMULATOR_WDA_PORT` | `8100` |
|
|
99
|
-
| `IOS_SIMULATOR_WDA_URL` |
|
|
98
|
+
| `IOS_SIMULATOR_WDA_PORT` | `8100` | Where `restart_wda` starts looking for a free port; the fallback runner port. |
|
|
99
|
+
| `IOS_SIMULATOR_WDA_URL` | none | Pins every simulator to one runner URL; rarely needed. |
|
|
100
100
|
| `IOS_SIMULATOR_MAX_TREE_BYTES` | `24000` | Byte cap on a `ui_tree` payload. |
|
|
101
101
|
| `IOS_SIMULATOR_TIMEOUT_MS` | `120000` | Budget for one `simctl` call; a boot is slow. |
|
|
102
102
|
| `IOS_SIMULATOR_WDA_TIMEOUT_MS` | `30000` | Budget for one WebDriverAgent call. |
|
|
@@ -137,6 +137,13 @@ scripts/wda.sh run # leave this running
|
|
|
137
137
|
scripts/wda.sh status # is it answering?
|
|
138
138
|
```
|
|
139
139
|
|
|
140
|
+
Several simulators can be driven at once, each with its own runner. The server
|
|
141
|
+
finds a simulator's runner by its port's owner, so no port needs configuring:
|
|
142
|
+
`ios_simulator_restart_wda {"device":"…"}` starts one on the first free port, and
|
|
143
|
+
from a shell `IOS_SIMULATOR_ID=<udid> USE_PORT=8101 scripts/wda.sh run` does the
|
|
144
|
+
same by hand. A call for a simulator whose runner is not up is refused rather
|
|
145
|
+
than sent to another simulator's.
|
|
146
|
+
|
|
140
147
|
Installed from npm, the same script is the `ios-simulator-wda` binary:
|
|
141
148
|
|
|
142
149
|
```bash
|
|
@@ -223,8 +230,9 @@ Xcode 27.0 (27A266a) with an iOS 27.0 runtime.
|
|
|
223
230
|
- **WebDriverAgent scans ports 8100-8199 when `USE_PORT` is unset.** With two
|
|
224
231
|
booted simulators, the second runner comes up healthy on 8101 and a server
|
|
225
232
|
pointed at 8100 drives the _first_ one while every call reports success.
|
|
226
|
-
`scripts/wda.sh` always pins `USE_PORT`, and
|
|
227
|
-
|
|
233
|
+
`scripts/wda.sh` always pins `USE_PORT`, and every UI call looks up the runner
|
|
234
|
+
it needs by its owner — `SIMULATOR_UDID` from `ps -Eww` on each listener
|
|
235
|
+
`lsof` finds in 8100-8199 — rather than trusting a fixed port.
|
|
228
236
|
- **`listapps` returns an old-style NeXTSTEP plist, not JSON.** `--json` is a flag
|
|
229
237
|
on `list` and nothing else.
|
|
230
238
|
- **A status bar override survives a reboot.** Clear it or it quietly pollutes
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { P as BUILD_INFO, a as createServer, n as loadConfig, t as defaultConfigPath } from "./config-BA3aCrX9.js";
|
|
3
3
|
import { ZodError } from "zod";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
5
5
|
// src/cli.ts
|
|
@@ -23,8 +23,8 @@ const pkg = readPackageJson();
|
|
|
23
23
|
const BUILD_INFO = {
|
|
24
24
|
name: pkg.name,
|
|
25
25
|
version: pkg.version,
|
|
26
|
-
gitCommit: "
|
|
27
|
-
gitCommitDate: "2026-11-
|
|
26
|
+
gitCommit: "07a8b77",
|
|
27
|
+
gitCommitDate: "2026-11-05T00:19:14+01:00"
|
|
28
28
|
};
|
|
29
29
|
// src/client/errors.ts
|
|
30
30
|
/** Raising the budget, or noticing the simulator died mid-call. */
|
|
@@ -55,6 +55,22 @@ const START_RUNNER_TOOL_REMEDY = "Start it with ios_simulator_restart_wda, which
|
|
|
55
55
|
const startRunnerRemedy = (allowWrites) => allowWrites ? START_RUNNER_TOOL_REMEDY : START_RUNNER_SCRIPT_REMEDY;
|
|
56
56
|
/** What to do when WebDriverAgent does not answer at all. */
|
|
57
57
|
const wdaUnavailableRemedy = (allowWrites) => `${startRunnerRemedy(allowWrites)} Everything except ios_simulator_ui_tree and the input tools works without it — screenshots go through simctl. If something else already forwards the port, set IOS_SIMULATOR_WDA_URL.`;
|
|
58
|
+
/**
|
|
59
|
+
* The target has no runner of its own, and the port this server would fall
|
|
60
|
+
* back to is another simulator's.
|
|
61
|
+
*
|
|
62
|
+
* Refused rather than forwarded, because the failure it prevents does not look
|
|
63
|
+
* like one: the other runner answers healthily, a tap lands on someone else's
|
|
64
|
+
* screen, and the UI tree that comes back describes an app the caller never
|
|
65
|
+
* launched.
|
|
66
|
+
*/
|
|
67
|
+
var RunnerBelongsElsewhereError = class extends IosError {
|
|
68
|
+
name = "RunnerBelongsElsewhereError";
|
|
69
|
+
constructor(opts) {
|
|
70
|
+
const { target, owner, port, freePort, allowWrites } = opts;
|
|
71
|
+
super(`No WebDriverAgent runner belongs to ${target.name}. Port ${port} is ${owner}'s runner, so a tap or UI tree read sent there would go to that simulator's screen instead.`, { remedy: allowWrites ? `Start one for this simulator with ios_simulator_restart_wda {"device":"${target.id}"} — it picks a free port (${freePort} right now) and this server finds it by itself. Give it about fifteen seconds.` : `Start one for this simulator on a free port: IOS_SIMULATOR_ID=${target.id} USE_PORT=${freePort} scripts/wda.sh run. This server finds it by itself once it answers.` });
|
|
72
|
+
}
|
|
73
|
+
};
|
|
58
74
|
/** No simulator matched, or several did and none was named. */
|
|
59
75
|
var SimulatorNotFoundError = class extends IosError {
|
|
60
76
|
name = "SimulatorNotFoundError";
|
|
@@ -249,6 +265,105 @@ var LaunchCaptures = class {
|
|
|
249
265
|
};
|
|
250
266
|
/** True when `path` exists on this Mac. */
|
|
251
267
|
const exists = async (path) => stat(path).then(() => true, () => false);
|
|
268
|
+
// src/client/runners.ts
|
|
269
|
+
/** WebDriverAgent's own scan range when `USE_PORT` is unset (FBConfiguration.m). */
|
|
270
|
+
const WDA_DEFAULT_PORT = 8100;
|
|
271
|
+
/**
|
|
272
|
+
* The ranges worth scanning: WebDriverAgent's default one, and the one this
|
|
273
|
+
* server allocates from when IOS_SIMULATOR_WDA_PORT moves it elsewhere.
|
|
274
|
+
*/
|
|
275
|
+
const runnerRanges = (basePort) => {
|
|
276
|
+
const ranges = [[WDA_DEFAULT_PORT, 8199]];
|
|
277
|
+
if (basePort < 8100 || basePort >= 8200) ranges.push([basePort, Math.min(65535, basePort + 100 - 1)]);
|
|
278
|
+
return ranges;
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* `lsof -F pn` output: a `p<pid>` line opens each process, then one `n<addr>`
|
|
282
|
+
* line per socket. A runner listening on both stacks shows up twice.
|
|
283
|
+
*/
|
|
284
|
+
const parseLsof = (stdout) => {
|
|
285
|
+
const seen = /* @__PURE__ */ new Set();
|
|
286
|
+
const out = [];
|
|
287
|
+
let pid;
|
|
288
|
+
for (const line of stdout.split("\n")) if (line.startsWith("p")) {
|
|
289
|
+
const value = Number(line.slice(1));
|
|
290
|
+
pid = Number.isInteger(value) ? value : void 0;
|
|
291
|
+
} else if (line.startsWith("n") && pid !== void 0) {
|
|
292
|
+
const port = Number(/:(\d+)$/.exec(line)?.[1]);
|
|
293
|
+
if (!Number.isInteger(port)) continue;
|
|
294
|
+
const key = `${pid}:${port}`;
|
|
295
|
+
if (seen.has(key)) continue;
|
|
296
|
+
seen.add(key);
|
|
297
|
+
out.push({
|
|
298
|
+
pid,
|
|
299
|
+
port
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* `ps -Eww -o pid=,command=` output, one process per line with its environment
|
|
306
|
+
* appended. Every process inside a simulator carries `SIMULATOR_UDID`.
|
|
307
|
+
*/
|
|
308
|
+
const parsePsUdids = (stdout) => {
|
|
309
|
+
const out = /* @__PURE__ */ new Map();
|
|
310
|
+
for (const line of stdout.split("\n")) {
|
|
311
|
+
const pid = Number(/^\s*(\d+)\s/.exec(line)?.[1]);
|
|
312
|
+
const udid = /SIMULATOR_UDID=([0-9A-Fa-f-]{36})/.exec(line)?.[1];
|
|
313
|
+
if (Number.isInteger(pid) && udid) out.set(pid, udid);
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
};
|
|
317
|
+
/**
|
|
318
|
+
* Every listener in the runner ranges, each attributed to its simulator.
|
|
319
|
+
*
|
|
320
|
+
* A simulator shares the host's network stack, so a runner's port is a host
|
|
321
|
+
* port and nothing in WebDriverAgent's `/status` names the simulator behind it
|
|
322
|
+
* — the process environment is the only reliable owner. Failure is an empty
|
|
323
|
+
* list rather than an error: lsof exits 1 when nothing matches, and not knowing
|
|
324
|
+
* who owns a port must degrade to the old fixed-port behaviour, not break
|
|
325
|
+
* every UI call.
|
|
326
|
+
*/
|
|
327
|
+
const scanRunners = async (exec, basePort, timeoutMs = 5e3) => {
|
|
328
|
+
let sockets;
|
|
329
|
+
try {
|
|
330
|
+
const args = [
|
|
331
|
+
"-nP",
|
|
332
|
+
"-sTCP:LISTEN",
|
|
333
|
+
"-F",
|
|
334
|
+
"pn"
|
|
335
|
+
];
|
|
336
|
+
for (const [from, to] of runnerRanges(basePort)) args.push(`-iTCP:${from}-${to}`);
|
|
337
|
+
sockets = parseLsof((await exec("/usr/sbin/lsof", args, timeoutMs)).stdout);
|
|
338
|
+
} catch {
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
if (sockets.length === 0) return [];
|
|
342
|
+
let udids = /* @__PURE__ */ new Map();
|
|
343
|
+
try {
|
|
344
|
+
const pids = [...new Set(sockets.map((s) => s.pid))].join(",");
|
|
345
|
+
udids = parsePsUdids((await exec("/bin/ps", [
|
|
346
|
+
"-Eww",
|
|
347
|
+
"-o",
|
|
348
|
+
"pid=,command=",
|
|
349
|
+
"-p",
|
|
350
|
+
pids
|
|
351
|
+
], timeoutMs)).stdout);
|
|
352
|
+
} catch {}
|
|
353
|
+
return sockets.map((s) => {
|
|
354
|
+
const udid = udids.get(s.pid);
|
|
355
|
+
return udid ? {
|
|
356
|
+
...s,
|
|
357
|
+
udid
|
|
358
|
+
} : s;
|
|
359
|
+
}).toSorted((a, b) => a.port - b.port);
|
|
360
|
+
};
|
|
361
|
+
/** The first port from `basePort` up that nothing listens on. */
|
|
362
|
+
const freePort = (listeners, basePort) => {
|
|
363
|
+
const taken = new Set(listeners.map((l) => l.port));
|
|
364
|
+
for (let port = basePort; port < basePort + 100 && port <= 65535; port++) if (!taken.has(port)) return port;
|
|
365
|
+
return basePort;
|
|
366
|
+
};
|
|
252
367
|
// src/client/shape.ts
|
|
253
368
|
/**
|
|
254
369
|
* Flatten the runtime-keyed map `list devices` returns.
|
|
@@ -771,6 +886,7 @@ var SimulatorClient = class {
|
|
|
771
886
|
wdaClients = /* @__PURE__ */ new Map();
|
|
772
887
|
/** `simctl list` is ~80ms and most tools want it twice; 2s collapses that. */
|
|
773
888
|
cache;
|
|
889
|
+
runnerCache;
|
|
774
890
|
profiles = /* @__PURE__ */ new Map();
|
|
775
891
|
constructor(opts) {
|
|
776
892
|
this.opts = opts;
|
|
@@ -929,15 +1045,10 @@ var SimulatorClient = class {
|
|
|
929
1045
|
return this.simctl.screenshotPng(target.id);
|
|
930
1046
|
}
|
|
931
1047
|
/**
|
|
932
|
-
* Where WebDriverAgent is
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
* port. That is also the trap — several booted simulators contend for the
|
|
937
|
-
* same one, and WebDriverAgent scans 8100-8199 when `USE_PORT` is unset, so a
|
|
938
|
-
* second runner comes up healthy on 8101 and a server pointed at 8100 drives
|
|
939
|
-
* the first simulator while reporting success. `ios_simulator_diagnostics`
|
|
940
|
-
* reports which simulator actually owns the port.
|
|
1048
|
+
* Where WebDriverAgent is *configured* to be: IOS_SIMULATOR_WDA_URL, or the
|
|
1049
|
+
* base port on loopback. Which runner a call actually reaches is decided per
|
|
1050
|
+
* simulator by `runnerUrl`; this is only the fallback when nothing better is
|
|
1051
|
+
* known.
|
|
941
1052
|
*/
|
|
942
1053
|
wdaUrl() {
|
|
943
1054
|
return this.opts.wdaUrl ?? `http://127.0.0.1:${this.opts.wdaPort}`;
|
|
@@ -946,24 +1057,110 @@ var SimulatorClient = class {
|
|
|
946
1057
|
get defaultSimulatorId() {
|
|
947
1058
|
return this.opts.defaultSimulatorId;
|
|
948
1059
|
}
|
|
1060
|
+
/** Where runner ports are allocated from, and the fallback when no owner is known. */
|
|
949
1061
|
get wdaPort() {
|
|
950
1062
|
return this.opts.wdaPort;
|
|
951
1063
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
1064
|
+
/**
|
|
1065
|
+
* Every listener in the runner ranges, attributed to its simulator. Cached
|
|
1066
|
+
* for 2s: every UI call asks, and lsof plus ps costs tens of milliseconds.
|
|
1067
|
+
*/
|
|
1068
|
+
async listRunners(opts = {}) {
|
|
1069
|
+
const cached = this.runnerCache;
|
|
1070
|
+
if (!opts.fresh && cached && Date.now() - cached.at < 2e3) return cached.listeners;
|
|
1071
|
+
const listeners = await scanRunners(this.exec, this.opts.wdaPort);
|
|
1072
|
+
this.runnerCache = {
|
|
1073
|
+
at: Date.now(),
|
|
1074
|
+
listeners
|
|
1075
|
+
};
|
|
1076
|
+
return listeners;
|
|
1077
|
+
}
|
|
1078
|
+
/** Called once a runner is started, so the next call looks again. */
|
|
1079
|
+
forgetRunners() {
|
|
1080
|
+
this.runnerCache = void 0;
|
|
1081
|
+
}
|
|
1082
|
+
/** The first port nothing listens on, from the base port up. */
|
|
1083
|
+
async freeRunnerPort() {
|
|
1084
|
+
return freePort(await this.listRunners({ fresh: true }), this.opts.wdaPort);
|
|
1085
|
+
}
|
|
1086
|
+
/** The port of this simulator's own runner, when it has one. */
|
|
1087
|
+
async ownRunnerPort(target) {
|
|
1088
|
+
const id = target.id.toLowerCase();
|
|
1089
|
+
return (await this.listRunners({ fresh: true })).find((l) => l.udid?.toLowerCase() === id)?.port;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Which runner a call for this simulator goes to.
|
|
1093
|
+
*
|
|
1094
|
+
* A simulator shares the host's network stack, so every runner is a host
|
|
1095
|
+
* loopback port, and one server process — Bastion runs one for every session
|
|
1096
|
+
* on the Mac — sees all of them. A fixed port therefore meant one simulator
|
|
1097
|
+
* per process, and worse, a second simulator's calls went to the first one's
|
|
1098
|
+
* runner while every tool reported success.
|
|
1099
|
+
*
|
|
1100
|
+
* So the owner is looked up rather than assumed: the simulator's own runner
|
|
1101
|
+
* wherever it listens; failing that the configured URL, *unless* that is
|
|
1102
|
+
* known to be another simulator's, which is refused. Not knowing the owner
|
|
1103
|
+
* (lsof saw nothing) keeps the configured URL, which is the behaviour from
|
|
1104
|
+
* before ownership was checked.
|
|
1105
|
+
*/
|
|
1106
|
+
async runnerUrl(target) {
|
|
1107
|
+
const listeners = await this.listRunners();
|
|
1108
|
+
const id = target.id.toLowerCase();
|
|
1109
|
+
const own = listeners.find((l) => l.udid?.toLowerCase() === id);
|
|
1110
|
+
if (!this.opts.wdaUrl && own) return `http://127.0.0.1:${own.port}`;
|
|
1111
|
+
const configured = this.wdaUrl();
|
|
1112
|
+
const port = loopbackPort(configured);
|
|
1113
|
+
const owner = port === void 0 ? void 0 : listeners.find((l) => l.port === port && l.udid)?.udid;
|
|
1114
|
+
if (owner && owner.toLowerCase() !== id) throw new RunnerBelongsElsewhereError({
|
|
1115
|
+
target,
|
|
1116
|
+
owner: (await this.listSimulators()).find((sim) => sim.id.toLowerCase() === owner.toLowerCase())?.name ?? owner,
|
|
1117
|
+
port,
|
|
1118
|
+
freePort: freePort(listeners, this.opts.wdaPort),
|
|
1119
|
+
allowWrites: this.opts.allowWrites !== false
|
|
1120
|
+
});
|
|
1121
|
+
return configured;
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* The WebDriverAgent client for one simulator, one per UDID so each keeps its
|
|
1125
|
+
* own session. The URL is resolved on every request, so a runner restarted on
|
|
1126
|
+
* another port is followed without a server restart.
|
|
1127
|
+
*/
|
|
1128
|
+
wda(target) {
|
|
1129
|
+
return this.wdaClient(target.id, () => this.runnerUrl(target));
|
|
1130
|
+
}
|
|
1131
|
+
/** The configured URL, unchecked — for diagnostics with no single target. */
|
|
1132
|
+
wdaConfigured() {
|
|
1133
|
+
return this.wdaClient("", async () => this.wdaUrl());
|
|
1134
|
+
}
|
|
1135
|
+
wdaClient(key, baseUrl) {
|
|
1136
|
+
const existing = this.wdaClients.get(key);
|
|
955
1137
|
if (existing) return existing;
|
|
956
1138
|
const client = new WdaClient({
|
|
957
|
-
baseUrl
|
|
1139
|
+
baseUrl,
|
|
958
1140
|
timeoutMs: this.opts.wdaTimeoutMs,
|
|
959
1141
|
remedies: simulatorWdaRemedies(this.opts.allowWrites !== false),
|
|
960
1142
|
...this.opts.fetch ? { fetch: this.opts.fetch } : {},
|
|
961
1143
|
...this.opts.logger ? { logger: this.opts.logger } : {}
|
|
962
1144
|
});
|
|
963
|
-
this.wdaClients.set(
|
|
1145
|
+
this.wdaClients.set(key, client);
|
|
964
1146
|
return client;
|
|
965
1147
|
}
|
|
966
1148
|
};
|
|
1149
|
+
/** The port of a loopback URL, or undefined for anything else. */
|
|
1150
|
+
const loopbackPort = (url) => {
|
|
1151
|
+
try {
|
|
1152
|
+
const parsed = new URL(url);
|
|
1153
|
+
if (![
|
|
1154
|
+
"127.0.0.1",
|
|
1155
|
+
"localhost",
|
|
1156
|
+
"[::1]"
|
|
1157
|
+
].includes(parsed.hostname)) return void 0;
|
|
1158
|
+
const port = Number(parsed.port || (parsed.protocol === "https:" ? 443 : 80));
|
|
1159
|
+
return Number.isInteger(port) ? port : void 0;
|
|
1160
|
+
} catch {
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
967
1164
|
// src/tools/util.ts
|
|
968
1165
|
/**
|
|
969
1166
|
* How the shared tools name themselves and each other in this server.
|
|
@@ -1182,40 +1379,6 @@ const registerAppTools = (server, client, launchArgs, outputDir) => {
|
|
|
1182
1379
|
};
|
|
1183
1380
|
// src/tools/diagnostics.ts
|
|
1184
1381
|
/**
|
|
1185
|
-
* Which simulator owns the WebDriverAgent port.
|
|
1186
|
-
*
|
|
1187
|
-
* This check has no device-server equivalent and it is the most valuable one
|
|
1188
|
-
* here. A simulator shares the host's network stack, so a runner's port is a
|
|
1189
|
-
* *host* port, and WebDriverAgent scans 8100-8199 when `USE_PORT` is unset. Two
|
|
1190
|
-
* booted simulators therefore give you a second runner answering healthily on
|
|
1191
|
-
* 8101 while a server pointed at 8100 drives the first one and reports success
|
|
1192
|
-
* on every call.
|
|
1193
|
-
*
|
|
1194
|
-
* Every process inside a simulator carries `SIMULATOR_UDID` in its environment,
|
|
1195
|
-
* and `ps -Eww` prints it, so the port's real owner is knowable rather than
|
|
1196
|
-
* assumed.
|
|
1197
|
-
*/
|
|
1198
|
-
const portOwner = async (client, port) => {
|
|
1199
|
-
try {
|
|
1200
|
-
const { stdout: pids } = await client.exec("/usr/sbin/lsof", [
|
|
1201
|
-
"-nP",
|
|
1202
|
-
`-iTCP:${port}`,
|
|
1203
|
-
"-sTCP:LISTEN",
|
|
1204
|
-
"-t"
|
|
1205
|
-
], 5e3);
|
|
1206
|
-
const pid = pids.trim().split("\n")[0];
|
|
1207
|
-
if (!pid) return void 0;
|
|
1208
|
-
const { stdout: env } = await client.exec("/bin/ps", [
|
|
1209
|
-
"-Eww",
|
|
1210
|
-
"-p",
|
|
1211
|
-
pid
|
|
1212
|
-
], 5e3);
|
|
1213
|
-
return /SIMULATOR_UDID=([0-9A-Fa-f-]{36})/.exec(env)?.[1];
|
|
1214
|
-
} catch {
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
};
|
|
1218
|
-
/**
|
|
1219
1382
|
* The whole point of this tool is that **it never throws**. Every other tool
|
|
1220
1383
|
* fails when nothing is booted, the runtime is missing or the runner is not up,
|
|
1221
1384
|
* and each of those looks much like the others from the outside. This one
|
|
@@ -1278,31 +1441,41 @@ const diagnose = async (client, ctx, deviceHint) => {
|
|
|
1278
1441
|
if (target) try {
|
|
1279
1442
|
display = await client.display(target);
|
|
1280
1443
|
} catch {}
|
|
1444
|
+
const listeners = await client.listRunners({ fresh: true });
|
|
1445
|
+
const names = new Map(simulators.map((sim) => [sim.id.toLowerCase(), sim.name]));
|
|
1446
|
+
const runners = listeners.flatMap((l) => l.udid ? [{
|
|
1447
|
+
udid: l.udid,
|
|
1448
|
+
name: names.get(l.udid.toLowerCase()) ?? l.udid,
|
|
1449
|
+
port: l.port
|
|
1450
|
+
}] : []);
|
|
1451
|
+
let url = client.wdaUrl();
|
|
1452
|
+
let refused = false;
|
|
1453
|
+
if (target) try {
|
|
1454
|
+
url = await client.runnerUrl(target);
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
refused = true;
|
|
1457
|
+
problems.push(err instanceof Error ? err.message : String(err));
|
|
1458
|
+
const remedy = err.remedy;
|
|
1459
|
+
if (remedy) nextSteps.push(remedy);
|
|
1460
|
+
}
|
|
1461
|
+
const port = portOf(url) ?? client.wdaPort;
|
|
1281
1462
|
const wda = {
|
|
1282
|
-
url
|
|
1283
|
-
port
|
|
1284
|
-
reachable: false
|
|
1463
|
+
url,
|
|
1464
|
+
port,
|
|
1465
|
+
reachable: false,
|
|
1466
|
+
runners
|
|
1285
1467
|
};
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1468
|
+
const owner = listeners.find((l) => l.port === port && l.udid)?.udid;
|
|
1469
|
+
if (owner) wda.boundTo = owner;
|
|
1470
|
+
if (refused) wda.error = "Not probed: the runner on this port belongs to another simulator.";
|
|
1471
|
+
else try {
|
|
1472
|
+
wda.state = await (target ? client.wda(target) : client.wdaConfigured()).status();
|
|
1291
1473
|
wda.reachable = true;
|
|
1292
1474
|
} catch (err) {
|
|
1293
1475
|
wda.error = err instanceof Error ? err.message : String(err);
|
|
1294
1476
|
problems.push("WebDriverAgent is not answering, so the UI tree and the input tools will fail.");
|
|
1295
1477
|
nextSteps.push((nothingBooted ? "Then, once it is booted: " : "") + startRunnerRemedy(ctx.allowWrites) + " Everything else — including screenshots — works without it.");
|
|
1296
1478
|
}
|
|
1297
|
-
if (wda.reachable) {
|
|
1298
|
-
const owner = await portOwner(client, client.wdaPort);
|
|
1299
|
-
if (owner) wda.boundTo = owner;
|
|
1300
|
-
if (owner && target && owner.toLowerCase() !== target.id.toLowerCase()) {
|
|
1301
|
-
const other = simulators.find((sim) => sim.id.toLowerCase() === owner.toLowerCase());
|
|
1302
|
-
problems.push(`Port ${client.wdaPort} is owned by ${other?.name ?? owner}, not ${target.name}. Every tap and every UI tree read would go to the wrong screen.`);
|
|
1303
|
-
nextSteps.push("WebDriverAgent scans 8100-8199 when USE_PORT is unset, so a second simulator's runner takes the next free port and nothing looks wrong. Give this one its own port with IOS_SIMULATOR_WDA_PORT and start its runner with a matching USE_PORT.");
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
1479
|
if (unavailable.length > 0) nextSteps.push(`${unavailable.length} simulators have no installed runtime and cannot be used at all. \`xcrun simctl delete unavailable\` clears them.`);
|
|
1307
1480
|
return {
|
|
1308
1481
|
ok: problems.length === 0,
|
|
@@ -1321,10 +1494,18 @@ const diagnose = async (client, ctx, deviceHint) => {
|
|
|
1321
1494
|
nextSteps
|
|
1322
1495
|
};
|
|
1323
1496
|
};
|
|
1497
|
+
const portOf = (url) => {
|
|
1498
|
+
try {
|
|
1499
|
+
const port = Number(new URL(url).port);
|
|
1500
|
+
return port > 0 ? port : void 0;
|
|
1501
|
+
} catch {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1324
1505
|
const registerDiagnosticsTools = (server, client, ctx) => {
|
|
1325
1506
|
server.registerTool("ios_simulator_diagnostics", {
|
|
1326
1507
|
title: "iOS Simulator: Diagnostics",
|
|
1327
|
-
description: "Report both lanes separately and never fail: which simulators exist and which are usable, which one this server would drive, its screen geometry, and whether WebDriverAgent is answering — including
|
|
1508
|
+
description: "Report both lanes separately and never fail: which simulators exist and which are usable, which one this server would drive, its screen geometry, and whether WebDriverAgent is answering — including every simulator's runner and its port, and which one the target's taps would reach. Start here when anything else misbehaves.",
|
|
1328
1509
|
inputSchema: z.object({ device: deviceArg }),
|
|
1329
1510
|
annotations: { readOnlyHint: true }
|
|
1330
1511
|
}, async ({ device }) => wrap(async () => diagnose(client, ctx, device)));
|
|
@@ -1939,10 +2120,10 @@ const runnerBuilt = async () => {
|
|
|
1939
2120
|
const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpawnRunner) => {
|
|
1940
2121
|
server.registerTool("ios_simulator_restart_wda", {
|
|
1941
2122
|
title: "iOS Simulator: Restart WebDriverAgent",
|
|
1942
|
-
description: "Start the WebDriverAgent runner, or restart it after it has died. Only the UI tree and the input tools need it — screenshots and everything simctl does keep working without it. The runner is started detached so it outlives this conversation, and its output goes to a log file rather than to you. It does not wait: give it fifteen seconds or so, then check ios_simulator_diagnostics.",
|
|
2123
|
+
description: "Start the WebDriverAgent runner, or restart it after it has died. Only the UI tree and the input tools need it — screenshots and everything simctl does keep working without it. Each simulator gets its own runner on its own port, so several can be driven at once. The runner is started detached so it outlives this conversation, and its output goes to a log file rather than to you. It does not wait: give it fifteen seconds or so, then check ios_simulator_diagnostics.",
|
|
1943
2124
|
inputSchema: z.object({
|
|
1944
2125
|
device: deviceArg,
|
|
1945
|
-
port: z.number().int().min(1).max(65535).optional().describe("Port to bind, passed to the runner as USE_PORT.
|
|
2126
|
+
port: z.number().int().min(1).max(65535).optional().describe("Port to bind, passed to the runner as USE_PORT. Leave it out: the simulator's current runner port is reused, or else the first free one from IOS_SIMULATOR_WDA_PORT up, and the other tools find the runner wherever it listens.")
|
|
1946
2127
|
}),
|
|
1947
2128
|
annotations: {
|
|
1948
2129
|
readOnlyHint: false,
|
|
@@ -1951,7 +2132,7 @@ const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpa
|
|
|
1951
2132
|
}
|
|
1952
2133
|
}, async ({ device, port }) => wrap(async () => {
|
|
1953
2134
|
const target = client.requireBooted(await client.resolveTarget(device));
|
|
1954
|
-
const usePort = port ?? client.
|
|
2135
|
+
const usePort = port ?? await client.ownRunnerPort(target) ?? await client.freeRunnerPort();
|
|
1955
2136
|
const { built, products } = await runnerBuilt();
|
|
1956
2137
|
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." });
|
|
1957
2138
|
const logPath = join(outputDir, "wda.log");
|
|
@@ -1965,6 +2146,7 @@ const registerRunnerTools = (server, client, outputDir, spawnRunner = defaultSpa
|
|
|
1965
2146
|
},
|
|
1966
2147
|
logPath
|
|
1967
2148
|
});
|
|
2149
|
+
client.forgetRunners();
|
|
1968
2150
|
if (pid === void 0) throw new IosError$1("Could not start the runner — the process had no pid.", { remedy: START_RUNNER_SCRIPT_REMEDY });
|
|
1969
2151
|
return {
|
|
1970
2152
|
started: true,
|
|
@@ -2194,6 +2376,6 @@ const loadConfig = (env = process.env, configPath = defaultConfigPath(env)) => {
|
|
|
2194
2376
|
openPath: pick(trimmed(env.IOS_SIMULATOR_OPEN_PATH), "openPath")
|
|
2195
2377
|
});
|
|
2196
2378
|
};
|
|
2197
|
-
export {
|
|
2379
|
+
export { WdaUnavailableError as A, START_RUNNER_TOOL_REMEDY as C, TOOLCHAIN_REMEDY as D, TIMEOUT_REMEDY as E, startRunnerRemedy as M, wdaUnavailableRemedy as N, ToolchainError as O, BUILD_INFO as P, START_RUNNER_SCRIPT_REMEDY as S, SimulatorNotFoundError as T, CommandTimeoutError as _, createServer as a, RunnerBelongsElsewhereError as b, assertNotBulkTarget as c, summarizeDevices as d, toListRow as f, CommandError$1 as g, toDisplayInfo as h, SERVER_VERSION as i, WritesDisabledError as j, WdaError 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, SimulatorNotBootedError as w, RuntimeUnavailableError as x, IosError$1 as y };
|
|
2198
2380
|
|
|
2199
|
-
//# sourceMappingURL=config-
|
|
2381
|
+
//# sourceMappingURL=config-BA3aCrX9.js.map
|