winedbg-mcp
Wraps the Wine debugger (winedbg) as MCP tools, letting an agent start or attach a debugger session to a Windows program running under Wine, execute arbitrary debugger commands in it (with per-call timeouts), and stop the session along with the debuggee's process group.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@winedbg-mcpstart winedbg on notepad.exe and set a breakpoint on CreateFileW"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
winedbg-mcp
An MCP server for interacting with winedbg (the Wine debugger). It wraps the interactive debugger so an LLM can drive it through MCP tools.
Status
The server is implemented and tested. src/ holds the MCP entry point, the
winedbg session state machine, the environment parsing, the command-line
parsing and the tool-argument validation; build/ is the compiled output of
bun run build; tests/ covers all of those, and CI (.github/workflows/ci.yml)
runs the install, bun run check (Biome, both type-check passes and the
suite), the build and the artifact check. The session tests drive
WinedbgSession against a stand-in that speaks the same Wine-dbg> prompt
protocol, so the suite needs no Wine. No test here has been run against a real
winedbg: the debugger is the one thing the fixtures replace, so the suite
proves the prompt protocol and the tool argument handling, not Wine itself. See
docs/THREAT_MODEL.md for the attack surface, the
trust boundaries and the mitigations, read off the source rather than off this
README.
CHANGELOG.md records what changed in each release.
Source layout, one concern per module:
Path | Concern |
| entrypoint: config load, transport, process lifecycle |
| the command line: |
| the MCP tool list and the call dispatch |
| validation of untyped tool arguments |
| the winedbg child process and its prompt protocol |
| the process, the clock and the process-group probe the session reaches the outside world through |
| the stderr log line format and the level filter |
| reading and validating the environment |
| defaults and limits shared across the above |
| the version reported to MCP clients, read from |
| asserts the built entry point runs and ships nothing but compiled JavaScript |
Related MCP server: GDB MCP Server
Prerequisites
Supported platforms are Linux and macOS. winedbg_stop signals a process group
rather than a single process, so the debuggee winedbg launched is stopped with
it, and that needs POSIX process groups: neither the detached child group nor
the negative-pid kill exists on Windows, where the same call would leave the
program under debug running. CI runs the whole gate on both, so the claim is
tested rather than inferred from POSIX.
Node.js 18 or higher is only needed if you run the built build/index.js
with node instead of bun, or to run scripts/verify-artifact.sh, which
checks the artifact under both hosts. package.json declares the same floor
under engines, so a client on an older host is warned at install time.
Installation
Clone the repository.
Install dependencies:
bun installCI runs the same install as
bun install --frozen-lockfile, sobun.lockis the only dependency set any build is allowed to resolve. A plainbun installupdates it, which is how a dependency change enters the tree.Build the project:
bun run build
The build writes build/index.js. To run the server straight from source
without a build step, use bun run dev (bun run start runs the built file).
The build clears build/ first, so a module deleted from src/ cannot linger
in the artifact, and the output is byte-identical wherever the checkout sits,
whatever the locale or timezone, and at any wall-clock time: tsc emits no
timestamp, no absolute path and no source map.
Configuration
To use this with an MCP client (like Claude Desktop or Gemini), configure the MCP server to point to the built index.js.
{
"mcpServers": {
"winedbg": {
"command": "bun",
"args": ["/path/to/winedbg-mcp/build/index.js"],
"env": {
"WINEDBG_MCP_BINARY": "/opt/wine-staging/bin/winedbg",
"WINEDBG_MCP_READY_TIMEOUT_MS": "60000"
}
}
}
}Environment variables
All four are optional and read once at startup. There are no secrets and no config file: the environment is the only place to set these.
That says what this server reads, not what its process holds. The environment an
MCP client launches a server with is the agent's own: API keys, registry tokens
and cloud credentials sit in it, and the program under debug is whoever supplied
the target. winedbg is therefore started with an allowlist rather than the
whole environment: PATH, HOME, DISPLAY, the WINE*, XDG_*, LC_*,
SDL_*, MESA_* and graphics-driver families, and the Windows-path variables a
program under Wine reads. Everything else stays here. Name a variable winedbg
turns out to need in WINEDBG_MCP_PASSTHROUGH_ENV. docs/THREAT_MODEL.md
records this and the rest of the attack surface.
Variable | Default | Valid values |
|
| A non-empty command name or path, with no NUL byte in it |
|
| Whole milliseconds, 1 to 600000. How long |
|
| Whole milliseconds, 1 to 600000. How long |
|
|
|
| empty | Comma-separated variable names to forward to |
A value the server cannot use stops it at startup with the variable named,
rather than failing later as a spawn error or a start timeout. That includes a
variable set to the empty string, a name that is not a variable name, and a
misspelled WINEDBG_MCP_* name, which would otherwise be ignored while the
deployment ran on defaults. The startup line
on stderr reports the values in effect:
{"time":"2026-09-27T10:00:00.000Z","level":"info","message":"winedbg MCP server running on stdio","version":"1.0.0","config":"WINEDBG_MCP_BINARY=winedbg WINEDBG_MCP_READY_TIMEOUT_MS=10000 WINEDBG_MCP_COMMAND_TIMEOUT_MS=30000 WINEDBG_MCP_LOG_LEVEL=info"}Logging
stdout carries JSON-RPC and nothing else, so every diagnostic goes to stderr as
one JSON object per line: an ISO-8601 time, a level, a fixed message and
flat named fields. A multiline winedbg reply therefore cannot break a line
parse, and a log aggregator can filter on a field instead of on a phrase.
The fields an operator pivots on:
Field | Where | Answers |
| one tool call, and every session line that call produced | ties the start, the outcome, the duration and the debugger lines of a call together, e.g. |
| one tool call | which of the three tools ran |
| tool call result | how long the call took, success or failure |
| a failure | why: the same text the client got back as the tool result |
| session start and exit | how long the debugger took to answer, and how it ended |
| a command that timed out | which command, and the bound it hit |
| a reply past the 1M buffer limit | that the reply was shortened, and by how much |
| a crash | an uncaught exception or a rejection nobody awaited, with the build it came from |
One tool call is one path through the log: a winedbg_start that hangs shows
tool call started and the winedbg spawned, waiting for its first prompt line
under one callId, then either the first prompt with its readyMs or the ready
timeout, then the tool call's own outcome with its durationMs. A callId is
absent from a line that belongs to the process rather than to a call: startup,
shutdown and a signal are the server's own.
A session that ends because the debugger died is logged at error; one that
ends because winedbg_stop, SIGTERM or the client hanging up asked for it is
logged at info, so a quiet log holds no shutdown noise. debug adds the
command text of each winedbg_execute. A crash is recorded before the process
leaves, on the same one-object-per-line surface, so it carries a level, a
timestamp and the version rather than node's plain-text output. There are no
metrics, traces or alerts to configure: the server is one process per MCP client
with nothing to scrape, so the log is the whole surface.
Tools Available
winedbg_start: Start or attach towinedbg. Use this before running any commands. Optionalargsare passed towinedbgunchanged, so anything it accepts works: the program to launch (e.g.{"args": ["myapp.exe"]}) or a PID to attach to ({"args": ["1234"]}).argsmust be an array of strings; a bare string is rejected rather than split into one argument per character. At most 64 entries, each at most 4096 characters, none carrying a NUL: an argv entry is cut at the first NUL by the C runtime, and an unbounded array is a caller filling the process table rather than a debugging session. Repeating the call with the sameargsis safe and launches nothing: it answers with the session already at its prompt, whether the first call is still waiting for that prompt or has been there for a while, and says that it launched nothing. A client that re-sends a start it never saw answered, or a model that repeats a call, therefore gets one debugger and one debuggee rather than a second launch of a program that has effects of its own.argsnaming something else is a different request and is refused.winedbg_execute: Execute one command in the activewinedbgsession (e.g.,{"command": "bt"}). Takes an optionaltimeoutin milliseconds (minimum 1, maximum 600000), defaulting toWINEDBG_MCP_COMMAND_TIMEOUT_MSso a deployment can set it once rather than on every call. The command is at most 4096 characters and carries no line break or NUL, for the reason under command framing below. This is the one tool that is not replay-safe: a repeated call runs the command again, so astep, acontor anything that writes debugger state advances the program a second time.winedbg_stop: Stop the activewinedbgsession. A start that follows a stop waits for the stopped debugger to be gone, so alternating the two cannot leave one detached process group, each holding a debuggee, per cycle. A stop returns once the signal is sent, since a tool call should not be held open for a grace period; the wait happens where nothing is waiting on it, in the nextwinedbg_startand in the server's own exit on SIGINT, SIGTERM or the end of stdin. Calling it again, or with nothing running, signals nothing a second time and still reports success.
Audit log
Every tool call is recorded on stderr, which is the operator's log and not the
reply stream, since the program under debug writes to that stream and could
otherwise forge a record of what it did. A start logs its arguments, a command
that is sent logs the command at debug, a command that times out logs the
command and its timeout at error, a stop shows up as the session's exit, and a
failure logs the tool and the message.
Each line is a JSON object, so a control character in a command is escaped rather than able to break the one-line parse, and a recorded field is at most as long as the argument limits above (4096 characters). Nothing bounds how many records a session writes, and no reply is logged, so the log is a record of what was asked for and not of what came back. A debugger line records the tool call that reached it, so what a session did is read under the call that asked for it rather than by matching text by hand.
Command framing
winedbg has no way to label which output belongs to which command; the only
marker in the stream is the Wine-dbg> prompt. Two consequences are visible
through the tools:
One command per
winedbg_executecall. A command carrying a line terminator is rejected, because every one of them draws its own prompt and puts every later reply one command behind. The rejected set is\n,\r, vertical tab, form feed, NEL (U+0085), the Unicode line and paragraph separators (U+2028, U+2029), and NUL. A stream reader splits on\n,\rand vertical tab, and readers disagree on the rest, so a command is one line only if it is one line under every one of them. NUL is not a line break, but it truncates the line for most C readers, which desynchronises the reply stream the same way a second line would. The check runs both at the tool-argument boundary and in the session, so the rule holds for a caller that reaches the session directly.After a command times out, further commands are refused until the debugger prints its prompt again. A debugger that has not returned to its prompt is not reading commands, and whatever it prints next belongs to the command that timed out. If it never comes back (a
continto a program that does not stop), callwinedbg_stopand start again.
A single reply is buffered up to 1M UTF-16 code units of decoded text, so a
BMP character costs one unit and an astral one costs two, whatever the target
prints. The child's output is decoded as UTF-8, and a byte sequence that is not
valid UTF-8 becomes U+FFFD rather than being passed through. A multi-byte
character split across two reads of the pipe decodes as the one character it
is, not as two replacement characters. Past the cap the oldest output is
dropped to keep the last three quarters, and the reply then opens with
[N characters of earlier output dropped: buffer limit], counting code points
rather than the units the cap is measured in. The cut moves back off the low
half of a surrogate pair, so it never lands inside one and no reply starts
with an unpaired surrogate.
Usage Example
Call
winedbg_startwith{"args": ["myapp.exe"]}.Call
winedbg_executewith{"command": "break main"}.Call
winedbg_executewith{"command": "run"}.Call
winedbg_executewith{"command": "bt"}to get a backtrace.Call
winedbg_stopwhen finished.
The debugger gets a process group of its own, and stopping signals the whole
group, so the program under debug does not outlive the debugger that owns it. A
winedbg_stop asks with SIGTERM and escalates to SIGKILL two seconds later.
When the server itself is exiting there is no time left to ask, so it signals
SIGKILL to the group outright rather than leave a debuggee behind.
Command line
The server takes no positional arguments and no options other than the two below. Everything else is the environment, and a flag does not exist to override it, so a client configuration cannot pass a misspelled flag and have the server start on the defaults anyway.
Usage: winedbg-mcp [OPTION]
Options:
-h, --help Print this help and exit
-V, --version Print the version and exit-- ends the options. The server takes no operands, so anything after it is
refused the same way an unknown flag is.
Invocation | Stream | Exit |
| Serves JSON-RPC on stdin/stdout | 0 on SIGINT, SIGTERM, or end of stdin, after winedbg and the debuggee it started are waited for (up to 6s) |
| Help on stdout | 0 |
| The | 0 |
| The offending argument, the one-line usage, and a pointer to | 2 |
| The reason and the variable, on stderr | 1 |
stdout carries protocol traffic and nothing else, so winedbg-mcp --help | less
and winedbg-mcp --version both behave, and every diagnostic goes to stderr.
The command line is resolved before the environment, so --help and
--version still work in a deployment whose WINEDBG_MCP_* value the server
would otherwise refuse to start on.
Tests
bun run check is the gate for this tree: Biome, then shellcheck over
scripts/, then the two type-check passes, then the suite. bun run typecheck
and bun test are the pieces it runs, for iterating on one of them at a time.
CI runs the build as well, so a
tree that type-checks but does not emit is red there rather than at release.
The suite runs against src/; the compiled layout gets its own check, run by
CI after bun run build.
bun run check # what CI runs
bun run lint # Biome formatting and lint rules, then shellcheck
bun run format # Biome autofix: formatting, imports and every safe rule fix
bun run typecheck # tsc on src/, then on src/ + tests/
bun run build # tsc, then the executable build/index.js
bun test
scripts/verify-artifact.shThe suite is one bun test over tests/, so the loop while editing is one file
or one test rather than the lot:
bun test tests/session.test.ts # one file
bun test -t "rejects a NUL" # every test whose name matches, in any filetests/session.test.ts drives WinedbgSession against tests/fake-winedbg.js,
a stand-in that speaks the same Wine-dbg> prompt protocol, so the suite runs
without Wine installed. It spawns a real child process and drives its stdio, so
a failure is a real spawn, stream or lifecycle failure rather than a mock
disagreeing, and it waits on real time, which is what covers the parts a
simulator cannot: a pipe the debugger stops reading, a signal with no exit code,
a debuggee that outlives its debugger.
tests/simulation.test.ts covers the same state machine with no process and no
host clock. It supplies its own SessionRuntime (see src/runtime.ts): a
virtual clock the session also measures against, a debugger in memory, and the
process-group probe the wait after a stop makes, answered from the fake's own
state rather than from the host's process table. One seed chooses every reply
delay, chunking pattern, crash and kill outcome, so a run is reproducible and a
failure prints the seed that produced it:
WINEDBG_MCP_SIM_SEED=1014 bun test tests/simulation.test.tstests/concurrency.test.ts drives callTool the way the server does, with
several tool calls in flight at once on one session: starts racing each other,
two commands racing for the single command slot, a stop racing an in-flight
command, and a burst where every answer names the command that asked for it.
The server hands tool calls in from the event loop without serializing them, so
that is the case worth pinning.
tests/validate.test.ts covers the tool-argument boundary and
tests/config.test.ts the environment parsing described above.
tests/cli.test.ts spawns the entry point to pin the exit codes and which
stream each message lands on, and tests/server.test.ts spawns it to speak
JSON-RPC over its stdio: the handshake, the tool list on the wire, a
start/execute/stop round trip, both kinds of refused call, the callId each
outcome is logged under, and the exit 0 a client hanging up gets. Between them
the two files reach src/index.ts, the one module no unit test can cover.
CI runs the same bun run check and bun run build on every push and pull
request, so a green local run is a green remote run. bun run typecheck covers
src/ on its own, which is the set that ships as build/, and then re-checks it
together with tests/ so a mistyped test helper fails the build rather than the
suite.
Formatting is Biome's, and the line width is 120 columns, the width the tree was
already written to. src/index.ts keeps one scoped noConsole suppression:
stdout carries the MCP JSON-RPC stream, so the configuration failure has to go to
stderr. Everything else it writes goes through the logger, which writes to
stderr by construction (src/logger.ts:45-48).
Troubleshooting
Every tool failure comes back as Error: <message> with the message the code
produced, so the text names the state to fix:
Message | Cause |
| A command ran with no live session, including one whose |
|
|
| One command per call, and the previous one has not answered yet. The message names that command and tells the caller to wait for its reply |
| An environment value the server cannot use, named in the message. The server exits with status 1 instead of starting on defaults |
| No prompt within |
| A start that was waiting for the previous debugger to be gone was cancelled, by |
| The command carried a line break or a NUL. One command per call, one line per command |
| An argv entry carried a NUL, which the C runtime cuts at. It names the index so the offending argument can be found |
| An argument past its bound. Shorten it, or split the work across calls |
License
ISC, declared in package.json. This tree ships no LICENSE file, so the full
terms are stated here only.
This server cannot be deployed
Maintenance
Related MCP Connectors
Agent Replay Debugger MCP — record every agent step + deterministic replay. Step-debugger for
Live browser debugging for AI assistants — DOM, console, network via MCP.
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Hyperbrowser MCP — wraps the Hyperbrowser AI-agent browsing API
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables dynamic debugging with GDB via the MCP protocol, allowing LLMs to execute GDB commands, manage breakpoints, control execution, and inspect program state.4-
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to interact with GDB for debugging via the MCP protocol. Supports setting breakpoints, stepping through code, inspecting memory and registers, and more.85MIT
- AlicenseNot gradedqualityCmaintenanceEnables headless debugging of Windows executables from Linux/macOS hosts by orchestrating winedbg's gdbserver and a GDB client, exposing 19 tools for launch, attach, breakpoints, stepping, register/memory access, and session lifecycle.MIT
- AlicenseBqualityCmaintenanceMCP server that wraps gdb to enable LLMs to drive live debugging sessions, including starting sessions on binaries, attaching to processes, and running commands.71,214 PyPI3MIT