use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::{info, instrument};
use crate::error::{Error, Result};
use crate::mcp::{McpClient, McpServer, McpTool};
use crate::tools::ToolRegistry;
use crate::llm::ToolSpec;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
#[derive(Debug, Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
#[serde(default)]
pub id: Option<Value>,
pub method: String,
#[serde(default)]
pub params: Value,
}
#[derive(Debug, Serialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
#[derive(Debug, Serialize)]
pub struct JsonRpcError {
pub code: i32,
pub message: String,
}
impl JsonRpcResponse {
pub fn success(id: Value, result: Value) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: Some(result),
error: None,
}
}
pub fn error(id: Value, code: i32, message: String) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(JsonRpcError { code, message }),
}
}
pub fn parse_error() -> Self {
Self::error(Value::Null, -32700, "Parse error".to_string())
}
pub fn method_not_found(id: Value) -> Self {
Self::error(id, -32601, "Method not found".to_string())
}
}
pub async fn dispatch_request(
request: &JsonRpcRequest,
tool_specs: &[ToolSpec],
tools: &ToolRegistry,
) -> Option<JsonRpcResponse> {
match request.method.as_str() {
"initialize" => Some(handle_initialize(request)),
"notifications/initialized" => None,
"tools/list" => Some(handle_tools_list(request, tool_specs)),
"tools/call" => Some(handle_tools_call(request, tools).await),
_ => {
let id = request.id.clone().unwrap_or(Value::Null);
Some(JsonRpcResponse::method_not_found(id))
}
}
}
fn handle_initialize(request: &JsonRpcRequest) -> JsonRpcResponse {
let id = request.id.clone().unwrap_or(Value::Null);
JsonRpcResponse::success(
id,
serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "recursive-mcp-server",
"version": "0.1.0"
}
}),
)
}
fn handle_tools_list(request: &JsonRpcRequest, tool_specs: &[ToolSpec]) -> JsonRpcResponse {
let id = request.id.clone().unwrap_or(Value::Null);
let tools: Vec<Value> = tool_specs
.iter()
.map(|spec| {
serde_json::json!({
"name": spec.name,
"description": spec.description,
"inputSchema": spec.parameters,
})
})
.collect();
JsonRpcResponse::success(id, serde_json::json!({ "tools": tools }))
}
async fn handle_tools_call(request: &JsonRpcRequest, tools: &ToolRegistry) -> JsonRpcResponse {
let id = request.id.clone().unwrap_or(Value::Null);
let tool_name = request
.params
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("");
let arguments = request
.params
.get("arguments")
.cloned()
.unwrap_or(serde_json::json!({}));
if tool_name.is_empty() {
return JsonRpcResponse::error(id, -32602, "Missing tool name".to_string());
}
match tools.get(tool_name) {
Some(tool) => match tool.execute(arguments).await {
Ok(text) => JsonRpcResponse::success(
id,
serde_json::json!({
"content": [{"type": "text", "text": text}]
}),
),
Err(e) => JsonRpcResponse::success(
id,
serde_json::json!({
"isError": true,
"content": [{"type": "text", "text": e.to_string()}]
}),
),
},
None => JsonRpcResponse::error(
id,
-32602,
format!("Tool not found: {tool_name}"),
),
}
}
pub struct McpServerRunner {
tool_specs: Vec<ToolSpec>,
tools: ToolRegistry,
}
impl McpServerRunner {
pub fn new(tools: ToolRegistry) -> Self {
let tool_specs = tools.specs();
Self { tool_specs, tools }
}
pub async fn run(&self) -> Result<()> {
let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();
self.run_on(BufReader::new(stdin), stdout).await
}
pub async fn run_on<R, W>(&self, reader: R, writer: W) -> Result<()>
where
R: tokio::io::AsyncBufRead + Unpin,
W: tokio::io::AsyncWrite + Unpin,
{
let mut lines = BufReader::new(reader).lines();
let mut out = writer;
while let Some(line) = lines.next_line().await? {
let line = line.trim().to_string();
if line.is_empty() {
continue;
}
let request: JsonRpcRequest = match serde_json::from_str(&line) {
Ok(req) => req,
Err(_) => {
let resp = JsonRpcResponse::parse_error();
let json = serde_json::to_string(&resp)?;
out.write_all(json.as_bytes()).await?;
out.write_all(b"\n").await?;
out.flush().await?;
continue;
}
};
if let Some(response) = dispatch_request(&request, &self.tool_specs, &self.tools).await {
let json = serde_json::to_string(&response)?;
out.write_all(json.as_bytes()).await?;
out.write_all(b"\n").await?;
out.flush().await?;
}
}
Ok(())
}
}
pub struct McpServerManager {
servers: Vec<McpServer>,
clients: Mutex<HashMap<String, Arc<Mutex<McpClient>>>>,
}
impl McpServerManager {
pub fn new(servers: Vec<McpServer>) -> Self {
Self {
servers,
clients: Mutex::new(HashMap::new()),
}
}
#[instrument(skip_all, name = "mcp.register_all")]
pub async fn register_all(&self, registry: &mut ToolRegistry) -> Result<Vec<(String, usize)>> {
let mut results = Vec::new();
for server in &self.servers {
let name = server.name.clone();
info!(server = %name, "Starting MCP server");
let client = McpClient::spawn(server).await.map_err(|e| {
Error::Tool {
name: format!("mcp_server:{name}"),
message: format!("Failed to start MCP server: {e}"),
}
})?;
let client = Arc::new(Mutex::new(client));
let tool_specs = client
.lock()
.await
.list_tools()
.await
.map_err(|e| Error::Tool {
name: format!("mcp_server:{name}"),
message: format!("Failed to discover tools from MCP server: {e}"),
})?;
let tool_count = tool_specs.len();
info!(
server = %name,
count = tool_count,
"Discovered MCP tools"
);
for spec in &tool_specs {
let tool = McpTool::new(client.clone(), spec.clone(), &name);
registry.register_mut(Arc::new(tool));
info!(
server = %name,
tool = %spec.name,
"Registered MCP tool"
);
}
self.clients.lock().await.insert(name.clone(), client);
results.push((name, tool_count));
}
Ok(results)
}
pub async fn shutdown(&self) {
let mut clients = self.clients.lock().await;
let names: Vec<String> = clients.keys().cloned().collect();
for name in &names {
info!(server = %name, "Shutting down MCP server");
clients.remove(name);
}
}
pub async fn running_count(&self) -> usize {
self.clients.lock().await.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mcp::McpServer;
#[tokio::test]
async fn empty_servers_registers_nothing() {
let manager = McpServerManager::new(vec![]);
let mut registry = ToolRegistry::local();
let results = manager.register_all(&mut registry).await.unwrap();
assert!(results.is_empty());
assert!(registry.names().is_empty());
}
#[test]
fn tool_name_format() {
let server = "my-server";
let tool = "read_file";
let namespaced = format!("mcp__{}__{}", server, tool);
assert_eq!(namespaced, "mcp__my-server__read_file");
}
#[test]
fn sse_server_config_detection() {
let server = McpServer {
name: "test-sse".into(),
command: String::new(),
args: vec![],
url: Some("http://localhost:8080/sse".into()),
};
assert!(server.url.is_some());
assert!(server.command.is_empty());
}
#[test]
fn stdio_server_config_detection() {
let server = McpServer {
name: "test-stdio".into(),
command: "echo".into(),
args: vec!["hello".into()],
url: None,
};
assert!(!server.command.is_empty());
assert!(server.url.is_none());
}
}