mobai-mcp 2.1.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 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(extractDSLScreenshots(body));
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
- return textResult(await doPost("/tests/cases/run", {
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
@@ -11,6 +11,18 @@ export const RESOURCES = [
11
11
  description: "Testing workflow, rules, error fixes, and .mob script syntax for test generation",
12
12
  mimeType: "text/plain",
13
13
  },
14
+ {
15
+ uri: "mobai://claude-code-preview",
16
+ name: "Claude Code Preview Setup",
17
+ description: "How to preview a MobAI device's control UI inside Claude Code's preview panel",
18
+ mimeType: "text/plain",
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
+ },
14
26
  ];
15
27
  export function getResourceContent(uri) {
16
28
  switch (uri) {
@@ -18,10 +30,45 @@ export function getResourceContent(uri) {
18
30
  return DEVICE_AUTOMATION_REF;
19
31
  case "mobai://reference/testing":
20
32
  return TESTING_REF;
33
+ case "mobai://claude-code-preview":
34
+ return CLAUDE_CODE_PREVIEW;
35
+ case "mobai://reference/debugging":
36
+ return DEBUGGING_REF;
21
37
  default:
22
38
  return null;
23
39
  }
24
40
  }
41
+ const CLAUDE_CODE_PREVIEW = `<claude-code-preview>
42
+ Prerequisite: the MobAI desktop app must be running. It owns the
43
+ localhost 8787 web server the preview panel will render.
44
+
45
+ 1. Call list_devices and grab the device's id and controlUrl.
46
+
47
+ 2. Write .claude/launch.json at the project root (or, inside a git
48
+ worktree, at the worktree root):
49
+
50
+ {
51
+ "version": "0.0.1",
52
+ "configurations": [{
53
+ "name": "MobAI — <device name>",
54
+ "runtimeExecutable": "sleep",
55
+ "runtimeArgs": ["86400"],
56
+ "port": 8787,
57
+ "url": "<controlUrl>"
58
+ }]
59
+ }
60
+
61
+ - runtimeExecutable + runtimeArgs is a no-op lifetime anchor for
62
+ Claude Code's panel; the real server is MobAI.
63
+ - port is the localhost port Claude Code binds the preview to;
64
+ always 8787 for MobAI.
65
+ - url is the device-specific URL (controlUrl from step 1) that the
66
+ panel actually displays.
67
+
68
+ 3. Call the mcp__Claude_Preview__preview_start tool with the "name"
69
+ from the configuration above.
70
+ </claude-code-preview>
71
+ `;
25
72
  const DEVICE_AUTOMATION_REF = `<device-automation-reference>
26
73
 
27
74
  <guide>
@@ -30,6 +77,7 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
30
77
  <script-format>
31
78
  {"version": "0.2", "steps": [...actions...], "on_fail": {"strategy": "retry", "max_retries": 2}}
32
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.
33
81
  </script-format>
34
82
 
35
83
  <important>
@@ -57,6 +105,21 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
57
105
 
58
106
  <workflow>Observe screen → plan → act via execute_dsl → verify (end script with wait_for stable + observe) → repeat until done.</workflow>
59
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
+
116
+ <per-app-skills>
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.
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/&lt;app-slug&gt;/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.
119
+
120
+ Also save reusable multi-step flows as labeled mobai CLI command sequences inside the same SKILL.md. When you confirm a flow works (login, dismiss onboarding, open-settings-and-toggle-X, checkout), add a section with a heading like "## Flow: login" and a fenced shell code block of "mobai ..." commands in order — one per step. Mark variable inputs with placeholders (&lt;EMAIL&gt;, &lt;OTP_CODE&gt;) so future sessions know what to substitute. On next run, replay the commands (shell them out or translate to execute_dsl) with placeholders substituted — this avoids re-deriving the flow from scratch. Shell commands are saved (not JSON DSL) because the MobAI CLI does not execute DSL JSON blobs, and shell commands stay replayable from either CLI or MCP sessions. If a snippet breaks because the app changed, update it in place.
121
+ </per-app-skills>
122
+
60
123
  <screenshot-tools>
61
124
  get_screenshot — fast low-quality image for LLM visual analysis.
62
125
  save_screenshot — full-quality PNG for reporting, debugging, or sharing.
@@ -253,6 +316,16 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
253
316
  <action name="reset_location">
254
317
  <example>{"action": "reset_location"}</example>
255
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>
256
329
  </native-actions>
257
330
 
258
331
  <web-actions>
@@ -464,6 +537,7 @@ const TESTING_REF = `<testing-reference>
464
537
  paste_text "Field" — paste clipboard into element
465
538
  set_location 40.7128,-74.0060 — simulate GPS location (lat,lon)
466
539
  reset_location — stop location simulation
540
+ siri "Search YouTube for cats" — invoke Siri with voice command (iOS only)
467
541
  observe — observe screen
468
542
  screenshot "path.png" — take screenshot
469
543
  </actions>
@@ -487,8 +561,30 @@ const TESTING_REF = `<testing-reference>
487
561
  # Device: iPhone 15 — device filter
488
562
  # Timeout: 30000 — global timeout (ms)
489
563
  # On-Fail: abort — abort or continue
564
+ # Param: username — declare a parameter (no default)
565
+ # Param: timeout = 5000 — declare with default value
490
566
  </metadata>
491
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
+
492
588
  <platform-blocks>
493
589
  # ios / # android — open platform block
494
590
  # end — close block
@@ -505,5 +601,121 @@ const TESTING_REF = `<testing-reference>
505
601
  </conditionals>
506
602
  </mob-script-syntax>
507
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
+
508
633
  </testing-reference>
509
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobai-mcp",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "mcpName": "io.github.MobAI-App/mobai-mcp",
5
5
  "description": "MCP server for MobAI - AI-powered mobile device automation",
6
6
  "type": "module",
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.1.0",
9
+ "version": "2.2.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "mobai-mcp",
14
- "version": "2.1.0",
14
+ "version": "2.2.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }