Windbg debugging
Drive WinDbg/DbgEng via the `windbg` MCP server to debug Windows crash dumps, live user-mode and kernel targets, and Time Travel Debugging (.run) traces. Use when analyzing a .dmp, attaching to a process or the kernel, or recording/navigating/analyzing a TTD trace.From its SKILL.md
npx -y skills add glslang/windbg-mcp --skill windbg-debuggingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.6 KB, as published. Nobody here has run it
WinDbg debugging via the windbg MCP server
This skill drives the windbg MCP server, which wraps WinDbg/DbgEng for four kinds of
Windows debugging: crash-dump analysis, live user-mode debugging, kernel
debugging, and Time Travel Debugging (TTD) of .run traces.
Verify the environment first. Most failures are setup, not debugging — wrong engine DLL, missing symbols, or no elevation. Read setup.md before the first session of a workflow you haven't run yet in this environment.
Pick a playbook
| Task | Playbook |
|---|---|
| Build / engine bundling / symbols / elevation | setup.md |
Triage a .dmp crash dump | crash-dump.md |
| Launch/attach a process, or debug the kernel | live-and-kernel.md |
| Walk kernel pools or user Segment Heaps | heap-walking.md |
Record / open / navigate / analyze a .run trace | ttd.md |
| Enumerate a driver's IOCTLs & test user-mode reachability | driver-ioctl.md |
Tool map
Knowing which verb exists keeps you from reaching for raw execute when a typed tool
already does the job.
| Group | Tools |
|---|---|
| Session | open_dump, open_trace, attach_kernel_local, attach_kernel, attach_process, launch, end_session, session_status |
| State | registers, read_memory, walk_memory, backtrace (typed frames, each module+RVA as well as its symbol), modules, threads, disassemble, dx |
| Control | go, step_over, step_into, set_breakpoint, run_to_address |
| Transaction | debug_batch — an ordered sequence with assertions and a rollback the engine runs, not the client |
| TTD nav | step_back (t-), step_over_back (p-), reverse_go (g-), goto_position (!tt) |
| TTD analysis | ttd_calls, ttd_memory, ttd_events, index_trace, record_trace |
| Kernel pool | pool_find_tag, pool_chunk, pool_census, pool_diagnostics |
| User Segment Heap | heap_list, heap_allocations, heap_chunk, heap_census, heap_diagnostics |
| Server | server_log — the server's own records: the supervisor's, plus your own sessions' workers, tagged by session |
| Raw | execute — run any debugger command, returns full text output |
The forward control tools (go/step_over/step_into) and the reverse ones
(reverse_go/step_over_back/step_back) mirror a debugger UI's F9/F8/F7 and their
Shift variants. They issue the command and pump the engine to the next stop — unlike
a bare execute, which only sets the run state and doesn't move the target.
When a sequence mutates the target, run it as one debug_batch, not as separate calls. A
patched byte, an armed breakpoint or a resumed thread has to be put back, and the call that would
have sent the cleanup is exactly the one that times out. A batch's always block is reached inside
the engine process on every path, including a failed assertion and an expired deadline, and part of
the budget is reserved so it has time to run; the report names the exact failing step, what each
step changed, whether the rollback completed, and whether the target is left stopped, running or
gone. Save what you are about to overwrite with an eval step's capture, and restore it in
always as {{name}}. A step can also ask the kernel pool what the pool_* tools ask —
pool_chunk, pool_find_tag, pool_census — so "capture the pointer, ask the allocator what it
is" stays inside the transaction instead of splitting it in two; a refresh there is bounded by
the step's share of the batch budget rather than by the walker's own, and says how much of the pool
it reached.
Two edges to keep in mind. If a step overruns far enough to consume the reserve too, cleanup is
skipped and the result says rollback: INCOMPLETE — believe it rather than the intent. And a
teardown while the batch runs — end_session, or a client disconnect — stops it at its next
step and rolls it back first, reported as BATCH: ABANDONED; it cannot cut short a step already
inside the debugger, so a batch built from long steps waits out the one it is in before it unwinds
(the teardown waits with it).
server_log answers "why did that fail?" when the result did not. It serves both processes'
records — the supervisor's, and those of the sessions you opened, each tagged with its session.
(A listener may hold several clients, and another client's worker records are no more readable than
its sessions are; under stdio there is one client, so this is everything.) That reaches the
run-up to a session that failed to open, which has no handle to ask anything else about. Reach
for it after an opener that errored, a worker that died, or a call that timed out with nothing to
show for it; session_status says what state a session is in, and this says what it did to get
there. It is also the only route to a worker's own output when the server is on another machine,
where its stderr is not on your screen.
Cross-cutting gotchas (apply to every workflow)
- One command at a time per session. Each session is its own engine process, so sessions run
independently — but within one, work is serial. Issue tool calls for a session sequentially —
await each result before the next: concurrent in-flight requests aren't ordered against each
other, so a pipelined call can run before
open_dumpestablishes a target and fail with0x80040205, and pipelining stateful debugger commands is unsafe regardless. (Normal MCP clients serialize call→result; this only bites custom/batched callers.) - Carry the
session_id. The tools that open a target return one; pass it on every later call — it routes the call to that target's engine. Omitting it means "whatever session is current" (the newest one still accepting work), which is fine when you only have one and a silent wrong-target read when you have more. - Opening a target does not close the ones you have. Up to four sessions at once, so you can
triage a dump while a kernel attach is live.
end_sessionwhen you are done with one; at the limit a new open reclaims the oldest idle session, and refuses if every session is busy. - If an open times out, do not retry it — that connects or spawns a second time. The timeout
abandons the wait, not the job, so the open may still land. Ask
session_status { "session_id": "<the id the timeout named>" }: it says what state the session is in and how long it has been there. For a kernel attach that distinction is the whole thing — a link still coming up (~25s) looks identical to a guest that will never dial in, and only the elapsed time tells them apart. Past that point the report says so, andend_sessionreclaims the session, terminating its engine process if the wait will not unwind. It never affects your other sessions. - A failed debugger operation comes back as a normal tool result, flagged as an error and carrying the debugger's own text — read it and adjust (wrong symbol, unmapped address, target still running). So do a refused handle and a session whose engine died; all of them are things you can act on. Only "no engine could be started at all" is a transport-level protocol error.
- Attach a KDNET kernel target by
profile, not by connection string. A connection string carries the target's debug key, and once it arrives as a tool argument it is in the transcript for good.attach_kernel { "profile": "<name>" }has the server resolve it locally instead, andattach_kernel {}with no arguments lists the profiles this host has — so you never have to guess a name. If none covers the target, ask the user to create a profile rather than asking for the connection string — specifically to add a line to%USERPROFILE%\.windbg-mcp\profiles.json, which is re-read on every attach, so it works with nothing restarted. (AWINDBG_MCP_PROFILE_<NAME>variable is read from the server's own environment at startup, so setting one in a shell now does nothing until the server restarts — don't offer it as the immediate fix.) Only if they decline isattach_kernel { "connection": "…" }the answer — and say that puts the key in the transcript, so it is their call. Never invent a key. live-and-kernel.md has the exact wording to give them. - Symbol names (
module!func) need three things together: (a)msdia140.dllandsymsrv.dllbundled next to the binary, (b) a symbol path — set it with theset_symbol_pathtool (srv*C:\ProgramData\Dbg\sym*https://msdl.microsoft.com/download/symbols,append: true), which goes through the DbgEng API and so avoids.sympathswallowing the rest of the command line — and (c) a module-qualified.reload /f <mod>at a stopped position (bare.reload /fwalks every loaded module, which on a live kernel is slow) (after ago/breakpoint, not straight off agoto_position/!tt). Without these you silently get export symbols only andmodule!namelookups fail. Address-based queries, navigation, and memory reads still work without symbols — query by address. file not foundfor a PDB usually means the engine, not the path.dbgeng.dllis in System32, so a binary with no DLLs beside it opens targets and runs commands happily — it just has nosymsrv.dllto read a symbol store and nomsdia140.dllto parse a PDB, and the error summary then blames the store (invalid UNC store,pingme.txt) while the PDB sits in it.!lmi <mod>settles it: a CODEVIEW line with a GUID plusSymbol Type: EXPORTmeans the identity was known and the lookup still failed — engine or store, not the target. No CODEVIEW line is the opposite problem, and a different fix. Verify withlm m <mod>((pdb symbols)vs(export symbols)) — never withx <mod>!<sym>, which prints nothing when unresolved, so its silence proves nothing. Details in setup.md.- Allocator tools need private PDB types, not exports —
pool_*resolvesntandheap_*resolvesntdll. Reload only the matching module (.reload /f ntor.reload /f ntdll.dll) and confirm(pdb symbols)withlm m <mod>. Missing types are a symbol problem on this host; symbols never come over the KD wire. See heap-walking.md. !-extension commands need the WinDbgwinext\extensions bundled next to the engine (setup.md) and the module-qualified form: use!ext.analyze -v, not bare!analyze(which this engine resolves to "No export analyze found" even after.load ext).open_dumpruns.load extfor you.read_memorytakes a numeric/0x-hex address only. For a register/symbol expression useexecutewithdb/dd(e.g.db @rip).- Never walk a list with a MASM
.forloop — usewalk_memory. One unmappedpoiaborts the whole script with0x80040205and returns nothing, so a table with one freed entry in it tells you only that something failed.walk_memorymarks the hole and keeps going. Name the nodes withaddresses(an explicit list),start+stride(an array), orstart+next_offset(a chain), andfieldsto read out of each; offsets may be negative, so a pool header at-16is an argument. A chain is the one walk a hole stops — it says which node — and it also reports null links, loops (with where the list closed), and the resume address when it hitscount. - Single-stepping needs a live thread context — valid only once the target is stopped
after a
go/step or a breakpoint hit. Stepping straight aftergoto_position 0(before any thread is live) returns0x80040205;goto a breakpoint first. dxis the escape hatch for the data model — any LINQ query beyond thettd_calls/ttd_memory/ttd_eventswrappers, e.g.@$cursession.TTD.Calls("ntdll!NtCreateFile").Where(c => c.ReturnValue != 0). Note it can also run debugger commands viaDebugger.Utility.Control.ExecuteCommand, so likeexecuteit is annotated destructive and retires yoursession_idwhen the expression touches command execution. Ordinary TTD queries don't trip it.- TTD is user-mode only (a Microsoft limitation) — you cannot time-travel a kernel target.
- Each tool call is bounded by a per-call timeout (~60s for load/exec waits); a
goagainst a long-running live target may hit it.