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
pwndoc_mcp_server/cli.py
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PwnDoc MCP Server - Command Line Interface.
|
|
3
|
+
|
|
4
|
+
Provides a rich CLI for managing the MCP server, configuration,
|
|
5
|
+
and PwnDoc interactions.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
pwndoc-mcp serve # Start MCP server (stdio)
|
|
9
|
+
pwndoc-mcp serve --transport sse # Start SSE server
|
|
10
|
+
pwndoc-mcp config init # Interactive configuration
|
|
11
|
+
pwndoc-mcp config show # Show current config
|
|
12
|
+
pwndoc-mcp test # Test connection
|
|
13
|
+
pwndoc-mcp version # Show version
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Optional, Union
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import typer # type: ignore[import-not-found]
|
|
24
|
+
from rich.console import Console # type: ignore[import-not-found]
|
|
25
|
+
from rich.panel import Panel # type: ignore[import-not-found]
|
|
26
|
+
from rich.prompt import Prompt # type: ignore[import-not-found]
|
|
27
|
+
from rich.syntax import Syntax # type: ignore[import-not-found]
|
|
28
|
+
from rich.table import Table # type: ignore[import-not-found]
|
|
29
|
+
|
|
30
|
+
HAS_RICH = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
HAS_RICH = False
|
|
33
|
+
typer = None # type: ignore[assignment]
|
|
34
|
+
Prompt = None # type: ignore[assignment,misc]
|
|
35
|
+
|
|
36
|
+
from pwndoc_mcp_server import __version__
|
|
37
|
+
from pwndoc_mcp_server.client import PwnDocClient, PwnDocError
|
|
38
|
+
from pwndoc_mcp_server.config import (
|
|
39
|
+
DEFAULT_CONFIG_FILE,
|
|
40
|
+
init_config_interactive,
|
|
41
|
+
load_config,
|
|
42
|
+
save_config,
|
|
43
|
+
)
|
|
44
|
+
from pwndoc_mcp_server.mcp_installer import (
|
|
45
|
+
get_all_mcp_servers,
|
|
46
|
+
get_claude_config_path,
|
|
47
|
+
install_mcp_config,
|
|
48
|
+
is_claude_installed,
|
|
49
|
+
uninstall_mcp_config,
|
|
50
|
+
)
|
|
51
|
+
from pwndoc_mcp_server.server import PwnDocMCPServer
|
|
52
|
+
|
|
53
|
+
# Create CLI app
|
|
54
|
+
if HAS_RICH:
|
|
55
|
+
app = typer.Typer(
|
|
56
|
+
name="pwndoc-mcp",
|
|
57
|
+
help="PwnDoc MCP Server - Model Context Protocol for Pentest Documentation",
|
|
58
|
+
add_completion=True,
|
|
59
|
+
)
|
|
60
|
+
console = Console()
|
|
61
|
+
|
|
62
|
+
def version_callback(value: bool):
|
|
63
|
+
"""Callback for --version flag."""
|
|
64
|
+
if value:
|
|
65
|
+
console.print(f"pwndoc-mcp-server version {__version__}")
|
|
66
|
+
console.print("Author: Walid Faour <security@walidfaour.com>")
|
|
67
|
+
raise typer.Exit()
|
|
68
|
+
|
|
69
|
+
@app.callback()
|
|
70
|
+
def cli_callback(
|
|
71
|
+
version: Optional[bool] = typer.Option(
|
|
72
|
+
None,
|
|
73
|
+
"--version",
|
|
74
|
+
"-v",
|
|
75
|
+
help="Show version and exit",
|
|
76
|
+
callback=version_callback,
|
|
77
|
+
is_eager=True,
|
|
78
|
+
)
|
|
79
|
+
):
|
|
80
|
+
"""PwnDoc MCP Server CLI."""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
app = None # type: ignore[assignment]
|
|
85
|
+
console = None # type: ignore[assignment]
|
|
86
|
+
version_callback = None # type: ignore[assignment]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def setup_logging(level: str = "INFO", log_file: Optional[str] = None):
|
|
90
|
+
"""Configure logging."""
|
|
91
|
+
handlers: list[logging.Handler] = [logging.StreamHandler()]
|
|
92
|
+
if log_file:
|
|
93
|
+
handlers.append(logging.FileHandler(log_file))
|
|
94
|
+
|
|
95
|
+
logging.basicConfig(
|
|
96
|
+
level=getattr(logging, level.upper()),
|
|
97
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
98
|
+
handlers=handlers,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# =============================================================================
|
|
103
|
+
# VERSION COMMAND
|
|
104
|
+
# =============================================================================
|
|
105
|
+
|
|
106
|
+
if HAS_RICH:
|
|
107
|
+
|
|
108
|
+
@app.command()
|
|
109
|
+
def version():
|
|
110
|
+
"""Show version information."""
|
|
111
|
+
console.print(
|
|
112
|
+
Panel(
|
|
113
|
+
f"[bold green]PwnDoc MCP Server[/bold green]\n"
|
|
114
|
+
f"Version: [cyan]{__version__}[/cyan]\n"
|
|
115
|
+
f"Author: [cyan]Walid Faour[/cyan]\n"
|
|
116
|
+
f"Email: [cyan]security@walidfaour.com[/cyan]\n"
|
|
117
|
+
f"Python: [cyan]{sys.version.split()[0]}[/cyan]",
|
|
118
|
+
title="Version Info",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# =============================================================================
|
|
124
|
+
# SERVE COMMAND
|
|
125
|
+
# =============================================================================
|
|
126
|
+
|
|
127
|
+
if HAS_RICH:
|
|
128
|
+
|
|
129
|
+
@app.command()
|
|
130
|
+
def serve(
|
|
131
|
+
transport: str = typer.Option("stdio", help="Transport type (stdio, sse)"),
|
|
132
|
+
host: str = typer.Option("127.0.0.1", help="Host for SSE/WebSocket"),
|
|
133
|
+
port: int = typer.Option(8080, help="Port for SSE/WebSocket"),
|
|
134
|
+
log_level: str = typer.Option("INFO", help="Log level"),
|
|
135
|
+
log_file: Optional[str] = typer.Option(None, help="Log file path"),
|
|
136
|
+
config_file: Optional[Path] = typer.Option(None, "--config", "-c", help="Config file path"),
|
|
137
|
+
url: Optional[str] = typer.Option(None, "--url", help="PwnDoc server URL"),
|
|
138
|
+
username: Optional[str] = typer.Option(None, "--username", "-u", help="PwnDoc username"),
|
|
139
|
+
password: Optional[str] = typer.Option(None, "--password", "-p", help="PwnDoc password"),
|
|
140
|
+
token: Optional[str] = typer.Option(None, "--token", "-t", help="PwnDoc JWT token"),
|
|
141
|
+
):
|
|
142
|
+
"""Start the MCP server."""
|
|
143
|
+
setup_logging(log_level, log_file)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
# Build config overrides from CLI arguments
|
|
147
|
+
overrides = {
|
|
148
|
+
"mcp_transport": transport,
|
|
149
|
+
"mcp_host": host,
|
|
150
|
+
"mcp_port": port,
|
|
151
|
+
"log_level": log_level,
|
|
152
|
+
"log_file": log_file or "",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Add authentication overrides if provided
|
|
156
|
+
if url:
|
|
157
|
+
overrides["url"] = url
|
|
158
|
+
if username:
|
|
159
|
+
overrides["username"] = username
|
|
160
|
+
if password:
|
|
161
|
+
overrides["password"] = password
|
|
162
|
+
if token:
|
|
163
|
+
overrides["token"] = token
|
|
164
|
+
|
|
165
|
+
config = load_config(config_file=config_file, **overrides)
|
|
166
|
+
|
|
167
|
+
if not config.is_configured:
|
|
168
|
+
console.print(
|
|
169
|
+
"[red]Error:[/red] Server not configured. Run 'pwndoc-mcp config init' first."
|
|
170
|
+
)
|
|
171
|
+
raise typer.Exit(1)
|
|
172
|
+
|
|
173
|
+
if transport != "stdio":
|
|
174
|
+
console.print("[green]Starting PwnDoc MCP Server[/green]")
|
|
175
|
+
console.print(f" Transport: [cyan]{transport}[/cyan]")
|
|
176
|
+
console.print(f" URL: [cyan]{config.url}[/cyan]")
|
|
177
|
+
if transport == "sse":
|
|
178
|
+
console.print(f" Endpoint: [cyan]http://{host}:{port}/mcp[/cyan]")
|
|
179
|
+
|
|
180
|
+
server = PwnDocMCPServer(config)
|
|
181
|
+
server.run(transport)
|
|
182
|
+
|
|
183
|
+
except KeyboardInterrupt:
|
|
184
|
+
console.print("\n[yellow]Server stopped[/yellow]")
|
|
185
|
+
except Exception as e:
|
|
186
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
187
|
+
raise typer.Exit(1)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# =============================================================================
|
|
191
|
+
# CONFIG COMMANDS
|
|
192
|
+
# =============================================================================
|
|
193
|
+
|
|
194
|
+
if HAS_RICH:
|
|
195
|
+
config_app = typer.Typer(help="Configuration management")
|
|
196
|
+
app.add_typer(config_app, name="config")
|
|
197
|
+
|
|
198
|
+
@config_app.command("init")
|
|
199
|
+
def config_init():
|
|
200
|
+
"""Interactive configuration wizard."""
|
|
201
|
+
try:
|
|
202
|
+
init_config_interactive()
|
|
203
|
+
console.print("[green]✓ Configuration complete![/green]")
|
|
204
|
+
except KeyboardInterrupt:
|
|
205
|
+
console.print("\n[yellow]Configuration cancelled[/yellow]")
|
|
206
|
+
|
|
207
|
+
@config_app.command("show")
|
|
208
|
+
def config_show(
|
|
209
|
+
reveal_secrets: bool = typer.Option(False, "--reveal", help="Show sensitive values"),
|
|
210
|
+
):
|
|
211
|
+
"""Show current configuration."""
|
|
212
|
+
config = load_config()
|
|
213
|
+
|
|
214
|
+
table = Table(title="Current Configuration")
|
|
215
|
+
table.add_column("Setting", style="cyan")
|
|
216
|
+
table.add_column("Value", style="green")
|
|
217
|
+
|
|
218
|
+
table.add_row("URL", config.url or "[dim]not set[/dim]")
|
|
219
|
+
table.add_row("Username", config.username or "[dim]not set[/dim]")
|
|
220
|
+
table.add_row(
|
|
221
|
+
"Password",
|
|
222
|
+
(
|
|
223
|
+
("*" * 8 if config.password else "[dim]not set[/dim]")
|
|
224
|
+
if not reveal_secrets
|
|
225
|
+
else config.password
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
table.add_row(
|
|
229
|
+
"Token",
|
|
230
|
+
(
|
|
231
|
+
("*" * 20 if config.token else "[dim]not set[/dim]")
|
|
232
|
+
if not reveal_secrets
|
|
233
|
+
else (config.token or "")
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
table.add_row("Verify SSL", str(config.verify_ssl))
|
|
237
|
+
table.add_row("Timeout", str(config.timeout))
|
|
238
|
+
table.add_row("Log Level", config.log_level)
|
|
239
|
+
table.add_row("MCP Transport", config.mcp_transport)
|
|
240
|
+
table.add_row("Auth Method", config.auth_method)
|
|
241
|
+
table.add_row(
|
|
242
|
+
"Configured",
|
|
243
|
+
"[green]Yes[/green]" if config.is_configured else "[red]No[/red]",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
console.print(table)
|
|
247
|
+
console.print(f"\nConfig file: [dim]{DEFAULT_CONFIG_FILE}[/dim]")
|
|
248
|
+
|
|
249
|
+
@config_app.command("set")
|
|
250
|
+
def config_set(
|
|
251
|
+
key: str = typer.Argument(..., help="Configuration key"),
|
|
252
|
+
value: str = typer.Argument(..., help="Configuration value"),
|
|
253
|
+
):
|
|
254
|
+
"""Set a configuration value."""
|
|
255
|
+
config = load_config()
|
|
256
|
+
|
|
257
|
+
if hasattr(config, key):
|
|
258
|
+
# Convert value to appropriate type
|
|
259
|
+
current = getattr(config, key)
|
|
260
|
+
converted_value: Union[bool, int, str]
|
|
261
|
+
if isinstance(current, bool):
|
|
262
|
+
converted_value = value.lower() in ("true", "1", "yes")
|
|
263
|
+
elif isinstance(current, int):
|
|
264
|
+
converted_value = int(value)
|
|
265
|
+
else:
|
|
266
|
+
converted_value = value
|
|
267
|
+
|
|
268
|
+
setattr(config, key, converted_value)
|
|
269
|
+
save_config(config)
|
|
270
|
+
console.print(f"[green]✓[/green] Set {key} = {value}")
|
|
271
|
+
else:
|
|
272
|
+
console.print(f"[red]Error:[/red] Unknown configuration key: {key}")
|
|
273
|
+
raise typer.Exit(1)
|
|
274
|
+
|
|
275
|
+
@config_app.command("path")
|
|
276
|
+
def config_path():
|
|
277
|
+
"""Show configuration file path."""
|
|
278
|
+
console.print(f"[cyan]{DEFAULT_CONFIG_FILE}[/cyan]")
|
|
279
|
+
if DEFAULT_CONFIG_FILE.exists():
|
|
280
|
+
console.print("[green] (exists)[/green]")
|
|
281
|
+
else:
|
|
282
|
+
console.print("[yellow] (not created)[/yellow]")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# =============================================================================
|
|
286
|
+
# TEST COMMAND
|
|
287
|
+
# =============================================================================
|
|
288
|
+
|
|
289
|
+
if HAS_RICH:
|
|
290
|
+
|
|
291
|
+
@app.command()
|
|
292
|
+
def test(
|
|
293
|
+
config_file: Optional[Path] = typer.Option(None, "--config", "-c", help="Config file path"),
|
|
294
|
+
url: Optional[str] = typer.Option(None, "--url", help="PwnDoc server URL"),
|
|
295
|
+
username: Optional[str] = typer.Option(None, "--username", "-u", help="PwnDoc username"),
|
|
296
|
+
password: Optional[str] = typer.Option(None, "--password", "-p", help="PwnDoc password"),
|
|
297
|
+
token: Optional[str] = typer.Option(None, "--token", "-t", help="PwnDoc JWT token"),
|
|
298
|
+
):
|
|
299
|
+
"""Test connection to PwnDoc server."""
|
|
300
|
+
# Build config overrides from CLI arguments
|
|
301
|
+
overrides = {}
|
|
302
|
+
if url:
|
|
303
|
+
overrides["url"] = url
|
|
304
|
+
if username:
|
|
305
|
+
overrides["username"] = username
|
|
306
|
+
if password:
|
|
307
|
+
overrides["password"] = password
|
|
308
|
+
if token:
|
|
309
|
+
overrides["token"] = token
|
|
310
|
+
|
|
311
|
+
config = load_config(config_file=config_file, **overrides)
|
|
312
|
+
|
|
313
|
+
if not config.is_configured:
|
|
314
|
+
console.print(
|
|
315
|
+
"[red]Error:[/red] Server not configured. Run 'pwndoc-mcp config init' first."
|
|
316
|
+
)
|
|
317
|
+
raise typer.Exit(1)
|
|
318
|
+
|
|
319
|
+
console.print(f"Testing connection to [cyan]{config.url}[/cyan]...")
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
with PwnDocClient(config) as client:
|
|
323
|
+
client.authenticate()
|
|
324
|
+
console.print("[green]✓ Authentication successful[/green]")
|
|
325
|
+
|
|
326
|
+
user = client.get_current_user()
|
|
327
|
+
console.print(f"[green]✓ Logged in as:[/green] {user.get('username', 'unknown')}")
|
|
328
|
+
|
|
329
|
+
audits = client.list_audits()
|
|
330
|
+
console.print(f"[green]✓ Found {len(audits)} audits[/green]")
|
|
331
|
+
|
|
332
|
+
console.print("\n[bold green]All tests passed![/bold green]")
|
|
333
|
+
|
|
334
|
+
except PwnDocError as e:
|
|
335
|
+
console.print(f"[red]✗ Connection failed:[/red] {e}")
|
|
336
|
+
raise typer.Exit(1)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
console.print(f"[red]✗ Error:[/red] {e}")
|
|
339
|
+
raise typer.Exit(1)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# =============================================================================
|
|
343
|
+
# QUERY COMMAND (for quick queries)
|
|
344
|
+
# =============================================================================
|
|
345
|
+
|
|
346
|
+
if HAS_RICH:
|
|
347
|
+
|
|
348
|
+
@app.command()
|
|
349
|
+
def query(
|
|
350
|
+
tool: str = typer.Argument(..., help="Tool name to call"),
|
|
351
|
+
params: Optional[str] = typer.Option(None, "--params", "-p", help="JSON parameters"),
|
|
352
|
+
config_file: Optional[Path] = typer.Option(None, "--config", "-c", help="Config file path"),
|
|
353
|
+
):
|
|
354
|
+
"""Execute a tool query directly."""
|
|
355
|
+
config = load_config(config_file=config_file)
|
|
356
|
+
|
|
357
|
+
if not config.is_configured:
|
|
358
|
+
console.print("[red]Error:[/red] Server not configured.")
|
|
359
|
+
raise typer.Exit(1)
|
|
360
|
+
|
|
361
|
+
server = PwnDocMCPServer(config)
|
|
362
|
+
|
|
363
|
+
if tool not in server._tools:
|
|
364
|
+
console.print(f"[red]Error:[/red] Unknown tool: {tool}")
|
|
365
|
+
console.print("\nAvailable tools:")
|
|
366
|
+
for t in sorted(server._tools.keys()):
|
|
367
|
+
console.print(f" • {t}")
|
|
368
|
+
raise typer.Exit(1)
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
arguments = json.loads(params) if params else {}
|
|
372
|
+
result = server._tools[tool].handler(**arguments)
|
|
373
|
+
|
|
374
|
+
# Pretty print result
|
|
375
|
+
syntax = Syntax(json.dumps(result, indent=2, default=str), "json", theme="monokai")
|
|
376
|
+
console.print(syntax)
|
|
377
|
+
|
|
378
|
+
except json.JSONDecodeError as e:
|
|
379
|
+
console.print(f"[red]Error:[/red] Invalid JSON parameters: {e}")
|
|
380
|
+
raise typer.Exit(1)
|
|
381
|
+
except Exception as e:
|
|
382
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
383
|
+
raise typer.Exit(1)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# =============================================================================
|
|
387
|
+
# TOOLS COMMAND
|
|
388
|
+
# =============================================================================
|
|
389
|
+
|
|
390
|
+
if HAS_RICH:
|
|
391
|
+
|
|
392
|
+
@app.command()
|
|
393
|
+
def tools():
|
|
394
|
+
"""List all available MCP tools."""
|
|
395
|
+
config = load_config()
|
|
396
|
+
server = PwnDocMCPServer(config)
|
|
397
|
+
|
|
398
|
+
table = Table(title="Available Tools")
|
|
399
|
+
table.add_column("Tool", style="cyan")
|
|
400
|
+
table.add_column("Description", style="white")
|
|
401
|
+
|
|
402
|
+
for name, tool in sorted(server._tools.items()):
|
|
403
|
+
table.add_row(
|
|
404
|
+
name,
|
|
405
|
+
tool.description[:60] + "..." if len(tool.description) > 60 else tool.description,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
console.print(table)
|
|
409
|
+
console.print(f"\nTotal: [cyan]{len(server._tools)}[/cyan] tools")
|
|
410
|
+
|
|
411
|
+
# =============================================================================
|
|
412
|
+
# MCP CLAUDE INTEGRATION COMMANDS
|
|
413
|
+
# =============================================================================
|
|
414
|
+
|
|
415
|
+
@app.command()
|
|
416
|
+
def claude_install(
|
|
417
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing configuration"),
|
|
418
|
+
env_file: Optional[str] = typer.Option(
|
|
419
|
+
None, "--env-file", help="Path to .env file with credentials"
|
|
420
|
+
),
|
|
421
|
+
skip_check: bool = typer.Option(
|
|
422
|
+
False, "--skip-check", help="Skip Claude Desktop installation check"
|
|
423
|
+
),
|
|
424
|
+
):
|
|
425
|
+
"""Install PwnDoc MCP server for Claude Desktop."""
|
|
426
|
+
try:
|
|
427
|
+
console.print("\n[cyan]Installing PwnDoc MCP for Claude Desktop...[/cyan]\n")
|
|
428
|
+
|
|
429
|
+
# Check if Claude Desktop is installed
|
|
430
|
+
if not skip_check and not is_claude_installed():
|
|
431
|
+
console.print(
|
|
432
|
+
"[yellow]⚠ Claude Desktop does not appear to be installed.[/yellow]\n"
|
|
433
|
+
)
|
|
434
|
+
console.print(
|
|
435
|
+
"The configuration will be created, but Claude Desktop won't use it until installed.\n"
|
|
436
|
+
)
|
|
437
|
+
console.print(
|
|
438
|
+
"To use with other MCP clients, see: [cyan]pwndoc-mcp serve --help[/cyan]\n"
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
proceed = Prompt.ask("Continue anyway?", choices=["y", "n"], default="n")
|
|
442
|
+
if proceed.lower() != "y":
|
|
443
|
+
console.print("[yellow]Installation cancelled[/yellow]")
|
|
444
|
+
raise typer.Exit(0)
|
|
445
|
+
|
|
446
|
+
# Build environment variables from config if needed
|
|
447
|
+
env_vars = {}
|
|
448
|
+
config = load_config()
|
|
449
|
+
if config.url:
|
|
450
|
+
env_vars["PWNDOC_URL"] = config.url
|
|
451
|
+
if config.username:
|
|
452
|
+
env_vars["PWNDOC_USERNAME"] = config.username
|
|
453
|
+
if config.password:
|
|
454
|
+
env_vars["PWNDOC_PASSWORD"] = config.password
|
|
455
|
+
if config.token:
|
|
456
|
+
env_vars["PWNDOC_TOKEN"] = config.token
|
|
457
|
+
|
|
458
|
+
# Install configuration
|
|
459
|
+
success = install_mcp_config(env=env_vars if env_vars else None, force=force)
|
|
460
|
+
|
|
461
|
+
if success:
|
|
462
|
+
config_path = get_claude_config_path()
|
|
463
|
+
console.print(
|
|
464
|
+
Panel(
|
|
465
|
+
f"[green]✓[/green] PwnDoc MCP installed successfully!\n\n"
|
|
466
|
+
f"Config file: [cyan]{config_path}[/cyan]\n\n"
|
|
467
|
+
f"[yellow]Next steps:[/yellow]\n"
|
|
468
|
+
f"1. Restart Claude Desktop\n"
|
|
469
|
+
f"2. PwnDoc tools will be available in Claude\n\n"
|
|
470
|
+
f"[dim]For other MCP clients: pwndoc-mcp serve[/dim]",
|
|
471
|
+
title="Installation Complete",
|
|
472
|
+
border_style="green",
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
else:
|
|
476
|
+
console.print("[yellow]Already installed (use --force to overwrite)[/yellow]")
|
|
477
|
+
|
|
478
|
+
except Exception as e:
|
|
479
|
+
console.print(f"[red]✗ Installation failed: {e}[/red]")
|
|
480
|
+
raise typer.Exit(1)
|
|
481
|
+
|
|
482
|
+
@app.command()
|
|
483
|
+
def claude_uninstall():
|
|
484
|
+
"""Remove PwnDoc MCP server from Claude Desktop."""
|
|
485
|
+
try:
|
|
486
|
+
console.print("\n[cyan]Uninstalling PwnDoc MCP from Claude Desktop...[/cyan]\n")
|
|
487
|
+
|
|
488
|
+
success = uninstall_mcp_config()
|
|
489
|
+
|
|
490
|
+
if success:
|
|
491
|
+
console.print(
|
|
492
|
+
Panel(
|
|
493
|
+
"[green]✓[/green] PwnDoc MCP uninstalled successfully!\n\n"
|
|
494
|
+
"[yellow]Restart Claude Desktop to apply changes.[/yellow]",
|
|
495
|
+
title="Uninstallation Complete",
|
|
496
|
+
border_style="green",
|
|
497
|
+
)
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
console.print("[yellow]Not installed[/yellow]")
|
|
501
|
+
|
|
502
|
+
except Exception as e:
|
|
503
|
+
console.print(f"[red]✗ Uninstallation failed: {e}[/red]")
|
|
504
|
+
raise typer.Exit(1)
|
|
505
|
+
|
|
506
|
+
@app.command()
|
|
507
|
+
def claude_status():
|
|
508
|
+
"""Show Claude Desktop MCP configuration status."""
|
|
509
|
+
try:
|
|
510
|
+
config_path = get_claude_config_path()
|
|
511
|
+
console.print(f"\nClaude config path: [cyan]{config_path}[/cyan]")
|
|
512
|
+
|
|
513
|
+
if not config_path.exists():
|
|
514
|
+
console.print("[yellow]Claude configuration file not found[/yellow]")
|
|
515
|
+
if is_claude_installed():
|
|
516
|
+
console.print("[green]✓[/green] Claude Desktop appears to be installed")
|
|
517
|
+
console.print("Config file will be created on first MCP installation")
|
|
518
|
+
else:
|
|
519
|
+
console.print("[yellow]Have you installed Claude Desktop?[/yellow]")
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
# Get all MCP servers
|
|
523
|
+
all_servers = get_all_mcp_servers()
|
|
524
|
+
|
|
525
|
+
if not all_servers:
|
|
526
|
+
console.print("\n[yellow]No MCP servers configured[/yellow]")
|
|
527
|
+
console.print("\nRun [cyan]pwndoc-mcp claude-install[/cyan] to install")
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
# Check if pwndoc-mcp is configured
|
|
531
|
+
mcp_config = all_servers.get("pwndoc-mcp")
|
|
532
|
+
|
|
533
|
+
console.print(f"\n[cyan]Found {len(all_servers)} MCP server(s) configured:[/cyan]\n")
|
|
534
|
+
|
|
535
|
+
if mcp_config:
|
|
536
|
+
console.print("[green]✓[/green] PwnDoc MCP is installed\n")
|
|
537
|
+
|
|
538
|
+
# Display pwndoc-mcp configuration
|
|
539
|
+
syntax = Syntax(
|
|
540
|
+
json.dumps({"pwndoc-mcp": mcp_config}, indent=2), "json", theme="monokai"
|
|
541
|
+
)
|
|
542
|
+
console.print(syntax)
|
|
543
|
+
|
|
544
|
+
# Show other servers if any
|
|
545
|
+
other_servers = {k: v for k, v in all_servers.items() if k != "pwndoc-mcp"}
|
|
546
|
+
if other_servers:
|
|
547
|
+
console.print(
|
|
548
|
+
f"\n[dim]Other MCP servers: {', '.join(other_servers.keys())}[/dim]"
|
|
549
|
+
)
|
|
550
|
+
else:
|
|
551
|
+
console.print("[yellow]PwnDoc MCP is not installed[/yellow]\n")
|
|
552
|
+
|
|
553
|
+
# Show what IS configured
|
|
554
|
+
console.print("[cyan]Configured MCP servers:[/cyan]")
|
|
555
|
+
for server_name in all_servers.keys():
|
|
556
|
+
console.print(f" • {server_name}")
|
|
557
|
+
|
|
558
|
+
console.print("\nRun [cyan]pwndoc-mcp claude-install[/cyan] to install PwnDoc MCP")
|
|
559
|
+
|
|
560
|
+
except Exception as e:
|
|
561
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
562
|
+
raise typer.Exit(1)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# =============================================================================
|
|
566
|
+
# MAIN ENTRY POINT
|
|
567
|
+
# =============================================================================
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def main():
|
|
571
|
+
"""Main entry point for CLI."""
|
|
572
|
+
if not HAS_RICH:
|
|
573
|
+
# Fallback to simple argparse if rich/typer not available
|
|
574
|
+
import argparse
|
|
575
|
+
|
|
576
|
+
parser = argparse.ArgumentParser(
|
|
577
|
+
description="PwnDoc MCP Server",
|
|
578
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
579
|
+
epilog="""
|
|
580
|
+
Examples:
|
|
581
|
+
pwndoc-mcp serve Start MCP server (stdio)
|
|
582
|
+
pwndoc-mcp serve --transport sse Start SSE server
|
|
583
|
+
|
|
584
|
+
For rich CLI experience, install: pip install typer[all] rich
|
|
585
|
+
""",
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
subparsers = parser.add_subparsers(dest="command", help="Commands")
|
|
589
|
+
|
|
590
|
+
# Serve command
|
|
591
|
+
serve_parser = subparsers.add_parser("serve", help="Start MCP server")
|
|
592
|
+
serve_parser.add_argument("--transport", default="stdio", choices=["stdio", "sse"])
|
|
593
|
+
serve_parser.add_argument("--host", default="127.0.0.1")
|
|
594
|
+
serve_parser.add_argument("--port", type=int, default=8080)
|
|
595
|
+
serve_parser.add_argument("--log-level", default="INFO")
|
|
596
|
+
serve_parser.add_argument("--config", "-c", type=Path)
|
|
597
|
+
|
|
598
|
+
# Version command
|
|
599
|
+
subparsers.add_parser("version", help="Show version")
|
|
600
|
+
|
|
601
|
+
# Test command
|
|
602
|
+
test_parser = subparsers.add_parser("test", help="Test connection")
|
|
603
|
+
test_parser.add_argument("--config", "-c", type=Path)
|
|
604
|
+
|
|
605
|
+
args = parser.parse_args()
|
|
606
|
+
|
|
607
|
+
if args.command == "version":
|
|
608
|
+
print(f"PwnDoc MCP Server v{__version__}")
|
|
609
|
+
elif args.command == "serve":
|
|
610
|
+
setup_logging(args.log_level)
|
|
611
|
+
config = load_config(
|
|
612
|
+
config_file=args.config,
|
|
613
|
+
mcp_transport=args.transport,
|
|
614
|
+
mcp_host=args.host,
|
|
615
|
+
mcp_port=args.port,
|
|
616
|
+
)
|
|
617
|
+
server = PwnDocMCPServer(config)
|
|
618
|
+
server.run()
|
|
619
|
+
elif args.command == "test":
|
|
620
|
+
config = load_config(config_file=args.config)
|
|
621
|
+
try:
|
|
622
|
+
with PwnDocClient(config) as client:
|
|
623
|
+
client.authenticate()
|
|
624
|
+
print("✓ Connection successful!")
|
|
625
|
+
except Exception as e:
|
|
626
|
+
print(f"✗ Connection failed: {e}")
|
|
627
|
+
sys.exit(1)
|
|
628
|
+
else:
|
|
629
|
+
parser.print_help()
|
|
630
|
+
else:
|
|
631
|
+
app()
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
if __name__ == "__main__":
|
|
635
|
+
main()
|