use crate::error::{Error, Result};
use crate::server::limits::PayloadLimits;
use crate::shared::middleware::{EnhancedMiddlewareChain, MiddlewareContext};
use crate::shared::protocol_helpers::{create_notification, create_request};
#[cfg(any(target_arch = "wasm32", test))]
use crate::types::jsonrpc::ResponsePayload;
#[cfg(target_arch = "wasm32")]
use crate::types::JSONRPCError;
use crate::types::caching::{project_caching_hints, Cacheable};
use crate::types::{
CallToolRequest, CallToolResult, ClientCapabilities, ClientRequest, Content, GetPromptRequest,
GetPromptResult, Implementation, InitializeRequest, InitializeResult, JSONRPCResponse,
ListPromptsRequest, ListPromptsResult, ListResourceTemplatesRequest,
ListResourceTemplatesResult, ListResourcesRequest, ListResourcesResult, ListToolsRequest,
ListToolsResult, Notification, PromptInfo, ProtocolVersion, ReadResourceRequest,
ReadResourceResult, Request, RequestId, ServerCapabilities, ToolInfo,
};
use async_trait::async_trait;
use serde_json::Value;
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use std::collections::HashSet;
use std::sync::Arc;
use crate::runtime::RwLock;
#[cfg(not(target_arch = "wasm32"))]
use super::auth::{AuthContext, AuthProvider, ToolAuthorizer};
#[cfg(not(target_arch = "wasm32"))]
use super::cancellation::{CancellationManager, RequestHandlerExtra};
#[cfg(not(target_arch = "wasm32"))]
use super::roots::RootsManager;
#[cfg(not(target_arch = "wasm32"))]
use super::subscriptions::SubscriptionManager;
#[cfg(not(target_arch = "wasm32"))]
use super::tasks::TaskRouter;
#[cfg(not(target_arch = "wasm32"))]
use super::tool_middleware::{ToolContext, ToolMiddlewareChain};
use super::{PromptHandler, ResourceHandler, SamplingHandler, ToolHandler};
#[cfg(not(target_arch = "wasm32"))]
use crate::types::tools::TaskSupport;
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
pub trait ProtocolHandler: Send + Sync {
async fn handle_request(
&self,
id: RequestId,
request: Request,
auth_context: Option<AuthContext>,
) -> JSONRPCResponse;
async fn handle_notification(&self, notification: Notification) -> Result<()>;
fn capabilities(&self) -> &ServerCapabilities;
fn info(&self) -> &Implementation;
}
#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
pub trait ProtocolHandler {
async fn handle_request(&self, id: RequestId, request: Request) -> JSONRPCResponse;
async fn handle_notification(&self, notification: Notification) -> Result<()>;
fn capabilities(&self) -> &ServerCapabilities;
fn info(&self) -> &Implementation;
}
#[cfg(feature = "mcp-apps")]
pub(crate) fn enrich_meta_for_host(
meta: &mut serde_json::Map<String, serde_json::Value>,
host: crate::types::mcp_apps::HostType,
) {
use crate::types::mcp_apps::HostType;
if host == HostType::ChatGpt {
if let Some(uri) = meta
.get("ui")
.and_then(|v| v.get("resourceUri"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
{
meta.entry("openai/outputTemplate".to_string())
.or_insert_with(|| serde_json::Value::String(uri));
meta.entry("openai/widgetAccessible".to_string())
.or_insert(serde_json::Value::Bool(true));
meta.entry("openai/toolInvocation/invoking".to_string())
.or_insert_with(|| serde_json::Value::String("Running...".into()));
meta.entry("openai/toolInvocation/invoked".to_string())
.or_insert_with(|| serde_json::Value::String("Done".into()));
}
}
}
const RESOURCE_PROPAGATION_PREFIXES: &[&str] = &[
"openai/outputTemplate",
"openai/toolInvocation/",
"openai/widgetAccessible",
];
pub(crate) fn build_uri_to_tool_meta(
tool_infos: &HashMap<String, ToolInfo>,
) -> HashMap<String, serde_json::Map<String, serde_json::Value>> {
let mut map = HashMap::new();
for info in tool_infos.values() {
if let Some(meta) = info.widget_meta() {
let uri = meta
.get("ui")
.and_then(|v| v.get("resourceUri"))
.and_then(|v| v.as_str());
if let Some(uri) = uri {
let propagated: serde_json::Map<String, serde_json::Value> = meta
.iter()
.filter(|(k, _)| {
RESOURCE_PROPAGATION_PREFIXES
.iter()
.any(|prefix| k.starts_with(prefix))
})
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if !propagated.is_empty() {
map.entry(uri.to_string()).or_insert(propagated);
}
}
}
}
map
}
#[allow(dead_code)]
#[allow(missing_debug_implementations)]
pub struct ServerCore {
info: Implementation,
capabilities: ServerCapabilities,
tools: HashMap<String, Arc<dyn ToolHandler>>,
prompts: HashMap<String, Arc<dyn PromptHandler>>,
tool_infos: HashMap<String, ToolInfo>,
uri_to_tool_meta: HashMap<String, serde_json::Map<String, serde_json::Value>>,
prompt_infos: HashMap<String, PromptInfo>,
resources: Option<Arc<dyn ResourceHandler>>,
sampling: Option<Arc<dyn SamplingHandler>>,
client_capabilities: Arc<RwLock<Option<ClientCapabilities>>>,
initialized: Arc<RwLock<bool>>,
cancellation_manager: CancellationManager,
roots_manager: Arc<RwLock<RootsManager>>,
subscription_manager: Arc<RwLock<SubscriptionManager>>,
auth_provider: Option<Arc<dyn AuthProvider>>,
tool_authorizer: Option<Arc<dyn ToolAuthorizer>>,
protocol_middleware: Arc<RwLock<EnhancedMiddlewareChain>>,
#[cfg(not(target_arch = "wasm32"))]
tool_middleware: Arc<RwLock<ToolMiddlewareChain>>,
#[cfg(not(target_arch = "wasm32"))]
task_router: Option<Arc<dyn TaskRouter>>,
#[cfg(not(target_arch = "wasm32"))]
task_store: Option<Arc<dyn crate::server::task_store::TaskStore>>,
#[cfg(not(target_arch = "wasm32"))]
suppress_double_wrap: HashSet<String>,
stateless_mode: bool,
payload_limits: PayloadLimits,
supported_protocol_versions: Vec<ProtocolVersion>,
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
request_state_codec: Option<Arc<crate::server::request_state::RequestStateCodec>>,
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::struct_field_names)]
server_request_dispatcher:
Option<Arc<crate::server::server_request_dispatcher::ServerRequestDispatcher>>,
#[cfg(not(target_arch = "wasm32"))]
peer_handle: Option<Arc<dyn crate::shared::peer::PeerHandle>>,
}
enum ToolCallOutcome {
Result(CallToolResult),
#[cfg(not(target_arch = "wasm32"))]
TaskCreated { task_value: Value },
}
impl ServerCore {
#[allow(clippy::too_many_arguments)]
pub fn new(
info: Implementation,
capabilities: ServerCapabilities,
tools: HashMap<String, Arc<dyn ToolHandler>>,
prompts: HashMap<String, Arc<dyn PromptHandler>>,
tool_infos: HashMap<String, ToolInfo>,
prompt_infos: HashMap<String, PromptInfo>,
resources: Option<Arc<dyn ResourceHandler>>,
sampling: Option<Arc<dyn SamplingHandler>>,
auth_provider: Option<Arc<dyn AuthProvider>>,
tool_authorizer: Option<Arc<dyn ToolAuthorizer>>,
protocol_middleware: Arc<RwLock<EnhancedMiddlewareChain>>,
#[cfg(not(target_arch = "wasm32"))] tool_middleware: Arc<RwLock<ToolMiddlewareChain>>,
#[cfg(not(target_arch = "wasm32"))] task_router: Option<Arc<dyn TaskRouter>>,
#[cfg(not(target_arch = "wasm32"))] task_store: Option<
Arc<dyn crate::server::task_store::TaskStore>,
>,
stateless_mode: bool,
payload_limits: PayloadLimits,
) -> Self {
let uri_to_tool_meta = build_uri_to_tool_meta(&tool_infos);
Self {
info,
capabilities,
tools,
prompts,
tool_infos,
uri_to_tool_meta,
prompt_infos,
resources,
sampling,
client_capabilities: Arc::new(RwLock::new(None)),
initialized: Arc::new(RwLock::new(false)),
cancellation_manager: CancellationManager::new(),
roots_manager: Arc::new(RwLock::new(RootsManager::new())),
subscription_manager: Arc::new(RwLock::new(SubscriptionManager::new())),
auth_provider,
tool_authorizer,
protocol_middleware,
#[cfg(not(target_arch = "wasm32"))]
tool_middleware,
#[cfg(not(target_arch = "wasm32"))]
task_router,
#[cfg(not(target_arch = "wasm32"))]
task_store,
#[cfg(not(target_arch = "wasm32"))]
suppress_double_wrap: HashSet::new(),
stateless_mode,
payload_limits,
supported_protocol_versions: crate::types::protocol::context::default_accept_list(),
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
request_state_codec: None,
#[cfg(not(target_arch = "wasm32"))]
server_request_dispatcher: None,
#[cfg(not(target_arch = "wasm32"))]
peer_handle: None,
}
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub fn with_server_request_dispatcher(
mut self,
dispatcher: Arc<crate::server::server_request_dispatcher::ServerRequestDispatcher>,
) -> Self {
let peer: Arc<dyn crate::shared::peer::PeerHandle> = Arc::new(
crate::server::peer_impl::DispatchPeerHandle::new(dispatcher.clone()),
);
self.peer_handle = Some(peer);
self.server_request_dispatcher = Some(dispatcher);
self
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub fn with_suppress_double_wrap(mut self, suppress: HashSet<String>) -> Self {
self.suppress_double_wrap = suppress;
self
}
#[must_use]
pub(crate) fn with_supported_protocol_versions(
mut self,
versions: Vec<ProtocolVersion>,
) -> Self {
self.supported_protocol_versions = versions;
self
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
#[must_use]
pub(crate) fn with_request_state_codec(
mut self,
codec: Option<Arc<crate::server::request_state::RequestStateCodec>>,
) -> Self {
self.request_state_codec = codec;
self
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub(crate) fn request_state_codec(
&self,
) -> Option<&crate::server::request_state::RequestStateCodec> {
self.request_state_codec.as_deref()
}
#[cfg(test)]
pub(crate) fn supported_protocol_versions(&self) -> &[ProtocolVersion] {
&self.supported_protocol_versions
}
#[cfg(test)]
pub(crate) fn is_v2_opted_in(&self) -> bool {
crate::types::protocol::context::is_v2_opted_in(&self.supported_protocol_versions)
}
fn resolve_ingress_protocol_context(
&self,
request: &Request,
) -> std::result::Result<
Option<crate::types::protocol::ProtocolContext>,
crate::types::protocol::context::ProtocolNegotiationError,
> {
resolve_ingress_protocol_context(&self.supported_protocol_versions, request)
}
#[inline]
fn attach_peer(&self, extra: RequestHandlerExtra) -> RequestHandlerExtra {
#[cfg(not(target_arch = "wasm32"))]
if let Some(peer) = self.peer_handle.as_ref() {
return extra.with_peer(peer.clone());
}
extra
}
pub fn payload_limits(&self) -> &PayloadLimits {
&self.payload_limits
}
pub async fn is_initialized(&self) -> bool {
contract_pre_session_lifecycle!();
*self.initialized.read().await
}
pub async fn get_client_capabilities(&self) -> Option<ClientCapabilities> {
self.client_capabilities.read().await.clone()
}
async fn handle_initialize(&self, init_req: &InitializeRequest) -> Result<InitializeResult> {
contract_pre_session_lifecycle!();
*self.client_capabilities.write().await = Some(init_req.capabilities.clone());
*self.initialized.write().await = true;
let negotiated_version = crate::negotiate_protocol_version(&init_req.protocol_version);
Ok(InitializeResult {
protocol_version: ProtocolVersion(negotiated_version.to_string()),
capabilities: project_capabilities_for_v1(&self.capabilities),
server_info: self.info.clone(),
instructions: None,
})
}
async fn handle_list_tools(&self, _req: &ListToolsRequest) -> Result<ListToolsResult> {
contract_pre_tool_dispatch_integrity!();
let tools: Vec<ToolInfo> = self.tool_infos.values().cloned().collect();
Ok(ListToolsResult {
tools,
next_cursor: None,
ttl_ms: None,
cache_scope: None,
})
}
async fn handle_call_tool(
&self,
req: &CallToolRequest,
auth_context: Option<AuthContext>,
protocol_context: Option<crate::types::protocol::ProtocolContext>,
) -> Result<ToolCallOutcome> {
contract_pre_tool_dispatch_integrity!();
let handler = self
.tools
.get(&req.name)
.ok_or_else(|| Error::internal(format!("Tool '{}' not found", req.name)))?;
if let Some(authorizer) = &self.tool_authorizer {
if let Some(ref auth_ctx) = auth_context {
if !authorizer.can_access_tool(auth_ctx, &req.name).await? {
return Err(Error::authentication(format!(
"User not authorized to call tool '{}'",
req.name
)));
}
}
}
#[cfg(not(target_arch = "wasm32"))]
let create_trigger = crate::server::task_dispatch::CreateTrigger::resolve(
protocol_context.as_ref().map(|ctx| ctx.era),
req.task.is_some(),
protocol_context.as_ref(),
);
let validation_era = protocol_context.as_ref().map(|ctx| ctx.era);
let request_id = format!("tool_{}", req.name);
let mut extra = self.attach_peer(
RequestHandlerExtra::new(
request_id.clone(),
self.cancellation_manager
.create_token(request_id.clone())
.await,
)
.with_auth_context(auth_context)
.with_task_request(req.task.clone())
.with_request_meta(request_meta_to_value(req._meta.as_ref()))
.with_protocol_context(protocol_context),
);
#[cfg(not(target_arch = "wasm32"))]
let result_meta_handle = extra.result_meta_handle();
#[cfg(not(target_arch = "wasm32"))]
let result = {
let context = ToolContext::new(&req.name, &request_id);
let mut args = req.arguments.clone();
self.tool_middleware
.read()
.await
.process_request(&req.name, &mut args, &mut extra, &context)
.await?;
if self.payload_limits.max_tool_args_bytes < usize::MAX {
let args_size = json_serialized_len(&args)?;
if args_size > self.payload_limits.max_tool_args_bytes {
return Err(Error::validation(format!(
"Tool arguments for '{}' exceed size limit ({} bytes > {} max)",
req.name, args_size, self.payload_limits.max_tool_args_bytes
)));
}
}
let output = handler.handle_output(args, extra).await;
match crate::server::task_dispatch::resolve_tool_output(output) {
crate::server::task_dispatch::DispatchOutput::Verbatim(call_result) => {
return Ok(ToolCallOutcome::Result(call_result));
},
crate::server::task_dispatch::DispatchOutput::Middleware(mut result) => {
if let Err(e) = self
.tool_middleware
.read()
.await
.process_response(&req.name, &mut result, &context)
.await
{
tracing::warn!("Tool response middleware processing failed: {}", e);
}
if let Err(ref e) = result {
self.tool_middleware
.read()
.await
.handle_tool_error(&req.name, e, &context)
.await;
}
result
},
}
};
#[cfg(target_arch = "wasm32")]
let result = {
let args = req.arguments.clone();
handler.handle(args, extra).await
};
let value = match result {
Ok(value) => value,
Err(crate::error::Error::ToolRejected { message, details }) => {
return Ok(ToolCallOutcome::Result(CallToolResult::rejected(
message, details,
)));
},
Err(e) => return Err(e),
};
let tool_info = self.tool_infos.get(&req.name);
#[cfg(not(target_arch = "wasm32"))]
{
let tool_task_support = tool_info
.as_ref()
.and_then(|info| info.execution.as_ref())
.and_then(|exec| exec.task_support.as_ref())
.copied();
if req.task.is_none() && matches!(tool_task_support, Some(TaskSupport::Required)) {
tracing::warn!(
tool = req.name.as_str(),
"Tool declares taskSupport=Required but client did not send task field; returning CallToolResult for compatibility"
);
}
match self
.task_dispatch()
.create_gate(create_trigger, tool_task_support, &value)
{
crate::server::task_dispatch::CreateGate::Create => {
return Ok(ToolCallOutcome::TaskCreated { task_value: value });
},
crate::server::task_dispatch::CreateGate::NotTaskShaped => {
tracing::debug!(
tool = req.name.as_str(),
"Tool declares taskSupport but returned non-Task value; using normal CallToolResult path"
);
},
crate::server::task_dispatch::CreateGate::Closed => {},
}
}
#[cfg(not(target_arch = "wasm32"))]
crate::server::task_dispatch::double_wrap_tripwire(
&req.name,
&value,
self.suppress_double_wrap.contains(req.name.as_str()),
);
if let Some(schema) = tool_info.and_then(|i| i.output_schema.as_ref()) {
crate::server::output_validation::warn_on_schema_mismatch(
&req.name,
schema,
&value,
validation_era,
);
}
let call_result = if let Some(info) = tool_info.filter(|i| i.widget_meta().is_some()) {
let summary = summarize_structured_output(&value);
CallToolResult::new(vec![Content::text(summary)]).with_widget_enrichment(info, value)
} else if tool_info.is_some_and(|i| i.output_schema.is_some()) {
CallToolResult::structured(value)
} else {
let text = serde_json::to_string_pretty(&value)?;
CallToolResult::new(vec![Content::text(text)])
};
#[cfg(not(target_arch = "wasm32"))]
let call_result = {
let mut call_result = call_result;
if let Some(handler_meta) = result_meta_handle.take_result_meta() {
crate::server::cancellation::merge_result_meta(&mut call_result, handler_meta);
}
call_result
};
Ok(ToolCallOutcome::Result(call_result))
}
async fn handle_list_prompts(&self, _req: &ListPromptsRequest) -> Result<ListPromptsResult> {
let prompts: Vec<PromptInfo> = self.prompt_infos.values().cloned().collect();
tracing::debug!(
target: "mcp.prompts",
count = prompts.len(),
"Returning prompts"
);
Ok(ListPromptsResult {
prompts,
next_cursor: None,
ttl_ms: None,
cache_scope: None,
})
}
async fn handle_get_prompt(
&self,
req: &GetPromptRequest,
auth_context: Option<AuthContext>,
protocol_context: Option<crate::types::protocol::ProtocolContext>,
) -> Result<GetPromptResult> {
let handler = self
.prompts
.get(&req.name)
.ok_or_else(|| Error::internal(format!("Prompt '{}' not found", req.name)))?;
let request_id = format!("prompt_{}", req.name);
let extra = self.attach_peer(
RequestHandlerExtra::new(
request_id.clone(),
self.cancellation_manager
.create_token(request_id.clone())
.await,
)
.with_auth_context(auth_context)
.with_request_meta(request_meta_to_value(req._meta.as_ref()))
.with_protocol_context(protocol_context),
);
handler.handle(req.arguments.clone(), extra).await
}
async fn handle_list_resources(
&self,
req: &ListResourcesRequest,
auth_context: Option<AuthContext>,
) -> Result<ListResourcesResult> {
let mut result = match &self.resources {
Some(handler) => {
let request_id = "list_resources".to_string();
let extra = self.attach_peer(
RequestHandlerExtra::new(
request_id.clone(),
self.cancellation_manager
.create_token(request_id.clone())
.await,
)
.with_auth_context(auth_context),
);
handler.list(req.cursor.clone(), extra).await?
},
None => ListResourcesResult {
resources: vec![],
next_cursor: None,
ttl_ms: None,
cache_scope: None,
},
};
if !self.uri_to_tool_meta.is_empty() {
for resource in &mut result.resources {
if let Some(tool_meta) = self.uri_to_tool_meta.get(&resource.uri) {
let meta = resource.meta.get_or_insert_with(serde_json::Map::new);
crate::types::ui::deep_merge(meta, tool_meta.clone());
}
}
}
Ok(result)
}
async fn handle_read_resource(
&self,
req: &ReadResourceRequest,
auth_context: Option<AuthContext>,
protocol_context: Option<crate::types::protocol::ProtocolContext>,
) -> Result<ReadResourceResult> {
let handler = self.resources.as_ref().ok_or_else(|| {
Error::internal(format!("Resource handler not available for '{}'", req.uri))
})?;
let request_id = format!("read_{}", req.uri);
let extra = self.attach_peer(
RequestHandlerExtra::new(
request_id.clone(),
self.cancellation_manager
.create_token(request_id.clone())
.await,
)
.with_auth_context(auth_context)
.with_request_meta(request_meta_to_value(req._meta.as_ref()))
.with_protocol_context(protocol_context),
);
let mut result = handler.read(&req.uri, extra).await?;
if !self.uri_to_tool_meta.is_empty() {
for content in &mut result.contents {
if let Content::Resource { uri, meta, .. } = content {
if let Some(tool_meta) = self.uri_to_tool_meta.get(uri.as_str()) {
let content_meta = meta.get_or_insert_with(serde_json::Map::new);
crate::types::ui::deep_merge(content_meta, tool_meta.clone());
}
}
}
}
Ok(result)
}
async fn handle_list_resource_templates(
&self,
_req: &ListResourceTemplatesRequest,
) -> Result<ListResourceTemplatesResult> {
Ok(ListResourceTemplatesResult {
resource_templates: vec![],
next_cursor: None,
ttl_ms: None,
cache_scope: None,
})
}
fn error_response(id: RequestId, code: i32, message: String) -> JSONRPCResponse {
contract_pre_error_code_mapping!();
#[cfg(not(target_arch = "wasm32"))]
{
crate::server::task_dispatch::error_response(id, code, message)
}
#[cfg(target_arch = "wasm32")]
{
JSONRPCResponse {
jsonrpc: "2.0".to_string(),
id,
payload: ResponsePayload::Error(JSONRPCError {
code,
message,
data: None,
}),
}
}
}
fn success_response(id: RequestId, result: Value) -> JSONRPCResponse {
#[cfg(not(target_arch = "wasm32"))]
{
crate::server::task_dispatch::success_response(id, result)
}
#[cfg(target_arch = "wasm32")]
{
JSONRPCResponse {
jsonrpc: "2.0".to_string(),
id,
payload: ResponsePayload::Result(result),
}
}
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
impl crate::server::middleware_executor::MiddlewareExecutor for ServerCore {
async fn execute_tool_with_middleware(
&self,
tool_name: &str,
mut args: Value,
mut extra: RequestHandlerExtra,
) -> Result<Value> {
let handler = self
.tools
.get(tool_name)
.ok_or_else(|| Error::internal(format!("Tool '{}' not found", tool_name)))?;
if let Some(authorizer) = &self.tool_authorizer {
if let Some(ref auth_ctx) = extra.auth_context {
if !authorizer.can_access_tool(auth_ctx, tool_name).await? {
return Err(Error::authentication(format!(
"User not authorized to call tool '{}'",
tool_name
)));
}
}
}
let context = ToolContext::new(tool_name, &extra.request_id);
self.tool_middleware
.read()
.await
.process_request(tool_name, &mut args, &mut extra, &context)
.await?;
let mut result = handler.handle(args, extra).await;
if let Err(e) = self
.tool_middleware
.read()
.await
.process_response(tool_name, &mut result, &context)
.await
{
tracing::warn!("Tool response middleware processing failed: {}", e);
}
if let Err(ref e) = result {
self.tool_middleware
.read()
.await
.handle_tool_error(tool_name, e, &context)
.await;
}
result
}
}
pub(crate) use crate::types::protocol::ServerDiscoverResult;
const EXPERIMENTAL_TASKS_KEY: &str = "tasks";
fn project_capabilities_for_v2(capabilities: &ServerCapabilities) -> ServerCapabilities {
let mut projected = capabilities.clone();
projected.tasks = None;
if let Some(experimental) = projected.experimental.as_mut() {
experimental.remove(EXPERIMENTAL_TASKS_KEY);
}
projected
}
pub(crate) fn project_capabilities_for_v1(capabilities: &ServerCapabilities) -> ServerCapabilities {
let auto_advertised = crate::server::task_dispatch::tasks_extension_value();
let mut projected = capabilities.clone();
let emptied_by_removal = projected.extensions.as_mut().is_some_and(|extensions| {
if extensions.get(crate::types::capabilities::TASKS_EXTENSION_KEY) != Some(&auto_advertised)
{
return false;
}
extensions.remove(crate::types::capabilities::TASKS_EXTENSION_KEY);
extensions.is_empty()
});
if emptied_by_removal {
projected.extensions = None;
}
projected
}
pub(crate) fn discover_result_from_capabilities(
capabilities: &ServerCapabilities,
info: &Implementation,
negotiated_version: String,
) -> ServerDiscoverResult {
ServerDiscoverResult {
protocol_version: negotiated_version,
capabilities: project_capabilities_for_v2(capabilities),
server_info: info.clone(),
ttl_ms: None,
cache_scope: None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResponseDisposition {
Complete,
#[cfg_attr(not(feature = "streamable-http"), allow(dead_code))]
InputRequired,
Task,
}
impl ResponseDisposition {
pub(crate) fn as_wire_str(self) -> &'static str {
match self {
Self::Complete => crate::types::mrtr::COMPLETE_RESULT_TYPE,
Self::InputRequired => crate::types::mrtr::INPUT_REQUIRED_RESULT_TYPE,
Self::Task => crate::types::mrtr::TASK_RESULT_TYPE,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReservedFieldOwner {
None,
#[cfg_attr(
not(any(feature = "streamable-http", feature = "testing")),
allow(dead_code)
)]
Mrtr,
TasksDispatch,
}
impl ReservedFieldOwner {
fn may_emit(self, field: &str) -> bool {
match self {
Self::None => false,
Self::Mrtr => {
field == crate::types::mrtr::REQUEST_STATE_KEY
|| field == crate::types::mrtr::INPUT_REQUESTS_KEY
},
Self::TasksDispatch => field == crate::types::mrtr::INPUT_REQUESTS_KEY,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct DispatchEnvelopeClaim {
pub(crate) disposition: ResponseDisposition,
pub(crate) owner: ReservedFieldOwner,
}
impl DispatchEnvelopeClaim {
pub(crate) const NONE: Self = Self {
disposition: ResponseDisposition::Complete,
owner: ReservedFieldOwner::None,
};
pub(crate) const TASKS_INPUT_REQUIRED: Self = Self {
disposition: ResponseDisposition::Complete,
owner: ReservedFieldOwner::TasksDispatch,
};
pub(crate) const TASK_CREATED: Self = Self {
disposition: ResponseDisposition::Task,
owner: ReservedFieldOwner::None,
};
pub(crate) fn or_egress(
self,
disposition: ResponseDisposition,
owner: ReservedFieldOwner,
) -> Self {
let egress = Self { disposition, owner };
if egress == Self::NONE {
self
} else {
egress
}
}
}
impl Default for DispatchEnvelopeClaim {
fn default() -> Self {
Self::NONE
}
}
pub(crate) const RESERVED_SERVER_INFO_KEY: &str = "io.modelcontextprotocol/serverInfo";
pub(crate) fn inject_v2_result_envelope(
response: &mut JSONRPCResponse,
protocol_context: Option<&crate::types::protocol::ProtocolContext>,
server_info: &Implementation,
disposition: ResponseDisposition,
owner: ReservedFieldOwner,
cacheable: Cacheable,
) {
let crate::types::jsonrpc::ResponsePayload::Result(ref mut value) = response.payload else {
return;
};
if !value.is_object() {
return;
}
let cacheable = match disposition {
ResponseDisposition::Complete => cacheable,
ResponseDisposition::InputRequired | ResponseDisposition::Task => Cacheable::No,
};
project_caching_hints(value, protocol_context.map(|c| c.era), cacheable);
if matches!(
protocol_context.map(|c| c.era),
Some(crate::types::protocol::Era::V2)
) {
own_reserved_result_fields(value, server_info, disposition, owner);
}
}
pub(crate) fn request_is_cacheable(request: &Request) -> Cacheable {
let Request::Client(boxed) = request else {
return Cacheable::No;
};
match boxed.as_ref() {
ClientRequest::ListTools(_) => Cacheable::Yes,
ClientRequest::ListResources(_) => Cacheable::Yes,
ClientRequest::ListResourceTemplates(_) => Cacheable::Yes,
ClientRequest::ReadResource(_) => Cacheable::Yes,
ClientRequest::ListPrompts(_) => Cacheable::Yes,
ClientRequest::Initialize(_)
| ClientRequest::CallTool(_)
| ClientRequest::GetPrompt(_)
| ClientRequest::Subscribe(_)
| ClientRequest::Unsubscribe(_)
| ClientRequest::Complete(_)
| ClientRequest::CreateMessage(_)
| ClientRequest::TasksGet(_)
| ClientRequest::TasksResult(_)
| ClientRequest::TasksList(_)
| ClientRequest::TasksCancel(_)
| ClientRequest::SetLoggingLevel { .. }
| ClientRequest::Ping => Cacheable::No,
}
}
pub(crate) fn result_meta_object_mut(
result: &mut Value,
) -> Option<&mut serde_json::Map<String, Value>> {
use crate::types::mrtr::META_KEY;
let object = result.as_object_mut()?;
let existing_is_object = matches!(object.get(META_KEY), Some(Value::Object(_)));
if !existing_is_object {
if object.contains_key(META_KEY) {
tracing::warn!(
target: "mcp.v2",
"a handler set result._meta to a non-object; replacing it with an object so \
the server-owned reserved keys can be attached"
);
}
object.insert(META_KEY.to_string(), Value::Object(serde_json::Map::new()));
}
object.get_mut(META_KEY).and_then(Value::as_object_mut)
}
pub(crate) fn own_reserved_result_fields(
result: &mut Value,
server_info: &Implementation,
disposition: ResponseDisposition,
owner: ReservedFieldOwner,
) {
let wire_result_type = disposition.as_wire_str();
if let Some(object) = result.as_object_mut() {
if object
.get(crate::types::mrtr::RESULT_TYPE_KEY)
.is_some_and(|existing| existing != wire_result_type)
{
tracing::warn!(
target: "mcp.v2",
field = crate::types::mrtr::RESULT_TYPE_KEY,
"overwrote a handler-supplied reserved result field with the server-computed \
value"
);
}
object.insert(
crate::types::mrtr::RESULT_TYPE_KEY.to_string(),
Value::String(wire_result_type.to_string()),
);
for field in [
crate::types::mrtr::REQUEST_STATE_KEY,
crate::types::mrtr::INPUT_REQUESTS_KEY,
] {
if owner.may_emit(field) {
continue;
}
if object.remove(field).is_some() {
tracing::warn!(
target: "mcp.v2",
field,
"removed a handler-supplied reserved result field from a result this \
egress did not mint"
);
}
}
}
let Some(meta) = result_meta_object_mut(result) else {
return;
};
if meta
.remove(crate::types::mrtr::MRTR_SIGNAL_META_KEY)
.is_some()
{
tracing::warn!(
target: "mcp.v2",
field = crate::types::mrtr::MRTR_SIGNAL_META_KEY,
"removed the pmcp-internal MRTR signal from an outgoing result"
);
}
if meta.contains_key(RESERVED_SERVER_INFO_KEY) {
tracing::warn!(
target: "mcp.v2",
field = RESERVED_SERVER_INFO_KEY,
"overwrote a handler-supplied reserved _meta field with the server's real \
Implementation"
);
}
meta.insert(
RESERVED_SERVER_INFO_KEY.to_string(),
serde_json::to_value(server_info).unwrap_or(Value::Null),
);
}
pub(crate) fn build_discover_response(
id: RequestId,
capabilities: &ServerCapabilities,
info: &Implementation,
protocol_context: Option<&crate::types::protocol::ProtocolContext>,
) -> JSONRPCResponse {
if !matches!(
protocol_context.map(|c| c.era),
Some(crate::types::protocol::Era::V2)
) {
return ServerCore::error_response(
id,
crate::types::protocol::error_codes::METHOD_NOT_FOUND,
"Method not found: server/discover".to_string(),
);
}
let negotiated_version = protocol_context.map_or_else(
|| crate::types::protocol::PROTOCOL_VERSION_2026_07_28.to_string(),
|ctx| ctx.negotiated_version.as_str().to_string(),
);
let result = discover_result_from_capabilities(capabilities, info, negotiated_version);
let mut response = ServerCore::success_response(id, serde_json::to_value(result).unwrap());
inject_v2_result_envelope(
&mut response,
protocol_context,
info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::Yes,
);
response
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) const ANONYMOUS_PRINCIPAL: &str = "";
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
const MRTR_REJECT_MESSAGE: &str = "invalid requestState";
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub(crate) const MAX_MRTR_ROUNDS: u8 = 16;
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
const MRTR_ROUND_CEILING_MESSAGE: &str =
"this request exceeded the server's multi-round-trip round limit";
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
const MRTR_ROUND_CEILING_INVARIANT_MESSAGE: &str =
"a requestState continuation cannot be minted past the server's round limit";
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
const MRTR_UNCANONICALIZABLE_MESSAGE: &str =
"these request params nest too deeply to be bound to a multi-round-trip continuation";
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
const MRTR_UNCANONICALIZABLE_INVARIANT_MESSAGE: &str =
"the originating request could not be canonicalized for the continuation binding";
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, Clone, Copy)]
pub(crate) struct MrtrPrincipal<'a> {
pub authenticated_subject: Option<&'a str>,
pub has_auth_provider: bool,
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn resolve_mrtr_principal(principal: MrtrPrincipal<'_>) -> Option<&str> {
match (principal.authenticated_subject, principal.has_auth_provider) {
(Some(subject), _) => Some(subject),
(None, true) => None,
(None, false) => Some(ANONYMOUS_PRINCIPAL),
}
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub(crate) fn client_request_mrtr_eligible(request: &ClientRequest) -> bool {
use crate::types::mrtr::{
mrtr_eligible, CALL_TOOL_METHOD, GET_PROMPT_METHOD, READ_RESOURCE_METHOD,
};
match request {
ClientRequest::CallTool(_) => mrtr_eligible(CALL_TOOL_METHOD),
ClientRequest::GetPrompt(_) => mrtr_eligible(GET_PROMPT_METHOD),
ClientRequest::ReadResource(_) => mrtr_eligible(READ_RESOURCE_METHOD),
ClientRequest::Initialize(_)
| ClientRequest::ListTools(_)
| ClientRequest::ListPrompts(_)
| ClientRequest::ListResources(_)
| ClientRequest::ListResourceTemplates(_)
| ClientRequest::Subscribe(_)
| ClientRequest::Unsubscribe(_)
| ClientRequest::Complete(_)
| ClientRequest::CreateMessage(_)
| ClientRequest::TasksGet(_)
| ClientRequest::TasksResult(_)
| ClientRequest::TasksList(_)
| ClientRequest::TasksCancel(_)
| ClientRequest::SetLoggingLevel { .. }
| ClientRequest::Ping => false,
}
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub(crate) fn mrtr_binding_parts(request: &Request) -> Option<(&'static str, Value)> {
let Request::Client(boxed) = request else {
return None;
};
if !client_request_mrtr_eligible(boxed.as_ref()) {
return None;
}
let mut frame = serde_json::to_value(boxed.as_ref()).ok()?;
let method = crate::types::mrtr::mrtr_method_static(frame.get("method")?.as_str()?)?;
let mut params = frame.get_mut("params").map_or(Value::Null, Value::take);
crate::types::mrtr::splice_mrtr_params(
&mut params,
&crate::types::mrtr::MrtrRequestParams::default(),
);
Some((method, params))
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
#[derive(Debug)]
pub(crate) enum MrtrIngest {
Inert,
Proceed {
continuation: Value,
round: u8,
kinds: Option<crate::types::mrtr::InputRequestKinds>,
},
Reject {
code: i32,
message: String,
},
Reelicit {
round: u8,
},
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub(crate) struct MrtrIngestInputs<'a> {
pub target: Option<&'a (&'static str, Value)>,
pub protocol_context: Option<&'a crate::types::protocol::ProtocolContext>,
pub principal: MrtrPrincipal<'a>,
pub codec: Option<&'a crate::server::request_state::RequestStateCodec>,
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub(crate) fn mrtr_ingest(inputs: &MrtrIngestInputs<'_>) -> MrtrIngest {
let Some(context) = inputs.protocol_context else {
return MrtrIngest::Inert;
};
if context.era != crate::types::protocol::Era::V2 {
return MrtrIngest::Inert;
}
let Some(target) = inputs.target else {
return MrtrIngest::Inert;
};
if !crate::types::mrtr::mrtr_eligible(target.0) {
return MrtrIngest::Inert;
}
let Some(token) = context.request_state_token() else {
return MrtrIngest::Inert;
};
let Some(principal) = resolve_mrtr_principal(inputs.principal) else {
tracing::warn!(
target: "mcp.mrtr",
method = target.0,
"refused a state-bearing request from an unauthenticated caller on an \
auth-configured server"
);
return MrtrIngest::Reject {
code: crate::types::protocol::error_codes::INVALID_PARAMS,
message: MRTR_REJECT_MESSAGE.to_string(),
};
};
let Some(codec) = inputs.codec else {
return MrtrIngest::Inert;
};
let binding = match crate::server::request_state::RequestBinding::from_request(
principal, target.0, &target.1,
) {
Ok(binding) => binding,
Err(error) => {
tracing::warn!(
target: "mcp.mrtr",
method = target.0,
max_depth = error.max,
"refused a state-bearing request whose params nest past the \
canonicalization depth limit — such a request has no digest that \
identifies it rather than a class of requests (D-113-M)"
);
return MrtrIngest::Reject {
code: crate::types::protocol::error_codes::INVALID_PARAMS,
message: MRTR_UNCANONICALIZABLE_MESSAGE.to_string(),
};
},
};
route_mrtr_verdict(codec.verify(token, &binding), target.0)
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
fn refuse_past_round_ceiling(round: u8, method: &str) -> Option<MrtrIngest> {
if round < MAX_MRTR_ROUNDS {
return None;
}
tracing::warn!(
target: "mcp.mrtr",
method,
round,
max_rounds = MAX_MRTR_ROUNDS,
"refused a requestState at or past the server's round ceiling — this client \
resent past the bound its own round limit should have stopped it at (D-113-L)"
);
Some(MrtrIngest::Reject {
code: crate::types::protocol::error_codes::INVALID_PARAMS,
message: MRTR_ROUND_CEILING_MESSAGE.to_string(),
})
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
fn route_mrtr_verdict(verdict: crate::server::request_state::Verdict, method: &str) -> MrtrIngest {
use crate::server::request_state::Verdict;
match verdict {
Verdict::Ok(continuation) => {
if let Some(refusal) = refuse_past_round_ceiling(continuation.round, method) {
return refusal;
}
MrtrIngest::Proceed {
continuation: continuation.state,
round: continuation.round,
kinds: continuation.kinds,
}
},
Verdict::AuthFailed => {
tracing::warn!(
target: "mcp.mrtr",
method,
"rejected a requestState that failed authentication — tampered, minted \
for a different principal, or replayed onto a different request"
);
MrtrIngest::Reject {
code: crate::types::protocol::error_codes::INVALID_PARAMS,
message: MRTR_REJECT_MESSAGE.to_string(),
}
},
Verdict::UnknownKey => {
tracing::warn!(
target: "mcp.mrtr",
method,
"requestState carries a key id this instance does not hold — re-eliciting \
from round 0 (D-04 multi-instance degradation)"
);
MrtrIngest::Reelicit { round: 0 }
},
Verdict::Expired(continuation) => {
if let Some(refusal) = refuse_past_round_ceiling(continuation.round, method) {
return refusal;
}
MrtrIngest::Reelicit {
round: continuation.round,
}
},
}
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
impl MrtrIngest {
pub(crate) fn apply(
self,
context: Option<crate::types::protocol::ProtocolContext>,
) -> std::result::Result<(Option<crate::types::protocol::ProtocolContext>, u8), (i32, String)>
{
match self {
Self::Inert => Ok((context, 0)),
Self::Proceed {
continuation,
round,
kinds,
} => {
let context = match context {
Some(ctx) => Some(
retype_verified_input_responses(ctx, kinds.as_ref())?
.with_verified_continuation(continuation, round),
),
None => None,
};
Ok((context, round))
},
Self::Reelicit { round } => Ok((
context.map(crate::types::protocol::ProtocolContext::without_mrtr),
round,
)),
Self::Reject { code, message } => Err((code, message)),
}
}
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
fn retype_verified_input_responses(
context: crate::types::protocol::ProtocolContext,
kinds: Option<&crate::types::mrtr::InputRequestKinds>,
) -> std::result::Result<crate::types::protocol::ProtocolContext, (i32, String)> {
let Some(raw) = context.input_responses_raw() else {
return Ok(context);
};
let retyped =
crate::types::mrtr::retype_input_responses_for_kinds(raw, kinds).map_err(|error| {
tracing::warn!(
target: "mcp.mrtr",
unsolicited = matches!(
error,
crate::types::mrtr::InputResponseTypingError::Unsolicited { .. }
),
"rejected an inputResponses entry that does not match the input request \
the server recorded making — before D-113-O this was silently \
reclassified and the handler re-elicited forever"
);
(
crate::types::protocol::error_codes::INVALID_PARAMS,
error.to_string(),
)
})?;
Ok(match retyped {
Some(typed) => context.with_kind_directed_input_responses(typed),
None => context,
})
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub(crate) struct MrtrEgressInputs<'a> {
pub target: Option<&'a (&'static str, Value)>,
pub protocol_context: Option<&'a crate::types::protocol::ProtocolContext>,
pub principal: MrtrPrincipal<'a>,
pub codec: Option<&'a crate::server::request_state::RequestStateCodec>,
pub round: u8,
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
#[derive(Debug)]
pub(crate) enum StrippedSignal {
Absent,
Present(Box<crate::types::mrtr::MrtrSignal>),
Malformed,
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub(crate) fn strip_mrtr_signal(result: &mut Value) -> StrippedSignal {
let Some(raw) = crate::types::mrtr::remove_mrtr_signal(result) else {
return StrippedSignal::Absent;
};
serde_json::from_value(raw).map_or(StrippedSignal::Malformed, |signal| {
StrippedSignal::Present(Box::new(signal))
})
}
#[cfg(not(feature = "streamable-http"))]
pub(crate) fn scrub_mrtr_signal(response: &mut JSONRPCResponse) {
if let crate::types::jsonrpc::ResponsePayload::Result(ref mut value) = response.payload {
if crate::types::mrtr::remove_mrtr_signal(value).is_some() {
tracing::warn!(
target: "mcp.mrtr",
field = crate::types::mrtr::MRTR_SIGNAL_META_KEY,
"removed the pmcp-internal MRTR signal from an outgoing result"
);
}
}
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
fn eligible_mrtr_target<'a>(inputs: &MrtrEgressInputs<'a>) -> Option<&'a (&'static str, Value)> {
if !matches!(
inputs.protocol_context.map(|ctx| ctx.era),
Some(crate::types::protocol::Era::V2)
) {
return None;
}
inputs
.target
.filter(|target| crate::types::mrtr::mrtr_eligible(target.0))
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
fn fail_mrtr_egress(
response: &mut JSONRPCResponse,
code: i32,
message: String,
data: Option<Value>,
) -> (ResponseDisposition, ReservedFieldOwner) {
response.payload =
crate::types::jsonrpc::ResponsePayload::Error(crate::types::jsonrpc::JSONRPCError {
code,
message,
data,
});
(ResponseDisposition::Complete, ReservedFieldOwner::None)
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub(crate) fn mrtr_egress(
response: &mut JSONRPCResponse,
inputs: &MrtrEgressInputs<'_>,
) -> (ResponseDisposition, ReservedFieldOwner) {
let stripped = match response.payload {
crate::types::jsonrpc::ResponsePayload::Result(ref mut value) => strip_mrtr_signal(value),
crate::types::jsonrpc::ResponsePayload::Error(_) => StrippedSignal::Absent,
};
let signal = match stripped {
StrippedSignal::Absent => return (ResponseDisposition::Complete, ReservedFieldOwner::None),
StrippedSignal::Malformed => {
tracing::error!(
target: "mcp.mrtr",
method = inputs.target.map(|target| target.0),
"a handler wrote the reserved MRTR signal key with a payload that is not a \
well-formed MrtrSignal"
);
return fail_mrtr_egress(
response,
crate::types::protocol::error_codes::INTERNAL_ERROR,
MRTR_MALFORMED_SIGNAL_MESSAGE.to_string(),
None,
);
},
StrippedSignal::Present(signal) => signal,
};
let Some(target) = eligible_mrtr_target(inputs) else {
tracing::error!(
target: "mcp.mrtr",
method = inputs.target.map(|target| target.0),
"a handler signalled input_required where the spec forbids it — on v1, on a \
non-opted-in request, or on a method outside tools/call, prompts/get and \
resources/read"
);
return fail_mrtr_egress(
response,
crate::types::protocol::error_codes::INTERNAL_ERROR,
MRTR_FORBIDDEN_PATH_MESSAGE.to_string(),
None,
);
};
if let Some(rejection) = reject_undeclared_capabilities(&signal, inputs, target.0) {
return fail_mrtr_egress(
response,
crate::types::protocol::error_codes::MISSING_REQUIRED_CLIENT_CAPABILITY,
rejection.0,
Some(rejection.1),
);
}
if let Err(error) = crate::types::mrtr::salient_param_digest(target.0, &target.1) {
tracing::warn!(
target: "mcp.mrtr",
method = target.0,
max_depth = error.max,
"refused to mint a continuation for params that nest past the \
canonicalization depth limit — no requestState was minted (D-113-M)"
);
return fail_mrtr_egress(
response,
crate::types::protocol::error_codes::INVALID_PARAMS,
MRTR_UNCANONICALIZABLE_MESSAGE.to_string(),
None,
);
}
match seal_input_required(response, &signal, target, inputs) {
Ok(minted) => minted,
Err(reason) => {
tracing::error!(target: "mcp.mrtr", reason, "could not emit an input_required result");
fail_mrtr_egress(
response,
crate::types::protocol::error_codes::INTERNAL_ERROR,
reason.to_string(),
None,
)
},
}
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
const MRTR_FORBIDDEN_PATH_MESSAGE: &str =
"the server produced an input_required signal on a request that cannot carry one";
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
const MRTR_MALFORMED_SIGNAL_MESSAGE: &str = "the server produced a malformed input_required signal";
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
const MISSING_CAPABILITY_MESSAGE: &str =
"the server needs a client capability this client did not declare";
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
fn reject_undeclared_capabilities(
signal: &crate::types::mrtr::MrtrSignal,
inputs: &MrtrEgressInputs<'_>,
method: &str,
) -> Option<(String, Value)> {
let declared = inputs
.protocol_context
.and_then(|context| context.client_capabilities.as_ref());
let missing = missing_client_capabilities(&signal.input_requests, declared)?;
let required =
serde_json::to_value(&missing).unwrap_or_else(|_| Value::Object(serde_json::Map::new()));
tracing::warn!(
target: "mcp.mrtr",
method,
required = %required,
"refused to emit inputRequests for a capability the client did not declare — no \
requestState was minted"
);
Some((
MISSING_CAPABILITY_MESSAGE.to_string(),
serde_json::json!({ "requiredCapabilities": required }),
))
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum MissingCapability {
Elicitation,
ElicitationUrl,
Sampling,
SamplingTools,
Roots,
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
#[derive(Debug, Default)]
struct MissingCapabilities(std::collections::BTreeSet<MissingCapability>);
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
impl MissingCapabilities {
fn note(
&mut self,
request: &crate::types::mrtr::InputRequest,
declared: Option<&crate::types::ClientCapabilities>,
) {
match request {
crate::types::mrtr::InputRequest::Elicitation(params) => {
self.note_elicitation(params, declared.and_then(|caps| caps.elicitation.as_ref()));
},
crate::types::mrtr::InputRequest::Sampling(params) => {
self.note_sampling(params, declared.and_then(|caps| caps.sampling.as_ref()));
},
crate::types::mrtr::InputRequest::ListRoots => {
if declared.is_none_or(|caps| caps.roots.is_none()) {
self.0.insert(MissingCapability::Roots);
}
},
}
}
fn note_elicitation(
&mut self,
params: &crate::types::elicitation::ElicitRequestParams,
declared: Option<&crate::types::capabilities::ElicitationCapabilities>,
) {
match (params, declared) {
(crate::types::elicitation::ElicitRequestParams::Form { .. }, None) => {
self.0.insert(MissingCapability::Elicitation);
},
(crate::types::elicitation::ElicitRequestParams::Url { .. }, None) => {
self.0.insert(MissingCapability::Elicitation);
self.0.insert(MissingCapability::ElicitationUrl);
},
(crate::types::elicitation::ElicitRequestParams::Form { .. }, Some(_)) => {},
(crate::types::elicitation::ElicitRequestParams::Url { .. }, Some(caps)) => {
if caps.url.is_none() {
self.0.insert(MissingCapability::ElicitationUrl);
}
},
}
}
fn note_sampling(
&mut self,
params: &crate::types::sampling::CreateMessageParams,
declared: Option<&crate::types::capabilities::SamplingCapabilities>,
) {
let needs_tools = params.tools.is_some() || params.tool_choice.is_some();
let tools_declared = declared.is_some_and(|caps| caps.tools.is_some());
if declared.is_none() {
self.0.insert(MissingCapability::Sampling);
}
if needs_tools && !tools_declared {
self.0.insert(MissingCapability::SamplingTools);
}
}
fn into_capabilities(self) -> Option<crate::types::ClientCapabilities> {
if self.0.is_empty() {
return None;
}
let empty = || Value::Object(serde_json::Map::new());
let has = |capability| self.0.contains(&capability);
let mut missing = crate::types::ClientCapabilities::default();
if has(MissingCapability::Elicitation) || has(MissingCapability::ElicitationUrl) {
missing.elicitation = Some(crate::types::capabilities::ElicitationCapabilities {
form: None,
url: has(MissingCapability::ElicitationUrl).then(empty),
});
}
if has(MissingCapability::Sampling) || has(MissingCapability::SamplingTools) {
missing.sampling = Some(crate::types::capabilities::SamplingCapabilities {
models: None,
context: None,
tools: has(MissingCapability::SamplingTools).then(empty),
});
}
if has(MissingCapability::Roots) {
missing.roots = Some(crate::types::capabilities::RootsCapabilities::default());
}
Some(missing)
}
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
fn missing_client_capabilities(
requests: &crate::types::mrtr::InputRequests,
declared: Option<&crate::types::ClientCapabilities>,
) -> Option<crate::types::ClientCapabilities> {
let mut missing = MissingCapabilities::default();
for request in requests.values() {
missing.note(request, declared);
}
missing.into_capabilities()
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
fn seal_input_required(
response: &mut JSONRPCResponse,
signal: &crate::types::mrtr::MrtrSignal,
target: &(&'static str, Value),
inputs: &MrtrEgressInputs<'_>,
) -> std::result::Result<(ResponseDisposition, ReservedFieldOwner), &'static str> {
let next_round = inputs.round.saturating_add(1);
if next_round > MAX_MRTR_ROUNDS {
return Err(MRTR_ROUND_CEILING_INVARIANT_MESSAGE);
}
let principal = resolve_mrtr_principal(inputs.principal)
.ok_or("a requestState continuation cannot be minted for an unauthenticated caller")?;
let codec = inputs
.codec
.ok_or("this server has no requestState codec configured")?;
let binding =
crate::server::request_state::RequestBinding::from_request(principal, target.0, &target.1)
.map_err(|_| MRTR_UNCANONICALIZABLE_INVARIANT_MESSAGE)?;
let kinds: crate::types::mrtr::InputRequestKinds = signal
.input_requests
.iter()
.map(|(key, request)| (key.clone(), request.kind()))
.collect();
let token = codec
.mint(&signal.continuation, &binding, next_round, Some(kinds))
.map_err(|_| "the requestState continuation could not be sealed")?;
let input_requests = serde_json::to_value(&signal.input_requests)
.map_err(|_| "the handler's inputRequests map is not serializable")?;
let crate::types::jsonrpc::ResponsePayload::Result(ref mut value) = response.payload else {
return Err("an input_required signal cannot ride on an error response");
};
let result = value
.as_object_mut()
.ok_or("an input_required result must be a JSON object")?;
result.insert(
crate::types::mrtr::INPUT_REQUESTS_KEY.to_string(),
input_requests,
);
result.insert(
crate::types::mrtr::REQUEST_STATE_KEY.to_string(),
Value::String(token),
);
Ok((ResponseDisposition::InputRequired, ReservedFieldOwner::Mrtr))
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub(crate) struct MrtrRound {
target: Option<(&'static str, Value)>,
subject: Option<String>,
has_auth_provider: bool,
round: u8,
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
impl MrtrRound {
pub(crate) fn begin(
request: &Request,
context: Option<crate::types::protocol::ProtocolContext>,
auth_subject: Option<&str>,
has_auth_provider: bool,
codec: Option<&crate::server::request_state::RequestStateCodec>,
) -> std::result::Result<(Self, Option<crate::types::protocol::ProtocolContext>), (i32, String)>
{
let target = context
.as_ref()
.filter(|context| context.era == crate::types::protocol::Era::V2)
.and_then(|_| mrtr_binding_parts(request));
let subject = target
.as_ref()
.and_then(|_| auth_subject.map(str::to_string));
let (context, round) = mrtr_ingest(&MrtrIngestInputs {
target: target.as_ref(),
protocol_context: context.as_ref(),
principal: MrtrPrincipal {
authenticated_subject: subject.as_deref(),
has_auth_provider,
},
codec,
})
.apply(context)?;
Ok((
Self {
target,
subject,
has_auth_provider,
round,
},
context,
))
}
pub(crate) fn finish(
&self,
response: &mut JSONRPCResponse,
context: Option<&crate::types::protocol::ProtocolContext>,
codec: Option<&crate::server::request_state::RequestStateCodec>,
) -> (ResponseDisposition, ReservedFieldOwner) {
mrtr_egress(
response,
&MrtrEgressInputs {
target: self.target.as_ref(),
protocol_context: context,
principal: MrtrPrincipal {
authenticated_subject: self.subject.as_deref(),
has_auth_provider: self.has_auth_provider,
},
codec,
round: self.round,
},
)
}
}
#[async_trait]
impl ProtocolHandler for ServerCore {
async fn handle_request(
&self,
id: RequestId,
request: Request,
auth_context: Option<AuthContext>,
) -> JSONRPCResponse {
let mut jsonrpc_request = create_request(id.clone(), request.clone());
let context = MiddlewareContext::with_request_id(id.to_string());
context.set_metadata("method".to_string(), jsonrpc_request.method.clone());
if let Err(e) = self
.protocol_middleware
.read()
.await
.process_request_with_context(&mut jsonrpc_request, &context)
.await
{
return Self::error_response(
id,
crate::types::protocol::error_codes::INTERNAL_ERROR,
e.to_string(),
);
}
let protocol_context = match self.resolve_ingress_protocol_context(&request) {
Ok(ctx) => ctx,
Err(negotiation_error) => {
let (code, message) = negotiation_error_to_rejection(&negotiation_error);
return Self::error_response(id, code, message);
},
};
#[cfg(feature = "streamable-http")]
let (mrtr, protocol_context) = match MrtrRound::begin(
&request,
protocol_context,
auth_context.as_ref().map(|ctx| ctx.subject.as_str()),
self.auth_provider.is_some(),
self.request_state_codec(),
) {
Ok(resolved) => resolved,
Err((code, message)) => return Self::error_response(id, code, message),
};
let cacheable = request_is_cacheable(&request);
let mut dispatch_claim = DispatchEnvelopeClaim::NONE;
let mut response = self
.handle_request_internal(
id.clone(),
request,
auth_context,
protocol_context.clone(),
&mut dispatch_claim,
)
.await;
#[cfg(feature = "streamable-http")]
let (disposition, reserved_field_owner) = mrtr.finish(
&mut response,
protocol_context.as_ref(),
self.request_state_codec(),
);
#[cfg(not(feature = "streamable-http"))]
let (disposition, reserved_field_owner) = {
scrub_mrtr_signal(&mut response);
(ResponseDisposition::Complete, ReservedFieldOwner::None)
};
let claim = dispatch_claim.or_egress(disposition, reserved_field_owner);
inject_v2_result_envelope(
&mut response,
protocol_context.as_ref(),
&self.info,
claim.disposition,
claim.owner,
cacheable,
);
if let Err(e) = self
.protocol_middleware
.read()
.await
.process_response_with_context(&mut response, &context)
.await
{
tracing::warn!("Response middleware processing failed: {}", e);
}
response
}
async fn handle_notification(&self, notification: Notification) -> Result<()> {
let mut jsonrpc_notification = create_notification(notification.clone());
let context = MiddlewareContext::default();
context.set_metadata("method".to_string(), jsonrpc_notification.method.clone());
if let Err(e) = self
.protocol_middleware
.read()
.await
.process_notification_with_context(&mut jsonrpc_notification, &context)
.await
{
tracing::warn!("Notification middleware processing failed: {}", e);
}
self.handle_notification_internal(notification).await
}
fn capabilities(&self) -> &ServerCapabilities {
&self.capabilities
}
fn info(&self) -> &Implementation {
&self.info
}
}
impl ServerCore {
#[cfg(not(target_arch = "wasm32"))]
fn resolve_task_owner(
&self,
auth_context: Option<&AuthContext>,
era: Option<crate::types::protocol::Era>,
) -> crate::server::task_dispatch::OwnerBinding {
self.task_dispatch().resolve_owner(auth_context, era)
}
#[cfg(not(target_arch = "wasm32"))]
fn task_dispatch(&self) -> crate::server::task_dispatch::TaskDispatch<'_> {
crate::server::task_dispatch::TaskDispatch {
task_store: &self.task_store,
task_router: &self.task_router,
has_auth_provider: self.auth_provider.is_some(),
}
}
#[cfg(not(target_arch = "wasm32"))]
async fn build_task_created_response(
&self,
id: RequestId,
task_value: Value,
auth_context: Option<&AuthContext>,
era: Option<crate::types::protocol::Era>,
) -> (JSONRPCResponse, DispatchEnvelopeClaim) {
self.task_dispatch()
.build_task_created_response(id, task_value, auth_context, era)
.await
}
async fn handle_request_internal(
&self,
id: RequestId,
request: Request,
auth_context: Option<AuthContext>,
protocol_context: Option<crate::types::protocol::ProtocolContext>,
dispatch_claim: &mut DispatchEnvelopeClaim,
) -> JSONRPCResponse {
contract_pre_session_lifecycle!();
match request {
Request::Client(ref boxed_req)
if matches!(**boxed_req, ClientRequest::Initialize(_)) =>
{
let ClientRequest::Initialize(init_req) = boxed_req.as_ref() else {
unreachable!("Pattern matched for Initialize");
};
match self.handle_initialize(init_req).await {
Ok(result) => Self::success_response(id, serde_json::to_value(result).unwrap()),
Err(e) => Self::error_response(
id,
crate::types::protocol::error_codes::INTERNAL_ERROR,
e.to_string(),
),
}
},
Request::Client(ref boxed_req) => {
if v1_initialize_gate_applies(
self.stateless_mode,
protocol_context.as_ref().map(|ctx| ctx.era),
) && !self.is_initialized().await
{
return Self::error_response(
id,
crate::types::protocol::error_codes::V1_TASK_PENDING,
"Server not initialized. Call initialize first.".to_string(),
);
}
match boxed_req.as_ref() {
ClientRequest::ListTools(req) => match self.handle_list_tools(req).await {
Ok(result) => {
Self::success_response(id, serde_json::to_value(result).unwrap())
},
Err(e) => Self::error_response(
id,
crate::types::protocol::error_codes::INTERNAL_ERROR,
e.to_string(),
),
},
ClientRequest::CallTool(req) => {
#[cfg(not(target_arch = "wasm32"))]
let call_tool_era = protocol_context.as_ref().map(|ctx| ctx.era);
#[cfg(not(target_arch = "wasm32"))]
if let Some(ref task_router) = self.task_router {
let tool_execution = self
.tool_infos
.get(&req.name)
.and_then(|m| m.execution.as_ref());
let needs_task = req.task.is_some() || {
let exec_value =
tool_execution.and_then(|e| serde_json::to_value(e).ok());
task_router.tool_requires_task(&req.name, exec_value.as_ref())
};
if needs_task {
let crate::server::task_dispatch::OwnerBinding::Owner(owner_id) =
self.resolve_task_owner(auth_context.as_ref(), call_tool_era)
else {
return crate::server::task_dispatch::authentication_required(
id,
crate::types::mrtr::CALL_TOOL_METHOD,
);
};
let task_params =
req.task.clone().unwrap_or_else(|| serde_json::json!({}));
#[allow(clippy::used_underscore_binding)]
let progress_token = req
._meta
.as_ref()
.and_then(|m| m.progress_token.as_ref())
.map(|t| serde_json::to_value(t).unwrap());
return match task_router
.handle_task_call(
&req.name,
req.arguments.clone(),
task_params,
&owner_id,
progress_token,
)
.await
{
Ok(result) => Self::success_response(id, result),
Err(e) => Self::error_response(
id,
crate::types::protocol::error_codes::INTERNAL_ERROR,
e.to_string(),
),
};
}
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::used_underscore_binding)]
let continuation_ctx = req
._meta
.as_ref()
.and_then(|m| m._task_id.clone())
.map(|task_id| (task_id, req.name.clone()));
match self
.handle_call_tool(req, auth_context.clone(), protocol_context)
.await
{
Ok(outcome) => match outcome {
#[cfg(not(target_arch = "wasm32"))]
ToolCallOutcome::TaskCreated { task_value } => {
let (response, claim) = self
.build_task_created_response(
id,
task_value,
auth_context.as_ref(),
call_tool_era,
)
.await;
*dispatch_claim = claim;
response
},
ToolCallOutcome::Result(result) => {
#[cfg(not(target_arch = "wasm32"))]
if let (Some((task_id, tool_name)), Some(ref task_router)) =
(continuation_ctx, &self.task_router)
{
match self.resolve_task_owner(
auth_context.as_ref(),
call_tool_era,
) {
crate::server::task_dispatch::OwnerBinding::Owner(
owner_id,
) => {
let tool_result_value =
serde_json::to_value(&result)
.unwrap_or_default();
if let Err(e) = task_router
.handle_workflow_continuation(
&task_id,
&tool_name,
tool_result_value,
&owner_id,
)
.await
{
tracing::warn!(
"Workflow continuation recording failed for task {}: {}",
task_id,
e
);
}
},
crate::server::task_dispatch::OwnerBinding::Refused => {
tracing::warn!(
target: "mcp.tasks",
task_id = %task_id,
tool = %tool_name,
"workflow continuation NOT recorded: an \
unauthenticated caller on an auth-configured \
server binds no v2 task owner"
);
},
}
}
Self::success_response(
id,
serde_json::to_value(result).unwrap(),
)
},
},
Err(e) => Self::error_response(
id,
crate::types::protocol::error_codes::INTERNAL_ERROR,
e.to_string(),
),
}
},
ClientRequest::ListPrompts(req) => match self.handle_list_prompts(req).await {
Ok(result) => {
Self::success_response(id, serde_json::to_value(result).unwrap())
},
Err(e) => Self::error_response(
id,
crate::types::protocol::error_codes::INTERNAL_ERROR,
e.to_string(),
),
},
ClientRequest::GetPrompt(req) => {
match self
.handle_get_prompt(req, auth_context.clone(), protocol_context)
.await
{
Ok(result) => {
Self::success_response(id, serde_json::to_value(result).unwrap())
},
Err(e) => Self::error_response(
id,
crate::types::protocol::error_codes::INTERNAL_ERROR,
e.to_string(),
),
}
},
ClientRequest::ListResources(req) => {
match self.handle_list_resources(req, auth_context.clone()).await {
Ok(result) => {
Self::success_response(id, serde_json::to_value(result).unwrap())
},
Err(e) => Self::error_response(
id,
crate::types::protocol::error_codes::INTERNAL_ERROR,
e.to_string(),
),
}
},
ClientRequest::ReadResource(req) => {
match self
.handle_read_resource(req, auth_context.clone(), protocol_context)
.await
{
Ok(result) => {
Self::success_response(id, serde_json::to_value(result).unwrap())
},
Err(e) => Self::error_response(
id,
crate::types::protocol::error_codes::INTERNAL_ERROR,
e.to_string(),
),
}
},
ClientRequest::ListResourceTemplates(req) => {
match self.handle_list_resource_templates(req).await {
Ok(result) => {
Self::success_response(id, serde_json::to_value(result).unwrap())
},
Err(e) => Self::error_response(
id,
crate::types::protocol::error_codes::INTERNAL_ERROR,
e.to_string(),
),
}
},
#[cfg(not(target_arch = "wasm32"))]
request @ (ClientRequest::TasksGet(_)
| ClientRequest::TasksResult(_)
| ClientRequest::TasksList(_)
| ClientRequest::TasksCancel(_)) => {
let (response, claim) = self
.task_dispatch()
.route_tasks_endpoint(
id,
request,
auth_context.as_ref(),
protocol_context.as_ref(),
)
.await;
*dispatch_claim = claim;
response
},
_ => Self::error_response(
id,
crate::types::protocol::error_codes::METHOD_NOT_FOUND,
"Method not supported".to_string(),
),
}
},
Request::Server(_) => Self::error_response(
id,
crate::types::protocol::error_codes::METHOD_NOT_FOUND,
"Method not supported".to_string(),
),
}
}
async fn handle_notification_internal(&self, _notification: Notification) -> Result<()> {
Ok(())
}
}
fn summarize_structured_output(value: &Value) -> String {
match value {
Value::Array(arr) => format_record_count(arr.len()),
Value::Object(map) => {
for key in ["results", "items", "data", "records", "rows", "entries"] {
if let Some(Value::Array(arr)) = map.get(key) {
return format_record_count(arr.len());
}
}
let field_count = map.len();
match field_count {
0 => "Empty result.".to_string(),
1 => "Result with 1 field.".to_string(),
n => format!("Result with {n} fields."),
}
},
Value::String(s) => {
if s.len() <= 200 {
s.clone()
} else {
let truncated: String = s.chars().take(200).collect();
format!("{truncated}...")
}
},
Value::Null => "No result.".to_string(),
other => other.to_string(),
}
}
fn format_record_count(len: usize) -> String {
match len {
0 => "No records returned.".to_string(),
1 => "1 record returned.".to_string(),
n => format!("{n} records returned."),
}
}
fn json_serialized_len(value: &impl serde::Serialize) -> Result<usize> {
struct CountingWriter(usize);
impl std::io::Write for CountingWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0 += buf.len();
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut counter = CountingWriter(0);
serde_json::to_writer(&mut counter, value)
.map_err(|e| Error::validation(format!("Cannot measure argument size: {e}")))?;
Ok(counter.0)
}
pub(crate) fn request_meta_to_value<T: serde::Serialize>(
meta: Option<&T>,
) -> Option<serde_json::Value> {
meta.and_then(|m| serde_json::to_value(m).ok())
}
#[allow(clippy::used_underscore_binding)] pub(crate) fn extract_request_meta_value(request: &Request) -> Option<serde_json::Value> {
match request {
Request::Client(boxed) => match boxed.as_ref() {
ClientRequest::CallTool(req) => request_meta_to_value(req._meta.as_ref()),
ClientRequest::GetPrompt(req) => request_meta_to_value(req._meta.as_ref()),
ClientRequest::ReadResource(req) => request_meta_to_value(req._meta.as_ref()),
ClientRequest::Initialize(_)
| ClientRequest::ListTools(_)
| ClientRequest::ListPrompts(_)
| ClientRequest::ListResources(_)
| ClientRequest::ListResourceTemplates(_)
| ClientRequest::Subscribe(_)
| ClientRequest::Unsubscribe(_)
| ClientRequest::Complete(_)
| ClientRequest::CreateMessage(_)
| ClientRequest::TasksGet(_)
| ClientRequest::TasksResult(_)
| ClientRequest::TasksList(_)
| ClientRequest::TasksCancel(_)
| ClientRequest::SetLoggingLevel { .. }
| ClientRequest::Ping => None,
},
Request::Server(_) => None,
}
}
pub(crate) fn resolve_ingress_protocol_context(
accept_list: &[crate::types::ProtocolVersion],
request: &Request,
) -> std::result::Result<
Option<crate::types::protocol::ProtocolContext>,
crate::types::protocol::context::ProtocolNegotiationError,
> {
if !crate::types::protocol::context::is_v2_opted_in(accept_list) {
return Ok(None);
}
let meta = extract_request_meta_value(request);
crate::types::protocol::context::resolve_protocol_context(accept_list, meta.as_ref())
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) const fn v1_initialize_gate_applies(
stateless_mode: bool,
era: Option<crate::types::protocol::Era>,
) -> bool {
!stateless_mode && !matches!(era, Some(crate::types::protocol::Era::V2))
}
pub(crate) fn negotiation_error_to_rejection(
error: &crate::types::protocol::context::ProtocolNegotiationError,
) -> (i32, String) {
use crate::types::protocol::context::ProtocolNegotiationError;
use crate::types::protocol::error_codes::INVALID_PARAMS;
match error {
ProtocolNegotiationError::UnsupportedVersion(v) => {
(INVALID_PARAMS, format!("Unsupported protocol version: {v}"))
},
ProtocolNegotiationError::MalformedMeta(reason) => {
(INVALID_PARAMS, format!("Malformed _meta: {reason}"))
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::tool_middleware::ToolMiddlewareChain;
use crate::types::ClientCapabilities;
struct TestTool;
#[async_trait]
impl ToolHandler for TestTool {
async fn handle(&self, _args: Value, _extra: RequestHandlerExtra) -> Result<Value> {
Ok(serde_json::json!({"result": "success"}))
}
}
fn build_tool_infos(
tools: &HashMap<String, Arc<dyn ToolHandler>>,
) -> HashMap<String, ToolInfo> {
tools
.iter()
.map(|(name, handler)| {
let mut info = handler
.metadata()
.unwrap_or_else(|| ToolInfo::new(name.clone(), None, serde_json::json!({})));
info.name.clone_from(name);
(name.clone(), info)
})
.collect()
}
#[tokio::test]
async fn test_server_core_initialization() {
let mut tools = HashMap::new();
tools.insert(
"test-tool".to_string(),
Arc::new(TestTool) as Arc<dyn ToolHandler>,
);
let tool_infos = build_tool_infos(&tools);
let server = ServerCore::new(
Implementation::new("test-server", "1.0.0"),
ServerCapabilities::tools_only(),
tools,
HashMap::new(),
tool_infos,
HashMap::new(),
None,
None,
None,
None,
Arc::new(RwLock::new(EnhancedMiddlewareChain::new())),
Arc::new(RwLock::new(ToolMiddlewareChain::new())),
None, None, false, PayloadLimits::default(),
);
assert!(!server.is_initialized().await);
let init_req = Request::Client(Box::new(ClientRequest::Initialize(InitializeRequest {
protocol_version: crate::DEFAULT_PROTOCOL_VERSION.to_string(),
capabilities: ClientCapabilities::default(),
client_info: Implementation::new("test-client", "1.0.0"),
})));
let response = server
.handle_request(RequestId::from(1i64), init_req, None)
.await;
match response.payload {
ResponsePayload::Result(_) => {
assert!(server.is_initialized().await);
},
ResponsePayload::Error(e) => panic!("Initialization failed: {}", e.message),
}
}
#[tokio::test]
async fn test_server_core_list_tools() {
let mut tools = HashMap::new();
tools.insert(
"test-tool".to_string(),
Arc::new(TestTool) as Arc<dyn ToolHandler>,
);
let tool_infos = build_tool_infos(&tools);
let server = ServerCore::new(
Implementation::new("test-server", "1.0.0"),
ServerCapabilities::tools_only(),
tools,
HashMap::new(),
tool_infos,
HashMap::new(),
None,
None,
None,
None,
Arc::new(RwLock::new(EnhancedMiddlewareChain::new())),
Arc::new(RwLock::new(ToolMiddlewareChain::new())),
None, None, false, PayloadLimits::default(),
);
let init_req = Request::Client(Box::new(ClientRequest::Initialize(InitializeRequest {
protocol_version: crate::DEFAULT_PROTOCOL_VERSION.to_string(),
capabilities: ClientCapabilities::default(),
client_info: Implementation::new("test-client", "1.0.0"),
})));
server
.handle_request(RequestId::from(1i64), init_req, None)
.await;
let list_req = Request::Client(Box::new(ClientRequest::ListTools(ListToolsRequest {
cursor: None,
})));
let response = server
.handle_request(RequestId::from(2i64), list_req, None)
.await;
match response.payload {
ResponsePayload::Result(result) => {
let tools_result: ListToolsResult = serde_json::from_value(result).unwrap();
assert_eq!(tools_result.tools.len(), 1);
assert_eq!(tools_result.tools[0].name, "test-tool");
},
ResponsePayload::Error(e) => panic!("List tools failed: {}", e.message),
}
}
struct EraProbeTool;
#[async_trait]
impl ToolHandler for EraProbeTool {
async fn handle(&self, _args: Value, extra: RequestHandlerExtra) -> Result<Value> {
let era = extra.era().map(|e| format!("{e:?}"));
let traceparent = extra.trace_context().map(|tc| tc.traceparent);
Ok(serde_json::json!({ "era": era, "traceparent": traceparent }))
}
}
fn probe_payload(result: &Value) -> Value {
let text = result["content"][0]["text"]
.as_str()
.expect("probe result carries text content");
serde_json::from_str(text).expect("probe text content is JSON")
}
fn probe_call_with_v2_meta() -> Request {
use crate::types::protocol::context::RESERVED_PROTOCOL_VERSION_KEY;
let meta = crate::types::protocol::RequestMeta::new()
.with_meta(
RESERVED_PROTOCOL_VERSION_KEY,
serde_json::json!("2026-07-28"),
)
.with_meta(
"traceparent",
serde_json::json!("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"),
);
Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
name: "probe".to_string(),
arguments: serde_json::json!({}),
_meta: Some(meta),
task: None,
})))
}
#[tokio::test]
async fn test_v2_meta_visible_in_handler_end_to_end() {
use crate::types::protocol::PROTOCOL_VERSION_2026_07_28;
let server = crate::server::builder::ServerCoreBuilder::new()
.name("probe-server")
.version("1.0.0")
.tool("probe", EraProbeTool)
.stateless_mode(true)
.with_supported_protocol_versions([
ProtocolVersion("2025-11-25".to_string()),
ProtocolVersion(PROTOCOL_VERSION_2026_07_28.to_string()),
])
.build()
.unwrap();
let response = server
.handle_request(RequestId::from(7i64), probe_call_with_v2_meta(), None)
.await;
match response.payload {
ResponsePayload::Result(result) => {
let probe = probe_payload(&result);
assert_eq!(probe["era"], "V2");
assert_eq!(
probe["traceparent"],
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
);
},
ResponsePayload::Error(e) => panic!("probe call failed: {}", e.message),
}
}
#[tokio::test]
async fn test_non_opted_in_server_resolves_no_context() {
let server = crate::server::builder::ServerCoreBuilder::new()
.name("v1-server")
.version("1.0.0")
.tool("probe", EraProbeTool)
.stateless_mode(true)
.build()
.unwrap();
let response = server
.handle_request(RequestId::from(8i64), probe_call_with_v2_meta(), None)
.await;
match response.payload {
ResponsePayload::Result(result) => {
let probe = probe_payload(&result);
assert_eq!(probe["era"], serde_json::Value::Null);
},
ResponsePayload::Error(e) => panic!("probe call failed: {}", e.message),
}
}
#[tokio::test]
async fn test_unsupported_version_rejected_at_ingress() {
use crate::types::protocol::context::RESERVED_PROTOCOL_VERSION_KEY;
use crate::types::protocol::PROTOCOL_VERSION_2026_07_28;
let server = crate::server::builder::ServerCoreBuilder::new()
.name("probe-server")
.version("1.0.0")
.tool("probe", EraProbeTool)
.stateless_mode(true)
.with_supported_protocol_versions([ProtocolVersion(
PROTOCOL_VERSION_2026_07_28.to_string(),
)])
.build()
.unwrap();
let meta = crate::types::protocol::RequestMeta::new().with_meta(
RESERVED_PROTOCOL_VERSION_KEY,
serde_json::json!("1999-01-01"),
);
let call = Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
name: "probe".to_string(),
arguments: serde_json::json!({}),
_meta: Some(meta),
task: None,
})));
let response = server
.handle_request(RequestId::from(9i64), call, None)
.await;
match response.payload {
ResponsePayload::Error(e) => {
assert_eq!(e.code, crate::types::protocol::error_codes::INVALID_PARAMS);
},
ResponsePayload::Result(_) => {
panic!("unsupported version must be rejected, not served")
},
}
}
fn v2_ctx() -> crate::types::protocol::ProtocolContext {
crate::types::protocol::ProtocolContext::new(
crate::types::protocol::Era::V2,
ProtocolVersion(crate::types::protocol::PROTOCOL_VERSION_2026_07_28.to_string()),
)
}
fn v1_ctx() -> crate::types::protocol::ProtocolContext {
crate::types::protocol::ProtocolContext::new(
crate::types::protocol::Era::V1,
ProtocolVersion("2025-11-25".to_string()),
)
}
fn discover_server() -> ServerCore {
crate::server::builder::ServerCoreBuilder::new()
.name("discover-server")
.version("9.9.9")
.tool("probe", EraProbeTool)
.stateless_mode(true)
.with_extension(
"io.example/experimental",
serde_json::json!({ "enabled": true }),
)
.with_supported_protocol_versions([
ProtocolVersion("2025-11-25".to_string()),
ProtocolVersion(crate::types::protocol::PROTOCOL_VERSION_2026_07_28.to_string()),
])
.build()
.unwrap()
}
#[test]
fn server_discover_v2_projects_capabilities_with_extensions() {
let server = discover_server();
let internal = crate::types::protocol::classify_internal_method(
"server/discover",
&serde_json::json!({}),
)
.expect("server/discover classifies as internal");
assert!(matches!(
internal,
crate::types::protocol::InternalClientRequest::ServerDiscover(_)
));
let ctx = v2_ctx();
let response = build_discover_response(
RequestId::from(1i64),
&server.capabilities,
&server.info,
Some(&ctx),
);
let ResponsePayload::Result(value) = response.payload else {
panic!("v2 server/discover must return a result");
};
assert_eq!(
value["capabilities"]["extensions"]["io.example/experimental"]["enabled"],
serde_json::json!(true)
);
assert_eq!(value["serverInfo"]["name"], "discover-server");
assert_eq!(value["serverInfo"]["version"], "9.9.9");
assert_eq!(value["protocolVersion"], "2026-07-28");
}
#[test]
fn server_discover_v1_returns_method_not_found() {
let server = discover_server();
let ctx = v1_ctx();
let resp = build_discover_response(
RequestId::from(2i64),
&server.capabilities,
&server.info,
Some(&ctx),
);
let ResponsePayload::Error(e) = resp.payload else {
panic!("v1 server/discover must be an error");
};
assert_eq!(
e.code,
crate::types::protocol::error_codes::METHOD_NOT_FOUND
);
assert_eq!(e.code, -32601);
let resp_none = build_discover_response(
RequestId::from(3i64),
&server.capabilities,
&server.info,
None,
);
let ResponsePayload::Error(e2) = resp_none.payload else {
panic!("context-less server/discover must be an error");
};
assert_eq!(e2.code, -32601);
}
#[test]
fn server_discover_public_parse_is_method_not_found() {
let req = crate::types::JSONRPCRequest::new(
RequestId::from(1i64),
"server/discover".to_string(),
Some(serde_json::json!({})),
);
let err = crate::shared::parse_request(req).unwrap_err();
assert!(err.to_string().contains("Method not found"));
}
#[tokio::test]
async fn server_discover_does_not_mutate_init_state() {
let server = crate::server::builder::ServerCoreBuilder::new()
.name("discover-server")
.version("1.0.0")
.tool("probe", EraProbeTool)
.with_supported_protocol_versions([ProtocolVersion(
crate::types::protocol::PROTOCOL_VERSION_2026_07_28.to_string(),
)])
.build()
.unwrap();
assert!(!server.is_initialized().await);
let ctx = v2_ctx();
let _ = build_discover_response(
RequestId::from(1i64),
&server.capabilities,
&server.info,
Some(&ctx),
);
assert!(
!server.is_initialized().await,
"server/discover must not flip initialization state"
);
}
#[test]
fn server_discover_wire_shape_golden() {
let caps = ServerCapabilities::tools_only();
let info = Implementation::new("golden-server", "1.2.3");
let result = discover_result_from_capabilities(&caps, &info, "2026-07-28".to_string());
let value = serde_json::to_value(&result).unwrap();
let expected = serde_json::json!({
"protocolVersion": "2026-07-28",
"capabilities": { "tools": { "listChanged": true } },
"serverInfo": { "name": "golden-server", "version": "1.2.3" }
});
assert_eq!(value, expected, "discover wire shape drifted from golden");
}
fn tasks_backed_capabilities() -> ServerCapabilities {
let mut caps = ServerCapabilities::default();
crate::server::task_dispatch::apply_tasks_capability_rule(&mut caps, &HashMap::new(), true)
.unwrap();
let mut experimental = HashMap::new();
experimental.insert("tasks".to_string(), serde_json::json!({ "legacy": true }));
experimental.insert("io.example/flag".to_string(), serde_json::json!(true));
caps.experimental = Some(experimental);
caps
}
#[test]
fn server_discover_projects_the_tasks_extension_and_hides_the_v1_tasks_keys() {
let info = Implementation::new("tasks-server", "1.0.0");
let result = discover_result_from_capabilities(
&tasks_backed_capabilities(),
&info,
"2026-07-28".to_string(),
);
let value = serde_json::to_value(&result).unwrap();
let caps = &value["capabilities"];
assert_eq!(
caps["extensions"][crate::types::capabilities::TASKS_EXTENSION_KEY],
serde_json::json!({}),
"v2 discover must advertise the tasks extension as the empty object: {value}"
);
assert!(
caps.get("tasks").is_none(),
"the v1 `tasks` capability must be ABSENT from a v2 discover: {value}"
);
assert!(
caps["experimental"].get("tasks").is_none(),
"the v1 `experimental.tasks` flag must be ABSENT from a v2 discover: {value}"
);
}
#[test]
fn server_discover_preserves_unrelated_experimental_keys() {
let info = Implementation::new("tasks-server", "1.0.0");
let result = discover_result_from_capabilities(
&tasks_backed_capabilities(),
&info,
"2026-07-28".to_string(),
);
let value = serde_json::to_value(&result).unwrap();
assert_eq!(
value["capabilities"]["experimental"]["io.example/flag"],
serde_json::json!(true),
"an unrelated experimental key must survive the v2 projection: {value}"
);
}
#[test]
fn v1_projection_drops_the_auto_advertised_tasks_extension_from_capabilities() {
let mut caps = ServerCapabilities::default();
crate::server::task_dispatch::apply_tasks_capability_rule(&mut caps, &HashMap::new(), true)
.unwrap();
assert!(
caps.extensions.is_some(),
"precondition: the build-time rule must have created the entry"
);
let projected = project_capabilities_for_v1(&caps);
let value = serde_json::to_value(&projected).unwrap();
assert!(
value.get("extensions").is_none(),
"a map holding nothing but the auto-advertised entry must be dropped \
entirely — `\"extensions\":{{}}` would itself be the v1 byte change: {value}"
);
assert!(
projected.tasks.is_some(),
"and the v1 negotiation home stays exactly as before: {value}"
);
assert!(
caps.extensions.is_some(),
"the projection must not mutate the stored capabilities"
);
}
#[test]
fn v1_projection_preserves_an_operator_configured_tasks_extension_in_capabilities() {
let configured = serde_json::json!({ "io.example/nonconformant": true });
let mut caps = ServerCapabilities::default();
let mut extensions = HashMap::new();
extensions.insert(
crate::types::capabilities::TASKS_EXTENSION_KEY.to_string(),
configured.clone(),
);
caps.extensions = Some(extensions);
let projected = project_capabilities_for_v1(&caps);
assert_eq!(
projected
.extensions
.as_ref()
.and_then(|map| map.get(crate::types::capabilities::TASKS_EXTENSION_KEY)),
Some(&configured),
"an operator-authored value must survive the v1 projection verbatim"
);
}
#[test]
fn v1_projection_leaves_unrelated_extensions_capabilities_intact() {
let mut caps = ServerCapabilities::default();
let mut extensions = HashMap::new();
extensions.insert(
crate::types::capabilities::TASKS_EXTENSION_KEY.to_string(),
crate::server::task_dispatch::tasks_extension_value(),
);
extensions.insert("io.example/skills".to_string(), serde_json::json!({}));
caps.extensions = Some(extensions);
let projected = project_capabilities_for_v1(&caps);
let extensions = projected.extensions.as_ref().expect("map is kept");
assert!(
extensions.contains_key("io.example/skills"),
"an unrelated extensions key must survive: {extensions:?}"
);
assert!(
!extensions.contains_key(crate::types::capabilities::TASKS_EXTENSION_KEY),
"and only the auto-advertised tasks entry is removed: {extensions:?}"
);
}
#[test]
fn server_discover_projection_never_mutates_the_stored_capabilities() {
let caps = tasks_backed_capabilities();
let before = serde_json::to_value(&caps).unwrap();
let info = Implementation::new("tasks-server", "1.0.0");
let first = serde_json::to_value(discover_result_from_capabilities(
&caps,
&info,
"2026-07-28".to_string(),
))
.unwrap();
let second = serde_json::to_value(discover_result_from_capabilities(
&caps,
&info,
"2026-07-28".to_string(),
))
.unwrap();
assert_eq!(
first, second,
"two projections of one server must be identical — no accumulated mutation"
);
assert_eq!(
serde_json::to_value(&caps).unwrap(),
before,
"the projection must leave the server's OWN capabilities untouched: \
they are per-server, the projection is per-request-era"
);
assert!(
caps.tasks.is_some(),
"specifically, the v1 tasks capability must still be there for the \
next v1 initialize client"
);
}
mod inject_v2_result_envelope {
use super::*;
fn result_response(id: i64, result: Value) -> JSONRPCResponse {
JSONRPCResponse {
jsonrpc: "2.0".to_string(),
id: RequestId::from(id),
payload: ResponsePayload::Result(result),
}
}
fn server_info_of(result: &Value) -> &Value {
&result["_meta"][RESERVED_SERVER_INFO_KEY]
}
#[test]
fn result_type_envelope_v2_object_gets_complete_and_server_info() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(1, serde_json::json!({ "tools": [] }));
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(v["resultType"], "complete");
assert_eq!(server_info_of(&v)["name"], "srv");
assert_eq!(server_info_of(&v)["version"], "2.0.0");
}
#[test]
fn result_type_envelope_overwrites_handler_disposition() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(1, serde_json::json!({ "resultType": "task", "x": 1 }));
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(
v["resultType"], "complete",
"the server-computed disposition must win"
);
assert_eq!(v["x"], 1, "non-reserved handler keys survive untouched");
assert_eq!(
ResponseDisposition::InputRequired.as_wire_str(),
"input_required"
);
assert_eq!(ResponseDisposition::Task.as_wire_str(), "task");
}
#[test]
fn handler_forged_input_required_is_overwritten_to_complete() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(
1,
serde_json::json!({
"tools": [],
"resultType": "input_required",
"requestState": "forged-token",
"inputRequests": { "x": { "method": "roots/list" } },
}),
);
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(v["resultType"], "complete");
assert!(
v.get("requestState").is_none(),
"a handler-supplied requestState must be removed: {v}"
);
assert!(
v.get("inputRequests").is_none(),
"a handler-supplied inputRequests must be removed: {v}"
);
assert!(v["tools"].is_array(), "the real payload survives");
}
#[test]
fn input_required_disposition_keeps_the_minted_mrtr_fields() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(
1,
serde_json::json!({
"content": [],
"requestState": "minted-token",
"inputRequests": { "x": { "method": "roots/list" } },
}),
);
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::InputRequired,
ReservedFieldOwner::Mrtr,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(v["resultType"], "input_required");
assert_eq!(v["requestState"], "minted-token");
assert!(v["inputRequests"]["x"].is_object());
}
#[test]
fn handler_supplied_server_info_is_overwritten() {
let info = Implementation::new("real-server", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(
1,
serde_json::json!({
"_meta": {
RESERVED_SERVER_INFO_KEY: { "name": "impersonated", "version": "0.0.0" },
},
}),
);
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(server_info_of(&v)["name"], "real-server");
assert_eq!(server_info_of(&v)["version"], "2.0.0");
}
#[test]
fn server_info_lives_inside_result_meta_not_at_the_top_level() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(1, serde_json::json!({ "tools": [] }));
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(v["_meta"][RESERVED_SERVER_INFO_KEY]["name"], "srv");
assert_eq!(v["_meta"][RESERVED_SERVER_INFO_KEY]["version"], "2.0.0");
assert!(
v.get("serverInfo").is_none(),
"the envelope must no longer write a top-level serverInfo: {v}"
);
assert_eq!(
RESERVED_SERVER_INFO_KEY,
"io.modelcontextprotocol/serverInfo"
);
}
#[test]
fn server_info_merges_into_an_existing_meta() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut created = result_response(1, serde_json::json!({}));
inject_v2_result_envelope(
&mut created,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = created.payload else {
panic!("expected result");
};
assert_eq!(
v["_meta"].as_object().expect("an object").len(),
1,
"a created _meta carries only the reserved key: {v}"
);
let mut merged = result_response(
2,
serde_json::json!({ "_meta": { "vendor/key": 1, "io.example/trace": "abc" } }),
);
inject_v2_result_envelope(
&mut merged,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = merged.payload else {
panic!("expected result");
};
assert_eq!(v["_meta"]["vendor/key"], 1);
assert_eq!(v["_meta"]["io.example/trace"], "abc");
assert_eq!(v["_meta"][RESERVED_SERVER_INFO_KEY]["name"], "srv");
}
#[test]
fn v1_gains_no_meta_from_the_envelope() {
let info = Implementation::new("srv", "2.0.0");
for ctx in [Some(v1_ctx()), None] {
let original = serde_json::json!({ "tools": [] });
let mut resp = result_response(1, original.clone());
inject_v2_result_envelope(
&mut resp,
ctx.as_ref(),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(v, original, "v1 must gain no _meta and stay byte-identical");
}
}
#[test]
fn every_v2_result_shape_carries_server_info_identically() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let shapes = [
("tools/call", serde_json::json!({ "content": [] })),
(
"prompts/get",
serde_json::json!({ "description": "d", "messages": [] }),
),
("resources/read", serde_json::json!({ "contents": [] })),
];
for (label, shape) in shapes {
let mut resp = result_response(1, shape);
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(
v["_meta"][RESERVED_SERVER_INFO_KEY]["name"], "srv",
"{label}"
);
assert!(v.get("serverInfo").is_none(), "{label}: {v}");
}
let mut input_required = result_response(
2,
serde_json::json!({ "content": [], "requestState": "t", "inputRequests": {} }),
);
inject_v2_result_envelope(
&mut input_required,
Some(&ctx),
&info,
ResponseDisposition::InputRequired,
ReservedFieldOwner::Mrtr,
Cacheable::No,
);
let ResponsePayload::Result(v) = input_required.payload else {
panic!("expected result");
};
assert_eq!(v["resultType"], "input_required");
assert_eq!(v["_meta"][RESERVED_SERVER_INFO_KEY]["name"], "srv");
let server = discover_server();
let discover_ctx = v2_ctx();
let response = build_discover_response(
RequestId::from(3i64),
&server.capabilities,
&server.info,
Some(&discover_ctx),
);
let ResponsePayload::Result(v) = response.payload else {
panic!("expected result");
};
assert_eq!(
v["serverInfo"]["name"], "discover-server",
"ServerDiscoverResult's OWN serverInfo field is not server-owned and survives"
);
assert_eq!(
v["_meta"][RESERVED_SERVER_INFO_KEY]["name"],
"discover-server"
);
}
#[test]
fn non_reserved_handler_meta_survives() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(
1,
serde_json::json!({ "_meta": { "vendor/key": 1, "io.example/trace": "abc" } }),
);
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(v["_meta"]["vendor/key"], 1);
assert_eq!(v["_meta"]["io.example/trace"], "abc");
}
#[test]
fn envelope_removes_the_internal_signal_key_defense_in_depth() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(
1,
serde_json::json!({
"_meta": { crate::types::mrtr::MRTR_SIGNAL_META_KEY: { "continuation": 1 } },
}),
);
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
let rendered = v.to_string();
assert!(
!rendered.contains(crate::types::mrtr::MRTR_SIGNAL_META_KEY),
"the internal signal leaked through the envelope: {rendered}"
);
assert_eq!(v["_meta"].as_object().expect("an object").len(), 1);
assert!(v["_meta"][RESERVED_SERVER_INFO_KEY].is_object());
}
#[test]
fn non_object_handler_meta_is_replaced_with_an_object() {
let mut result = serde_json::json!({ "_meta": "not-an-object" });
let meta = result_meta_object_mut(&mut result).expect("an object result");
meta.insert("vendor/key".to_string(), serde_json::json!(1));
assert_eq!(result["_meta"]["vendor/key"], 1);
assert!(result["_meta"].is_object());
}
#[test]
fn result_meta_object_mut_declines_a_non_object_result() {
let mut scalar = serde_json::json!(42);
assert!(result_meta_object_mut(&mut scalar).is_none());
let mut null = Value::Null;
assert!(result_meta_object_mut(&mut null).is_none());
}
#[test]
fn result_type_envelope_non_object_and_error_untouched() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut scalar = result_response(1, serde_json::json!(42));
inject_v2_result_envelope(
&mut scalar,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = scalar.payload else {
panic!("expected result");
};
assert_eq!(v, serde_json::json!(42));
let mut null = result_response(2, Value::Null);
inject_v2_result_envelope(
&mut null,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = null.payload else {
panic!("expected result");
};
assert_eq!(v, Value::Null);
let mut err =
ServerCore::error_response(RequestId::from(3i64), -32601, "nope".to_string());
inject_v2_result_envelope(
&mut err,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
assert!(matches!(err.payload, ResponsePayload::Error(_)));
}
#[test]
fn result_type_envelope_v1_byte_identical_golden() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v1_ctx();
let original = serde_json::json!({ "tools": [], "nextCursor": null });
let mut resp = result_response(1, original.clone());
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(v, original, "v1 success must stay byte-identical");
let mut resp_none = result_response(2, original.clone());
inject_v2_result_envelope(
&mut resp_none,
None,
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v2) = resp_none.payload else {
panic!("expected result");
};
assert_eq!(v2, original);
let mut err = ServerCore::error_response(
RequestId::from(3i64),
-32002,
"Task not completed".to_string(),
);
let before = serde_json::to_value(&err).unwrap();
inject_v2_result_envelope(
&mut err,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let after = serde_json::to_value(&err).unwrap();
assert_eq!(before, after, "v1 error must stay byte-identical");
}
#[tokio::test]
async fn result_type_envelope_end_to_end_v2_handle_request() {
let server = discover_server();
let response = server
.handle_request(RequestId::from(1i64), probe_call_with_v2_meta(), None)
.await;
let ResponsePayload::Result(v) = response.payload else {
panic!("expected result");
};
assert_eq!(v["resultType"], "complete");
assert_eq!(server_info_of(&v)["name"], "discover-server");
}
#[test]
fn v2_cacheable_result_gains_both_hints_with_the_safe_defaults() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(1, serde_json::json!({ "tools": [] }));
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::Yes,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(
v["ttlMs"],
serde_json::json!(crate::types::DEFAULT_TTL_MS),
"the v2 wire REQUIRES ttlMs; the default 0 means `immediately stale`, \
which asserts nothing about cacheability. Got {v}"
);
assert_eq!(
v["cacheScope"],
serde_json::json!("private"),
"D-08: an un-considered response must default to `private`. Defaulting to \
`public` would be a cross-authorization-context data leak — a shared \
gateway would be authorized to serve one caller's response body to \
another caller holding a different access token. Got {v}"
);
assert_eq!(v["tools"], serde_json::json!([]), "the payload survives");
}
#[test]
fn v2_handler_set_hints_survive_the_projection_unmodified() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(
1,
serde_json::json!({ "tools": [], "ttlMs": 300_000, "cacheScope": "public" }),
);
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::Yes,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(
v["ttlMs"],
serde_json::json!(300_000),
"a handler-set ttlMs must survive verbatim — the projection ENSURES, it \
does not overwrite. Got {v}"
);
assert_eq!(
v["cacheScope"],
serde_json::json!("public"),
"a handler-set cacheScope must survive verbatim, got {v}"
);
}
#[test]
fn v2_non_cacheable_result_gains_neither_hint() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
for shape in [
serde_json::json!({ "content": [] }), serde_json::json!({}), ] {
let mut resp = result_response(1, shape);
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::No,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert!(
v.get("ttlMs").is_none() && v.get("cacheScope").is_none(),
"D-07: only the six CacheableResult extenders carry these keys, got {v}"
);
assert_eq!(v["resultType"], "complete");
}
}
#[test]
fn a_non_complete_disposition_suppresses_the_hints_even_when_cacheable() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
for (disposition, owner, wire) in [
(
ResponseDisposition::InputRequired,
ReservedFieldOwner::Mrtr,
"input_required",
),
(ResponseDisposition::Task, ReservedFieldOwner::None, "task"),
] {
let mut resp = result_response(1, serde_json::json!({ "contents": [] }));
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
disposition,
owner,
Cacheable::Yes,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(
v["resultType"], wire,
"the disposition must still reach the wire, got {v}"
);
assert!(
v.get("ttlMs").is_none() && v.get("cacheScope").is_none(),
"a `{wire}` body is NOT a CacheableResult extender, so it must carry \
neither hint however the REQUEST was classified. Got {v}"
);
}
}
#[test]
fn v1_cacheable_result_gains_neither_hint() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v1_ctx();
let original = serde_json::json!({ "tools": [], "nextCursor": null });
let mut resp = result_response(1, original.clone());
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::Yes,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(
v, original,
"a v1 response must stay byte-identical: no envelope key AND no caching hint"
);
}
#[test]
fn v1_strips_a_handler_set_hint() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v1_ctx();
let mut resp = result_response(
1,
serde_json::json!({
"resources": [],
"nextCursor": null,
"ttlMs": 300_000,
"cacheScope": "public",
}),
);
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::Yes,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert!(
v.get("ttlMs").is_none(),
"D-11: a v1 wire must NEVER carry a v2 field. An ensure-only projection \
would have left this handler-set ttlMs in place. Got {v}"
);
assert!(
v.get("cacheScope").is_none(),
"D-11: a v1 wire must NEVER carry a v2 field; cacheScope leaked. Got {v}"
);
assert_eq!(
v,
serde_json::json!({ "resources": [], "nextCursor": null }),
"the strip must disturb nothing else"
);
}
#[test]
fn no_protocol_context_is_treated_as_v1() {
let info = Implementation::new("srv", "2.0.0");
let mut resp = result_response(
1,
serde_json::json!({
"contents": [],
"ttlMs": 300_000,
"cacheScope": "public",
}),
);
inject_v2_result_envelope(
&mut resp,
None,
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::Yes,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(
v,
serde_json::json!({ "contents": [] }),
"an era-less dispatcher (WasmMcpServer passes exactly this) must STRIP \
both keys — D-11. Got {v}"
);
}
#[test]
fn an_error_payload_is_untouched_on_both_eras() {
let info = Implementation::new("srv", "2.0.0");
for ctx in [Some(v2_ctx()), Some(v1_ctx()), None] {
let mut err = ServerCore::error_response(
RequestId::from(3i64),
-32002,
"Task not completed".to_string(),
);
let before = serde_json::to_value(&err).unwrap();
inject_v2_result_envelope(
&mut err,
ctx.as_ref(),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::Yes,
);
let after = serde_json::to_value(&err).unwrap();
assert_eq!(
before, after,
"an error payload carries no result body, so it can carry no hint"
);
}
}
#[test]
fn a_non_object_result_is_untouched() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
for shape in [
serde_json::json!(42),
Value::Null,
serde_json::json!([1, 2, 3]),
serde_json::json!("a string"),
] {
let mut resp = result_response(1, shape.clone());
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::Yes,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(v, shape, "a non-object result body must be left alone");
}
}
#[test]
fn the_injected_scope_is_the_serialization_of_the_enum_default() {
let info = Implementation::new("srv", "2.0.0");
let ctx = v2_ctx();
let mut resp = result_response(1, serde_json::json!({ "prompts": [] }));
inject_v2_result_envelope(
&mut resp,
Some(&ctx),
&info,
ResponseDisposition::Complete,
ReservedFieldOwner::None,
Cacheable::Yes,
);
let ResponsePayload::Result(v) = resp.payload else {
panic!("expected result");
};
assert_eq!(
v["cacheScope"],
serde_json::to_value(crate::types::CacheScope::default()).unwrap(),
"the injected default must BE the enum's serialization, not a parallel \
string literal that can drift from it"
);
assert_eq!(
v["ttlMs"],
serde_json::to_value(crate::types::DEFAULT_TTL_MS).unwrap(),
"same for the ttl: the constant is the single source"
);
}
#[tokio::test]
async fn response_middleware_still_runs_after_the_projection_and_this_is_a_known_limitation(
) {
use crate::shared::middleware::{
AdvancedMiddleware, EnhancedMiddlewareChain, MiddlewareContext,
};
struct KeyDeletingMiddleware {
key: &'static str,
}
#[async_trait]
impl AdvancedMiddleware for KeyDeletingMiddleware {
fn name(&self) -> &'static str {
"key-deleting-probe"
}
async fn on_response_with_context(
&self,
response: &mut JSONRPCResponse,
_context: &MiddlewareContext,
) -> Result<()> {
if let ResponsePayload::Result(ref mut value) = response.payload {
if let Some(object) = value.as_object_mut() {
object.remove(self.key);
}
}
Ok(())
}
}
struct HintingResource;
#[async_trait]
impl ResourceHandler for HintingResource {
async fn read(
&self,
uri: &str,
_extra: RequestHandlerExtra,
) -> Result<ReadResourceResult> {
Ok(ReadResourceResult::new(vec![Content::resource_with_text(
uri,
"hi",
"text/plain",
)]))
}
async fn list(
&self,
_cursor: Option<String>,
_extra: RequestHandlerExtra,
) -> Result<ListResourcesResult> {
Ok(ListResourcesResult::new(vec![]))
}
}
async fn read_with_middleware(deleted_key: &'static str) -> Value {
let mut chain = EnhancedMiddlewareChain::new();
chain.add(Arc::new(KeyDeletingMiddleware { key: deleted_key }));
let server = crate::server::builder::ServerCoreBuilder::new()
.name("middleware-ordering-probe")
.version("1.0.0")
.resources(HintingResource)
.stateless_mode(true)
.protocol_middleware(Arc::new(RwLock::new(chain)))
.with_supported_protocol_versions([ProtocolVersion(
crate::types::protocol::PROTOCOL_VERSION_2026_07_28.to_string(),
)])
.build()
.unwrap();
let meta = crate::types::protocol::RequestMeta::new().with_meta(
crate::types::protocol::context::RESERVED_PROTOCOL_VERSION_KEY,
serde_json::json!("2026-07-28"),
);
let request =
Request::Client(Box::new(ClientRequest::ReadResource(ReadResourceRequest {
uri: "mem://x".to_string(),
_meta: Some(meta),
})));
let response = server
.handle_request(RequestId::from(1i64), request, None)
.await;
let ResponsePayload::Result(v) = response.payload else {
panic!("expected a result");
};
v
}
let v = read_with_middleware("ttlMs").await;
assert!(
v.get("ttlMs").is_none(),
"KNOWN LIMITATION, asserted deliberately: inject_v2_result_envelope runs \
BEFORE process_response_with_context, which takes `&mut JSONRPCResponse`, \
so response middleware WINS over the projection. Response middleware \
MUST NOT mutate ttlMs, cacheScope, resultType or serverInfo — a \
middleware that needs to influence cacheability must set the fields on \
the result TYPE before dispatch returns. If this assertion now FAILS, \
the ordering changed: that is a v2 wire-behaviour change and must be a \
deliberate decision (115-08 tripwire, 115-10 deferred item), not a \
silent one. Got {v}"
);
assert_eq!(
v["cacheScope"],
serde_json::json!("private"),
"the key the middleware did NOT touch still carries the projection, which \
is what makes this a measurement of ORDERING rather than of the \
projection being absent. Got {v}"
);
let v = read_with_middleware("cacheScope").await;
assert!(
v.get("cacheScope").is_none(),
"the ordering limitation is per-response, not per-key: \
process_response_with_context can remove cacheScope just as readily as \
ttlMs. Got {v}"
);
assert_eq!(
v["ttlMs"],
serde_json::json!(crate::types::DEFAULT_TTL_MS),
"and the untouched key still carries the projection, got {v}"
);
}
}
#[tokio::test]
async fn test_stateless_mode_allows_requests_without_init() {
let mut tools = HashMap::new();
tools.insert(
"test-tool".to_string(),
Arc::new(TestTool) as Arc<dyn ToolHandler>,
);
let tool_infos = build_tool_infos(&tools);
let server = ServerCore::new(
Implementation::new("test-server", "1.0.0"),
ServerCapabilities::tools_only(),
tools,
HashMap::new(),
tool_infos,
HashMap::new(),
None,
None,
None,
None,
Arc::new(RwLock::new(EnhancedMiddlewareChain::new())),
Arc::new(RwLock::new(ToolMiddlewareChain::new())),
None, None, true, PayloadLimits::default(),
);
let list_req = Request::Client(Box::new(ClientRequest::ListTools(ListToolsRequest {
cursor: None,
})));
let response = server
.handle_request(RequestId::from(1i64), list_req, None)
.await;
match response.payload {
ResponsePayload::Result(result) => {
let tools_result: ListToolsResult = serde_json::from_value(result).unwrap();
assert_eq!(tools_result.tools.len(), 1);
assert_eq!(tools_result.tools[0].name, "test-tool");
},
ResponsePayload::Error(e) => panic!(
"List tools should succeed in stateless mode without init: {}",
e.message
),
}
}
#[tokio::test]
async fn test_normal_mode_requires_initialization() {
let mut tools = HashMap::new();
tools.insert(
"test-tool".to_string(),
Arc::new(TestTool) as Arc<dyn ToolHandler>,
);
let tool_infos = build_tool_infos(&tools);
let server = ServerCore::new(
Implementation::new("test-server", "1.0.0"),
ServerCapabilities::tools_only(),
tools,
HashMap::new(),
tool_infos,
HashMap::new(),
None,
None,
None,
None,
Arc::new(RwLock::new(EnhancedMiddlewareChain::new())),
Arc::new(RwLock::new(ToolMiddlewareChain::new())),
None, None, false, PayloadLimits::default(),
);
let list_req = Request::Client(Box::new(ClientRequest::ListTools(ListToolsRequest {
cursor: None,
})));
let response = server
.handle_request(RequestId::from(1i64), list_req, None)
.await;
match response.payload {
ResponsePayload::Result(_) => {
panic!("List tools should fail in normal mode without initialization")
},
ResponsePayload::Error(e) => {
assert_eq!(e.code, -32002);
assert!(e.message.contains("not initialized"));
},
}
}
#[test]
fn test_build_uri_to_tool_meta_indexes_by_standard_key() {
let mut tool_infos = HashMap::new();
let mut info = ToolInfo::new(
"chess",
Some("Chess tool".to_string()),
serde_json::json!({"type": "object"}),
);
let mut meta = serde_json::Map::new();
meta.insert(
"ui".to_string(),
serde_json::json!({"resourceUri": "ui://chess/board"}),
);
meta.insert(
"openai/outputTemplate".to_string(),
serde_json::json!("ui://chess/board"),
);
info._meta = Some(meta);
tool_infos.insert("chess".to_string(), info);
let index = build_uri_to_tool_meta(&tool_infos);
assert!(
index.contains_key("ui://chess/board"),
"must index by ui.resourceUri value"
);
}
#[cfg(feature = "mcp-apps")]
#[test]
fn test_build_uri_to_tool_meta_includes_openai_when_present() {
let mut tool_infos = HashMap::new();
let mut info = ToolInfo::new(
"chess",
Some("Chess tool".to_string()),
serde_json::json!({"type": "object"}),
);
let mut meta = serde_json::Map::new();
meta.insert(
"ui".to_string(),
serde_json::json!({"resourceUri": "ui://chess/board"}),
);
meta.insert(
"openai/outputTemplate".to_string(),
serde_json::json!("ui://chess/board"),
);
meta.insert(
"openai/widgetAccessible".to_string(),
serde_json::json!(true),
);
info._meta = Some(meta);
tool_infos.insert("chess".to_string(), info);
let index = build_uri_to_tool_meta(&tool_infos);
assert!(index.contains_key("ui://chess/board"));
let entry = &index["ui://chess/board"];
assert!(
entry.contains_key("openai/outputTemplate"),
"must include openai/outputTemplate in index entry"
);
assert!(
entry.contains_key("openai/widgetAccessible"),
"must include openai/widgetAccessible in index entry"
);
}
#[test]
fn test_build_uri_to_tool_meta_skips_empty_propagation() {
let mut tool_infos = HashMap::new();
let mut info = ToolInfo::new(
"chess",
Some("Chess tool".to_string()),
serde_json::json!({"type": "object"}),
);
let mut meta = serde_json::Map::new();
meta.insert(
"ui".to_string(),
serde_json::json!({"resourceUri": "ui://chess/board"}),
);
info._meta = Some(meta);
tool_infos.insert("chess".to_string(), info);
let index = build_uri_to_tool_meta(&tool_infos);
assert!(
!index.contains_key("ui://chess/board"),
"must not index tools with no propagation-eligible keys"
);
}
#[test]
fn test_summarize_array() {
let empty = serde_json::json!([]);
assert_eq!(summarize_structured_output(&empty), "No records returned.");
let single = serde_json::json!([{"id": 1}]);
assert_eq!(summarize_structured_output(&single), "1 record returned.");
let multi = serde_json::json!([1, 2, 3, 4, 5]);
assert_eq!(summarize_structured_output(&multi), "5 records returned.");
}
#[test]
fn test_summarize_object_with_collection() {
let val = serde_json::json!({"results": [1, 2, 3], "total": 3});
assert_eq!(summarize_structured_output(&val), "3 records returned.");
let val = serde_json::json!({"items": [], "page": 1});
assert_eq!(summarize_structured_output(&val), "No records returned.");
let val = serde_json::json!({"data": [{"name": "a"}]});
assert_eq!(summarize_structured_output(&val), "1 record returned.");
}
#[test]
fn test_summarize_plain_object() {
let val = serde_json::json!({"name": "test", "value": 42});
assert_eq!(summarize_structured_output(&val), "Result with 2 fields.");
let val = serde_json::json!({});
assert_eq!(summarize_structured_output(&val), "Empty result.");
}
#[test]
fn test_summarize_primitives() {
assert_eq!(summarize_structured_output(&Value::Null), "No result.");
assert_eq!(
summarize_structured_output(&serde_json::json!("hello")),
"hello"
);
assert_eq!(summarize_structured_output(&serde_json::json!(42)), "42");
}
#[test]
fn test_summarize_string_truncation_multibyte() {
let long_emoji = "\u{1F600}".repeat(201);
let result = summarize_structured_output(&Value::String(long_emoji));
assert!(result.ends_with("..."));
assert!(result.len() > 3);
}
mod phase_112_09_context_spine {
use super::*;
use crate::types::protocol::{Era, ProtocolContext, RequestMeta};
use std::sync::Mutex;
fn get_prompt_request(name: &str, meta: Option<RequestMeta>) -> Request {
Request::Client(Box::new(ClientRequest::GetPrompt(GetPromptRequest {
name: name.to_string(),
arguments: HashMap::new(),
_meta: meta,
})))
}
fn read_resource_request(uri: &str, meta: Option<RequestMeta>) -> Request {
Request::Client(Box::new(ClientRequest::ReadResource(ReadResourceRequest {
uri: uri.to_string(),
_meta: meta,
})))
}
#[test]
fn extract_request_meta_value_reads_prompt_and_resource_meta() {
let meta = RequestMeta::new().with_meta("ns/key", serde_json::json!("v"));
let expected = serde_json::to_value(&meta).unwrap();
let got = extract_request_meta_value(&get_prompt_request("p", Some(meta.clone())));
assert_eq!(got, Some(expected.clone()));
let got = extract_request_meta_value(&read_resource_request("mem://x", Some(meta)));
assert_eq!(got, Some(expected));
assert_eq!(
extract_request_meta_value(&get_prompt_request("p", None)),
None
);
assert_eq!(
extract_request_meta_value(&read_resource_request("mem://x", None)),
None
);
}
#[test]
fn all_meta_bearing_client_requests_are_extracted() {
let meta = RequestMeta::new().with_meta("io.example/x", serde_json::json!(1));
let expected = serde_json::to_value(&meta).unwrap();
let mut call_tool_req = CallToolRequest::new("t", serde_json::json!({}));
call_tool_req._meta = Some(meta.clone());
let call_tool = Request::Client(Box::new(ClientRequest::CallTool(call_tool_req)));
let get_prompt = get_prompt_request("p", Some(meta.clone()));
let read_resource = read_resource_request("mem://x", Some(meta));
for req in [&call_tool, &get_prompt, &read_resource] {
assert_eq!(
extract_request_meta_value(req),
Some(expected.clone()),
"every _meta-bearing ClientRequest variant must extract Some"
);
}
}
#[test]
fn typed_extractor_scope_is_the_three_meta_bearing_methods() {
for method in [
"tools/list",
"prompts/list",
"resources/list",
"resources/templates/list",
] {
let client: ClientRequest = serde_json::from_value(serde_json::json!({
"method": method,
"params": { "_meta": { "ns/key": "v" } },
}))
.unwrap_or_else(|e| panic!("{method} must deserialize: {e}"));
let req = Request::Client(Box::new(client));
assert_eq!(
extract_request_meta_value(&req),
None,
"{method} has no typed _meta field; the HTTP path reads the raw body"
);
}
}
#[test]
fn spec_spelled_meta_on_the_wire_reaches_era_resolution() {
let expected = serde_json::json!({ "ns/key": "v" });
for (method, params) in [
(
"tools/call",
serde_json::json!({ "name": "t", "arguments": {}, "_meta": { "ns/key": "v" } }),
),
(
"prompts/get",
serde_json::json!({ "name": "p", "arguments": {}, "_meta": { "ns/key": "v" } }),
),
(
"resources/read",
serde_json::json!({ "uri": "mem://x", "_meta": { "ns/key": "v" } }),
),
] {
let client: ClientRequest = serde_json::from_value(serde_json::json!({
"method": method,
"params": params,
}))
.unwrap_or_else(|e| panic!("{method} must deserialize: {e}"));
let req = Request::Client(Box::new(client));
assert_eq!(
extract_request_meta_value(&req),
Some(expected.clone()),
"{method} must read the SPEC-spelled `_meta`, not `meta`"
);
}
}
proptest::proptest! {
#[test]
fn extract_request_meta_value_fuzz_never_panics(
key in "[a-zA-Z0-9._/-]{0,64}",
strval in ".{0,4096}",
use_prompt in proptest::prelude::any::<bool>(),
) {
let meta = RequestMeta::new().with_meta(key, serde_json::json!(strval));
let expected = serde_json::to_value(&meta).unwrap();
let req = if use_prompt {
get_prompt_request("p", Some(meta))
} else {
read_resource_request("mem://x", Some(meta))
};
proptest::prop_assert_eq!(extract_request_meta_value(&req), Some(expected));
}
}
#[derive(Clone, Debug, Default, PartialEq)]
struct Captured {
era: Option<Era>,
has_client_info: bool,
traceparent: Option<String>,
}
struct CapturingPrompt(Arc<Mutex<Option<Captured>>>);
#[async_trait]
impl PromptHandler for CapturingPrompt {
async fn handle(
&self,
_args: HashMap<String, String>,
extra: RequestHandlerExtra,
) -> Result<GetPromptResult> {
*self.0.lock().unwrap() = Some(Captured {
era: extra.era(),
has_client_info: extra.client_info().is_some(),
traceparent: extra.trace_context().map(|t| t.traceparent),
});
Ok(GetPromptResult::new(vec![], None))
}
}
struct CapturingResource(Arc<Mutex<Option<Captured>>>);
#[async_trait]
impl ResourceHandler for CapturingResource {
async fn read(
&self,
_uri: &str,
extra: RequestHandlerExtra,
) -> Result<ReadResourceResult> {
*self.0.lock().unwrap() = Some(Captured {
era: extra.era(),
has_client_info: extra.client_info().is_some(),
traceparent: extra.trace_context().map(|t| t.traceparent),
});
Ok(ReadResourceResult::new(vec![Content::text("ok")]))
}
async fn list(
&self,
_cursor: Option<String>,
_extra: RequestHandlerExtra,
) -> Result<ListResourcesResult> {
Ok(ListResourcesResult {
resources: vec![],
next_cursor: None,
ttl_ms: None,
cache_scope: None,
})
}
}
fn build_core(
prompt_cap: Arc<Mutex<Option<Captured>>>,
resource_cap: Arc<Mutex<Option<Captured>>>,
) -> ServerCore {
let mut prompts: HashMap<String, Arc<dyn PromptHandler>> = HashMap::new();
prompts.insert(
"greeting".to_string(),
Arc::new(CapturingPrompt(prompt_cap)) as Arc<dyn PromptHandler>,
);
let resources: Option<Arc<dyn ResourceHandler>> =
Some(Arc::new(CapturingResource(resource_cap)));
ServerCore::new(
Implementation::new("test-server", "1.0.0"),
ServerCapabilities::default(),
HashMap::new(),
prompts,
HashMap::new(),
HashMap::new(),
resources,
None,
None,
None,
Arc::new(RwLock::new(EnhancedMiddlewareChain::new())),
Arc::new(RwLock::new(ToolMiddlewareChain::new())),
None, None, true, PayloadLimits::default(),
)
.with_supported_protocol_versions(vec![
ProtocolVersion("2025-11-25".to_string()),
ProtocolVersion(crate::types::protocol::PROTOCOL_VERSION_2026_07_28.to_string()),
])
}
fn v2_meta_with_trace() -> RequestMeta {
RequestMeta::new().with_meta(
"traceparent",
serde_json::json!("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"),
)
}
fn v2_context() -> ProtocolContext {
ProtocolContext::new(
Era::V2,
ProtocolVersion(crate::types::protocol::PROTOCOL_VERSION_2026_07_28.to_string()),
)
.with_client_info(Implementation::new("test-client", "9.9.9"))
}
#[tokio::test]
async fn prompt_resource_protocol_context_via_dispatch_core() {
let pcap = Arc::new(Mutex::new(None));
let rcap = Arc::new(Mutex::new(None));
let core = build_core(pcap.clone(), rcap.clone());
core.handle_request_internal(
RequestId::from(1i64),
get_prompt_request("greeting", Some(v2_meta_with_trace())),
None,
Some(v2_context()),
&mut DispatchEnvelopeClaim::default(),
)
.await;
core.handle_request_internal(
RequestId::from(2i64),
read_resource_request("mem://greeting", Some(v2_meta_with_trace())),
None,
Some(v2_context()),
&mut DispatchEnvelopeClaim::default(),
)
.await;
for cap in [&pcap, &rcap] {
let c = cap.lock().unwrap().clone().expect("handler ran");
assert_eq!(c.era, Some(Era::V2), "era must be V2 on a v2 dispatch");
assert!(c.has_client_info, "client_info must be visible on v2");
assert_eq!(
c.traceparent.as_deref(),
Some("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"),
"trace_context must reflect the W3C traceparent (proves with_request_meta)"
);
}
let pcap = Arc::new(Mutex::new(None));
let rcap = Arc::new(Mutex::new(None));
let core = build_core(pcap.clone(), rcap.clone());
let v1 = ProtocolContext::new(Era::V1, ProtocolVersion("2025-11-25".to_string()));
core.handle_request_internal(
RequestId::from(3i64),
get_prompt_request("greeting", None),
None,
Some(v1.clone()),
&mut DispatchEnvelopeClaim::default(),
)
.await;
core.handle_request_internal(
RequestId::from(4i64),
read_resource_request("mem://greeting", None),
None,
Some(v1),
&mut DispatchEnvelopeClaim::default(),
)
.await;
assert_eq!(pcap.lock().unwrap().clone().unwrap().era, Some(Era::V1));
assert_eq!(rcap.lock().unwrap().clone().unwrap().era, Some(Era::V1));
let pcap = Arc::new(Mutex::new(None));
let rcap = Arc::new(Mutex::new(None));
let core = build_core(pcap.clone(), rcap.clone());
core.handle_request_internal(
RequestId::from(5i64),
get_prompt_request("greeting", None),
None,
None,
&mut DispatchEnvelopeClaim::default(),
)
.await;
core.handle_request_internal(
RequestId::from(6i64),
read_resource_request("mem://greeting", None),
None,
None,
&mut DispatchEnvelopeClaim::default(),
)
.await;
assert_eq!(pcap.lock().unwrap().clone().unwrap().era, None);
assert_eq!(rcap.lock().unwrap().clone().unwrap().era, None);
}
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
mod mrtr_ingest_tests {
use super::super::*;
use crate::server::request_state::{
Continuation, RequestBinding, RequestStateCodec, Verdict,
};
use crate::types::protocol::{Era, ProtocolContext};
use crate::types::{CallToolRequest, ListToolsRequest, ProtocolVersion};
use serde_json::json;
use std::time::Duration;
const KEY_A: [u8; 32] = [0x11; 32];
const KEY_B: [u8; 32] = [0x22; 32];
const ALICE: &str = "alice";
fn codec(key: &[u8; 32], ttl_secs: u64) -> RequestStateCodec {
RequestStateCodec::new(key, Duration::from_secs(ttl_secs)).expect("codec builds")
}
fn nested_arguments(levels: usize) -> Value {
let mut value = json!("leaf");
for _ in 0..levels {
value = json!({ "n": value });
}
value
}
fn arguments_at_the_cap() -> Value {
nested_arguments(crate::types::mrtr::MAX_CANONICAL_DEPTH - 1)
}
fn arguments_past_the_cap() -> Value {
nested_arguments(crate::types::mrtr::MAX_CANONICAL_DEPTH)
}
fn call_tool(arguments: Value) -> Request {
Request::Client(Box::new(ClientRequest::CallTool(CallToolRequest {
name: "search".to_string(),
arguments,
_meta: None,
task: None,
})))
}
fn v2_context() -> ProtocolContext {
ProtocolContext::new(Era::V2, ProtocolVersion("2026-07-28".to_string()))
}
fn mint_for(
codec: &RequestStateCodec,
principal: &str,
request: &Request,
state: &Value,
round: u8,
) -> String {
let target = mrtr_binding_parts(request).expect("an MRTR-eligible request");
let binding = RequestBinding::from_request(principal, target.0, &target.1)
.expect("the fixture params are inside the canonical depth cap");
codec
.mint(state, &binding, round, None)
.expect("mint succeeds")
}
fn ingest(
request: &Request,
token: Option<&str>,
subject: Option<&str>,
has_auth_provider: bool,
codec: Option<&RequestStateCodec>,
) -> MrtrIngest {
let mut context = v2_context();
if let Some(token) = token {
context = context.with_mrtr_params(crate::types::mrtr::MrtrRequestParams {
input_responses: None,
input_responses_raw: None,
request_state: Some(token.to_string()),
});
}
let target = mrtr_binding_parts(request);
mrtr_ingest(&MrtrIngestInputs {
target: target.as_ref(),
protocol_context: Some(&context),
principal: MrtrPrincipal {
authenticated_subject: subject,
has_auth_provider,
},
codec,
})
}
#[test]
fn valid_token_proceeds_with_state_and_round() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let token = mint_for(&codec, ALICE, &request, &json!({ "step": 7 }), 2);
let verdict = ingest(&request, Some(&token), Some(ALICE), false, Some(&codec));
let MrtrIngest::Proceed {
continuation,
round,
kinds: _,
} = verdict
else {
panic!("a live, authentic token must Proceed, got {verdict:?}");
};
assert_eq!(continuation, json!({ "step": 7 }));
assert_eq!(round, 2);
}
#[test]
fn tampered_token_rejects_and_never_reelicits() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let token = format!(
"{}-TAMPERED",
mint_for(&codec, ALICE, &request, &json!({}), 0)
);
let verdict = ingest(&request, Some(&token), Some(ALICE), false, Some(&codec));
let MrtrIngest::Reject { code, message } = verdict else {
panic!("a tampered token must Reject, got {verdict:?}");
};
assert_eq!(code, crate::types::protocol::error_codes::INVALID_PARAMS);
assert_eq!(message, MRTR_REJECT_MESSAGE);
}
#[test]
fn principal_mismatch_rejects() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let token = mint_for(&codec, ALICE, &request, &json!({}), 0);
let verdict = ingest(&request, Some(&token), Some("bob"), false, Some(&codec));
assert!(
matches!(verdict, MrtrIngest::Reject { .. }),
"a cross-principal replay must Reject, got {verdict:?}"
);
}
#[test]
fn originating_request_mismatch_rejects() {
let codec = codec(&KEY_A, 300);
let minted_for = call_tool(json!({ "q": "a" }));
let token = mint_for(&codec, ALICE, &minted_for, &json!({}), 0);
let other_args = call_tool(json!({ "q": "b" }));
assert!(
matches!(
ingest(&other_args, Some(&token), Some(ALICE), false, Some(&codec)),
MrtrIngest::Reject { .. }
),
"a token replayed onto different arguments must Reject"
);
let other_method = Request::Client(Box::new(ClientRequest::GetPrompt(
crate::types::GetPromptRequest {
name: "search".to_string(),
arguments: HashMap::new(),
_meta: None,
},
)));
assert!(
matches!(
ingest(
&other_method,
Some(&token),
Some(ALICE),
false,
Some(&codec)
),
MrtrIngest::Reject { .. }
),
"a tools/call token replayed onto prompts/get must Reject"
);
}
#[test]
fn unknown_key_reelicits_from_round_zero() {
let minting = codec(&KEY_B, 300);
let serving = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let token = mint_for(&minting, ALICE, &request, &json!({ "step": 4 }), 3);
let verdict = ingest(&request, Some(&token), Some(ALICE), false, Some(&serving));
assert!(
matches!(verdict, MrtrIngest::Reelicit { round: 0 }),
"an unknown key id must re-elicit from round 0, got {verdict:?}"
);
}
#[test]
fn expired_token_reelicits_preserving_the_round() {
let minting = codec(&KEY_A, 0);
let serving = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let token = mint_for(&minting, ALICE, &request, &json!({ "step": 1 }), 5);
let verdict = ingest(&request, Some(&token), Some(ALICE), false, Some(&serving));
assert!(
matches!(verdict, MrtrIngest::Reelicit { round: 5 }),
"an expired token must re-elicit at its own round, got {verdict:?}"
);
}
#[test]
fn an_uncanonicalizable_request_presenting_a_token_is_refused() {
let codec = codec(&KEY_A, 300);
let shallow = call_tool(json!({ "q": "a" }));
let token = mint_for(&codec, ALICE, &shallow, &json!({ "step": 1 }), 0);
let deep = call_tool(arguments_past_the_cap());
let verdict = ingest(&deep, Some(&token), Some(ALICE), false, Some(&codec));
let MrtrIngest::Reject { code, message } = verdict else {
panic!("an unbindable state-bearing request must Reject, got {verdict:?}");
};
assert_eq!(code, crate::types::protocol::error_codes::INVALID_PARAMS);
assert_eq!(message, MRTR_UNCANONICALIZABLE_MESSAGE);
assert_ne!(
message, MRTR_REJECT_MESSAGE,
"the refusal must precede `verify` — reaching the tag check would \
have produced the generic authentication message instead"
);
assert!(
verdict_is_rejected(&deep, &token, &codec),
"the handler must never be invoked for an unbindable request"
);
}
fn verdict_is_rejected(request: &Request, token: &str, codec: &RequestStateCodec) -> bool {
ingest(request, Some(token), Some(ALICE), false, Some(codec))
.apply(Some(v2_context()))
.is_err()
}
#[test]
fn a_request_exactly_at_the_depth_cap_still_mints_and_verifies() {
let codec = codec(&KEY_A, 300);
let request = call_tool(arguments_at_the_cap());
let token = mint_for(&codec, ALICE, &request, &json!({ "step": 3 }), 1);
let verdict = ingest(&request, Some(&token), Some(ALICE), false, Some(&codec));
let MrtrIngest::Proceed {
continuation,
round,
kinds: _,
} = verdict
else {
panic!("a request AT the cap must still Proceed, got {verdict:?}");
};
assert_eq!(continuation, json!({ "step": 3 }));
assert_eq!(round, 1);
}
#[test]
fn a_deep_request_that_never_touches_mrtr_is_unaffected() {
let codec = codec(&KEY_A, 300);
let deep = call_tool(arguments_past_the_cap());
let verdict = ingest(&deep, None, Some(ALICE), false, Some(&codec));
assert!(
matches!(verdict, MrtrIngest::Inert),
"a deep request with NO requestState must be Inert, got {verdict:?}"
);
let (context, round) = verdict
.apply(Some(v2_context()))
.expect("Inert is not a rejection");
assert!(context
.expect("the context survives")
.mrtr_continuation()
.is_none());
assert_eq!(round, 0);
let v1 = ProtocolContext::new(Era::V1, ProtocolVersion("2025-11-25".to_string()));
let target = mrtr_binding_parts(&deep);
let v1_verdict = mrtr_ingest(&MrtrIngestInputs {
target: target.as_ref(),
protocol_context: Some(&v1),
principal: MrtrPrincipal {
authenticated_subject: Some(ALICE),
has_auth_provider: false,
},
codec: Some(&codec),
});
assert!(matches!(v1_verdict, MrtrIngest::Inert));
}
fn continuation_at(round: u8) -> Continuation {
Continuation {
state: json!({ "step": 1 }),
exp: 0,
round,
kinds: None,
}
}
#[test]
fn round_ceiling_refuses_every_authentic_verdict_at_the_ceiling() {
for (label, verdict) in [
("Ok", Verdict::Ok(continuation_at(MAX_MRTR_ROUNDS))),
(
"Expired",
Verdict::Expired(continuation_at(MAX_MRTR_ROUNDS)),
),
] {
let routed = route_mrtr_verdict(verdict, "tools/call");
let MrtrIngest::Reject { code, message } = routed else {
panic!("{label} at the ceiling must Reject, got {routed:?}");
};
assert_eq!(
code,
crate::types::protocol::error_codes::INVALID_PARAMS,
"{label}: the sibling MRTR reject code, so the v2 HTTP status \
mapping is unchanged"
);
assert_eq!(message, MRTR_ROUND_CEILING_MESSAGE, "{label}");
assert_ne!(
message, MRTR_REJECT_MESSAGE,
"{label}: a ceiling refusal happens AFTER the token verified, so it \
is not an authentication oracle and must not hide behind the \
generic message"
);
}
}
#[test]
fn round_ceiling_admits_exactly_one_below_itself() {
let below = MAX_MRTR_ROUNDS - 1;
let proceed = route_mrtr_verdict(Verdict::Ok(continuation_at(below)), "tools/call");
let MrtrIngest::Proceed {
continuation,
round,
kinds: _,
} = proceed
else {
panic!("ceiling - 1 must still Proceed, got {proceed:?}");
};
assert_eq!(round, below);
assert_eq!(continuation, json!({ "step": 1 }));
let reelicit =
route_mrtr_verdict(Verdict::Expired(continuation_at(below)), "tools/call");
assert!(
matches!(reelicit, MrtrIngest::Reelicit { round } if round == below),
"ceiling - 1 must still re-elicit at its own round, got {reelicit:?}"
);
}
#[test]
fn unknown_key_still_resets_to_round_zero_under_the_ceiling() {
assert!(matches!(
route_mrtr_verdict(Verdict::UnknownKey, "tools/call"),
MrtrIngest::Reelicit { round: 0 }
));
}
#[test]
fn auth_failure_keeps_the_generic_message() {
let routed = route_mrtr_verdict(Verdict::AuthFailed, "tools/call");
let MrtrIngest::Reject { message, .. } = routed else {
panic!("AuthFailed must Reject, got {routed:?}");
};
assert_eq!(message, MRTR_REJECT_MESSAGE);
}
proptest::proptest! {
#[test]
fn no_authentic_round_can_reach_saturation(round in 0u8..=u8::MAX) {
for verdict in [
Verdict::Ok(continuation_at(round)),
Verdict::Expired(continuation_at(round)),
] {
match route_mrtr_verdict(verdict, "tools/call").apply(Some(v2_context())) {
Err((code, message)) => {
proptest::prop_assert!(round >= MAX_MRTR_ROUNDS);
proptest::prop_assert_eq!(
code,
crate::types::protocol::error_codes::INVALID_PARAMS
);
proptest::prop_assert_eq!(message, MRTR_ROUND_CEILING_MESSAGE);
},
Ok((_, threaded)) => {
proptest::prop_assert!(threaded < MAX_MRTR_ROUNDS);
proptest::prop_assert_eq!(
u16::from(threaded.saturating_add(1)),
u16::from(threaded) + 1
);
proptest::prop_assert!(threaded.saturating_add(1) <= MAX_MRTR_ROUNDS);
},
}
}
}
}
#[test]
fn ignores_a_request_state_on_a_non_eligible_method() {
let codec = codec(&KEY_A, 300);
let list = Request::Client(Box::new(ClientRequest::ListTools(ListToolsRequest {
cursor: None,
})));
assert!(mrtr_binding_parts(&list).is_none());
let verdict = ingest(&list, Some("anything"), Some(ALICE), false, Some(&codec));
assert!(
matches!(verdict, MrtrIngest::Inert),
"MRTR must be inert outside the three eligible methods, got {verdict:?}"
);
}
#[test]
fn is_inert_on_v1_and_without_a_token_or_codec() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let token = mint_for(&codec, ALICE, &request, &json!({}), 0);
let target = mrtr_binding_parts(&request);
let v1 = ProtocolContext::new(Era::V1, ProtocolVersion("2025-11-25".to_string()))
.with_mrtr_params(crate::types::mrtr::MrtrRequestParams {
input_responses: None,
input_responses_raw: None,
request_state: Some(token.clone()),
});
assert!(matches!(
mrtr_ingest(&MrtrIngestInputs {
target: target.as_ref(),
protocol_context: Some(&v1),
principal: MrtrPrincipal {
authenticated_subject: Some(ALICE),
has_auth_provider: false,
},
codec: Some(&codec),
}),
MrtrIngest::Inert
));
assert!(matches!(
mrtr_ingest(&MrtrIngestInputs {
target: target.as_ref(),
protocol_context: None,
principal: MrtrPrincipal {
authenticated_subject: Some(ALICE),
has_auth_provider: false,
},
codec: Some(&codec),
}),
MrtrIngest::Inert
));
assert!(matches!(
ingest(&request, None, Some(ALICE), false, Some(&codec)),
MrtrIngest::Inert
));
assert!(matches!(
ingest(&request, Some(&token), Some(ALICE), false, None),
MrtrIngest::Inert
));
}
#[test]
fn auth_configured_server_refuses_an_unauthenticated_caller() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let token = mint_for(&codec, ANONYMOUS_PRINCIPAL, &request, &json!({}), 0);
let verdict = ingest(&request, Some(&token), None, true, Some(&codec));
let MrtrIngest::Reject { code, .. } = verdict else {
panic!("an auth-configured server must refuse MRTR here, got {verdict:?}");
};
assert_eq!(code, crate::types::protocol::error_codes::INVALID_PARAMS);
}
#[test]
fn anonymous_principal_is_used_only_without_an_auth_provider() {
assert_eq!(ANONYMOUS_PRINCIPAL, "");
assert_eq!(
resolve_mrtr_principal(MrtrPrincipal {
authenticated_subject: None,
has_auth_provider: false,
}),
Some(ANONYMOUS_PRINCIPAL)
);
assert_eq!(
resolve_mrtr_principal(MrtrPrincipal {
authenticated_subject: None,
has_auth_provider: true,
}),
None,
"fail closed on an auth-configured server"
);
assert_eq!(
resolve_mrtr_principal(MrtrPrincipal {
authenticated_subject: Some(ALICE),
has_auth_provider: true,
}),
Some(ALICE)
);
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let token = mint_for(&codec, ANONYMOUS_PRINCIPAL, &request, &json!({ "a": 1 }), 0);
assert!(matches!(
ingest(&request, Some(&token), None, false, Some(&codec)),
MrtrIngest::Proceed { .. }
));
}
#[test]
fn apply_proceed_surfaces_continuation_and_round() {
let (context, round) = MrtrIngest::Proceed {
continuation: json!({ "step": 3 }),
round: 2,
kinds: None,
}
.apply(Some(v2_context()))
.expect("Proceed is not a rejection");
let context = context.expect("context survives");
assert_eq!(context.mrtr_continuation(), Some(&json!({ "step": 3 })));
assert_eq!(context.mrtr_round(), Some(2));
assert_eq!(round, 2, "egress mints the next token at round + 1");
}
#[test]
fn apply_reelicit_strips_every_signal_and_keeps_the_round() {
let carried = v2_context()
.with_mrtr_params(crate::types::mrtr::MrtrRequestParams {
input_responses: Some(crate::types::mrtr::InputResponses::new()),
input_responses_raw: None,
request_state: Some("token".to_string()),
})
.with_verified_continuation(json!({ "step": 1 }), 4);
let (context, round) = MrtrIngest::Reelicit { round: 4 }
.apply(Some(carried))
.expect("Reelicit is not a rejection");
let context = context.expect("context survives");
assert!(context.input_responses().is_none());
assert!(context.request_state_token().is_none());
assert!(context.mrtr_continuation().is_none());
assert!(context.mrtr_round().is_none());
assert_eq!(round, 4, "the expired token's round is preserved");
}
#[test]
fn apply_reject_is_an_error_so_the_handler_never_runs() {
let outcome = MrtrIngest::Reject {
code: crate::types::protocol::error_codes::INVALID_PARAMS,
message: MRTR_REJECT_MESSAGE.to_string(),
}
.apply(Some(v2_context()));
let Err((code, message)) = outcome else {
panic!("Reject must short-circuit dispatch");
};
assert_eq!(code, crate::types::protocol::error_codes::INVALID_PARAMS);
assert_eq!(message, MRTR_REJECT_MESSAGE);
}
#[test]
fn apply_inert_leaves_the_context_untouched() {
let (context, round) = MrtrIngest::Inert
.apply(Some(v2_context()))
.expect("Inert is not a rejection");
let context = context.expect("context survives");
assert!(context.mrtr_continuation().is_none());
assert_eq!(round, 0);
}
fn overlapping_answer() -> Value {
json!({
"action": "accept",
"content": { "type": "text", "text": "hello" },
"model": "attacker-chosen-model",
})
}
fn context_answering(answers: &[(&str, Value)]) -> crate::types::protocol::ProtocolContext {
let raw: serde_json::Map<String, Value> = answers
.iter()
.map(|(key, value)| ((*key).to_string(), value.clone()))
.collect();
let mut params = json!({ "name": "t", "arguments": {} });
params["inputResponses"] = Value::Object(raw);
let mrtr = crate::types::mrtr::extract_mrtr_params(¶ms)
.expect("the fixture answers are inside every ingress bound");
v2_context().with_mrtr_params(mrtr)
}
fn kinds_of(
entries: &[(&str, crate::types::mrtr::InputRequestKind)],
) -> crate::types::mrtr::InputRequestKinds {
entries
.iter()
.map(|(key, kind)| ((*key).to_string(), *kind))
.collect()
}
fn proceed_with(
context: crate::types::protocol::ProtocolContext,
kinds: Option<crate::types::mrtr::InputRequestKinds>,
) -> std::result::Result<(Option<crate::types::protocol::ProtocolContext>, u8), (i32, String)>
{
MrtrIngest::Proceed {
continuation: json!({ "step": 1 }),
round: 1,
kinds,
}
.apply(Some(context))
}
#[test]
fn the_literal_d113o_answer_reaches_the_handler_as_an_elicitation() {
let (context, _) = proceed_with(
context_answering(&[("k", overlapping_answer())]),
Some(kinds_of(&[(
"k",
crate::types::mrtr::InputRequestKind::Elicitation,
)])),
)
.expect("a valid ElicitResult answered to an elicitation proceeds");
assert!(matches!(
context
.expect("context survives")
.input_responses()
.expect("answers")["k"],
crate::types::mrtr::InputResponse::Elicitation(_)
));
}
#[test]
fn an_answer_that_cannot_be_the_requested_kind_is_rejected_at_the_verified_path() {
let sampling_only = json!({
"content": { "type": "text", "text": "hello" },
"model": "attacker-chosen-model",
});
let outcome = proceed_with(
context_answering(&[("k", sampling_only)]),
Some(kinds_of(&[(
"k",
crate::types::mrtr::InputRequestKind::Elicitation,
)])),
);
let Err((code, message)) = outcome else {
panic!("an answer that is not an ElicitResult must short-circuit dispatch");
};
assert_eq!(code, crate::types::protocol::error_codes::INVALID_PARAMS);
assert!(
message.contains("\"k\""),
"the refusal must NAME the key it is about: {message}"
);
assert!(
message.contains("elicitation/create"),
"...and the kind the server actually requested there: {message}"
);
assert_ne!(
message, MRTR_REJECT_MESSAGE,
"it fires only AFTER the tag check passed, so it is not an authentication \
oracle and must not hide behind the generic message"
);
}
#[test]
fn a_correctly_shaped_answer_reaches_the_handler_typed_by_kind() {
let (context, round) = proceed_with(
context_answering(&[("k", json!({ "action": "accept", "content": { "v": 1 } }))]),
Some(kinds_of(&[(
"k",
crate::types::mrtr::InputRequestKind::Elicitation,
)])),
)
.expect("a well-shaped answer proceeds");
let context = context.expect("context survives");
assert_eq!(round, 1);
assert!(matches!(
context.input_responses().expect("answers survive")["k"],
crate::types::mrtr::InputResponse::Elicitation(_)
));
assert_eq!(context.mrtr_continuation(), Some(&json!({ "step": 1 })));
}
#[test]
fn the_same_bytes_are_accepted_when_sampling_is_what_was_requested() {
let (context, _) = proceed_with(
context_answering(&[("k", overlapping_answer())]),
Some(kinds_of(&[(
"k",
crate::types::mrtr::InputRequestKind::Sampling,
)])),
)
.expect("the overlapping object IS a valid CreateMessageResult");
assert!(matches!(
context
.expect("context survives")
.input_responses()
.expect("answers")["k"],
crate::types::mrtr::InputResponse::Sampling(_)
));
}
#[test]
fn an_unsolicited_key_is_rejected_at_the_verified_path() {
let outcome = proceed_with(
context_answering(&[("surprise", json!({ "roots": [] }))]),
Some(kinds_of(&[(
"k",
crate::types::mrtr::InputRequestKind::Elicitation,
)])),
);
let Err((code, message)) = outcome else {
panic!("an unsolicited key must short-circuit dispatch");
};
assert_eq!(code, crate::types::protocol::error_codes::INVALID_PARAMS);
assert!(
!message.contains("surprise"),
"an unsolicited key is CLIENT-chosen, so it must not be echoed: {message}"
);
}
#[test]
fn a_pre_kinds_continuation_degrades_to_untagged_instead_of_rejecting() {
let (context, _) =
proceed_with(context_answering(&[("k", overlapping_answer())]), None)
.expect("a pre-kinds continuation must never reject");
assert!(matches!(
context
.expect("context survives")
.input_responses()
.expect("answers")["k"],
crate::types::mrtr::InputResponse::Sampling(_)
));
}
#[test]
fn a_verified_round_with_no_answers_is_not_a_mismatch() {
let (context, _) = proceed_with(
v2_context(),
Some(kinds_of(&[(
"k",
crate::types::mrtr::InputRequestKind::Elicitation,
)])),
)
.expect("answering nothing is not an error");
assert!(context
.expect("context survives")
.input_responses()
.is_none());
}
mod mrtr_egress {
use super::*;
fn form_requests() -> crate::types::mrtr::InputRequests {
let mut requests = crate::types::mrtr::InputRequests::new();
requests.insert(
"user_name".to_string(),
crate::types::mrtr::InputRequest::Elicitation(Box::new(
crate::types::elicitation::ElicitRequestParams::Form {
message: "Who are you?".to_string(),
requested_schema: json!({ "type": "object" }),
},
)),
);
requests
}
fn signal_meta() -> Value {
let (_, value) = crate::types::mrtr::MrtrSignal {
input_requests: form_requests(),
continuation: json!({ "step": 1 }),
}
.into_meta_entry()
.expect("signal serializes");
value
}
fn signalling_response() -> JSONRPCResponse {
signalling_response_for(&signal_meta())
}
fn signalling_response_for(signal: &Value) -> JSONRPCResponse {
ServerCore::success_response(
RequestId::from(1i64),
json!({
"content": [],
"_meta": { crate::types::mrtr::MRTR_SIGNAL_META_KEY: signal },
}),
)
}
fn v2_context_all_caps() -> ProtocolContext {
v2_context().with_client_capabilities(caps(
Some(crate::types::capabilities::ElicitationCapabilities {
form: None,
url: Some(json!({})),
}),
Some(crate::types::capabilities::SamplingCapabilities::default()),
Some(crate::types::capabilities::RootsCapabilities::default()),
))
}
fn caps(
elicitation: Option<crate::types::capabilities::ElicitationCapabilities>,
sampling: Option<crate::types::capabilities::SamplingCapabilities>,
roots: Option<crate::types::capabilities::RootsCapabilities>,
) -> crate::types::ClientCapabilities {
crate::types::ClientCapabilities {
sampling,
elicitation,
roots,
..Default::default()
}
}
fn error_of(response: &JSONRPCResponse) -> &crate::types::jsonrpc::JSONRPCError {
match response.payload {
ResponsePayload::Error(ref error) => error,
ResponsePayload::Result(_) => panic!("expected an error payload"),
}
}
fn egress_with(
response: &mut JSONRPCResponse,
context: Option<&ProtocolContext>,
codec: Option<&RequestStateCodec>,
round: u8,
) -> (ResponseDisposition, ReservedFieldOwner) {
let request = call_tool(json!({}));
let target = mrtr_binding_parts(&request);
mrtr_egress(
response,
&MrtrEgressInputs {
target: target.as_ref(),
protocol_context: context,
principal: MrtrPrincipal {
authenticated_subject: Some(ALICE),
has_auth_provider: false,
},
codec,
round,
},
)
}
fn result_of(response: &JSONRPCResponse) -> &Value {
match response.payload {
ResponsePayload::Result(ref value) => value,
ResponsePayload::Error(_) => panic!("expected a result payload"),
}
}
#[test]
fn egress_emits_input_required_with_a_round_plus_one_token() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let target = mrtr_binding_parts(&request);
let context = v2_context_all_caps();
let mut response = signalling_response();
let (disposition, owner) = mrtr_egress(
&mut response,
&MrtrEgressInputs {
target: target.as_ref(),
protocol_context: Some(&context),
principal: MrtrPrincipal {
authenticated_subject: Some(ALICE),
has_auth_provider: false,
},
codec: Some(&codec),
round: 4,
},
);
assert_eq!(
(disposition, owner),
(ResponseDisposition::InputRequired, ReservedFieldOwner::Mrtr)
);
assert!(
result_of(&response).get("resultType").is_none(),
"egress must not write resultType — the envelope owns it"
);
let server_info = Implementation::new("test", "1.0.0");
inject_v2_result_envelope(
&mut response,
Some(&context),
&server_info,
disposition,
owner,
Cacheable::No,
);
let result = result_of(&response);
assert_eq!(result["resultType"], "input_required");
assert!(
result["inputRequests"]
.as_object()
.is_some_and(|m| !m.is_empty()),
"the re-elicitation must carry REAL inputRequests, got {result}"
);
let token = result["requestState"]
.as_str()
.expect("a fresh requestState is minted");
assert!(
!serde_json::to_string(result)
.expect("serializes")
.contains(crate::types::mrtr::MRTR_SIGNAL_META_KEY),
"got {result}"
);
assert_eq!(result["_meta"].as_object().expect("an object").len(), 1);
assert!(result["_meta"][RESERVED_SERVER_INFO_KEY].is_object());
let binding = RequestBinding::from_request(
ALICE,
target.as_ref().expect("eligible").0,
&target.as_ref().expect("eligible").1,
)
.expect("the fixture params are inside the canonical depth cap");
let crate::server::request_state::Verdict::Ok(continuation) =
codec.verify(token, &binding)
else {
panic!("the freshly minted token must verify");
};
assert_eq!(continuation.round, 5);
assert_eq!(continuation.state, json!({ "step": 1 }));
}
#[test]
fn egress_strips_the_internal_signal_on_every_path() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let target = mrtr_binding_parts(&request);
let v1 = ProtocolContext::new(Era::V1, ProtocolVersion("2025-11-25".to_string()));
let list = Request::Client(Box::new(ClientRequest::ListTools(ListToolsRequest {
cursor: None,
})));
let list_target = mrtr_binding_parts(&list);
let v2 = v2_context_all_caps();
for (label, context, target) in [
("v1 era", Some(&v1), target.as_ref()),
("no resolved context", None, target.as_ref()),
("non-eligible method", Some(&v2), list_target.as_ref()),
] {
let mut response = signalling_response();
let (disposition, owner) = mrtr_egress(
&mut response,
&MrtrEgressInputs {
target,
protocol_context: context,
principal: MrtrPrincipal {
authenticated_subject: Some(ALICE),
has_auth_provider: false,
},
codec: Some(&codec),
round: 0,
},
);
assert_eq!(
(disposition, owner),
(ResponseDisposition::Complete, ReservedFieldOwner::None),
"{label}"
);
let rendered =
serde_json::to_string(&response).expect("the response serializes");
assert!(
!rendered.contains(crate::types::mrtr::MRTR_SIGNAL_META_KEY),
"{label}: the internal MRTR signal leaked onto the wire: {rendered}"
);
assert!(
!rendered.contains("\"step\""),
"{label}: the plaintext continuation leaked onto the wire: {rendered}"
);
assert!(
!rendered.contains("resultType"),
"{label}: input_required must not be emitted here"
);
assert_eq!(
error_of(&response).code,
crate::types::protocol::error_codes::INTERNAL_ERROR,
"{label}: a forbidden-path signal must fail loudly"
);
assert_eq!(error_of(&response).message, MRTR_FORBIDDEN_PATH_MESSAGE);
}
}
#[test]
fn egress_fails_loudly_on_a_malformed_signal() {
let codec = codec(&KEY_A, 300);
let context = v2_context_all_caps();
let mut response = signalling_response_for(&json!("not-a-signal"));
let (disposition, owner) =
egress_with(&mut response, Some(&context), Some(&codec), 0);
assert_eq!(
(disposition, owner),
(ResponseDisposition::Complete, ReservedFieldOwner::None)
);
assert_eq!(
error_of(&response).code,
crate::types::protocol::error_codes::INTERNAL_ERROR
);
assert_eq!(error_of(&response).message, MRTR_MALFORMED_SIGNAL_MESSAGE);
let rendered = serde_json::to_string(&response).expect("serializes");
assert!(!rendered.contains(crate::types::mrtr::MRTR_SIGNAL_META_KEY));
}
#[test]
fn every_eligible_result_type_can_carry_the_signal() {
let (key, value) = crate::types::mrtr::MrtrSignal {
input_requests: form_requests(),
continuation: json!({ "step": 1 }),
}
.into_meta_entry()
.expect("signal serializes");
let mut resource = crate::types::ReadResourceResult::new(vec![]);
let mut meta = serde_json::Map::new();
meta.insert(key.clone(), value.clone());
resource._meta = Some(Value::Object(meta.clone()));
let resource = serde_json::to_value(&resource).expect("serializes");
assert!(resource["_meta"][&key].is_object());
let mut prompt = crate::types::GetPromptResult {
description: None,
messages: vec![],
_meta: None,
};
prompt._meta = Some(meta.clone());
let prompt = serde_json::to_value(&prompt).expect("serializes");
assert!(prompt["_meta"][&key].is_object());
for shape in [resource, prompt] {
let codec = codec(&KEY_A, 300);
let context = v2_context_all_caps();
let mut response = ServerCore::success_response(RequestId::from(1i64), shape);
let (disposition, owner) =
egress_with(&mut response, Some(&context), Some(&codec), 0);
assert_eq!(
(disposition, owner),
(ResponseDisposition::InputRequired, ReservedFieldOwner::Mrtr)
);
let result = result_of(&response);
assert!(result["requestState"].is_string());
assert!(result["inputRequests"]["user_name"].is_object());
assert!(!serde_json::to_string(result)
.expect("serializes")
.contains(crate::types::mrtr::MRTR_SIGNAL_META_KEY));
}
}
#[test]
fn absent_read_resource_meta_emits_no_key() {
let result = crate::types::ReadResourceResult::new(vec![]);
let value = serde_json::to_value(&result).expect("serializes");
assert_eq!(value, json!({ "contents": [] }));
}
#[test]
fn capability_precheck_precedes_minting() {
let context = v2_context().with_client_capabilities(caps(
None,
Some(crate::types::capabilities::SamplingCapabilities::default()),
Some(crate::types::capabilities::RootsCapabilities::default()),
));
let mut response = signalling_response();
let (disposition, owner) = egress_with(&mut response, Some(&context), None, 0);
assert_eq!(
(disposition, owner),
(ResponseDisposition::Complete, ReservedFieldOwner::None)
);
let error = error_of(&response);
assert_eq!(
error.code,
crate::types::protocol::error_codes::MISSING_REQUIRED_CLIENT_CAPABILITY,
"the capability check must precede the mint, which has no codec here"
);
let codec = codec(&KEY_A, 300);
let mut with_codec = signalling_response();
let _ = egress_with(&mut with_codec, Some(&context), Some(&codec), 0);
let rendered = serde_json::to_string(&with_codec).expect("serializes");
assert!(
!rendered.contains("requestState"),
"a rejected result must mint nothing: {rendered}"
);
}
#[test]
fn reelicit_round_three_mints_round_four() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let target = mrtr_binding_parts(&request).expect("eligible");
let context = v2_context_all_caps();
let mut response = signalling_response();
let (disposition, owner) =
egress_with(&mut response, Some(&context), Some(&codec), 3);
assert_eq!(
(disposition, owner),
(ResponseDisposition::InputRequired, ReservedFieldOwner::Mrtr)
);
let token = result_of(&response)["requestState"]
.as_str()
.expect("a token is minted");
let binding = RequestBinding::from_request(ALICE, target.0, &target.1)
.expect("the fixture params are inside the canonical depth cap");
let crate::server::request_state::Verdict::Ok(continuation) =
codec.verify(token, &binding)
else {
panic!("the freshly minted token must verify");
};
assert_eq!(continuation.round, 4);
}
#[test]
fn consecutive_rounds_mint_distinct_incrementing_tokens() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let target = mrtr_binding_parts(&request).expect("eligible");
let binding = RequestBinding::from_request(ALICE, target.0, &target.1)
.expect("the fixture params are inside the canonical depth cap");
let context = v2_context_all_caps();
let mut first = signalling_response();
let _ = egress_with(&mut first, Some(&context), Some(&codec), 0);
let first_token = result_of(&first)["requestState"]
.as_str()
.expect("token")
.to_string();
let mut second = signalling_response();
let _ = egress_with(&mut second, Some(&context), Some(&codec), 1);
let second_token = result_of(&second)["requestState"]
.as_str()
.expect("token")
.to_string();
assert_ne!(first_token, second_token, "each round mints a fresh token");
let round_of = |token: &str| match codec.verify(token, &binding) {
crate::server::request_state::Verdict::Ok(continuation) => continuation.round,
other => panic!("a freshly minted token must verify, got {other:?}"),
};
assert_eq!(round_of(&first_token), 1);
assert_eq!(round_of(&second_token), 2);
}
#[test]
fn mint_backstop_refuses_at_the_ceiling_and_admits_one_below() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let target = mrtr_binding_parts(&request).expect("eligible");
let context = v2_context_all_caps();
let mut admitted = signalling_response();
let (disposition, owner) = egress_with(
&mut admitted,
Some(&context),
Some(&codec),
MAX_MRTR_ROUNDS - 1,
);
assert_eq!(
(disposition, owner),
(ResponseDisposition::InputRequired, ReservedFieldOwner::Mrtr)
);
let token = result_of(&admitted)["requestState"]
.as_str()
.expect("a token is minted one below the ceiling");
let binding = RequestBinding::from_request(ALICE, target.0, &target.1)
.expect("the fixture params are inside the canonical depth cap");
let Verdict::Ok(continuation) = codec.verify(token, &binding) else {
panic!("the freshly minted token must verify");
};
assert_eq!(
continuation.round, MAX_MRTR_ROUNDS,
"the last admissible mint lands exactly ON the ceiling, which the \
ingress bound then refuses when it is presented"
);
let mut refused = signalling_response();
let (disposition, owner) =
egress_with(&mut refused, Some(&context), Some(&codec), MAX_MRTR_ROUNDS);
assert_eq!(
(disposition, owner),
(ResponseDisposition::Complete, ReservedFieldOwner::None)
);
assert_eq!(
error_of(&refused).code,
crate::types::protocol::error_codes::INTERNAL_ERROR
);
assert_eq!(
error_of(&refused).message,
MRTR_ROUND_CEILING_INVARIANT_MESSAGE
);
let rendered = serde_json::to_string(&refused).expect("serializes");
assert!(
!rendered.contains(&format!("\"{}\":", crate::types::mrtr::REQUEST_STATE_KEY)),
"nothing may be minted past the ceiling: {rendered}"
);
assert!(
!rendered.contains("\"step\""),
"and the plaintext continuation must not leak either: {rendered}"
);
}
fn egress_for_arguments(
response: &mut JSONRPCResponse,
arguments: Value,
codec: Option<&RequestStateCodec>,
) -> (ResponseDisposition, ReservedFieldOwner) {
let request = call_tool(arguments);
let target = mrtr_binding_parts(&request);
let context = v2_context_all_caps();
mrtr_egress(
response,
&MrtrEgressInputs {
target: target.as_ref(),
protocol_context: Some(&context),
principal: MrtrPrincipal {
authenticated_subject: Some(ALICE),
has_auth_provider: false,
},
codec,
round: 0,
},
)
}
#[test]
fn egress_refuses_to_mint_for_an_uncanonicalizable_request() {
let codec = codec(&KEY_A, 300);
let mut response = signalling_response();
let (disposition, owner) =
egress_for_arguments(&mut response, arguments_past_the_cap(), Some(&codec));
assert_eq!(
(disposition, owner),
(ResponseDisposition::Complete, ReservedFieldOwner::None)
);
assert_eq!(
error_of(&response).code,
crate::types::protocol::error_codes::INVALID_PARAMS,
"the client's params caused this, so it is not an INTERNAL_ERROR"
);
assert_eq!(error_of(&response).message, MRTR_UNCANONICALIZABLE_MESSAGE);
let rendered = serde_json::to_string(&response).expect("serializes");
assert!(
!rendered.contains(&format!("\"{}\":", crate::types::mrtr::REQUEST_STATE_KEY)),
"no continuation may be minted for an unidentifiable request: {rendered}"
);
assert!(
!rendered.contains("\"step\""),
"and the plaintext continuation must not leak either: {rendered}"
);
}
#[test]
fn egress_at_the_depth_cap_still_mints_a_verifiable_token() {
let codec = codec(&KEY_A, 300);
let request = call_tool(arguments_at_the_cap());
let target = mrtr_binding_parts(&request).expect("eligible");
let mut response = signalling_response();
let (disposition, owner) =
egress_for_arguments(&mut response, arguments_at_the_cap(), Some(&codec));
assert_eq!(
(disposition, owner),
(ResponseDisposition::InputRequired, ReservedFieldOwner::Mrtr)
);
let token = result_of(&response)["requestState"]
.as_str()
.expect("a token is minted at the cap");
let binding = RequestBinding::from_request(ALICE, target.0, &target.1)
.expect("params at the cap still bind");
let Verdict::Ok(continuation) = codec.verify(token, &binding) else {
panic!("the freshly minted token must verify");
};
assert_eq!(continuation.round, 1);
}
#[test]
fn the_depth_refusal_precedes_every_mint_precondition() {
let mut response = signalling_response();
let (disposition, owner) =
egress_for_arguments(&mut response, arguments_past_the_cap(), None);
assert_eq!(
(disposition, owner),
(ResponseDisposition::Complete, ReservedFieldOwner::None)
);
assert_eq!(
error_of(&response).message,
MRTR_UNCANONICALIZABLE_MESSAGE,
"the depth check must precede the codec lookup"
);
assert_ne!(
error_of(&response).message,
MRTR_UNCANONICALIZABLE_INVARIANT_MESSAGE,
"the mint-site backstop must be UNREACHABLE while step (3b) stands"
);
}
#[test]
fn mint_backstop_precedes_every_other_mint_precondition() {
let context = v2_context_all_caps();
let mut response = signalling_response();
let (disposition, owner) =
egress_with(&mut response, Some(&context), None, MAX_MRTR_ROUNDS);
assert_eq!(
(disposition, owner),
(ResponseDisposition::Complete, ReservedFieldOwner::None)
);
assert_eq!(
error_of(&response).message,
MRTR_ROUND_CEILING_INVARIANT_MESSAGE,
"the round check must precede the codec lookup"
);
}
#[test]
fn exactly_three_client_request_variants_are_mrtr_eligible() {
let eligible: Vec<&str> = every_client_request()
.iter()
.filter(|(_, request)| client_request_mrtr_eligible(request))
.map(|(label, _)| *label)
.collect();
assert_eq!(
eligible,
vec!["tools/call", "prompts/get", "resources/read"],
"the spec confines input_required to exactly these three methods"
);
}
#[test]
fn enum_eligibility_agrees_with_the_method_table() {
for (method, request) in every_client_request() {
assert_eq!(
client_request_mrtr_eligible(&request),
crate::types::mrtr::mrtr_eligible(method),
"{method}: the enum tripwire and MRTR_METHODS disagree"
);
}
for row in &crate::types::mrtr::MRTR_METHODS {
assert!(
every_client_request()
.iter()
.any(|(method, request)| *method == row.method
&& client_request_mrtr_eligible(request)),
"{}: a table row with no eligible enum variant",
row.method
);
}
}
#[test]
fn binding_parts_cover_exactly_the_method_table() {
let covered: Vec<&'static str> = every_client_request()
.into_iter()
.filter_map(|(_, request)| {
mrtr_binding_parts(&Request::Client(Box::new(request))).map(|(m, _)| m)
})
.collect();
let expected: Vec<&'static str> = crate::types::mrtr::MRTR_METHODS
.iter()
.map(|row| row.method)
.collect();
assert_eq!(covered, expected);
}
fn every_client_request() -> Vec<(&'static str, ClientRequest)> {
use crate::types::prompts::ListPromptsRequest;
use crate::types::protocol::{
CompleteRequest, CompletionArgument, CompletionReference, InitializeRequest,
};
use crate::types::resources::{ListResourceTemplatesRequest, ListResourcesRequest};
vec![
(
"initialize",
ClientRequest::Initialize(InitializeRequest {
protocol_version: "2026-07-28".to_string(),
capabilities: crate::types::ClientCapabilities::default(),
client_info: Implementation::new("c", "1"),
}),
),
(
"tools/call",
ClientRequest::CallTool(CallToolRequest {
name: "search".to_string(),
arguments: json!({}),
_meta: None,
task: None,
}),
),
(
"prompts/get",
ClientRequest::GetPrompt(crate::types::GetPromptRequest {
name: "greeting".to_string(),
arguments: HashMap::new(),
_meta: None,
}),
),
(
"resources/read",
ClientRequest::ReadResource(crate::types::ReadResourceRequest {
uri: "mem://x".to_string(),
_meta: None,
}),
),
(
"tools/list",
ClientRequest::ListTools(ListToolsRequest { cursor: None }),
),
(
"prompts/list",
ClientRequest::ListPrompts(ListPromptsRequest { cursor: None }),
),
(
"resources/list",
ClientRequest::ListResources(ListResourcesRequest { cursor: None }),
),
(
"resources/templates/list",
ClientRequest::ListResourceTemplates(ListResourceTemplatesRequest {
cursor: None,
}),
),
(
"resources/subscribe",
ClientRequest::Subscribe(crate::types::SubscribeRequest {
uri: "mem://x".to_string(),
}),
),
(
"resources/unsubscribe",
ClientRequest::Unsubscribe(crate::types::UnsubscribeRequest {
uri: "mem://x".to_string(),
}),
),
(
"completion/complete",
ClientRequest::Complete(CompleteRequest {
r#ref: CompletionReference::Prompt {
name: "p".to_string(),
},
argument: CompletionArgument {
name: "a".to_string(),
value: String::new(),
},
}),
),
(
"sampling/createMessage",
ClientRequest::CreateMessage(Box::new(
crate::types::sampling::CreateMessageParams::new(vec![]),
)),
),
(
"tasks/get",
ClientRequest::TasksGet(crate::types::tasks::GetTaskRequest {
task_id: "t".to_string(),
}),
),
(
"tasks/result",
ClientRequest::TasksResult(crate::types::tasks::GetTaskPayloadRequest {
task_id: "t".to_string(),
}),
),
(
"tasks/list",
ClientRequest::TasksList(crate::types::tasks::ListTasksRequest {
cursor: None,
}),
),
(
"tasks/cancel",
ClientRequest::TasksCancel(crate::types::tasks::CancelTaskRequest {
task_id: "t".to_string(),
result: None,
}),
),
(
"logging/setLevel",
ClientRequest::SetLoggingLevel {
level: crate::types::notifications::LoggingLevel::Info,
},
),
("ping", ClientRequest::Ping),
]
}
fn requests_of(entries: Vec<(&str, crate::types::mrtr::InputRequest)>) -> Value {
let mut map = crate::types::mrtr::InputRequests::new();
for (key, request) in entries {
map.insert(key.to_string(), request);
}
let (_, value) = crate::types::mrtr::MrtrSignal {
input_requests: map,
continuation: json!({ "step": 1 }),
}
.into_meta_entry()
.expect("signal serializes");
value
}
fn url_elicitation() -> crate::types::mrtr::InputRequest {
crate::types::mrtr::InputRequest::Elicitation(Box::new(
crate::types::elicitation::ElicitRequestParams::Url {
message: "Approve the payment".to_string(),
elicitation_id: "e1".to_string(),
url: "https://example.test/approve".to_string(),
},
))
}
fn form_elicitation() -> crate::types::mrtr::InputRequest {
crate::types::mrtr::InputRequest::Elicitation(Box::new(
crate::types::elicitation::ElicitRequestParams::Form {
message: "Who are you?".to_string(),
requested_schema: json!({ "type": "object" }),
},
))
}
fn reject_for(
signal: &Value,
declared: crate::types::ClientCapabilities,
) -> crate::types::jsonrpc::JSONRPCError {
let codec = codec(&KEY_A, 300);
let context = v2_context().with_client_capabilities(declared);
let mut response = signalling_response_for(signal);
let (disposition, owner) =
egress_with(&mut response, Some(&context), Some(&codec), 0);
assert_eq!(
(disposition, owner),
(ResponseDisposition::Complete, ReservedFieldOwner::None)
);
error_of(&response).clone()
}
#[test]
fn undeclared_elicitation_is_minus_32021_with_an_object_payload() {
let error = reject_for(
&requests_of(vec![("who", form_elicitation())]),
caps(None, None, None),
);
assert_eq!(error.code, -32021);
assert_eq!(
error.code,
crate::types::protocol::error_codes::MISSING_REQUIRED_CLIENT_CAPABILITY
);
let required = &error.data.as_ref().expect("a payload")["requiredCapabilities"];
assert!(
required.is_object(),
"requiredCapabilities MUST be a ClientCapabilities object, not an array \
or a string list: {required}"
);
assert!(!required.is_array());
assert_eq!(required, &json!({ "elicitation": {} }));
}
#[test]
fn url_elicitation_against_a_form_only_client_is_minus_32021() {
let form_only = caps(
Some(crate::types::capabilities::ElicitationCapabilities {
form: Some(
crate::types::capabilities::FormElicitationCapability::default(),
),
url: None,
}),
None,
None,
);
let error = reject_for(&requests_of(vec![("pay", url_elicitation())]), form_only);
assert_eq!(
error.code,
crate::types::protocol::error_codes::MISSING_REQUIRED_CLIENT_CAPABILITY
);
let required = &error.data.as_ref().expect("a payload")["requiredCapabilities"];
assert_eq!(required, &json!({ "elicitation": { "url": {} } }));
}
#[test]
fn url_elicitation_against_a_url_capable_client_passes() {
let codec = codec(&KEY_A, 300);
let context = v2_context_all_caps();
let mut response =
signalling_response_for(&requests_of(vec![("pay", url_elicitation())]));
let (disposition, owner) =
egress_with(&mut response, Some(&context), Some(&codec), 0);
assert_eq!(
(disposition, owner),
(ResponseDisposition::InputRequired, ReservedFieldOwner::Mrtr)
);
assert!(result_of(&response)["inputRequests"]["pay"].is_object());
}
#[test]
fn undeclared_sampling_is_minus_32021() {
let error = reject_for(
&requests_of(vec![(
"draft",
crate::types::mrtr::InputRequest::Sampling(Box::new(
crate::types::sampling::CreateMessageParams::new(vec![]),
)),
)]),
caps(
Some(crate::types::capabilities::ElicitationCapabilities::default()),
None,
None,
),
);
assert_eq!(
error.code,
crate::types::protocol::error_codes::MISSING_REQUIRED_CLIENT_CAPABILITY
);
let required = &error.data.as_ref().expect("a payload")["requiredCapabilities"];
assert_eq!(required, &json!({ "sampling": {} }));
}
#[test]
fn tool_augmented_sampling_needs_the_tools_sub_capability() {
let mut params = crate::types::sampling::CreateMessageParams::new(vec![]);
params.tools = Some(vec![]);
let error = reject_for(
&requests_of(vec![(
"draft",
crate::types::mrtr::InputRequest::Sampling(Box::new(params)),
)]),
caps(
None,
Some(crate::types::capabilities::SamplingCapabilities::default()),
None,
),
);
assert_eq!(
error.code,
crate::types::protocol::error_codes::MISSING_REQUIRED_CLIENT_CAPABILITY
);
let required = &error.data.as_ref().expect("a payload")["requiredCapabilities"];
assert_eq!(required, &json!({ "sampling": { "tools": {} } }));
}
#[test]
fn undeclared_roots_is_minus_32021() {
let error = reject_for(
&requests_of(vec![("roots", crate::types::mrtr::InputRequest::ListRoots)]),
caps(None, None, None),
);
assert_eq!(
error.code,
crate::types::protocol::error_codes::MISSING_REQUIRED_CLIENT_CAPABILITY
);
let required = &error.data.as_ref().expect("a payload")["requiredCapabilities"];
assert!(required["roots"].is_object());
}
#[test]
fn a_mixed_map_is_rejected_wholesale_never_partially_emitted() {
let error = reject_for(
&requests_of(vec![
("who", form_elicitation()),
("roots", crate::types::mrtr::InputRequest::ListRoots),
]),
caps(
Some(crate::types::capabilities::ElicitationCapabilities::default()),
None,
None,
),
);
assert_eq!(
error.code,
crate::types::protocol::error_codes::MISSING_REQUIRED_CLIENT_CAPABILITY
);
let required = &error.data.as_ref().expect("a payload")["requiredCapabilities"];
assert!(
required.get("elicitation").is_none(),
"a DECLARED capability must not appear in the missing set: {required}"
);
assert!(required["roots"].is_object());
}
#[test]
fn input_requests_keys_are_unique_by_construction() {
let mut map = crate::types::mrtr::InputRequests::new();
map.insert("dup".to_string(), form_elicitation());
map.insert(
"dup".to_string(),
crate::types::mrtr::InputRequest::ListRoots,
);
assert_eq!(map.len(), 1, "a BTreeMap cannot hold a duplicate key");
let serialized = serde_json::to_value(&map).expect("serializes");
assert_eq!(
serialized.as_object().expect("an object").len(),
1,
"and the wire shape carries the key exactly once"
);
}
#[test]
fn egress_fails_closed_when_it_cannot_mint() {
let request = call_tool(json!({}));
let target = mrtr_binding_parts(&request);
let context = v2_context_all_caps();
let mut response = signalling_response();
let (disposition, owner) = mrtr_egress(
&mut response,
&MrtrEgressInputs {
target: target.as_ref(),
protocol_context: Some(&context),
principal: MrtrPrincipal {
authenticated_subject: None,
has_auth_provider: true,
},
codec: None,
round: 0,
},
);
assert_eq!(
(disposition, owner),
(ResponseDisposition::Complete, ReservedFieldOwner::None)
);
let ResponsePayload::Error(ref error) = response.payload else {
panic!("an unmintable continuation must fail closed with an error");
};
assert_eq!(
error.code,
crate::types::protocol::error_codes::INTERNAL_ERROR
);
}
#[test]
fn egress_is_a_noop_without_a_signal() {
let codec = codec(&KEY_A, 300);
let request = call_tool(json!({}));
let target = mrtr_binding_parts(&request);
let context = v2_context();
let original = json!({ "content": [], "_meta": { "vendor/key": 1 } });
let mut response =
ServerCore::success_response(RequestId::from(1i64), original.clone());
let (disposition, owner) = mrtr_egress(
&mut response,
&MrtrEgressInputs {
target: target.as_ref(),
protocol_context: Some(&context),
principal: MrtrPrincipal {
authenticated_subject: Some(ALICE),
has_auth_provider: false,
},
codec: Some(&codec),
round: 0,
},
);
assert_eq!(
(disposition, owner),
(ResponseDisposition::Complete, ReservedFieldOwner::None)
);
assert_eq!(result_of(&response), &original);
}
}
#[test]
fn binding_parts_cover_exactly_the_eligible_methods() {
for (request, method) in [
(call_tool(json!({})), "tools/call"),
(
Request::Client(Box::new(ClientRequest::GetPrompt(
crate::types::GetPromptRequest {
name: "greeting".to_string(),
arguments: HashMap::new(),
_meta: None,
},
))),
"prompts/get",
),
(
Request::Client(Box::new(ClientRequest::ReadResource(
crate::types::ReadResourceRequest {
uri: "mem://greeting".to_string(),
_meta: None,
},
))),
"resources/read",
),
] {
let (resolved, params) =
mrtr_binding_parts(&request).expect("an MRTR-eligible request");
assert_eq!(resolved, method);
assert!(
crate::types::mrtr::mrtr_eligible(resolved),
"{method} must be in the ONE MRTR method table"
);
assert!(params.get("inputResponses").is_none());
assert!(params.get("requestState").is_none());
}
assert!(
mrtr_binding_parts(&Request::Client(Box::new(ClientRequest::ListTools(
ListToolsRequest { cursor: None }
))))
.is_none()
);
}
}
}