mobridge-mcp 0.0.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/README.md +155 -0
- package/dist/adb-baseline/act.mjs +1 -0
- package/dist/adb-baseline/capture-lib.mjs +2 -0
- package/dist/main.obf.cjs +2 -0
- package/dist/protocol/fixtures/01-login-views/expected.json +32 -0
- package/dist/protocol/fixtures/01-login-views/snapshot.json +20 -0
- package/dist/protocol/fixtures/02-alert-dialog/expected.json +22 -0
- package/dist/protocol/fixtures/02-alert-dialog/snapshot.json +20 -0
- package/dist/protocol/fixtures/03-recyclerview-list/expected.json +22 -0
- package/dist/protocol/fixtures/03-recyclerview-list/snapshot.json +19 -0
- package/dist/protocol/fixtures/04-ambiguous-ctas/expected.json +17 -0
- package/dist/protocol/fixtures/04-ambiguous-ctas/snapshot.json +21 -0
- package/dist/protocol/fixtures/05-compose-settings/expected.json +17 -0
- package/dist/protocol/fixtures/05-compose-settings/snapshot.json +19 -0
- package/dist/protocol/fixtures/06-canvas-game/expected.json +12 -0
- package/dist/protocol/fixtures/06-canvas-game/snapshot.json +16 -0
- package/dist/protocol/fixtures/07-search-results/expected.json +22 -0
- package/dist/protocol/fixtures/07-search-results/snapshot.json +19 -0
- package/dist/protocol/fixtures/README.md +34 -0
- package/dist/protocol/platform-capabilities.json +95 -0
- package/dist/protocol/schemas/action.schema.json +170 -0
- package/dist/protocol/schemas/common.schema.json +59 -0
- package/dist/protocol/schemas/diff.schema.json +74 -0
- package/dist/protocol/schemas/error.schema.json +34 -0
- package/dist/protocol/schemas/node-ref.schema.json +100 -0
- package/dist/protocol/schemas/query.schema.json +53 -0
- package/dist/protocol/schemas/screen-snapshot.schema.json +50 -0
- package/dist/protocol/schemas/semantic-node.schema.json +95 -0
- package/dist/protocol/schemas/tools.schema.json +215 -0
- package/dist/protocol/schemas/wait.schema.json +57 -0
- package/dist/protocol/synonyms.json +74 -0
- package/dist/protocol/validate/package-lock.json +71 -0
- package/dist/protocol/validate/package.json +13 -0
- package/dist/protocol/validate/query-engine.mjs +1 -0
- package/dist/protocol/validate/run-fixtures.mjs +6 -0
- package/dist/protocol/validate/schema-lint.mjs +2 -0
- package/dist/protocol/validate/validate-file.mjs +1 -0
- package/integrity.json +4 -0
- package/loader.cjs +85 -0
- package/package.json +40 -0
- package/resources/android/app-developer-debug.apk +0 -0
- package/resources/ios/MobileSemantic/Fixture/MobileSemanticFixtureApp.swift +50 -0
- package/resources/ios/MobileSemantic/MobileSemantic.xcodeproj/project.pbxproj +463 -0
- package/resources/ios/MobileSemantic/MobileSemantic.xcodeproj/xcshareddata/xcschemes/MobileSemanticUITests.xcscheme +97 -0
- package/resources/ios/MobileSemantic/UITests/BridgeServer.swift +216 -0
- package/resources/ios/MobileSemantic/UITests/XcuiSnapshot.swift +73 -0
- package/resources/ios/MobileSemantic/gen_project.rb +67 -0
package/README.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# mobridge-mcp
|
|
2
|
+
|
|
3
|
+
**MoBridge** is a Model Context Protocol (MCP) server that gives an AI assistant fast, accurate **eyes and hands on a real Android (and iOS) device** — through the accessibility tree, not screen-scraping. The agent observes the screen as a small semantic model, finds a target, acts on it, and verifies the result. Full-screen pixel screenshots are available when they are genuinely needed.
|
|
4
|
+
|
|
5
|
+
- **Perception-first.** `screen.observe` returns a lean, deterministic view of what is on screen (app, screen title, actionable controls) — typically ~100× smaller than a full accessibility dump.
|
|
6
|
+
- **Targeted, not brute force.** `screen.find` resolves one target to an act-able `NodeRef` seeded by the platform's own query APIs, with a full-tree oracle as the correctness fallback.
|
|
7
|
+
- **Correctness gates.** Every action re-resolves the target on a fresh capture and is generation-checked: a stale reference fails closed (`STALE_NODE_REF`) instead of acting on the wrong node. No silent "closest match".
|
|
8
|
+
- **Screenshots when you want them.** `screen.capture` returns an inline image (fast WebP by default, lossless PNG on request). `FLAG_SECURE` windows are always refused.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx -y mobridge-mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That launches the interactive installer, which wires MoBridge into the AI agents you choose. Restart the agent afterwards and ask it to connect to your device.
|
|
19
|
+
|
|
20
|
+
Requires **Node.js ≥ 18**. For a real Android device you also need **adb** (Android Platform Tools) on your `PATH` and USB debugging enabled.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### 1. Run the installer
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx -y mobridge-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You'll get a menu (powered by `@clack/prompts`):
|
|
33
|
+
|
|
34
|
+
1. **Pick your agents** — a multi-select of every client the installer can configure (pre-checked if it detects one already installed).
|
|
35
|
+
2. **Pick the platform** — Android or iOS.
|
|
36
|
+
3. **Pick the backend** — `ws` (real device over adb, recommended), `mock` (no device, to explore the tools), or `ios`.
|
|
37
|
+
|
|
38
|
+
It then writes one `mcpServers` entry into each selected agent's config, **backing up the file first** and touching nothing else. Re-running is safe and idempotent.
|
|
39
|
+
|
|
40
|
+
Non-interactive (CI / scripted):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx -y mobridge-mcp install --yes \
|
|
44
|
+
--agent claude-code,cursor,codex --platform android --backend ws
|
|
45
|
+
npx -y mobridge-mcp install --dry-run # show what it would write, change nothing
|
|
46
|
+
npx -y mobridge-mcp remove # remove the MoBridge entry again
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Supported agents & config locations
|
|
50
|
+
|
|
51
|
+
| Agent | Config file | Format |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| Claude Code (CLI) | `~/.claude.json` | JSON |
|
|
54
|
+
| Claude Desktop | macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`<br>Windows: `%APPDATA%\Claude\claude_desktop_config.json`<br>Linux: `~/.config/Claude/claude_desktop_config.json` | JSON |
|
|
55
|
+
| Cursor | `~/.cursor/mcp.json` | JSON |
|
|
56
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` | JSON |
|
|
57
|
+
| Codex CLI | `~/.codex/config.toml` | TOML |
|
|
58
|
+
|
|
59
|
+
### 3. Manual installation
|
|
60
|
+
|
|
61
|
+
If you'd rather edit the config yourself, add this server entry.
|
|
62
|
+
|
|
63
|
+
**JSON agents** (Claude, Cursor, Windsurf) — merge into `mcpServers`:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"mobridge": {
|
|
69
|
+
"command": "npx",
|
|
70
|
+
"args": ["-y", "mobridge-mcp", "serve"],
|
|
71
|
+
"env": { "MSM_PLATFORM": "android", "MSM_BACKEND": "ws", "MSM_TOOL_PROFILE": "full" }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Codex CLI** — append to `~/.codex/config.toml`:
|
|
78
|
+
|
|
79
|
+
```toml
|
|
80
|
+
[mcp_servers.mobridge]
|
|
81
|
+
command = "npx"
|
|
82
|
+
args = ["-y", "mobridge-mcp", "serve"]
|
|
83
|
+
|
|
84
|
+
[mcp_servers.mobridge.env]
|
|
85
|
+
MSM_PLATFORM = "android"
|
|
86
|
+
MSM_BACKEND = "ws"
|
|
87
|
+
MSM_TOOL_PROFILE = "full"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
On **Windows**, MCP clients spawn `npx.cmd`; if a client fails to start the server, use `"command": "cmd"` with `"args": ["/c", "npx", "-y", "mobridge-mcp", "serve"]`. Inside JSON on Windows, escape backslashes in any paths (`\\`).
|
|
91
|
+
|
|
92
|
+
Restart the agent after editing its config.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Connecting an Android device
|
|
97
|
+
|
|
98
|
+
1. Enable **Developer options → USB debugging** on the phone and plug it in over USB.
|
|
99
|
+
2. Confirm `adb devices` lists it (accept the RSA prompt on the phone).
|
|
100
|
+
3. Ask the agent to call `mobile.device.connect`. On first connect the companion runtime is provisioned automatically (the on-device app + its accessibility service).
|
|
101
|
+
|
|
102
|
+
Keep the screen awake during long sessions:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
adb shell svc power stayon usb
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Prefer a wired USB connection — wireless adb links can drop and interrupt the session.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## What the agent gets
|
|
113
|
+
|
|
114
|
+
Progressive disclosure — cheap first, expensive only on demand:
|
|
115
|
+
|
|
116
|
+
| Tool | Purpose |
|
|
117
|
+
|---|---|
|
|
118
|
+
| `mobile.screen.app` | O(1) — which app/screen am I on? |
|
|
119
|
+
| `mobile.screen.observe` | "AI eyes": a lean list of on-screen text + actionable controls |
|
|
120
|
+
| `mobile.screen.find` / `screen.target` | resolve a target to a `NodeRef` (never coordinates) |
|
|
121
|
+
| `mobile.screen.node` / `screen.children` | inspect one node / one level, on demand |
|
|
122
|
+
| `mobile.screen.act` | re-resolve + act + return before/after evidence for verification |
|
|
123
|
+
| `mobile.screen.capture` | inline screenshot (`preset: "ai"` small & fast, `"full"` lossless) |
|
|
124
|
+
| `mobile.screen.snapshot` | the full accessibility tree — the expensive escape hatch |
|
|
125
|
+
|
|
126
|
+
The AI stays in charge of *observe → understand → decide → act*. MoBridge only makes the **observe** step fast and precise; it never guesses intent or chains actions on its own.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Privacy & network model
|
|
131
|
+
|
|
132
|
+
- The device runtime talks to this local MCP server over an adb-forwarded loopback socket. Nothing is sent to third parties.
|
|
133
|
+
- Screenshots stay on device unless a tool call explicitly returns one to the agent; secure (`FLAG_SECURE`) windows are never captured.
|
|
134
|
+
- The installer only writes local agent config files (with backups). It installs no system services and needs no `local.properties`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Troubleshooting
|
|
139
|
+
|
|
140
|
+
- **Agent doesn't see the tools** — restart the agent after install; confirm the `mcpServers` entry is present and valid JSON/TOML.
|
|
141
|
+
- **`npx` fails on Windows** — use the `cmd /c npx …` form shown above.
|
|
142
|
+
- **`mobile.device.connect` fails** — check `adb devices`, re-accept the USB debugging prompt, and make sure only one device is attached (or pass the serial).
|
|
143
|
+
- **Empty / one-node captures right after reinstalling the runtime** — toggle the phone's accessibility service off and on, then retry.
|
|
144
|
+
|
|
145
|
+
Run the server directly (what the agent runs) for a smoke test:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx -y mobridge-mcp serve
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
UNLICENSED — internal distribution. See the repository for terms.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{execFileSync as u}from"node:child_process";function p(r){const t=r?["-s",r]:[];return e=>u("adb",[...t,...e],{maxBuffer:32*1024*1024})}function l(r){const[t,e,n,o]=r;return[Math.round((t+n)/2),Math.round((e+o)/2)]}function i(r,t){if(!t.bounds)throw new Error("node has no bounds; cannot coordinate-tap");const[e,n]=l(t.bounds);return r(["shell","input","tap",String(e),String(n)]),{method:"GESTURE_FALLBACK",tappedAt:[e,n],target:{role:t.role,label:t.label,text:t.text}}}function s(r,t,e){const[n,o]=l(t.bounds);return r(["shell","input","tap",String(n),String(o)]),r(["shell","input","text",e.replace(/ /g,"%s")]),{method:"GESTURE_FALLBACK",target:{role:t.role,label:t.label}}}function c(r,t){const e={BACK:"KEYCODE_BACK",HOME:"KEYCODE_HOME",RECENTS:"KEYCODE_APP_SWITCH"}[t];if(!e)throw new Error("unsupported global action "+t);return r(["shell","input","keyevent",e]),{method:"GLOBAL_KEY",key:e}}export{l as center,c as globalAction,p as makeAdb,s as setText,i as tapNode};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{execFileSync as E}from"node:child_process";import{createHash as N}from"node:crypto";function x(t){const n=t?["-s",t]:[];return e=>E("adb",[...n,...e],{maxBuffer:64*1024*1024})}function L(t){const n=/\[(-?\d+),(-?\d+)\]\[(-?\d+),(-?\d+)\]/.exec(t||"");return n?[+n[1],+n[2],+n[3],+n[4]]:null}function R(t){const n={},e=/([\w-]+)="([^"]*)"/g;let i;for(;i=e.exec(t);)n[i[1]]=i[2].replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"');return n}const o=t=>t==="true";function O(t,n){const e=(t||"").toLowerCase();return e.includes("edittext")?"INPUT":e.includes("switch")?"SWITCH":e.includes("checkbox")?"CHECKBOX":e.includes("radio")?"RADIO":e.includes("button")?"BUTTON":e.includes("webview")?"WEB_VIEW":e.includes("recyclerview")||e.includes("listview")||e.includes("scrollview")||e.includes("gridview")?"LIST":e.includes("seekbar")?"SLIDER":e.includes("progress")?"PROGRESS":e.includes("tab")?"TAB":e.includes("imageview")||e.includes("imagebutton")?o(n.clickable)?"BUTTON":"IMAGE":e.includes("textview")?"TEXT":e.includes("edit")?"INPUT":o(n.clickable)?"BUTTON":"UNKNOWN"}function A(t,n){const e=[];return o(n.clickable)&&e.push("CLICK"),o(n["long-clickable"])&&e.push("LONG_CLICK"),t==="INPUT"&&e.push("FOCUS","SET_TEXT","CLEAR_TEXT"),o(n.scrollable)&&e.push("SCROLL_FORWARD","SCROLL_BACKWARD"),o(n.checkable)&&e.push("SELECT"),[...new Set(e)]}function D(t){if(t.role==="SCREEN")return!0;if(t.bounds&&t.bounds[0]===t.bounds[2]&&t.bounds[1]===t.bounds[3])return!1;const e=t.actions.length>0,i=t.text&&t.text.trim()||t.label&&t.label.trim(),r=["LIST","DIALOG","WEB_VIEW","CANVAS"].includes(t.role);return e||i||r}function S({serial:t=null,compact:n=!1}={}){const e=x(t);e(["shell","uiautomator","dump","/sdcard/msm_dump.xml"]);const i=e(["exec-out","cat","/sdcard/msm_dump.xml"]).toString("utf8"),r=[],a=[];let p=0,d=null;const h=/<(\/?)node\b([^>]*?)(\/?)>/g;let f;for(;f=h.exec(i);){if(f[1]==="/"){a.pop();continue}const u=f[3]==="/",s=R(f[2]),b="n"+p++,l=a.length?a[a.length-1]:null;!d&&s.package&&(d=s.package);const c=p===1?"SCREEN":O(s.class,s),g=o(s.password);r.push({nodeId:b,parentId:l,role:c,label:s["content-desc"]||null,text:s.text?s.text:null,value:c==="INPUT"?g?null:s.text||"":null,hint:null,resourceId:s["resource-id"]||null,className:s.class||null,state:{enabled:o(s.enabled),clickable:o(s.clickable),focused:o(s.focused),selected:o(s.selected),checked:s.checkable==="true"?o(s.checked):null,password:g},actions:A(c,s),bounds:L(s.bounds),source:"ACCESSIBILITY",confidence:1}),u||a.push(b)}let w=r;if(n){const u=new Set(r.filter(D).map(l=>l.nodeId)),s=new Map(r.map(l=>[l.nodeId,l])),b=l=>{let c=l.parentId?s.get(l.parentId):null;for(;c&&!u.has(c.nodeId);)c=c.parentId?s.get(c.parentId):null;return c?c.nodeId:null};w=r.filter(l=>u.has(l.nodeId)).map(l=>({...l,parentId:b(l)}))}const T=x(t)(["shell","wm","size"]).toString(),I=/(\d+)x(\d+)/.exec(T),C=I&&+I[1]>+I[2]?"landscape":"portrait",k=w.map(u=>`${u.role}|${u.text}|${u.label}|${u.resourceId}|${(u.bounds||[]).join(",")}`).join(`
|
|
2
|
+
`),m=N("sha1").update(k).digest("hex").slice(0,8);return{schemaVersion:1,deviceId:t||"adb-default",snapshotId:"s_"+Date.now().toString(36),timestamp:Date.now(),packageName:d||"unknown.pkg",windowId:0,orientation:C,screenStateId:(d||"?")+":"+m,compact:n,nodes:w,treeHash:m,_rawNodeCount:r.length}}async function v({serial:t=null,compact:n=!0,timeoutMs:e=6e3,stableForMs:i=600}={}){const r=Date.now();let a=null,p=Date.now();for(;Date.now()-r<e;){const d=S({serial:t,compact:n});if(d.treeHash!==a)a=d.treeHash,p=Date.now();else if(Date.now()-p>=i)return d;await new Promise(h=>setTimeout(h,200))}return S({serial:t,compact:n})}export{S as captureSnapshot,x as makeAdb,v as waitStable};
|