Skip to content

Commit 3b5ea1f

Browse files
committed
Add debug continue and auto-resume on attach
1 parent 38cdd18 commit 3b5ea1f

13 files changed

Lines changed: 338 additions & 179 deletions

File tree

.smithery/index.cjs

Lines changed: 166 additions & 164 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Changelog
22

3+
## [Unreleased]
4+
### Added
5+
- Add Smithery support for packaging/distribution.
6+
- Add DAP-based debugger backend and simulator debugging toolset (attach, breakpoints, stack, variables, LLDB command).
7+
- Add session-status MCP resource with session identifiers.
8+
- Add UI automation guard that blocks UI tools when the debugger is paused.
9+
10+
### Changed
11+
- Migrate to Zod v4.
12+
- Improve session default handling (reconcile mutual exclusivity and ignore explicit undefined clears).
13+
14+
### Fixed
15+
- Update UI automation guard guidance to point at `debug_continue` when paused.
16+
- Fix tool loading bugs in static tool registration.
17+
318
## [1.16.0] - 2025-12-30
419
- Remove dynamic tool discovery (`discover_tools`) and `XCODEBUILDMCP_DYNAMIC_TOOLS`. Use `XCODEBUILDMCP_ENABLED_WORKFLOWS` to limit startup tool registration.
520
- Add MCP tool annotations to all tools.

docs/TOOLS.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# XcodeBuildMCP Tools Reference
22

3-
XcodeBuildMCP provides 70 tools organized into 13 workflow groups for comprehensive Apple development workflows.
3+
XcodeBuildMCP provides 71 tools organized into 13 workflow groups for comprehensive Apple development workflows.
44

55
## Workflow Groups
66

@@ -69,11 +69,12 @@ XcodeBuildMCP provides 70 tools organized into 13 workflow groups for comprehens
6969
- `session_set_defaults` - Set the session defaults needed by many tools. Most tools require one or more session defaults to be set before they can be used. Agents should set all relevant defaults up front in a single call (e.g., project/workspace, scheme, simulator or device ID, useLatestOS) to avoid iterative prompts; only set the keys your workflow needs.
7070
- `session_show_defaults` - Show current session defaults.
7171
### Simulator Debugging (`debugging`)
72-
**Purpose**: Interactive iOS Simulator debugging tools: attach LLDB, manage breakpoints, inspect stack/variables, and run LLDB commands. (7 tools)
72+
**Purpose**: Interactive iOS Simulator debugging tools: attach LLDB, manage breakpoints, inspect stack/variables, and run LLDB commands. (8 tools)
7373

7474
- `debug_attach_sim` - Attach LLDB to a running iOS simulator app. Provide bundleId or pid, plus simulator defaults.
7575
- `debug_breakpoint_add` - Add a breakpoint by file/line or function name for the active debug session.
7676
- `debug_breakpoint_remove` - Remove a breakpoint by id for the active debug session.
77+
- `debug_continue` - Resume execution in the active debug session or a specific debugSessionId.
7778
- `debug_detach` - Detach the current debugger session or a specific debugSessionId.
7879
- `debug_lldb_command` - Run an arbitrary LLDB command within the active debug session.
7980
- `debug_stack` - Return a thread backtrace from the active debug session.
@@ -116,9 +117,9 @@ XcodeBuildMCP provides 70 tools organized into 13 workflow groups for comprehens
116117

117118
## Summary Statistics
118119

119-
- **Total Tools**: 70 canonical tools + 22 re-exports = 92 total
120+
- **Total Tools**: 71 canonical tools + 22 re-exports = 93 total
120121
- **Workflow Groups**: 13
121122

122123
---
123124

124-
*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2026-01-03*
125+
*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2026-01-04*
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Investigation: Debugger attaches in stopped state after launch
2+
3+
## Summary
4+
Reproduced: attaching the debugger leaves the simulator app in a stopped state. UI automation is blocked by the guard because the debugger reports `state=stopped`. The attach flow does not issue any resume/continue, so the process remains paused after attach.
5+
6+
## Symptoms
7+
- After attaching debugger to Calculator, UI automation taps fail because the app is paused.
8+
- UI guard blocks with `state=stopped` immediately after attach.
9+
10+
## Investigation Log
11+
12+
### 2025-02-14 - Repro (CalculatorApp on iPhone 17 simulator)
13+
**Hypothesis:** Attach leaves the process stopped, which triggers the UI automation guard.
14+
**Findings:** `debug_attach_sim` attached to a running CalculatorApp (DAP backend), then `tap` was blocked with `state=stopped`.
15+
**Evidence:** `tap` returned "UI automation blocked: app is paused in debugger" with `state=stopped` and the current debug session ID.
16+
**Conclusion:** Confirmed.
17+
18+
### 2025-02-14 - Code Review (attach flow)
19+
**Hypothesis:** The attach implementation does not resume the process.
20+
**Findings:** The attach flow never calls any resume/continue primitive.
21+
- `debug_attach_sim` creates a session and returns without resuming.
22+
- DAP backend attach flow (`initialize -> attach -> configurationDone`) has no `continue`.
23+
- LLDB CLI backend uses `process attach --pid` and never `process continue`.
24+
- UI automation guard blocks when state is `stopped`.
25+
**Evidence:** `src/mcp/tools/debugging/debug_attach_sim.ts`, `src/utils/debugger/backends/dap-backend.ts`, `src/utils/debugger/backends/lldb-cli-backend.ts`, `src/utils/debugger/ui-automation-guard.ts`.
26+
**Conclusion:** Confirmed. Stopped state originates from debugger attach semantics, and the tool never resumes.
27+
28+
## Root Cause
29+
The debugger attach path halts the target process (standard debugger behavior) and there is no subsequent resume/continue step. This leaves the process in `stopped` state, which causes `guardUiAutomationAgainstStoppedDebugger` to block UI tools like `tap`.
30+
31+
## Recommendations
32+
1. Add a first-class `debug_continue` tool backed by a backend-level `continue()` API to resume without relying on LLDB command evaluation.
33+
2. Add an optional `continueOnAttach` (or `stopOnAttach`) parameter to `debug_attach_sim`, with a default suited for UI automation workflows.
34+
3. Update guard messaging to recommend `debug_continue` (not `debug_lldb_command continue`, which is unreliable on DAP).
35+
36+
## Preventive Measures
37+
- Document that UI tools require the target process to be running, and that debugger attach may pause execution by default.
38+
- Add a state check or auto-resume option when attaching in automation contexts.

src/core/generated-plugins.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,24 @@ export const WORKFLOW_LOADERS = {
1414
const tool_2 = await import('../mcp/tools/debugging/debug_breakpoint_remove.ts').then(
1515
(m) => m.default,
1616
);
17-
const tool_3 = await import('../mcp/tools/debugging/debug_detach.ts').then((m) => m.default);
18-
const tool_4 = await import('../mcp/tools/debugging/debug_lldb_command.ts').then(
17+
const tool_3 = await import('../mcp/tools/debugging/debug_continue.ts').then((m) => m.default);
18+
const tool_4 = await import('../mcp/tools/debugging/debug_detach.ts').then((m) => m.default);
19+
const tool_5 = await import('../mcp/tools/debugging/debug_lldb_command.ts').then(
1920
(m) => m.default,
2021
);
21-
const tool_5 = await import('../mcp/tools/debugging/debug_stack.ts').then((m) => m.default);
22-
const tool_6 = await import('../mcp/tools/debugging/debug_variables.ts').then((m) => m.default);
22+
const tool_6 = await import('../mcp/tools/debugging/debug_stack.ts').then((m) => m.default);
23+
const tool_7 = await import('../mcp/tools/debugging/debug_variables.ts').then((m) => m.default);
2324

2425
return {
2526
workflow,
2627
debug_attach_sim: tool_0,
2728
debug_breakpoint_add: tool_1,
2829
debug_breakpoint_remove: tool_2,
29-
debug_detach: tool_3,
30-
debug_lldb_command: tool_4,
31-
debug_stack: tool_5,
32-
debug_variables: tool_6,
30+
debug_continue: tool_3,
31+
debug_detach: tool_4,
32+
debug_lldb_command: tool_5,
33+
debug_stack: tool_6,
34+
debug_variables: tool_7,
3335
};
3436
},
3537
device: async () => {

src/mcp/tools/debugging/debug_attach_sim.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ const baseSchemaObject = z.object({
3333
.describe("Bundle identifier of the app to attach (e.g., 'com.example.MyApp')"),
3434
pid: z.number().int().positive().optional().describe('Process ID to attach directly'),
3535
waitFor: z.boolean().optional().describe('Wait for the process to appear when attaching'),
36+
continueOnAttach: z
37+
.boolean()
38+
.optional()
39+
.default(true)
40+
.describe('Resume execution automatically after attaching (default: true)'),
3641
makeCurrent: z
3742
.boolean()
3843
.optional()
@@ -109,20 +114,39 @@ export async function debug_attach_simLogic(
109114
debuggerManager.setCurrentSession(session.id);
110115
}
111116

117+
const shouldContinue = params.continueOnAttach ?? true;
118+
if (shouldContinue) {
119+
try {
120+
await debuggerManager.resumeSession(session.id);
121+
} catch (error) {
122+
const message = error instanceof Error ? error.message : String(error);
123+
try {
124+
await debuggerManager.detachSession(session.id);
125+
} catch {
126+
// Best-effort cleanup; keep original resume error.
127+
}
128+
return createErrorResponse('Failed to resume debugger after attach', message);
129+
}
130+
}
131+
112132
const warningText = simResult.warning ? `⚠️ ${simResult.warning}\n\n` : '';
113133
const currentText = isCurrent
114134
? 'This session is now the current debug session.'
115135
: 'This session is not set as the current session.';
136+
const resumeText = shouldContinue
137+
? 'Execution resumed after attach.'
138+
: 'Execution is paused. Use debug_continue to resume before UI automation.';
116139

117140
const backendLabel = session.backend === 'dap' ? 'DAP debugger' : 'LLDB';
118141

119142
return createTextResponse(
120143
`${warningText}✅ Attached ${backendLabel} to simulator process ${pid} (${simulatorId}).\n\n` +
121144
`Debug session ID: ${session.id}\n` +
122-
`${currentText}\n\n` +
145+
`${currentText}\n` +
146+
`${resumeText}\n\n` +
123147
`Next steps:\n` +
124148
`1. debug_breakpoint_add({ debugSessionId: "${session.id}", file: "...", line: 123 })\n` +
125-
`2. debug_lldb_command({ debugSessionId: "${session.id}", command: "continue" })\n` +
149+
`2. debug_continue({ debugSessionId: "${session.id}" })\n` +
126150
`3. debug_stack({ debugSessionId: "${session.id}" })`,
127151
);
128152
} catch (error) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as z from 'zod';
2+
import { ToolResponse } from '../../../types/common.ts';
3+
import { createErrorResponse, createTextResponse } from '../../../utils/responses/index.ts';
4+
import { createTypedToolWithContext } from '../../../utils/typed-tool-factory.ts';
5+
import {
6+
getDefaultDebuggerToolContext,
7+
type DebuggerToolContext,
8+
} from '../../../utils/debugger/index.ts';
9+
10+
const debugContinueSchema = z.object({
11+
debugSessionId: z
12+
.string()
13+
.optional()
14+
.describe('Debug session ID to resume (defaults to current session)'),
15+
});
16+
17+
export type DebugContinueParams = z.infer<typeof debugContinueSchema>;
18+
19+
export async function debug_continueLogic(
20+
params: DebugContinueParams,
21+
ctx: DebuggerToolContext,
22+
): Promise<ToolResponse> {
23+
try {
24+
const targetId = params.debugSessionId ?? ctx.debugger.getCurrentSessionId();
25+
await ctx.debugger.resumeSession(targetId ?? undefined);
26+
27+
return createTextResponse(`✅ Resumed debugger session${targetId ? ` ${targetId}` : ''}.`);
28+
} catch (error) {
29+
const message = error instanceof Error ? error.message : String(error);
30+
return createErrorResponse('Failed to resume debugger', message);
31+
}
32+
}
33+
34+
export default {
35+
name: 'debug_continue',
36+
description: 'Resume execution in the active debug session or a specific debugSessionId.',
37+
schema: debugContinueSchema.shape,
38+
handler: createTypedToolWithContext<DebugContinueParams, DebuggerToolContext>(
39+
debugContinueSchema,
40+
debug_continueLogic,
41+
getDefaultDebuggerToolContext,
42+
),
43+
};

src/utils/debugger/__tests__/debugger-manager-dap.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function createBackend(overrides: Partial<DebuggerBackend> = {}): DebuggerBacken
1010
attach: async () => {},
1111
detach: async () => {},
1212
runCommand: async () => '',
13+
resume: async () => {},
1314
addBreakpoint: async (spec: BreakpointSpec): Promise<BreakpointInfo> => ({
1415
id: 1,
1516
spec,

src/utils/debugger/backends/DebuggerBackend.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface DebuggerBackend {
77
detach(): Promise<void>;
88

99
runCommand(command: string, opts?: { timeoutMs?: number }): Promise<string>;
10+
resume(opts?: { threadId?: number }): Promise<void>;
1011

1112
addBreakpoint(spec: BreakpointSpec, opts?: { condition?: string }): Promise<BreakpointInfo>;
1213
removeBreakpoint(id: number): Promise<string>;

src/utils/debugger/backends/dap-backend.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,22 @@ class DapBackend implements DebuggerBackend {
152152
}
153153
}
154154

155+
async resume(opts?: { threadId?: number }): Promise<void> {
156+
return this.enqueue(async () => {
157+
this.ensureAttached();
158+
159+
let threadId = opts?.threadId;
160+
if (!threadId) {
161+
const thread = await this.resolveThread();
162+
threadId = thread.id;
163+
}
164+
165+
await this.request('continue', { threadId });
166+
this.executionState = { status: 'running' };
167+
this.lastStoppedThreadId = null;
168+
});
169+
}
170+
155171
async addBreakpoint(
156172
spec: BreakpointSpec,
157173
opts?: { condition?: string },

0 commit comments

Comments
 (0)