Skip to main content
Glama
README.md
# cdbmcp

A standalone **MCP (Model Context Protocol) server for Windows** that attaches
to a running C++ process and exposes **three tools** — `evaluate`, `map_get`,
`enumerate` — to inspect the target's live memory via a **resident `cdb.exe`**
subprocess.

It is a faithful Windows port of [gdbmcp](https://github.com/luckinbyte/gdbmcp):
zero source-level intrusion (no target code or build is modified). The AI writes
the field/path it wants as a C++ expression; cdbmcp passes it verbatim to cdb,
which resolves symbols from the target's PDB and returns the value.

```
MCP client ──stdio or HTTP──▶ cdbmcp (Python) ──text cmds──▶ cdb.exe ──debug attach──▶ target C++ process (+PDB)
```

## What works (and what doesn't) — read this first

cdb's C++ expression evaluator is powerful but has the same hard limit as GDB on
a stock MSVC STL target: **inlined STL methods cannot be called.**

| Want | Works? | How |
| --- | --- | --- |
| Read a field / follow pointers | ✅ | `obj->member.field` |
| Call an **out-of-line** function (singleton accessor) | ⚠️ opt-in | needs `--allow-calls`; `Instance()->field` (runs target code) |
| Dump a whole struct / container (`??`) | ✅ (text) | default `evaluate` |
| List elements of any container | ✅ | `enumerate` (STL walker first, `dx`/NatVis fallback) |
| Call **inlined STL** (`map::find`, `map::operator[]`, `vector::operator[]`) | ❌ | use `map_get` / `enumerate`, or an out-of-line accessor |

These limits come from the C++ evaluator / MSVC STL, not from cdbmcp — cdbmcp
only passes the expression through.

## Target pause model

cdb attaches with a debug attach, which stops the target at the initial
breakpoint. cdbmcp keeps the target **running between queries** and briefly
interrupts it (Ctrl-Break) per tool call — a few milliseconds for a pure data
read, longer if the expression triggers an inferior function call. A wall-clock
watchdog (which forces a break into the engine) aborts any call that does not
return. On cdbmcp exit (stdin closed, Ctrl-C/Break, console close) it
**detaches** (`qd`, never kills) the target, which resumes normal execution.

Set `"pause_mode": "frozen"` to instead break once at attach and keep the target
stopped for the whole session (debug fallback; freezes the live process).

## Requirements

- **Windows x64**.
- **cdb.exe** (Windows Debugging Tools / Windows Kits). Auto-detected from the
  usual `Windows Kits\10\Debuggers\x64` paths, `--cdb-path`, or `CDB_PATH`.
- A **target** built with debug info (a matching PDB) for symbol resolution.
- **Privilege** to attach to the target (same user / same integrity, or run
  elevated if the target is elevated). Only one debugger may attach at a time.

## Install

```bat
pip install -e .
```

(Adds the `mcp` dependency and an entry point `cdbmcp`.)

## Run

cdbmcp is **always a single standalone process that attaches to the target PID
at startup** and stays attached for its whole life (it never respawns cdb per
query). The only difference between the two modes is the transport:

```
                       ┌──────────────────────────────────────────┐
   AI client ─stdio/──▶│  cdbmcp.exe (standalone, long-running)   │
   HTTP                │   · attaches to target PID at startup    │──▶ live C++ target (+PDB)
  (Claude Code /       │   · resident cdb, read-only              │
   Cursor / remote /   │   · stdio  OR  streamable-http + bearer  │
   many at once)       └──────────────────────────────────────────┘
```

- **stdio** (default): the MCP client launches cdbmcp as its own subprocess and
  talks over stdin/stdout. Simplest for a local Claude Code / Cursor.
- **HTTP**: you start cdbmcp yourself as a **long-running service**; many and/or
  remote clients then connect to `http://<host>:<port>/mcp`. This is the model
  when cdbmcp should be one shared process serving several AI clients.

Common flags (both modes): `--pid <PID>` (required), `--cdb-path D:\Debuggers\cdb.exe`,
`--symbol-path "srv*C:\Symbols;D:\your\app"`, `--pause-mode per_query|frozen`,
`--config config.json`. Run `python -m cdbmcp --help` for the full list.

### stdio

```bat
python -m cdbmcp --pid <PID> --cdb-path D:\Debuggers\cdb.exe
```

Client config (Claude Code / Cursor) — client launches cdbmcp:

```json
{
  "mcpServers": {
    "cdbmcp": { "command": "python",
                "args": ["-m", "cdbmcp", "--pid", "12345",
                         "--cdb-path", "D:\\Debuggers\\cdb.exe"] }
  }
}
```

### HTTP (standalone service, multi/remote client)

Start cdbmcp once as a service (it attaches immediately and stays up):

```bat
python -m cdbmcp --transport http --host 0.0.0.0 --port 8000 ^
    --pid <PID> --cdb-path D:\Debuggers\cdb.exe ^
    --symbol-path "srv*C:\Symbols;D:\your\app" ^
    --auth-token SECRET
```

Endpoint: `POST http://<host>:8000/mcp` (MCP streamable-http). Clients connect
to the URL with the bearer token:

```json
{
  "mcpServers": {
    "cdbmcp": { "url": "http://your-host:8000/mcp",
                "headers": { "Authorization": "Bearer SECRET" } }
  }
}
```

Security: `evaluate` reads arbitrary target memory, so **any remote exposure
must be authenticated**. Without `--auth-token` / `CDBMCP_TOKEN` cdbmcp warns
and forces `127.0.0.1`-only. For production put a reverse proxy (nginx/caddy,
TLS) in front and bind loopback — otherwise the token travels in cleartext.
Queries are serialized through one cdb session, so concurrent clients queue
cleanly rather than stepping on each other.

## The tools

All three are called the same way — a `tools/call` JSON-RPC over stdio or HTTP,
after the standard `initialize` / `notifications/initialized` handshake.

> End-to-end demos (build the fixture first, see below): `python tests/demo_ai_session.py`
> drives cdbmcp over **stdio** and `python tests/demo_http_session.py` drives a
> **standalone HTTP** cdbmcp, each running real AI-style queries against the
> fixture.

### `evaluate(expression, format="text")`

Evaluate a read-only C++ expression. `text` is a WinDbg-style recursive dump;
`json` returns a **typed tree** `{name, type, value|children}` parsed from
cdb's `dx`/NatVis — scalars coerced to int/float/bool, `std::string` shown as
its content, structs/containers recursed (depth/width capped), STL bookkeeping
(`[Raw View]`/size/capacity/allocator…) pruned. On error returns a plain
diagnostic string; server, debugger and target never crash.

### Inferior function calls (`--allow-calls`)

cdb's C++ evaluator (`??`) cannot call functions, so an expression like
`GameWorld::Instance()->players` fails with "Extra character error". With
`--allow-calls` (or `"allow_inferior_calls": true` in config) cdbmcp resolves a
**leading accessor call** via cdb `.call` (it runs the function in the target,
reads the returned pointer) and rewrites the rest to member access — so
`GameWorld::Instance()->players`, `map_get("GameWorld::Instance()->players", "1001")`
etc. work. Nested calls (`A()->B()`) are not flattened; prefer the leading
accessor + member access + the container tools.

> ⚠️ `.call` **runs target code** (side effects, possible deadlock) and breaks
> the read-only guarantee — hence OFF by default. cdbmcp prints a warning when
> it is on. Only the leading call is executed; everything after it stays
> member-access-only.

### `map_get(map_expr, key_expr, format="text")`

Look up one element by key in `std::map` / `multimap` without calling any
inlined STL method. `std::map`/`multimap` (red-black tree) and
`std::unordered_map`/`unordered_multimap` (`_Hash` element chain) are walked
directly and the key is compared in-engine (`first == key_expr`). Struct keys
without a usable `==` fall back to a `dx` dump. Use a C-style cast for struct
keys: `map_get("world->m_players", "(EntityID)1002")`.

### `enumerate(container_expr, limit=100, format="text")`

List up to `limit` elements of any STL container. `map`/`set`/`multimap`/
`multiset` (tree), `unordered_*` (`_Hash` chain) and `vector` are all walked
directly (no inferior calls); anything unrecognised falls back to `dx`/NatVis.

## Test fixture

`tests/fixtures/target.cpp` is a tiny C++ process with a singleton `GameWorld`
holding `std::map<int,Player>`, `std::unordered_map<int,Item>` and
`std::vector<Item>`, plus out-of-line accessors.

```bat
cd tests\fixtures
build.bat               :: produces target.exe + target.pdb
target.exe              :: prints PID=..., then idles
:: in another shell:
python -m cdbmcp --pid <that PID>
```

## Architecture / code map

| File | Role |
| --- | --- |
| `cdbmcp/cdb_controller.py` | Resident cdb subprocess; attach / interrupt / go / detach; query serialization; watchdog break |
| `cdbmcp/cdb_protocol.py` | Byte-oriented reader with sentinel-based delimiting (cdb has no GDB/MI) |
| `cdbmcp/evaluator.py` | `evaluate`: validate → interrupt → `??` (text) / `dx` (json tree) → resume; `resolve_calls` (`.call`, opt-in); error mapping |
| `cdbmcp/container_query.py` | `map_get` / `enumerate`: STL walker first, `dx`/NatVis fallback |
| `cdbmcp/stl_walker.py` | Walk MSVC STL tree / unordered (`_Hash`) / vector internals via the C++ EE — no inferior calls |
| `cdbmcp/formatters.py` | text/json rendering; prompt/banner cleanup; `dx` tree → typed JSON parser |
| `cdbmcp/watchdog.py` | Cancellable wall-clock timer → forced break |
| `cdbmcp/errors.py` | Error classification + friendly messages |
| `cdbmcp/auth.py` | Shared-secret bearer auth + pure-ASGI middleware (HTTP transport) |
| `cdbmcp/http_transport.py` | Streamable-HTTP transport (uvicorn) with bearer auth |
| `cdbmcp/lifecycle.py` | atexit + console-control handlers → clean detach |
| `cdbmcp/mcp_server.py` | FastMCP server registering the three tools |
| `cdbmcp/config.py` | Config load (JSON + CLI + cdb discovery) |
| `cdbmcp/__main__.py` | CLI entry: parse args, attach, serve stdio or HTTP |

## Differences from gdbmcp

- **Engine**: GDB/MI subprocess → resident **cdb.exe** subprocess (text protocol
  with sentinel delimiting, since cdb has no MI).
- **STL layout**: libstdc++/libc++ → **MSVC STL**. Tree (`_Tree`),
  unordered (`_Hash` chain) and vector internals are walked directly via the
  C++ EE (no inferior calls); `dx`/NatVis is the fallback for anything else.
- **Interrupt**: `-exec interrupt` → **`GenerateConsoleCtrlEvent` (Ctrl-Break to
  cdb's process group)**. A raw `0x03` byte on cdb's piped stdin does NOT break
  a running target — console control events don't flow through pipes — so cdbmcp
  launches cdb in its own process group and signals it (allocating a console
  when cdbmcp itself has none, e.g. under an MCP client).
- **Symbols**: DWARF/`-g` → **PDB** + `_NT_SYMBOL_PATH` / `--symbol-path`.
- **Attach overhead**: a live C++ target raises first-chance exceptions
  constantly; cdbmcp runs `sxn eh` so the engine does not break on them.
- **Transports**: stdio (default) and HTTP (streamable-http) with shared-secret
  bearer auth.