feat!: add MRTR behavior support (SEP-2322) (#929)

* feat!: add MRTR behavior support

* feat: harden SEP-2322 MRTR support

* ci: diff public API only on features common to base and head

cargo public-api builds both revisions with the same feature set, so a
feature introduced (or removed) by a PR broke the build of the other
revision. Restrict the all-features diff to features present in both the
base and head revisions.
This commit is contained in:
Dale Seo 2026-07-10 12:02:26 -04:00 • committed by GitHub
commit dd30a70f84
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 2121 additions and 127 deletions

View file

@ -27,6 +27,9 @@ impl<H: ServerHandler> Service<RoleServer> for H {
) -> Result<<RoleServer as ServiceRole>::Resp, McpError> {
// `context` is moved into the dispatch below, so read the negotiated version first.
let protocol_version = context.protocol_version();
let mrtr_supported = protocol_version
.as_ref()
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
let result = match request {
ClientRequest::InitializeRequest(request) => self
.initialize(request.params, context)
@ -46,7 +49,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
ClientRequest::GetPromptRequest(request) => self
.get_prompt(request.params, context)
.await
.map(ServerResult::GetPromptResult),
.map(ServerResult::from),
ClientRequest::ListPromptsRequest(request) => self
.list_prompts(request.params, context)
.await
@ -62,7 +65,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
ClientRequest::ReadResourceRequest(request) => self
.read_resource(request.params, context)
.await
.map(ServerResult::ReadResourceResult),
.map(ServerResult::from),
ClientRequest::SubscribeRequest(request) => self
.subscribe(request.params, context)
.await
@ -105,7 +108,7 @@ impl<H: ServerHandler> Service<RoleServer> for H {
} else {
self.call_tool(request.params, context)
.await
.map(ServerResult::CallToolResult)
.map(ServerResult::from)
}
}
ClientRequest::ListToolsRequest(request) => self
@ -133,6 +136,17 @@ impl<H: ServerHandler> Service<RoleServer> for H {
.await
.map(ServerResult::CancelTaskResult),
};
let result = result.and_then(|result| {
if matches!(result, ServerResult::InputRequiredResult(_)) && !mrtr_supported {
Err(McpError::invalid_request(
"InputRequiredResult requires negotiated protocol version 2026-07-28 or newer",
None,
))
} else {
Ok(result)
}
});
// SEP-2164: peers negotiating 2026-07-28+ get the standard INVALID_PARAMS code for
// resource-not-found; older peers keep RESOURCE_NOT_FOUND. ISO `YYYY-MM-DD` versions
// compare lexically the same as chronologically.
@ -229,7 +243,7 @@ macro_rules! server_handler_methods {
&self,
request: GetPromptRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<GetPromptResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<GetPromptResponse, McpError>> + MaybeSendFuture + '_ {
std::future::ready(Err(McpError::method_not_found::<GetPromptRequestMethod>()))
}
fn list_prompts(
@ -259,7 +273,7 @@ macro_rules! server_handler_methods {
&self,
request: ReadResourceRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ReadResourceResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<ReadResourceResponse, McpError>> + MaybeSendFuture + '_ {
std::future::ready(Err(
McpError::method_not_found::<ReadResourceRequestMethod>(),
))
@ -312,7 +326,7 @@ macro_rules! server_handler_methods {
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CallToolResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<CallToolResponse, McpError>> + MaybeSendFuture + '_ {
std::future::ready(Err(McpError::method_not_found::<CallToolRequestMethod>()))
}
fn list_tools(
@ -485,7 +499,7 @@ macro_rules! impl_server_handler_for_wrapper {
&self,
request: GetPromptRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<GetPromptResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<GetPromptResponse, McpError>> + MaybeSendFuture + '_ {
(**self).get_prompt(request, context)
}
@ -518,7 +532,7 @@ macro_rules! impl_server_handler_for_wrapper {
&self,
request: ReadResourceRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<ReadResourceResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<ReadResourceResponse, McpError>> + MaybeSendFuture + '_ {
(**self).read_resource(request, context)
}
@ -542,7 +556,7 @@ macro_rules! impl_server_handler_for_wrapper {
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CallToolResult, McpError>> + MaybeSendFuture + '_ {
) -> impl Future<Output = Result<CallToolResponse, McpError>> + MaybeSendFuture + '_ {
(**self).call_tool(request, context)
}

View file

@ -15,7 +15,7 @@ pub use super::common::{Extension, RequestId};
use crate::{
RoleServer,
handler::server::wrapper::Parameters,
model::{GetPromptResult, PromptMessage},
model::{GetPromptResponse, GetPromptResult, InputRequiredResult, PromptMessage},
service::{MaybeBoxFuture, MaybeSend, MaybeSendFuture, RequestContext},
};
@ -59,12 +59,12 @@ pub trait GetPromptHandler<S, A> {
fn handle(
self,
context: PromptContext<'_, S>,
) -> MaybeBoxFuture<'_, Result<GetPromptResult, crate::ErrorData>>;
) -> MaybeBoxFuture<'_, Result<GetPromptResponse, crate::ErrorData>>;
}
/// Type alias for dynamic prompt handlers
#[cfg(not(feature = "local"))]
pub type DynGetPromptHandler<S> = dyn for<'a> Fn(PromptContext<'a, S>) -> BoxFuture<'a, Result<GetPromptResult, crate::ErrorData>>
pub type DynGetPromptHandler<S> = dyn for<'a> Fn(PromptContext<'a, S>) -> BoxFuture<'a, Result<GetPromptResponse, crate::ErrorData>>
+ Send
+ Sync;
@ -73,7 +73,7 @@ pub type DynGetPromptHandler<S> = dyn for<'a> Fn(
PromptContext<'a, S>,
) -> futures::future::LocalBoxFuture<
'a,
Result<GetPromptResult, crate::ErrorData>,
Result<GetPromptResponse, crate::ErrorData>,
>;
/// Adapter type for async methods that return `Vec<PromptMessage>`
@ -91,28 +91,35 @@ pub struct SyncPromptMethodAdapter<P, R>(PhantomData<fn(P) -> R>);
/// Trait for types that can be converted into GetPromptResult
pub trait IntoGetPromptResult {
fn into_get_prompt_result(self) -> Result<GetPromptResult, crate::ErrorData>;
fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData>;
}
impl IntoGetPromptResult for GetPromptResult {
fn into_get_prompt_result(self) -> Result<GetPromptResult, crate::ErrorData> {
Ok(self)
fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData> {
Ok(self.into())
}
}
impl IntoGetPromptResult for InputRequiredResult {
fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData> {
Ok(self.into())
}
}
impl IntoGetPromptResult for Vec<PromptMessage> {
fn into_get_prompt_result(self) -> Result<GetPromptResult, crate::ErrorData> {
fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData> {
Ok(GetPromptResult {
result_type: Default::default(),
description: None,
messages: self,
meta: None,
})
}
.into())
}
}
impl<T: IntoGetPromptResult> IntoGetPromptResult for Result<T, crate::ErrorData> {
fn into_get_prompt_result(self) -> Result<GetPromptResult, crate::ErrorData> {
fn into_get_prompt_result(self) -> Result<GetPromptResponse, crate::ErrorData> {
self.and_then(|v| v.into_get_prompt_result())
}
}
@ -129,7 +136,7 @@ pin_project_lite::pin_project! {
},
Ready {
#[pin]
result: futures::future::Ready<Result<GetPromptResult, crate::ErrorData>>,
result: futures::future::Ready<Result<GetPromptResponse, crate::ErrorData>>,
}
}
}
@ -139,7 +146,7 @@ where
F: Future<Output = R>,
R: IntoGetPromptResult,
{
type Output = Result<GetPromptResult, crate::ErrorData>;
type Output = Result<GetPromptResponse, crate::ErrorData>;
fn poll(
self: std::pin::Pin<&mut Self>,
@ -216,7 +223,7 @@ macro_rules! impl_prompt_handler_for {
fn handle(
self,
mut context: PromptContext<'_, S>,
) -> MaybeBoxFuture<'_, Result<GetPromptResult, crate::ErrorData>>
) -> MaybeBoxFuture<'_, Result<GetPromptResponse, crate::ErrorData>>
{
$(
let result = $Tn::from_context_part(&mut context);
@ -249,7 +256,7 @@ macro_rules! impl_prompt_handler_for {
fn handle(
self,
mut context: PromptContext<'_, S>,
) -> MaybeBoxFuture<'_, Result<GetPromptResult, crate::ErrorData>>
) -> MaybeBoxFuture<'_, Result<GetPromptResponse, crate::ErrorData>>
{
$(
let result = $Tn::from_context_part(&mut context);
@ -280,7 +287,7 @@ macro_rules! impl_prompt_handler_for {
fn handle(
self,
mut context: PromptContext<'_, S>,
) -> MaybeBoxFuture<'_, Result<GetPromptResult, crate::ErrorData>>
) -> MaybeBoxFuture<'_, Result<GetPromptResponse, crate::ErrorData>>
{
// Extract all parameters before moving into the async block
$(
@ -315,7 +322,7 @@ macro_rules! impl_prompt_handler_for {
fn handle(
self,
mut context: PromptContext<'_, S>,
) -> MaybeBoxFuture<'_, Result<GetPromptResult, crate::ErrorData>>
) -> MaybeBoxFuture<'_, Result<GetPromptResponse, crate::ErrorData>>
{
$(
let result = $Tn::from_context_part(&mut context);

View file

@ -106,7 +106,7 @@ where
context,
);
let result = self.tool_router.call(tool_call_context).await?;
Ok(ServerResult::CallToolResult(result))
Ok(ServerResult::from(result))
} else {
self.service
.handle_request(ClientRequest::CallToolRequest(request), context)
@ -129,7 +129,7 @@ where
context,
);
let result = self.prompt_router.get_prompt(prompt_context).await?;
Ok(ServerResult::GetPromptResult(result))
Ok(ServerResult::from(result))
} else {
self.service
.handle_request(ClientRequest::GetPromptRequest(request), context)
@ -193,7 +193,7 @@ mod tests {
async fn test_router_deferred_notifier_e2e() {
let mut router = Router::new(DummyHandler).with_tool(tool::ToolRoute::new_dyn(
Tool::new("my_tool", "test", Arc::new(Default::default())),
|_ctx| Box::pin(async { Ok(CallToolResult::default()) }),
|_ctx| Box::pin(async { Ok(CallToolResult::default().into()) }),
));
let id_provider: Arc<dyn RequestIdProvider> =

View file

@ -2,7 +2,7 @@ use std::{borrow::Cow, sync::Arc};
use crate::{
handler::server::prompt::{DynGetPromptHandler, GetPromptHandler, PromptContext},
model::{GetPromptResult, Prompt},
model::{GetPromptResponse, Prompt},
service::{MaybeBoxFuture, MaybeSend},
};
@ -50,7 +50,8 @@ impl<S: MaybeSend + 'static> PromptRoute<S> {
where
H: for<'a> Fn(
PromptContext<'a, S>,
) -> MaybeBoxFuture<'a, Result<GetPromptResult, crate::ErrorData>>
)
-> MaybeBoxFuture<'a, Result<GetPromptResponse, crate::ErrorData>>
+ MaybeSend
+ 'static,
{
@ -175,7 +176,7 @@ where
pub async fn get_prompt(
&self,
context: PromptContext<'_, S>,
) -> Result<GetPromptResult, crate::ErrorData> {
) -> Result<GetPromptResponse, crate::ErrorData> {
let item = self.map.get(context.name.as_str()).ok_or_else(|| {
crate::ErrorData::invalid_params(
format!("prompt '{}' not found", context.name),

View file

@ -137,21 +137,19 @@ use crate::{
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext},
tool_name_validation::validate_and_warn_tool_name,
},
model::{CallToolResult, ContentBlock, ErrorCode, Tool, ToolAnnotations},
model::{CallToolResponse, CallToolResult, ContentBlock, ErrorCode, Tool, ToolAnnotations},
service::{MaybeBoxFuture, MaybeSend},
};
const TOOL_ARGUMENT_DESERIALIZATION_ERROR_PREFIX: &str = "failed to deserialize parameters:";
fn into_tool_argument_error(error: crate::ErrorData) -> Result<CallToolResult, crate::ErrorData> {
fn into_tool_argument_error(error: crate::ErrorData) -> Result<CallToolResponse, crate::ErrorData> {
if error.code == ErrorCode::INVALID_PARAMS
&& error
.message
.starts_with(TOOL_ARGUMENT_DESERIALIZATION_ERROR_PREFIX)
{
return Ok(CallToolResult::error(vec![ContentBlock::text(
error.message,
)]));
return Ok(CallToolResult::error(vec![ContentBlock::text(error.message)]).into());
}
Err(error)
@ -200,7 +198,8 @@ impl<S: MaybeSend + 'static> ToolRoute<S> {
where
C: for<'a> Fn(
ToolCallContext<'a, S>,
) -> MaybeBoxFuture<'a, Result<CallToolResult, crate::ErrorData>>
)
-> MaybeBoxFuture<'a, Result<CallToolResponse, crate::ErrorData>>
+ MaybeSend
+ 'static,
{
@ -561,7 +560,7 @@ where
pub async fn call(
&self,
context: ToolCallContext<'_, S>,
) -> Result<CallToolResult, crate::ErrorData> {
) -> Result<crate::model::CallToolResponse, crate::ErrorData> {
let name = context.name();
if self.disabled.contains(name) {
return Err(crate::ErrorData::invalid_params("tool not found", None));
@ -679,6 +678,9 @@ mod tests {
.call(ctx)
.await
.expect("argument validation should be a tool result");
let CallToolResponse::Complete(result) = result else {
panic!("expected complete CallToolResult");
};
assert_eq!(result.is_error, Some(true));
let text = result
@ -696,7 +698,7 @@ mod tests {
let service = DummyService;
let mut router = ToolRouter::new().with_route(ToolRoute::new_dyn(
crate::model::Tool::new("test_tool", "a test tool", Arc::new(Default::default())),
|_ctx| Box::pin(async { Ok(CallToolResult::default()) }),
|_ctx| Box::pin(async { Ok(CallToolResult::default().into()) }),
));
router.disable_route("test_tool");

View file

@ -16,7 +16,10 @@ pub use super::{
use crate::{
RoleServer,
handler::server::wrapper::Parameters,
model::{CallToolRequestParams, CallToolResult, IntoContents, JsonObject},
model::{
CallToolRequestParams, CallToolResponse, CallToolResult, InputRequiredResult, IntoContents,
JsonObject,
},
service::{MaybeBoxFuture, MaybeSend, MaybeSendFuture, RequestContext},
};
@ -77,36 +80,46 @@ impl<S> AsRequestContext for ToolCallContext<'_, S> {
}
pub trait IntoCallToolResult {
fn into_call_tool_result(self) -> Result<CallToolResult, crate::ErrorData>;
fn into_call_tool_result(self) -> Result<CallToolResponse, crate::ErrorData>;
}
impl<T: IntoContents> IntoCallToolResult for T {
fn into_call_tool_result(self) -> Result<CallToolResult, crate::ErrorData> {
Ok(CallToolResult::success(self.into_contents()))
fn into_call_tool_result(self) -> Result<CallToolResponse, crate::ErrorData> {
Ok(CallToolResult::success(self.into_contents()).into())
}
}
impl IntoCallToolResult for CallToolResult {
fn into_call_tool_result(self) -> Result<CallToolResult, crate::ErrorData> {
Ok(self)
fn into_call_tool_result(self) -> Result<CallToolResponse, crate::ErrorData> {
Ok(self.into())
}
}
impl IntoCallToolResult for InputRequiredResult {
fn into_call_tool_result(self) -> Result<CallToolResponse, crate::ErrorData> {
Ok(self.into())
}
}
impl IntoCallToolResult for crate::ErrorData {
fn into_call_tool_result(self) -> Result<CallToolResult, crate::ErrorData> {
fn into_call_tool_result(self) -> Result<CallToolResponse, crate::ErrorData> {
Err(self)
}
}
impl<T: IntoCallToolResult, E: IntoCallToolResult> IntoCallToolResult for Result<T, E> {
fn into_call_tool_result(self) -> Result<CallToolResult, crate::ErrorData> {
fn into_call_tool_result(self) -> Result<CallToolResponse, crate::ErrorData> {
match self {
Ok(value) => value.into_call_tool_result(),
Err(error) => match error.into_call_tool_result() {
Ok(mut result) => {
Ok(CallToolResponse::Complete(mut result)) => {
result.is_error = Some(true);
Ok(result)
Ok(result.into())
}
Ok(CallToolResponse::InputRequired(_)) => Err(crate::ErrorData::internal_error(
"InputRequiredResult cannot be returned from a tool error branch",
None,
)),
Err(e) => Err(e),
},
}
@ -124,7 +137,7 @@ pin_project_lite::pin_project! {
},
Ready {
#[pin]
result: Ready<Result<CallToolResult, crate::ErrorData>>,
result: Ready<Result<CallToolResponse, crate::ErrorData>>,
}
}
}
@ -134,7 +147,7 @@ where
F: Future<Output = R>,
R: IntoCallToolResult,
{
type Output = Result<CallToolResult, crate::ErrorData>;
type Output = Result<CallToolResponse, crate::ErrorData>;
fn poll(
self: std::pin::Pin<&mut Self>,
@ -153,20 +166,21 @@ pub trait CallToolHandler<S, A> {
fn call(
self,
context: ToolCallContext<'_, S>,
) -> MaybeBoxFuture<'_, Result<CallToolResult, crate::ErrorData>>;
) -> MaybeBoxFuture<'_, Result<CallToolResponse, crate::ErrorData>>;
}
#[cfg(not(feature = "local"))]
pub type DynCallToolHandler<S> = dyn for<'s> Fn(ToolCallContext<'s, S>) -> BoxFuture<'s, Result<CallToolResult, crate::ErrorData>>
pub type DynCallToolHandler<S> = dyn for<'s> Fn(ToolCallContext<'s, S>) -> BoxFuture<'s, Result<CallToolResponse, crate::ErrorData>>
+ Send
+ Sync;
#[cfg(feature = "local")]
pub type DynCallToolHandler<S> =
dyn for<'s> Fn(
ToolCallContext<'s, S>,
)
-> futures::future::LocalBoxFuture<'s, Result<CallToolResult, crate::ErrorData>>;
pub type DynCallToolHandler<S> = dyn for<'s> Fn(
ToolCallContext<'s, S>,
) -> futures::future::LocalBoxFuture<
's,
Result<CallToolResponse, crate::ErrorData>,
>;
// Tool-specific extractor for tool name
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
@ -205,7 +219,10 @@ impl<S> FromContextPart<ToolCallContext<'_, S>> for JsonObject {
}
impl<'s, S> ToolCallContext<'s, S> {
pub fn invoke<H, A>(self, h: H) -> MaybeBoxFuture<'s, Result<CallToolResult, crate::ErrorData>>
pub fn invoke<H, A>(
self,
h: H,
) -> MaybeBoxFuture<'s, Result<CallToolResponse, crate::ErrorData>>
where
H: CallToolHandler<S, A>,
{
@ -248,7 +265,7 @@ macro_rules! impl_for {
fn call(
self,
mut context: ToolCallContext<'_, S>,
) -> MaybeBoxFuture<'_, Result<CallToolResult, crate::ErrorData>>{
) -> MaybeBoxFuture<'_, Result<CallToolResponse, crate::ErrorData>>{
$(
let result = $Tn::from_context_part(&mut context);
let $Tn = match result {
@ -279,7 +296,7 @@ macro_rules! impl_for {
fn call(
self,
mut context: ToolCallContext<S>,
) -> MaybeBoxFuture<'static, Result<CallToolResult, crate::ErrorData>>{
) -> MaybeBoxFuture<'static, Result<CallToolResponse, crate::ErrorData>>{
$(
let result = $Tn::from_context_part(&mut context);
let $Tn = match result {
@ -308,7 +325,7 @@ macro_rules! impl_for {
fn call(
self,
mut context: ToolCallContext<S>,
) -> MaybeBoxFuture<'static, Result<CallToolResult, crate::ErrorData>> {
) -> MaybeBoxFuture<'static, Result<CallToolResponse, crate::ErrorData>> {
$(
let result = $Tn::from_context_part(&mut context);
let $Tn = match result {
@ -333,7 +350,7 @@ macro_rules! impl_for {
fn call(
self,
mut context: ToolCallContext<S>,
) -> MaybeBoxFuture<'static, Result<CallToolResult, crate::ErrorData>> {
) -> MaybeBoxFuture<'static, Result<CallToolResponse, crate::ErrorData>> {
$(
let result = $Tn::from_context_part(&mut context);
let $Tn = match result {

View file

@ -3,7 +3,10 @@ use std::borrow::Cow;
use schemars::JsonSchema;
use serde::Serialize;
use crate::{handler::server::tool::IntoCallToolResult, model::CallToolResult};
use crate::{
handler::server::tool::IntoCallToolResult,
model::{CallToolResponse, CallToolResult},
};
/// Json wrapper for structured output
///
@ -27,7 +30,7 @@ impl<T: JsonSchema> JsonSchema for Json<T> {
// Implementation for Json<T> to create structured content
impl<T: Serialize + JsonSchema + 'static> IntoCallToolResult for Json<T> {
fn into_call_tool_result(self) -> Result<CallToolResult, crate::ErrorData> {
fn into_call_tool_result(self) -> Result<CallToolResponse, crate::ErrorData> {
let value = serde_json::to_value(self.0).map_err(|e| {
crate::ErrorData::internal_error(
format!("Failed to serialize structured content: {}", e),
@ -35,6 +38,6 @@ impl<T: Serialize + JsonSchema + 'static> IntoCallToolResult for Json<T> {
)
})?;
Ok(CallToolResult::structured(value))
Ok(CallToolResult::structured(value).into())
}
}