mseep-mcp-server-windbg 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ from .server import serve
2
+
3
+ def main():
4
+ """MCP WinDBG Server - Windows crash dump analysis functionality for MCP"""
5
+ import argparse
6
+ import asyncio
7
+
8
+ parser = argparse.ArgumentParser(
9
+ description="Give a model the ability to analyze Windows crash dumps with WinDBG/CDB"
10
+ )
11
+ parser.add_argument("--cdb-path", type=str, help="Custom path to cdb.exe")
12
+ parser.add_argument("--symbols-path", type=str, help="Custom symbols path")
13
+ parser.add_argument("--timeout", type=int, default=30, help="Command timeout in seconds")
14
+ parser.add_argument("--verbose", action="store_true", help="Enable verbose output")
15
+
16
+ args = parser.parse_args()
17
+ asyncio.run(serve(
18
+ cdb_path=args.cdb_path,
19
+ symbols_path=args.symbols_path,
20
+ timeout=args.timeout,
21
+ verbose=args.verbose
22
+ ))
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main()
@@ -0,0 +1,5 @@
1
+ # __main__.py
2
+
3
+ from mcp_server_windbg import main
4
+
5
+ main()
@@ -0,0 +1,253 @@
1
+ import subprocess
2
+ import threading
3
+ import re
4
+ import os
5
+ import platform
6
+ from typing import List, Optional
7
+
8
+ # Regular expression to detect CDB prompts
9
+ PROMPT_REGEX = re.compile(r"^\d+:\d+>\s*$")
10
+
11
+ # Command marker to reliably detect command completion
12
+ COMMAND_MARKER = ".echo COMMAND_COMPLETED_MARKER"
13
+ COMMAND_MARKER_PATTERN = re.compile(r"COMMAND_COMPLETED_MARKER")
14
+
15
+ # Default paths where cdb.exe might be located
16
+ DEFAULT_CDB_PATHS = [
17
+ r"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe",
18
+ r"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe",
19
+ r"C:\Program Files\Debugging Tools for Windows (x64)\cdb.exe",
20
+ r"C:\Program Files\Debugging Tools for Windows (x86)\cdb.exe",
21
+ ]
22
+
23
+ class CDBError(Exception):
24
+ """Custom exception for CDB-related errors"""
25
+ pass
26
+
27
+ class CDBSession:
28
+ def __init__(
29
+ self,
30
+ dump_path: Optional[str] = None,
31
+ remote_connection: Optional[str] = None,
32
+ cdb_path: Optional[str] = None,
33
+ symbols_path: Optional[str] = None,
34
+ initial_commands: Optional[List[str]] = None,
35
+ timeout: int = 10,
36
+ verbose: bool = False,
37
+ additional_args: Optional[List[str]] = None
38
+ ):
39
+ """
40
+ Initialize a new CDB debugging session.
41
+
42
+ Args:
43
+ dump_path: Path to the crash dump file (mutually exclusive with remote_connection)
44
+ remote_connection: Remote debugging connection string (e.g., "tcp:Port=5005,Server=192.168.0.100")
45
+ cdb_path: Custom path to cdb.exe. If None, will try to find it automatically
46
+ symbols_path: Custom symbols path. If None, uses default Windows symbols
47
+ initial_commands: List of commands to run when CDB starts
48
+ timeout: Timeout in seconds for waiting for CDB responses
49
+ verbose: Whether to print additional debug information
50
+ additional_args: Additional arguments to pass to cdb.exe
51
+
52
+ Raises:
53
+ CDBError: If cdb.exe cannot be found or started
54
+ FileNotFoundError: If the dump file cannot be found
55
+ ValueError: If invalid parameters are provided
56
+ """
57
+ # Validate that exactly one of dump_path or remote_connection is provided
58
+ if not dump_path and not remote_connection:
59
+ raise ValueError("Either dump_path or remote_connection must be provided")
60
+ if dump_path and remote_connection:
61
+ raise ValueError("dump_path and remote_connection are mutually exclusive")
62
+
63
+ if dump_path and not os.path.isfile(dump_path):
64
+ raise FileNotFoundError(f"Dump file not found: {dump_path}")
65
+
66
+ self.dump_path = dump_path
67
+ self.remote_connection = remote_connection
68
+ self.timeout = timeout
69
+ self.verbose = verbose
70
+
71
+ # Find cdb executable
72
+ self.cdb_path = self._find_cdb_executable(cdb_path)
73
+ if not self.cdb_path:
74
+ raise CDBError("Could not find cdb.exe. Please provide a valid path.")
75
+
76
+ # Prepare command args
77
+ cmd_args = [self.cdb_path]
78
+
79
+ # Add connection type specific arguments
80
+ if self.dump_path:
81
+ cmd_args.extend(["-z", self.dump_path])
82
+ elif self.remote_connection:
83
+ cmd_args.extend(["-remote", self.remote_connection])
84
+
85
+ # Add symbols path if provided
86
+ if symbols_path:
87
+ cmd_args.extend(["-y", symbols_path])
88
+
89
+ # Add any additional arguments
90
+ if additional_args:
91
+ cmd_args.extend(additional_args)
92
+
93
+ try:
94
+ self.process = subprocess.Popen(
95
+ cmd_args,
96
+ stdin=subprocess.PIPE,
97
+ stdout=subprocess.PIPE,
98
+ stderr=subprocess.STDOUT,
99
+ text=True,
100
+ bufsize=1
101
+ )
102
+ except Exception as e:
103
+ raise CDBError(f"Failed to start CDB process: {str(e)}")
104
+
105
+ self.output_lines = []
106
+ self.lock = threading.Lock()
107
+ self.ready_event = threading.Event()
108
+ self.reader_thread = threading.Thread(target=self._read_output)
109
+ self.reader_thread.daemon = True
110
+ self.reader_thread.start()
111
+
112
+ # Wait for CDB to initialize by sending an echo marker
113
+ try:
114
+ self._wait_for_prompt(timeout=self.timeout)
115
+ except CDBError:
116
+ self.shutdown()
117
+ raise CDBError("CDB initialization timed out")
118
+
119
+ # Run initial commands if provided
120
+ if initial_commands:
121
+ for cmd in initial_commands:
122
+ self.send_command(cmd)
123
+
124
+ def _find_cdb_executable(self, custom_path: Optional[str] = None) -> Optional[str]:
125
+ """Find the cdb.exe executable"""
126
+ if custom_path and os.path.isfile(custom_path):
127
+ return custom_path
128
+
129
+ # If we're on Windows, try the default paths
130
+ if platform.system() == "Windows":
131
+ for path in DEFAULT_CDB_PATHS:
132
+ if os.path.isfile(path):
133
+ return path
134
+
135
+ return None
136
+
137
+ def _read_output(self):
138
+ """Thread function to continuously read CDB output"""
139
+ if not self.process or not self.process.stdout:
140
+ return
141
+
142
+ buffer = []
143
+ try:
144
+ for line in self.process.stdout:
145
+ line = line.rstrip()
146
+ if self.verbose:
147
+ print(f"CDB > {line}")
148
+
149
+ with self.lock:
150
+ buffer.append(line)
151
+ # Check if the marker is in this line
152
+ if COMMAND_MARKER_PATTERN.search(line):
153
+ # Remove the marker line itself
154
+ if buffer and COMMAND_MARKER_PATTERN.search(buffer[-1]):
155
+ buffer.pop()
156
+ self.output_lines = buffer
157
+ buffer = []
158
+ self.ready_event.set()
159
+ except (IOError, ValueError) as e:
160
+ if self.verbose:
161
+ print(f"CDB output reader error: {e}")
162
+
163
+ def _wait_for_prompt(self, timeout=None):
164
+ """Wait for CDB to be ready for commands by sending a marker"""
165
+ try:
166
+ self.ready_event.clear()
167
+ self.process.stdin.write(f"{COMMAND_MARKER}\n")
168
+ self.process.stdin.flush()
169
+
170
+ if not self.ready_event.wait(timeout=timeout or self.timeout):
171
+ raise CDBError(f"Timed out waiting for CDB prompt")
172
+ except IOError as e:
173
+ raise CDBError(f"Failed to communicate with CDB: {str(e)}")
174
+
175
+ def send_command(self, command: str, timeout: Optional[int] = None) -> List[str]:
176
+ """
177
+ Send a command to CDB and return the output
178
+
179
+ Args:
180
+ command: The command to send
181
+ timeout: Custom timeout for this command (overrides instance timeout)
182
+
183
+ Returns:
184
+ List of output lines from CDB
185
+
186
+ Raises:
187
+ CDBError: If the command times out or CDB is not responsive
188
+ """
189
+ if not self.process:
190
+ raise CDBError("CDB process is not running")
191
+
192
+ self.ready_event.clear()
193
+ with self.lock:
194
+ self.output_lines = []
195
+
196
+ try:
197
+ # Send the command followed by our marker to detect completion
198
+ self.process.stdin.write(f"{command}\n{COMMAND_MARKER}\n")
199
+ self.process.stdin.flush()
200
+ except IOError as e:
201
+ raise CDBError(f"Failed to send command: {str(e)}")
202
+
203
+ cmd_timeout = timeout or self.timeout
204
+ if not self.ready_event.wait(timeout=cmd_timeout):
205
+ raise CDBError(f"Command timed out after {cmd_timeout} seconds: {command}")
206
+
207
+ with self.lock:
208
+ result = self.output_lines.copy()
209
+ self.output_lines = []
210
+ return result
211
+
212
+ def shutdown(self):
213
+ """Clean up and terminate the CDB process"""
214
+ try:
215
+ if self.process and self.process.poll() is None:
216
+ try:
217
+ if self.remote_connection:
218
+ # For remote connections, send CTRL+B to detach
219
+ self.process.stdin.write("\x02") # CTRL+B
220
+ self.process.stdin.flush()
221
+ else:
222
+ # For dump files, send 'q' to quit
223
+ self.process.stdin.write("q\n")
224
+ self.process.stdin.flush()
225
+ self.process.wait(timeout=1)
226
+ except Exception:
227
+ pass
228
+
229
+ if self.process.poll() is None:
230
+ self.process.terminate()
231
+ self.process.wait(timeout=3)
232
+ except Exception as e:
233
+ if self.verbose:
234
+ print(f"Error during shutdown: {e}")
235
+ finally:
236
+ self.process = None
237
+
238
+ def get_session_id(self) -> str:
239
+ """Get a unique identifier for this CDB session."""
240
+ if self.dump_path:
241
+ return os.path.abspath(self.dump_path)
242
+ elif self.remote_connection:
243
+ return f"remote:{self.remote_connection}"
244
+ else:
245
+ raise CDBError("Session has no valid identifier")
246
+
247
+ def __enter__(self):
248
+ """Support for context manager protocol"""
249
+ return self
250
+
251
+ def __exit__(self, exc_type, exc_val, exc_tb):
252
+ """Clean up when exiting context manager"""
253
+ self.shutdown()
@@ -0,0 +1,470 @@
1
+ import os
2
+ import traceback
3
+ import glob
4
+ import winreg
5
+ from typing import Dict, Optional
6
+
7
+ from .cdb_session import CDBSession, CDBError
8
+
9
+ from mcp.shared.exceptions import McpError
10
+ from mcp.server import Server
11
+ from mcp.server.stdio import stdio_server
12
+ from mcp.types import (
13
+ ErrorData,
14
+ TextContent,
15
+ Tool,
16
+ INVALID_PARAMS,
17
+ INTERNAL_ERROR,
18
+ )
19
+ from pydantic import BaseModel, Field, model_validator
20
+
21
+ # Dictionary to store CDB sessions keyed by dump file path
22
+ active_sessions: Dict[str, CDBSession] = {}
23
+
24
+ def get_local_dumps_path() -> Optional[str]:
25
+ """Get the local dumps path from the Windows registry."""
26
+ try:
27
+ with winreg.OpenKey(
28
+ winreg.HKEY_LOCAL_MACHINE,
29
+ r"SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps"
30
+ ) as key:
31
+ dump_folder, _ = winreg.QueryValueEx(key, "DumpFolder")
32
+ if os.path.exists(dump_folder) and os.path.isdir(dump_folder):
33
+ return dump_folder
34
+ except (OSError, WindowsError):
35
+ # Registry key might not exist or other issues
36
+ pass
37
+
38
+ # Default Windows dump location
39
+ default_path = os.path.join(os.environ.get("LOCALAPPDATA", ""), "CrashDumps")
40
+ if os.path.exists(default_path) and os.path.isdir(default_path):
41
+ return default_path
42
+
43
+ return None
44
+
45
+ class OpenWindbgDump(BaseModel):
46
+ """Parameters for analyzing a crash dump."""
47
+ dump_path: str = Field(description="Path to the Windows crash dump file")
48
+ include_stack_trace: bool = Field(description="Whether to include stack traces in the analysis")
49
+ include_modules: bool = Field(description="Whether to include loaded module information")
50
+ include_threads: bool = Field(description="Whether to include thread information")
51
+
52
+
53
+ class OpenWindbgRemote(BaseModel):
54
+ """Parameters for connecting to a remote debug session."""
55
+ connection_string: str = Field(description="Remote connection string (e.g., 'tcp:Port=5005,Server=192.168.0.100')")
56
+ include_stack_trace: bool = Field(default=False, description="Whether to include stack traces in the analysis")
57
+ include_modules: bool = Field(default=False, description="Whether to include loaded module information")
58
+ include_threads: bool = Field(default=False, description="Whether to include thread information")
59
+
60
+
61
+ class RunWindbgCmdParams(BaseModel):
62
+ """Parameters for executing a WinDBG command."""
63
+ dump_path: Optional[str] = Field(default=None, description="Path to the Windows crash dump file")
64
+ connection_string: Optional[str] = Field(default=None, description="Remote connection string (e.g., 'tcp:Port=5005,Server=192.168.0.100')")
65
+ command: str = Field(description="WinDBG command to execute")
66
+
67
+ @model_validator(mode='after')
68
+ def validate_connection_params(self):
69
+ """Validate that exactly one of dump_path or connection_string is provided."""
70
+ if not self.dump_path and not self.connection_string:
71
+ raise ValueError("Either dump_path or connection_string must be provided")
72
+ if self.dump_path and self.connection_string:
73
+ raise ValueError("dump_path and connection_string are mutually exclusive")
74
+ return self
75
+
76
+
77
+ class CloseWindbgDumpParams(BaseModel):
78
+ """Parameters for unloading a crash dump."""
79
+ dump_path: str = Field(description="Path to the Windows crash dump file to unload")
80
+
81
+
82
+ class CloseWindbgRemoteParams(BaseModel):
83
+ """Parameters for closing a remote debugging connection."""
84
+ connection_string: str = Field(description="Remote connection string to close")
85
+
86
+
87
+ class ListWindbgDumpsParams(BaseModel):
88
+ """Parameters for listing crash dumps in a directory."""
89
+ directory_path: Optional[str] = Field(
90
+ default=None,
91
+ description="Directory path to search for dump files. If not specified, will use the configured dump path from registry."
92
+ )
93
+ recursive: bool = Field(
94
+ default=False,
95
+ description="Whether to search recursively in subdirectories"
96
+ )
97
+
98
+
99
+ def get_or_create_session(
100
+ dump_path: Optional[str] = None,
101
+ connection_string: Optional[str] = None,
102
+ cdb_path: Optional[str] = None,
103
+ symbols_path: Optional[str] = None,
104
+ timeout: int = 30,
105
+ verbose: bool = False
106
+ ) -> CDBSession:
107
+ """Get an existing CDB session or create a new one."""
108
+ if not dump_path and not connection_string:
109
+ raise ValueError("Either dump_path or connection_string must be provided")
110
+ if dump_path and connection_string:
111
+ raise ValueError("dump_path and connection_string are mutually exclusive")
112
+
113
+ # Create session identifier
114
+ if dump_path:
115
+ session_id = os.path.abspath(dump_path)
116
+ else:
117
+ session_id = f"remote:{connection_string}"
118
+
119
+ if session_id not in active_sessions or active_sessions[session_id] is None:
120
+ try:
121
+ session = CDBSession(
122
+ dump_path=dump_path,
123
+ remote_connection=connection_string,
124
+ cdb_path=cdb_path,
125
+ symbols_path=symbols_path,
126
+ timeout=timeout,
127
+ verbose=verbose
128
+ )
129
+ active_sessions[session_id] = session
130
+ return session
131
+ except Exception as e:
132
+ raise McpError(ErrorData(
133
+ code=INTERNAL_ERROR,
134
+ message=f"Failed to create CDB session: {str(e)}"
135
+ ))
136
+
137
+ return active_sessions[session_id]
138
+
139
+
140
+ def unload_session(dump_path: Optional[str] = None, connection_string: Optional[str] = None) -> bool:
141
+ """Unload and clean up a CDB session."""
142
+ if not dump_path and not connection_string:
143
+ return False
144
+ if dump_path and connection_string:
145
+ return False
146
+
147
+ # Create session identifier
148
+ if dump_path:
149
+ session_id = os.path.abspath(dump_path)
150
+ else:
151
+ session_id = f"remote:{connection_string}"
152
+
153
+ if session_id in active_sessions and active_sessions[session_id] is not None:
154
+ try:
155
+ active_sessions[session_id].shutdown()
156
+ del active_sessions[session_id]
157
+ return True
158
+ except Exception:
159
+ return False
160
+
161
+ return False
162
+
163
+
164
+ def execute_common_analysis_commands(session: CDBSession) -> dict:
165
+ """
166
+ Execute common analysis commands and return the results.
167
+
168
+ Returns a dictionary with the results of various analysis commands.
169
+ """
170
+ results = {}
171
+
172
+ try:
173
+ results["info"] = session.send_command(".lastevent")
174
+ results["exception"] = session.send_command("!analyze -v")
175
+ results["modules"] = session.send_command("lm")
176
+ results["threads"] = session.send_command("~")
177
+ except CDBError as e:
178
+ results["error"] = str(e)
179
+
180
+ return results
181
+
182
+
183
+ async def serve(
184
+ cdb_path: Optional[str] = None,
185
+ symbols_path: Optional[str] = None,
186
+ timeout: int = 30,
187
+ verbose: bool = False,
188
+ ) -> None:
189
+ """Run the WinDBG MCP server.
190
+
191
+ Args:
192
+ cdb_path: Optional custom path to cdb.exe
193
+ symbols_path: Optional custom symbols path
194
+ timeout: Command timeout in seconds
195
+ verbose: Whether to enable verbose output
196
+ """
197
+ server = Server("mcp-windbg")
198
+
199
+ @server.list_tools()
200
+ async def list_tools() -> list[Tool]:
201
+ return [
202
+ Tool(
203
+ name="open_windbg_dump",
204
+ description="""
205
+ Analyze a Windows crash dump file using WinDBG/CDB.
206
+ This tool executes common WinDBG commands to analyze the crash dump and returns the results.
207
+ """,
208
+ inputSchema=OpenWindbgDump.model_json_schema(),
209
+ ),
210
+ Tool(
211
+ name="open_windbg_remote",
212
+ description="""
213
+ Connect to a remote debugging session using WinDBG/CDB.
214
+ This tool establishes a remote debugging connection and allows you to analyze the target process.
215
+ """,
216
+ inputSchema=OpenWindbgRemote.model_json_schema(),
217
+ ),
218
+ Tool(
219
+ name="run_windbg_cmd",
220
+ description="""
221
+ Execute a specific WinDBG command on a loaded crash dump or remote session.
222
+ This tool allows you to run any WinDBG command and get the output.
223
+ """,
224
+ inputSchema=RunWindbgCmdParams.model_json_schema(),
225
+ ),
226
+ Tool(
227
+ name="close_windbg_dump",
228
+ description="""
229
+ Unload a crash dump and release resources.
230
+ Use this tool when you're done analyzing a crash dump to free up resources.
231
+ """,
232
+ inputSchema=CloseWindbgDumpParams.model_json_schema(),
233
+ ),
234
+ Tool(
235
+ name="close_windbg_remote",
236
+ description="""
237
+ Close a remote debugging connection and release resources.
238
+ Use this tool when you're done with a remote debugging session to free up resources.
239
+ """,
240
+ inputSchema=CloseWindbgRemoteParams.model_json_schema(),
241
+ ),
242
+ Tool(
243
+ name="list_windbg_dumps",
244
+ description="""
245
+ List Windows crash dump files in the specified directory.
246
+ This tool helps you discover available crash dumps that can be analyzed.
247
+ """,
248
+ inputSchema=ListWindbgDumpsParams.model_json_schema(),
249
+ )
250
+ ]
251
+
252
+ @server.call_tool()
253
+ async def call_tool(name, arguments: dict) -> list[TextContent]:
254
+ try:
255
+ if name == "open_windbg_dump":
256
+ # Check if dump_path is missing or empty
257
+ if "dump_path" not in arguments or not arguments.get("dump_path"):
258
+ local_dumps_path = get_local_dumps_path()
259
+ dumps_found_text = ""
260
+
261
+ if local_dumps_path:
262
+ # Find dump files in the local dumps directory
263
+ search_pattern = os.path.join(local_dumps_path, "*.*dmp")
264
+ dump_files = glob.glob(search_pattern)
265
+
266
+ if dump_files:
267
+ dumps_found_text = f"\n\nI found {len(dump_files)} crash dump(s) in {local_dumps_path}:\n\n"
268
+ for i, dump_file in enumerate(dump_files[:10]): # Limit to 10 dumps to avoid clutter
269
+ try:
270
+ size_mb = round(os.path.getsize(dump_file) / (1024 * 1024), 2)
271
+ except (OSError, IOError):
272
+ size_mb = "unknown"
273
+
274
+ dumps_found_text += f"{i+1}. {dump_file} ({size_mb} MB)\n"
275
+
276
+ if len(dump_files) > 10:
277
+ dumps_found_text += f"\n... and {len(dump_files) - 10} more dump files.\n"
278
+
279
+ dumps_found_text += "\nYou can analyze one of these dumps by specifying its path."
280
+
281
+ return [TextContent(
282
+ type="text",
283
+ text=f"Please provide a path to a crash dump file to analyze.{dumps_found_text}\n\n"
284
+ f"You can use the 'list_windbg_dumps' tool to discover available crash dumps."
285
+ )]
286
+
287
+ args = OpenWindbgDump(**arguments)
288
+ session = get_or_create_session(
289
+ dump_path=args.dump_path, cdb_path=cdb_path, symbols_path=symbols_path, timeout=timeout, verbose=verbose
290
+ )
291
+
292
+ results = []
293
+
294
+ crash_info = session.send_command(".lastevent")
295
+ results.append("### Crash Information\n```\n" + "\n".join(crash_info) + "\n```\n\n")
296
+
297
+ # Run !analyze -v
298
+ analysis = session.send_command("!analyze -v")
299
+ results.append("### Crash Analysis\n```\n" + "\n".join(analysis) + "\n```\n\n")
300
+
301
+ # Optional
302
+ if args.include_stack_trace:
303
+ stack = session.send_command("kb")
304
+ results.append("### Stack Trace\n```\n" + "\n".join(stack) + "\n```\n\n")
305
+
306
+ if args.include_modules:
307
+ modules = session.send_command("lm")
308
+ results.append("### Loaded Modules\n```\n" + "\n".join(modules) + "\n```\n\n")
309
+
310
+ if args.include_threads:
311
+ threads = session.send_command("~")
312
+ results.append("### Threads\n```\n" + "\n".join(threads) + "\n```\n\n")
313
+
314
+ return [TextContent(type="text", text="".join(results))]
315
+
316
+ elif name == "open_windbg_remote":
317
+ args = OpenWindbgRemote(**arguments)
318
+ session = get_or_create_session(
319
+ connection_string=args.connection_string, cdb_path=cdb_path, symbols_path=symbols_path, timeout=timeout, verbose=verbose
320
+ )
321
+
322
+ results = []
323
+
324
+ # Get target information for remote debugging
325
+ target_info = session.send_command("!peb")
326
+ results.append("### Target Process Information\n```\n" + "\n".join(target_info) + "\n```\n\n")
327
+
328
+ # Get current state
329
+ current_state = session.send_command("r")
330
+ results.append("### Current Registers\n```\n" + "\n".join(current_state) + "\n```\n\n")
331
+
332
+ # Optional
333
+ if args.include_stack_trace:
334
+ stack = session.send_command("kb")
335
+ results.append("### Stack Trace\n```\n" + "\n".join(stack) + "\n```\n\n")
336
+
337
+ if args.include_modules:
338
+ modules = session.send_command("lm")
339
+ results.append("### Loaded Modules\n```\n" + "\n".join(modules) + "\n```\n\n")
340
+
341
+ if args.include_threads:
342
+ threads = session.send_command("~")
343
+ results.append("### Threads\n```\n" + "\n".join(threads) + "\n```\n\n")
344
+
345
+ return [TextContent(
346
+ type="text",
347
+ text="".join(results)
348
+ )]
349
+
350
+ elif name == "run_windbg_cmd":
351
+ args = RunWindbgCmdParams(**arguments)
352
+ session = get_or_create_session(
353
+ dump_path=args.dump_path, connection_string=args.connection_string,
354
+ cdb_path=cdb_path, symbols_path=symbols_path, timeout=timeout, verbose=verbose
355
+ )
356
+ output = session.send_command(args.command)
357
+
358
+ return [TextContent(
359
+ type="text",
360
+ text=f"Command: {args.command}\n\nOutput:\n```\n" + "\n".join(output) + "\n```"
361
+ )]
362
+
363
+ elif name == "close_windbg_dump":
364
+ args = CloseWindbgDumpParams(**arguments)
365
+ success = unload_session(dump_path=args.dump_path)
366
+ if success:
367
+ return [TextContent(
368
+ type="text",
369
+ text=f"Successfully unloaded crash dump: {args.dump_path}"
370
+ )]
371
+ else:
372
+ return [TextContent(
373
+ type="text",
374
+ text=f"No active session found for crash dump: {args.dump_path}"
375
+ )]
376
+
377
+ elif name == "close_windbg_remote":
378
+ args = CloseWindbgRemoteParams(**arguments)
379
+ success = unload_session(connection_string=args.connection_string)
380
+ if success:
381
+ return [TextContent(
382
+ type="text",
383
+ text=f"Successfully closed remote connection: {args.connection_string}"
384
+ )]
385
+ else:
386
+ return [TextContent(
387
+ type="text",
388
+ text=f"No active session found for remote connection: {args.connection_string}"
389
+ )]
390
+
391
+ elif name == "list_windbg_dumps":
392
+ args = ListWindbgDumpsParams(**arguments)
393
+
394
+ if args.directory_path is None:
395
+ args.directory_path = get_local_dumps_path()
396
+ if args.directory_path is None:
397
+ raise McpError(ErrorData(
398
+ code=INVALID_PARAMS,
399
+ message="No directory path specified and no default dump path found in registry."
400
+ ))
401
+
402
+ if not os.path.exists(args.directory_path) or not os.path.isdir(args.directory_path):
403
+ raise McpError(ErrorData(
404
+ code=INVALID_PARAMS,
405
+ message=f"Directory not found: {args.directory_path}"
406
+ ))
407
+
408
+ # Determine search pattern based on recursion flag
409
+ search_pattern = os.path.join(args.directory_path, "**", "*.*dmp") if args.recursive else os.path.join(args.directory_path, "*.*dmp")
410
+
411
+ # Find all dump files
412
+ dump_files = glob.glob(search_pattern, recursive=args.recursive)
413
+
414
+ # Sort alphabetically for consistent results
415
+ dump_files.sort()
416
+
417
+ if not dump_files:
418
+ return [TextContent(
419
+ type="text",
420
+ text=f"No crash dump files (*.*dmp) found in {args.directory_path}"
421
+ )]
422
+
423
+ # Format the results
424
+ result_text = f"Found {len(dump_files)} crash dump file(s) in {args.directory_path}:\n\n"
425
+ for i, dump_file in enumerate(dump_files):
426
+ # Get file size in MB
427
+ try:
428
+ size_mb = round(os.path.getsize(dump_file) / (1024 * 1024), 2)
429
+ except (OSError, IOError):
430
+ size_mb = "unknown"
431
+
432
+ result_text += f"{i+1}. {dump_file} ({size_mb} MB)\n"
433
+
434
+ return [TextContent(
435
+ type="text",
436
+ text=result_text
437
+ )]
438
+
439
+ raise McpError(ErrorData(
440
+ code=INVALID_PARAMS,
441
+ message=f"Unknown tool: {name}"
442
+ ))
443
+
444
+ except McpError:
445
+ raise
446
+ except Exception as e:
447
+ traceback_str = traceback.format_exc()
448
+ raise McpError(ErrorData(
449
+ code=INTERNAL_ERROR,
450
+ message=f"Error executing tool {name}: {str(e)}\n{traceback_str}"
451
+ ))
452
+
453
+ options = server.create_initialization_options()
454
+ async with stdio_server() as (read_stream, write_stream):
455
+ await server.run(read_stream, write_stream, options, raise_exceptions=True)
456
+
457
+ # Clean up function to ensure all sessions are closed when the server exits
458
+ def cleanup_sessions():
459
+ """Close all active CDB sessions."""
460
+ for dump_path, session in active_sessions.items():
461
+ try:
462
+ if session is not None:
463
+ session.shutdown()
464
+ except Exception:
465
+ pass
466
+ active_sessions.clear()
467
+
468
+ # Register cleanup on module exit
469
+ import atexit
470
+ atexit.register(cleanup_sessions)
@@ -0,0 +1,119 @@
1
+ import os
2
+ import pytest
3
+
4
+ from mcp_server_windbg.cdb_session import CDBSession, CDBError, DEFAULT_CDB_PATHS
5
+
6
+ # Path to the test dump file
7
+ TEST_DUMP_PATH = os.path.join(os.path.dirname(__file__), 'dumps', 'DemoCrash1.exe.7088.dmp')
8
+
9
+ def setup_cdb_session():
10
+ """Helper function to create a CDB session"""
11
+ if not os.path.exists(TEST_DUMP_PATH):
12
+ pytest.skip("Test dump file not found")
13
+
14
+ if not any(os.path.exists(path) for path in DEFAULT_CDB_PATHS):
15
+ pytest.skip("CDB executable not found")
16
+
17
+ return CDBSession(
18
+ dump_path=TEST_DUMP_PATH,
19
+ timeout=20,
20
+ verbose=True
21
+ )
22
+
23
+ def test_basic_cdb_command():
24
+ """Test basic CDB command execution"""
25
+ session = setup_cdb_session()
26
+ try:
27
+ output = session.send_command("version")
28
+ assert len(output) > 0
29
+ assert any("Microsoft (R) Windows Debugger" in line for line in output)
30
+ finally:
31
+ session.shutdown()
32
+
33
+ def test_command_sequence():
34
+ """Test multiple commands in sequence"""
35
+ session = setup_cdb_session()
36
+ try:
37
+ # Basic command sequence
38
+ commands = ["version", ".sympath", "!analyze -v", "lm", "~"]
39
+ results = []
40
+
41
+ for cmd in commands:
42
+ output = session.send_command(cmd)
43
+ results.append((cmd, output))
44
+ assert len(output) > 0
45
+
46
+ # Check expected output patterns
47
+ assert any("Microsoft (R) Windows Debugger" in line for line in results[0][1])
48
+ assert any("Symbol search path is:" in line for line in results[1][1])
49
+ assert any("start" in line.lower() for line in results[3][1])
50
+ finally:
51
+ session.shutdown()
52
+
53
+ def test_module_inspection():
54
+ """Test module inspection capabilities"""
55
+ session = setup_cdb_session()
56
+ try:
57
+ # Get module list
58
+ modules_output = session.send_command("lm")
59
+
60
+ # Find a common Windows module
61
+ target_modules = ['ntdll', 'kernel32']
62
+ module_name = None
63
+
64
+ for target in target_modules:
65
+ for line in modules_output:
66
+ if target in line.lower():
67
+ parts = line.split()
68
+ for part in parts:
69
+ if target in part.lower():
70
+ module_name = part
71
+ break
72
+ if module_name:
73
+ break
74
+ if module_name:
75
+ break
76
+
77
+ assert module_name is not None
78
+
79
+ # Get module details
80
+ module_info = session.send_command(f"lmv m {module_name}")
81
+ assert len(module_info) > 0
82
+ assert any(module_name.lower() in line.lower() for line in module_info)
83
+
84
+ # Get stack info
85
+ stack_info = session.send_command("k 5")
86
+ assert len(stack_info) > 0
87
+ finally:
88
+ session.shutdown()
89
+
90
+ def test_thread_context():
91
+ """Test thread context operations"""
92
+ session = setup_cdb_session()
93
+ try:
94
+ # Get thread list
95
+ thread_list = session.send_command("~")
96
+
97
+ # Select first thread
98
+ thread_id = "0"
99
+ for line in thread_list:
100
+ if line.strip().startswith("#"):
101
+ parts = line.split()
102
+ if len(parts) > 1:
103
+ thread_id = parts[1].strip(":")
104
+ break
105
+
106
+ # Switch to thread and check registers
107
+ session.send_command(f"~{thread_id}s")
108
+ registers = session.send_command("r")
109
+ assert len(registers) > 0
110
+ assert any("eax" in line.lower() or "rax" in line.lower() for line in registers)
111
+
112
+ # Check stack trace
113
+ stack = session.send_command("k")
114
+ assert len(stack) > 0
115
+ finally:
116
+ session.shutdown()
117
+
118
+ if __name__ == "__main__":
119
+ pytest.main(["-v", __file__])
@@ -0,0 +1,232 @@
1
+ import pytest
2
+ import subprocess
3
+ import time
4
+ import threading
5
+ import os
6
+ import sys
7
+ from typing import Optional
8
+
9
+ # Add the src directory to the Python path
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
11
+
12
+ from mcp_server_windbg.cdb_session import CDBSession, CDBError
13
+ from mcp_server_windbg.server import get_or_create_session, unload_session
14
+
15
+
16
+ class CDBServerProcess:
17
+ """Helper class to manage a CDB server process for testing."""
18
+
19
+ def __init__(self, port: int = 5005):
20
+ self.port = port
21
+ self.process: Optional[subprocess.Popen] = None
22
+ self.output_lines = []
23
+ self.reader_thread: Optional[threading.Thread] = None
24
+ self.running = False
25
+
26
+ def start(self, timeout: int = 10) -> bool:
27
+ """Start the CDB server process."""
28
+ try:
29
+ # Find cdb.exe
30
+ cdb_path = self._find_cdb_executable()
31
+ if not cdb_path:
32
+ raise Exception("Could not find cdb.exe")
33
+
34
+ # Use CDB to launch and debug a new instance of CDB
35
+ self.process = subprocess.Popen(
36
+ [cdb_path, "-o", cdb_path],
37
+ stdin=subprocess.PIPE,
38
+ stdout=subprocess.PIPE,
39
+ stderr=subprocess.STDOUT,
40
+ text=True,
41
+ bufsize=1
42
+ )
43
+
44
+ # Start output reader thread
45
+ self.running = True
46
+ self.reader_thread = threading.Thread(target=self._read_output)
47
+ self.reader_thread.daemon = True
48
+ self.reader_thread.start()
49
+
50
+ # Wait for CDB to initialize
51
+ if not self._wait_for_prompt(timeout):
52
+ return False
53
+
54
+ # Start the remote server
55
+ server_command = f".server tcp:port={self.port}\n"
56
+ self.process.stdin.write(server_command)
57
+ self.process.stdin.flush()
58
+
59
+ # Wait for server to start and check for success message
60
+ start_time = time.time()
61
+ while time.time() - start_time < 5:
62
+ recent_lines = self.output_lines[-10:]
63
+ if any("Server started" in line for line in recent_lines):
64
+ return True
65
+ time.sleep(0.1)
66
+
67
+ return True # Assume success if we got this far
68
+
69
+ except Exception as e:
70
+ print(f"Failed to start CDB server: {e}")
71
+ self.cleanup()
72
+ return False
73
+
74
+ def cleanup(self):
75
+ """Clean up the CDB server process."""
76
+ self.running = False
77
+
78
+ if self.process and self.process.poll() is None:
79
+ try:
80
+ # Send quit command
81
+ self.process.stdin.write("q\n")
82
+ self.process.stdin.flush()
83
+ self.process.wait(timeout=3)
84
+ except Exception:
85
+ pass
86
+
87
+ if self.process.poll() is None:
88
+ self.process.terminate()
89
+ try:
90
+ self.process.wait(timeout=3)
91
+ except subprocess.TimeoutExpired:
92
+ self.process.kill()
93
+
94
+ if self.reader_thread and self.reader_thread.is_alive():
95
+ self.reader_thread.join(timeout=1)
96
+
97
+ self.process = None
98
+
99
+ def _find_cdb_executable(self) -> Optional[str]:
100
+ """Find the cdb.exe executable."""
101
+ default_paths = [
102
+ r"C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe",
103
+ r"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe",
104
+ ]
105
+
106
+ for path in default_paths:
107
+ if os.path.isfile(path):
108
+ return path
109
+ return None
110
+
111
+ def _read_output(self):
112
+ """Thread function to read CDB output."""
113
+ if not self.process or not self.process.stdout:
114
+ return
115
+
116
+ try:
117
+ for line in self.process.stdout:
118
+ line = line.rstrip()
119
+ self.output_lines.append(line)
120
+ print(f"CDB Server: {line}") # Debug output
121
+ except Exception as e:
122
+ print(f"CDB server output reader error: {e}")
123
+
124
+ def _wait_for_prompt(self, timeout: int) -> bool:
125
+ """Wait for CDB to be ready."""
126
+ start_time = time.time()
127
+ while time.time() - start_time < timeout:
128
+ # Look for the CDB prompt pattern (e.g., "0:000>")
129
+ recent_lines = self.output_lines[-10:] # Check last 10 lines
130
+ for line in recent_lines:
131
+ if ":000>" in line or "Break instruction exception" in line:
132
+ return True
133
+ time.sleep(0.1)
134
+ return False
135
+
136
+
137
+ @pytest.mark.skipif(not os.name == 'nt', reason="Windows-only test")
138
+ class TestRemoteDebugging:
139
+ """Test cases for remote debugging functionality."""
140
+
141
+ def test_remote_debugging_workflow(self):
142
+ """Test the complete remote debugging workflow."""
143
+ server = CDBServerProcess(port=5005)
144
+ connection_string = "tcp:Port=5005,Server=127.0.0.1"
145
+
146
+ try:
147
+ # Start the CDB server process
148
+ assert server.start(timeout=15), "Failed to start CDB server process"
149
+
150
+ # Test opening remote connection
151
+ session = get_or_create_session(connection_string=connection_string, timeout=10, verbose=True)
152
+ assert session is not None, "Failed to create remote session"
153
+
154
+ # Test sending a command
155
+ try:
156
+ output = session.send_command("r") # Show registers
157
+ assert len(output) > 0, "No output from remote command"
158
+ print(f"Remote command output: {output[:3]}") # Show first 3 lines
159
+ except CDBError as e:
160
+ # Sometimes the first command might timeout during connection establishment
161
+ print(f"First command failed (this might be expected): {e}")
162
+
163
+ # Test that session exists in active sessions
164
+ from mcp_server_windbg.server import active_sessions
165
+ session_id = f"remote:{connection_string}"
166
+ assert session_id in active_sessions, "Session not found in active sessions"
167
+
168
+ # Test closing the remote connection
169
+ success = unload_session(connection_string=connection_string)
170
+ assert success, "Failed to unload remote session"
171
+
172
+ # Verify session was removed
173
+ assert session_id not in active_sessions, "Session still exists after unloading"
174
+
175
+ finally:
176
+ # Clean up the server process
177
+ server.cleanup()
178
+
179
+ def test_remote_connection_validation(self):
180
+ """Test validation of remote connection parameters."""
181
+ # Test that CDBSession validates parameters correctly
182
+ with pytest.raises(ValueError, match="Either dump_path or remote_connection must be provided"):
183
+ CDBSession()
184
+
185
+ with pytest.raises(ValueError, match="dump_path and remote_connection are mutually exclusive"):
186
+ CDBSession(dump_path="test.dmp", remote_connection="tcp:Port=5005,Server=127.0.0.1")
187
+
188
+ def test_invalid_remote_connection(self):
189
+ """Test handling of invalid remote connections."""
190
+ invalid_connection = "tcp:Port=99999,Server=192.168.255.255" # Invalid server
191
+
192
+ with pytest.raises(CDBError):
193
+ session = CDBSession(remote_connection=invalid_connection, timeout=2)
194
+ # The session creation might succeed but commands should fail
195
+ session.send_command("r")
196
+
197
+
198
+ if __name__ == "__main__":
199
+ # Run a simple test manually
200
+ print("Running remote debugging test...")
201
+
202
+ server = CDBServerProcess(port=5005)
203
+ connection_string = "tcp:Port=5005,Server=127.0.0.1"
204
+
205
+ try:
206
+ print("Starting CDB server...")
207
+ if server.start(timeout=15):
208
+ print("CDB server started successfully")
209
+
210
+ print("Creating remote session...")
211
+ session = get_or_create_session(connection_string=connection_string, timeout=10, verbose=True)
212
+
213
+ print("Sending test command...")
214
+ try:
215
+ output = session.send_command("r")
216
+ print(f"Command successful, got {len(output)} lines of output")
217
+ except Exception as e:
218
+ print(f"Command failed: {e}")
219
+
220
+ print("Closing remote session...")
221
+ unload_session(connection_string=connection_string)
222
+ print("Test completed successfully!")
223
+
224
+ else:
225
+ print("Failed to start CDB server")
226
+
227
+ except Exception as e:
228
+ print(f"Test failed: {e}")
229
+
230
+ finally:
231
+ print("Cleaning up...")
232
+ server.cleanup()
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.4
2
+ Name: mseep-mcp-server-windbg
3
+ Version: 0.1.1
4
+ Summary: A Model Context Protocol server providing tools to analyze Windows crash dumps using WinDBG/CDB
5
+ Author-email: mseep <support@skydeck.ai>
6
+ License: MIT
7
+ Keywords: windbg,cdb,mcp,llm,crash-analysis
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/plain
15
+ License-File: LICENSE
16
+ Requires-Dist: mcp>=1.1.3
17
+ Requires-Dist: pydantic>=2.0.0
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest>=7.0.0; extra == "test"
20
+ Dynamic: license-file
21
+
22
+ Package managed by MseeP.ai
@@ -0,0 +1,12 @@
1
+ mcp_server_windbg/__init__.py,sha256=GGTyCCpp4MB3aHa5thBWClJck4PEiQnoeizp9Cg3nMg,870
2
+ mcp_server_windbg/__main__.py,sha256=ZUj85sUn92Bn9CSn2O0wM0IhCOQX_ytg6yKfF3EPA24,57
3
+ mcp_server_windbg/cdb_session.py,sha256=c4WIJkwCIgw0E2K4MujPuxcbWus5ebukSv5d9aGQ82g,9636
4
+ mcp_server_windbg/server.py,sha256=BbqWQeTvZVwGAwh5MBY1tDED3oUHnqZss3wEGZDFhiQ,19823
5
+ mcp_server_windbg/tests/test_cdb.py,sha256=5dtSqxxGQjA-m2a0OJzC6ZX27ly9WLeQm-gAhvXbqL8,3841
6
+ mcp_server_windbg/tests/test_remote_debugging.py,sha256=KHiGXI5wWFZlkdpthG5sW7xIMVdUC11FVlZE5aorTc8,8955
7
+ mseep_mcp_server_windbg-0.1.1.dist-info/licenses/LICENSE,sha256=YTULkMwOLkCBYY7hwX7JcjXMiWeySkSXuNoDLmFumZo,1073
8
+ mseep_mcp_server_windbg-0.1.1.dist-info/METADATA,sha256=x7pGWSMvQDsoBKVx3eKlJe8Zerg4ZpIjK7AyHjOvjOA,765
9
+ mseep_mcp_server_windbg-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ mseep_mcp_server_windbg-0.1.1.dist-info/entry_points.txt,sha256=KIah6e21FKHlv8JTie5znauqKuj95wSWS9TL1pxxeSQ,61
11
+ mseep_mcp_server_windbg-0.1.1.dist-info/top_level.txt,sha256=ltKYfBdrn5P9HKNJepxct0dnGXLv08OQiahdmQtOH_w,18
12
+ mseep_mcp_server_windbg-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mcp-server-windbg = mcp_server_windbg:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sven Scharmentke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ mcp_server_windbg