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:L123 and 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_execute actions.
  • 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:

  1. Find the target process id by package or process name.
  2. Forward a local TCP port to the app JDWP endpoint.
  3. Attach with JDI SocketAttach.
  4. Use the returned VirtualMachine for breakpoints, event queues, stack frames, and variables.

Breakpoint behavior:

  • If a class is already loaded, the server installs a BreakpointRequest immediately.
  • If a class is not loaded, the server installs a ClassPrepareRequest and creates the real breakpoint later.
  • Breakpoints use SUSPEND_EVENT_THREAD, so only the hit thread is suspended.

Requirements

  • JDK with the jdk.jdi module.
  • Android SDK platform-tools.
  • adb available on PATH, or set ANDROID_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_resume or debug_detach after a breakpoint hit.
  • Prefer small explicit variable paths over expanding huge objects.
  • Use autoResolveFrame=true for Kotlin coroutine, lambda, and inline frames.
  • Use timeline_export(includeHtml=true) when a durable report is needed.
  • Use debug_breakpoint_results before 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; use action_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

ToolPurpose
device_listList adb devices
screen_snapshotCapture screenshot, UI tree, screen size, and current activity
action_executeExecute an allowlisted Android input action
source_resolveResolve JVM/Kotlin class names to source candidates
debug_attachAttach to a debuggable Android process
debug_detachDetach and remove adb forwarding
debug_set_breakpointSet a class + line breakpoint
debug_clear_breakpointDelete a breakpoint
debug_list_breakpointsList breakpoint state
debug_wait_eventWait for a breakpoint hit
debug_read_variablesRead variables from a suspended frame
debug_set_breakpoint_groupSet candidate breakpoint lines
debug_clear_breakpoint_groupClear a candidate breakpoint group
debug_list_breakpoint_groupsList candidate breakpoint groups
debug_resumeResume the suspended event
debug_frame_snapshotCache the current stack frames
debug_get_snapshotRead a cached frame snapshot
debug_breakpoint_resultsRead breakpoint evidence grouped by source line
debug_clear_breakpoint_resultsClear breakpoint evidence
debug_run_planRun a short server-side debug plan
debug_plan_eventsRead or wait for plan progress events
debug_get_plan_reportRead the current or final plan report
debug_abort_planAbort a running plan
debug_pause_planAsk a plan to pause at the next handled event
timeline_getRead the session timeline
timeline_exportExport session.json and optional session.html
timeline_clearClear 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, or processName is 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: call debug_wait_event first, 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.