Files
sovereign_browser_rust/src/agent_mcp.rs
T

310 lines
8.8 KiB
Rust

//! MCP (Model Context Protocol) server for agent tools
//!
//! Port of `agent_mcp.c` / `agent_mcp.h` from the C project.
//!
//! Implements a Streamable HTTP server on port 17777 that accepts
//! JSON-RPC 2.0 requests (initialize, tools/list, tools/call) and
//! dispatches them to the agent_tools module.
use std::collections::HashMap;
use std::io::{Read, Write, BufRead, BufReader};
use std::net::{TcpListener, TcpStream};
use std::sync::Mutex;
use std::thread;
use once_cell::sync::Lazy;
use crate::agent_tools;
/// MCP tool definition.
#[derive(Debug, Clone, serde::Serialize)]
pub struct McpTool {
pub name: String,
pub description: String,
pub input_schema: serde_json::Value,
}
/// MCP server state.
static G_MCP_STATE: Lazy<Mutex<McpState>> = Lazy::new(|| Mutex::new(McpState::default()));
struct McpState {
running: bool,
port: u16,
}
impl Default for McpState {
fn default() -> Self {
McpState {
running: false,
port: 17777,
}
}
}
/// Initialize the MCP server.
pub fn agent_mcp_init() {
let mut state = G_MCP_STATE.lock().unwrap();
state.port = 17777;
}
/// Start the MCP server (spawns a background thread).
pub fn agent_mcp_start() -> Result<(), Box<dyn std::error::Error>> {
let mut state = G_MCP_STATE.lock().unwrap();
if state.running {
return Ok(());
}
state.running = true;
let port = state.port;
drop(state);
println!("[mcp] MCP server starting on port {}", port);
thread::spawn(move || {
run_server(port);
});
Ok(())
}
/// Stop the MCP server.
pub fn agent_mcp_stop() {
let mut state = G_MCP_STATE.lock().unwrap();
state.running = false;
println!("[mcp] MCP server stopped");
}
/// Check if the MCP server is running.
pub fn agent_mcp_is_running() -> bool {
let state = G_MCP_STATE.lock().unwrap();
state.running
}
/// Get the MCP server port.
pub fn agent_mcp_get_port() -> u16 {
let state = G_MCP_STATE.lock().unwrap();
state.port
}
/// Main server loop: accept connections and handle HTTP requests.
fn run_server(port: u16) {
let listener = match TcpListener::bind(("127.0.0.1", port)) {
Ok(l) => l,
Err(e) => {
eprintln!("[mcp] Failed to bind port {}: {}", port, e);
// Try fallback ports
for p in (port + 1)..(port + 100) {
if let Ok(l) = TcpListener::bind(("127.0.0.1", p)) {
println!("[mcp] MCP server bound to fallback port {}", p);
let mut state = G_MCP_STATE.lock().unwrap();
state.port = p;
drop(state);
handle_connections(l);
return;
}
}
eprintln!("[mcp] Failed to bind any port in range {}-{}", port, port + 100);
return;
}
};
println!("[mcp] MCP server listening on http://127.0.0.1:{}/mcp", port);
handle_connections(listener);
}
/// Accept and handle connections.
fn handle_connections(listener: TcpListener) {
for stream in listener.incoming() {
match stream {
Ok(stream) => {
thread::spawn(move || {
let _ = handle_connection(stream);
});
}
Err(e) => {
eprintln!("[mcp] Accept error: {}", e);
}
}
}
}
/// Handle a single HTTP connection.
fn handle_connection(mut stream: TcpStream) -> std::io::Result<()> {
let mut reader = BufReader::new(stream.try_clone()?);
let mut line = String::new();
// Read the request line (e.g. "POST /mcp HTTP/1.1").
if reader.read_line(&mut line)? == 0 {
return Ok(());
}
let is_mcp = line.contains("/mcp") || line.contains("/");
// Read headers.
let mut headers: HashMap<String, String> = HashMap::new();
let mut content_length: usize = 0;
loop {
line.clear();
if reader.read_line(&mut line)? == 0 {
break;
}
let trimmed = line.trim();
if trimmed.is_empty() {
break; // End of headers
}
if let Some(pos) = trimmed.find(':') {
let key = trimmed[..pos].trim().to_lowercase();
let val = trimmed[pos + 1..].trim().to_string();
if key == "content-length" {
content_length = val.parse().unwrap_or(0);
}
headers.insert(key, val);
}
}
// Read body if present.
let mut body = String::new();
if content_length > 0 {
let mut buf = vec![0u8; content_length];
reader.read_exact(&mut buf)?;
body = String::from_utf8_lossy(&buf).to_string();
}
// Handle the request.
let (status, response_body) = if is_mcp && !body.is_empty() {
match agent_mcp_handle_request(&body) {
Ok(resp) => ("200 OK", resp),
Err(e) => (
"500 Internal Server Error",
serde_json::json!({
"jsonrpc": "2.0",
"error": { "code": -32603, "message": e.to_string() }
})
.to_string(),
),
}
} else if is_mcp && body.is_empty() {
// GET request to /mcp — return server info.
(
"200 OK",
serde_json::json!({
"server": "sovereign-browser-mcp",
"version": crate::version::VERSION,
"capabilities": {
"tools": {}
}
})
.to_string(),
)
} else {
("404 Not Found", "Not Found".to_string())
};
// Send HTTP response.
let response = format!(
"HTTP/1.1 {}\r\n\
Content-Type: application/json\r\n\
Content-Length: {}\r\n\
Access-Control-Allow-Origin: *\r\n\
Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n\
Access-Control-Allow-Headers: Content-Type\r\n\
\r\n\
{}",
status,
response_body.len(),
response_body
);
stream.write_all(response.as_bytes())?;
stream.flush()?;
Ok(())
}
/// Handle an MCP JSON-RPC request.
pub fn agent_mcp_handle_request(body: &str) -> Result<String, Box<dyn std::error::Error>> {
let request: serde_json::Value = serde_json::from_str(body)?;
let method = request["method"]
.as_str()
.ok_or("Missing method")?;
match method {
"initialize" => {
Ok(serde_json::json!({
"jsonrpc": "2.0",
"id": request["id"],
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "sovereign-browser",
"version": crate::version::VERSION
}
}
})
.to_string())
}
"tools/list" => {
let tools = agent_tools::agent_tools_get_all();
let mcp_tools: Vec<McpTool> = tools
.into_iter()
.map(|t| McpTool {
name: t.name,
description: t.description,
input_schema: t.input_schema,
})
.collect();
Ok(serde_json::json!({
"jsonrpc": "2.0",
"id": request["id"],
"result": { "tools": mcp_tools }
})
.to_string())
}
"tools/call" => {
let name = request["params"]["name"]
.as_str()
.ok_or("Missing tool name")?;
let args = &request["params"]["arguments"];
match agent_tools::agent_tools_execute(name, args) {
Ok(result) => {
Ok(serde_json::json!({
"jsonrpc": "2.0",
"id": request["id"],
"result": { "content": [{ "type": "text", "text": serde_json::to_string(&result)? }] }
})
.to_string())
}
Err(e) => {
Ok(serde_json::json!({
"jsonrpc": "2.0",
"id": request["id"],
"error": { "code": -32000, "message": e }
})
.to_string())
}
}
}
"ping" => {
Ok(serde_json::json!({
"jsonrpc": "2.0",
"id": request["id"],
"result": {}
})
.to_string())
}
_ => {
Ok(serde_json::json!({
"jsonrpc": "2.0",
"id": request["id"],
"error": { "code": -32601, "message": format!("Method not found: {}", method) }
})
.to_string())
}
}
}