mobai-mcp 2.2.0 → 2.2.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/README.md +1 -1
- package/dist/index.js +194 -36
- package/dist/resources.js +166 -0
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ Tests are `.mob` files on disk inside project directories. You read, write, and
|
|
|
107
107
|
|---|---|
|
|
108
108
|
| `test_get_active` | Get the active test project directory and its `.mob` cases |
|
|
109
109
|
| `test_list_projects` | List all known test project directories with their `.mob` cases |
|
|
110
|
-
| `test_run` | Run a `.mob` test case on a device (`project_dir` + `case_path` + `device_id`) |
|
|
110
|
+
| `test_run` | Run a `.mob` test case on a device (`project_dir` + `case_path` + `device_id`, optional `params` for `${name}` substitution) |
|
|
111
111
|
|
|
112
112
|
## Resources
|
|
113
113
|
|
package/dist/index.js
CHANGED
|
@@ -22,15 +22,6 @@ function ensureScreenshotDir() {
|
|
|
22
22
|
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
function saveBase64ToTemp(base64Data, prefix) {
|
|
26
|
-
if (!base64Data || base64Data.length <= 200)
|
|
27
|
-
return null;
|
|
28
|
-
ensureScreenshotDir();
|
|
29
|
-
const filename = `${prefix}_${Date.now()}.png`;
|
|
30
|
-
const filePath = path.join(SCREENSHOT_DIR, filename);
|
|
31
|
-
fs.writeFileSync(filePath, Buffer.from(base64Data, "base64"));
|
|
32
|
-
return filePath;
|
|
33
|
-
}
|
|
34
25
|
function screenshotToFile(body) {
|
|
35
26
|
if (body?.path) {
|
|
36
27
|
return `Screenshot saved to ${body.path}`;
|
|
@@ -47,28 +38,6 @@ function screenshotToFile(body) {
|
|
|
47
38
|
}
|
|
48
39
|
return JSON.stringify(body, null, 2);
|
|
49
40
|
}
|
|
50
|
-
function extractDSLScreenshots(body) {
|
|
51
|
-
if (!body?.step_results)
|
|
52
|
-
return body;
|
|
53
|
-
for (const step of body.step_results) {
|
|
54
|
-
const native = step.result?.observations?.native;
|
|
55
|
-
if (native?.screenshot && typeof native.screenshot === "string" && native.screenshot.length > 200) {
|
|
56
|
-
const filePath = saveBase64ToTemp(native.screenshot, "observe");
|
|
57
|
-
if (filePath) {
|
|
58
|
-
native.screenshot = filePath;
|
|
59
|
-
native.screenshot_saved = true;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (step.debug?.screenshot && typeof step.debug.screenshot === "string" && step.debug.screenshot.length > 200) {
|
|
63
|
-
const filePath = saveBase64ToTemp(step.debug.screenshot, "debug");
|
|
64
|
-
if (filePath) {
|
|
65
|
-
step.debug.screenshot = filePath;
|
|
66
|
-
step.debug.screenshot_saved = true;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return body;
|
|
71
|
-
}
|
|
72
41
|
// ---------------------------------------------------------------------------
|
|
73
42
|
// HTTP helpers
|
|
74
43
|
// ---------------------------------------------------------------------------
|
|
@@ -127,6 +96,7 @@ const server = new Server({ name: "mobai", version: "1.0.0" }, {
|
|
|
127
96
|
instructions: `MobAI controls Android and iOS devices. Before starting any device task, read the relevant MCP resources:
|
|
128
97
|
- mobai://reference/device-automation — how to control devices (read before ANY device interaction)
|
|
129
98
|
- mobai://reference/testing — .mob script syntax (read ONLY when user asks to create or fix test scripts)
|
|
99
|
+
- mobai://reference/debugging — how to attach lldb, set breakpoints, inspect state (read before ANY debug_* tool)
|
|
130
100
|
Check available skills in current work directory and load any relevant to the user's request.`,
|
|
131
101
|
});
|
|
132
102
|
// ---------------------------------------------------------------------------
|
|
@@ -272,17 +242,106 @@ Input: JSON string with "version": "0.2" and "steps" array. Example:
|
|
|
272
242
|
},
|
|
273
243
|
{
|
|
274
244
|
name: "test_run",
|
|
275
|
-
description: "Run a .mob test case on a device. The case_path is relative to the project directory.",
|
|
245
|
+
description: "Run a .mob test case on a device. The case_path is relative to the project directory. Pass params to supply values for ${name} substitution in the script.",
|
|
276
246
|
inputSchema: {
|
|
277
247
|
type: "object",
|
|
278
248
|
properties: {
|
|
279
249
|
project_dir: { type: "string", description: "Absolute path to the project directory" },
|
|
280
250
|
case_path: { type: "string", description: "Relative path to the .mob file within the project, e.g. auth/login.mob" },
|
|
281
251
|
device_id: { type: "string", description: "Device ID to run the test on" },
|
|
252
|
+
params: { type: "object", additionalProperties: { type: "string" }, description: "Optional key-value parameters for ${name} substitution in the script" },
|
|
282
253
|
},
|
|
283
254
|
required: ["project_dir", "case_path", "device_id"],
|
|
284
255
|
},
|
|
285
256
|
},
|
|
257
|
+
// Live app debugging via lldb-dap. iOS only. Read mobai://reference/debugging
|
|
258
|
+
// before using any of these.
|
|
259
|
+
{
|
|
260
|
+
name: "debug_attach",
|
|
261
|
+
description: "Start a debug session for an iOS app. Provide either bundle_id (launches and attaches) or pid (attaches to a running process). Optional breakpoints[] are armed before the target resumes. Read mobai://reference/debugging first.",
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
device_id: { type: "string", description: "Device ID" },
|
|
266
|
+
bundle_id: { type: "string", description: "App bundle ID to launch and attach. Either this or pid is required." },
|
|
267
|
+
pid: { type: "number", description: "Attach to an already-running PID. Either this or bundle_id is required." },
|
|
268
|
+
breakpoints: {
|
|
269
|
+
type: "array",
|
|
270
|
+
items: { type: "string" },
|
|
271
|
+
description: `Initial breakpoint specs. "File.swift:42" (preferred), "Module.Type.method" (no parameter signature), "-[Class method:]", or runtime symbol.`,
|
|
272
|
+
},
|
|
273
|
+
stop_on_entry: { type: "boolean", description: "Simulator only — pause at first instruction." },
|
|
274
|
+
},
|
|
275
|
+
required: ["device_id"],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "debug_state",
|
|
280
|
+
description: "Query the current debug session. Returns {state, breakpoints} by default. Set include_stack=true to also fetch the stack of the stopped thread; include_vars=true to also fetch frame[0] locals; include_threads=true to enumerate all threads.",
|
|
281
|
+
inputSchema: {
|
|
282
|
+
type: "object",
|
|
283
|
+
properties: {
|
|
284
|
+
device_id: { type: "string", description: "Device ID" },
|
|
285
|
+
include_stack: { type: "boolean", description: "Include stack of stopped thread." },
|
|
286
|
+
include_vars: { type: "boolean", description: "Include frame[0] locals." },
|
|
287
|
+
include_threads: { type: "boolean", description: "Include all threads." },
|
|
288
|
+
},
|
|
289
|
+
required: ["device_id"],
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: "debug_breakpoint",
|
|
294
|
+
description: "Add or remove a breakpoint in the active debug session. For action=add provide spec; for action=remove provide id.",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: "object",
|
|
297
|
+
properties: {
|
|
298
|
+
device_id: { type: "string", description: "Device ID" },
|
|
299
|
+
action: { type: "string", enum: ["add", "remove"], description: `"add" or "remove"` },
|
|
300
|
+
spec: { type: "string", description: `Breakpoint spec for action=add. "File.swift:42", "Module.Type.method", "-[Class method:]", or runtime symbol.` },
|
|
301
|
+
id: { type: "number", description: "Breakpoint id for action=remove." },
|
|
302
|
+
},
|
|
303
|
+
required: ["device_id", "action"],
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
name: "debug_eval",
|
|
308
|
+
description: `Evaluate a Swift/ObjC expression at the current pause. Session must be paused. Examples: "p defaultPrivate", "po self.viewModel.user.email", "frame variable".`,
|
|
309
|
+
inputSchema: {
|
|
310
|
+
type: "object",
|
|
311
|
+
properties: {
|
|
312
|
+
device_id: { type: "string", description: "Device ID" },
|
|
313
|
+
expression: { type: "string", description: "Expression to evaluate" },
|
|
314
|
+
frame_id: { type: "number", description: "Optional frame id to evaluate in" },
|
|
315
|
+
},
|
|
316
|
+
required: ["device_id", "expression"],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "debug_step",
|
|
321
|
+
description: `Advance the target.\n "in" — step into next call (blocks ~ms, returns {state, breakpoints, stack, frame0_locals})\n "over" — step over next call (same shape)\n "out" — run until current frame returns (same shape)\n "continue" — resume until next breakpoint (fire-and-forget; returns just {state, breakpoints} — poll debug_state for next stop)`,
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
device_id: { type: "string", description: "Device ID" },
|
|
326
|
+
direction: { type: "string", enum: ["in", "over", "out", "continue"], description: `"in" | "over" | "out" | "continue"` },
|
|
327
|
+
include_stack: { type: "boolean", description: `Include the new stack. Default true. Ignored for direction="continue".` },
|
|
328
|
+
include_vars: { type: "boolean", description: `Include the new frame[0] locals. Default true. Ignored for direction="continue".` },
|
|
329
|
+
},
|
|
330
|
+
required: ["device_id", "direction"],
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "debug_detach",
|
|
335
|
+
description: "End the debug session. Pass kill=true to terminate the debuggee; otherwise it keeps running.",
|
|
336
|
+
inputSchema: {
|
|
337
|
+
type: "object",
|
|
338
|
+
properties: {
|
|
339
|
+
device_id: { type: "string", description: "Device ID" },
|
|
340
|
+
kill: { type: "boolean", description: "Terminate debuggee on detach." },
|
|
341
|
+
},
|
|
342
|
+
required: ["device_id"],
|
|
343
|
+
},
|
|
344
|
+
},
|
|
286
345
|
];
|
|
287
346
|
// ---------------------------------------------------------------------------
|
|
288
347
|
// List tools
|
|
@@ -347,19 +406,118 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
347
406
|
throw new Error("invalid DSL JSON: " + commandsStr);
|
|
348
407
|
}
|
|
349
408
|
const body = await doPost(`/devices/${args?.device_id}/dsl/execute`, script);
|
|
350
|
-
return textResult(
|
|
409
|
+
return textResult(body);
|
|
351
410
|
}
|
|
352
411
|
// Test management
|
|
353
412
|
case "test_get_active":
|
|
354
413
|
return textResult(await doGet("/tests/active"));
|
|
355
414
|
case "test_list_projects":
|
|
356
415
|
return textResult(await doGet("/tests/projects"));
|
|
357
|
-
case "test_run":
|
|
358
|
-
|
|
416
|
+
case "test_run": {
|
|
417
|
+
const body = {
|
|
359
418
|
project_dir: args?.project_dir,
|
|
360
419
|
case_path: args?.case_path,
|
|
361
420
|
device_id: args?.device_id,
|
|
362
|
-
}
|
|
421
|
+
};
|
|
422
|
+
const rawParams = args?.params;
|
|
423
|
+
if (rawParams && typeof rawParams === "object") {
|
|
424
|
+
const params = {};
|
|
425
|
+
for (const [k, v] of Object.entries(rawParams)) {
|
|
426
|
+
params[k] = String(v);
|
|
427
|
+
}
|
|
428
|
+
body.params = params;
|
|
429
|
+
}
|
|
430
|
+
return textResult(await doPost("/tests/cases/run", body));
|
|
431
|
+
}
|
|
432
|
+
// Debug session
|
|
433
|
+
case "debug_attach": {
|
|
434
|
+
const dev = args?.device_id;
|
|
435
|
+
const bundleID = args?.bundle_id;
|
|
436
|
+
const pid = args?.pid;
|
|
437
|
+
if (!bundleID && !pid)
|
|
438
|
+
throw new Error("either bundle_id or pid is required");
|
|
439
|
+
const body = {};
|
|
440
|
+
if (Array.isArray(args?.breakpoints))
|
|
441
|
+
body.breakpoints = args.breakpoints;
|
|
442
|
+
if (args?.stop_on_entry)
|
|
443
|
+
body.stopOnEntry = true;
|
|
444
|
+
let path;
|
|
445
|
+
if (pid && pid > 0) {
|
|
446
|
+
body.pid = pid;
|
|
447
|
+
path = `/devices/${dev}/debug-session/attach-running`;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
body.bundleId = bundleID;
|
|
451
|
+
path = `/devices/${dev}/debug-session/attach`;
|
|
452
|
+
}
|
|
453
|
+
return textResult(await doPost(path, body));
|
|
454
|
+
}
|
|
455
|
+
case "debug_detach": {
|
|
456
|
+
const dev = args?.device_id;
|
|
457
|
+
const body = args?.kill ? { kill: true } : {};
|
|
458
|
+
return textResult(await doRequest("DELETE", `/devices/${dev}/debug-session`, body));
|
|
459
|
+
}
|
|
460
|
+
case "debug_state": {
|
|
461
|
+
const dev = args?.device_id;
|
|
462
|
+
const includeStack = args?.include_stack === true;
|
|
463
|
+
const includeVars = args?.include_vars === true;
|
|
464
|
+
const includeThreads = args?.include_threads === true;
|
|
465
|
+
const base = `/devices/${dev}/debug-session`;
|
|
466
|
+
const snap = await doGet(base);
|
|
467
|
+
if (snap?.state === "paused") {
|
|
468
|
+
if (includeThreads) {
|
|
469
|
+
const t = await doGet(`${base}/threads`);
|
|
470
|
+
snap.threads = t?.threads;
|
|
471
|
+
}
|
|
472
|
+
if (includeStack || includeVars) {
|
|
473
|
+
const stack = await doGet(`${base}/stack`);
|
|
474
|
+
if (includeStack)
|
|
475
|
+
snap.stack = stack?.frames;
|
|
476
|
+
if (includeVars && Array.isArray(stack?.frames) && stack.frames.length > 0) {
|
|
477
|
+
const frameID = stack.frames[0].id;
|
|
478
|
+
const vars = await doGet(`${base}/frames/${frameID}/variables`);
|
|
479
|
+
snap.frame0_locals = vars?.scopes;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return textResult(snap);
|
|
484
|
+
}
|
|
485
|
+
case "debug_breakpoint": {
|
|
486
|
+
const dev = args?.device_id;
|
|
487
|
+
const action = args?.action;
|
|
488
|
+
if (action === "add") {
|
|
489
|
+
const spec = args?.spec;
|
|
490
|
+
if (!spec)
|
|
491
|
+
throw new Error("spec is required for action=add");
|
|
492
|
+
return textResult(await doPost(`/devices/${dev}/debug-session/breakpoints`, { spec }));
|
|
493
|
+
}
|
|
494
|
+
else if (action === "remove") {
|
|
495
|
+
const id = args?.id;
|
|
496
|
+
if (!id || id <= 0)
|
|
497
|
+
throw new Error("id is required for action=remove");
|
|
498
|
+
return textResult(await doDelete(`/devices/${dev}/debug-session/breakpoints/${id}`));
|
|
499
|
+
}
|
|
500
|
+
throw new Error(`action must be "add" or "remove"`);
|
|
501
|
+
}
|
|
502
|
+
case "debug_eval": {
|
|
503
|
+
const dev = args?.device_id;
|
|
504
|
+
const body = { expression: args?.expression };
|
|
505
|
+
if (args?.frame_id)
|
|
506
|
+
body.frameId = args.frame_id;
|
|
507
|
+
return textResult(await doPost(`/devices/${dev}/debug-session/eval`, body));
|
|
508
|
+
}
|
|
509
|
+
case "debug_step": {
|
|
510
|
+
const dev = args?.device_id;
|
|
511
|
+
const direction = args?.direction;
|
|
512
|
+
if (!direction)
|
|
513
|
+
throw new Error(`direction is required ("in" | "over" | "out")`);
|
|
514
|
+
const body = { direction };
|
|
515
|
+
if (typeof args?.include_stack === "boolean")
|
|
516
|
+
body.includeStack = args.include_stack;
|
|
517
|
+
if (typeof args?.include_vars === "boolean")
|
|
518
|
+
body.includeVars = args.include_vars;
|
|
519
|
+
return textResult(await doPost(`/devices/${dev}/debug-session/step`, body));
|
|
520
|
+
}
|
|
363
521
|
default:
|
|
364
522
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
365
523
|
}
|
package/dist/resources.js
CHANGED
|
@@ -17,6 +17,12 @@ export const RESOURCES = [
|
|
|
17
17
|
description: "How to preview a MobAI device's control UI inside Claude Code's preview panel",
|
|
18
18
|
mimeType: "text/plain",
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
uri: "mobai://reference/debugging",
|
|
22
|
+
name: "App Debugging Reference",
|
|
23
|
+
description: "How to attach lldb, set breakpoints, inspect stack/variables, evaluate Swift/ObjC expressions — read before any debug_* tool",
|
|
24
|
+
mimeType: "text/plain",
|
|
25
|
+
},
|
|
20
26
|
];
|
|
21
27
|
export function getResourceContent(uri) {
|
|
22
28
|
switch (uri) {
|
|
@@ -26,6 +32,8 @@ export function getResourceContent(uri) {
|
|
|
26
32
|
return TESTING_REF;
|
|
27
33
|
case "mobai://claude-code-preview":
|
|
28
34
|
return CLAUDE_CODE_PREVIEW;
|
|
35
|
+
case "mobai://reference/debugging":
|
|
36
|
+
return DEBUGGING_REF;
|
|
29
37
|
default:
|
|
30
38
|
return null;
|
|
31
39
|
}
|
|
@@ -69,6 +77,7 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
69
77
|
<script-format>
|
|
70
78
|
{"version": "0.2", "steps": [...actions...], "on_fail": {"strategy": "retry", "max_retries": 2}}
|
|
71
79
|
Every script must include "version": "0.2" and a "steps" array.
|
|
80
|
+
Optional "params": {"name": "default_value"} declares parameters. Callers supply values via the API; \${name} is substituted in step string fields at runtime.
|
|
72
81
|
</script-format>
|
|
73
82
|
|
|
74
83
|
<important>
|
|
@@ -96,6 +105,14 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
96
105
|
|
|
97
106
|
<workflow>Observe screen → plan → act via execute_dsl → verify (end script with wait_for stable + observe) → repeat until done.</workflow>
|
|
98
107
|
|
|
108
|
+
<siri-shortcuts>
|
|
109
|
+
iOS only. Before navigating through multiple screens to reach a feature, check if Siri can take you there directly. Many apps register SiriKit intents and App Shortcuts — a single siri action can replace 5-10 tap/scroll/wait steps.
|
|
110
|
+
Use observe with include: installed_apps to check what an app exposes. Common shortcuts: play media, send messages, open specific screens, make payments, start workouts, get directions.
|
|
111
|
+
Examples: "Open my cart in Amazon", "Play my playlist on Spotify", "Show my reservations in Booking", "Search YouTube for cats".
|
|
112
|
+
If Siri asks a follow-up question, dismiss and re-invoke with a more specific prompt that includes the missing detail.
|
|
113
|
+
Always prefer siri over manual UI navigation when the app supports it — it is faster, more reliable, and survives UI redesigns.
|
|
114
|
+
</siri-shortcuts>
|
|
115
|
+
|
|
99
116
|
<per-app-skills>
|
|
100
117
|
Before working with a known app, check ~/.claude/skills/ for a skill matching its bundle id or name (e.g. com-instagram-android, uber) and load it — it may already encode selectors, flows, and quirks learned on a prior run.
|
|
101
118
|
When you discover app-specific gotchas that would cost future sessions time — unstable selectors that only work with a specific predicate, hidden taps, flows that need an extra wait_for, React Native / Flutter screens that need OCR, dialogs that hijack input — create or update a skill at ~/.claude/skills/<app-slug>/SKILL.md capturing the finding. Keep each skill short: the specific quirk, the selector/flow that works, and one sentence on why the obvious approach fails. Do not write generic mobile-automation advice there — that belongs in this reference.
|
|
@@ -299,6 +316,16 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
299
316
|
<action name="reset_location">
|
|
300
317
|
<example>{"action": "reset_location"}</example>
|
|
301
318
|
</action>
|
|
319
|
+
|
|
320
|
+
<action name="siri">
|
|
321
|
+
iOS only. Sends a voice command to Siri via XCUISiriService. Auto-approves consent dialogs, captures Siri's response text, then dismisses the Siri UI.
|
|
322
|
+
Use for triggering SiriKit intents and App Shortcuts registered by apps (media playback, messaging, banking shortcuts, etc.).
|
|
323
|
+
The captured response is stored in "siri_response" and returned in the step result. If Siri asks a follow-up question, reformulate the prompt with more detail and call siri again.
|
|
324
|
+
<field name="prompt" required="yes">Voice command text</field>
|
|
325
|
+
<example>{"action": "siri", "prompt": "Search YouTube for cat videos"}</example>
|
|
326
|
+
<example>{"action": "siri", "prompt": "Send an email to john@example.com via Gmail"}</example>
|
|
327
|
+
<note>Check the app's siri field in the installed apps list (observe with include: installed_apps) to see which intents and activities it supports before calling siri.</note>
|
|
328
|
+
</action>
|
|
302
329
|
</native-actions>
|
|
303
330
|
|
|
304
331
|
<web-actions>
|
|
@@ -510,6 +537,7 @@ const TESTING_REF = `<testing-reference>
|
|
|
510
537
|
paste_text "Field" — paste clipboard into element
|
|
511
538
|
set_location 40.7128,-74.0060 — simulate GPS location (lat,lon)
|
|
512
539
|
reset_location — stop location simulation
|
|
540
|
+
siri "Search YouTube for cats" — invoke Siri with voice command (iOS only)
|
|
513
541
|
observe — observe screen
|
|
514
542
|
screenshot "path.png" — take screenshot
|
|
515
543
|
</actions>
|
|
@@ -533,8 +561,30 @@ const TESTING_REF = `<testing-reference>
|
|
|
533
561
|
# Device: iPhone 15 — device filter
|
|
534
562
|
# Timeout: 30000 — global timeout (ms)
|
|
535
563
|
# On-Fail: abort — abort or continue
|
|
564
|
+
# Param: username — declare a parameter (no default)
|
|
565
|
+
# Param: timeout = 5000 — declare with default value
|
|
536
566
|
</metadata>
|
|
537
567
|
|
|
568
|
+
<variables>
|
|
569
|
+
\${name} substitution: use \${param_name} anywhere in a step line to reference a parameter or extracted value.
|
|
570
|
+
Parameters declared via # Param: are available as \${name}. Extracted values (see extract below) are also available.
|
|
571
|
+
Example:
|
|
572
|
+
# Param: email
|
|
573
|
+
# Param: password = secret123
|
|
574
|
+
type "Email" → "\${email}"
|
|
575
|
+
type "Password" → "\${password}"
|
|
576
|
+
</variables>
|
|
577
|
+
|
|
578
|
+
<extract>
|
|
579
|
+
extract key from "Element" — extract text from matched element into \${key}
|
|
580
|
+
extract key from #AccessibilityID — extract text by accessibility ID
|
|
581
|
+
extract key from ~"partial" regex:"(\\d+)" — extract with regex capture group
|
|
582
|
+
extract key screenshot — save screenshot to disk, store path in \${key}
|
|
583
|
+
extract key = "literal value" — store a literal string in \${key}
|
|
584
|
+
Extracted values are available as \${key} in subsequent steps and returned in the API response as "extracted" map.
|
|
585
|
+
The optional regex: modifier applies a regex to the matched text; if it has a capture group, group 1 is stored.
|
|
586
|
+
</extract>
|
|
587
|
+
|
|
538
588
|
<platform-blocks>
|
|
539
589
|
# ios / # android — open platform block
|
|
540
590
|
# end — close block
|
|
@@ -551,5 +601,121 @@ const TESTING_REF = `<testing-reference>
|
|
|
551
601
|
</conditionals>
|
|
552
602
|
</mob-script-syntax>
|
|
553
603
|
|
|
604
|
+
<apis>
|
|
605
|
+
Mobile apps can be turned into callable APIs by saving parameterized .mob scripts to the APIs directory.
|
|
606
|
+
|
|
607
|
+
<directory>{MOBAI_DATA_DIR}/apis/ — global directory for API scripts. Each .mob file is a named API. Subdirectories are supported and become slash-separated names (e.g. apis/youtube/search.mob is callable as "youtube/search"). Resolves to ~/Library/Application Support/mobai/data/apis on macOS, %AppData%/mobai/data/apis on Windows, ~/.config/mobai/data/apis on Linux.</directory>
|
|
608
|
+
|
|
609
|
+
<workflow-create-api>
|
|
610
|
+
When the user asks to create an API from a mobile app flow:
|
|
611
|
+
1. Observe the app and understand the flow
|
|
612
|
+
2. Write a .mob script with # Param: declarations for inputs and extract actions for outputs
|
|
613
|
+
3. Save it to {MOBAI_DATA_DIR}/apis/{name}.mob — flat (gmail-send.mob) or nested (gmail/send.mob)
|
|
614
|
+
4. Test it with test_run using project_dir: {MOBAI_DATA_DIR}/apis/ and case_path: {name}.mob
|
|
615
|
+
5. List available APIs: GET /api/v1/apis
|
|
616
|
+
Call an API: POST /api/v1/apis/run/{name} with {"device_id": "...", "params": {...}}
|
|
617
|
+
The {name} segment is the path inside apis/ minus the .mob extension.
|
|
618
|
+
API runs do not persist results to .mobai/runs/ — only the extracted values come back in the response.
|
|
619
|
+
</workflow-create-api>
|
|
620
|
+
|
|
621
|
+
<example-api>
|
|
622
|
+
# Search YouTube
|
|
623
|
+
# Param: query
|
|
624
|
+
siri "Search YouTube for \${query}"
|
|
625
|
+
wait_for ~"\${query}" timeout:5000
|
|
626
|
+
extract result from ~"\${query}"
|
|
627
|
+
|
|
628
|
+
POST /api/v1/apis/run/youtube-search {"device_id":"X","params":{"query":"cats"}}
|
|
629
|
+
→ {"result": "cats"}
|
|
630
|
+
</example-api>
|
|
631
|
+
</apis>
|
|
632
|
+
|
|
554
633
|
</testing-reference>
|
|
555
634
|
`;
|
|
635
|
+
const DEBUGGING_REF = `<debugging-reference>
|
|
636
|
+
|
|
637
|
+
<scope>
|
|
638
|
+
Live debugging of an iOS app running on a connected device or booted simulator. Attach lldb, set breakpoints, inspect stack and variables, evaluate Swift/ObjC expressions, continue. Six MCP tools cover the full workflow.
|
|
639
|
+
|
|
640
|
+
Requires: a debug-signed build of the app (debug provisioning profile with get-task-allow). App Store / TestFlight builds cannot be attached. iOS 17+ for physical devices. macOS host with Xcode installed.
|
|
641
|
+
</scope>
|
|
642
|
+
|
|
643
|
+
<workflow>
|
|
644
|
+
Bps fire asynchronously when the user (or your execute_dsl) drives the UI. The agent observes via debug_state, not by waiting on debug_continue.
|
|
645
|
+
|
|
646
|
+
1. debug_attach {device_id, bundle_id, breakpoints: ["File.swift:42"]}
|
|
647
|
+
2. (trigger the action — usually via execute_dsl)
|
|
648
|
+
3. debug_state {device_id, include_stack: true, include_vars: true} // poll until state == "paused"
|
|
649
|
+
4. debug_eval {device_id, expression: "po self.viewModel.user"}
|
|
650
|
+
5. debug_step {device_id, direction: "continue"} // resume; fire-and-forget
|
|
651
|
+
6. (loop 2-5 as needed)
|
|
652
|
+
7. debug_detach {device_id}
|
|
653
|
+
|
|
654
|
+
direction: "continue" is fire-and-forget. For deterministic line-stepping use "in" / "over" / "out" — those block (~ms) and return fresh stack + locals.
|
|
655
|
+
</workflow>
|
|
656
|
+
|
|
657
|
+
<tools>
|
|
658
|
+
debug_attach — start a debug session.
|
|
659
|
+
device_id (required), bundle_id OR pid (one required), breakpoints (optional [string]),
|
|
660
|
+
stop_on_entry (optional bool, simulator only).
|
|
661
|
+
|
|
662
|
+
debug_state — query the current session state.
|
|
663
|
+
device_id (required), include_stack (bool, default false), include_vars (bool, default false), include_threads (bool, default false).
|
|
664
|
+
Default returns just {state, breakpoints}. Stack, frame[0] locals, and the thread list are opt-in (each costs a round-trip; ~few seconds on physical hardware).
|
|
665
|
+
|
|
666
|
+
debug_breakpoint — add or remove a breakpoint.
|
|
667
|
+
device_id, action: "add" | "remove", spec (for add), id (for remove).
|
|
668
|
+
|
|
669
|
+
debug_eval — evaluate a Swift/ObjC expression at the current pause.
|
|
670
|
+
device_id, expression, frame_id (optional). Session must be paused.
|
|
671
|
+
|
|
672
|
+
debug_step — advance the target.
|
|
673
|
+
device_id, direction one of:
|
|
674
|
+
"in" / "over" / "out" — block ~ms until next stop; return {state, breakpoints, stack, frame0_locals}.
|
|
675
|
+
"continue" — fire-and-forget; return {state, breakpoints}; poll debug_state for next stop.
|
|
676
|
+
include_stack / include_vars (default true; ignored for "continue").
|
|
677
|
+
|
|
678
|
+
debug_detach — end the session. kill (default false) terminates the debuggee.
|
|
679
|
+
</tools>
|
|
680
|
+
|
|
681
|
+
<breakpoint-specs>
|
|
682
|
+
Three accepted forms. Prefer file:line for application code.
|
|
683
|
+
|
|
684
|
+
"File.swift:42" file:line (basename or absolute path)
|
|
685
|
+
"Module.Type.method" Swift demangled prefix (NO parameter signature, NO return type)
|
|
686
|
+
"-[ClassName method:]" ObjC method
|
|
687
|
+
"swift_willThrow" bare runtime symbol
|
|
688
|
+
|
|
689
|
+
Caveats:
|
|
690
|
+
- Release/optimized builds without DWARF return verified=false.
|
|
691
|
+
- swift_willThrow / objc_exception_throw fire on EVERY internal Swift/ObjC throw — Apple frameworks throw constantly under the hood. Use only when actually hunting an uncaught error.
|
|
692
|
+
</breakpoint-specs>
|
|
693
|
+
|
|
694
|
+
<eval-expressions>
|
|
695
|
+
debug_eval runs lldb expression --. Accepts ObjC++ syntax by default; Swift syntax when frame is in a Swift compile unit.
|
|
696
|
+
|
|
697
|
+
p expr evaluate, default-format
|
|
698
|
+
po expr call objects description
|
|
699
|
+
frame variable list all locals (no eval — fast)
|
|
700
|
+
bt full backtrace
|
|
701
|
+
image lookup -n NAME resolve a symbol name to module + addresses
|
|
702
|
+
</eval-expressions>
|
|
703
|
+
|
|
704
|
+
<state-machine>
|
|
705
|
+
paused — debug_eval works; debug_continue resumes.
|
|
706
|
+
running — debug_eval returns 409; debug_breakpoint still works for next hit.
|
|
707
|
+
dead — exited or crashed. Detach and reattach.
|
|
708
|
+
|
|
709
|
+
While the foreground app on a device is paused at a bp, UI input via execute_dsl tap/swipe blocks at WDA until you debug_continue.
|
|
710
|
+
</state-machine>
|
|
711
|
+
|
|
712
|
+
<common-failures>
|
|
713
|
+
"lldb-dap not found" — install Xcode 15+.
|
|
714
|
+
"device debugging requires iOS 17+" — physical-device path needs the on-device tunnel.
|
|
715
|
+
"bundle is not installed on device" — install_app first with a debug-signed build.
|
|
716
|
+
verified=false — symbol mangling mismatch or no debug info. Try image lookup -n NAME via debug_eval.
|
|
717
|
+
"___lldb_unnamed_symbol_*" in stacks — dSYM not loaded. For device builds, run debug_eval "target symbols add /path/to/MyApp.app.dSYM".
|
|
718
|
+
</common-failures>
|
|
719
|
+
|
|
720
|
+
</debugging-reference>
|
|
721
|
+
`;
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/MobAI-App/mobai-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "2.2.
|
|
9
|
+
"version": "2.2.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "mobai-mcp",
|
|
14
|
-
"version": "2.2.
|
|
14
|
+
"version": "2.2.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|