310 lines
8.8 KiB
Rust
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())
|
|
}
|
|
}
|
|
}
|