Skip to main content
Glama
phimage
by phimage
README.md
# oh-mcp-dap

An [MCP](https://modelcontextprotocol.io) server that gives an AI agent a real
debugger. It speaks the [Debug Adapter Protocol
(DAP)](https://microsoft.github.io/debug-adapter-protocol/) to gdb, lldb,
debugpy, delve, and any other adapter you configure — launch or attach to a
program, set breakpoints, step, and inspect threads, frames, variables, memory,
and disassembly.

It is a thin MCP wrapper around the **DAP engine from
[oh-my-pi](https://github.com/can1357/oh-my-pi)** (`@oh-my-pi/pi-coding-agent`),
reusing its session manager and adapter-config system rather than reimplementing
DAP. See [Attribution](#attribution).

> ⚗️ **Experiment.** This is an exploratory project — an experiment in reusing
> oh-my-pi's DAP engine behind a single-tool MCP server. Interfaces may change,
> it has not been battle-tested, and it has not yet been run end-to-end against a
> live client in this form. Feedback and breakage reports welcome.
>
> Runs on Bun (see [Requirements](#requirements)).

---

## Why one tool (the design difference)

Most MCP DAP servers expose **one MCP tool per DAP operation**:
`set_breakpoint`, `continue`, `step_over`, `evaluate`, `stack_trace`,
`read_memory`, … — twenty-plus separate tools.

**oh-mcp-dap exposes a single `debug` tool** with an `action` field that selects
the operation. This is the same shape oh-my-pi uses internally, and it's a
deliberate choice:

| | Tool-per-function | Single action tool (this server) |
|---|---|---|
| Tools added to the model's context | 20–30 | 1 |
| Token cost of the tool list | High, always present | Low |
| Where the operation lives | The tool name | The `action` argument |
| Adding a new DAP op | New tool, new registration | New `action` enum value |
| Discoverability for the model | Many shallow tools | One tool whose description lists the workflow |

Why the single tool wins for a debugger specifically:

- **Context economy.** Every registered tool's name + schema sits in the model's
  context on every turn. A debugger's operations are numerous but individually
  tiny; paying 25 tool-schemas' worth of tokens to describe them crowds out the
  tools the agent actually reasons about. One tool keeps the surface flat.
- **The operations are one workflow, not 25 capabilities.** `launch →
  set_breakpoint → continue → stack_trace → variables → step` is a single
  stateful conversation with one session. Modeling it as one tool with an
  `action` mirrors that: the description teaches the *flow*, and the shared
  parameter set (`file`, `line`, `frame_id`, `expression`, …) is documented once.
- **Coherent state.** There is exactly one active session. A single tool makes
  that constraint obvious instead of spreading it across many tools that all
  implicitly mutate the same hidden session.
- **Cheap evolution.** New adapter capabilities become new `action` values; no
  client re-discovery churn.

The tradeoff is honest: a single tool has a broader input schema, and a client
UI that renders per-tool affordances sees only one entry. For an agent driving a
debugger, the context savings and workflow clarity are worth it. If you want the
per-tool model instead, this server isn't that — by design.

---

## What it exposes

One tool, `debug`. Required field `action`; the rest are that action's
arguments. Actions:

**Session:** `launch`, `attach`, `terminate`, `sessions`, `output`
**Breakpoints:** `set_breakpoint`, `remove_breakpoint`,
`set_instruction_breakpoint`, `remove_instruction_breakpoint`,
`data_breakpoint_info`, `set_data_breakpoint`, `remove_data_breakpoint`
**Execution:** `continue`, `step_over`, `step_in`, `step_out`, `pause`
**Inspection:** `threads`, `stack_trace`, `scopes`, `variables`, `evaluate`,
`modules`, `loaded_sources`, `read_memory`, `write_memory`, `disassemble`
**Escape hatch:** `custom_request` (any raw DAP request)

Typical flow:

```jsonc
{ "action": "launch", "program": "./my_app" }
{ "action": "set_breakpoint", "file": "src/main.c", "line": 42 }
{ "action": "continue" }
{ "action": "stack_trace" }
{ "action": "scopes", "frame_id": 0 }
{ "action": "variables", "scope_id": 1001 }
{ "action": "terminate" }
```

---

## Requirements

- **[Bun](https://bun.sh) ≥ 1.3.14.** The upstream DAP engine is Bun-native
  (ships TypeScript, uses Bun's YAML). This server runs on Bun; Node is not
  supported.
- The **debug adapter(s)** for your language on `PATH` (e.g. `gdb`, `lldb-dap`,
  `debugpy`, `dlv`). Adapters are external programs — this server only drives
  them.

## Install

```bash
git clone <this-repo> oh-mcp-dap
cd oh-mcp-dap
bun install
```

Smoke-check that it starts (it will wait on stdin for a client — Ctrl-C to exit):

```bash
bun run src/server.ts --workspace .
```

## Configure your MCP client

Point your client at the server with Bun. Set the workspace to the project you
want to debug — that's where `dap.json` and root markers are resolved from.

Claude Code / Claude Desktop (`mcpServers`):

```jsonc
{
  "mcpServers": {
    "dap": {
      "command": "bun",
      "args": ["run", "/absolute/path/to/oh-mcp-dap/src/server.ts"],
      "env": { "OH_MCP_DAP_WORKSPACE": "/absolute/path/to/your/project" }
    }
  }
}
```

The workspace can be set three ways (first wins): `--workspace <dir>` /
`-w <dir>` arg, `OH_MCP_DAP_WORKSPACE` env, else the server's `process.cwd()`.
Any individual call may also override it with a `cwd` argument.

---

## Adapter configuration (`dap.json`)

Adapters are configured exactly as in oh-my-pi. A set of **built-in adapters**
ships out of the box and any `dap.json` you provide is **merged over** them, so
you can add new debuggers or tweak existing ones without redefining everything.

Built-ins include: `gdb`, `lldb-dap`, `codelldb`, `debugpy`, `dlv`,
`js-debug-adapter`, `netcoredbg`, `kotlin-debug-adapter`, `rdbg`,
`php-debug-adapter`, `bash-debug-adapter`, `dart`, `flutter`,
`elixir-ls-debugger`.

### Add any adapter

Drop a `dap.json` in your workspace (the file is auto-discovered — see
[discovery order](#discovery-order)). Example: a custom **4D** adapter that this
repo ships in [`dap.json`](dap.json):

```json
{
  "adapters": {
    "4d": {
      "command": "node",
      "args": ["4d-dap-bridge.js", "${port}"],
      "connectMode": "tcp",
      "languages": ["4d"],
      "fileTypes": [".4dm"],
      "rootMarkers": [".4DProject", "Project"],
      "launchDefaults": { "request": "launch" },
      "attachDefaults": { "request": "attach" }
    }
  }
}
```

### Adapter fields

| Field | Meaning |
|---|---|
| `command` | Adapter executable (resolved on `PATH`, or a relative/absolute path). |
| `args` | Launch args. `${port}` is substituted with a free port for socket/tcp adapters. |
| `connectMode` | `"stdio"` (default), `"socket"`, or `"tcp"`. How the server talks to the adapter. |
| `languages` | Language ids this adapter serves (informational / selection). |
| `fileTypes` | File extensions (e.g. `.py`) used to auto-select the adapter for a target. |
| `rootMarkers` | Files/dirs that mark a project root (e.g. `Cargo.toml`), used for selection. |
| `launchDefaults` / `attachDefaults` | Default DAP request bodies merged into every launch/attach. |
| `acceptsDirectoryProgram` | `true` if the adapter accepts a directory as `program` (e.g. `dlv`). |

Auto-selection: when a call omits `adapter`, the server picks one by the
target's file type and nearest root marker. Pass `adapter` to force a choice.

### Discovery order

`dap.json` (also `.dap.json`, `dap.yaml`, `dap.yml`) is discovered from, and
merged in this precedence (later overrides earlier), all on top of the
built-ins:

1. built-in defaults
2. `~/dap.json` (home)
3. user config dirs
4. project config dirs (relative to the workspace)
5. `<workspace>/dap.json`

So a workspace-local `dap.json` has the final say.

---

## How it works

```
MCP client ──stdio JSON-RPC──▶ src/server.ts        (one "debug" tool)
                                   │
                                   ▼
                              src/dispatch.ts        (action → engine → text)
                                   │
                                   ▼
                              src/dap-core.ts  ──▶  @oh-my-pi/pi-coding-agent/dap
                                                     • dapSessionManager (the real DAP client + session)
                                                     • dap.json config + adapter selection
```

- [`src/server.ts`](src/server.ts) — MCP stdio server; registers the single tool
  and routes calls.
- [`src/schema.ts`](src/schema.ts) — the tool's JSON Schema + description.
- [`src/dispatch.ts`](src/dispatch.ts) — the action switch (adapted from
  oh-my-pi's `debug` tool, agent-framework couplings removed).
- [`src/format.ts`](src/format.ts) — plain-text renderers for DAP responses.
- [`src/dap-core.ts`](src/dap-core.ts) — the single import seam onto the engine.

### Depending vs. vendoring

This server **depends** on the published `@oh-my-pi/pi-coding-agent` package and
imports its official `/dap` subpath — no engine code is copied in. That keeps the
debugger logic upstream (bug fixes flow in) at the cost of pulling a large dep
tree at install time (only DAP-reachable code loads at runtime).

If you ever need to cut that dependency, everything the engine provides is behind
[`src/dap-core.ts`](src/dap-core.ts). Vendoring `packages/coding-agent/src/dap/**`
(MIT) and its few internal helpers, then rewriting only that one file, is the
supported escape hatch.

### A note on stdout

The MCP stdio transport owns `stdout` for JSON-RPC. The server forces the
upstream logger's console transport off and mirrors diagnostics to `stderr`, so
adapter/engine logs never corrupt the protocol stream. Watch `stderr` for
`[oh-mcp-dap]` lines.

---

## Attribution

Built on **oh-my-pi** by Mario Zechner and Can Bölük (MIT). The DAP engine is
used as a dependency; the formatters and dispatch logic are adapted from
`packages/coding-agent/src/tools/debug.ts`. See [NOTICE](NOTICE) and
[LICENSE](LICENSE).

This project is MIT-licensed.