use std::{
collections::HashMap,
io::{self, BufRead, Write},
sync::Arc,
};
use llm_tool::{ToolContext, ToolDefinition, ToolRegistry};
use tracing::{debug, error, info};
use crate::protocol::{
self, Capabilities, ContentItem, InitializeResult, JSONRPC_VERSION, JsonRpcRequest,
JsonRpcResponse, McpToolSchema, PromptCapabilities, ResourceCapabilities, ServerInfo,
ToolCallParams, ToolCallResult, ToolCapabilities, ToolsListResult,
};
#[derive(Clone)]
pub struct McpServer {
name: String,
version: String,
registry: Arc<ToolRegistry>,
context: Arc<ToolContext>,
cached_tools_list: Arc<ToolsListResult>,
prompts: Arc<HashMap<&'static str, Box<dyn llm_tool::ErasedPrompt>>>,
resources: Arc<Vec<Box<dyn llm_tool::ErasedResource>>>,
}
impl McpServer {
#[must_use]
pub fn new(
name: impl Into<String>,
version: impl Into<String>,
registry: ToolRegistry,
) -> Self {
let cached_tools_list = Arc::new(build_tools_list_response(®istry));
Self {
name: name.into(),
version: version.into(),
registry: Arc::new(registry),
context: Arc::new(ToolContext::new(None)),
cached_tools_list,
prompts: Arc::new(HashMap::new()),
resources: Arc::new(Vec::new()),
}
}
#[must_use]
pub fn with_context(mut self, context: ToolContext) -> Self {
self.context = Arc::new(context);
self
}
#[must_use]
pub fn with_prompt<P: llm_tool::RustPrompt + 'static>(mut self, prompt: P) -> Self {
let Ok(mut map) = Arc::try_unwrap(self.prompts) else {
panic!("cannot add prompt after server has been cloned");
};
map.insert(P::NAME, Box::new(prompt));
self.prompts = Arc::new(map);
self
}
#[must_use]
pub fn with_resource<R: llm_tool::RustResource + 'static>(mut self, resource: R) -> Self {
let Ok(mut vec) = Arc::try_unwrap(self.resources) else {
panic!("cannot add resource after server has been cloned");
};
vec.push(Box::new(resource));
self.resources = Arc::new(vec);
self
}
#[must_use]
pub fn registry(&self) -> &ToolRegistry {
&self.registry
}
pub fn run_stdio(&self) -> io::Result<()> {
self.run(io::stdin().lock(), io::stdout().lock())
}
pub fn run(&self, reader: impl BufRead, mut writer: impl Write) -> io::Result<()> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()?;
self.run_with_runtime(&rt, reader, &mut writer)
}
pub fn run_with_runtime(
&self,
rt: &tokio::runtime::Runtime,
reader: impl BufRead,
writer: &mut impl Write,
) -> io::Result<()> {
for line_result in reader.lines() {
let line = line_result?;
if line.trim().is_empty() {
continue;
}
debug!(request = %line, "mcp request");
let Some(response_val) = rt.block_on(self.handle_message(&line)) else {
debug!("dropping notification response");
continue;
};
let json = serde_json::to_string(&response_val).map_err(|e| {
error!(error = %e, "failed to serialize JSON-RPC response");
io::Error::other(e)
})?;
debug!(response = %json, "mcp response");
writeln!(writer, "{json}")?;
writer.flush()?;
}
info!("input stream closed — shutting down");
Ok(())
}
pub async fn run_async(
&self,
reader: impl tokio::io::AsyncBufRead + Unpin,
mut writer: impl tokio::io::AsyncWrite + Unpin,
) -> io::Result<()> {
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
let mut lines = reader.lines();
while let Some(line) = lines.next_line().await? {
if line.trim().is_empty() {
continue;
}
debug!(request = %line, "mcp request");
let Some(response_val) = self.handle_message(&line).await else {
debug!("dropping notification response");
continue;
};
let json = serde_json::to_string(&response_val).map_err(|e| {
error!(error = %e, "failed to serialize JSON-RPC response");
io::Error::other(e)
})?;
debug!(response = %json, "mcp response");
writer.write_all(format!("{json}\n").as_bytes()).await?;
writer.flush().await?;
}
info!("input stream closed — shutting down");
Ok(())
}
pub async fn listen_tcp(&self, addr: impl tokio::net::ToSocketAddrs) -> io::Result<()> {
let listener = tokio::net::TcpListener::bind(addr).await?;
info!(addr = ?listener.local_addr()?, "listening on TCP for MCP connections");
self.run_tcp_listener(listener).await
}
pub async fn run_tcp_listener(&self, listener: tokio::net::TcpListener) -> io::Result<()> {
loop {
let (mut socket, peer_addr) = listener.accept().await?;
info!(peer = %peer_addr, "accepted MCP TCP connection");
let server = self.clone();
tokio::spawn(async move {
let (reader, writer) = socket.split();
let reader = tokio::io::BufReader::new(reader);
if let Err(e) = server.run_async(reader, writer).await {
error!(peer = %peer_addr, error = %e, "MCP TCP connection error");
}
info!(peer = %peer_addr, "MCP TCP connection closed");
});
}
}
#[cfg(unix)]
pub async fn listen_unix(&self, path: impl AsRef<std::path::Path>) -> io::Result<()> {
let path = path.as_ref();
if path.exists() {
let _ = std::fs::remove_file(path);
}
let listener = tokio::net::UnixListener::bind(path)?;
info!(path = ?path, "listening on Unix domain socket for MCP connections");
self.run_unix_listener(listener).await
}
#[cfg(unix)]
pub async fn run_unix_listener(&self, listener: tokio::net::UnixListener) -> io::Result<()> {
loop {
let (mut socket, _) = listener.accept().await?;
info!("accepted MCP Unix domain socket connection");
let server = self.clone();
tokio::spawn(async move {
let (reader, writer) = socket.split();
let reader = tokio::io::BufReader::new(reader);
if let Err(e) = server.run_async(reader, writer).await {
error!(error = %e, "MCP Unix connection error");
}
info!("MCP Unix connection closed");
});
}
}
pub async fn handle_request(&self, line: &str) -> JsonRpcResponse {
if let Some(first_non_ws) = line.trim_start().as_bytes().first() {
if *first_non_ws == b'[' {
return JsonRpcResponse::error(
None,
protocol::INVALID_REQUEST,
"batch requests must be processed via handle_message or run/run_async",
);
}
}
let request: JsonRpcRequest = match serde_json::from_str(line) {
Ok(r) => r,
Err(e) => {
return JsonRpcResponse::error(
None,
protocol::PARSE_ERROR,
format!("invalid JSON: {e}"),
);
}
};
if request.version != JSONRPC_VERSION {
return JsonRpcResponse::error(
request.id,
protocol::INVALID_REQUEST,
format!(
"invalid jsonrpc version: expected \"2.0\", got \"{}\"",
request.version
),
);
}
self.dispatch_method(request).await
}
pub async fn handle_message(&self, line: &str) -> Option<serde_json::Value> {
let trimmed = line.trim();
if trimmed.is_empty() {
return None;
}
if let Some(first_non_ws) = trimmed.as_bytes().first() {
if *first_non_ws == b'[' {
return self.handle_batch_request(trimmed).await;
}
}
let response = self.handle_request(trimmed).await;
if response.id.is_none() {
None
} else {
Some(serde_json::to_value(&response).expect("MCP response must be JSON-serializable"))
}
}
async fn handle_batch_request(&self, line: &str) -> Option<serde_json::Value> {
let val: serde_json::Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(e) => {
let resp = JsonRpcResponse::error(
None,
protocol::PARSE_ERROR,
format!("invalid JSON: {e}"),
);
return Some(serde_json::to_value(&resp).expect("serializable"));
}
};
let Some(arr) = val.as_array() else {
let resp = JsonRpcResponse::error(
None,
protocol::INVALID_REQUEST,
"expected JSON array for batch request",
);
return Some(serde_json::to_value(&resp).expect("serializable"));
};
if arr.is_empty() {
let resp = JsonRpcResponse::error(
None,
protocol::INVALID_REQUEST,
"batch request array cannot be empty",
);
return Some(serde_json::to_value(&resp).expect("serializable"));
}
let mut responses = Vec::with_capacity(arr.len());
for item in arr {
let resp_opt = match serde_json::from_value::<JsonRpcRequest>(item.clone()) {
Ok(request) => {
if request.version == JSONRPC_VERSION {
let resp = self.dispatch_method(request).await;
if resp.id.is_none() { None } else { Some(resp) }
} else {
Some(JsonRpcResponse::error(
request.id,
protocol::INVALID_REQUEST,
format!(
"invalid jsonrpc version: expected \"2.0\", got \"{}\"",
request.version
),
))
}
}
Err(e) => {
let id = item.as_object().and_then(|o| o.get("id").cloned());
Some(JsonRpcResponse::error(
id,
protocol::INVALID_REQUEST,
format!("invalid request object in batch: {e}"),
))
}
};
if let Some(resp) = resp_opt {
responses.push(serde_json::to_value(&resp).expect("serializable"));
}
}
if responses.is_empty() {
None
} else {
Some(serde_json::Value::Array(responses))
}
}
async fn dispatch_method(&self, request: JsonRpcRequest) -> JsonRpcResponse {
let id = request.id.clone();
match request.method.as_str() {
"initialize" => self.handle_initialize(id, request.params.as_ref()),
"ping" | "logging/setLevel" => JsonRpcResponse::success(id, protocol::EmptyResult {}),
"notifications/initialized" | "initialized" => {
JsonRpcResponse::success(id, protocol::EmptyResult {})
}
"notifications/cancelled" => {
debug!("received cancellation notification");
JsonRpcResponse::success(id, protocol::EmptyResult {})
}
"tools/list" => self.handle_tools_list(id),
"tools/call" => self.handle_tools_call(id, request.params).await,
"resources/list" => {
let list = protocol::ResourcesListResult {
resources: self
.resources
.iter()
.map(|r| {
let def = r.definition();
protocol::Resource {
uri: def.uri_template,
name: def.name,
description: def.description,
mime_type: def.mime_type,
}
})
.collect(),
};
JsonRpcResponse::success(id, list)
}
"resources/templates/list" => {
let list = protocol::ResourceTemplatesListResult {
resource_templates: self.resources.iter().map(|r| r.definition()).collect(),
};
JsonRpcResponse::success(id, list)
}
"resources/read" => self.handle_resources_read(id, request.params).await,
"prompts/list" => {
let list = protocol::PromptsListResult {
prompts: self.prompts.values().map(|p| p.definition()).collect(),
};
JsonRpcResponse::success(id, list)
}
"prompts/get" => self.handle_prompts_get(id, request.params).await,
"completion/complete" => {
JsonRpcResponse::success(id, protocol::CompletionCompleteResult::default())
}
"notifications/progress" | "notifications/message" => {
debug!("received progress/message notification");
JsonRpcResponse::success(id, protocol::EmptyResult {})
}
other => JsonRpcResponse::error(
id,
protocol::METHOD_NOT_FOUND,
format!("unknown method: {other}"),
),
}
}
const PROTOCOL_VERSION: &str = "2024-11-05";
fn handle_initialize(
&self,
id: Option<serde_json::Value>,
params: Option<&serde_json::Value>,
) -> JsonRpcResponse {
info!(server = %self.name, version = %self.version, "MCP initialize");
if let Some(p) = params {
if let Some(client_ver) = p.get("protocolVersion").and_then(|v| v.as_str()) {
debug!(client_version = %client_ver, server_version = Self::PROTOCOL_VERSION, "protocol version negotiation");
}
}
JsonRpcResponse::success(
id,
InitializeResult {
protocol_version: Self::PROTOCOL_VERSION,
server_info: ServerInfo {
name: self.name.clone(),
version: self.version.clone(),
},
capabilities: Capabilities {
tools: ToolCapabilities {},
resources: ResourceCapabilities {},
prompts: PromptCapabilities {},
},
},
)
}
fn handle_tools_list(&self, id: Option<serde_json::Value>) -> JsonRpcResponse {
info!(count = self.registry.len(), "tools/list");
JsonRpcResponse::success(id, (*self.cached_tools_list).clone())
}
async fn handle_tools_call(
&self,
id: Option<serde_json::Value>,
params: Option<serde_json::Value>,
) -> JsonRpcResponse {
let Some(raw_params) = params else {
return JsonRpcResponse::error(
id,
protocol::INVALID_PARAMS,
"tools/call requires params with 'name' and 'arguments'",
);
};
let call_params: ToolCallParams = match serde_json::from_value(raw_params) {
Ok(p) => p,
Err(e) => {
return JsonRpcResponse::error(
id,
protocol::INVALID_PARAMS,
format!("invalid tools/call params: {e}"),
);
}
};
debug!(tool = %call_params.name, "tools/call");
match self
.registry
.dispatch(&call_params.name, call_params.arguments, &self.context)
.await
{
Ok(output) => JsonRpcResponse::success(
id,
ToolCallResult {
content: vec![ContentItem {
content_type: "text",
text: output.content().to_owned(),
}],
is_error: false,
},
),
Err(e) => {
JsonRpcResponse::success(
id,
ToolCallResult {
content: vec![ContentItem {
content_type: "text",
text: e.to_string(),
}],
is_error: true,
},
)
}
}
}
async fn handle_prompts_get(
&self,
id: Option<serde_json::Value>,
params: Option<serde_json::Value>,
) -> JsonRpcResponse {
let Some(p_val) = params else {
return JsonRpcResponse::error(
id,
protocol::INVALID_PARAMS,
"missing params for prompts/get",
);
};
let get_params: protocol::GetPromptParams = match serde_json::from_value(p_val) {
Ok(v) => v,
Err(e) => {
return JsonRpcResponse::error(
id,
protocol::INVALID_PARAMS,
format!("invalid params: {e}"),
);
}
};
let Some(prompt) = self.prompts.get(get_params.name.as_str()) else {
return JsonRpcResponse::error(
id,
protocol::INVALID_PARAMS,
format!("unknown prompt: {}", get_params.name),
);
};
let fut = prompt.render_erased(get_params.arguments);
match fut.await {
Ok(output) => {
let messages = output
.messages
.into_iter()
.map(|m| protocol::PromptMessage {
role: m.role.into_owned(),
content: protocol::PromptMessageContent::Text { text: m.content },
})
.collect();
let res = protocol::GetPromptResult {
description: None,
messages,
};
JsonRpcResponse::success(id, res)
}
Err(err) => JsonRpcResponse::error(id, protocol::INVALID_PARAMS, err.message),
}
}
async fn handle_resources_read(
&self,
id: Option<serde_json::Value>,
params: Option<serde_json::Value>,
) -> JsonRpcResponse {
let Some(p_val) = params else {
return JsonRpcResponse::error(
id,
protocol::INVALID_PARAMS,
"missing params for resources/read",
);
};
let read_params: protocol::ReadResourceParams = match serde_json::from_value(p_val) {
Ok(v) => v,
Err(e) => {
return JsonRpcResponse::error(
id,
protocol::INVALID_PARAMS,
format!("invalid params: {e}"),
);
}
};
let mut matched_fut = None;
for res in self.resources.iter() {
if let Some(fut) = res.read_erased(&read_params.uri) {
matched_fut = Some(fut);
break;
}
}
let Some(fut) = matched_fut else {
return JsonRpcResponse::error(
id,
protocol::INVALID_PARAMS,
format!("resource not found matching URI: {}", read_params.uri),
);
};
match fut.await {
Ok(output) => {
let res = protocol::ReadResourceResult {
contents: output.contents,
};
JsonRpcResponse::success(id, res)
}
Err(err) => JsonRpcResponse::error(id, protocol::INVALID_PARAMS, err.message),
}
}
}
fn build_tools_list_response(registry: &ToolRegistry) -> ToolsListResult {
let tools = registry
.definitions()
.iter()
.map(definition_to_mcp_schema)
.collect();
ToolsListResult { tools }
}
fn definition_to_mcp_schema(def: &ToolDefinition) -> McpToolSchema {
McpToolSchema {
name: def.name.clone(),
description: def.description.clone(),
input_schema: def.parameter_schema.clone(),
}
}
#[cfg(test)]
mod tests;