use std::collections::HashMap;
use std::sync::Arc;
use std::task::{Context, Poll};
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tower::Service;
use turbomcp_core::{
CancellationToken, JsonRpcError, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest,
JsonRpcResponse, McpError, ProtocolVersion, RequestContext, RequestId, meta,
};
use turbomcp_protocol::neutral::CachePolicy;
use turbomcp_protocol::{methods, version};
use turbomcp_service::{ProtocolError, mcp_to_jsonrpc_error, mcp_to_jsonrpc_error_for};
use crate::extension::{Extension, ExtensionRequest};
use crate::inflight::InFlightRegistry;
use crate::mrtr::{PendingRequests, StateSigner};
use crate::router::MethodRouter;
use crate::session::{SessionBackend, SessionStore};
use crate::subscriptions::{ServerNotifier, SubscriptionRegistry};
use crate::tasks::{TaskBackend, TaskStore};
use crate::traits::McpServerCore;
mod augment;
mod capability;
mod handshake;
mod legacy_tasks;
mod listen;
mod params;
use augment::try_augment_call;
use capability::{DraftWire, Legacy0618Wire, LegacyWire, dispatch_capability};
use handshake::{discover_response, handle_initialize};
use legacy_tasks::{
handle_tasks_method, has_task_field, legacy_list_tools_with_task_support, task_augmented_call,
};
use listen::handle_subscriptions_listen;
use params::{
build_context, extract_log_level, legacy_context, parse_set_level_params, parse_uri_param,
};
pub struct VersionDispatcher<S> {
server: S,
router: Arc<MethodRouter<S>>,
supported: Vec<ProtocolVersion>,
shared: Shared,
}
#[derive(Clone)]
struct Shared {
sessions: Arc<dyn SessionBackend>,
tasks: Option<Arc<dyn TaskBackend>>,
inflight: Arc<InFlightRegistry>,
subs: Arc<SubscriptionRegistry>,
signer: Arc<StateSigner>,
pending: Arc<PendingRequests>,
extensions: Arc<Vec<Arc<dyn Extension>>>,
strict_elicitation_keys: bool,
cache: CachePolicies,
visibility: crate::visibility::Policy,
header_params: Arc<tokio::sync::OnceCell<HashMap<String, Vec<HeaderParam>>>>,
}
#[derive(Clone, Debug)]
struct HeaderParam {
header: String,
path: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CachePolicies {
pub(crate) tools_list: CachePolicy,
pub(crate) resources_list: CachePolicy,
pub(crate) resource_templates_list: CachePolicy,
pub(crate) resources_read: CachePolicy,
pub(crate) prompts_list: CachePolicy,
pub(crate) discover: CachePolicy,
}
impl CachePolicies {
#[must_use]
pub fn uniform(policy: CachePolicy) -> Self {
Self {
tools_list: policy,
resources_list: policy,
resource_templates_list: policy,
resources_read: policy,
prompts_list: policy,
discover: policy,
}
}
#[must_use]
pub fn tools_list(mut self, policy: CachePolicy) -> Self {
self.tools_list = policy;
self
}
#[must_use]
pub fn resources_list(mut self, policy: CachePolicy) -> Self {
self.resources_list = policy;
self
}
#[must_use]
pub fn resource_templates_list(mut self, policy: CachePolicy) -> Self {
self.resource_templates_list = policy;
self
}
#[must_use]
pub fn resources_read(mut self, policy: CachePolicy) -> Self {
self.resources_read = policy;
self
}
#[must_use]
pub fn prompts_list(mut self, policy: CachePolicy) -> Self {
self.prompts_list = policy;
self
}
#[must_use]
pub fn discover(mut self, policy: CachePolicy) -> Self {
self.discover = policy;
self
}
}
impl Default for CachePolicies {
fn default() -> Self {
Self::uniform(CachePolicy::NO_CACHE)
}
}
impl From<CachePolicy> for CachePolicies {
fn from(policy: CachePolicy) -> Self {
Self::uniform(policy)
}
}
impl Shared {
async fn sweep_idle_sessions(&self) {
for id in self.sessions.sweep_expired().await {
self.subs.legacy_remove(&id);
}
}
async fn terminate_session(&self, id: &str) -> bool {
let existed = self.sessions.remove(id).await;
self.subs.legacy_remove(id);
existed
}
}
#[derive(Clone)]
pub struct DispatcherSessionTerminator {
shared: Shared,
}
impl turbomcp_service::SessionTerminator for DispatcherSessionTerminator {
fn terminate<'a>(&'a self, session_id: &'a str) -> turbomcp_service::TerminateFuture<'a> {
Box::pin(self.shared.terminate_session(session_id))
}
}
impl<S: Clone> Clone for VersionDispatcher<S> {
fn clone(&self) -> Self {
Self {
server: self.server.clone(),
router: Arc::clone(&self.router),
supported: self.supported.clone(),
shared: self.shared.clone(),
}
}
}
impl<S: McpServerCore> VersionDispatcher<S> {
#[must_use]
pub fn new(server: S, router: MethodRouter<S>) -> Self {
let supported = server.supported_versions().to_vec();
Self {
server,
router: Arc::new(router),
supported,
shared: Shared {
sessions: Arc::new(SessionStore::default()),
tasks: None,
inflight: Arc::new(InFlightRegistry::default()),
subs: Arc::new(SubscriptionRegistry::default()),
signer: Arc::new(StateSigner::new()),
pending: Arc::new(PendingRequests::default()),
extensions: Arc::new(Vec::new()),
strict_elicitation_keys: false,
cache: CachePolicies::default(),
visibility: None,
header_params: Arc::new(tokio::sync::OnceCell::new()),
},
}
}
#[must_use]
pub fn with_visibility(mut self, policy: Arc<dyn crate::VisibilityPolicy>) -> Self {
self.shared.visibility = Some(policy);
self
}
#[must_use]
pub fn notifier(&self) -> ServerNotifier {
ServerNotifier::new(Arc::clone(&self.shared.subs))
}
#[must_use]
pub fn session_terminator(&self) -> DispatcherSessionTerminator {
DispatcherSessionTerminator {
shared: self.shared.clone(),
}
}
pub async fn close_subscriptions(&self) {
self.shared.subs.close_all().await;
}
#[must_use]
pub fn strict_elicitation_keys(mut self) -> Self {
self.shared.strict_elicitation_keys = true;
self
}
#[must_use]
pub fn with_cache_policy(mut self, cache: impl Into<CachePolicies>) -> Self {
self.shared.cache = cache.into();
self
}
#[must_use]
pub fn with_state_key(mut self, key: [u8; 32]) -> Self {
self.shared.signer = Arc::new(StateSigner::from_key(key));
self
}
#[must_use]
pub fn with_task_support(mut self) -> Self {
self.shared.tasks = Some(Arc::new(TaskStore::default()));
self
}
#[must_use]
pub fn with_extension(mut self, extension: Arc<dyn Extension>) -> Self {
let mut extensions = Vec::clone(&self.shared.extensions);
extensions.push(extension);
self.shared.extensions = Arc::new(extensions);
self
}
#[must_use]
pub fn with_session_idle_timeout(mut self, timeout: std::time::Duration) -> Self {
self.shared.sessions = Arc::new(
SessionStore::with_capacity(SessionStore::DEFAULT_CAPACITY)
.with_idle_timeout(Some(timeout)),
);
self
}
#[must_use]
pub fn with_session_backend(mut self, backend: Arc<dyn SessionBackend>) -> Self {
self.shared.sessions = backend;
self
}
#[must_use]
pub fn with_task_backend(mut self, backend: Arc<dyn TaskBackend>) -> Self {
self.shared.tasks = Some(backend);
self
}
}
impl<S: McpServerCore> Service<JsonRpcMessage> for VersionDispatcher<S> {
type Response = Option<JsonRpcMessage>;
type Error = ProtocolError;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, msg: JsonRpcMessage) -> Self::Future {
let server = self.server.clone();
let router = Arc::clone(&self.router);
let supported = self.supported.clone();
let shared = self.shared.clone();
Box::pin(async move { handle(server, router, supported, shared, msg).await })
}
}
async fn handle<S: McpServerCore>(
server: S,
router: Arc<MethodRouter<S>>,
supported: Vec<ProtocolVersion>,
shared: Shared,
msg: JsonRpcMessage,
) -> Result<Option<JsonRpcMessage>, ProtocolError> {
if !msg.has_valid_version() {
return Ok(match msg {
JsonRpcMessage::Request(req) => Some(
JsonRpcResponse::error(
req.id,
JsonRpcError {
code: -32600,
message: "invalid jsonrpc version (expected \"2.0\")".to_owned(),
data: None,
},
)
.into(),
),
JsonRpcMessage::Notification(_) | JsonRpcMessage::Response(_) => None,
});
}
match msg {
JsonRpcMessage::Request(req) => {
let cancel = CancellationToken::new();
let _guard = connection_id(req.params.as_ref())
.map(|conn| shared.inflight.register(conn, &req.id, cancel.clone()));
if req.method == methods::request::SUBSCRIPTIONS_LISTEN {
return handle_subscriptions_listen(
&router,
&supported,
&shared.subs,
&shared.extensions,
&req,
&cancel,
)
.await;
}
let dispatch =
handle_request(server, &router, &supported, &shared, req, cancel.clone());
tokio::select! {
() = cancel.cancelled() => Ok(None),
out = dispatch => Ok(Some(out?)),
}
}
JsonRpcMessage::Notification(n) => {
handle_notification(&shared.inflight, &shared.subs, &n);
Ok(None)
}
JsonRpcMessage::Response(resp) => {
if !shared.pending.complete(resp) {
tracing::debug!("ignoring unsolicited client->server response");
}
Ok(None)
}
}
}
#[derive(Deserialize)]
struct RawCancelledParams {
#[serde(rename = "requestId")]
request_id: RequestId,
#[serde(default)]
reason: Option<String>,
}
fn handle_notification(
inflight: &InFlightRegistry,
subs: &SubscriptionRegistry,
n: &JsonRpcNotification,
) {
match n.method.as_str() {
methods::notification::CANCELLED => {
let Some(conn) = connection_id(n.params.as_ref()) else {
tracing::debug!("notifications/cancelled without a connection; ignored");
return;
};
let Some(parsed) = n
.params
.as_ref()
.and_then(|p| serde_json::from_value::<RawCancelledParams>(p.clone()).ok())
else {
tracing::debug!("malformed notifications/cancelled; ignored");
return;
};
let fired = inflight.cancel(conn, &parsed.request_id);
let unsubscribed = subs.remove(conn, &parsed.request_id);
tracing::debug!(
request_id = ?parsed.request_id,
reason = parsed.reason.as_deref().unwrap_or(""),
fired,
unsubscribed,
"notifications/cancelled"
);
}
methods::notification::INITIALIZED => {
tracing::debug!("received notifications/initialized");
}
other => tracing::debug!(method = other, "unhandled notification"),
}
}
async fn handle_request<S: McpServerCore>(
server: S,
router: &MethodRouter<S>,
supported: &[ProtocolVersion],
shared: &Shared,
req: JsonRpcRequest,
cancel: CancellationToken,
) -> Result<JsonRpcMessage, ProtocolError> {
let Shared {
sessions,
tasks,
subs,
..
} = shared;
let id = req.id.clone();
let method = req.method.clone();
if let Some(ext) = shared
.extensions
.iter()
.find(|e| e.methods().contains(&method.as_str()))
.cloned()
&& matches!(
classify_version(req.params.as_ref(), supported),
VersionRoute::Modern
)
{
let ctx = build_context(&req);
if !context_declares_extension(&ctx, ext.id()) {
return Ok(error_response(id, &McpError::method_not_found(method)));
}
let connection_id = connection_id(req.params.as_ref()).map(str::to_owned);
return Ok(ext
.dispatch(ExtensionRequest {
request: req,
context: ctx,
connection_id,
})
.await);
}
if REMOVED_IN_STATELESS.contains(&method.as_str())
&& version::request_protocol_version(req.params.as_ref())
.is_some_and(|v| v == ProtocolVersion::V2026_07_28)
{
return Ok(error_response(id, &McpError::method_not_found(method)));
}
match method.as_str() {
methods::request::DISCOVER => {
if let Some(field) = meta::missing_request_envelope_field(req.params.as_ref()) {
return Ok(invalid_envelope(id, field, supported));
}
Ok(discover_response(
id,
&server,
router,
supported,
&shared.extensions,
shared.cache.discover,
))
}
methods::request::PING => Ok(JsonRpcResponse::success(id, serde_json::json!({})).into()),
methods::request::INITIALIZE => {
shared.sweep_idle_sessions().await;
let tasks_enabled = tasks.is_some() && router.has_tools();
let reply = handle_initialize(
&server,
router,
supported,
sessions.as_ref(),
tasks_enabled,
&req,
)
.await;
if matches!(&reply, JsonRpcMessage::Response(r) if r.error.is_none())
&& let Some(sid) = session_id(req.params.as_ref())
{
subs.legacy_touch(sid, connection_id(req.params.as_ref()));
}
Ok(reply)
}
methods::request::TOOLS_LIST
| methods::request::TOOLS_CALL
| methods::request::RESOURCES_LIST
| methods::request::RESOURCES_TEMPLATES_LIST
| methods::request::RESOURCES_READ
| methods::request::PROMPTS_LIST
| methods::request::PROMPTS_GET
| methods::request::COMPLETION_COMPLETE => {
match classify_version(req.params.as_ref(), supported) {
VersionRoute::Modern => {
let mut ctx = build_context(&req);
ctx.cancellation = cancel;
match extract_log_level(req.params.as_ref()) {
Ok(level) => ctx.log_level = level,
Err(e) => return Ok(error_response(id, &e)),
}
if method == methods::request::TOOLS_CALL
&& let Some(resp) =
try_augment_call(&server, router, &req, &ctx, &shared.extensions, &id)
.await
{
return Ok(resp);
}
Ok(
dispatch_capability::<S, DraftWire>(server, router, &req, &ctx, shared, id)
.await,
)
}
VersionRoute::Legacy(revision) => {
let mut ctx = match legacy_context(sessions.as_ref(), &req).await? {
Ok(ctx) => ctx,
Err(response) => return Ok(response),
};
ctx.cancellation = cancel;
if let Some(sid) = session_id(req.params.as_ref()) {
subs.legacy_touch(sid, connection_id(req.params.as_ref()));
}
if let Some(store) = tasks.as_ref().filter(|_| revision.has_tasks()) {
if method == methods::request::TOOLS_CALL
&& has_task_field(req.params.as_ref())
{
return Ok(
task_augmented_call(server, router, store, ctx, &req, id).await
);
}
if method == methods::request::TOOLS_LIST {
return Ok(legacy_list_tools_with_task_support(
server, router, &req, ctx, id,
)
.await);
}
}
Ok(match revision {
LegacyRevision::V2025_11_25 => {
dispatch_capability::<S, LegacyWire>(
server, router, &req, &ctx, shared, id,
)
.await
}
LegacyRevision::V2025_06_18 => {
dispatch_capability::<S, Legacy0618Wire>(
server, router, &req, &ctx, shared, id,
)
.await
}
})
}
VersionRoute::Unsupported(requested) => {
Ok(unsupported_version(id, requested, supported))
}
VersionRoute::InvalidEnvelope(field) => Ok(invalid_envelope(id, field, supported)),
}
}
methods::request::RESOURCES_SUBSCRIBE | methods::request::RESOURCES_UNSUBSCRIBE => {
match classify_version(req.params.as_ref(), supported) {
VersionRoute::Legacy(_) => {
if let Err(response) = legacy_context(sessions.as_ref(), &req).await? {
return Ok(response);
}
if !router.has_resources() {
return Ok(error_response(id, &McpError::method_not_found(method)));
}
let uri = match parse_uri_param(req.params.as_ref(), &method) {
Ok(uri) => uri,
Err(e) => return Ok(error_response(id, &e)),
};
let sid = session_id(req.params.as_ref()).unwrap_or_default();
if method == methods::request::RESOURCES_SUBSCRIBE {
subs.legacy_subscribe(sid, connection_id(req.params.as_ref()), uri);
} else {
subs.legacy_unsubscribe(sid, &uri);
}
Ok(JsonRpcResponse::success(id, serde_json::json!({})).into())
}
VersionRoute::Modern => Ok(error_response(id, &McpError::method_not_found(method))),
VersionRoute::Unsupported(requested) => {
Ok(unsupported_version(id, requested, supported))
}
VersionRoute::InvalidEnvelope(field) => Ok(invalid_envelope(id, field, supported)),
}
}
methods::request::LOGGING_SET_LEVEL => {
match classify_version(req.params.as_ref(), supported) {
VersionRoute::Legacy(_) => {
if let Err(response) = legacy_context(sessions.as_ref(), &req).await? {
return Ok(response);
}
if !router.has_logging() {
return Ok(error_response(id, &McpError::method_not_found(method)));
}
let level = match parse_set_level_params(req.params.as_ref()) {
Ok(level) => level,
Err(e) => return Ok(error_response(id, &e)),
};
let sid = session_id(req.params.as_ref()).unwrap_or_default();
sessions.set_log_level(sid, level).await;
Ok(JsonRpcResponse::success(id, serde_json::json!({})).into())
}
VersionRoute::Modern => Ok(error_response(id, &McpError::method_not_found(method))),
VersionRoute::Unsupported(requested) => {
Ok(unsupported_version(id, requested, supported))
}
VersionRoute::InvalidEnvelope(field) => Ok(invalid_envelope(id, field, supported)),
}
}
methods::request::TASKS_LIST
| methods::request::TASKS_GET
| methods::request::TASKS_CANCEL
| methods::request::TASKS_RESULT => {
match classify_version(req.params.as_ref(), supported) {
VersionRoute::Legacy(rev) if !rev.has_tasks() => {
Ok(error_response(id, &McpError::method_not_found(method)))
}
VersionRoute::Legacy(_) => {
if let Err(response) = legacy_context(sessions.as_ref(), &req).await? {
return Ok(response);
}
let Some(store) = tasks else {
return Ok(error_response(id, &McpError::method_not_found(method)));
};
let sid = session_id(req.params.as_ref())
.unwrap_or_default()
.to_owned();
Ok(handle_tasks_method(store, &sid, method.as_str(), &req, id).await)
}
VersionRoute::Modern => Ok(error_response(id, &McpError::method_not_found(method))),
VersionRoute::Unsupported(requested) => {
Ok(unsupported_version(id, requested, supported))
}
VersionRoute::InvalidEnvelope(field) => Ok(invalid_envelope(id, field, supported)),
}
}
other => Ok(error_response(id, &McpError::method_not_found(other))),
}
}
enum VersionRoute {
Modern,
Legacy(LegacyRevision),
Unsupported(Option<String>),
InvalidEnvelope(&'static str),
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum LegacyRevision {
V2025_06_18,
V2025_11_25,
}
impl LegacyRevision {
const fn version(self) -> ProtocolVersion {
match self {
Self::V2025_06_18 => ProtocolVersion::V2025_06_18,
Self::V2025_11_25 => ProtocolVersion::V2025_11_25,
}
}
fn has_tasks(self) -> bool {
self.version().has_core_tasks()
}
}
fn classify_version(params: Option<&Value>, supported: &[ProtocolVersion]) -> VersionRoute {
match version::request_protocol_version(params) {
Some(v) if !supported.contains(&v) => {
VersionRoute::Unsupported(Some(v.as_str().to_owned()))
}
Some(ProtocolVersion::V2025_06_18) => VersionRoute::Legacy(LegacyRevision::V2025_06_18),
Some(ProtocolVersion::V2025_11_25) => VersionRoute::Legacy(LegacyRevision::V2025_11_25),
Some(_) => match meta::missing_request_envelope_field(params) {
Some(field) => VersionRoute::InvalidEnvelope(field),
None => VersionRoute::Modern,
},
None => VersionRoute::InvalidEnvelope(meta::keys::PROTOCOL_VERSION),
}
}
fn collect_header_params(schema: &Value, path: &mut Vec<String>, out: &mut Vec<HeaderParam>) {
let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
return;
};
for (name, subschema) in properties {
path.push(name.clone());
if let Some(header) = subschema.get("x-mcp-header").and_then(Value::as_str)
&& !header.is_empty()
{
out.push(HeaderParam {
header: header.to_ascii_lowercase(),
path: path.clone(),
});
}
collect_header_params(subschema, path, out);
path.pop();
}
}
fn argument_at<'a>(arguments: &'a Value, path: &[String]) -> Option<&'a Value> {
path.iter().try_fold(arguments, |v, key| v.get(key))
}
fn invalid_envelope(id: RequestId, field: &str, supported: &[ProtocolVersion]) -> JsonRpcMessage {
let err = JsonRpcError {
code: -32602,
message: format!("request `_meta` is missing the required field `{field}`"),
data: Some(serde_json::json!({
"missingField": field,
"supported": supported.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
})),
};
JsonRpcResponse::error(id, err).into()
}
const REMOVED_IN_STATELESS: &[&str] = &[
methods::request::INITIALIZE,
methods::request::PING,
methods::request::LOGGING_SET_LEVEL,
methods::request::RESOURCES_SUBSCRIBE,
methods::request::RESOURCES_UNSUBSCRIBE,
];
fn session_id(params: Option<&Value>) -> Option<&str> {
params?
.get("_meta")?
.get(meta::internal::SESSION_ID)?
.as_str()
}
fn connection_id(params: Option<&Value>) -> Option<&str> {
params?
.get("_meta")?
.get(meta::internal::CONNECTION_ID)?
.as_str()
}
fn context_declares_extension(ctx: &RequestContext, ext_id: &str) -> bool {
ctx.client_capabilities
.as_ref()
.and_then(|caps| caps.get("extensions"))
.and_then(Value::as_object)
.is_some_and(|exts| exts.contains_key(ext_id))
}
fn ok_value<T: Serialize>(id: RequestId, value: &T) -> JsonRpcMessage {
match serde_json::to_value(value) {
Ok(v) => JsonRpcResponse::success(id, v).into(),
Err(e) => error_response(id, &McpError::internal(format!("serialize result: {e}"))),
}
}
fn error_response(id: RequestId, err: &McpError) -> JsonRpcMessage {
JsonRpcResponse::error(id, mcp_to_jsonrpc_error(err)).into()
}
fn error_response_for(id: RequestId, version: &ProtocolVersion, err: &McpError) -> JsonRpcMessage {
JsonRpcResponse::error(id, mcp_to_jsonrpc_error_for(err, version)).into()
}
fn missing_capability_response(id: RequestId, extension_id: &str) -> JsonRpcMessage {
let err = JsonRpcError {
code: turbomcp_core::codes::MISSING_REQUIRED_CLIENT_CAPABILITY,
message: "missing required client capability".to_owned(),
data: Some(serde_json::json!({
"requiredCapabilities": { "extensions": { extension_id: {} } }
})),
};
JsonRpcResponse::error(id, err).into()
}
fn unsupported_version(
id: RequestId,
requested: Option<String>,
supported: &[ProtocolVersion],
) -> JsonRpcMessage {
let err = ProtocolError::UnsupportedVersion {
requested,
supported: supported.iter().map(|v| v.as_str().to_owned()).collect(),
};
err.into_response(id).into()
}