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.
- mcp_server_windbg/__init__.py +26 -0
- mcp_server_windbg/__main__.py +5 -0
- mcp_server_windbg/cdb_session.py +253 -0
- mcp_server_windbg/server.py +470 -0
- mcp_server_windbg/tests/test_cdb.py +119 -0
- mcp_server_windbg/tests/test_remote_debugging.py +232 -0
- mseep_mcp_server_windbg-0.1.1.dist-info/METADATA +22 -0
- mseep_mcp_server_windbg-0.1.1.dist-info/RECORD +12 -0
- mseep_mcp_server_windbg-0.1.1.dist-info/WHEEL +5 -0
- mseep_mcp_server_windbg-0.1.1.dist-info/entry_points.txt +2 -0
- mseep_mcp_server_windbg-0.1.1.dist-info/licenses/LICENSE +21 -0
- mseep_mcp_server_windbg-0.1.1.dist-info/top_level.txt +1 -0
@@ -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,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,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
|