budikomarudin commited on
Commit
e39b1bb
·
verified ·
1 Parent(s): 1e8b88b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +144 -738
app.py CHANGED
@@ -2,17 +2,26 @@ 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
@@ -30,349 +39,72 @@ class FastAgentMCPClient:
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
@@ -380,39 +112,28 @@ class MCPChatbot:
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",
@@ -434,22 +155,16 @@ class MCPChatbot:
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
@@ -457,358 +172,130 @@ class MCPChatbot:
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",
@@ -817,82 +304,68 @@ def create_mcp_servers_config(burp_url: str, viper_url: str) -> Dict[str, MCPSSE
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):
@@ -900,9 +373,9 @@ def create_interface():
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...",
@@ -910,18 +383,8 @@ def create_interface():
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(
@@ -930,33 +393,26 @@ def create_interface():
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(
@@ -967,151 +423,102 @@ def create_interface():
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",
@@ -1120,5 +527,4 @@ if __name__ == "__main__":
1120
  debug=True
1121
  )
1122
  finally:
1123
- # Cleanup
1124
  asyncio.run(cleanup())
 
2
  import asyncio
3
  import json
4
  import logging
5
+ from typing import List, Dict, Any, Tuple
6
  from dataclasses import dataclass, field
7
+ import requests
8
+ from smolagents.mcp_client import MCPClient
 
 
9
 
10
  # Setup logging
11
  logging.basicConfig(level=logging.INFO)
12
  logger = logging.getLogger(__name__)
13
 
14
+ def get_tools(url):
15
+ tools = []
16
+ with MCPClient({"url": url}) as tool_objs:
17
+ for t in tool_objs:
18
+ tools.append(t)
19
+ # Logging tool names and count
20
+ logger.info(f"[get_tools] Found {len(tools)} tools:")
21
+ for t in tools:
22
+ logger.info(f" - {t.name}")
23
+ return tools
24
+
25
  @dataclass
26
  class MCPSSEServerConfig:
27
  name: str
 
39
  self.client_session = None
40
 
41
  async def connect(self):
42
+ """Establish connection using smolagents[mcp] MCPClient for tool listing"""
43
  try:
44
+ logger.info(f"Connecting to {self.config.name} using smolagents[mcp] MCPClient for tool listing")
45
+ # Use MCPClient from smolagents to list tools
46
+ loop = asyncio.get_event_loop()
47
+ self.tools = await loop.run_in_executor(None, lambda: get_tools(self.config.url))
 
 
 
 
 
 
 
 
 
 
 
 
48
  self.connected = True
49
+ logger.info(f"Successfully connected to {self.config.name} with {len(self.tools)} tools (smolagents)")
 
50
  except Exception as e:
51
+ logger.error(f"Failed to connect to {self.config.name} using smolagents[mcp]: {e}")
52
  # Fallback to manual SSE implementation
53
  await self._fallback_connect()
54
 
55
  async def _fallback_connect(self):
56
+ """Fallback connection method using smolagents[mcp] MCPClient for tool listing"""
57
  try:
58
+ logger.info(f"Attempting fallback connection for {self.config.name} using smolagents[mcp] MCPClient")
59
+ loop = asyncio.get_event_loop()
60
+ self.tools = await loop.run_in_executor(None, lambda: get_tools(self.config.url))
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  self.connected = True
62
+ logger.info(f"Fallback connection successful for {self.config.name} (smolagents)")
 
63
  except Exception as e:
64
  logger.warning(f"Fallback connection failed for {self.config.name}: {e}")
 
65
  self.connected = True
66
  self.tools = []
67
  logger.info(f"Graceful connection established for {self.config.name} (no tools)")
68
 
69
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
70
+ """Call a tool using smolagents[mcp] MCPClient only"""
71
  try:
72
+ loop = asyncio.get_event_loop()
73
+ def call_tool_sync(url, tool_name, arguments):
74
+ with MCPClient({"url": url}) as tool_objs:
75
+ tool_obj = next((t for t in tool_objs if t.name == tool_name), None)
76
+ if not tool_obj:
77
+ return [{"type": "text", "text": f"Error: Tool '{tool_name}' not found"}]
78
+ return tool_obj.call(**arguments)
79
+ result = await loop.run_in_executor(None, call_tool_sync, self.config.url, tool_name, arguments)
80
+ return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  except Exception as e:
82
+ logger.error(f"smolagents tool call failed: {e}")
83
+ return [{"type": "text", "text": f"Error: {str(e)}"}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  async def close(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  self.connected = False
87
 
88
  class MCPChatbot:
89
  def __init__(self, anthropic_api_key: str, mcp_servers: Dict[str, MCPSSEServerConfig]):
90
+ self.api_key = anthropic_api_key
91
  self.mcp_servers = mcp_servers
92
  self.clients = {}
93
  self.available_tools = {}
94
+
95
  async def initialize_mcp_servers(self):
96
  """Initialize connections to all MCP SSE servers using fast-agent-mcp"""
97
  for server_name, server_config in self.mcp_servers.items():
98
  try:
99
  logger.info(f"Connecting to MCP SSE server: {server_name}")
 
100
  client = FastAgentMCPClient(server_config)
101
  await client.connect()
 
102
  self.clients[server_name] = client
103
  self.available_tools[server_name] = client.tools
 
104
  if client.connected:
105
  logger.info(f"Successfully connected to {server_name} with {len(client.tools)} tools")
106
  else:
107
  logger.warning(f"Partial connection to {server_name}")
 
108
  except Exception as e:
109
  logger.error(f"Failed to connect to {server_name}: {e}")
110
  continue
 
112
  def format_tools_for_claude(self) -> List[Dict]:
113
  """Format tools from MCP for Claude API with enhanced context"""
114
  claude_tools = []
 
115
  for server_name, tools in self.available_tools.items():
116
  for tool in tools:
 
117
  server_context = ""
118
  if server_name == "burp_mcp":
119
  server_context = " (Burp Suite - Web Security Testing)"
120
  elif server_name == "viper_mcp":
121
  server_context = " (Metasploit - Penetration Testing)"
122
+ tool_description = getattr(tool, 'description', f"Tool from {server_name}")
 
123
  enhanced_description = f"{tool_description}{server_context}"
 
124
  claude_tool = {
125
+ "name": f"{server_name}_{getattr(tool, 'name', 'unknown')}",
126
  "description": enhanced_description,
127
+ "input_schema": getattr(tool, 'input_schema', {
128
  "type": "object",
129
  "properties": {},
130
  "required": []
131
+ })
 
 
 
 
 
132
  }
133
  claude_tools.append(claude_tool)
 
134
  return claude_tools
135
 
136
  def _get_server_capabilities(self, server_name: str) -> List[str]:
 
137
  capabilities_map = {
138
  "burp_mcp": [
139
  "Web application security testing",
 
155
  return capabilities_map.get(server_name, [])
156
 
157
  async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
158
+ parts = tool_name.rsplit('_', 1)
 
159
  if len(parts) < 2:
160
  raise ValueError(f"Invalid tool name format: {tool_name}")
 
161
  server_name = parts[0]
162
  actual_tool_name = parts[1]
 
163
  if server_name not in self.clients:
164
  raise ValueError(f"Server {server_name} not available")
 
165
  client = self.clients[server_name]
 
166
  if not client.connected:
167
  raise ValueError(f"Server {server_name} not connected")
 
168
  try:
169
  result = await client.call_tool(actual_tool_name, arguments)
170
  return result
 
172
  logger.error(f"Error executing tool {tool_name}: {e}")
173
  return [{"type": "text", "text": f"Error: {str(e)}"}]
174
 
175
+ async def chat(self, message: str, history: list) -> tuple:
 
176
  try:
177
+ # Build mcp_servers payload for Anthropic API
178
+ mcp_servers = []
179
+ for server_name, server_config in self.mcp_servers.items():
180
+ mcp_server = {
181
+ "type": "url",
182
+ "url": server_config.url,
183
+ "name": server_name
184
+ }
185
+ if "Authorization" in server_config.headers:
186
+ mcp_server["authorization_token"] = server_config.headers["Authorization"]
187
+ mcp_servers.append(mcp_server)
188
+
189
+ def filter_message_fields(msg):
190
+ return {"role": msg.get("role"), "content": msg.get("content")}
191
+
192
+ messages = []
193
+ if history:
194
+ for msg in history[-10:]:
195
+ if isinstance(msg, dict) and "role" in msg and "content" in msg:
196
+ messages.append(filter_message_fields(msg))
197
+ messages.append({"role": "user", "content": message})
198
+
199
+ payload = {
200
+ "model": "claude-3-5-sonnet-20241022",
201
+ "max_tokens": 1000,
202
+ "messages": messages,
203
+ "mcp_servers": mcp_servers,
204
+ "temperature": 0.1,
205
+ "system": "You are Klaide, a Kali Linux AI Desktop assistant."
206
+ }
207
+ logger.info(f"[Anthropic API] Payload: {json.dumps(payload, indent=2)}")
208
+ headers = {
209
+ "Content-Type": "application/json",
210
+ "X-API-Key": self.api_key,
211
+ "anthropic-version": "2023-06-01",
212
+ "anthropic-beta": "mcp-client-2025-04-04"
213
+ }
214
+ url = "https://api.anthropic.com/v1/messages"
215
+ resp = requests.post(url, headers=headers, data=json.dumps(payload))
216
+ if resp.status_code != 200:
217
+ raise Exception(f"Anthropic API error: {resp.status_code} - {resp.text}")
218
+ response = resp.json()
219
  assistant_message = ""
220
+ for content in response.get("content", []):
221
+ if content.get("type") == "text":
222
+ assistant_message += content.get("text", "")
223
+ elif content.get("type") == "mcp_tool_result":
224
+ for item in content.get("content", []):
225
+ if isinstance(item, dict) and item.get("type") == "text":
226
+ assistant_message += f"\n📊 **Tool Result**: {item.get('text', '')}\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  else:
228
+ assistant_message += f"\n📊 **Tool Result**: {json.dumps(item, indent=2, ensure_ascii=False)}\n"
229
+ history = history + [
230
+ {"role": "user", "content": message},
231
+ {"role": "assistant", "content": assistant_message}
232
+ ]
 
 
 
 
233
  return history, ""
 
234
  except Exception as e:
235
  error_msg = f"❌ **Error**: {str(e)}"
236
+ history = history + [
237
+ {"role": "user", "content": message},
238
+ {"role": "assistant", "content": error_msg}
239
+ ]
240
  return history, ""
241
 
242
  async def _build_messages_with_context(self, message: str, history: List[List[str]], tools_context: str) -> Tuple[str, List[Dict]]:
243
+ system_content = f"""You are Klaide, a Kali Linux AI Desktop assistant that controls cybersecurity tools through MCP servers.\nYou help users perform penetration testing, vulnerability assessment, and security analysis.\n\n{tools_context}\n\nInstructions:\n1. Analyze the user's request in the context of available MCP tools\n2. Use the appropriate tools for cybersecurity tasks\n3. Provide helpful guidance and explanations\n4. Be specific about which Kali Linux tools or techniques to use\n5. Always prioritize security and ethical hacking practices\n6. When using tools, explain what you're doing and why\n\nAvailable tool format: Use the tools provided in the tools list for executing commands."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  messages = []
 
245
  if history:
246
  for user_msg, assistant_msg in history[-5:]:
247
  messages.append({"role": "user", "content": user_msg})
248
  if assistant_msg:
249
  messages.append({"role": "assistant", "content": assistant_msg})
 
 
250
  messages.append({"role": "user", "content": message})
 
251
  return system_content, messages
252
 
253
  async def _get_tools_context(self) -> str:
 
254
  context_parts = []
 
255
  context_parts.append("=== MCP SERVERS CONTEXT ===")
 
256
  for server_name, client in self.clients.items():
257
  if not client.connected:
258
  continue
 
259
  context_parts.append(f"\n[{server_name.upper()} SERVER]")
260
  context_parts.append(f"URL: {client.config.url}")
261
  context_parts.append(f"Status: Connected")
 
 
262
  tools = self.available_tools.get(server_name, [])
263
  context_parts.append(f"Available Tools: {len(tools)}")
 
264
  if tools:
265
  context_parts.append("Tools List:")
266
  for tool in tools:
267
+ tool_name = getattr(tool, 'name', 'unknown')
268
+ tool_desc = getattr(tool, 'description', 'No description')
269
  context_parts.append(f" - {tool_name}: {tool_desc}")
270
+ schema = getattr(tool, 'input_schema', {})
271
+ if schema and isinstance(schema, dict) and schema.get('properties'):
 
 
272
  props = list(schema['properties'].keys())
273
  context_parts.append(f" Parameters: {', '.join(props)}")
 
 
274
  if server_name == "burp_mcp":
275
  context_parts.append("Capabilities:")
276
  context_parts.append(" - Web application security testing")
277
  context_parts.append(" - Vulnerability scanning")
278
  context_parts.append(" - HTTP request/response analysis")
279
  context_parts.append(" - Burp Suite integration")
 
280
  elif server_name == "viper_mcp":
281
  context_parts.append("Capabilities:")
282
  context_parts.append(" - Penetration testing")
283
  context_parts.append(" - Exploit development")
284
  context_parts.append(" - Metasploit Framework integration")
285
  context_parts.append(" - Payload generation")
 
 
286
  connected_count = sum(1 for client in self.clients.values() if client.connected)
287
  total_tools = sum(len(tools) for tools in self.available_tools.values())
 
288
  context_parts.append(f"\n[SYSTEM STATUS]")
289
  context_parts.append(f"Connected Servers: {connected_count}/{len(self.clients)}")
290
  context_parts.append(f"Total Available Tools: {total_tools}")
291
  context_parts.append(f"MCP Client: Fast-Agent-MCP")
 
292
  return "\n".join(context_parts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
 
294
+ async def get_server_status(self) -> str:
295
+ return await self._get_tools_context()
296
 
297
  def create_mcp_servers_config(burp_url: str, viper_url: str) -> Dict[str, MCPSSEServerConfig]:
 
298
  servers = {}
 
299
  if burp_url.strip():
300
  servers["burp_mcp"] = MCPSSEServerConfig(
301
  name="burp_mcp",
 
304
  timeout=50,
305
  sse_read_timeout=50
306
  )
 
307
  if viper_url.strip():
308
  servers["viper_mcp"] = MCPSSEServerConfig(
309
+ name="viper_mcp",
310
  url=viper_url.strip(),
311
  headers={},
312
  timeout=50,
313
  sse_read_timeout=50
314
  )
 
315
  return servers
316
 
317
+ chatbot = None
318
+
319
  async def initialize_chatbot(api_key: str, burp_url: str, viper_url: str):
 
320
  global chatbot
 
321
  if not api_key:
322
  return "❌ Please enter Anthropic API Key"
 
323
  if not burp_url.strip() and not viper_url.strip():
324
  return "❌ Please enter at least one MCP server URL"
 
325
  try:
326
  mcp_servers = create_mcp_servers_config(burp_url, viper_url)
327
  chatbot = MCPChatbot(api_key, mcp_servers)
328
  await chatbot.initialize_mcp_servers()
 
329
  connected_servers = [name for name, client in chatbot.clients.items() if client.connected]
330
  total_tools = sum(len(tools) for tools in chatbot.available_tools.values())
 
331
  if connected_servers:
332
  status_msg = f"✅ Klaide successfully initialized with Fast-Agent-MCP!\n"
333
  status_msg += f"🔗 Connected servers: {', '.join(connected_servers)}\n"
334
  status_msg += f"🛠️ Total tools available: {total_tools}\n"
335
  status_msg += f"🚀 MCP Client: Fast-Agent-MCP with fallback support\n"
 
 
336
  for server_name, client in chatbot.clients.items():
337
  if client.connected:
338
  method = "Native" if client.client_session else "Fallback"
339
  status_msg += f"📡 {server_name}: {method} connection\n"
 
340
  return status_msg
341
  else:
342
  return "⚠️ Klaide initialized but no servers connected. Please check your URLs."
 
343
  except Exception as e:
344
  return f"❌ Initialization error: {str(e)}"
345
 
346
  async def chat_wrapper(message, history):
 
347
  if not chatbot:
348
+ history = history + [
349
+ {"role": "user", "content": message},
350
+ {"role": "assistant", "content": "❌ Klaide not initialized. Please enter API Key first."}
351
+ ]
352
  return history, ""
 
353
  return await chatbot.chat(message, history)
354
 
355
  async def get_status():
 
356
  if not chatbot:
357
  return "❌ Klaide not initialized"
 
358
  return await chatbot.get_server_status()
359
 
360
  async def cleanup():
 
361
  global chatbot
362
  if chatbot:
363
  await chatbot.close_all_connections()
364
 
 
365
  def create_interface():
366
  with gr.Blocks(title="Klaide (Kali Linux AI Desktop)", theme=gr.themes.Soft()) as demo:
367
  gr.Markdown("# 🐉 Klaide (**Kali Linux AI Desktop**)")
368
  gr.Markdown("Controlling Kali Linux Desktop with AI using MCP Server.")
 
369
  with gr.Tab("💬 Console"):
370
  with gr.Row():
371
  with gr.Column(scale=3):
 
373
  label="Klaide Console",
374
  height=500,
375
  show_copy_button=True,
376
+ avatar_images=("assets/user.png", "assets/csalab.png"),
377
+ type="messages"
378
  )
 
379
  with gr.Row():
380
  msg = gr.Textbox(
381
  placeholder="Ask Klaide to control your Kali Linux tools...",
 
383
  scale=4
384
  )
385
  send_btn = gr.Button("Send", scale=1, variant="primary")
 
 
 
 
 
 
 
 
 
386
  with gr.Tab("⚙️ Settings"):
387
  gr.Markdown("## Setup Configuration")
 
388
  with gr.Row():
389
  with gr.Column():
390
  api_key_input = gr.Textbox(
 
393
  placeholder="sk-ant-...",
394
  info="Required: Your Anthropic Claude API key"
395
  )
 
396
  burp_url_input = gr.Textbox(
397
  label="Burp MCP Server URL",
398
  placeholder="https://burp.csalab.app/sse",
399
  value="https://burp.csalab.app/sse",
400
  info="Optional: URL for Burp Suite MCP server"
401
  )
 
402
  viper_url_input = gr.Textbox(
403
+ label="Viper MCP Server URL",
404
  placeholder="https://msf.csalab.app/your-id/sse",
405
+ value="https://msf.csalab.app/3cbf712b45cc11f0/sse",
406
  info="Optional: URL for Metasploit MCP server"
407
  )
 
408
  with gr.Row():
409
  init_btn = gr.Button("Initialize Klaide", variant="primary", scale=2)
410
  test_urls_btn = gr.Button("Test URLs", variant="secondary", scale=1)
 
411
  init_status = gr.Textbox(
412
+ label="Klaide Status",
413
  interactive=False,
414
  lines=4
415
  )
 
 
 
416
  with gr.Accordion("Advanced Settings", open=False):
417
  gr.Markdown("### Timeout Configuration")
418
  timeout_slider = gr.Slider(
 
423
  label="Connection Timeout (seconds)",
424
  info="Timeout for server connections and requests"
425
  )
 
426
  gr.Markdown("### Custom Headers")
427
  custom_headers = gr.Textbox(
428
  label="Custom Headers (JSON format)",
429
  placeholder='{"Authorization": "Bearer token", "X-API-Key": "key"}',
430
  info="Optional: Custom headers for server requests"
431
  )
432
+ with gr.Tab("📊 Server Status"):
433
+ status_btn = gr.Button("Refresh Status")
434
+ status_display = gr.Textbox(
435
+ label="Status",
436
+ lines=4
437
+ )
438
  def chat_fn(message, history):
439
+ try:
440
+ result = asyncio.run(chat_wrapper(message, history))
441
+ if isinstance(result, tuple) and len(result) == 2:
442
+ return result
443
+ # fallback: return empty chat if error
444
+ return history, ""
445
+ except Exception as e:
446
+ # fallback: return error in chat
447
+ if isinstance(history, list):
448
+ history = history + [
449
+ {"role": "user", "content": message},
450
+ {"role": "assistant", "content": f"❌ Error: {str(e)}"}
451
+ ]
452
+ return history, ""
453
  def init_fn(api_key, burp_url, viper_url):
454
  return asyncio.run(initialize_chatbot(api_key, burp_url, viper_url))
 
455
  def status_fn():
456
  return asyncio.run(get_status())
 
457
  async def test_urls_async(burp_url, viper_url):
 
458
  results = []
 
 
459
  if burp_url.strip():
460
  burp_result = await test_single_url_fast_agent("Burp", burp_url.strip())
461
  results.append(burp_result)
462
  else:
463
  results.append("⏭️ Burp Server: URL not provided")
 
 
464
  if viper_url.strip():
465
  viper_result = await test_single_url_fast_agent("Viper", viper_url.strip())
466
  results.append(viper_result)
467
  else:
468
  results.append("⏭️ Viper Server: URL not provided")
 
469
  return "\n".join(results)
 
470
  async def test_single_url_fast_agent(server_name, url):
 
471
  test_results = []
 
 
472
  try:
473
  config = MCPSSEServerConfig(name=f"test_{server_name.lower()}", url=url)
474
  test_client = FastAgentMCPClient(config)
 
 
475
  config.timeout = 10
476
  await test_client.connect()
 
477
  if test_client.connected:
478
  tool_count = len(test_client.tools)
479
  if test_client.client_session:
480
  test_results.append(f"✅ {server_name} Server: Fast-Agent-MCP native ({tool_count} tools)")
481
  else:
482
  test_results.append(f"✅ {server_name} Server: Fast-Agent-MCP fallback ({tool_count} tools)")
 
483
  await test_client.close()
484
  return "\n".join(test_results)
485
  else:
486
  test_results.append(f"⚠️ {server_name} Server: Fast-Agent-MCP failed")
 
487
  await test_client.close()
 
488
  except Exception as e:
489
  test_results.append(f"❌ {server_name} Server: Fast-Agent-MCP error - {str(e)[:50]}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  return "\n".join(test_results)
 
491
  def test_urls_fn(burp_url, viper_url):
492
  return asyncio.run(test_urls_async(burp_url, viper_url))
 
 
493
  send_btn.click(
494
  chat_fn,
495
  inputs=[msg, chatbot_ui],
496
  outputs=[chatbot_ui, msg]
497
  )
 
498
  msg.submit(
499
  chat_fn,
500
  inputs=[msg, chatbot_ui],
501
  outputs=[chatbot_ui, msg]
502
  )
 
 
 
 
 
 
503
  init_btn.click(
504
  init_fn,
505
  inputs=[api_key_input, burp_url_input, viper_url_input],
506
  outputs=[init_status]
507
  )
 
508
  test_urls_btn.click(
509
  test_urls_fn,
510
  inputs=[burp_url_input, viper_url_input],
511
  outputs=[init_status]
512
  )
 
513
  status_btn.click(
514
  status_fn,
515
  outputs=[status_display]
516
  )
 
 
517
  demo.load(None, None, None)
 
518
  return demo
519
 
520
  if __name__ == "__main__":
521
  try:
 
522
  demo = create_interface()
523
  demo.launch(
524
  server_name="0.0.0.0",
 
527
  debug=True
528
  )
529
  finally:
 
530
  asyncio.run(cleanup())