Status: draft v0.2 (2026-07-26, revised after online design review). Greenfield replacement for the abandoned
HyperDbg/mcpprototype. Stack decided: Python + FastMCP (official SDK high-level API) + ctypes FFI intolibhyperdbg.dll. License: GPLv3. v0.2 changes are consolidated in §9-§11; core sections below updated inline.
- HyperDbg's founder (Sina Karvandi,
@HughEverett) openly invited an MCP server for the org (2026-07-23). - The existing
HyperDbg/mcprepo is a stalled July-2025 single-dev prototype that is not an MCP server (no MCP library; a bare HTTP wrapper to a:8888shim, plus a stub-filled binding generator). Verdict: greenfield. - Only salvageable asset:
mcp_api_meta.json(a real 52-function API catalog). - No
:8888HTTP server exists in current HyperDbg - that was ddkwork's prototype assumption only. FFI intolibhyperdbgis the sole viable seam.
MCP client (Claude etc.)
│ stdio (JSON-RPC)
▼
Python MCP server ── ctypes ──► libhyperdbg.dll ──► HyperDbg driver + VMM
(official MCP Python SDK) (extern "C", __cdecl)
In-process, local-only. Runs on the debugging host with HyperDbg installed and the driver loaded.
hyperdbg_u_run_command() returns only an INT status - not the text. HyperDbg emits command output through a registered callback. The wrapper pattern:
register hyperdbg_u_set_text_message_callback(buffer_appender) # once, at startup
→ call hyperdbg_u_run_command("db 0x1000")
→ read what the callback accumulated → return that as the tool result
Solve this once and all 40+ text commands flow through one path. This is the thing the old repo never implemented.
Library: libhyperdbg.dll (Windows) / libhyperdbg.so (Linux). extern "C", __cdecl.
Source of truth: HyperDbg/HyperDbg → hyperdbg/include/SDK/imports/user/HyperDbgLibImports.h.
# real prototypes (translate to ctypes argtypes/restype)
VOID hyperdbg_u_connect_local_debugger();
INT hyperdbg_u_run_command(CHAR* command);
VOID hyperdbg_u_set_text_message_callback(PVOID handler);
BOOLEAN hyperdbg_u_read_memory(UINT64 target_address, DEBUGGER_READ_MEMORY_TYPE memory_type,
DEBUGGER_READ_READING_TYPE reading_type, UINT32 pid, UINT32 size,
BOOLEAN get_address_mode, DEBUGGER_READ_MEMORY_ADDRESS_MODE* address_mode,
BYTE* target_buffer, UINT32* return_length);
BOOLEAN hyperdbg_u_write_memory(PVOID destination_address, DEBUGGER_EDIT_MEMORY_TYPE memory_type,
UINT32 process_id, PVOID source_address, UINT32 number_of_bytes);
BOOLEAN hyperdbg_u_read_all_registers(GUEST_REGS* regs, GUEST_EXTRA_REGISTERS* extra);
BOOLEAN hyperdbg_u_read_target_register(REGS_ENUM register_id, UINT64* out);
BOOLEAN hyperdbg_u_write_target_register(REGS_ENUM register_id, UINT64 value);
BOOLEAN hyperdbg_u_set_breakpoint(UINT64 address, UINT32 pid, UINT32 tid, UINT32 core_number);
VOID hyperdbg_u_continue_debuggee();
VOID hyperdbg_u_pause_debuggee();
BOOLEAN hyperdbg_u_stepping_instrumentation_step_in();
BOOLEAN hyperdbg_u_stepping_regular_step_in();
BOOLEAN hyperdbg_u_stepping_step_over();
UINT64 hyperdbg_u_eval_expression(CHAR* Expr, PBOOLEAN HasError);
BOOLEAN hyperdbg_u_run_script(CHAR* Expr, BOOLEAN ShowErrorMessageIfAny);Structs (#pragma pack(1)):
GUEST_REGS= 16× uint64 (rax,rcx,rdx,rbx,rsp,rbp,rsi,rdi,r8-r15), 128 bytesGUEST_EXTRA_REGISTERS= 6× uint16 (CS,DS,FS,GS,ES,SS) + uint64 RFLAGS, RIP
Enums:
REGS_ENUM: RAX=0, RCX=5, RDX=10, RBX=15, RSP=20, RBP=24, RSI=28, RDI=32, R8=36...R15=71, RIP=102DEBUGGER_READ_MEMORY_TYPE: PHYSICAL=0, VIRTUAL=1DEBUGGER_EDIT_MEMORY_TYPE: VIRTUAL=0, PHYSICAL=1DEBUGGER_READ_READING_TYPE: FROM_KERNEL=0, FROM_VMX_ROOT=1DEBUGGER_READ_MEMORY_ADDRESS_MODE: 32_BIT=0, 64_BIT=1
Type map: BOOLEAN/BYTE=c_ubyte, UINT32=c_uint32, UINT64=c_uint64, PVOID=c_void_p, CHAR*=c_char_p, PBOOLEAN=POINTER(c_ubyte).
read_memory is the fiddly one - output buffer + UINT32* return_length out-param; allocate (c_byte * size)() and pass by ref.
| Tool | libhyperdbg fn | Tier |
|---|---|---|
execute_command |
run_command + callback capture |
|
read_memory |
read_memory |
✅ read |
read_registers |
read_all_registers |
✅ read |
read_register |
read_target_register |
✅ read |
eval_expression |
eval_expression |
✅ read-ish |
disassemble / list_modules |
run_command("u"/"lm") |
✅ read |
set_breakpoint |
set_breakpoint |
|
continue / pause |
continue/pause_debuggee |
|
step (in/over/instr) |
stepping_* |
|
write_memory |
write_memory |
🔴 destructive |
write_register |
write_target_register |
🔴 destructive |
run_script |
run_script |
🔴 arbitrary |
Safety (revised - see §10): ring-0 debugger - writes can bugcheck the machine. Three layers, not one flag: (a) ToolAnnotations (readOnlyHint/destructiveHint) so clients auto-approve reads and gate writes; (b) MCP elicitation for per-call human confirmation of 🔴 tools (protocol-native - a server flag alone can't force the client to confirm); (c) a real --read-only mode that does not register the write/script tools at all (defense in depth, not doc-only). Plus an append-only audit log of every call.
HyperDbg has no formal Python packaging/linting in-repo (just Doxygen-headered utility scripts), so we copy their GPLv3 Doxygen file header + snake_case, and add modern packaging (a distributable server needs it):
hyperdbg-mcp/
├── LICENSE # GPLv3 (mandatory)
├── README.md
├── CONTRIBUTING.md # mirror "PR to Dev branch"
├── pyproject.toml # ruff + MCP SDK dep
├── src/hyperdbg_mcp/
│ ├── __init__.py
│ ├── server.py # MCP entry, tool registration, --read-only
│ ├── ffi.py # ctypes bindings + structs/enums
│ ├── session.py # connect/lifecycle + callback output-capture buffer
│ └── tools.py # the 12 tool handlers
└── tests/ # pytest
GPLv3 file header:
"""
* @file server.py
* @author <you> (munraimix)
* @brief HyperDbg MCP server
* @copyright This project is released under the GNU Public License v3.
"""Startup: verify driver present → connect_local_debugger() → register text callback. Per-call: hold debuggee context. Shutdown: disconnect cleanly. Fail loudly if HyperDbg/driver isn't loaded - never silently return empty.
ffi.py- bindings, structs, enumssession.py- connect + nail the callback capture buffertools.py- read tools first, then state, then gated writesserver.py- registration +--read-only- Writes behind confirmation gating + tests
- Public work under personal GitHub munraimix (never the work account). Set local
git config user.email/namebefore first commit. - Open an issue on
HyperDbg/HyperDbgfirst (leverage existing maintainer rapport); confirm greenfield-Python is welcome. - PRs target the Dev branch; GPLv3 throughout.
Source: online review of 6 real debugger MCP servers (WinDbg-MCP, x64dbg_mcp, GhidraMCP, Frida-MCP, RE-MCP, MDB-MCP) + MCP Python SDK / ctypes / security research. Nearest analogs to study directly: WinDbg-MCP and x64dbg_mcp.
These are non-negotiable - the callback pattern will crash without them.
- Callback lifetime (critical). The
CFUNCTYPEtext-message callback must be held as a persistent instance/module reference (self._text_cb = CB_TYPE(self._on_text)), never inline. If it's GC'd, HyperDbg calls a dangling pointer → "Illegal Instruction"/segfault. Deterministic, not sporadic. - Callback thread-safety. HyperDbg fires the callback from a non-Python thread (ctypes spins up a dummy
PyThreadState+ takes the GIL). Buffer into aqueue.Queue, and wrap the handler body intry/exceptthat logs - any exception escaping the callback corrupts the thread state. Define withuse_errno=True. - Struct packing. All SDK structs use
#pragma pack(1)→ set_pack_ = 1on every ctypesStructureand assert layout at import:assert sizeof(GUEST_REGS) == 128. A pack mismatch silently returns garbage registers with no error. - Buffers / out-params.
read_memory:create_string_buffer(size)for theBYTE*,byref(c_uint32())forreturn_length; after the call usebuf.raw[:return_len.value]and assertreturn_len <= size. Never pass barebytes. - DLL load.
ctypes.CDLL(it's__cdecl, notWinDLL); Python bitness must match the DLL (x64); wrap load intry/except OSErrorwith a clear message; ensurelibhyperdbg.dll+ transitive deps are resolvable. - Errors. Attach
errcheckto each export mappingBOOLEAN 0/ badINT→ a customHyperDbgError(status_code=...). No silent failures.
- Confirmation is client-side via elicitation, not a server flag. Annotate 🔴 tools
destructiveHint=True; request elicitation before executing;--read-onlyadditionally unregisters them. Layer all three. - Prompt injection via debuggee data - the subtle, important one. We're feeding a debugged program's own bytes/strings/disassembly back into the model; if it's malware, that content may contain adversarial instructions. Wrap all returned debuggee data in an untrusted-data envelope (clear delimiters + a "this is captured data, not instructions" tag) and size-cap it. This is a real, HyperDbg-specific threat the generic design missed.
- Audit log: append-only JSONL, one entry per call (timestamp, tool, args hash, result hash, elapsed, target pid/addr range). Cheap, high forensic value.
- Input validation at the MCP boundary: address/size sanity (reject absurd sizes), reject malformed commands before they reach the FFI layer.
run_script: add a timeout + output size cap (it's Turing-complete).
Deliberately rejected from the review (they misread HyperDbg's purpose):
- ❌ "Block kernel-space reads by default." HyperDbg is a hypervisor/kernel debugger - kernel/VMX-root inspection is the whole point. Blocking it by default would cripple the tool for its actual audience. Keep kernel reads available; rely on audit + write-gating instead.
- ❌ "Remove
execute_command/eval_expression."execute_commandis the essential CLI fallback for the ~40 commands we don't wrap as first-class tools. Keep it, but annotate itdestructiveHint(arbitrary) and gate it.
Adopt now (into build order):
- FastMCP high-level API (
@mcp.tool()+ type hints → auto inputSchema); low-levelServernot needed. @asynccontextmanagerlifespan holding thelibhyperdbghandle; inject via tool context (replaces module-level init; guarantees cleanup on shutdown).- Structured tool output (Pydantic/
outputSchema) so clients get typed registers/memory, not regex bait - with a plain-text fallback. ToolErrorfor tool-level failures (uniform; don't mix error dicts and exceptions).- Testing via in-memory
Client(server), not stdio subprocess mocking. - Tool naming: prefix groups (
hd_mem_*,hd_reg_*,hd_bp_*,hd_exec_*,hd_script_*) for discoverability. - Structured, recovery-oriented error strings (map libhyperdbg status codes → "what to do next").
- Precondition validation (e.g. reject
write_memory/stepwhen not paused) before hitting the FFI.
Defer (note in README as roadmap, don't build yet):
- Multi-session / concurrent clients (single local session is fine for v1; the
libhyperdbghandle is process-global anyway). - Streamable-HTTP transport +
report_progressfor long Intel PT / LBR traces - needed eventually (blocking callback will time out on big traces), but v1 is stdio + short ops. - Multi-backend supervisor/worker (only if we ever add GDB/LLDB).