mod json_rpc;
#[cfg(test)]
mod tests;
use std::sync::Arc;
use crate::app::App;
use crate::application::Application;
use crate::core::New;
use crate::header::Header;
use crate::mime_type::MimeType;
use crate::range::Range;
use crate::request::Request;
use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
use crate::server::ConnectionInfo;
const PROTOCOL_VERSION: &str = "2024-11-05";
#[derive(Clone, Debug)]
pub struct McpContent {
pub kind: &'static str,
pub text: String,
pub mime_type: Option<String>,
}
impl McpContent {
pub fn text(s: impl Into<String>) -> Self {
McpContent { kind: "text", text: s.into(), mime_type: None }
}
pub fn json(s: impl Into<String>) -> Self {
McpContent { kind: "text", text: s.into(), mime_type: Some("application/json".to_string()) }
}
fn to_content_json(&self) -> String {
let escaped = json_escape(&self.text);
format!(r#"{{"type":"{}","text":"{}"}}"#, self.kind, escaped)
}
fn mime(&self) -> &str {
self.mime_type.as_deref().unwrap_or("text/plain")
}
}
#[derive(Clone, Debug)]
pub struct PromptMessage {
pub role: &'static str,
pub content: McpContent,
}
impl PromptMessage {
pub fn user(text: impl Into<String>) -> Self {
PromptMessage { role: "user", content: McpContent::text(text) }
}
pub fn assistant(text: impl Into<String>) -> Self {
PromptMessage { role: "assistant", content: McpContent::text(text) }
}
fn to_json(&self) -> String {
format!(
r#"{{"role":"{}","content":{}}}"#,
self.role,
self.content.to_content_json(),
)
}
}
pub struct PromptArgDef {
pub name: String,
pub description: String,
pub required: bool,
}
impl PromptArgDef {
pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
PromptArgDef { name: name.into(), description: description.into(), required: true }
}
pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
PromptArgDef { name: name.into(), description: description.into(), required: false }
}
}
type ToolFn = Arc<dyn Fn(&str) -> Result<McpContent, String> + Send + Sync>;
type ResourceFn = Arc<dyn Fn(&str) -> Result<McpContent, String> + Send + Sync>;
type PromptFn = Arc<dyn Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync>;
struct ToolDef {
name: String,
description: String,
input_schema: String,
handler: ToolFn,
}
struct ResourceDef {
uri_template: String,
name: String,
description: String,
handler: ResourceFn,
}
struct PromptDef {
name: String,
description: String,
arguments: Vec<PromptArgDef>,
handler: PromptFn,
}
pub struct McpServer {
server_name: String,
server_version: String,
path: String,
tools: Vec<ToolDef>,
resources: Vec<ResourceDef>,
prompts: Vec<PromptDef>,
}
impl McpServer {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
McpServer {
server_name: name.into(),
server_version: version.into(),
path: "/mcp".to_string(),
tools: vec![],
resources: vec![],
prompts: vec![],
}
}
pub fn at(mut self, path: impl Into<String>) -> Self {
self.path = path.into();
self
}
pub fn tool<F>(mut self, name: &str, description: &str, input_schema: &str, handler: F) -> Self
where
F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
{
self.tools.push(ToolDef {
name: name.to_string(),
description: description.to_string(),
input_schema: input_schema.to_string(),
handler: Arc::new(handler),
});
self
}
pub fn resource<F>(mut self, uri_template: &str, name: &str, description: &str, handler: F) -> Self
where
F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
{
self.resources.push(ResourceDef {
uri_template: uri_template.to_string(),
name: name.to_string(),
description: description.to_string(),
handler: Arc::new(handler),
});
self
}
pub fn prompt<F>(mut self, name: &str, description: &str, handler: F) -> Self
where
F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
{
self.prompts.push(PromptDef {
name: name.to_string(),
description: description.to_string(),
arguments: vec![],
handler: Arc::new(handler),
});
self
}
pub fn prompt_with_args<F>(
mut self,
name: &str,
description: &str,
args: Vec<PromptArgDef>,
handler: F,
) -> Self
where
F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
{
self.prompts.push(PromptDef {
name: name.to_string(),
description: description.to_string(),
arguments: args,
handler: Arc::new(handler),
});
self
}
pub fn handle_request(&self, body: &str) -> Response {
let method = match json_rpc::extract_str(body, "method") {
Some(m) => m,
None => return rpc_error(None, json_rpc::INVALID_REQUEST, "Missing method"),
};
let id = json_rpc::extract_id(body);
if method == "notifications/initialized" || (id.is_none() && method != "ping") {
return no_content();
}
let result: Result<String, (i32, String)> = match method.as_str() {
"initialize" => self.do_initialize(),
"ping" => Ok("{}".to_string()),
"tools/list" => self.do_tools_list(),
"tools/call" => self.do_tools_call(body),
"resources/list" => self.do_resources_list(),
"resources/read" => self.do_resources_read(body),
"prompts/list" => self.do_prompts_list(),
"prompts/get" => self.do_prompts_get(body),
_ => Err((json_rpc::METHOD_NOT_FOUND, format!("Unknown method: {method}"))),
};
let id_str = id.as_deref().unwrap_or("null");
match result {
Ok(result_json) => json_response(&format!(
r#"{{"jsonrpc":"2.0","result":{result_json},"id":{id_str}}}"#
)),
Err((code, msg)) => {
let escaped = json_escape(&msg);
json_response(&format!(
r#"{{"jsonrpc":"2.0","error":{{"code":{code},"message":"{escaped}"}},"id":{id_str}}}"#
))
}
}
}
fn do_initialize(&self) -> Result<String, (i32, String)> {
let caps = format!(
r#"{{"tools":{{"listChanged":false}},"resources":{{"subscribe":false,"listChanged":false}},"prompts":{{"listChanged":false}}}}"#
);
Ok(format!(
r#"{{"protocolVersion":"{PROTOCOL_VERSION}","capabilities":{caps},"serverInfo":{{"name":"{}","version":"{}"}}}}"#,
json_escape(&self.server_name),
json_escape(&self.server_version),
))
}
fn do_tools_list(&self) -> Result<String, (i32, String)> {
let items: Vec<String> = self.tools.iter().map(|t| {
format!(
r#"{{"name":"{}","description":"{}","inputSchema":{}}}"#,
json_escape(&t.name),
json_escape(&t.description),
t.input_schema,
)
}).collect();
Ok(format!(r#"{{"tools":[{}]}}"#, items.join(",")))
}
fn do_tools_call(&self, body: &str) -> Result<String, (i32, String)> {
let params = json_rpc::extract_raw(body, "params")
.ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
let name = json_rpc::extract_str(¶ms, "name")
.ok_or((json_rpc::INVALID_PARAMS, "Missing tool name".to_string()))?;
let args = json_rpc::extract_raw(¶ms, "arguments")
.unwrap_or_else(|| "{}".to_string());
let tool = self.tools.iter().find(|t| t.name == name)
.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Unknown tool: {name}")))?;
match (tool.handler)(&args) {
Ok(c) => Ok(format!(
r#"{{"content":[{}],"isError":false}}"#,
c.to_content_json(),
)),
Err(e) => {
let escaped = json_escape(&e);
Ok(format!(
r#"{{"content":[{{"type":"text","text":"{escaped}"}}],"isError":true}}"#
))
}
}
}
fn do_resources_list(&self) -> Result<String, (i32, String)> {
let items: Vec<String> = self.resources.iter().map(|r| {
format!(
r#"{{"uri":"{}","name":"{}","description":"{}","mimeType":"text/plain"}}"#,
json_escape(&r.uri_template),
json_escape(&r.name),
json_escape(&r.description),
)
}).collect();
Ok(format!(r#"{{"resources":[{}]}}"#, items.join(",")))
}
fn do_resources_read(&self, body: &str) -> Result<String, (i32, String)> {
let params = json_rpc::extract_raw(body, "params")
.ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
let uri = json_rpc::extract_str(¶ms, "uri")
.ok_or((json_rpc::INVALID_PARAMS, "Missing uri".to_string()))?;
let resource = self.resources.iter().find(|r| uri_matches(&r.uri_template, &uri))
.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Resource not found: {uri}")))?;
match (resource.handler)(&uri) {
Ok(c) => {
let text_esc = json_escape(&c.text);
let uri_esc = json_escape(&uri);
Ok(format!(
r#"{{"contents":[{{"uri":"{uri_esc}","mimeType":"{}","text":"{text_esc}"}}]}}"#,
c.mime(),
))
}
Err(e) => Err((json_rpc::INVALID_PARAMS, e)),
}
}
fn do_prompts_list(&self) -> Result<String, (i32, String)> {
let items: Vec<String> = self.prompts.iter().map(|p| {
let arg_defs: Vec<String> = p.arguments.iter().map(|a| {
format!(
r#"{{"name":"{}","description":"{}","required":{}}}"#,
json_escape(&a.name),
json_escape(&a.description),
a.required,
)
}).collect();
format!(
r#"{{"name":"{}","description":"{}","arguments":[{}]}}"#,
json_escape(&p.name),
json_escape(&p.description),
arg_defs.join(","),
)
}).collect();
Ok(format!(r#"{{"prompts":[{}]}}"#, items.join(",")))
}
fn do_prompts_get(&self, body: &str) -> Result<String, (i32, String)> {
let params = json_rpc::extract_raw(body, "params")
.ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
let name = json_rpc::extract_str(¶ms, "name")
.ok_or((json_rpc::INVALID_PARAMS, "Missing prompt name".to_string()))?;
let args = json_rpc::extract_raw(¶ms, "arguments")
.unwrap_or_else(|| "{}".to_string());
let prompt = self.prompts.iter().find(|p| p.name == name)
.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Unknown prompt: {name}")))?;
match (prompt.handler)(&args) {
Ok(msgs) => {
let msg_jsons: Vec<String> = msgs.iter().map(|m| m.to_json()).collect();
Ok(format!(
r#"{{"description":"{}","messages":[{}]}}"#,
json_escape(&prompt.description),
msg_jsons.join(","),
))
}
Err(e) => Err((json_rpc::INVALID_PARAMS, e)),
}
}
}
impl Application for McpServer {
fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
if request.request_uri == self.path {
return Ok(match request.method.as_str() {
"POST" => {
let body = std::str::from_utf8(&request.body).unwrap_or("");
self.handle_request(body)
}
"OPTIONS" => {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
r.headers.push(Header {
name: "Allow".to_string(),
value: "POST, OPTIONS".to_string(),
});
r
}
_ => {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n405_method_not_allowed.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n405_method_not_allowed.reason_phrase.to_string();
r.headers.push(Header {
name: "Allow".to_string(),
value: "POST, OPTIONS".to_string(),
});
r.content_range_list = vec![Range::get_content_range(
b"MCP endpoint only accepts POST".to_vec(),
MimeType::TEXT_PLAIN.to_string(),
)];
r
}
});
}
App::new().execute(request, connection)
}
}
pub fn extract_arg(arguments: &str, name: &str) -> Option<String> {
json_rpc::extract_str(arguments, name)
}
fn json_response(body: &str) -> Response {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
r.content_range_list = vec![Range::get_content_range(
body.as_bytes().to_vec(),
MimeType::APPLICATION_JSON.to_string(),
)];
r
}
fn no_content() -> Response {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n202_accepted.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n202_accepted.reason_phrase.to_string();
r
}
fn rpc_error(id: Option<&str>, code: i32, message: &str) -> Response {
let id_str = id.unwrap_or("null");
let escaped = json_escape(message);
json_response(&format!(
r#"{{"jsonrpc":"2.0","error":{{"code":{code},"message":"{escaped}"}},"id":{id_str}}}"#
))
}
pub(crate) fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 4);
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => { let _ = std::fmt::Write::write_fmt(&mut out, format_args!("\\u{:04x}", c as u32)); }
c => out.push(c),
}
}
out
}
fn uri_matches(template: &str, uri: &str) -> bool {
match template.find('{') {
Some(pos) => uri.starts_with(&template[..pos]),
None => template == uri,
}
}