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,950 @@
1
+ """
2
+ PwnDoc MCP Server - Model Context Protocol implementation.
3
+
4
+ This module implements the MCP server that exposes PwnDoc functionality
5
+ to AI assistants like Claude through a standardized protocol.
6
+
7
+ Supports multiple transports:
8
+ - stdio: Standard input/output (default, for Claude Desktop)
9
+ - sse: Server-Sent Events (for web clients)
10
+ - websocket: WebSocket (for real-time applications)
11
+ """
12
+
13
+ import asyncio
14
+ import json
15
+ import logging
16
+ import sys
17
+ from dataclasses import dataclass, field
18
+ from typing import Any, Callable, Dict, List, Optional, Union
19
+
20
+ from .client import PwnDocClient, PwnDocError
21
+ from .config import Config, load_config
22
+ from .version import get_version
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ @dataclass
28
+ class Tool:
29
+ """MCP Tool definition."""
30
+
31
+ name: str
32
+ description: str
33
+ parameters: Dict[str, Any]
34
+ handler: Callable
35
+ required: List[str] = field(default_factory=list)
36
+
37
+
38
+ @dataclass
39
+ class MCPMessage:
40
+ """MCP protocol message."""
41
+
42
+ jsonrpc: str = "2.0"
43
+ id: Optional[Union[str, int]] = None
44
+ method: Optional[str] = None
45
+ params: Optional[Dict[str, Any]] = None
46
+ result: Optional[Any] = None
47
+ error: Optional[Dict[str, Any]] = None
48
+
49
+
50
+ class PwnDocMCPServer:
51
+ """
52
+ Model Context Protocol server for PwnDoc.
53
+
54
+ Exposes PwnDoc API functionality as MCP tools that can be called
55
+ by AI assistants like Claude.
56
+
57
+ Example:
58
+ >>> server = PwnDocMCPServer(config)
59
+ >>> server.run() # Starts stdio transport
60
+ """
61
+
62
+ SERVER_NAME = "pwndoc-mcp-server"
63
+ SERVER_VERSION = get_version()
64
+ PROTOCOL_VERSION = "2024-11-05"
65
+
66
+ def __init__(self, config: Optional[Config] = None, transport: Optional[str] = None):
67
+ """
68
+ Initialize MCP server.
69
+
70
+ Args:
71
+ config: Configuration object (loads from environment if not provided)
72
+ transport: Transport type (stdio, sse, websocket)
73
+ """
74
+ self.config = config or load_config()
75
+ self.transport = transport or self.config.mcp_transport
76
+
77
+ # Validate transport
78
+ valid_transports = ["stdio", "sse", "websocket"]
79
+ if self.transport not in valid_transports:
80
+ raise ValueError(f"Invalid transport: {self.transport}")
81
+
82
+ self._client: Optional[PwnDocClient] = None
83
+ self._tools: Dict[str, Tool] = {}
84
+ self._initialized = False
85
+
86
+ # Register all tools
87
+ self._register_tools()
88
+
89
+ logger.info(f"PwnDocMCPServer initialized with {len(self._tools)} tools")
90
+
91
+ @property
92
+ def name(self) -> str:
93
+ """Get server name."""
94
+ return self.SERVER_NAME
95
+
96
+ @property
97
+ def version(self) -> str:
98
+ """Get server version."""
99
+ return self.SERVER_VERSION
100
+
101
+ @property
102
+ def client(self) -> PwnDocClient:
103
+ """Get or create PwnDoc client."""
104
+ if self._client is None:
105
+ self._client = PwnDocClient(self.config)
106
+ self._client.authenticate()
107
+ return self._client
108
+
109
+ def _register_tools(self):
110
+ """Register all available tools."""
111
+
112
+ # =====================================================================
113
+ # AUDIT TOOLS
114
+ # =====================================================================
115
+
116
+ self._register_tool(
117
+ name="list_audits",
118
+ description="List all audits/pentests. Can filter by finding title.",
119
+ parameters={
120
+ "type": "object",
121
+ "properties": {
122
+ "finding_title": {
123
+ "type": "string",
124
+ "description": "Filter audits containing findings with this title (optional)",
125
+ }
126
+ },
127
+ },
128
+ handler=self._handle_list_audits,
129
+ )
130
+
131
+ self._register_tool(
132
+ name="get_audit",
133
+ description="Get detailed information about a specific audit including all findings, scope, sections, and metadata.",
134
+ parameters={
135
+ "type": "object",
136
+ "properties": {
137
+ "audit_id": {"type": "string", "description": "The audit ID (MongoDB ObjectId)"}
138
+ },
139
+ "required": ["audit_id"],
140
+ },
141
+ handler=self._handle_get_audit,
142
+ )
143
+
144
+ self._register_tool(
145
+ name="create_audit",
146
+ description="Create a new audit/pentest.",
147
+ parameters={
148
+ "type": "object",
149
+ "properties": {
150
+ "name": {"type": "string", "description": "Audit name"},
151
+ "language": {"type": "string", "description": "Language code (e.g., 'en')"},
152
+ "audit_type": {"type": "string", "description": "Type of audit"},
153
+ },
154
+ "required": ["name", "language", "audit_type"],
155
+ },
156
+ handler=self._handle_create_audit,
157
+ )
158
+
159
+ self._register_tool(
160
+ name="update_audit_general",
161
+ description="Update general information of an audit.",
162
+ parameters={
163
+ "type": "object",
164
+ "properties": {
165
+ "audit_id": {"type": "string", "description": "The audit ID"},
166
+ "name": {"type": "string", "description": "Audit name"},
167
+ "client": {"type": "string", "description": "Client ID"},
168
+ "company": {"type": "string", "description": "Company ID"},
169
+ "date_start": {"type": "string", "description": "Start date (ISO format)"},
170
+ "date_end": {"type": "string", "description": "End date (ISO format)"},
171
+ "scope": {
172
+ "type": "array",
173
+ "items": {"type": "string"},
174
+ "description": "Scope items",
175
+ },
176
+ },
177
+ "required": ["audit_id"],
178
+ },
179
+ handler=self._handle_update_audit_general,
180
+ )
181
+
182
+ self._register_tool(
183
+ name="delete_audit",
184
+ description="Delete an audit permanently.",
185
+ parameters={
186
+ "type": "object",
187
+ "properties": {
188
+ "audit_id": {"type": "string", "description": "The audit ID to delete"}
189
+ },
190
+ "required": ["audit_id"],
191
+ },
192
+ handler=self._handle_delete_audit,
193
+ )
194
+
195
+ self._register_tool(
196
+ name="generate_audit_report",
197
+ description="Generate and download the audit report (DOCX).",
198
+ parameters={
199
+ "type": "object",
200
+ "properties": {"audit_id": {"type": "string", "description": "The audit ID"}},
201
+ "required": ["audit_id"],
202
+ },
203
+ handler=self._handle_generate_report,
204
+ )
205
+
206
+ # =====================================================================
207
+ # FINDING TOOLS
208
+ # =====================================================================
209
+
210
+ self._register_tool(
211
+ name="get_audit_findings",
212
+ description="Get all findings/vulnerabilities from a specific audit.",
213
+ parameters={
214
+ "type": "object",
215
+ "properties": {"audit_id": {"type": "string", "description": "The audit ID"}},
216
+ "required": ["audit_id"],
217
+ },
218
+ handler=self._handle_get_findings,
219
+ )
220
+
221
+ self._register_tool(
222
+ name="get_finding",
223
+ description="Get details of a specific finding in an audit.",
224
+ parameters={
225
+ "type": "object",
226
+ "properties": {
227
+ "audit_id": {"type": "string", "description": "The audit ID"},
228
+ "finding_id": {"type": "string", "description": "The finding ID"},
229
+ },
230
+ "required": ["audit_id", "finding_id"],
231
+ },
232
+ handler=self._handle_get_finding,
233
+ )
234
+
235
+ self._register_tool(
236
+ name="create_finding",
237
+ description="Create a new finding in an audit.",
238
+ parameters={
239
+ "type": "object",
240
+ "properties": {
241
+ "audit_id": {"type": "string", "description": "The audit ID"},
242
+ "title": {"type": "string", "description": "Finding title"},
243
+ "description": {"type": "string", "description": "Detailed description"},
244
+ "observation": {"type": "string", "description": "Observation/evidence"},
245
+ "remediation": {"type": "string", "description": "Remediation steps"},
246
+ "cvssv3": {"type": "string", "description": "CVSS v3 score/vector"},
247
+ "priority": {"type": "integer", "description": "Priority (1-4)"},
248
+ "category": {"type": "string", "description": "Category"},
249
+ "vuln_type": {"type": "string", "description": "Vulnerability type"},
250
+ "poc": {"type": "string", "description": "Proof of concept"},
251
+ "scope": {"type": "string", "description": "Affected scope"},
252
+ "references": {
253
+ "type": "array",
254
+ "items": {"type": "string"},
255
+ "description": "References",
256
+ },
257
+ },
258
+ "required": ["audit_id", "title"],
259
+ },
260
+ handler=self._handle_create_finding,
261
+ )
262
+
263
+ self._register_tool(
264
+ name="update_finding",
265
+ description="Update an existing finding.",
266
+ parameters={
267
+ "type": "object",
268
+ "properties": {
269
+ "audit_id": {"type": "string", "description": "The audit ID"},
270
+ "finding_id": {"type": "string", "description": "The finding ID"},
271
+ "title": {"type": "string"},
272
+ "description": {"type": "string"},
273
+ "observation": {"type": "string"},
274
+ "remediation": {"type": "string"},
275
+ "cvssv3": {"type": "string"},
276
+ "priority": {"type": "integer"},
277
+ "category": {"type": "string"},
278
+ "vuln_type": {"type": "string"},
279
+ "poc": {"type": "string"},
280
+ "scope": {"type": "string"},
281
+ "references": {"type": "array", "items": {"type": "string"}},
282
+ },
283
+ "required": ["audit_id", "finding_id"],
284
+ },
285
+ handler=self._handle_update_finding,
286
+ )
287
+
288
+ self._register_tool(
289
+ name="delete_finding",
290
+ description="Delete a finding from an audit.",
291
+ parameters={
292
+ "type": "object",
293
+ "properties": {
294
+ "audit_id": {"type": "string", "description": "The audit ID"},
295
+ "finding_id": {"type": "string", "description": "The finding ID to delete"},
296
+ },
297
+ "required": ["audit_id", "finding_id"],
298
+ },
299
+ handler=self._handle_delete_finding,
300
+ )
301
+
302
+ self._register_tool(
303
+ name="search_findings",
304
+ description="Search for findings across all audits by various criteria.",
305
+ parameters={
306
+ "type": "object",
307
+ "properties": {
308
+ "title": {"type": "string", "description": "Search by finding title"},
309
+ "category": {"type": "string", "description": "Filter by category"},
310
+ "severity": {
311
+ "type": "string",
312
+ "description": "Filter by severity (Critical, High, Medium, Low)",
313
+ },
314
+ "status": {"type": "string", "description": "Filter by status"},
315
+ },
316
+ },
317
+ handler=self._handle_search_findings,
318
+ )
319
+
320
+ self._register_tool(
321
+ name="get_all_findings_with_context",
322
+ description="Get ALL findings from ALL audits with full context (company, dates, team, scope, description, CWE, references) in a single request.",
323
+ parameters={
324
+ "type": "object",
325
+ "properties": {
326
+ "include_failed": {
327
+ "type": "boolean",
328
+ "description": "Include 'Failed' category findings (default: false)",
329
+ },
330
+ "exclude_categories": {
331
+ "type": "array",
332
+ "items": {"type": "string"},
333
+ "description": "Categories to exclude",
334
+ },
335
+ },
336
+ },
337
+ handler=self._handle_get_all_findings_with_context,
338
+ )
339
+
340
+ # =====================================================================
341
+ # CLIENT & COMPANY TOOLS
342
+ # =====================================================================
343
+
344
+ self._register_tool(
345
+ name="list_clients",
346
+ description="List all clients.",
347
+ parameters={"type": "object", "properties": {}},
348
+ handler=self._handle_list_clients,
349
+ )
350
+
351
+ self._register_tool(
352
+ name="create_client",
353
+ description="Create a new client.",
354
+ parameters={
355
+ "type": "object",
356
+ "properties": {
357
+ "firstname": {"type": "string", "description": "First name"},
358
+ "lastname": {"type": "string", "description": "Last name"},
359
+ "email": {"type": "string", "description": "Client email"},
360
+ "phone": {"type": "string", "description": "Phone number"},
361
+ "cell": {"type": "string", "description": "Cell phone"},
362
+ "title": {"type": "string", "description": "Job title"},
363
+ "company": {"type": "string", "description": "Company ID"},
364
+ },
365
+ "required": ["email", "firstname", "lastname"],
366
+ },
367
+ handler=self._handle_create_client,
368
+ )
369
+
370
+ self._register_tool(
371
+ name="update_client",
372
+ description="Update an existing client.",
373
+ parameters={
374
+ "type": "object",
375
+ "properties": {
376
+ "client_id": {"type": "string", "description": "Client ID"},
377
+ "firstname": {"type": "string", "description": "First name"},
378
+ "lastname": {"type": "string", "description": "Last name"},
379
+ "email": {"type": "string", "description": "Client email"},
380
+ "phone": {"type": "string", "description": "Phone number"},
381
+ "cell": {"type": "string", "description": "Cell phone"},
382
+ "title": {"type": "string", "description": "Job title"},
383
+ "company": {"type": "string", "description": "Company ID"},
384
+ },
385
+ "required": ["client_id"],
386
+ },
387
+ handler=self._handle_update_client,
388
+ )
389
+
390
+ self._register_tool(
391
+ name="delete_client",
392
+ description="Delete a client.",
393
+ parameters={
394
+ "type": "object",
395
+ "properties": {
396
+ "client_id": {"type": "string", "description": "Client ID to delete"}
397
+ },
398
+ "required": ["client_id"],
399
+ },
400
+ handler=self._handle_delete_client,
401
+ )
402
+
403
+ self._register_tool(
404
+ name="list_companies",
405
+ description="List all companies.",
406
+ parameters={"type": "object", "properties": {}},
407
+ handler=self._handle_list_companies,
408
+ )
409
+
410
+ self._register_tool(
411
+ name="create_company",
412
+ description="Create a new company.",
413
+ parameters={
414
+ "type": "object",
415
+ "properties": {
416
+ "name": {"type": "string", "description": "Company name"},
417
+ "short_name": {"type": "string", "description": "Short name/abbreviation"},
418
+ "logo": {"type": "string", "description": "Logo (base64)"},
419
+ },
420
+ "required": ["name"],
421
+ },
422
+ handler=self._handle_create_company,
423
+ )
424
+
425
+ # =====================================================================
426
+ # VULNERABILITY TEMPLATE TOOLS
427
+ # =====================================================================
428
+
429
+ self._register_tool(
430
+ name="list_vulnerabilities",
431
+ description="List all vulnerability templates in the library.",
432
+ parameters={"type": "object", "properties": {}},
433
+ handler=self._handle_list_vulnerabilities,
434
+ )
435
+
436
+ self._register_tool(
437
+ name="get_vulnerabilities_by_locale",
438
+ description="Get vulnerability templates for a specific language.",
439
+ parameters={
440
+ "type": "object",
441
+ "properties": {
442
+ "locale": {
443
+ "type": "string",
444
+ "description": "Language code (e.g., 'en', 'fr')",
445
+ "default": "en",
446
+ }
447
+ },
448
+ },
449
+ handler=self._handle_get_vulnerabilities_by_locale,
450
+ )
451
+
452
+ self._register_tool(
453
+ name="create_vulnerability",
454
+ description="Create a new vulnerability template.",
455
+ parameters={
456
+ "type": "object",
457
+ "properties": {
458
+ "details": {"type": "object", "description": "Vulnerability details by locale"},
459
+ "cvssv3": {"type": "string", "description": "CVSS v3 score"},
460
+ "priority": {"type": "integer", "description": "Priority (1-4)"},
461
+ "remediation_complexity": {
462
+ "type": "integer",
463
+ "description": "Complexity (1-3)",
464
+ },
465
+ "category": {"type": "string", "description": "Category"},
466
+ },
467
+ "required": ["details"],
468
+ },
469
+ handler=self._handle_create_vulnerability,
470
+ )
471
+
472
+ # =====================================================================
473
+ # USER TOOLS
474
+ # =====================================================================
475
+
476
+ self._register_tool(
477
+ name="list_users",
478
+ description="List all users (admin only).",
479
+ parameters={"type": "object", "properties": {}},
480
+ handler=self._handle_list_users,
481
+ )
482
+
483
+ self._register_tool(
484
+ name="get_current_user",
485
+ description="Get current authenticated user's info.",
486
+ parameters={"type": "object", "properties": {}},
487
+ handler=self._handle_get_current_user,
488
+ )
489
+
490
+ # =====================================================================
491
+ # SETTINGS & DATA TOOLS
492
+ # =====================================================================
493
+
494
+ self._register_tool(
495
+ name="list_templates",
496
+ description="List all report templates.",
497
+ parameters={"type": "object", "properties": {}},
498
+ handler=self._handle_list_templates,
499
+ )
500
+
501
+ self._register_tool(
502
+ name="list_languages",
503
+ description="List all configured languages.",
504
+ parameters={"type": "object", "properties": {}},
505
+ handler=self._handle_list_languages,
506
+ )
507
+
508
+ self._register_tool(
509
+ name="list_audit_types",
510
+ description="List all audit types.",
511
+ parameters={"type": "object", "properties": {}},
512
+ handler=self._handle_list_audit_types,
513
+ )
514
+
515
+ self._register_tool(
516
+ name="get_statistics",
517
+ description="Get comprehensive statistics about audits, findings, clients, and more.",
518
+ parameters={"type": "object", "properties": {}},
519
+ handler=self._handle_get_statistics,
520
+ )
521
+
522
+ def _register_tool(
523
+ self,
524
+ name: str,
525
+ description: str,
526
+ parameters: Dict[str, Any],
527
+ handler: Callable,
528
+ required: Optional[List[str]] = None,
529
+ ):
530
+ """Register a tool."""
531
+ self._tools[name] = Tool(
532
+ name=name,
533
+ description=description,
534
+ parameters=parameters,
535
+ handler=handler,
536
+ required=required or parameters.get("required", []),
537
+ )
538
+
539
+ # =========================================================================
540
+ # TOOL HANDLERS
541
+ # =========================================================================
542
+
543
+ def _handle_list_audits(self, finding_title: Optional[str] = None) -> List[Dict]:
544
+ return self.client.list_audits(finding_title)
545
+
546
+ def _handle_get_audit(self, audit_id: str) -> Dict:
547
+ return self.client.get_audit(audit_id)
548
+
549
+ def _handle_create_audit(self, name: str, language: str, audit_type: str) -> Dict:
550
+ return self.client.create_audit(name, language, audit_type)
551
+
552
+ def _handle_update_audit_general(self, audit_id: str, **kwargs) -> Dict:
553
+ return self.client.update_audit_general(audit_id, **kwargs)
554
+
555
+ def _handle_delete_audit(self, audit_id: str) -> Dict:
556
+ self.client.delete_audit(audit_id)
557
+ return {"success": True, "message": f"Audit {audit_id} deleted"}
558
+
559
+ def _handle_generate_report(self, audit_id: str) -> Dict:
560
+ content = self.client.generate_report(audit_id)
561
+ return {"success": True, "size_bytes": len(content)}
562
+
563
+ def _handle_get_findings(self, audit_id: str) -> List[Dict]:
564
+ return self.client.get_findings(audit_id)
565
+
566
+ def _handle_get_finding(self, audit_id: str, finding_id: str) -> Dict:
567
+ return self.client.get_finding(audit_id, finding_id)
568
+
569
+ def _handle_create_finding(self, audit_id: str, **kwargs) -> Dict:
570
+ return self.client.create_finding(audit_id, **kwargs)
571
+
572
+ def _handle_update_finding(self, audit_id: str, finding_id: str, **kwargs) -> Dict:
573
+ return self.client.update_finding(audit_id, finding_id, **kwargs)
574
+
575
+ def _handle_delete_finding(self, audit_id: str, finding_id: str) -> Dict:
576
+ self.client.delete_finding(audit_id, finding_id)
577
+ return {"success": True, "message": f"Finding {finding_id} deleted"}
578
+
579
+ def _handle_search_findings(self, **kwargs) -> List[Dict]:
580
+ return self.client.search_findings(**kwargs)
581
+
582
+ def _handle_get_all_findings_with_context(
583
+ self, include_failed: bool = False, exclude_categories: Optional[List[str]] = None
584
+ ) -> List[Dict]:
585
+ return self.client.get_all_findings_with_context(include_failed, exclude_categories)
586
+
587
+ def _handle_list_clients(self) -> List[Dict]:
588
+ return self.client.list_clients()
589
+
590
+ def _handle_create_client(self, **kwargs) -> Dict:
591
+ return self.client.create_client(**kwargs)
592
+
593
+ def _handle_update_client(self, client_id: str, **kwargs) -> Dict:
594
+ return self.client.update_client(client_id, **kwargs)
595
+
596
+ def _handle_delete_client(self, client_id: str) -> Dict:
597
+ self.client.delete_client(client_id)
598
+ return {"success": True, "message": f"Client {client_id} deleted"}
599
+
600
+ def _handle_list_companies(self) -> List[Dict]:
601
+ return self.client.list_companies()
602
+
603
+ def _handle_create_company(self, **kwargs) -> Dict:
604
+ return self.client.create_company(**kwargs)
605
+
606
+ def _handle_list_vulnerabilities(self) -> List[Dict]:
607
+ return self.client.list_vulnerabilities()
608
+
609
+ def _handle_get_vulnerabilities_by_locale(self, locale: str = "en") -> List[Dict]:
610
+ return self.client.get_vulnerabilities_by_locale(locale)
611
+
612
+ def _handle_create_vulnerability(self, **kwargs) -> Dict:
613
+ return self.client.create_vulnerability(**kwargs)
614
+
615
+ def _handle_list_users(self) -> List[Dict]:
616
+ return self.client.list_users()
617
+
618
+ def _handle_get_current_user(self) -> Dict:
619
+ return self.client.get_current_user()
620
+
621
+ def _handle_list_templates(self) -> List[Dict]:
622
+ return self.client.list_templates()
623
+
624
+ def _handle_list_languages(self) -> List[Dict]:
625
+ return self.client.list_languages()
626
+
627
+ def _handle_list_audit_types(self) -> List[Dict]:
628
+ return self.client.list_audit_types()
629
+
630
+ def _handle_get_statistics(self) -> Dict:
631
+ return self.client.get_statistics()
632
+
633
+ # =========================================================================
634
+ # PUBLIC API METHODS (for testing and direct use)
635
+ # =========================================================================
636
+
637
+ async def handle_initialize(self, params: Dict) -> Dict:
638
+ """
639
+ Handle MCP initialize request (async public method).
640
+
641
+ Args:
642
+ params: Initialize parameters
643
+
644
+ Returns:
645
+ Initialize response with capabilities
646
+ """
647
+ return self._handle_initialize(params)
648
+
649
+ async def handle_list_tools(self) -> List[Dict]:
650
+ """
651
+ Handle list tools request (async public method).
652
+
653
+ Returns:
654
+ List of tool definitions
655
+ """
656
+ from typing import cast
657
+
658
+ result = self._handle_list_tools({})
659
+ return cast(List[Dict], result.get("tools", []))
660
+
661
+ async def handle_call_tool(self, name: str, arguments: Dict) -> Any:
662
+ """
663
+ Handle call tool request (async public method).
664
+
665
+ Args:
666
+ name: Tool name
667
+ arguments: Tool arguments
668
+
669
+ Returns:
670
+ Tool result
671
+
672
+ Raises:
673
+ ValueError: If tool is unknown
674
+ """
675
+ params = {"name": name, "arguments": arguments}
676
+ return self._handle_call_tool(params)
677
+
678
+ def _format_result(self, data: Any) -> str:
679
+ """
680
+ Format result data for output.
681
+
682
+ Args:
683
+ data: Data to format
684
+
685
+ Returns:
686
+ Formatted string
687
+ """
688
+ if isinstance(data, str):
689
+ return data
690
+ elif data is None:
691
+ return "null"
692
+ else:
693
+ return json.dumps(data, indent=2, default=str)
694
+
695
+ # =========================================================================
696
+ # MCP PROTOCOL HANDLING
697
+ # =========================================================================
698
+
699
+ def _handle_initialize(self, params: Dict) -> Dict:
700
+ """Handle MCP initialize request."""
701
+ self._initialized = True
702
+ return {
703
+ "protocolVersion": self.PROTOCOL_VERSION,
704
+ "capabilities": {
705
+ "tools": {"listChanged": True},
706
+ "logging": {},
707
+ },
708
+ "serverInfo": {
709
+ "name": self.SERVER_NAME,
710
+ "version": self.SERVER_VERSION,
711
+ },
712
+ }
713
+
714
+ def _handle_list_tools(self, params: Dict) -> Dict:
715
+ """Handle tools/list request."""
716
+ tools = []
717
+ for tool in self._tools.values():
718
+ tools.append(
719
+ {
720
+ "name": tool.name,
721
+ "description": tool.description,
722
+ "inputSchema": tool.parameters,
723
+ }
724
+ )
725
+ return {"tools": tools}
726
+
727
+ def _handle_call_tool(self, params: Dict) -> Dict:
728
+ """Handle tools/call request."""
729
+ tool_name = params.get("name")
730
+ arguments = params.get("arguments", {})
731
+
732
+ if tool_name not in self._tools:
733
+ raise ValueError(f"Unknown tool: {tool_name}")
734
+
735
+ tool = self._tools[tool_name]
736
+
737
+ try:
738
+ result = tool.handler(**arguments)
739
+ return {
740
+ "content": [{"type": "text", "text": json.dumps(result, indent=2, default=str)}]
741
+ }
742
+ except PwnDocError as e:
743
+ return {"content": [{"type": "text", "text": f"Error: {str(e)}"}], "isError": True}
744
+
745
+ def _handle_message(self, message: Dict) -> Optional[Dict]:
746
+ """Process an incoming MCP message."""
747
+ method = message.get("method")
748
+ params = message.get("params", {})
749
+ msg_id = message.get("id")
750
+
751
+ handlers = {
752
+ "initialize": self._handle_initialize,
753
+ "initialized": lambda p: None, # Notification, no response
754
+ "tools/list": self._handle_list_tools,
755
+ "tools/call": self._handle_call_tool,
756
+ "ping": lambda p: {},
757
+ }
758
+
759
+ if method in handlers:
760
+ try:
761
+ result = handlers[method](params)
762
+ if result is None: # Notification
763
+ return None
764
+ return {"jsonrpc": "2.0", "id": msg_id, "result": result}
765
+ except Exception as e:
766
+ logger.exception(f"Error handling {method}")
767
+ return {
768
+ "jsonrpc": "2.0",
769
+ "id": msg_id,
770
+ "error": {"code": -32603, "message": str(e)},
771
+ }
772
+ else:
773
+ return {
774
+ "jsonrpc": "2.0",
775
+ "id": msg_id,
776
+ "error": {"code": -32601, "message": f"Method not found: {method}"},
777
+ }
778
+
779
+ # =========================================================================
780
+ # TRANSPORT IMPLEMENTATIONS
781
+ # =========================================================================
782
+
783
+ def run_stdio(self):
784
+ """Run server with stdio transport (for Claude Desktop)."""
785
+ logger.info("Starting PwnDoc MCP Server (stdio transport)")
786
+
787
+ while True:
788
+ try:
789
+ line = sys.stdin.readline()
790
+ if not line:
791
+ break
792
+
793
+ message = json.loads(line)
794
+ logger.debug(f"Received: {message.get('method', 'response')}")
795
+
796
+ response = self._handle_message(message)
797
+ if response:
798
+ sys.stdout.write(json.dumps(response) + "\n")
799
+ sys.stdout.flush()
800
+
801
+ except json.JSONDecodeError as e:
802
+ logger.warning(f"Invalid JSON: {e}")
803
+ except KeyboardInterrupt:
804
+ logger.info("Server interrupted")
805
+ break
806
+ except Exception as e:
807
+ logger.exception(f"Error: {e}")
808
+
809
+ async def run_sse(self, host: str = "127.0.0.1", port: int = 8080):
810
+ """Run server with SSE transport."""
811
+ try:
812
+ from aiohttp import web
813
+ except ImportError:
814
+ raise ImportError("aiohttp required for SSE transport: pip install aiohttp")
815
+
816
+ async def handle_sse(request):
817
+ response = web.StreamResponse(
818
+ status=200,
819
+ headers={
820
+ "Content-Type": "text/event-stream",
821
+ "Cache-Control": "no-cache",
822
+ "Connection": "keep-alive",
823
+ },
824
+ )
825
+ await response.prepare(request)
826
+
827
+ # Read messages from POST body
828
+ data = await request.json()
829
+ result = self._handle_message(data)
830
+
831
+ if result:
832
+ await response.write(f"data: {json.dumps(result)}\n\n".encode())
833
+
834
+ return response
835
+
836
+ app = web.Application()
837
+ app.router.add_post("/mcp", handle_sse)
838
+
839
+ runner = web.AppRunner(app)
840
+ await runner.setup()
841
+ site = web.TCPSite(runner, host, port)
842
+ await site.start()
843
+
844
+ logger.info(f"SSE server running at http://{host}:{port}/mcp")
845
+
846
+ # Keep running
847
+ while True:
848
+ await asyncio.sleep(3600)
849
+
850
+ def run(self, transport: Optional[str] = None):
851
+ """
852
+ Run the MCP server.
853
+
854
+ Args:
855
+ transport: Transport type (stdio, sse, websocket). Defaults to config value.
856
+ """
857
+ transport = transport or self.config.mcp_transport
858
+
859
+ if transport == "stdio":
860
+ self.run_stdio()
861
+ elif transport == "sse":
862
+ asyncio.run(self.run_sse(self.config.mcp_host, self.config.mcp_port))
863
+ else:
864
+ raise ValueError(f"Unsupported transport: {transport}")
865
+
866
+
867
+ # Module-level constant for tool definitions (for compatibility)
868
+ TOOL_DEFINITIONS: Optional[List[Dict]] = None
869
+
870
+
871
+ def _get_tool_definitions() -> List[Dict]:
872
+ """
873
+ Get tool definitions from server instance.
874
+
875
+ Returns:
876
+ List of tool definitions
877
+ """
878
+ # Create a temporary server to extract tool definitions
879
+ config = Config(url="http://temp", token="temp")
880
+ server = PwnDocMCPServer(config)
881
+ tools = []
882
+ for tool in server._tools.values():
883
+ tools.append(
884
+ {
885
+ "name": tool.name,
886
+ "description": tool.description,
887
+ "inputSchema": tool.parameters,
888
+ }
889
+ )
890
+ return tools
891
+
892
+
893
+ # Initialize TOOL_DEFINITIONS on module load
894
+ TOOL_DEFINITIONS = _get_tool_definitions()
895
+
896
+
897
+ def create_server(config: Optional[Config] = None, **kwargs) -> PwnDocMCPServer:
898
+ """
899
+ Create and configure a PwnDoc MCP server.
900
+
901
+ Args:
902
+ config: Configuration object (if provided, kwargs are ignored)
903
+ **kwargs: Configuration parameters (url, token, etc.)
904
+
905
+ Returns:
906
+ Configured PwnDocMCPServer instance
907
+
908
+ Raises:
909
+ ValueError: If configuration is invalid
910
+
911
+ Example:
912
+ >>> server = create_server(url="https://pwndoc.com", token="...")
913
+ >>> server = create_server(config)
914
+ """
915
+ if config is None:
916
+ config = Config(**kwargs)
917
+
918
+ # Validate configuration
919
+ errors = config.validate()
920
+ if errors:
921
+ raise ValueError(f"Invalid configuration: {'; '.join(errors)}")
922
+
923
+ return PwnDocMCPServer(config)
924
+
925
+
926
+ def main():
927
+ """Main entry point."""
928
+ import argparse
929
+
930
+ parser = argparse.ArgumentParser(description="PwnDoc MCP Server")
931
+ parser.add_argument("--transport", choices=["stdio", "sse"], default="stdio")
932
+ parser.add_argument("--host", default="127.0.0.1")
933
+ parser.add_argument("--port", type=int, default=8080)
934
+ parser.add_argument("--log-level", default="INFO")
935
+ args = parser.parse_args()
936
+
937
+ logging.basicConfig(level=getattr(logging, args.log_level))
938
+
939
+ config = load_config(
940
+ mcp_transport=args.transport,
941
+ mcp_host=args.host,
942
+ mcp_port=args.port,
943
+ )
944
+
945
+ server = PwnDocMCPServer(config)
946
+ server.run()
947
+
948
+
949
+ if __name__ == "__main__":
950
+ main()