Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 6 additions & 23 deletions crates/rmcp/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,13 @@ pub trait ServiceRole: std::fmt::Debug + Send + Sync + 'static + Copy + Clone {
async {}
}

/// Rejects outbound requests when the negotiated protocol forbids this role
/// from sending any.
#[doc(hidden)]
fn enforce_request_association(
_request: &Self::Req,
_peer_info: Option<&Self::PeerInfo>,
_in_request_handler_scope: bool,
) -> Result<(), ServiceError> {
fn enforce_outbound_request(_peer_info: Option<&Self::PeerInfo>) -> Result<(), ServiceError> {
Ok(())
}

/// Receive-side counterpart of [`Self::enforce_request_association`]:
/// SEP-2260 says clients receiving a server-to-client request with no
/// associated outbound request should reject it with invalid params. An
/// error return is sent back to the peer instead of dispatching to the
Expand Down Expand Up @@ -231,25 +228,15 @@ tokio::task_local! {
pub(crate) static ORIGINATING_REQUEST: RequestId;
}

pub(crate) fn in_request_handler_scope() -> bool {
ORIGINATING_REQUEST.try_with(|_| ()).is_ok()
}

/// Marker in an outbound request's non-serialized [`Extensions`] identifying
/// the in-flight peer request it was issued from (SEP-2260). Attached for both
/// roles whenever a request is sent from within a request handler; the
/// streamable HTTP server reads it to deliver server-initiated requests on the
/// originating request's SSE stream. Never on the wire (SEP-2260 defines no
/// wire field), so session managers that serialize messages between processes
/// lose it and such requests fall back to the standalone stream with a warning.
///
/// # Caller requirements
///
/// From protocol version `2026-07-28`, server-to-client sampling, roots, and
/// elicitation requests must be issued while handling a client request;
/// outside a handler they return an `invalid_request` error. The association
/// is task-local and does not cross `tokio::spawn`, so use the task manager
/// for long-running work.
/// The association is task-local and does not cross `tokio::spawn`, so use the
/// task manager for long-running work.
///
/// The client receive-side mirror is [`InboundStreamOrigin`].
#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -873,11 +860,7 @@ impl<R: ServiceRole> Peer<R> {
options: PeerRequestOptions,
subscription_sender: Option<SubscriptionChannel<R::PeerNot>>,
) -> Result<RequestHandle<R>, ServiceError> {
R::enforce_request_association(
&request,
self.peer_info().as_deref(),
in_request_handler_scope(),
)?;
R::enforce_outbound_request(self.peer_info().as_deref())?;
if let Ok(originating) = ORIGINATING_REQUEST.try_with(|id| id.clone()) {
request
.extensions_mut()
Expand Down
69 changes: 19 additions & 50 deletions crates/rmcp/src/service/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,25 +51,10 @@ impl ServiceRole for RoleServer {
}
}

fn enforce_request_association(
request: &Self::Req,
peer_info: Option<&Self::PeerInfo>,
in_request_handler_scope: bool,
) -> Result<(), ServiceError> {
let restricted = matches!(
request,
ServerRequest::CreateMessageRequest(_)
| ServerRequest::ListRootsRequest(_)
| ServerRequest::ElicitRequest(_)
);
if !restricted {
return Ok(());
}
let strict =
peer_info.is_some_and(|info| info.protocol_version >= ProtocolVersion::V_2026_07_28);
if strict && !in_request_handler_scope {
fn enforce_outbound_request(peer_info: Option<&Self::PeerInfo>) -> Result<(), ServiceError> {
if peer_info.is_some_and(|info| info.protocol_version >= ProtocolVersion::V_2026_07_28) {
return Err(ServiceError::McpError(ErrorData::invalid_request(
"SEP-2260: server-to-client requests must be associated with an originating client request",
"server-to-client requests are not allowed on protocol 2026-07-28 or later; return InputRequiredResult instead",
None,
)));
}
Expand Down Expand Up @@ -894,10 +879,8 @@ impl Peer<RoleServer> {
}
}

/// # SEP-2260: request association
///
/// From protocol version `2026-07-28` this must be issued while handling a
/// client request; see [`OriginatingRequestId`].
/// Errors on protocol `2026-07-28` or later, which forbids server-to-client
/// requests; return an [`InputRequiredResult`](crate::model::InputRequiredResult) instead.
#[deprecated(
since = "1.8.0",
note = "Sampling is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
Expand Down Expand Up @@ -932,10 +915,8 @@ impl Peer<RoleServer> {
}
}
method!(
/// # SEP-2260: request association
///
/// From protocol version `2026-07-28` this must be issued while handling a
/// client request; see [`OriginatingRequestId`].
/// Errors on protocol `2026-07-28` or later, which forbids server-to-client
/// requests; return an [`InputRequiredResult`](crate::model::InputRequiredResult) instead.
#[deprecated(
since = "1.8.0",
note = "Roots is deprecated by SEP-2577 and will be removed in a future release. See https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577"
Expand All @@ -944,18 +925,14 @@ impl Peer<RoleServer> {
);
#[cfg(feature = "elicitation")]
method!(
/// # SEP-2260: request association
///
/// From protocol version `2026-07-28` this must be issued while handling a
/// client request; see [`OriginatingRequestId`].
/// Errors on protocol `2026-07-28` or later, which forbids server-to-client
/// requests; return an [`InputRequiredResult`](crate::model::InputRequiredResult) instead.
peer_req create_elicitation ElicitRequest(ElicitRequestParams) => ElicitResult
);
#[cfg(feature = "elicitation")]
method!(
/// # SEP-2260: request association
///
/// From protocol version `2026-07-28` this must be issued while handling a
/// client request; see [`OriginatingRequestId`].
/// Errors on protocol `2026-07-28` or later, which forbids server-to-client
/// requests; return an [`InputRequiredResult`](crate::model::InputRequiredResult) instead.
peer_req_with_timeout create_elicitation_with_timeout ElicitRequest(ElicitRequestParams) => ElicitResult
);

Expand Down Expand Up @@ -1185,10 +1162,8 @@ impl Peer<RoleServer> {
/// # }
/// ```
///
/// # SEP-2260: request association
///
/// From protocol version `2026-07-28` this must be issued while handling a
/// client request; see [`OriginatingRequestId`].
/// Errors on protocol `2026-07-28` or later, which forbids server-to-client
/// requests; return an [`InputRequiredResult`](crate::model::InputRequiredResult) instead.
#[cfg(all(feature = "schemars", feature = "elicitation"))]
pub async fn elicit<T>(&self, message: impl Into<String>) -> Result<Option<T>, ElicitationError>
where
Expand Down Expand Up @@ -1251,10 +1226,8 @@ impl Peer<RoleServer> {
/// # }
/// ```
///
/// # SEP-2260: request association
///
/// From protocol version `2026-07-28` this must be issued while handling a
/// client request; see [`OriginatingRequestId`].
/// Errors on protocol `2026-07-28` or later, which forbids server-to-client
/// requests; return an [`InputRequiredResult`](crate::model::InputRequiredResult) instead.
#[cfg(all(feature = "schemars", feature = "elicitation"))]
pub async fn elicit_with_timeout<T>(
&self,
Expand Down Expand Up @@ -1351,10 +1324,8 @@ impl Peer<RoleServer> {
/// }
/// ```
///
/// # SEP-2260: request association
///
/// From protocol version `2026-07-28` this must be issued while handling a
/// client request; see [`OriginatingRequestId`].
/// Errors on protocol `2026-07-28` or later, which forbids server-to-client
/// requests; return an [`InputRequiredResult`](crate::model::InputRequiredResult) instead.
#[cfg(feature = "elicitation")]
pub async fn elicit_url(
&self,
Expand Down Expand Up @@ -1407,10 +1378,8 @@ impl Peer<RoleServer> {
/// }
/// ```
///
/// # SEP-2260: request association
///
/// From protocol version `2026-07-28` this must be issued while handling a
/// client request; see [`OriginatingRequestId`].
/// Errors on protocol `2026-07-28` or later, which forbids server-to-client
/// requests; return an [`InputRequiredResult`](crate::model::InputRequiredResult) instead.
#[cfg(feature = "elicitation")]
pub async fn elicit_url_with_timeout(
&self,
Expand Down
13 changes: 6 additions & 7 deletions crates/rmcp/src/task_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -933,10 +933,7 @@ mod tests {

#[tokio::test]
async fn task_operation_reestablishes_request_association_scope() {
use crate::{
model::RequestId,
service::{ORIGINATING_REQUEST, in_request_handler_scope},
};
use crate::{model::RequestId, service::ORIGINATING_REQUEST};

let manager = TaskManager::new();
let observed = Arc::new(Mutex::new(None::<bool>));
Expand All @@ -947,7 +944,8 @@ mod tests {
manager.spawn(TaskOptions::default(), move |_ctx| {
let observed_in_task = observed_in_task.clone();
Box::pin(async move {
*observed_in_task.lock().unwrap() = Some(in_request_handler_scope());
*observed_in_task.lock().unwrap() =
Some(ORIGINATING_REQUEST.try_with(|_| ()).is_ok());
Ok(ok_result("done"))
})
})
Expand All @@ -969,7 +967,7 @@ mod tests {

#[tokio::test]
async fn task_operation_without_originating_request_is_unscoped() {
use crate::service::in_request_handler_scope;
use crate::service::ORIGINATING_REQUEST;

let manager = TaskManager::new();
let observed = Arc::new(Mutex::new(None::<bool>));
Expand All @@ -978,7 +976,8 @@ mod tests {
manager.spawn(TaskOptions::default(), move |_ctx| {
let observed_in_task = observed_in_task.clone();
Box::pin(async move {
*observed_in_task.lock().unwrap() = Some(in_request_handler_scope());
*observed_in_task.lock().unwrap() =
Some(ORIGINATING_REQUEST.try_with(|_| ()).is_ok());
Ok(ok_result("done"))
})
});
Expand Down
9 changes: 7 additions & 2 deletions crates/rmcp/src/transport/streamable_http_server/tower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2361,8 +2361,13 @@ where
// ignore
Ok(accepted_response())
}
ClientJsonRpcMessage::Response(_json_rpc_response) => Ok(accepted_response()),
ClientJsonRpcMessage::Error(_json_rpc_error) => Ok(accepted_response()),
// A stateless request has no pending server-to-client request to answer.
ClientJsonRpcMessage::Response(_) | ClientJsonRpcMessage::Error(_) => {
Ok(invalid_request_jsonrpc_response(
None,
"stateless server does not accept JSON-RPC responses",
))
}
}
}
}
Expand Down
Loading
Loading