mobridge-mcp 0.1.0 → 0.2.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.
@@ -66,19 +66,26 @@
66
66
  "note": "APP ok; CLIPBOARD limited; no general notification listener."
67
67
  },
68
68
  "mobile.batch": {
69
- "level": "FULL"
69
+ "level": "NONE",
70
+ "reasonCode": "NOT_IMPLEMENTED_ON_IOS",
71
+ "note": "The Runner serves no batch method and nothing composes it host-side. Advertised FULL until the I10 parity gate proved otherwise."
70
72
  },
71
73
  "mobile.gesture.perform": {
72
74
  "level": "FULL"
73
75
  },
74
76
  "mobile.screen.scrollTo": {
75
- "level": "FULL"
77
+ "level": "PARTIAL",
78
+ "reasonCode": "BOUNDED_SWIPE_SEARCH",
79
+ "note": "Host-side bounded swipe search; the query is the only stop condition. No native scroll-to-element on iOS."
76
80
  },
77
81
  "mobile.screen.capture": {
78
- "level": "FULL"
82
+ "level": "NONE",
83
+ "reasonCode": "NOT_IMPLEMENTED_ON_IOS",
84
+ "note": "The Runner serves no screenshot method yet."
79
85
  },
80
86
  "mobile.input.text": {
81
- "level": "FULL"
87
+ "level": "FULL",
88
+ "note": "Host-side: routed to the Runner's screen.act SET_TEXT (same re-resolution and evidence)."
82
89
  },
83
90
  "mobile.workflow.run": {
84
91
  "level": "FULL"
@@ -108,16 +115,22 @@
108
115
  },
109
116
  "mobile.app.control": {
110
117
  "level": "FULL"
118
+ },
119
+ "mobile.screen.target": {
120
+ "level": "FULL",
121
+ "note": "Resolved host-side in TS (the Runner has no screen.target): ancestor-chain collapse when actionable:true."
111
122
  }
112
123
  },
113
124
  "physical": {
114
125
  "mobile.device.state": {
115
126
  "level": "PARTIAL",
116
- "reasonCode": "ORIENTATION_AND_APPSTATE_ONLY"
127
+ "reasonCode": "ORIENTATION_AND_APPSTATE_ONLY",
128
+ "note": "Host-side from the snapshot; battery/network/locale/storage are not available to a UI-test process."
117
129
  },
118
130
  "mobile.app.list": {
119
131
  "level": "PARTIAL",
120
- "reasonCode": "DEVICECTL_OR_MDM"
132
+ "reasonCode": "DEVICECTL_OR_MDM",
133
+ "note": "Host-side via devicectl/simctl; bundle ids only."
121
134
  },
122
135
  "mobile.app.forceStop": {
123
136
  "level": "NONE",
@@ -164,9 +177,10 @@
164
177
  },
165
178
  "mobile.screen.inspect": {
166
179
  "level": "PARTIAL",
167
- "reasonCode": "DELTA_ONLY_ON_IOS",
180
+ "reasonCode": "DELTA_AND_TARGET_ONLY_ON_IOS",
168
181
  "allowedOperations": [
169
- "DELTA"
182
+ "DELTA",
183
+ "TARGET"
170
184
  ]
171
185
  }
172
186
  },
@@ -237,4 +251,4 @@
237
251
  }
238
252
  }
239
253
  }
240
- }
254
+ }
@@ -34,6 +34,8 @@
34
34
  "sessionId": { "type": "string" },
35
35
  "capabilities": { "type": "array", "items": { "type": "string" } },
36
36
  "expiresInMinutes": { "type": "integer" },
37
+ "simulated": { "type": "boolean", "description": "TRUE when this session is the built-in fixture backend, not a real device. Nothing done in a simulated session reaches hardware." },
38
+ "warning": { "type": "string", "description": "Human-readable caveat about the session, e.g. that it is simulated and how to reach a real device." },
37
39
  "platform": { "enum": ["ANDROID", "IOS"], "description": "Session platform. Optional for back-compat." },
38
40
  "mode": { "type": ["string", "null"], "description": "iOS driver mode (PHYSICAL|SIMULATOR); null on Android." },
39
41
  "supportedTools": { "type": "array", "items": { "type": "string" }, "description": "Tool names actually advertised this session (platform ∩ profile). Informational; gating is at registration." },
package/integrity.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "buildId": "38d941d-20260804170901",
3
- "bundleSha256": "0bbc92d88bbe706644d0099c0cdc46bc2b1934674826e07a865f85c3332a8baa"
2
+ "buildId": "024caf7-20260805172927",
3
+ "bundleSha256": "8ce65e79be14dc229d97b3657b648e21488e5c702d7a2381e987c63cb75965dd"
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 = "38d941d-20260804170901"; // pack-npx stamps the real build id
17
+ const BUILD_ID = "024caf7-20260805172927"; // 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.1.0",
3
+ "version": "0.2.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",
@@ -63,6 +63,12 @@ final class BridgeServer: XCTestCase {
63
63
 
64
64
  // MARK: dispatch
65
65
 
66
+ /// Every method this Runner serves. Keep in lockstep with the switch in the dispatcher below.
67
+ static let servedMethods = [
68
+ "HELLO", "PING", "SHUTDOWN", "bridge.methods",
69
+ "device.connect", "app.open", "app.terminate", "screen.get", "screen.act", "gesture.perform", "system.action",
70
+ ]
71
+
66
72
  private func handle(_ req: [String: Any]) -> ([String: Any], Bool) {
67
73
  let requestId = req["requestId"] as? Int ?? 0
68
74
  let method = req["method"] as? String ?? ""
@@ -82,12 +88,30 @@ final class BridgeServer: XCTestCase {
82
88
  return ok(["protocolVersion": 1, "runnerInstanceId": runnerInstanceId, "sessionToken": sessionToken, "maxPayloadBytes": 16 * 1024 * 1024, "runnerState": "READY"])
83
89
  case "PING":
84
90
  return ok(["pong": true])
91
+ case "bridge.methods":
92
+ // The Runner's OWN list of served methods — the single source of truth the capability
93
+ // manifest is checked against (I10). Hand-maintaining the manifest is what let it claim
94
+ // FULL for methods that were never implemented; this list cannot drift from the switch
95
+ // below without someone editing it, and the parity gate fails loudly when it does.
96
+ return ok(["methods": Self.servedMethods])
85
97
  case "SHUTDOWN":
86
98
  return ([ "requestId": requestId, "result": ["bye": true] ], true)
87
99
  case "device.connect":
88
100
  return ok(["deviceId": params["deviceId"] as? String ?? "ios-sim", "sessionId": runnerInstanceId, "capabilities": ["screen.get", "screen.find", "screen.act", "screen.wait", "screen.diff"]])
89
101
  case "app.open":
90
102
  return appOpen(params, ok: ok, err: err)
103
+ case "app.terminate":
104
+ // The real terminate for a PHYSICAL device. The host path shelled out to `xcrun simctl
105
+ // terminate`, which is Simulator-only — it could never have worked on hardware. XCUITest
106
+ // owns the app process here, so this is both correct and the only thing available.
107
+ let bid = params["bundleId"] as? String ?? ""
108
+ guard !bid.isEmpty else {
109
+ return err("ACTION_REJECTED", "app.terminate needs a bundleId.", ["Pass the target app's bundle identifier."])
110
+ }
111
+ let target = XCUIApplication(bundleIdentifier: bid)
112
+ let wasRunning = target.state != .notRunning
113
+ target.terminate()
114
+ return ok(["terminated": true, "bundleId": bid, "wasRunning": wasRunning])
91
115
  case "screen.get":
92
116
  return ok(snapshot())
93
117
  case "screen.act":
@@ -200,11 +224,19 @@ final class BridgeServer: XCTestCase {
200
224
  private func screenAct(_ p: [String: Any], ok: ([String: Any]) -> ([String: Any], Bool), err: (String, String, [String]) -> ([String: Any], Bool)) -> ([String: Any], Bool) {
201
225
  let before = snapshot()
202
226
  let verb = p["verb"] as? String ?? "CLICK"
227
+ // Narrow the live re-query to the element TYPE TS resolved, not just its label: a table row and
228
+ // the static text inside it share a label, so a label-only query counted 2 and this defence
229
+ // refused every list tap. `nativeType` is this Runner's own "XCUIElementType<raw>" string coming
230
+ // back, so no role mapping is invented here; absent or unparsable, the scope stays .any and the
231
+ // behaviour is exactly what it was.
232
+ let scope: XCUIElement.ElementType = (p["nativeType"] as? String)
233
+ .flatMap { UInt( $0.replacingOccurrences(of: "XCUIElementType", with: "")) }
234
+ .flatMap { XCUIElement.ElementType(rawValue: $0) } ?? .any
203
235
  let query: XCUIElementQuery
204
236
  if let aid = p["automationId"] as? String, !aid.isEmpty {
205
- query = app.descendants(matching: .any).matching(identifier: aid)
237
+ query = app.descendants(matching: scope).matching(identifier: aid)
206
238
  } else if let label = p["label"] as? String, !label.isEmpty {
207
- query = app.descendants(matching: .any).matching(NSPredicate(format: "label == %@", label))
239
+ query = app.descendants(matching: scope).matching(NSPredicate(format: "label == %@", label))
208
240
  } else {
209
241
  return err("NODE_NOT_FOUND", "screen.act needs automationId or label.", ["Resolve a NodeRef first."])
210
242
  }