mobile-debug-mcp 0.33.0 → 0.34.0

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.
Files changed (87) hide show
  1. package/.github/workflows/ci.yml +21 -0
  2. package/README.md +7 -0
  3. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  4. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  5. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  6. package/agent-plugin/.mcp.json +1 -1
  7. package/agent-plugin/README.md +4 -0
  8. package/agent-plugin/mcp.json +1 -1
  9. package/agent-plugin/plugin.json +1 -1
  10. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +88 -4
  11. package/dist/debugger/android.js +78 -0
  12. package/dist/debugger/guard.js +30 -0
  13. package/dist/debugger/sessions.js +262 -0
  14. package/dist/debugger/worker.js +165 -0
  15. package/dist/interact/index.js +16 -0
  16. package/dist/observe/evidence.js +9 -2
  17. package/dist/observe/readiness-service.js +9 -0
  18. package/dist/observe/readiness.js +15 -8
  19. package/dist/observe/system-trace-android.js +135 -0
  20. package/dist/observe/system-trace.js +285 -0
  21. package/dist/server/debugger-handlers.js +38 -0
  22. package/dist/server/debugger-schema.js +42 -0
  23. package/dist/server/journey-schema.js +1 -0
  24. package/dist/server/system-trace-schema.js +17 -0
  25. package/dist/server/tool-definitions.js +6 -1
  26. package/dist/server/tool-handlers.js +203 -96
  27. package/dist/server-core.js +3 -3
  28. package/dist/server.js +34 -0
  29. package/dist/system/index.js +5 -4
  30. package/dist/utils/exec.js +2 -0
  31. package/dist/utils/java.js +14 -12
  32. package/dist/utils/operation-budget.js +2 -0
  33. package/dist/utils/resolve-device.js +2 -0
  34. package/docs/CHANGELOG.md +4 -0
  35. package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
  36. package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
  37. package/docs/specs/023-android-system-traces/intent.md +40 -0
  38. package/docs/specs/023-android-system-traces/plan.md +117 -0
  39. package/docs/specs/023-android-system-traces/spec.md +120 -0
  40. package/docs/specs/024-android-heap-dump-capture/intent.md +39 -0
  41. package/docs/specs/025-android-runtime-debugger/intent.md +40 -0
  42. package/docs/specs/025-android-runtime-debugger/plan.md +104 -0
  43. package/docs/specs/025-android-runtime-debugger/spec.md +182 -0
  44. package/docs/tools/TOOLS.md +3 -1
  45. package/docs/tools/debugger.md +104 -0
  46. package/docs/tools/evidence.md +5 -2
  47. package/docs/tools/observe.md +12 -5
  48. package/docs/tools/system-traces.md +126 -0
  49. package/package.json +1 -1
  50. package/src/debugger/android.ts +65 -0
  51. package/src/debugger/guard.ts +26 -0
  52. package/src/debugger/sessions.ts +168 -0
  53. package/src/debugger/worker.ts +122 -0
  54. package/src/interact/index.ts +16 -15
  55. package/src/observe/evidence.ts +9 -2
  56. package/src/observe/readiness-service.ts +5 -0
  57. package/src/observe/readiness.ts +12 -6
  58. package/src/observe/system-trace-android.ts +133 -0
  59. package/src/observe/system-trace.ts +218 -0
  60. package/src/server/debugger-handlers.ts +29 -0
  61. package/src/server/debugger-schema.ts +37 -0
  62. package/src/server/journey-schema.ts +1 -0
  63. package/src/server/system-trace-schema.ts +17 -0
  64. package/src/server/tool-definitions.ts +6 -1
  65. package/src/server/tool-handlers.ts +92 -7
  66. package/src/server-core.ts +3 -3
  67. package/src/server.ts +24 -0
  68. package/src/system/index.ts +7 -5
  69. package/src/utils/exec.ts +2 -0
  70. package/src/utils/java.ts +13 -12
  71. package/src/utils/operation-budget.ts +2 -0
  72. package/src/utils/resolve-device.ts +2 -0
  73. package/test/device/automated/debugger/runtime.android.test.ts +203 -0
  74. package/test/device/automated/observe/system-trace.smoke.ts +104 -0
  75. package/test/fixtures/debugger-app/MainActivity.java +46 -0
  76. package/test/fixtures/debugger-app/NoLocals.java +7 -0
  77. package/test/fixtures/debugger-app/README.md +13 -0
  78. package/test/fixtures/debugger-app/build.ts +29 -0
  79. package/test/helpers/perfetto.ts +59 -0
  80. package/test/unit/debugger/guards.test.ts +46 -0
  81. package/test/unit/debugger/native.test.ts +33 -0
  82. package/test/unit/debugger/sessions.test.ts +83 -0
  83. package/test/unit/observe/readiness.test.ts +3 -1
  84. package/test/unit/observe/system-trace-android.test.ts +52 -0
  85. package/test/unit/observe/system-trace.test.ts +166 -0
  86. package/test/unit/server/system-trace.test.ts +64 -0
  87. package/worker/DebuggerWorker.java +160 -0
@@ -0,0 +1,182 @@
1
+ # Specification: Android runtime debugger
2
+
3
+ Date: 2026-10-30. Status: approved.
4
+ Intent: [approved intent](intent.md), approved by the user's “yes” in this conversation.
5
+ Source: [issue #68](https://github.com/clivejefferies/mobile-debug-tools/issues/68).
6
+
7
+ ## Behaviour and supported environment
8
+
9
+ Provide managed Java/Kotlin bytecode debugging of running, debuggable Android
10
+ apps using an independently developed Java JDI worker over ADB-forwarded JDWP.
11
+ No Debroid implementation or artifacts are reused. Require host JDK 17+ with
12
+ `jdk.jdi` and `javac`, Android platform-tools, an authorized Android device, and
13
+ a JDWP-visible app process. Package worker sources with the npm distribution;
14
+ compile into a private, version-keyed host cache on first use. Missing tooling
15
+ must not prevent ordinary MCP tools or server startup.
16
+
17
+ Initial verification target: API 36 arm64 emulator; other Android versions and
18
+ physical devices require separate evidence. iOS, native frames, expression
19
+ evaluation, method invocation, mutation, coroutine reconstruction, heap capture,
20
+ launch-under-debugger, and changing app debuggability are excluded. Kotlin
21
+ inspection follows emitted JVM/DEX debug metadata; do not promise source-level
22
+ coroutine or inline-function reconstruction.
23
+
24
+ ## MCP contracts
25
+
26
+ All new inputs reject unknown fields. Strings are nonempty and bounded to 512
27
+ characters unless otherwise stated; IDs are opaque server-issued strings.
28
+ Every tool accepts `timeoutMs` (integer 100–30000, default 10000); event waits
29
+ default to 1000. Tables list other inputs; `?` means optional.
30
+
31
+ | Tool | Inputs | Result payload |
32
+ | --- | --- | --- |
33
+ | `get_debugger_capabilities` | `platform: android`, `deviceId`, `appId`, `processId?` | Prerequisites, candidate processes, supported operations and limitations; no attach |
34
+ | `attach_debugger` | `platform: android`, `deviceId`, `appId`, `processId?` | Session and resolved target |
35
+ | `get_debugger_state` | `debugSessionId` | Current state, stop summary, breakpoint statuses, cleanup |
36
+ | `set_debugger_breakpoint` | `debugSessionId`, `className`, `line` | Breakpoint ID, requested/resolved locations, `pending`/`verified`/`rejected` |
37
+ | `remove_debugger_breakpoint` | `debugSessionId`, `breakpointId` | Removal outcome; repeated removal of an owned ID succeeds |
38
+ | `wait_for_debugger_event` | `debugSessionId`, `afterSequence?` | Ordered events, next cursor and explicit overflow indication |
39
+ | `get_debugger_stack` | `debugSessionId`, `stopId`, `threadId`, `offset?`, `limit?` | Frames, opaque frame IDs and pagination |
40
+ | `get_debugger_locals` | `debugSessionId`, `stopId`, `frameId` | Bounded shallow local values and availability |
41
+ | `step_debugger` | `debugSessionId`, `stopId`, `threadId`, `kind: into/over/out` | Acknowledged transition to running; await a later event separately |
42
+ | `resume_debugger` | `debugSessionId`, `stopId` | Acknowledged resume |
43
+ | `detach_debugger` | `debugSessionId` | Terminal state and cleanup; idempotent and usable for cleanup retry |
44
+
45
+ `processId` and `line` are positive safe integers. Omitted process ID is allowed
46
+ only when exactly one matching app process is JDWP-visible. Resolve package UID,
47
+ process name, PID and process-start identity before and after attach; reject
48
+ ambiguity, mismatch or PID reuse. Never select the first connected device.
49
+
50
+ Responses use existing MCP text JSON serialization:
51
+ `{success, debug_session_id, device, target, state, captured_at_ms, capabilities,
52
+ limitations, cleanup, ...payload}`; pre-session fields may be null. Target includes
53
+ app ID, PID, process name and process-start identity. Failures add
54
+ `error: {code, message, retryable}`. Codes cover `INVALID_ARGUMENT`,
55
+ `DEBUGGER_UNSUPPORTED`, `DEVICE_UNAVAILABLE`, `PROCESS_NOT_FOUND`,
56
+ `PROCESS_AMBIGUOUS`, `PROCESS_CHANGED`, `DEBUGGER_BUSY`, `DEBUGGER_ATTACH_FAILED`,
57
+ `DEBUGGER_NOT_FOUND`, `DEBUGGER_NOT_PAUSED`, `DEBUGGER_STALE_REFERENCE`,
58
+ `DEBUGGER_LOCATION_UNAVAILABLE`, `DEBUGGER_DISCONNECTED`, `DEBUGGER_TIMEOUT`,
59
+ `DEBUGGER_CANCELLED`, `DEBUGGER_WORKER_FAILED`, and `DEBUGGER_CLEANUP_FAILED`.
60
+ No automatic retry of attach, step or resume. Do not claim command delivery
61
+ establishes a later stop or UI outcome.
62
+
63
+ ## Stops, inspection and events
64
+
65
+ States: `attaching`, `running`, `paused`, `detaching`, `detached`, `disconnected`,
66
+ `failed`. Serialize state-changing commands per session; event polling must not
67
+ block resume/detach. One active session per device, at most four per server.
68
+ Reject an existing debugger attachment without replacing it.
69
+
70
+ Breakpoints use exact binary class names and 1-based lines, without arbitrary
71
+ source paths. Bind all matching executable locations in loaded class loaders;
72
+ report class-loader identity and resolved source/line. Unloaded classes remain
73
+ pending via class-prepare requests. Missing executable lines/debug metadata are
74
+ explicitly unavailable; never silently move the breakpoint. Limit 100 breakpoints.
75
+
76
+ Breakpoint and step events suspend all app threads. Retain owned EventSets and
77
+ resume through their matching ownership; do not decrement unrelated suspension
78
+ counts. Non-stop events are promptly released. Each new stop has a unique
79
+ `stopId`, triggering thread ID, reason, locations and matching breakpoint IDs.
80
+ Simultaneous events share a stop summary. Step uses one-shot line stepping on
81
+ the chosen suspended thread; a competing breakpoint also completes that step
82
+ attempt and removes its request. A step acknowledgement does not wait for a stop.
83
+
84
+ Thread/frame IDs are stop-scoped. Resume, step, disconnect and detach invalidate
85
+ all inspection references before further commands can use them. Stack pagination
86
+ defaults to 20, maximum 100 per call and 1000 frames per stop. Locals return at
87
+ most 100 entries: primitives, null, strings truncated to 256 characters, and
88
+ object type/opaque identity summaries, without field traversal or `toString()`.
89
+ Expose truncation and missing debug metadata; unavailable locals are distinct
90
+ from a proven empty scope. Investigation found ART may return an empty variable
91
+ table without distinguishing absent metadata from no variables; report availability
92
+ as unknown with `EMPTY_OR_UNAVAILABLE_VARIABLE_METADATA`, rather than inferring an
93
+ empty scope. No retained object pinning.
94
+
95
+ Maintain a 256-event ring per session with increasing sequence, host receipt time,
96
+ kind and optional stop summary. Default cursor is zero; return at most 100 events
97
+ and a continuation cursor. Cursor overrun is explicit. Reject future cursors;
98
+ an empty wait returns success with `timed_out: true`, without changing the session.
99
+
100
+ ## UI coordination and resource safety
101
+
102
+ While a session is paused, conservatively block action dispatch, lifecycle/data
103
+ management, and UI waits/assertions on that device with `DEBUGGER_PAUSED` before
104
+ native work. Journeys stop with completed steps preserved and no automatic retry.
105
+ Check at dispatch and within polling; a pause racing an already dispatched action
106
+ must preserve delivery evidence and mark verification blocked. Do not queue inputs
107
+ for resume. Use a device dispatch gate to serialize pause transitions with input
108
+ submission; inputs already submitted cannot be retracted.
109
+
110
+ Readiness tools return evidence-backed `debugger_paused` with debug session/target
111
+ identity and host timestamp without acquiring a UI tree. This is device-scoped
112
+ blocking evidence, not proof the paused app is foreground. Screenshots, raw trees
113
+ and logs remain available within their existing bounds and carry the known pause
114
+ annotation; their success does not establish readiness. Other devices and iOS
115
+ retain existing behaviour. Unknown/disconnected debugger state cannot imply pause.
116
+
117
+ Detach automatically after 60 seconds continuously paused, five minutes without
118
+ client activity, or 30 minutes total session lifetime; polling cannot extend the
119
+ pause/total limit. Emit the expiry reason. Timeouts/cancellation of inspection
120
+ leave a known session intact; uncertain state-changing commands trigger teardown
121
+ and invalidate references. Cancelling an event wait only cancels that wait.
122
+
123
+ Cleanup has its own ten-second deadline: remove owned requests, release owned
124
+ suspensions/dispose the JDI connection, stop the owned worker, and remove only
125
+ the session's exact ADB forward. Never kill/restart the app, ADB server or another
126
+ debugger. On worker failure, disconnect its transport and verify obtainable
127
+ cleanup facts; never claim the app resumed without evidence. Report cleanup as
128
+ `pending`/`complete`/`failed`, remaining resources and execution state as unknown
129
+ when necessary. Block new sessions on that device until owned cleanup completes.
130
+ The worker watches parent-pipe EOF; server shutdown also requests cleanup. Abrupt
131
+ host/device loss may prevent confirmed cleanup and must be documented explicitly.
132
+
133
+ Use argv-based subprocesses and a dedicated worker with bounded JSON-lines IPC
134
+ (1 MiB/message), request IDs and stderr-only diagnostics. JDI runs through an
135
+ owned loopback forward, never a public listener. Whitelist commands; do not log
136
+ locals or protocol values. Locals may contain sensitive data and are returned
137
+ only on request; no persistent inspection dump or upload. Retain up to 100 terminal
138
+ session summaries for ten minutes, clearing locals, frames and events at teardown.
139
+
140
+ ## Acceptance and verification
141
+
142
+ 1. A dedicated debuggable Java fixture attaches on the selected device/process;
143
+ ambiguous, non-debuggable, replaced and already-debugged targets fail safely.
144
+ 2. Pending/loaded breakpoints stop at the expected line; stack and known local
145
+ values are correct; each step mode and resume produce observed execution.
146
+ 3. Old stop/frame/thread references fail after resume; missing metadata and
147
+ truncation are explicit.
148
+ 4. UI action/wait/readiness tools recognise pauses, including mid-journey stops;
149
+ another device is unaffected and resumed UI changes are independently verified.
150
+ 5. Explicit detach, expiry, cancellation, disconnect and worker failure invalidate
151
+ references and clean up owned resources without altering unrelated forwards.
152
+
153
+ Use focused self-running unit tests plus a new isolated fixture with debug line
154
+ and local-variable tables. Run `npm run verify`, `npm run test:device`, and focused
155
+ end-to-end calls through the updated server. Record exact build/device and each
156
+ journey as pass/fail/blocked/unrun. Current CI uses Node 18; manual Android CI uses
157
+ JDK 17/API 30. Existing smoke tests do not prove debugger behaviour; add worker
158
+ compilation/packaging checks and explicit debugger coverage.
159
+
160
+ Affected areas: new debugger/worker modules, tool definitions/handlers, readiness,
161
+ interaction dispatch/polling, lifecycle guards, shutdown, native-tool status,
162
+ packaging, fixtures/tests and tool/README/changelog documentation. Correct existing
163
+ JDWP capability reporting: ADB/device presence alone is not debugger readiness.
164
+
165
+ ## Investigation and approval
166
+
167
+ Checkout 0.33.0 contains no debugger implementation. Host Java 23 and ADB 37.0.1
168
+ are available; outside sandbox, `emulator-5554` is online (API 36 arm64). Existing
169
+ latency fixture lacks `android:debuggable`; a dedicated fixture is required.
170
+ No debugger session was attached and no debugging journey is verified yet.
171
+
172
+ Primary references: [JDI VM lifecycle](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.jdi/com/sun/jdi/VirtualMachine.html),
173
+ [EventSet suspension ownership](https://docs.oracle.com/en/java/javase/21/docs/api/jdk.jdi/com/sun/jdi/event/EventSet.html),
174
+ [frame lifetime](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.jdi/com/sun/jdi/StackFrame.html),
175
+ [step requests](https://docs.oracle.com/en/java/javase/17/docs/api/jdk.jdi/com/sun/jdi/request/StepRequest.html),
176
+ and [ADB](https://developer.android.com/tools/adb). ART/JDI interoperability must
177
+ be established by fixture evidence; API documentation alone does not prove it.
178
+
179
+ The user approved this complete specification with “yes” on 2026-10-30 in this
180
+ conversation, including all-thread
181
+ suspension, conservative device-wide UI blocking, shallow locals, JDK dependency,
182
+ and automatic detach limits. Implementation is authorized within this scope.
@@ -8,7 +8,9 @@ This repository groups the MCP tools into four areas aligned with the codebase:
8
8
  - [system](system.md) — environment and health checks
9
9
 
10
10
  Correlation metadata and explicit action references are documented in
11
- [correlated evidence](evidence.md).
11
+ [correlated evidence](evidence.md). Android performance capture is documented in
12
+ [system traces](system-traces.md). Android bytecode debugging is documented in
13
+ [runtime debugger](debugger.md).
12
14
 
13
15
  ## Agent guidance
14
16
 
@@ -0,0 +1,104 @@
1
+ # Android runtime debugger
2
+
3
+ Optional managed Java/Kotlin bytecode debugging through an independently developed
4
+ JDI worker. Requires JDK 17+ (`java`, `javac`, `jdk.jdi`), Android platform-tools,
5
+ an authorized device and an already-running JDWP-visible debuggable app.
6
+ Android Studio JBR/standard JDK discovery follows existing Java helpers; `ADB_PATH`
7
+ is supported. The worker is compiled on demand into a private source/JDK-keyed
8
+ host temporary cache. Ordinary tools do not require Java or start a debugger.
9
+ Cache directories named `mobile-debug-jdi-*` can be removed when the server exits.
10
+
11
+ ## Tools and flow
12
+
13
+ 1. `get_debugger_capabilities` with `platform: "android"`, explicit `deviceId`,
14
+ `appId`, and optional positive `processId` probes prerequisites/candidate processes.
15
+ General `get_system_status` does not establish target-specific debugger readiness.
16
+ 2. `attach_debugger` with the same target returns `debug_session_id`. If multiple
17
+ matching debuggable processes exist, provide `processId`. Package UID, process
18
+ name, PID and process-start identity are checked. Other debuggers are not replaced.
19
+ 3. `set_debugger_breakpoint` takes `debugSessionId`, exact binary `className` and
20
+ 1-based executable `line`. It returns `result.breakpoint_id`, status and resolved
21
+ source locations/class-loader IDs. Unloaded classes remain `pending`; missing
22
+ line metadata is rejected explicitly. No automatic source search/line relocation.
23
+ 4. Trigger execution, then `wait_for_debugger_event` with `debugSessionId` and
24
+ `afterSequence` (initially zero). Retain `next_cursor`. Stop events contain
25
+ `stop_id`, `thread_id` and source locations. A step acknowledgement is not a stop.
26
+ 5. `get_debugger_stack` takes `debugSessionId`, `stopId`, `threadId`, optional
27
+ `offset` and `limit` (20 default, 100 maximum; 1000-frame cap).
28
+ `get_debugger_locals` takes `debugSessionId`, `stopId`, `frameId`. It returns
29
+ `result.availability`, up to 100 shallow locals, and truncation information.
30
+ Primitive values are textual; strings truncate at 256 characters; objects have
31
+ type/identity summaries. Missing variable metadata is explicit. ART can return an empty table without
32
+ distinguishing absent metadata from a genuinely empty scope; these responses
33
+ use `availability: "unknown"`, `reason: "EMPTY_OR_UNAVAILABLE_VARIABLE_METADATA"`.
34
+ 6. `step_debugger` takes `debugSessionId`, current `stopId`, `threadId` and
35
+ `kind: "into" | "over" | "out"`. It resumes and requests one line step.
36
+ `resume_debugger` takes `debugSessionId`, `stopId`. Both expire all old stop,
37
+ thread and frame references. Await the next stop using the event cursor.
38
+ 7. `remove_debugger_breakpoint` takes `debugSessionId`, `breakpointId` and is
39
+ idempotent for an owned ID. `get_debugger_state` reads stop, breakpoint, terminal
40
+ reason and cleanup status. `detach_debugger` disposes the session and supports
41
+ repeated calls/retrying incomplete cleanup.
42
+
43
+ Every tool accepts integer `timeoutMs`, 100–30000 (default 10000; event waits
44
+ 1000). Unknown arguments are rejected. Other strings are limited to 512 characters.
45
+ All session tools use camelCase input IDs; response fields use snake_case.
46
+ Successful operation payloads are under `result` where applicable; common fields
47
+ include `success`, `debug_session_id`, `device`, `target`, `state`,
48
+ `captured_at_ms`, `capabilities`, `limitations`, `cleanup`, `reason`, and `stop`.
49
+ Failures carry `error: {code, message, retryable}`. Event polls use top-level
50
+ `events`, `next_cursor`, `overflow`, `timed_out`; empty waits are successful.
51
+ The 256-event ring returns at most 100 entries; overflow must not be ignored.
52
+
53
+ ## Suspension, limits and cleanup
54
+
55
+ Stops suspend all app threads. UI actions, app lifecycle/data changes, waits and
56
+ assertions on that device return `DEBUGGER_PAUSED`; inputs are not queued. A pause
57
+ racing input submission preserves delivery uncertainty and blocks verification.
58
+ Journeys preserve completed steps. Other devices are unaffected.
59
+
60
+ `get_app_readiness` returns evidence-backed `debugger_paused` without reading the
61
+ UI; readiness waits fail with `DEBUGGER_PAUSED`. This signal covers managed Android
62
+ sessions only and does not establish foreground app identity. Raw trees, screenshots
63
+ and logs remain available within their own timeouts; snapshot/log acquisition may
64
+ still fail while the app is suspended. Known pauses are annotated on observations.
65
+
66
+ The worker and host enforce automatic detach after 60 seconds continuously paused,
67
+ five minutes without client commands or 30 minutes total. Polling cannot extend
68
+ the pause or total limit. One active session per device, four per server; at most
69
+ 100 distinct breakpoint IDs per session. Terminal summaries expire after ten
70
+ minutes and are capped at 100; inspection values and event history are cleared.
71
+
72
+ Detach releases debugger-owned suspension and removes only owned requests,
73
+ worker and the exact ADB forward. Cleanup is bounded separately to ten seconds;
74
+ retry `detach_debugger` if `cleanup.status` is `failed`. Never kill an app or ADB
75
+ server to clean up. Unexpected worker/transport loss reports execution state as
76
+ unknown; no resumption claim is inferred. Hard host/device loss can prevent
77
+ confirmed cleanup. Server shutdown and worker parent-pipe EOF request cleanup.
78
+
79
+ MCP request cancellation is supported. Cancelling an event wait leaves the
80
+ session intact. Inspection cancellation/timeouts preserve a known session;
81
+ uncertain mutation/attach outcomes trigger teardown. Do not automatically retry
82
+ step, resume or attach.
83
+
84
+ ## Limits, privacy and evidence
85
+
86
+ There is no expression evaluation, object traversal, method invocation, mutation,
87
+ coroutine reconstruction, native debugger, iOS debugger or heap capture. Kotlin
88
+ results follow emitted bytecode/debug metadata; optimized/obfuscated apps may lack
89
+ useful source lines or locals. Locals may contain private data: values are returned
90
+ only on request, without protocol/value logging, persistent dumps or upload.
91
+ Worker heap is capped at 128 MiB; IPC and messages are bounded; forwarding remains loopback-only.
92
+
93
+ Verified environments and remaining checks are recorded in
94
+ [the implementation plan](../specs/025-android-runtime-debugger/plan.md).
95
+ To run the isolated fixture journey (installs then removes its own test package):
96
+
97
+ ```sh
98
+ MCP_DEBUGGER_DEVICE=emulator-5554 npx tsx test/device/automated/debugger/runtime.android.test.ts
99
+ # Also exercise the real 60-second pause expiry:
100
+ MCP_DEBUGGER_DEVICE=emulator-5554 MCP_DEBUGGER_EXTENDED=1 npx tsx test/device/automated/debugger/runtime.android.test.ts
101
+ ```
102
+
103
+ The ordinary device suite explicitly reports this journey unrun unless the device
104
+ is selected. Fixture compilation requires SDK platform/build-tools 36 and `javac -g`.
@@ -73,7 +73,10 @@ after it, or causality. No implicit action-window filtering is applied.
73
73
 
74
74
  Existing payloads, image blocks, and log blocks remain available. Snapshot evidence
75
75
  sits alongside `raw` and `semantic`. Android ADB and supported iOS simulators share
76
- this contract; existing provider limitations still apply. `trace_context` reports
77
- `{ "supported": false, "trace_id": null, "markers": [] }` for future trace linkage.
76
+ this contract; existing provider limitations still apply. `trace_context` defaults
77
+ to `{ "supported": false, "trace_id": null, "markers": [] }`. Journeys explicitly
78
+ linked to an active Android [system trace](system-traces.md) and their steps expose
79
+ `{ "supported": true, "trace_id": "…", "markers": [] }`; association does not align
80
+ clocks or inject device markers.
78
81
  Correlation history stores identifiers, scope, timing, and ordering only, without
79
82
  captured content, arguments, persistence, or telemetry.
@@ -13,6 +13,9 @@ They are **not** the primary success signal when an applicable `expect_*` tool e
13
13
  Covered observations support optional `actionRef` and return acquisition/freshness
14
14
  metadata; see [correlated evidence](evidence.md).
15
15
 
16
+ Android performance investigations can use [system traces](system-traces.md)
17
+ to bracket a journey and retrieve a local Perfetto artifact.
18
+
16
19
  ## get_logs
17
20
 
18
21
  Fetch recent logs as structured entries optimized for AI agents.
@@ -240,7 +243,8 @@ Both return `success`, resolved `device`, `readiness`, `elapsed_ms`, and a
240
243
  or settled; wait succeeds only for settled. Codes are `READINESS_UNAVAILABLE`,
241
244
  `READINESS_TIMEOUT`, and `DEVICE_UNAVAILABLE`. Invalid/unknown inputs are rejected.
242
245
 
243
- `readiness.state` is loading, transitioning, settled, or unknown. Visible native
246
+ `readiness.state` is loading, transitioning, settled, unknown, or debugger_paused
247
+ for an owned Android debugger stop. Visible native
244
248
  progress indicators establish loading; exact Loading / Please wait labels
245
249
  (ignoring case, whitespace and trailing dots/ellipsis) are heuristic loading.
246
250
  Loading takes priority over unchanged trees. Two different valid signatures show
@@ -252,12 +256,15 @@ keywords such as a button named Progress are not sufficient loading evidence.
252
256
  The summary includes `basis` (observed/heuristic/insufficient), `scope: device_ui`,
253
257
  latest successful capture time and snapshot session/revision, sample
254
258
  count, `stable_for_ms`, up to eight signal-code evidence records, limitations,
255
- and capability flags. Evidence excludes UI text and full trees. Unresponsive and
256
- debugger-paused detection are explicitly unsupported on both platforms; neither
257
- slow hierarchy retrieval nor a deadline expiry proves either state.
259
+ and capability flags. Evidence excludes UI text and full trees. Managed Android
260
+ [debugger pauses](debugger.md) are reported from session evidence without acquiring
261
+ a UI tree; readiness waits fail with `DEBUGGER_PAUSED`. External-debugger and iOS
262
+ pause detection remain unsupported. Unresponsive detection is unsupported; neither
263
+ slow hierarchy retrieval nor a deadline expiry proves a pause or unresponsiveness.
258
264
 
259
265
  Tree responses (including compact and nested trees) add a single-sample readiness
260
- summary without extra reads. Such a summary reports loading or unknown; it does
266
+ summary without extra reads. It reports an owned debugger pause when known;
267
+ otherwise a single-sample summary reports loading or unknown; it does
261
268
  not inherit stability from an earlier request. Existing loading/delta fields and
262
269
  compact error evidence are preserved.
263
270
 
@@ -0,0 +1,126 @@
1
+ # Android system traces
2
+
3
+ Use system traces to collect scheduling and system annotations around a slow
4
+ interaction. Capture is optional, device-wide and Android-only; it does not
5
+ automatically diagnose performance problems. No root or Android CLI installation
6
+ is required. The device must provide Perfetto (Android API 29+), authorised ADB,
7
+ tracing services, required sched/input categories, readable process identity,
8
+ base64 decoding and writable `/data/misc/perfetto-traces` storage.
9
+
10
+ ## Tools
11
+
12
+ All inputs reject unknown fields. `platform` is `android` or `ios`; iOS is
13
+ unsupported and never starts capture. Optional `deviceId` uses existing device
14
+ resolution; supplying one is recommended when multiple devices are connected.
15
+
16
+ | Tool | Arguments | Result |
17
+ | --- | --- | --- |
18
+ | `get_system_trace_capabilities` | `platform`, `deviceId?` | `supported`, `backend`, resolved `device`, prerequisite booleans, reasons, enabled sources, unavailable categories, limitations. No recording. |
19
+ | `start_system_trace` | `platform`, `deviceId?`, `durationMs?` | Owned recording session after data-source acknowledgement. Duration is an integer 1,000–120,000 ms, default 30,000. |
20
+ | `get_system_trace_status` | `traceId` | Reconcile process/deadline and finalise ended recording. |
21
+ | `stop_system_trace` | `traceId` | Gracefully stop and retrieve local artifact; retry transfer/cleanup after failure. |
22
+ | `cancel_system_trace` | `traceId` | Stop and discard partial files. Already retrieved artifacts remain local. |
23
+
24
+ Trace control IDs are UUIDs and cannot redirect to another device. One capture
25
+ per device and four pending sessions per server are allowed. Pending failed
26
+ cleanup reserves its slot until resolved. Terminal records with complete cleanup
27
+ expire after 10 minutes or eviction beyond 100 records. Server restart invalidates
28
+ IDs. Repeated terminal operations return the stored outcome.
29
+
30
+ ## Agent workflow and interpretation
31
+
32
+ Trace a reproducible slow or stalled interaction when existing UI timing/state
33
+ and logs leave a performance question unresolved. Check tool discovery first:
34
+ older installed servers may not expose these tools. Capability failure is a
35
+ reported limitation; it is not evidence that the app is frozen.
36
+
37
+ Establish the initial UI state and include an outcome assertion in the journey.
38
+ The launch-only example below demonstrates bracketing; it does not prove an app
39
+ outcome. Use a fresh screenshot or semantic assertion appropriate to the actual
40
+ screen. Settings, for example, may resume a subpage rather than show its main
41
+ title. Allow enough capture time for complete observations as well as inputs.
42
+
43
+ Always finalise an owned recording, including when a journey fails. Stop to keep
44
+ diagnostic evidence; cancel to discard it. Check both `state` and `cleanup`, and
45
+ use status/retry for an unresolved session before starting another capture.
46
+ Do not rely on automatic duration expiry as routine cleanup.
47
+
48
+ A useful report identifies the device, journey and assertion outcomes, trace ID,
49
+ linked correlation IDs, coverage, artifact path and cleanup outcome. `covered`
50
+ describes the capture session lifetime, not proof that every event survived the
51
+ ring buffer. Capture success and valid trace contents are separate from a
52
+ performance diagnosis. These tools do not return CPU summaries, frame metrics,
53
+ blocked-thread analysis or a root cause. Inspect the artifact with an available
54
+ analyser before making those claims; otherwise report analysis **unrun** and
55
+ provide the local artifact for follow-up. Host/device clocks remain unaligned.
56
+
57
+ ## Bracket a journey
58
+
59
+ 1. `get_system_trace_capabilities({"platform":"android","deviceId":"emulator-5554"})`
60
+ 2. `start_system_trace({"platform":"android","deviceId":"emulator-5554","durationMs":30000})`
61
+ 3. Use the returned `trace_id` as `systemTraceId`:
62
+
63
+ ```json
64
+ {
65
+ "platform": "android",
66
+ "deviceId": "emulator-5554",
67
+ "systemTraceId": "<trace_id>",
68
+ "steps": [
69
+ { "id": "launch", "type": "start_app", "appId": "com.android.settings", "verificationMode": "none" }
70
+ ]
71
+ }
72
+ ```
73
+
74
+ 4. `stop_system_trace({"traceId":"<trace_id>"})` returns `artifact.path` for local
75
+ inspection in Perfetto. Delete the file/directory when finished.
76
+
77
+ An invalid, inactive, foreign-server or different-device reference prevents all
78
+ journey steps. A failed journey leaves the capture available to stop or cancel;
79
+ journeys do not control its lifetime. Without `systemTraceId`, journey behaviour
80
+ and evidence remain unchanged. Linked journey/step evidence has
81
+ `trace_context: {"supported":true,"trace_id":"…","markers":[]}`. The journey
82
+ returns `system_trace` with `trace_id`, `coverage` (`covered` or `partial` if
83
+ capture ended during execution), and `clock_alignment:"unknown"`. Trace results
84
+ list linked `correlation_ids`. These are associations, not injected markers or
85
+ proof of causality; host intervals are not aligned with device trace clocks.
86
+
87
+ ## Session response and failures
88
+
89
+ JSON responses contain `trace_id`, process-scoped `session_id`, `device`,
90
+ `backend:"perfetto"`, `state` (starting/recording/stopping/completed/cancelled/failed),
91
+ nullable `reason` and `completion_reason`, host UTC capture bounds, nullable
92
+ `artifact`, `cleanup`, `correlation_ids`, `enabled_sources`, and
93
+ `unavailable_categories`. Capture bounds include command/acknowledgement latency.
94
+
95
+ `artifact` contains absolute `path`, `format:"perfetto"` and `size_bytes`.
96
+ `cleanup` contains `status` (pending/complete/failed) and `remaining_resources`.
97
+ Errors preserve known session metadata and set MCP `isError`. Argument/unknown-ID
98
+ errors return `{error:{tool,code,message}}` without a session. Codes include
99
+ `INVALID_ARGUMENT`, `UNSUPPORTED_PLATFORM`, `TRACE_UNSUPPORTED`, `TRACE_BUSY`,
100
+ `TRACE_NOT_FOUND`, `TRACE_START_FAILED`, `TRACE_STOP_FAILED`,
101
+ `TRACE_ARTIFACT_FAILED`, `ACTION_TIMEOUT`, and `TRACE_CANCELLED`.
102
+ Capability failures return ordinary `supported:false` responses with reasons.
103
+ Read state/cleanup independently: a successful artifact transfer can still have
104
+ failed device-file cleanup. Retry stop/status, or cancel to discard failed output.
105
+
106
+ ## Limits and privacy
107
+
108
+ The fixed preset requests sched, freq, idle, am, wm, gfx, view and input categories,
109
+ a 16 MiB ring buffer and a 32 MiB maximum output. Missing optional categories are
110
+ reported; missing sched/input rejects start. No arbitrary config, wildcard app
111
+ annotations, heap, screenshot or logcat sources are enabled. Device/system
112
+ annotations may still contain sensitive information; this is not app-private data.
113
+
114
+ Capture has a device-side duration limit and a server finalisation timer. Start
115
+ acknowledgement has a 35-second budget, graceful stop 10 seconds, and transfer
116
+ 30 seconds. Only the recorded PID/start-time/output-path owner can be signalled;
117
+ no kill-by-name cleanup. A device disconnect or uncertain stop reports failure,
118
+ never a successful artifact. Partial transfers are discarded; bounded device
119
+ output remains retryable. Successful transfers remove owned device files.
120
+
121
+ Host artifacts use unique restricted temporary directories and remain for
122
+ user-managed deletion. Nothing is uploaded or returned as base64. Normal server
123
+ shutdown attempts owned cleanup for up to 10 seconds. A server crash can leave
124
+ a uniquely named bounded device file for manual removal; restart performs no
125
+ broad cleanup sweep. Device suspend can extend Perfetto's own monotonic duration
126
+ limit; the server timer bounds wall-clock lifetime while the server is running.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "MCP server for mobile app debugging (Android + iOS), with focus on security and reliability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,65 @@
1
+ import { resolveAdbCmd } from '../utils/android/utils.js'
2
+ import { execFile } from 'node:child_process'
3
+ import { promisify } from 'node:util'
4
+ import { DebuggerError, debuggerRemaining, debuggerSignal, nativeError } from './worker.js'
5
+ const exec = promisify(execFile)
6
+ export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number }
7
+ export interface DebugNative {
8
+ candidates(device: string, app: string): Promise<DebugTarget[]>
9
+ identity(device: string, target: DebugTarget): Promise<DebugTarget>
10
+ forward(device: string, pid: number): Promise<number>
11
+ removeForward(device: string, port: number, pid: number): Promise<void>
12
+ }
13
+ export class AndroidDebugNative implements DebugNative {
14
+ async adb(device: string, args: string[]) {
15
+ try { return (await exec(resolveAdbCmd(), ['-s', device, ...args], { timeout: debuggerRemaining(5000), signal: debuggerSignal(), maxBuffer: 1024 * 1024 })).stdout.trim() }
16
+ catch (e) { throw nativeError(e, 'DEVICE_UNAVAILABLE') }
17
+ }
18
+ async identity(device: string, target: DebugTarget): Promise<DebugTarget> {
19
+ try {
20
+ const stat = await this.adb(device, ['shell', 'cat', `/proc/${target.pid}/stat`])
21
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(/\s+/)
22
+ const status = await this.adb(device, ['shell', 'cat', `/proc/${target.pid}/status`])
23
+ const uid = Number(/^Uid:\s+(\d+)/m.exec(status)?.[1])
24
+ const name = (await this.adb(device, ['shell', 'cat', `/proc/${target.pid}/cmdline`])).split('\0')[0]
25
+ if (!/^\d+$/.test(fields[19] ?? '') || uid !== target.uid || name !== target.process_name || (target.start_identity && fields[19] !== target.start_identity)) throw new Error('identity')
26
+ return { ...target, start_identity: fields[19] }
27
+ } catch (e) { if (e instanceof DebuggerError && ['DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED', 'DEVICE_UNAVAILABLE'].includes(e.code)) throw e; throw new DebuggerError('PROCESS_CHANGED') }
28
+ }
29
+ async candidates(device: string, app: string) {
30
+ if (!/^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/.test(app)) throw new DebuggerError('INVALID_ARGUMENT')
31
+ const packages = await this.adb(device, ['shell', 'cmd', 'package', 'list', 'packages', '-U', app])
32
+ const uid = Number(new RegExp(`^package:${app.replaceAll('.', '\\.')} uid:(\\d+)$`, 'm').exec(packages)?.[1])
33
+ if (!Number.isSafeInteger(uid)) throw new DebuggerError('PROCESS_NOT_FOUND')
34
+ // `adb jdwp` is a subscription, not a one-shot command. Bound the sample.
35
+ let jdwpOutput: string
36
+ try { jdwpOutput = (await exec(resolveAdbCmd(), ['-s', device, 'jdwp'], { timeout: debuggerRemaining(500), signal: debuggerSignal(), maxBuffer: 1024 * 1024 })).stdout }
37
+ catch (e: any) {
38
+ if (debuggerSignal()?.aborted) throw new DebuggerError('DEBUGGER_CANCELLED')
39
+ debuggerRemaining(1)
40
+ if (!e.killed || e.stderr?.trim()) throw nativeError(e, 'DEVICE_UNAVAILABLE')
41
+ jdwpOutput = e.stdout ?? ''
42
+ }
43
+ const jdwp = new Set(jdwpOutput.split(/\s+/).filter(id => /^\d+$/.test(id)))
44
+ const ps = await this.adb(device, ['shell', 'ps', '-A', '-o', 'PID,NAME'])
45
+ const result: DebugTarget[] = []
46
+ for (const line of ps.split('\n').slice(1)) {
47
+ const [pid, name] = line.trim().split(/\s+/)
48
+ if (jdwp.has(pid) && (name === app || name.startsWith(app + ':'))) result.push(await this.identity(device, { app_id: app, pid: Number(pid), process_name: name, start_identity: '', uid }))
49
+ }
50
+ return result
51
+ }
52
+ async forward(device: string, pid: number) {
53
+ const port = Number(await this.adb(device, ['forward', '--no-rebind', 'tcp:0', `jdwp:${pid}`]))
54
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) throw new DebuggerError('DEBUGGER_ATTACH_FAILED')
55
+ return port
56
+ }
57
+ async removeForward(device: string, port: number, pid: number) {
58
+ const list = await this.adb(device, ['forward', '--list'])
59
+ const entry = list.split('\n').find(line => line.trim().split(/\s+/)[1] === `tcp:${port}`)
60
+ if (!entry) return
61
+ const [serial, local, remote] = entry.trim().split(/\s+/)
62
+ if (serial !== device || local !== `tcp:${port}` || remote !== `jdwp:${pid}`) throw new DebuggerError('DEBUGGER_CLEANUP_FAILED')
63
+ await this.adb(device, ['forward', '--remove', `tcp:${port}`])
64
+ }
65
+ }
@@ -0,0 +1,26 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks'
2
+ import { DebuggerError } from './worker.js'
3
+ let pauseProvider: (device: string) => any = () => null
4
+ export function registerDebuggerPauseProvider(provider: typeof pauseProvider) { pauseProvider = provider }
5
+ const context = new AsyncLocalStorage<{ platform: string; device: string; dispatched: boolean }>()
6
+ export function withDebuggerGuard<T>(platform: string, device: string, operation: () => Promise<T>) {
7
+ return context.run({ platform, device, dispatched: false }, operation)
8
+ }
9
+ // Synchronous checks and native submission share a JS turn with pause updates.
10
+ export function checkDebuggerGuard(dispatch = false) {
11
+ const current = context.getStore()
12
+ if (!current || current.platform !== 'android') return
13
+ if (pauseProvider(current.device)) throw new DebuggerError('DEBUGGER_PAUSED')
14
+ if (dispatch) current.dispatched = true
15
+ }
16
+ export function guardDispatched() { return context.getStore()?.dispatched ?? false }
17
+ export function pausedReadiness(device: string) {
18
+ const paused = pauseProvider(device)
19
+ if (!paused) return null
20
+ return { state: 'debugger_paused' as const, scope: 'device_ui' as const, basis: 'observed' as const,
21
+ captured_at_ms: paused.captured_at_ms, sample_count: 0, stable_for_ms: 0, snapshot_session_id: null, snapshot_revision: null,
22
+ evidence: [{ code: 'DEBUGGER_PAUSED', source: 'debugger', captured_at_ms: paused.captured_at_ms, snapshot_revision: null }],
23
+ limitations: ['FOREGROUND_APP_UNVERIFIED', 'DEVICE_WIDE_UI_GUARD'],
24
+ capabilities: { loading: true, transitioning: true, settled: true, unresponsive: false, debugger_paused: true },
25
+ debug_session_id: paused.debug_session_id, target: paused.target }
26
+ }