pwndoc-mcp-server 1.0.2__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.

Potentially problematic release.


This version of pwndoc-mcp-server might be problematic. Click here for more details.

@@ -0,0 +1,57 @@
1
+ """
2
+ PwnDoc MCP Server
3
+
4
+ Model Context Protocol server for PwnDoc penetration testing documentation.
5
+ """
6
+
7
+ from pwndoc_mcp_server.version import get_version
8
+
9
+ __version__ = get_version()
10
+ __author__ = "Walid Faour"
11
+
12
+ # Export main classes and functions
13
+ from pwndoc_mcp_server.client import (
14
+ AuthenticationError,
15
+ NotFoundError,
16
+ PwnDocClient,
17
+ PwnDocError,
18
+ RateLimitError,
19
+ )
20
+ from pwndoc_mcp_server.config import (
21
+ Config,
22
+ get_config_path,
23
+ init_config_interactive,
24
+ load_config,
25
+ save_config,
26
+ )
27
+ from pwndoc_mcp_server.logging_config import LogLevel, get_logger, setup_logging
28
+ from pwndoc_mcp_server.server import (
29
+ TOOL_DEFINITIONS,
30
+ PwnDocMCPServer,
31
+ create_server,
32
+ )
33
+
34
+ __all__ = [
35
+ # Version
36
+ "__version__",
37
+ # Config
38
+ "Config",
39
+ "get_config_path",
40
+ "init_config_interactive",
41
+ "load_config",
42
+ "save_config",
43
+ # Client
44
+ "PwnDocClient",
45
+ "PwnDocError",
46
+ "AuthenticationError",
47
+ "RateLimitError",
48
+ "NotFoundError",
49
+ # Server
50
+ "PwnDocMCPServer",
51
+ "TOOL_DEFINITIONS",
52
+ "create_server",
53
+ # Logging
54
+ "LogLevel",
55
+ "setup_logging",
56
+ "get_logger",
57
+ ]
@@ -0,0 +1,441 @@
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.server import PwnDocMCPServer
45
+
46
+ # Create CLI app
47
+ if HAS_RICH:
48
+ app = typer.Typer(
49
+ name="pwndoc-mcp",
50
+ help="PwnDoc MCP Server - Model Context Protocol for Pentest Documentation",
51
+ add_completion=True,
52
+ )
53
+ console = Console()
54
+
55
+ def version_callback(value: bool):
56
+ """Callback for --version flag."""
57
+ if value:
58
+ console.print(f"pwndoc-mcp-server version {__version__}")
59
+ raise typer.Exit()
60
+
61
+ @app.callback()
62
+ def cli_callback(
63
+ version: Optional[bool] = typer.Option(
64
+ None,
65
+ "--version",
66
+ "-v",
67
+ help="Show version and exit",
68
+ callback=version_callback,
69
+ is_eager=True,
70
+ )
71
+ ):
72
+ """PwnDoc MCP Server CLI."""
73
+ pass
74
+
75
+ else:
76
+ app = None # type: ignore[assignment]
77
+ console = None # type: ignore[assignment]
78
+ version_callback = None # type: ignore[assignment]
79
+
80
+
81
+ def setup_logging(level: str = "INFO", log_file: Optional[str] = None):
82
+ """Configure logging."""
83
+ handlers: list[logging.Handler] = [logging.StreamHandler()]
84
+ if log_file:
85
+ handlers.append(logging.FileHandler(log_file))
86
+
87
+ logging.basicConfig(
88
+ level=getattr(logging, level.upper()),
89
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
90
+ handlers=handlers,
91
+ )
92
+
93
+
94
+ # =============================================================================
95
+ # VERSION COMMAND
96
+ # =============================================================================
97
+
98
+ if HAS_RICH:
99
+
100
+ @app.command()
101
+ def version():
102
+ """Show version information."""
103
+ console.print(
104
+ Panel(
105
+ f"[bold green]PwnDoc MCP Server[/bold green]\n"
106
+ f"Version: [cyan]{__version__}[/cyan]\n"
107
+ f"Python: [cyan]{sys.version.split()[0]}[/cyan]",
108
+ title="Version Info",
109
+ )
110
+ )
111
+
112
+
113
+ # =============================================================================
114
+ # SERVE COMMAND
115
+ # =============================================================================
116
+
117
+ if HAS_RICH:
118
+
119
+ @app.command()
120
+ def serve(
121
+ transport: str = typer.Option("stdio", help="Transport type (stdio, sse)"),
122
+ host: str = typer.Option("127.0.0.1", help="Host for SSE/WebSocket"),
123
+ port: int = typer.Option(8080, help="Port for SSE/WebSocket"),
124
+ log_level: str = typer.Option("INFO", help="Log level"),
125
+ log_file: Optional[str] = typer.Option(None, help="Log file path"),
126
+ config_file: Optional[Path] = typer.Option(None, "--config", "-c", help="Config file path"),
127
+ ):
128
+ """Start the MCP server."""
129
+ setup_logging(log_level, log_file)
130
+
131
+ try:
132
+ config = load_config(
133
+ config_file=config_file,
134
+ mcp_transport=transport,
135
+ mcp_host=host,
136
+ mcp_port=port,
137
+ log_level=log_level,
138
+ log_file=log_file,
139
+ )
140
+
141
+ if not config.is_configured:
142
+ console.print(
143
+ "[red]Error:[/red] Server not configured. Run 'pwndoc-mcp config init' first."
144
+ )
145
+ raise typer.Exit(1)
146
+
147
+ if transport != "stdio":
148
+ console.print("[green]Starting PwnDoc MCP Server[/green]")
149
+ console.print(f" Transport: [cyan]{transport}[/cyan]")
150
+ console.print(f" URL: [cyan]{config.url}[/cyan]")
151
+ if transport == "sse":
152
+ console.print(f" Endpoint: [cyan]http://{host}:{port}/mcp[/cyan]")
153
+
154
+ server = PwnDocMCPServer(config)
155
+ server.run(transport)
156
+
157
+ except KeyboardInterrupt:
158
+ console.print("\n[yellow]Server stopped[/yellow]")
159
+ except Exception as e:
160
+ console.print(f"[red]Error:[/red] {e}")
161
+ raise typer.Exit(1)
162
+
163
+
164
+ # =============================================================================
165
+ # CONFIG COMMANDS
166
+ # =============================================================================
167
+
168
+ if HAS_RICH:
169
+ config_app = typer.Typer(help="Configuration management")
170
+ app.add_typer(config_app, name="config")
171
+
172
+ @config_app.command("init")
173
+ def config_init():
174
+ """Interactive configuration wizard."""
175
+ try:
176
+ init_config_interactive()
177
+ console.print("[green]✓ Configuration complete![/green]")
178
+ except KeyboardInterrupt:
179
+ console.print("\n[yellow]Configuration cancelled[/yellow]")
180
+
181
+ @config_app.command("show")
182
+ def config_show(
183
+ reveal_secrets: bool = typer.Option(False, "--reveal", help="Show sensitive values"),
184
+ ):
185
+ """Show current configuration."""
186
+ config = load_config()
187
+
188
+ table = Table(title="Current Configuration")
189
+ table.add_column("Setting", style="cyan")
190
+ table.add_column("Value", style="green")
191
+
192
+ table.add_row("URL", config.url or "[dim]not set[/dim]")
193
+ table.add_row("Username", config.username or "[dim]not set[/dim]")
194
+ table.add_row(
195
+ "Password",
196
+ (
197
+ ("*" * 8 if config.password else "[dim]not set[/dim]")
198
+ if not reveal_secrets
199
+ else config.password
200
+ ),
201
+ )
202
+ table.add_row(
203
+ "Token",
204
+ (
205
+ ("*" * 20 if config.token else "[dim]not set[/dim]")
206
+ if not reveal_secrets
207
+ else (config.token or "")
208
+ ),
209
+ )
210
+ table.add_row("Verify SSL", str(config.verify_ssl))
211
+ table.add_row("Timeout", str(config.timeout))
212
+ table.add_row("Log Level", config.log_level)
213
+ table.add_row("MCP Transport", config.mcp_transport)
214
+ table.add_row("Auth Method", config.auth_method)
215
+ table.add_row(
216
+ "Configured",
217
+ "[green]Yes[/green]" if config.is_configured else "[red]No[/red]",
218
+ )
219
+
220
+ console.print(table)
221
+ console.print(f"\nConfig file: [dim]{DEFAULT_CONFIG_FILE}[/dim]")
222
+
223
+ @config_app.command("set")
224
+ def config_set(
225
+ key: str = typer.Argument(..., help="Configuration key"),
226
+ value: str = typer.Argument(..., help="Configuration value"),
227
+ ):
228
+ """Set a configuration value."""
229
+ config = load_config()
230
+
231
+ if hasattr(config, key):
232
+ # Convert value to appropriate type
233
+ current = getattr(config, key)
234
+ converted_value: Union[bool, int, str]
235
+ if isinstance(current, bool):
236
+ converted_value = value.lower() in ("true", "1", "yes")
237
+ elif isinstance(current, int):
238
+ converted_value = int(value)
239
+ else:
240
+ converted_value = value
241
+
242
+ setattr(config, key, converted_value)
243
+ save_config(config)
244
+ console.print(f"[green]✓[/green] Set {key} = {value}")
245
+ else:
246
+ console.print(f"[red]Error:[/red] Unknown configuration key: {key}")
247
+ raise typer.Exit(1)
248
+
249
+ @config_app.command("path")
250
+ def config_path():
251
+ """Show configuration file path."""
252
+ console.print(f"[cyan]{DEFAULT_CONFIG_FILE}[/cyan]")
253
+ if DEFAULT_CONFIG_FILE.exists():
254
+ console.print("[green] (exists)[/green]")
255
+ else:
256
+ console.print("[yellow] (not created)[/yellow]")
257
+
258
+
259
+ # =============================================================================
260
+ # TEST COMMAND
261
+ # =============================================================================
262
+
263
+ if HAS_RICH:
264
+
265
+ @app.command()
266
+ def test(
267
+ config_file: Optional[Path] = typer.Option(None, "--config", "-c", help="Config file path"),
268
+ ):
269
+ """Test connection to PwnDoc server."""
270
+ config = load_config(config_file=config_file)
271
+
272
+ if not config.is_configured:
273
+ console.print(
274
+ "[red]Error:[/red] Server not configured. Run 'pwndoc-mcp config init' first."
275
+ )
276
+ raise typer.Exit(1)
277
+
278
+ console.print(f"Testing connection to [cyan]{config.url}[/cyan]...")
279
+
280
+ try:
281
+ with PwnDocClient(config) as client:
282
+ client.authenticate()
283
+ console.print("[green]✓ Authentication successful[/green]")
284
+
285
+ user = client.get_current_user()
286
+ console.print(f"[green]✓ Logged in as:[/green] {user.get('username', 'unknown')}")
287
+
288
+ audits = client.list_audits()
289
+ console.print(f"[green]✓ Found {len(audits)} audits[/green]")
290
+
291
+ console.print("\n[bold green]All tests passed![/bold green]")
292
+
293
+ except PwnDocError as e:
294
+ console.print(f"[red]✗ Connection failed:[/red] {e}")
295
+ raise typer.Exit(1)
296
+ except Exception as e:
297
+ console.print(f"[red]✗ Error:[/red] {e}")
298
+ raise typer.Exit(1)
299
+
300
+
301
+ # =============================================================================
302
+ # QUERY COMMAND (for quick queries)
303
+ # =============================================================================
304
+
305
+ if HAS_RICH:
306
+
307
+ @app.command()
308
+ def query(
309
+ tool: str = typer.Argument(..., help="Tool name to call"),
310
+ params: Optional[str] = typer.Option(None, "--params", "-p", help="JSON parameters"),
311
+ config_file: Optional[Path] = typer.Option(None, "--config", "-c", help="Config file path"),
312
+ ):
313
+ """Execute a tool query directly."""
314
+ config = load_config(config_file=config_file)
315
+
316
+ if not config.is_configured:
317
+ console.print("[red]Error:[/red] Server not configured.")
318
+ raise typer.Exit(1)
319
+
320
+ server = PwnDocMCPServer(config)
321
+
322
+ if tool not in server._tools:
323
+ console.print(f"[red]Error:[/red] Unknown tool: {tool}")
324
+ console.print("\nAvailable tools:")
325
+ for t in sorted(server._tools.keys()):
326
+ console.print(f" • {t}")
327
+ raise typer.Exit(1)
328
+
329
+ try:
330
+ arguments = json.loads(params) if params else {}
331
+ result = server._tools[tool].handler(**arguments)
332
+
333
+ # Pretty print result
334
+ syntax = Syntax(json.dumps(result, indent=2, default=str), "json", theme="monokai")
335
+ console.print(syntax)
336
+
337
+ except json.JSONDecodeError as e:
338
+ console.print(f"[red]Error:[/red] Invalid JSON parameters: {e}")
339
+ raise typer.Exit(1)
340
+ except Exception as e:
341
+ console.print(f"[red]Error:[/red] {e}")
342
+ raise typer.Exit(1)
343
+
344
+
345
+ # =============================================================================
346
+ # TOOLS COMMAND
347
+ # =============================================================================
348
+
349
+ if HAS_RICH:
350
+
351
+ @app.command()
352
+ def tools():
353
+ """List all available MCP tools."""
354
+ config = load_config()
355
+ server = PwnDocMCPServer(config)
356
+
357
+ table = Table(title="Available Tools")
358
+ table.add_column("Tool", style="cyan")
359
+ table.add_column("Description", style="white")
360
+
361
+ for name, tool in sorted(server._tools.items()):
362
+ table.add_row(
363
+ name,
364
+ tool.description[:60] + "..." if len(tool.description) > 60 else tool.description,
365
+ )
366
+
367
+ console.print(table)
368
+ console.print(f"\nTotal: [cyan]{len(server._tools)}[/cyan] tools")
369
+
370
+
371
+ # =============================================================================
372
+ # MAIN ENTRY POINT
373
+ # =============================================================================
374
+
375
+
376
+ def main():
377
+ """Main entry point for CLI."""
378
+ if not HAS_RICH:
379
+ # Fallback to simple argparse if rich/typer not available
380
+ import argparse
381
+
382
+ parser = argparse.ArgumentParser(
383
+ description="PwnDoc MCP Server",
384
+ formatter_class=argparse.RawDescriptionHelpFormatter,
385
+ epilog="""
386
+ Examples:
387
+ pwndoc-mcp serve Start MCP server (stdio)
388
+ pwndoc-mcp serve --transport sse Start SSE server
389
+
390
+ For rich CLI experience, install: pip install typer[all] rich
391
+ """,
392
+ )
393
+
394
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
395
+
396
+ # Serve command
397
+ serve_parser = subparsers.add_parser("serve", help="Start MCP server")
398
+ serve_parser.add_argument("--transport", default="stdio", choices=["stdio", "sse"])
399
+ serve_parser.add_argument("--host", default="127.0.0.1")
400
+ serve_parser.add_argument("--port", type=int, default=8080)
401
+ serve_parser.add_argument("--log-level", default="INFO")
402
+ serve_parser.add_argument("--config", "-c", type=Path)
403
+
404
+ # Version command
405
+ subparsers.add_parser("version", help="Show version")
406
+
407
+ # Test command
408
+ test_parser = subparsers.add_parser("test", help="Test connection")
409
+ test_parser.add_argument("--config", "-c", type=Path)
410
+
411
+ args = parser.parse_args()
412
+
413
+ if args.command == "version":
414
+ print(f"PwnDoc MCP Server v{__version__}")
415
+ elif args.command == "serve":
416
+ setup_logging(args.log_level)
417
+ config = load_config(
418
+ config_file=args.config,
419
+ mcp_transport=args.transport,
420
+ mcp_host=args.host,
421
+ mcp_port=args.port,
422
+ )
423
+ server = PwnDocMCPServer(config)
424
+ server.run()
425
+ elif args.command == "test":
426
+ config = load_config(config_file=args.config)
427
+ try:
428
+ with PwnDocClient(config) as client:
429
+ client.authenticate()
430
+ print("✓ Connection successful!")
431
+ except Exception as e:
432
+ print(f"✗ Connection failed: {e}")
433
+ sys.exit(1)
434
+ else:
435
+ parser.print_help()
436
+ else:
437
+ app()
438
+
439
+
440
+ if __name__ == "__main__":
441
+ main()