Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- **Attach verification window default raised 5s → 20s** — the `attach_to_process` thread-poll deadline only ever bites when the debug adapter is alive but the target is slow to become debuggable (js-debug child-session adoption on a loaded host, a warming JVM); adapter death still fails fast via the proxy-gone latch, and the poll returns the moment threads appear, so healthy attaches never pay for the headroom. A false "attach failed" on a healthy target is strictly worse for an agent than a slower genuine failure. Pass a small `verifyTimeout` for fast failure-by-design probes

### Fixed
- **A paused session can no longer answer `get_stack_trace` with a silent empty success** — some adapters report a stop before the stack is materialized (netcoredbg right after the post-attach pause — the milder sibling of #353's `0x80131302`), and the tracked thread can be a frameless runtime thread. The agent-facing stack path now retries an empty-but-successful stackTrace within a bounded window (~3s, exits on first frame), falls back to scanning the other stopped threads and adopts the first one with frames (annotated via a `note` in the response, so scopes/evaluate anchor correctly), and — if everything stays frameless — returns the empty result with a `note` saying what to try instead. Not-paused and no-known-thread empty results now carry an explanatory `note` too

### Added
- **CodeLLDB ships as per-platform npm packages (esbuild pattern)** — five new packages `@debugmcp/codelldb-{win32-x64,darwin-x64,darwin-arm64,linux-x64,linux-arm64}` (versioned by the CodeLLDB release, currently 1.11.8, payload staged from the digest-pinned VSIXs) are `optionalDependencies` of `@debugmcp/mcp-debugger`, so npm installs exactly the one matching your os/cpu. Rust and C/C++ debugging now work out of the box on every platform npm serves — previously the CLI tarball bundled linux-x64 only — and the core tarball shrinks from ~54 MB to a few MB. The resolver probes the installed platform package last — after the vendor tree and after `CODELLDB_PATH` — so an explicit `CODELLDB_PATH` still overrides the auto-installed package, and installs with `--omit=optional` keep working via `CODELLDB_PATH` (#383)

Expand Down
10 changes: 7 additions & 3 deletions docs/patterns/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,11 +236,15 @@ target (e.g. a busy or warming JVM) can legitimately need longer than the
default window, the window is caller-configurable (issue #143):

- The default lives in the protected, test-shrinkable field
`attachVerifyTimeoutMs` (5s), following the `stepGraceMs`/`pauseGraceMs`
field pattern.
`attachVerifyTimeoutMs` (20s), following the `stepGraceMs`/`pauseGraceMs`
field pattern. It is deliberately generous: adapter death fails fast
regardless (the proxy-gone latch), so the deadline only bites when the
adapter is alive but the target is slow to report threads — where a false
"attach failed" is worse than a slow genuine failure.
- Callers override it per attach via the `verifyTimeout` (ms) argument on
`attach_to_process` / `create_debug_session`; the value is validated
(positive finite number) and clamped to 10 minutes.
(positive finite number) and clamped to 10 minutes. Pass a small value for
fast failure-by-design probes.
- The failure text comes from `ErrorMessages.attachVerifyFailed(timeoutMs,
lastFailure)`, which names the `verifyTimeout` knob so a caller that hit the
window on a slow target knows how to retry.
Expand Down
2 changes: 1 addition & 1 deletion skills/debugging/references/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ continue_execution {"sessionId": "<id>"} // required with suspend=y to let
```

- **Deferred breakpoints work natively:** for classes not yet loaded, the JDI bridge registers a `ClassPrepareRequest` and binds the breakpoint when the JVM loads the class, then reports `verified: true`. Set breakpoints freely before or after attach — no re-sends needed.
- For a busy or warming JVM, raise `verifyTimeout` (ms) on `attach_to_process` — attach fails if no thread is reported within ~5 s by default. Attach sessions skip host-side file checks, so remote paths are fine.
- For a busy or warming JVM, raise `verifyTimeout` (ms) on `attach_to_process` — attach fails if no thread is reported within ~20 s by default. Attach sessions skip host-side file checks, so remote paths are fine.

## Quirks

Expand Down
2 changes: 1 addition & 1 deletion skills/debugging/references/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ set_breakpoint { "sessionId": "<id>", "file": "/abs/path/server.js", "line
continue_execution { "sessionId": "<id>" }
```

Shorthand: `create_debug_session { "language": "javascript", "host": "127.0.0.1", "port": 9229 }` attaches in one call. Attach is verified by polling for threads (`verifyTimeout`, default ~5000 ms). Detach with `detach_from_process { "sessionId": "<id>", "terminateProcess": false }`; remote debugging beyond a reachable host/port requires manual configuration (e.g. an SSH tunnel).
Shorthand: `create_debug_session { "language": "javascript", "host": "127.0.0.1", "port": 9229 }` attaches in one call. Attach is verified by polling for threads (`verifyTimeout`, default ~20000 ms). Detach with `detach_from_process { "sessionId": "<id>", "terminateProcess": false }`; remote debugging beyond a reachable host/port requires manual configuration (e.g. an SSH tunnel).

## Quirks

Expand Down
2 changes: 1 addition & 1 deletion skills/debugging/references/python.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ set_breakpoint { "sessionId": "<id>", "file": "/abs/path/script.py", "line
continue_execution { "sessionId": "<id>" }
```

Shorthand: `create_debug_session { "language": "python", "host": "127.0.0.1", "port": 5678 }` creates the session and attaches in one call. After the handshake the attach is verified by polling for threads; raise `verifyTimeout` (default ~5000 ms) for slow targets. Detach with `detach_from_process { "sessionId": "<id>", "terminateProcess": false }`.
Shorthand: `create_debug_session { "language": "python", "host": "127.0.0.1", "port": 5678 }` creates the session and attaches in one call. After the handshake the attach is verified by polling for threads; raise `verifyTimeout` (default ~20000 ms) for slow targets. Detach with `detach_from_process { "sessionId": "<id>", "terminateProcess": false }`.

## Quirks

Expand Down
28 changes: 20 additions & 8 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,11 @@ export class DebugMcpServer {
if (typeof currentThreadId !== 'number') {
throw new ProxyNotRunningError(sessionId || 'unknown', 'get stack trace');
}
return this.sessionManager.getStackTraceDetailed(sessionId, currentThreadId, includeInternals);
// ensureStackReady: the thread above was resolved implicitly (the MCP tool
// has no threadId argument), so a paused session answering with zero
// frames gets the bounded readiness retry + thread scan instead of a
// confusing empty success.
return this.sessionManager.getStackTraceDetailed(sessionId, currentThreadId, includeInternals, { ensureStackReady: true });
}

public async getScopes(sessionId: string, frameId: number): Promise<DebugProtocol.Scope[]> {
Expand Down Expand Up @@ -1080,7 +1084,7 @@ export class DebugMcpServer {

return {
tools: [
{ name: 'create_debug_session', description: 'Create a new debugging session. Provide host and port to attach to a running process; omit them for launch mode', inputSchema: { type: 'object', properties: { language: { type: 'string', enum: supportedLanguages, description: 'Programming language for debugging' }, name: { type: 'string', description: 'Optional session name' }, executablePath: {type: 'string', description: 'Path to language executable (optional, will auto-detect if not provided)'}, host: { type: 'string', description: 'Host to attach to for remote debugging (optional, triggers attach mode)' }, port: { type: 'number', description: 'Debug port to attach to for remote debugging (optional, triggers attach mode)' }, timeout: { type: 'number', description: 'Connection timeout in milliseconds for attach mode (default: 30000)' }, verifyTimeout: { type: 'number', description: 'Attach mode only: how long to wait (ms) for the debugger to report at least one thread after attaching before failing the attach (default: 5000, max: 600000)' }, adapterConfig: { type: 'object', description: 'Attach mode only: adapter-specific attach configuration merged into the attach config (see attach_to_process)', additionalProperties: true } }, required: ['language'] } },
{ name: 'create_debug_session', description: 'Create a new debugging session. Provide host and port to attach to a running process; omit them for launch mode', inputSchema: { type: 'object', properties: { language: { type: 'string', enum: supportedLanguages, description: 'Programming language for debugging' }, name: { type: 'string', description: 'Optional session name' }, executablePath: {type: 'string', description: 'Path to language executable (optional, will auto-detect if not provided)'}, host: { type: 'string', description: 'Host to attach to for remote debugging (optional, triggers attach mode)' }, port: { type: 'number', description: 'Debug port to attach to for remote debugging (optional, triggers attach mode)' }, timeout: { type: 'number', description: 'Connection timeout in milliseconds for attach mode (default: 30000)' }, verifyTimeout: { type: 'number', description: 'Attach mode only: how long to wait (ms) for the debugger to report at least one thread after attaching before failing the attach (default: 20000, max: 600000)' }, adapterConfig: { type: 'object', description: 'Attach mode only: adapter-specific attach configuration merged into the attach config (see attach_to_process)', additionalProperties: true } }, required: ['language'] } },
{ name: 'list_supported_languages', description: 'List all supported debugging languages with metadata', inputSchema: { type: 'object', properties: {} } },
{ name: 'list_debug_sessions', description: 'List all active debugging sessions. Paused sessions include lastStop with the reason for the most recent stop (e.g. "breakpoint" vs "exception")', inputSchema: { type: 'object', properties: {} } },
{ name: 'set_breakpoint', description: 'Set a breakpoint. Setting breakpoints on non-executable lines (structural, declarative) may lead to unexpected behavior', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' }, file: { type: 'string', description: 'Path to the source file or Java FQCN. For Java, passing a fully-qualified class name (e.g. "com.example.MyClass" or "com.example.Outer$Inner") is preferred — it works reliably with all classloaders including custom classloaders. Alternatively, use absolute file paths.' }, line: { type: 'number', description: 'Line number where to set breakpoint. Executable statements (assignments, function calls, conditionals, returns) work best. Structural lines (function/class definitions), declarative lines (imports), or non-executable lines (comments, blank lines) may cause unexpected stepping behavior' }, ...setBreakpointExtraProps, condition: { type: 'string', description: 'Optional expression: only break (or log) when it evaluates truthy' }, logMessage: { type: 'string', description: 'Create a logpoint: instead of pausing, log this message when the line is hit. Expressions in {curly braces} are interpolated (e.g. "order={orderId} total={total}"). Messages arrive in get_output while the program runs at full speed. Supported by Python, JavaScript, Go, and Rust adapters; not by Java or .NET' }, suspendPolicy: { type: 'string', enum: ['all', 'thread'], description: 'Suspend policy when breakpoint is hit: "all" suspends all threads (default), "thread" only suspends the event thread. Only supported by the Java/JDI adapter.' } }, required: setBreakpointRequired } },
Expand Down Expand Up @@ -1113,15 +1117,15 @@ export class DebugMcpServer {
}
},
{ name: 'restart_debugging', description: 'Restart the debuggee: terminate the current program (if still running) and relaunch it with the same configuration as the last start_debugging. All current breakpoints are re-applied automatically. The get_output buffer starts fresh — read from since=0 after a restart. Works while running, paused, or after the program has exited. Not available for attach sessions or sessions never launched', inputSchema: { type: 'object', properties: { sessionId: { type: 'string' } }, required: ['sessionId'] } },
{ name: 'attach_to_process', description: 'Attach to a running process for debugging. After the attach handshake the target is verified by polling for threads; if none are reported within verifyTimeout (~5s default) the attach fails and the debug proxy is torn down', inputSchema: {
{ name: 'attach_to_process', description: 'Attach to a running process for debugging. After the attach handshake the target is verified by polling for threads; if none are reported within verifyTimeout (~20s default) the attach fails and the debug proxy is torn down', inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
port: { type: 'number', description: 'Debug port to attach to' },
host: { type: 'string', description: 'Host to attach to (default: localhost)' },
processId: { type: ['number', 'string'], description: 'Process ID (for local attach, language-specific)' },
timeout: { type: 'number', description: 'Connection timeout in milliseconds (default: 30000)' },
verifyTimeout: { type: 'number', description: 'How long to wait (ms) for the debugger to report at least one thread after attaching before failing the attach (default: 5000, max: 600000). Increase for targets that are slow to become debuggable, e.g. a busy or warming JVM' },
verifyTimeout: { type: 'number', description: 'How long to wait (ms) for the debugger to report at least one thread after attaching before failing the attach (default: 20000, max: 600000). Decrease for fast failure-by-design probes; increase for targets that are exceptionally slow to become debuggable' },
sourcePaths: { type: 'array', items: { type: 'string' }, description: 'Source paths for code mapping' },
stopOnEntry: { type: 'boolean', description: 'Stop on entry after attaching' },
justMyCode: { type: 'boolean', description: 'Only debug user code (skip library code)' },
Expand Down Expand Up @@ -2055,13 +2059,21 @@ export class DebugMcpServer {
stopReason: lastStop?.reason,
lastStop
};
// Issue #346: when the language policy hid frames, say so in the
// response instead of relying on the agent knowing filtering exists.
// Anything the result needs explaining (not paused, stack came
// from a different thread, all threads frameless) plus the
// issue #346 hidden-frames disclosure share the note field.
const notes: string[] = [];
if (stackTrace.note) {
notes.push(stackTrace.note);
}
if (stackTrace.hiddenFrameCount > 0) {
payload.hiddenFrames = stackTrace.hiddenFrameCount;
payload.note = stackTrace.allFramesInternal
notes.push(stackTrace.allFramesInternal
? `All ${stackTrace.totalFrameCount} frames are internal/runtime frames; showing the top internal frame so scopes and evaluate still work. Pass includeInternals: true to see the full stack.`
: `${stackTrace.hiddenFrameCount} internal frame(s) hidden — pass includeInternals: true to see them.`;
: `${stackTrace.hiddenFrameCount} internal frame(s) hidden — pass includeInternals: true to see them.`);
}
if (notes.length > 0) {
payload.note = notes.join(' ');
}
result = { content: [{ type: 'text', text: JSON.stringify(payload) }] };
} catch (error) {
Expand Down
Loading
Loading