Skip to content

Commit 66c7000

Browse files
authored
docs: document session management for streamable HTTP transport (#674)
1 parent 5fa012d commit 66c7000

3 files changed

Lines changed: 140 additions & 5 deletions

File tree

crates/rmcp/src/transport/streamable_http_server/session.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
//! Session management for the Streamable HTTP transport.
2+
//!
3+
//! A *session* groups the logically related interactions between a single MCP
4+
//! client and the server, starting from the `initialize` handshake. The server
5+
//! assigns each session a unique [`SessionId`] (returned to the client via the
6+
//! `Mcp-Session-Id` response header) and the client includes that ID on every
7+
//! subsequent request.
8+
//!
9+
//! Two tool calls carrying the same session ID come from the same logical
10+
//! session; different IDs mean different clients or conversations.
11+
//!
12+
//! # Implementations
13+
//!
14+
//! * [`local::LocalSessionManager`] — in-memory session store (default).
15+
//! * [`never::NeverSessionManager`] — rejects all session operations, used
16+
//! when stateful mode is disabled.
17+
//!
18+
//! # Custom session managers
19+
//!
20+
//! Implement the [`SessionManager`] trait to back sessions with a database,
21+
//! Redis, or any other external store.
22+
123
use futures::Stream;
224

325
pub use crate::transport::common::server_side_http::{ServerSseMessage, SessionId};
@@ -9,40 +31,66 @@ use crate::{
931
pub mod local;
1032
pub mod never;
1133

34+
/// Controls how MCP sessions are created, validated, and closed.
35+
///
36+
/// The [`StreamableHttpService`](super::StreamableHttpService) calls into this
37+
/// trait for every HTTP request that carries (or should carry) a session ID.
38+
///
39+
/// See the [module-level docs](self) for background on sessions.
1240
pub trait SessionManager: Send + Sync + 'static {
1341
type Error: std::error::Error + Send + 'static;
1442
type Transport: crate::transport::Transport<RoleServer>;
15-
/// Create a new session with the given id and configuration.
43+
44+
/// Create a new session and return its ID together with the transport
45+
/// that will be used to exchange MCP messages within this session.
1646
fn create_session(
1747
&self,
1848
) -> impl Future<Output = Result<(SessionId, Self::Transport), Self::Error>> + Send;
49+
50+
/// Forward the first message (the `initialize` request) to the session.
1951
fn initialize_session(
2052
&self,
2153
id: &SessionId,
2254
message: ClientJsonRpcMessage,
2355
) -> impl Future<Output = Result<ServerJsonRpcMessage, Self::Error>> + Send;
56+
57+
/// Return `true` if a session with the given ID exists and is active.
2458
fn has_session(&self, id: &SessionId)
2559
-> impl Future<Output = Result<bool, Self::Error>> + Send;
60+
61+
/// Close and remove the session. Corresponds to an HTTP DELETE request
62+
/// with `Mcp-Session-Id`.
2663
fn close_session(&self, id: &SessionId)
2764
-> impl Future<Output = Result<(), Self::Error>> + Send;
65+
66+
/// Route a client request into the session and return an SSE stream
67+
/// carrying the server's response(s).
2868
fn create_stream(
2969
&self,
3070
id: &SessionId,
3171
message: ClientJsonRpcMessage,
3272
) -> impl Future<
3373
Output = Result<impl Stream<Item = ServerSseMessage> + Send + Sync + 'static, Self::Error>,
3474
> + Send;
75+
76+
/// Accept a notification, response, or error message from the client
77+
/// without producing a response stream.
3578
fn accept_message(
3679
&self,
3780
id: &SessionId,
3881
message: ClientJsonRpcMessage,
3982
) -> impl Future<Output = Result<(), Self::Error>> + Send;
83+
84+
/// Create an SSE stream not tied to a specific client request (HTTP GET).
4085
fn create_standalone_stream(
4186
&self,
4287
id: &SessionId,
4388
) -> impl Future<
4489
Output = Result<impl Stream<Item = ServerSseMessage> + Send + Sync + 'static, Self::Error>,
4590
> + Send;
91+
92+
/// Resume an SSE stream from the given `Last-Event-ID`, replaying any
93+
/// events the client missed.
4694
fn resume(
4795
&self,
4896
id: &SessionId,

crates/rmcp/src/transport/streamable_http_server/tower.rs

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,89 @@ fn validate_protocol_version_header(headers: &http::HeaderMap) -> Result<(), Box
9696
Ok(())
9797
}
9898

99-
/// # Streamable Http Server
99+
/// # Streamable HTTP server
100100
///
101-
/// ## Extract information from raw http request
101+
/// An HTTP service that implements the
102+
/// [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http)
103+
/// for MCP servers.
104+
///
105+
/// ## Session management
106+
///
107+
/// When [`StreamableHttpServerConfig::stateful_mode`] is `true` (the default),
108+
/// the server creates a session for each client that sends an `initialize`
109+
/// request. The session ID is returned in the `Mcp-Session-Id` response header
110+
/// and the client must include it on all subsequent requests.
111+
///
112+
/// Two tool calls carrying the same `Mcp-Session-Id` come from the same logical
113+
/// session (typically one conversation in an LLM client). Different session IDs
114+
/// mean different sessions.
115+
///
116+
/// The [`SessionManager`] trait controls how sessions are stored and routed:
117+
///
118+
/// * [`LocalSessionManager`](super::session::local::LocalSessionManager) —
119+
/// in-memory session store (default).
120+
/// * [`NeverSessionManager`](super::session::never::NeverSessionManager) —
121+
/// disables sessions entirely (stateless mode).
122+
///
123+
/// ## Accessing HTTP request data from tool handlers
124+
///
125+
/// The service consumes the request body but injects the remaining
126+
/// [`http::request::Parts`] into [`crate::model::Extensions`], which is
127+
/// accessible through [`crate::service::RequestContext`].
128+
///
129+
/// ### Reading the raw HTTP parts
102130
///
103-
/// The http service will consume the request body, however the rest part will be remain and injected into [`crate::model::Extensions`],
104-
/// which you can get from [`crate::service::RequestContext`].
105131
/// ```rust
106132
/// use rmcp::handler::server::tool::Extension;
107133
/// use http::request::Parts;
108134
/// async fn my_tool(Extension(parts): Extension<Parts>) {
109135
/// tracing::info!("http parts:{parts:?}")
110136
/// }
111137
/// ```
138+
///
139+
/// ### Reading the session ID inside a tool handler
140+
///
141+
/// ```rust,ignore
142+
/// use rmcp::handler::server::tool::Extension;
143+
/// use rmcp::service::RequestContext;
144+
/// use rmcp::model::RoleServer;
145+
///
146+
/// #[tool(description = "session-aware tool")]
147+
/// async fn my_tool(
148+
/// &self,
149+
/// Extension(parts): Extension<http::request::Parts>,
150+
/// ) -> Result<CallToolResult, rmcp::ErrorData> {
151+
/// if let Some(session_id) = parts.headers.get("mcp-session-id") {
152+
/// tracing::info!(?session_id, "called from session");
153+
/// }
154+
/// // ...
155+
/// # todo!()
156+
/// }
157+
/// ```
158+
///
159+
/// ### Accessing custom axum/tower extension state
160+
///
161+
/// State added via axum's `Extension` layer is available inside
162+
/// `Parts.extensions`:
163+
///
164+
/// ```rust,ignore
165+
/// use rmcp::service::RequestContext;
166+
/// use rmcp::model::RoleServer;
167+
///
168+
/// #[derive(Clone)]
169+
/// struct AppState { /* ... */ }
170+
///
171+
/// #[tool(description = "example")]
172+
/// async fn my_tool(
173+
/// &self,
174+
/// ctx: RequestContext<RoleServer>,
175+
/// ) -> Result<CallToolResult, rmcp::ErrorData> {
176+
/// let parts = ctx.extensions.get::<http::request::Parts>().unwrap();
177+
/// let state = parts.extensions.get::<AppState>().unwrap();
178+
/// // use state...
179+
/// # todo!()
180+
/// }
181+
/// ```
112182
pub struct StreamableHttpService<S, M = super::session::local::LocalSessionManager> {
113183
pub config: StreamableHttpServerConfig,
114184
session_manager: Arc<M>,

examples/servers/src/common/counter.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,23 @@ impl Counter {
136136
(a + b).to_string(),
137137
)]))
138138
}
139+
140+
/// Returns the `Mcp-Session-Id` of the current session (streamable HTTP only).
141+
#[tool(description = "Get the session ID for this connection")]
142+
fn get_session_id(&self, ctx: RequestContext<RoleServer>) -> Result<CallToolResult, McpError> {
143+
let session_id = ctx
144+
.extensions
145+
.get::<axum::http::request::Parts>()
146+
.and_then(|parts| parts.headers.get("mcp-session-id"))
147+
.map(|v| v.to_str().unwrap_or("(non-ascii)").to_owned());
148+
149+
match session_id {
150+
Some(id) => Ok(CallToolResult::success(vec![Content::text(id)])),
151+
None => Ok(CallToolResult::success(vec![Content::text(
152+
"no session (not running over streamable HTTP?)",
153+
)])),
154+
}
155+
}
139156
}
140157

141158
#[prompt_router]

0 commit comments

Comments
 (0)