use std::collections::{HashMap, VecDeque};
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::Router;
use axum::body::{Body, to_bytes};
use axum::extract::DefaultBodyLimit;
use axum::http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use bytes::Bytes;
use dashmap::DashMap;
use futures::StreamExt;
use tokio::sync::{Mutex, RwLock, mpsc, oneshot};
use tokio_util::sync::CancellationToken;
use tower_http::cors::{AllowOrigin, CorsLayer};
use tower_http::limit::RequestBodyLimitLayer;
use turbomcp_core::error::{McpError, McpResult};
use turbomcp_core::handler::McpHandler;
use turbomcp_core::jsonrpc::{JsonRpcResponse as CoreJsonRpcResponse, JsonRpcResponsePayload};
use turbomcp_transport::security::{
OriginConfig, SecurityHeaders, extract_client_ip, extract_client_ip_with_trust,
validate_origin, validate_origin_value,
};
use turbomcp_types::{ClientCapabilities, ProtocolVersion};
use uuid::Uuid;
use crate::config::{HttpSessionConfig, RateLimiter, ServerConfig};
use crate::context::{Cancellable, McpSession, RequestContext, SessionFuture};
use crate::router::{self, JsonRpcIncoming, JsonRpcOutgoing};
use turbomcp_core::auth::Principal;
use super::{PendingHandlerGuard, jsonrpc_id_key};
const MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
const SSE_KEEP_ALIVE_SECS: u64 = 30;
const MAX_PENDING_SERVER_REQUESTS: usize = 64;
const SERVER_REQUEST_TIMEOUT_SECS: u64 = 60;
const MAX_REPLAY_EVENTS: usize = 64;
const SSE_STREAM_BUFFER: usize = 256;
const MAX_RETAINED_STREAMS: usize = 8;
type PendingServerResponse = oneshot::Sender<McpResult<serde_json::Value>>;
type PendingServerRequests = Arc<Mutex<HashMap<String, PendingServerResponse>>>;
type SseEvent = (String, Arc<str>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum StreamKind {
Listening,
Pending,
InFlight,
Answered,
}
#[derive(Debug)]
struct StreamState {
sender: Option<mpsc::Sender<SseEvent>>,
kind: StreamKind,
next_seq: u64,
history: VecDeque<(u64, SseEvent)>,
}
impl StreamState {
fn new(sender: mpsc::Sender<SseEvent>, kind: StreamKind) -> Self {
Self {
sender: Some(sender),
kind,
next_seq: 1,
history: VecDeque::new(),
}
}
fn is_attached(&self) -> bool {
self.sender
.as_ref()
.is_some_and(|sender| !sender.is_closed())
}
fn next_event(&self, session_id: &str, stream_id: &str, message: &str) -> SseEvent {
(
format!("{session_id}-{stream_id}-{}", self.next_seq),
Arc::from(message),
)
}
fn retain(&mut self, event: SseEvent) {
let seq = self.next_seq;
self.next_seq = seq.saturating_add(1);
while self.history.len() >= MAX_REPLAY_EVENTS {
self.history.pop_front();
}
self.history.push_back((seq, event));
}
fn emit(&mut self, session_id: &str, stream_id: &str, message: &str) -> bool {
let Some(sender) = self.sender.as_ref() else {
return false;
};
let event = self.next_event(session_id, stream_id, message);
match sender.try_send(event.clone()) {
Ok(()) => {}
Err(mpsc::error::TrySendError::Full(_)) => {
tracing::warn!(
session_id,
stream_id,
"SSE stream queue full; dropping event"
);
return false;
}
Err(mpsc::error::TrySendError::Closed(_)) => {
self.sender = None;
return false;
}
}
self.retain(event);
true
}
fn answer(&mut self, session_id: &str, stream_id: &str, response: Option<&str>) {
let sender = self.sender.take();
if let Some(response) = response {
let event = self.next_event(session_id, stream_id, response);
if let Some(sender) = sender
&& sender.try_send(event.clone()).is_err()
{
tracing::debug!(
session_id,
stream_id,
"POST stream not readable; response kept for replay"
);
}
self.retain(event);
}
self.kind = StreamKind::Answered;
}
fn replay_after(&self, last_seq: u64) -> Vec<SseEvent> {
self.history
.iter()
.filter(|(seq, _)| *seq > last_seq)
.map(|(_, event)| event.clone())
.collect()
}
}
fn parse_event_id(event_id: &str) -> Option<(&str, &str, u64)> {
let (head, seq) = event_id.rsplit_once('-')?;
let (session_id, stream_id) = head.rsplit_once('-')?;
Some((session_id, stream_id, seq.parse().ok()?))
}
#[derive(Debug)]
struct SessionData {
streams: Vec<(String, StreamState)>,
protocol_version: Option<ProtocolVersion>,
client_capabilities: Option<ClientCapabilities>,
pending_server_requests: PendingServerRequests,
next_server_request_id: u64,
pending_handlers: Arc<DashMap<String, CancellationToken>>,
idle: IdleClock,
owner: Option<String>,
}
impl SessionData {
fn is_expired(&self, now: Instant, idle_timeout: Duration) -> bool {
self.idle.idle_for(now) >= idle_timeout
&& self.pending_handlers.is_empty()
&& !self.streams.iter().any(|(_, state)| state.is_attached())
}
}
#[derive(Debug, Clone)]
struct IdleClock(Arc<parking_lot::Mutex<Instant>>);
impl IdleClock {
fn new() -> Self {
Self(Arc::new(parking_lot::Mutex::new(Instant::now())))
}
fn reset(&self) {
*self.0.lock() = Instant::now();
}
fn idle_for(&self, now: Instant) -> Duration {
now.saturating_duration_since(*self.0.lock())
}
}
struct ResetOnDrop(Option<IdleClock>);
impl Drop for ResetOnDrop {
fn drop(&mut self) {
if let Some(clock) = self.0.take() {
clock.reset();
}
}
}
#[cfg(not(feature = "internal-bench"))]
#[derive(Clone, Debug)]
pub(crate) struct SessionManager {
sessions: Arc<RwLock<HashMap<String, SessionData>>>,
limits: HttpSessionConfig,
}
#[cfg(feature = "internal-bench")]
#[doc(hidden)]
#[derive(Clone, Debug)]
pub struct SessionManager {
sessions: Arc<RwLock<HashMap<String, SessionData>>>,
limits: HttpSessionConfig,
}
impl Default for SessionManager {
fn default() -> Self {
Self::new()
}
}
impl SessionManager {
#[allow(dead_code)]
fn new_inner() -> Self {
Self::with_limits(HttpSessionConfig::default())
}
fn with_limits(limits: HttpSessionConfig) -> Self {
Self {
sessions: Arc::new(RwLock::new(HashMap::new())),
limits,
}
}
#[cfg(not(feature = "internal-bench"))]
pub(crate) fn new() -> Self {
Self::new_inner()
}
#[cfg(feature = "internal-bench")]
#[doc(hidden)]
pub fn new() -> Self {
Self::new_inner()
}
#[allow(dead_code)]
async fn create_session_inner(
&self,
_initialize_request_id: Option<&serde_json::Value>,
) -> String {
let session_id = Uuid::new_v4().to_string();
self.sessions.write().await.insert(
session_id.clone(),
SessionData {
streams: Vec::new(),
protocol_version: None,
client_capabilities: None,
pending_server_requests: Arc::new(Mutex::new(HashMap::new())),
next_server_request_id: 1,
pending_handlers: Arc::new(DashMap::new()),
idle: IdleClock::new(),
owner: None,
},
);
tracing::debug!("Created SSE session: {}", session_id);
session_id
}
async fn admit_session(&self) -> bool {
let mut sessions = self.sessions.write().await;
let now = Instant::now();
let expired: Vec<String> = sessions
.iter()
.filter(|(_, data)| data.is_expired(now, self.limits.idle_timeout))
.map(|(id, _)| id.clone())
.collect();
for session_id in expired {
if let Some(data) = sessions.remove(&session_id) {
tracing::debug!(session_id, "Reaped idle HTTP session");
Self::end_session(&session_id, data);
}
}
sessions.len() < self.limits.max_sessions
}
async fn touch_session(&self, session_id: &str, owner: Option<&str>) -> bool {
let mut sessions = self.sessions.write().await;
let Some(data) = sessions.get(session_id) else {
return false;
};
if data.owner.as_deref() != owner {
return false;
}
if data.is_expired(Instant::now(), self.limits.idle_timeout) {
if let Some(data) = sessions.remove(session_id) {
tracing::debug!(session_id, "Reaped idle HTTP session");
Self::end_session(session_id, data);
}
return false;
}
data.idle.reset();
true
}
async fn bind_owner(&self, session_id: &str, owner: Option<String>) {
if let Some(data) = self.sessions.write().await.get_mut(session_id) {
data.owner = owner;
}
}
async fn idle_clock(&self, session_id: &str) -> Option<IdleClock> {
self.sessions
.read()
.await
.get(session_id)
.map(|data| data.idle.clone())
}
fn end_session(session_id: &str, data: SessionData) {
for entry in data.pending_handlers.iter() {
entry.value().cancel();
}
turbomcp_core::context::clear_session_log_state(session_id);
}
#[cfg(not(feature = "internal-bench"))]
pub(crate) async fn create_session(
&self,
initialize_request_id: Option<&serde_json::Value>,
) -> String {
self.create_session_inner(initialize_request_id).await
}
#[cfg(feature = "internal-bench")]
#[doc(hidden)]
pub async fn create_session(
&self,
initialize_request_id: Option<&serde_json::Value>,
) -> String {
self.create_session_inner(initialize_request_id).await
}
#[allow(dead_code)]
async fn remove_session_inner(&self, session_id: &str) -> bool {
let Some(data) = self.sessions.write().await.remove(session_id) else {
return false;
};
Self::end_session(session_id, data);
tracing::debug!("Removed session: {}", session_id);
true
}
#[cfg(not(feature = "internal-bench"))]
pub(crate) async fn remove_session(&self, session_id: &str) -> bool {
self.remove_session_inner(session_id).await
}
#[cfg(feature = "internal-bench")]
#[doc(hidden)]
pub async fn remove_session(&self, session_id: &str) -> bool {
self.remove_session_inner(session_id).await
}
#[allow(dead_code)]
async fn subscribe_session_inner(&self, session_id: &str) -> Option<mpsc::Receiver<SseEvent>> {
self.open_stream(session_id, StreamKind::Listening)
.await
.map(|(_, _, rx)| rx)
}
#[cfg(not(feature = "internal-bench"))]
#[allow(dead_code)]
pub(crate) async fn subscribe_session(
&self,
session_id: &str,
) -> Option<mpsc::Receiver<SseEvent>> {
self.subscribe_session_inner(session_id).await
}
#[cfg(feature = "internal-bench")]
#[doc(hidden)]
pub async fn subscribe_session(&self, session_id: &str) -> Option<mpsc::Receiver<SseEvent>> {
self.subscribe_session_inner(session_id).await
}
async fn open_stream(
&self,
session_id: &str,
kind: StreamKind,
) -> Option<(String, String, mpsc::Receiver<SseEvent>)> {
let mut sessions = self.sessions.write().await;
let data = sessions.get_mut(session_id)?;
let stream_id = Uuid::new_v4().simple().to_string();
let (tx, rx) = mpsc::channel(SSE_STREAM_BUFFER);
data.streams
.push((stream_id.clone(), StreamState::new(tx, kind)));
if kind != StreamKind::Pending {
Self::evict_excess_streams(data);
}
let primer_id = format!("{session_id}-{stream_id}-0");
Some((stream_id, primer_id, rx))
}
async fn resume_stream(
&self,
session_id: &str,
last_event_id: &str,
) -> Option<(Vec<SseEvent>, mpsc::Receiver<SseEvent>)> {
let (event_session, stream_id, last_seq) = parse_event_id(last_event_id)?;
if event_session != session_id {
tracing::warn!(
session_id,
last_event_id,
"Ignoring a Last-Event-ID that names a different session"
);
return None;
}
let mut sessions = self.sessions.write().await;
let data = sessions.get_mut(session_id)?;
let (_, state) = data.streams.iter_mut().find(|(id, _)| id == stream_id)?;
let (tx, rx) = mpsc::channel(SSE_STREAM_BUFFER);
match state.kind {
StreamKind::Pending => return None,
StreamKind::Answered => drop(tx),
StreamKind::Listening | StreamKind::InFlight => state.sender = Some(tx),
}
Some((state.replay_after(last_seq), rx))
}
async fn send_to_stream(&self, session_id: &str, stream_id: &str, message: &str) -> bool {
let mut sessions = self.sessions.write().await;
let Some(data) = sessions.get_mut(session_id) else {
return false;
};
let Some((_, state)) = data.streams.iter_mut().find(|(id, _)| id == stream_id) else {
return false;
};
if !state.emit(session_id, stream_id, message) {
return false;
}
if state.kind == StreamKind::Pending {
state.kind = StreamKind::InFlight;
Self::evict_excess_streams(data);
}
true
}
async fn finish_stream(
&self,
session_id: &str,
stream_id: &str,
response: Option<&str>,
) -> bool {
let mut sessions = self.sessions.write().await;
let Some(data) = sessions.get_mut(session_id) else {
return false;
};
let Some(index) = data.streams.iter().position(|(id, _)| id == stream_id) else {
return false;
};
if data.streams[index].1.kind == StreamKind::Pending {
data.streams.remove(index);
return false;
}
data.streams[index]
.1
.answer(session_id, stream_id, response);
true
}
fn evict_excess_streams(data: &mut SessionData) {
loop {
let retained = data
.streams
.iter()
.filter(|(_, state)| state.kind != StreamKind::Pending)
.count();
if retained <= MAX_RETAINED_STREAMS {
return;
}
let newest_listening = data
.streams
.iter()
.rposition(|(_, state)| state.kind == StreamKind::Listening);
let victim = data
.streams
.iter()
.enumerate()
.filter_map(|(index, (_, state))| {
let rank = match state.kind {
StreamKind::Answered => 0,
StreamKind::Listening if Some(index) != newest_listening => 1,
_ => return None,
};
let attached = u8::from(state.is_attached());
Some(((attached, rank), index))
})
.min()
.map(|(_, index)| index);
match victim {
Some(index) => {
data.streams.remove(index);
}
None => return,
}
}
}
fn route_to_listening_stream(session_id: &str, data: &mut SessionData, message: &str) -> bool {
for index in (0..data.streams.len()).rev() {
let (stream_id, state) = &mut data.streams[index];
if state.kind != StreamKind::Listening || state.sender.is_none() {
continue;
}
let stream_id = stream_id.clone();
if state.emit(session_id, &stream_id, message) {
return true;
}
}
false
}
#[allow(dead_code)]
async fn send_to_session_inner(&self, session_id: &str, message: &str) -> bool {
let mut sessions = self.sessions.write().await;
let Some(data) = sessions.get_mut(session_id) else {
return false;
};
Self::route_to_listening_stream(session_id, data, message)
}
#[cfg(not(feature = "internal-bench"))]
pub(crate) async fn send_to_session(&self, session_id: &str, message: &str) -> bool {
self.send_to_session_inner(session_id, message).await
}
#[cfg(feature = "internal-bench")]
#[doc(hidden)]
pub async fn send_to_session(&self, session_id: &str, message: &str) -> bool {
self.send_to_session_inner(session_id, message).await
}
#[allow(dead_code)]
async fn broadcast_inner(&self, message: &str) {
let mut sessions = self.sessions.write().await;
for (session_id, data) in sessions.iter_mut() {
if !Self::route_to_listening_stream(session_id, data, message) {
tracing::warn!("No live subscriber for session {}", session_id);
}
}
}
#[cfg(not(feature = "internal-bench"))]
#[allow(dead_code)]
pub(crate) async fn broadcast(&self, message: &str) {
self.broadcast_inner(message).await
}
#[cfg(feature = "internal-bench")]
#[doc(hidden)]
pub async fn broadcast(&self, message: &str) {
self.broadcast_inner(message).await
}
#[allow(dead_code)] pub(crate) async fn session_count(&self) -> usize {
self.sessions.read().await.len()
}
pub(crate) async fn set_initialized(
&self,
session_id: &str,
version: ProtocolVersion,
client_capabilities: ClientCapabilities,
) {
if let Some(data) = self.sessions.write().await.get_mut(session_id) {
data.protocol_version = Some(version);
data.client_capabilities = Some(client_capabilities);
}
}
pub(crate) async fn get_protocol_version(&self, session_id: &str) -> Option<ProtocolVersion> {
self.sessions
.read()
.await
.get(session_id)
.and_then(|data| data.protocol_version.clone())
}
pub(crate) async fn get_client_capabilities(
&self,
session_id: &str,
) -> Option<ClientCapabilities> {
self.sessions
.read()
.await
.get(session_id)
.and_then(|data| data.client_capabilities.clone())
}
async fn register_pending_server_request(
&self,
session_id: &str,
response_tx: PendingServerResponse,
) -> McpResult<String> {
let (request_id, pending) = {
let mut sessions = self.sessions.write().await;
let Some(data) = sessions.get_mut(session_id) else {
return Err(McpError::transport("HTTP session not found"));
};
let request_id = format!("s-{}", data.next_server_request_id);
data.next_server_request_id = data.next_server_request_id.saturating_add(1);
(request_id, Arc::clone(&data.pending_server_requests))
};
let mut pending = pending.lock().await;
if pending.len() >= MAX_PENDING_SERVER_REQUESTS {
return Err(McpError::server_overloaded());
}
pending.insert(request_id.clone(), response_tx);
Ok(request_id)
}
async fn remove_pending_server_request(&self, session_id: &str, request_id: &str) -> bool {
let Some(pending) = self.pending_server_requests(session_id).await else {
return false;
};
pending.lock().await.remove(request_id).is_some()
}
async fn complete_pending_server_response(
&self,
session_id: &str,
response: CoreJsonRpcResponse,
) -> Result<(), StatusCode> {
let Some(request_id) = response.id.as_request_id().map(ToString::to_string) else {
return Err(StatusCode::BAD_REQUEST);
};
let Some(pending) = self.pending_server_requests(session_id).await else {
return Err(StatusCode::NOT_FOUND);
};
let Some(response_tx) = pending.lock().await.remove(&request_id) else {
tracing::warn!(
session_id,
request_id,
"Received response for unknown HTTP server request"
);
return Err(StatusCode::BAD_REQUEST);
};
let result = match response.payload {
JsonRpcResponsePayload::Success { result } => Ok(result),
JsonRpcResponsePayload::Error { error } => {
Err(McpError::from_rpc_code(error.code, error.message))
}
};
response_tx
.send(result)
.map_err(|_| StatusCode::BAD_REQUEST)
}
async fn pending_server_requests(&self, session_id: &str) -> Option<PendingServerRequests> {
self.sessions
.read()
.await
.get(session_id)
.map(|data| Arc::clone(&data.pending_server_requests))
}
async fn register_pending_handler(
&self,
session_id: &str,
key: String,
) -> Option<(CancellationToken, PendingHandlerGuard)> {
let handlers = {
let sessions = self.sessions.read().await;
Arc::clone(&sessions.get(session_id)?.pending_handlers)
};
let token = CancellationToken::new();
if handlers.insert(key.clone(), token.clone()).is_some() {
tracing::warn!(
request_id = %key,
"Request id reused while the first is still in flight"
);
}
let guard = PendingHandlerGuard::new(Arc::clone(&handlers), Some(key));
Some((token, guard))
}
async fn cancel_pending_handler(&self, session_id: &str, key: &str) -> bool {
let handlers = {
let sessions = self.sessions.read().await;
match sessions.get(session_id) {
Some(data) => Arc::clone(&data.pending_handlers),
None => return false,
}
};
match handlers.remove(key) {
Some((_, token)) => {
token.cancel();
true
}
None => false,
}
}
}
#[derive(Debug, Clone)]
struct HttpSessionHandle {
session_id: String,
session_manager: SessionManager,
request_timeout: Duration,
request_stream: Option<String>,
}
impl HttpSessionHandle {
fn new(session_id: impl Into<String>, session_manager: SessionManager) -> Self {
Self {
session_id: session_id.into(),
session_manager,
request_timeout: Duration::from_secs(SERVER_REQUEST_TIMEOUT_SECS),
request_stream: None,
}
}
fn with_request_stream(mut self, stream_id: Option<String>) -> Self {
self.request_stream = stream_id;
self
}
async fn deliver(&self, payload: &str) -> bool {
if let Some(ref stream_id) = self.request_stream
&& self
.session_manager
.send_to_stream(&self.session_id, stream_id, payload)
.await
{
return true;
}
self.session_manager
.send_to_session(&self.session_id, payload)
.await
}
async fn cancel_server_request(&self, request_id: &str, reason: &str) {
if !self
.session_manager
.remove_pending_server_request(&self.session_id, request_id)
.await
{
return;
}
let notification = serde_json::json!({
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": { "requestId": request_id, "reason": reason },
});
if let Ok(payload) = serde_json::to_string(¬ification)
&& !self.deliver(&payload).await
{
tracing::debug!(
session_id = %self.session_id,
request_id,
"No stream to carry notifications/cancelled"
);
}
}
}
struct OutstandingRequest {
session: Option<HttpSessionHandle>,
request_id: String,
}
impl OutstandingRequest {
fn new(session: &HttpSessionHandle, request_id: &str) -> Self {
Self {
session: Some(session.clone()),
request_id: request_id.to_string(),
}
}
fn settle(mut self) {
self.session = None;
}
async fn cancel(mut self, reason: &str) {
if let Some(session) = self.session.take() {
session
.cancel_server_request(&self.request_id, reason)
.await;
}
}
}
impl Drop for OutstandingRequest {
fn drop(&mut self) {
let Some(session) = self.session.take() else {
return;
};
let request_id = std::mem::take(&mut self.request_id);
if let Ok(runtime) = tokio::runtime::Handle::try_current() {
runtime.spawn(async move {
session
.cancel_server_request(&request_id, "request abandoned by the server")
.await;
});
}
}
}
impl McpSession for HttpSessionHandle {
fn client_capabilities<'a>(&'a self) -> SessionFuture<'a, Option<ClientCapabilities>> {
Box::pin(async move {
Ok(self
.session_manager
.get_client_capabilities(&self.session_id)
.await)
})
}
fn protocol_version<'a>(&'a self) -> SessionFuture<'a, Option<ProtocolVersion>> {
Box::pin(async move {
Ok(self
.session_manager
.get_protocol_version(&self.session_id)
.await)
})
}
fn call<'a>(
&'a self,
method: &'a str,
params: serde_json::Value,
) -> SessionFuture<'a, serde_json::Value> {
Box::pin(async move {
let (response_tx, response_rx) = oneshot::channel();
let request_id = self
.session_manager
.register_pending_server_request(&self.session_id, response_tx)
.await?;
let outstanding = OutstandingRequest::new(self, &request_id);
let request = serde_json::json!({
"jsonrpc": "2.0",
"id": request_id,
"method": method,
"params": params,
});
let payload = match serde_json::to_string(&request) {
Ok(payload) => payload,
Err(e) => {
outstanding.settle();
self.session_manager
.remove_pending_server_request(&self.session_id, &request_id)
.await;
return Err(McpError::serialization(e.to_string()));
}
};
if !self.deliver(&payload).await {
outstanding.settle();
self.session_manager
.remove_pending_server_request(&self.session_id, &request_id)
.await;
return Err(McpError::unavailable(
"No active SSE stream for HTTP session",
));
}
match tokio::time::timeout(self.request_timeout, response_rx).await {
Ok(Ok(result)) => {
outstanding.settle();
result
}
Ok(Err(_)) => {
outstanding.settle();
Err(McpError::transport("HTTP session response channel closed"))
}
Err(_) => {
outstanding.cancel("timed out waiting for a response").await;
Err(McpError::timeout(format!(
"Timed out waiting for response to server request {request_id}"
)))
}
}
})
}
fn notify<'a>(&'a self, method: &'a str, params: serde_json::Value) -> SessionFuture<'a, ()> {
Box::pin(async move {
let notification = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
});
let payload = serde_json::to_string(¬ification)
.map_err(|e| McpError::serialization(e.to_string()))?;
if self.deliver(&payload).await {
Ok(())
} else {
Err(McpError::unavailable(
"No active SSE stream for HTTP session",
))
}
})
}
}
pub async fn run<H: McpHandler>(handler: &H, addr: &str) -> McpResult<()> {
handler.on_initialize().await?;
let shutdown = CancellationToken::new();
let app = build_router_with_shutdown(handler.clone(), None, None, shutdown.clone());
let socket_addr: SocketAddr = addr
.parse()
.map_err(|e| McpError::internal(format!("Invalid address '{}': {}", addr, e)))?;
let listener = tokio::net::TcpListener::bind(socket_addr)
.await
.map_err(|e| McpError::internal(format!("Failed to bind to {}: {}", addr, e)))?;
tracing::info!(
"MCP server listening on http://{} (GET/POST/DELETE /, /mcp; GET /sse)",
socket_addr
);
serve_until(listener, app, shutdown, shutdown_signal(), None).await?;
handler.on_shutdown().await?;
Ok(())
}
async fn serve_until(
listener: tokio::net::TcpListener,
app: Router,
shutdown: CancellationToken,
signal: impl std::future::Future<Output = ()> + Send + 'static,
drain: Option<Duration>,
) -> McpResult<()> {
let signalled = shutdown.clone();
let serve = axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(async move {
signal.await;
tracing::info!("Shutdown signal received, draining HTTP server");
signalled.cancel();
});
let deadline = async {
shutdown.cancelled().await;
match drain {
Some(drain) => tokio::time::sleep(drain).await,
None => std::future::pending().await,
}
};
tokio::select! {
served = std::future::IntoFuture::into_future(serve) => {
served.map_err(|e| McpError::internal(format!("Server error: {}", e)))
}
() = deadline => {
tracing::warn!(
"In-flight requests outlived the {:?} graceful-shutdown drain; stopping",
drain.unwrap_or_default()
);
Ok(())
}
}
}
async fn shutdown_signal() {
let ctrl_c = async {
let _ = tokio::signal::ctrl_c().await;
};
#[cfg(unix)]
let terminate = async {
if let Ok(mut sig) =
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
{
sig.recv().await;
}
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}
pub async fn run_with_config<H: McpHandler>(
handler: &H,
addr: &str,
config: &ServerConfig,
) -> McpResult<()> {
run_with_shutdown(handler, addr, config, None).await
}
pub async fn run_with_shutdown<H: McpHandler>(
handler: &H,
addr: &str,
config: &ServerConfig,
graceful_shutdown: Option<Duration>,
) -> McpResult<()> {
handler.on_initialize().await?;
let rate_limiter = config
.rate_limit
.as_ref()
.map(|cfg| Arc::new(RateLimiter::new(cfg.clone())));
let shutdown = CancellationToken::new();
let app = build_router_with_shutdown(
handler.clone(),
rate_limiter,
Some(config.clone()),
shutdown.clone(),
);
let socket_addr: SocketAddr = addr
.parse()
.map_err(|e| McpError::internal(format!("Invalid address '{}': {}", addr, e)))?;
let listener = tokio::net::TcpListener::bind(socket_addr)
.await
.map_err(|e| McpError::internal(format!("Failed to bind to {}: {}", addr, e)))?;
let rate_limit_info = config
.rate_limit
.as_ref()
.map(|cfg| {
format!(
" (rate limit: {}/{}s)",
cfg.max_requests,
cfg.window.as_secs()
)
})
.unwrap_or_default();
tracing::info!(
"MCP server listening on http://{}{} (GET/POST/DELETE /, /mcp; GET /sse)",
socket_addr,
rate_limit_info
);
serve_until(
listener,
app,
shutdown,
shutdown_signal(),
graceful_shutdown,
)
.await?;
handler.on_shutdown().await?;
Ok(())
}
#[derive(Clone)]
pub(crate) struct SseState<H: McpHandler> {
handler: H,
session_manager: SessionManager,
rate_limiter: Option<Arc<RateLimiter>>,
config: Option<ServerConfig>,
shutdown: CancellationToken,
}
pub(crate) fn build_router<H: McpHandler>(
handler: H,
rate_limiter: Option<Arc<RateLimiter>>,
config: Option<ServerConfig>,
) -> Router {
build_router_with_shutdown(handler, rate_limiter, config, CancellationToken::new())
}
fn build_router_with_shutdown<H: McpHandler>(
handler: H,
rate_limiter: Option<Arc<RateLimiter>>,
config: Option<ServerConfig>,
shutdown: CancellationToken,
) -> Router {
let max_body_size = config
.as_ref()
.map_or(MAX_BODY_SIZE, |config| config.max_message_size);
let cors = cors_layer(config.as_ref());
let session_limits = config
.as_ref()
.map_or_else(HttpSessionConfig::default, |config| config.http_sessions);
let state = SseState {
handler,
session_manager: SessionManager::with_limits(session_limits),
rate_limiter,
config,
shutdown,
};
let router = Router::new()
.route(
"/",
post(handle_json_rpc::<H>)
.get(handle_sse::<H>)
.delete(handle_delete_session::<H>),
)
.route(
"/mcp",
post(handle_json_rpc::<H>)
.get(handle_sse::<H>)
.delete(handle_delete_session::<H>),
)
.route("/sse", get(handle_sse::<H>));
let router = match state
.config
.as_ref()
.and_then(|config| config.authorization.as_ref())
{
Some(authorization) => {
let root = "/.well-known/oauth-protected-resource";
let path = authorization.metadata_path();
let router = router.route(root, get(protected_resource_metadata::<H>));
if path == root {
router
} else {
router.route(&path, get(protected_resource_metadata::<H>))
}
}
None => router,
};
let router = router
.layer(DefaultBodyLimit::max(max_body_size))
.layer(RequestBodyLimitLayer::new(max_body_size));
let router = match cors {
Some(cors) => router.layer(cors),
None => router,
};
router.with_state(state)
}
fn cors_layer(config: Option<&ServerConfig>) -> Option<CorsLayer> {
let config = config.filter(|config| config.origin_validation.cors)?;
let policy = origin_config(Some(config));
Some(
CorsLayer::new()
.allow_origin(AllowOrigin::predicate(move |origin: &HeaderValue, _| {
origin
.to_str()
.is_ok_and(|origin| validate_origin_value(&policy, origin).is_ok())
}))
.allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS])
.allow_headers([
header::ACCEPT,
header::AUTHORIZATION,
header::CONTENT_TYPE,
HeaderName::from_static("last-event-id"),
HeaderName::from_static("mcp-protocol-version"),
HeaderName::from_static("mcp-session-id"),
])
.expose_headers([
HeaderName::from_static("mcp-session-id"),
header::WWW_AUTHENTICATE,
])
.max_age(Duration::from_secs(24 * 60 * 60)),
)
}
async fn route_with_version_tracking<H: McpHandler>(
handler: &H,
request: router::JsonRpcIncoming,
session_manager: &SessionManager,
config: Option<&ServerConfig>,
session_id: Option<&str>,
request_stream: Option<String>,
principal: Option<Principal>,
) -> router::JsonRpcOutgoing {
let (ctx, _pending_guard) = http_request_context(
session_manager,
session_id,
request.id.as_ref(),
request_stream,
principal,
)
.await;
if request.method == "initialize" {
let client_capabilities =
super::client_capabilities_from_initialize_params(request.params.as_ref());
let response = router::route_request_with_config(handler, request, &ctx, config).await;
if let (Some(sid), Some(result)) = (session_id, response.result.as_ref())
&& let Some(version_str) = result.get("protocolVersion").and_then(|v| v.as_str())
{
let version = ProtocolVersion::from(version_str);
session_manager
.set_initialized(sid, version, client_capabilities)
.await;
tracing::debug!(
session_id = sid,
protocol_version = version_str,
"Stored negotiated protocol version for session"
);
}
return response;
}
if let Some(sid) = session_id
&& let Some(version) = session_manager.get_protocol_version(sid).await
{
return router::route_request_versioned(handler, request, &ctx, &version).await;
}
router::route_request_with_config(handler, request, &ctx, config).await
}
async fn http_request_context(
session_manager: &SessionManager,
session_id: Option<&str>,
request_id: Option<&serde_json::Value>,
request_stream: Option<String>,
principal: Option<Principal>,
) -> (RequestContext, Option<PendingHandlerGuard>) {
let mut ctx = RequestContext::http();
let key = request_id.map(jsonrpc_id_key);
if let Some(principal) = principal {
ctx = ctx
.with_user_id(principal.subject.clone())
.with_principal(principal);
}
if let Some(ref key) = key {
ctx = ctx.with_request_id(key.clone());
}
let mut guard = None;
if let Some(session_id) = session_id {
let session = Arc::new(
HttpSessionHandle::new(session_id.to_string(), session_manager.clone())
.with_request_stream(request_stream),
) as Arc<dyn McpSession>;
ctx = ctx
.with_session_id(session_id.to_string())
.with_session(session);
if let Some(key) = key
&& let Some((token, pending_guard)) = session_manager
.register_pending_handler(session_id, key)
.await
{
ctx = ctx.with_cancellation_token(Arc::new(token) as Arc<dyn Cancellable>);
guard = Some(pending_guard);
}
}
(ctx, guard)
}
fn parse_session_id(headers: &HeaderMap) -> Option<String> {
headers
.get("mcp-session-id")
.and_then(|v| v.to_str().ok())
.map(str::to_owned)
}
fn is_length_limit_error(err: &axum::Error) -> bool {
let mut source: Option<&(dyn std::error::Error + 'static)> = Some(err);
while let Some(current) = source {
if current.to_string() == "length limit exceeded" {
return true;
}
source = current.source();
}
false
}
fn session_header_value(session_id: &str) -> HeaderValue {
HeaderValue::from_str(session_id)
.unwrap_or_else(|_| HeaderValue::from_static("invalid-session"))
}
fn to_security_headers(headers: &HeaderMap) -> SecurityHeaders {
headers
.iter()
.filter_map(|(name, value)| {
value
.to_str()
.ok()
.map(|value| (name.as_str().to_string(), value.to_string()))
})
.collect()
}
fn extract_request_ip(
headers: &HeaderMap,
extensions: &axum::http::Extensions,
config: Option<&ServerConfig>,
) -> Option<IpAddr> {
let security_headers = to_security_headers(headers);
let peer_ip = extensions
.get::<axum::extract::ConnectInfo<SocketAddr>>()
.map(|connect_info| connect_info.0.ip());
match peer_ip {
Some(peer) => {
let trusted = config
.map(|c| c.origin_validation.trusted_proxies.as_slice())
.unwrap_or(&[]);
Some(extract_client_ip_with_trust(
&security_headers,
peer,
trusted,
))
}
None => {
extract_client_ip(&security_headers)
}
}
}
fn origin_config(config: Option<&ServerConfig>) -> OriginConfig {
let Some(config) = config else {
return OriginConfig::default();
};
OriginConfig {
allowed_origins: config.origin_validation.allowed_origins.clone(),
allow_localhost: config.origin_validation.allow_localhost,
allow_any: config.origin_validation.allow_any,
allow_missing: config.origin_validation.allow_missing_origin,
}
}
fn validate_origin_header(
headers: &HeaderMap,
client_ip: Option<IpAddr>,
config: Option<&ServerConfig>,
) -> Result<(), StatusCode> {
let security_headers = to_security_headers(headers);
let origin_config = origin_config(config);
let client_ip = client_ip.unwrap_or(IpAddr::from([0, 0, 0, 0]));
validate_origin(&origin_config, &security_headers, client_ip).map_err(|error| {
tracing::warn!(%error, "Rejected HTTP request with invalid origin");
StatusCode::FORBIDDEN
})
}
fn json_response(status: StatusCode, body: JsonRpcOutgoing) -> Response {
(status, axum::Json(body)).into_response()
}
fn empty_response(status: StatusCode) -> Response {
status.into_response()
}
#[allow(dead_code)]
fn sse_event_bytes_inner(id: &str, event_type: Option<&str>, data: &str) -> Bytes {
let capacity = 5 + id.len() + event_type.map_or(0, |t| t.len() + 8) + data.len() + 8;
let mut event = String::with_capacity(capacity);
event.push_str("id: ");
event.push_str(id);
event.push('\n');
if let Some(event_type) = event_type {
event.push_str("event: ");
event.push_str(event_type);
event.push('\n');
}
if data.is_empty() {
event.push_str("data:\n");
} else {
for line in data.split('\n') {
event.push_str("data: ");
event.push_str(line.strip_suffix('\r').unwrap_or(line));
event.push('\n');
}
}
event.push('\n');
Bytes::from(event)
}
#[cfg(not(feature = "internal-bench"))]
pub(crate) fn sse_event_bytes(id: &str, event_type: Option<&str>, data: &str) -> Bytes {
sse_event_bytes_inner(id, event_type, data)
}
#[cfg(feature = "internal-bench")]
#[doc(hidden)]
pub fn sse_event_bytes(id: &str, event_type: Option<&str>, data: &str) -> Bytes {
sse_event_bytes_inner(id, event_type, data)
}
fn validate_protocol_header(
headers: &HeaderMap,
config: Option<&ServerConfig>,
expected: Option<&ProtocolVersion>,
) -> Result<(), StatusCode> {
let Some(raw) = headers.get("mcp-protocol-version") else {
if expected.is_some() {
tracing::debug!(
"Post-init request missing Mcp-Protocol-Version header; continuing with session version"
);
}
return Ok(());
};
let value = raw.to_str().map_err(|_| StatusCode::BAD_REQUEST)?;
let version = ProtocolVersion::from(value);
let protocol_config = config.map(|cfg| cfg.protocol.clone()).unwrap_or_default();
if !protocol_config.is_supported(&version) {
return Err(StatusCode::BAD_REQUEST);
}
if let Some(expected) = expected
&& expected != &version
{
return Err(StatusCode::BAD_REQUEST);
}
Ok(())
}
async fn resolve_session_for_request<H: McpHandler>(
state: &SseState<H>,
headers: &HeaderMap,
method: &str,
owner: Option<&str>,
) -> Result<Option<String>, StatusCode> {
let session_id = parse_session_id(headers);
if method == "initialize" {
if session_id.is_some() {
return Err(StatusCode::BAD_REQUEST);
}
validate_protocol_header(headers, state.config.as_ref(), None)?;
return Ok(None);
}
if method == "ping" && session_id.is_none() {
validate_protocol_header(headers, state.config.as_ref(), None)?;
return Ok(None);
}
let Some(session_id) = session_id else {
return Err(StatusCode::BAD_REQUEST);
};
if !state
.session_manager
.touch_session(&session_id, owner)
.await
{
return Err(StatusCode::NOT_FOUND);
}
let expected = state
.session_manager
.get_protocol_version(&session_id)
.await;
validate_protocol_header(headers, state.config.as_ref(), expected.as_ref())?;
Ok(Some(session_id))
}
async fn resolve_session_for_response<H: McpHandler>(
state: &SseState<H>,
headers: &HeaderMap,
owner: Option<&str>,
) -> Result<String, StatusCode> {
let Some(session_id) = parse_session_id(headers) else {
return Err(StatusCode::BAD_REQUEST);
};
if !state
.session_manager
.touch_session(&session_id, owner)
.await
{
return Err(StatusCode::NOT_FOUND);
}
let expected = state
.session_manager
.get_protocol_version(&session_id)
.await;
validate_protocol_header(headers, state.config.as_ref(), expected.as_ref())?;
Ok(session_id)
}
async fn handle_client_json_rpc_response<H: McpHandler>(
state: &SseState<H>,
headers: &HeaderMap,
response: CoreJsonRpcResponse,
owner: Option<&str>,
) -> Response {
let session_id = match resolve_session_for_response(state, headers, owner).await {
Ok(session_id) => session_id,
Err(status) => return empty_response(status),
};
match state
.session_manager
.complete_pending_server_response(&session_id, response)
.await
{
Ok(()) => empty_response(StatusCode::ACCEPTED),
Err(status) => empty_response(status),
}
}
fn accepts_event_stream(headers: &HeaderMap) -> bool {
let Some(accept) = headers.get(header::ACCEPT).and_then(|v| v.to_str().ok()) else {
return false;
};
accept.split(',').any(|entry| {
let mime = entry.split(';').next().unwrap_or("").trim();
mime.eq_ignore_ascii_case("text/event-stream")
|| mime.eq_ignore_ascii_case("text/*")
|| mime == "*/*"
})
}
fn dispatch_request<H: McpHandler>(
state: &SseState<H>,
request: JsonRpcIncoming,
session_id: Option<String>,
request_stream: Option<String>,
principal: Option<Principal>,
) -> tokio::task::JoinHandle<JsonRpcOutgoing> {
let handler = state.handler.clone();
let session_manager = state.session_manager.clone();
let config = state.config.clone();
tokio::spawn(async move {
let response = route_with_version_tracking(
&handler,
request,
&session_manager,
config.as_ref(),
session_id.as_deref(),
request_stream,
principal,
)
.await;
if let Some(ref session_id) = session_id
&& let Some(clock) = session_manager.idle_clock(session_id).await
{
clock.reset();
}
response
})
}
fn dispatch_outcome(
outcome: Result<JsonRpcOutgoing, tokio::task::JoinError>,
id: Option<serde_json::Value>,
method: &str,
) -> JsonRpcOutgoing {
match outcome {
Ok(response) => response,
Err(error) => {
tracing::error!(
method = %method,
%error,
"Handler panicked"
);
if id.is_none() {
return JsonRpcOutgoing::notification_ack();
}
JsonRpcOutgoing::error(
id,
McpError::internal(format!("Handler panicked while serving {method}")),
)
}
}
}
fn sse_response(session_id: &str, body: Body) -> Response {
let mut response = Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/event-stream")
.header(header::CACHE_CONTROL, "no-cache")
.body(body)
.expect("SSE response builder should be valid");
response
.headers_mut()
.insert("mcp-session-id", session_header_value(session_id));
response
}
fn stream_events(
mut rx: mpsc::Receiver<SseEvent>,
activity: ResetOnDrop,
) -> impl futures::Stream<Item = Result<Bytes, std::convert::Infallible>> {
async_stream::stream! {
let _activity = activity;
loop {
match tokio::time::timeout(Duration::from_secs(SSE_KEEP_ALIVE_SECS), rx.recv()).await {
Ok(Some((id, data))) => {
yield Ok(sse_event_bytes(&id, Some("message"), &data));
}
Ok(None) => break,
Err(_) => yield Ok(Bytes::from_static(b": keep-alive\n\n")),
}
}
}
}
async fn stream_json_rpc<H: McpHandler>(
state: SseState<H>,
request: JsonRpcIncoming,
session_id: String,
principal: Option<Principal>,
) -> Response {
let method = request.method.clone();
let request_id = request.id.clone();
let Some((stream_id, primer_id, mut stream_rx)) = state
.session_manager
.open_stream(&session_id, StreamKind::Pending)
.await
else {
return empty_response(StatusCode::NOT_FOUND);
};
let dispatch = dispatch_request(
&state,
request,
Some(session_id.clone()),
Some(stream_id.clone()),
principal,
);
let (unstreamed_tx, mut unstreamed_rx) = oneshot::channel();
let session_manager = state.session_manager.clone();
let finish_session_id = session_id.clone();
tokio::spawn(async move {
let response = dispatch_outcome(dispatch.await, request_id, &method);
let payload = if response.should_send() {
serde_json::to_string(&response).ok()
} else {
None
};
if !session_manager
.finish_stream(&finish_session_id, &stream_id, payload.as_deref())
.await
{
let _ = unstreamed_tx.send(response);
}
});
let unstreamed = |response: Result<JsonRpcOutgoing, oneshot::error::RecvError>| match response {
Ok(response) if response.should_send() => json_response(StatusCode::OK, response),
Ok(_) => empty_response(StatusCode::ACCEPTED),
Err(_) => empty_response(StatusCode::INTERNAL_SERVER_ERROR),
};
let first = tokio::select! {
biased;
first = stream_rx.recv() => first,
response = &mut unstreamed_rx => return unstreamed(response),
};
let Some(first) = first else {
return unstreamed(unstreamed_rx.await);
};
let opening = [
Bytes::from_static(b": connected\n\n"),
sse_event_bytes(&primer_id, None, ""),
sse_event_bytes(&first.0, Some("message"), &first.1),
];
let activity = ResetOnDrop(state.session_manager.idle_clock(&session_id).await);
let stream = futures::stream::iter(opening.map(Ok)).chain(stream_events(stream_rx, activity));
sse_response(&session_id, Body::from_stream(stream))
}
fn admit_request<H: McpHandler>(
state: &SseState<H>,
parts: &axum::http::request::Parts,
) -> Result<Option<IpAddr>, StatusCode> {
let client_ip = extract_request_ip(&parts.headers, &parts.extensions, state.config.as_ref());
validate_origin_header(&parts.headers, client_ip, state.config.as_ref())?;
Ok(client_ip)
}
async fn authorize_request<H: McpHandler>(
state: &SseState<H>,
headers: &HeaderMap,
) -> Result<Option<Principal>, Box<Response>> {
let Some(authorization) = state
.config
.as_ref()
.and_then(|config| config.authorization.as_ref())
else {
return Ok(None);
};
let credentials = headers
.get(header::AUTHORIZATION)
.and_then(|value| value.to_str().ok());
match authorization.authorize(credentials).await {
Ok(principal) => Ok(Some(principal)),
Err(rejection) => {
let status = match rejection {
crate::BearerRejection::InsufficientScope { .. } => StatusCode::FORBIDDEN,
crate::BearerRejection::InvalidToken(_) => StatusCode::UNAUTHORIZED,
};
tracing::debug!(%rejection, "Refused an HTTP request");
let mut response = empty_response(status);
if let Ok(challenge) =
HeaderValue::from_str(&authorization.challenge(&rejection, credentials.is_some()))
{
response
.headers_mut()
.insert(header::WWW_AUTHENTICATE, challenge);
}
Err(Box::new(response))
}
}
}
async fn protected_resource_metadata<H: McpHandler>(
axum::extract::State(state): axum::extract::State<SseState<H>>,
) -> Response {
match state
.config
.as_ref()
.and_then(|config| config.authorization.as_ref())
{
Some(authorization) => axum::Json(authorization.metadata_document()).into_response(),
None => empty_response(StatusCode::NOT_FOUND),
}
}
async fn handle_json_rpc<H: McpHandler>(
axum::extract::State(state): axum::extract::State<SseState<H>>,
request: axum::http::Request<Body>,
) -> Response {
let (parts, body) = request.into_parts();
let client_ip = match admit_request(&state, &parts) {
Ok(client_ip) => client_ip,
Err(status) => return empty_response(status),
};
let principal = match authorize_request(&state, &parts.headers).await {
Ok(principal) => principal,
Err(response) => return *response,
};
let owner = principal.as_ref().map(|p| p.subject.clone());
let headers = parts.headers;
if let Some(ref limiter) = state.rate_limiter {
let client_id = client_ip.map(|ip| ip.to_string());
if !limiter.check(client_id.as_deref()) {
tracing::warn!("Rate limit exceeded for HTTP client");
return empty_response(StatusCode::TOO_MANY_REQUESTS);
}
}
let max_body_size = state
.config
.as_ref()
.map_or(MAX_BODY_SIZE, |config| config.max_message_size);
if let Some(declared_len) = headers
.get(axum::http::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<usize>().ok())
&& declared_len > max_body_size
{
return empty_response(StatusCode::PAYLOAD_TOO_LARGE);
}
let payload = match to_bytes(body, max_body_size).await {
Ok(body) => match serde_json::from_slice::<serde_json::Value>(&body) {
Ok(payload) => payload,
Err(_) => return empty_response(StatusCode::BAD_REQUEST),
},
Err(err) => {
let status = if is_length_limit_error(&err) {
StatusCode::PAYLOAD_TOO_LARGE
} else {
StatusCode::BAD_REQUEST
};
return empty_response(status);
}
};
if let Ok(response) = serde_json::from_value::<CoreJsonRpcResponse>(payload.clone()) {
return handle_client_json_rpc_response(&state, &headers, response, owner.as_deref()).await;
}
let request = match serde_json::from_value::<JsonRpcIncoming>(payload) {
Ok(request) => request,
Err(_) => return empty_response(StatusCode::BAD_REQUEST),
};
let is_initialize = request.method == "initialize";
let client_capabilities = if is_initialize {
Some(super::client_capabilities_from_initialize_params(
request.params.as_ref(),
))
} else {
None
};
let session_id = match resolve_session_for_request(
&state,
&headers,
&request.method,
owner.as_deref(),
)
.await
{
Ok(session_id) => session_id,
Err(status) => return empty_response(status),
};
if is_initialize && !state.session_manager.admit_session().await {
tracing::warn!("HTTP session cap reached; refusing initialize");
return empty_response(StatusCode::SERVICE_UNAVAILABLE);
}
if request.method == "notifications/cancelled" {
if let (Some(session_id), Some(cancelled_id)) = (
session_id.as_deref(),
request.params.as_ref().and_then(|p| p.get("requestId")),
) {
let key = jsonrpc_id_key(cancelled_id);
let reason = request
.params
.as_ref()
.and_then(|p| p.get("reason"))
.and_then(|r| r.as_str())
.unwrap_or("client requested cancellation");
if state
.session_manager
.cancel_pending_handler(session_id, &key)
.await
{
tracing::debug!(
request_id = %key,
reason = %reason,
"Cancelling in-flight handler",
);
}
}
return empty_response(StatusCode::ACCEPTED);
}
if request.id.is_some()
&& !is_initialize
&& let Some(session_id) = session_id.clone()
&& accepts_event_stream(&headers)
{
return stream_json_rpc(state, request, session_id, principal).await;
}
let initialize_request_id = request.id.clone();
let method = request.method.clone();
let request_id = request.id.clone();
let response = dispatch_outcome(
dispatch_request(&state, request, session_id.clone(), None, principal).await,
request_id,
&method,
);
if !response.should_send() {
return empty_response(StatusCode::ACCEPTED);
}
if is_initialize
&& let Some(result) = response.result.as_ref()
&& let Some(version_str) = result.get("protocolVersion").and_then(|v| v.as_str())
{
let session_id = state
.session_manager
.create_session(initialize_request_id.as_ref())
.await;
state.session_manager.bind_owner(&session_id, owner).await;
state
.session_manager
.set_initialized(
&session_id,
ProtocolVersion::from(version_str),
client_capabilities.unwrap_or_default(),
)
.await;
let mut response = json_response(StatusCode::OK, response);
response
.headers_mut()
.insert("mcp-session-id", session_header_value(&session_id));
return response;
}
json_response(StatusCode::OK, response)
}
async fn handle_sse<H: McpHandler>(
axum::extract::State(state): axum::extract::State<SseState<H>>,
request: axum::http::Request<Body>,
) -> Response {
let (parts, _) = request.into_parts();
if let Err(status) = admit_request(&state, &parts) {
return empty_response(status);
}
let owner = match authorize_request(&state, &parts.headers).await {
Ok(principal) => principal.map(|p| p.subject),
Err(response) => return *response,
};
let headers = parts.headers;
let session_id = match parse_session_id(&headers) {
Some(session_id) => session_id,
None => return empty_response(StatusCode::BAD_REQUEST),
};
if !state
.session_manager
.touch_session(&session_id, owner.as_deref())
.await
{
return empty_response(StatusCode::NOT_FOUND);
}
let expected = state
.session_manager
.get_protocol_version(&session_id)
.await;
if validate_protocol_header(&headers, state.config.as_ref(), expected.as_ref()).is_err() {
return empty_response(StatusCode::BAD_REQUEST);
}
let resumed = match headers.get("last-event-id").and_then(|v| v.to_str().ok()) {
Some(last_event_id) => {
state
.session_manager
.resume_stream(&session_id, last_event_id)
.await
}
None => None,
};
let mut opening = vec![Bytes::from_static(b": connected\n\n")];
let rx = match resumed {
Some((replay, rx)) => {
opening.extend(
replay
.iter()
.map(|(id, data)| sse_event_bytes(id, Some("message"), data)),
);
rx
}
None => match state
.session_manager
.open_stream(&session_id, StreamKind::Listening)
.await
{
Some((_, primer_id, rx)) => {
opening.push(sse_event_bytes(&primer_id, None, ""));
rx
}
None => return empty_response(StatusCode::NOT_FOUND),
},
};
let activity = ResetOnDrop(state.session_manager.idle_clock(&session_id).await);
let events = stream_events(rx, activity).take_until(state.shutdown.clone().cancelled_owned());
let stream = futures::stream::iter(opening.into_iter().map(Ok)).chain(events);
sse_response(&session_id, Body::from_stream(stream))
}
async fn handle_delete_session<H: McpHandler>(
axum::extract::State(state): axum::extract::State<SseState<H>>,
request: axum::http::Request<Body>,
) -> Response {
let (parts, _) = request.into_parts();
if let Err(status) = admit_request(&state, &parts) {
return empty_response(status);
}
let owner = match authorize_request(&state, &parts.headers).await {
Ok(principal) => principal.map(|p| p.subject),
Err(response) => return *response,
};
let headers = parts.headers;
let Some(session_id) = parse_session_id(&headers) else {
return empty_response(StatusCode::BAD_REQUEST);
};
if !state
.session_manager
.touch_session(&session_id, owner.as_deref())
.await
{
return empty_response(StatusCode::NOT_FOUND);
}
let expected = state
.session_manager
.get_protocol_version(&session_id)
.await;
if validate_protocol_header(&headers, state.config.as_ref(), expected.as_ref()).is_err() {
return empty_response(StatusCode::BAD_REQUEST);
}
if state.session_manager.remove_session(&session_id).await {
return empty_response(StatusCode::NO_CONTENT);
}
empty_response(StatusCode::NOT_FOUND)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
use tower::ServiceExt;
use turbomcp_core::context::RequestContext as CoreRequestContext;
use turbomcp_types::{
Prompt, PromptResult, Resource, ResourceResult, ServerInfo, Tool, ToolResult,
};
#[derive(Clone)]
struct TestHandler;
impl McpHandler for TestHandler {
fn server_info(&self) -> ServerInfo {
ServerInfo::new("test", "1.0.0")
}
fn list_tools(&self) -> Vec<Tool> {
Vec::new()
}
fn list_resources(&self) -> Vec<Resource> {
Vec::new()
}
fn list_prompts(&self) -> Vec<Prompt> {
Vec::new()
}
async fn call_tool(
&self,
name: &str,
_args: Value,
_ctx: &CoreRequestContext,
) -> McpResult<ToolResult> {
Err(McpError::tool_not_found(name))
}
async fn read_resource(
&self,
uri: &str,
_ctx: &CoreRequestContext,
) -> McpResult<ResourceResult> {
Err(McpError::resource_not_found(uri))
}
async fn get_prompt(
&self,
name: &str,
_args: Option<Value>,
_ctx: &CoreRequestContext,
) -> McpResult<PromptResult> {
Err(McpError::prompt_not_found(name))
}
}
#[test]
fn sse_event_bytes_formats_multiline_data_and_strips_crlf() {
let event = sse_event_bytes("session-stream-1", Some("message"), "one\r\ntwo\nthree");
let text = std::str::from_utf8(event.as_ref()).expect("valid utf8");
assert_eq!(
text,
"id: session-stream-1\nevent: message\ndata: one\ndata: two\ndata: three\n\n"
);
}
#[test]
fn sse_event_bytes_formats_empty_data_without_retry() {
let event = sse_event_bytes("session-stream-1", None, "");
let text = std::str::from_utf8(event.as_ref()).expect("valid utf8");
assert_eq!(text, "id: session-stream-1\ndata:\n\n");
assert!(!text.contains("retry:"));
}
#[tokio::test]
async fn send_to_session_routes_to_single_subscriber() {
let manager = SessionManager::new();
let session_id = manager.create_session(None).await;
let mut rx1 = manager
.subscribe_session(&session_id)
.await
.expect("first subscribe");
let mut rx2 = manager
.subscribe_session(&session_id)
.await
.expect("second subscribe");
assert!(manager.send_to_session(&session_id, "hello").await);
let first = tokio::time::timeout(std::time::Duration::from_millis(100), rx1.recv()).await;
let second = tokio::time::timeout(std::time::Duration::from_millis(100), rx2.recv()).await;
let first_got = matches!(first, Ok(Some((_, ref data))) if &**data == "hello");
let second_got = matches!(second, Ok(Some((_, ref data))) if &**data == "hello");
assert!(
first_got ^ second_got,
"message must reach exactly one subscriber, got first={first:?}, second={second:?}"
);
}
async fn serve_for_shutdown(
drain: Option<Duration>,
) -> (
String,
oneshot::Sender<()>,
tokio::task::JoinHandle<McpResult<()>>,
) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}", listener.local_addr().unwrap());
let shutdown = CancellationToken::new();
let app = build_router_with_shutdown(TestHandler, None, None, shutdown.clone());
let (signal, signalled) = oneshot::channel::<()>();
let server = tokio::spawn(serve_until(
listener,
app,
shutdown,
async move {
let _ = signalled.await;
},
drain,
));
(base, signal, server)
}
#[tokio::test]
async fn shutdown_ends_listening_streams() {
let (base, signal, server) = serve_for_shutdown(None).await;
let client = reqwest::Client::new();
let initialized = client
.post(format!("{base}/mcp"))
.header(header::ACCEPT, "application/json, text/event-stream")
.json(&serde_json::json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "t", "version": "1" }
}
}))
.send()
.await
.unwrap();
let session = initialized.headers()["mcp-session-id"]
.to_str()
.unwrap()
.to_owned();
let listening = client
.get(format!("{base}/mcp"))
.header(header::ACCEPT, "text/event-stream")
.header("mcp-session-id", &session)
.header("mcp-protocol-version", "2025-11-25")
.send()
.await
.unwrap();
assert_eq!(listening.status(), StatusCode::OK);
signal.send(()).unwrap();
let finished = tokio::time::timeout(Duration::from_secs(5), server)
.await
.expect("the server stops once its listening streams end");
finished.unwrap().unwrap();
tokio::time::timeout(Duration::from_secs(5), listening.bytes())
.await
.expect("the stream ended")
.unwrap();
}
#[tokio::test]
async fn the_drain_is_bounded() {
use tokio::io::AsyncWriteExt;
let (base, signal, server) = serve_for_shutdown(Some(Duration::from_millis(200))).await;
let mut stuck = tokio::net::TcpStream::connect(base.trim_start_matches("http://"))
.await
.unwrap();
stuck
.write_all(
b"POST /mcp HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\n\
Content-Length: 100\r\n\r\n{\"jsonrpc\":",
)
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
let started = Instant::now();
signal.send(()).unwrap();
tokio::time::timeout(Duration::from_secs(5), server)
.await
.expect("the drain stops at its deadline")
.unwrap()
.unwrap();
assert!(started.elapsed() >= Duration::from_millis(200));
}
#[tokio::test]
async fn build_router_uses_configured_http_body_limit() {
let config = ServerConfig::builder()
.max_message_size(1024)
.allow_any_origin(true)
.build();
let app = build_router(TestHandler, None, Some(config));
let request = axum::http::Request::builder()
.method("POST")
.uri("/mcp")
.header(axum::http::header::CONTENT_TYPE, "application/json")
.body(Body::from("x".repeat(2048)))
.expect("request");
let response = app.oneshot(request).await.expect("response");
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
async fn in_flight_stream(
manager: &SessionManager,
session_id: &str,
) -> (String, mpsc::Receiver<SseEvent>) {
let (stream_id, _, rx) = manager
.open_stream(session_id, StreamKind::Pending)
.await
.expect("session exists");
assert!(manager.send_to_stream(session_id, &stream_id, "{}").await);
(stream_id, rx)
}
async fn stream_ids(manager: &SessionManager, session_id: &str) -> Vec<String> {
manager.sessions.read().await[session_id]
.streams
.iter()
.map(|(id, _)| id.clone())
.collect()
}
#[tokio::test]
async fn eviction_spares_in_flight_streams_and_the_newest_listener() {
let manager = SessionManager::new();
let session_id = manager.create_session(None).await;
let (listener, _listener_rx) = {
let (id, _, rx) = manager
.open_stream(&session_id, StreamKind::Listening)
.await
.unwrap();
(id, rx)
};
let mut in_flight = Vec::new();
for _ in 0..MAX_RETAINED_STREAMS + 3 {
in_flight.push(in_flight_stream(&manager, &session_id).await);
}
let ids = stream_ids(&manager, &session_id).await;
assert!(
ids.contains(&listener),
"the only listener is never evicted"
);
for (id, _) in &in_flight {
assert!(ids.contains(id), "an in-flight stream is never evicted");
}
}
#[tokio::test]
async fn answered_streams_are_evicted_before_anything_live() {
let manager = SessionManager::new();
let session_id = manager.create_session(None).await;
let (listener, _, _listener_rx) = manager
.open_stream(&session_id, StreamKind::Listening)
.await
.unwrap();
let mut answered = Vec::new();
for _ in 0..MAX_RETAINED_STREAMS {
let (id, rx) = in_flight_stream(&manager, &session_id).await;
assert!(manager.finish_stream(&session_id, &id, Some("{}")).await);
drop(rx);
answered.push(id);
}
let mut pending = Vec::new();
for _ in 0..MAX_RETAINED_STREAMS {
let (id, _, rx) = manager
.open_stream(&session_id, StreamKind::Pending)
.await
.unwrap();
pending.push((id, rx));
}
let (live, _live_rx) = in_flight_stream(&manager, &session_id).await;
let ids = stream_ids(&manager, &session_id).await;
assert!(ids.contains(&listener));
assert!(ids.contains(&live));
assert!(pending.iter().all(|(id, _)| ids.contains(id)));
assert!(!ids.contains(&answered[0]) && !ids.contains(&answered[1]));
assert!(answered[2..].iter().all(|id| ids.contains(id)));
}
#[test]
fn a_trusted_proxy_is_read_from_a_real_header_map() {
let config = ServerConfig::builder()
.origin_validation(crate::config::OriginValidationConfig {
trusted_proxies: vec!["127.0.0.1".to_string()],
..Default::default()
})
.build();
let mut headers = HeaderMap::new();
headers.insert("X-Forwarded-For", HeaderValue::from_static("203.0.113.7"));
let mut extensions = axum::http::Extensions::new();
extensions.insert(axum::extract::ConnectInfo(SocketAddr::from((
[127, 0, 0, 1],
4000,
))));
assert_eq!(
extract_request_ip(&headers, &extensions, Some(&config)),
Some("203.0.113.7".parse().unwrap())
);
let mut extensions = axum::http::Extensions::new();
extensions.insert(axum::extract::ConnectInfo(SocketAddr::from((
[198, 51, 100, 1],
4000,
))));
assert_eq!(
extract_request_ip(&headers, &extensions, Some(&config)),
Some("198.51.100.1".parse().unwrap())
);
}
}