cdbmcp
by luckinbyte
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.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues