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
29 changes: 29 additions & 0 deletions .github/workflows/canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,22 @@ jobs:
--transcript "$TRANSCRIPTS/python.jsonl" \
-- node "$CLI" stdio

- name: Smoke cycle (python, protocol 2026-07-28)
# Every other cycle opens with `initialize`: the 2025 era, which stays
# covered. From 0.27.0 the bundle serves 2026-07-28 too (issue #1025):
# the same cycle, opened with server/discover, each request carrying
# its own envelope. The driver decides from the installed package's
# own version — skipped with a notice before 0.27.0, strict from it,
# prereleases included (scripts/canary-era-gate.mjs).
run: |
ARTIFACT_VERSION="$(node -p "require(process.argv[1]).version" "$(npm root -g)/@debugmcp/mcp-debugger/package.json")"
echo "ARTIFACT_VERSION=$ARTIFACT_VERSION" >> "$GITHUB_ENV"
node scripts/canary-smoke.mjs --lang python --era 2026-07-28 --artifact-version "$ARTIFACT_VERSION" \
--program "$GITHUB_WORKSPACE/examples/python/simple_test.py" --line 11 \
--expect-line --expect-var a --expect-var b \
--transcript "$TRANSCRIPTS/python-2026.jsonl" \
-- node "$CLI" stdio

- name: Smoke cycle (rust — CodeLLDB engine proof)
env:
LEG: ${{ matrix.leg }}
Expand Down Expand Up @@ -424,6 +440,19 @@ jobs:
--transcript "$TRANSCRIPTS/python.jsonl" \
-- docker run -i --rm -v "$GITHUB_WORKSPACE:/workspace" "$IMAGE" stdio

- name: Smoke cycle (python, protocol 2026-07-28)
# Read the version from the pulled image's synchronized shared package.
# npm's latest tag can move before Docker's during a release; querying
# npm here can apply the new era's requirements to an older image.
run: |
ARTIFACT_VERSION="$(docker run --rm --entrypoint node "$IMAGE" -p "require('/app/node_modules/@debugmcp/shared/package.json').version")"
echo "ARTIFACT_VERSION=$ARTIFACT_VERSION" >> "$GITHUB_ENV"
node scripts/canary-smoke.mjs --lang python --era 2026-07-28 --artifact-version "$ARTIFACT_VERSION" \
--program examples/python/simple_test.py --line 11 \
--expect-line --expect-var a --expect-var b \
--transcript "$TRANSCRIPTS/python-2026.jsonl" \
-- docker run -i --rm -v "$GITHUB_WORKSPACE:/workspace" "$IMAGE" stdio

- name: Smoke cycle (rust — CodeLLDB engine proof)
# The image ships no compiler; build the fixture in a rust container
# against the same /workspace mount so the DWARF paths match what the
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ The dev proxy (`tools/dev-proxy/dev-proxy.mjs`) is a lightweight MCP proxy that
- `DEV_PROXY_BUILD_TIMEOUT_MS` — Build timeout in milliseconds (default: 120000)
- `DEV_PROXY_ROOT` — Project root (default: auto-detected)
- `DEV_PROXY_BACKEND_TRANSPORT` — `http` (default) or `stdio`; any other value refuses to start, naming the variable
- `DEV_PROXY_BACKEND_ERA` — `auto` (default: negotiate the era with the backend) or `legacy` (speak 2025-11-25 to it — the way to exercise the 2025 era through the proxy, which otherwise lands on 2026-07-28 with every current backend); any other value refuses to start, naming the variable
- `DEV_PROXY_BACKEND_CMD` — Custom backend command override (e.g. `docker run ...`)
- `DEV_PROXY_DISCOVERY_WAIT_MS` — How long a request waits for a backend start or restart to settle (default: 15000; keep it under the client's MCP startup timeout, `0` disables the wait)

Expand Down
1 change: 1 addition & 0 deletions changelog.d/1025-dev-proxy-era.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**`DEV_PROXY_BACKEND_ERA` chooses the protocol era the dev proxy speaks to its backend** — the proxy negotiates by default (`auto`), so against a current backend it always lands on 2026-07-28 and nothing driven through it exercises the 2025 era that most clients still speak. `DEV_PROXY_BACKEND_ERA=legacy` opens with `initialize` instead (2025-11-25); `dev_server_status` shows the era in force, and any other value refuses to start, naming the variable (#1025)
2 changes: 1 addition & 1 deletion changelog.d/1025-idle-policy.added.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
**An opt-in per-debug-session idle policy; nothing expires by default.** A debug session ends by its own close, its program ending, or the server exiting, on every transport — no timer ends one unless the operator asks. With `MCP_DEBUG_SESSION_IDLE_MS` set, a debug-session tree that no tool call has named for that long is closed through the normal close cascade — a launch root closes its subtree and ends its program, an attach root releases its targets — unless a client is attending the process (an open SSE stream), in which case nothing expires; a tool call naming any session in a tree refreshes the whole tree, an in-flight `wait_for_stop` holds it, and children never expire on a clock of their own. The sweep runs every `MCP_DEBUG_SESSION_IDLE_SWEEP_MS` (default 60 s). The agent can see it: `list_debug_sessions` rows carry `idleMs` (and `expiresInMs` when a policy is on), a tool call naming a session the policy closed answers what happened — when, under which window, and whether its program was ended or released (`closedBy: "idle-policy"`) — instead of "Session not found", and the server instructions mention the policy when it is on. The HTTP reapers (`MCP_HTTP_STALE_SESSION_MS`, `MCP_HTTP_STREAM_LOST_SESSION_MS`) still reap abandoned MCP sessions, but reaping one no longer closes any debug session: **a crashed client's debug sessions stay until someone closes them, the idle window (if set) runs out, or the server exits.** Set the variable on a shared or long-lived `http` server, and wherever an attached process must not stay paused (the Kubernetes recipe sets 5 minutes). Step 2 of #1025.
**An opt-in per-debug-session idle policy; nothing expires by default.** A debug session ends by its own close, its program ending, or the server exiting, on every transport — no timer ends one unless the operator asks. With `MCP_DEBUG_SESSION_IDLE_MS` set, a debug-session tree that no tool call has named for that long is closed through the normal close cascade — a launch root closes its subtree and ends its program, an attach root releases its targets — unless a client is attending the process (an open SSE stream), in which case nothing expires; a tool call naming any session in a tree refreshes the whole tree, an in-flight `wait_for_stop` holds it, and children never expire on a clock of their own. The sweep runs every `MCP_DEBUG_SESSION_IDLE_SWEEP_MS` (default 60 s). The agent can see it: `list_debug_sessions` rows carry `idleMs` (and `expiresInMs` when a policy is on), a tool call naming a session the policy closed answers what happened — when, under which window, and whether its program was ended, released, or was not running by then (`closedBy: "idle-policy"`) — instead of "Session not found", and the server instructions mention the policy when it is on. The HTTP reapers (`MCP_HTTP_STALE_SESSION_MS`, `MCP_HTTP_STREAM_LOST_SESSION_MS`) still reap abandoned MCP sessions, but reaping one no longer closes any debug session: **a crashed client's debug sessions stay until someone closes them, the idle window (if set) runs out, or the server exits.** Set the variable on a shared or long-lived `http` server, and wherever an attached process must not stay paused (the Kubernetes recipe sets 5 minutes). Step 2 of #1025.
1 change: 1 addition & 0 deletions changelog.d/1025-restart-description.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**The `restart_debugging` description no longer says attach sessions can never restart** — a Flutter session hot-restarts in place, an attached one included (same process, output buffer kept, `hotRestart: true` in the answer), but the tool description every client reads still said "Not available for attach sessions". It now states the Flutter path and that attach sessions of the other adapters are the ones refused (#1025)
1 change: 1 addition & 0 deletions changelog.d/1025-setting-warnings.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**A rejected `MCP_HTTP_*` setting is named in the log; its value is not** — an invalid `MCP_HTTP_STALE_SESSION_MS`, `MCP_HTTP_STREAM_LOST_SESSION_MS` or `MCP_HTTP_STALE_SWEEP_INTERVAL_MS` used to be quoted in the warning that rejects it, so a secret pasted into the wrong variable, or a value with a line break in it, was copied into the server log. The warning now names the variable and the default in force, as the debug-session idle settings already do (#1025)
7 changes: 4 additions & 3 deletions docs/architecture/session-tree.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ default, the attach measurement, the lifecycle suites) in
program's own is a child session, the throwaway release, `outputOn`, a child's close as a release,
the fork/grandchild suite and the parent-exit measurement below) merged in
[#1094](https://github.com/debugmcp/mcp-debugger/pull/1094), PR 2 (the launch and verified-local
attach defaults on, the attach, cascade and cluster suites) follows it.
attach defaults on, the attach, cascade and cluster suites) in
[#1096](https://github.com/debugmcp/mcp-debugger/pull/1096).
Scope: debuggee children reported by the adapter (debugpy subprocesses, js-debug forks and, later,
browser targets). The tree is additive to the shipped tool surface: no new tool, no changed
parameter, new fields only.
Expand Down Expand Up @@ -292,8 +293,8 @@ fold children into the one session, or ignore them. Surveyed 2026-11-03:
| python | debugpy 1.8.20 | `startDebugging` when advertised, else a `debugpyAttach` event with the same body; child parked until a client attaches | reject the request: the adapter unblocks the child itself, 100 ms | own lines on the child connection with `redirectOutput`, plus the parent's pipe copy | none (a parked child cannot be attached by the agent: no port is reported to it) | [spike notes](../python/spike-notes-subprocess.md), measured |
| javascript | js-debug 1.140.0 | one `startDebugging { __pendingTargetId }` per process, incl. the program itself; child parked in `waitForDebugger` | a throwaway attach-then-detach ([#501](https://github.com/debugmcp/mcp-debugger/issues/501)); rejecting parks the child forever | tree-wide on the root connection, no attribution | `attach_to_process` by pid or inspector port, which is a new root, not a child | [spike notes](../javascript/spike-notes-forks.md), measured |
| javascript (browser) | js-debug `pwa-chrome` | the same `startDebugging`, one per page, frame and worker | as above | unmeasured | none | [#989](https://github.com/debugmcp/mcp-debugger/issues/989), unmeasured |
| ruby | debug gem 1.11.0 (rdbg) | **out of band**: with `fork_mode: both` (the default) the forked child runs `SESSION.activate on_fork: true` and opens its own debug server — the same TCP port with `RUBY_DEBUG_PORT_RANGE` unset (ten retries, then it raises), the next free port in the range otherwise — announced only by a `Debugger can attach via TCP/IP (host:port)` line on the child's stderr; nothing on the parent's DAP connection | not needed: the child is not parked | the child's stderr reaches the launch session through the inherited pipe | `attach_to_process` by port against the child's own server (the adapter already connects to `rdbg --open` sockets directly), a new root | debug gem source read 2026-11-03: `lib/debug/session.rb` `__fork_setup_for_debugger`, `lib/debug/server.rb` `UI_TcpServer#accept`; Ruby on Windows has no `fork`, so a measurement needs Linux |
| rust, cpp, cobol | CodeLLDB / LLDB 22.1.8 | **follow**: `target.process.follow-fork-mode` is `parent` or `child` — the debugger keeps one process and releases the other; `stop-on-exec` is `true`; Linux and FreeBSD only; never a report, never a second session | not applicable | not applicable | cpp and cobol: `attach_to_process` by pid, a new root; rust: no attach mode | setting values read from the vendored `lldb.exe` 2026-11-03 (`follow-fork-mode = parent`); [LLVM D100503](https://reviews.llvm.org/D100503) |
| ruby | debug gem 1.11.0 (rdbg) | **out of band**: with `fork_mode: both` (the default) the forked child runs `SESSION.activate on_fork: true` and opens its own debug server — the same TCP port with `RUBY_DEBUG_PORT_RANGE` unset (ten retries, then it raises), the next free port in the range otherwise — announced only by a `Debugger can attach via TCP/IP (host:port)` line on the child's stderr; nothing on the parent's DAP connection. A second reading of `server.rb` (2026-11-11) disagrees with that last clause: the TCP server's `accept` calls `super` first, which serves the inherited, already-connected socket (`@sock_for_fork`, `already_connected: true`) before it opens a listener of its own. Unmeasured either way; the [#1085](https://github.com/debugmcp/mcp-debugger/issues/1085) spike settles it | not needed: the child is not parked | the child's stderr reaches the launch session through the inherited pipe | `attach_to_process` by port against the child's own server (the adapter already connects to `rdbg --open` sockets directly), a new root | debug gem source read 2026-11-03: `lib/debug/session.rb` `__fork_setup_for_debugger`, `lib/debug/server.rb` `UI_TcpServer#accept`; Ruby on Windows has no `fork`, so a measurement needs Linux |
| rust, cpp, cobol | CodeLLDB / LLDB 22.1.8 | **follow**: `target.process.follow-fork-mode` is `parent` or `child` — the debugger keeps one process and releases the other; `stop-on-exec` is `true`; Linux and FreeBSD only; never an automatic report. Since CodeLLDB 1.12 the scripting API has `codelldb.start_debugging(request, configuration)` (the vendored `adapter/scripts/codelldb/api.py`), which makes the adapter send a `startDebugging` reverse request: a script-driven second session, unmeasured here | not applicable | not applicable | cpp and cobol: `attach_to_process` by pid, a new root; rust: no attach mode | setting values read from the vendored `lldb.exe` 2026-11-03 (`follow-fork-mode = parent`); [LLVM D100503](https://reviews.llvm.org/D100503) |
| go | Delve (CI pins 1.24.2 as the oldest supported, 1.27.2 current) | **follow and fold**: `target follow-exec [-on [regex]]` attaches to child processes the target execs, `target list` / `target switch` move between them inside the one session; DAP `launch` takes `followExec` / `followExecRegex` since 1.26.0 (attach: "not supported yet"); one `process` event, one goroutine list; Linux first (1.21.0), Windows follow since 1.21.2 | not applicable | not applicable | no attach mode | Delve CHANGELOG 1.21.0 / 1.21.2 / 1.26.0 / 1.27.1, `Documentation/cli/README.md` (`target`), `service/dap/server.go` (`followExec`), read 2026-11-03; how a second target is presented over DAP is unmeasured |
| java | the JDI bridge (`JdiDapServer.java`) | none: a child JVM is a new process that needs its own JDWP agent; JDI has no process following | not applicable | not applicable | `attach_to_process` by JDWP port, a new root | our own bridge; no `startDebugging`, no child handling in it |
| dotnet | netcoredbg | none documented | not applicable | not applicable | `attach_to_process` by pid, a new root | netcoredbg README (attach by pid only; nothing on child processes), read 2026-11-03 |
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture/testing-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ Two tiers, split along the seam between this server's own code and the SDK, and

Everything after the hand-off is the SDK's conformance, and `scripts/mcp-conformance.mjs` (`pnpm test:conformance` for the 2025-11-25 lane, `pnpm test:conformance:2026` for the 2026-07-28 one) runs the protocol org's `@modelcontextprotocol/conformance` suite against a local `http` server in two lanes on Linux. The legacy lane runs the pinned 0.1.16 CLI for the scenarios that apply to a server without its reference fixtures — initialize, ping, the three listings, multiple SSE streams, DNS-rebinding protection. The 2026-07-28 lane (`--era 2026-07-28`, CI job `mcp-conformance-2026`) runs the pinned 0.2 alpha's `--requirements 2026-07-28` set — everything the revision requires of a server — and lets the suite judge it: the suite reads the baseline (`scripts/conformance/expected-failures-2026-07-28.yaml`) with its own `--expected-failures`, fails on a failure or warning that is not listed and on a listed entry that passes, and its exit code is the verdict; the script adds no judging of its own. The baseline lists the checks that cannot be tested without the suite's reference fixtures (one in each `tools-call-*`, `prompts-get-*` and `resources-read-*` scenario, four in `server-stateless`) or without an optional feature this server does not declare (`completion-complete`, the `input-required-result-*` scenarios), each a single `scenario:check-id` with its reason — never a whole scenario, so the other checks of those scenarios, the schema validation of every message this server sends among them, stay enforced; by the suite's policy a missing prerequisite is a failure, not a skip, and a baseline is its named mechanism for that. The suite has no session-lifecycle scenario, which is why the first tier exists; both CLIs are pinned. One caveat for local runs: Node 23 to 24.19 and 26.0 to 26.6 abort inside `process.exit` on Windows (nodejs/node#56645, fixed in 24.20.0 and 26.7.0) after the suite has printed everything, so there the legacy lane reads the printed summaries and the 2026 lane reads the suite's closing line when that abort is in the output; CI runs Linux and takes the exit code.

The packaged artifacts are held to both eras as well (issue #1025). The Docker and npx Python smokes (`tests/e2e/docker/docker-smoke-python.test.ts`, `tests/e2e/npx/npx-smoke-python.test.ts`) run their cycle with the SDK client's default, which opens with `initialize`, and once more pinned to 2026-07-28 (`protocol: 'modern'` on the smoke helpers; a transport subclass, so the probe runs on the connection rather than on a sibling `docker run`). The published-artifact canary does the same against what users install: `scripts/canary-smoke.mjs` is a raw 2025-era client, and `--era 2026-07-28` runs the cycle in the modern era for artifacts from 0.27.0 on (`scripts/canary-era-gate.mjs`).

## Coverage Strategy

**Provider:** Istanbul. **Reporters:** text, json, html, json-summary (output to `./coverage/`). `reportOnFailure: true` ensures partial coverage is captured even when tests fail. Coverage is configured at the **root** of `vitest.config.ts`, not per project — a per-project `coverage` block is silently ignored.
Expand Down
Loading
Loading