mobile-debug-mcp 0.35.0 → 0.36.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 (112) hide show
  1. package/.cursor-plugin/marketplace.json +1 -1
  2. package/.github/workflows/ci.yml +1 -1
  3. package/README.md +86 -24
  4. package/agent-plugin/.antigravity-plugin/README.md +48 -0
  5. package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
  6. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  7. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  8. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  9. package/agent-plugin/.mcp.json +1 -1
  10. package/agent-plugin/README.md +37 -8
  11. package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
  12. package/agent-plugin/cursor/LICENSE +201 -0
  13. package/agent-plugin/cursor/README.md +9 -0
  14. package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
  15. package/agent-plugin/mcp.json +1 -1
  16. package/agent-plugin/mcp_config.json +13 -0
  17. package/agent-plugin/plugin.json +1 -1
  18. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +36 -16
  19. package/dist/debugger/guard.js +4 -4
  20. package/dist/debugger/ios-worker.js +62 -0
  21. package/dist/debugger/ios.js +57 -0
  22. package/dist/debugger/sessions.js +33 -16
  23. package/dist/debugger/worker.js +6 -4
  24. package/dist/interact/index.js +12 -2
  25. package/dist/manage/ios.js +2 -0
  26. package/dist/observe/ios-trace/artifact.js +63 -0
  27. package/dist/observe/ios-trace/capabilities.js +86 -0
  28. package/dist/observe/ios-trace/native.js +93 -0
  29. package/dist/observe/ios-trace/notification.js +35 -0
  30. package/dist/observe/ios-trace/process.js +98 -0
  31. package/dist/observe/ios-trace/sessions.js +244 -0
  32. package/dist/observe/readiness-service.js +1 -1
  33. package/dist/observe/readiness.js +4 -4
  34. package/dist/observe/system-trace-error.js +7 -0
  35. package/dist/observe/system-trace-router.js +81 -0
  36. package/dist/observe/system-trace.js +4 -8
  37. package/dist/server/handlers/debugger-handlers.js +27 -8
  38. package/dist/server/handlers/diagnostic.js +12 -4
  39. package/dist/server/handlers/tool-handlers.js +19 -9
  40. package/dist/server/schemas/debugger-schema.js +10 -7
  41. package/dist/server/schemas/system-trace-schema.js +14 -9
  42. package/dist/server-core.js +1 -1
  43. package/dist/server.js +4 -4
  44. package/dist/utils/ios/utils.js +3 -0
  45. package/docs/CHANGELOG.md +23 -1
  46. package/docs/agent-plugin-installation-verification.md +39 -0
  47. package/docs/code-structure.md +3 -1
  48. package/docs/specs/031-ios-system-traces/intent.md +38 -0
  49. package/docs/specs/031-ios-system-traces/investigation.md +92 -0
  50. package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
  51. package/docs/specs/031-ios-system-traces/plan.md +138 -0
  52. package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
  53. package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
  54. package/docs/specs/031-ios-system-traces/spec.md +135 -0
  55. package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
  56. package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
  57. package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
  58. package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
  59. package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
  60. package/docs/tools/TOOLS.md +2 -2
  61. package/docs/tools/debugger.md +45 -5
  62. package/docs/tools/system-traces.md +81 -14
  63. package/package.json +2 -1
  64. package/scripts/agent-plugin.mjs +55 -5
  65. package/src/debugger/android.ts +1 -1
  66. package/src/debugger/guard.ts +5 -5
  67. package/src/debugger/ios-worker.ts +49 -0
  68. package/src/debugger/ios.ts +45 -0
  69. package/src/debugger/lldb/breakpoints.py +51 -0
  70. package/src/debugger/lldb/inspection.py +109 -0
  71. package/src/debugger/lldb/worker.py +203 -0
  72. package/src/debugger/sessions.ts +24 -12
  73. package/src/debugger/worker.ts +7 -4
  74. package/src/interact/index.ts +13 -2
  75. package/src/manage/ios.ts +2 -0
  76. package/src/observe/ios-trace/artifact.ts +55 -0
  77. package/src/observe/ios-trace/capabilities.ts +74 -0
  78. package/src/observe/ios-trace/native.ts +72 -0
  79. package/src/observe/ios-trace/notification.ts +30 -0
  80. package/src/observe/ios-trace/process.ts +82 -0
  81. package/src/observe/ios-trace/sessions.ts +204 -0
  82. package/src/observe/readiness-service.ts +1 -1
  83. package/src/observe/readiness.ts +4 -4
  84. package/src/observe/system-trace-error.ts +3 -0
  85. package/src/observe/system-trace-router.ts +61 -0
  86. package/src/observe/system-trace.ts +4 -4
  87. package/src/server/handlers/debugger-handlers.ts +28 -9
  88. package/src/server/handlers/diagnostic.ts +11 -4
  89. package/src/server/handlers/tool-handlers.ts +20 -9
  90. package/src/server/schemas/debugger-schema.ts +10 -7
  91. package/src/server/schemas/system-trace-schema.ts +15 -9
  92. package/src/server-core.ts +1 -1
  93. package/src/server.ts +3 -3
  94. package/src/utils/ios/utils.ts +3 -0
  95. package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
  96. package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
  97. package/test/fixtures/contracts/tool-definitions.json +192 -32
  98. package/test/fixtures/ios-debugger-app/Main.swift +95 -0
  99. package/test/fixtures/ios-debugger-app/README.md +27 -0
  100. package/test/fixtures/ios-debugger-app/build.ts +21 -0
  101. package/test/helpers/ios-trace-shutdown.ts +29 -0
  102. package/test/unit/debugger/handle-guards.test.ts +88 -0
  103. package/test/unit/debugger/ios-preparation.test.ts +57 -0
  104. package/test/unit/debugger/ios.test.ts +104 -0
  105. package/test/unit/debugger/lldb-worker.py +166 -0
  106. package/test/unit/debugger/lldb-worker.test.ts +10 -0
  107. package/test/unit/observe/ios-trace-native.test.ts +90 -0
  108. package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
  109. package/test/unit/observe/readiness.test.ts +1 -1
  110. package/test/unit/observe/system-trace-router.test.ts +49 -0
  111. package/test/unit/quality/agent-plugin-export.test.ts +38 -0
  112. package/test/unit/server/system-trace.test.ts +12 -3
@@ -0,0 +1,153 @@
1
+ # iOS runtime debugger — specification
2
+
3
+ Source: [issue #70](https://github.com/clivejefferies/mobile-debug-tools/issues/70).
4
+ Scope: [accepted intent](intent.md), approved by the user's “ok” on 2026-11-04.
5
+ Status: **approved** by the user's “ok” on 2026-11-04 in response to the complete
6
+ specification and approval request in this chat. Implementation authorised.
7
+
8
+ ## Behaviour and supported environment
9
+
10
+ Add optional LLDB-backed debugging for an already-running development app on an
11
+ explicit iOS simulator UDID, on macOS with full Xcode and its Swift-enabled LLDB.
12
+ Initial verification targets the installed Xcode 27.2, LLDB 2103.0.34.407 and Swift
13
+ 6.4.2 toolchain; other versions remain unverified until separately exercised.
14
+ Physical devices, remote hosts and production/optimised apps are unsupported in
15
+ this initial matrix. Normal mobile tools must not acquire an LLDB dependency.
16
+
17
+ Provide source breakpoints, stop events, bounded stacks and shallow locals,
18
+ into/over/out stepping, resume and detach through the existing eleven tools.
19
+ Attach may briefly stop the process for setup, but returns running unless an
20
+ actual stop event has been retained; never discard a racing breakpoint stop.
21
+ LLDB stops suspend the target process, not the whole simulator.
22
+
23
+ ## Tool contract and compatibility
24
+
25
+ Extend `get_debugger_capabilities` and `attach_debugger` to accept
26
+ `platform: "android" | "ios"`, existing explicit `deviceId`, `appId` and optional
27
+ positive `processId`. For iOS, require a simulator UDID and valid bundle identifier.
28
+ Probe without attaching or launching the app. Report missing prerequisites,
29
+ unsupported target types, absent apps/processes and ambiguous candidates
30
+ explicitly. A probe cannot guarantee that later attach permission will succeed.
31
+
32
+ Preserve all existing Android requests. Extend `set_debugger_breakpoint` with
33
+ optional `sourceFile` (nonempty absolute local source path, maximum 512 characters).
34
+ Require exactly one of `className` or `sourceFile`, with the existing positive
35
+ 1-based `line`. Android sessions require `className`; iOS sessions require
36
+ `sourceFile`. Invalid combinations fail `INVALID_ARGUMENT`. Resolve the full
37
+ path against loaded debug information, never a basename fallback or shell search.
38
+ Return owned breakpoint ID, pending/resolved/rejected status and actual locations.
39
+ Unloaded images can remain pending. Only exact executable line matches may be
40
+ enabled; locations silently moved by LLDB are rejected and removed.
41
+
42
+ Keep session, stop, thread and frame arguments, tool names, response modes,
43
+ pagination, defaults and timeout bounds unchanged. Existing common response and
44
+ error shapes remain: `success`, session/target/device metadata, state, timestamp,
45
+ capabilities, limitations, cleanup, reason, stop, operation `result`, and sanitized
46
+ `error: {code, message, retryable, diagnostic?}`. Device platform is correct on
47
+ success and failure. iOS target metadata carries bundle ID, PID, executable name,
48
+ process-start identity and nullable UID; Android retains its existing fields.
49
+
50
+ Capability values are backend-specific. iOS advertises supported core operations;
51
+ `evaluation`, `mutation`, `coroutines` and `native` remain false (`native` denotes
52
+ an exposed native-specific inspection capability, not LLDB's implementation).
53
+ Add iOS-only boolean `async_execution_inspection` and `async_stepping`; the former
54
+ requires the async acceptance journey below, the latter a separate demonstrated
55
+ step across suspension. False values carry explanatory limitations. No suspended
56
+ task enumeration, task identity API or logical-task reconstruction is promised.
57
+ Preserve compact mode's existing omissions and full attach/capabilities/state
58
+ responses. Unknown fields remain rejected.
59
+
60
+ ## Async semantics
61
+
62
+ Source breakpoints must work before and after a controlled Swift `await`, with
63
+ useful known primitive locals at each actual stop. Include nested child work,
64
+ actor/executor transitions and a separately triggered task cancellation path.
65
+ Use deterministic fixture gates and completion/cancellation UI state.
66
+
67
+ Stepping acknowledges delivery and resumes; only a subsequent event establishes
68
+ where execution stopped. Exercise ordinary helper into/over/out and an `await`
69
+ boundary. Delegate stepping to Swift-enabled LLDB; do not equate a physical
70
+ thread ID with stable task identity. If the tested LLDB route follows the async
71
+ continuation, report the observed source stop and advertise `async_stepping`.
72
+ Otherwise report it false with the limitation and demonstrate recovery through
73
+ independent after-await breakpoints. Unexpected missing async locals or source
74
+ stops fail core async acceptance rather than being labelled support.
75
+
76
+ Stacks describe LLDB's observed frames; label unavailable source/variable data
77
+ and generated frames honestly. Do not claim a complete logical async ancestry.
78
+ No expression evaluation, object descriptions, custom formatters that execute
79
+ code, property getters, dynamic method calls or continuation-field traversal.
80
+
81
+ ## Ownership, failure and safety
82
+
83
+ Resolve simulator, bundle executable and running PID together; validate executable
84
+ path, start identity and simulator membership before and after attach. Never attach
85
+ to an arbitrary host PID, launch a missing app or replace another debugger.
86
+ Reject identity changes and duplicate owned sessions. Serialize session mutations;
87
+ isolate sessions and guards by platform plus device, including identical IDs.
88
+
89
+ Reuse stop-scoped opaque references, the 256-event ring/100-event response limit,
90
+ 1000-frame cap, default 20/max 100 stack page, max 100 locals and 256-character
91
+ string bound. Object locals expose type/opaque identity only. Missing or
92
+ inaccessible variables are explicit; an empty list cannot prove availability.
93
+
94
+ Generalise pause guards/readiness to iOS lifecycle actions, input, waits and
95
+ assertions. Preserve device-wide conservative guarding, racing-dispatch
96
+ uncertainty and observation access within existing deadlines. Do not claim
97
+ foreground identity from debugger state.
98
+
99
+ Use a dedicated owned LLDB worker with bounded structured IPC, argument-array
100
+ subprocess calls, disabled user init files and no arbitrary command interface.
101
+ Sanitize errors using the existing bounded diagnostic catalog; iOS recovery
102
+ guidance names Xcode/LLDB rather than JDK/ADB. Never log worker messages, locals,
103
+ raw stderr, exception text or source contents. Debug inspection stays local and
104
+ request-scoped; public evidence uses only the disposable fixture.
105
+
106
+ Retain 100–30000 ms request bounds, 60-second pause, five-minute idle and
107
+ 30-minute session expiry, including independent worker watchdogs. Event-wait
108
+ cancellation preserves a usable session. Inspection timeouts preserve a known
109
+ session; uncertain attach or mutation outcomes trigger owned teardown and report
110
+ execution uncertainty. No automatic retries of attach/step/resume.
111
+
112
+ Detach releases owned suspension, breakpoints, step plans and worker resources;
113
+ never kill the app, another debugger or simulator. EOF/shutdown/expiry attempt
114
+ detach. Cleanup is idempotent and retryable with explicit remaining resources;
115
+ transport loss or forced host termination cannot imply confirmed recovery.
116
+
117
+ ## Acceptance and verification
118
+
119
+ 1. Real MCP simulator journey: attach, exact and pending source breakpoint,
120
+ stop, source stack, known locals, helper stepping, resume, removal and detach.
121
+ 2. Async journey: before/after-await locals, nested work and executor transition,
122
+ completion, cancellation recovery, and recorded boundary-step outcome.
123
+ 3. Safety: stale references, cross-session/device isolation, paused input and
124
+ readiness, cancelled event wait, process exit, attach failure, timeout,
125
+ automatic expiry and repeatable cleanup. Verify app usability after detach.
126
+ 4. Contract checks cover registration, runtime validation, dispatch, both response
127
+ modes, diagnostics and Android compatibility. Run `npm run verify`, applicable
128
+ `npm run test:device`, focused iOS MCP journeys and Android debugger regressions.
129
+ 5. Record fixture/server hashes, Xcode/LLDB/Swift/runtime versions, target UDID,
130
+ exact actions, observed values and independent UI-tree assertions. Report
131
+ physical-device and unavailable coverage as unrun, never inferred.
132
+
133
+ Likely affected areas: `src/debugger/`, debugger schemas/handlers/serialization,
134
+ iOS interaction/readiness boundaries, fixture/device/unit trees, packaging and
135
+ debugger tool documentation/README/changelog. Reuse worker/session abstractions
136
+ without routing Android through LLDB. Main risks are simulator process attribution,
137
+ LLDB/Python distribution, async metadata, source relocation and teardown races.
138
+
139
+ ## Investigation evidence and remaining prerequisites
140
+
141
+ On 2026-11-04, local version probes found the toolchain above. Sandboxed simctl
142
+ could not contact CoreSimulator; an authorised outside-sandbox read succeeded and
143
+ found no booted simulators. No existing iOS debugger implementation or live iOS
144
+ debug journey is claimed. Implementation verification requires booting an
145
+ available simulator and installing only the disposable development fixture.
146
+
147
+ Apple documents async-aware stepping since Swift 6.2 in
148
+ [What's new in Swift](https://developer.apple.com/videos/play/wwdc2025/245/).
149
+ The [LLDB SBFrame API](https://lldb.llvm.org/python_reference/lldb.SBFrame-class.html)
150
+ provides frame-variable access. These support the proposed route, not proof of
151
+ this server's behaviour; exact worker loading, attach and safe variable extraction
152
+ must be validated before claiming support. If unavailable, record the concrete
153
+ blocker and return for a scope decision rather than weakening acceptance.
@@ -11,8 +11,8 @@ Use the guides below when you need a tool contract or a specific workflow.
11
11
  - [Interaction](interact.md) — UI resolution, actions, waits, and deterministic verification.
12
12
  - [Environment diagnostics](system.md) — toolchain discovery and health checks.
13
13
  - [Evidence correlation](correlated-evidence.md) — action references, capture timing, and freshness.
14
- - [Android system traces](system-traces.md) — performance capture and local Perfetto artifacts.
15
- - [Android runtime debugger](debugger.md) — process attachment, breakpoints, and bounded runtime inspection.
14
+ - [System traces](system-traces.md) — Android Perfetto and iOS simulator Time Profiler capture with local artifacts.
15
+ - [Android and iOS simulator runtime debugger](debugger.md) — process attachment, breakpoints, and bounded runtime inspection.
16
16
 
17
17
  ## Interaction principles
18
18
 
@@ -1,4 +1,4 @@
1
- # Android runtime debugger
1
+ # Runtime debugger
2
2
 
3
3
  [Documentation](../README.md) · [Tool reference](TOOLS.md)
4
4
 
@@ -98,7 +98,7 @@ state-changing command. Timeout or cancellation does not prove nondelivery.
98
98
  Retry incomplete cleanup with `detach_debugger`; never automatically retry
99
99
  attach, step or resume, or kill the app/ADB server. Diagnostics improve failure
100
100
  classification and do not establish the cause of a previously intermittent error.
101
- This contract applies to the Android backend; iOS debugging remains unsupported.
101
+ These details describe Android; see the iOS simulator backend below for its prerequisites and recovery guidance.
102
102
 
103
103
  ## Suspension, limits and cleanup
104
104
 
@@ -108,8 +108,8 @@ racing input submission preserves delivery uncertainty and blocks verification.
108
108
  Journeys preserve completed steps. Other devices are unaffected.
109
109
 
110
110
  `get_app_readiness` returns evidence-backed `debugger_paused` without reading the
111
- UI; readiness waits fail with `DEBUGGER_PAUSED`. This signal covers managed Android
112
- sessions only and does not establish foreground app identity. Raw trees, screenshots
111
+ UI; readiness waits fail with `DEBUGGER_PAUSED`. This signal covers managed Android and iOS simulator
112
+ sessions and does not establish foreground app identity. Raw trees, screenshots
113
113
  and logs remain available within their own timeouts; snapshot/log acquisition may
114
114
  still fail while the app is suspended. Known pauses are annotated on observations.
115
115
 
@@ -134,7 +134,7 @@ step, resume or attach.
134
134
  ## Limits, privacy and evidence
135
135
 
136
136
  There is no expression evaluation, object traversal, method invocation, mutation,
137
- coroutine reconstruction, native debugger, iOS debugger or heap capture. Kotlin
137
+ coroutine reconstruction, native-specific inspection or heap capture. The optional iOS simulator backend is described below. Kotlin
138
138
  results follow emitted bytecode/debug metadata; optimized/obfuscated apps may lack
139
139
  useful source lines or locals. Locals may contain private data: values are returned
140
140
  only on request, without protocol/value logging, persistent dumps or upload.
@@ -178,3 +178,43 @@ is selected. Fixture compilation requires SDK platform/build-tools 36 and `javac
178
178
  - [App management](manage.md): build, install, and launch the app before attaching.
179
179
  - [App readiness](observe.md#app-readiness): understand known debugger-pause evidence.
180
180
  - [Android system traces](system-traces.md): investigate performance without paused-state inspection.
181
+
182
+ ## iOS simulator backend
183
+
184
+ The same eleven tools accept `platform: "ios"` for target capability probes and
185
+ attach. Supply an explicit booted simulator UDID and bundle `appId`; an optional
186
+ `processId` must belong to that simulator's installed app. Requires macOS and full
187
+ Xcode with its Swift-enabled LLDB and Python bindings. Physical iOS devices remain
188
+ unsupported. Attach never launches an app, chooses a host process by name alone,
189
+ replaces another debugger, or changes security settings.
190
+
191
+ For `set_debugger_breakpoint`, supply `sourceFile` (absolute local source path)
192
+ and 1-based `line` for iOS; continue using binary `className` for Android. Exactly
193
+ one selector is required. iOS reports `resolved`, `pending` or `rejected` with
194
+ actual locations; LLDB line relocation is disabled. Pending breakpoints may bind
195
+ when their image loads. Other tool arguments, stop-scoped references, stack/locals
196
+ limits, event cursors, response modes and expiry durations follow the flow above.
197
+
198
+ iOS returns backend-specific capabilities and limitations; `target.uid` may be
199
+ null and `target.executable_path` identifies the verified simulator executable.
200
+ `async_execution_inspection` and `async_stepping` are independent capability flags.
201
+ False means the capability is not established for the backend's supported matrix;
202
+ inspect the limitations. A source breakpoint after await and a shallow local do
203
+ not enumerate suspended tasks or establish logical task identity. Step requests
204
+ acknowledge delivery; inspect the subsequent actual stop. Object locals expose
205
+ only bounded type/opaque identity; Swift standard numeric/bool wrappers are read
206
+ from static storage; Swift strings and other aggregates retain type summaries. No expressions, descriptions, dynamic getters or arbitrary
207
+ LLDB commands execute through this API.
208
+
209
+ Known iOS pauses guard lifecycle actions, input, waits and assertions on that
210
+ platform/device and supply `debugger_paused` readiness. Raw observations remain
211
+ available within their own deadlines. Detach removes owned breakpoints and worker
212
+ resources and releases owned suspension; it never kills the app or simulator.
213
+ A worker exit without confirmed detach/process exit cannot prove cleanup; retry
214
+ `detach_debugger` and inspect `cleanup.remaining_resources`. Failure diagnostics
215
+ provide static Xcode/LLDB-specific recovery guidance without raw worker text.
216
+
217
+ Use the [isolated simulator fixture](../../test/fixtures/ios-debugger-app/README.md)
218
+ for real MCP runtime and async checks. The [implementation plan](../specs/032-ios-runtime-debugger/plan.md)
219
+ records exact verification and unavailable coverage. An installed/published package,
220
+ physical device, or other Xcode version requires separate evidence.
@@ -1,9 +1,10 @@
1
- # Android system traces
1
+ # System traces for Android and iOS
2
2
 
3
3
  [Documentation](../README.md) · [Tool reference](TOOLS.md)
4
4
 
5
5
  Use system traces to collect scheduling and system annotations around a slow
6
- interaction. Capture is optional, device-wide and Android-only; it does not
6
+ interaction. Capture is optional; Android uses device-wide Perfetto and iOS simulators use
7
+ app-attached Instruments Time Profiler. It does not
7
8
  automatically diagnose performance problems. No root or Android CLI installation
8
9
  is required. The device must provide Perfetto (Android API 29+), authorised ADB,
9
10
  tracing services, required sched/input categories, readable process identity,
@@ -11,14 +12,14 @@ base64 decoding and writable `/data/misc/perfetto-traces` storage.
11
12
 
12
13
  ## Tools
13
14
 
14
- All inputs reject unknown fields. `platform` is `android` or `ios`; iOS is
15
- unsupported and never starts capture. Optional `deviceId` uses existing device
16
- resolution; supplying one is recommended when multiple devices are connected.
15
+ All inputs reject unknown fields. `platform` is `android` or `ios`. Android's
16
+ optional `deviceId` uses existing device resolution. iOS requires an explicit
17
+ booted simulator UDID, plus `appId` for start; it never records the Mac host.
17
18
 
18
19
  | Tool | Arguments | Result |
19
20
  | --- | --- | --- |
20
- | `get_system_trace_capabilities` | `platform`, `deviceId?` | `supported`, `backend`, resolved `device`, prerequisite booleans, reasons, enabled sources, unavailable categories, limitations. No recording. |
21
- | `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. |
21
+ | `get_system_trace_capabilities` | `platform`, `deviceId?`, `appId?`, `template?` | Common: `supported`, `backend`, resolved `device`, reasons and limitations. Android: prerequisite booleans, `enabled_sources`, `unavailable_categories`. iOS: prerequisites as `available`/`missing`/`unknown`, `discovered_templates`, `templates`, `app`, `xcode_version`, `recovery`. No recording. |
22
+ | `start_system_trace` | `platform`, `deviceId?`, `durationMs?`, `appId?`, `template?` | Owned recording session after native readiness acknowledgement. Duration is an integer 1,000–120,000 ms, default 30,000. |
22
23
  | `get_system_trace_status` | `traceId` | Reconcile process/deadline and finalise ended recording. |
23
24
  | `stop_system_trace` | `traceId` | Gracefully stop and retrieve local artifact; retry transfer/cleanup after failure. |
24
25
  | `cancel_system_trace` | `traceId` | Stop and discard partial files. Already retrieved artifacts remain local. |
@@ -26,7 +27,7 @@ resolution; supplying one is recommended when multiple devices are connected.
26
27
  Trace control IDs are UUIDs and cannot redirect to another device. One capture
27
28
  per device and four pending sessions per server are allowed. Pending failed
28
29
  cleanup reserves its slot until resolved. Terminal records with complete cleanup
29
- expire after 10 minutes or eviction beyond 100 records. Server restart invalidates
30
+ expire after 10 minutes or eviction beyond 100 records per backend (the router retains at most 100 completed IDs). Server restart invalidates
30
31
  IDs. Repeated terminal operations return the stored outcome.
31
32
 
32
33
  ## Capture workflow
@@ -89,12 +90,13 @@ proof of causality; host intervals are not aligned with device trace clocks.
89
90
  ## Session response and failures
90
91
 
91
92
  JSON responses contain `trace_id`, process-scoped `session_id`, `device`,
92
- `backend:"perfetto"`, `state` (starting/recording/stopping/completed/cancelled/failed),
93
+ `backend:"perfetto"` (Android) or `"xctrace"` (iOS), `state` (starting/recording/stopping/completed/cancelled/failed),
93
94
  nullable `reason` and `completion_reason`, host UTC capture bounds, nullable
94
- `artifact`, `cleanup`, `correlation_ids`, `enabled_sources`, and
95
- `unavailable_categories`. Capture bounds include command/acknowledgement latency.
95
+ `artifact`, `cleanup`, and `correlation_ids`. Android additionally returns
96
+ `enabled_sources` and `unavailable_categories`. Capture bounds include command/acknowledgement latency.
96
97
 
97
- `artifact` contains absolute `path`, `format:"perfetto"` and `size_bytes`.
98
+ Android `artifact` contains absolute `path`, `format:"perfetto"` and `size_bytes`;
99
+ see the iOS section below for Instruments artifact and session fields.
98
100
  `cleanup` contains `status` (pending/complete/failed) and `remaining_resources`.
99
101
  Errors preserve known session metadata and set MCP `isError`. Argument/unknown-ID
100
102
  errors return `{error:{tool,code,message}}` without a session. Codes include
@@ -105,7 +107,7 @@ Capability failures return ordinary `supported:false` responses with reasons.
105
107
  Read state/cleanup independently: a successful artifact transfer can still have
106
108
  failed device-file cleanup. Retry stop/status, or cancel to discard failed output.
107
109
 
108
- ## Limits and privacy
110
+ ## Android limits and privacy
109
111
 
110
112
  The fixed preset requests sched, freq, idle, am, wm, gfx, view and input categories,
111
113
  a 16 MiB ring buffer and a 32 MiB maximum output. Missing optional categories are
@@ -127,8 +129,73 @@ a uniquely named bounded device file for manual removal; restart performs no
127
129
  broad cleanup sweep. Device suspend can extend Perfetto's own monotonic duration
128
130
  limit; the server timer bounds wall-clock lifetime while the server is running.
129
131
 
132
+ ## iOS simulator Instruments capture
133
+
134
+ Requires macOS, full Xcode with Instruments and clang, a booted simulator visible
135
+ in both `simctl` and `xctrace list devices`, and an already running development
136
+ app. Journey input also needs a compatible idb companion; capability discovery
137
+ for capture does not establish HID input support. The initial verified target is iPhone 17 Pro simulator / iOS 26.4.1 with
138
+ Xcode 27.2. Physical-device capture is unsupported and unrun. Tool installation,
139
+ OS profiling permissions and pairing/security changes are user actions.
140
+
141
+ ```json
142
+ {"platform":"ios","deviceId":"<simulator-UDID>","appId":"<bundle-id>","template":"Time Profiler","durationMs":60000}
143
+ ```
144
+
145
+ Use this target with capabilities and start, then bracket a `run_journey` on the
146
+ same simulator using the returned `systemTraceId`, and stop/cancel as above.
147
+ Omitting `template` defaults to Time Profiler. `appId` and `template` are rejected
148
+ on Android. Capabilities do not start capture; permissions and symbols remain
149
+ `unknown` until separately established. Installed templates are listed separately
150
+ from support: System Trace scheduling coverage is unverified, Animation Hitches
151
+ reported unsupported on the tested simulator, and Hangs is bundled within Time
152
+ Profiler rather than a selectable standalone template. No silent substitution.
153
+
154
+ The server resolves the bundle to an exact current executable/PID, pins the UDID,
155
+ and waits for a uniquely named Darwin recording-start notification before
156
+ returning `recording`. It compiles a small private listener with Xcode clang in
157
+ the owned temporary directory. A failed/uncertain start cleans up and returns a
158
+ failure, not an automatic retry. Errors such as `APP_NOT_RUNNING`,
159
+ `PROFILING_PERMISSION_REQUIRED`, `READINESS_TIMEOUT` and `TARGET_DISCONNECTED`
160
+ include bounded recovery guidance; raw recorder output is not returned.
161
+
162
+ Only the session-owned recorder is signalled; cleanup never terminates the app.
163
+ iOS readiness has a 35-second budget, stop/finalisation 30 seconds and artifact
164
+ validation 30 seconds. Recording is bounded by native duration and server
165
+ monitoring. Monitoring retains the pinned app identity and tolerates two consecutive
166
+ non-definitive command failures, resetting after a successful poll. The third
167
+ fails capture with its bounded reason (`COMMAND_FAILED`, `ACTION_TIMEOUT`, or
168
+ `TRACE_MONITOR_FAILED` for an unclassified failure). Storage limits, changed app
169
+ identity and a missing running app fail immediately. Expiry and recorder exit
170
+ are still checked after a tolerated poll failure. Shutdown attempts cancellation under an overall ten-second budget.
171
+ Failed export can be retried using stop/status without starting another recording;
172
+ cancel removes owned partial files. Completed artifacts remain for user deletion.
173
+
174
+ The `.trace` result may be a bundle/directory. `artifact` adds
175
+ `format:"instruments"`, `kind`, recursive `size_bytes`, and
176
+ `validation:{readable,schema,sample_rows}`. Validation checks a readable TOC,
177
+ matching target UUID/PID/template, `time-sample` schema and exported sample data.
178
+ Zero rows mean no CPU sample evidence. Warnings are preserved as bounded
179
+ classifications. This establishes readable capture, not successful symbolication,
180
+ working hang detection or root-cause diagnosis. Do not treat a saved failed trace
181
+ as a successful recording.
182
+
183
+ Results also include `app_id`, `pid`, `executable`, `template`,
184
+ `recording_ready_at_ms`, `warnings` and `clock_alignment:"unknown"`. The profiled
185
+ app stays pinned even if the journey navigates elsewhere; linked IDs do not prove
186
+ that other apps are captured. Start/stop during a linked journey reports partial
187
+ coverage. Host capture bounds include startup/finalisation uncertainty.
188
+
189
+ At least 1 GiB free output space is required before recording. The server checks
190
+ a 256 MiB recursive size threshold each second; this is a soft limit and can be
191
+ overshot during polling/finalisation. Oversized output fails and is discarded.
192
+ Final bundles reject symlinks and use restricted file/directory permissions.
193
+ Even app-attached capture may include sensitive system metadata, paths, symbols,
194
+ logs and signposts. Keep artifacts local and delete them when finished; no upload,
195
+ base64 response, permission bypass or restart-wide cleanup sweep occurs.
196
+
130
197
  ## Related guides
131
198
 
132
199
  - [Interaction](interact.md#interaction-execution-controls-rfc-018): run the journey being measured.
133
200
  - [Evidence correlation](correlated-evidence.md): interpret action and observation timing.
134
- - [Android runtime debugger](debugger.md): inspect paused application state.
201
+ - [Runtime debugger](debugger.md): inspect paused application state.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-debug-mcp",
3
- "version": "0.35.0",
3
+ "version": "0.36.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": {
@@ -19,6 +19,7 @@
19
19
  "test": "npm run test:unit",
20
20
  "lint": "eslint src test --quiet && npm run check:quality && npm run check:architecture",
21
21
  "lint:fix": "eslint src test --fix && npm run check:quality && npm run check:architecture",
22
+ "package:antigravity": "node scripts/agent-plugin.mjs --export-antigravity",
22
23
  "sync:agent-plugin": "node scripts/agent-plugin.mjs --sync",
23
24
  "check:quality": "node scripts/code-quality.mjs",
24
25
  "check:architecture": "node scripts/check-architecture.mjs",
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert/strict'
2
- import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { copyFileSync, cpSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
3
3
  import { fileURLToPath } from 'node:url'
4
4
  import path from 'node:path'
5
5
 
@@ -43,22 +43,53 @@ const cursorManifest = {
43
43
  },
44
44
  },
45
45
  }
46
+ const antigravityManifest = {
47
+ $schema: 'https://antigravity.google/schemas/v1/plugin.json',
48
+ ...sharedMetadata,
49
+ displayName: extensions['com.openai'].interface.displayName,
50
+ logo: 'assets/mobile-debug-tools.svg',
51
+ suggestedPrompts: extensions['com.openai'].interface.defaultPrompt,
52
+ }
53
+ const antigravityMcp = {
54
+ mcpServers: Object.fromEntries(Object.entries(codexMcp.mcpServers).map(([name, server]) => {
55
+ const { type: _transport, ...settings } = server
56
+ return [name, settings]
57
+ })),
58
+ }
59
+ const mcpConfigs = [
60
+ ['.mcp.json', codexMcp],
61
+ ['mcp_config.json', antigravityMcp],
62
+ ]
46
63
  const manifests = [
47
64
  ['.codex-plugin/plugin.json', codexManifest],
48
65
  ['.claude-plugin/plugin.json', claudeManifest],
49
66
  ['.cursor-plugin/plugin.json', cursorManifest],
67
+ ['.antigravity-plugin/plugin.json', antigravityManifest],
50
68
  ]
51
69
 
70
+ const cursorFiles = [
71
+ ['.cursor-plugin/plugin.json', `${JSON.stringify(cursorManifest, null, 2)}\n`],
72
+ ['skills/mobile-debug-mcp/SKILL.md', readFileSync(packagedSkill)],
73
+ ['LICENSE', readFileSync(sourceLicense)],
74
+ ['README.md', '# Mobile Debug Tools for Cursor\n\nGenerated by `npm run sync:agent-plugin`; edit the canonical plugin source.\n\nCopy the contents of this directory into `~/.cursor/plugins/local/mobile-debug-tools`,\nthen run Developer: Reload Window and check Customize for the skill and MCP server.\nDo not copy the portable parent plugin: its `${PLUGIN_DATA}` variable is not expanded by Cursor.\n\nSetup and support: https://github.com/clivejefferies/mobile-debug-tools\n'],
75
+ ]
52
76
  if (process.argv[2] === '--sync') {
53
77
  copyFileSync(sourceLicense, packagedLicense)
54
- writeFileSync(path.join(pluginRoot, '.mcp.json'), `${JSON.stringify(codexMcp, null, 2)}\n`)
78
+ for (const [relativePath, contents] of cursorFiles) {
79
+ const target = path.join(pluginRoot, 'cursor', relativePath)
80
+ mkdirSync(path.dirname(target), { recursive: true })
81
+ writeFileSync(target, contents)
82
+ }
83
+ for (const [relativePath, config] of mcpConfigs) {
84
+ writeFileSync(path.join(pluginRoot, relativePath), `${JSON.stringify(config, null, 2)}\n`)
85
+ }
55
86
  for (const [relativePath, manifest] of manifests) {
56
87
  const manifestPath = path.join(pluginRoot, relativePath)
57
88
  mkdirSync(path.dirname(manifestPath), { recursive: true })
58
89
  writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
59
90
  }
60
91
  }
61
- else if (process.argv[2] !== '--check') throw new Error('Use --sync or --check')
92
+ else if (!['--check', '--export-antigravity'].includes(process.argv[2])) throw new Error('Use --sync, --check or --export-antigravity <new-directory>')
62
93
 
63
94
  assert.equal(plugin.$schema, 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json')
64
95
  assert.equal(plugin.name, 'mobile-debug-tools')
@@ -73,7 +104,10 @@ assert.equal(server.command, 'npx')
73
104
  assert.deepEqual(server.args, ['--yes', `${packageInfo.name}@${packageInfo.version}`, 'server'])
74
105
  assert.equal(server.cwd, '${PLUGIN_DATA}', 'npx must run outside the same-named source checkout')
75
106
  assert.deepEqual(readFileSync(packagedLicense), readFileSync(sourceLicense), 'packaged license must match its source')
76
- assert.deepEqual(JSON.parse(readFileSync(path.join(pluginRoot, '.mcp.json'), 'utf8')), codexMcp, 'Codex MCP configuration must match the portable plugin')
107
+ for (const [relativePath, config] of mcpConfigs) {
108
+ assert.deepEqual(JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')), config, `${relativePath} must match the portable MCP configuration`)
109
+ }
110
+ assert.ok(readFileSync(path.join(pluginRoot, antigravityManifest.logo)).length > 0, 'Antigravity logo must be bundled')
77
111
  for (const [relativePath, expectedManifest] of manifests) {
78
112
  assert.deepEqual(
79
113
  JSON.parse(readFileSync(path.join(pluginRoot, relativePath), 'utf8')),
@@ -81,6 +115,9 @@ for (const [relativePath, expectedManifest] of manifests) {
81
115
  `${relativePath} must match the portable plugin`,
82
116
  )
83
117
  }
118
+ for (const [relativePath, contents] of cursorFiles) {
119
+ assert.deepEqual(readFileSync(path.join(pluginRoot, 'cursor', relativePath)), Buffer.from(contents), `Cursor native ${relativePath} must match its source`)
120
+ }
84
121
  for (const marketplacePath of [
85
122
  '.agents/plugins/marketplace.json',
86
123
  '.claude-plugin/marketplace.json',
@@ -90,9 +127,22 @@ for (const marketplacePath of [
90
127
  assert.equal(marketplace.name, plugin.name, `${marketplacePath} name must match the plugin`)
91
128
  assert.equal(marketplace.plugins.length, 1, `${marketplacePath} must expose one plugin`)
92
129
  assert.equal(marketplace.plugins[0].name, plugin.name, `${marketplacePath} plugin name must match`)
93
- assert.equal(marketplace.plugins[0].source?.path ?? marketplace.plugins[0].source, './agent-plugin', `${marketplacePath} source must point to the plugin`)
130
+ assert.equal(marketplace.plugins[0].source?.path ?? marketplace.plugins[0].source, marketplacePath.startsWith('.cursor-plugin') ? './agent-plugin/cursor' : './agent-plugin', `${marketplacePath} source must point to the plugin`)
94
131
  }
95
132
  const skillName = readFileSync(packagedSkill, 'utf8').match(/^---\r?\nname: ([a-z0-9-]+)\r?\n/)?.[1]
96
133
  assert.equal(skillName, path.basename(path.dirname(packagedSkill)), 'packaged skill directory must match its frontmatter name')
97
134
 
98
135
  console.log('Agent plugin package is in sync')
136
+
137
+ if (process.argv[2] === '--export-antigravity') {
138
+ assert.ok(process.argv[3], 'Supply a new output directory for the Antigravity export')
139
+ const target = path.resolve(process.argv[3])
140
+ // Refuse existing directories rather than overwrite source or stale package files.
141
+ mkdirSync(target)
142
+ writeFileSync(path.join(target, 'plugin.json'), `${JSON.stringify(antigravityManifest, null, 2)}\n`)
143
+ writeFileSync(path.join(target, 'mcp_config.json'), `${JSON.stringify(antigravityMcp, null, 2)}\n`)
144
+ copyFileSync(path.join(pluginRoot, '.antigravity-plugin/README.md'), path.join(target, 'README.md'))
145
+ copyFileSync(packagedLicense, path.join(target, 'LICENSE'))
146
+ for (const directory of ['skills', 'assets']) cpSync(path.join(pluginRoot, directory), path.join(target, directory), { recursive: true, filter: file => path.basename(file) !== '.DS_Store' && !path.basename(file).startsWith('._') })
147
+ console.log(`Antigravity package exported to ${target}`)
148
+ }
@@ -3,7 +3,7 @@ import { execFile } from 'node:child_process'
3
3
  import { promisify } from 'node:util'
4
4
  import { DebuggerError, debuggerRemaining, debuggerSignal, nativeError } from './worker.js'
5
5
  const exec = promisify(execFile)
6
- export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number }
6
+ export interface DebugTarget { app_id: string; pid: number; process_name: string; start_identity: string; uid: number | null; executable_path?: string }
7
7
  export interface DebugNative {
8
8
  candidates(device: string, app: string): Promise<DebugTarget[]>
9
9
  identity(device: string, target: DebugTarget): Promise<DebugTarget>
@@ -5,7 +5,7 @@ interface PauseEvidence {
5
5
  captured_at_ms: number
6
6
  target: unknown
7
7
  }
8
- let pauseProvider: (device: string) => PauseEvidence | null = () => null
8
+ let pauseProvider: (device: string, platform?: string) => PauseEvidence | null = () => null
9
9
  export function registerDebuggerPauseProvider(provider: typeof pauseProvider) { pauseProvider = provider }
10
10
  const context = new AsyncLocalStorage<{ platform: string; device: string; dispatched: boolean }>()
11
11
  export function withDebuggerGuard<T>(platform: string, device: string, operation: () => Promise<T>) {
@@ -14,13 +14,13 @@ export function withDebuggerGuard<T>(platform: string, device: string, operation
14
14
  // Synchronous checks and native submission share a JS turn with pause updates.
15
15
  export function checkDebuggerGuard(dispatch = false) {
16
16
  const current = context.getStore()
17
- if (!current || current.platform !== 'android') return
18
- if (pauseProvider(current.device)) throw new DebuggerError('DEBUGGER_PAUSED')
17
+ if (!current) return
18
+ if (pauseProvider(current.device, current.platform)) throw new DebuggerError('DEBUGGER_PAUSED')
19
19
  if (dispatch) current.dispatched = true
20
20
  }
21
21
  export function guardDispatched() { return context.getStore()?.dispatched ?? false }
22
- export function pausedReadiness(device: string) {
23
- const paused = pauseProvider(device)
22
+ export function pausedReadiness(device: string, platform = 'android') {
23
+ const paused = pauseProvider(device, platform)
24
24
  if (!paused) return null
25
25
  return { state: 'debugger_paused' as const, scope: 'device_ui' as const, basis: 'observed' as const,
26
26
  captured_at_ms: paused.captured_at_ms, sample_count: 0, stable_for_ms: 0, snapshot_session_id: null, snapshot_revision: null,
@@ -0,0 +1,49 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import { getXcrunCmd } from '../utils/ios/utils.js'
3
+ import { iosExec } from './ios.js'
4
+ import { DebuggerProcess, type WorkerEvent, type DebugWorker } from './worker.js'
5
+ import { DebuggerError } from './diagnostics.js'
6
+ export const iosCapabilities = { attach: true, breakpoints: true, stacks: true, locals: true, step: true, resume: true,
7
+ evaluation: false, mutation: false, coroutines: false, native: false, async_execution_inspection: true, async_stepping: true }
8
+ export const iosLimitations = ['IOS_SIMULATOR_ONLY', 'DEBUG_METADATA_REQUIRED', 'SHALLOW_LOCALS', 'ALL_THREADS_SUSPEND', 'DEVICE_WIDE_UI_GUARD', 'ASYNC_STEPPING_TOOLCHAIN_DEPENDENT', 'AGGREGATE_VALUES_TYPE_ONLY', 'NO_TASK_ENUMERATION']
9
+ export const iosWorkerSource = fileURLToPath(new URL('../../src/debugger/lldb/worker.py', import.meta.url))
10
+ let prepared: Promise<{ python: string; env: NodeJS.ProcessEnv }> | undefined
11
+ export function prepareIOSWorker() {
12
+ prepared ??= (async () => {
13
+ if (process.platform !== 'darwin') throw new DebuggerError('DEBUGGER_UNSUPPORTED')
14
+ const pythonPath = await iosExec(getXcrunCmd(), ['lldb', '-P'])
15
+ const python = await iosExec(getXcrunCmd(), ['--find', 'python3'])
16
+ // Ignore user Python startup/customisation and LLDB init files.
17
+ const env = { PATH: process.env.PATH, HOME: process.env.HOME, PYTHONPATH: pythonPath, PYTHONNOUSERSITE: '1' }
18
+ const ready = await iosExec(python, ['-s', '-B', '-c', 'import lldb; d=lldb.SBDebugger.Create(False); print("ready" if d.IsValid() else "unavailable"); lldb.SBDebugger.Destroy(d)'], env)
19
+ if (ready !== 'ready') throw new DebuggerError('DEBUGGER_UNSUPPORTED')
20
+ return { python, env }
21
+ })().catch(error => { prepared = undefined; throw error })
22
+ return prepared
23
+ }
24
+ export async function createIOSWorker(onEvent: (event: WorkerEvent) => void) {
25
+ const { python, env } = await prepareIOSWorker()
26
+ let released = false
27
+ const worker = new DebuggerProcess(python, '', event => {
28
+ if (event.event === 'terminated' && ['process_exit', 'detached', 'pause_limit', 'idle_limit', 'session_limit'].includes(String(event.reason))) released = true
29
+ onEvent(event)
30
+ }, { args: ['-u', '-s', '-B', iosWorkerSource], env })
31
+ return new IOSWorker(worker, () => released)
32
+ }
33
+ /** A dead worker is not proof that its owned suspension was released. */
34
+ export class IOSWorker implements DebugWorker {
35
+ private detached = false
36
+ constructor(private worker: DebugWorker, private released: () => boolean) {}
37
+ async call(command: string, args: Record<string, unknown> = {}, timeout = 10000) {
38
+ const result = await this.worker.call(command, args, timeout)
39
+ if (command === 'detach' && result.detached === true) this.detached = true
40
+ return result
41
+ }
42
+ async close() {
43
+ if (!this.detached && !this.released()) {
44
+ try { await this.call('detach', {}, 1000) } catch { /* Report uncertainty after bounded transport shutdown. */ }
45
+ }
46
+ await this.worker.close()
47
+ if (!this.detached && !this.released()) throw new DebuggerError('DEBUGGER_CLEANUP_FAILED')
48
+ }
49
+ }