use crate::error::{ErrorCode, ToolError};
use crate::jsonrpc::{ErrorObject, Outgoing, RequestId};
use crate::protocol;
use crate::protocol::{
CacheScope, CallToolResult, DiscoverResult, Prompt, PromptMessage, PromptsListResult, Resource,
ResourceContents, ResourcesListResult, ResultType, ServerInfo, TextContent, Tool,
ToolAnnotations, ToolsListResult,
};
use crate::zod::{Issues, Path, Schema};
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
use std::collections::BTreeMap;
use std::panic::AssertUnwindSafe;
use std::sync::Arc;
use std::time::{Duration, Instant};
pub type ToolResult = Result<Value, ToolError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallStatus {
Success,
ProtocolError(i64),
ToolFailure(ErrorCode),
Notification,
}
#[derive(Debug, Clone)]
pub struct CallEvent<'a> {
pub method: &'a str,
pub name: Option<&'a str>,
pub duration: Duration,
pub status: CallStatus,
}
type ToolFn = dyn Fn(&Value) -> Result<Value, ToolFailure> + Send + Sync;
type ResourceFn = dyn Fn() -> Result<ResourceContents, String> + Send + Sync;
type PromptFn = dyn Fn(&Value) -> Result<Vec<PromptMessage>, String> + Send + Sync;
type CallHook = dyn Fn(&CallEvent<'_>) + Send + Sync;
static EMPTY_OBJECT: std::sync::OnceLock<Value> = std::sync::OnceLock::new();
fn empty_object() -> &'static Value {
EMPTY_OBJECT.get_or_init(|| json!({}))
}
pub const DEFAULT_LIST_TTL_MS: u64 = 60_000;
pub const MAX_INFLIGHT_HANDLERS: usize = 256;
pub const MAX_RESPONSE_BYTES: usize = 16 * 1024 * 1024;
const MAX_ECHO_CHARS: usize = 128;
const RESOURCE_FAILURE: i64 = -32000;
fn sanitize_echo(raw: &str) -> String {
let mut out = String::with_capacity(raw.len().min(MAX_ECHO_CHARS + 4));
for (idx, ch) in raw.chars().enumerate() {
if idx >= MAX_ECHO_CHARS {
out.push('…');
break;
}
if ch.is_control() {
out.push('\u{FFFD}');
} else {
out.push(ch);
}
}
out
}
struct HandlerSlotGuard(Arc<std::sync::atomic::AtomicUsize>);
impl Drop for HandlerSlotGuard {
fn drop(&mut self) {
self.0.fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
}
}
enum Outcome<T> {
Done(T),
Panicked,
TimedOut,
Overloaded,
}
fn run_with_timeout<T: Send + 'static>(
timeout: Duration,
inflight: &Arc<std::sync::atomic::AtomicUsize>,
max_inflight: usize,
f: impl FnOnce() -> T + Send + 'static,
) -> Outcome<T> {
let prev = inflight.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
if prev >= max_inflight {
inflight.fetch_sub(1, std::sync::atomic::Ordering::AcqRel);
return Outcome::Overloaded;
}
let guard = HandlerSlotGuard(Arc::clone(inflight));
let (tx, rx) = std::sync::mpsc::channel();
let spawned = std::thread::Builder::new()
.name("litemcp-handler".into())
.spawn(move || {
let _guard = guard;
let _ = tx.send(std::panic::catch_unwind(AssertUnwindSafe(f)));
});
if spawned.is_err() {
return Outcome::Panicked;
}
match rx.recv_timeout(timeout) {
Ok(Ok(value)) => Outcome::Done(value),
Ok(Err(_)) => Outcome::Panicked,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Outcome::TimedOut,
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Outcome::Panicked,
}
}
#[derive(Debug)]
enum ToolFailure {
Tool(ToolError),
Protocol(i64, String),
}
#[derive(Clone)]
struct ToolDef {
description: Option<String>,
schema: Option<Arc<dyn Schema>>,
output_schema: Option<Arc<dyn Schema>>,
output_object_rooted: bool,
annotations: Option<ToolAnnotations>,
f: Arc<ToolFn>,
}
impl ToolDef {
fn sync_output_shape(&mut self) {
self.output_object_rooted = self
.output_schema
.as_ref()
.is_some_and(|schema| output_is_object_rooted(schema.as_ref()));
}
}
#[derive(Clone)]
struct ResourceDef {
name: String,
description: Option<String>,
f: Arc<ResourceFn>,
}
#[derive(Clone)]
struct PromptDef {
description: Option<String>,
schema: Option<Arc<dyn Schema>>,
f: Arc<PromptFn>,
}
#[derive(Debug, Clone, Default)]
pub struct McpHeaders {
pub method: Option<String>,
pub name: Option<String>,
}
#[derive(Clone)]
pub struct ServerCore {
name: String,
version: String,
meta: Value,
tools: Arc<BTreeMap<String, ToolDef>>,
resources: Arc<BTreeMap<String, ResourceDef>>,
prompts: Arc<BTreeMap<String, PromptDef>>,
tool_timeout: Option<Duration>,
max_inflight_handlers: usize,
inflight_handlers: Arc<std::sync::atomic::AtomicUsize>,
max_response_bytes: usize,
hooks: Arc<Vec<Arc<CallHook>>>,
tools_list_cache: std::sync::OnceLock<Value>,
resources_list_cache: std::sync::OnceLock<Value>,
prompts_list_cache: std::sync::OnceLock<Value>,
discover_cache: std::sync::OnceLock<Value>,
initialize_cache: std::sync::OnceLock<Value>,
ping_cache: std::sync::OnceLock<Value>,
}
#[derive(Serialize)]
struct CallToolResultRef<'a> {
#[serde(rename = "resultType")]
result_type: ResultType,
content: &'a [TextContent],
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
is_error: Option<bool>,
#[serde(rename = "structuredContent", skip_serializing_if = "Option::is_none")]
structured_content: Option<&'a Value>,
#[serde(rename = "_meta")]
meta: &'a Value,
}
#[derive(Serialize)]
struct ReadResourceResultRef<'a> {
#[serde(rename = "resultType")]
result_type: ResultType,
#[serde(rename = "ttlMs")]
ttl_ms: u64,
#[serde(rename = "cacheScope")]
cache_scope: CacheScope,
contents: &'a [ResourceContents],
#[serde(rename = "_meta")]
meta: &'a Value,
}
#[derive(Serialize)]
struct GetPromptResultRef<'a> {
#[serde(rename = "resultType")]
result_type: ResultType,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<&'a str>,
messages: &'a [PromptMessage],
#[serde(rename = "_meta")]
meta: &'a Value,
}
impl ServerCore {
pub fn dispatch(&self, body: &[u8]) -> Option<Vec<u8>> {
self.dispatch_with_headers(body, None)
}
pub fn dispatch_with_headers(
&self,
body: &[u8],
headers: Option<&McpHeaders>,
) -> Option<Vec<u8>> {
let incoming = match crate::jsonrpc::parse_incoming(body) {
Ok(incoming) => incoming,
Err(crate::jsonrpc::ParseFail::Syntax) => {
return Some(Outgoing::parse_error().to_vec());
}
Err(fail) => {
let id = fail.id().cloned();
let message = match fail {
crate::jsonrpc::ParseFail::Syntax => String::new(),
crate::jsonrpc::ParseFail::Invalid { message, .. } => message,
};
return Some(
Outgoing::invalid_request(id, format!("invalid request: {message}")).to_vec(),
);
}
};
let started = Instant::now();
let id = incoming
.id
.as_ref()
.map(RequestId::to_value)
.unwrap_or(Value::Null);
let params = incoming.params.as_ref().unwrap_or_else(|| empty_object());
let name = requested_name(&incoming.method, params);
if let Some(headers) = headers {
if let Some(header_method) = &headers.method {
if header_method.is_empty() || header_method != &incoming.method {
return Some(self.protocol_failure(
&id,
&incoming,
name,
started,
ErrorObject::with_data(
ErrorObject::HEADER_MISMATCH,
"Mcp-Method header does not match the request body",
json!({
"header": header_method,
"body": incoming.method,
}),
),
));
}
}
if let Some(header_name) = &headers.name {
let body_name = params.get("name").and_then(Value::as_str).unwrap_or("");
let expects_name = matches!(
incoming.method.as_str(),
protocol::method::TOOLS_CALL | protocol::method::PROMPTS_GET
);
if !expects_name
|| header_name.is_empty()
|| body_name.is_empty()
|| header_name != body_name
{
return Some(self.protocol_failure(
&id,
&incoming,
name,
started,
ErrorObject::with_data(
ErrorObject::HEADER_MISMATCH,
"Mcp-Name header does not match the request body",
json!({
"header": header_name,
"body": body_name,
}),
),
));
}
}
}
if incoming.is_notification() {
if protocol::is_action(&incoming.method) {
return Some(self.protocol_failure(
&Value::Null,
&incoming,
name,
started,
ErrorObject::new(
ErrorObject::INVALID_REQUEST,
format!(
"notification rejected: `{}` executes server-side code — \
a request (with `id`) is required to obtain the result",
sanitize_echo(&incoming.method)
),
),
));
}
self.emit_call(
&incoming.method,
None,
started.elapsed(),
CallStatus::Notification,
);
return None;
}
if let Some(err) = self.check_protocol_version(params) {
return Some(self.protocol_failure(&id, &incoming, name, started, err));
}
let (mut reply, mut status) = match incoming.method.as_str() {
protocol::method::DISCOVER => (
Outgoing::success_vec(&id, self.discover_json()),
CallStatus::Success,
),
protocol::method::INITIALIZE => (
Outgoing::success_vec(&id, self.initialize_json()),
CallStatus::Success,
),
protocol::method::PING => (
Outgoing::success_vec(&id, self.ping_json()),
CallStatus::Success,
),
protocol::method::TOOLS_LIST => (
Outgoing::success_vec(&id, self.tools_list_json()),
CallStatus::Success,
),
protocol::method::TOOLS_CALL => self.call_tool(&id, params),
protocol::method::RESOURCES_LIST => (
Outgoing::success_vec(&id, self.resources_list_json()),
CallStatus::Success,
),
protocol::method::RESOURCES_READ => self.read_resource(&id, params),
protocol::method::PROMPTS_LIST => (
Outgoing::success_vec(&id, self.prompts_list_json()),
CallStatus::Success,
),
protocol::method::PROMPTS_GET => self.get_prompt(&id, params),
other => (
Outgoing::failure(
id.clone(),
ErrorObject::new(
ErrorObject::METHOD_NOT_FOUND,
format!("Unknown method: {}", sanitize_echo(other)),
),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::METHOD_NOT_FOUND),
),
};
if self.max_response_bytes > 0 && reply.len() > self.max_response_bytes {
let actual = reply.len();
reply = Outgoing::failure(
id,
ErrorObject::new(
ErrorObject::INTERNAL_ERROR,
format!(
"response size ({actual} bytes) exceeds maximum ({} bytes)",
self.max_response_bytes
),
),
)
.to_vec();
status = CallStatus::ProtocolError(ErrorObject::INTERNAL_ERROR);
}
self.emit_call(&incoming.method, name, started.elapsed(), status);
Some(reply)
}
fn protocol_failure(
&self,
id: &Value,
incoming: &crate::jsonrpc::Incoming,
name: Option<&str>,
started: Instant,
error: ErrorObject,
) -> Vec<u8> {
let code = error.code;
let outgoing = Outgoing::failure(id.clone(), error);
self.emit_call(
&incoming.method,
name,
started.elapsed(),
CallStatus::ProtocolError(code),
);
outgoing.to_vec()
}
fn emit_call(&self, method: &str, name: Option<&str>, duration: Duration, status: CallStatus) {
if self.hooks.is_empty() {
return;
}
let event = CallEvent {
method,
name,
duration,
status,
};
for hook in self.hooks.iter() {
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| hook(&event)));
}
}
fn check_protocol_version(&self, params: &Value) -> Option<ErrorObject> {
let requested = params
.get("_meta")
.and_then(|meta| meta.get(protocol::META_PROTOCOL_VERSION))
.and_then(Value::as_str)?;
if protocol::SUPPORTED_VERSIONS.contains(&requested) {
return None;
}
let safe = sanitize_echo(requested);
Some(ErrorObject::with_data(
ErrorObject::UNSUPPORTED_PROTOCOL_VERSION,
format!("Unsupported protocol version: {safe}"),
json!({
"supported": protocol::SUPPORTED_VERSIONS,
"requested": safe,
}),
))
}
fn meta(&self) -> Value {
self.meta.clone()
}
fn capabilities(&self) -> Value {
let mut capabilities = json!({ "tools": {} });
if !self.resources.is_empty() {
capabilities["resources"] = json!({});
}
if !self.prompts.is_empty() {
capabilities["prompts"] = json!({});
}
capabilities
}
fn discover_json(&self) -> &Value {
self.discover_cache
.get_or_init(|| serde_json::to_value(self.discover()).unwrap_or(Value::Null))
}
fn initialize_json(&self) -> &Value {
self.initialize_cache.get_or_init(|| {
json!({
"resultType": "complete",
"protocolVersion": protocol::PROTOCOL_VERSION,
"capabilities": self.capabilities(),
"serverInfo": ServerInfo {
name: self.name.clone(),
version: self.version.clone(),
},
"_meta": self.meta(),
})
})
}
fn ping_json(&self) -> &Value {
self.ping_cache.get_or_init(|| {
json!({
"resultType": "complete",
"_meta": self.meta(),
})
})
}
fn tools_list_json(&self) -> &Value {
self.tools_list_cache.get_or_init(|| {
serde_json::to_value(self.list_tools()).unwrap_or_else(|_| json!({ "tools": [] }))
})
}
fn resources_list_json(&self) -> &Value {
self.resources_list_cache.get_or_init(|| {
serde_json::to_value(self.list_resources())
.unwrap_or_else(|_| json!({ "resources": [] }))
})
}
fn prompts_list_json(&self) -> &Value {
self.prompts_list_cache.get_or_init(|| {
serde_json::to_value(self.list_prompts()).unwrap_or_else(|_| json!({ "prompts": [] }))
})
}
fn discover(&self) -> DiscoverResult {
DiscoverResult {
result_type: ResultType::Complete,
supported_versions: protocol::SUPPORTED_VERSIONS.to_vec(),
capabilities: self.capabilities(),
meta: self.meta(),
}
}
fn list_tools(&self) -> ToolsListResult {
let tools = self
.tools
.iter()
.map(|(name, def)| Tool {
name: name.clone(),
description: def.description.clone(),
annotations: def.annotations.clone(),
input_schema: Some(
def.schema
.as_ref()
.map(|schema| schema.json())
.unwrap_or_else(|| json!({ "type": "object" })),
),
output_schema: def.output_schema.as_ref().map(|schema| schema.json()),
})
.collect();
ToolsListResult {
result_type: ResultType::Complete,
ttl_ms: DEFAULT_LIST_TTL_MS,
cache_scope: CacheScope::Private,
tools,
meta: self.meta(),
}
}
fn list_resources(&self) -> ResourcesListResult {
let resources = self
.resources
.iter()
.map(|(uri, def)| Resource {
uri: uri.clone(),
name: def.name.clone(),
description: def.description.clone(),
mime_type: None,
})
.collect();
ResourcesListResult {
result_type: ResultType::Complete,
ttl_ms: DEFAULT_LIST_TTL_MS,
cache_scope: CacheScope::Private,
resources,
meta: Some(self.meta()),
}
}
fn list_prompts(&self) -> PromptsListResult {
let prompts = self
.prompts
.iter()
.map(|(name, def)| Prompt {
name: name.clone(),
description: def.description.clone(),
arguments: def
.schema
.as_ref()
.and_then(|schema| derive_prompt_arguments(&schema.json())),
})
.collect();
PromptsListResult {
result_type: ResultType::Complete,
ttl_ms: DEFAULT_LIST_TTL_MS,
cache_scope: CacheScope::Private,
prompts,
meta: Some(self.meta()),
}
}
fn read_resource(&self, id: &Value, params: &Value) -> (Vec<u8>, CallStatus) {
let Some(uri) = params.get("uri").and_then(Value::as_str) else {
return (
Outgoing::failure(
id.clone(),
ErrorObject::new(ErrorObject::INVALID_PARAMS, "missing params.uri"),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::INVALID_PARAMS),
);
};
let Some(def) = self.resources.get(uri) else {
return (
Outgoing::failure(
id.clone(),
ErrorObject::new(
ErrorObject::INVALID_PARAMS,
format!("Unknown resource: {}", sanitize_echo(uri)),
),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::INVALID_PARAMS),
);
};
let handler = Arc::clone(&def.f);
let contents = match self.run_bounded(move || handler()) {
Ok(contents) => contents,
Err(failure) => {
let code = failure.code;
return (
Outgoing::failure(id.clone(), failure).to_vec(),
CallStatus::ProtocolError(code),
);
}
};
match contents {
Ok(contents) => (
Outgoing::success_vec(
id,
&ReadResourceResultRef {
result_type: ResultType::Complete,
ttl_ms: DEFAULT_LIST_TTL_MS,
cache_scope: CacheScope::Private,
contents: std::slice::from_ref(&contents),
meta: &self.meta,
},
),
CallStatus::Success,
),
Err(message) => (
Outgoing::failure(
id.clone(),
ErrorObject::new(RESOURCE_FAILURE, format!("Resource read failed: {message}")),
)
.to_vec(),
CallStatus::ProtocolError(RESOURCE_FAILURE),
),
}
}
fn get_prompt(&self, id: &Value, params: &Value) -> (Vec<u8>, CallStatus) {
let Some(name) = params.get("name").and_then(Value::as_str) else {
return (
Outgoing::failure(
id.clone(),
ErrorObject::new(ErrorObject::INVALID_PARAMS, "missing params.name"),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::INVALID_PARAMS),
);
};
let Some(def) = self.prompts.get(name) else {
return (
Outgoing::failure(
id.clone(),
ErrorObject::new(
ErrorObject::INVALID_PARAMS,
format!("Unknown prompt: {}", sanitize_echo(name)),
),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::INVALID_PARAMS),
);
};
let arguments = match params.get("arguments") {
None => empty_object(),
Some(value) if value.is_object() => value,
Some(_) => {
return (
Outgoing::failure(
id.clone(),
ErrorObject::new(
ErrorObject::INVALID_PARAMS,
"params.arguments must be an object",
),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::INVALID_PARAMS),
);
}
};
let mut injected_arguments;
let arguments = match &def.schema {
Some(schema) if schema.has_defaults() => {
injected_arguments = arguments.clone();
schema.apply_defaults(&mut injected_arguments);
&injected_arguments
}
_ => arguments,
};
if let Some(schema) = &def.schema {
let mut issues = Issues::new();
let mut path = Path::root("arguments");
schema.validate(&mut path, arguments, &mut issues);
if !issues.is_empty() {
let rendered = issues.render();
return (
Outgoing::failure(
id.clone(),
ErrorObject::with_data(
ErrorObject::INVALID_PARAMS,
format!("Invalid arguments: {}", rendered.join(" ; ")),
json!({ "issues": rendered }),
),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::INVALID_PARAMS),
);
}
}
let handler = Arc::clone(&def.f);
let arguments = arguments.clone();
let messages = match self.run_bounded(move || handler(&arguments)) {
Ok(messages) => messages,
Err(failure) => {
let code = failure.code;
return (
Outgoing::failure(id.clone(), failure).to_vec(),
CallStatus::ProtocolError(code),
);
}
};
match messages {
Ok(messages) => (
Outgoing::success_vec(
id,
&GetPromptResultRef {
result_type: ResultType::Complete,
description: def.description.as_deref(),
messages: &messages,
meta: &self.meta,
},
),
CallStatus::Success,
),
Err(message) => (
Outgoing::failure(
id.clone(),
ErrorObject::new(RESOURCE_FAILURE, format!("Prompt failed: {message}")),
)
.to_vec(),
CallStatus::ProtocolError(RESOURCE_FAILURE),
),
}
}
fn run_bounded<T: Send + 'static>(
&self,
f: impl FnOnce() -> Result<T, String> + Send + 'static,
) -> Result<Result<T, String>, ErrorObject> {
match self.tool_timeout {
None => match std::panic::catch_unwind(AssertUnwindSafe(f)) {
Ok(result) => Ok(result),
Err(_) => Err(ErrorObject::new(
ErrorObject::INTERNAL_ERROR,
"internal error (handler panicked)",
)),
},
Some(timeout) => match run_with_timeout(
timeout,
&self.inflight_handlers,
self.max_inflight_handlers,
f,
) {
Outcome::Done(result) => Ok(result),
Outcome::Panicked => Err(ErrorObject::new(
ErrorObject::INTERNAL_ERROR,
"internal error (handler panicked)",
)),
Outcome::TimedOut => Err(ErrorObject::new(
RESOURCE_FAILURE,
format!("execution timed out after {} ms", timeout.as_millis()),
)),
Outcome::Overloaded => Err(ErrorObject::new(
RESOURCE_FAILURE,
"too many concurrent handlers in flight",
)),
},
}
}
fn call_tool(&self, id: &Value, params: &Value) -> (Vec<u8>, CallStatus) {
let Some(name) = params.get("name").and_then(Value::as_str) else {
return (
Outgoing::failure(
id.clone(),
ErrorObject::new(ErrorObject::INVALID_PARAMS, "missing params.name"),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::INVALID_PARAMS),
);
};
let Some(tool) = self.tools.get(name) else {
return (
Outgoing::failure(
id.clone(),
ErrorObject::new(
ErrorObject::INVALID_PARAMS,
format!("Unknown tool: {}", sanitize_echo(name)),
),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::INVALID_PARAMS),
);
};
let arguments = match params.get("arguments") {
None => empty_object(),
Some(value) if value.is_object() => value,
Some(_) => {
return (
Outgoing::failure(
id.clone(),
ErrorObject::new(
ErrorObject::INVALID_PARAMS,
"params.arguments must be an object",
),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::INVALID_PARAMS),
);
}
};
let mut injected_arguments;
let arguments = match &tool.schema {
Some(schema) if schema.has_defaults() => {
injected_arguments = arguments.clone();
schema.apply_defaults(&mut injected_arguments);
&injected_arguments
}
_ => arguments,
};
if let Some(schema) = &tool.schema {
let mut issues = Issues::new();
let mut path = Path::root("arguments");
schema.validate(&mut path, arguments, &mut issues);
if !issues.is_empty() {
let rendered = issues.render();
return (
Outgoing::failure(
id.clone(),
ErrorObject::with_data(
ErrorObject::INVALID_PARAMS,
format!("Invalid arguments: {}", rendered.join(" ; ")),
json!({ "issues": rendered }),
),
)
.to_vec(),
CallStatus::ProtocolError(ErrorObject::INVALID_PARAMS),
);
}
}
let execution = match self.tool_timeout {
None => match std::panic::catch_unwind(AssertUnwindSafe(|| (tool.f)(arguments))) {
Ok(result) => result,
Err(_) => Err(ToolFailure::Protocol(
ErrorObject::INTERNAL_ERROR,
"internal error (handler panicked)".to_string(),
)),
},
Some(timeout) => {
let owned = arguments.clone();
let handler = Arc::clone(&tool.f);
match run_with_timeout(
timeout,
&self.inflight_handlers,
self.max_inflight_handlers,
move || handler(&owned),
) {
Outcome::Done(result) => result,
Outcome::Panicked => Err(ToolFailure::Protocol(
ErrorObject::INTERNAL_ERROR,
"internal error (handler panicked)".to_string(),
)),
Outcome::TimedOut => Err(ToolFailure::Tool(ToolError::timeout(format!(
"tool execution timed out after {} ms",
timeout.as_millis()
)))),
Outcome::Overloaded => Err(ToolFailure::Tool(ToolError::rate_limited(
"too many concurrent handlers in flight",
))),
}
}
};
let (result, status) = match execution {
Ok(value) => {
match &tool.output_schema {
None => (
CallToolResult::from(Ok::<Value, ToolError>(value)),
CallStatus::Success,
),
Some(output) => {
let structured = if tool.output_object_rooted && value.is_object() {
value
} else if tool.output_object_rooted {
serde_json::json!({ "result": value })
} else {
value
};
let mut issues = Issues::new();
let mut path = Path::root("output");
output.validate(&mut path, &structured, &mut issues);
if issues.is_empty() {
(
CallToolResult::from_structured(structured),
CallStatus::Success,
)
} else {
let rendered = issues.render().join(" ; ");
let error = ToolError::internal(format!(
"structured output does not conform to the declared outputSchema: {rendered}"
));
let code = error.code;
(
CallToolResult::from(Err::<Value, ToolError>(error)),
CallStatus::ToolFailure(code),
)
}
}
}
}
Err(ToolFailure::Tool(error)) => {
let code = error.code;
(
CallToolResult::from(Err::<Value, ToolError>(error)),
CallStatus::ToolFailure(code),
)
}
Err(ToolFailure::Protocol(code, message)) => {
return (
Outgoing::failure(id.clone(), ErrorObject::new(code, message)).to_vec(),
CallStatus::ProtocolError(code),
);
}
};
(
Outgoing::success_vec(
id,
&CallToolResultRef {
result_type: result.result_type,
content: &result.content,
is_error: result.is_error,
structured_content: result.structured_content.as_ref(),
meta: &self.meta,
},
),
status,
)
}
}
fn derive_prompt_arguments(schema_json: &Value) -> Option<Vec<crate::protocol::PromptArgument>> {
let properties = schema_json.get("properties")?.as_object()?;
let required: Vec<&str> = schema_json
.get("required")
.and_then(Value::as_array)
.map(|items| items.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
let arguments = properties
.iter()
.map(|(name, prop)| crate::protocol::PromptArgument {
name: name.clone(),
description: prop
.get("description")
.and_then(Value::as_str)
.map(str::to_string),
required: Some(required.contains(&name.as_str())),
})
.collect();
Some(arguments)
}
fn requested_name<'a>(method: &str, params: &'a Value) -> Option<&'a str> {
let key = if method == protocol::method::RESOURCES_READ {
"uri"
} else {
"name"
};
params.get(key).and_then(Value::as_str)
}
fn output_is_object_rooted(schema: &dyn Schema) -> bool {
let json = schema.json();
let Some(object) = json.as_object() else {
return false;
};
match object.get("type") {
Some(Value::String(kind)) => kind == "object",
None => object.contains_key("properties") || object.contains_key("required"),
Some(_) => false,
}
}
#[derive(Clone)]
pub struct ServerBuilder {
name: String,
version: String,
route: String,
allow_origins: Vec<String>,
read_timeout: Duration,
max_connections: usize,
tools: BTreeMap<String, ToolDef>,
connection_ttl: Duration,
tool_timeout: Option<Duration>,
annotations: BTreeMap<String, ToolAnnotations>,
output_schemas: BTreeMap<String, Arc<dyn Schema>>,
resources: BTreeMap<String, ResourceDef>,
prompts: BTreeMap<String, PromptDef>,
hooks: Vec<Arc<CallHook>>,
}
impl ServerBuilder {
#[must_use]
pub fn new(name: &str, version: &str) -> Self {
Self {
name: name.to_string(),
version: version.to_string(),
route: "/mcp".to_string(),
allow_origins: Vec::new(),
read_timeout: crate::http::READ_TIMEOUT,
max_connections: crate::http::MAX_CONNECTIONS,
tools: BTreeMap::new(),
annotations: BTreeMap::new(),
output_schemas: BTreeMap::new(),
resources: BTreeMap::new(),
prompts: BTreeMap::new(),
connection_ttl: crate::http::CONNECTION_TTL,
tool_timeout: None,
hooks: Vec::new(),
}
}
#[must_use]
pub fn on_call<F>(mut self, f: F) -> Self
where
F: Fn(&CallEvent<'_>) + Send + Sync + 'static,
{
self.hooks.push(Arc::new(f));
self
}
#[must_use]
pub fn connection_ttl(mut self, ttl: Duration) -> Self {
self.connection_ttl = ttl;
self
}
#[must_use]
pub fn read_timeout(mut self, timeout: Duration) -> Self {
self.read_timeout = timeout;
self
}
#[must_use]
pub fn max_connections(mut self, max: usize) -> Self {
self.max_connections = max;
self
}
#[must_use]
pub fn route(mut self, route: &str) -> Self {
self.route = route.to_string();
self
}
#[must_use]
pub fn allow_origins(mut self, origins: &[&str]) -> Self {
self.allow_origins = origins.iter().map(|o| (*o).to_string()).collect();
self
}
#[must_use]
pub fn tool_timeout(mut self, timeout: Duration) -> Self {
self.tool_timeout = Some(timeout);
self
}
#[must_use]
pub fn output_schema<S: Schema + 'static>(mut self, tool: &str, schema: S) -> Self {
let schema = Arc::new(schema);
if let Some(def) = self.tools.get_mut(tool) {
def.output_schema = Some(schema);
def.sync_output_shape();
} else {
self.output_schemas.insert(tool.to_string(), schema);
}
self
}
#[must_use]
pub fn resource<F>(mut self, uri: &str, name: &str, description: &str, f: F) -> Self
where
F: Fn() -> Result<ResourceContents, String> + Send + Sync + 'static,
{
self.resources.insert(
uri.to_string(),
ResourceDef {
name: name.to_string(),
description: Some(description.to_string()),
f: Arc::new(f),
},
);
self
}
#[must_use]
pub fn prompt<F>(mut self, name: &str, description: &str, f: F) -> Self
where
F: Fn(&Value) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
{
self.prompts.insert(
name.to_string(),
PromptDef {
description: Some(description.to_string()),
schema: None,
f: Arc::new(f),
},
);
self
}
pub fn prompt_typed<S, F>(mut self, name: &str, description: &str, schema: S, f: F) -> Self
where
S: Schema + 'static,
F: Fn(&Value) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
{
self.prompts.insert(
name.to_string(),
PromptDef {
description: Some(description.to_string()),
schema: Some(Arc::new(schema)),
f: Arc::new(f),
},
);
self
}
#[must_use]
pub fn with_tool(self, register: impl FnOnce(Self) -> Self) -> Self {
register(self)
}
fn take_output_and_annotations(
&mut self,
name: &str,
) -> (Option<Arc<dyn Schema>>, Option<ToolAnnotations>) {
let previous_output = self
.tools
.get(name)
.and_then(|def| def.output_schema.clone());
let previous_annotations = self.tools.get(name).and_then(|def| def.annotations.clone());
let output_schema = self.output_schemas.remove(name).or(previous_output);
let annotations = self.annotations.remove(name).or(previous_annotations);
(output_schema, annotations)
}
pub fn tool<F>(mut self, name: &str, description: &str, f: F) -> Self
where
F: Fn(&Value) -> ToolResult + Send + Sync + 'static,
{
let wrapped = move |args: &Value| f(args).map_err(ToolFailure::Tool);
let (output_schema, annotations) = self.take_output_and_annotations(name);
let mut def = ToolDef {
description: Some(description.to_string()),
schema: None,
output_schema,
output_object_rooted: false,
annotations,
f: Arc::new(wrapped),
};
def.sync_output_shape();
self.tools.insert(name.to_string(), def);
self
}
pub fn tool_typed<S, F>(mut self, name: &str, description: &str, schema: S, f: F) -> Self
where
S: Schema + 'static,
F: Fn(&Value) -> ToolResult + Send + Sync + 'static,
{
let wrapped = move |args: &Value| f(args).map_err(ToolFailure::Tool);
let (output_schema, annotations) = self.take_output_and_annotations(name);
let mut def = ToolDef {
description: Some(description.to_string()),
schema: Some(Arc::new(schema)),
output_schema,
output_object_rooted: false,
annotations,
f: Arc::new(wrapped),
};
def.sync_output_shape();
self.tools.insert(name.to_string(), def);
self
}
pub fn tool_parse<S, T, F>(mut self, name: &str, description: &str, schema: S, f: F) -> Self
where
S: Schema + 'static,
T: DeserializeOwned,
F: Fn(T) -> ToolResult + Send + Sync + 'static,
{
let handler = Arc::new(move |value: &Value| match serde_json::to_vec(value) {
Err(error) => Err(ToolFailure::Protocol(
ErrorObject::INVALID_PARAMS,
format!("unreadable arguments: {error}"),
)),
Ok(buffer) => match serde_json::from_slice::<T>(&buffer) {
Ok(parsed) => f(parsed).map_err(ToolFailure::Tool),
Err(error) => Err(ToolFailure::Protocol(
ErrorObject::INVALID_PARAMS,
format!("arguments do not match the expected type: {error}"),
)),
},
});
let (output_schema, annotations) = self.take_output_and_annotations(name);
let mut def = ToolDef {
description: Some(description.to_string()),
schema: Some(Arc::new(schema)),
output_schema,
output_object_rooted: false,
annotations,
f: handler,
};
def.sync_output_shape();
self.tools.insert(name.to_string(), def);
self
}
#[must_use]
pub fn annotate(mut self, tool: &str, annotations: ToolAnnotations) -> Self {
if let Some(def) = self.tools.get_mut(tool) {
def.annotations = Some(annotations);
} else {
self.annotations.insert(tool.to_string(), annotations);
}
self
}
#[must_use]
pub fn core(&self) -> ServerCore {
let mut tools = self.tools.clone();
for (name, def) in tools.iter_mut() {
if let Some(annotations) = self.annotations.get(name) {
def.annotations = Some(annotations.clone());
}
if let Some(output) = self.output_schemas.get(name) {
def.output_schema = Some(Arc::clone(output));
}
def.sync_output_shape();
}
let meta = json!({
protocol::META_SERVER_INFO: ServerInfo {
name: self.name.clone(),
version: self.version.clone(),
}
});
ServerCore {
name: self.name.clone(),
version: self.version.clone(),
meta,
tools: Arc::new(tools),
resources: Arc::new(self.resources.clone()),
prompts: Arc::new(self.prompts.clone()),
tool_timeout: self.tool_timeout,
max_inflight_handlers: MAX_INFLIGHT_HANDLERS,
inflight_handlers: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
max_response_bytes: MAX_RESPONSE_BYTES,
hooks: Arc::new(self.hooks.clone()),
tools_list_cache: std::sync::OnceLock::new(),
resources_list_cache: std::sync::OnceLock::new(),
prompts_list_cache: std::sync::OnceLock::new(),
discover_cache: std::sync::OnceLock::new(),
initialize_cache: std::sync::OnceLock::new(),
ping_cache: std::sync::OnceLock::new(),
}
}
pub fn bind(self, addr: &str) -> std::io::Result<crate::http::BoundServer> {
if !self.route.starts_with('/')
|| self.route.contains(['?', '#'])
|| self
.route
.chars()
.any(|c| c.is_whitespace() || c.is_control() || !c.is_ascii())
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"route invalide {:?} : elle doit commencer par '/', être en ASCII visible sans espace et ne pas contenir '?' ni '#'",
self.route
),
));
}
if self.route == crate::http::HEALTH_ROUTE {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!(
"route invalide {:?} : elle masque la sonde de vie {}",
self.route,
crate::http::HEALTH_ROUTE
),
));
}
let listener = std::net::TcpListener::bind(addr)?;
let mut tools = self.tools;
for (name, def) in tools.iter_mut() {
if let Some(annotations) = self.annotations.get(name) {
def.annotations = Some(annotations.clone());
}
if let Some(output) = self.output_schemas.get(name) {
def.output_schema = Some(Arc::clone(output));
}
def.sync_output_shape();
}
let meta = json!({
protocol::META_SERVER_INFO: ServerInfo {
name: self.name.clone(),
version: self.version.clone(),
}
});
let core = Arc::new(ServerCore {
name: self.name,
version: self.version,
meta,
tools: Arc::new(tools),
resources: Arc::new(self.resources),
prompts: Arc::new(self.prompts),
tool_timeout: self.tool_timeout,
max_inflight_handlers: MAX_INFLIGHT_HANDLERS,
inflight_handlers: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
max_response_bytes: MAX_RESPONSE_BYTES,
hooks: Arc::new(self.hooks),
tools_list_cache: std::sync::OnceLock::new(),
resources_list_cache: std::sync::OnceLock::new(),
prompts_list_cache: std::sync::OnceLock::new(),
discover_cache: std::sync::OnceLock::new(),
initialize_cache: std::sync::OnceLock::new(),
ping_cache: std::sync::OnceLock::new(),
});
Ok(crate::http::BoundServer {
listener,
core,
route: Arc::new(self.route),
allow_origins: Arc::new(self.allow_origins),
read_timeout: self.read_timeout,
max_connections: self.max_connections,
connection_ttl: self.connection_ttl,
live_connections: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
refusals_in_flight: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
shutdown: Arc::new(std::sync::atomic::AtomicBool::new(false)),
})
}
}
#[cfg(test)]
mod tests_limites_2026_10_25 {
use super::*;
use std::sync::atomic::Ordering;
use std::time::Duration;
#[test]
fn inflight_cap_refuses_extra_handler_threads_and_releases_slots() {
let inflight = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let barrier = Arc::new(std::sync::Barrier::new(2));
let (b, i) = (Arc::clone(&barrier), Arc::clone(&inflight));
let first = std::thread::spawn(move || {
run_with_timeout(Duration::from_secs(5), &i, 1, move || {
b.wait();
1u8
})
});
let mut tours = 0;
while inflight.load(Ordering::Acquire) == 0 {
tours += 1;
assert!(
tours < 1_000_000,
"le premier handler n'a jamais pris son slot"
);
std::thread::yield_now();
}
let second = run_with_timeout(Duration::from_secs(5), &inflight, 1, || 2u8);
assert!(
matches!(second, Outcome::Overloaded),
"plafond d'inflight non appliqué"
);
barrier.wait();
assert!(matches!(first.join().unwrap(), Outcome::Done(1)));
let mut tours = 0;
while inflight.load(Ordering::Acquire) != 0 {
tours += 1;
assert!(tours < 1_000_000, "slot inflight jamais rendu");
std::thread::yield_now();
}
}
}