Files
NeuroSploit/backend/api/v1/mcp.py
CyberSecurityUP e0935793c5 NeuroSploit v3.2 - Autonomous AI Penetration Testing Platform
116 modules | 100 vuln types | 18 API routes | 18 frontend pages

Major features:
- VulnEngine: 100 vuln types, 526+ payloads, 12 testers, anti-hallucination prompts
- Autonomous Agent: 3-stream auto pentest, multi-session (5 concurrent), pause/resume/stop
- CLI Agent: Claude Code / Gemini CLI / Codex CLI inside Kali containers
- Validation Pipeline: negative controls, proof of execution, confidence scoring, judge
- AI Reasoning: ReACT engine, token budget, endpoint classifier, CVE hunter, deep recon
- Multi-Agent: 5 specialists + orchestrator + researcher AI + vuln type agents
- RAG System: BM25/TF-IDF/ChromaDB vectorstore, few-shot, reasoning templates
- Smart Router: 20 providers (8 CLI OAuth + 12 API), tier failover, token refresh
- Kali Sandbox: container-per-scan, 56 tools, VPN support, on-demand install
- Full IA Testing: methodology-driven comprehensive pentest sessions
- Notifications: Discord, Telegram, WhatsApp/Twilio multi-channel alerts
- Frontend: React/TypeScript with 18 pages, real-time WebSocket updates
2026-02-22 17:59:28 -03:00

321 lines
10 KiB
Python

"""
NeuroSploit v3 - MCP Server Management API
CRUD for Model Context Protocol server connections.
Persists to config/config.json mcp_servers section.
"""
import json
import asyncio
from pathlib import Path
from typing import Optional, List, Dict
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
router = APIRouter()
CONFIG_PATH = Path(__file__).parent.parent.parent.parent / "config" / "config.json"
BUILTIN_SERVER = "neurosploit_tools"
# --- Schemas ---
class MCPServerCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="Unique server identifier")
transport: str = Field("stdio", description="Transport type: stdio or sse")
command: Optional[str] = Field(None, description="Command for stdio transport")
args: Optional[List[str]] = Field(None, description="Args for stdio transport")
url: Optional[str] = Field(None, description="URL for sse transport")
env: Optional[Dict[str, str]] = Field(None, description="Environment variables")
description: str = Field("", description="Server description")
enabled: bool = Field(True, description="Whether server is enabled")
class MCPServerUpdate(BaseModel):
transport: Optional[str] = None
command: Optional[str] = None
args: Optional[List[str]] = None
url: Optional[str] = None
env: Optional[Dict[str, str]] = None
description: Optional[str] = None
enabled: Optional[bool] = None
class MCPServerResponse(BaseModel):
name: str
transport: str
command: Optional[str] = None
args: Optional[List[str]] = None
url: Optional[str] = None
env: Optional[Dict[str, str]] = None
description: str = ""
enabled: bool = True
is_builtin: bool = False
class MCPToolResponse(BaseModel):
name: str
description: str
server_name: str
# --- Config helpers ---
def _read_config() -> dict:
if not CONFIG_PATH.exists():
return {}
with open(CONFIG_PATH) as f:
return json.load(f)
def _write_config(config: dict):
with open(CONFIG_PATH, "w") as f:
json.dump(config, f, indent=4)
def _get_mcp_servers(config: dict) -> dict:
return config.get("mcp_servers", {})
def _server_to_response(name: str, server: dict) -> MCPServerResponse:
return MCPServerResponse(
name=name,
transport=server.get("transport", "stdio"),
command=server.get("command"),
args=server.get("args"),
url=server.get("url"),
env=server.get("env"),
description=server.get("description", ""),
enabled=server.get("enabled", True),
is_builtin=(name == BUILTIN_SERVER),
)
# --- Endpoints ---
@router.get("/servers", response_model=List[MCPServerResponse])
async def list_servers():
"""List all configured MCP servers."""
config = _read_config()
servers = _get_mcp_servers(config)
return [_server_to_response(name, srv) for name, srv in servers.items()]
@router.get("/servers/{name}", response_model=MCPServerResponse)
async def get_server(name: str):
"""Get a specific MCP server configuration."""
config = _read_config()
servers = _get_mcp_servers(config)
if name not in servers:
raise HTTPException(404, f"MCP server '{name}' not found")
return _server_to_response(name, servers[name])
@router.post("/servers", response_model=MCPServerResponse)
async def create_server(body: MCPServerCreate):
"""Add a new MCP server configuration."""
config = _read_config()
if "mcp_servers" not in config:
config["mcp_servers"] = {}
servers = config["mcp_servers"]
if body.name in servers:
raise HTTPException(409, f"Server '{body.name}' already exists")
# Validate transport-specific fields
if body.transport == "stdio" and not body.command:
raise HTTPException(400, "stdio transport requires 'command' field")
if body.transport == "sse" and not body.url:
raise HTTPException(400, "sse transport requires 'url' field")
server_config = {
"transport": body.transport,
"description": body.description,
"enabled": body.enabled,
}
if body.command:
server_config["command"] = body.command
if body.args:
server_config["args"] = body.args
if body.url:
server_config["url"] = body.url
if body.env:
server_config["env"] = body.env
servers[body.name] = server_config
_write_config(config)
return _server_to_response(body.name, server_config)
@router.put("/servers/{name}", response_model=MCPServerResponse)
async def update_server(name: str, body: MCPServerUpdate):
"""Update an MCP server configuration."""
config = _read_config()
servers = _get_mcp_servers(config)
if name not in servers:
raise HTTPException(404, f"MCP server '{name}' not found")
srv = servers[name]
if body.transport is not None:
srv["transport"] = body.transport
if body.command is not None:
srv["command"] = body.command
if body.args is not None:
srv["args"] = body.args
if body.url is not None:
srv["url"] = body.url
if body.env is not None:
srv["env"] = body.env
if body.description is not None:
srv["description"] = body.description
if body.enabled is not None:
srv["enabled"] = body.enabled
_write_config(config)
return _server_to_response(name, srv)
@router.delete("/servers/{name}")
async def delete_server(name: str):
"""Delete an MCP server configuration."""
if name == BUILTIN_SERVER:
raise HTTPException(403, f"Cannot delete built-in server '{BUILTIN_SERVER}'")
config = _read_config()
servers = _get_mcp_servers(config)
if name not in servers:
raise HTTPException(404, f"MCP server '{name}' not found")
del servers[name]
_write_config(config)
return {"message": f"Server '{name}' deleted"}
@router.post("/servers/{name}/toggle", response_model=MCPServerResponse)
async def toggle_server(name: str):
"""Toggle a server's enabled state."""
config = _read_config()
servers = _get_mcp_servers(config)
if name not in servers:
raise HTTPException(404, f"MCP server '{name}' not found")
srv = servers[name]
srv["enabled"] = not srv.get("enabled", True)
_write_config(config)
return _server_to_response(name, srv)
@router.post("/servers/{name}/test")
async def test_server_connection(name: str):
"""Test connection to an MCP server."""
config = _read_config()
servers = _get_mcp_servers(config)
if name not in servers:
raise HTTPException(404, f"MCP server '{name}' not found")
srv = servers[name]
transport = srv.get("transport", "stdio")
try:
if transport == "sse":
# Test SSE endpoint
import aiohttp
url = srv.get("url", "")
if not url:
return {"success": False, "error": "No URL configured", "tools_count": 0}
async with aiohttp.ClientSession() as session:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=5)) as resp:
if resp.status < 400:
return {"success": True, "message": f"SSE endpoint reachable (HTTP {resp.status})", "tools_count": 0}
return {"success": False, "error": f"HTTP {resp.status}", "tools_count": 0}
elif transport == "stdio":
# Test stdio by checking command exists
import shutil
command = srv.get("command", "")
if not command:
return {"success": False, "error": "No command configured", "tools_count": 0}
if shutil.which(command):
return {"success": True, "message": f"Command '{command}' found in PATH", "tools_count": 0}
else:
return {"success": False, "error": f"Command '{command}' not found in PATH", "tools_count": 0}
except asyncio.TimeoutError:
return {"success": False, "error": "Connection timed out (5s)", "tools_count": 0}
except Exception as e:
return {"success": False, "error": str(e), "tools_count": 0}
@router.get("/servers/{name}/tools", response_model=List[MCPToolResponse])
async def list_server_tools(name: str):
"""List available tools from an MCP server.
For the built-in server, returns tools from the registry.
For external servers, attempts to connect and query.
"""
config = _read_config()
servers = _get_mcp_servers(config)
if name not in servers:
raise HTTPException(404, f"MCP server '{name}' not found")
# For builtin server, return tools from the MCP server module
if name == BUILTIN_SERVER:
try:
from core.mcp_server import TOOLS
return [
MCPToolResponse(
name=t["name"],
description=t.get("description", ""),
server_name=name,
)
for t in TOOLS
]
except ImportError:
return []
# For external servers, try to connect via MCPToolClient
try:
from core.mcp_client import MCPToolClient
# Build minimal config for this single server
client_config = {
"mcp_servers": {
"enabled": True,
"servers": {name: servers[name]}
}
}
client = MCPToolClient(client_config)
connected = await asyncio.wait_for(client.connect(name), timeout=10)
if not connected:
raise HTTPException(502, f"Failed to connect to MCP server '{name}'")
tools_dict = await client.list_tools(name)
tool_list = tools_dict.get(name, [])
await client.disconnect_all()
return [
MCPToolResponse(
name=t.get("name", ""),
description=t.get("description", ""),
server_name=name,
)
for t in tool_list
]
except ImportError:
raise HTTPException(501, "MCP client library not installed")
except asyncio.TimeoutError:
raise HTTPException(504, "Connection to MCP server timed out (10s)")
except HTTPException:
raise
except Exception as e:
raise HTTPException(502, f"Failed to list tools: {str(e)}")