mobridge-mcp 0.2.0 → 0.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.
@@ -24,6 +24,10 @@
24
24
  "mobile.device.connect": {
25
25
  "level": "FULL"
26
26
  },
27
+ "mobile.device.list": {
28
+ "level": "FULL",
29
+ "note": "Host-side enumeration, platform-neutral. It was omitted here while staying registered and callable, so an iOS session's supportedTools hid a tool that worked — a contract lie in the direction nobody checks for."
30
+ },
27
31
  "mobile.app.open": {
28
32
  "level": "FULL",
29
33
  "allowedOperations": [
@@ -57,13 +61,24 @@
57
61
  "note": "iOS has no global BACK/RECENTS/NOTIFICATION_SHADE; BACK is a semantic UI action (find nav button + act)."
58
62
  },
59
63
  "mobile.capability.invoke": {
60
- "level": "PARTIAL",
61
- "allowedOperations": [
62
- "APP",
63
- "CLIPBOARD"
64
- ],
65
- "reasonCode": "NO_NOTIFICATION_LISTENER",
66
- "note": "APP ok; CLIPBOARD limited; no general notification listener."
64
+ "level": "NONE",
65
+ "reasonCode": "NO_BRIDGE_METHOD",
66
+ "note": "The Runner serves no capability.invoke and nothing composes it host-side; every call answered ACTION_UNSUPPORTED. Advertised PARTIAL[APP,CLIPBOARD] until a live tool sweep called it."
67
+ },
68
+ "mobile.screen.watch": {
69
+ "level": "NONE",
70
+ "reasonCode": "NO_EVENT_STREAM_ON_IOS",
71
+ "note": "watch blocks on the Android companion's accessibility event stream; XCUITest exposes no equivalent. Use screen.wait (polling) on iOS."
72
+ },
73
+ "mobile.screen.analyze": {
74
+ "level": "NONE",
75
+ "reasonCode": "DEPENDS_ON_SCREEN_OBSERVE",
76
+ "note": "analyze is host-side but reads screen.observe, which the Runner does not serve."
77
+ },
78
+ "mobile.screen.record": {
79
+ "level": "NONE",
80
+ "reasonCode": "ANDROID_ONLY",
81
+ "note": "Host-side adb screenrecord. iOS recording would be a Runner/simctl path that does not exist."
67
82
  },
68
83
  "mobile.batch": {
69
84
  "level": "NONE",
@@ -105,13 +120,9 @@
105
120
  "replacesAndroidTool": "mobile.diag.batterystats"
106
121
  },
107
122
  "runtime.setup": {
108
- "level": "PARTIAL",
109
- "reasonCode": "OPEN_AND_VERIFY_ARE_ADVISORY_ON_IOS",
110
- "allowedOperations": [
111
- "STATUS",
112
- "OPEN",
113
- "VERIFY"
114
- ]
123
+ "level": "NONE",
124
+ "reasonCode": "ANDROID_COMPANION_ONLY",
125
+ "note": "setup.status/open/verify are the Android companion's own permission centre. The Runner serves none of them; iOS setup is device.connect's provisioning path."
115
126
  },
116
127
  "mobile.app.control": {
117
128
  "level": "FULL"
@@ -177,10 +188,11 @@
177
188
  },
178
189
  "mobile.screen.inspect": {
179
190
  "level": "PARTIAL",
180
- "reasonCode": "DELTA_AND_TARGET_ONLY_ON_IOS",
191
+ "reasonCode": "NO_VISIBLE_OR_CHILDREN_ON_IOS",
181
192
  "allowedOperations": [
182
193
  "DELTA",
183
- "TARGET"
194
+ "TARGET",
195
+ "APP"
184
196
  ]
185
197
  }
186
198
  },
@@ -244,9 +256,11 @@
244
256
  },
245
257
  "mobile.screen.inspect": {
246
258
  "level": "PARTIAL",
247
- "reasonCode": "DELTA_ONLY_ON_IOS",
259
+ "reasonCode": "NO_VISIBLE_OR_CHILDREN_ON_IOS",
248
260
  "allowedOperations": [
249
- "DELTA"
261
+ "DELTA",
262
+ "TARGET",
263
+ "APP"
250
264
  ]
251
265
  }
252
266
  }
@@ -39,7 +39,8 @@
39
39
  "description": "Whether the after-snapshot was captured on a settled screen."
40
40
  },
41
41
  "actionMethod": {
42
- "enum": ["ACCESSIBILITY_ACTION", "ACCESSIBILITY_ACTION_ANCESTOR", "GESTURE_FALLBACK"]
42
+ "enum": ["ACCESSIBILITY_ACTION", "ACCESSIBILITY_ACTION_ANCESTOR", "GESTURE_FALLBACK", "GLOBAL_ACTION"],
43
+ "description": "GLOBAL_ACTION = a device-level navigation key (Android performGlobalAction, iOS XCUIDevice press) — no node is targeted."
43
44
  },
44
45
 
45
46
  "actionRequest": {
@@ -50,7 +51,17 @@
50
51
  "action": { "$ref": "#/$defs/actionVerb" },
51
52
  "target": {
52
53
  "$ref": "msm:node-ref.schema.json",
53
- "description": "Required for node actions."
54
+ "description": "The NodeRef to act on, copied verbatim from a find/inspect result. Mutually exclusive with `find`."
55
+ },
56
+ "find": {
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "required": ["query"],
60
+ "description": "Locate the target and act in one call, instead of passing `target`. The query is resolved immediately before the action and re-resolved ONCE if the screen moved underneath, which closes the race the separate find-then-act pair leaves open. Matching several nodes is AMBIGUOUS_MATCH with candidates — it never picks one.",
61
+ "properties": {
62
+ "query": { "type": "object", "description": "SemanticQuery, same shape as mobile.screen.find." },
63
+ "actionable": { "type": "boolean", "default": true, "description": "Resolve to the nearest ACTIONABLE ancestor when the matched node cannot be acted on. Never climbs past an ancestor covering more than one match." }
64
+ }
54
65
  },
55
66
  "value": {
56
67
  "type": ["string", "null"],
@@ -87,6 +98,11 @@
87
98
  "generation": {
88
99
  "type": ["integer", "null"],
89
100
  "description": "Capture generation the target ref was built against (from screen.find/target). When set, the runtime hard-checks it and returns STALE_NODE_REF if the screen advanced — it never re-resolves a stale ref to a similar node."
101
+ },
102
+ "diffDetail": {
103
+ "enum": ["SUMMARY", "FULL"],
104
+ "default": "SUMMARY",
105
+ "description": "How much of the semantic delta observation.diff carries. SUMMARY caps each bucket at 8 entries and adds counts + truncated; FULL returns every node delta. A screen transition diffs hundreds of nodes and the caller rarely needs them all — observation.diffRef re-fetches the exact pair via mobile.screen.diff."
90
106
  }
91
107
  },
92
108
  "allOf": [
@@ -123,13 +139,34 @@
123
139
  },
124
140
  "target": {
125
141
  "type": "object",
126
- "description": "Human-readable identity of the acted node.",
142
+ "description": "Human-readable identity of the acted node. For app.open, `label` is the package OBSERVED in front afterwards and `requested` is what the call asked for — they differ whenever the launch did not land where it was aimed.",
127
143
  "properties": {
128
144
  "role": { "$ref": "msm:common.schema.json#/$defs/role" },
129
- "label": { "type": ["string", "null"] }
145
+ "label": { "type": ["string", "null"] },
146
+ "requested": { "type": ["string", "null"], "description": "app.open only: the package or deep link the caller asked for." }
130
147
  }
131
148
  },
132
- "mutationClass": {
149
+ "app": {
150
+ "type": "object",
151
+ "additionalProperties": true,
152
+ "description": "The foreground app OBSERVED after the action. Present on app.open and system.action so a caller learns where it actually ended up without a second round trip.",
153
+ "properties": {
154
+ "platform": { "type": ["string", "null"] },
155
+ "appId": { "type": ["string", "null"], "description": "Package name on Android, bundle id on iOS." },
156
+ "title": { "type": ["string", "null"] },
157
+ "generation": { "type": ["integer", "null"] }
158
+ }
159
+ },
160
+ "appInstallCheck": {
161
+ "enum": ["INSTALLED", "NOT_INSTALLED", "UNKNOWN_TIMEOUT", null],
162
+ "description": "app.open only (iOS): whether the target bundle was confirmed installed before launching. UNKNOWN_TIMEOUT means the inventory did not answer in time — the launch was attempted without asserting installation."
163
+ },
164
+ "warnings": {
165
+ "type": "array",
166
+ "items": { "type": "string" },
167
+ "description": "Non-fatal notices about how this result was produced (e.g. an unverified install state). Never a substitute for an error."
168
+ },
169
+ "mutationClass": {
133
170
  "enum": ["READ_ONLY", "REVERSIBLE", "SIDE_EFFECT", "EXTERNAL_SIDE_EFFECT", "DESTRUCTIVE"],
134
171
  "description": "Advisory, payload-derived classification of how mutating this specific action was. Metadata for the host, not a gate."
135
172
  },
@@ -23,7 +23,7 @@
23
23
  "$defs": {
24
24
  "code": {
25
25
  "enum": [
26
- "DEVICE_OFFLINE", "SESSION_EXPIRED", "PERMISSION_REQUIRED", "WINDOW_UNAVAILABLE",
26
+ "DEVICE_OFFLINE", "SESSION_EXPIRED", "PERMISSION_REQUIRED", "WINDOW_UNAVAILABLE", "DEVICE_LOCKED",
27
27
  "SECURE_WINDOW", "NODE_NOT_FOUND", "AMBIGUOUS_MATCH", "STALE_NODE_REF",
28
28
  "WEAK_REFERENCE_UNRESOLVABLE",
29
29
  "ACTION_UNSUPPORTED", "ACTION_REJECTED", "NO_EFFECT", "VERIFY_TIMEOUT",
@@ -0,0 +1,65 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "msm:scenario.schema.json",
4
+ "title": "ScenarioRecord",
5
+ "description": "A saved, replayable test workflow. Stores the QUERY that located each target, never the NodeRef it produced: refs are generation-scoped, so every replay re-resolves from scratch.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "scenarioVersion", "id", "intent", "platform", "status", "steps", "warnings", "recordedAt"],
9
+ "properties": {
10
+ "schemaVersion": { "type": "integer", "const": 1, "description": "File format. Independent of scenarioVersion." },
11
+ "scenarioVersion": { "type": "integer", "minimum": 1, "description": "The definition's revision. Owned by the store: 1 on create, previous+1 on update." },
12
+ "id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{1,63}$", "description": "Names the user-visible goal (settings-network), never the steps (tap-button-1)." },
13
+ "intent": { "type": "string", "minLength": 1, "description": "One sentence. This is how a later agent recognises the scenario as a match for a task." },
14
+ "platform": { "enum": ["android", "ios"], "description": "Execution constraint: a mismatch is rejected before the device is touched." },
15
+ "status": {
16
+ "enum": ["RECORDED", "VERIFIED"],
17
+ "description": "RECORDED on save. VERIFIED only when explicitly saved after a successful run — a run never rewrites the artifact."
18
+ },
19
+ "arrange": {
20
+ "type": "object",
21
+ "additionalProperties": false,
22
+ "description": "Precondition, deliberately limited to bringing one app to the front.",
23
+ "properties": { "packageName": { "type": "string" } }
24
+ },
25
+ "steps": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/step" } },
26
+ "warnings": { "type": "array", "items": { "type": "string" }, "description": "Why a step is not replayable as recorded: unverified action, unresolved target, redacted input." },
27
+ "recordedAt": { "type": "string" },
28
+ "recordedOn": {
29
+ "type": "object",
30
+ "additionalProperties": true,
31
+ "properties": { "deviceId": { "type": "string" }, "osVersion": { "type": "string" } }
32
+ }
33
+ },
34
+ "$defs": {
35
+ "step": {
36
+ "type": "object",
37
+ "additionalProperties": false,
38
+ "required": ["id", "action"],
39
+ "description": "The workflow step shape, plus inputRequired. One vocabulary, so replay is a delegation rather than a second executor.",
40
+ "properties": {
41
+ "id": { "type": "string", "minLength": 1 },
42
+ "find": {
43
+ "type": "object",
44
+ "additionalProperties": false,
45
+ "required": ["query"],
46
+ "properties": {
47
+ "query": { "type": "object", "description": "Re-resolved on every run. A target that moved is still found; a target that no longer exists fails rather than falling back to coordinates." },
48
+ "actionable": { "type": "boolean", "default": true }
49
+ }
50
+ },
51
+ "action": { "type": "string" },
52
+ "value": { "type": ["string", "null"] },
53
+ "inputRequired": {
54
+ "type": "boolean",
55
+ "description": "The value was redacted. RUN yields INPUT_REQUIRED and the caller supplies it for that run only; it is never written back."
56
+ },
57
+ "verify": { "type": "object", "description": "Recorded only when a verification actually occurred. Never inferred from the verb." },
58
+ "unresolved": { "type": "boolean", "description": "No query could be rebuilt for this step; kept for the reader, refused by RUN." },
59
+ "onError": { "enum": ["ABORT", "RETRY", "CONTINUE"], "default": "ABORT" },
60
+ "maxRetries": { "type": "integer", "minimum": 0, "default": 1 },
61
+ "pause": { "type": "boolean", "default": false }
62
+ }
63
+ }
64
+ }
65
+ }
@@ -59,7 +59,12 @@
59
59
  "packageName": { "$ref": "msm:common.schema.json#/$defs/packageName" },
60
60
  "bundleId": { "$ref": "msm:common.schema.json#/$defs/bundleId" },
61
61
  "deepLink": { "type": "string" },
62
- "verify": { "$ref": "msm:wait.schema.json" }
62
+ "expectPackage": {
63
+ "$ref": "msm:common.schema.json#/$defs/packageName",
64
+ "description": "The app you expect to be in front afterwards. Needed to VERIFY a deep link: without a packageName or this, the runtime has nothing to check the outcome against and returns CONDITION_UNKNOWN rather than guessing that any foreground change meant success."
65
+ },
66
+ "verify": { "$ref": "msm:wait.schema.json" },
67
+ "diffDetail": { "enum": ["SUMMARY", "FULL"], "default": "SUMMARY", "description": "How much of the semantic delta observation.diff carries. SUMMARY caps each bucket at 8 entries and adds counts + truncated; FULL returns every node delta. Use observation.diffRef with mobile.screen.diff when the full delta is wanted." }
63
68
  }
64
69
  },
65
70
  "output": {
package/integrity.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "buildId": "024caf7-20260805172927",
3
- "bundleSha256": "8ce65e79be14dc229d97b3657b648e21488e5c702d7a2381e987c63cb75965dd"
2
+ "buildId": "fa820e4-20260811154759",
3
+ "bundleSha256": "eee1c1832e3b057e5ec10683658c87759866d7a85e1c24ba4982b39808894bdd"
4
4
  }
package/loader.cjs CHANGED
@@ -14,7 +14,7 @@ const os = require("node:os");
14
14
  const path = require("node:path");
15
15
  const crypto = require("node:crypto");
16
16
 
17
- const BUILD_ID = "024caf7-20260805172927"; // pack-npx stamps the real build id
17
+ const BUILD_ID = "fa820e4-20260811154759"; // pack-npx stamps the real build id
18
18
  const ROOT = __dirname; // package root (loader.cjs sits here)
19
19
  const DIST = path.join(ROOT, "dist");
20
20
  const BUNDLE = path.join(DIST, "main.obf.cjs");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobridge-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "MoBridge MCP — a stdio MCP server exposing on-device Android/iOS UIs as fast semantic read / observe / find / act / verify tools plus screenshots. Ships an installer that wires it into Claude Code, Claude Desktop, Cursor and Codex.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -194,7 +194,7 @@ final class BridgeServer: XCTestCase {
194
194
  if let snap = try? app.snapshot() {
195
195
  return Xcui.toScreenSnapshot(snap, bundleId: bundle, deviceId: "ios-sim")
196
196
  }
197
- return ["schemaVersion": 1, "platform": "IOS", "nodes": [], "treeHash": "0", "packageName": bundle, "appId": bundle, "snapshotId": "s_empty", "deviceId": "ios-sim", "windowId": 0, "orientation": "portrait", "screenStateId": "ios:0", "compact": true, "timestamp": ISO8601DateFormatter().string(from: Date())]
197
+ return ["schemaVersion": 1, "platform": "IOS", "nodes": [], "treeHash": "0", "packageName": bundle, "appId": bundle, "snapshotId": "s_empty", "deviceId": "ios-sim", "windowId": 0, "orientation": "portrait", "screenStateId": "ios:0", "compact": true, "timestamp": Int(Date().timeIntervalSince1970 * 1000)]
198
198
  }
199
199
 
200
200
  private func appOpen(_ p: [String: Any], ok: ([String: Any]) -> ([String: Any], Bool), err: (String, String, [String]) -> ([String: Any], Bool)) -> ([String: Any], Bool) {
@@ -79,7 +79,9 @@ enum Xcui {
79
79
  return [
80
80
  "schemaVersion": 1, "platform": "IOS", "deviceId": deviceId,
81
81
  "snapshotId": "s_\(UUID().uuidString.prefix(8))",
82
- "timestamp": ISO8601DateFormatter().string(from: Date()),
82
+ // Epoch MILLISECONDS, per common.schema.json#/$defs/timestampMs. An ISO8601 string here
83
+ // made every iOS screen.get fail a strict client's output-schema check.
84
+ "timestamp": Int(Date().timeIntervalSince1970 * 1000),
83
85
  "packageName": bundleId, "appId": bundleId, "windowId": 0, "orientation": "portrait",
84
86
  "screenStateId": "ios:\(th.prefix(4))", "compact": true, "nodes": nodes, "treeHash": th,
85
87
  "truncated": truncated,