Full security platform with web dashboard, 16 Flask blueprints, 26 modules, autonomous AI agent, WebUSB hardware support, and Archon Android companion app. Includes Hash Toolkit, debug console, anti-stalkerware shield, Metasploit/RouterSploit integration, WireGuard VPN, OSINT reconnaissance, and multi-backend LLM support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
586 lines
19 KiB
Python
586 lines
19 KiB
Python
"""
|
|
AUTARCH MCP Server
|
|
Exposes AUTARCH tools via Model Context Protocol (MCP)
|
|
for use with Claude Desktop, Claude Code, and other MCP clients.
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
import json
|
|
import socket
|
|
import subprocess
|
|
import threading
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
# Ensure core is importable
|
|
_app_dir = Path(__file__).resolve().parent.parent
|
|
if str(_app_dir) not in sys.path:
|
|
sys.path.insert(0, str(_app_dir))
|
|
|
|
from core.config import get_config
|
|
from core.paths import find_tool, get_app_dir
|
|
|
|
# MCP server state
|
|
_server_process = None
|
|
_server_thread = None
|
|
|
|
|
|
def get_autarch_tools():
|
|
"""Build the list of AUTARCH tools to expose via MCP."""
|
|
tools = []
|
|
|
|
# ── Network Scanning ──
|
|
tools.append({
|
|
'name': 'nmap_scan',
|
|
'description': 'Run an nmap scan against a target. Returns scan results.',
|
|
'params': {
|
|
'target': {'type': 'string', 'description': 'Target IP, hostname, or CIDR range', 'required': True},
|
|
'ports': {'type': 'string', 'description': 'Port specification (e.g. "22,80,443" or "1-1024")', 'required': False},
|
|
'scan_type': {'type': 'string', 'description': 'Scan type: quick, full, stealth, vuln', 'required': False},
|
|
}
|
|
})
|
|
|
|
# ── GeoIP Lookup ──
|
|
tools.append({
|
|
'name': 'geoip_lookup',
|
|
'description': 'Look up geographic and network information for an IP address.',
|
|
'params': {
|
|
'ip': {'type': 'string', 'description': 'IP address to look up', 'required': True},
|
|
}
|
|
})
|
|
|
|
# ── DNS Lookup ──
|
|
tools.append({
|
|
'name': 'dns_lookup',
|
|
'description': 'Perform DNS lookups for a domain.',
|
|
'params': {
|
|
'domain': {'type': 'string', 'description': 'Domain name to look up', 'required': True},
|
|
'record_type': {'type': 'string', 'description': 'Record type: A, AAAA, MX, NS, TXT, CNAME, SOA', 'required': False},
|
|
}
|
|
})
|
|
|
|
# ── WHOIS ──
|
|
tools.append({
|
|
'name': 'whois_lookup',
|
|
'description': 'Perform WHOIS lookup for a domain or IP.',
|
|
'params': {
|
|
'target': {'type': 'string', 'description': 'Domain or IP to look up', 'required': True},
|
|
}
|
|
})
|
|
|
|
# ── Packet Capture ──
|
|
tools.append({
|
|
'name': 'packet_capture',
|
|
'description': 'Capture network packets using tcpdump. Returns captured packet summary.',
|
|
'params': {
|
|
'interface': {'type': 'string', 'description': 'Network interface (e.g. eth0, wlan0)', 'required': False},
|
|
'count': {'type': 'integer', 'description': 'Number of packets to capture (default 10)', 'required': False},
|
|
'filter': {'type': 'string', 'description': 'BPF filter expression', 'required': False},
|
|
}
|
|
})
|
|
|
|
# ── WireGuard Status ──
|
|
tools.append({
|
|
'name': 'wireguard_status',
|
|
'description': 'Get WireGuard VPN tunnel status and peer information.',
|
|
'params': {}
|
|
})
|
|
|
|
# ── UPnP Status ──
|
|
tools.append({
|
|
'name': 'upnp_status',
|
|
'description': 'Get UPnP port mapping status.',
|
|
'params': {}
|
|
})
|
|
|
|
# ── System Info ──
|
|
tools.append({
|
|
'name': 'system_info',
|
|
'description': 'Get AUTARCH system information: hostname, platform, uptime, tool availability.',
|
|
'params': {}
|
|
})
|
|
|
|
# ── LLM Chat ──
|
|
tools.append({
|
|
'name': 'llm_chat',
|
|
'description': 'Send a message to the currently configured LLM backend and get a response.',
|
|
'params': {
|
|
'message': {'type': 'string', 'description': 'Message to send to the LLM', 'required': True},
|
|
'system_prompt': {'type': 'string', 'description': 'Optional system prompt', 'required': False},
|
|
}
|
|
})
|
|
|
|
# ── Android Device Info ──
|
|
tools.append({
|
|
'name': 'android_devices',
|
|
'description': 'List connected Android devices via ADB.',
|
|
'params': {}
|
|
})
|
|
|
|
# ── Config Get/Set ──
|
|
tools.append({
|
|
'name': 'config_get',
|
|
'description': 'Get an AUTARCH configuration value.',
|
|
'params': {
|
|
'section': {'type': 'string', 'description': 'Config section (e.g. autarch, llama, wireguard)', 'required': True},
|
|
'key': {'type': 'string', 'description': 'Config key', 'required': True},
|
|
}
|
|
})
|
|
|
|
return tools
|
|
|
|
|
|
def execute_tool(name: str, arguments: dict) -> str:
|
|
"""Execute an AUTARCH tool and return the result as a string."""
|
|
config = get_config()
|
|
|
|
if name == 'nmap_scan':
|
|
return _run_nmap(arguments, config)
|
|
elif name == 'geoip_lookup':
|
|
return _run_geoip(arguments)
|
|
elif name == 'dns_lookup':
|
|
return _run_dns(arguments)
|
|
elif name == 'whois_lookup':
|
|
return _run_whois(arguments)
|
|
elif name == 'packet_capture':
|
|
return _run_tcpdump(arguments)
|
|
elif name == 'wireguard_status':
|
|
return _run_wg_status(config)
|
|
elif name == 'upnp_status':
|
|
return _run_upnp_status(config)
|
|
elif name == 'system_info':
|
|
return _run_system_info()
|
|
elif name == 'llm_chat':
|
|
return _run_llm_chat(arguments, config)
|
|
elif name == 'android_devices':
|
|
return _run_adb_devices()
|
|
elif name == 'config_get':
|
|
return _run_config_get(arguments, config)
|
|
else:
|
|
return json.dumps({'error': f'Unknown tool: {name}'})
|
|
|
|
|
|
def _run_nmap(args: dict, config) -> str:
|
|
nmap = find_tool('nmap')
|
|
if not nmap:
|
|
return json.dumps({'error': 'nmap not found'})
|
|
|
|
target = args.get('target', '')
|
|
if not target:
|
|
return json.dumps({'error': 'target is required'})
|
|
|
|
cmd = [str(nmap)]
|
|
scan_type = args.get('scan_type', 'quick')
|
|
if scan_type == 'stealth':
|
|
cmd.extend(['-sS', '-T2'])
|
|
elif scan_type == 'full':
|
|
cmd.extend(['-sV', '-sC', '-O'])
|
|
elif scan_type == 'vuln':
|
|
cmd.extend(['-sV', '--script=vuln'])
|
|
else:
|
|
cmd.extend(['-sV', '-T4'])
|
|
|
|
ports = args.get('ports', '')
|
|
if ports:
|
|
cmd.extend(['-p', ports])
|
|
|
|
cmd.append(target)
|
|
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
return json.dumps({
|
|
'stdout': result.stdout,
|
|
'stderr': result.stderr,
|
|
'exit_code': result.returncode
|
|
})
|
|
except subprocess.TimeoutExpired:
|
|
return json.dumps({'error': 'Scan timed out after 120 seconds'})
|
|
except Exception as e:
|
|
return json.dumps({'error': str(e)})
|
|
|
|
|
|
def _run_geoip(args: dict) -> str:
|
|
ip = args.get('ip', '')
|
|
if not ip:
|
|
return json.dumps({'error': 'ip is required'})
|
|
|
|
try:
|
|
import urllib.request
|
|
url = f"http://ip-api.com/json/{ip}?fields=status,message,country,regionName,city,zip,lat,lon,timezone,isp,org,as,query"
|
|
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
return resp.read().decode()
|
|
except Exception as e:
|
|
return json.dumps({'error': str(e)})
|
|
|
|
|
|
def _run_dns(args: dict) -> str:
|
|
domain = args.get('domain', '')
|
|
if not domain:
|
|
return json.dumps({'error': 'domain is required'})
|
|
|
|
record_type = args.get('record_type', 'A')
|
|
try:
|
|
result = subprocess.run(
|
|
['dig', '+short', domain, record_type],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
records = [r for r in result.stdout.strip().split('\n') if r]
|
|
return json.dumps({'domain': domain, 'type': record_type, 'records': records})
|
|
except FileNotFoundError:
|
|
# Fallback to socket for A records
|
|
try:
|
|
ips = socket.getaddrinfo(domain, None)
|
|
records = list(set(addr[4][0] for addr in ips))
|
|
return json.dumps({'domain': domain, 'type': 'A', 'records': records})
|
|
except Exception as e:
|
|
return json.dumps({'error': str(e)})
|
|
except Exception as e:
|
|
return json.dumps({'error': str(e)})
|
|
|
|
|
|
def _run_whois(args: dict) -> str:
|
|
target = args.get('target', '')
|
|
if not target:
|
|
return json.dumps({'error': 'target is required'})
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
['whois', target],
|
|
capture_output=True, text=True, timeout=15
|
|
)
|
|
return json.dumps({'target': target, 'output': result.stdout[:4000]})
|
|
except FileNotFoundError:
|
|
return json.dumps({'error': 'whois command not found'})
|
|
except Exception as e:
|
|
return json.dumps({'error': str(e)})
|
|
|
|
|
|
def _run_tcpdump(args: dict) -> str:
|
|
tcpdump = find_tool('tcpdump')
|
|
if not tcpdump:
|
|
return json.dumps({'error': 'tcpdump not found'})
|
|
|
|
cmd = [str(tcpdump), '-n']
|
|
iface = args.get('interface', '')
|
|
if iface:
|
|
cmd.extend(['-i', iface])
|
|
|
|
count = args.get('count', 10)
|
|
cmd.extend(['-c', str(count)])
|
|
|
|
bpf_filter = args.get('filter', '')
|
|
if bpf_filter:
|
|
cmd.append(bpf_filter)
|
|
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
return json.dumps({
|
|
'stdout': result.stdout,
|
|
'stderr': result.stderr,
|
|
'exit_code': result.returncode
|
|
})
|
|
except subprocess.TimeoutExpired:
|
|
return json.dumps({'error': 'Capture timed out'})
|
|
except Exception as e:
|
|
return json.dumps({'error': str(e)})
|
|
|
|
|
|
def _run_wg_status(config) -> str:
|
|
wg = find_tool('wg')
|
|
if not wg:
|
|
return json.dumps({'error': 'wg not found'})
|
|
|
|
iface = config.get('wireguard', 'interface', 'wg0')
|
|
try:
|
|
result = subprocess.run(
|
|
[str(wg), 'show', iface],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
return json.dumps({
|
|
'interface': iface,
|
|
'output': result.stdout,
|
|
'active': result.returncode == 0
|
|
})
|
|
except Exception as e:
|
|
return json.dumps({'error': str(e)})
|
|
|
|
|
|
def _run_upnp_status(config) -> str:
|
|
upnpc = find_tool('upnpc')
|
|
if not upnpc:
|
|
return json.dumps({'error': 'upnpc not found'})
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
[str(upnpc), '-l'],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
return json.dumps({
|
|
'output': result.stdout,
|
|
'exit_code': result.returncode
|
|
})
|
|
except Exception as e:
|
|
return json.dumps({'error': str(e)})
|
|
|
|
|
|
def _run_system_info() -> str:
|
|
import platform
|
|
|
|
info = {
|
|
'hostname': socket.gethostname(),
|
|
'platform': platform.platform(),
|
|
'python': platform.python_version(),
|
|
'arch': platform.machine(),
|
|
}
|
|
|
|
try:
|
|
info['ip'] = socket.gethostbyname(socket.gethostname())
|
|
except Exception:
|
|
info['ip'] = '127.0.0.1'
|
|
|
|
try:
|
|
with open('/proc/uptime') as f:
|
|
uptime_secs = float(f.read().split()[0])
|
|
days = int(uptime_secs // 86400)
|
|
hours = int((uptime_secs % 86400) // 3600)
|
|
info['uptime'] = f"{days}d {hours}h"
|
|
except Exception:
|
|
info['uptime'] = 'N/A'
|
|
|
|
# Tool availability
|
|
tools = {}
|
|
for tool in ['nmap', 'tshark', 'tcpdump', 'upnpc', 'wg', 'adb']:
|
|
tools[tool] = find_tool(tool) is not None
|
|
info['tools'] = tools
|
|
|
|
config = get_config()
|
|
info['llm_backend'] = config.get('autarch', 'llm_backend', 'local')
|
|
|
|
return json.dumps(info)
|
|
|
|
|
|
def _run_llm_chat(args: dict, config) -> str:
|
|
message = args.get('message', '')
|
|
if not message:
|
|
return json.dumps({'error': 'message is required'})
|
|
|
|
try:
|
|
from core.llm import get_llm, LLMError
|
|
llm = get_llm()
|
|
if not llm.is_loaded:
|
|
llm.load_model()
|
|
|
|
system_prompt = args.get('system_prompt', None)
|
|
response = llm.chat(message, system_prompt=system_prompt)
|
|
return json.dumps({
|
|
'response': response,
|
|
'model': llm.model_name,
|
|
'backend': config.get('autarch', 'llm_backend', 'local')
|
|
})
|
|
except Exception as e:
|
|
return json.dumps({'error': str(e)})
|
|
|
|
|
|
def _run_adb_devices() -> str:
|
|
adb = find_tool('adb')
|
|
if not adb:
|
|
return json.dumps({'error': 'adb not found'})
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
[str(adb), 'devices', '-l'],
|
|
capture_output=True, text=True, timeout=10
|
|
)
|
|
lines = result.stdout.strip().split('\n')[1:] # Skip header
|
|
devices = []
|
|
for line in lines:
|
|
if line.strip():
|
|
parts = line.split()
|
|
if len(parts) >= 2:
|
|
dev = {'serial': parts[0], 'state': parts[1]}
|
|
# Parse extra info
|
|
for part in parts[2:]:
|
|
if ':' in part:
|
|
k, v = part.split(':', 1)
|
|
dev[k] = v
|
|
devices.append(dev)
|
|
return json.dumps({'devices': devices})
|
|
except Exception as e:
|
|
return json.dumps({'error': str(e)})
|
|
|
|
|
|
def _run_config_get(args: dict, config) -> str:
|
|
section = args.get('section', '')
|
|
key = args.get('key', '')
|
|
if not section or not key:
|
|
return json.dumps({'error': 'section and key are required'})
|
|
|
|
# Block sensitive keys
|
|
if key.lower() in ('api_key', 'password', 'secret_key', 'token'):
|
|
return json.dumps({'error': 'Cannot read sensitive configuration values'})
|
|
|
|
value = config.get(section, key, fallback='(not set)')
|
|
return json.dumps({'section': section, 'key': key, 'value': value})
|
|
|
|
|
|
def create_mcp_server():
|
|
"""Create and return the FastMCP server instance."""
|
|
from mcp.server.fastmcp import FastMCP
|
|
|
|
mcp = FastMCP("autarch", instructions="AUTARCH security framework tools")
|
|
|
|
# Register all tools
|
|
tool_defs = get_autarch_tools()
|
|
|
|
@mcp.tool()
|
|
def nmap_scan(target: str, ports: str = "", scan_type: str = "quick") -> str:
|
|
"""Run an nmap network scan against a target. Returns scan results including open ports and services."""
|
|
return execute_tool('nmap_scan', {'target': target, 'ports': ports, 'scan_type': scan_type})
|
|
|
|
@mcp.tool()
|
|
def geoip_lookup(ip: str) -> str:
|
|
"""Look up geographic and network information for an IP address."""
|
|
return execute_tool('geoip_lookup', {'ip': ip})
|
|
|
|
@mcp.tool()
|
|
def dns_lookup(domain: str, record_type: str = "A") -> str:
|
|
"""Perform DNS lookups for a domain. Supports A, AAAA, MX, NS, TXT, CNAME, SOA record types."""
|
|
return execute_tool('dns_lookup', {'domain': domain, 'record_type': record_type})
|
|
|
|
@mcp.tool()
|
|
def whois_lookup(target: str) -> str:
|
|
"""Perform WHOIS lookup for a domain or IP address."""
|
|
return execute_tool('whois_lookup', {'target': target})
|
|
|
|
@mcp.tool()
|
|
def packet_capture(interface: str = "", count: int = 10, filter: str = "") -> str:
|
|
"""Capture network packets using tcpdump. Returns captured packet summary."""
|
|
return execute_tool('packet_capture', {'interface': interface, 'count': count, 'filter': filter})
|
|
|
|
@mcp.tool()
|
|
def wireguard_status() -> str:
|
|
"""Get WireGuard VPN tunnel status and peer information."""
|
|
return execute_tool('wireguard_status', {})
|
|
|
|
@mcp.tool()
|
|
def upnp_status() -> str:
|
|
"""Get UPnP port mapping status."""
|
|
return execute_tool('upnp_status', {})
|
|
|
|
@mcp.tool()
|
|
def system_info() -> str:
|
|
"""Get AUTARCH system information: hostname, platform, uptime, tool availability."""
|
|
return execute_tool('system_info', {})
|
|
|
|
@mcp.tool()
|
|
def llm_chat(message: str, system_prompt: str = "") -> str:
|
|
"""Send a message to the currently configured LLM backend and get a response."""
|
|
args = {'message': message}
|
|
if system_prompt:
|
|
args['system_prompt'] = system_prompt
|
|
return execute_tool('llm_chat', args)
|
|
|
|
@mcp.tool()
|
|
def android_devices() -> str:
|
|
"""List connected Android devices via ADB."""
|
|
return execute_tool('android_devices', {})
|
|
|
|
@mcp.tool()
|
|
def config_get(section: str, key: str) -> str:
|
|
"""Get an AUTARCH configuration value. Sensitive keys (api_key, password) are blocked."""
|
|
return execute_tool('config_get', {'section': section, 'key': key})
|
|
|
|
return mcp
|
|
|
|
|
|
def run_stdio():
|
|
"""Run the MCP server in stdio mode (for Claude Desktop / Claude Code)."""
|
|
mcp = create_mcp_server()
|
|
mcp.run(transport='stdio')
|
|
|
|
|
|
def run_sse(host: str = '0.0.0.0', port: int = 8081):
|
|
"""Run the MCP server in SSE (Server-Sent Events) mode for web clients."""
|
|
mcp = create_mcp_server()
|
|
mcp.run(transport='sse', host=host, port=port)
|
|
|
|
|
|
def get_mcp_config_snippet() -> str:
|
|
"""Generate the JSON config snippet for Claude Desktop / Claude Code."""
|
|
app_dir = get_app_dir()
|
|
python = sys.executable
|
|
|
|
config = {
|
|
"mcpServers": {
|
|
"autarch": {
|
|
"command": python,
|
|
"args": [str(app_dir / "core" / "mcp_server.py"), "--stdio"],
|
|
"env": {}
|
|
}
|
|
}
|
|
}
|
|
return json.dumps(config, indent=2)
|
|
|
|
|
|
def get_server_status() -> dict:
|
|
"""Check if the MCP server is running."""
|
|
global _server_process
|
|
if _server_process and _server_process.poll() is None:
|
|
return {'running': True, 'pid': _server_process.pid, 'mode': 'sse'}
|
|
return {'running': False}
|
|
|
|
|
|
def start_sse_server(host: str = '0.0.0.0', port: int = 8081) -> dict:
|
|
"""Start the MCP SSE server in the background."""
|
|
global _server_process
|
|
|
|
status = get_server_status()
|
|
if status['running']:
|
|
return {'ok': False, 'error': f'Already running (PID {status["pid"]})'}
|
|
|
|
python = sys.executable
|
|
script = str(Path(__file__).resolve())
|
|
|
|
_server_process = subprocess.Popen(
|
|
[python, script, '--sse', '--host', host, '--port', str(port)],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE
|
|
)
|
|
|
|
return {'ok': True, 'pid': _server_process.pid, 'host': host, 'port': port}
|
|
|
|
|
|
def stop_sse_server() -> dict:
|
|
"""Stop the MCP SSE server."""
|
|
global _server_process
|
|
|
|
status = get_server_status()
|
|
if not status['running']:
|
|
return {'ok': False, 'error': 'Not running'}
|
|
|
|
_server_process.terminate()
|
|
try:
|
|
_server_process.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
_server_process.kill()
|
|
_server_process = None
|
|
return {'ok': True}
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import argparse
|
|
parser = argparse.ArgumentParser(description='AUTARCH MCP Server')
|
|
parser.add_argument('--stdio', action='store_true', help='Run in stdio mode (for Claude Desktop/Code)')
|
|
parser.add_argument('--sse', action='store_true', help='Run in SSE mode (for web clients)')
|
|
parser.add_argument('--host', default='0.0.0.0', help='SSE host (default: 0.0.0.0)')
|
|
parser.add_argument('--port', type=int, default=8081, help='SSE port (default: 8081)')
|
|
args = parser.parse_args()
|
|
|
|
if args.sse:
|
|
print(f"Starting AUTARCH MCP server (SSE) on {args.host}:{args.port}")
|
|
run_sse(host=args.host, port=args.port)
|
|
else:
|
|
# Default to stdio
|
|
run_stdio()
|