mobai-mcp 2.3.0 → 2.4.0
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 +109 -11
- package/dist/resources.js +63 -10
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -140,7 +140,7 @@ Pass this as the `commands` argument (a JSON string) to `execute_dsl` along with
|
|
|
140
140
|
|
|
141
141
|
## Troubleshooting
|
|
142
142
|
|
|
143
|
-
**"Connection refused"** — Make sure the MobAI desktop app is running and the API is reachable at `http://127.0.0.1:8686`.
|
|
143
|
+
**"Connection refused" / "Could not reach the MobAI desktop app"** — Make sure the MobAI desktop app is installed and running, and the API is reachable at `http://127.0.0.1:8686`. If you don't have it yet, download and install it from [https://mobai.run/download](https://mobai.run/download).
|
|
144
144
|
|
|
145
145
|
**"Bridge not running"** — Call `start_bridge` first. The iOS bridge can take up to a minute to come up.
|
|
146
146
|
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,28 @@ import { RESOURCES, getResourceContent } from "./resources.js";
|
|
|
15
15
|
const API_BASE_URL = "http://127.0.0.1:8686/api/v1";
|
|
16
16
|
const DEFAULT_TIMEOUT_MS = 300000; // 5 minutes (matches Go httpClient timeout)
|
|
17
17
|
const SCREENSHOT_DIR = path.join(os.tmpdir(), "mobai", "screenshots");
|
|
18
|
+
const DOWNLOAD_URL = "https://mobai.run/download";
|
|
19
|
+
// Message shown when the MobAI desktop app is not reachable at its local API.
|
|
20
|
+
const APP_NOT_RUNNING_MESSAGE = `Could not reach the MobAI desktop app at 127.0.0.1:8686. ` +
|
|
21
|
+
`Make sure the MobAI desktop app is installed and running, then try again. ` +
|
|
22
|
+
`If you don't have it yet, download and install it from ${DOWNLOAD_URL}.`;
|
|
23
|
+
/**
|
|
24
|
+
* Detects the "connection refused" / "could not connect" family of errors that
|
|
25
|
+
* Node's fetch throws when nothing is listening on the MobAI API port. These
|
|
26
|
+
* surface as a TypeError ("fetch failed") whose `cause` carries an errno code
|
|
27
|
+
* such as ECONNREFUSED / ENOTFOUND / ECONNRESET.
|
|
28
|
+
*/
|
|
29
|
+
function isConnectionError(err) {
|
|
30
|
+
if (!(err instanceof Error))
|
|
31
|
+
return false;
|
|
32
|
+
const codes = ["ECONNREFUSED", "ENOTFOUND", "ECONNRESET", "EHOSTUNREACH", "ETIMEDOUT"];
|
|
33
|
+
const cause = err.cause;
|
|
34
|
+
const causeCode = cause && typeof cause === "object" ? cause.code : undefined;
|
|
35
|
+
if (typeof causeCode === "string" && codes.includes(causeCode))
|
|
36
|
+
return true;
|
|
37
|
+
// Fallback: undici reports a bare "fetch failed" TypeError for these.
|
|
38
|
+
return err.name === "TypeError" && /fetch failed/i.test(err.message);
|
|
39
|
+
}
|
|
18
40
|
// ---------------------------------------------------------------------------
|
|
19
41
|
// Screenshot helpers
|
|
20
42
|
// ---------------------------------------------------------------------------
|
|
@@ -55,7 +77,16 @@ async function doRequest(method, urlPath, payload, timeoutMs = DEFAULT_TIMEOUT_M
|
|
|
55
77
|
if (payload !== undefined && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
56
78
|
opts.body = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
57
79
|
}
|
|
58
|
-
|
|
80
|
+
let response;
|
|
81
|
+
try {
|
|
82
|
+
response = await fetch(url, opts);
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
if (isConnectionError(err)) {
|
|
86
|
+
throw new Error(APP_NOT_RUNNING_MESSAGE);
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
59
90
|
clearTimeout(timeoutId);
|
|
60
91
|
const text = await response.text();
|
|
61
92
|
let body;
|
|
@@ -192,6 +223,12 @@ const TOOLS = [
|
|
|
192
223
|
properties: {
|
|
193
224
|
device_id: { type: "string", description: "Device ID" },
|
|
194
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" },
|
|
195
232
|
},
|
|
196
233
|
required: ["device_id", "path"],
|
|
197
234
|
},
|
|
@@ -211,16 +248,62 @@ const TOOLS = [
|
|
|
211
248
|
// DSL execution
|
|
212
249
|
{
|
|
213
250
|
name: "execute_dsl",
|
|
214
|
-
description: `
|
|
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.
|
|
252
|
+
|
|
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.
|
|
254
|
+
|
|
255
|
+
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".
|
|
215
256
|
|
|
216
|
-
|
|
257
|
+
Every action below appears at least once - this is the full command surface (semantics, defaults, and platform notes are in mobai://reference/device-automation):
|
|
258
|
+
{"version":"0.2","params":{"query":"shoes"},"on_fail":{"strategy":"retry","max_retries":2,"retry_delay_ms":1000,"fallback_strategy":{"strategy":"skip"}},"steps":[
|
|
259
|
+
{"action":"open_app","bundle_id":"com.apple.Preferences","fresh":true,"debug":false},
|
|
260
|
+
{"action":"kill_app","bundle_id":"com.apple.mobilesafari"},
|
|
261
|
+
{"action":"open_link","url":"myapp://profile/42"},
|
|
262
|
+
{"action":"tap","predicate":{"text_contains":"Wi-Fi","type":"button"}},
|
|
263
|
+
{"action":"tap","coords":{"x":200,"y":640}},
|
|
264
|
+
{"action":"double_tap","predicate":{"text":"Photo"}},
|
|
265
|
+
{"action":"two_finger_tap","predicate":{"text":"Map"}},
|
|
266
|
+
{"action":"long_press","predicate":{"text":"Item"},"duration_ms":1000},
|
|
267
|
+
{"action":"pinch","predicate":{"text_contains":"Map"},"scale":0.5,"velocity":1.0},
|
|
268
|
+
{"action":"type","text":"\${query}","predicate":{"type":"input"},"clear_first":true,"dismiss_keyboard":true},
|
|
269
|
+
{"action":"type_secret","secret_id":"test-account-password","predicate":{"type":"input","text_contains":"Password"}},
|
|
270
|
+
{"action":"clear","predicate":{"type":"input"}},
|
|
271
|
+
{"action":"toggle","predicate":{"type":"switch","text_contains":"Wi-Fi"},"state":"on"},
|
|
272
|
+
{"action":"press_key","key":"enter"},
|
|
273
|
+
{"action":"swipe","direction":"up","distance":"medium"},
|
|
274
|
+
{"action":"swipe","from_coords":{"x":200,"y":600},"to_coords":{"x":200,"y":200},"duration_ms":300},
|
|
275
|
+
{"action":"scroll","direction":"down","to_element":{"predicate":{"text":"Privacy"}},"predicate":{"type":"scrollview"},"max_scrolls":10,"amount":"page"},
|
|
276
|
+
{"action":"drag","from":{"predicate":{"text":"Item"}},"to_element":{"predicate":{"text":"Trash"}},"press_duration_ms":500,"hold_duration_ms":200,"duration_ms":500},
|
|
277
|
+
{"action":"drag","from_coords":{"x":100,"y":400},"to_coords":{"x":300,"y":400}},
|
|
278
|
+
{"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}]},
|
|
279
|
+
{"action":"navigate","target":"home"},
|
|
280
|
+
{"action":"set_location","lat":40.7128,"lon":-74.0060},
|
|
281
|
+
{"action":"reset_location"},
|
|
282
|
+
{"action":"siri","prompt":"Search YouTube for cat videos"},
|
|
283
|
+
{"action":"if_exists","predicate":{"text":"Allow"},"then":[{"action":"tap","predicate":{"text":"Allow"}}],"else":[{"action":"tap","predicate":{"text":"Don't Allow"}}]},
|
|
284
|
+
{"action":"delay","duration_ms":1000},
|
|
285
|
+
{"action":"wait_for","predicate":{"text":"Welcome"},"timeout_ms":5000,"poll_interval_ms":500},
|
|
286
|
+
{"action":"assert_exists","predicate":{"text":"Success"},"timeout_ms":3000},
|
|
287
|
+
{"action":"assert_not_exists","predicate":{"text":"Error"}},
|
|
288
|
+
{"action":"assert_count","predicate":{"type":"cell"},"count":5},
|
|
289
|
+
{"action":"assert_screen_changed","threshold_percent":15},
|
|
290
|
+
{"action":"metrics_start","types":["system_cpu","fps"],"interval_ms":1000,"label":"login_flow","capture_logs":true,"thresholds":{"cpu_high":80,"fps_low":45}},
|
|
291
|
+
{"action":"metrics_stop","format":"summary"},
|
|
292
|
+
{"action":"record_start","file_path":"/tmp/mobai/rec"},
|
|
293
|
+
{"action":"record_stop"},
|
|
294
|
+
{"action":"select_web_context","url_contains":"google.com"},
|
|
295
|
+
{"action":"navigate","context":"web","url":"https://example.com"},
|
|
296
|
+
{"action":"tap","context":"web","predicate":{"css_selector":"button.submit"}},
|
|
297
|
+
{"action":"type","context":"web","predicate":{"css_selector":"input#email"},"text":"user@example.com","clear_first":true},
|
|
298
|
+
{"action":"execute_js","context":"web","script":"return document.title","async":false},
|
|
299
|
+
{"action":"screenshot","file_path":"/tmp/mobai","name":"final_state"},
|
|
300
|
+
{"action":"wait_for","stable":true,"timeout_ms":3000},
|
|
301
|
+
{"action":"observe","include":["ui_tree"],"only_visible":true,"filter":{"text_regex":"Settings|Wi-Fi"},"store_as":"end_state"}
|
|
302
|
+
]}
|
|
217
303
|
|
|
218
|
-
|
|
219
|
-
{"
|
|
220
|
-
|
|
221
|
-
{"action":"tap","predicate":{"text_contains":"Wi-Fi"}},
|
|
222
|
-
{"action":"wait_for","predicate":{"type":"switch"},"timeout_ms":3000}
|
|
223
|
-
]}`,
|
|
304
|
+
Predicate - object passed in a step's "predicate"/"from"/"to_element". Combine fields (AND); prefer text_contains over exact text. All native fields:
|
|
305
|
+
{"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"}}
|
|
306
|
+
Web predicate (context:"web") uses instead: {"css_selector":"button.submit"}.`,
|
|
224
307
|
inputSchema: {
|
|
225
308
|
type: "object",
|
|
226
309
|
properties: {
|
|
@@ -406,8 +489,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
406
489
|
}
|
|
407
490
|
case "list_apps":
|
|
408
491
|
return textResult(await doGet(`/devices/${args?.device_id}/apps`));
|
|
409
|
-
case "install_app":
|
|
410
|
-
|
|
492
|
+
case "install_app": {
|
|
493
|
+
const body = { path: args?.path };
|
|
494
|
+
if (args?.resign) {
|
|
495
|
+
body.resign = true;
|
|
496
|
+
if (args?.apple_id)
|
|
497
|
+
body.appleId = args.apple_id;
|
|
498
|
+
if (args?.password)
|
|
499
|
+
body.password = args.password;
|
|
500
|
+
if (args?.profile_path)
|
|
501
|
+
body.profilePath = args.profile_path;
|
|
502
|
+
if (args?.cert_path)
|
|
503
|
+
body.certPath = args.cert_path;
|
|
504
|
+
if (args?.key_path)
|
|
505
|
+
body.keyPath = args.key_path;
|
|
506
|
+
}
|
|
507
|
+
return textResult(await doPost(`/devices/${args?.device_id}/install-app`, body));
|
|
508
|
+
}
|
|
411
509
|
case "uninstall_app":
|
|
412
510
|
return textResult(await doDelete(`/devices/${args?.device_id}/apps/${encodeURIComponent(args?.bundle_id)}`));
|
|
413
511
|
// DSL execution
|
package/dist/resources.js
CHANGED
|
@@ -123,7 +123,7 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
123
123
|
<screenshot-tools>
|
|
124
124
|
get_screenshot — fast low-quality image for LLM visual analysis.
|
|
125
125
|
save_screenshot — full-quality PNG for reporting, debugging, or sharing.
|
|
126
|
-
|
|
126
|
+
A screenshot is a single settled frame — it cannot capture motion. Anything transient (animations, transitions, loading spinners; a screen transition is often only ~300ms) will be missed or caught mid-frame. To verify transitional behavior, use record_start/record_stop, which samples continuously and flags suspicious frames.
|
|
127
127
|
</screenshot-tools>
|
|
128
128
|
|
|
129
129
|
<infinite-scrolling>To collect data from infinite-scrolling views (feeds, search results), scroll to load a batch first, then observe with only_visible:false to get all loaded items in one go.</infinite-scrolling>
|
|
@@ -132,6 +132,7 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
132
132
|
Element not visible — use scroll with to_element to find it.
|
|
133
133
|
App launches and page transitions take time — use wait_for or delay.
|
|
134
134
|
Observe before acting on unfamiliar screens.
|
|
135
|
+
NO_MATCH / failed assert_exists: if the element exists off-screen, the error lists it under "candidates" — scroll to bring it into view (off-screen elements cannot be tapped). Empty candidates means it is genuinely absent or not yet rendered.
|
|
135
136
|
</troubleshooting>
|
|
136
137
|
</guide>
|
|
137
138
|
|
|
@@ -145,6 +146,8 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
145
146
|
<field name="text_contains" type="string">Substring, case-insensitive — preferred for most matching</field>
|
|
146
147
|
<field name="text_starts_with" type="string">Prefix match</field>
|
|
147
148
|
<field name="text_regex" type="string">Regex pattern — use for dynamic text (numbers, dates, counts)</field>
|
|
149
|
+
<field name="value" type="string">Exact match on the element's entered/current value (not its label/placeholder). Use to verify what was typed into a field — text matching sees the placeholder, value sees the content. Shown as content="..." in the UI tree. Secure fields are masked, so only length/non-empty is meaningful.</field>
|
|
150
|
+
<field name="value_contains" type="string">Substring match (case-insensitive) on the entered/current value</field>
|
|
148
151
|
<field name="type" type="string">button, input, switch, text, image, cell, scrollview</field>
|
|
149
152
|
<field name="accessibility_id" type="string">Exact match on the #id shown in UI tree (without the # prefix)</field>
|
|
150
153
|
<field name="enabled" type="bool">Enabled state</field>
|
|
@@ -158,7 +161,6 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
158
161
|
|
|
159
162
|
<predicate context="web">
|
|
160
163
|
<field name="css_selector" type="string">CSS selector</field>
|
|
161
|
-
<field name="xpath" type="string">XPath expression</field>
|
|
162
164
|
</predicate>
|
|
163
165
|
|
|
164
166
|
<failure-strategy>
|
|
@@ -173,8 +175,11 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
173
175
|
|
|
174
176
|
<action name="open_app">
|
|
175
177
|
<field name="bundle_id" required="yes"/>
|
|
178
|
+
<field name="fresh" type="bool">Kill the app before launching to ensure a clean start from the home screen. Use when the app may have been left on an arbitrary screen from a previous run.</field>
|
|
179
|
+
<field name="debug" type="bool">ONLY for debug-built apps (e.g. Flutter dev builds, Xcode debug builds) that need a debugger attached to run. Attaches debugserver, streams stdout/stderr to a log file; result has log_path. Do NOT use for release/App Store apps — they launch fine with debug: false.</field>
|
|
176
180
|
<example>{"action": "open_app", "bundle_id": "com.apple.Preferences"}</example>
|
|
177
|
-
<
|
|
181
|
+
<example>{"action": "open_app", "bundle_id": "com.apple.Preferences", "fresh": true}</example>
|
|
182
|
+
<note>If open_app fails or the app disappears immediately after launch, the app has likely crashed. Do NOT retry or try alternative launch methods — start crash investigation instead. Use debug: true (or metrics_start with capture_logs: true) to capture device logs, then diagnose.</note>
|
|
178
183
|
</action>
|
|
179
184
|
|
|
180
185
|
<action name="tap">
|
|
@@ -211,6 +216,21 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
211
216
|
<example>{"action": "type", "text": "Hello", "predicate": {"type": "input"}, "clear_first": true}</example>
|
|
212
217
|
</action>
|
|
213
218
|
|
|
219
|
+
<action name="type_secret">
|
|
220
|
+
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.
|
|
221
|
+
<field name="secret_id" required="yes">Id of a stored credential (see list_secrets)</field>
|
|
222
|
+
<field name="predicate">Optional target field to focus first</field>
|
|
223
|
+
<field name="clear_first" type="bool"/>
|
|
224
|
+
<field name="dismiss_keyboard" type="bool" default="false"/>
|
|
225
|
+
<example>{"action": "type_secret", "secret_id": "test-account-password", "predicate": {"type": "input", "text_contains": "Password"}}</example>
|
|
226
|
+
</action>
|
|
227
|
+
|
|
228
|
+
<action name="clear">
|
|
229
|
+
Clear a field's text without typing. With a predicate, focuses that field first; without one, clears the currently focused field.
|
|
230
|
+
<field name="predicate">Optional target element</field>
|
|
231
|
+
<example>{"action": "clear", "predicate": {"type": "input"}}</example>
|
|
232
|
+
</action>
|
|
233
|
+
|
|
214
234
|
<action name="swipe">
|
|
215
235
|
Direction = finger movement. Use direction OR from_coords/to_coords.
|
|
216
236
|
<field name="direction">up, down, left, right</field>
|
|
@@ -223,11 +243,13 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
223
243
|
|
|
224
244
|
<action name="scroll">
|
|
225
245
|
Direction = semantic (where to look), not finger movement.
|
|
226
|
-
<field name="direction" required="yes">down (look below), up (look above)
|
|
246
|
+
<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>
|
|
247
|
+
<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>
|
|
227
248
|
<field name="to_element" type="TargetElement"/>
|
|
228
249
|
<field name="max_scrolls" type="int" default="10"/>
|
|
229
250
|
<field name="amount">small, page, full</field>
|
|
230
251
|
<example>{"action": "scroll", "direction": "down", "to_element": {"predicate": {"text": "Privacy"}}, "max_scrolls": 10}</example>
|
|
252
|
+
<example>{"action": "scroll", "direction": "right", "predicate": {"type": "scrollview"}}</example>
|
|
231
253
|
<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>
|
|
232
254
|
</action>
|
|
233
255
|
|
|
@@ -243,6 +265,12 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
243
265
|
<example>{"action": "drag", "from": {"predicate": {"text": "App"}}, "to_element": {"predicate": {"text": "Folder"}}, "press_duration_ms": 500, "hold_duration_ms": 200}</example>
|
|
244
266
|
</action>
|
|
245
267
|
|
|
268
|
+
<action name="drag_path">
|
|
269
|
+
<field name="points" type="array of {x, y, duration_ms}" required="true">Single-finger drag along a multi-point path. Each point's duration_ms is the time to move to it from the previous point. The first point is the touch-down location and its duration_ms is an optional initial press-hold (omit or 0 for none). Needs at least 2 points; every point after the first must have duration_ms > 0. Use this (not drag) for swipe-path gestures like unlock patterns, freeform draws, or curved scrolls.</field>
|
|
270
|
+
<example>{"action": "drag_path", "points": [{"x": 100, "y": 400}, {"x": 150, "y": 300, "duration_ms": 150}, {"x": 300, "y": 500, "duration_ms": 300}]}</example>
|
|
271
|
+
<example>{"action": "drag_path", "points": [{"x": 100, "y": 400, "duration_ms": 200}, {"x": 300, "y": 400, "duration_ms": 250}]}</example>
|
|
272
|
+
</action>
|
|
273
|
+
|
|
246
274
|
<action name="press_key">
|
|
247
275
|
<field name="key" required="yes"/>
|
|
248
276
|
<platform name="android">enter, tab, delete, escape, volume_up, volume_down, home, back, recent_apps, mute, power, play_pause, next, previous</platform>
|
|
@@ -319,8 +347,15 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
319
347
|
<example>{"action": "reset_location"}</example>
|
|
320
348
|
</action>
|
|
321
349
|
|
|
350
|
+
<action name="open_link">
|
|
351
|
+
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.
|
|
352
|
+
<field name="url" required="yes">Full URL or deep link, e.g. https://example.com or myapp://profile/42</field>
|
|
353
|
+
<example>{"action": "open_link", "url": "myapp://profile/42"}</example>
|
|
354
|
+
<example>{"action": "open_link", "url": "https://example.com"}</example>
|
|
355
|
+
</action>
|
|
356
|
+
|
|
322
357
|
<action name="siri">
|
|
323
|
-
iOS only. Sends a voice command to Siri
|
|
358
|
+
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.
|
|
324
359
|
Use for triggering SiriKit intents and App Shortcuts registered by apps (media playback, messaging, banking shortcuts, etc.).
|
|
325
360
|
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.
|
|
326
361
|
<field name="prompt" required="yes">Voice command text</field>
|
|
@@ -342,7 +377,7 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
342
377
|
</action>
|
|
343
378
|
|
|
344
379
|
<action name="tap/type/press_key/wait_for/if_exists">
|
|
345
|
-
Same fields as native but use css_selector
|
|
380
|
+
Same fields as native but use css_selector in predicate.
|
|
346
381
|
<example>{"action": "tap", "context": "web", "predicate": {"css_selector": "button.submit"}}</example>
|
|
347
382
|
<example>{"action": "type", "context": "web", "predicate": {"css_selector": "input#email"}, "text": "user@example.com", "clear_first": true}</example>
|
|
348
383
|
Web press_key keys: enter, tab, delete, escape.
|
|
@@ -383,6 +418,15 @@ const DEVICE_AUTOMATION_REF = `<device-automation-reference>
|
|
|
383
418
|
<example>{"action": "assert_screen_changed", "threshold_percent": 15}</example>
|
|
384
419
|
<note>Pattern: observe(screenshot) then action then delay then assert_screen_changed. Do NOT observe after the action — it resets the baseline.</note>
|
|
385
420
|
</action>
|
|
421
|
+
|
|
422
|
+
<action name="ai_assert">
|
|
423
|
+
<field name="assert_prompt" required="yes"/>
|
|
424
|
+
<field name="include" type="[]string" note="opt-in extra context: screenshot, ocr (iOS). UI tree + the source script are always included."/>
|
|
425
|
+
<field name="timeout_ms" type="int" note="bounds the verdict (LLM/CLI reply), excluding context gathering. Default 60000."/>
|
|
426
|
+
<field name="message" note="prefixes the failure reason"/>
|
|
427
|
+
<example>{"action": "ai_assert", "assert_prompt": "the reply answers the user's question and is not an error", "include": ["screenshot"]}</example>
|
|
428
|
+
<note>Judges a natural-language assertion with the user's configured agent — either an LLM API provider (direct call) or Claude Code (spawned, reports back via report_assertion). Use for non-deterministic content (AI/LLM output, dynamic feeds) where exact-match assertions don't work. Treat as a soft assertion — it is non-deterministic.</note>
|
|
429
|
+
</action>
|
|
386
430
|
</assertions>
|
|
387
431
|
|
|
388
432
|
<metrics>
|
|
@@ -435,7 +479,7 @@ const TESTING_REF = `<testing-reference>
|
|
|
435
479
|
|
|
436
480
|
<rules>
|
|
437
481
|
<rule>Never ask the user for information you can get yourself — use observe, list_apps, get_ui_tree.</rule>
|
|
438
|
-
<rule>
|
|
482
|
+
<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>
|
|
439
483
|
<rule>Always use predicates over coordinates — predicates survive layout changes.</rule>
|
|
440
484
|
<rule>Always prefer UI tree and OCR over screenshots for element discovery.</rule>
|
|
441
485
|
<rule>Your first action must be observe — never ask the user first.</rule>
|
|
@@ -495,6 +539,7 @@ const TESTING_REF = `<testing-reference>
|
|
|
495
539
|
|
|
496
540
|
<actions>
|
|
497
541
|
app "com.example.app" — launch app
|
|
542
|
+
app "com.example.app" fresh — kill + launch for clean state
|
|
498
543
|
kill_app "com.example.app" — force-close app
|
|
499
544
|
tap "Text" — tap by text
|
|
500
545
|
tap "Field" near "Label" — tap near another element
|
|
@@ -514,6 +559,8 @@ const TESTING_REF = `<testing-reference>
|
|
|
514
559
|
type ~"Field" → "text" — partial match field
|
|
515
560
|
type "Field" → "text" clear — clear first
|
|
516
561
|
type "Field" → "text" keep_keyboard — keep keyboard open
|
|
562
|
+
type_secret "Field" → "secret-id" - type a stored credential (value never in script/logs)
|
|
563
|
+
type_secret "secret-id" - type credential into focused field
|
|
517
564
|
swipe up|down|left|right — swipe direction
|
|
518
565
|
swipe up distance:long — short/medium/long
|
|
519
566
|
swipe up duration:500 — custom duration (ms)
|
|
@@ -521,11 +568,13 @@ const TESTING_REF = `<testing-reference>
|
|
|
521
568
|
scroll down — scroll
|
|
522
569
|
scroll down to "Element" — scroll until visible
|
|
523
570
|
scroll down to "Footer" max_scrolls:10 — limit attempts
|
|
571
|
+
scroll right in type:scrollview - target a specific container
|
|
524
572
|
toggle "Wi-Fi" on|off — toggle switch
|
|
525
573
|
toggle type:switch near "Wi-Fi" on — modifier-only
|
|
526
574
|
drag "Item" to "Trash" — drag element
|
|
527
575
|
drag 100,200 to 300,400 duration:500 — coordinate drag
|
|
528
576
|
drag "App" to "Folder" press_duration:500 hold_duration:200 — press-hold-move-hold-release
|
|
577
|
+
drag_path 100,400 150,300:150 300,500:300 - multi-point path (X,Y:moveMs, first point's :ms = optional press-hold)
|
|
529
578
|
wait_for "Element" timeout:5000 — wait for element
|
|
530
579
|
wait_for type:button bounds:bottom_half timeout:3000 — modifier-only
|
|
531
580
|
delay 1000 — wait N ms
|
|
@@ -539,6 +588,7 @@ const TESTING_REF = `<testing-reference>
|
|
|
539
588
|
paste_text "Field" — paste clipboard into element
|
|
540
589
|
set_location 40.7128,-74.0060 — simulate GPS location (lat,lon)
|
|
541
590
|
reset_location — stop location simulation
|
|
591
|
+
open_link "myapp://profile/42" - open a URL or deep link (routes to the app)
|
|
542
592
|
siri "Search YouTube for cats" — invoke Siri with voice command (iOS only)
|
|
543
593
|
observe — observe screen
|
|
544
594
|
screenshot "path.png" — take screenshot
|
|
@@ -548,6 +598,8 @@ const TESTING_REF = `<testing-reference>
|
|
|
548
598
|
assert_exists "Element" — element is on screen
|
|
549
599
|
assert_not_exists "Element" — element is NOT on screen
|
|
550
600
|
assert_exists "Header" bounds:top_right — with region filter
|
|
601
|
+
assert_exists value:"hello" — assert a field's entered value (exact); sees typed content, not placeholder
|
|
602
|
+
assert_exists value_contains:"@mail" — assert a substring of the entered value
|
|
551
603
|
assert_count "Cell" expected:5 — element count
|
|
552
604
|
checkpoint "name" — mark checkpoint
|
|
553
605
|
</assertions>
|
|
@@ -620,9 +672,10 @@ const TESTING_REF = `<testing-reference>
|
|
|
620
672
|
When the user asks to create an API from a mobile app flow:
|
|
621
673
|
1. Observe the app and understand the flow
|
|
622
674
|
2. Write a .mob script with # Param: declarations for inputs and extract actions for outputs
|
|
623
|
-
3.
|
|
624
|
-
4.
|
|
625
|
-
5.
|
|
675
|
+
3. Use app "bundle.id" fresh to ensure a clean start — the app may be left on any screen from a previous call
|
|
676
|
+
4. Save it to {MOBAI_DATA_DIR}/apis/{name}.mob — flat (gmail-send.mob) or nested (gmail/send.mob)
|
|
677
|
+
5. Test it with test_run using project_dir: {MOBAI_DATA_DIR}/apis/ and case_path: {name}.mob
|
|
678
|
+
6. List available APIs: GET /api/v1/apis
|
|
626
679
|
Call an API: POST /api/v1/apis/run/{name} with {"device_id": "...", "params": {...}}
|
|
627
680
|
The {name} segment is the path inside apis/ minus the .mob extension.
|
|
628
681
|
API runs do not persist results to .mobai/runs/ — only the extracted values come back in the response.
|
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.
|
|
9
|
+
"version": "2.4.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "mobai-mcp",
|
|
14
|
-
"version": "2.
|
|
14
|
+
"version": "2.4.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|