budikomarudin commited on
Commit
ae1f568
·
verified ·
1 Parent(s): 9b3a65c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1124 -0
app.py ADDED
@@ -0,0 +1,1124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import asyncio
3
+ import json
4
+ import logging
5
+ from typing import List, Dict, Any, Optional, Tuple
6
+ from dataclasses import dataclass, field
7
+ import anthropic
8
+ import aiohttp
9
+ from mcp import ClientSession
10
+ from mcp.client.sse import sse_client
11
+
12
+ # Setup logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ @dataclass
17
+ class MCPSSEServerConfig:
18
+ name: str
19
+ url: str
20
+ headers: Dict[str, str] = field(default_factory=dict)
21
+ timeout: int = 50
22
+ sse_read_timeout: int = 50
23
+
24
+ class FastAgentMCPClient:
25
+ def __init__(self, config: MCPSSEServerConfig):
26
+ self.config = config
27
+ self.session = None
28
+ self.tools = []
29
+ self.connected = False
30
+ self.client_session = None
31
+
32
+ async def connect(self):
33
+ """Establish connection using fast-agent-mcp"""
34
+ try:
35
+ logger.info(f"Connecting to {self.config.name} using fast-agent-mcp")
36
+
37
+ # Use MCP SSE client from fast-agent-mcp
38
+ self.client_session = await sse_client(
39
+ self.config.url,
40
+ headers=self.config.headers,
41
+ timeout=self.config.timeout
42
+ )
43
+
44
+ # Initialize the session
45
+ await self.client_session.initialize()
46
+
47
+ # Get available tools
48
+ tools_result = await self.client_session.list_tools()
49
+ self.tools = tools_result.tools if hasattr(tools_result, 'tools') else []
50
+
51
+ self.connected = True
52
+ logger.info(f"Successfully connected to {self.config.name} with {len(self.tools)} tools")
53
+
54
+ except Exception as e:
55
+ logger.error(f"Failed to connect to {self.config.name} using fast-agent-mcp: {e}")
56
+ # Fallback to manual SSE implementation
57
+ await self._fallback_connect()
58
+
59
+ async def _fallback_connect(self):
60
+ """Fallback connection method using manual SSE handling"""
61
+ try:
62
+ logger.info(f"Attempting fallback connection for {self.config.name}")
63
+
64
+ # Create HTTP session for manual SSE handling
65
+ connector = aiohttp.TCPConnector(limit=100, limit_per_host=30)
66
+ self.session = aiohttp.ClientSession(
67
+ timeout=aiohttp.ClientTimeout(total=self.config.timeout),
68
+ connector=connector,
69
+ connector_owner=True
70
+ )
71
+
72
+ # Test SSE endpoint
73
+ await self._test_sse_endpoint()
74
+
75
+ # Try to get server capabilities
76
+ await self._probe_server_capabilities()
77
+
78
+ self.connected = True
79
+ logger.info(f"Fallback connection successful for {self.config.name}")
80
+
81
+ except Exception as e:
82
+ logger.warning(f"Fallback connection failed for {self.config.name}: {e}")
83
+ # Still mark as connected for graceful degradation
84
+ self.connected = True
85
+ self.tools = []
86
+ logger.info(f"Graceful connection established for {self.config.name} (no tools)")
87
+
88
+ async def _test_sse_endpoint(self):
89
+ """Test SSE endpoint connectivity"""
90
+ try:
91
+ async with self.session.get(
92
+ self.config.url,
93
+ headers={
94
+ "Accept": "text/event-stream",
95
+ "Cache-Control": "no-cache",
96
+ "Connection": "keep-alive",
97
+ "User-Agent": "Klaide-FastAgent-MCP/1.0.0",
98
+ **self.config.headers
99
+ },
100
+ timeout=aiohttp.ClientTimeout(total=10),
101
+ allow_redirects=True,
102
+ max_redirects=10
103
+ ) as response:
104
+
105
+ # Log redirect chain if any
106
+ if response.history:
107
+ redirect_chain = " -> ".join([str(h.url) for h in response.history])
108
+ logger.info(f"SSE endpoint redirected: {redirect_chain} -> {response.url}")
109
+
110
+ if response.status in [200, 204]:
111
+ logger.info(f"SSE endpoint accessible for {self.config.name}")
112
+ elif response.status == 405:
113
+ # Handle 405 gracefully - might be expecting different method
114
+ logger.warning(f"SSE endpoint returned 405 for {self.config.name} - continuing gracefully")
115
+ else:
116
+ logger.warning(f"SSE endpoint returned {response.status} for {self.config.name}")
117
+
118
+ except Exception as e:
119
+ logger.warning(f"SSE endpoint test failed for {self.config.name}: {e}")
120
+
121
+ async def _probe_server_capabilities(self):
122
+ """Probe server for MCP capabilities"""
123
+ probe_methods = [
124
+ self._probe_jsonrpc_post,
125
+ self._probe_jsonrpc_get,
126
+ self._probe_rest_api,
127
+ self._probe_websocket
128
+ ]
129
+
130
+ for method in probe_methods:
131
+ try:
132
+ tools = await method()
133
+ if tools:
134
+ self.tools = tools
135
+ logger.info(f"Successfully probed {self.config.name} and found {len(tools)} tools")
136
+ return
137
+ except Exception as e:
138
+ logger.debug(f"Probe method failed for {self.config.name}: {e}")
139
+ continue
140
+
141
+ # If all probes fail, set empty tools
142
+ self.tools = []
143
+ logger.info(f"All probe methods failed for {self.config.name}, setting empty tools")
144
+
145
+ async def _probe_jsonrpc_post(self):
146
+ """Probe using JSON-RPC POST method"""
147
+ # Try to initialize with JSON-RPC
148
+ init_message = {
149
+ "jsonrpc": "2.0",
150
+ "id": 1,
151
+ "method": "initialize",
152
+ "params": {
153
+ "protocolVersion": "2024-11-05",
154
+ "capabilities": {"tools": {}},
155
+ "clientInfo": {"name": "klaide-fast-agent", "version": "1.0.0"}
156
+ }
157
+ }
158
+
159
+ async with self.session.post(
160
+ self.config.url,
161
+ json=init_message,
162
+ headers={
163
+ "Content-Type": "application/json",
164
+ "User-Agent": "Klaide-FastAgent-MCP/1.0.0",
165
+ **self.config.headers
166
+ },
167
+ timeout=aiohttp.ClientTimeout(total=15),
168
+ allow_redirects=True,
169
+ max_redirects=10
170
+ ) as response:
171
+
172
+ if response.status != 200:
173
+ raise Exception(f"Init failed with status {response.status}")
174
+
175
+ result = await response.json()
176
+ if "error" in result:
177
+ raise Exception(f"Init error: {result['error']}")
178
+
179
+ # Get tools list
180
+ tools_message = {
181
+ "jsonrpc": "2.0",
182
+ "id": 2,
183
+ "method": "tools/list",
184
+ "params": {}
185
+ }
186
+
187
+ async with self.session.post(
188
+ self.config.url,
189
+ json=tools_message,
190
+ headers={
191
+ "Content-Type": "application/json",
192
+ "User-Agent": "Klaide-FastAgent-MCP/1.0.0",
193
+ **self.config.headers
194
+ },
195
+ timeout=aiohttp.ClientTimeout(total=15),
196
+ allow_redirects=True,
197
+ max_redirects=10
198
+ ) as response:
199
+
200
+ if response.status != 200:
201
+ raise Exception(f"Tools list failed with status {response.status}")
202
+
203
+ tools_response = await response.json()
204
+ if "result" in tools_response and "tools" in tools_response["result"]:
205
+ return tools_response["result"]["tools"]
206
+
207
+ return []
208
+
209
+ async def _probe_jsonrpc_get(self):
210
+ """Probe using JSON-RPC GET method with query parameters"""
211
+ try:
212
+ # Try GET with tools list
213
+ async with self.session.get(
214
+ f"{self.config.url}?method=tools/list",
215
+ headers={
216
+ "Accept": "application/json",
217
+ "User-Agent": "Klaide-FastAgent-MCP/1.0.0",
218
+ **self.config.headers
219
+ },
220
+ timeout=aiohttp.ClientTimeout(total=10),
221
+ allow_redirects=True,
222
+ max_redirects=10
223
+ ) as response:
224
+
225
+ if response.status == 200:
226
+ result = await response.json()
227
+ if "tools" in result:
228
+ return result["tools"]
229
+ elif "result" in result and "tools" in result["result"]:
230
+ return result["result"]["tools"]
231
+
232
+ except Exception:
233
+ pass
234
+
235
+ return []
236
+
237
+ async def _probe_rest_api(self):
238
+ """Probe using REST API endpoints"""
239
+ rest_endpoints = [
240
+ f"{self.config.url.rstrip('/sse')}/tools",
241
+ f"{self.config.url.rstrip('/sse')}/api/tools",
242
+ f"{self.config.url.rstrip('/sse')}/mcp/tools",
243
+ f"{self.config.url}/tools"
244
+ ]
245
+
246
+ for endpoint in rest_endpoints:
247
+ try:
248
+ async with self.session.get(
249
+ endpoint,
250
+ headers={
251
+ "Accept": "application/json",
252
+ "User-Agent": "Klaide-FastAgent-MCP/1.0.0",
253
+ **self.config.headers
254
+ },
255
+ timeout=aiohttp.ClientTimeout(total=10),
256
+ allow_redirects=True,
257
+ max_redirects=10
258
+ ) as response:
259
+
260
+ if response.status == 200:
261
+ result = await response.json()
262
+ if isinstance(result, list):
263
+ return result
264
+ elif "tools" in result:
265
+ return result["tools"]
266
+
267
+ except Exception:
268
+ continue
269
+
270
+ return []
271
+
272
+ async def _probe_websocket(self):
273
+ """Probe using WebSocket connection"""
274
+ try:
275
+ # Convert HTTP URL to WebSocket URL
276
+ ws_url = self.config.url.replace('https://', 'wss://').replace('http://', 'ws://')
277
+
278
+ # This would require websockets library
279
+ # For now, just return empty list
280
+ logger.debug(f"WebSocket probe not implemented for {ws_url}")
281
+ return []
282
+
283
+ except Exception:
284
+ return []
285
+
286
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
287
+ """Call a tool using the best available method"""
288
+ if self.client_session:
289
+ # Use fast-agent-mcp client session
290
+ try:
291
+ result = await self.client_session.call_tool(tool_name, arguments)
292
+ return result.content if hasattr(result, 'content') else result
293
+ except Exception as e:
294
+ logger.error(f"Fast-agent tool call failed: {e}")
295
+ # Fall through to manual method
296
+
297
+ # Fallback to manual tool calling
298
+ if self.session:
299
+ message = {
300
+ "jsonrpc": "2.0",
301
+ "id": int(asyncio.get_event_loop().time() * 1000),
302
+ "method": "tools/call",
303
+ "params": {
304
+ "name": tool_name,
305
+ "arguments": arguments
306
+ }
307
+ }
308
+
309
+ try:
310
+ async with self.session.post(
311
+ self.config.url,
312
+ json=message,
313
+ headers={
314
+ "Content-Type": "application/json",
315
+ "User-Agent": "Klaide-FastAgent-MCP/1.0.0",
316
+ **self.config.headers
317
+ },
318
+ timeout=aiohttp.ClientTimeout(total=self.config.sse_read_timeout),
319
+ allow_redirects=True,
320
+ max_redirects=10
321
+ ) as response:
322
+
323
+ if response.status == 200:
324
+ result = await response.json()
325
+ if "result" in result:
326
+ return result["result"].get("content", [])
327
+
328
+ raise Exception(f"Tool call failed with status {response.status}")
329
+
330
+ except Exception as e:
331
+ logger.error(f"Manual tool call failed: {e}")
332
+ return [{"type": "text", "text": f"Error: {str(e)}"}]
333
+
334
+ return [{"type": "text", "text": "Error: No connection available"}]
335
+
336
+ async def close(self):
337
+ """Close all connections"""
338
+ if self.client_session:
339
+ try:
340
+ await self.client_session.close()
341
+ except Exception:
342
+ pass
343
+
344
+ if self.session:
345
+ try:
346
+ await self.session.close()
347
+ except Exception:
348
+ pass
349
+
350
+ self.connected = False
351
+
352
+ class MCPChatbot:
353
+ def __init__(self, anthropic_api_key: str, mcp_servers: Dict[str, MCPSSEServerConfig]):
354
+ self.anthropic_client = anthropic.Anthropic(api_key=anthropic_api_key)
355
+ self.mcp_servers = mcp_servers
356
+ self.clients = {}
357
+ self.available_tools = {}
358
+
359
+ async def initialize_mcp_servers(self):
360
+ """Initialize connections to all MCP SSE servers using fast-agent-mcp"""
361
+ for server_name, server_config in self.mcp_servers.items():
362
+ try:
363
+ logger.info(f"Connecting to MCP SSE server: {server_name}")
364
+
365
+ client = FastAgentMCPClient(server_config)
366
+ await client.connect()
367
+
368
+ self.clients[server_name] = client
369
+ self.available_tools[server_name] = client.tools
370
+
371
+ if client.connected:
372
+ logger.info(f"Successfully connected to {server_name} with {len(client.tools)} tools")
373
+ else:
374
+ logger.warning(f"Partial connection to {server_name}")
375
+
376
+ except Exception as e:
377
+ logger.error(f"Failed to connect to {server_name}: {e}")
378
+ continue
379
+
380
+ def format_tools_for_claude(self) -> List[Dict]:
381
+ """Format tools from MCP for Claude API with enhanced context"""
382
+ claude_tools = []
383
+
384
+ for server_name, tools in self.available_tools.items():
385
+ for tool in tools:
386
+ # Enhanced tool description with server context
387
+ server_context = ""
388
+ if server_name == "burp_mcp":
389
+ server_context = " (Burp Suite - Web Security Testing)"
390
+ elif server_name == "viper_mcp":
391
+ server_context = " (Metasploit - Penetration Testing)"
392
+
393
+ tool_description = tool.get('description', f"Tool from {server_name}")
394
+ enhanced_description = f"{tool_description}{server_context}"
395
+
396
+ claude_tool = {
397
+ "name": f"{server_name}_{tool.get('name', 'unknown')}",
398
+ "description": enhanced_description,
399
+ "input_schema": tool.get('inputSchema', {
400
+ "type": "object",
401
+ "properties": {},
402
+ "required": []
403
+ }),
404
+ "server_context": {
405
+ "server_name": server_name,
406
+ "server_url": self.clients[server_name].config.url if server_name in self.clients else "",
407
+ "capabilities": self._get_server_capabilities(server_name)
408
+ }
409
+ }
410
+ claude_tools.append(claude_tool)
411
+
412
+ return claude_tools
413
+
414
+ def _get_server_capabilities(self, server_name: str) -> List[str]:
415
+ """Get capabilities for a specific server"""
416
+ capabilities_map = {
417
+ "burp_mcp": [
418
+ "Web application security testing",
419
+ "Vulnerability scanning",
420
+ "HTTP request/response analysis",
421
+ "Spider/crawling functionality",
422
+ "Intruder attacks",
423
+ "Repeater functionality"
424
+ ],
425
+ "viper_mcp": [
426
+ "Penetration testing",
427
+ "Exploit development",
428
+ "Payload generation",
429
+ "Network reconnaissance",
430
+ "Post-exploitation",
431
+ "Metasploit module execution"
432
+ ]
433
+ }
434
+ return capabilities_map.get(server_name, [])
435
+
436
+ async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
437
+ """Execute tool through appropriate MCP server"""
438
+ parts = tool_name.split('_', 1)
439
+ if len(parts) < 2:
440
+ raise ValueError(f"Invalid tool name format: {tool_name}")
441
+
442
+ server_name = parts[0]
443
+ actual_tool_name = parts[1]
444
+
445
+ if server_name not in self.clients:
446
+ raise ValueError(f"Server {server_name} not available")
447
+
448
+ client = self.clients[server_name]
449
+
450
+ if not client.connected:
451
+ raise ValueError(f"Server {server_name} not connected")
452
+
453
+ try:
454
+ result = await client.call_tool(actual_tool_name, arguments)
455
+ return result
456
+ except Exception as e:
457
+ logger.error(f"Error executing tool {tool_name}: {e}")
458
+ return [{"type": "text", "text": f"Error: {str(e)}"}]
459
+
460
+ async def chat(self, message: str, history: List[List[str]]) -> tuple:
461
+ """Main chat function using messages API with tools context"""
462
+ try:
463
+ # Get available tools and their context
464
+ tools = self.format_tools_for_claude()
465
+ tools_context = await self._get_tools_context()
466
+
467
+ # Bangun system_content dan messages
468
+ system_content, messages = await self._build_messages_with_context(message, history, tools_context)
469
+
470
+ # Panggil Claude API
471
+ response = self.anthropic_client.messages.create(
472
+ model="claude-3-5-sonnet-20241022",
473
+ max_tokens=4000,
474
+ messages=messages,
475
+ tools=tools,
476
+ temperature=0.1,
477
+ system=system_content
478
+ )
479
+
480
+ # Process response
481
+ assistant_message = ""
482
+
483
+ for content in response.content:
484
+ if content.type == "text":
485
+ assistant_message += content.text
486
+ elif content.type == "tool_use":
487
+ # Execute tool
488
+ try:
489
+ tool_result = await self.execute_tool(
490
+ content.name,
491
+ content.input
492
+ )
493
+
494
+ # Format tool result
495
+ assistant_message += f"\n\n🔧 **Tool Executed**: {content.name}\n"
496
+
497
+ if isinstance(tool_result, list):
498
+ for item in tool_result:
499
+ if isinstance(item, dict) and item.get("type") == "text":
500
+ assistant_message += f"📊 **Result**: {item.get('text', '')}\n"
501
+ else:
502
+ assistant_message += f"📊 **Result**: {json.dumps(item, indent=2, ensure_ascii=False)}\n"
503
+ else:
504
+ assistant_message += f"📊 **Result**: {json.dumps(tool_result, indent=2, ensure_ascii=False)}\n"
505
+
506
+ except Exception as e:
507
+ assistant_message += f"\n\n❌ **Tool execution failed**: {content.name}\n"
508
+ assistant_message += f"🚫 **Error**: {str(e)}\n"
509
+
510
+ # Update history
511
+ history.append([message, assistant_message])
512
+
513
+ return history, ""
514
+
515
+ except Exception as e:
516
+ error_msg = f"❌ **Error**: {str(e)}"
517
+ history.append([message, error_msg])
518
+ return history, ""
519
+
520
+ async def _build_messages_with_context(self, message: str, history: List[List[str]], tools_context: str) -> Tuple[str, List[Dict]]:
521
+ """Build messages array with tools context integration"""
522
+ # Pisahkan system_content
523
+ system_content = f"""You are Klaide, a Kali Linux AI Desktop assistant that controls cybersecurity tools through MCP servers.
524
+ You help users perform penetration testing, vulnerability assessment, and security analysis.
525
+
526
+ {tools_context}
527
+
528
+ Instructions:
529
+ 1. Analyze the user's request in the context of available MCP tools
530
+ 2. Use the appropriate tools for cybersecurity tasks
531
+ 3. Provide helpful guidance and explanations
532
+ 4. Be specific about which Kali Linux tools or techniques to use
533
+ 5. Always prioritize security and ethical hacking practices
534
+ 6. When using tools, explain what you're doing and why
535
+
536
+ Available tool format: Use the tools provided in the tools list for executing commands."""
537
+ messages = []
538
+ # Tambahkan history dan pesan user seperti biasa, TANPA pesan role "system"
539
+ if history:
540
+ for user_msg, assistant_msg in history[-5:]:
541
+ messages.append({"role": "user", "content": user_msg})
542
+ if assistant_msg:
543
+ messages.append({"role": "assistant", "content": assistant_msg})
544
+
545
+ # Add current message
546
+ messages.append({"role": "user", "content": message})
547
+
548
+ return system_content, messages
549
+
550
+ async def _get_tools_context(self) -> str:
551
+ """Get context information from all connected MCP servers"""
552
+ context_parts = []
553
+
554
+ context_parts.append("=== MCP SERVERS CONTEXT ===")
555
+
556
+ for server_name, client in self.clients.items():
557
+ if not client.connected:
558
+ continue
559
+
560
+ context_parts.append(f"\n[{server_name.upper()} SERVER]")
561
+ context_parts.append(f"URL: {client.config.url}")
562
+ context_parts.append(f"Status: Connected")
563
+
564
+ # Get tools information
565
+ tools = self.available_tools.get(server_name, [])
566
+ context_parts.append(f"Available Tools: {len(tools)}")
567
+
568
+ if tools:
569
+ context_parts.append("Tools List:")
570
+ for tool in tools:
571
+ tool_name = tool.get('name', 'unknown')
572
+ tool_desc = tool.get('description', 'No description')
573
+ context_parts.append(f" - {tool_name}: {tool_desc}")
574
+
575
+ # Add input schema info
576
+ schema = tool.get('inputSchema', {})
577
+ if schema.get('properties'):
578
+ props = list(schema['properties'].keys())
579
+ context_parts.append(f" Parameters: {', '.join(props)}")
580
+
581
+ # Get server capabilities
582
+ if server_name == "burp_mcp":
583
+ context_parts.append("Capabilities:")
584
+ context_parts.append(" - Web application security testing")
585
+ context_parts.append(" - Vulnerability scanning")
586
+ context_parts.append(" - HTTP request/response analysis")
587
+ context_parts.append(" - Burp Suite integration")
588
+
589
+ elif server_name == "viper_mcp":
590
+ context_parts.append("Capabilities:")
591
+ context_parts.append(" - Penetration testing")
592
+ context_parts.append(" - Exploit development")
593
+ context_parts.append(" - Metasploit Framework integration")
594
+ context_parts.append(" - Payload generation")
595
+
596
+ # Add system status
597
+ connected_count = sum(1 for client in self.clients.values() if client.connected)
598
+ total_tools = sum(len(tools) for tools in self.available_tools.values())
599
+
600
+ context_parts.append(f"\n[SYSTEM STATUS]")
601
+ context_parts.append(f"Connected Servers: {connected_count}/{len(self.clients)}")
602
+ context_parts.append(f"Total Available Tools: {total_tools}")
603
+ context_parts.append(f"MCP Client: Fast-Agent-MCP")
604
+
605
+ return "\n".join(context_parts)
606
+
607
+ async def _build_completion_prompt(self, message: str, history: List[List[str]], tools_context: str) -> str:
608
+ """Build completion prompt with conversation history and tools context (deprecated - now using messages)"""
609
+ # This method is kept for reference but not used anymore
610
+ # We now use _build_messages_with_context() instead
611
+ prompt_parts = []
612
+
613
+ # System prompt
614
+ prompt_parts.append("You are Klaide, a Kali Linux AI Desktop assistant that controls cybersecurity tools through MCP servers.")
615
+ prompt_parts.append("You help users perform penetration testing, vulnerability assessment, and security analysis.")
616
+ prompt_parts.append("")
617
+
618
+ # Tools context
619
+ prompt_parts.append(tools_context)
620
+ prompt_parts.append("")
621
+
622
+ # Conversation history
623
+ if history:
624
+ prompt_parts.append("=== CONVERSATION HISTORY ===")
625
+ for user_msg, assistant_msg in history[-5:]: # Last 5 exchanges
626
+ prompt_parts.append(f"Human: {user_msg}")
627
+ prompt_parts.append(f"Assistant: {assistant_msg}")
628
+ prompt_parts.append("")
629
+
630
+ # Current message
631
+ prompt_parts.append("=== CURRENT REQUEST ===")
632
+ prompt_parts.append(f"Human: {message}")
633
+ prompt_parts.append("")
634
+
635
+ # Instructions
636
+ prompt_parts.append("=== INSTRUCTIONS ===")
637
+ prompt_parts.append("1. Analyze the user's request in the context of available MCP tools")
638
+ prompt_parts.append("2. Use the appropriate tools for cybersecurity tasks")
639
+ prompt_parts.append("3. Provide helpful cybersecurity guidance and explanations")
640
+ prompt_parts.append("4. Be specific about which Kali Linux tools or techniques to use")
641
+ prompt_parts.append("5. Always prioritize security and ethical hacking practices")
642
+ prompt_parts.append("")
643
+ prompt_parts.append("Assistant: ")
644
+
645
+ return "\n".join(prompt_parts)
646
+
647
+ async def _extract_tool_calls_from_completion(self, completion_text: str) -> List[Dict[str, Any]]:
648
+ """Extract tool calls from completion text (deprecated - now using native tool calling)"""
649
+ # This method is kept for reference but not used anymore
650
+ # We now use Claude's native tool calling in messages API
651
+ import re
652
+
653
+ tool_calls = []
654
+
655
+ # Pattern to match TOOL_CALL[tool_name](arguments)
656
+ pattern = r'TOOL_CALL\[([^\]]+)\]\(([^)]*)\)'
657
+ matches = re.findall(pattern, completion_text)
658
+
659
+ for match in matches:
660
+ tool_name = match[0].strip()
661
+ args_str = match[1].strip()
662
+
663
+ # Parse arguments (simple JSON-like parsing)
664
+ try:
665
+ if args_str:
666
+ # Handle simple key=value pairs
667
+ arguments = {}
668
+ if '=' in args_str:
669
+ pairs = args_str.split(',')
670
+ for pair in pairs:
671
+ if '=' in pair:
672
+ key, value = pair.split('=', 1)
673
+ key = key.strip().strip('"\'')
674
+ value = value.strip().strip('"\'')
675
+ arguments[key] = value
676
+ else:
677
+ # Try to parse as JSON
678
+ arguments = json.loads(args_str) if args_str else {}
679
+ else:
680
+ arguments = {}
681
+
682
+ tool_calls.append({
683
+ 'name': tool_name,
684
+ 'arguments': arguments
685
+ })
686
+
687
+ except Exception as e:
688
+ logger.warning(f"Failed to parse tool arguments: {args_str}, error: {e}")
689
+ # Still add the tool call with empty arguments
690
+ tool_calls.append({
691
+ 'name': tool_name,
692
+ 'arguments': {}
693
+ })
694
+
695
+ return tool_calls
696
+
697
+ async def get_server_status(self) -> str:
698
+ """Get comprehensive status of all MCP servers with enhanced context"""
699
+ status = "📡 **Kali Linux MCP Servers Status (Fast-Agent-MCP + Enhanced Context):**\n\n"
700
+
701
+ if not self.clients:
702
+ status += "❌ **No servers configured**\n"
703
+ status += "Please configure at least one MCP server URL in Settings.\n"
704
+ return status
705
+
706
+ for server_name, client in self.clients.items():
707
+ status += f"## 🖥️ **{server_name.upper()}**\n"
708
+ status += f"**URL**: `{client.config.url}`\n"
709
+
710
+ try:
711
+ if not client.connected:
712
+ status += "**Status**: ❌ **DISCONNECTED**\n"
713
+ status += "**Connection Method**: Fast-Agent-MCP + Fallback\n"
714
+ status += "**Tools**: ⚠️ No tools available\n"
715
+ status += "**Context**: Not available for enhanced context\n\n"
716
+ continue
717
+
718
+ tools = self.available_tools.get(server_name, [])
719
+
720
+ if not tools:
721
+ status += "**Status**: ⚠️ **CONNECTED BUT NO TOOLS**\n"
722
+ status += "**Connection Method**: Fast-Agent-MCP (Graceful mode)\n"
723
+ status += "**Tools**: 📭 Empty tools list\n"
724
+ status += "**Context**: Limited context for AI\n"
725
+ else:
726
+ status += f"**Status**: ✅ **CONNECTED & OPERATIONAL**\n"
727
+
728
+ # Determine connection method
729
+ if client.client_session:
730
+ status += "**Connection Method**: 🚀 Fast-Agent-MCP (Native)\n"
731
+ else:
732
+ status += "**Connection Method**: 🔄 Fast-Agent-MCP (Fallback)\n"
733
+
734
+ status += f"**Tools Available**: 🛠️ **{len(tools)} tools**\n"
735
+ status += f"**AI Context**: ✅ **Full context available**\n"
736
+
737
+ # Display tools with enhanced context integration
738
+ status += "**Tools List** (Available with enhanced context):\n"
739
+ for i, tool in enumerate(tools, 1):
740
+ tool_name = tool.get('name', 'Unknown')
741
+ tool_desc = tool.get('description', 'No description available')
742
+
743
+ if len(tool_desc) > 60:
744
+ tool_desc = tool_desc[:57] + "..."
745
+
746
+ status += f" `{i:2d}.` **{tool_name}** - {tool_desc}\n"
747
+
748
+ # Add server-specific capabilities for enhanced context
749
+ capabilities = self._get_server_capabilities(server_name)
750
+ status += "**Enhanced Context Capabilities**:\n"
751
+ for cap in capabilities:
752
+ status += f" • {cap}\n"
753
+
754
+ # Connection health info
755
+ status += f"**Timeout**: {client.config.timeout}s\n"
756
+ status += f"**SSE Timeout**: {client.config.sse_read_timeout}s\n"
757
+
758
+ except Exception as e:
759
+ status += "**Status**: ❌ **ERROR**\n"
760
+ status += f"**Error Details**: {str(e)}\n"
761
+ status += "**Tools**: ⚠️ Unable to retrieve tools\n"
762
+ status += "**Context**: Error in enhanced context\n"
763
+
764
+ status += "\n" + "─" * 50 + "\n\n"
765
+
766
+ # Add enhanced context summary
767
+ connected_count = sum(1 for client in self.clients.values() if client.connected)
768
+ total_tools = sum(len(tools) for tools in self.available_tools.values())
769
+
770
+ status += "## 📊 **Enhanced Context Summary**\n"
771
+ status += f"**Connected Servers**: {connected_count}/{len(self.clients)}\n"
772
+ status += f"**Total Available Tools**: {total_tools}\n"
773
+ status += f"**MCP Client**: Fast-Agent-MCP with enhanced context\n"
774
+ status += f"**AI Model**: Claude-3.5-Sonnet (Messages API with Context)\n"
775
+
776
+ # Context information
777
+ if total_tools > 0:
778
+ status += f"**Context Integration**: ✅ **Full MCP context available**\n"
779
+ status += f"**AI Features**:\n"
780
+ status += f" • Rich server context in system messages\n"
781
+ status += f" • Native tool calling with Claude Messages API\n"
782
+ status += f" • Enhanced cybersecurity guidance\n"
783
+ status += f" • Conversation history integration\n"
784
+ status += f" • Server capability awareness\n"
785
+ else:
786
+ status += f"**Context Integration**: ⚠️ **Limited context**\n"
787
+
788
+ if connected_count == 0:
789
+ status += "**Overall Status**: ❌ **NO SERVERS OPERATIONAL**\n"
790
+ status += "**Action Required**: Check server URLs and network connectivity\n"
791
+ elif connected_count == len(self.clients):
792
+ status += "**Overall Status**: ✅ **ALL SYSTEMS OPERATIONAL**\n"
793
+ status += "**Ready**: Klaide enhanced context mode ready for cybersecurity tasks\n"
794
+ else:
795
+ status += "**Overall Status**: ⚠️ **PARTIAL CONNECTIVITY**\n"
796
+ status += "**Action**: Some servers need attention\n"
797
+
798
+ return status
799
+
800
+ async def close_all_connections(self):
801
+ """Close all MCP connections"""
802
+ for client in self.clients.values():
803
+ await client.close()
804
+
805
+ # Global chatbot instance
806
+ chatbot = None
807
+
808
+ def create_mcp_servers_config(burp_url: str, viper_url: str) -> Dict[str, MCPSSEServerConfig]:
809
+ """Create MCP servers configuration from URLs"""
810
+ servers = {}
811
+
812
+ if burp_url.strip():
813
+ servers["burp_mcp"] = MCPSSEServerConfig(
814
+ name="burp_mcp",
815
+ url=burp_url.strip(),
816
+ headers={},
817
+ timeout=50,
818
+ sse_read_timeout=50
819
+ )
820
+
821
+ if viper_url.strip():
822
+ servers["viper_mcp"] = MCPSSEServerConfig(
823
+ name="viper_mcp",
824
+ url=viper_url.strip(),
825
+ headers={},
826
+ timeout=50,
827
+ sse_read_timeout=50
828
+ )
829
+
830
+ return servers
831
+
832
+ async def initialize_chatbot(api_key: str, burp_url: str, viper_url: str):
833
+ """Initialize Klaide with API key and server URLs using fast-agent-mcp"""
834
+ global chatbot
835
+
836
+ if not api_key:
837
+ return "❌ Please enter Anthropic API Key"
838
+
839
+ if not burp_url.strip() and not viper_url.strip():
840
+ return "❌ Please enter at least one MCP server URL"
841
+
842
+ try:
843
+ mcp_servers = create_mcp_servers_config(burp_url, viper_url)
844
+ chatbot = MCPChatbot(api_key, mcp_servers)
845
+ await chatbot.initialize_mcp_servers()
846
+
847
+ connected_servers = [name for name, client in chatbot.clients.items() if client.connected]
848
+ total_tools = sum(len(tools) for tools in chatbot.available_tools.values())
849
+
850
+ if connected_servers:
851
+ status_msg = f"✅ Klaide successfully initialized with Fast-Agent-MCP!\n"
852
+ status_msg += f"🔗 Connected servers: {', '.join(connected_servers)}\n"
853
+ status_msg += f"🛠️ Total tools available: {total_tools}\n"
854
+ status_msg += f"🚀 MCP Client: Fast-Agent-MCP with fallback support\n"
855
+
856
+ # Add connection method info
857
+ for server_name, client in chatbot.clients.items():
858
+ if client.connected:
859
+ method = "Native" if client.client_session else "Fallback"
860
+ status_msg += f"📡 {server_name}: {method} connection\n"
861
+
862
+ return status_msg
863
+ else:
864
+ return "⚠️ Klaide initialized but no servers connected. Please check your URLs."
865
+
866
+ except Exception as e:
867
+ return f"❌ Initialization error: {str(e)}"
868
+
869
+ async def chat_wrapper(message, history):
870
+ """Wrapper for chat function"""
871
+ if not chatbot:
872
+ history.append([message, "❌ Klaide not initialized. Please enter API Key first."])
873
+ return history, ""
874
+
875
+ return await chatbot.chat(message, history)
876
+
877
+ async def get_status():
878
+ """Get server status"""
879
+ if not chatbot:
880
+ return "❌ Klaide not initialized"
881
+
882
+ return await chatbot.get_server_status()
883
+
884
+ async def cleanup():
885
+ """Cleanup function"""
886
+ global chatbot
887
+ if chatbot:
888
+ await chatbot.close_all_connections()
889
+
890
+ # Gradio Interface
891
+ def create_interface():
892
+ with gr.Blocks(title="Klaide (Kali Linux AI Desktop)", theme=gr.themes.Soft()) as demo:
893
+ gr.Markdown("# 🐉 Klaide (**Kali Linux AI Desktop**)")
894
+ gr.Markdown("Controlling Kali Linux Desktop with AI using MCP Server.")
895
+
896
+ with gr.Tab("💬 Console"):
897
+ with gr.Row():
898
+ with gr.Column(scale=3):
899
+ chatbot_ui = gr.Chatbot(
900
+ label="Klaide Console",
901
+ height=500,
902
+ show_copy_button=True,
903
+ avatar_images=("assets/user.png", "assets/csalab.png")
904
+ )
905
+
906
+ with gr.Row():
907
+ msg = gr.Textbox(
908
+ placeholder="Ask Klaide to control your Kali Linux tools...",
909
+ label="Command Prompt",
910
+ scale=4
911
+ )
912
+ send_btn = gr.Button("Send", scale=1, variant="primary")
913
+
914
+ with gr.Column(scale=1):
915
+ gr.Markdown("### 🔧 Controls")
916
+ clear_btn = gr.Button("Clear Chat", variant="secondary")
917
+
918
+ gr.Markdown("### 📊 Server Status")
919
+ status_btn = gr.Button("Refresh Status")
920
+ status_display = gr.Markdown("Status will be displayed here...")
921
+
922
+ with gr.Tab("⚙️ Settings"):
923
+ gr.Markdown("## Setup Configuration")
924
+
925
+ with gr.Row():
926
+ with gr.Column():
927
+ api_key_input = gr.Textbox(
928
+ label="Anthropic API Key",
929
+ type="password",
930
+ placeholder="sk-ant-...",
931
+ info="Required: Your Anthropic Claude API key"
932
+ )
933
+
934
+ burp_url_input = gr.Textbox(
935
+ label="Burp MCP Server URL",
936
+ placeholder="https://burp.csalab.app/sse",
937
+ value="https://burp.csalab.app/sse",
938
+ info="Optional: URL for Burp Suite MCP server"
939
+ )
940
+
941
+ viper_url_input = gr.Textbox(
942
+ label="Viper MCP Server URL",
943
+ placeholder="https://msf.csalab.app/your-id/sse",
944
+ value="https://msf.csalab.app/02b77a05348211f0/sse",
945
+ info="Optional: URL for Metasploit MCP server"
946
+ )
947
+
948
+ with gr.Row():
949
+ init_btn = gr.Button("Initialize Klaide", variant="primary", scale=2)
950
+ test_urls_btn = gr.Button("Test URLs", variant="secondary", scale=1)
951
+
952
+ init_status = gr.Textbox(
953
+ label="Klaide Status",
954
+ interactive=False,
955
+ lines=4
956
+ )
957
+
958
+ gr.Markdown("## Klaide (Kali Linux AI Desktop)")
959
+
960
+ with gr.Accordion("Advanced Settings", open=False):
961
+ gr.Markdown("### Timeout Configuration")
962
+ timeout_slider = gr.Slider(
963
+ minimum=10,
964
+ maximum=120,
965
+ value=50,
966
+ step=5,
967
+ label="Connection Timeout (seconds)",
968
+ info="Timeout for server connections and requests"
969
+ )
970
+
971
+ gr.Markdown("### Custom Headers")
972
+ custom_headers = gr.Textbox(
973
+ label="Custom Headers (JSON format)",
974
+ placeholder='{"Authorization": "Bearer token", "X-API-Key": "key"}',
975
+ info="Optional: Custom headers for server requests"
976
+ )
977
+
978
+ # Event handlers
979
+ def chat_fn(message, history):
980
+ return asyncio.run(chat_wrapper(message, history))
981
+
982
+ def init_fn(api_key, burp_url, viper_url):
983
+ return asyncio.run(initialize_chatbot(api_key, burp_url, viper_url))
984
+
985
+ def status_fn():
986
+ return asyncio.run(get_status())
987
+
988
+ async def test_urls_async(burp_url, viper_url):
989
+ """Test server URLs connectivity with fast-agent-mcp methods"""
990
+ results = []
991
+
992
+ # Test Burp URL
993
+ if burp_url.strip():
994
+ burp_result = await test_single_url_fast_agent("Burp", burp_url.strip())
995
+ results.append(burp_result)
996
+ else:
997
+ results.append("⏭️ Burp Server: URL not provided")
998
+
999
+ # Test Viper URL
1000
+ if viper_url.strip():
1001
+ viper_result = await test_single_url_fast_agent("Viper", viper_url.strip())
1002
+ results.append(viper_result)
1003
+ else:
1004
+ results.append("⏭️ Viper Server: URL not provided")
1005
+
1006
+ return "\n".join(results)
1007
+
1008
+ async def test_single_url_fast_agent(server_name, url):
1009
+ """Test a single URL using fast-agent-mcp approach"""
1010
+ test_results = []
1011
+
1012
+ # Method 1: Try fast-agent-mcp native connection
1013
+ try:
1014
+ config = MCPSSEServerConfig(name=f"test_{server_name.lower()}", url=url)
1015
+ test_client = FastAgentMCPClient(config)
1016
+
1017
+ # Quick connection test (timeout faster for testing)
1018
+ config.timeout = 10
1019
+ await test_client.connect()
1020
+
1021
+ if test_client.connected:
1022
+ tool_count = len(test_client.tools)
1023
+ if test_client.client_session:
1024
+ test_results.append(f"✅ {server_name} Server: Fast-Agent-MCP native ({tool_count} tools)")
1025
+ else:
1026
+ test_results.append(f"✅ {server_name} Server: Fast-Agent-MCP fallback ({tool_count} tools)")
1027
+
1028
+ await test_client.close()
1029
+ return "\n".join(test_results)
1030
+ else:
1031
+ test_results.append(f"⚠️ {server_name} Server: Fast-Agent-MCP failed")
1032
+
1033
+ await test_client.close()
1034
+
1035
+ except Exception as e:
1036
+ test_results.append(f"❌ {server_name} Server: Fast-Agent-MCP error - {str(e)[:50]}...")
1037
+
1038
+ # Method 2: Manual SSE test
1039
+ try:
1040
+ connector = aiohttp.TCPConnector(limit=10, limit_per_host=5)
1041
+ async with aiohttp.ClientSession(
1042
+ timeout=aiohttp.ClientTimeout(total=10),
1043
+ connector=connector
1044
+ ) as session:
1045
+
1046
+ # Test SSE endpoint
1047
+ async with session.get(
1048
+ url,
1049
+ headers={
1050
+ "Accept": "text/event-stream",
1051
+ "User-Agent": "Klaide-FastAgent-Test/1.0.0"
1052
+ },
1053
+ allow_redirects=True,
1054
+ max_redirects=10
1055
+ ) as response:
1056
+
1057
+ if response.status in [200, 204]:
1058
+ test_results.append(f"✅ {server_name} Server: SSE endpoint accessible")
1059
+ elif response.status == 405:
1060
+ test_results.append(f"⚠️ {server_name} Server: SSE returns 405 (will use graceful handling)")
1061
+ else:
1062
+ test_results.append(f"⚠️ {server_name} Server: SSE returns HTTP {response.status}")
1063
+
1064
+ except Exception as e:
1065
+ test_results.append(f"❌ {server_name} Server: SSE test failed - {str(e)[:50]}...")
1066
+
1067
+ return "\n".join(test_results)
1068
+
1069
+ def test_urls_fn(burp_url, viper_url):
1070
+ return asyncio.run(test_urls_async(burp_url, viper_url))
1071
+
1072
+ # Bind events
1073
+ send_btn.click(
1074
+ chat_fn,
1075
+ inputs=[msg, chatbot_ui],
1076
+ outputs=[chatbot_ui, msg]
1077
+ )
1078
+
1079
+ msg.submit(
1080
+ chat_fn,
1081
+ inputs=[msg, chatbot_ui],
1082
+ outputs=[chatbot_ui, msg]
1083
+ )
1084
+
1085
+ clear_btn.click(
1086
+ lambda: ([], ""),
1087
+ outputs=[chatbot_ui, msg]
1088
+ )
1089
+
1090
+ init_btn.click(
1091
+ init_fn,
1092
+ inputs=[api_key_input, burp_url_input, viper_url_input],
1093
+ outputs=[init_status]
1094
+ )
1095
+
1096
+ test_urls_btn.click(
1097
+ test_urls_fn,
1098
+ inputs=[burp_url_input, viper_url_input],
1099
+ outputs=[init_status]
1100
+ )
1101
+
1102
+ status_btn.click(
1103
+ status_fn,
1104
+ outputs=[status_display]
1105
+ )
1106
+
1107
+ # Cleanup on app close
1108
+ demo.load(None, None, None)
1109
+
1110
+ return demo
1111
+
1112
+ if __name__ == "__main__":
1113
+ try:
1114
+ # Create and launch interface
1115
+ demo = create_interface()
1116
+ demo.launch(
1117
+ server_name="0.0.0.0",
1118
+ server_port=7860,
1119
+ share=True,
1120
+ debug=True
1121
+ )
1122
+ finally:
1123
+ # Cleanup
1124
+ asyncio.run(cleanup())