MCP Service User Guide
Expose AI Agent phone automation capabilities as standard tools via MCP (Model Context Protocol), for use by external AI Agents such as OpenClaw, Qoder, Claude Desktop, etc.
Provides two access channels:
Channel Transport Supported Clients Status A. Local MCP Server (Recommended) stdio JSON-RPC + WebSocket relay OpenClaw / Qoder / Claude Desktop and any standard MCP client ✅ Stable B. WebMCP (Browser Native) document.modelContextChrome built-in AI (Gemini Nano) and other in-page Agents 🧪 Experimental (Chrome 149+ Origin Trial)
Table of Contents
- Architecture Overview
- Flow Diagrams
- Channel A: Local MCP Server (Recommended)
- Channel B: WebMCP (Browser Native)
- Tool Capability Overview
- Tool Definitions
- Skill Configuration
- Integration Code Details
- UI Status Indicators
- FAQ
- Appendix: AgentTask Data Structure
Architecture Overview
┌──────────────────────────────────────────────────────────────────────┐
│ External AI Agent │
│ (OpenClaw / Qoder / Claude Desktop / Chrome AI) │
└───────────┬──────────────────────────────────────────┬───────────────┘
│ Channel A: stdio JSON-RPC 2.0 │ Channel B:
▼ │ document.modelContext
┌───────────────────────────────┐ │ .callTool()
│ mcp-server/dist/server.js │ ▼
│ (Node.js McpServer) │ ┌───────────────────────────────┐
│ 6 Tools registered on stdio │ │ Chrome WebMCP Runtime │
└───────────┬───────────────────┘ │ (#enable-webmcp-testing) │
│ WebSocket └───────────────┬───────────────┘
│ ws://localhost:7788 │ execute()
▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ Browser Page (Mobile AI Use) │
│ McpRelayClient (Channel A) WebMcpService (Channel B) │
│ └────────────┬────────────┘ │
│ ▼ McpServiceContext (shared) │
│ Plan-and-Resolve Agent / ReAct Agent │
└─────────────────────────────┬────────────────────────────────────────┘
│ Web ADB
▼
┌──────────────────────────────────────────────────────────────────────┐
│ Android / iOS Device │
└──────────────────────────────────────────────────────────────────────┘
Both channels ultimately share the same McpServiceContext, with identical tool semantics; the only differences are in the transport layer and some return formats (see Tool Definitions).
Flow Diagrams
Channel A: Local MCP Server Call Flow
sequenceDiagram
participant Agent as External Agent
(OpenClaw/Qoder)
participant MCP as mcp-server
(Node.js, stdio)
participant WS as WebSocket Relay
ws://localhost:7788
participant Relay as McpRelayClient
(Browser)
participant Panel as AiChatPanel
participant Device as Phone Device
Note over MCP,Relay: Relay auto-connects after page opens
(reconnects every 30s on disconnect)
Agent->>MCP: tools/call run_agent_task {goal}
MCP->>WS: {id, tool, args} (JSON)
WS->>Relay: Forward request
Relay->>Relay: Check device connection & Agent idle
Relay->>Panel: runAgentForMcp(goal, signal)
Panel->>Device: Plan-and-Resolve: screenshot → vision analysis → touch
Device-->>Panel: Operation result
Panel-->>Relay: AgentTask (finished)
Relay->>WS: {id, result: Markdown report}
WS->>MCP: Match pending request
MCP-->>Agent: CallToolResultChannel B: WebMCP Service Registration Flow
flowchart TD
A[Start] --> B[Mobile AI Use]
B --> C{document.modelContext exists?}
C -->|No| D[WebMCP not registered
Relay client only]
C -->|Yes| E[Create WebMcpService instance]
E --> F[register: register 6 Tools]
F --> G[External in-page Agent can discover and call]
D --> H[McpRelayClient.connect]
G --> H
H --> I{Connected to ws://localhost:7788?}
I -->|Yes| J[Relay badge green]
I -->|No| K[Relay badge gray
Auto-retry in 30s]Task Execution State Machine
stateDiagram-v2
[*] --> idle: Initialize
idle --> planning: run_agent_task triggered
planning --> executing: Plan complete
executing --> finished: All steps succeeded / Scroll extraction complete
executing --> failed: Step execution failed
executing --> aborted: abort_task called / Timeout
finished --> [*]
failed --> [*]
aborted --> [*]💡 Special termination condition: When the Agent executes a scroll extraction successfully, the extraction result is returned directly as the task conclusion.
Channel A: Local MCP Server (Recommended)
Project Download
Directory Structure
mcp-server/
├── src/server.ts ← McpServer + WebSocket relay implementation
├── dist/server.js ← Build output (MCP client config points to this file)
├── package.json ← name: phone-agent-mcp
└── tsconfig.json
How It Works
- MCP Protocol Layer: Uses
@modelcontextprotocol/sdk'sMcpServer+StdioServerTransport, communicating with MCP clients via stdio JSON-RPC 2.0; Server name isphone-agent - WebSocket Relay Layer: Embedded
WebSocketServer(default port 7788), the browser page'sMcpRelayClientconnects actively - Call Forwarding: Upon receiving
tools/call, sends{id, tool, args}to the browser viacallBrowser(), waits for the browser to return{id, result}, then returns to the MCP client - Connection Self-Check: 3 seconds after the browser connects, the Server automatically calls
get_device_statusonce to verify the link - Single Connection Model: Only one browser connection is supported at a time; new connections replace old ones
Startup Steps
cd mcp-server
npm install # Install dependencies on first run
npm run build # Compile TypeScript → dist/server.js
npm start # Start (stdio + WebSocket :7788)
# Development mode (no-build hot execution):
npm run dev # tsx src/server.ts
Then:
- Open the Mobile AI Use page (e.g.
https://mobile-ai-use.com/mobileAi),McpRelayClientauto-connects to the relay after page load - Confirm the MCP Relay badge is green at the top of the AI chat panel
- Connect an Android phone via USB and confirm screen mirroring is working
Environment Variables
| Variable | Default | Description |
|---|---|---|
PHONE_AGENT_WS_PORT |
7788 |
WebSocket relay port. After modification, the browser-side McpRelayClient connection URL must be updated accordingly |
MCP Client Configuration
OpenClaw (openclaw.json):
{
mcp: {
servers: {
"phone-agent": {
command: "node",
// Replace <PROJECT_ROOT> with the actual absolute path
args: ["<PROJECT_ROOT>/mcp-server/dist/server.js"],
env: { PHONE_AGENT_WS_PORT: "7788" }
}
}
}
}
Or via CLI:
openclaw mcp set phone-agent --command node --args "<PROJECT_ROOT>/mcp-server/dist/server.js"
Qoder (.qoder/mcp.json or global MCP config):
{
"mcpServers": {
"phone-agent": {
"command": "node",
"args": ["<PROJECT_ROOT>/mcp-server/dist/server.js"],
"env": { "PHONE_AGENT_WS_PORT": "7788" }
}
}
}
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"phone-agent": {
"command": "node",
"args": ["<PROJECT_ROOT>/mcp-server/dist/server.js"]
}
}
}
WebSocket Relay Protocol
// Server → Browser (Request)
interface WsRequest {
id: string; // Unique request ID (timestamp + random string)
tool: string; // Tool name, e.g. 'run_agent_task'
args: Record<string, unknown>; // Tool parameters
}
// Browser → Server (Response)
interface WsResponse {
id: string; // Corresponds to request ID
result: McpToolResult; // { content: [...], isError? }
}
// Browser → Server (Intermediate progress, only pushed for run_agent_task)
interface WsProgress {
id: string; // Corresponds to request ID
type: 'progress'; // Server uses this field to distinguish from final response
seq: number; // Progress sequence number, incrementing from 1
message: string; // Human-readable process description
}
Multiple WsProgress messages may appear on the same id before a final WsResponse closes it. When the Server receives a progress frame, it only forwards — it does not end the request or reset the timeout.
Real-Time Agent Execution Progress Reporting
During run_agent_task execution, the browser pushes the Agent's thinking and execution process to the Server in real time, which then converts it to MCP notifications for external clients (OpenClaw / Qoder / Claude Desktop) to display, preventing the "frozen" feeling during long tasks.
Reporting Points (corresponding to Agent lifecycle callbacks in runAgent):
| Point | Reported Content |
|---|---|
onPlanned |
📋 Execution plan generated (N steps total) + step title list |
onStepStart |
▶️ Step N: Title + step description |
onStepText |
Buffered, not directly reported (triggered per token; direct passthrough would flood the channel) |
onToolCall |
First flush buffered thinking text (💭 ...), then report 🔧 Calling tool name(args) |
onToolResult |
→ Result (truncated to 300 characters) |
onStepEnd |
Flush remaining thinking + ✅/❌/⏭️ Step N completed (status, duration Xs) |
onError |
❌ Agent execution error: ... |
Server-Side Forwarding Strategy (mcp-server/src/server.ts):
- Client declares willingness to receive progress via
_meta.progressTokenin the request → sendnotifications/progress(progresstakesseq,messagetakes the process text) - No progressToken declared → falls back to
notifications/message(logging,level: info,logger: phone-agent), ensuring process visibility - Notification send failures are logged to stderr only — Agent task execution is never interrupted
Channel B (WebMCP) has no corresponding notification mechanism; the
onProgressparameter is optional and not passed on the WebMCP side.
Browser-Side McpRelayClient
File: relay-client.ts
- Auto
connect()on page load, auto-reconnects every 30 seconds on disconnect - Shares the exact same
McpServiceContextas WebMCP; tool dispatch logic centralized in_dispatch() onStatusChange(connected)callback drives UI badge staterun_agent_taskreturn value is formatted by_formatTaskMarkdown()into a readable Markdown report (task summary + per-step title/status/duration/tool call list) for easy reference by external Agents_sendProgress()maintains incrementing sequence perreqId; silently ignored on disconnect or send error (progress display is supplementary — must not affect the main flow)
Channel B: WebMCP (Browser Native)
🧪 Experimental capability, depends on Chrome Origin Trial, suitable for in-page Agents (e.g. Chrome built-in AI). External process clients should use Channel A.
Browser Requirements
| Item | Requirement |
|---|---|
| Browser | Chrome 149+ (Canary / Dev Channel) |
| Flag | chrome://flags/#enable-webmcp-testing → Enabled |
| Protocol | Page must be accessed via HTTPS or localhost |
| API Entry | document.modelContext (legacy navigator.modelContext is deprecated; code uses it only as fallback) |
Enable Steps
- Enable WebMCP Flag: Enter
chrome://flags/#enable-webmcp-testingin the address bar, set toEnabled, then restart the browser - Verify API Availability:
// DevTools Console console.log(typeof document.modelContext); // "object" console.log(typeof document.modelContext.registerTool); // "function" - Open the WebADB Page and Connect Phone: On page load,
WebMcpService.register()automatically registers 6 Tools (console shows[WebMCP] 6 tools registeredlog)
Tool Capability Overview
Both channels register tools with identical names and parameters:
| # | Tool Name | Type | Description | Server-Side Timeout (Channel A) |
|---|---|---|---|---|
| 1 | run_agent_task |
Write | Run AI Agent automation task on the phone | timeoutMs + 5s (default 20 min) |
| 2 | abort_task |
Write | Abort the currently running Agent task | 10s |
| 3 | get_task_result |
Read | Query detailed task results by taskId | 10s |
| 4 | get_latest_task |
Read | Get the latest task result in the current conversation | 10s |
| 5 | take_screenshot |
Read | Phone screen screenshot (returns PNG base64) | 30s |
| 6 | get_device_status |
Read | Get connected device information | 15s |
In Channel B (WebMCP),
run_agent_taskhas arequiresUserApproval: trueannotation — Chrome will show a user confirmation dialog. In Channel A, the correspondingdestructiveHint: trueannotation means confirmation depends on the MCP client's policy.
Tool Definitions
1. run_agent_task
Run an AI Agent automation task on the connected phone. The Agent uses a Plan-and-Resolve architecture, achieving goals through screen visual understanding and touch operations.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
goal |
string | ✅ | Natural language task goal, e.g. "Open WeChat and send a message to Zhang San: see you tomorrow" |
timeoutMs |
number | ❌ | Maximum execution time (ms). Channel A default 1200000 (20 min), Channel B default 120000 (2 min) |
Pre-Checks: Returns No phone device connected. if device is not connected; returns Agent is already running. Please wait or call abort_task first. if Agent is running.
Return Format (Channel A — Markdown Report):
## ✅ Agent Task Completed
**Goal**: Open WeChat and send a message to Zhang San
**Status**: finished **Steps**: 4/4 succeeded **Duration**: 32.5s
**Conclusion**: WeChat message sent successfully
---
### ✅ Step 1: Open WeChat (5.2s)
- `tap({"x":540,"y":1200})` → tapped at (540,1200)
...
Return Format (Channel B — JSON):
{
"content": [{
"type": "text",
"text": "{\"taskId\":\"abc123\",\"status\":\"finished\",\"conclusion\":\"WeChat message sent successfully\",\"stepsCompleted\":4,\"totalSteps\":4,\"durationMs\":32500,\"error\":null}"
}]
}
💡 Scroll Content Extraction Task: If the goal involves "extract list content / scroll capture", the Agent will call the
ExtractScrollingContenttool (auto-scroll screenshots → stitch long image → AI analysis). After extraction completes, the task auto-terminates, with the extraction summary returned asconclusion.
2. abort_task
Abort the currently running Agent task. No parameters.
Return Example:
{ "content": [{ "type": "text", "text": "Task aborted." }] }
Returns No task is currently running. when no task is running.
3. get_task_result
Query detailed results of a completed task by taskId (including per-step execution records).
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
taskId |
string | ✅ | Task ID returned by run_agent_task |
includeSteps |
boolean | ❌ | Whether to include per-step details (default true) |
Return Example:
{
"content": [{
"type": "text",
"text": "{\"taskId\":\"abc123\",\"goal\":\"Open WeChat and send message\",\"status\":\"finished\",\"conclusion\":\"Task completed\",\"stepsCompleted\":3,\"totalSteps\":3,\"durationMs\":28000,\"steps\":[{\"index\":1,\"title\":\"Open WeChat\",\"status\":\"success\",\"durationMs\":5200,\"error\":null}]}"
}]
}
4. get_latest_task
Get the full result (including steps) of the most recent Agent task in the current conversation. No parameters. Return format is the same as get_task_result.
5. take_screenshot
Take a screenshot of the connected phone screen, returns a PNG image (base64 encoded). No parameters.
Return Example:
{
"content": [{
"type": "image",
"data": "iVBORw0KGgoAAAANSUhEUg...",
"mimeType": "image/png"
}]
}
6. get_device_status
Get basic information about the currently connected phone device. No parameters.
Return Example:
{
"content": [{
"type": "text",
"text": "{\"connected\":true,\"model\":\"Pixel 7\",\"brand\":\"google\",\"release\":\"14\",\"sdk\":\"34\"}"
}]
}
Returns {"connected": false} when no device is connected.
Skill Configuration
The project includes a built-in Skill definition file: /skills/phone-agent/SKILL.md download, which can be directly recognized and loaded by OpenClaw / Qoder.
---
name: phone-agent
description: Invoke WebADB AI Agent via local MCP Server to execute automation tasks on the connected phone
tools:
- mcp__phone-agent__run_agent_task
- mcp__phone-agent__abort_task
- mcp__phone-agent__take_screenshot
- mcp__phone-agent__get_device_status
- mcp__phone-agent__get_task_result
- mcp__phone-agent__get_latest_task
---
After configuration, simply describe your task in natural language:
- "Open WeChat, find the conversation with Zhang San, and send a 'Hello'"
- "Take a screenshot to see what's currently on the phone screen"
- "Open Xiaohongshu and search for deepseek, extract the titles and authors of the top 10 notes"
- "Abort the currently running task"
- "What was the result of the most recent Agent task"
Typical tool call sequence:
get_device_status # Confirm device is online
run_agent_task {goal} # Dispatch task (blocks until complete, returns Markdown report)
get_latest_task # Supplementary query when structured data is needed
Integration Code Details
Core Files
mcp-server/src/server.ts ← Local MCP Server (Channel A server)
skills/phone-agent/SKILL.md ← Skill definition
McpServiceContext Interface
| Method | Return Type | Description |
|---|---|---|
runAgent(goal, signal) |
Promise<AgentTask | void> |
Execute Agent task |
getTask(taskId) |
AgentTask | undefined |
Query task by ID |
getAllTasks() |
AgentTask[] |
All tasks in current conversation |
screenshot() |
Promise<string> |
Screenshot (data URL) |
deviceInfo() |
Promise<DeviceInfo> |
Device information |
isRunning() |
boolean |
Whether Agent is running |
abort() |
void |
Abort current task |
isConnected() |
boolean |
Whether device is connected |
Key Implementation Details
executeProperty Name (Channel B): WebMCP spec requires tool handler functions to be namedexecute(nothandler)document.modelContext: The API is mounted ondocument. Earlier versions mounted onnavigator; Chrome still retains it but outputs anavigator.modelContext is deprecatedwarning on access;WebMcpService.getModelContext()prioritizesdocument, only falling back tonavigatorafter checking within(directly reading the property would trigger the warning)- Concurrency Protection:
run_agent_taskchecksisRunning()before execution — only one Agent task allowed at a time - Timeout Control: Browser-side via
AbortController+setTimeout; Channel A Server-side has an additionaltimeoutMs + 5sfallback timeout, returningisError: trueon timeout instead of hanging - Reconnection:
McpRelayClientretries every 30 seconds after disconnect; on the Server side, duplicate browser connections cause the old connection to be replaced - Request Pairing: The relay layer uses unique
idto pair requests/responses in thependingMap; browser disconnect does not affect timeout handling for other pending requests
UI Status Indicators
The AI Agent chat panel provides a MCP Relay badge at the top, indicating the connection status between the browser and the local MCP Server (Channel A):
| Status | Style | Meaning |
|---|---|---|
| 🟢 Green breathing animation | mcp-badge--ready |
Relay connected — OpenClaw / Qoder can call the phone Agent |
| ⚪ Gray | mcp-badge--off |
Relay not connected (local MCP Server not running) |
Hover tooltip:
- Connected:
MCP Relay connected: OpenClaw / Qoder can call the phone Agent - Not connected:
MCP Relay disconnected: please start the local MCP server (cd mcp-server && npm start)
Channel B (WebMCP) registration results are confirmed via console log: on success, outputs
[WebMCP] 6 tools registered: ...; when the environment is unsupported, outputs a warn message.
FAQ
Channel A (Local MCP Server)
Q: Tool call returns "Browser not connected. Open the WebADB page first."
Cause: The browser page is not open, or McpRelayClient hasn't connected to the relay.
Troubleshooting:
- Confirm
mcp-serveris running (terminal should showWebSocket relay on ws://localhost:7788) - Open the WebADB page and confirm the Relay badge is green
- If the Server was just started, wait up to 30 seconds (browser auto-reconnect interval) or refresh the page
Q: Relay badge stays gray
Troubleshooting:
- Is the local MCP Server running:
cd mcp-server && npm start - Has the port been changed:
PHONE_AGENT_WS_PORTand the browser-side connection URL (defaultws://localhost:7788) must match - If the port is occupied, switch to another port and update the
envin the MCP client configuration accordingly
Q: Tool call times out (Tool "run_agent_task" timed out after ...ms)
Cause: Agent task execution time exceeds timeoutMs (Server-side fallback is timeoutMs + 5s).
Solution: For complex tasks, explicitly pass a larger timeoutMs; or call abort_task to interrupt, then split the task and retry.
Q: Multiple Mobile-AI-Use pages open, causing abnormal behavior
Cause: The relay only supports one browser connection at a time; the last connected page replaces previous connections.
Solution: Keep only one Mobile-AI-Use page tab open.
Channel B (WebMCP)
Q: Error Failed to read the 'execute' property from 'ModelContextTool'
Cause: The tool definition uses handler instead of execute as the handler function property name.
mc.registerTool({
name: 'my_tool',
inputSchema: { ... },
execute: async (params) => { ... }, // ✅ Correct
// handler: async (params) => { ... }, // ❌ Wrong
});
Q: document.modelContext is undefined
Troubleshooting Steps:
- Confirm Chrome version ≥ 149 (
chrome://version) - Confirm
chrome://flags/#enable-webmcp-testingis enabled - Confirm the page is accessed via HTTPS or localhost
- Restart the browser and retry
Q: External Agent cannot see registered Tools
Possible Causes:
- The page is not the active tab (Chrome may suspend MCP services for background tabs)
isAvailablereturned false during registration (check console warn logs)- Origin mismatch (the external Agent's configured origin must match the page URL)
General
Q: run_agent_task returns "Agent is already running"
Cause: Only one Agent task is allowed to run at a time.
Solution: Call abort_task first to interrupt the current task, then re-dispatch.
Q: Screenshot returns blank or errors
Troubleshooting:
- Confirm the device is connected via USB and screen mirroring is working
- Confirm
get_device_statusreturnsconnected: true - Check if the Scrcpy service is running normally
Appendix: AgentTask Data Structure
interface AgentTask {
id: string; // Unique task ID
goal: string; // User goal (natural language)
steps: AgentStep[]; // Step list from the Plan phase
status: 'idle' | 'planning' | 'executing' | 'finished' | 'failed' | 'aborted';
conclusion?: string; // Final conclusion (extraction summary for scroll extraction tasks)
error?: string; // Error message
createdAt: number; // Creation timestamp
startedAt?: number; // Execution start timestamp
finishedAt?: number; // Completion timestamp
}
interface AgentStep {
id: string;
index: number; // Step number (starting from 1)
title: string; // Step title
description: string; // Detailed description
summary?: string; // Execution summary (referenced in Markdown reports)
status: 'pending' | 'running' | 'success' | 'failed' | 'skipped';
toolCalls: ToolCallRecord[]; // Tool call records
beforeScreenshot?: string; // Pre-execution screenshot
afterScreenshot?: string; // Post-execution screenshot
error?: string;
startedAt?: number;
finishedAt?: number;
}