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.
- pwndoc_mcp_server/__init__.py +57 -0
- pwndoc_mcp_server/cli.py +441 -0
- pwndoc_mcp_server/client.py +870 -0
- pwndoc_mcp_server/config.py +411 -0
- pwndoc_mcp_server/logging_config.py +329 -0
- pwndoc_mcp_server/server.py +950 -0
- pwndoc_mcp_server/version.py +26 -0
- pwndoc_mcp_server-1.0.2.dist-info/METADATA +110 -0
- pwndoc_mcp_server-1.0.2.dist-info/RECORD +11 -0
- pwndoc_mcp_server-1.0.2.dist-info/WHEEL +4 -0
- pwndoc_mcp_server-1.0.2.dist-info/entry_points.txt +2 -0
|
@@ -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()
|