use std::sync::Arc;
use crate::application::mcp::initialize::{read_offer, server_capabilities_json};
use crate::application::mcp::input_required::{InputRound, capability_for, is_input_required};
use crate::application::registry::listing::Listing;
use crate::application::server::builder::{DispatchError, Server};
use crate::application::server::context::{PeerSession, RequestContext};
use crate::domain::feature::{Feature, FeatureProfile};
use crate::domain::idempotency::{Claim, IdempotencyKey};
use crate::domain::ir::JsonObject;
use crate::domain::ir::LogLevel;
use crate::domain::registry::{Primitive, PrimitiveKind, PromptArgument, Registered};
use crate::domain::{
CapabilitySet, Negotiation, ProtocolVersion, SupportedVersions, TransportKind,
client_method_defined_in, discover_defined_in, handshake_defined_in, negotiate_server, resolve,
transport_allowed,
};
#[derive(Clone)]
pub struct Dispatcher {
server: Arc<Server>,
}
impl core::fmt::Debug for Dispatcher {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Dispatcher")
.field("name", &self.server.name())
.field("tool_count", &self.server.tool_count())
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy)]
enum SubscribeOp {
Subscribe,
Unsubscribe,
}
impl Dispatcher {
#[must_use]
pub const fn new(server: Arc<Server>) -> Self {
Self { server }
}
#[must_use]
pub const fn server(&self) -> &Arc<Server> {
&self.server
}
#[must_use]
pub fn state(&self) -> &Arc<crate::application::server::context::StateMap> {
self.server.state()
}
pub fn handshake(
&self,
ctx: &RequestContext,
params: &JsonObject,
) -> Result<(JsonObject, PeerSession), DispatchError> {
let (version, profile) = negotiate(ctx, params)?;
let reply = self.initialize_result(version, profile)?;
Ok((reply, ctx.peer().clone().with_profile(profile)))
}
fn initialize_result(
&self,
version: ProtocolVersion,
profile: FeatureProfile,
) -> Result<JsonObject, DispatchError> {
if !handshake_defined_in(version) {
return Err(DispatchError::UnknownMethod);
}
let result = serde_json::json!({
"protocolVersion": version.as_wire(),
"capabilities": server_capabilities_json(profile),
"serverInfo": {
"name": self.server.name(),
"version": self.server.version(),
},
});
JsonObject::from_value(result).map_err(|_| DispatchError::MalformedInitialize)
}
pub async fn dispatch(
&self,
ctx: &RequestContext,
method: &str,
params: &JsonObject,
) -> Result<JsonObject, DispatchError> {
let version = ctx.peer().negotiated();
let answered = self.route(ctx, method, params).await?;
let stamped = stamp_result(answered, Cacheable::No, version);
Ok(self.identify(stamped, version))
}
fn identify(&self, result: JsonObject, version: ProtocolVersion) -> JsonObject {
if !result_envelope_defined_in(version) {
return result;
}
let Ok(serde_json::Value::Object(mut members)) =
serde_json::from_str::<serde_json::Value>(result.as_str())
else {
return result;
};
let meta = members
.entry("_meta")
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
if let Some(meta) = meta.as_object_mut() {
meta.insert(
"io.modelcontextprotocol/serverInfo".to_owned(),
serde_json::json!({
"name": self.server.name(),
"version": self.server.version(),
}),
);
}
JsonObject::from_value(serde_json::Value::Object(members)).unwrap_or(result)
}
fn input_round(&self, params: &JsonObject) -> Result<InputRound, DispatchError> {
let responses = match parsed_param(params, "inputResponses")? {
None | Some(serde_json::Value::Null) => None,
Some(serde_json::Value::Object(map)) => Some(map),
Some(_) => return Err(DispatchError::MalformedParams),
};
let state = match parsed_param(params, "requestState")? {
None | Some(serde_json::Value::Null) => None,
Some(serde_json::Value::String(sealed)) => {
let key = self
.server
.request_state_key()
.ok_or(DispatchError::TamperedRequestState)?;
Some(
key.open(&sealed)
.ok_or(DispatchError::TamperedRequestState)?
.to_owned(),
)
}
Some(_) => return Err(DispatchError::MalformedParams),
};
Ok(InputRound {
responses,
state,
key: self.server.request_state_key().cloned(),
})
}
fn check_interim(
ctx: &RequestContext,
method: &str,
result: JsonObject,
) -> Result<JsonObject, DispatchError> {
let Ok(serde_json::Value::Object(members)) =
serde_json::from_str::<serde_json::Value>(result.as_str())
else {
return Ok(result);
};
if !is_input_required(&members) {
return Ok(result);
}
if !accepts_input_round(method) || !result_envelope_defined_in(ctx.peer().negotiated()) {
return Err(DispatchError::HandlerFailed(
"an InputRequiredResult is not defined for this request".to_owned(),
));
}
let mut required: Vec<&'static str> = Vec::new();
if let Some(requests) = members
.get("inputRequests")
.and_then(serde_json::Value::as_object)
{
for request in requests.values() {
let Some(capability) = request
.get("method")
.and_then(serde_json::Value::as_str)
.and_then(capability_for)
else {
continue;
};
let feature = match capability {
"sampling" => Feature::Sampling,
"elicitation" => Feature::Elicitation,
_ => Feature::Roots,
};
if !ctx.peer().allows(feature) && !required.contains(&capability) {
required.push(capability);
}
}
}
if !required.is_empty() {
return Err(DispatchError::MissingClientCapability { required });
}
Ok(result)
}
async fn route(
&self,
ctx: &RequestContext,
method: &str,
params: &JsonObject,
) -> Result<JsonObject, DispatchError> {
let version = ctx.peer().negotiated();
if !client_method_defined_in(version, method) {
return Err(DispatchError::UnknownMethod);
}
let round;
let ctx = if accepts_input_round(method) && result_envelope_defined_in(version) {
round = ctx.clone().with_input_round(self.input_round(params)?);
&round
} else {
ctx
};
let answered = self.route_defined(ctx, method, params).await?;
Self::check_interim(ctx, method, answered)
}
async fn route_defined(
&self,
ctx: &RequestContext,
method: &str,
params: &JsonObject,
) -> Result<JsonObject, DispatchError> {
match method {
"initialize" => self.initialize(ctx, params),
"server/discover" => Self::discover(ctx),
"ping" => Ok(JsonObject::default()),
"tools/list" => self.list(ctx, PrimitiveKind::Tool, "tools"),
"resources/list" => self.list(ctx, PrimitiveKind::Resource, "resources"),
"resources/templates/list" => {
self.list(ctx, PrimitiveKind::ResourceTemplate, "resourceTemplates")
}
"prompts/list" => self.list(ctx, PrimitiveKind::Prompt, "prompts"),
"tools/call" => self.call_tool(ctx, params).await,
"resources/read" => self.read_resource(ctx, params).await,
"prompts/get" => self.get_prompt(ctx, params).await,
"logging/setLevel" => Self::set_log_level(ctx, params),
"completion/complete" => Self::complete(ctx, params),
"resources/subscribe" => self.subscribe(ctx, params, SubscribeOp::Subscribe),
"resources/unsubscribe" => self.subscribe(ctx, params, SubscribeOp::Unsubscribe),
_ => Err(DispatchError::UnknownMethod),
}
}
fn discover(ctx: &RequestContext) -> Result<JsonObject, DispatchError> {
if !discover_defined_in(ctx.peer().negotiated()) {
return Err(DispatchError::UnknownMethod);
}
let result = serde_json::json!({
"resultType": "complete",
"supportedVersions": SupportedVersions::all()
.map_err(|_| DispatchError::UnnegotiableVersion)?
.into_iter()
.rev()
.map(|version| version.as_wire().into_owned())
.collect::<Vec<_>>(),
"ttlMs": 0,
"cacheScope": "private",
"capabilities": server_capabilities_json(ctx.peer().profile()),
});
JsonObject::from_value(result).map_err(|_| DispatchError::MalformedParams)
}
fn subscribe(
&self,
ctx: &RequestContext,
params: &JsonObject,
op: SubscribeOp,
) -> Result<JsonObject, DispatchError> {
Self::require(ctx, Feature::ResourcesSubscribe)?;
let uri = string_param(params, "uri")?;
if self.server.reader(&uri).is_none() {
return Err(DispatchError::UnknownResource);
}
let handle = self.server.handle();
match op {
SubscribeOp::Subscribe => handle.watch_resource(ctx.peer().sink(), &uri, None),
SubscribeOp::Unsubscribe => handle.unwatch_resource(ctx.peer().sink(), &uri),
}
Ok(JsonObject::default())
}
async fn read_resource(
&self,
ctx: &RequestContext,
params: &JsonObject,
) -> Result<JsonObject, DispatchError> {
Self::require(ctx, Feature::Resources)?;
let uri = string_param(params, "uri")?;
let reader = self
.server
.reader(&uri)
.ok_or(DispatchError::UnknownResource)?;
Ok(stamp_result(
reader(ctx.clone(), uri).await?,
Cacheable::Yes,
ctx.peer().negotiated(),
))
}
async fn get_prompt(
&self,
ctx: &RequestContext,
params: &JsonObject,
) -> Result<JsonObject, DispatchError> {
Self::require(ctx, Feature::Prompts)?;
let name = string_param(params, "name")?;
let arguments = match parsed_param(params, "arguments")? {
None | Some(serde_json::Value::Null) => JsonObject::default(),
Some(arguments) => {
JsonObject::from_value(arguments).map_err(|_| DispatchError::MalformedParams)?
}
};
let getter = self
.server
.getter(&name)
.ok_or(DispatchError::UnknownPrompt)?;
getter(ctx.clone(), arguments).await
}
fn set_log_level(
ctx: &RequestContext,
params: &JsonObject,
) -> Result<JsonObject, DispatchError> {
Self::require(ctx, Feature::Logging)?;
let level = string_param(params, "level")?;
let level = level
.parse::<LogLevel>()
.map_err(|_| DispatchError::MalformedParams)?;
ctx.peer().log_gate().set(level);
Ok(JsonObject::default())
}
fn complete(ctx: &RequestContext, _params: &JsonObject) -> Result<JsonObject, DispatchError> {
Self::require(ctx, Feature::Completions)?;
JsonObject::new(r#"{"completion":{"values":[]}}"#)
.map_err(|_| DispatchError::MalformedParams)
}
fn initialize(
&self,
ctx: &RequestContext,
params: &JsonObject,
) -> Result<JsonObject, DispatchError> {
let (version, profile) = negotiate(ctx, params)?;
self.initialize_result(version, profile)
}
fn list(
&self,
ctx: &RequestContext,
kind: PrimitiveKind,
key: &str,
) -> Result<JsonObject, DispatchError> {
Self::require(ctx, kind.feature())?;
let listing =
self.server
.registry()
.list_visible(ctx.peer().auth(), &ctx.peer().profile(), kind);
Ok(stamp_result(
wrap_listing(key, &listing),
Cacheable::Yes,
ctx.peer().negotiated(),
))
}
async fn call_tool(
&self,
ctx: &RequestContext,
params: &JsonObject,
) -> Result<JsonObject, DispatchError> {
Self::require(ctx, Feature::Tools)?;
let (name, arguments) = split_call_params(params)?;
let key = IdempotencyKey::for_method_call(
&ctx.peer().session(),
ctx.request_id(),
"tools/call",
&name,
);
let token = match self.server.idempotency().claim(&key)? {
Claim::Completed(recorded) => return Ok(recorded),
Claim::InFlight => {
return Err(DispatchError::DuplicateInFlight(
ctx.request_id().to_string(),
));
}
Claim::Claimed { token } => token,
};
let outcome = self.server.call(ctx, &name, arguments).await;
match outcome {
Ok(result) => {
self.server.idempotency().complete(&token, result.clone())?;
Ok(result)
}
Err(error) => {
self.server.idempotency().abandon(&token)?;
Err(error)
}
}
}
fn require(ctx: &RequestContext, feature: Feature) -> Result<(), DispatchError> {
if ctx.peer().allows(feature) {
return Ok(());
}
Err(DispatchError::NotCallable)
}
}
fn split_call_params(params: &JsonObject) -> Result<(String, JsonObject), DispatchError> {
let parsed: serde_json::Value =
serde_json::from_str(params.as_str()).map_err(|_| DispatchError::MalformedParams)?;
let object = parsed.as_object().ok_or(DispatchError::MalformedParams)?;
let name = object
.get("name")
.and_then(serde_json::Value::as_str)
.ok_or(DispatchError::MalformedParams)?
.to_string();
let arguments = match object.get("arguments") {
None | Some(serde_json::Value::Null) => JsonObject::default(),
Some(arguments) => {
JsonObject::from_value(arguments.clone()).map_err(|_| DispatchError::MalformedParams)?
}
};
Ok((name, arguments))
}
fn string_param(params: &JsonObject, key: &str) -> Result<String, DispatchError> {
parsed_param(params, key)?
.and_then(|value| value.as_str().map(str::to_string))
.ok_or(DispatchError::MalformedParams)
}
fn parsed_param(
params: &JsonObject,
key: &str,
) -> Result<Option<serde_json::Value>, DispatchError> {
let parsed: serde_json::Value =
serde_json::from_str(params.as_str()).map_err(|_| DispatchError::MalformedParams)?;
let object = parsed.as_object().ok_or(DispatchError::MalformedParams)?;
Ok(object.get(key).cloned())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Cacheable {
Yes,
No,
}
fn stamp_result(result: JsonObject, cacheable: Cacheable, version: ProtocolVersion) -> JsonObject {
if !result_envelope_defined_in(version) {
return result;
}
let mut merged: serde_json::Map<String, serde_json::Value> =
match serde_json::from_str(result.as_str()) {
Ok(serde_json::Value::Object(members)) => members,
_ => return result,
};
merged
.entry("resultType")
.or_insert_with(|| serde_json::Value::from("complete"));
if cacheable == Cacheable::Yes {
merged.insert("ttlMs".to_owned(), serde_json::Value::from(0));
merged.insert("cacheScope".to_owned(), serde_json::Value::from("private"));
}
JsonObject::from_value(serde_json::Value::Object(merged)).unwrap_or(result)
}
fn accepts_input_round(method: &str) -> bool {
matches!(method, "tools/call" | "prompts/get" | "resources/read")
}
fn without_nulls(mut value: serde_json::Value) -> serde_json::Value {
if let Some(members) = value.as_object_mut() {
members.retain(|_, member| !member.is_null());
}
value
}
#[must_use]
pub const fn result_envelope_defined_in(version: ProtocolVersion) -> bool {
matches!(
version,
ProtocolVersion::V2026_07_28 | ProtocolVersion::Unknown(_)
)
}
fn wrap_listing(key: &str, listing: &Listing) -> JsonObject {
let entries: Vec<serde_json::Value> = listing
.visible()
.iter()
.map(|entry| without_nulls(listing_entry(entry)))
.collect();
JsonObject::from_value(serde_json::json!({ key: entries })).unwrap_or_default()
}
fn listing_entry(registered: &Registered) -> serde_json::Value {
match registered.primitive() {
Primitive::Tool(tool) => {
let mut entry = serde_json::json!({
"name": tool.name.as_str(),
"description": tool.description,
});
if let (serde_json::Value::Object(map), Ok(schema)) = (
&mut entry,
serde_json::from_str::<serde_json::Value>(tool.input_schema.as_str()),
) {
map.insert("inputSchema".to_string(), schema);
}
entry
}
Primitive::Resource(resource) => serde_json::json!({
"uri": resource.uri.as_str(),
"name": resource.name,
"description": resource.description,
"mimeType": resource.mime_type,
}),
Primitive::ResourceTemplate(template) => serde_json::json!({
"uriTemplate": template.uri_template.as_str(),
"name": template.name,
"description": template.description,
"mimeType": template.mime_type,
}),
Primitive::Prompt(prompt) => serde_json::json!({
"name": prompt.name.as_str(),
"description": prompt.description,
"arguments": prompt.arguments.iter().map(prompt_argument).collect::<Vec<_>>(),
}),
}
}
fn prompt_argument(argument: &PromptArgument) -> serde_json::Value {
without_nulls(serde_json::json!({
"name": argument.name,
"description": argument.description,
"required": argument.required,
}))
}
fn negotiate(
ctx: &RequestContext,
params: &JsonObject,
) -> Result<(ProtocolVersion, FeatureProfile), DispatchError> {
let transport = ctx.peer().transport();
let supported = supported_on(transport)?;
let offer = read_offer(params).map_err(|_| DispatchError::MalformedInitialize)?;
let version = negotiate_server(offer.version, &supported).version();
let profile = resolve(Negotiation {
version,
client_caps: offer.capabilities,
server_caps: CapabilitySet::all_for(version),
transport,
});
Ok((version, profile))
}
fn supported_on(transport: TransportKind) -> Result<SupportedVersions, DispatchError> {
let all = SupportedVersions::all().map_err(|_| DispatchError::UnnegotiableVersion)?;
if transport != TransportKind::LegacySse {
return Ok(all);
}
let defining: Vec<ProtocolVersion> = all
.as_slice()
.iter()
.copied()
.filter(|version| transport_allowed(*version, transport))
.collect();
SupportedVersions::from_slice(&defining).map_err(|_| DispatchError::UnnegotiableVersion)
}
#[cfg(test)]
mod tests {
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::{DispatchError, Dispatcher};
use crate::application::server::context::{
CallbackExchange, ExchangeError, PeerSession, RequestContext,
};
use crate::application::server::{Server, ToolHandler};
use crate::application::session::{FrameSink, SinkError};
use crate::domain::feature::{Negotiation, resolve};
use crate::domain::ir::{JsonObject, JsonObjectError};
use crate::domain::registry::{AuthContext, Primitive, Registered, Scope, ScopeSet, Tool};
use crate::domain::session::{CancellationToken, SessionId};
use crate::domain::{Capability, CapabilitySet, ProtocolVersion, RequestId, TransportKind};
#[derive(Debug)]
enum Failure {
Message(String),
Fixture(JsonObjectError),
Result(serde_json::Error),
}
impl core::fmt::Display for Failure {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Message(text) => f.write_str(text),
Self::Fixture(error) => write!(f, "bad fixture: {error}"),
Self::Result(error) => write!(f, "unreadable result: {error}"),
}
}
}
impl std::error::Error for Failure {}
impl From<String> for Failure {
fn from(text: String) -> Self {
Self::Message(text)
}
}
impl From<&str> for Failure {
fn from(text: &str) -> Self {
Self::Message(text.to_string())
}
}
impl From<JsonObjectError> for Failure {
fn from(error: JsonObjectError) -> Self {
Self::Fixture(error)
}
}
impl From<DispatchError> for Failure {
fn from(error: DispatchError) -> Self {
Self::Message(error.to_string())
}
}
impl From<serde_json::Error> for Failure {
fn from(error: serde_json::Error) -> Self {
Self::Result(error)
}
}
type TestError = Failure;
fn fail(message: impl Into<String>) -> TestError {
Failure::Message(message.into())
}
struct NoExchange;
impl CallbackExchange for NoExchange {
fn exchange(
&self,
method: &str,
_params: &JsonObject,
) -> impl std::future::Future<Output = Result<JsonObject, ExchangeError>> + Send {
let _ = method;
std::future::ready(Err(ExchangeError::new("no callbacks here")))
}
}
struct NoSink;
impl FrameSink for NoSink {
fn send(
&self,
_frame: &str,
) -> impl std::future::Future<Output = Result<(), SinkError>> + Send {
std::future::ready(Ok(()))
}
}
fn answering(answer: &'static str) -> ToolHandler {
let answer: JsonObject = JsonObject::new(answer).unwrap_or_default();
Arc::new(move |_ctx, _args| {
let answer = answer.clone();
Box::pin(async move { answer })
})
}
fn dispatcher() -> Result<Dispatcher, TestError> {
let restricted = Registered::from(Primitive::from(Tool::new(
"secret",
r#"{"type":"object"}"#,
)?))
.requiring_scope(Scope::Read);
Ok(Dispatcher::new(Arc::new(
Server::builder("test")
.tool(Tool::new("echo", r#"{"type":"object"}"#)?, answering("{}"))
.restricted_tool(restricted, answering("{}"))
.build(),
)))
}
fn peer(version: ProtocolVersion, scopes: ScopeSet, server_caps: CapabilitySet) -> PeerSession {
PeerSession::new(
version,
resolve(Negotiation {
version,
client_caps: CapabilitySet::empty(),
server_caps,
transport: TransportKind::StreamableHttp,
}),
SessionId::new(1),
TransportKind::Stdio,
Arc::new(NoExchange),
Arc::new(NoSink),
)
.with_auth(AuthContext::new("tester", scopes))
}
fn ctx(peer: &PeerSession) -> RequestContext {
RequestContext::for_request(peer, RequestId::text("r1"), CancellationToken::new(), None)
}
fn caps(version: ProtocolVersion) -> CapabilitySet {
CapabilitySet::all_for(version)
}
fn peer_on(
version: ProtocolVersion,
scopes: ScopeSet,
server_caps: CapabilitySet,
id: u64,
) -> PeerSession {
PeerSession::new(
version,
resolve(Negotiation {
version,
client_caps: CapabilitySet::empty(),
server_caps,
transport: TransportKind::StreamableHttp,
}),
SessionId::new(id),
TransportKind::Stdio,
Arc::new(NoExchange),
Arc::new(NoSink),
)
.with_auth(AuthContext::new("tester", scopes))
}
fn charge_tool() -> Result<Tool, TestError> {
Ok(Tool::new("charge", r#"{"type":"object"}"#)?)
}
fn init_params(version: &str) -> Result<JsonObject, TestError> {
Ok(JsonObject::new(
r#"{"protocolVersion":""#.to_string()
+ version
+ r#"","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}"#,
)?)
}
fn init_params_with(version: &str, capabilities: &str) -> Result<JsonObject, TestError> {
Ok(JsonObject::new(
r#"{"protocolVersion":""#.to_string()
+ version
+ r#"","capabilities":"#
+ capabilities
+ r#","clientInfo":{"name":"probe","version":"0"}}"#,
)?)
}
fn parsed(object: &JsonObject) -> Result<serde_json::Value, TestError> {
Ok(serde_json::from_str(object.as_str())?)
}
#[tokio::test]
async fn advertised_capabilities_follow_the_negotiated_revision() -> Result<(), TestError> {
let session_version = ProtocolVersion::V2025_06_18;
let peer = peer(
session_version,
ScopeSet::all(),
CapabilitySet::all_for(session_version),
);
let result = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2024-11-05")?)
.await
.map_err(|error| Failure::Message(format!("the handshake refused: {error}")))?;
let capabilities = parsed(&result)?
.get("capabilities")
.cloned()
.ok_or("the handshake advertised no capabilities")?;
assert!(
capabilities.get("elicitation").is_none(),
"2024-11-05 has no elicitation capability: {capabilities}"
);
Ok(())
}
#[tokio::test]
async fn the_client_capabilities_are_read_not_assumed() -> Result<(), TestError> {
let session_version = ProtocolVersion::V2025_11_25;
let peer = peer(
session_version,
ScopeSet::all(),
CapabilitySet::all_for(session_version),
);
dispatcher()?
.dispatch(
&ctx(&peer),
"initialize",
&init_params_with("2025-11-25", r#"{"roots":{"listChanged":true}}"#)?,
)
.await
.map_err(|error| Failure::Message(format!("the handshake refused: {error}")))?;
Ok(())
}
#[tokio::test]
async fn initialize_reports_the_negotiated_revision() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let result = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2024-11-05")?)
.await
.map_err(|error| Failure::Message(format!("the handshake refused: {error}")))?;
let reported = parsed(&result)?
.get("protocolVersion")
.and_then(serde_json::Value::as_str)
.ok_or("the handshake did not report a revision")?
.to_string();
assert_eq!(reported, "2024-11-05");
Ok(())
}
#[tokio::test]
async fn a_client_newer_than_this_build_gets_the_newest_handshake() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let result = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2099-01-01")?)
.await
.map_err(|error| Failure::Message(format!("a newer client was refused: {error}")))?;
let reported = parsed(&result)?
.get("protocolVersion")
.and_then(serde_json::Value::as_str)
.ok_or("the handshake did not report a revision")?
.to_string();
assert_eq!(
reported,
ProtocolVersion::V2025_11_25.to_string(),
"the newest revision this build can actually complete a handshake on"
);
Ok(())
}
#[tokio::test]
async fn the_handshake_advertises_only_server_capabilities() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let result = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2025-06-18")?)
.await
.map_err(|error| Failure::Message(format!("the handshake refused: {error}")))?;
let capabilities = parsed(&result)?
.get("capabilities")
.cloned()
.ok_or("the handshake advertised no capabilities")?;
for forbidden in ["roots", "sampling", "elicitation"] {
assert!(
capabilities.get(forbidden).is_none(),
"a client-side capability reached the server advertisement: {forbidden}"
);
}
assert!(
capabilities.get("tools").is_some(),
"a session that negotiated tools did not advertise them"
);
Ok(())
}
#[tokio::test]
async fn initialize_answers_on_a_session_with_no_features() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::empty());
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2025-06-18")?)
.await;
assert!(outcome.is_ok(), "the handshake was gated off: {outcome:?}");
Ok(())
}
#[tokio::test]
async fn a_malformed_handshake_is_refused_whatever_is_missing() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let bodies = [
"{}",
r#"{"capabilities":{},"clientInfo":{"name":"p","version":"0"}}"#,
r#"{"protocolVersion":"2025-06-18","clientInfo":{"name":"p","version":"0"}}"#,
r#"{"protocolVersion":"2025-06-18","capabilities":{}}"#,
r#"{"protocolVersion":"2025-06-18","capabilities":[],"clientInfo":{"name":"p"}}"#,
r#"{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{}}"#,
r#"{"protocolVersion":"nonsense","capabilities":{},"clientInfo":{"name":"p"}}"#,
];
for body in bodies {
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &JsonObject::new(body)?)
.await;
assert_eq!(
outcome,
Err(DispatchError::MalformedInitialize),
"this body was accepted: {body}"
);
}
Ok(())
}
#[tokio::test]
async fn a_session_without_the_handshake_refuses_the_method() -> Result<(), TestError> {
let version = ProtocolVersion::V2026_07_28;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "initialize", &init_params("2026-07-28")?)
.await;
assert_eq!(outcome, Err(DispatchError::UnknownMethod));
Ok(())
}
#[tokio::test]
async fn an_undefined_method_is_refused() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "tools/frobnicate", &JsonObject::default())
.await;
assert_eq!(outcome, Err(DispatchError::UnknownMethod));
Ok(())
}
#[tokio::test]
async fn a_call_with_no_name_is_malformed_rather_than_unknown() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let params = JsonObject::new(r#"{"arguments":{}}"#)?;
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await;
assert_eq!(outcome, Err(DispatchError::MalformedParams));
Ok(())
}
#[tokio::test]
async fn a_call_without_arguments_still_reaches_the_handler() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let params = JsonObject::new(r#"{"name":"echo"}"#)?;
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await;
assert_eq!(
outcome.map(|answer| answer.as_str().to_string()),
Ok("{}".to_string())
);
Ok(())
}
#[tokio::test]
async fn a_method_the_session_lacks_is_refused() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::empty());
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "tools/list", &JsonObject::default())
.await;
assert_eq!(outcome, Err(DispatchError::NotCallable));
Ok(())
}
#[tokio::test]
async fn discover_answers_only_where_sep_2575_defines_it() -> Result<(), TestError> {
let modern = ProtocolVersion::V2026_07_28;
let answered = dispatcher()?
.dispatch(
&ctx(&peer(modern, ScopeSet::empty(), caps(modern))),
"server/discover",
&JsonObject::default(),
)
.await
.map_err(|error| Failure::from(error.to_string()))?;
let parsed: serde_json::Value = serde_json::from_str(answered.as_str())
.map_err(|error| Failure::from(error.to_string()))?;
for member in [
"resultType",
"supportedVersions",
"ttlMs",
"cacheScope",
"capabilities",
] {
assert!(
parsed.get(member).is_some(),
"DiscoverResult is missing required member `{member}`",
);
}
assert_eq!(
parsed.get("resultType").and_then(serde_json::Value::as_str),
Some("complete")
);
let older = ProtocolVersion::V2025_11_25;
let refused = dispatcher()?
.dispatch(
&ctx(&peer(older, ScopeSet::empty(), caps(older))),
"server/discover",
&JsonObject::default(),
)
.await;
assert_eq!(
refused.map(|answer| answer.as_str().to_string()),
Err(DispatchError::UnknownMethod),
"SEP-2575 defines server/discover only from 2026-07-28",
);
Ok(())
}
#[tokio::test]
async fn discover_advertises_the_profile_the_server_honours() -> Result<(), TestError> {
let version = ProtocolVersion::V2026_07_28;
let server_caps = CapabilitySet::empty().with(Capability::Tools);
let peer = peer(version, ScopeSet::empty(), server_caps);
let answered = dispatcher()?
.dispatch(&ctx(&peer), "server/discover", &JsonObject::default())
.await
.map_err(|error| Failure::from(error.to_string()))?;
let parsed: serde_json::Value = serde_json::from_str(answered.as_str())
.map_err(|error| Failure::from(error.to_string()))?;
let capabilities = parsed
.get("capabilities")
.ok_or("the result carried no capabilities")?;
assert!(
capabilities.get("tools").is_some(),
"tools was negotiated and not advertised: {capabilities}",
);
assert!(
capabilities.get("prompts").is_none(),
"prompts was not negotiated and was advertised anyway: {capabilities}",
);
Ok(())
}
#[tokio::test]
async fn the_result_envelope_is_stamped_only_from_2026_07_28() -> Result<(), TestError> {
let modern = ProtocolVersion::V2026_07_28;
let listed = dispatcher()?
.dispatch(
&ctx(&peer(modern, ScopeSet::empty(), caps(modern))),
"tools/list",
&JsonObject::default(),
)
.await
.map_err(|error| Failure::from(error.to_string()))?;
let listed: serde_json::Value = serde_json::from_str(listed.as_str())
.map_err(|error| Failure::from(error.to_string()))?;
for member in ["resultType", "ttlMs", "cacheScope"] {
assert!(
listed.get(member).is_some(),
"a cacheable result is missing `{member}`: {listed}",
);
}
let older = ProtocolVersion::V2025_11_25;
let legacy = dispatcher()?
.dispatch(
&ctx(&peer(older, ScopeSet::empty(), caps(older))),
"tools/list",
&JsonObject::default(),
)
.await
.map_err(|error| Failure::from(error.to_string()))?;
let legacy: serde_json::Value = serde_json::from_str(legacy.as_str())
.map_err(|error| Failure::from(error.to_string()))?;
for member in ["resultType", "ttlMs", "cacheScope"] {
assert!(
legacy.get(member).is_none(),
"`{member}` is not defined before 2026-07-28 and was stamped anyway: {legacy}",
);
}
Ok(())
}
#[tokio::test]
async fn ping_answers_with_no_features_at_all() -> Result<(), TestError> {
let version = ProtocolVersion::V2024_11_05;
let peer = peer(version, ScopeSet::empty(), CapabilitySet::empty());
let outcome = dispatcher()?
.dispatch(&ctx(&peer), "ping", &JsonObject::default())
.await;
assert_eq!(
outcome.map(|answer| answer.as_str().to_string()),
Ok("{}".to_string())
);
Ok(())
}
#[tokio::test]
async fn the_listing_hides_a_tool_the_caller_may_not_see() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::empty(), CapabilitySet::all_for(version));
let listed = dispatcher()?
.dispatch(&ctx(&peer), "tools/list", &JsonObject::default())
.await
.map_err(|error| Failure::from(error.to_string()))?;
let parsed: serde_json::Value = serde_json::from_str(listed.as_str())
.map_err(|error| Failure::from(error.to_string()))?;
let names: Vec<&str> = parsed
.get("tools")
.and_then(serde_json::Value::as_array)
.ok_or("the listing has no tools array")?
.iter()
.filter_map(|tool| tool.get("name"))
.filter_map(serde_json::Value::as_str)
.collect();
assert_eq!(names, ["echo"]);
Ok(())
}
#[tokio::test]
async fn the_listing_shows_the_tool_to_a_caller_holding_its_scope() -> Result<(), TestError> {
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), CapabilitySet::all_for(version));
let listed = dispatcher()?
.dispatch(&ctx(&peer), "tools/list", &JsonObject::default())
.await
.map_err(|error| Failure::from(error.to_string()))?;
assert!(
listed.as_str().contains("secret"),
"an unrestricted caller could not see the restricted tool: {}",
listed.as_str()
);
Ok(())
}
fn counting(count: &Arc<AtomicUsize>, answer: &'static str) -> Result<Dispatcher, TestError> {
let answer = JsonObject::new(answer).unwrap_or_default();
let seen = Arc::clone(count);
let handler: ToolHandler = Arc::new(move |_ctx, _args| {
let count = Arc::clone(&seen);
let answer = answer.clone();
Box::pin(async move {
count.fetch_add(1, Ordering::SeqCst);
answer
})
});
Ok(Dispatcher::new(Arc::new(
Server::builder("test")
.tool(charge_tool()?, handler)
.build(),
)))
}
#[tokio::test]
async fn a_retry_after_a_dropped_connection_runs_the_tool_once() -> Result<(), TestError> {
let count = Arc::new(AtomicUsize::new(0));
let dispatcher = counting(&count, r#"{"charged":true}"#)?;
let peer = peer(
ProtocolVersion::V2025_06_18,
ScopeSet::all(),
caps(ProtocolVersion::V2025_06_18),
);
let params = JsonObject::new(r#"{"name":"charge"}"#)?;
let first = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await?;
let second = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await?;
if count.load(Ordering::SeqCst) != 1 {
return Err(fail(format!(
"the tool ran {} times",
count.load(Ordering::SeqCst)
)));
}
if first != second {
return Err(fail("the retry was not answered from the recording"));
}
Ok(())
}
#[tokio::test]
async fn the_same_request_id_on_another_session_is_not_a_retry() -> Result<(), TestError> {
let count = Arc::new(AtomicUsize::new(0));
let dispatcher = counting(&count, r#"{"charged":true}"#)?;
let version = ProtocolVersion::V2025_06_18;
let params = JsonObject::new(r#"{"name":"charge"}"#)?;
let first_peer = peer(version, ScopeSet::all(), caps(version));
let second_peer = peer_on(version, ScopeSet::all(), caps(version), 2);
let _ = dispatcher
.dispatch(&ctx(&first_peer), "tools/call", ¶ms)
.await?;
let _ = dispatcher
.dispatch(&ctx(&second_peer), "tools/call", ¶ms)
.await?;
if count.load(Ordering::SeqCst) != 2 {
return Err(fail(format!(
"two sessions ran the tool {} time(s); the second was treated as a retry",
count.load(Ordering::SeqCst)
)));
}
Ok(())
}
#[derive(Debug, Default)]
struct Journal {
log: std::sync::Mutex<Vec<&'static str>>,
}
impl Journal {
fn entries(&self) -> Vec<&'static str> {
self.log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
}
fn room_for(keys: usize) -> NonZeroUsize {
NonZeroUsize::new(keys).unwrap_or(NonZeroUsize::MIN)
}
struct Recording {
inner: crate::adapters::secondary::idempotency::InMemoryIdempotencyStore<JsonObject>,
log: Arc<Journal>,
}
impl crate::application::idempotency::IdempotencyStore for Recording {
type Result = JsonObject;
fn claim(
&self,
key: &crate::domain::idempotency::IdempotencyKey,
) -> Result<
crate::domain::idempotency::Claim<Self::Result>,
crate::domain::idempotency::IdempotencyError,
> {
self.log
.log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push("claim");
self.inner.claim(key)
}
fn complete(
&self,
token: &crate::domain::idempotency::ClaimToken,
result: Self::Result,
) -> Result<(), crate::domain::idempotency::IdempotencyError> {
self.log
.log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push("complete");
self.inner.complete(token, result)
}
fn abandon(
&self,
token: &crate::domain::idempotency::ClaimToken,
) -> Result<(), crate::domain::idempotency::IdempotencyError> {
self.log
.log
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push("abandon");
self.inner.abandon(token)
}
}
#[tokio::test]
async fn a_successful_call_claims_then_records() -> Result<(), TestError> {
let journal = Arc::new(Journal::default());
let store = Arc::new(Recording {
inner: crate::adapters::secondary::idempotency::InMemoryIdempotencyStore::with_capacity(
room_for(8),
),
log: Arc::clone(&journal),
});
let count = Arc::new(AtomicUsize::new(0));
let seen = Arc::clone(&count);
let handler: ToolHandler = Arc::new(move |_ctx, _args| {
let count = Arc::clone(&seen);
Box::pin(async move {
count.fetch_add(1, Ordering::SeqCst);
JsonObject::new(r#"{"charged":true}"#).unwrap_or_default()
})
});
let dispatcher = Dispatcher::new(Arc::new(
Server::builder("test")
.tool(charge_tool()?, handler)
.idempotency(store)
.build(),
));
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::all(), caps(version));
let params = JsonObject::new(r#"{"name":"charge"}"#)?;
let _ = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await?;
let _ = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await?;
if journal.entries() != vec!["claim", "complete", "claim"] {
return Err(fail(format!("the store saw {:?}", journal.entries())));
}
if count.load(Ordering::SeqCst) != 1 {
return Err(fail(format!(
"the tool ran {} times",
count.load(Ordering::SeqCst)
)));
}
Ok(())
}
#[tokio::test]
async fn a_refused_call_releases_the_key() -> Result<(), TestError> {
let journal = Arc::new(Journal::default());
let store = Arc::new(Recording {
inner: crate::adapters::secondary::idempotency::InMemoryIdempotencyStore::with_capacity(
room_for(8),
),
log: Arc::clone(&journal),
});
let secret = Registered::from(Primitive::from(Tool::new(
"secret",
r#"{"type":"object"}"#,
)?))
.requiring_scope(Scope::Read);
let handler: ToolHandler = Arc::new(|_ctx, _args| {
Box::pin(async { JsonObject::new(r#"{"leaked":true}"#).unwrap_or_default() })
});
let dispatcher = Dispatcher::new(Arc::new(
Server::builder("test")
.restricted_tool(secret, handler)
.idempotency(store)
.build(),
));
let version = ProtocolVersion::V2025_06_18;
let peer = peer(version, ScopeSet::empty(), caps(version));
let params = JsonObject::new(r#"{"name":"secret"}"#)?;
for _ in 0..2 {
let outcome = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await;
if outcome != Err(DispatchError::UnknownTool) {
return Err(fail(format!("a hidden tool answered {outcome:?}")));
}
}
if journal.entries() != vec!["claim", "abandon", "claim", "abandon"] {
return Err(fail(format!("the store saw {:?}", journal.entries())));
}
Ok(())
}
#[tokio::test]
async fn a_duplicate_while_the_first_is_running_is_refused() -> Result<(), TestError> {
let count = Arc::new(AtomicUsize::new(0));
let (gate_tx, gate_rx) = tokio::sync::oneshot::channel::<()>();
let gate_rx = Arc::new(tokio::sync::Mutex::new(Some(gate_rx)));
let seen = Arc::clone(&count);
let handler: ToolHandler = Arc::new(move |_ctx, _args| {
let count = Arc::clone(&seen);
let gate_rx = Arc::clone(&gate_rx);
Box::pin(async move {
let waiting = { gate_rx.lock().await.take() };
if let Some(receiver) = waiting {
let _ = receiver.await;
}
count.fetch_add(1, Ordering::SeqCst);
JsonObject::new(r#"{"charged":true}"#).unwrap_or_default()
})
});
let dispatcher = Dispatcher::new(Arc::new(
Server::builder("test")
.tool(charge_tool()?, handler)
.build(),
));
let peer = peer(
ProtocolVersion::V2025_06_18,
ScopeSet::all(),
caps(ProtocolVersion::V2025_06_18),
);
let params = JsonObject::new(r#"{"name":"charge"}"#)?;
let running = tokio::spawn({
let dispatcher = Dispatcher::new(Arc::clone(dispatcher.server()));
let peer = peer_on(
ProtocolVersion::V2025_06_18,
ScopeSet::all(),
caps(ProtocolVersion::V2025_06_18),
1,
);
let params = JsonObject::new(r#"{"name":"charge"}"#)?;
async move {
dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await
}
});
tokio::task::yield_now().await;
let duplicate = dispatcher
.dispatch(&ctx(&peer), "tools/call", ¶ms)
.await;
let _ = gate_tx.send(());
let _ = running.await;
match duplicate {
Err(DispatchError::DuplicateInFlight(_)) => {}
other => return Err(fail(format!("the duplicate answered {other:?}"))),
}
if count.load(Ordering::SeqCst) != 1 {
return Err(fail(format!(
"the tool ran {} time(s) while a duplicate was refused",
count.load(Ordering::SeqCst)
)));
}
Ok(())
}
}