pwndoc-mcp-server 1.0.8__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.
- pwndoc_mcp_server/__init__.py +87 -0
- pwndoc_mcp_server/cli.py +635 -0
- pwndoc_mcp_server/client.py +1122 -0
- pwndoc_mcp_server/config.py +414 -0
- pwndoc_mcp_server/logging_config.py +329 -0
- pwndoc_mcp_server/mcp_installer.py +348 -0
- pwndoc_mcp_server/server.py +1987 -0
- pwndoc_mcp_server/version.py +26 -0
- pwndoc_mcp_server-1.0.8.dist-info/METADATA +552 -0
- pwndoc_mcp_server-1.0.8.dist-info/RECORD +12 -0
- pwndoc_mcp_server-1.0.8.dist-info/WHEEL +4 -0
- pwndoc_mcp_server-1.0.8.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PwnDoc MCP Server - Logging Configuration.
|
|
3
|
+
|
|
4
|
+
This module provides comprehensive logging setup with support for:
|
|
5
|
+
- Console output with colors
|
|
6
|
+
- File logging with rotation
|
|
7
|
+
- JSON structured logging
|
|
8
|
+
- Log level configuration
|
|
9
|
+
- Performance metrics logging
|
|
10
|
+
|
|
11
|
+
Environment Variables:
|
|
12
|
+
PWNDOC_LOG_LEVEL - Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
13
|
+
PWNDOC_LOG_FILE - Path to log file
|
|
14
|
+
PWNDOC_LOG_FORMAT - Log format (text, json)
|
|
15
|
+
PWNDOC_LOG_MAX_SIZE - Max log file size in MB (default: 10)
|
|
16
|
+
PWNDOC_LOG_BACKUPS - Number of backup files to keep (default: 5)
|
|
17
|
+
|
|
18
|
+
Log Levels:
|
|
19
|
+
DEBUG - Detailed information for debugging
|
|
20
|
+
INFO - General operational messages
|
|
21
|
+
WARNING - Warning messages for potential issues
|
|
22
|
+
ERROR - Error messages for failures
|
|
23
|
+
CRITICAL - Critical errors that may cause shutdown
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> from pwndoc_mcp_server.logging_config import setup_logging
|
|
27
|
+
>>> setup_logging(level="DEBUG", log_file="/var/log/pwndoc-mcp.log")
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import logging.handlers
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
from enum import Enum
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any, Dict, Optional
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LogLevel(Enum):
|
|
42
|
+
"""Log level enumeration."""
|
|
43
|
+
|
|
44
|
+
DEBUG = logging.DEBUG
|
|
45
|
+
INFO = logging.INFO
|
|
46
|
+
WARNING = logging.WARNING
|
|
47
|
+
ERROR = logging.ERROR
|
|
48
|
+
CRITICAL = logging.CRITICAL
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Custom log format
|
|
52
|
+
DEFAULT_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
53
|
+
DETAILED_FORMAT = (
|
|
54
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(funcName)s - %(message)s"
|
|
55
|
+
)
|
|
56
|
+
SIMPLE_FORMAT = "%(levelname)s: %(message)s"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ColoredFormatter(logging.Formatter):
|
|
60
|
+
"""Colored log formatter for console output."""
|
|
61
|
+
|
|
62
|
+
COLORS = {
|
|
63
|
+
"DEBUG": "\033[36m", # Cyan
|
|
64
|
+
"INFO": "\033[32m", # Green
|
|
65
|
+
"WARNING": "\033[33m", # Yellow
|
|
66
|
+
"ERROR": "\033[31m", # Red
|
|
67
|
+
"CRITICAL": "\033[35m", # Magenta
|
|
68
|
+
}
|
|
69
|
+
RESET = "\033[0m"
|
|
70
|
+
|
|
71
|
+
def format(self, record):
|
|
72
|
+
color = self.COLORS.get(record.levelname, "")
|
|
73
|
+
record.levelname = f"{color}{record.levelname}{self.RESET}"
|
|
74
|
+
return super().format(record)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class JSONFormatter(logging.Formatter):
|
|
78
|
+
"""JSON log formatter for structured logging."""
|
|
79
|
+
|
|
80
|
+
def format(self, record) -> str:
|
|
81
|
+
log_obj = {
|
|
82
|
+
"timestamp": datetime.utcnow().isoformat() + "Z",
|
|
83
|
+
"level": record.levelname,
|
|
84
|
+
"logger": record.name,
|
|
85
|
+
"message": record.getMessage(),
|
|
86
|
+
"module": record.module,
|
|
87
|
+
"function": record.funcName,
|
|
88
|
+
"line": record.lineno,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Add exception info if present
|
|
92
|
+
if record.exc_info:
|
|
93
|
+
log_obj["exception"] = self.formatException(record.exc_info)
|
|
94
|
+
|
|
95
|
+
# Add extra fields
|
|
96
|
+
if hasattr(record, "extra"):
|
|
97
|
+
log_obj.update(record.extra)
|
|
98
|
+
|
|
99
|
+
return json.dumps(log_obj)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SafeStreamHandler(logging.StreamHandler):
|
|
103
|
+
"""StreamHandler that handles Unicode encoding errors gracefully on Windows."""
|
|
104
|
+
|
|
105
|
+
def emit(self, record):
|
|
106
|
+
"""Emit a record, handling Unicode encoding errors."""
|
|
107
|
+
try:
|
|
108
|
+
super().emit(record)
|
|
109
|
+
except UnicodeEncodeError:
|
|
110
|
+
# If we get an encoding error, try to encode with 'replace' errors
|
|
111
|
+
try:
|
|
112
|
+
msg = self.format(record)
|
|
113
|
+
stream = self.stream
|
|
114
|
+
# Encode with errors='replace' to substitute unmappable characters
|
|
115
|
+
if hasattr(stream, "encoding") and stream.encoding:
|
|
116
|
+
# Encode and decode with replace to substitute unmappable chars
|
|
117
|
+
safe_msg = msg.encode(stream.encoding, errors="replace").decode(
|
|
118
|
+
stream.encoding, errors="replace"
|
|
119
|
+
)
|
|
120
|
+
stream.write(safe_msg)
|
|
121
|
+
stream.write(self.terminator)
|
|
122
|
+
self.flush()
|
|
123
|
+
else:
|
|
124
|
+
# If no encoding info, try with utf-8
|
|
125
|
+
safe_msg = msg.encode("utf-8", errors="replace").decode(
|
|
126
|
+
"utf-8", errors="replace"
|
|
127
|
+
)
|
|
128
|
+
stream.write(safe_msg)
|
|
129
|
+
stream.write(self.terminator)
|
|
130
|
+
self.flush()
|
|
131
|
+
except Exception:
|
|
132
|
+
self.handleError(record)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class PerformanceLogger:
|
|
136
|
+
"""Logger for performance metrics."""
|
|
137
|
+
|
|
138
|
+
def __init__(self, logger: logging.Logger):
|
|
139
|
+
self.logger = logger
|
|
140
|
+
self._metrics: Dict[str, Any] = {}
|
|
141
|
+
|
|
142
|
+
def start_timer(self, name: str):
|
|
143
|
+
"""Start a performance timer."""
|
|
144
|
+
import time
|
|
145
|
+
|
|
146
|
+
self._metrics[name] = {"start": time.time()}
|
|
147
|
+
|
|
148
|
+
def stop_timer(self, name: str) -> float:
|
|
149
|
+
"""Stop timer and log duration."""
|
|
150
|
+
import time
|
|
151
|
+
|
|
152
|
+
if name in self._metrics:
|
|
153
|
+
duration: float = time.time() - float(self._metrics[name]["start"])
|
|
154
|
+
self.logger.debug(f"Performance: {name} took {duration:.3f}s")
|
|
155
|
+
return duration
|
|
156
|
+
return 0.0
|
|
157
|
+
|
|
158
|
+
def log_metric(self, name: str, value: Any):
|
|
159
|
+
"""Log a performance metric."""
|
|
160
|
+
self.logger.debug(f"Metric: {name} = {value}")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def setup_logging(
|
|
164
|
+
level: str = "INFO",
|
|
165
|
+
log_file: Optional[str] = None,
|
|
166
|
+
log_format: str = "text",
|
|
167
|
+
max_size_mb: int = 10,
|
|
168
|
+
max_bytes: Optional[int] = None,
|
|
169
|
+
backup_count: int = 5,
|
|
170
|
+
json_output: bool = False,
|
|
171
|
+
colored: bool = True,
|
|
172
|
+
console: bool = True,
|
|
173
|
+
name: Optional[str] = None,
|
|
174
|
+
) -> logging.Logger:
|
|
175
|
+
"""
|
|
176
|
+
Configure logging for the application.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
180
|
+
log_file: Path to log file (optional)
|
|
181
|
+
log_format: Format type ('text', 'json', 'detailed')
|
|
182
|
+
max_size_mb: Maximum log file size in MB
|
|
183
|
+
max_bytes: Maximum log file size in bytes (overrides max_size_mb)
|
|
184
|
+
backup_count: Number of backup files to keep
|
|
185
|
+
json_output: Use JSON format for all output
|
|
186
|
+
colored: Use colored output for console
|
|
187
|
+
console: Enable console output (default: True)
|
|
188
|
+
name: Logger name (default: root logger)
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Logger configured for the application
|
|
192
|
+
|
|
193
|
+
Example:
|
|
194
|
+
>>> logger = setup_logging(level="DEBUG", log_file="app.log")
|
|
195
|
+
>>> logger.info("Application started")
|
|
196
|
+
"""
|
|
197
|
+
# Check environment variables for overrides
|
|
198
|
+
env_level = os.environ.get("PWNDOC_LOG_LEVEL")
|
|
199
|
+
if env_level:
|
|
200
|
+
level = env_level
|
|
201
|
+
|
|
202
|
+
env_file = os.environ.get("PWNDOC_LOG_FILE")
|
|
203
|
+
if env_file:
|
|
204
|
+
log_file = env_file
|
|
205
|
+
|
|
206
|
+
# Get logger (root or named)
|
|
207
|
+
if name:
|
|
208
|
+
root_logger = logging.getLogger(name)
|
|
209
|
+
else:
|
|
210
|
+
root_logger = logging.getLogger()
|
|
211
|
+
|
|
212
|
+
root_logger.setLevel(getattr(logging, level.upper()))
|
|
213
|
+
|
|
214
|
+
# Clear existing handlers (close them first to avoid resource warnings)
|
|
215
|
+
for handler in root_logger.handlers[:]:
|
|
216
|
+
handler.close()
|
|
217
|
+
root_logger.removeHandler(handler)
|
|
218
|
+
root_logger.handlers = []
|
|
219
|
+
|
|
220
|
+
# Select format
|
|
221
|
+
formatter: logging.Formatter
|
|
222
|
+
if json_output or log_format == "json":
|
|
223
|
+
formatter = JSONFormatter()
|
|
224
|
+
elif log_format == "detailed":
|
|
225
|
+
if colored and sys.stdout.isatty():
|
|
226
|
+
formatter = ColoredFormatter(DETAILED_FORMAT)
|
|
227
|
+
else:
|
|
228
|
+
formatter = logging.Formatter(DETAILED_FORMAT)
|
|
229
|
+
else:
|
|
230
|
+
if colored and sys.stdout.isatty():
|
|
231
|
+
formatter = ColoredFormatter(DEFAULT_FORMAT)
|
|
232
|
+
else:
|
|
233
|
+
formatter = logging.Formatter(DEFAULT_FORMAT)
|
|
234
|
+
|
|
235
|
+
# Console handler (if enabled)
|
|
236
|
+
if console:
|
|
237
|
+
# On Windows, use SafeStreamHandler to handle Unicode encoding errors
|
|
238
|
+
if sys.platform == "win32":
|
|
239
|
+
console_handler = SafeStreamHandler(sys.stderr)
|
|
240
|
+
else:
|
|
241
|
+
console_handler = logging.StreamHandler(sys.stderr)
|
|
242
|
+
console_handler.setFormatter(formatter)
|
|
243
|
+
root_logger.addHandler(console_handler)
|
|
244
|
+
|
|
245
|
+
# File handler (if specified)
|
|
246
|
+
if log_file:
|
|
247
|
+
log_path = Path(log_file)
|
|
248
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
249
|
+
|
|
250
|
+
# Calculate max bytes
|
|
251
|
+
if max_bytes is None:
|
|
252
|
+
max_bytes = max_size_mb * 1024 * 1024
|
|
253
|
+
|
|
254
|
+
# Use rotating file handler with UTF-8 encoding
|
|
255
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
256
|
+
log_path,
|
|
257
|
+
maxBytes=max_bytes,
|
|
258
|
+
backupCount=backup_count,
|
|
259
|
+
encoding="utf-8",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Always use non-colored formatter for files
|
|
263
|
+
if json_output:
|
|
264
|
+
file_handler.setFormatter(JSONFormatter())
|
|
265
|
+
else:
|
|
266
|
+
file_handler.setFormatter(logging.Formatter(DEFAULT_FORMAT))
|
|
267
|
+
|
|
268
|
+
root_logger.addHandler(file_handler)
|
|
269
|
+
|
|
270
|
+
# Configure library loggers
|
|
271
|
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
272
|
+
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
|
273
|
+
|
|
274
|
+
return root_logger
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
|
278
|
+
"""
|
|
279
|
+
Get a logger for a specific module.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
name: Logger name (typically __name__), if None returns root logger
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
Configured logger instance
|
|
286
|
+
|
|
287
|
+
Example:
|
|
288
|
+
>>> logger = get_logger(__name__)
|
|
289
|
+
>>> logger.info("Module initialized")
|
|
290
|
+
>>> logger = get_logger() # Get root logger
|
|
291
|
+
"""
|
|
292
|
+
if name is None:
|
|
293
|
+
return logging.getLogger()
|
|
294
|
+
return logging.getLogger(name)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def log_request(logger: logging.Logger, method: str, endpoint: str, duration: float = 0):
|
|
298
|
+
"""Log an API request."""
|
|
299
|
+
logger.debug(f"API Request: {method} {endpoint} ({duration:.3f}s)")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def log_error(logger: logging.Logger, error: Exception, context: Optional[str] = None):
|
|
303
|
+
"""Log an error with context."""
|
|
304
|
+
msg = f"Error: {type(error).__name__}: {error}"
|
|
305
|
+
if context:
|
|
306
|
+
msg = f"{context} - {msg}"
|
|
307
|
+
logger.error(msg, exc_info=True)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# Environment-based configuration
|
|
311
|
+
def setup_from_env() -> logging.Logger:
|
|
312
|
+
"""
|
|
313
|
+
Configure logging from environment variables.
|
|
314
|
+
|
|
315
|
+
Environment Variables:
|
|
316
|
+
PWNDOC_LOG_LEVEL - Log level
|
|
317
|
+
PWNDOC_LOG_FILE - Log file path
|
|
318
|
+
PWNDOC_LOG_FORMAT - Format (text, json, detailed)
|
|
319
|
+
PWNDOC_LOG_MAX_SIZE - Max file size in MB
|
|
320
|
+
PWNDOC_LOG_BACKUPS - Backup file count
|
|
321
|
+
"""
|
|
322
|
+
return setup_logging(
|
|
323
|
+
level=os.environ.get("PWNDOC_LOG_LEVEL", "INFO"),
|
|
324
|
+
log_file=os.environ.get("PWNDOC_LOG_FILE"),
|
|
325
|
+
log_format=os.environ.get("PWNDOC_LOG_FORMAT", "text"),
|
|
326
|
+
max_size_mb=int(os.environ.get("PWNDOC_LOG_MAX_SIZE", "10")),
|
|
327
|
+
backup_count=int(os.environ.get("PWNDOC_LOG_BACKUPS", "5")),
|
|
328
|
+
json_output=os.environ.get("PWNDOC_LOG_FORMAT") == "json",
|
|
329
|
+
)
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP Configuration Installer for Claude Desktop.
|
|
3
|
+
|
|
4
|
+
Automatically configures Claude Desktop to use the PwnDoc MCP server
|
|
5
|
+
by updating the appropriate mcp_servers.json file for each platform.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_claude_config_path() -> Path:
|
|
21
|
+
"""
|
|
22
|
+
Get the Claude Desktop MCP configuration file path for the current platform.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Path to Claude's claude_desktop_config.json file
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
RuntimeError: If platform is not supported
|
|
29
|
+
"""
|
|
30
|
+
system = platform.system()
|
|
31
|
+
|
|
32
|
+
if system == "Linux":
|
|
33
|
+
return Path.home() / ".config" / "Claude" / "claude_desktop_config.json"
|
|
34
|
+
elif system == "Darwin": # macOS
|
|
35
|
+
return (
|
|
36
|
+
Path.home()
|
|
37
|
+
/ "Library"
|
|
38
|
+
/ "Application Support"
|
|
39
|
+
/ "Claude"
|
|
40
|
+
/ "claude_desktop_config.json"
|
|
41
|
+
)
|
|
42
|
+
elif system == "Windows":
|
|
43
|
+
appdata = Path(os.environ.get("APPDATA", Path.home() / "AppData" / "Roaming"))
|
|
44
|
+
return appdata / "Claude" / "claude_desktop_config.json"
|
|
45
|
+
else:
|
|
46
|
+
raise RuntimeError(f"Unsupported platform: {system}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_claude_installed() -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Check if Claude Desktop appears to be installed.
|
|
52
|
+
|
|
53
|
+
Checks for config file existence first (most reliable indicator),
|
|
54
|
+
then checks for application installation.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if Claude Desktop installation is detected
|
|
58
|
+
"""
|
|
59
|
+
# First check if config file or config directory exists (most reliable)
|
|
60
|
+
config_path = get_claude_config_path()
|
|
61
|
+
if config_path.exists() or config_path.parent.exists():
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
system = platform.system()
|
|
65
|
+
|
|
66
|
+
if system == "Linux":
|
|
67
|
+
# Check for Claude Desktop in common locations
|
|
68
|
+
claude_paths = [
|
|
69
|
+
Path.home() / ".local" / "share" / "applications" / "claude.desktop",
|
|
70
|
+
Path("/usr/share/applications/claude.desktop"),
|
|
71
|
+
]
|
|
72
|
+
return any(p.exists() for p in claude_paths)
|
|
73
|
+
|
|
74
|
+
elif system == "Darwin": # macOS
|
|
75
|
+
# Check for Claude.app in Applications
|
|
76
|
+
app_paths = [
|
|
77
|
+
Path("/Applications/Claude.app"),
|
|
78
|
+
Path.home() / "Applications" / "Claude.app",
|
|
79
|
+
]
|
|
80
|
+
return any(p.exists() for p in app_paths)
|
|
81
|
+
|
|
82
|
+
elif system == "Windows":
|
|
83
|
+
# Check for Claude in Program Files or AppData
|
|
84
|
+
local_appdata = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
85
|
+
claude_paths = [
|
|
86
|
+
Path(os.environ.get("PROGRAMFILES", "C:\\Program Files")) / "Claude",
|
|
87
|
+
local_appdata / "Programs" / "Claude",
|
|
88
|
+
]
|
|
89
|
+
return any(p.exists() for p in claude_paths)
|
|
90
|
+
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def detect_python_executable() -> str:
|
|
95
|
+
"""
|
|
96
|
+
Detect the Python executable being used.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Path to current Python executable
|
|
100
|
+
"""
|
|
101
|
+
return sys.executable
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def detect_pwndoc_mcp_path() -> Optional[str]:
|
|
105
|
+
"""
|
|
106
|
+
Detect the path to pwndoc-mcp executable or module.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Path to pwndoc-mcp command or None if not found
|
|
110
|
+
"""
|
|
111
|
+
# Check if installed as a package with CLI entry point
|
|
112
|
+
pwndoc_mcp_bin = shutil.which("pwndoc-mcp")
|
|
113
|
+
if pwndoc_mcp_bin:
|
|
114
|
+
return pwndoc_mcp_bin
|
|
115
|
+
|
|
116
|
+
# Check if we're running from source
|
|
117
|
+
try:
|
|
118
|
+
import pwndoc_mcp_server # noqa: F401
|
|
119
|
+
|
|
120
|
+
# If running from source, use python -m
|
|
121
|
+
return f"{sys.executable} -m pwndoc_mcp_server.server"
|
|
122
|
+
except ImportError:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def create_mcp_config(
|
|
129
|
+
command: Optional[str] = None,
|
|
130
|
+
args: Optional[list] = None,
|
|
131
|
+
env: Optional[Dict[str, str]] = None,
|
|
132
|
+
) -> Dict[str, Any]:
|
|
133
|
+
"""
|
|
134
|
+
Create MCP server configuration for PwnDoc.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
command: Command to run (auto-detected if None)
|
|
138
|
+
args: Command arguments
|
|
139
|
+
env: Environment variables
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
MCP server configuration dict
|
|
143
|
+
"""
|
|
144
|
+
if command is None:
|
|
145
|
+
detected = detect_pwndoc_mcp_path()
|
|
146
|
+
if not detected:
|
|
147
|
+
raise RuntimeError("Could not detect pwndoc-mcp installation")
|
|
148
|
+
command = detected
|
|
149
|
+
|
|
150
|
+
if args is None:
|
|
151
|
+
args = ["serve"]
|
|
152
|
+
|
|
153
|
+
config: Dict[str, Any] = {
|
|
154
|
+
"command": command,
|
|
155
|
+
"args": args,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if env:
|
|
159
|
+
config["env"] = env
|
|
160
|
+
|
|
161
|
+
return config
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def load_existing_config(config_path: Path) -> Dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
Load existing Claude MCP configuration.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
config_path: Path to claude_desktop_config.json
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Existing configuration dict with mcpServers key or empty dict
|
|
173
|
+
"""
|
|
174
|
+
if not config_path.exists():
|
|
175
|
+
return {"mcpServers": {}}
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
content = config_path.read_text()
|
|
179
|
+
data = json.loads(content)
|
|
180
|
+
if not isinstance(data, dict):
|
|
181
|
+
return {"mcpServers": {}}
|
|
182
|
+
|
|
183
|
+
# Ensure mcpServers key exists
|
|
184
|
+
if "mcpServers" not in data:
|
|
185
|
+
data["mcpServers"] = {}
|
|
186
|
+
|
|
187
|
+
return data
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.warning(f"Failed to load existing config: {e}")
|
|
190
|
+
return {"mcpServers": {}}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def save_mcp_config(config: Dict[str, Any], config_path: Path, backup: bool = True) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Save MCP configuration to Claude's config file.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
config: Full MCP configuration dict
|
|
199
|
+
config_path: Path to mcp_servers.json
|
|
200
|
+
backup: Create backup before saving
|
|
201
|
+
"""
|
|
202
|
+
# Ensure directory exists
|
|
203
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
|
|
205
|
+
# Create backup if requested and file exists
|
|
206
|
+
if backup and config_path.exists():
|
|
207
|
+
backup_path = config_path.with_suffix(".json.backup")
|
|
208
|
+
shutil.copy(config_path, backup_path)
|
|
209
|
+
logger.info(f"Created backup at {backup_path}")
|
|
210
|
+
|
|
211
|
+
# Write configuration
|
|
212
|
+
config_path.write_text(json.dumps(config, indent=2))
|
|
213
|
+
logger.info(f"Saved configuration to {config_path}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def install_mcp_config(
|
|
217
|
+
command: Optional[str] = None,
|
|
218
|
+
args: Optional[list] = None,
|
|
219
|
+
env: Optional[Dict[str, str]] = None,
|
|
220
|
+
force: bool = False,
|
|
221
|
+
) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Install PwnDoc MCP server configuration for Claude Desktop.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
command: Command to run (auto-detected if None)
|
|
227
|
+
args: Command arguments
|
|
228
|
+
env: Environment variables
|
|
229
|
+
force: Overwrite existing pwndoc-mcp configuration
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
True if installation succeeded
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
RuntimeError: If installation fails
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
# Get Claude config path
|
|
239
|
+
config_path = get_claude_config_path()
|
|
240
|
+
logger.info(f"Claude config path: {config_path}")
|
|
241
|
+
|
|
242
|
+
# Load existing configuration
|
|
243
|
+
full_config = load_existing_config(config_path)
|
|
244
|
+
|
|
245
|
+
# Check if pwndoc-mcp already configured
|
|
246
|
+
if "pwndoc-mcp" in full_config.get("mcpServers", {}) and not force:
|
|
247
|
+
logger.warning("pwndoc-mcp already configured (use --force to overwrite)")
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
# Create pwndoc-mcp configuration
|
|
251
|
+
pwndoc_config = create_mcp_config(command=command, args=args, env=env)
|
|
252
|
+
|
|
253
|
+
# Add to mcpServers
|
|
254
|
+
full_config["mcpServers"]["pwndoc-mcp"] = pwndoc_config
|
|
255
|
+
|
|
256
|
+
# Save configuration
|
|
257
|
+
save_mcp_config(full_config, config_path)
|
|
258
|
+
|
|
259
|
+
return True
|
|
260
|
+
|
|
261
|
+
except Exception as e:
|
|
262
|
+
logger.error(f"Failed to install MCP configuration: {e}")
|
|
263
|
+
raise RuntimeError(f"Installation failed: {e}")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def uninstall_mcp_config() -> bool:
|
|
267
|
+
"""
|
|
268
|
+
Remove PwnDoc MCP server configuration from Claude Desktop.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
True if removal succeeded
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
config_path = get_claude_config_path()
|
|
275
|
+
|
|
276
|
+
if not config_path.exists():
|
|
277
|
+
logger.info("No Claude configuration found")
|
|
278
|
+
return True
|
|
279
|
+
|
|
280
|
+
full_config = load_existing_config(config_path)
|
|
281
|
+
|
|
282
|
+
if "pwndoc-mcp" not in full_config.get("mcpServers", {}):
|
|
283
|
+
logger.info("pwndoc-mcp not configured")
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
# Remove pwndoc-mcp entry from mcpServers
|
|
287
|
+
del full_config["mcpServers"]["pwndoc-mcp"]
|
|
288
|
+
|
|
289
|
+
# Save updated configuration
|
|
290
|
+
save_mcp_config(full_config, config_path)
|
|
291
|
+
|
|
292
|
+
logger.info("pwndoc-mcp configuration removed")
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.error(f"Failed to uninstall MCP configuration: {e}")
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def show_mcp_config() -> Optional[Dict[str, Any]]:
|
|
301
|
+
"""
|
|
302
|
+
Show current PwnDoc MCP configuration in Claude Desktop.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Current pwndoc-mcp configuration or None
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
config_path = get_claude_config_path()
|
|
309
|
+
|
|
310
|
+
if not config_path.exists():
|
|
311
|
+
logger.info("No Claude configuration found")
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
full_config = load_existing_config(config_path)
|
|
315
|
+
mcp_servers = full_config.get("mcpServers", {})
|
|
316
|
+
pwndoc_config = mcp_servers.get("pwndoc-mcp")
|
|
317
|
+
|
|
318
|
+
if pwndoc_config is None:
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
return dict(pwndoc_config) if isinstance(pwndoc_config, dict) else None
|
|
322
|
+
|
|
323
|
+
except Exception as e:
|
|
324
|
+
logger.error(f"Failed to read MCP configuration: {e}")
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_all_mcp_servers() -> Dict[str, Any]:
|
|
329
|
+
"""
|
|
330
|
+
Get all MCP servers configured in Claude Desktop.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Dict of all MCP servers or empty dict
|
|
334
|
+
"""
|
|
335
|
+
try:
|
|
336
|
+
config_path = get_claude_config_path()
|
|
337
|
+
|
|
338
|
+
if not config_path.exists():
|
|
339
|
+
return {}
|
|
340
|
+
|
|
341
|
+
full_config = load_existing_config(config_path)
|
|
342
|
+
mcp_servers = full_config.get("mcpServers", {})
|
|
343
|
+
|
|
344
|
+
return dict(mcp_servers) if isinstance(mcp_servers, dict) else {}
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
logger.error(f"Failed to read MCP servers: {e}")
|
|
348
|
+
return {}
|