In Part 1 of this article series, I demonstrated the configuration steps for using natural language processing in analyzing a Windows crash dump. In this blog, I dive far deeper, using vibe coding to extend the use of MCP for Windows kernel debugging.
Part 1 of this blog series built upon the work developed by Sven Scharmentke, who wrote the fascinating article entitled The Future of Crash Analysis: AI Meets WinDBG. His GitHub repository, mcp-debug, contains the code that uses AI to analyze Windows crash dumps and perform user-space debugging using Microsoft’s “CDB” utility. Specifically, it uses Model Context Protocol (MCP) as an interface with an LLM and GitHub Copilot to do some amazing things: taking debugging into the 21st century, and eliminating the arcane command set tribal knowledge of the WinDbg utilities that are the purview of very few deeply experienced engineers. This is groundbreaking material: it makes advanced technology, akin to magic, much more accessible to the many, not just the few.
As I tinkered with Sven’s code, I began to wonder: could it be extended to accommodate deep Windows kernel interactive debugging as well? In my previous work, I used JTAG extensively to explore the kernel using ASSET’s SourcePoint product on a remote AAEON UP Xtreme i11 Tiger Lake board. This is a very powerful combination. But SourcePoint has a learning curve as well, and although it has many advantages, it lacks some of the capabilities of the Microsoft WinDbg kernel debugging tool; what if I could combine the power of WinDbg with natural language processing via LLMs to dig even deeper into the kernel? Here’s a picture of what I am trying to do:

The host PC is running GitHub Copilot within VS Code, with a connection to Claude Sonnet 4.5; and MCP is being used to convert natural language into specific WinDbg/KD commands sent to the target, extending our debugging capabilities for kernel research.
You might still ask, what’s the point? Well, this would give researchers enormous power for kernel debugging and vulnerability research. Image being able to use plain language to explore the rogue driver code as documented in some of our blog articles, such as Methodology of Reversing Vulnerable Killer Drivers by Ivan Cabrera and Understanding Out-Of-Bounds in Windows Kernel Drivers by Jay Pandya. The possibilities are endless.
Of course, this is a prodigious undertaking (otherwise someone else would have probably done it already). Combine that with Sven’s use of the Python programming language, with which I’m not currently all that familiar with. But I decided to jump in with both feet; and Python is also the language of AI, so it’s a great learning experience. That’s where the vibe coding came in. There’s nothing like getting totally hands-on and going in over your head to force us to learn! So, I began.
First of all, it was important to understand the overall structure of the source in Sven’s mcp-windbg repository. The main body of the code revolves around two files: server.py, which sets up and tears down resources for the debugging sessions and crashdumps, runs WinDbg commands, etc.; and cdb_session.py, that manages the CDB sessions, sending commands, waiting for commands to finish and triggering on prompts, etc. I quickly realized that CDB and KD (the kernel debugger I would be using) are very different in operation. I’d have to extend the functionality of server.py to accommodate how KD sessions are set up, which is quite different; and a new kd_session.py would be needed to continuously read the KD debugger’s output (which is unique), wait for prompts, send commands, etc. Sounds simple, right? Well, it wasn’t, as you’ll see.
Starting with the server, I created an additional function to mirror the existing get_or_create_session(), named get_or_create_kd_session, solely for the purpose of managing kernel debugging sessions, and assume the target is remote and accessible via TCP/IP:
def get_or_create_kd_session(kernel_connection: str,
symbols_path: Optional[str] = None,
timeout: int = 30,
kd_flags: Optional[str] = None) -> KDSession:
sid = f"kernel:{kernel_connection}"
sess = active_kernel_sessions.get(sid)
if sess is None:
sess = KDSession(kernel_connection=kernel_connection,
symbols_path=symbols_path,
timeout=float(timeout),
kd_flags=kd_flags)
sess.start()
active_kernel_sessions[sid] = sess
return sess
I also had to add a few tools:
Tool(
name="open_windbg_kernel",
description="""
Open a kernel debugging session using KD (kd.exe).
Accepts '-kl' for local kernel or transports like 'net:port=...,key=...', 'usb3:targetname=...', 'com:port=COM1,baud=115200'.
Optionally returns threads, stack, and modules on connect.
""",
inputSchema=OpenWindbgKernel.model_json_schema(),
),
Tool(
name="close_windbg_kernel",
description="""
Close a kernel debugging session previously opened with open_windbg_kernel.
""",
inputSchema=CloseWindbgKernelParams.model_json_schema(),
),
Tool(
name="send_break",
description="""
Send CTRL-C signal to break into a running kernel debugging session.
This interrupts the target and breaks into the kernel debugger.
""",
inputSchema=SendBreakParams.model_json_schema(),
),
See that send_break tool above? That was an early attempt at addressing one of the fundamental differences between a kernel and userland debugging session. The KD application first establishes a connection to a remote KDNET agent running on the target; and then one must reset the target in order to break in.
What this looks like is that when the target is in a Running state, and you do an open_windbg_kernel, you get this out this text via stdio:
PS C:\Users\alans> kd -k net:port=50000,key=cja5yc9a64kf.2hmf45lejxq8z.3or47kcoz7uc4.3a6e8x9lpigeo
************* Preparing the environment for Debugger Extensions Gallery repositories **************
ExtensionRepository : Implicit
UseExperimentalFeatureForNugetShare : true
AllowNugetExeUpdate : true
NonInteractiveNuget : true
AllowNugetMSCredentialProviderInstall : true
AllowParallelInitializationOfLocalRepositories : true
EnableRedirectToV8JsProvider : false
-- Configuring repositories
----> Repository : LocalInstalled, Enabled: true
----> Repository : UserExtensions, Enabled: true
>>>>>>>>>>>>> Preparing the environment for Debugger Extensions Gallery repositories completed, duration 0.015 seconds
************* Waiting for Debugger Extensions Gallery to Initialize **************
>>>>>>>>>>>>> Waiting for Debugger Extensions Gallery to Initialize completed, duration 0.360 seconds
----> Repository : UserExtensions, Enabled: true, Packages count: 0
----> Repository : LocalInstalled, Enabled: true, Packages count: 29
Microsoft (R) Windows Debugger Version 10.0.26100.6584 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.
Using NET for debugging
Opened WinSock 2.0
Kernel Debug Target Status: [no_debuggee]; Retries: [0] times in last [7] seconds.
Waiting to reconnect...
Connected to target 192.168.68.55 on port 50000 on local IP 192.168.68.81.
You can get the target MAC address by running .kdtargetmac command.
And then you need to go over to the target and manually reset it, typically with “shutdown -r -t 0” from a CMD window. Then you get a bunch more text in immediately:
Connected to Windows 10 26100 x64 target at (Tue Nov 11 14:34:36.979 2025 (UTC - 6:00)), ptr64 TRUE
Kernel Debugger connection established.
Symbol search path is: srv*
Executable search path is:
Windows 10 Kernel Version 26100 MP (4 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Edition build lab: 26100.1.amd64fre.ge_release.240331-1435
Kernel base = 0xfffff800`7f200000 PsLoadedModuleList = 0xfffff800`800f4f10
Debug session time: Tue Nov 11 14:34:54.996 2025 (UTC - 6:00)
System Uptime: 0 days 0:14:04.732
Shutdown occurred at (Tue Nov 11 14:34:39.868 2025 (UTC - 6:00))...unloading all symbol tables.
Using NET for debugging
Opened WinSock 2.0
Waiting to reconnect...
Connected to target 192.168.68.55 on port 50000 on local IP 192.168.68.81.
You can get the target MAC address by running .kdtargetmac command.
Connected to Windows 10 26100 x64 target at (Tue Nov 11 14:34:52.789 2025 (UTC - 6:00)), ptr64 TRUE
Kernel Debugger connection established.
************* Path validation summary **************
Response Time (ms) Location
Deferred srv*
Symbol search path is: srv*
Executable search path is:
Windows 10 Kernel Version 26100 MP (1 procs) Free x64
Edition build lab: 26100.1.amd64fre.ge_release.240331-1435
Kernel base = 0xfffff802`afc00000 PsLoadedModuleList = 0xfffff802`b0af4f10
System Uptime: 0 days 0:00:01.377
KDTARGET: Refreshing KD connection
DriverEntry failed 0xc00000bb for driver \REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\NetworkPrivacyPolicy
Finally, a CTRL-C is necessary to break into the target and put it into a halted state. Then the debugger banner shows up, and you see the below:
Break instruction exception - code 80000003 (first chance)
*******************************************************************************
* *
* You are seeing this message because you pressed either *
* CTRL+C (if you run console kernel debugger) or, *
* CTRL+BREAK (if you run GUI kernel debugger), *
* on your debugger machine's keyboard. *
* *
* THIS IS NOT A BUG OR A SYSTEM CRASH *
* *
* If you did not intend to break into the debugger, press the "g" key, then *
* press the "Enter" key now. This message might immediately reappear. If it *
* does, press "g" and "Enter" again. *
* *
*******************************************************************************
nt!DbgBreakPointWithStatus:
fffff802`b00f7240 cc int 3
2: kd>
This is clunky, and somewhat hard to write code for!
But, before I get back to this, I also had to create a new kd_session.py, that had the _reader, _collect_until_prompt, sync, and other routines. Although this was a very short routine, it turned out to be a major stumbling block, and ultimately stopped me from successfully implementing the solution. Although I could successfully connect to the target, for example with:
kd -k net:port=50000,key=cja5yc9a64kf.2hmf45lejxq8z.3or47kcoz7uc4.3a6e8x9lpigeo
I could initially not get the code to send a CTRL-BREAK or CTRL-C, because this keyboard-driven interrupt could not be sent through the interface. A CTRL-C keyboard sequence cannot be sent through the MCP WinDbg tools; the tools only support sending text commands to the debugger, not raw keyboard input or control characters. And using the “-b” parameter with the KD command sequence to break upon connection didn’t work—it’s been deprecated. So even when I overcame this, I still only got an intermittent connection to the target, and MCP would lose sync with the target state; sometimes I could get one or two commands to work, and then the host would suddenly think that the target was in a Running state, precluding further debugging actions.
Want to see what some of the vibe coding and debugging looked like as I worked on this project? If so, check out our video here:
After a few days of working on this, even AI was ready to throw in the towel: in GitHub Copilot’s own words:
Unfortunately, CTRL-BREAK also doesn’t halt the target. All three approaches have failed:
❌ os.kill(signal.CTRL_C_EVENT) – doesn’t halt target
❌ stdin.write(b’\x03′) – doesn’t halt target
❌ os.kill(signal.CTRL_BREAK_EVENT) – doesn’t halt target
The root cause is fundamental: kd.exe is designed for interactive console use, but when run via subprocess.Popen with redirected I/O (stdin=PIPE, stdout=PIPE), it loses the console control signal handling that makes CTRL-C/CTRL-BREAK work.
The architectural issue:
CREATE_NEW_PROCESS_GROUP flag (line 62) is needed to send signals via os.kill()
But this same flag isolates the process from receiving console control signals properly
kd.exe expects direct console attachment, not redirected pipes
Recommendation: Accept that programmatic break-in isn’t feasible with the current subprocess architecture. Users must manually press CTRL-C in the console window for interactive debugging.
Should I:
Update documentation to clearly state manual CTRL-C is required for breaking into running targets
Keep send_break() as “best effort” with the warning already added
Remove send_break() entirely since it doesn’t work reliably
Or would you like to explore alternative approaches like using winpty (adds dependency) or Windows API calls to attach a console?
And I tried many other things. For example, I created a separate CMD window to launch KD, and then issued a manual CTRL-C to break into the target, then quit, and let Copilot/MCP take over. But still no joy. After many hours of experimentation, I was ready to throw in the towel as well.
But then I decided to approach it differently, and go to 30,000 feet, by asking GitHub Copilot if there was a better overall approach. After some back-and-forth, it came back with an intriguing new option:
Option 2: MCP Controls WinDbg via COM/DbgEng (Best Long-term)
Instead of subprocess with kd.exe, use the DbgEng.dll COM interface:
- MCP would use the same debugging engine as WinDbg
- Could programmatically send break commands
- No stdio redirection issues
- This is how serious debugging tools are built
Code sketch:
import comtypes
from comtypes.client import CreateObject
# Use DbgEng COM interface
client = CreateObject(“DbgEng.DebugClient”)
control = client.QueryInterface(IDebugControl)
control.SetInterrupt(DEBUG_INTERRUPT_ACTIVE) # Send break
For production use:
Implement Option 2 (DbgEng COM). This would:
- ✓ Allow programmatic break commands
- ✓ Eliminate stdio buffering issues
- ✓ Provide reliable state tracking
- ✓ Match how WinDbg itself works internally
- ✗ More complex implementation
The DbgEng approach is what professional debugging tools like WinDbgX, Visual Studio debugger, and commercial tools use. It’s the “right” way to build programmatic debugging interfaces on Windows.
Would you like me to explore the DbgEng COM approach? It would solve both the break-in problem and the buffering issues we’ve been fighting with kd.exe stdio.
That sounds wonderful! So, that’s what I’m going to try next. Stay tuned for the next article on this subject.