Skip to main content
Glama
XC881

xcnodejs-debugger-mcp

by XC881
README.md
# xcnodejs-debugger-mcp

[English](README.md) | [中文](README.zh-CN.md)

A **Node.js debugger MCP server** for local coding agents.

The agent calls MCP tools. This server talks **Chrome DevTools Protocol (CDP)** to a Node process started with V8 Inspector (`--inspect-brk=127.0.0.1:0`). Architecture:

```text
MCP client (agent)  →  this server  →  CDP / WebSocket  →  Node Inspector  →  your JS
```

It is a **VS Code Node debug subset** (launch, attach, breakpoints, step, stack, variables, console, restart) exposed as MCP tools. It is **not** a wrapper around `node inspect`, the Debug Adapter Protocol (DAP), or `ms-vscode.js-debug`.

- **Repository:** https://github.com/XC881/xcnodejs_debugger_mcp
- **Author:** XC881 team
- **License:** [XC881 Non-Sale Source License](LICENSE) — commercial use and free modification allowed; selling the software is prohibited
- **Node.js:** 20+

## Why this exists

Agents need a debugger they can drive with tools, not a GUI. Existing options usually take one of these paths:

| Approach | What it does | Why we did not |
| --- | --- | --- |
| Wrap `node inspect` | Drive the CLI inspector | Fragile text UI, not a protocol |
| Wrap DAP / `vscode-js-debug` | Agent talks DAP; js-debug talks CDP | Extra process, DAP session lifetime, adapter-specific quirks |
| Chrome DevTools only | Human UI on `devtools://` | Not callable from an MCP host |
| **This server** | MCP tools → CDP → Node | One hop, Node-only, session-stable restart |

`microsoft/vscode-js-debug` is an excellent **recipe** (child auto-attach, `NodeWorker`, logpoints, inspect-brk hold). We read it that way. It is **not** a runtime dependency and is not spawned.

## What is different

1. **MCP → CDP → Node**, not MCP → DAP → js-debug → CDP → Node.
2. **stdio and Streamable HTTP** on one server. Debug sessions live in the process, not per HTTP request.
3. **Several programs at once.** Each `debug_launch` / `debug_attach` returns a `sessionId` (`dbg-1`, `dbg-2`, …).
4. **VS Code Restart, not nodemon.** `debug_restart` kills a launched debuggee (or re-attaches), reapplies breakpoints, keeps `sessionId`, and **does not drop the MCP connection**.
5. **The program’s `node_modules`, not the MCP server’s.** Default `cwd` is the package root of `program` (walks up, skips directories inside `node_modules`). Breakpoints accept a path or a package specifier such as `demo-ext`.
6. **Hold at start.** Launch uses `--inspect-brk` and returns `awaiting_start` so you can bind breakpoints in the entry script, `--require` hooks, and `node_modules` **before** user code runs.
7. **Token-aware variables.** Preview (default 32 properties), expand by `objectId`, page with `cursor`. Cycles are `[Circular]` / `已回环`. Truncation is `已折叠`. The heap is not dumped into the model context.
8. **Inspector stays on loopback.** `--allow-remote` only allows the MCP HTTP bind address to leave `127.0.0.1`. CDP is always `127.0.0.1`.
9. **Children and workers.** `child_process` auto-attach via a loopback hub + preload. `worker_threads` via Inspector `NodeWorker` multiplexed on the parent WebSocket.
10. **Logpoints, pid attach, source maps, optional Babel.** DAP-style `{expr}` logpoints, attach by pid, `file://` / inline / loopback `http(s)` maps, `@babel/register` for `.jsx`/`.ts`/`.tsx` when the **program** has a Babel config.

## Features

- Launch / attach (`wsUrl`, host+port, or pid) / disconnect
- stdio MCP (default) and Streamable HTTP (`--http`, loopback unless `--allow-remote`)
- Multiple concurrent sessions (`debug_list_sessions`)
- Line breakpoints, conditions, DAP-style logpoints
- `debugger;` pauses when attached
- Continue, pause, step in / over / out
- `debug_wait_for_pause` and `debug_resume`
- `debug_restart` (launch relaunch or attach rediscover)
- Source maps: `file://`, inline `data:`, loopback `http(s)`, and `http(s)` maps when the script URL is also `http(s)`
- `child_process` auto-attach and `worker_threads` (`NodeWorker`)
- Call stack, evaluate, stdout/stderr/console
- `debug_list_scripts` (entry, preloads, plugins, internals)
- Skip `node:` / `internal/` frames by default

## Requirements

- Node.js **20+**
- A trusted local workspace (launch and evaluate run as the MCP user)

```bash
git clone git@github.com:XC881/xcnodejs_debugger_mcp.git
cd xcnodejs_debugger_mcp
npm install
```

## Run

```bash
npm start            # stdio MCP (default; stdout is the protocol)
npm run start:http   # Streamable HTTP on http://127.0.0.1:3930
npm test
npm run build        # dist/index.js
```

Logs and warnings go to **stderr**. Do not print to stdout in stdio mode.

### CLI

```text
xcnodejs-debugger-mcp [--stdio | --http] [--host 127.0.0.1] [--port 3930] [--allow-remote]
```

| Flag | Meaning |
| --- | --- |
| `--stdio` | MCP over stdin/stdout (default) |
| `--http` | Streamable HTTP |
| `--host` | Bind address for `--http` (default `127.0.0.1`) |
| `--port` | Bind port (default `3930`; `0` = ephemeral) |
| `--allow-remote` | Allow `--http` to bind a non-loopback address. **Inspector CDP stays `127.0.0.1`.** |

## MCP host config

Development (tsx, no build):

```json
{
  "mcpServers": {
    "xcnodejs-debugger": {
      "command": "npx",
      "args": ["tsx", "/absolute/path/to/xcnodejs_debugger_mcp/src/index.ts"]
    }
  }
}
```

After `npm run build`:

```json
{
  "mcpServers": {
    "xcnodejs-debugger": {
      "command": "node",
      "args": ["/absolute/path/to/xcnodejs_debugger_mcp/dist/index.js"]
    }
  }
}
```

HTTP (loopback). Start `npm run start:http` first:

```json
{
  "mcpServers": {
    "xcnodejs-debugger": {
      "url": "http://127.0.0.1:3930"
    }
  }
}
```

HTTP sessions live in the **server process**, not per request. Closing one HTTP call does not disconnect debuggees.

## Typical flow

`debug_launch` returns `awaiting_start`. The inspector is attached, but **user code has not run yet** (including `--require` / `--import` / loaders). Set breakpoints, then start the isolate.

1. `debug_launch` `{ "program": "app.js" }` → `sessionId`, state `awaiting_start`
2. `debug_set_breakpoint` `{ "file": "app.js", "line": 12 }` (repeat for extension files)
3. `debug_continue` — returns immediately and starts user code
4. Exercise the program (HTTP request, timer, …)
5. `debug_wait_for_pause`
6. `debug_get_stack` / `debug_get_variables` / `debug_evaluate`
7. `debug_disconnect`

`debug_resume` is continue + wait (scripted stepping). `debug_continue` + `debug_wait_for_pause` is the split to use when the process must keep running while you poke it from outside.

Lines and columns are **1-based**.

## Tools

When more than one session is live, pass `sessionId` on every tool except `debug_launch` / `debug_attach` / `debug_list_sessions`.

| Tool | Role |
| --- | --- |
| `debug_launch` | Spawn with `--inspect-brk=127.0.0.1:0`. Returns `awaiting_start`. |
| `debug_attach` | Attach by `wsUrl`, `host`+`port` (`GET /json/list`), or `pid`. Loopback only. |
| `debug_list_sessions` | List `sessionId`, state, pid, program, label. |
| `debug_status` | One session: idle / connecting / awaiting_start / paused / running / closed. |
| `debug_disconnect` | Close one session or `all`. Default: kill launch processes; leave attach targets running unless `terminate=true`. |
| `debug_restart` | VS Code Restart: relaunch or re-attach, reapply breakpoints, keep `sessionId` and MCP. |
| `debug_set_breakpoint` | Path or package specifier. Optional `condition`, `logMessage`. |
| `debug_list_breakpoints` | Breakpoints in a session. |
| `debug_remove_breakpoint` | Remove by id (`bp-N`). |
| `debug_list_scripts` | Scripts the inspector has parsed. |
| `debug_continue` | Start isolate if `awaiting_start`, or resume. Does not wait. |
| `debug_wait_for_pause` | Start if needed; return current pause; or block until `Debugger.paused`. One waiter per session. |
| `debug_resume` | Continue (or start) and wait for the next pause. |
| `debug_pause` | `Debugger.pause`. |
| `debug_step_over` / `debug_step_into` / `debug_step_out` | Step and return the new snapshot. |
| `debug_get_stack` | Current frames (paused). |
| `debug_get_variables` | Locals/closures, or expand `objectId`. Preview + paging. |
| `debug_evaluate` | `Debugger.evaluateOnCallFrame` when paused, else `Runtime.evaluate`. |
| `debug_get_output` | stdout, stderr, `Runtime.consoleAPICalled`. Pass `cursor` for incremental reads. |

### `debug_launch` arguments

| Argument | Meaning |
| --- | --- |
| `program` | Entry script, absolute or relative to `cwd` |
| `args` | Program arguments |
| `cwd` | Working directory. Default: package root of `program` |
| `env` | Extra env (merged over the MCP process env) |
| `runtimeExecutable` | Runtime binary (default: the Node running this server) |
| `runtimeArgs` | Extra runtime args after inspect/preload flags (`--inspect*` is stripped) |
| `require` / `import` / `loader` | Optional `--require` / `--import` / `--experimental-loader`, resolved from the **program** `node_modules` |
| `autoAttachChildren` | Auto-attach `child_process` and `worker_threads`. Default `true` |
| `autoLoadBabel` | Auto `--require @babel/register` when a Babel config exists. Default: on for `.jsx`/`.ts`/`.tsx`, off for `.js` |
| `label` | Shown in `debug_list_sessions` |

### `debug_attach` arguments

Provide **one of**: `wsUrl`, `host`+`port`, or `pid`.

| Argument | Meaning |
| --- | --- |
| `wsUrl` | Full inspector WebSocket URL (`ws://127.0.0.1:…`) |
| `host` | Inspector host (default `127.0.0.1`). Non-loopback is rejected |
| `port` | Inspector HTTP port for `/json/list` |
| `pid` | Attach by process id. Enables inspector with `process._debugProcess` or `SIGUSR1` if needed |
| `label` | Optional label |

Pid attach only uses ports owned by that pid (and loopback `/json/list`). Restart of a pid session rediscovers the inspector **without** signaling again (SIGUSR1 can toggle inspector off).

## Breakpoints, logpoints, `debugger;`

```json
{ "file": "app.js", "line": 12 }
{ "file": "demo-ext", "line": 2 }
{ "file": "app.js", "line": 20, "condition": "i === 3" }
{ "file": "app.js", "line": 20, "logMessage": "i={i} name={user.name}" }
```

- `file` is a path or a package name resolved from the debuggee `node_modules`.
- `condition` is JavaScript. Pause only when it is truthy.
- `logMessage` is a DAP-style logpoint: `{expr}` is interpolated via `console.log` and **does not pause**. Combined with `condition` when both are set. Empty `{}` stays literal. `%` in static text becomes `%%`.
- `debugger;` pauses after the inspect-brk entry pause, when the inspector is attached.

## Variables (token-aware)

`debug_get_variables` does not dump whole objects.

- Default page size **32** (`maxProperties`, max 200)
- `objectId` expands a nested object
- `cursor` / `nextCursor` pages the rest
- `includeGlobal` / `scopeIndex` for global/script scopes (omitted by default)
- Cycles: `[Circular]` / `已回环` (V8 mints a new `objectId` per preview; cycles are detected with `===` via `Runtime.callFunctionOn`)
- Truncated strings and extra properties: `已折叠`

Strings in previews are capped (256 characters).

## Source maps

Original sources are used for breakpoints, stack frames, and excerpts when a map is available:

- `file://` map files next to generated JS
- Inline `data:` maps
- Loopback `http(s)` maps (including from a `file://` script)
- `http(s)` maps when the **script URL** is also `http(s)`

Absolute **non-loopback** `http(s)` maps on a `file://` script are **not** fetched (SSRF). Redirects are not followed. Payload cap is 5 MB.

## Restart

`debug_restart` matches VS Code Restart:

- **Launch:** kill the debuggee, spawn the same config, reapply breakpoints. Edited JS is loaded. `sessionId` unchanged. MCP stays up.
- **Attach:** disconnect CDP, do not kill the debuggee, rediscover `host`+`port` (or reuse `wsUrl` / pid without re-signaling), reattach, reapply breakpoints.

This is not file-watch reload / nodemon.

## Children and workers

On launch, unless `autoAttachChildren: false`:

- **`child_process`:** `NODE_OPTIONS=--require=<preload>` reports a new inspector URL to a loopback hub. The child is a new session with `parentSessionId`. Breakpoints are copied. Disconnect parent disconnects children first.
- **`worker_threads`:** `NodeWorker.enable({ waitForDebuggerOnStart: true })`. The worker is a nested CDP session on the parent WebSocket (not a second inspector port). Entry `Break on start` is skipped so copied breakpoints can hit.

The preload skips `-e` / `--eval` and threads that are not the main thread (workers go through `NodeWorker` only).

## Node extensions and Babel

This server does **not** use its own `node_modules` to load the debuggee’s plugins.

Default `cwd` is found by walking up from `program`, skipping paths inside `node_modules`, until `package.json` or `node_modules` is found.

If you omit `require` / `import` / `loader`, Node loads packages the usual way. Set a breakpoint with a specifier or a path:

```json
{ "file": "demo-ext", "line": 2 }
```

```json
{ "file": "node_modules/babel-plugin-foo/lib/index.js", "line": 12 }
```

Optional preloads still work; bare names resolve from the **program file** `node_modules`:

```json
{
  "program": "app.js",
  "require": ["@babel/register"]
}
```

`.jsx` / `.ts` / `.tsx` programs auto-prepend `@babel/register` from that `node_modules` when a Babel config exists (`babel.config.*`, `.babelrc*`, or `package.json#babel`). `.js` is left to Node and source maps unless you set `autoLoadBabel: true`. Set `autoLoadBabel: false` to skip.

Set breakpoints in the plugin **and** in `app.js` before `debug_continue`.

## Security

- Launch executes a process as the MCP user.
- `debug_evaluate` runs JavaScript inside that process.
- Inspector CDP is **always** `127.0.0.1`. `--allow-remote` does not change that.
- Use only on a trusted local workspace.
- Source-map HTTP fetch is bounded (loopback / same-scheme script, no redirects, 5 MB, 5 s).

## Tests

```bash
npm test
```

Integration coverage includes launch/attach, closures, `debugger;`, ESM/`require` packages, source maps, child auto-attach, workers, logpoints, pid attach, Babel `.tsx` hook, and stdio/HTTP MCP.

## Mapping from VS Code `launch.json`

| `launch.json` | MCP |
| --- | --- |
| `program` | `program` |
| `args` | `args` |
| `cwd` | `cwd` (default: package root of `program`) |
| `env` | `env` |
| `runtimeExecutable` | `runtimeExecutable` |
| `runtimeArgs` | `runtimeArgs` |
| `require` / `import` / `loader` | `require` / `import` / `loader` |
| `autoAttachChildProcesses` | `autoAttachChildren` (also `worker_threads`) |
| | `autoLoadBabel` |
| | `label` |

Launch always injects `--inspect-brk=127.0.0.1:0` and holds the isolate until continue/resume so pending breakpoints bind in preloaded modules.

## Acknowledgements

- [Node.js](https://nodejs.org/) — V8 Inspector / `--inspect` / `--inspect-brk`
- [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) — the wire protocol this server speaks
- [Model Context Protocol](https://modelcontextprotocol.io/) and [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server) — MCP stdio and Streamable HTTP
- [microsoft/vscode-js-debug](https://github.com/microsoft/vscode-js-debug) — **recipe only** (inspect-brk hold, child auto-attach, `NodeWorker`, logpoint condition shape). Not linked, not spawned, not wrapped
- [Debug Adapter Protocol](https://microsoft.github.io/debug-adapter-protocol/) — UX mapping for Restart and logpoints; this server does not implement or wrap DAP
- [ws](https://github.com/websockets/ws) — Inspector WebSocket
- [zod](https://github.com/colinhacks/zod) — tool schemas

## Author

**XC881 team**

Issues and source: https://github.com/XC881/xcnodejs_debugger_mcp

TDQS

A3.7/5.0

Scored across 21 tools

Disambiguation3/5

Most tool groups are clearly separated by resource (breakpoints, sessions, stepping), but debug_continue, debug_resume, debug_wait_for_pause, and debug_pause have overlapping control-flow behavior that could cause an agent to pick the wrong one. The descriptions clarify the differences, but the boundaries are still somewhat subtle.

Naming Consistency5/5

All tools use a consistent debug_ prefix with snake_case and a predictable verb_noun pattern (debug_set_breakpoint, debug_list_sessions, debug_get_variables). Phrasal verbs like debug_step_out and debug_wait_for_pause are still intuitive and fit the same scheme.

Tool Count3/5

21 tools is on the heavy side, though the domain of a Node.js debugger naturally requires many operations. The count is justified by the breadth of lifecycle, breakpoint, stepping, and inspection features, but it is still more than the typical well-scoped MCP server.

Completeness4/5

The tool surface covers the core debugging lifecycle well: launch/attach, control, breakpoints, stepping, stack/variables, evaluation, output, and session management. Minor gaps exist such as no explicit conditional breakpoint support, exception breakpoint configuration, or source retrieval, but agents can accomplish most debugging workflows.

Maintenance

ActivityMaintained
ResponsivenessNo issues