Autarch Will Control The Internet
This commit is contained in:
585
core/mcp_server.py
Normal file
585
core/mcp_server.py
Normal file
@@ -0,0 +1,585 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user