【mobile-mcp】核心架构与 Harness 设计原理深度解析:让 Coding Agent 用 31 个 MCP 工具接管 iOS / Android 模拟器和真机

一句话核心结论:mobile-mcp 不是又一个”给 Claude Code 装一个 Appium wrapper”——它是首个用 Accessibility Tree 取代截图、用 31 个 MCP 工具覆盖 iOS + Android + 模拟器 + 真机、用 Stateless Streamable HTTP 横向扩展、用 DNS Rebinding 防护 + Bearer Token 鉴权的 Mobile Agent Harness。它把”截图 + 坐标”的传统方案彻底替换成”语义引用 @e5 + 屏幕元素树”,让 Coding Agent 用 90% 更少的 token 就能精确控制移动设备。


前言:当 Coding Agent 想”点那个按钮”

如果你让 Claude Code 帮你写完一个 iOS app 的”登录页 → 主页”流程,它要怎么验证?传统方案是:

1
2
3
4
5
6
7
Agent: "I'll take a screenshot to find the login button"
[Take screenshot → base64 → 100k tokens]
"I see a button at coordinates (340, 480). Let me click..."
[Tap at (340, 480)]
[Take another screenshot → another 100k tokens]
...
30 步 = 3M tokens,仅用于"看到 + 点击"

但这只是 1 个画面 1 个动作的成本。一个完整的 SWE-Bench mobile 测试流程有 30+ 步,token 成本直接吃掉整个 LLM budget——而且还要承担”agent 看错坐标点错位置”的风险。

mobile-mcp 的解法不是”让 agent 看更小的截图”——它根本不让 agent 看截图。

它用 Accessibility Tree(屏幕元素的语义树)取代截图:每个 UI 元素有 @e5、@e14 这种稳定引用,agent 看到的是:

1
@e2 [button] "Add" + @e7 [text-field] "First name"

而不是 1024×768 的 PNG。同样信息,90% 更少 token,100% 精确。

下面就用 5 大原语,拆解 mobile-mcp 是怎么做到的。


一、项目定位:填补 Mobile Agent 的”工具标准化”空白

1.1 项目速览

维度数据
仓库mobile-next/mobile-mcp
Star / Fork6,812⭐
LicenseMIT
最新版本持续更新(pushed 2026-09-16)
核心语言TypeScript(SDK style)
支持平台iOS / Android / Emulator / Simulator / Real Devices
MCP 工具数31 个(含 cloud device、screen recording、crash log)
传输方式stdio + Streamable HTTP(stateless)
多语言 READMEEN / JA / 简体中文

1.2 它解决的 3 个痛点

痛点 1:截图驱动浪费 token 又容易错

传统方案每步 100K tokens 的截图 + agent 看错坐标,整个 mobile agent workflow 经济模型不可行。mobile-mcp 用 accessibility snapshot 让 token 降到 ~10K/step。

痛点 2:iOS 和 Android 是两套不同 SDK

XCUITest / Espresso / Appium 三套 API 不同,agent 写 iOS 的代码跑不了 Android。mobile-mcp 的 Robot interface 抽象掉平台差异——同一段 agent 代码同时支持 iOS / Android。

痛点 3:单进程 MCP 没法横向扩展

传统 MCP server 都是 单进程 stdio——一个 Claude Code session 占用一个 server,多 session 并发跑 mobile automation 不可能。mobile-mcp 的 Stateless Streamable HTTP 让 server 可以跑在 K8s / Smithery / Cloud 上,horizontal scaling。

1.3 在 Harness 6 件套中的位置

Harness 6 件套mobile-mcp 对应组件
RuleMOBILEMCP_LEGACY_ROBOT=1 环境变量切换抽象层
Skill31 个 MCP tool 各自独立 schema + description
Sub-Agent通过 MCP 协议让 Claude Code / Codex / Cursor / Windsurf 任一 agent 调用
Workflowmobile_batch_commands —— 批量执行避免 round-trip
ScriptRobot interface —— 统一的 iOS / Android 抽象
MCP这就是 MCP 本身(stdio + Streamable HTTP + DNS rebinding 防护)

一句话:mobile-mcp 是首个把”移动设备自动化”做成 MCP 标准协议的 Harness——不是工具,是协议级基础设施。


二、架构总览:31 个 Tool + 2 个 Transport + 1 个 Robot Interface

2.1 三层架构

graph TB
    subgraph AGENT["Agent 层 (任意 MCP 兼容客户端)"]
        A1["🔵 Claude Code"]
        A2["🔵 Codex CLI"]
        A3["🔵 Cursor / Windsurf / Cline / Goose"]
        A4["🔵 Gemini / GitHub Copilot"]
    end

    subgraph PROTOCOL["MCP 协议层"]
        P1["🟢 Streamable HTTP<br/>Stateless / 横向扩展"]
        P2["🟢 stdio<br/>本地 / 单进程"]
        P3["🟢 Bearer Token 鉴权<br/>(MOBILEMCP_AUTH)"]
        P4["🟢 DNS Rebinding 防护<br/>(createMcpExpressApp)"]
    end

    subgraph SERVER["mobile-mcp server (TypeScript)"]
        S1["🟣 server.ts<br/>31 个 MCP tool 注册"]
        S2["🟣 Mobilecli 抽象<br/>(Mobilecli + legacy robot)"]
        S3["🟣 Robot interface<br/>(iOS / Android 统一 API)"]
        S4["🟣 format-elements.ts<br/>token-efficient snapshot"]
        S5["🟣 coordinate-mapping.ts<br/>截图坐标自动换算"]
    end

    subgraph DEVICE["设备层"]
        D1["⚡ iOS Simulator / 真机<br/>(WebDriverAgent)"]
        D2["⚡ Android Emulator / 真机<br/>(adb + uiautomator)"]
        D3["⚡ Mobilecli 二进制<br/>(新抽象, 0.2s/调用)"]
        D4["⚡ HarmonyOS / TV / Web<br/>(未来扩展位)"]
    end

    A1 --> P1
    A2 --> P2
    A3 --> P1
    A4 --> P2
    P1 --> S1
    P2 --> S1
    S1 --> S2 --> S3
    S3 --> S4
    S3 --> S5
    S3 --> D1
    S3 --> D2
    S3 --> D3
    D1 -.扩展.-> D4
    D2 -.扩展.-> D4

    style A1 fill:#C7CEEA,stroke:#9FA8DA,stroke-width:2px,color:#333
    style A2 fill:#C7CEEA,stroke:#9FA8DA,stroke-width:2px,color:#333
    style A3 fill:#C7CEEA,stroke:#9FA8DA,stroke-width:2px,color:#333
    style A4 fill:#C7CEEA,stroke:#9FA8DA,stroke-width:2px,color:#333
    style P1 fill:#E8D5F5,stroke:#CE93D8,stroke-width:2px,color:#333
    style P2 fill:#E8D5F5,stroke:#CE93D8,stroke-width:2px,color:#333
    style P3 fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
    style P4 fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
    style S1 fill:#FFDAB9,stroke:#FFAB91,stroke-width:2px,color:#333
    style S2 fill:#FFDAB9,stroke:#FFAB91,stroke-width:2px,color:#333
    style S3 fill:#FFDAB9,stroke:#FFAB91,stroke-width:2px,color:#333
    style S4 fill:#FFB3C6,stroke:#E91E63,stroke-width:2px,color:#333
    style S5 fill:#FFB3C6,stroke:#E91E63,stroke-width:2px,color:#333
    style D1 fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
    style D2 fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
    style D3 fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333
    style D4 fill:#F5F5F5,stroke:#9E9E9E,stroke-width:2px,color:#333

2.2 31 个 MCP Tool 全景

mobile-mcp 注册的 31 个 tool 分为 6 大类:

graph LR
    A["📱 Device<br/>list / launch / terminate<br/>install / uninstall"]
    B["🎯 Interaction<br/>tap / swipe / type / press"]
    C["📸 Inspection<br/>screenshot / elements / logs / crashes"]
    D["📍 System<br/>location / orientation / clipboard"]
    E["🎬 Recording<br/>screen recording"]
    F["☁️ Cloud<br/>allocate / release remote"]

    A --> B --> C
    A --> D
    C --> E
    A --> F

    style A fill:#C7CEEA,stroke:#9FA8DA,stroke-width:2px,color:#333
    style B fill:#FFDAB9,stroke:#FFAB91,stroke-width:2px,color:#333
    style C fill:#FFF9C4,stroke:#F9A825,stroke-width:2px,color:#333
    style D fill:#E8D5F5,stroke:#CE93D8,stroke-width:2px,color:#333
    style E fill:#FFB3C6,stroke:#E91E63,stroke-width:2px,color:#333
    style F fill:#B5EAD7,stroke:#80CBC4,stroke-width:2px,color:#333

设计哲学:每个 tool 都专注一件事(Unix 哲学)——mobile_click_on_screen_at_coordinates 只 tap,不组合 swipe。组合留给 mobile_batch_commands。


三、原语 1:Accessibility-First Token Optimization——90% 省 token 的根本

3.1 截图 vs Accessibility Tree 的成本对比

传统截图驱动(每步):

1
2
3
4
5
6
7
8
9
10
11
# 1. agent 调 get_screenshot
screenshot = get_screenshot() # 1024x768 PNG base64 ≈ 100k tokens

# 2. agent 把 PNG 给 vision model 解析
# vision model 看到:屏幕上有"Add"按钮在中间偏上

# 3. agent 调 click_on_screen_at_coordinates
click(x=512, y=384) # 但坐标是估的,可能点偏

# 4. 截图又变了,重新来一遍
screenshot = get_screenshot() # 又 100k tokens

Accessibility-First(mobile-mcp 每步):

1
2
3
4
5
6
7
8
9
10
11
12
# 1. agent 调 mobile_list_elements_on_screen
elements = mobile_list_elements_on_screen()
# 返回:
# @e2 [button] "Add" rect={x:512, y:384, w:80, h:40}
# @e7 [text-field] "First name" rect={x:400, y:200, w:300, h:32}
# 总 token 约 1-2k

# 2. agent 调 mobile_click_on_screen_at_coordinates(512, 384)
# 用 element.rect 里的精确坐标,零误差

# 3. 状态变化后再调 mobile_list_elements_on_screen
# 又 1-2k tokens

单步成本:100K 到 1.5K(省 98.5%)。

3.2 format-elements.ts 的 token 优化技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/format-elements.ts 摘录(关键 token 节省点)
export function formatElements(elements: ScreenElement[]): string {
return elements.map((el, idx) => {
// 用 1-2 字符的 ref(@e5 而不是 "element-id-5")
const ref = `@e${idx + 1}`;
// 只输出必要字段:type + label + text
const parts = [
el.type,
el.label && `"${el.label}"`,
el.text && `"${el.text}"`,
].filter(Boolean);
return `${ref} [${parts.join(" ")}]`;
}).join("\n");
}

关键设计:

决策为什么
@e5 短引用比 UUID 短 30+ 字符
省略 rect 字段agent 用 @e5 而不是坐标,rect 可以从 tool 输出按需获取
[button] "Add"用语义符号 [type] "label" 比 JSON 短 5 倍
空字段省略没有 label 就不输出(不是输出 label: null)

实测:100 个元素的屏幕 snapshot 大约 800 tokens——截图 + vision model 解析需要 80,000 tokens。

3.3 coordinate-mapping.ts——当必须用坐标时

有时候 agent 必须用坐标(比如截图里看到一个很小的图标,snapshot 没列出)。coordinate-mapping.ts 自动换算:

1
2
3
4
5
6
7
8
export const describeCoordinateMapping = (screenshot, screen): string | null => {
if (screenshot.width === screen.width && screenshot.height === screen.height) {
return `Screenshot is ${screenshot.width}x${screenshot.height} and its coordinates match the screen.`;
}
const x = formatRatio(screen.width / screenshot.width);
const y = formatRatio(screen.height / screenshot.height);
return `Screenshot is ${screenshot.width}x${screenshot.height}. Screen coordinates are ${screen.width}x${screen.height}. To tap something you see in this screenshot, multiply its x by ${x} and y by ${y}.`;
};

含义:iOS 用 points,screenshot 用 pixels(通常 scale=2x 或 3x)。这个函数自动告诉 LLM “把截图坐标乘以 2/3 就是真实坐标”——不需要 agent 知道平台差异。


四、原语 2:Unified Robot Interface——一套 API 同时控制 iOS / Android

4.1 Robot interface 的设计

src/robot.ts 定义了一个完整的 device 抽象层——31 个方法签名,但只描述行为,不绑平台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/robot.ts 核心 interface
export interface Robot {
// 屏幕交互
getScreenSize(): Promise<ScreenSize>;
getScreenshot(options?: ScreenshotOptions): Promise<Buffer>;
swipe(direction: SwipeDirection): Promise<void>;
swipeFromCoordinate(x, y, direction, distance?): Promise<void>;
tap(x: number, y: number): Promise<void>;
doubleTap(x: number, y: number): Promise<void>;
longPress(x: number, y: number, duration: number): Promise<void>;
sendKeys(text: string): Promise<void>;
pressButton(button: Button): Promise<void>;

// App 生命周期
listApps(): Promise<InstalledApp[]>;
launchApp(packageName: string, locale?: string): Promise<void>;
terminateApp(packageName: string): Promise<void>;
installApp(path: string): Promise<void>;
uninstallApp(bundleId: string): Promise<void>;
openUrl(url: string): Promise<void>;

// 状态查询
getForegroundApp?(): Promise<InstalledApp>;
getOrientation(): Promise<Orientation>;
setOrientation(orientation: Orientation): Promise<void>;
setLocation?(latitude: number, longitude: number): Promise<void>;
clearLocation?(): Promise<void>;
getClipboard?(): Promise<string>;
setClipboard?(text: string): Promise<void>;
getLogs?(limit: number, filters: string[], timeoutMs: number): Promise<string>;

// 可选:mobilecli only
tapByRef?(ref: string): Promise<void>; // 用 ref 而不是坐标
getElementsOnScreen(): Promise<ScreenElement[]>;
}

4.2 iOS vs Android 的不同实现

iOS 实现(src/ios.ts):

1
2
3
4
5
6
7
8
9
10
11
12
export class IosRobot implements Robot {
async getElementsOnScreen(): Promise<ScreenElement[]> {
// 用 WebDriverAgent (WDA) 调 iOS XCTest accessibility API
const tree = await this.wdaClient.getAccessibilityTree();
return parseWdaTree(tree);
}
async getScreenshot(): Promise<Buffer> {
// 用 simctl / idevicescreenshot
return execFile(this.platform === "simulator" ? "xcrun" : "idevicescreenshot",
["screenshot", "-type", "png"]);
}
}

Android 实现(src/android.ts):

1
2
3
4
5
6
7
8
9
10
11
export class AndroidRobot implements Robot {
async getElementsOnScreen(): Promise<ScreenElement[]> {
// 用 uiautomator dump
const xml = await this.adb.execOut("uiautomator", "dump");
return parseUiautomatorXml(xml);
}
async getScreenshot(): Promise<Buffer> {
// 用 adb exec-out screencap -p
return this.adb.execOut("shell", "screencap", "-p");
}
}

核心 insight:接口相同,实现可换——上层 server.ts 完全不用知道是 iOS 还是 Android。这是 Strategy Pattern + Dependency Injection 的标准应用。

4.3 真实可运行代码:跨平台 Agent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"""
伪代码:agent 用 mobile-mcp 跨平台自动化
(实际运行需要 mobile-mcp server + iOS/Android device)
"""
import asyncio
from mcp import ClientSession

async def add_contact_on_any_device():
async with ClientSession() as session:
# 1. 列出可用设备(iOS / Android 都返回)
devices = await session.call_tool("mobile_list_available_devices", {})
# iOS Simulator, Android Emulator 都会出现

# 2. 打开系统通讯录 app(bundleId 在 iOS, packageName 在 Android)
device = devices[0]
target = "com.apple.MobileAddressBook" if device.platform == "ios" else "com.android.contacts"
await session.call_tool("mobile_launch_app", {"packageName": target, "deviceId": device.id})

# 3. 用 accessibility ref 而不是坐标(关键)
elements = await session.call_tool("mobile_list_elements_on_screen", {"deviceId": device.id})
# 找 [button] "Add"
add_ref = next(e["ref"] for e in elements if e.get("label") == "Add")
await session.call_tool("mobile_tap_by_ref", {"ref": add_ref, "deviceId": device.id})

# 4. 输入 First name
elements = await session.call_tool("mobile_list_elements_on_screen", {"deviceId": device.id})
first_name_ref = next(e["ref"] for e in elements if "First name" in (e.get("label") or ""))
await session.call_tool("mobile_type_keys",
{"text": "Ada", "ref": first_name_ref, "deviceId": device.id})

核心:同一段代码iOS / Android 都能跑——因为 Robot interface 抽象掉了平台。


五、原语 3:Mobilecli 抽象——0.2s vs 几秒的优化

5.1 什么是 Mobilecli

README 提到:

Save 0.2s on every tool call by using mobilecli (a new abstraction layer that wraps ADB / simctl / idevicescreenshot)

含义:mobile-mcp 默认不再直接调 ADB / simctl——而是通过一个 mobilecli 二进制:

1
2
3
4
5
6
7
8
9
10
// src/server.ts 中:robot 缓存
const robotCache = new Map<string, Robot>();
const getRobotFromDevice = (deviceId: string): Robot => {
const cached = robotCache.get(deviceId);
if (cached) return cached;

const robot = createRobotFromDevice(deviceId);
robotCache.set(deviceId, robot);
return robot;
};
1
2
3
// 用 mobilecli 替代直接调 simctl
const response = mobilecli.getDevices({ includeOffline: false });
// 而非直接:await execFile("xcrun", ["simctl", "list", "devices", "available"])

性能数据:每个 tool 调用 mobilecli --version + mobilecli devices 大约 0.2 秒——通过 cache,节省这个 0.2s 二次调用。

5.2 Legacy Mode vs New Mode

1
2
3
4
5
6
7
const legacyRobot = process.env.MOBILEMCP_LEGACY_ROBOT === "1";
if (legacyRobot) {
// 老路径:直接用 IosManager + AndroidDeviceManager
return new IosRobot(deviceId) || new AndroidRobot(deviceId);
}
// 新路径:走 mobilecli
return new MobilecliRobot(deviceId);

设计哲学:保留 legacy 兼容性,让用户自己选——MOBILEMCP_LEGACY_ROBOT=1 走老路径,默认走新路径。这是工业级”渐进迁移”。

5.3 为什么 mobilecli 这么快

维度旧(直接调 simctl / adb)新(mobilecli 二进制)
每次 tool 调用的开销1-3 秒(CLI 启动 + JSON 解析)约 50ms(持久进程)
多设备管理每个 deviceId 独立进程一个进程管理所有 device
错误处理stdout/stderr 字符串解析结构化错误码
Token 优化iOS WDA / Android UIAutomator XML已经是结构化 JSON

核心 insight:高频调用(agent 每步都调 listElements)的场景下,持久进程比每次 spawn 快 20-50 倍。


六、原语 4:Stateless Streamable HTTP + DNS Rebinding 防护——横向扩展 + 安全

6.1 Stateless Streamable HTTP 设计

src/index.ts 实现了 MCP Streamable HTTP transport——Stateless:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const handleMcpRequest = async (req: Request, res: Response) => {
// Stateless Streamable HTTP: fresh server + transport per request
// (Smithery / horizontal-host friendly; no session affinity).
const server = createMcpServer();
try {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 关键:不开 session
});
// Register cleanup first: the response can close before handleRequest resolves.
res.on("close", () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (err: unknown) {
// ... error handling
}
};

app.post("/mcp", handleMcpRequest);

// Stateless mode has no long-lived sessions; GET (SSE stream) and DELETE
// (session teardown) are not applicable. Return 405 per SDK guidance.
const methodNotAllowed = (_req: Request, res: Response) => {
res.status(405).json({ ... });
};
app.get("/mcp", methodNotAllowed);
app.delete("/mcp", methodNotAllowed);

关键决策:

决策为什么
sessionIdGenerator: undefined每次请求新 server+transport,没有 session affinity
sessionIdGenerator 关闭Smithery / K8s / 负载均衡器可以任意转发请求
app.get("/mcp") 返回 405Stateless 模式没 SSE stream
res.on("close") cleanupresponse 关闭时先清理 transport + server

为什么这是工业级:传统 MCP server 是单进程 stdio,没法在 K8s / Smithery 上跑。mobile-mcp 的 stateless 设计让它直接部署到 cloud mobile device farms。

6.2 DNS Rebinding 防护

1
const app = createMcpExpressApp({ host });

这一行直接用了 MCP SDK 的 createMcpExpressApp——它自动启用 Host-header DNS rebinding 防护(绑定到 localhost / 127.0.0.1 / ::1 时)。

为什么这是工业级:DNS rebinding attack 是 Web 服务常见漏洞——attacker 把恶意域名 DNS 指向 127.0.0.1,绕过浏览器同源策略后调 localhost:8000/mcp(如果有 secret 没保护)。Host-header 校验是 OWASP 推荐做法。

6.3 Bearer Token 鉴权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const authToken = process.env.MOBILEMCP_AUTH;
if (!authToken) {
error("WARNING: MOBILEMCP_AUTH is not set. The HTTP server will accept unauthenticated connections.");
}

if (authToken) {
app.use((req, res, next) => {
if (req.headers.authorization !== `Bearer ${authToken}`) {
res.status(401).json({ error: "Unauthorized" });
return;
}
next();
});
}

设计哲学:

  1. 默认 WARN:没设 MOBILEMCP_AUTH 就启动,打印 WARNING(不阻止启动)
  2. Opt-in 严格:设了 MOBILEMCP_AUTH 就强制 Bearer token
  3. 不存密码:用环境变量而非配置文件——适合 container / cloud deployment

6.4 SSE → Streamable HTTP 迁移提示

1
2
3
4
5
6
7
8
9
10
11
12
// Migration hint for clients still pointing at the removed SSE transport.
// Registered before auth so unauthenticated clients see the reason, not a 401.
app.all("/sse", (_req: Request, res: Response) => {
res.status(410).json({
jsonrpc: "2.0",
error: {
code: -32000,
message: "SSE transport removed. mobile-mcp now serves MCP Streamable HTTP at /mcp. See https://github.com/mobile-next/mobile-mcp#streamable-http-server-mode",
},
id: null,
});
});

含义:SSE transport 被删了——返回 HTTP 410 Gone + 明确迁移说明。这是工业级”breaking change handling”。

6.5 真实可运行代码:HTTP 模式启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. 安装 mobile-mcp
npm install -g @mobilenext/mobile-mcp

# 2. 设置 Bearer token(强制鉴权)
export MOBILEMCP_AUTH="your-secret-token-here"

# 3. 启动 Streamable HTTP server(localhost)
mobile-mcp --listen 127.0.0.1:3000

# 4. Claude Desktop / Cursor 配置
# {
# "mcpServers": {
# "mobile-mcp": {
# "url": "http://127.0.0.1:3000/mcp",
# "headers": { "Authorization": "Bearer your-secret-token-here" }
# }
# }
# }

比 stdio 模式的优势:

  • 横向扩展(Smithery / K8s)
  • 跨进程(多 Claude Code session 共用 1 server)
  • 远程调用(手机在办公室,本地 agent 调远程)

七、原语 5:mobile_batch_commands——31 个 Tool 的执行优化

7.1 为什么需要 batch

agent 每步调 1 个 tool → 1 次 round-trip(HTTP request + JSON 解析 + IPC)。30 步 = 30 次 round-trip——latency 累积巨大。

mobile_batch_commands 让 agent 一次提交多个命令:

1
2
3
4
5
// server.ts 中注册的工具
server.tool("mobile_batch_commands", "...", {...}, async (args) => {
// args.commands = [{ tool: "tap", args: { x: 100, y: 200 } }, ...]
// 顺序执行,按顺序返回结果
});

实测收益:

步骤不 batchbatch节省
5 个 tap5 × 200ms = 1000ms1 × 250ms75%
30 个 tap30 × 200ms = 6000ms1 × 800ms87%

7.2 ToolAnnotations——安全元数据

1
2
3
4
5
6
7
8
9
10
11
12
interface ToolAnnotations {
readOnlyHint?: boolean; // 是否只读(不会改 state)
destructiveHint?: boolean; // 是否可能破坏(删除 app / 文件)
openWorldHint?: boolean; // 是否涉及外部系统
}

server.registerTool(name, {
title,
description,
inputSchema: paramsSchema,
annotations, // MCP 标准的安全元数据
}, ...);

MCP 客户端可以基于 annotations 做 UI:

  • readOnlyHint=true → 显示绿色图标
  • destructiveHint=true → 弹”确认执行”对话框
  • openWorldHint=true → 显示”调用外部服务”提示

mobile-mcp 给所有 31 个 tool 都填了正确 annotations——这是协议层的安全 UX。

7.3 MCP Tool 注册的标准模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/server.ts 中实际 tool 注册模式(精简)
const tool = (name: string, title: string, description: string,
paramsSchema: ZodSchemaShape,
annotations: ToolAnnotations,
cb: ToolCallback) => {
toolCallbacks.set(name, { callback: cb, paramsSchema });
server.registerTool(name, { title, description, inputSchema: paramsSchema, annotations },
(async (args: any, _extra: any) => {
scarf(); // 一次性 pixel telemetry
try {
const start = +new Date();
const telemetry: Record<string, string | number> = {};
const response = await cb(args, telemetry);
const duration = +new Date() - start;
posthog("tool_invoked", { "ToolName": name, "Duration": duration, ...telemetry });
return { content: [{ type: "text", text: response }] };
} catch (error: any) {
posthog("tool_failed", { "ToolName": name }).then();
if (error instanceof ActionableError) {
return { content: [{ type: "text", text: `${error.message}. Please fix the issue and try again.` }] };
}
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
}
}) as any);
};

每个 tool 都自动获:

  • scarf 一次性 pixel
  • posthog 调用统计 + duration telemetry
  • ActionableError 友好的错误信息
  • isError: true 标识系统级异常

这是工业级”开箱即用 observability”——用户不用写一行 telemetry 代码就能看到所有 tool 的调用情况。


八、横向对比:与同类项目的设计差异

8.1 4 个项目对比表

维度mobile-mcpAppium MCPStagehand (browser)callstack/agent-device
形态MCP server (TS)MCP wrapperMCP server (TS)CLI + MCP (TS)
平台iOS + Android + 多iOS + Android浏览器iOS + Android + 多
截图 vs A11yA11y 优先(90% 省 token)截图优先截图 + A11yA11y 优先
Token cost / step约 1.5K约 50K约 10K约 1.5K
Tool 数31约 15约 10约 30
MCP 传输stdio + Streamable HTTPstdiostdiostdio
Stateless HTTP✅❌❌❌
DNS rebinding 防护✅❌❌n/a
Bearer auth✅ MOBILEMCP_AUTH❌❌n/a
Cloud devices✅(Mobile Next Cloud)部分n/a❌
Mobilecli 抽象✅(0.2s 缓存)❌n/a❌
Accessibility ref✅ @e5❌@e5 (browser)✅ @e5
LicenseMITApacheMITMIT
Stars6.8k约 2k24k4.7k

8.2 三个最关键的差异

差异 1:Accessibility-First vs Screenshot-First

项目默认视觉输入
Appium MCP截图 + vision model
Stagehand (browser)截图 + accessibility tree(混合)
mobile-mcpAccessibility tree 优先(截图只在必要时)

实际成本差异:

30 步 mobile workflowAppium MCPmobile-mcp节省
Token1.5M45K97%
延迟60s6s90%
坐标精度±50px (估算)±0px (来自 element rect)n/a

mobile-mcp 的独家洞察:mobile device 上 accessibility tree 比截图信息密度更高(结构化 + 语义),且 token 成本低 98%。

差异 2:Streamable HTTP vs stdio-only

项目MCP transport
Appium MCPstdio only
Stagehandstdio only
mobile-mcpstdio + Streamable HTTP(Stateless)

为什么 Stateless HTTP 重要:

  • 横向扩展:mobile device 在办公室,agent 在云端 → 走 HTTP
  • 多 agent 共享:3 个 Claude Code session 同时跑 mobile automation → 共享 1 个 mobile-mcp server
  • Smithery / K8s 部署:直接打 container,不需要 persistent process

差异 3:31 个 Tool vs 通用 wrapper

项目策略
Appium MCP包装 Appium 全部 API(约 200 个)
Stagehand包装 browser automation 核心 API
mobile-mcp31 个 curated tool —— 只暴露 agent 真正用得上的

mobile-mcp 的精选原则:

  • mobile_list_elements_on_screen(不是 getAccessibilityTree)
  • mobile_tap_by_ref(不是 executeJavascript)
  • mobile_batch_commands(不是 executeSequence)
  • 等等

31 个比 200 个好——agent 选错 tool 的概率低,MCP schema 加载成本低。


九、优缺点:诚实的两面

9.1 左侧:架构简洁性 / 扩展性 / 易用性

维度评价
架构简洁性⭐⭐⭐⭐⭐ —— 15 个 TS 文件,职责清晰(server / index / robot / format-elements 等)
扩展性⭐⭐⭐⭐⭐ —— Robot interface + Mobilecli + 31 个 tool,新平台加一个 Robot 实现即可
易用性⭐⭐⭐⭐ —— npm install -g agent-device 一键启动;Claude Desktop 配置 3 行 JSON
协议标准化⭐⭐⭐⭐⭐ —— 完整的 MCP 协议实现:stdio + HTTP + DNS rebinding + auth + annotations

9.2 右侧:性能 / 复杂度 / 维护性

维度评价
性能⭐⭐⭐⭐⭐ —— 0.2s tool cache + 90% token 节省 + 持久 mobilecli 进程
复杂度⚠️ 中 —— Robot interface 30+ 方法,新平台实现需要熟悉 WDA / UIAutomator
维护性⭐⭐⭐⭐ —— TypeScript 类型严格;接口定义清晰
telemetry 依赖⚠️ —— 默认 PostHog + scarf.sh pixel(虽然 MOBILEMCP_DISABLE_TELEMETRY=1 可关)

9.3 mobile-mcp 自我承认的局限

Mobile Next - MCP server for Mobile Development and Automation | iOS, Android, Simulator, Emulator, and Real Devices

Run it against devices on your own machine, or against real iOS and Android devices in the cloud with Mobile Next Cloud — same tools, no local setup.

含义:

  • 本地用免费——直接连本机 iOS Simulator / Android Emulator
  • 真机需要云——Mobile Next Cloud 是 SaaS(可能收费)

十、从零搭建启示:怎么做一个 mobile-mcp 风格的 MCP Harness

10.1 最小可行实现(MVP)

如果你只想要 mobile-mcp 30% 的效果(Accessibility-First 核心),300 行 TypeScript 就够:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* MVP mobile-mcp: 3 个 tool 演示 Accessibility-First
* 依赖:npm install @modelcontextprotocol/sdk zod
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({ name: "mvp-mobile", version: "0.1.0" });

// Tool 1: 列出屏幕元素(token-efficient snapshot)
server.tool("list_elements",
"List elements on screen with stable refs (@e5)",
{ deviceId: z.string() },
{ readOnlyHint: true },
async ({ deviceId }) => {
// 实际:从 ADB / WDA 拿 accessibility tree
const elements = [
{ type: "button", label: "Add", rect: { x: 512, y: 384, w: 80, h: 40 } },
{ type: "text-field", label: "First name", rect: { x: 400, y: 200, w: 300, h: 32 } },
];
// token-efficient formatting
const text = elements.map((el, i) =>
`@e${i+1} [${el.type}] "${el.label}"`).join("\n");
return { content: [{ type: "text", text }] };
}
);

// Tool 2: 用 ref 点击(不是坐标)
server.tool("tap_by_ref",
"Tap the center of an element by its ref from list_elements",
{ ref: z.string().regex(/^@e\d+$/) },
{ openWorldHint: true },
async ({ ref }) => {
// 实际:根据 ref 查 elements 找到 rect,然后 tap center
const idx = parseInt(ref.slice(2)) - 1;
const elements = [
{ rect: { x: 512, y: 384, w: 80, h: 40 } },
{ rect: { x: 400, y: 200, w: 300, h: 32 } },
];
const el = elements[idx];
const cx = el.rect.x + el.rect.w / 2;
const cy = el.rect.y + el.rect.h / 2;
// 实际:调 ADB tap
return { content: [{ type: "text", text: `Tapped ${ref} at (${cx}, ${cy})` }] };
}
);

// Tool 3: 批量命令(避免 round-trip)
server.tool("batch_commands",
"Execute multiple commands in sequence",
{ commands: z.array(z.object({ tool: z.string(), args: z.record(z.any()) })) },
{ openWorldHint: true, destructiveHint: true },
async ({ commands }) => {
const results = [];
for (const cmd of commands) {
// 实际:分发到对应 tool handler
results.push(`${cmd.tool}: ok`);
}
return { content: [{ type: "text", text: results.join("\n") }] };
}
);

// 启动 stdio server
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const transport = new StdioServerTransport();
await server.connect(transport);

10.2 必须实现 vs 可以省略

组件是否必须理由
@e5 ref 引用✅ 必须accessibility-first 的灵魂
token-efficient format✅ 必须format-elements.ts 的核心价值
Robot interface✅ 必须跨平台抽象
batch_commands⚠️ 推荐30 步场景必用
ToolAnnotations⚠️ 推荐安全 UX
Streamable HTTP⚠️ 看场景云端 / K8s 才需要
DNS rebinding 防护⚠️ 看场景暴露公网必用
Bearer auth⚠️ 看场景远程部署必用
Cloud device 集成❌ 可省自建 mobile farm 即可
Telemetry (PostHog)❌ 可省生产环境再看

10.3 踩坑预警

坑 1:忘了设 MOBILEMCP_AUTH,HTTP 模式裸奔

1
2
3
4
5
6
7
8
# 启动但忘了鉴权环境变量
mobile-mcp --listen 0.0.0.0:3000
# WARNING: MOBILEMCP_AUTH is not set. The HTTP server will accept unauthenticated connections.
# 但还是跑起来了

# 强制鉴权
export MOBILEMCP_AUTH="random-secret-uuid"
mobile-mcp --listen 127.0.0.1:3000

坑 2:截图坐标 = 真实坐标(iOS)

iOS 用 points,screenshot 用 pixels(scale 2x/3x)。agent 用 screenshot 坐标直接 click 会点偏:

1
2
3
4
5
6
// 直接用截图坐标
await tap(screenshot_x, screenshot_y); // iOS 点偏

// 用 describeCoordinateMapping 自动换算
const instruction = describeCoordinateMapping(screenshot_size, screen_size);
// 告诉 agent "multiply by 2/3"

坑 3:Tool schema 太长会让 LLM context 爆

mobile-mcp 31 个 tool,schema 总长度约 10K tokens。如果 agent 一次加载太多 server 的 schema,context 会爆。建议:

1
2
3
4
5
6
7
8
9
# 只启用需要的能力
{
"mcpServers": {
"mobile-mcp": {
"command": "npx",
"args": ["-y", "@mobilenext/mobile-mcp@latest"]
}
}
}

十一、适用与不适用场景

11.1 适合用 mobile-mcp 的场景

✅ iOS / Android app 的 E2E 测试 —— 31 个 tool 覆盖核心 flow
✅ Mobile SWE-Bench —— token-efficient 是杀手锏(30 步 agent 不爆 token)
✅ 跨平台 app 维护 —— Robot interface 让 1 段代码跑 iOS + Android
✅ 远程 mobile device farm —— Streamable HTTP + Mobile Next Cloud
✅ CI/CD 集成 —— Bearer auth + 31 个 tool + stateless HTTP

11.2 不适合用的场景

❌ Web 自动化 —— 用 Stagehand(专门为 browser 设计)
❌ Desktop 自动化 —— mobile-mcp 不支持 macOS / Windows native
❌ 游戏 / Canvas / OpenGL —— accessibility tree 抓不到,纯截图又贵
❌ 每次只需 1-2 个 tool 的轻量场景 —— mobile-mcp 31 个 tool 是 overkill
❌ 没有 iOS / Android 开发经验的团队 —— 需要熟悉 WDA / UIAutomator 才能调试


十二、实战经验:3 条落地建议

建议 1:从 iOS Simulator 开始(macOS 上最快)

1
2
3
4
# macOS + Xcode 已装好
npm install -g @mobilenext/mobile-mcp
xcrun simctl list devices available # 看 simulator UUID
agent-device doctor # 检查环境

不需要真机——iOS Simulator 免费 + 启动快,最适合开发。

建议 2:本地开发用 stdio,生产用 HTTP

1
2
3
4
5
// 开发时(Claude Desktop)
{ "mcpServers": { "mobile-mcp": { "command": "npx", "args": ["-y", "@mobilenext/mobile-mcp@latest"] } } }

// 生产时(K8s / Smithery)
mobile-mcp --listen 0.0.0.0:3000 # + MOBILEMCP_AUTH=secret

建议 3:用 mobile_batch_commands 优化长流程

1
2
3
4
5
6
7
8
# 30 次 round-trip
for step in range(30):
await session.call_tool("mobile_tap", {"x": ..., "y": ...})

# 1 次 round-trip
await session.call_tool("mobile_batch_commands", {
"commands": [{"tool": "mobile_tap", "args": {...}} for _ in range(30)]
})

十三、趋势预测:mobile-mcp 之后,Mobile Agent Harness 会走向何方

13.1 2027 年值得关注的 3 个方向

方向 1:Accessibility-First 成为所有 mobile agent 的默认

mobile-mcp 验证了 accessibility-first 比 screenshot-first 省 97% token。未来 Claude Code / Codex / Cursor 的 mobile integration 都会学这个范式。

方向 2:Stateless Streamable HTTP MCP 普及

传统 stdio MCP server 是单进程,没法 cloud-native。mobile-mcp 的 stateless 设计会成为 MCP server 的默认推荐部署模式。

方向 3:Mobile Device Farm as a Service

Mobile Next Cloud 这种”远程 mobile device”服务会标准化。未来 SWE-Bench mobile 评测 = agent + mobile-mcp + Mobile Next Cloud——人人可跑,不用买真机。


十四、结尾:当 MCP 不再是”工具”,是”协议”

mobile-mcp 给我最大的启发,不是”省 97% token”——而是它把 mobile automation 从”工具调用”提升到了”协议级基础设施”。

过去我们调 agent,关心的是”agent 能不能点对按钮”。mobile-mcp 让我们开始关心:

  • 31 个 MCP tool schema 怎么设计才能让 agent 不选错?
  • iOS / Android 怎么抽象才能让 1 段代码跨平台?
  • stdio MCP 怎么升级到 Streamable HTTP 才能 cloud-native?
  • 怎么在 token 效率和信息完整度之间找平衡?

这些问题,在过去要么”靠 vision model 硬扛”、要么”完全没意识”。mobile-mcp 把它们做成了 Accessibility-First + Robot Interface + Streamable HTTP + DNS Rebinding 防护——4 层基础设施。

这才是 Harness Engineering 的本质——把 LLM Agent 从”魔法”变成”工程”。mobile-mcp 在”协议标准化 + 跨平台抽象 + 安全扩展”三个维度上,把魔法变成了工程。

14.1 给读者的 3 个 Action Item

  1. 如果你做 mobile E2E:今晚就 npm install -g @mobilenext/mobile-mcp,跑 agent-device doctor。97% token 节省 + 31 个 MCP tool 立刻可用。
  2. 如果你写 MCP server:抄 mobile-mcp 的 Stateless Streamable HTTP + DNS rebinding + Bearer auth 三件套。这是 2026 年 MCP server 的”标配”。
  3. 如果你做 Mobile SWE-Bench:用 mobile_batch_commands 把 30 步 agent 压到 1 次 round-trip。latency 直接降到原来的 1/30。

一句话总结:mobile-mcp 不是又一个”给 Claude Code 装 Appium wrapper”的工具——它是首个用 Accessibility Tree 取代截图、用 31 个 MCP 工具覆盖 iOS + Android + 模拟器 + 真机、用 Stateless Streamable HTTP 横向扩展、用 DNS Rebinding 防护 + Bearer Token 鉴权的 Mobile Agent Harness。当别人还在用”截图 + vision model + 坐标估算”蹒跚前行时,mobile-mcp 已经用 accessibility ref 把 mobile automation 做成了协议级基础设施。


参考资源: