pub(crate) mod json_rpc;
pub mod gateway;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
#[cfg(feature = "http2")]
use std::future::Future;
#[cfg(feature = "http2")]
use std::pin::Pin;
use std::sync::mpsc::{self, SyncSender};
use std::sync::{Arc, Mutex, RwLock};
use std::time::Duration;
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;
pub(crate) const PROTOCOL_VERSION: &str = "2025-11-25";
pub(crate) const SUPPORTED_PROTOCOL_VERSIONS: [&str; 4] = ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"];
const ASSUMED_PROTOCOL_VERSION: &str = "2025-03-26";
pub(crate) fn negotiate_version(requested: Option<&str>) -> &'static str {
requested
.and_then(|v| SUPPORTED_PROTOCOL_VERSIONS.iter().find(|s| **s == v).copied())
.unwrap_or(PROTOCOL_VERSION)
}
#[derive(Clone, Debug)]
pub struct McpContent {
pub kind: &'static str,
pub text: String,
pub mime_type: Option<String>,
pub uri: Option<String>,
}
impl McpContent {
pub fn text(s: impl Into<String>) -> Self {
McpContent { kind: "text", text: s.into(), mime_type: None, uri: None }
}
pub fn json(s: impl Into<String>) -> Self {
McpContent { kind: "text", text: s.into(), mime_type: Some("application/json".to_string()), uri: None }
}
pub fn structured(json: impl Into<String>) -> Self {
McpContent { kind: "structured", text: json.into(), mime_type: Some("application/json".to_string()), uri: None }
}
pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
McpContent { kind: "audio", text: data.into(), mime_type: Some(mime_type.into()), uri: None }
}
pub fn blob(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
McpContent { kind: "blob", text: data.into(), mime_type: Some(mime_type.into()), uri: None }
}
pub fn blocks(items: impl IntoIterator<Item = McpContent>) -> Self {
let rendered: Vec<String> = items.into_iter().map(|item| item.to_content_json()).collect();
McpContent { kind: "blocks", text: rendered.join(","), mime_type: None, uri: None }
}
pub(crate) fn raw_result(json: impl Into<String>) -> Self {
McpContent { kind: "result", text: json.into(), mime_type: Some("application/json".to_string()), uri: None }
}
pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
McpContent { kind: "image", text: data.into(), mime_type: Some(mime_type.into()), uri: None }
}
pub fn embedded(uri: impl Into<String>, text: impl Into<String>, mime_type: impl Into<String>) -> Self {
McpContent { kind: "resource", text: text.into(), mime_type: Some(mime_type.into()), uri: Some(uri.into()) }
}
fn to_content_json(&self) -> String {
match self.kind {
"image" | "audio" => format!(
r#"{{"type":"{}","data":"{}","mimeType":"{}"}}"#,
self.kind,
json_escape(&self.text),
json_escape(self.mime_type.as_deref().unwrap_or("application/octet-stream")),
),
"blob" => format!(
r#"{{"type":"resource","resource":{{"uri":"{}","mimeType":"{}","blob":"{}"}}}}"#,
json_escape(self.uri.as_deref().unwrap_or("")),
json_escape(self.mime()),
json_escape(&self.text),
),
"blocks" => self.text.clone(),
"resource" => format!(
r#"{{"type":"resource","resource":{{"uri":"{}","mimeType":"{}","text":"{}"}}}}"#,
json_escape(self.uri.as_deref().unwrap_or("")),
json_escape(self.mime_type.as_deref().unwrap_or("text/plain")),
json_escape(&self.text),
),
_ => format!(r#"{{"type":"text","text":"{}"}}"#, json_escape(&self.text)),
}
}
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(),
)
}
}
#[derive(Clone)]
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 }
}
}
#[derive(Debug, Clone, Default)]
pub struct McpContext {
pub client_name: Option<String>,
pub client_version: Option<String>,
pub session_id: Option<String>,
pub auth_claims: Option<String>,
pub progress_token: Option<String>,
sse_clients: Option<Arc<Mutex<Vec<SseClient>>>>,
cancellation: Option<Arc<std::sync::atomic::AtomicBool>>,
sampling_supported: bool,
pending_replies: Option<Arc<Mutex<HashMap<String, mpsc::Sender<Result<String, String>>>>>>,
roots_supported: bool,
sessions: Option<Arc<Mutex<HashMap<String, StoredClientInfo>>>>,
request_stream: Option<SseSender>,
}
impl McpContext {
pub fn report_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
let (Some(token), Some(sse_clients)) = (&self.progress_token, &self.sse_clients) else {
return;
};
let total_field = match total {
Some(t) => format!(r#","total":{t}"#),
None => String::new(),
};
let message_field = match message {
Some(m) => format!(r#","message":"{}""#, json_escape(m)),
None => String::new(),
};
let params = format!(
r#"{{"progressToken":{token},"progress":{progress}{total_field}{message_field}}}"#
);
let json = render_notification("notifications/progress", Some(¶ms));
if let Some(stream) = &self.request_stream {
let _ = stream.send(format!("data: {json}\n\n").into_bytes());
} else if let Some(session) = &self.session_id {
send_sse_to_sessions(sse_clients, std::slice::from_ref(session), &json);
} else {
broadcast_sse_to(sse_clients, &json);
}
}
pub fn is_cancelled(&self) -> bool {
self.cancellation.as_ref().is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
}
pub fn sample(&self, request: SamplingRequest, timeout: Duration) -> Result<SamplingResponse, String> {
if !self.sampling_supported {
return Err("the connected client did not declare sampling support in its initialize capabilities".to_string());
}
let messages_json: Vec<String> = request.messages.iter().map(|m| m.to_json()).collect();
let system_prompt_field = match &request.system_prompt {
Some(s) => format!(r#","systemPrompt":"{}""#, json_escape(s)),
None => String::new(),
};
let params = format!(
r#"{{"messages":[{}],"maxTokens":{}{system_prompt_field}}}"#,
messages_json.join(","), request.max_tokens,
);
let result_json = self.send_and_wait("sampling/createMessage", Some(¶ms), timeout)?;
parse_sampling_response(&result_json)
}
pub fn list_roots(&self, timeout: Duration) -> Result<Vec<McpRoot>, String> {
if !self.roots_supported {
return Err("the connected client did not declare roots support in its initialize capabilities".to_string());
}
if let (Some(session_id), Some(sessions)) = (&self.session_id, &self.sessions) {
if let Some(cached) = sessions.lock().unwrap().get(session_id).and_then(|info| info.roots.clone()) {
return Ok(cached);
}
}
let result_json = self.send_and_wait("roots/list", None, timeout)?;
let roots = parse_roots_response(&result_json)?;
if let (Some(session_id), Some(sessions)) = (&self.session_id, &self.sessions) {
if let Some(info) = sessions.lock().unwrap().get_mut(session_id) {
info.roots = Some(roots.clone());
}
}
Ok(roots)
}
fn send_and_wait(&self, method: &str, params_json: Option<&str>, timeout: Duration) -> Result<String, String> {
let session_id = self.session_id.clone()
.ok_or_else(|| format!("{method} requires a session (Mcp-Session-Id)"))?;
let sse_clients = self.sse_clients.as_ref()
.ok_or_else(|| format!("{method} requires a live server (this context has none)"))?;
let pending = self.pending_replies.as_ref()
.ok_or_else(|| format!("{method} requires a live server (this context has none)"))?;
let request_id = format!("\"{}\"", crate::request_id::generate_request_id());
let (tx, rx) = mpsc::channel::<Result<String, String>>();
pending.lock().unwrap().insert(request_id.clone(), tx);
let params_field = match params_json {
Some(p) => format!(r#","params":{p}"#),
None => String::new(),
};
let rpc = format!(r#"{{"jsonrpc":"2.0","id":{request_id},"method":"{method}"{params_field}}}"#);
send_sse_to_sessions(sse_clients, &[session_id], &rpc);
let outcome = rx.recv_timeout(timeout);
pending.lock().unwrap().remove(&request_id);
match outcome {
Ok(Ok(result_json)) => Ok(result_json),
Ok(Err(e)) => Err(e),
Err(_) => Err(format!("{method} timed out waiting for the client's response")),
}
}
}
pub struct SamplingRequest {
pub messages: Vec<PromptMessage>,
pub max_tokens: u32,
pub system_prompt: Option<String>,
}
pub struct SamplingResponse {
pub role: String,
pub content: McpContent,
pub model: String,
pub stop_reason: Option<String>,
}
fn parse_sampling_response(result_json: &str) -> Result<SamplingResponse, String> {
let role = json_rpc::extract_str(result_json, "role").unwrap_or_else(|| "assistant".to_string());
let content_json = json_rpc::extract_raw(result_json, "content")
.ok_or_else(|| "sampling response missing content".to_string())?;
let kind = json_rpc::extract_str(&content_json, "type").unwrap_or_else(|| "text".to_string());
let content = match kind.as_str() {
"image" => McpContent::image(
json_rpc::extract_str(&content_json, "data").unwrap_or_default(),
json_rpc::extract_str(&content_json, "mimeType").unwrap_or_default(),
),
_ => McpContent::text(json_rpc::extract_str(&content_json, "text").unwrap_or_default()),
};
let model = json_rpc::extract_str(result_json, "model").unwrap_or_default();
let stop_reason = json_rpc::extract_str(result_json, "stopReason");
Ok(SamplingResponse { role, content, model, stop_reason })
}
#[derive(Debug, Clone)]
pub struct McpRoot {
pub uri: String,
pub name: Option<String>,
}
fn parse_roots_response(result_json: &str) -> Result<Vec<McpRoot>, String> {
let roots_array = json_rpc::extract_raw(result_json, "roots")
.ok_or_else(|| "roots/list response missing roots".to_string())?;
let roots = json_rpc::split_array_elements(&roots_array).iter().map(|elem| {
McpRoot {
uri: json_rpc::extract_str(elem, "uri").unwrap_or_default(),
name: json_rpc::extract_str(elem, "name"),
}
}).collect();
Ok(roots)
}
#[cfg(feature = "sso")]
struct OAuthConfig {
jwks: crate::sso::jwks::JwksCache,
provider: crate::sso::OidcProvider,
audience: String,
}
#[derive(Debug, Clone)]
struct StoredClientInfo {
last_seen: std::time::Instant,
protocol_version: &'static str,
name: Option<String>,
version: Option<String>,
supports_sampling: bool,
supports_roots: bool,
roots: Option<Vec<McpRoot>>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ToolAnnotations {
pub read_only_hint: Option<bool>,
pub destructive_hint: Option<bool>,
pub idempotent_hint: Option<bool>,
pub open_world_hint: Option<bool>,
}
impl ToolAnnotations {
fn to_json(self) -> String {
let mut fields = Vec::with_capacity(4);
if let Some(v) = self.read_only_hint {
fields.push(format!(r#""readOnlyHint":{v}"#));
}
if let Some(v) = self.destructive_hint {
fields.push(format!(r#""destructiveHint":{v}"#));
}
if let Some(v) = self.idempotent_hint {
fields.push(format!(r#""idempotentHint":{v}"#));
}
if let Some(v) = self.open_world_hint {
fields.push(format!(r#""openWorldHint":{v}"#));
}
format!("{{{}}}", fields.join(","))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Debug,
Info,
Notice,
Warning,
Error,
Critical,
Alert,
Emergency,
}
impl LogLevel {
pub fn parse(s: &str) -> Option<Self> {
match s {
"debug" => Some(LogLevel::Debug),
"info" => Some(LogLevel::Info),
"notice" => Some(LogLevel::Notice),
"warning" => Some(LogLevel::Warning),
"error" => Some(LogLevel::Error),
"critical" => Some(LogLevel::Critical),
"alert" => Some(LogLevel::Alert),
"emergency" => Some(LogLevel::Emergency),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
LogLevel::Debug => "debug",
LogLevel::Info => "info",
LogLevel::Notice => "notice",
LogLevel::Warning => "warning",
LogLevel::Error => "error",
LogLevel::Critical => "critical",
LogLevel::Alert => "alert",
LogLevel::Emergency => "emergency",
}
}
}
type ToolFn = Arc<dyn Fn(McpContext, &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>;
type CompletionFn = Arc<dyn Fn(&str, &str) -> Result<Vec<String>, String> + Send + Sync>;
type ToolFilter = Arc<dyn Fn(&McpContext, &str) -> bool + Send + Sync>;
#[cfg(feature = "http2")]
type AsyncToolFn = Arc<dyn Fn(String) -> Pin<Box<dyn Future<Output = Result<McpContent, String>> + Send>> + Send + Sync>;
#[derive(Clone)]
struct ToolDef {
name: String,
description: String,
input_schema: String,
annotations: Option<ToolAnnotations>,
meta: ToolMeta,
handler: ToolFn,
}
#[derive(Clone, Default)]
struct ToolMeta {
title: Option<String>,
icons: Vec<McpIcon>,
output_schema: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct McpIcon {
pub src: String,
pub mime_type: Option<String>,
pub sizes: Vec<String>,
}
impl McpIcon {
pub fn new(src: impl Into<String>) -> Self {
McpIcon { src: src.into(), mime_type: None, sizes: Vec::new() }
}
pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
pub fn size(mut self, size: impl Into<String>) -> Self {
self.sizes.push(size.into());
self
}
fn to_json(&self) -> String {
let mut out = format!(r#"{{"src":"{}""#, json_escape(&self.src));
if let Some(mime) = &self.mime_type {
out.push_str(&format!(r#","mimeType":"{}""#, json_escape(mime)));
}
if !self.sizes.is_empty() {
let sizes: Vec<String> = self.sizes.iter().map(|s| format!(r#""{}""#, json_escape(s))).collect();
out.push_str(&format!(r#","sizes":[{}]"#, sizes.join(",")));
}
out.push('}');
out
}
}
fn icons_json(icons: &[McpIcon]) -> String {
if icons.is_empty() {
return String::new();
}
let icons: Vec<String> = icons.iter().map(McpIcon::to_json).collect();
format!(r#","icons":[{}]"#, icons.join(","))
}
#[cfg(feature = "http2")]
#[derive(Clone)]
struct AsyncToolDef {
name: String,
description: String,
input_schema: String,
annotations: Option<ToolAnnotations>,
meta: ToolMeta,
handler: AsyncToolFn,
}
#[derive(Clone)]
struct ResourceDef {
uri_template: String,
name: String,
description: String,
handler: ResourceFn,
}
#[derive(Clone)]
struct PromptDef {
name: String,
description: String,
arguments: Vec<PromptArgDef>,
handler: PromptFn,
}
#[derive(Clone)]
struct CompletionDef {
ref_type: String,
ref_name: String,
handler: CompletionFn,
}
#[derive(Clone)]
pub struct McpServer {
server_name: String,
server_version: String,
path: String,
tools: Arc<RwLock<Vec<ToolDef>>>,
#[cfg(feature = "http2")]
async_tools: Arc<RwLock<Vec<AsyncToolDef>>>,
resources: Arc<RwLock<Vec<ResourceDef>>>,
prompts: Arc<RwLock<Vec<PromptDef>>>,
completions: Arc<RwLock<Vec<CompletionDef>>>,
fallback: Option<Arc<dyn Application + Send + Sync>>,
auth_token: Option<String>,
bearer_tokens: Vec<(String, String)>,
tool_filter: Option<ToolFilter>,
allowed_origins: Vec<String>,
#[cfg(feature = "sso")]
oauth: Option<Arc<OAuthConfig>>,
page_size: Option<usize>,
sessions: Arc<Mutex<HashMap<String, StoredClientInfo>>>,
session_ttl: Duration,
max_sessions: usize,
max_sse_clients: usize,
server_title: Option<String>,
server_icons: Vec<McpIcon>,
sse_clients: Arc<Mutex<Vec<SseClient>>>,
logging_enabled: bool,
min_log_level: Arc<Mutex<LogLevel>>,
cancellations: Arc<Mutex<HashMap<String, Arc<std::sync::atomic::AtomicBool>>>>,
subscriptions: Arc<Mutex<HashMap<String, Vec<String>>>>,
pending_replies: Arc<Mutex<HashMap<String, mpsc::Sender<Result<String, String>>>>>,
}
#[derive(Debug)]
struct SseClient {
session_id: Option<String>,
sender: SseSender,
}
type SseSender = SyncSender<Vec<u8>>;
const SSE_CHANNEL_CAPACITY: usize = 32;
pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(3600);
pub const DEFAULT_MAX_SESSIONS: usize = 10_000;
pub const DEFAULT_MAX_SSE_CLIENTS: usize = 1000;
const SSE_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(15);
const MAX_COMPLETION_VALUES: usize = 100;
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: Arc::new(RwLock::new(Vec::new())),
#[cfg(feature = "http2")]
async_tools: Arc::new(RwLock::new(Vec::new())),
resources: Arc::new(RwLock::new(Vec::new())),
prompts: Arc::new(RwLock::new(Vec::new())),
completions: Arc::new(RwLock::new(Vec::new())),
fallback: None,
auth_token: None,
bearer_tokens: Vec::new(),
tool_filter: None,
allowed_origins: Vec::new(),
#[cfg(feature = "sso")]
oauth: None,
page_size: None,
sessions: Arc::new(Mutex::new(HashMap::new())),
session_ttl: DEFAULT_SESSION_TTL,
max_sessions: DEFAULT_MAX_SESSIONS,
max_sse_clients: DEFAULT_MAX_SSE_CLIENTS,
server_title: None,
server_icons: Vec::new(),
sse_clients: Arc::new(Mutex::new(Vec::new())),
logging_enabled: false,
min_log_level: Arc::new(Mutex::new(LogLevel::Debug)),
cancellations: Arc::new(Mutex::new(HashMap::new())),
subscriptions: Arc::new(Mutex::new(HashMap::new())),
pending_replies: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn session_ttl(mut self, ttl: Duration) -> Self {
self.session_ttl = ttl;
self
}
pub fn max_sessions(mut self, n: usize) -> Self {
self.max_sessions = n.max(1);
self
}
pub fn max_sse_clients(mut self, n: usize) -> Self {
self.max_sse_clients = n;
self
}
pub fn tool_title(self, name: &str, title: impl Into<String>) -> Self {
let title = title.into();
self.with_tool_meta(name, |meta| meta.title = Some(title.clone()));
self
}
pub fn tool_icon(self, name: &str, icon: McpIcon) -> Self {
self.with_tool_meta(name, |meta| meta.icons.push(icon.clone()));
self
}
pub fn tool_output_schema(self, name: &str, schema: &str) -> Self {
let schema = schema.to_string();
self.with_tool_meta(name, |meta| meta.output_schema = Some(schema.clone()));
self
}
pub fn tool_annotations(self, name: &str, annotations: ToolAnnotations) -> Self {
for tool in self.tools.write().unwrap().iter_mut().filter(|t| t.name == name) {
tool.annotations = Some(annotations.clone());
}
#[cfg(feature = "http2")]
for tool in self.async_tools.write().unwrap().iter_mut().filter(|t| t.name == name) {
tool.annotations = Some(annotations.clone());
}
self
}
fn with_tool_meta(&self, name: &str, mut change: impl FnMut(&mut ToolMeta)) {
for tool in self.tools.write().unwrap().iter_mut().filter(|t| t.name == name) {
change(&mut tool.meta);
}
#[cfg(feature = "http2")]
for tool in self.async_tools.write().unwrap().iter_mut().filter(|t| t.name == name) {
change(&mut tool.meta);
}
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.server_title = Some(title.into());
self
}
pub fn icon(mut self, icon: McpIcon) -> Self {
self.server_icons.push(icon);
self
}
fn touch_session(&self, session_id: &str) -> bool {
let expired = {
let mut sessions = self.sessions.lock().unwrap();
match sessions.get_mut(session_id) {
None => return false,
Some(info) if info.last_seen.elapsed() > self.session_ttl => {
sessions.remove(session_id);
true
}
Some(info) => {
info.last_seen = std::time::Instant::now();
false
}
}
};
if expired {
self.forget_sessions(&[session_id.to_string()]);
return false;
}
true
}
fn make_room_for_a_session(&self) {
let evicted: Vec<String> = {
let mut sessions = self.sessions.lock().unwrap();
let ttl = self.session_ttl;
let mut evicted: Vec<String> = sessions.iter()
.filter(|(_, info)| info.last_seen.elapsed() > ttl)
.map(|(id, _)| id.clone())
.collect();
for id in &evicted {
sessions.remove(id);
}
if sessions.len() >= self.max_sessions {
let mut by_age: Vec<(std::time::Instant, String)> =
sessions.iter().map(|(id, info)| (info.last_seen, id.clone())).collect();
by_age.sort();
for (_, id) in by_age.into_iter().take(sessions.len() + 1 - self.max_sessions) {
sessions.remove(&id);
evicted.push(id);
}
}
evicted
};
if !evicted.is_empty() {
self.forget_sessions(&evicted);
}
}
fn forget_sessions(&self, ids: &[String]) {
let mut subscriptions = self.subscriptions.lock().unwrap();
for subscribers in subscriptions.values_mut() {
subscribers.retain(|sid| !ids.contains(sid));
}
subscriptions.retain(|_, subscribers| !subscribers.is_empty());
drop(subscriptions);
self.sse_clients.lock().unwrap()
.retain(|client| client.session_id.as_ref().is_none_or(|sid| !ids.contains(sid)));
}
pub fn session_count(&self) -> usize {
self.sessions.lock().unwrap().len()
}
pub fn page_size(mut self, n: usize) -> Self {
self.page_size = Some(n.max(1));
self
}
pub fn notify(&self, method: &str, params_json: Option<&str>) {
let json = render_notification(method, params_json);
broadcast_sse_to(&self.sse_clients, &json);
}
pub fn notify_resource_updated(&self, uri: &str) {
let session_ids = match self.subscriptions.lock().unwrap().get(uri) {
Some(ids) if !ids.is_empty() => ids.clone(),
_ => return,
};
let params = format!(r#"{{"uri":"{}"}}"#, json_escape(uri));
let json = render_notification("notifications/resources/updated", Some(¶ms));
send_sse_to_sessions(&self.sse_clients, &session_ids, &json);
}
fn start_sse_stream(&self, request: &Request) -> Response {
let session_id = request.get_header("Mcp-Session-Id".to_string()).map(|h| h.value.clone());
let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(SSE_CHANNEL_CAPACITY);
{
let mut clients = self.sse_clients.lock().unwrap();
if clients.len() >= self.max_sse_clients {
clients.retain(|client| !matches!(client.sender.try_send(b": ping\n\n".to_vec()), Err(mpsc::TrySendError::Disconnected(_))));
}
if clients.len() >= self.max_sse_clients {
let mut busy = Response::new();
busy.status_code = *STATUS_CODE_REASON_PHRASE.n503_service_unavailable.status_code;
busy.reason_phrase = STATUS_CODE_REASON_PHRASE.n503_service_unavailable.reason_phrase.to_string();
busy.headers.push(Header { name: "Retry-After".into(), value: "30".into() });
return busy;
}
clients.push(SseClient { session_id: session_id.map(|v| v.into_owned()), sender: tx });
}
let mut response = Response::new();
response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
response.headers.push(Header {
name: Header::_CONTENT_TYPE.into(),
value: "text/event-stream".into(),
});
response.headers.push(Header {
name: Header::_CACHE_CONTROL.into(),
value: "no-cache".into(),
});
response.headers.push(Header {
name: "X-Accel-Buffering".into(),
value: "no".into(),
});
response.stream_pipe = Some(Box::new(SseChannelReader::new(rx)));
response
}
pub fn logging_enabled(mut self) -> Self {
self.logging_enabled = true;
self
}
pub fn log(&self, level: LogLevel, logger: Option<&str>, data_json: &str) {
if level < *self.min_log_level.lock().unwrap() {
return;
}
let logger_field = match logger {
Some(l) => format!(r#","logger":"{}""#, json_escape(l)),
None => String::new(),
};
let params = format!(r#"{{"level":"{}"{logger_field},"data":{data_json}}}"#, level.as_str());
self.notify("notifications/message", Some(¶ms));
}
fn do_set_log_level(&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 level_str = json_rpc::extract_str(¶ms, "level")
.ok_or((json_rpc::INVALID_PARAMS, "Missing level".to_string()))?;
let level = LogLevel::parse(&level_str)
.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Unknown log level: {level_str}")))?;
*self.min_log_level.lock().unwrap() = level;
Ok("{}".to_string())
}
pub fn register_tool<F>(&self, name: &str, description: &str, input_schema: &str, handler: F)
where
F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
{
self.tools.write().unwrap().push(ToolDef {
name: name.to_string(),
description: description.to_string(),
input_schema: input_schema.to_string(),
annotations: None,
meta: ToolMeta::default(),
handler: Arc::new(move |_ctx: McpContext, args: &str| handler(args)),
});
self.notify("notifications/tools/list_changed", None);
}
#[cfg(feature = "http2")]
pub fn register_async_tool<F, Fut>(&self, name: &str, description: &str, input_schema: &str, handler: F)
where
F: Fn(&str) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<McpContent, String>> + Send + 'static,
{
self.async_tools.write().unwrap().push(AsyncToolDef {
name: name.to_string(),
description: description.to_string(),
input_schema: input_schema.to_string(),
annotations: None,
meta: ToolMeta::default(),
handler: Arc::new(move |args: String| Box::pin(handler(&args))),
});
self.notify("notifications/tools/list_changed", None);
}
pub fn remove_tool(&self, name: &str) -> bool {
#[cfg_attr(not(feature = "http2"), allow(unused_mut))]
let mut removed = {
let mut tools = self.tools.write().unwrap();
let before = tools.len();
tools.retain(|t| t.name != name);
tools.len() != before
};
#[cfg(feature = "http2")]
{
let mut async_tools = self.async_tools.write().unwrap();
let before = async_tools.len();
async_tools.retain(|t| t.name != name);
removed = removed || async_tools.len() != before;
}
if removed {
self.notify("notifications/tools/list_changed", None);
}
removed
}
pub fn register_resource<F>(&self, uri_template: &str, name: &str, description: &str, handler: F)
where
F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
{
self.resources.write().unwrap().push(ResourceDef {
uri_template: uri_template.to_string(),
name: name.to_string(),
description: description.to_string(),
handler: Arc::new(handler),
});
self.notify("notifications/resources/list_changed", None);
}
pub fn remove_resource(&self, uri_template: &str) -> bool {
let removed = {
let mut resources = self.resources.write().unwrap();
let before = resources.len();
resources.retain(|r| r.uri_template != uri_template);
resources.len() != before
};
if removed {
self.notify("notifications/resources/list_changed", None);
}
removed
}
pub fn register_prompt<F>(&self, name: &str, description: &str, handler: F)
where
F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
{
self.prompts.write().unwrap().push(PromptDef {
name: name.to_string(),
description: description.to_string(),
arguments: vec![],
handler: Arc::new(handler),
});
self.notify("notifications/prompts/list_changed", None);
}
pub fn remove_prompt(&self, name: &str) -> bool {
let removed = {
let mut prompts = self.prompts.write().unwrap();
let before = prompts.len();
prompts.retain(|p| p.name != name);
prompts.len() != before
};
if removed {
self.notify("notifications/prompts/list_changed", None);
}
removed
}
pub fn require_bearer(mut self, token: impl Into<String>) -> Self {
self.auth_token = Some(token.into());
self
}
pub fn require_bearer_tokens<I, T, N>(mut self, tokens: I) -> Self
where
I: IntoIterator<Item = (T, N)>,
T: Into<String>,
N: Into<String>,
{
self.bearer_tokens.extend(tokens.into_iter().map(|(token, identity)| (token.into(), identity.into())));
self
}
pub fn tool_filter<F>(mut self, allow: F) -> Self
where
F: Fn(&McpContext, &str) -> bool + Send + Sync + 'static,
{
self.tool_filter = Some(Arc::new(allow));
self
}
fn tool_allowed(&self, ctx: &McpContext, name: &str) -> bool {
self.tool_filter.as_ref().is_none_or(|allow| allow(ctx, name))
}
fn check_bearer(&self, request: &Request) -> Result<Option<String>, Response> {
if self.auth_token.is_none() && self.bearer_tokens.is_empty() {
return Ok(None);
}
let provided = request.headers.iter()
.find(|h| h.name.eq_ignore_ascii_case("authorization"))
.map(|h| h.value.as_ref())
.unwrap_or("");
let bearer = provided.strip_prefix("Bearer ").unwrap_or("");
let mut identity = None;
for (token, name) in &self.bearer_tokens {
if bearer_matches(token, bearer) && identity.is_none() {
identity = Some(name);
}
}
if let Some(identity) = identity {
return Ok(Some(format!(r#"{{"sub":"{}"}}"#, json_escape(identity))));
}
match &self.auth_token {
Some(expected) if bearer_matches(expected, bearer) => Ok(None),
_ => Err(unauthorized()),
}
}
pub fn allowed_origins<I, S>(mut self, origins: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_origins = origins.into_iter().map(Into::into).collect();
self
}
#[cfg(feature = "sso")]
pub fn require_oauth(mut self, provider: crate::sso::OidcProvider, audience: impl Into<String>) -> Self {
self.oauth = Some(Arc::new(OAuthConfig {
jwks: crate::sso::jwks::JwksCache::new(&provider.jwks_uri),
provider,
audience: audience.into(),
}));
self
}
pub fn wrap(mut self, app: impl Application + Send + Sync + 'static) -> Self {
self.fallback = Some(Arc::new(app));
self
}
pub fn at(mut self, path: impl Into<String>) -> Self {
self.path = path.into();
self
}
pub fn tool<F>(self, name: &str, description: &str, input_schema: &str, handler: F) -> Self
where
F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
{
self.tools.write().unwrap().push(ToolDef {
name: name.to_string(),
description: description.to_string(),
input_schema: input_schema.to_string(),
annotations: None,
meta: ToolMeta::default(),
handler: Arc::new(move |_ctx: McpContext, args: &str| handler(args)),
});
self
}
#[cfg(feature = "http2")]
pub fn async_tool<F, Fut>(self, name: &str, description: &str, input_schema: &str, handler: F) -> Self
where
F: Fn(&str) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<McpContent, String>> + Send + 'static,
{
self.async_tools.write().unwrap().push(AsyncToolDef {
name: name.to_string(),
description: description.to_string(),
input_schema: input_schema.to_string(),
annotations: None,
meta: ToolMeta::default(),
handler: Arc::new(move |args: String| Box::pin(handler(&args))),
});
self
}
pub fn tool_annotated<F>(
self,
name: &str,
description: &str,
input_schema: &str,
annotations: ToolAnnotations,
handler: F,
) -> Self
where
F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
{
self.tools.write().unwrap().push(ToolDef {
name: name.to_string(),
description: description.to_string(),
input_schema: input_schema.to_string(),
annotations: Some(annotations),
meta: ToolMeta::default(),
handler: Arc::new(move |_ctx: McpContext, args: &str| handler(args)),
});
self
}
pub fn tool_with_context<F>(self, name: &str, description: &str, input_schema: &str, handler: F) -> Self
where
F: Fn(McpContext, &str) -> Result<McpContent, String> + Send + Sync + 'static,
{
self.tools.write().unwrap().push(ToolDef {
name: name.to_string(),
description: description.to_string(),
input_schema: input_schema.to_string(),
annotations: None,
meta: ToolMeta::default(),
handler: Arc::new(handler),
});
self
}
#[cfg(feature = "serde")]
pub fn typed_tool<In, Out, F>(self, name: &str, description: &str, handler: F) -> Self
where
In: serde::de::DeserializeOwned + crate::schema::JsonSchema,
Out: serde::Serialize + crate::schema::JsonSchema,
F: Fn(McpContext, In) -> Result<Out, String> + Send + Sync + 'static,
{
let output_schema = Out::json_schema();
let structured = crate::schema::is_object_schema(&output_schema);
let server = self.tool_with_context(name, description, &In::json_schema(), move |ctx, args| {
let args = if args.trim().is_empty() { "{}" } else { args };
let input: In = serde_json::from_str(args).map_err(|e| format!("Invalid arguments: {e}"))?;
let output = handler(ctx, input)?;
let json = serde_json::to_string(&output).map_err(|e| format!("Could not serialize the result: {e}"))?;
Ok(if structured { McpContent::structured(json) } else { McpContent::json(json) })
});
if structured {
server.tool_output_schema(name, &output_schema)
} else {
server
}
}
pub fn resource<F>(self, uri_template: &str, name: &str, description: &str, handler: F) -> Self
where
F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
{
self.resources.write().unwrap().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>(self, name: &str, description: &str, handler: F) -> Self
where
F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
{
self.prompts.write().unwrap().push(PromptDef {
name: name.to_string(),
description: description.to_string(),
arguments: vec![],
handler: Arc::new(handler),
});
self
}
pub fn prompt_with_args<F>(
self,
name: &str,
description: &str,
args: Vec<PromptArgDef>,
handler: F,
) -> Self
where
F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
{
self.prompts.write().unwrap().push(PromptDef {
name: name.to_string(),
description: description.to_string(),
arguments: args,
handler: Arc::new(handler),
});
self
}
pub fn completion<F>(self, ref_type: &str, ref_name: &str, handler: F) -> Self
where
F: Fn(&str, &str) -> Result<Vec<String>, String> + Send + Sync + 'static,
{
self.completions.write().unwrap().push(CompletionDef {
ref_type: ref_type.to_string(),
ref_name: ref_name.to_string(),
handler: Arc::new(handler),
});
self
}
pub fn handle_request(&self, body: &str) -> Response {
self.handle_request_with_context(body, McpContext::default())
}
pub fn handle_request_with_context(&self, body: &str, ctx: McpContext) -> Response {
let trimmed = body.trim_start();
if trimmed.starts_with('[') {
return self.handle_batch(trimmed, ctx);
}
let id = json_rpc::extract_id(body);
let method = match json_rpc::extract_str(body, "method") {
Some(m) => m,
None => {
if let Some(id) = &id {
if self.try_deliver_sampling_response(id, body) {
return no_content();
}
}
return rpc_error(None, json_rpc::INVALID_REQUEST, "Missing method");
}
};
if method == "notifications/cancelled" {
self.handle_cancellation(body);
return no_content();
}
if method == "notifications/roots/list_changed" {
self.invalidate_roots_cache(&ctx.session_id);
return no_content();
}
if method == "notifications/initialized" || (id.is_none() && method != "ping") {
return no_content();
}
let result = self.dispatch_with_cancellation(&method, body, ctx, &id);
let id_str = id.as_deref().unwrap_or("null");
let is_ok = result.is_ok();
let mut response = json_response(&Self::format_result(id_str, &result));
if method == "initialize" && is_ok {
self.start_session(body, &mut response);
}
response
}
fn handle_batch(&self, array_body: &str, ctx: McpContext) -> Response {
let elements = json_rpc::split_array_elements(array_body);
if elements.is_empty() {
return rpc_error(None, json_rpc::INVALID_REQUEST, "Invalid Request");
}
let mut parts: Vec<String> = Vec::new();
let mut session_init_body: Option<String> = None;
for elem in &elements {
let id = json_rpc::extract_id(elem);
let method = match json_rpc::extract_str(elem, "method") {
Some(m) => m,
None => {
if let Some(id) = &id {
if self.try_deliver_sampling_response(id, elem) {
continue; }
}
parts.push(Self::format_result(
"null",
&Err((json_rpc::INVALID_REQUEST, "Missing method".to_string())),
));
continue;
}
};
if method == "notifications/cancelled" {
self.handle_cancellation(elem);
continue;
}
if method == "notifications/roots/list_changed" {
self.invalidate_roots_cache(&ctx.session_id);
continue;
}
if method == "notifications/initialized" || (id.is_none() && method != "ping") {
continue;
}
let result = self.dispatch_with_cancellation(&method, elem, ctx.clone(), &id);
let id_str = id.as_deref().unwrap_or("null");
let is_ok = result.is_ok();
if method == "initialize" && is_ok && session_init_body.is_none() {
session_init_body = Some(elem.clone());
}
parts.push(Self::format_result(id_str, &result));
}
if parts.is_empty() {
return no_content();
}
let mut response = json_response(&format!("[{}]", parts.join(",")));
if let Some(init_body) = session_init_body {
self.start_session(&init_body, &mut response);
}
response
}
fn dispatch(&self, method: &str, body: &str, ctx: McpContext) -> Result<String, (i32, String)> {
match method {
"initialize" => self.do_initialize(body),
"ping" => Ok("{}".to_string()),
"tools/list" => self.do_tools_list(body, &ctx),
"tools/call" => self.do_tools_call(body, ctx),
"resources/list" => self.do_resources_list(body),
"resources/read" => self.do_resources_read(body),
"resources/subscribe" => self.do_resource_subscribe(body, ctx.session_id),
"resources/unsubscribe" => self.do_resource_unsubscribe(body, ctx.session_id),
"prompts/list" => self.do_prompts_list(body),
"prompts/get" => self.do_prompts_get(body),
"logging/setLevel" => self.do_set_log_level(body),
"completion/complete" => self.do_completion(body),
_ => Err((json_rpc::METHOD_NOT_FOUND, format!("Unknown method: {method}"))),
}
}
fn dispatch_with_cancellation(
&self,
method: &str,
body: &str,
ctx: McpContext,
id: &Option<String>,
) -> Result<String, (i32, String)> {
let Some(id_str) = (method == "tools/call").then_some(id.as_ref()).flatten() else {
return self.dispatch(method, body, ctx);
};
let flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
self.cancellations.lock().unwrap().insert(id_str.clone(), flag.clone());
let ctx = McpContext { cancellation: Some(flag), ..ctx };
let result = self.dispatch(method, body, ctx);
self.cancellations.lock().unwrap().remove(id_str);
result
}
fn handle_cancellation(&self, body: &str) {
let Some(params) = json_rpc::extract_raw(body, "params") else { return };
let Some(request_id) = json_rpc::extract_raw(¶ms, "requestId") else { return };
if let Some(flag) = self.cancellations.lock().unwrap().get(&request_id) {
flag.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
fn invalidate_roots_cache(&self, session_id: &Option<String>) {
let Some(sid) = session_id else { return };
if let Some(info) = self.sessions.lock().unwrap().get_mut(sid) {
info.roots = None;
}
}
fn try_deliver_sampling_response(&self, id: &str, body: &str) -> bool {
let sender = match self.pending_replies.lock().unwrap().remove(id) {
Some(s) => s,
None => return false,
};
let payload = match json_rpc::extract_raw(body, "result") {
Some(result) => Ok(result),
None => {
let message = json_rpc::extract_raw(body, "error")
.and_then(|e| json_rpc::extract_str(&e, "message"))
.unwrap_or_else(|| "sampling/createMessage request failed".to_string());
Err(message)
}
};
let _ = sender.send(payload); true
}
fn format_result(id_str: &str, result: &Result<String, (i32, String)>) -> String {
match result {
Ok(result_json) => format!(
r#"{{"jsonrpc":"2.0","result":{result_json},"id":{id_str}}}"#
),
Err((code, msg)) => {
let escaped = json_escape(msg);
format!(
r#"{{"jsonrpc":"2.0","error":{{"code":{code},"message":"{escaped}"}},"id":{id_str}}}"#
)
}
}
}
fn start_session(&self, body: &str, response: &mut Response) {
let params = json_rpc::extract_raw(body, "params");
let client_info = params.as_deref().and_then(|p| json_rpc::extract_raw(p, "clientInfo"));
let (name, version) = match &client_info {
Some(info) => (
json_rpc::extract_str(info, "name"),
json_rpc::extract_str(info, "version"),
),
None => (None, None),
};
let capabilities = params.as_deref().and_then(|p| json_rpc::extract_raw(p, "capabilities"));
let supports_sampling = capabilities.as_deref()
.and_then(|c| json_rpc::extract_raw(c, "sampling"))
.is_some();
let supports_roots = capabilities.as_deref()
.and_then(|c| json_rpc::extract_raw(c, "roots"))
.is_some();
eprintln!(
"[mcp] initialize from client {} v{}",
name.as_deref().unwrap_or("unknown"),
version.as_deref().unwrap_or("unknown"),
);
let session_id = crate::request_id::generate_request_id();
self.make_room_for_a_session();
self.sessions
.lock()
.unwrap()
.insert(session_id.clone(), StoredClientInfo { last_seen: std::time::Instant::now(), protocol_version: negotiate_version(
params.as_deref().and_then(|p| json_rpc::extract_str(p, "protocolVersion")).as_deref()), name, version, supports_sampling, supports_roots, roots: None });
response.headers.push(Header {
name: "Mcp-Session-Id".into(),
value: session_id.into(),
});
}
fn do_initialize(&self, body: &str) -> Result<String, (i32, String)> {
let params = json_rpc::extract_raw(body, "params");
let client_version = params.as_deref().and_then(|p| json_rpc::extract_str(p, "protocolVersion"));
let negotiated_version = negotiate_version(client_version.as_deref());
let logging_cap = if self.logging_enabled { r#","logging":{}"# } else { "" };
let completions_cap = if self.completions.read().unwrap().is_empty() { "" } else { r#","completions":{}"# };
let caps = format!(
r#"{{"tools":{{"listChanged":true}},"resources":{{"subscribe":true,"listChanged":true}},"prompts":{{"listChanged":true}}{logging_cap}{completions_cap}}}"#
);
Ok(format!(
r#"{{"protocolVersion":"{}","capabilities":{caps},"serverInfo":{{"name":"{}"{}"version":"{}"{}}}}}"#,
json_escape(negotiated_version),
json_escape(&self.server_name),
self.server_title.as_ref().map(|t| format!(r#","title":"{}","#, json_escape(t))).unwrap_or_else(|| ",".to_string()),
json_escape(&self.server_version),
icons_json(&self.server_icons),
))
}
fn do_tools_list(&self, body: &str, ctx: &McpContext) -> Result<String, (i32, String)> {
#[cfg_attr(not(feature = "http2"), allow(unused_mut))]
let mut items: Vec<String> = self.tools.read().unwrap().iter()
.filter(|t| self.tool_allowed(ctx, &t.name))
.map(|t| Self::render_tool_list_entry(&t.name, &t.description, &t.input_schema, t.annotations, &t.meta))
.collect();
#[cfg(feature = "http2")]
items.extend(self.async_tools.read().unwrap().iter()
.filter(|t| self.tool_allowed(ctx, &t.name))
.map(|t| Self::render_tool_list_entry(&t.name, &t.description, &t.input_schema, t.annotations, &t.meta)));
let (page, next_cursor) = self.paginate(&items, body)?;
Ok(format!(r#"{{"tools":[{}]{}}}"#, page.join(","), next_cursor_json(&next_cursor)))
}
fn render_tool_list_entry(name: &str, description: &str, input_schema: &str, annotations: Option<ToolAnnotations>, meta: &ToolMeta) -> String {
let annotations_field = match annotations {
Some(a) => format!(r#","annotations":{}"#, a.to_json()),
None => String::new(),
};
let title = meta.title.as_ref().map(|t| format!(r#","title":"{}""#, json_escape(t))).unwrap_or_default();
let output_schema = meta.output_schema.as_ref().map(|schema| format!(r#","outputSchema":{schema}"#)).unwrap_or_default();
format!(
r#"{{"name":"{}"{title},"description":"{}","inputSchema":{}{output_schema}{}{}}}"#,
json_escape(name), json_escape(description), input_schema, annotations_field, icons_json(&meta.icons),
)
}
pub(crate) fn validate_arguments(schema: &str, args: &str) -> Result<(), String> {
let Some(schema) = json_rpc::object_members(schema) else { return Ok(()) };
let field = |name: &str| schema.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str());
let Some(arguments) = json_rpc::object_members(args) else {
return Err("Invalid arguments: expected a JSON object".to_string());
};
if let Some(required) = field("required") {
for name in json_rpc::split_array_elements(required) {
let name = name.trim().trim_matches('"');
if !arguments.iter().any(|(k, _)| k == name) {
return Err(format!("Invalid arguments: missing required property \"{name}\""));
}
}
}
let Some(properties) = field("properties").and_then(json_rpc::object_members) else { return Ok(()) };
for (name, value) in &arguments {
let Some((_, property)) = properties.iter().find(|(k, _)| k == name) else { continue };
let Some(declared) = json_rpc::object_members(property)
.and_then(|p| p.into_iter().find(|(k, _)| k == "type").map(|(_, v)| v))
else { continue };
let allowed: Vec<String> = if declared.starts_with('[') {
json_rpc::split_array_elements(&declared).iter().map(|t| t.trim().trim_matches('"').to_string()).collect()
} else {
vec![declared.trim_matches('"').to_string()]
};
let actual = json_type(value);
let fits = allowed.iter().any(|t| t == actual || (t == "number" && actual == "integer"));
if !fits {
return Err(format!("Invalid arguments: property \"{name}\" must be {}, got {actual}", allowed.join(" or ")));
}
}
Ok(())
}
fn do_tools_call(&self, body: &str, ctx: McpContext) -> 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());
if !self.tool_allowed(&ctx, &name) {
return Err((json_rpc::INVALID_PARAMS, format!("Unknown tool: {name}")));
}
let progress_token = json_rpc::extract_raw(¶ms, "_meta")
.and_then(|meta| json_rpc::extract_raw(&meta, "progressToken"));
let ctx = McpContext { progress_token, ..ctx };
let sync_handler = {
let tools = self.tools.read().unwrap();
tools.iter().find(|t| t.name == name).map(|t| (t.handler.clone(), t.input_schema.clone()))
};
if let Some((handler, schema)) = sync_handler {
if let Err(invalid) = Self::validate_arguments(&schema, &args) {
return Ok(Self::format_tool_result(Err(invalid)));
}
return Ok(Self::format_tool_result(handler(ctx, &args)));
}
#[cfg(feature = "http2")]
{
let async_handler = {
let async_tools = self.async_tools.read().unwrap();
async_tools.iter().find(|t| t.name == name).map(|t| (t.handler.clone(), t.input_schema.clone()))
};
if let Some((handler, schema)) = async_handler {
if let Err(invalid) = Self::validate_arguments(&schema, &args) {
return Ok(Self::format_tool_result(Err(invalid)));
}
let args_owned = args.clone();
let result = crate::async_bridge::block_on_isolated(move || handler(args_owned));
return Ok(Self::format_tool_result(result));
}
}
Err((json_rpc::INVALID_PARAMS, format!("Unknown tool: {name}")))
}
fn format_tool_result(result: Result<McpContent, String>) -> String {
match result {
Ok(c) if c.kind == "result" => c.text,
Ok(c) if c.kind == "blocks" => format!(r#"{{"content":[{}],"isError":false}}"#, c.text),
Ok(c) if c.kind == "structured" => format!(
r#"{{"content":[{{"type":"text","text":"{}"}}],"structuredContent":{},"isError":false}}"#,
json_escape(&c.text),
c.text,
),
Ok(c) => format!(
r#"{{"content":[{}],"isError":false}}"#,
c.to_content_json(),
),
Err(e) => {
let escaped = json_escape(&e);
format!(
r#"{{"content":[{{"type":"text","text":"{escaped}"}}],"isError":true}}"#
)
}
}
}
fn do_resources_list(&self, body: &str) -> Result<String, (i32, String)> {
let items: Vec<String> = self.resources.read().unwrap().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();
let (page, next_cursor) = self.paginate(&items, body)?;
Ok(format!(r#"{{"resources":[{}]{}}}"#, page.join(","), next_cursor_json(&next_cursor)))
}
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 handler = {
let resources = self.resources.read().unwrap();
resources.iter().find(|r| uri_matches(&r.uri_template, &uri)).map(|r| r.handler.clone())
}.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Resource not found: {uri}")))?;
match handler(&uri) {
Ok(c) => {
let text_esc = json_escape(&c.text);
let uri_esc = json_escape(&uri);
let field = if c.kind == "blob" { "blob" } else { "text" };
Ok(format!(
r#"{{"contents":[{{"uri":"{uri_esc}","mimeType":"{}","{field}":"{text_esc}"}}]}}"#,
json_escape(c.mime()),
))
}
Err(e) => Err((json_rpc::INVALID_PARAMS, e)),
}
}
fn do_resource_subscribe(&self, body: &str, session_id: Option<String>) -> 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 session_id = session_id.ok_or((
json_rpc::INVALID_PARAMS,
"resources/subscribe requires an Mcp-Session-Id header (call initialize first)".to_string(),
))?;
let mut subs = self.subscriptions.lock().unwrap();
let subscribers = subs.entry(uri).or_default();
if !subscribers.contains(&session_id) {
subscribers.push(session_id);
}
Ok("{}".to_string())
}
fn do_resource_unsubscribe(&self, body: &str, session_id: Option<String>) -> 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 session_id = session_id.ok_or((
json_rpc::INVALID_PARAMS,
"resources/unsubscribe requires an Mcp-Session-Id header".to_string(),
))?;
let mut subs = self.subscriptions.lock().unwrap();
if let Some(subscribers) = subs.get_mut(&uri) {
subscribers.retain(|s| s != &session_id);
if subscribers.is_empty() {
subs.remove(&uri);
}
}
Ok("{}".to_string())
}
fn do_prompts_list(&self, body: &str) -> Result<String, (i32, String)> {
let items: Vec<String> = self.prompts.read().unwrap().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();
let (page, next_cursor) = self.paginate(&items, body)?;
Ok(format!(r#"{{"prompts":[{}]{}}}"#, page.join(","), next_cursor_json(&next_cursor)))
}
fn paginate<'a>(&self, items: &'a [String], body: &str) -> Result<(&'a [String], Option<String>), (i32, String)> {
let page_size = match self.page_size {
Some(n) => n,
None => return Ok((items, None)),
};
let cursor = json_rpc::extract_raw(body, "params")
.and_then(|p| json_rpc::extract_str(&p, "cursor"));
let offset = match cursor {
Some(c) => decode_cursor(&c)
.ok_or((json_rpc::INVALID_PARAMS, "Invalid cursor".to_string()))?,
None => 0,
};
if offset >= items.len() {
return Ok((&[], None));
}
let end = (offset + page_size).min(items.len());
let next_cursor = if end < items.len() { Some(encode_cursor(end)) } else { None };
Ok((&items[offset..end], next_cursor))
}
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 (description, handler) = {
let prompts = self.prompts.read().unwrap();
prompts.iter().find(|p| p.name == name).map(|p| (p.description.clone(), p.handler.clone()))
}.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Unknown prompt: {name}")))?;
match handler(&args) {
Ok(msgs) => {
let msg_jsons: Vec<String> = msgs.iter().map(|m| m.to_json()).collect();
Ok(format!(
r#"{{"description":"{}","messages":[{}]}}"#,
json_escape(&description),
msg_jsons.join(","),
))
}
Err(e) => Err((json_rpc::INVALID_PARAMS, e)),
}
}
fn do_completion(&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 reference = json_rpc::extract_raw(¶ms, "ref")
.ok_or((json_rpc::INVALID_PARAMS, "Missing ref".to_string()))?;
let ref_type_raw = json_rpc::extract_str(&reference, "type")
.ok_or((json_rpc::INVALID_PARAMS, "Missing ref.type".to_string()))?;
let ref_type = ref_type_raw.strip_prefix("ref/").unwrap_or(&ref_type_raw);
let ref_name = json_rpc::extract_str(&reference, "name")
.ok_or((json_rpc::INVALID_PARAMS, "Missing ref.name".to_string()))?;
let argument = json_rpc::extract_raw(¶ms, "argument")
.ok_or((json_rpc::INVALID_PARAMS, "Missing argument".to_string()))?;
let arg_name = json_rpc::extract_str(&argument, "name")
.ok_or((json_rpc::INVALID_PARAMS, "Missing argument.name".to_string()))?;
let partial = json_rpc::extract_str(&argument, "value").unwrap_or_default();
let handler = {
let completions = self.completions.read().unwrap();
completions.iter()
.find(|c| c.ref_type == ref_type && c.ref_name == ref_name)
.map(|c| c.handler.clone())
};
let values = match handler {
Some(h) => h(&arg_name, &partial).map_err(|e| (json_rpc::INVALID_PARAMS, e))?,
None => vec![],
};
let total = values.len();
let has_more = total > MAX_COMPLETION_VALUES;
let page = if has_more { &values[..MAX_COMPLETION_VALUES] } else { &values[..] };
let values_json: Vec<String> = page.iter().map(|v| format!(r#""{}""#, json_escape(v))).collect();
Ok(format!(
r#"{{"completion":{{"values":[{}],"hasMore":{has_more},"total":{total}}}}}"#,
values_json.join(","),
))
}
fn stream_tool_call(&self, body: &str, mut ctx: McpContext) -> Response {
let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(32);
ctx.request_stream = Some(tx.clone());
let server = self.clone();
let body = body.to_string();
std::thread::spawn(move || {
let response = server.handle_request_with_context(&body, ctx);
let json = response.content_range_list.first().map(|part| String::from_utf8_lossy(&part.body).into_owned()).unwrap_or_default();
let _ = tx.send(format!("data: {json}\n\n").into_bytes());
});
let mut response = Response::new();
response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
response.headers.push(Header { name: Header::_CONTENT_TYPE.into(), value: "text/event-stream".into() });
response.headers.push(Header { name: Header::_CACHE_CONTROL.into(), value: "no-cache".into() });
response.headers.push(Header { name: "X-Accel-Buffering".into(), value: "no".into() });
response.stream_pipe = Some(Box::new(SseChannelReader::new(rx)));
response
}
fn context_for(&self, request: &Request) -> McpContext {
let session_id = request
.get_header("Mcp-Session-Id".to_string())
.map(|h| h.value.to_string());
let (client_name, client_version, sampling_supported, roots_supported) = match &session_id {
Some(sid) => match self.sessions.lock().unwrap().get(sid) {
Some(info) => (info.name.clone(), info.version.clone(), info.supports_sampling, info.supports_roots),
None => (None, None, false, false),
},
None => (None, None, false, false),
};
McpContext {
client_name,
client_version,
session_id,
auth_claims: None,
progress_token: None,
sse_clients: Some(self.sse_clients.clone()),
cancellation: None,
sampling_supported,
pending_replies: Some(self.pending_replies.clone()),
roots_supported,
sessions: Some(self.sessions.clone()),
request_stream: None,
}
}
}
fn wants_progress_stream(request: &Request, body: &str) -> bool {
let accepts_sse = request.headers.iter()
.filter(|h| h.name.eq_ignore_ascii_case("accept"))
.any(|h| h.value.to_ascii_lowercase().contains("text/event-stream"));
if !accepts_sse || body.trim_start().starts_with('[') {
return false;
}
let Some(members) = json_rpc::object_members(body) else { return false };
let member = |key: &str| members.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str());
member("method") == Some(r#""tools/call""#)
&& member("id").is_some()
&& member("params")
.and_then(json_rpc::object_members)
.and_then(|params| params.into_iter().find(|(k, _)| k == "_meta"))
.and_then(|(_, meta)| json_rpc::object_members(&meta))
.is_some_and(|meta| meta.iter().any(|(k, _)| k == "progressToken"))
}
fn render_notification(method: &str, params_json: Option<&str>) -> String {
let params_field = match params_json {
Some(p) => format!(r#","params":{p}"#),
None => String::new(),
};
format!(r#"{{"jsonrpc":"2.0","method":"{}"{}}}"#, json_escape(method), params_field)
}
fn broadcast_sse_to(clients: &Arc<Mutex<Vec<SseClient>>>, json: &str) {
let frame = format!("data: {json}\n\n").into_bytes();
let mut clients = clients.lock().unwrap();
clients.retain(|c| c.sender.try_send(frame.clone()).is_ok());
}
fn send_sse_to_sessions(clients: &Arc<Mutex<Vec<SseClient>>>, session_ids: &[String], json: &str) {
let frame = format!("data: {json}\n\n").into_bytes();
let mut clients = clients.lock().unwrap();
clients.retain(|c| {
let is_target = c.session_id.as_deref().is_some_and(|sid| session_ids.iter().any(|s| s == sid));
if is_target {
c.sender.try_send(frame.clone()).is_ok()
} else {
true
}
});
}
struct SseChannelReader {
rx: mpsc::Receiver<Vec<u8>>,
leftover: Vec<u8>,
}
impl SseChannelReader {
fn new(rx: mpsc::Receiver<Vec<u8>>) -> Self {
SseChannelReader { rx, leftover: Vec::new() }
}
}
impl std::io::Read for SseChannelReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.leftover.is_empty() {
loop {
match self.rx.recv_timeout(SSE_KEEPALIVE_INTERVAL) {
Ok(frame) => { self.leftover = frame; break; }
Err(mpsc::RecvTimeoutError::Timeout) => {
self.leftover = b": keep-alive\n\n".to_vec();
break;
}
Err(mpsc::RecvTimeoutError::Disconnected) => return Ok(0),
}
}
}
let n = self.leftover.len().min(buf.len());
buf[..n].copy_from_slice(&self.leftover[..n]);
self.leftover.drain(..n);
Ok(n)
}
}
impl Application for McpServer {
fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
#[cfg(feature = "sso")]
if request.method == "GET" {
if let Some(oauth) = &self.oauth {
let target = request.request_uri.split('?').next().unwrap_or("");
let suffixed = format!("{}{}", PROTECTED_RESOURCE_PATH, self.path);
if target == PROTECTED_RESOURCE_PATH || target == suffixed {
return Ok(protected_resource_document(oauth, request, &self.path));
}
}
}
if request.request_uri == self.path {
if let Some(origin) = request.headers.iter().find(|h| h.name.eq_ignore_ascii_case("origin")) {
let origin = origin.value.trim();
if !self.allowed_origins.iter().any(|o| o.trim_end_matches('/').eq_ignore_ascii_case(origin.trim_end_matches('/'))) {
return Ok(forbidden_origin());
}
}
if request.method == "POST" {
let content_type = request.headers.iter()
.find(|h| h.name.eq_ignore_ascii_case("content-type"))
.map(|h| h.value.as_ref())
.unwrap_or("");
let media_type = content_type.split(';').next().unwrap_or("").trim();
if !media_type.eq_ignore_ascii_case("application/json") {
return Ok(unsupported_media_type());
}
}
#[cfg_attr(not(feature = "sso"), allow(unused_mut))]
let mut auth_claims_json: Option<String> = None;
#[cfg(feature = "sso")]
let oauth_configured = self.oauth.is_some();
#[cfg(not(feature = "sso"))]
let oauth_configured = false;
#[cfg(feature = "sso")]
if let Some(oauth) = &self.oauth {
let provided = request.headers.iter()
.find(|h| h.name.eq_ignore_ascii_case("authorization"))
.map(|h| h.value.as_ref())
.unwrap_or("");
let bearer = provided.strip_prefix("Bearer ").unwrap_or("");
if bearer.is_empty() {
return Ok(unauthorized_with_metadata(oauth, request, &self.path));
}
let opts = crate::sso::jwks::VerifyOptions {
audience: &oauth.audience,
issuer: &oauth.provider.issuer,
leeway_secs: 60,
};
match oauth.jwks.verify_jwt(bearer, &opts) {
Ok(claims) => auth_claims_json = serde_json::to_string(&claims).ok(),
Err(_) => return Ok(unauthorized_with_metadata(oauth, request, &self.path)),
}
}
if !oauth_configured {
match self.check_bearer(request) {
Ok(claims) => auth_claims_json = claims,
Err(response) => return Ok(response),
}
}
let session_id = request.headers.iter()
.find(|h| h.name.eq_ignore_ascii_case("mcp-session-id"))
.map(|h| h.value.to_string());
if let Some(sid) = &session_id {
let initializing = request.method == "POST"
&& json_rpc::extract_str(std::str::from_utf8(&request.body).unwrap_or(""), "method").as_deref() == Some("initialize");
if request.method != "OPTIONS" && !initializing && !self.touch_session(sid) {
return Ok(session_not_found());
}
}
let header_version = request.headers.iter()
.find(|h| h.name.eq_ignore_ascii_case("mcp-protocol-version"))
.map(|h| h.value.trim().to_string());
if let Some(v) = &header_version {
if !SUPPORTED_PROTOCOL_VERSIONS.contains(&v.as_str()) {
return Ok(bad_protocol_version(v));
}
}
if request.method == "POST" && request.body.iter().find(|b| !b.is_ascii_whitespace()) == Some(&b'[') {
let version = header_version.clone()
.or_else(|| session_id.as_ref().and_then(|sid| self.sessions.lock().unwrap().get(sid).map(|s| s.protocol_version.to_string())))
.unwrap_or_else(|| ASSUMED_PROTOCOL_VERSION.to_string());
if version.as_str() >= "2025-06-18" {
return Ok(rpc_error(None, json_rpc::INVALID_REQUEST,
&format!("JSON-RPC batches are not supported in protocol version {version}")));
}
}
return Ok(match request.method.as_str() {
"DELETE" => match session_id {
Some(sid) => {
self.sessions.lock().unwrap().remove(&sid);
self.forget_sessions(&[sid]);
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n204_no_content.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string();
r
}
None => {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n400_bad_request.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string();
r.content_range_list = vec![Range::get_content_range(
b"DELETE /mcp needs an Mcp-Session-Id".to_vec(),
MimeType::TEXT_PLAIN.to_string(),
)];
r
}
},
"POST" => {
let body = std::str::from_utf8(&request.body).unwrap_or("");
let mut ctx = self.context_for(request);
ctx.auth_claims = auth_claims_json;
if wants_progress_stream(request, body) {
self.stream_tool_call(body, ctx)
} else {
self.handle_request_with_context(body, ctx)
}
}
"GET" => self.start_sse_stream(request),
"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".into(),
value: "GET, POST, DELETE, OPTIONS".into(),
});
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".into(),
value: "GET, POST, DELETE, OPTIONS".into(),
});
r.content_range_list = vec![Range::get_content_range(
b"MCP endpoint only accepts GET (SSE), POST or DELETE".to_vec(),
MimeType::TEXT_PLAIN.to_string(),
)];
r
}
});
}
match &self.fallback {
Some(app) => app.execute(request, connection),
None => App::new().execute(request, connection),
}
}
fn runs_inline(&self, request: &Request, connection: &ConnectionInfo) -> bool {
if request.request_uri == self.path || request.request_uri.starts_with("/.well-known/") {
return false;
}
match &self.fallback {
Some(app) => app.runs_inline(request, connection),
None => App::is_inline_request(request),
}
}
fn streams_body(&self, request: &Request, connection: &ConnectionInfo) -> bool {
if request.request_uri == self.path || request.request_uri.starts_with("/.well-known/") {
return false;
}
self.fallback.as_ref().is_some_and(|app| app.streams_body(request, connection))
}
#[cfg(feature = "http2")]
fn execute_async<'a>(&'a self, request: &'a Request, connection: &'a ConnectionInfo) -> Option<crate::application::AsyncResponse<'a>> {
if request.request_uri == self.path || request.request_uri.starts_with("/.well-known/") {
return None;
}
self.fallback.as_ref()?.execute_async(request, connection)
}
}
pub fn extract_arg(arguments: &str, name: &str) -> Option<String> {
if let Some(members) = json_rpc::object_members(arguments) {
let (_, raw) = members.into_iter().find(|(key, _)| key == name)?;
return match raw.as_bytes().first() {
Some(b'"') => json_rpc::extract_str(&format!(r#"{{"v":{raw}}}"#), "v"),
Some(b'{' | b'[') | None => None,
_ if raw == "null" => None,
_ => Some(raw),
};
}
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 bearer_matches(expected: &str, provided: &str) -> bool {
!expected.is_empty() && crate::proxy_config::constant_time_eq(expected.as_bytes(), provided.as_bytes())
}
fn forbidden_origin() -> Response {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
r.content_range_list = vec![Range::get_content_range(
b"Origin not allowed".to_vec(),
MimeType::TEXT_PLAIN.to_string(),
)];
r
}
fn unsupported_media_type() -> Response {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n415_unsupported_media_type.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n415_unsupported_media_type.reason_phrase.to_string();
r.content_range_list = vec![Range::get_content_range(
b"MCP requests must be application/json".to_vec(),
MimeType::TEXT_PLAIN.to_string(),
)];
r
}
fn bad_protocol_version(version: &str) -> Response {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n400_bad_request.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string();
r.content_range_list = vec![Range::get_content_range(
format!("Unsupported MCP-Protocol-Version {:?}; supported: {}", version, SUPPORTED_PROTOCOL_VERSIONS.join(", ")).into_bytes(),
MimeType::TEXT_PLAIN.to_string(),
)];
r
}
fn session_not_found() -> Response {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
r.content_range_list = vec![Range::get_content_range(
br#"{"jsonrpc":"2.0","error":{"code":-32001,"message":"Session not found: initialize a new one"},"id":null}"#.to_vec(),
MimeType::APPLICATION_JSON.to_string(),
)];
r
}
fn unauthorized() -> Response {
let mut r = Response::new();
r.status_code = *STATUS_CODE_REASON_PHRASE.n401_unauthorized.status_code;
r.reason_phrase = STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string();
r.headers.push(Header {
name: "WWW-Authenticate".into(),
value: "Bearer".into(),
});
r.content_range_list = vec![Range::get_content_range(
b"Unauthorized".to_vec(),
MimeType::TEXT_PLAIN.to_string(),
)];
r
}
#[cfg(feature = "sso")]
const PROTECTED_RESOURCE_PATH: &str = "/.well-known/oauth-protected-resource";
#[cfg(feature = "sso")]
fn resource_identifier(oauth: &OAuthConfig, request: &Request, path: &str) -> String {
if oauth.audience.starts_with("https://") || oauth.audience.starts_with("http://") {
return oauth.audience.clone();
}
format!("{}{}", request_origin(request), path)
}
#[cfg(feature = "sso")]
fn request_origin(request: &Request) -> String {
let host = request.headers.iter()
.find(|h| h.name.eq_ignore_ascii_case("host"))
.map(|h| h.value.to_string())
.unwrap_or_else(|| "localhost".to_string());
let local = host.starts_with("localhost") || host.starts_with("127.") || host.starts_with("[::1]");
format!("{}://{}", if local { "http" } else { "https" }, host)
}
#[cfg(feature = "sso")]
fn metadata_url(oauth: &OAuthConfig, request: &Request, path: &str) -> String {
let resource = resource_identifier(oauth, request, path);
let origin = match resource.find("://").and_then(|i| resource[i + 3..].find('/').map(|j| i + 3 + j)) {
Some(end) => resource[..end].to_string(),
None => resource,
};
format!("{origin}{PROTECTED_RESOURCE_PATH}")
}
#[cfg(feature = "sso")]
fn protected_resource_document(oauth: &OAuthConfig, request: &Request, path: &str) -> Response {
let body = format!(
r#"{{"resource":"{}","authorization_servers":["{}"],"bearer_methods_supported":["header"]}}"#,
json_escape(&resource_identifier(oauth, request, path)),
json_escape(&oauth.provider.issuer),
);
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.into_bytes(),
MimeType::APPLICATION_JSON.to_string(),
)];
r
}
#[cfg(feature = "sso")]
fn unauthorized_with_metadata(oauth: &OAuthConfig, request: &Request, path: &str) -> Response {
let mut r = unauthorized();
if let Some(header) = r.headers.iter_mut().find(|h| h.name.eq_ignore_ascii_case("www-authenticate")) {
header.value = format!(r#"Bearer resource_metadata="{}""#, metadata_url(oauth, request, path)).into();
}
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}}}"#
))
}
fn json_type(raw: &str) -> &'static str {
let raw = raw.trim();
match raw.as_bytes().first() {
Some(b'"') => "string",
Some(b'{') => "object",
Some(b'[') => "array",
Some(b't' | b'f') => "boolean",
Some(b'n') => "null",
_ if raw.bytes().all(|b| b.is_ascii_digit() || b == b'-') => "integer",
_ => "number",
}
}
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 next_cursor_json(next_cursor: &Option<String>) -> String {
match next_cursor {
Some(c) => format!(r#","nextCursor":"{}""#, json_escape(c)),
None => String::new(),
}
}
fn encode_cursor(offset: usize) -> String {
base64_encode(offset.to_string().as_bytes())
}
fn decode_cursor(cursor: &str) -> Option<usize> {
String::from_utf8(base64_decode(cursor)?).ok()?.parse().ok()
}
const BASE64_TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn base64_encode(data: &[u8]) -> String {
let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(BASE64_TABLE[((n >> 18) & 0x3F) as usize] as char);
out.push(BASE64_TABLE[((n >> 12) & 0x3F) as usize] as char);
out.push(if chunk.len() > 1 { BASE64_TABLE[((n >> 6) & 0x3F) as usize] as char } else { '=' });
out.push(if chunk.len() > 2 { BASE64_TABLE[(n & 0x3F) as usize] as char } else { '=' });
}
out
}
fn base64_decode(s: &str) -> Option<Vec<u8>> {
fn sextet(c: u8) -> Option<u32> {
match c {
b'A'..=b'Z' => Some((c - b'A') as u32),
b'a'..=b'z' => Some((c - b'a' + 26) as u32),
b'0'..=b'9' => Some((c - b'0' + 52) as u32),
b'+' => Some(62),
b'/' => Some(63),
_ => None,
}
}
let trimmed = s.trim_end_matches('=');
let bytes = trimmed.as_bytes();
let mut out = Vec::with_capacity(bytes.len() * 3 / 4 + 3);
for chunk in bytes.chunks(4) {
if chunk.len() == 1 {
return None; }
let vals: Vec<u32> = chunk.iter().map(|&b| sextet(b)).collect::<Option<Vec<_>>>()?;
let n = vals.iter().enumerate().fold(0u32, |acc, (i, &v)| acc | (v << (18 - 6 * i)));
out.push(((n >> 16) & 0xFF) as u8);
if vals.len() > 2 { out.push(((n >> 8) & 0xFF) as u8); }
if vals.len() > 3 { out.push((n & 0xFF) as u8); }
}
Some(out)
}
fn uri_matches(template: &str, uri: &str) -> bool {
match template.find('{') {
Some(pos) => uri.starts_with(&template[..pos]),
None => template == uri,
}
}