mobai-mcp 2.3.1 → 2.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/index.js CHANGED
@@ -223,6 +223,12 @@ const TOOLS = [
223
223
  properties: {
224
224
  device_id: { type: "string", description: "Device ID" },
225
225
  path: { type: "string", description: "Local file path to the app (.apk or .ipa)" },
226
+ resign: { type: "boolean", description: "Sign the IPA with iloader before installing (iOS only)" },
227
+ apple_id: { type: "string", description: "Apple ID for signing (optional; used when resign=true)" },
228
+ password: { type: "string", description: "Apple ID password for signing (optional; uses cached credentials if empty)" },
229
+ profile_path: { type: "string", description: "Provisioning profile to reuse for an offline re-sign (with cert_path/key_path); falls back to online signing if invalid" },
230
+ cert_path: { type: "string", description: "Signing certificate PEM to reuse for an offline re-sign" },
231
+ key_path: { type: "string", description: "Signing private key PEM to reuse for an offline re-sign" },
226
232
  },
227
233
  required: ["device_id", "path"],
228
234
  },
@@ -242,16 +248,65 @@ const TOOLS = [
242
248
  // DSL execution
243
249
  {
244
250
  name: "execute_dsl",
245
- description: `Execute a batch of DSL commands on a device. This is the primary tool for all device interaction — tap, type, swipe, observe, launch apps, assertions, web automation, and more.
251
+ description: `The example below is the full command surface. For richer semantics - per-action defaults, platform notes, retry/failure strategies, observe and scroll guidance, web/OCR caveats - read the MCP resource mobai://reference/device-automation. Read it the first time you hit anything the example doesn't make obvious.
246
252
 
247
- You MUST read the MCP resource mobai://reference/device-automation to learn how to control devices before using this tool.
253
+ Execute a batch of DSL commands on a device. This is the primary tool for all device interaction - tap, type, swipe, observe, launch apps, assertions, web automation, and more.
248
254
 
249
- Input: JSON string with "version": "0.2" and "steps" array. Example:
250
- {"version":"0.2","steps":[
251
- {"action":"open_app","bundle_id":"com.apple.Preferences"},
252
- {"action":"tap","predicate":{"text_contains":"Wi-Fi"}},
253
- {"action":"wait_for","predicate":{"type":"switch"},"timeout_ms":3000}
254
- ]}`,
255
+ To find visual/UI bugs on the current screen, run a single {"action":"audit"} step: it returns deterministic findings (touch targets, clipping, alignment, spacing, safe area, accessibility) plus an annotated screenshot whose red boxes are labelled with the element ids in the findings. Findings are evidence, not verdicts - cross-check the annotated screenshot to decide which are real bugs.
256
+
257
+ Input: JSON string with "version": "0.2" and a "steps" array. Optional script-wide "params" (declared defaults, substituted as \${name}) and "on_fail" (strategy: abort|skip|retry|replan|require_user; retry also takes max_retries, retry_delay_ms, fallback_strategy). Any step may carry its own "on_fail".
258
+
259
+ Every action below appears at least once - this is the full command surface (semantics, defaults, and platform notes are in mobai://reference/device-automation):
260
+ {"version":"0.2","params":{"query":"shoes"},"on_fail":{"strategy":"retry","max_retries":2,"retry_delay_ms":1000,"fallback_strategy":{"strategy":"skip"}},"steps":[
261
+ {"action":"open_app","bundle_id":"com.apple.Preferences","fresh":true,"debug":false},
262
+ {"action":"kill_app","bundle_id":"com.apple.mobilesafari"},
263
+ {"action":"open_link","url":"myapp://profile/42"},
264
+ {"action":"tap","predicate":{"text_contains":"Wi-Fi","type":"button"}},
265
+ {"action":"tap","coords":{"x":200,"y":640}},
266
+ {"action":"double_tap","predicate":{"text":"Photo"}},
267
+ {"action":"two_finger_tap","predicate":{"text":"Map"}},
268
+ {"action":"long_press","predicate":{"text":"Item"},"duration_ms":1000},
269
+ {"action":"pinch","predicate":{"text_contains":"Map"},"scale":0.5,"velocity":1.0},
270
+ {"action":"type","text":"\${query}","predicate":{"type":"input"},"clear_first":true,"dismiss_keyboard":true},
271
+ {"action":"type_secret","secret_id":"test-account-password","predicate":{"type":"input","text_contains":"Password"}},
272
+ {"action":"clear","predicate":{"type":"input"}},
273
+ {"action":"toggle","predicate":{"type":"switch","text_contains":"Wi-Fi"},"state":"on"},
274
+ {"action":"press_key","key":"enter"},
275
+ {"action":"swipe","direction":"up","distance":"medium"},
276
+ {"action":"swipe","from_coords":{"x":200,"y":600},"to_coords":{"x":200,"y":200},"duration_ms":300},
277
+ {"action":"scroll","direction":"down","to_element":{"predicate":{"text":"Privacy"}},"predicate":{"type":"scrollview"},"max_scrolls":10,"amount":"page"},
278
+ {"action":"drag","from":{"predicate":{"text":"Item"}},"to_element":{"predicate":{"text":"Trash"}},"press_duration_ms":500,"hold_duration_ms":200,"duration_ms":500},
279
+ {"action":"drag","from_coords":{"x":100,"y":400},"to_coords":{"x":300,"y":400}},
280
+ {"action":"drag_path","points":[{"x":100,"y":400,"duration_ms":200},{"x":150,"y":300,"duration_ms":150},{"x":300,"y":500,"duration_ms":300}]},
281
+ {"action":"navigate","target":"home"},
282
+ {"action":"set_location","lat":40.7128,"lon":-74.0060},
283
+ {"action":"reset_location"},
284
+ {"action":"siri","prompt":"Search YouTube for cat videos"},
285
+ {"action":"if_exists","predicate":{"text":"Allow"},"then":[{"action":"tap","predicate":{"text":"Allow"}}],"else":[{"action":"tap","predicate":{"text":"Don't Allow"}}]},
286
+ {"action":"delay","duration_ms":1000},
287
+ {"action":"wait_for","predicate":{"text":"Welcome"},"timeout_ms":5000,"poll_interval_ms":500},
288
+ {"action":"assert_exists","predicate":{"text":"Success"},"timeout_ms":3000},
289
+ {"action":"assert_not_exists","predicate":{"text":"Error"}},
290
+ {"action":"assert_count","predicate":{"type":"cell"},"count":5},
291
+ {"action":"assert_screen_changed","threshold_percent":15},
292
+ {"action":"metrics_start","types":["system_cpu","fps"],"interval_ms":1000,"label":"login_flow","capture_logs":true,"thresholds":{"cpu_high":80,"fps_low":45}},
293
+ {"action":"metrics_stop","format":"summary"},
294
+ {"action":"record_start","file_path":"/tmp/mobai/rec"},
295
+ {"action":"record_stop"},
296
+ {"action":"select_web_context","url_contains":"google.com"},
297
+ {"action":"navigate","context":"web","url":"https://example.com"},
298
+ {"action":"tap","context":"web","predicate":{"css_selector":"button.submit"}},
299
+ {"action":"type","context":"web","predicate":{"css_selector":"input#email"},"text":"user@example.com","clear_first":true},
300
+ {"action":"execute_js","context":"web","script":"return document.title","async":false},
301
+ {"action":"screenshot","file_path":"/tmp/mobai","name":"final_state"},
302
+ {"action":"wait_for","stable":true,"timeout_ms":3000},
303
+ {"action":"observe","include":["ui_tree"],"only_visible":true,"filter":{"text_regex":"Settings|Wi-Fi"},"store_as":"end_state"},
304
+ {"action":"audit"}
305
+ ]}
306
+
307
+ Predicate - object passed in a step's "predicate"/"from"/"to_element". Combine fields (AND); prefer text_contains over exact text. All native fields:
308
+ {"text":"exact","text_contains":"substr","text_starts_with":"prefix","text_regex":"\\d+ items","value":"typed value","value_contains":"typed substr","type":"button|input|switch|text|image|cell|scrollview","accessibility_id":"login_btn","enabled":true,"visible":true,"selected":false,"index":0,"bounds_hint":"top_half|bottom_half|left_half|right_half|center","near":{"text_contains":"Email","direction":"below|above|left|right|any","max_distance":100},"parent_of":{"text":"child label"}}
309
+ Web predicate (context:"web") uses instead: {"css_selector":"button.submit"}.`,
255
310
  inputSchema: {
256
311
  type: "object",
257
312
  properties: {
@@ -437,8 +492,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
437
492
  }
438
493
  case "list_apps":
439
494
  return textResult(await doGet(`/devices/${args?.device_id}/apps`));
440
- case "install_app":
441
- return textResult(await doPost(`/devices/${args?.device_id}/install-app`, { path: args?.path }));
495
+ case "install_app": {
496
+ const body = { path: args?.path };
497
+ if (args?.resign) {
498
+ body.resign = true;
499
+ if (args?.apple_id)
500
+ body.appleId = args.apple_id;
501
+ if (args?.password)
502
+ body.password = args.password;
503
+ if (args?.profile_path)
504
+ body.profilePath = args.profile_path;
505
+ if (args?.cert_path)
506
+ body.certPath = args.cert_path;
507
+ if (args?.key_path)
508
+ body.keyPath = args.key_path;
509
+ }
510
+ return textResult(await doPost(`/devices/${args?.device_id}/install-app`, body));
511
+ }
442
512
  case "uninstall_app":
443
513
  return textResult(await doDelete(`/devices/${args?.device_id}/apps/${encodeURIComponent(args?.bundle_id)}`));
444
514
  // DSL execution
package/dist/resources.js CHANGED
@@ -92,6 +92,7 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
92
92
  <observe-guidance>
93
93
  Prefer UI tree and OCR over screenshots — screenshots consume far more context and are slower. Only use screenshots for visual verification (layout, colors, images).
94
94
  only_visible: false — use ONLY for data collection from scrollable views (returns ALL elements including off-screen). NEVER when interacting — off-screen elements cannot be tapped.
95
+ compact: true — token-lean ui_tree (same elements as the full tree, as flat [index] Type "label" #id [state] lines; coordinates and indentation removed, ~2x fewer tokens). Use when you select by id/index/text. Keep the default full tree when you need coordinates or spatial layout (e.g. coordinate taps, near/spatial reasoning).
95
96
  </observe-guidance>
96
97
 
97
98
  <ocr-fallback>
@@ -161,7 +162,6 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
161
162
 
162
163
  <predicate context="web">
163
164
  <field name="css_selector" type="string">CSS selector</field>
164
- <field name="xpath" type="string">XPath expression</field>
165
165
  </predicate>
166
166
 
167
167
  <failure-strategy>
@@ -217,6 +217,15 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
217
217
  <example>{"action": "type", "text": "Hello", "predicate": {"type": "input"}, "clear_first": true}</example>
218
218
  </action>
219
219
 
220
+ <action name="type_secret">
221
+ Type a stored credential without ever exposing its value. secret_id names a credential saved in the app; its value is resolved from the keychain at run time and typed on-device - it never appears in the script, logs, results, or the model context. Use for passwords/tokens in login steps. Same field-targeting as type.
222
+ <field name="secret_id" required="yes">Id of a stored credential (see list_secrets)</field>
223
+ <field name="predicate">Optional target field to focus first</field>
224
+ <field name="clear_first" type="bool"/>
225
+ <field name="dismiss_keyboard" type="bool" default="false"/>
226
+ <example>{"action": "type_secret", "secret_id": "test-account-password", "predicate": {"type": "input", "text_contains": "Password"}}</example>
227
+ </action>
228
+
220
229
  <action name="clear">
221
230
  Clear a field's text without typing. With a predicate, focuses that field first; without one, clears the currently focused field.
222
231
  <field name="predicate">Optional target element</field>
@@ -235,11 +244,13 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
235
244
 
236
245
  <action name="scroll">
237
246
  Direction = semantic (where to look), not finger movement.
238
- <field name="direction" required="yes">down (look below), up (look above)</field>
247
+ <field name="direction" required="yes">down (look below), up (look above), left (look left), right (look right). All four directions are supported - use left/right for horizontal scrollviews (carousels, palettes, paging) instead of falling back to raw-coordinate swipe.</field>
248
+ <field name="predicate" type="Predicate">Targets a specific scrollable container so the gesture lands inside it (e.g. {"type": "scrollview"} or {"parent_of": {"text": "Work"}}). Without it the gesture is applied at screen center - set this when multiple scrollviews share the screen (e.g. a horizontal palette above a vertical list) to avoid scrolling the wrong one.</field>
239
249
  <field name="to_element" type="TargetElement"/>
240
250
  <field name="max_scrolls" type="int" default="10"/>
241
251
  <field name="amount">small, page, full</field>
242
252
  <example>{"action": "scroll", "direction": "down", "to_element": {"predicate": {"text": "Privacy"}}, "max_scrolls": 10}</example>
253
+ <example>{"action": "scroll", "direction": "right", "predicate": {"type": "scrollview"}}</example>
243
254
  <note>scroll with to_element returns "reached end of scrollable content" if the list ends before the element is found. If it returns "element not found after scrolling" instead, the list has more content — increase max_scrolls or call scroll again to continue searching.</note>
244
255
  </action>
245
256
 
@@ -294,11 +305,17 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
294
305
  <field name="store_as" type="string">Store result in named variable</field>
295
306
  <field name="include_keyboard" type="bool" default="false"/>
296
307
  <field name="only_visible" type="bool" default="true">MUST be false when collecting data from scrollable lists/search results. MUST be true when interacting with elements — off-screen elements cannot be tapped.</field>
308
+ <field name="compact" type="bool" default="false">Token-lean ui_tree: same elements as the full tree, formatted as flat [index] Type "label" #id [state] lines with coordinates and indentation removed (~2x fewer tokens). Use when selecting by id/index/text; keep false when you need coordinates or spatial layout.</field>
297
309
  <field name="filter">text_regex or bounds {"x","y","width","height"} to reduce output</field>
298
310
  <note>OCR (iOS only): returns text with tap coordinates. Useful for system dialogs missing from UI tree.</note>
299
311
  <example>{"action": "observe", "include": ["ui_tree"], "filter": {"text_regex": "Settings|Wi-Fi"}}</example>
300
312
  </action>
301
313
 
314
+ <action name="audit">
315
+ <note>Runs deterministic UI checks on the current native screen and returns findings plus an annotated screenshot (path under observations.native.screenshot, with red boxes labelled by element id). Findings are EVIDENCE, not conclusions (e.g. margins are 18px/34px, not "misaligned") - cross-check the annotated screenshot to decide which are real bugs. Checks: touch_target, clipping, alignment, spacing, safe_area, accessibility. Dense grids of tiny repeated controls (paint canvas, keyboard) are skipped to avoid noise.</note>
316
+ <example>{"action": "audit"}</example>
317
+ </action>
318
+
302
319
  <action name="wait_for">
303
320
  <field name="predicate">Required unless stable is true</field>
304
321
  <field name="stable" type="bool" default="false">Wait for UI to stop changing</field>
@@ -337,6 +354,13 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
337
354
  <example>{"action": "reset_location"}</example>
338
355
  </action>
339
356
 
357
+ <action name="open_link">
358
+ Opens a URL or deep link on the device. The OS routes custom schemes (myapp://path) to the registered app, so this jumps straight to a screen without navigating the UI. Use for deep-link entry points instead of tapping through the app.
359
+ <field name="url" required="yes">Full URL or deep link, e.g. https://example.com or myapp://profile/42</field>
360
+ <example>{"action": "open_link", "url": "myapp://profile/42"}</example>
361
+ <example>{"action": "open_link", "url": "https://example.com"}</example>
362
+ </action>
363
+
340
364
  <action name="siri">
341
365
  iOS only. Sends a voice command to Siri service on iOS devices. Auto-approves consent dialogs, captures Siri's response text, then dismisses the Siri UI.
342
366
  Use for triggering SiriKit intents and App Shortcuts registered by apps (media playback, messaging, banking shortcuts, etc.).
@@ -360,7 +384,7 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
360
384
  </action>
361
385
 
362
386
  <action name="tap/type/press_key/wait_for/if_exists">
363
- Same fields as native but use css_selector or xpath in predicate.
387
+ Same fields as native but use css_selector in predicate.
364
388
  <example>{"action": "tap", "context": "web", "predicate": {"css_selector": "button.submit"}}</example>
365
389
  <example>{"action": "type", "context": "web", "predicate": {"css_selector": "input#email"}, "text": "user@example.com", "clear_first": true}</example>
366
390
  Web press_key keys: enter, tab, delete, escape.
@@ -462,7 +486,7 @@ const TESTING_REF = `<testing-reference>
462
486
 
463
487
  <rules>
464
488
  <rule>Never ask the user for information you can get yourself — use observe, list_apps, get_ui_tree.</rule>
465
- <rule>Always add wait_for before every element interaction (tap, type, toggle, long_press, double_tap, drag). Exception: the element was asserted on the immediately preceding line.</rule>
489
+ <rule>Add wait_for before an interaction only when the PREVIOUS action may have changed the screen and the target might not be ready yet - after opening an app, navigating, a screen transition, opening a modal/sheet, scrolling, or submitting something that reloads. If the previous action left the same screen up (typing into a field, toggling a switch, tapping a control that stays put) the element is already there - skip wait_for. Never add it before the element you just asserted.</rule>
466
490
  <rule>Always use predicates over coordinates — predicates survive layout changes.</rule>
467
491
  <rule>Always prefer UI tree and OCR over screenshots for element discovery.</rule>
468
492
  <rule>Your first action must be observe — never ask the user first.</rule>
@@ -542,6 +566,8 @@ const TESTING_REF = `<testing-reference>
542
566
  type ~"Field" → "text" — partial match field
543
567
  type "Field" → "text" clear — clear first
544
568
  type "Field" → "text" keep_keyboard — keep keyboard open
569
+ type_secret "Field" → "secret-id" - type a stored credential (value never in script/logs)
570
+ type_secret "secret-id" - type credential into focused field
545
571
  swipe up|down|left|right — swipe direction
546
572
  swipe up distance:long — short/medium/long
547
573
  swipe up duration:500 — custom duration (ms)
@@ -549,6 +575,7 @@ const TESTING_REF = `<testing-reference>
549
575
  scroll down — scroll
550
576
  scroll down to "Element" — scroll until visible
551
577
  scroll down to "Footer" max_scrolls:10 — limit attempts
578
+ scroll right in type:scrollview - target a specific container
552
579
  toggle "Wi-Fi" on|off — toggle switch
553
580
  toggle type:switch near "Wi-Fi" on — modifier-only
554
581
  drag "Item" to "Trash" — drag element
@@ -568,6 +595,7 @@ const TESTING_REF = `<testing-reference>
568
595
  paste_text "Field" — paste clipboard into element
569
596
  set_location 40.7128,-74.0060 — simulate GPS location (lat,lon)
570
597
  reset_location — stop location simulation
598
+ open_link "myapp://profile/42" - open a URL or deep link (routes to the app)
571
599
  siri "Search YouTube for cats" — invoke Siri with voice command (iOS only)
572
600
  observe — observe screen
573
601
  screenshot "path.png" — take screenshot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobai-mcp",
3
- "version": "2.3.1",
3
+ "version": "2.4.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.3.1",
9
+ "version": "2.4.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "mobai-mcp",
14
- "version": "2.3.1",
14
+ "version": "2.4.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }