Skip to content

Commit 4256250

Browse files
committed
feat(service): attach originating request id to in-handler outbound requests (SEP-2260)
Reuses the ORIGINATING_REQUEST task-local from modelcontextprotocol#1027; the marker rides the request's non-serialized Extensions so the streamable HTTP server can route associated requests to the originating SSE stream.
1 parent 003314a commit 4256250

1 file changed

Lines changed: 70 additions & 0 deletions

File tree

crates/rmcp/src/service.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,23 @@ pub(crate) fn in_request_handler_scope() -> bool {
176176
ORIGINATING_REQUEST.try_with(|_| ()).is_ok()
177177
}
178178

179+
/// Marker stored in an outbound request's [`Extensions`](crate::model::Extensions)
180+
/// identifying the in-flight peer request it was issued from (SEP-2260).
181+
///
182+
/// Attached automatically whenever a request is sent from within a request
183+
/// handler. The streamable HTTP server uses it to deliver server-initiated
184+
/// requests on the originating client request's SSE stream.
185+
///
186+
/// # In-memory only
187+
///
188+
/// `Extensions` are never serialized, so this marker does not appear on the
189+
/// wire (SEP-2260 defines no wire field). Session managers that serialize
190+
/// messages between processes lose it; such requests fall back to the
191+
/// standalone stream (logged as a warning).
192+
#[derive(Debug, Clone, PartialEq, Eq)]
193+
#[expect(clippy::exhaustive_structs, reason = "intentionally exhaustive")]
194+
pub struct OriginatingRequestId(pub RequestId);
195+
179196
pub type TxJsonRpcMessage<R> =
180197
JsonRpcMessage<<R as ServiceRole>::Req, <R as ServiceRole>::Resp, <R as ServiceRole>::Not>;
181198
pub type RxJsonRpcMessage<R> = JsonRpcMessage<
@@ -747,6 +764,11 @@ impl<R: ServiceRole> Peer<R> {
747764
self.peer_info().as_deref(),
748765
in_request_handler_scope(),
749766
)?;
767+
if let Ok(originating) = ORIGINATING_REQUEST.try_with(|id| id.clone()) {
768+
request
769+
.extensions_mut()
770+
.insert(OriginatingRequestId(originating));
771+
}
750772
let id = self.request_id_provider.next_request_id();
751773
let progress_token = self.progress_token_provider.next_progress_token();
752774
if let Some(metadata) = self.client_request_metadata.get() {
@@ -1631,3 +1653,51 @@ where
16311653
dg: ct.drop_guard(),
16321654
}
16331655
}
1656+
1657+
#[cfg(all(test, feature = "server"))]
1658+
mod sep2260_marker_tests {
1659+
use std::sync::Arc;
1660+
1661+
use super::*;
1662+
use crate::model::{PingRequest, RequestId, ServerRequest};
1663+
1664+
fn ping() -> ServerRequest {
1665+
ServerRequest::PingRequest(PingRequest {
1666+
method: Default::default(),
1667+
extensions: Default::default(),
1668+
})
1669+
}
1670+
1671+
async fn send_and_capture(scope: Option<RequestId>) -> <RoleServer as ServiceRole>::Req {
1672+
// peer_info None => enforce_request_association is non-strict, so the
1673+
// send is accepted regardless of scope; we only inspect the sink message.
1674+
let (peer, mut rx) =
1675+
Peer::<RoleServer>::new(Arc::new(AtomicU32RequestIdProvider::default()), None);
1676+
let send = peer.send_request_with_option(ping(), PeerRequestOptions::no_options());
1677+
let _handle = match scope {
1678+
Some(id) => ORIGINATING_REQUEST.scope(id, send).await.unwrap(),
1679+
None => send.await.unwrap(),
1680+
};
1681+
let PeerSinkMessage::Request { request, .. } = rx.recv().await.expect("sink message")
1682+
else {
1683+
panic!("expected a request sink message");
1684+
};
1685+
request
1686+
}
1687+
1688+
#[tokio::test]
1689+
async fn outbound_request_carries_originating_id_when_in_scope() {
1690+
let request = send_and_capture(Some(RequestId::Number(7))).await;
1691+
let marker = request
1692+
.extensions()
1693+
.get::<OriginatingRequestId>()
1694+
.expect("marker attached");
1695+
assert_eq!(marker.0, RequestId::Number(7));
1696+
}
1697+
1698+
#[tokio::test]
1699+
async fn outbound_request_has_no_marker_outside_scope() {
1700+
let request = send_and_capture(None).await;
1701+
assert!(request.extensions().get::<OriginatingRequestId>().is_none());
1702+
}
1703+
}

0 commit comments

Comments
 (0)