use std::{
fmt,
io::{self, BufRead, Write},
sync::Arc,
};
use llm_tool::{PromptRegistry, ResourceRegistry, 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<serde_json::Value>,
prompts: Arc<PromptRegistry>,
resources: Arc<ResourceRegistry>,
}
pub struct McpServerBuilder {
name: String,
version: String,
registry: ToolRegistry,
context: ToolContext,
prompts: PromptRegistry,
resources: ResourceRegistry,
}
impl McpServerBuilder {
#[must_use]
pub fn new(
name: impl Into<String>,
version: impl Into<String>,
registry: ToolRegistry,
) -> Self {
Self {
name: name.into(),
version: version.into(),
registry,
context: ToolContext::new(),
prompts: PromptRegistry::new(),
resources: ResourceRegistry::new(),
}
}
#[must_use]
pub fn with_context(mut self, context: ToolContext) -> Self {
self.context = context;
self
}
#[must_use]
pub fn with_prompt<P: llm_tool::RustPrompt + 'static>(mut self, prompt: P) -> Self {
self.prompts.register(prompt);
self
}
#[must_use]
pub fn with_resource<R: llm_tool::RustResource + 'static>(mut self, resource: R) -> Self {
self.resources.register(resource);
self
}
#[must_use]
pub fn build(self) -> McpServer {
let cached_tools_list = Arc::new(build_tools_list_value(&self.registry));
McpServer {
name: self.name,
version: self.version,
registry: Arc::new(self.registry),
context: Arc::new(self.context),
cached_tools_list,
prompts: Arc::new(self.prompts),
resources: Arc::new(self.resources),
}
}
}
impl McpServer {
#[must_use]
pub fn new(
name: impl Into<String>,
version: impl Into<String>,
registry: ToolRegistry,
) -> Self {
McpServerBuilder::new(name, version, registry).build()
}
#[must_use]
pub fn builder(
name: impl Into<String>,
version: impl Into<String>,
registry: ToolRegistry,
) -> McpServerBuilder {
McpServerBuilder::new(name, version, registry)
}
#[must_use]
pub fn with_context(mut self, context: ToolContext) -> Self {
self.context = Arc::new(context);
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<()> {
let mut out_buf: Vec<u8> = Vec::new();
for line_result in reader.lines() {
let line = line_result?;
if line.trim().is_empty() {
continue;
}
debug!(request = %line, "mcp request");
let Some(outcome) = rt.block_on(self.handle_message(&line)) else {
debug!("dropping notification response");
continue;
};
out_buf.clear();
outcome.write_json(&mut out_buf);
debug!(response = %String::from_utf8_lossy(&out_buf), "mcp response");
out_buf.push(b'\n');
writer.write_all(&out_buf)?;
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();
let mut out_buf: Vec<u8> = Vec::new();
while let Some(line) = lines.next_line().await? {
if line.trim().is_empty() {
continue;
}
debug!(request = %line, "mcp request");
let Some(outcome) = self.handle_message(&line).await else {
debug!("dropping notification response");
continue;
};
out_buf.clear();
outcome.write_json(&mut out_buf);
debug!(response = %String::from_utf8_lossy(&out_buf), "mcp response");
out_buf.push(b'\n');
writer.write_all(&out_buf).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() {
if let Err(e) = std::fs::remove_file(path) {
tracing::warn!(path = ?path, error = %e, "failed to remove stale socket file");
}
}
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(crate) 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<RpcOutcome> {
let trimmed = line.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.as_bytes().first() == Some(&b'[') {
return self.dispatch_batch(trimmed).await;
}
let response = self.handle_request(trimmed).await;
if response.id.is_none() {
None
} else {
Some(RpcOutcome::Single(response))
}
}
async fn dispatch_batch(&self, line: &str) -> Option<RpcOutcome> {
let val: serde_json::Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(e) => {
return Some(RpcOutcome::Single(JsonRpcResponse::error(
None,
protocol::PARSE_ERROR,
format!("invalid JSON: {e}"),
)));
}
};
let Some(arr) = val.as_array() else {
return Some(RpcOutcome::Single(JsonRpcResponse::error(
None,
protocol::INVALID_REQUEST,
"expected JSON array for batch request",
)));
};
if arr.is_empty() {
return Some(RpcOutcome::Single(JsonRpcResponse::error(
None,
protocol::INVALID_REQUEST,
"batch request array cannot be empty",
)));
}
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(resp);
}
}
if responses.is_empty() {
None
} else {
Some(RpcOutcome::Batch(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
.definitions()
.into_iter()
.map(|def| 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.definitions(),
};
JsonRpcResponse::success(id, list)
}
"resources/read" => self.handle_resources_read(id, request.params).await,
"prompts/list" => {
let list = protocol::PromptsListResult {
prompts: self.prompts.definitions(),
};
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 {
jsonrpc: JSONRPC_VERSION,
id,
result: Some((*self.cached_tools_list).clone()),
error: None,
}
}
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
{
Some(Ok(output)) => JsonRpcResponse::success(
id,
ToolCallResult {
content: vec![ContentItem {
content_type: "text",
text: output.content().to_owned(),
}],
is_error: false,
},
),
Some(Err(e)) => JsonRpcResponse::success(
id,
ToolCallResult {
content: vec![ContentItem {
content_type: "text",
text: e.to_string(),
}],
is_error: true,
},
),
None => JsonRpcResponse::success(
id,
ToolCallResult {
content: vec![ContentItem {
content_type: "text",
text: format!("Unknown tool: {}", call_params.name),
}],
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_result) = self
.prompts
.render(&get_params.name, get_params.arguments)
.await
else {
return JsonRpcResponse::error(
id,
protocol::INVALID_PARAMS,
format!("unknown prompt: {}", get_params.name),
);
};
match prompt_result {
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 Some(resource_result) = self.resources.read(&read_params.uri).await else {
return JsonRpcResponse::error(
id,
protocol::INVALID_PARAMS,
format!("resource not found matching URI: {}", read_params.uri),
);
};
match resource_result {
Ok(output) => {
let res = protocol::ReadResourceResult {
contents: output.contents,
};
JsonRpcResponse::success(id, res)
}
Err(err) => JsonRpcResponse::error(id, protocol::INVALID_PARAMS, err.message),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RpcOutcome {
Single(JsonRpcResponse),
Batch(Vec<JsonRpcResponse>),
}
impl serde::Serialize for RpcOutcome {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
RpcOutcome::Single(response) => response.serialize(serializer),
RpcOutcome::Batch(responses) => responses.serialize(serializer),
}
}
}
impl RpcOutcome {
#[must_use]
pub fn to_wire(&self) -> String {
serde_json::to_string(self).expect("MCP response must be JSON-serializable")
}
pub fn write_json(&self, buf: &mut Vec<u8>) {
serde_json::to_writer(buf, self).expect("MCP response must be JSON-serializable");
}
#[must_use]
pub fn is_batch(&self) -> bool {
matches!(self, RpcOutcome::Batch(_))
}
#[must_use]
pub fn into_responses(self) -> Vec<JsonRpcResponse> {
match self {
RpcOutcome::Single(response) => vec![response],
RpcOutcome::Batch(responses) => responses,
}
}
}
impl fmt::Display for RpcOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_wire())
}
}
fn build_tools_list_value(registry: &ToolRegistry) -> serde_json::Value {
let tools = registry
.definitions()
.iter()
.map(definition_to_mcp_schema)
.collect();
let list = ToolsListResult { tools };
serde_json::to_value(list).expect("tools/list schema must be JSON-serializable")
}
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;