use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;
use futures::FutureExt;
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use turbomcp_core::{
JsonRpcMessage, JsonRpcRequest, JsonRpcResponse, McpError, McpResult, ProtocolVersion,
RequestContext, RequestId, meta,
};
use turbomcp_protocol::v2025_06_18::types as v0618;
use turbomcp_protocol::v2025_11_25::types as legacy;
use turbomcp_protocol::v2026_07_28::types as v0728;
use turbomcp_protocol::{methods, neutral};
use crate::context::{
CallToolContext, CompleteContext, GetPromptContext, ListPromptsContext,
ListResourceTemplatesContext, ListResourcesContext, ListToolsContext, ReadResourceContext,
};
use crate::logging::LogSender;
use crate::mrtr::{ClientHandle, PendingRequests, StateSigner};
use crate::progress::ProgressReporter;
use crate::router::MethodRouter;
use crate::traits::McpServerCore;
use crate::visibility::{self, ComponentKind, VisibleComponent};
use super::params::{
parse_call_tool_params, parse_complete_params, parse_get_prompt_params, parse_list_params,
parse_read_resource_params,
};
use super::{
HeaderParam, Shared, argument_at, collect_header_params, connection_id, error_response_for,
ok_value, session_id,
};
fn with_cache_default<N>(
fut: Option<BoxFuture<'static, Result<N, McpError>>>,
policy: neutral::CachePolicy,
) -> Option<BoxFuture<'static, Result<N, McpError>>>
where
N: neutral::Cacheable + Send + 'static,
{
fut.map(|f| {
async move {
f.await.map(|mut n| {
n.cache_policy_mut().get_or_insert(policy);
n
})
}
.boxed()
})
}
async fn finish<N, W>(
id: RequestId,
method: &str,
version: &ProtocolVersion,
fut: Option<BoxFuture<'static, Result<N, McpError>>>,
) -> JsonRpcMessage
where
W: Serialize + From<N>,
{
match fut {
None => error_response_for(id, version, &McpError::method_not_found(method)),
Some(f) => match f.await {
Ok(result) => ok_value(id, &W::from(result)),
Err(e) => error_response_for(id, version, &e),
},
}
}
pub(super) trait WireFamily {
const MRTR: bool;
const VERSION: ProtocolVersion;
type ListTools: Serialize + From<neutral::ListToolsResult>;
type CallTool: Serialize + From<neutral::CallToolResult>;
type ListResources: Serialize + From<neutral::ListResourcesResult>;
type ListResourceTemplates: Serialize + From<neutral::ListResourceTemplatesResult>;
type ReadResource: Serialize + From<neutral::ReadResourceResult>;
type ListPrompts: Serialize + From<neutral::ListPromptsResult>;
type GetPrompt: Serialize + From<neutral::GetPromptResult>;
type Complete: Serialize + From<neutral::CompleteResult>;
}
pub(super) struct DraftWire;
impl WireFamily for DraftWire {
const MRTR: bool = true;
const VERSION: ProtocolVersion = ProtocolVersion::V2026_07_28;
type ListTools = v0728::ListToolsResult;
type CallTool = v0728::CallToolResult;
type ListResources = v0728::ListResourcesResult;
type ListResourceTemplates = v0728::ListResourceTemplatesResult;
type ReadResource = v0728::ReadResourceResult;
type ListPrompts = v0728::ListPromptsResult;
type GetPrompt = v0728::GetPromptResult;
type Complete = v0728::CompleteResult;
}
pub(super) struct LegacyWire;
impl WireFamily for LegacyWire {
const MRTR: bool = false;
const VERSION: ProtocolVersion = ProtocolVersion::V2025_11_25;
type ListTools = legacy::ListToolsResult;
type CallTool = legacy::CallToolResult;
type ListResources = legacy::ListResourcesResult;
type ListResourceTemplates = legacy::ListResourceTemplatesResult;
type ReadResource = legacy::ReadResourceResult;
type ListPrompts = legacy::ListPromptsResult;
type GetPrompt = legacy::GetPromptResult;
type Complete = legacy::CompleteResult;
}
pub(super) struct Legacy0618Wire;
impl WireFamily for Legacy0618Wire {
const MRTR: bool = false;
const VERSION: ProtocolVersion = ProtocolVersion::V2025_06_18;
type ListTools = v0618::ListToolsResult;
type CallTool = v0618::CallToolResult;
type ListResources = v0618::ListResourcesResult;
type ListResourceTemplates = v0618::ListResourceTemplatesResult;
type ReadResource = v0618::ReadResourceResult;
type ListPrompts = v0618::ListPromptsResult;
type GetPrompt = v0618::GetPromptResult;
type Complete = v0618::CompleteResult;
}
fn with_visibility<N>(
fut: Option<BoxFuture<'static, Result<N, McpError>>>,
shared: &Shared,
ctx: &RequestContext,
filter: fn(&visibility::Policy, &RequestContext, &mut N),
) -> Option<BoxFuture<'static, Result<N, McpError>>>
where
N: Send + 'static,
{
let policy = shared.visibility.clone();
if policy.is_none() {
return fut;
}
let ctx = ctx.clone();
fut.map(|f| {
async move {
f.await.map(|mut n| {
filter(&policy, &ctx, &mut n);
n
})
}
.boxed()
})
}
enum Component<'a> {
Tool(&'a str),
Resource(&'a str),
Prompt(&'a str),
}
async fn check_header_mirrors<S: McpServerCore>(
shared: &Shared,
router: &MethodRouter<S>,
server: &S,
ctx: &RequestContext,
req: &JsonRpcRequest,
params: &neutral::CallToolParams,
) -> McpResult<()> {
let Some(observed) = req
.params
.as_ref()
.and_then(|p| p.get("_meta"))
.and_then(|m| m.get(meta::internal::OBSERVED_HEADER_PARAMS))
.and_then(Value::as_array)
else {
return Ok(());
};
let index = shared
.header_params
.get_or_init(|| build_header_param_index(router, server, ctx))
.await;
let Some(declared) = index.get(params.name.as_str()).filter(|d| !d.is_empty()) else {
return Ok(());
};
let arguments = Value::Object(params.arguments.clone());
for param in declared {
if argument_at(&arguments, ¶m.path).is_none() {
continue; }
let sent = observed
.iter()
.filter_map(Value::as_str)
.any(|h| h.eq_ignore_ascii_case(¶m.header));
if !sent {
return Err(McpError::HeaderMismatch(format!(
"Mcp-Param-{} header is missing but `{}` is present in the request body",
param.header,
param.path.join(".")
)));
}
}
Ok(())
}
async fn build_header_param_index<S: McpServerCore>(
router: &MethodRouter<S>,
server: &S,
ctx: &RequestContext,
) -> HashMap<String, Vec<HeaderParam>> {
let mut index = HashMap::new();
let Some(fut) = router.dispatch_list_tools(
server.clone(),
ListToolsContext::new(ctx.clone()),
neutral::ListParams::default(),
) else {
return index;
};
let Ok(listed) = fut.await else { return index };
for tool in listed.tools {
let mut found = Vec::new();
collect_header_params(&tool.input_schema, &mut Vec::new(), &mut found);
if !found.is_empty() {
index.insert(tool.name, found);
}
}
index
}
async fn hidden<S: McpServerCore>(
shared: &Shared,
router: &MethodRouter<S>,
server: &S,
ctx: &RequestContext,
component: Component<'_>,
) -> bool {
let Some(policy) = shared.visibility.as_ref() else {
return false;
};
let params = neutral::ListParams::default();
let judge = |kind, id: &str, meta: &Map<String, Value>| {
!policy.is_visible(&VisibleComponent {
kind,
id,
meta,
request: ctx,
})
};
match component {
Component::Tool(name) => {
let Some(fut) = router.dispatch_list_tools(
server.clone(),
ListToolsContext::new(ctx.clone()),
params,
) else {
return false;
};
let Ok(listed) = fut.await else { return false };
listed
.tools
.iter()
.find(|t| t.name == name)
.is_some_and(|t| judge(ComponentKind::Tool, &t.name, &t.meta))
}
Component::Prompt(name) => {
let Some(fut) = router.dispatch_list_prompts(
server.clone(),
ListPromptsContext::new(ctx.clone()),
params,
) else {
return false;
};
let Ok(listed) = fut.await else { return false };
listed
.prompts
.iter()
.find(|p| p.name == name)
.is_some_and(|p| judge(ComponentKind::Prompt, &p.name, &p.meta))
}
Component::Resource(uri) => {
if let Some(fut) = router.dispatch_list_resources(
server.clone(),
ListResourcesContext::new(ctx.clone()),
params.clone(),
) && let Ok(listed) = fut.await
&& let Some(r) = listed.resources.iter().find(|r| r.uri == uri)
{
return judge(ComponentKind::Resource, &r.uri, &r.meta);
}
let Some(fut) = router.dispatch_list_resource_templates(
server.clone(),
ListResourceTemplatesContext::new(ctx.clone()),
params,
) else {
return false;
};
let Ok(listed) = fut.await else { return false };
listed
.resource_templates
.iter()
.find(|t| {
crate::__macro_support::match_uri_template(&t.uri_template, uri).is_some()
})
.is_some_and(|t| judge(ComponentKind::ResourceTemplate, &t.uri_template, &t.meta))
}
}
}
pub(super) async fn dispatch_capability<S: McpServerCore, W: WireFamily>(
server: S,
router: &MethodRouter<S>,
req: &JsonRpcRequest,
ctx: &RequestContext,
shared: &Shared,
id: RequestId,
) -> JsonRpcMessage {
let signer = &shared.signer;
let pending = &shared.pending;
let method = req.method.as_str();
let ctx = ctx.clone();
let list_params = parse_list_params(req.params.as_ref());
match method {
methods::request::TOOLS_LIST => {
let fut =
router.dispatch_list_tools(server, ListToolsContext::new(ctx.clone()), list_params);
let fut = with_visibility(fut, shared, &ctx, visibility::filter_tools);
let fut = with_cache_default(fut, shared.cache.tools_list);
finish::<_, W::ListTools>(id, method, &W::VERSION, fut).await
}
methods::request::TOOLS_CALL => {
let params = match parse_call_tool_params(req.params.as_ref()) {
Ok(p) => p,
Err(e) => return error_response_for(id, &W::VERSION, &e),
};
if let Err(e) =
check_header_mirrors::<S>(shared, router, &server, &ctx, req, ¶ms).await
{
return error_response_for(id, &W::VERSION, &e);
}
if hidden(shared, router, &server, &ctx, Component::Tool(¶ms.name)).await {
return ok_value(
id,
&W::CallTool::from(neutral::CallToolResult::error(format!(
"unknown tool: {}",
params.name
))),
);
}
let handle = match mrtr_handle::<W>(
req,
&ctx,
signer,
pending,
shared.strict_elicitation_keys,
) {
Ok(h) => h,
Err(e) => return error_response_for(id, &W::VERSION, &e),
};
let fut = router.dispatch_call_tool(
server,
CallToolContext::new(ctx.clone())
.with_client(handle.clone())
.with_progress(progress_reporter::<W>(req))
.with_log(log_sender::<W>(req, &ctx, router.has_logging())),
params,
);
let subject = ctx.identity.subject().map(str::to_owned);
finish_mrtr::<_, W::CallTool>(
id,
MrtrTurn {
method,
version: &W::VERSION,
subject,
handle: &handle,
signer,
mrtr_enabled: W::MRTR,
},
fut,
)
.await
}
methods::request::RESOURCES_LIST => {
let fut = router.dispatch_list_resources(
server,
ListResourcesContext::new(ctx.clone()),
list_params,
);
let fut = with_visibility(fut, shared, &ctx, visibility::filter_resources);
let fut = with_cache_default(fut, shared.cache.resources_list);
finish::<_, W::ListResources>(id, method, &W::VERSION, fut).await
}
methods::request::RESOURCES_TEMPLATES_LIST => {
let fut = router.dispatch_list_resource_templates(
server,
ListResourceTemplatesContext::new(ctx.clone()),
list_params,
);
let fut = with_visibility(fut, shared, &ctx, visibility::filter_resource_templates);
let fut = with_cache_default(fut, shared.cache.resource_templates_list);
finish::<_, W::ListResourceTemplates>(id, method, &W::VERSION, fut).await
}
methods::request::RESOURCES_READ => {
let params = match parse_read_resource_params(req.params.as_ref()) {
Ok(p) => p,
Err(e) => return error_response_for(id, &W::VERSION, &e),
};
if hidden(
shared,
router,
&server,
&ctx,
Component::Resource(¶ms.uri),
)
.await
{
return error_response_for(
id,
&W::VERSION,
&McpError::resource_not_found(params.uri),
);
}
let handle = match mrtr_handle::<W>(
req,
&ctx,
signer,
pending,
shared.strict_elicitation_keys,
) {
Ok(h) => h,
Err(e) => return error_response_for(id, &W::VERSION, &e),
};
let fut = router.dispatch_read_resource(
server,
ReadResourceContext::new(ctx.clone())
.with_client(handle.clone())
.with_progress(progress_reporter::<W>(req))
.with_log(log_sender::<W>(req, &ctx, router.has_logging())),
params,
);
let fut = with_cache_default(fut, shared.cache.resources_read);
let subject = ctx.identity.subject().map(str::to_owned);
finish_mrtr::<_, W::ReadResource>(
id,
MrtrTurn {
method,
version: &W::VERSION,
subject,
handle: &handle,
signer,
mrtr_enabled: W::MRTR,
},
fut,
)
.await
}
methods::request::PROMPTS_LIST => {
let fut = router.dispatch_list_prompts(
server,
ListPromptsContext::new(ctx.clone()),
list_params,
);
let fut = with_visibility(fut, shared, &ctx, visibility::filter_prompts);
let fut = with_cache_default(fut, shared.cache.prompts_list);
finish::<_, W::ListPrompts>(id, method, &W::VERSION, fut).await
}
methods::request::PROMPTS_GET => {
let params = match parse_get_prompt_params(req.params.as_ref()) {
Ok(p) => p,
Err(e) => return error_response_for(id, &W::VERSION, &e),
};
if hidden(
shared,
router,
&server,
&ctx,
Component::Prompt(¶ms.name),
)
.await
{
return error_response_for(
id,
&W::VERSION,
&McpError::invalid_params(format!("unknown prompt: {}", params.name)),
);
}
let handle = match mrtr_handle::<W>(
req,
&ctx,
signer,
pending,
shared.strict_elicitation_keys,
) {
Ok(h) => h,
Err(e) => return error_response_for(id, &W::VERSION, &e),
};
let fut = router.dispatch_get_prompt(
server,
GetPromptContext::new(ctx.clone())
.with_client(handle.clone())
.with_progress(progress_reporter::<W>(req))
.with_log(log_sender::<W>(req, &ctx, router.has_logging())),
params,
);
let subject = ctx.identity.subject().map(str::to_owned);
finish_mrtr::<_, W::GetPrompt>(
id,
MrtrTurn {
method,
version: &W::VERSION,
subject,
handle: &handle,
signer,
mrtr_enabled: W::MRTR,
},
fut,
)
.await
}
methods::request::COMPLETION_COMPLETE => {
let params = match parse_complete_params(req.params.as_ref()) {
Ok(p) => p,
Err(e) => return error_response_for(id, &W::VERSION, &e),
};
let fut = router.dispatch_complete(server, CompleteContext::new(ctx), params);
finish::<_, W::Complete>(id, method, &W::VERSION, fut).await
}
_ => unreachable!("dispatch_capability called with an unrouted method"),
}
}
#[derive(Deserialize, Default)]
struct RawMrtrFields {
#[serde(rename = "inputResponses", default)]
input_responses: Option<BTreeMap<String, Value>>,
#[serde(rename = "requestState", default)]
request_state: Option<String>,
}
fn mrtr_handle<W: WireFamily>(
req: &JsonRpcRequest,
ctx: &RequestContext,
signer: &StateSigner,
pending: &Arc<PendingRequests>,
strict_keys: bool,
) -> Result<ClientHandle, McpError> {
if !W::MRTR {
return Ok(match session_id(req.params.as_ref()) {
Some(session) => ClientHandle::bidi(
session,
connection_id(req.params.as_ref()).unwrap_or_default(),
Arc::clone(pending),
ctx.client_capabilities.clone(),
),
None => ClientHandle::unavailable("no session for inline bidirectional requests"),
});
}
let fields: RawMrtrFields = req
.params
.as_ref()
.and_then(|p| serde_json::from_value(p.clone()).ok())
.unwrap_or_default();
let state_in = match &fields.request_state {
Some(token) => Some(signer.verify(&req.method, ctx.identity.subject(), token)?),
None => None,
};
Ok(ClientHandle::mrtr(
connection_id(req.params.as_ref()).unwrap_or_default(),
ctx.client_capabilities.clone(),
fields.input_responses.unwrap_or_default(),
state_in,
strict_keys,
))
}
pub(super) struct MrtrTurn<'a> {
pub(super) method: &'a str,
pub(super) version: &'a ProtocolVersion,
pub(super) subject: Option<String>,
pub(super) handle: &'a ClientHandle,
pub(super) signer: &'a StateSigner,
pub(super) mrtr_enabled: bool,
}
async fn finish_mrtr<N, WIRE>(
id: RequestId,
turn: MrtrTurn<'_>,
fut: Option<BoxFuture<'static, Result<N, McpError>>>,
) -> JsonRpcMessage
where
WIRE: Serialize + From<N>,
{
let MrtrTurn {
method,
version,
subject,
handle,
signer,
mrtr_enabled,
} = turn;
let Some(f) = fut else {
return error_response_for(id, version, &McpError::method_not_found(method));
};
match f.await {
Ok(result) => ok_value(id, &WIRE::from(result)),
Err(McpError::InputRequired) if mrtr_enabled => {
let collected = handle.collected();
let state_out = handle.state_out();
if collected.is_empty() && state_out.is_none() {
return error_response_for(
id,
version,
&McpError::internal("MRTR abort recorded no input requests"),
);
}
let mut result = Map::new();
result.insert(
"resultType".to_owned(),
serde_json::json!(neutral::result_type::INPUT_REQUIRED),
);
if !collected.is_empty() {
result.insert(
"inputRequests".to_owned(),
Value::Object(collected.into_iter().collect()),
);
}
if let Some(data) = state_out {
match signer.sign(method, subject.as_deref(), &data) {
Ok(token) => {
result.insert("requestState".to_owned(), serde_json::json!(token));
}
Err(e) => return error_response_for(id, version, &e),
}
}
JsonRpcResponse::success(id, Value::Object(result)).into()
}
Err(e) => error_response_for(id, version, &e),
}
}
fn log_sender<W: WireFamily>(
req: &JsonRpcRequest,
ctx: &RequestContext,
logging_enabled: bool,
) -> LogSender {
let Some(min) = ctx.log_level.filter(|_| logging_enabled) else {
return LogSender::disabled();
};
let connection = connection_id(req.params.as_ref())
.unwrap_or_default()
.to_owned();
let session = if W::MRTR {
String::new()
} else {
session_id(req.params.as_ref())
.unwrap_or_default()
.to_owned()
};
LogSender::new(min, connection, session)
}
fn progress_reporter<W: WireFamily>(req: &JsonRpcRequest) -> ProgressReporter {
let token = req
.params
.as_ref()
.and_then(|p| p.get("_meta"))
.and_then(|m| m.get(meta::keys::PROGRESS_TOKEN));
let Some(token) = token else {
return ProgressReporter::disabled();
};
if !(token.is_string() || token.is_i64() || token.is_u64()) {
tracing::warn!(?token, "progressToken must be a string or integer; ignored");
return ProgressReporter::disabled();
}
let connection = connection_id(req.params.as_ref())
.unwrap_or_default()
.to_owned();
let session = if W::MRTR {
String::new()
} else {
session_id(req.params.as_ref())
.unwrap_or_default()
.to_owned()
};
ProgressReporter::new(token.clone(), connection, session)
}