mobridge-mcp 0.0.9 → 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.
- package/dist/main.obf.cjs +1 -1
- package/dist/protocol/platform-capabilities.json +242 -83
- package/dist/protocol/schemas/tools.schema.json +2 -0
- package/integrity.json +2 -2
- package/loader.cjs +1 -1
- package/package.json +1 -1
- package/resources/android/app-developer-release.apk +0 -0
- package/resources/ios/MobileSemantic/UITests/BridgeServer.swift +34 -2
- package/resources/android/app-developer-debug.apk +0 -0
|
@@ -1,95 +1,254 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "Operation-level platform capability manifest. SINGLE SOURCE OF TRUTH for iOS tool registration, per-call operation validation, the device.connect envelope (supportedTools/unsupportedCapabilities), docs and tests. Android is intentionally NOT enumerated: on Android the platform filter is a no-op and the existing profile system is authoritative, so nothing here can make the Android surface drift. A tool with no iOS entry (in `common` or the active mode) is level NONE => NOT registered on iOS. Entries are version-qualified (minIosVersion/minXcodeVersion) because support varies by toolchain; connect resolves the effective set against the detected environment.",
|
|
3
|
-
"version":
|
|
4
|
-
"platforms": [
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": 2,
|
|
4
|
+
"platforms": [
|
|
5
|
+
"ANDROID",
|
|
6
|
+
"IOS"
|
|
7
|
+
],
|
|
8
|
+
"iosModes": [
|
|
9
|
+
"physical",
|
|
10
|
+
"simulator"
|
|
11
|
+
],
|
|
7
12
|
"unsupportedCapabilities": {
|
|
8
|
-
"physical": [
|
|
9
|
-
|
|
13
|
+
"physical": [
|
|
14
|
+
"APP_CLEAR_DATA",
|
|
15
|
+
"APP_PERMISSION_GRANT",
|
|
16
|
+
"BATTERYSTATS"
|
|
17
|
+
],
|
|
18
|
+
"simulator": [
|
|
19
|
+
"BATTERYSTATS"
|
|
20
|
+
]
|
|
10
21
|
},
|
|
11
|
-
|
|
12
22
|
"ios": {
|
|
13
23
|
"common": {
|
|
14
|
-
"mobile.device.connect": {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"mobile.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"mobile.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"mobile.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"mobile.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"mobile.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
24
|
+
"mobile.device.connect": {
|
|
25
|
+
"level": "FULL"
|
|
26
|
+
},
|
|
27
|
+
"mobile.app.open": {
|
|
28
|
+
"level": "FULL",
|
|
29
|
+
"allowedOperations": [
|
|
30
|
+
"APP_OPEN",
|
|
31
|
+
"APP_DEEP_LINK",
|
|
32
|
+
"APP_BRING_TO_FRONT",
|
|
33
|
+
"APP_CLOSE_UI"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"mobile.screen.get": {
|
|
37
|
+
"level": "FULL"
|
|
38
|
+
},
|
|
39
|
+
"mobile.screen.find": {
|
|
40
|
+
"level": "FULL"
|
|
41
|
+
},
|
|
42
|
+
"mobile.screen.act": {
|
|
43
|
+
"level": "FULL"
|
|
44
|
+
},
|
|
45
|
+
"mobile.screen.wait": {
|
|
46
|
+
"level": "FULL"
|
|
47
|
+
},
|
|
48
|
+
"mobile.screen.diff": {
|
|
49
|
+
"level": "FULL"
|
|
50
|
+
},
|
|
51
|
+
"mobile.system.action": {
|
|
52
|
+
"level": "PARTIAL",
|
|
53
|
+
"allowedOperations": [
|
|
54
|
+
"HOME"
|
|
55
|
+
],
|
|
56
|
+
"reasonCode": "NO_GLOBAL_BACK_OR_RECENTS",
|
|
57
|
+
"note": "iOS has no global BACK/RECENTS/NOTIFICATION_SHADE; BACK is a semantic UI action (find nav button + act)."
|
|
58
|
+
},
|
|
59
|
+
"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."
|
|
67
|
+
},
|
|
68
|
+
"mobile.batch": {
|
|
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."
|
|
72
|
+
},
|
|
73
|
+
"mobile.gesture.perform": {
|
|
74
|
+
"level": "FULL"
|
|
75
|
+
},
|
|
76
|
+
"mobile.screen.scrollTo": {
|
|
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."
|
|
80
|
+
},
|
|
81
|
+
"mobile.screen.capture": {
|
|
82
|
+
"level": "NONE",
|
|
83
|
+
"reasonCode": "NOT_IMPLEMENTED_ON_IOS",
|
|
84
|
+
"note": "The Runner serves no screenshot method yet."
|
|
85
|
+
},
|
|
86
|
+
"mobile.input.text": {
|
|
87
|
+
"level": "FULL",
|
|
88
|
+
"note": "Host-side: routed to the Runner's screen.act SET_TEXT (same re-resolution and evidence)."
|
|
89
|
+
},
|
|
90
|
+
"mobile.workflow.run": {
|
|
91
|
+
"level": "FULL"
|
|
92
|
+
},
|
|
93
|
+
"ios.app.terminate": {
|
|
94
|
+
"level": "FULL",
|
|
95
|
+
"note": "Terminate the test-session app only; NOT an OS force-stop."
|
|
96
|
+
},
|
|
97
|
+
"ios.diag.logs": {
|
|
98
|
+
"level": "PARTIAL",
|
|
99
|
+
"reasonCode": "PROCESS_SCOPED",
|
|
100
|
+
"replacesAndroidTool": "mobile.diag.logcat"
|
|
101
|
+
},
|
|
102
|
+
"ios.diag.energyProfile": {
|
|
103
|
+
"level": "PARTIAL",
|
|
104
|
+
"reasonCode": "OWN_CONTRACT",
|
|
105
|
+
"replacesAndroidTool": "mobile.diag.batterystats"
|
|
106
|
+
},
|
|
107
|
+
"runtime.setup": {
|
|
108
|
+
"level": "PARTIAL",
|
|
109
|
+
"reasonCode": "OPEN_AND_VERIFY_ARE_ADVISORY_ON_IOS",
|
|
110
|
+
"allowedOperations": [
|
|
111
|
+
"STATUS",
|
|
112
|
+
"OPEN",
|
|
113
|
+
"VERIFY"
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
"mobile.app.control": {
|
|
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."
|
|
122
|
+
}
|
|
49
123
|
},
|
|
50
|
-
|
|
51
124
|
"physical": {
|
|
52
|
-
"mobile.device.state": {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"mobile.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
"mobile.app.forceStop": {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"mobile.app.
|
|
68
|
-
|
|
69
|
-
|
|
125
|
+
"mobile.device.state": {
|
|
126
|
+
"level": "PARTIAL",
|
|
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."
|
|
129
|
+
},
|
|
130
|
+
"mobile.app.list": {
|
|
131
|
+
"level": "PARTIAL",
|
|
132
|
+
"reasonCode": "DEVICECTL_OR_MDM",
|
|
133
|
+
"note": "Host-side via devicectl/simctl; bundle ids only."
|
|
134
|
+
},
|
|
135
|
+
"mobile.app.forceStop": {
|
|
136
|
+
"level": "NONE",
|
|
137
|
+
"replacementTool": "ios.app.terminate",
|
|
138
|
+
"reasonCode": "NO_OS_FORCE_STOP"
|
|
139
|
+
},
|
|
140
|
+
"mobile.app.clearData": {
|
|
141
|
+
"level": "NONE",
|
|
142
|
+
"reasonCode": "NO_PUBLIC_API"
|
|
143
|
+
},
|
|
144
|
+
"mobile.app.uninstall": {
|
|
145
|
+
"level": "PARTIAL",
|
|
146
|
+
"reasonCode": "SIGNED_DEVICECTL_OR_MDM"
|
|
147
|
+
},
|
|
148
|
+
"mobile.app.install": {
|
|
149
|
+
"level": "PARTIAL",
|
|
150
|
+
"reasonCode": "SIGNED_IPA_OR_MDM"
|
|
151
|
+
},
|
|
152
|
+
"mobile.app.permission.grant": {
|
|
153
|
+
"level": "NONE",
|
|
154
|
+
"reasonCode": "NO_PUBLIC_API"
|
|
155
|
+
},
|
|
156
|
+
"mobile.app.permission.revoke": {
|
|
157
|
+
"level": "PARTIAL",
|
|
158
|
+
"replacementTool": "ios.permission.reset",
|
|
159
|
+
"reasonCode": "XCUI_RESET_ONLY"
|
|
160
|
+
},
|
|
161
|
+
"ios.permission.reset": {
|
|
162
|
+
"level": "PARTIAL",
|
|
163
|
+
"reasonCode": "XCUI_RESET_AUTHORIZATION"
|
|
164
|
+
},
|
|
165
|
+
"mobile.diag": {
|
|
166
|
+
"level": "PARTIAL",
|
|
167
|
+
"reasonCode": "SUBSET_OF_KINDS_ON_IOS",
|
|
168
|
+
"allowedOperations": [
|
|
169
|
+
"CRASH",
|
|
170
|
+
"PERF",
|
|
171
|
+
"APP_INFO",
|
|
172
|
+
"STARTUP",
|
|
173
|
+
"BATTERY",
|
|
174
|
+
"NETSTATS",
|
|
175
|
+
"STORAGE"
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
"mobile.screen.inspect": {
|
|
179
|
+
"level": "PARTIAL",
|
|
180
|
+
"reasonCode": "DELTA_AND_TARGET_ONLY_ON_IOS",
|
|
181
|
+
"allowedOperations": [
|
|
182
|
+
"DELTA",
|
|
183
|
+
"TARGET"
|
|
184
|
+
]
|
|
185
|
+
}
|
|
70
186
|
},
|
|
71
|
-
|
|
72
187
|
"simulator": {
|
|
73
|
-
"mobile.device.state": {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"mobile.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"mobile.app.clearData": {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
188
|
+
"mobile.device.state": {
|
|
189
|
+
"level": "FULL"
|
|
190
|
+
},
|
|
191
|
+
"mobile.app.list": {
|
|
192
|
+
"level": "FULL"
|
|
193
|
+
},
|
|
194
|
+
"mobile.app.forceStop": {
|
|
195
|
+
"level": "NONE",
|
|
196
|
+
"replacementTool": "ios.app.terminate",
|
|
197
|
+
"reasonCode": "NO_OS_FORCE_STOP"
|
|
198
|
+
},
|
|
199
|
+
"mobile.app.clearData": {
|
|
200
|
+
"level": "NONE",
|
|
201
|
+
"replacementTool": "ios.simulator.resetAppData",
|
|
202
|
+
"reasonCode": "USE_REINSTALL_NOT_ERASE"
|
|
203
|
+
},
|
|
204
|
+
"mobile.app.uninstall": {
|
|
205
|
+
"level": "FULL"
|
|
206
|
+
},
|
|
207
|
+
"mobile.app.install": {
|
|
208
|
+
"level": "FULL"
|
|
209
|
+
},
|
|
210
|
+
"mobile.app.permission.grant": {
|
|
211
|
+
"level": "PARTIAL",
|
|
212
|
+
"reasonCode": "SIMCTL_PRIVACY"
|
|
213
|
+
},
|
|
214
|
+
"mobile.app.permission.revoke": {
|
|
215
|
+
"level": "PARTIAL",
|
|
216
|
+
"reasonCode": "SIMCTL_PRIVACY"
|
|
217
|
+
},
|
|
218
|
+
"ios.permission.reset": {
|
|
219
|
+
"level": "FULL",
|
|
220
|
+
"reasonCode": "SIMCTL_PRIVACY"
|
|
221
|
+
},
|
|
222
|
+
"ios.simulator.resetAppData": {
|
|
223
|
+
"level": "FULL",
|
|
224
|
+
"note": "uninstall + reinstall the pinned artifact; NOT a device erase."
|
|
225
|
+
},
|
|
226
|
+
"ios.simulator.eraseDevice": {
|
|
227
|
+
"level": "FULL",
|
|
228
|
+
"destructive": true,
|
|
229
|
+
"defaultOff": true,
|
|
230
|
+
"reasonCode": "FULL_SIMULATOR_WIPE"
|
|
231
|
+
},
|
|
232
|
+
"mobile.diag": {
|
|
233
|
+
"level": "PARTIAL",
|
|
234
|
+
"reasonCode": "SUBSET_OF_KINDS_ON_IOS",
|
|
235
|
+
"allowedOperations": [
|
|
236
|
+
"CRASH",
|
|
237
|
+
"PERF",
|
|
238
|
+
"APP_INFO",
|
|
239
|
+
"STARTUP",
|
|
240
|
+
"BATTERY",
|
|
241
|
+
"NETSTATS",
|
|
242
|
+
"STORAGE"
|
|
243
|
+
]
|
|
244
|
+
},
|
|
245
|
+
"mobile.screen.inspect": {
|
|
246
|
+
"level": "PARTIAL",
|
|
247
|
+
"reasonCode": "DELTA_ONLY_ON_IOS",
|
|
248
|
+
"allowedOperations": [
|
|
249
|
+
"DELTA"
|
|
250
|
+
]
|
|
251
|
+
}
|
|
93
252
|
}
|
|
94
253
|
}
|
|
95
|
-
}
|
|
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
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 = "
|
|
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.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",
|
|
Binary file
|
|
@@ -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:
|
|
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:
|
|
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
|
}
|
|
Binary file
|