use serde::de::DeserializeOwned;
pub fn assert_roundtrips_through_client<T>(real_dispatch_output: serde_json::Value)
where
T: DeserializeOwned,
{
let pretty = serde_json::to_string_pretty(&real_dispatch_output)
.unwrap_or_else(|_| real_dispatch_output.to_string());
if let Err(error) = serde_json::from_value::<T>(real_dispatch_output) {
panic!(
"dispatch output does not deserialize into `{}`: {error}\noffending output was:\n{pretty}",
std::any::type_name::<T>(),
);
}
}
pub const META_PROTOCOL_VERSION: &str =
crate::types::protocol::context::RESERVED_PROTOCOL_VERSION_KEY;
pub const META_CLIENT_INFO: &str = crate::types::protocol::context::RESERVED_CLIENT_INFO_KEY;
pub const META_CLIENT_CAPABILITIES: &str =
crate::types::protocol::context::RESERVED_CLIENT_CAPABILITIES_KEY;
#[cfg(all(not(target_arch = "wasm32"), feature = "streamable-http"))]
pub const META_SERVER_INFO: &str = crate::server::core::RESERVED_SERVER_INFO_KEY;
pub const HEADER_SENTINEL_PREFIX: &str = crate::types::mrtr::HEADER_SENTINEL_PREFIX;
pub const HEADER_SENTINEL_SUFFIX: &str = crate::types::mrtr::HEADER_SENTINEL_SUFFIX;
#[must_use]
pub fn encode_mcp_name(value: &str) -> String {
crate::types::mrtr::encode_header_value(value)
}
#[must_use]
pub fn decode_mcp_name(raw: &str) -> Option<String> {
crate::types::mrtr::decode_header_value(raw)
}
#[must_use]
pub fn routing_name_key(method: &str) -> Option<&'static str> {
crate::types::mrtr::name_bearing_key(method)
}
#[must_use]
pub fn method_is_mrtr_eligible(method: &str) -> bool {
crate::types::mrtr::mrtr_eligible(method)
}
#[cfg(not(target_arch = "wasm32"))]
pub const V2_TASKS_METHOD_RETIRED: &str = crate::server::task_dispatch::V2_TASKS_METHOD_RETIRED;
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
#[must_use]
pub fn mint_request_state(
key: &[u8; 32],
ttl: std::time::Duration,
principal: &str,
method: &str,
params: &serde_json::Value,
state: &serde_json::Value,
round: u8,
) -> Option<String> {
let codec = crate::server::request_state::RequestStateCodec::new(key, ttl).ok()?;
let binding =
crate::server::request_state::RequestBinding::from_request(principal, method, params)
.ok()?;
codec.mint(state, &binding, round, None).ok()
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
#[must_use]
pub fn open_request_state(
key: &[u8; 32],
principal: &str,
method: &str,
params: &serde_json::Value,
token: &str,
) -> Option<(serde_json::Value, u8)> {
let codec = crate::server::request_state::RequestStateCodec::new(
key,
std::time::Duration::from_secs(1),
)
.ok()?;
let binding =
crate::server::request_state::RequestBinding::from_request(principal, method, params)
.ok()?;
match codec.verify(token, &binding) {
crate::server::request_state::Verdict::Ok(continuation) => {
Some((continuation.state, continuation.round))
},
_ => None,
}
}
#[cfg(all(feature = "streamable-http", not(target_arch = "wasm32")))]
pub const ANONYMOUS_PRINCIPAL: &str = crate::server::core::ANONYMOUS_PRINCIPAL;
pub const RESERVED_INPUT_REQUESTS: &str = crate::types::mrtr::INPUT_REQUESTS_KEY;
pub const RESERVED_REQUEST_STATE: &str = crate::types::mrtr::REQUEST_STATE_KEY;
pub const MAX_INPUT_RESPONSES: usize = crate::types::mrtr::MAX_INPUT_RESPONSES;
pub const MAX_INPUT_RESPONSE_BYTES: usize = crate::types::mrtr::MAX_INPUT_RESPONSE_BYTES;
pub const MAX_INPUT_RESPONSES_TOTAL_BYTES: usize =
crate::types::mrtr::MAX_INPUT_RESPONSES_TOTAL_BYTES;
pub const MAX_INPUT_RESPONSE_DEPTH: usize = crate::types::mrtr::MAX_INPUT_RESPONSE_DEPTH;
#[cfg(not(target_arch = "wasm32"))]
pub use reserved_fields::{
v1_result_envelope, v2_result_envelope, CapturedWarning, EnvelopeOutcome, ReservedFieldEgress,
};
#[cfg(not(target_arch = "wasm32"))]
mod reserved_fields {
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CapturedWarning {
pub target: String,
pub field: Option<String>,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct EnvelopeOutcome {
pub bytes: String,
pub warnings: Vec<CapturedWarning>,
}
impl EnvelopeOutcome {
#[must_use]
pub fn warned_about(&self, field: &str) -> bool {
self.warnings
.iter()
.any(|warning| warning.field.as_deref() == Some(field))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReservedFieldEgress {
NoEgress,
Mrtr,
TasksDispatch,
}
#[must_use]
pub fn v2_result_envelope(
result: serde_json::Value,
egress: ReservedFieldEgress,
) -> EnvelopeOutcome {
run_envelope(result, Some(crate::types::protocol::Era::V2), egress)
}
#[must_use]
pub fn v1_result_envelope(result: serde_json::Value) -> EnvelopeOutcome {
run_envelope(result, Some(crate::types::protocol::Era::V1), egress_none())
}
fn egress_none() -> ReservedFieldEgress {
ReservedFieldEgress::NoEgress
}
fn run_envelope(
result: serde_json::Value,
era: Option<crate::types::protocol::Era>,
egress: ReservedFieldEgress,
) -> EnvelopeOutcome {
let (disposition, owner) = match egress {
ReservedFieldEgress::NoEgress => (
crate::server::core::ResponseDisposition::Complete,
crate::server::core::ReservedFieldOwner::None,
),
ReservedFieldEgress::Mrtr => (
crate::server::core::ResponseDisposition::InputRequired,
crate::server::core::ReservedFieldOwner::Mrtr,
),
ReservedFieldEgress::TasksDispatch => (
crate::server::core::ResponseDisposition::Complete,
crate::server::core::ReservedFieldOwner::TasksDispatch,
),
};
let context = era.map(|era| {
let version = match era {
crate::types::protocol::Era::V2 => {
crate::types::protocol::PROTOCOL_VERSION_2026_07_28
},
crate::types::protocol::Era::V1 => crate::LATEST_PROTOCOL_VERSION,
};
crate::types::protocol::ProtocolContext::new(
era,
crate::types::protocol::ProtocolVersion(version.to_string()),
)
});
let server_info =
crate::types::Implementation::new("reserved-field-registry-probe", "1.0.0");
let mut response = crate::types::jsonrpc::JSONRPCResponse {
jsonrpc: "2.0".to_string(),
id: crate::types::RequestId::from(1i64),
payload: crate::types::jsonrpc::ResponsePayload::Result(result),
};
let events = Arc::new(Mutex::new(Vec::new()));
{
let collector = capture::Collector {
events: Arc::clone(&events),
};
tracing::subscriber::with_default(collector, || {
crate::server::core::inject_v2_result_envelope(
&mut response,
context.as_ref(),
&server_info,
disposition,
owner,
crate::types::caching::Cacheable::No,
);
});
}
let warnings = std::mem::take(&mut *events.lock().unwrap_or_else(|e| e.into_inner()));
EnvelopeOutcome {
bytes: serde_json::to_string(&response).unwrap_or_default(),
warnings,
}
}
mod capture {
use super::{Arc, CapturedWarning, Mutex};
pub(super) struct Collector {
pub(super) events: Arc<Mutex<Vec<CapturedWarning>>>,
}
#[derive(Default)]
struct Fields {
message: String,
field: Option<String>,
}
impl tracing::field::Visit for Fields {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
self.assign(field.name(), value.to_string());
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.assign(field.name(), format!("{value:?}"));
}
}
impl Fields {
fn assign(&mut self, name: &str, value: String) {
match name {
"message" => self.message = value,
"field" => self.field = Some(value),
_ => {},
}
}
}
impl tracing::Subscriber for Collector {
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
true
}
fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
tracing::span::Id::from_u64(1)
}
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {
}
fn event(&self, event: &tracing::Event<'_>) {
let mut fields = Fields::default();
event.record(&mut fields);
let captured = CapturedWarning {
target: event.metadata().target().to_string(),
field: fields.field,
message: fields.message,
};
if let Ok(mut events) = self.events.lock() {
events.push(captured);
}
}
fn enter(&self, _span: &tracing::span::Id) {}
fn exit(&self, _span: &tracing::span::Id) {}
}
}
}
#[cfg(test)]
mod tests {
use super::assert_roundtrips_through_client;
use crate::types::tasks::{CreateTaskResult, GetTaskResult, Task, TaskStatus};
fn sample_task() -> Task {
Task::new("t-positive", TaskStatus::Working)
.with_timestamps("2026-06-21T00:00:00Z", "2026-06-21T00:00:00Z")
}
#[test]
fn passes_on_valid_get_task_result() {
let value = serde_json::to_value(GetTaskResult::new(sample_task())).unwrap();
assert_roundtrips_through_client::<GetTaskResult>(value);
}
#[test]
fn passes_on_valid_create_task_result() {
let mut value = serde_json::to_value(CreateTaskResult::new(sample_task())).unwrap();
value.as_object_mut().unwrap().insert(
"_meta".to_string(),
serde_json::json!({ "io.modelcontextprotocol/related-task": { "taskId": "t-positive" } }),
);
assert_roundtrips_through_client::<CreateTaskResult>(value);
}
#[test]
#[should_panic(expected = "does not deserialize into")]
fn panics_on_exact_historical_flat_task_shape() {
let flat_task = serde_json::to_value(Task::new("t-1", TaskStatus::Working)).unwrap();
assert!(flat_task.get("taskId").is_some());
assert!(flat_task.get("task").is_none());
assert_roundtrips_through_client::<GetTaskResult>(flat_task);
}
}