Android Debug MCP
June 4, 2026 · View on GitHub
中文 | English
Android Debug MCP is a local MCP server that lets AI agents debug debuggable Android apps at runtime.
It exposes screen inspection, allowlisted device actions, JDWP/JDI attach, Java/Kotlin line breakpoints, scoped variable reads, timeline export, and breakpoint evidence recording as MCP tools.
Architecture In One Sentence
An AI client calls a local Kotlin MCP server; the server controls the device through adb, attaches to the app through JDWP/JDI, and returns structured breakpoint, variable, screen, and timeline evidence back to the AI.
Why This Exists
AI can read source code, but source code alone is often not enough for Android runtime bugs. Real bugs depend on the current screen, process, thread, stack frame, object state, and event order.
Android Debug MCP gives AI a safe, controlled, and reviewable runtime debugging bridge:
- The AI decides the next debugging step.
- The MCP server performs reliable low-level debug operations.
- Conclusions come from breakpoint, variable, screen, and timeline evidence.
Capabilities
- List connected Android devices.
- Capture screenshots, UIAutomator trees, screen size, and current activity.
- Execute allowlisted input actions such as tap, swipe, text, back, launch, and UI text based taps.
- Attach to a debuggable Android app through
adb forward tcp:<port> jdwp:<pid>. - Set Java/Kotlin class + line breakpoints, including breakpoints for classes not loaded yet.
- Wait for breakpoint events and inspect thread, stack, local names, class, method, and line.
- Read selected variables and fields from the suspended stack frame.
- Resolve Kotlin/JVM class names back to likely source files in multi-module projects.
- Record a session timeline and export
session.json/session.html. - Group breakpoint evidence by
File.kt:L123and preserve global hit order. - Run short server-side debug plans for deterministic repeated collection loops.
Safety Boundaries
- Java/Kotlin runtime debugging only.
- No native or C++ debugging.
- No variable mutation.
- No arbitrary method invocation.
- No arbitrary
adb shell. - Device operations must use allowlisted
action_executeactions. - Breakpoint variable reads are scoped to the current suspended event.
These limits keep runtime evidence trustworthy. A debug tool that mutates app state too freely can create the bug it is trying to diagnose.
How It Works
flowchart LR AI["AI Client"] --> MCP["MCP JSON-RPC over stdio"] MCP --> Server["android-debug-mcp Kotlin Server"] Server --> ADB["adb"] ADB --> Device["Android Device"] Server --> JDI["JDI VirtualMachine"] JDI --> JDWP["adb forward tcp:port -> jdwp:pid"] JDWP --> App["Debuggable App Process"] Server --> Evidence["Timeline + Breakpoint Evidence"]
The server speaks MCP JSON-RPC over stdio. Device-level operations use adb; runtime breakpoints and stack/variable inspection use JDI over a JDWP port forwarded through adb.
Attach flow:
- Find the target process id by package or process name.
- Forward a local TCP port to the app JDWP endpoint.
- Attach with JDI
SocketAttach. - Use the returned
VirtualMachinefor breakpoints, event queues, stack frames, and variables.
Breakpoint behavior:
- If a class is already loaded, the server installs a
BreakpointRequestimmediately. - If a class is not loaded, the server installs a
ClassPrepareRequestand creates the real breakpoint later. - Breakpoints use
SUSPEND_EVENT_THREAD, so only the hit thread is suspended.
Requirements
- JDK with the
jdk.jdimodule. - Android SDK platform-tools.
adbavailable on PATH, or setANDROID_DEBUG_MCP_ADB.- A connected Android device or emulator with USB debugging enabled.
- A debuggable target app.
Build
./gradlew build
./gradlew installDist
Run the MCP server:
build/install/android-debug-mcp/bin/android-debug-mcp
The server waits for MCP JSON-RPC messages on stdio.
Usage Guide For AI Agents
The intended workflow is: you describe the Android runtime problem, the AI chooses breakpoints and variable paths, and this MCP server performs the actual device control, JDWP/JDI attach, breakpoint handling, variable reads, and evidence recording.
1. Prepare The Target App
Make sure the Android app is debuggable and running on a connected device or emulator:
adb devices
The AI needs at least:
packageName: Android package name of the target app.projectRoot: local source checkout used for class and line discovery.- A concrete bug, workflow, or runtime question.
Optional but useful:
processName: required when the debug target is a secondary process.deviceSerial: required when more than one adb device is connected.- Suspected class names, method names, routes, logs, or UI steps.
2. Install The Codex Plugin
This repository includes an optional local Codex plugin under codex-marketplace/.
codex plugin marketplace add /path/to/android-debug-mcp/codex-marketplace
codex plugin add android-debug-mcp@android-debug-local
Point the plugin to this repository:
export ANDROID_DEBUG_MCP_REPO=/path/to/android-debug-mcp
If your Codex environment does not inherit shell environment variables, edit codex-marketplace/plugins/android-debug-mcp/.mcp.json and replace ${ANDROID_DEBUG_MCP_REPO...} with your absolute checkout path.
3. Start A New AI Thread
After installing the plugin, start a new Codex thread so the MCP server and skill are loaded.
Prompt template:
Use $android-runtime-debug to debug my Android app.
packageName: com.example.app
processName: com.example.app
projectRoot: /path/to/android-project
Problem:
When I tap Refresh on the home screen, the list does not update.
Please inspect the current screen, choose precise Java/Kotlin breakpoints from source, drive the app with allowed Android actions, read variables after breakpoint hits, resume after every pause, and detach at the end.
Keep breakpoint evidence for this session. At the end, group observed values by File.kt:Lxx and use breakpointOrder to explain the runtime sequence.
Multi-device setup:
deviceSerial: emulator-5554
Secondary process:
processName: com.example.app:worker
4. What The AI Will Do
The AI should normally call tools in this order:
device_list
screen_snapshot
source_resolve
debug_attach
debug_set_breakpoint
action_execute
debug_wait_event
debug_read_variables
debug_resume
debug_breakpoint_results
timeline_export
debug_detach
For uncertain Kotlin line mappings, the AI may use:
debug_set_breakpoint_group
For repeated deterministic collection, the AI may use a short:
debug_run_plan
debug_run_plan should be short. It is useful for reducing repetitive wait/read/resume loops, but it should not replace AI reasoning. If runtime evidence is surprising, the AI should pause, inspect evidence, and decide the next step interactively.
5. Good Prompts
Use $android-runtime-debug.
The app package is com.example.app and source is at /path/to/android-project.
On the Search screen, typing a keyword and pressing Enter shows an empty result list.
Find where the query request is built, inspect the runtime query parameters, and export a timeline HTML report.
For evidence-based bug analysis:
Use $android-runtime-debug.
Keep breakpoint evidence.
For every breakpoint hit, read the variables needed to prove why the state is wrong.
At the end, use breakpointOrder and the values under each File.kt:Lxx key to explain the root cause.
6. Rules The AI Should Follow
- Always call
debug_resumeordebug_detachafter a breakpoint hit. - Prefer small explicit variable paths over expanding huge objects.
- Use
autoResolveFrame=truefor Kotlin coroutine, lambda, and inline frames. - Use
timeline_export(includeHtml=true)when a durable report is needed. - Use
debug_breakpoint_resultsbefore final analysis when breakpoint evidence matters. - Do not ask the server to mutate variables or invoke arbitrary app methods.
- Do not use arbitrary
adb shell; useaction_execute.
7. Direct MCP Use Without Codex
Any MCP client can launch:
build/install/android-debug-mcp/bin/android-debug-mcp
The transport is stdio. The client should call initialize, then tools/list, then tools/call.
Typical Debug Flow
List devices:
{"name": "device_list", "arguments": {}}
Capture screen state:
{
"name": "screen_snapshot",
"arguments": {
"includeScreenshot": true,
"includeUiTree": true
}
}
Resolve source:
{
"name": "source_resolve",
"arguments": {
"className": "com.example.app.feature.HomeFragment",
"projectRoot": "/path/to/android-project",
"maxResults": 20
}
}
Attach to the app:
{
"name": "debug_attach",
"arguments": {
"packageName": "com.example.app",
"processName": "com.example.app"
}
}
Set a breakpoint:
{
"name": "debug_set_breakpoint",
"arguments": {
"sessionId": "session-...",
"className": "com.example.app.feature.HomeFragment",
"line": 123
}
}
Trigger the code path:
{
"name": "action_execute",
"arguments": {
"sessionId": "session-...",
"action": {
"type": "tap_text",
"text": "Refresh",
"match": "exact"
}
}
}
Wait for a hit:
{
"name": "debug_wait_event",
"arguments": {
"sessionId": "session-...",
"timeoutMs": 15000,
"includeLocals": false,
"includeScreen": true
}
}
Read variables:
{
"name": "debug_read_variables",
"arguments": {
"sessionId": "session-...",
"frameId": 0,
"paths": ["this", "request", "this.state"],
"autoResolveFrame": true,
"frameSearchLimit": 20,
"maxDepth": 1,
"maxFields": 20,
"maxArrayItems": 20,
"maxStringLength": 1000
}
}
Resume:
{"name": "debug_resume", "arguments": {"sessionId": "session-..."}}
Always resume or detach after a breakpoint hit.
Read breakpoint evidence:
{
"name": "debug_breakpoint_results",
"arguments": {
"sessionId": "session-...",
"maxHits": 200
}
}
Export the timeline:
{
"name": "timeline_export",
"arguments": {
"sessionId": "session-...",
"includeHtml": true
}
}
Detach:
{"name": "debug_detach", "arguments": {"sessionId": "session-..."}}
Device Actions
Common allowlisted actions:
{"type": "tap", "x": 300, "y": 1200}
{"type": "swipe", "x1": 610, "y1": 2100, "x2": 610, "y2": 700, "durationMs": 500}
{"type": "text", "text": "hello"}
{"type": "keyevent", "keyCode": 66}
{"type": "back"}
{"type": "home"}
{"type": "enter"}
{"type": "launch", "packageName": "com.example.app"}
{"type": "pull_to_refresh"}
{"type": "tap_text", "text": "Refresh", "match": "exact"}
{"type": "tap_content_desc", "contentDescription": "Back"}
{"type": "scroll_to_text", "text": "Load more", "direction": "down", "maxSwipes": 8}
{"type": "tap_bounds", "bounds": {"left": 10, "top": 20, "right": 180, "bottom": 88}}
Candidate Breakpoint Groups
For unstable Kotlin line mappings, install multiple candidate lines and let the server stop only when required variables are readable.
{
"name": "debug_set_breakpoint_group",
"arguments": {
"sessionId": "session-...",
"name": "request-state",
"candidates": [
{"className": "com.example.app.feature.HomeFragment", "line": 120},
{"className": "com.example.app.feature.HomeFragment", "line": 128},
{"className": "com.example.app.feature.HomeFragment", "line": 136}
],
"requiredPaths": ["request", "this.state"],
"autoResumeUntilReadable": true,
"frameSearchLimit": 20
}
}
Short Debug Plan Example
{
"name": "debug_run_plan",
"arguments": {
"sessionId": "session-...",
"plan": {
"name": "one-off-refresh-trace",
"setup": [
{
"kind": "line_breakpoint",
"className": "com.example.app.feature.HomeFragment",
"line": 123
}
],
"trigger": [
{
"kind": "android_action",
"action": {
"type": "tap_text",
"text": "Refresh",
"match": "exact"
}
}
],
"onEvent": [
{
"match": {"event": "breakpoint_hit"},
"actions": [
{"kind": "snapshot", "depth": 12},
{
"kind": "read_variables",
"paths": ["request", "this.state"],
"autoResolveFrame": true,
"frameSearchLimit": 20
},
{"kind": "resume"}
]
}
],
"limits": {
"timeoutMs": 60000,
"maxEvents": 20
}
}
}
}
Tool Reference
| Tool | Purpose |
|---|---|
device_list | List adb devices |
screen_snapshot | Capture screenshot, UI tree, screen size, and current activity |
action_execute | Execute an allowlisted Android input action |
source_resolve | Resolve JVM/Kotlin class names to source candidates |
debug_attach | Attach to a debuggable Android process |
debug_detach | Detach and remove adb forwarding |
debug_set_breakpoint | Set a class + line breakpoint |
debug_clear_breakpoint | Delete a breakpoint |
debug_list_breakpoints | List breakpoint state |
debug_wait_event | Wait for a breakpoint hit |
debug_read_variables | Read variables from a suspended frame |
debug_set_breakpoint_group | Set candidate breakpoint lines |
debug_clear_breakpoint_group | Clear a candidate breakpoint group |
debug_list_breakpoint_groups | List candidate breakpoint groups |
debug_resume | Resume the suspended event |
debug_frame_snapshot | Cache the current stack frames |
debug_get_snapshot | Read a cached frame snapshot |
debug_breakpoint_results | Read breakpoint evidence grouped by source line |
debug_clear_breakpoint_results | Clear breakpoint evidence |
debug_run_plan | Run a short server-side debug plan |
debug_plan_events | Read or wait for plan progress events |
debug_get_plan_report | Read the current or final plan report |
debug_abort_plan | Abort a running plan |
debug_pause_plan | Ask a plan to pause at the next handled event |
timeline_get | Read the session timeline |
timeline_export | Export session.json and optional session.html |
timeline_clear | Clear a timeline |
Development Checks
./gradlew -q build
./gradlew -q installDist
List MCP tools:
printf 'Content-Length: 58\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}Content-Length: 58\r\n\r\n{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
| build/install/android-debug-mcp/bin/android-debug-mcp
Troubleshooting
pid not found: the app process is not running, orprocessNameis wrong.no_location: the line is not executable, the source does not match the APK, or Kotlin line mapping shifted the location.no suspended breakpoint event: calldebug_wait_eventfirst, or pause/yield a debug plan.vm_in_plan: a plan is running; read plan events, pause the plan, or abort it before mutating the session.- Variable output is too large: lower render limits or use projection/artifact options.
Design Principles
- The AI decides the next debugging step.
- The server performs reliable low-level runtime operations.
- Runtime conclusions should come from breakpoint, variable, screen, and timeline evidence.
- Debug plans are short-distance accelerators, not pre-written reasoning scripts.
- Default behavior is read-only except for explicit UI input actions and resume/detach.