Skip to main content

tower_mcp/transport/
http.rs

1//! Streamable HTTP transport for MCP
2//!
3//! Implements the Streamable HTTP transport from MCP specification 2025-11-25,
4//! with version-gated support for the 2026-07-28 stateless protocol (SEP-2575 /
5//! SEP-2567) when the `protocol-2026-07-28` feature is compiled in.
6//!
7//! ## Features
8//!
9//! - Single endpoint for POST (requests) and GET (SSE notifications)
10//! - Session management via `MCP-Session-Id` header
11//! - SSE streaming for server notifications and progress updates
12//! - SSE event IDs and stream resumption via `Last-Event-ID` header (SEP-1699)
13//! - Configurable session TTL and cleanup
14//! - **Sampling support**: Server-to-client LLM requests via SSE + POST
15//! - **2026 protocol mode** (`protocol-2026-07-28` feature): version-gated
16//!   dispatch with per-request `_meta` and no session handshake
17//!
18//! ## Stateless mode (2026-07-28 protocol)
19//!
20//! When the `protocol-2026-07-28` feature (or its former `stateless` alias) is
21//! compiled in, the transport handles two distinct stateless paths:
22//!
23//! ### Automatic version-gated path (2026-07-28)
24//!
25//! Any request that arrives with `MCP-Protocol-Version: 2026-07-28` and no
26//! `mcp-session-id` header is dispatched statelessly, regardless
27//! of whether [`HttpTransport::stateless()`] was called. The client is fully
28//! self-identifying: every request carries its protocol version, client info,
29//! and client capabilities in the `_meta` object; no initialize handshake is
30//! needed. Handlers access this data via
31//! [`RequestContext::per_request_meta()`](crate::context::RequestContext::per_request_meta).
32//!
33//! This path runs before the legacy SEP-1442 opt-in path, so 2026-07-28
34//! clients are always handled correctly even on transports that never call
35//! `HttpTransport::stateless()`.
36//!
37//! ### Legacy SEP-1442 opt-in path
38//!
39//! Calling [`HttpTransport::stateless()`] with a [`crate::stateless::StatelessConfig`]
40//! activates the older SEP-1442-style opt-in stateless behavior for clients that
41//! do not carry `MCP-Protocol-Version: 2026-07-28`. See
42//! [`crate::stateless::StatelessConfig`] for details on what this path controls.
43//!
44//! Stateful clients (those sending an `mcp-session-id`) continue to work
45//! normally on the same transport alongside both stateless paths.
46//!
47//! ## `subscriptions/listen` SSE stream
48//!
49//! Clients using the 2026-07-28 protocol open a server-to-client notification
50//! stream by POSTing a `subscriptions/listen` JSON-RPC request. The server responds
51//! with `Content-Type: text/event-stream` and streams zero or more
52//! `notifications/*` events until the client disconnects.
53//!
54//! This replaces the `GET /` SSE endpoint used by the 2025-11-25 protocol. The
55//! `GET /` endpoint is still supported for 2025-11-25 sessions; `subscriptions/listen`
56//! is only available for 2026-07-28 clients.
57//!
58//! ```text
59//! Client (2026-07-28)                          Server
60//!   |                                            |
61//!   |-- POST / {method: "subscriptions/listen",  |
62//!   |           MCP-Protocol-Version: 2026-07-28} -->|
63//!   |<-- 200 Content-Type: text/event-stream ----|
64//!   |<-- event: message (notification) ----------|
65//!   |<-- event: message (notification) ----------|
66//!   |   (client disconnects)                     |
67//! ```
68//!
69//! ## SEP-2243 HTTP headers
70//!
71//! SEP-2243 defines HTTP headers that let load balancers, proxies, and
72//! observability tools inspect MCP traffic without parsing the JSON-RPC body:
73//!
74//! | Header | Required when | Description |
75//! |--------|---------------|-------------|
76//! | `Mcp-Method` | All POST requests (strict mode) | Mirrors the JSON-RPC `method` field |
77//! | `Mcp-Name` | `tools/call`, `prompts/get`, `resources/read` (strict mode) | Mirrors `params.name` or `params.uri` |
78//! | `MCP-Protocol-Version` | All requests (strict mode) | The protocol version in use |
79//!
80//! Validation is **lenient** for 2025-11-25 clients: headers present in the
81//! request are validated for consistency with the body, but missing headers are
82//! not an error. Validation is **strict** for 2026-07-28 clients: `Mcp-Method`
83//! must be present on every POST and `Mcp-Name` must be present for the three
84//! named methods. Violations return `-32020` (HeaderMismatch).
85//!
86//! The public constants [`MCP_METHOD_HEADER`], [`MCP_NAME_HEADER`], and
87//! [`MCP_PARAM_HEADER_PREFIX`] hold the canonical lowercase header names.
88//!
89//! ## Sampling (Server-to-Client Requests)
90//!
91//! When using `HttpTransport::new(router).with_sampling()`, tool handlers can request
92//! LLM completions from the client. The flow is:
93//!
94//! 1. Tool handler calls `ctx.sample(params)`
95//! 2. Server upgrades that originating POST response to SSE and sends the
96//!    sampling request on it
97//! 3. Client receives the request and processes it
98//! 4. Client sends the response as a POST to the MCP endpoint
99//! 5. Server routes the response back to the waiting handler and finishes the
100//!    original POST SSE stream with the tool result
101//!
102//! Restricted server-to-client requests are never sent on the standalone GET
103//! notification stream. Associated POST streams are process-local and are not
104//! resumable; deployments using sampling, elicitation, or roots requests need
105//! session affinity for the duration of the exchange.
106//!
107//! ## Session Reconnection
108//!
109//! When a session is not found (e.g., after server restart or session expiration),
110//! the server returns a JSON-RPC error with code `-32005` (SessionNotFound).
111//! Clients should handle this by re-initializing the connection:
112//!
113//! ```text
114//! Client                          Server
115//!   |                               |
116//!   |-- tools/list (old session) -->|
117//!   |<-- error: SessionNotFound ----|
118//!   |                               |
119//!   |-- initialize --------------->|
120//!   |<-- result + new session id ---|
121//!   |                               |
122//!   |-- tools/list (new session) -->|
123//!   |<-- result -------------------|
124//! ```
125//!
126//! ## SSE Stream Resumption (SEP-1699)
127//!
128//! Each SSE event includes a unique, monotonically increasing event ID. If a
129//! client disconnects and reconnects, it can include the `Last-Event-ID` header
130//! with the ID of the last event it received. The server will replay any buffered
131//! events with IDs greater than the provided ID before continuing with live events.
132//!
133//! ```text
134//! Client                              Server
135//!   |-- GET / (Accept: text/event-stream) -->|
136//!   |<-- id:0, data:{progress...} -----------|
137//!   |<-- id:1, data:{progress...} -----------|
138//!   |<-- id:2, data:{progress...} -----------|
139//!   |                                        |
140//!   |  ** Client disconnects **              |
141//!   |                                        |
142//!   |                   (server buffers id:3, id:4, id:5)
143//!   |                                        |
144//!   |-- GET / (Last-Event-ID: 2) ----------->|
145//!   |<-- id:3, data:{...} (replayed) --------|
146//!   |<-- id:4, data:{...} (replayed) --------|
147//!   |<-- id:5, data:{...} (replayed) --------|
148//!   |<-- id:6, data:{...} (live) ------------|
149//! ```
150//!
151//! The server buffers up to 1000 events per session by default.
152//!
153//! ## Error Codes
154//!
155//! | Code    | Name                      | Description                                        |
156//! |---------|---------------------------|----------------------------------------------------|
157//! | -32020  | HeaderMismatch            | Required HTTP header missing or inconsistent with body (SEP-2243, strict mode) |
158//! | -32022  | UnsupportedProtocolVersion| Server does not support the requested protocol version (SEP-2575) |
159//! | -32005  | SessionNotFound           | Session expired or server restarted                |
160//! | -32006  | SessionRequired           | MCP-Session-Id header missing                      |
161//!
162//! ## Session Handling
163//!
164//! By default, sessions are optional: requests without an `mcp-session-id`
165//! header are allowed and receive a transient, pre-initialized session. This
166//! ensures compatibility with clients (Codex CLI, Cursor, etc.) that don't
167//! carry the session ID forward after initialization.
168//!
169//! Clients that do send session IDs continue to work normally.
170//!
171//! To require strict session management (reject requests without a session ID),
172//! use [`HttpTransport::require_sessions()`]:
173//!
174//! ```rust,ignore
175//! let transport = HttpTransport::new(router).require_sessions();
176//! ```
177//!
178//! ## CORS Support
179//!
180//! Browser-based MCP clients require CORS headers. Since [`HttpTransport::into_router()`]
181//! returns a standard [`axum::Router`], you can add CORS support using
182//! `tower_http::cors::CorsLayer`:
183//!
184//! ```rust,ignore
185//! use tower_mcp::McpRouter;
186//! use tower_mcp::transport::http::HttpTransport;
187//! use tower_http::cors::{CorsLayer, Any};
188//! use http::Method;
189//!
190//! # #[tokio::main]
191//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
192//! let router = McpRouter::new().server_info("my-server", "1.0.0");
193//! let transport = HttpTransport::new(router);
194//!
195//! // Wrap the axum router with CORS middleware
196//! let app = transport.into_router().layer(
197//!     CorsLayer::new()
198//!         .allow_origin(Any)
199//!         .allow_methods([Method::GET, Method::POST, Method::DELETE])
200//!         .allow_headers(Any)
201//!         .expose_headers(Any),
202//! );
203//!
204//! let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
205//! axum::serve(listener, app).await?;
206//! # Ok(())
207//! # }
208//! ```
209//!
210//! For production, replace `Any` origins with your specific allowed origins.
211//!
212//! **Note:** [`HttpTransport::layer()`] applies middleware at the *MCP request* level
213//! (inside the JSON-RPC service). CORS must be applied at the *HTTP* level using
214//! `into_router().layer(...)` as shown above.
215//!
216//! # Example
217//!
218//! ```rust,no_run
219//! use tower_mcp::{BoxError, McpRouter, ToolBuilder, CallToolResult};
220//! use tower_mcp::transport::http::HttpTransport;
221//! use schemars::JsonSchema;
222//! use serde::Deserialize;
223//!
224//! #[derive(Debug, Deserialize, JsonSchema)]
225//! struct Input { value: String }
226//!
227//! #[tokio::main]
228//! async fn main() -> Result<(), BoxError> {
229//!     let tool = ToolBuilder::new("echo")
230//!         .handler(|i: Input| async move { Ok(CallToolResult::text(i.value)) })
231//!         .build();
232//!
233//!     let router = McpRouter::new()
234//!         .server_info("my-server", "1.0.0")
235//!         .tool(tool);
236//!
237//!     let transport = HttpTransport::new(router);
238//!
239//!     // Run on localhost:3000
240//!     transport.serve("127.0.0.1:3000").await?;
241//!     Ok(())
242//! }
243//! ```
244
245use std::collections::HashMap;
246use std::convert::Infallible;
247use std::future::Future;
248use std::pin::Pin;
249use std::sync::Arc;
250use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
251use std::time::{Duration, Instant};
252
253use axum::{
254    Router,
255    extract::State,
256    http::{HeaderMap, HeaderValue, StatusCode, header},
257    response::{IntoResponse, Response, Sse, sse::Event},
258    routing::{delete, get, post},
259};
260#[cfg(feature = "stateless")]
261use tokio::sync::mpsc;
262use tokio::sync::{Mutex, RwLock, broadcast, oneshot};
263use tokio_stream::StreamExt;
264use tokio_stream::wrappers::BroadcastStream;
265
266#[cfg(feature = "stateless")]
267use crate::context::ServerNotification;
268use crate::context::{
269    ChannelClientRequester, ClientRequesterHandle, NotificationReceiver, OutgoingRequest,
270    OutgoingRequestReceiver, notification_channel, outgoing_request_channel,
271};
272use crate::error::{Error, JsonRpcError, Result};
273#[cfg(feature = "stateless")]
274use crate::error::{ErrorCode, McpErrorCode};
275use crate::inspection::{McpDirection, McpProtocolRevision};
276use crate::jsonrpc::{JsonRpcService, apply_protocol_result_fields, inspect_runtime_value};
277#[cfg(feature = "stateless")]
278use crate::protocol::SubscriptionFilter;
279use crate::protocol::{
280    ClientCapabilities, Implementation, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest,
281    JsonRpcResponse, LATEST_PROTOCOL_VERSION, McpNotification, PROTOCOL_VERSION_2026_07_28,
282    RequestId,
283};
284use crate::router::{McpRouter, RouterRequest, RouterResponse};
285use crate::transport::service::{
286    CatchError, InjectAnnotations, McpBoxService, ServiceFactory, identity_factory,
287};
288#[cfg(feature = "stateless")]
289use crate::transport::subscriptions::{
290    subscription_complete_response, subscription_matches, tagged_subscription_notification,
291};
292use crate::{ProtocolSupport, ProtocolSupportError};
293use tower::util::BoxCloneService;
294
295/// SEP-2575 per-request `_meta` extraction. Pulls `StatelessRequestMeta` from
296/// the parsed request params and inserts it into the per-request `Extensions`
297/// so handlers can read it via `ctx.per_request_meta()`. No-op if the request
298/// has no `_meta`, params aren't an object, or the meta can't deserialize.
299#[cfg(feature = "stateless")]
300fn stash_per_request_meta(req: &JsonRpcRequest, ext: &mut crate::router::Extensions) {
301    if let Some(params) = req.params.as_ref()
302        && let Some(meta) = crate::stateless::StatelessRequestMeta::from_params(params)
303    {
304        ext.insert(meta);
305    }
306}
307
308/// Header name for MCP session ID
309pub const MCP_SESSION_ID_HEADER: &str = "mcp-session-id";
310
311/// Header name for MCP protocol version
312pub const MCP_PROTOCOL_VERSION_HEADER: &str = "mcp-protocol-version";
313
314/// SEP-2243: header that mirrors the JSON-RPC `method` field for HTTP
315/// intermediaries (load balancers, observability) so they can route or
316/// classify MCP traffic without parsing the body. Required on all POST
317/// requests when the negotiated protocol version implements SEP-2243.
318pub const MCP_METHOD_HEADER: &str = "mcp-method";
319
320/// SEP-2243: header that mirrors `params.name` (for `tools/call` and
321/// `prompts/get`) or `params.uri` (for `resources/read`). Required for
322/// those three methods when the negotiated protocol version implements
323/// SEP-2243.
324pub const MCP_NAME_HEADER: &str = "mcp-name";
325
326/// SEP-2243: prefix for custom headers derived from tool parameters
327/// marked with the `x-mcp-header` JSON Schema extension. The full header
328/// name is `Mcp-Param-{Name}`.
329pub const MCP_PARAM_HEADER_PREFIX: &str = "mcp-param-";
330
331/// Default maximum POST body size in bytes (4 MiB, matching rmcp).
332///
333/// See [`HttpTransport::max_body_size`].
334pub const DEFAULT_MAX_BODY_SIZE: usize = 4 * 1024 * 1024;
335
336/// SSE event type for JSON-RPC messages
337const SSE_MESSAGE_EVENT: &str = "message";
338
339/// Header name for Last-Event-ID (for SSE stream resumption per SEP-1699)
340const LAST_EVENT_ID_HEADER: &str = "last-event-id";
341
342/// Pending request waiting for a response from the client
343struct PendingRequest {
344    response_tx: oneshot::Sender<Result<serde_json::Value>>,
345}
346
347type AssociatedCall = Pin<Box<dyn Future<Output = Result<JsonRpcResponse>> + Send + 'static>>;
348
349/// Session state for HTTP transport
350/// How a session produces its MCP service for request processing.
351enum SessionServiceSource {
352    /// Session was created from an McpRouter with a factory for middleware wrapping.
353    Router {
354        router: McpRouter,
355        factory: ServiceFactory,
356    },
357    /// Session was created from a pre-built boxed service (e.g., McpProxy).
358    /// Wrapped in Mutex because BoxCloneService is Send but not Sync,
359    /// and Session must be Sync for Arc<Session> to be Send.
360    Boxed(std::sync::Mutex<McpBoxService>),
361}
362
363struct Session {
364    /// Session ID
365    id: String,
366    /// Source for creating the MCP service
367    service_source: SessionServiceSource,
368    /// Broadcast channel for SSE notifications and outgoing requests
369    notifications_tx: broadcast::Sender<String>,
370    /// When this session was created
371    created_at: Instant,
372    /// Last time this session was accessed
373    last_accessed: RwLock<Instant>,
374    /// Pending outgoing requests waiting for responses
375    pending_requests: Mutex<HashMap<RequestId, PendingRequest>>,
376    /// Session-wide allocator for request-scoped server-to-client request IDs.
377    ///
378    /// Each originating POST owns a separate channel, but IDs must remain
379    /// unique across concurrent POSTs in the same session.
380    request_id_allocator: Option<Arc<AtomicI64>>,
381    /// Negotiated protocol version (set after initialize)
382    protocol_version: RwLock<String>,
383    /// Client implementation info advertised in the `initialize` request.
384    ///
385    /// Populated by `handle_post` after a successful initialize response,
386    /// and restored from a [`SessionRecord`](crate::session_store::SessionRecord)
387    /// when a session is rebuilt from the persistent store. `None` until the
388    /// first initialize completes.
389    client_info: RwLock<Option<Implementation>>,
390    /// Client capabilities advertised in the `initialize` request.
391    ///
392    /// Populated by `handle_post` after a successful initialize response,
393    /// and restored from a [`SessionRecord`](crate::session_store::SessionRecord)
394    /// when a session is rebuilt from the persistent store. `None` until the
395    /// first initialize completes.
396    client_capabilities: RwLock<Option<ClientCapabilities>>,
397    /// Counter for SSE event IDs (for stream resumption per SEP-1699)
398    event_counter: AtomicU64,
399    /// Pluggable store for SSE events (enables cross-instance replay)
400    event_store: Arc<dyn crate::event_store::EventStore>,
401    /// Whether `notifications/initialized` has been received from the client.
402    ///
403    /// Per the MCP 2025-11-25 spec, clients MUST send this notification after
404    /// receiving the `initialize` response and before sending any other requests.
405    /// Checked by `handle_post` when `strict_initialization` is enabled on
406    /// [`SessionConfig`]. Pre-initialized sessions (optional_sessions path) and
407    /// restored sessions start with this set to `true`.
408    initialized_notification_received: std::sync::atomic::AtomicBool,
409}
410
411impl Session {
412    fn new(
413        router: McpRouter,
414        sampling_enabled: bool,
415        service_factory: ServiceFactory,
416        event_store: Arc<dyn crate::event_store::EventStore>,
417    ) -> Self {
418        let (notifications_tx, _) = broadcast::channel(100);
419
420        // Set up notification forwarding: mpsc -> broadcast
421        // The router sends notifications (progress, log, resource updates) to
422        // an mpsc channel. We bridge these to the session's broadcast channel
423        // so they reach connected SSE clients.
424        let (notif_sender, mut notif_receiver) = notification_channel(256);
425        let router = router.with_notification_sender(notif_sender);
426
427        let broadcast_tx = notifications_tx.clone();
428        tokio::spawn(async move {
429            while let Some(notification) = notif_receiver.recv().await {
430                if let Some(json) = crate::transport::stdio::serialize_notification(&notification) {
431                    // Best effort: if no subscribers, the message is dropped
432                    let _ = broadcast_tx.send(json);
433                }
434            }
435        });
436
437        let request_id_allocator = if sampling_enabled {
438            Some(Arc::new(AtomicI64::new(1)))
439        } else {
440            None
441        };
442
443        let now = Instant::now();
444        Self {
445            id: uuid::Uuid::new_v4().to_string(),
446            service_source: SessionServiceSource::Router {
447                router,
448                factory: service_factory,
449            },
450            notifications_tx,
451            created_at: now,
452            last_accessed: RwLock::new(now),
453            pending_requests: Mutex::new(HashMap::new()),
454            request_id_allocator,
455            protocol_version: RwLock::new(LATEST_PROTOCOL_VERSION.to_string()),
456            client_info: RwLock::new(None),
457            client_capabilities: RwLock::new(None),
458            event_counter: AtomicU64::new(0),
459            event_store,
460            initialized_notification_received: std::sync::atomic::AtomicBool::new(false),
461        }
462    }
463
464    /// Create a session from a pre-built boxed service.
465    ///
466    /// This is used when the transport is created via [`HttpTransport::from_service()`].
467    /// Notification bridging and sampling setup are skipped — the caller is
468    /// responsible for configuring these on the service before passing it in.
469    fn from_service(
470        service: McpBoxService,
471        event_store: Arc<dyn crate::event_store::EventStore>,
472    ) -> Self {
473        let (notifications_tx, _) = broadcast::channel(100);
474
475        let now = Instant::now();
476        Self {
477            id: uuid::Uuid::new_v4().to_string(),
478            service_source: SessionServiceSource::Boxed(std::sync::Mutex::new(service)),
479            notifications_tx,
480            created_at: now,
481            last_accessed: RwLock::new(now),
482            pending_requests: Mutex::new(HashMap::new()),
483            request_id_allocator: None,
484            protocol_version: RwLock::new(LATEST_PROTOCOL_VERSION.to_string()),
485            client_info: RwLock::new(None),
486            client_capabilities: RwLock::new(None),
487            event_counter: AtomicU64::new(0),
488            event_store,
489            initialized_notification_received: std::sync::atomic::AtomicBool::new(false),
490        }
491    }
492
493    /// Rebuild a session from a [`SessionRecord`] so a request for an
494    /// unknown session ID can be served transparently.
495    ///
496    /// The router is pre-marked initialized and the protocol version is
497    /// restored from the record. Runtime state (broadcast channels,
498    /// pending-request table) is freshly allocated — in-flight state from
499    /// before the rebuild is not recovered. The `event_counter` is left at
500    /// zero; the [`SessionRegistry`] seeds it from the event store so
501    /// future event IDs don't collide with buffered ones.
502    fn restored(
503        record: &crate::session_store::SessionRecord,
504        router: McpRouter,
505        sampling_enabled: bool,
506        service_factory: ServiceFactory,
507        event_store: Arc<dyn crate::event_store::EventStore>,
508    ) -> Self {
509        // Skip the Initializing intermediate state — this session was
510        // already initialized on the original instance.
511        router.session().mark_initialized();
512
513        let (notifications_tx, _) = broadcast::channel(100);
514        let (notif_sender, mut notif_receiver) = notification_channel(256);
515        let router = router.with_notification_sender(notif_sender);
516
517        let broadcast_tx = notifications_tx.clone();
518        tokio::spawn(async move {
519            while let Some(notification) = notif_receiver.recv().await {
520                if let Some(json) = crate::transport::stdio::serialize_notification(&notification) {
521                    let _ = broadcast_tx.send(json);
522                }
523            }
524        });
525
526        let request_id_allocator = if sampling_enabled {
527            Some(Arc::new(AtomicI64::new(1)))
528        } else {
529            None
530        };
531
532        let now = Instant::now();
533        Self {
534            id: record.id.clone(),
535            service_source: SessionServiceSource::Router {
536                router,
537                factory: service_factory,
538            },
539            notifications_tx,
540            created_at: now,
541            last_accessed: RwLock::new(now),
542            pending_requests: Mutex::new(HashMap::new()),
543            request_id_allocator,
544            protocol_version: RwLock::new(record.protocol_version.clone()),
545            client_info: RwLock::new(record.client_info.clone()),
546            client_capabilities: RwLock::new(record.client_capabilities.clone()),
547            event_counter: AtomicU64::new(0),
548            event_store,
549            // Restored sessions already completed the handshake on a previous
550            // instance; treat `notifications/initialized` as already received.
551            initialized_notification_received: std::sync::atomic::AtomicBool::new(true),
552        }
553    }
554
555    /// Rebuild a session from a [`SessionRecord`] for transports built
556    /// with [`HttpTransport::from_service`]. The service's internal state
557    /// (if any) is not restored — the caller is responsible for anything
558    /// beyond the metadata in the record.
559    fn from_service_restored(
560        service: McpBoxService,
561        record: &crate::session_store::SessionRecord,
562        event_store: Arc<dyn crate::event_store::EventStore>,
563    ) -> Self {
564        let (notifications_tx, _) = broadcast::channel(100);
565        let now = Instant::now();
566        Self {
567            id: record.id.clone(),
568            service_source: SessionServiceSource::Boxed(std::sync::Mutex::new(service)),
569            notifications_tx,
570            created_at: now,
571            last_accessed: RwLock::new(now),
572            pending_requests: Mutex::new(HashMap::new()),
573            request_id_allocator: None,
574            protocol_version: RwLock::new(record.protocol_version.clone()),
575            client_info: RwLock::new(record.client_info.clone()),
576            client_capabilities: RwLock::new(record.client_capabilities.clone()),
577            event_counter: AtomicU64::new(0),
578            event_store,
579            // Restored sessions already completed the handshake on a previous
580            // instance; treat `notifications/initialized` as already received.
581            initialized_notification_received: std::sync::atomic::AtomicBool::new(true),
582        }
583    }
584
585    /// Create a middleware-wrapped service from this session's service source.
586    fn make_service(&self) -> McpBoxService {
587        match &self.service_source {
588            SessionServiceSource::Router { router, factory } => (factory)(router.clone()),
589            SessionServiceSource::Boxed(mutex) => mutex.lock().unwrap().clone(),
590        }
591    }
592
593    /// Handle a client notification (fire-and-forget, no response).
594    ///
595    /// For router-based sessions, delegates to the router's notification handler.
596    /// For service-based sessions, notifications are logged but not processed
597    /// (the service should handle its own notification needs).
598    fn handle_notification(&self, notification: McpNotification) {
599        match &self.service_source {
600            SessionServiceSource::Router { router, .. } => {
601                router.handle_notification(notification);
602            }
603            SessionServiceSource::Boxed(_) => {
604                tracing::debug!(
605                    notification = ?notification,
606                    "Notification received on service-based session (not forwarded)"
607                );
608            }
609        }
610    }
611
612    /// Get the next SSE event ID for this session.
613    ///
614    /// Event IDs are monotonically increasing per session, enabling
615    /// stream resumption via the Last-Event-ID header (SEP-1699).
616    fn next_event_id(&self) -> u64 {
617        self.event_counter.fetch_add(1, Ordering::SeqCst)
618    }
619
620    /// Buffer an event for potential replay (SEP-1699).
621    ///
622    /// Delegates to the configured [`EventStore`](crate::event_store::EventStore).
623    /// Store errors are logged but non-fatal — the transport continues
624    /// serving the client even if the external event buffer is unavailable,
625    /// since the event has already been sent on the live SSE stream.
626    async fn buffer_event(&self, id: u64, data: String) {
627        let record = crate::event_store::EventRecord::new(id, data);
628        if let Err(e) = self.event_store.append(&self.id, record).await {
629            tracing::warn!(session_id = %self.id, event_id = id, error = %e, "Failed to append event to event store");
630        }
631    }
632
633    /// Get buffered events after the given event ID.
634    ///
635    /// Returns events with IDs greater than `after_id`, in order. Used for
636    /// stream resumption when a client reconnects with the `Last-Event-ID`
637    /// header. Store errors produce an empty replay list and are logged.
638    async fn get_events_after(&self, after_id: u64) -> Vec<crate::event_store::EventRecord> {
639        match self.event_store.replay_after(&self.id, after_id).await {
640            Ok(events) => events,
641            Err(e) => {
642                tracing::warn!(session_id = %self.id, error = %e, "Failed to replay events from event store");
643                Vec::new()
644            }
645        }
646    }
647
648    /// Update the last accessed time
649    async fn touch(&self) {
650        *self.last_accessed.write().await = Instant::now();
651    }
652
653    /// Check if the session has expired
654    async fn is_expired(&self, ttl: Duration) -> bool {
655        self.last_accessed.read().await.elapsed() > ttl
656    }
657
658    /// Store a pending request
659    async fn add_pending_request(
660        &self,
661        id: RequestId,
662        response_tx: oneshot::Sender<Result<serde_json::Value>>,
663    ) {
664        let mut pending = self.pending_requests.lock().await;
665        pending.insert(id, PendingRequest { response_tx });
666    }
667
668    /// Complete a pending request with a response
669    async fn complete_pending_request(
670        &self,
671        id: &RequestId,
672        result: Result<serde_json::Value>,
673    ) -> bool {
674        let pending = {
675            let mut pending_requests = self.pending_requests.lock().await;
676            pending_requests.remove(id)
677        };
678
679        match pending {
680            Some(pending) => {
681                // Send result to waiter (ignore if they've dropped the receiver)
682                let _ = pending.response_tx.send(result);
683                true
684            }
685            None => false,
686        }
687    }
688
689    /// Fail request-scoped client requests whose originating POST is gone.
690    async fn fail_pending_requests(&self, ids: &[RequestId], message: &str) {
691        let removed = {
692            let mut pending = self.pending_requests.lock().await;
693            ids.iter()
694                .filter_map(|id| pending.remove(id))
695                .collect::<Vec<_>>()
696        };
697
698        for pending in removed {
699            let _ = pending
700                .response_tx
701                .send(Err(Error::Transport(message.to_string())));
702        }
703    }
704}
705
706/// Default session TTL (30 minutes)
707pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(30 * 60);
708
709/// Default cleanup interval (1 minute)
710const DEFAULT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
711
712/// Configuration for session management
713#[derive(Debug, Clone)]
714pub struct SessionConfig {
715    /// Time-to-live for inactive sessions
716    pub ttl: Duration,
717    /// Maximum number of sessions (None = unlimited)
718    pub max_sessions: Option<usize>,
719    /// How often to run the cleanup task
720    pub cleanup_interval: Duration,
721    /// Whether to enforce that clients send `notifications/initialized` before
722    /// making any non-initialize requests, per the MCP 2025-11-25 spec.
723    ///
724    /// When `true` (the default), the transport returns a JSON-RPC
725    /// `InvalidRequest` error (-32600) to any request received before
726    /// `notifications/initialized` on a 2025-11-25 session-based connection.
727    ///
728    /// Set to `false` to restore the previous lenient behavior, e.g. in
729    /// dev/test scenarios where the full MCP handshake is inconvenient.
730    pub strict_initialization: bool,
731}
732
733impl Default for SessionConfig {
734    fn default() -> Self {
735        Self {
736            ttl: DEFAULT_SESSION_TTL,
737            max_sessions: None,
738            cleanup_interval: DEFAULT_CLEANUP_INTERVAL,
739            strict_initialization: true,
740        }
741    }
742}
743
744impl SessionConfig {
745    /// Create a new session config with the given TTL
746    pub fn with_ttl(ttl: Duration) -> Self {
747        Self {
748            ttl,
749            ..Default::default()
750        }
751    }
752
753    /// Set the maximum number of sessions
754    pub fn max_sessions(mut self, max: usize) -> Self {
755        self.max_sessions = Some(max);
756        self
757    }
758
759    /// Set the cleanup interval
760    pub fn cleanup_interval(mut self, interval: Duration) -> Self {
761        self.cleanup_interval = interval;
762        self
763    }
764
765    /// Enable or disable strict initialization enforcement.
766    ///
767    /// When enabled (default), the transport enforces that clients send
768    /// `notifications/initialized` before any other requests on a
769    /// 2025-11-25 session-based connection, per the MCP spec. Requests
770    /// that arrive before this notification receive a JSON-RPC
771    /// `InvalidRequest` error (-32600).
772    ///
773    /// Disable this for dev/test scenarios where the full MCP handshake
774    /// is inconvenient.
775    pub fn strict_initialization(mut self, enabled: bool) -> Self {
776        self.strict_initialization = enabled;
777        self
778    }
779}
780
781/// Registry coordinating live session runtime state with a pluggable
782/// persistent [`SessionStore`](crate::session_store::SessionStore).
783///
784/// - Runtime state (broadcast channels, pending requests, live services) is
785///   kept in the in-process `sessions` map and cannot be serialized.
786/// - Persistent metadata (IDs, timestamps, protocol version) is mirrored into
787///   the caller-supplied [`SessionStore`]. The default
788///   [`MemorySessionStore`](crate::session_store::MemorySessionStore) keeps
789///   metadata in-process (same behavior as before this trait existed).
790struct SessionRegistry {
791    sessions: RwLock<HashMap<String, Arc<Session>>>,
792    config: SessionConfig,
793    sampling_enabled: bool,
794    persistent: Arc<dyn crate::session_store::SessionStore>,
795    events: Arc<dyn crate::event_store::EventStore>,
796    /// Source for rebuilding services when restoring a session.
797    service_source: ServiceSource,
798    /// If `true`, a request for an unknown session ID whose record is not
799    /// in the persistent store spins up a new session with synthetic
800    /// client info instead of returning 404 (see anubis-mcp #125 for the
801    /// precedent).
802    auto_reinit: bool,
803}
804
805impl SessionRegistry {
806    fn new(
807        config: SessionConfig,
808        sampling_enabled: bool,
809        persistent: Arc<dyn crate::session_store::SessionStore>,
810        events: Arc<dyn crate::event_store::EventStore>,
811        service_source: ServiceSource,
812        auto_reinit: bool,
813    ) -> Self {
814        Self {
815            sessions: RwLock::new(HashMap::new()),
816            config,
817            sampling_enabled,
818            persistent,
819            events,
820            service_source,
821            auto_reinit,
822        }
823    }
824
825    /// Build a SessionRecord reflecting the given live Session.
826    async fn record_for(&self, session: &Session) -> crate::session_store::SessionRecord {
827        let protocol_version = session.protocol_version.read().await.clone();
828        let last_accessed = session.last_accessed.read().await;
829        let mut record = crate::session_store::SessionRecord::new(
830            session.id.clone(),
831            protocol_version,
832            self.config.ttl,
833        );
834        // Populate the client identity / capabilities advertised at
835        // initialize time so persisted records faithfully describe the
836        // session. These remain `None` until a successful initialize.
837        record.client_info = session.client_info.read().await.clone();
838        record.client_capabilities = session.client_capabilities.read().await.clone();
839        // Convert from monotonic Instant to SystemTime approximation.
840        let now = std::time::SystemTime::now();
841        let created_ago = session.created_at.elapsed();
842        let last_accessed_ago = last_accessed.elapsed();
843        record.created_at = now.checked_sub(created_ago).unwrap_or(now);
844        record.last_accessed = now.checked_sub(last_accessed_ago).unwrap_or(now);
845        record.expires_at = record.last_accessed + self.config.ttl;
846        record
847    }
848
849    /// Persist metadata for a newly created session, logging on failure.
850    ///
851    /// Persistence errors are intentionally non-fatal: the live runtime
852    /// session is already registered locally, so the transport can continue
853    /// serving requests even if the external store is briefly unavailable.
854    async fn persist_new(&self, session: &Session) {
855        let record = self.record_for(session).await;
856        if let Err(e) = self.persistent.create(&mut record.clone()).await {
857            tracing::warn!(session_id = %session.id, error = %e, "Failed to persist session record");
858        }
859    }
860
861    /// Persist an update to an existing session's record (upsert).
862    ///
863    /// Called after the session's state changes in a way that should be
864    /// reflected in the persistent store -- notably after a successful
865    /// `initialize` so the stored record carries the client's advertised
866    /// `client_info` and `capabilities` (rather than the defaults captured
867    /// at create time). Failures are logged but non-fatal.
868    async fn save_record(&self, session: &Session) {
869        let record = self.record_for(session).await;
870        if let Err(e) = self.persistent.save(&record).await {
871            tracing::warn!(session_id = %session.id, error = %e, "Failed to save session record");
872        }
873    }
874
875    async fn create(
876        &self,
877        router: McpRouter,
878        service_factory: ServiceFactory,
879    ) -> Option<Arc<Session>> {
880        let session = {
881            let mut sessions = self.sessions.write().await;
882
883            // Check max sessions limit
884            if let Some(max) = self.config.max_sessions
885                && sessions.len() >= max
886            {
887                tracing::warn!(
888                    max_sessions = max,
889                    current = sessions.len(),
890                    "Session limit reached, rejecting new session"
891                );
892                return None;
893            }
894
895            let session = Arc::new(Session::new(
896                router,
897                self.sampling_enabled,
898                service_factory,
899                self.events.clone(),
900            ));
901            sessions.insert(session.id.clone(), session.clone());
902            tracing::debug!(session_id = %session.id, sampling = self.sampling_enabled, "Created new session");
903            session
904        };
905        self.persist_new(&session).await;
906        Some(session)
907    }
908
909    async fn create_from_service(&self, service: McpBoxService) -> Option<Arc<Session>> {
910        let session = {
911            let mut sessions = self.sessions.write().await;
912
913            if let Some(max) = self.config.max_sessions
914                && sessions.len() >= max
915            {
916                tracing::warn!(
917                    max_sessions = max,
918                    current = sessions.len(),
919                    "Session limit reached, rejecting new session"
920                );
921                return None;
922            }
923
924            let session = Arc::new(Session::from_service(service, self.events.clone()));
925            sessions.insert(session.id.clone(), session.clone());
926            tracing::debug!(session_id = %session.id, "Created new session from service");
927            session
928        };
929        self.persist_new(&session).await;
930        Some(session)
931    }
932
933    /// Create a new session with its router already marked as initialized.
934    ///
935    /// Used by the optional-sessions feature to serve requests from clients
936    /// that skip the initialize handshake.
937    async fn create_initialized(
938        &self,
939        router: McpRouter,
940        service_factory: ServiceFactory,
941    ) -> Option<Arc<Session>> {
942        // Pre-initialize the router's session state so it won't reject requests
943        router.session().mark_initialized();
944
945        let session = {
946            let mut sessions = self.sessions.write().await;
947
948            if let Some(max) = self.config.max_sessions
949                && sessions.len() >= max
950            {
951                return None;
952            }
953
954            let session = Arc::new(Session::new(
955                router,
956                self.sampling_enabled,
957                service_factory,
958                self.events.clone(),
959            ));
960            // Pre-initialized sessions bypass the full MCP handshake (they
961            // exist for clients that don't track session IDs). Mark the
962            // notification as already received so strict_initialization checks
963            // don't reject their requests.
964            session
965                .initialized_notification_received
966                .store(true, Ordering::Release);
967            sessions.insert(session.id.clone(), session.clone());
968            tracing::debug!(session_id = %session.id, "Created pre-initialized session (optional_sessions)");
969            session
970        };
971        self.persist_new(&session).await;
972        Some(session)
973    }
974
975    /// Create a pre-initialized session from a boxed service.
976    async fn create_initialized_from_service(
977        &self,
978        service: McpBoxService,
979    ) -> Option<Arc<Session>> {
980        let session = {
981            let mut sessions = self.sessions.write().await;
982
983            if let Some(max) = self.config.max_sessions
984                && sessions.len() >= max
985            {
986                return None;
987            }
988
989            let session = Arc::new(Session::from_service(service, self.events.clone()));
990            // Pre-initialized sessions bypass the full MCP handshake; mark the
991            // notification as already received.
992            session
993                .initialized_notification_received
994                .store(true, Ordering::Release);
995            sessions.insert(session.id.clone(), session.clone());
996            tracing::debug!(session_id = %session.id, "Created pre-initialized session from service (optional_sessions)");
997            session
998        };
999        self.persist_new(&session).await;
1000        Some(session)
1001    }
1002
1003    async fn get(&self, id: &str) -> Option<Arc<Session>> {
1004        // Fast path: the session is live in this process.
1005        {
1006            let sessions = self.sessions.read().await;
1007            if let Some(s) = sessions.get(id).cloned() {
1008                s.touch().await;
1009                return Some(s);
1010            }
1011        }
1012
1013        // Slow path #1: the session is unknown locally but the persistent
1014        // store has a record — rebuild it.
1015        match self.persistent.load(id).await {
1016            Ok(Some(record)) => {
1017                tracing::info!(session_id = %id, "Restoring session from persistent store");
1018                if let Some(session) = self.restore_from_record(record).await {
1019                    return Some(session);
1020                }
1021            }
1022            Ok(None) => {}
1023            Err(e) => {
1024                tracing::warn!(session_id = %id, error = %e, "Failed to load session record");
1025            }
1026        }
1027
1028        // Slow path #2 (opt-in): auto-reinitialize with synthetic client
1029        // info so the client can continue without a re-handshake. Useful
1030        // for single-instance restarts where no external store is
1031        // configured; loses original client identity.
1032        if self.auto_reinit {
1033            tracing::info!(session_id = %id, "Auto-reinitializing unknown session");
1034            return self.auto_reinitialize(id).await;
1035        }
1036
1037        None
1038    }
1039
1040    /// Restore a live [`Session`] from a persisted [`SessionRecord`].
1041    ///
1042    /// The caller must ensure the record's ID is not already live locally;
1043    /// on success the session is inserted into the local registry, the
1044    /// event counter is seeded so new event IDs don't collide with
1045    /// buffered ones, and the record's `last_accessed` is refreshed and
1046    /// saved back to the store.
1047    async fn restore_from_record(
1048        &self,
1049        record: crate::session_store::SessionRecord,
1050    ) -> Option<Arc<Session>> {
1051        let session = {
1052            let mut sessions = self.sessions.write().await;
1053
1054            if let Some(max) = self.config.max_sessions
1055                && sessions.len() >= max
1056            {
1057                tracing::warn!(
1058                    max_sessions = max,
1059                    "Session limit reached, cannot restore session"
1060                );
1061                return None;
1062            }
1063
1064            // Guard against a concurrent create that beat us here.
1065            if let Some(existing) = sessions.get(&record.id).cloned() {
1066                existing.touch().await;
1067                return Some(existing);
1068            }
1069
1070            let session: Arc<Session> = match &self.service_source {
1071                ServiceSource::Router { router, factory } => Arc::new(Session::restored(
1072                    &record,
1073                    router.with_fresh_session(),
1074                    self.sampling_enabled,
1075                    factory.clone(),
1076                    self.events.clone(),
1077                )),
1078                ServiceSource::Service(svc) => {
1079                    let service = svc.lock().unwrap().clone();
1080                    Arc::new(Session::from_service_restored(
1081                        service,
1082                        &record,
1083                        self.events.clone(),
1084                    ))
1085                }
1086            };
1087
1088            sessions.insert(record.id.clone(), session.clone());
1089            tracing::debug!(session_id = %session.id, "Restored session into local registry");
1090            session
1091        };
1092
1093        // Seed the event counter past the highest buffered event ID so new
1094        // SSE events don't collide with ones the client may still replay.
1095        if let Ok(events) = self.events.replay_after(&record.id, 0).await
1096            && let Some(max_id) = events.iter().map(|e| e.id).max()
1097        {
1098            session
1099                .event_counter
1100                .store(max_id + 1, std::sync::atomic::Ordering::SeqCst);
1101        }
1102
1103        // Refresh last_accessed in the store so the record doesn't expire
1104        // immediately after restore.
1105        let mut refreshed = record;
1106        refreshed.touch(self.config.ttl);
1107        if let Err(e) = self.persistent.save(&refreshed).await {
1108            tracing::warn!(session_id = %refreshed.id, error = %e, "Failed to refresh restored session record");
1109        }
1110
1111        Some(session)
1112    }
1113
1114    /// Create a new session with the requested ID and synthetic client
1115    /// info, skipping the initialize handshake. Used when `auto_reinit`
1116    /// is enabled and no stored record exists.
1117    ///
1118    /// Loses the original client's identity and capabilities — the server
1119    /// sees a session from client `"auto-recovered"`.
1120    async fn auto_reinitialize(&self, id: &str) -> Option<Arc<Session>> {
1121        let mut record = crate::session_store::SessionRecord::new(
1122            id.to_string(),
1123            LATEST_PROTOCOL_VERSION.to_string(),
1124            self.config.ttl,
1125        );
1126        record.client_info = Some(crate::protocol::Implementation {
1127            name: "auto-recovered".into(),
1128            version: "unknown".into(),
1129            title: None,
1130            description: None,
1131            icons: None,
1132            website_url: None,
1133            meta: None,
1134        });
1135        record.client_capabilities = Some(crate::protocol::ClientCapabilities::default());
1136
1137        // Persist first so a concurrent request sees the record. Ignore
1138        // persistence errors; the in-memory session will still work.
1139        if let Err(e) = self.persistent.create(&mut record).await {
1140            tracing::warn!(session_id = %id, error = %e, "Failed to persist auto-reinitialized session");
1141        }
1142
1143        self.restore_from_record(record).await
1144    }
1145
1146    async fn remove(&self, id: &str) -> bool {
1147        let removed = {
1148            let mut sessions = self.sessions.write().await;
1149            sessions.remove(id).is_some()
1150        };
1151        if removed {
1152            tracing::debug!(session_id = %id, "Removed session");
1153            if let Err(e) = self.persistent.delete(id).await {
1154                tracing::warn!(session_id = %id, error = %e, "Failed to delete session record");
1155            }
1156            if let Err(e) = self.events.purge_session(id).await {
1157                tracing::warn!(session_id = %id, error = %e, "Failed to purge session events");
1158            }
1159        }
1160        removed
1161    }
1162
1163    /// Send a pre-serialized JSON notification to every live session's SSE
1164    /// broadcast channel.
1165    ///
1166    /// Used by the external-notification fan-out task. Failures to send
1167    /// (no SSE subscribers attached to a session yet) are silent — the
1168    /// broadcast channel drops the message naturally.
1169    async fn broadcast_to_all(&self, json: &str) {
1170        let sessions = self.sessions.read().await;
1171        for session in sessions.values() {
1172            let _ = session.notifications_tx.send(json.to_string());
1173        }
1174    }
1175
1176    /// Remove expired sessions, returns count of removed sessions
1177    async fn cleanup_expired(&self) -> usize {
1178        let expired = {
1179            let mut sessions = self.sessions.write().await;
1180            let ttl = self.config.ttl;
1181
1182            let mut expired = Vec::new();
1183            for (id, session) in sessions.iter() {
1184                if session.is_expired(ttl).await {
1185                    expired.push(id.clone());
1186                }
1187            }
1188
1189            for id in &expired {
1190                sessions.remove(id);
1191                tracing::debug!(session_id = %id, "Expired session removed");
1192            }
1193
1194            if !expired.is_empty() {
1195                tracing::info!(
1196                    expired_count = expired.len(),
1197                    remaining = sessions.len(),
1198                    "Session cleanup completed"
1199                );
1200            }
1201            expired
1202        };
1203
1204        for id in &expired {
1205            if let Err(e) = self.persistent.delete(id).await {
1206                tracing::warn!(session_id = %id, error = %e, "Failed to delete expired session record");
1207            }
1208            if let Err(e) = self.events.purge_session(id).await {
1209                tracing::warn!(session_id = %id, error = %e, "Failed to purge expired session events");
1210            }
1211        }
1212
1213        expired.len()
1214    }
1215}
1216
1217/// Metadata about an active session.
1218///
1219/// Returned by [`SessionHandle::list_sessions()`].
1220#[derive(Debug, Clone)]
1221pub struct SessionInfo {
1222    /// The session ID.
1223    pub id: String,
1224    /// How long ago this session was created.
1225    pub created_at: Duration,
1226    /// How long ago this session was last accessed.
1227    pub last_activity: Duration,
1228}
1229
1230/// A handle for managing HTTP transport sessions and final subscription streams.
1231///
1232/// Obtained from [`HttpTransport::into_router_with_handle()`] or
1233/// [`HttpTransport::into_router_at_with_handle()`]. The handle is cheap to
1234/// clone and can be shared across threads.
1235///
1236/// # Example
1237///
1238/// ```rust,ignore
1239/// use tower_mcp::transport::http::HttpTransport;
1240///
1241/// let transport = HttpTransport::new(router);
1242/// let (router, handle) = transport.into_router_with_handle();
1243///
1244/// // Later, in an admin endpoint:
1245/// let count = handle.session_count().await;
1246/// for info in handle.list_sessions().await {
1247///     println!("{}: created {:?} ago", info.id, info.created_at);
1248/// }
1249/// handle.terminate_session("session-id").await;
1250///
1251/// // During graceful server shutdown (with the `stateless` feature):
1252/// handle.close_subscriptions();
1253/// ```
1254#[derive(Clone)]
1255pub struct SessionHandle {
1256    store: Arc<SessionRegistry>,
1257    #[cfg(feature = "stateless")]
1258    modern_subscriptions: Arc<ModernSubscriptionRegistry>,
1259}
1260
1261impl SessionHandle {
1262    /// Returns the number of currently active sessions.
1263    pub async fn session_count(&self) -> usize {
1264        self.store.sessions.read().await.len()
1265    }
1266
1267    /// Returns metadata for all active sessions.
1268    pub async fn list_sessions(&self) -> Vec<SessionInfo> {
1269        let sessions = self.store.sessions.read().await;
1270        let mut infos = Vec::with_capacity(sessions.len());
1271        for session in sessions.values() {
1272            let last_accessed = session.last_accessed.read().await;
1273            infos.push(SessionInfo {
1274                id: session.id.clone(),
1275                created_at: session.created_at.elapsed(),
1276                last_activity: last_accessed.elapsed(),
1277            });
1278        }
1279        infos
1280    }
1281
1282    /// Terminates a session by ID, returning `true` if the session existed.
1283    pub async fn terminate_session(&self, id: &str) -> bool {
1284        self.store.remove(id).await
1285    }
1286
1287    /// Returns the number of active final-protocol subscription streams.
1288    #[cfg(feature = "stateless")]
1289    pub fn subscription_count(&self) -> usize {
1290        self.modern_subscriptions.len()
1291    }
1292
1293    /// Gracefully finish every active final-protocol subscription stream.
1294    ///
1295    /// Each stream receives its terminal `SubscriptionsListenResult` before
1296    /// closing. Returns the number of streams that were drained.
1297    #[cfg(feature = "stateless")]
1298    pub fn close_subscriptions(&self) -> usize {
1299        self.modern_subscriptions.close_all()
1300    }
1301}
1302
1303/// The source of the MCP service for session creation.
1304#[derive(Clone)]
1305enum ServiceSource {
1306    /// Created from an McpRouter with a factory for middleware wrapping.
1307    Router {
1308        router: McpRouter,
1309        factory: ServiceFactory,
1310    },
1311    /// Created from a pre-built boxed service (e.g., McpProxy).
1312    /// Wrapped in Arc<Mutex<_>> because BoxCloneService is Send but not Sync.
1313    Service(Arc<std::sync::Mutex<McpBoxService>>),
1314}
1315
1316/// Shared state for the HTTP transport
1317struct AppState {
1318    /// Source for creating new session services
1319    service_source: ServiceSource,
1320    /// Exact protocol versions accepted and advertised by this transport.
1321    protocol_support: ProtocolSupport,
1322    /// Session store
1323    sessions: Arc<SessionRegistry>,
1324    /// Whether to validate Origin header
1325    validate_origin: bool,
1326    /// Allowed origins (if validation is enabled)
1327    allowed_origins: Vec<String>,
1328    /// Whether to validate Host header (defense against direct DNS rebinding)
1329    validate_host: bool,
1330    /// Allowed hosts (host:port). Localhost variants are always allowed.
1331    allowed_hosts: Vec<String>,
1332    /// Whether sessions are optional (for clients that don't track session IDs)
1333    optional_sessions: bool,
1334    /// Whether to enforce `notifications/initialized` before tool dispatch
1335    /// (see [`SessionConfig::strict_initialization`]).
1336    strict_initialization: bool,
1337    /// SEP-1442 stateless mode configuration
1338    #[cfg(feature = "stateless")]
1339    stateless_config: Option<crate::stateless::StatelessConfig>,
1340    /// Whether to stamp server identity into `_meta` on 2026-07-28 stateless
1341    /// responses (see [`HttpTransport::stamp_server_info()`]).
1342    #[cfg(feature = "stateless")]
1343    stamp_server_info: bool,
1344    /// Active final-protocol `subscriptions/listen` streams.
1345    #[cfg(feature = "stateless")]
1346    modern_subscriptions: Arc<ModernSubscriptionRegistry>,
1347    /// Whether to wrap synchronous responses in SSE format (rmcp compat)
1348    sse_responses: bool,
1349    /// Maximum accepted POST body size in bytes
1350    max_body_size: usize,
1351}
1352
1353#[cfg(feature = "stateless")]
1354struct ModernSubscription {
1355    subscription_id: RequestId,
1356    filter: SubscriptionFilter,
1357    tx: mpsc::UnboundedSender<String>,
1358    started: std::time::Instant,
1359}
1360
1361/// Process-local registry for sessionless final-protocol subscriptions.
1362///
1363/// The 2026-07-28 transport deliberately has no session or replay state.
1364/// Each listen POST owns one sender, removed when its response stream drops.
1365#[cfg(feature = "stateless")]
1366struct ModernSubscriptionRegistry {
1367    next_key: AtomicU64,
1368    subscriptions: std::sync::Mutex<HashMap<u64, ModernSubscription>>,
1369    server_info: Option<Implementation>,
1370    observer: Option<Arc<dyn crate::transport::subscriptions::SubscriptionObserver>>,
1371}
1372
1373#[cfg(feature = "stateless")]
1374impl ModernSubscriptionRegistry {
1375    fn new(
1376        server_info: Option<Implementation>,
1377        observer: Option<Arc<dyn crate::transport::subscriptions::SubscriptionObserver>>,
1378    ) -> Self {
1379        Self {
1380            next_key: AtomicU64::new(0),
1381            subscriptions: std::sync::Mutex::new(HashMap::new()),
1382            server_info,
1383            observer,
1384        }
1385    }
1386
1387    fn observe_close(
1388        &self,
1389        subscription: &ModernSubscription,
1390        reason: crate::transport::subscriptions::SubscriptionCloseReason,
1391    ) {
1392        if let Some(observer) = &self.observer {
1393            observer.on_close(crate::transport::subscriptions::SubscriptionClose {
1394                subscription_id: subscription.subscription_id.clone(),
1395                reason,
1396                duration: subscription.started.elapsed(),
1397            });
1398        }
1399    }
1400
1401    fn register(
1402        self: &Arc<Self>,
1403        subscription_id: RequestId,
1404        filter: SubscriptionFilter,
1405    ) -> (mpsc::UnboundedReceiver<String>, ModernSubscriptionGuard) {
1406        let key = self.next_key.fetch_add(1, Ordering::Relaxed);
1407        let (tx, rx) = mpsc::unbounded_channel();
1408        self.subscriptions.lock().unwrap().insert(
1409            key,
1410            ModernSubscription {
1411                subscription_id,
1412                filter,
1413                tx,
1414                started: std::time::Instant::now(),
1415            },
1416        );
1417        (
1418            rx,
1419            ModernSubscriptionGuard {
1420                key,
1421                registry: self.clone(),
1422            },
1423        )
1424    }
1425
1426    /// Route subscription-scoped notifications and return whether the
1427    /// notification belongs exclusively on listen streams.
1428    fn publish(&self, notification: &ServerNotification) -> bool {
1429        let subscription_scoped = matches!(
1430            notification,
1431            ServerNotification::ResourceUpdated { .. }
1432                | ServerNotification::ResourcesListChanged
1433                | ServerNotification::ToolsListChanged
1434                | ServerNotification::PromptsListChanged
1435                | ServerNotification::FinalTaskStatusChanged(_)
1436        );
1437        if !subscription_scoped {
1438            return false;
1439        }
1440
1441        let mut subscriptions = self.subscriptions.lock().unwrap();
1442        tracing::trace!(
1443            active_subscriptions = subscriptions.len(),
1444            notification = ?notification,
1445            "Routing final-protocol subscription notification"
1446        );
1447        subscriptions.retain(|_, subscription| {
1448            let keep = if subscription_matches(notification, &subscription.filter)
1449                && let Some(json) =
1450                    tagged_subscription_notification(notification, &subscription.subscription_id)
1451            {
1452                subscription.tx.send(json).is_ok()
1453            } else {
1454                !subscription.tx.is_closed()
1455            };
1456            if !keep {
1457                // Detected here rather than at guard drop: the entry is
1458                // removed exactly once, so the close is reported exactly once.
1459                self.observe_close(
1460                    subscription,
1461                    crate::transport::subscriptions::SubscriptionCloseReason::Disconnected,
1462                );
1463            }
1464            keep
1465        });
1466        true
1467    }
1468
1469    fn len(&self) -> usize {
1470        self.subscriptions.lock().unwrap().len()
1471    }
1472
1473    /// Gracefully finish every active HTTP listen stream.
1474    fn close_all(&self) -> usize {
1475        let subscriptions = {
1476            let mut active = self.subscriptions.lock().unwrap();
1477            active
1478                .drain()
1479                .map(|(_, subscription)| subscription)
1480                .collect::<Vec<_>>()
1481        };
1482        let count = subscriptions.len();
1483        for subscription in subscriptions {
1484            self.observe_close(
1485                &subscription,
1486                crate::transport::subscriptions::SubscriptionCloseReason::Drained,
1487            );
1488            let response = subscription_complete_response(
1489                subscription.subscription_id,
1490                self.server_info.clone(),
1491            );
1492            if let Ok(json) = serde_json::to_string(&response) {
1493                let _ = subscription.tx.send(json);
1494            }
1495        }
1496        count
1497    }
1498}
1499
1500#[cfg(feature = "stateless")]
1501impl Default for ModernSubscriptionRegistry {
1502    fn default() -> Self {
1503        Self::new(None, None)
1504    }
1505}
1506
1507#[cfg(feature = "stateless")]
1508struct ModernSubscriptionGuard {
1509    key: u64,
1510    registry: Arc<ModernSubscriptionRegistry>,
1511}
1512
1513#[cfg(feature = "stateless")]
1514impl Drop for ModernSubscriptionGuard {
1515    fn drop(&mut self) {
1516        let removed = self
1517            .registry
1518            .subscriptions
1519            .lock()
1520            .unwrap()
1521            .remove(&self.key);
1522        if let Some(subscription) = removed {
1523            self.registry.observe_close(
1524                &subscription,
1525                crate::transport::subscriptions::SubscriptionCloseReason::Disconnected,
1526            );
1527        }
1528    }
1529}
1530
1531/// Configuration for OAuth 2.1 Protected Resource Metadata.
1532///
1533/// When set on [`HttpTransport`], a `GET` endpoint is added at the resource's
1534/// path-aware RFC 9728 well-known location.
1535#[cfg(feature = "oauth")]
1536#[derive(Clone)]
1537pub(crate) struct OAuthConfig {
1538    /// Protected Resource Metadata to serve at the well-known endpoint.
1539    pub(crate) metadata: crate::oauth::ProtectedResourceMetadata,
1540}
1541
1542/// HTTP transport for MCP servers
1543///
1544/// Implements the Streamable HTTP transport from the MCP specification.
1545///
1546/// # Construction
1547///
1548/// There are two ways to create an `HttpTransport`:
1549///
1550/// - [`HttpTransport::new(router)`](HttpTransport::new) — wraps an [`McpRouter`], with full
1551///   support for per-session notification bridging, sampling, and `.layer()` middleware.
1552///
1553/// - [`HttpTransport::from_service(service)`](HttpTransport::from_service) — wraps any
1554///   `Service<RouterRequest>` (e.g., [`McpProxy`](crate::proxy::McpProxy)). The service is
1555///   cloned for each session. Notification bridging and sampling are not set up automatically;
1556///   the caller should configure these on the service before passing it in.
1557///   `.layer()` is not supported in this mode.
1558pub struct HttpTransport {
1559    service_source: ServiceSource,
1560    protocol_support: ProtocolSupport,
1561    validate_origin: bool,
1562    allowed_origins: Vec<String>,
1563    validate_host: bool,
1564    allowed_hosts: Vec<String>,
1565    session_config: SessionConfig,
1566    sampling_enabled: bool,
1567    optional_sessions: bool,
1568    session_store: Arc<dyn crate::session_store::SessionStore>,
1569    event_store: Arc<dyn crate::event_store::EventStore>,
1570    auto_reinit_sessions: bool,
1571    /// Caller-owned receiver for notifications pushed from outside any
1572    /// request handler. Drained by a background task and fanned out to
1573    /// every live session's SSE stream.
1574    external_notifications: Option<NotificationReceiver>,
1575    #[cfg(feature = "stateless")]
1576    stateless_config: Option<crate::stateless::StatelessConfig>,
1577    /// When true, 2026-07-28 stateless responses carry server identity in
1578    /// `_meta["io.modelcontextprotocol/serverInfo"]`.
1579    ///
1580    /// See [`HttpTransport::stamp_server_info()`] for details.
1581    #[cfg(feature = "stateless")]
1582    stamp_server_info: bool,
1583    #[cfg(feature = "oauth")]
1584    oauth_config: Option<OAuthConfig>,
1585    /// When true, synchronous JSON-RPC responses are wrapped in SSE format.
1586    ///
1587    /// See [`HttpTransport::sse_responses()`] for details.
1588    sse_responses: bool,
1589    /// Maximum accepted POST body size in bytes.
1590    ///
1591    /// See [`HttpTransport::max_body_size()`] for details.
1592    max_body_size: usize,
1593}
1594
1595impl HttpTransport {
1596    /// Create a new HTTP transport wrapping an MCP router.
1597    ///
1598    /// Supports per-session notification bridging, sampling, and `.layer()` middleware.
1599    pub fn new(router: McpRouter) -> Self {
1600        Self {
1601            service_source: ServiceSource::Router {
1602                router,
1603                factory: identity_factory(),
1604            },
1605            protocol_support: ProtocolSupport::default(),
1606            validate_origin: true,
1607            allowed_origins: vec![],
1608            validate_host: true,
1609            allowed_hosts: vec![],
1610            session_config: SessionConfig::default(),
1611            sampling_enabled: false,
1612            optional_sessions: true,
1613            session_store: Arc::new(crate::session_store::MemorySessionStore::new()),
1614            event_store: Arc::new(crate::event_store::MemoryEventStore::new()),
1615            auto_reinit_sessions: false,
1616            external_notifications: None,
1617            #[cfg(feature = "stateless")]
1618            stateless_config: None,
1619            #[cfg(feature = "stateless")]
1620            stamp_server_info: true,
1621            #[cfg(feature = "oauth")]
1622            oauth_config: None,
1623            sse_responses: false,
1624            max_body_size: DEFAULT_MAX_BODY_SIZE,
1625        }
1626    }
1627
1628    /// Create an HTTP transport from a pre-built service.
1629    ///
1630    /// This accepts any `Service<RouterRequest>` implementation, such as
1631    /// [`McpProxy`](crate::proxy::McpProxy). The service is cloned for each
1632    /// HTTP session.
1633    ///
1634    /// Notification bridging and sampling are **not** set up automatically.
1635    /// The caller should configure these on the service before passing it in.
1636    ///
1637    /// `.layer()` is not supported when using `from_service()` — wrap the
1638    /// service with middleware before passing it in.
1639    ///
1640    /// # Example
1641    ///
1642    /// ```rust,ignore
1643    /// use tower_mcp::transport::http::HttpTransport;
1644    /// use tower_mcp::proxy::McpProxy;
1645    ///
1646    /// let proxy: McpProxy = /* ... */;
1647    /// let transport = HttpTransport::from_service(proxy);
1648    /// transport.serve("127.0.0.1:3000").await?;
1649    /// ```
1650    pub fn from_service<S>(service: S) -> Self
1651    where
1652        S: tower::Service<
1653                RouterRequest,
1654                Response = RouterResponse,
1655                Error = std::convert::Infallible,
1656            > + Clone
1657            + Send
1658            + 'static,
1659        S::Future: Send,
1660    {
1661        Self {
1662            service_source: ServiceSource::Service(Arc::new(std::sync::Mutex::new(
1663                BoxCloneService::new(service),
1664            ))),
1665            protocol_support: ProtocolSupport::default(),
1666            validate_origin: true,
1667            allowed_origins: vec![],
1668            validate_host: true,
1669            allowed_hosts: vec![],
1670            session_config: SessionConfig::default(),
1671            sampling_enabled: false,
1672            optional_sessions: true,
1673            session_store: Arc::new(crate::session_store::MemorySessionStore::new()),
1674            event_store: Arc::new(crate::event_store::MemoryEventStore::new()),
1675            auto_reinit_sessions: false,
1676            external_notifications: None,
1677            #[cfg(feature = "stateless")]
1678            stateless_config: None,
1679            #[cfg(feature = "stateless")]
1680            stamp_server_info: true,
1681            #[cfg(feature = "oauth")]
1682            oauth_config: None,
1683            sse_responses: false,
1684            max_body_size: DEFAULT_MAX_BODY_SIZE,
1685        }
1686    }
1687
1688    /// Create an HTTP transport that drains a caller-owned notification
1689    /// channel and fans the items out to every live session's SSE stream.
1690    ///
1691    /// This mirrors [`GenericStdioTransport::with_notifications`](crate::transport::stdio::GenericStdioTransport::with_notifications)
1692    /// and is the supported way to push server-originated notifications
1693    /// (e.g. `notifications/resources/updated`) from outside any request
1694    /// handler — background tasks, lifecycle hooks, anything async that
1695    /// needs to notify subscribed clients.
1696    ///
1697    /// Per-session notification channels (in-handler `ctx.send_log()`,
1698    /// progress updates) are unaffected. The external channel runs in
1699    /// parallel and broadcasts to every active session; MCP clients are
1700    /// expected to ignore notifications they didn't subscribe to.
1701    ///
1702    /// # Example
1703    ///
1704    /// ```rust,no_run
1705    /// use tower_mcp::{BoxError, McpRouter};
1706    /// use tower_mcp::context::{ServerNotification, notification_channel};
1707    /// use tower_mcp::transport::http::HttpTransport;
1708    ///
1709    /// #[tokio::main]
1710    /// async fn main() -> Result<(), BoxError> {
1711    ///     let (notif_tx, notif_rx) = notification_channel(256);
1712    ///
1713    ///     let router = McpRouter::new().server_info("my-server", "1.0.0");
1714    ///
1715    ///     // Hold onto notif_tx in your application state so background tasks
1716    ///     // can push notifications. tx is `Clone`.
1717    ///     let pusher = notif_tx.clone();
1718    ///     tokio::spawn(async move {
1719    ///         let _ = pusher.send(ServerNotification::ResourceUpdated {
1720    ///             uri: "claude://chats/123".to_string(),
1721    ///         }).await;
1722    ///     });
1723    ///
1724    ///     let transport = HttpTransport::with_notifications(router, notif_rx);
1725    ///     transport.serve("127.0.0.1:3000").await?;
1726    ///     Ok(())
1727    /// }
1728    /// ```
1729    pub fn with_notifications(router: McpRouter, notification_rx: NotificationReceiver) -> Self {
1730        Self {
1731            external_notifications: Some(notification_rx),
1732            ..Self::new(router)
1733        }
1734    }
1735
1736    /// Attach a caller-owned notification receiver after construction.
1737    ///
1738    /// Useful when wrapping a pre-built service via
1739    /// [`from_service`](Self::from_service), where setting a sender on the
1740    /// router isn't part of the flow. See [`with_notifications`](Self::with_notifications)
1741    /// for the typical router-based path.
1742    pub fn external_notifications(mut self, notification_rx: NotificationReceiver) -> Self {
1743        self.external_notifications = Some(notification_rx);
1744        self
1745    }
1746
1747    /// Enable sampling support for this transport.
1748    ///
1749    /// When sampling is enabled, tool handlers can use `ctx.sample()` to
1750    /// request LLM completions from connected clients. The server sends each
1751    /// request on the SSE response stream of the POST that caused it, and the
1752    /// client responds via a separate POST. These associated streams are not
1753    /// replayed; use session affinity while a request is in flight.
1754    ///
1755    /// # Example
1756    ///
1757    /// ```rust,no_run
1758    /// use tower_mcp::{BoxError, McpRouter, ToolBuilder, CallToolResult, CreateMessageParams, SamplingMessage};
1759    /// use tower_mcp::extract::{Context, RawArgs};
1760    /// use tower_mcp::transport::http::HttpTransport;
1761    ///
1762    /// #[tokio::main]
1763    /// async fn main() -> Result<(), BoxError> {
1764    ///     let tool = ToolBuilder::new("ai-tool")
1765    ///         .extractor_handler((), |ctx: Context, RawArgs(_): RawArgs| async move {
1766    ///             // Request LLM completion from client
1767    ///             let params = CreateMessageParams::new(
1768    ///                 vec![SamplingMessage::user("Summarize this...")],
1769    ///                 500,
1770    ///             );
1771    ///             let result = ctx.sample(params).await?;
1772    ///             Ok(CallToolResult::text(format!("{:?}", result.content)))
1773    ///         })
1774    ///         .build();
1775    ///
1776    ///     let router = McpRouter::new()
1777    ///         .server_info("my-server", "1.0.0")
1778    ///         .tool(tool);
1779    ///
1780    ///     let transport = HttpTransport::new(router).with_sampling();
1781    ///     transport.serve("127.0.0.1:3000").await?;
1782    ///     Ok(())
1783    /// }
1784    /// ```
1785    pub fn with_sampling(mut self) -> Self {
1786        self.sampling_enabled = true;
1787        self
1788    }
1789
1790    /// Require strict session management.
1791    ///
1792    /// When enabled, requests without an `mcp-session-id` header are rejected
1793    /// with a `SessionRequired` error (-32006). Clients must complete the
1794    /// `initialize` handshake and include the session ID on all subsequent
1795    /// requests, as specified by the MCP 2025-11-25 spec.
1796    ///
1797    /// By default, sessions are optional for compatibility with clients
1798    /// (Codex CLI, Cursor, etc.) that don't carry the session ID forward
1799    /// after initialization.
1800    ///
1801    /// # Example
1802    ///
1803    /// ```rust,no_run
1804    /// use tower_mcp::McpRouter;
1805    /// use tower_mcp::transport::http::HttpTransport;
1806    ///
1807    /// #[tokio::main]
1808    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
1809    ///     let router = McpRouter::new().server_info("my-server", "1.0.0");
1810    ///     let transport = HttpTransport::new(router).require_sessions();
1811    ///     transport.serve("127.0.0.1:3000").await?;
1812    ///     Ok(())
1813    /// }
1814    /// ```
1815    pub fn require_sessions(mut self) -> Self {
1816        self.optional_sessions = false;
1817        self
1818    }
1819
1820    /// Set the exact protocol versions this transport accepts and advertises.
1821    ///
1822    /// By default, every protocol implementation compiled into tower-mcp is
1823    /// enabled. This setting can narrow that set per server instance. Versions
1824    /// are advertised by `server/discover` in the order supplied.
1825    pub fn protocol_support(mut self, support: ProtocolSupport) -> Self {
1826        self.protocol_support = support;
1827        self
1828    }
1829
1830    /// Construct and set an exact runtime protocol-version allow-list.
1831    ///
1832    /// Returns an error when the list is empty, duplicated, or names a version
1833    /// whose Cargo feature was not compiled.
1834    pub fn protocol_versions<I, S>(
1835        mut self,
1836        versions: I,
1837    ) -> std::result::Result<Self, ProtocolSupportError>
1838    where
1839        I: IntoIterator<Item = S>,
1840        S: Into<String>,
1841    {
1842        self.protocol_support = ProtocolSupport::try_new(versions)?;
1843        Ok(self)
1844    }
1845
1846    /// Enable SSE-wrapping for synchronous JSON-RPC responses.
1847    ///
1848    /// When enabled, synchronous responses (initialize, tools/list, tools/call, etc.)
1849    /// are returned with `Content-Type: text/event-stream` and formatted as an SSE
1850    /// message event:
1851    ///
1852    /// ```text
1853    /// event: message
1854    /// data: {"jsonrpc":"2.0","id":1,"result":{...}}
1855    ///
1856    /// ```
1857    ///
1858    /// This matches the behavior of rmcp's `StreamableHttpService`, which always uses
1859    /// SSE format for all responses. The MCP Streamable HTTP spec allows both bare
1860    /// JSON and SSE for synchronous responses; this option is provided for
1861    /// compatibility with clients that expect rmcp's SSE-always behavior.
1862    ///
1863    /// **Known divergence from rmcp:** rmcp's `StreamableHttpService` always uses SSE
1864    /// for synchronous responses by default. tower-mcp defaults to bare JSON (the
1865    /// spec-correct choice, matching the SHOULD in the 2025-11-25 spec). Use
1866    /// `.sse_responses(true)` to match rmcp's behavior when targeting clients
1867    /// written against rmcp.
1868    ///
1869    /// The existing SSE notification stream (GET `/`) and `subscriptions/listen` stream
1870    /// (2026-07-28+) are unaffected by this flag.
1871    ///
1872    /// Default: `false` (bare JSON, `Content-Type: application/json`).
1873    ///
1874    /// # Example
1875    ///
1876    /// ```rust,ignore
1877    /// let transport = HttpTransport::new(router).sse_responses(true);
1878    /// ```
1879    pub fn sse_responses(mut self, enabled: bool) -> Self {
1880        self.sse_responses = enabled;
1881        self
1882    }
1883
1884    /// Whether 2026-07-28 stateless responses carry server identity in
1885    /// `_meta["io.modelcontextprotocol/serverInfo"]`.
1886    ///
1887    /// Per SEP-2575, servers SHOULD identify themselves in each result's
1888    /// `_meta` "unless specifically configured not to do so" -- this is that
1889    /// configuration. Only applies to the version-gated 2026-07-28 stateless
1890    /// dispatch path (`stateless` feature); other protocol versions and
1891    /// transports are unaffected, and identity there is carried by
1892    /// `initialize`'s top-level `serverInfo` instead.
1893    ///
1894    /// Only takes effect when the transport was built from an [`McpRouter`]
1895    /// (`HttpTransport::new`); a transport built from a pre-built service
1896    /// (`HttpTransport::from_service`) has no router to read identity from
1897    /// and never stamps, regardless of this setting.
1898    ///
1899    /// Default: `true`.
1900    ///
1901    /// # Example
1902    ///
1903    /// ```rust,ignore
1904    /// let transport = HttpTransport::new(router).stamp_server_info(false);
1905    /// ```
1906    #[cfg(feature = "stateless")]
1907    pub fn stamp_server_info(mut self, enabled: bool) -> Self {
1908        self.stamp_server_info = enabled;
1909        self
1910    }
1911
1912    /// Set the maximum accepted POST body size in bytes.
1913    ///
1914    /// Requests whose body exceeds the limit are rejected with HTTP 413
1915    /// (Payload Too Large) before any JSON parsing or dispatch happens.
1916    /// A `Content-Length` header above the limit short-circuits without
1917    /// reading the body; chunked bodies are capped while streaming.
1918    ///
1919    /// Default: 4 MiB ([`DEFAULT_MAX_BODY_SIZE`]), matching rmcp.
1920    ///
1921    /// # Interplay with axum's `DefaultBodyLimit`
1922    ///
1923    /// axum's built-in [`DefaultBodyLimit`](axum::extract::DefaultBodyLimit)
1924    /// (2 MB by default) only applies to body-consuming extractors such as
1925    /// `Bytes`, `String`, and `Json`. The MCP endpoint consumes the raw
1926    /// [`Request`](axum::extract::Request) and reads the body itself, so
1927    /// `DefaultBodyLimit` never applies to it; this transport-level limit
1928    /// is the only bound on the MCP POST body. Layering
1929    /// `DefaultBodyLimit` onto the router returned by
1930    /// [`into_router`](Self::into_router) does not change the MCP
1931    /// endpoint's behavior.
1932    ///
1933    /// # Example
1934    ///
1935    /// ```rust,no_run
1936    /// use tower_mcp::McpRouter;
1937    /// use tower_mcp::transport::http::HttpTransport;
1938    ///
1939    /// let router = McpRouter::new().server_info("my-server", "1.0.0");
1940    /// // Accept request bodies up to 1 MiB.
1941    /// let transport = HttpTransport::new(router).max_body_size(1024 * 1024);
1942    /// ```
1943    pub fn max_body_size(mut self, bytes: usize) -> Self {
1944        self.max_body_size = bytes;
1945        self
1946    }
1947
1948    /// Enable the legacy SEP-1442 stateless opt-in path.
1949    ///
1950    /// This activates the SEP-1442-style stateless behavior for clients that
1951    /// do NOT send `MCP-Protocol-Version: 2026-07-28`. Specifically, when a
1952    /// [`crate::stateless::StatelessConfig`] is set:
1953    ///
1954    /// - Requests without a session ID can be served without an initialize
1955    ///   handshake (if [`crate::stateless::StatelessConfig::optional_sessions`]
1956    ///   is `true`).
1957    /// - The `server/discover` RPC is enabled (if
1958    ///   [`crate::stateless::StatelessConfig::enable_discover`] is `true`).
1959    /// - Protocol version may be required in every request body (if
1960    ///   [`crate::stateless::StatelessConfig::require_protocol_version`] is `true`).
1961    ///
1962    /// **Note:** this method does NOT control the automatic version-gated
1963    /// stateless path for 2026-07-28+ clients. When the `stateless` feature
1964    /// is compiled in, any request with `MCP-Protocol-Version: 2026-07-28`
1965    /// and no `mcp-session-id` is dispatched statelessly regardless of
1966    /// whether this method is called. See the [`crate::stateless`] module
1967    /// documentation for the full two-path explanation.
1968    ///
1969    /// Stateful clients (those that send `mcp-session-id`) continue to work
1970    /// normally on the same transport.
1971    ///
1972    /// # Example
1973    ///
1974    /// ```rust,no_run
1975    /// use tower_mcp::McpRouter;
1976    /// use tower_mcp::transport::http::HttpTransport;
1977    /// use tower_mcp::stateless::StatelessConfig;
1978    ///
1979    /// #[tokio::main]
1980    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
1981    ///     let router = McpRouter::new().server_info("my-server", "1.0.0");
1982    ///     // Enables the SEP-1442 opt-in path. 2026-07-28 clients are
1983    ///     // handled statelessly regardless of this call.
1984    ///     let transport = HttpTransport::new(router)
1985    ///         .stateless(StatelessConfig::new());
1986    ///     transport.serve("127.0.0.1:3000").await?;
1987    ///     Ok(())
1988    /// }
1989    /// ```
1990    #[cfg(feature = "stateless")]
1991    pub fn stateless(mut self, config: crate::stateless::StatelessConfig) -> Self {
1992        self.stateless_config = Some(config);
1993        self
1994    }
1995
1996    /// Disable Origin header validation (not recommended for production)
1997    pub fn disable_origin_validation(mut self) -> Self {
1998        self.validate_origin = false;
1999        self
2000    }
2001
2002    /// Set allowed origins for CORS/security validation
2003    pub fn allowed_origins(mut self, origins: Vec<String>) -> Self {
2004        self.allowed_origins = origins;
2005        self
2006    }
2007
2008    /// Disable Host header validation (not recommended when binding to a
2009    /// non-loopback interface).
2010    ///
2011    /// Host validation is the defense-in-depth pair to Origin validation: it
2012    /// rejects requests whose `Host` header doesn't match the server's
2013    /// expected hostname, blocking direct DNS-rebinding attacks where a
2014    /// malicious site resolves its own domain to `127.0.0.1`.
2015    pub fn disable_host_validation(mut self) -> Self {
2016        self.validate_host = false;
2017        self
2018    }
2019
2020    /// Set allowed hosts for the `Host` header allowlist.
2021    ///
2022    /// Each entry should be a `host:port` pair (e.g. `"api.example.com"`,
2023    /// `"api.example.com:8443"`). Localhost variants (`localhost`,
2024    /// `127.0.0.1`, `::1`, with any port) are always accepted regardless
2025    /// of this list.
2026    ///
2027    /// When the `Host` header is missing, the validator falls back to the
2028    /// HTTP/2 `:authority` pseudo-header from `request.uri().authority()`,
2029    /// since middleware like `axum::Router::nest` can strip the synthesized
2030    /// `Host` header before it reaches our handler.
2031    pub fn allowed_hosts(mut self, hosts: Vec<String>) -> Self {
2032        self.allowed_hosts = hosts;
2033        self
2034    }
2035
2036    /// Configure session management (TTL, max sessions, cleanup interval)
2037    pub fn session_config(mut self, config: SessionConfig) -> Self {
2038        self.session_config = config;
2039        self
2040    }
2041
2042    /// Set session TTL (convenience method)
2043    pub fn session_ttl(mut self, ttl: Duration) -> Self {
2044        self.session_config.ttl = ttl;
2045        self
2046    }
2047
2048    /// Set maximum number of concurrent sessions (convenience method)
2049    pub fn max_sessions(mut self, max: usize) -> Self {
2050        self.session_config.max_sessions = Some(max);
2051        self
2052    }
2053
2054    /// Configure a pluggable [`SessionStore`](crate::session_store::SessionStore)
2055    /// for persisting session metadata.
2056    ///
2057    /// The default is an in-process
2058    /// [`MemorySessionStore`](crate::session_store::MemorySessionStore) —
2059    /// supply an external store (Redis, Postgres, etc.) to share session
2060    /// metadata across server instances behind a load balancer.
2061    ///
2062    /// Runtime state (broadcast channels, pending requests, service
2063    /// instances) is always kept per-instance; only persistent metadata is
2064    /// mirrored to the store.
2065    ///
2066    /// # Example
2067    ///
2068    /// ```rust,no_run
2069    /// use std::sync::Arc;
2070    /// use tower_mcp::{HttpTransport, McpRouter};
2071    /// use tower_mcp::session_store::{MemorySessionStore, SessionStore};
2072    ///
2073    /// let router = McpRouter::new();
2074    /// let store: Arc<dyn SessionStore> = Arc::new(MemorySessionStore::new());
2075    /// let transport = HttpTransport::new(router).session_store(store);
2076    /// ```
2077    pub fn session_store(mut self, store: Arc<dyn crate::session_store::SessionStore>) -> Self {
2078        self.session_store = store;
2079        self
2080    }
2081
2082    /// Configure a pluggable [`EventStore`](crate::event_store::EventStore)
2083    /// for SSE event buffering and stream resumption.
2084    ///
2085    /// The default is an in-process
2086    /// [`MemoryEventStore`](crate::event_store::MemoryEventStore) with a
2087    /// 1000-event ring buffer per session — supply an external store (Redis,
2088    /// etc.) so clients can resume SSE streams after reconnecting to a
2089    /// different server instance behind a load balancer (SEP-1699).
2090    ///
2091    /// Typically paired with a matching
2092    /// [`session_store`](Self::session_store) so both session metadata and
2093    /// buffered events survive across instances.
2094    ///
2095    /// # Example
2096    ///
2097    /// ```rust,no_run
2098    /// use std::sync::Arc;
2099    /// use tower_mcp::{HttpTransport, McpRouter};
2100    /// use tower_mcp::event_store::{EventStore, MemoryEventStore};
2101    ///
2102    /// let router = McpRouter::new();
2103    /// let store: Arc<dyn EventStore> = Arc::new(MemoryEventStore::new());
2104    /// let transport = HttpTransport::new(router).event_store(store);
2105    /// ```
2106    pub fn event_store(mut self, store: Arc<dyn crate::event_store::EventStore>) -> Self {
2107        self.event_store = store;
2108        self
2109    }
2110
2111    /// Enable auto-reinitialization for unknown session IDs.
2112    ///
2113    /// When a request arrives with an `mcp-session-id` that is not live
2114    /// locally and has no record in the configured
2115    /// [`session_store`](Self::session_store), the transport normally
2116    /// returns a session-not-found error. With this flag enabled, the
2117    /// transport instead spins up a new session claiming that ID and
2118    /// completes the initialize handshake internally with synthetic
2119    /// client info (`name = "auto-recovered"`, empty capabilities).
2120    ///
2121    /// This lets tolerant clients continue after a server restart without
2122    /// repeating the handshake, at the cost of losing the original
2123    /// client's identity and negotiated capabilities. Prefer pairing this
2124    /// with a real [`session_store`](Self::session_store) — the store
2125    /// path runs first and preserves full identity when a record exists.
2126    ///
2127    /// Disabled by default. This is the pattern established by
2128    /// [anubis-mcp #125](https://github.com/zoedsoupe/anubis-mcp/pull/125).
2129    ///
2130    /// # Example
2131    ///
2132    /// ```rust,no_run
2133    /// use tower_mcp::{HttpTransport, McpRouter};
2134    ///
2135    /// let router = McpRouter::new();
2136    /// let transport = HttpTransport::new(router).auto_reinitialize_sessions(true);
2137    /// ```
2138    pub fn auto_reinitialize_sessions(mut self, enabled: bool) -> Self {
2139        self.auto_reinit_sessions = enabled;
2140        self
2141    }
2142
2143    /// Configure OAuth 2.1 Protected Resource Metadata for this transport.
2144    ///
2145    /// This lower-level method only serves metadata; it does not install token
2146    /// or scope enforcement. Prefer [`Self::into_oauth_router`] for a complete,
2147    /// fail-closed MCP resource-server setup.
2148    ///
2149    /// # Example
2150    ///
2151    /// ```rust,ignore
2152    /// use tower_mcp::oauth::ProtectedResourceMetadata;
2153    /// use tower_mcp::transport::http::HttpTransport;
2154    /// use tower_mcp::McpRouter;
2155    ///
2156    /// let metadata = ProtectedResourceMetadata::new("https://mcp.example.com")
2157    ///     .authorization_server("https://auth.example.com")
2158    ///     .scope("mcp:read");
2159    ///
2160    /// let router = McpRouter::new().server_info("my-server", "1.0.0");
2161    /// let transport = HttpTransport::new(router).oauth(metadata);
2162    /// ```
2163    #[cfg(feature = "oauth")]
2164    pub fn oauth(mut self, metadata: crate::oauth::ProtectedResourceMetadata) -> Self {
2165        self.oauth_config = Some(OAuthConfig { metadata });
2166        self
2167    }
2168
2169    /// Build a fully protected OAuth resource-server router.
2170    ///
2171    /// This validates the Protected Resource Metadata, serves it at the
2172    /// path-aware RFC 9728 endpoint, validates bearer tokens, independently
2173    /// enforces the token audience against `metadata.resource`, and installs
2174    /// fail-closed per-operation scope enforcement.
2175    ///
2176    /// # Errors
2177    ///
2178    /// Returns an error when the resource metadata is not suitable for an MCP
2179    /// resource server.
2180    #[cfg(feature = "oauth")]
2181    pub fn into_oauth_router<V>(
2182        self,
2183        validator: V,
2184        metadata: crate::oauth::ProtectedResourceMetadata,
2185        policy: crate::oauth::ScopePolicy,
2186    ) -> std::result::Result<Router, crate::oauth::ProtectedResourceMetadataError>
2187    where
2188        V: crate::oauth::TokenValidator,
2189    {
2190        let (router, _) = self.into_oauth_router_with_handle(validator, metadata, policy)?;
2191        Ok(router)
2192    }
2193
2194    /// Build a fully protected OAuth router and return its session handle.
2195    ///
2196    /// This is the session-management variant of [`Self::into_oauth_router`].
2197    #[cfg(feature = "oauth")]
2198    pub fn into_oauth_router_with_handle<V>(
2199        self,
2200        validator: V,
2201        metadata: crate::oauth::ProtectedResourceMetadata,
2202        policy: crate::oauth::ScopePolicy,
2203    ) -> std::result::Result<(Router, SessionHandle), crate::oauth::ProtectedResourceMetadataError>
2204    where
2205        V: crate::oauth::TokenValidator,
2206    {
2207        metadata.validate()?;
2208        let oauth_layer =
2209            crate::oauth::OAuthLayer::new(validator, metadata.clone()).scope_policy(policy.clone());
2210        let transport = self
2211            .layer(crate::oauth::ScopeEnforcementLayer::new(policy))
2212            .oauth(metadata);
2213        let (router, handle) = transport.into_router_with_handle();
2214        Ok((router.layer(oauth_layer), handle))
2215    }
2216
2217    /// Build a fully protected OAuth router mounted at `path`.
2218    ///
2219    /// The metadata route is derived from `metadata.resource`, not from the
2220    /// local mount path, so it remains correct for path-based resource URLs.
2221    #[cfg(feature = "oauth")]
2222    pub fn into_oauth_router_at<V>(
2223        self,
2224        path: &str,
2225        validator: V,
2226        metadata: crate::oauth::ProtectedResourceMetadata,
2227        policy: crate::oauth::ScopePolicy,
2228    ) -> std::result::Result<Router, crate::oauth::ProtectedResourceMetadataError>
2229    where
2230        V: crate::oauth::TokenValidator,
2231    {
2232        let (router, _) =
2233            self.into_oauth_router_at_with_handle(path, validator, metadata, policy)?;
2234        Ok(router)
2235    }
2236
2237    /// Build a path-mounted protected OAuth router and return its session handle.
2238    #[cfg(feature = "oauth")]
2239    pub fn into_oauth_router_at_with_handle<V>(
2240        self,
2241        path: &str,
2242        validator: V,
2243        metadata: crate::oauth::ProtectedResourceMetadata,
2244        policy: crate::oauth::ScopePolicy,
2245    ) -> std::result::Result<(Router, SessionHandle), crate::oauth::ProtectedResourceMetadataError>
2246    where
2247        V: crate::oauth::TokenValidator,
2248    {
2249        metadata.validate()?;
2250        let oauth_layer =
2251            crate::oauth::OAuthLayer::new(validator, metadata.clone()).scope_policy(policy.clone());
2252        let transport = self
2253            .layer(crate::oauth::ScopeEnforcementLayer::new(policy))
2254            .oauth(metadata);
2255        let (router, handle) = transport.into_router_at_with_handle(path);
2256        Ok((router.layer(oauth_layer), handle))
2257    }
2258
2259    /// Apply a tower middleware layer to MCP request processing.
2260    ///
2261    /// # Panics
2262    ///
2263    /// Panics if this transport was created via [`from_service()`](Self::from_service).
2264    /// When using `from_service()`, wrap the service with middleware before passing it in.
2265    pub fn layer<L>(mut self, layer: L) -> Self
2266    where
2267        L: tower::Layer<McpRouter> + Send + Sync + 'static,
2268        L::Service:
2269            tower::Service<RouterRequest, Response = RouterResponse> + Clone + Send + 'static,
2270        <L::Service as tower::Service<RouterRequest>>::Error: std::fmt::Display + Send,
2271        <L::Service as tower::Service<RouterRequest>>::Future: Send,
2272    {
2273        match &mut self.service_source {
2274            ServiceSource::Router { factory, .. } => {
2275                *factory = Arc::new(move |router: McpRouter| {
2276                    let annotations = router.tool_annotations_map();
2277                    let wrapped = layer.layer(router);
2278                    tower::util::BoxCloneService::new(InjectAnnotations::new(
2279                        CatchError::new(wrapped),
2280                        annotations,
2281                    ))
2282                });
2283            }
2284            ServiceSource::Service(_) => {
2285                panic!(
2286                    "layer() cannot be used with from_service() — \
2287                     wrap the service with middleware before passing it in"
2288                );
2289            }
2290        }
2291        self
2292    }
2293
2294    fn build_state(&self) -> Arc<AppState> {
2295        #[cfg(feature = "stateless")]
2296        let modern_subscriptions = Arc::new(ModernSubscriptionRegistry::new(
2297            match &self.service_source {
2298                ServiceSource::Router { router, .. } if self.stamp_server_info => {
2299                    Some(router.implementation())
2300                }
2301                _ => None,
2302            },
2303            match &self.service_source {
2304                ServiceSource::Router { router, .. } => router.subscription_observer(),
2305                ServiceSource::Service(_) => None,
2306            },
2307        ));
2308
2309        // Keep one transport-lifetime notification sender registered with
2310        // dynamic registries. Per-request senders intentionally are not
2311        // registered, but dynamic mutations still need a stable path to all
2312        // active final-protocol listen streams.
2313        #[cfg(feature = "stateless")]
2314        let service_source = match &self.service_source {
2315            ServiceSource::Router { router, factory } => {
2316                let (tx, mut rx) = notification_channel(256);
2317                let direct_subscriptions = modern_subscriptions.clone();
2318                router.attach_modern_notification_sink(Arc::new(move |notification| {
2319                    direct_subscriptions.publish(notification)
2320                }));
2321                let subscriptions = modern_subscriptions.clone();
2322                tokio::spawn(async move {
2323                    while let Some(notification) = rx.recv().await {
2324                        subscriptions.publish(&notification);
2325                    }
2326                });
2327                ServiceSource::Router {
2328                    router: router.clone().with_notification_sender(tx),
2329                    factory: factory.clone(),
2330                }
2331            }
2332            ServiceSource::Service(service) => ServiceSource::Service(service.clone()),
2333        };
2334        #[cfg(not(feature = "stateless"))]
2335        let service_source = self.service_source.clone();
2336
2337        let sessions = Arc::new(SessionRegistry::new(
2338            self.session_config.clone(),
2339            self.sampling_enabled,
2340            self.session_store.clone(),
2341            self.event_store.clone(),
2342            service_source.clone(),
2343            self.auto_reinit_sessions,
2344        ));
2345
2346        // Spawn cleanup task
2347        let cleanup_sessions = sessions.clone();
2348        let cleanup_interval = self.session_config.cleanup_interval;
2349        tokio::spawn(async move {
2350            loop {
2351                tokio::time::sleep(cleanup_interval).await;
2352                cleanup_sessions.cleanup_expired().await;
2353            }
2354        });
2355
2356        Arc::new(AppState {
2357            service_source,
2358            protocol_support: self.protocol_support.clone(),
2359            sessions,
2360            validate_origin: self.validate_origin,
2361            allowed_origins: self.allowed_origins.clone(),
2362            validate_host: self.validate_host,
2363            allowed_hosts: self.allowed_hosts.clone(),
2364            optional_sessions: self.optional_sessions,
2365            strict_initialization: self.session_config.strict_initialization,
2366            #[cfg(feature = "stateless")]
2367            stateless_config: self.stateless_config.clone(),
2368            #[cfg(feature = "stateless")]
2369            stamp_server_info: self.stamp_server_info,
2370            #[cfg(feature = "stateless")]
2371            modern_subscriptions,
2372            sse_responses: self.sse_responses,
2373            max_body_size: self.max_body_size,
2374        })
2375    }
2376
2377    /// Build the axum router for this transport.
2378    pub fn into_router(self) -> Router {
2379        let (router, _handle) = self.into_router_with_handle();
2380        router
2381    }
2382
2383    /// Build the axum router and return a [`SessionHandle`] for managing
2384    /// sessions and final subscription streams.
2385    ///
2386    /// # Example
2387    ///
2388    /// ```rust,ignore
2389    /// let transport = HttpTransport::new(router);
2390    /// let (router, handle) = transport.into_router_with_handle();
2391    ///
2392    /// // Use handle in an admin endpoint
2393    /// let count = handle.session_count().await;
2394    /// ```
2395    pub fn into_router_with_handle(mut self) -> (Router, SessionHandle) {
2396        let external_rx = self.external_notifications.take();
2397        let state = self.build_state();
2398        let handle = SessionHandle {
2399            store: state.sessions.clone(),
2400            #[cfg(feature = "stateless")]
2401            modern_subscriptions: state.modern_subscriptions.clone(),
2402        };
2403
2404        spawn_external_notification_fanout(
2405            external_rx,
2406            state.sessions.clone(),
2407            #[cfg(feature = "stateless")]
2408            state.modern_subscriptions.clone(),
2409        );
2410
2411        let router = Router::new()
2412            .route("/", post(handle_post))
2413            .route("/", get(handle_get))
2414            .route("/", delete(handle_delete))
2415            .route("/health", get(handle_health))
2416            .with_state(state);
2417
2418        #[cfg(feature = "oauth")]
2419        let router = self.add_oauth_route(router, "");
2420
2421        (router, handle)
2422    }
2423
2424    /// Build an axum router mounted at a specific path.
2425    pub fn into_router_at(self, path: &str) -> Router {
2426        let (router, _handle) = self.into_router_at_with_handle(path);
2427        router
2428    }
2429
2430    /// Build an axum router mounted at a specific path and return a
2431    /// [`SessionHandle`] for managing sessions and final subscription streams.
2432    pub fn into_router_at_with_handle(mut self, path: &str) -> (Router, SessionHandle) {
2433        let external_rx = self.external_notifications.take();
2434        let state = self.build_state();
2435        let handle = SessionHandle {
2436            store: state.sessions.clone(),
2437            #[cfg(feature = "stateless")]
2438            modern_subscriptions: state.modern_subscriptions.clone(),
2439        };
2440
2441        spawn_external_notification_fanout(
2442            external_rx,
2443            state.sessions.clone(),
2444            #[cfg(feature = "stateless")]
2445            state.modern_subscriptions.clone(),
2446        );
2447
2448        let mcp_router = Router::new()
2449            .route("/", post(handle_post))
2450            .route("/", get(handle_get))
2451            .route("/", delete(handle_delete))
2452            .route("/health", get(handle_health))
2453            .with_state(state);
2454
2455        let router = Router::new().nest(path, mcp_router);
2456
2457        #[cfg(feature = "oauth")]
2458        let router = self.add_oauth_route(router, path);
2459
2460        (router, handle)
2461    }
2462
2463    /// Serve the transport on the given address
2464    ///
2465    /// This is a convenience method that creates a TCP listener and serves the transport.
2466    pub async fn serve(self, addr: &str) -> Result<()> {
2467        let listener = tokio::net::TcpListener::bind(addr)
2468            .await
2469            .map_err(|e| Error::Transport(format!("Failed to bind to {}: {}", addr, e)))?;
2470
2471        tracing::info!("MCP HTTP transport listening on {}", addr);
2472
2473        let router = self.into_router();
2474        axum::serve(listener, router)
2475            .await
2476            .map_err(|e| Error::Transport(format!("Server error: {}", e)))?;
2477
2478        Ok(())
2479    }
2480
2481    /// Add the OAuth Protected Resource Metadata well-known route if configured.
2482    #[cfg(feature = "oauth")]
2483    fn add_oauth_route(&self, router: Router, _base_path: &str) -> Router {
2484        if let Some(ref config) = self.oauth_config {
2485            let metadata = config.metadata.clone();
2486            let well_known_path =
2487                crate::oauth::ProtectedResourceMetadata::well_known_path_for_resource(
2488                    &metadata.resource,
2489                )
2490                .unwrap_or_else(|_| {
2491                    crate::oauth::ProtectedResourceMetadata::well_known_path().to_string()
2492                });
2493            router.route(
2494                &well_known_path,
2495                get(move || {
2496                    let m = metadata.clone();
2497                    async move { axum::Json(m) }
2498                }),
2499            )
2500        } else {
2501            router
2502        }
2503    }
2504}
2505
2506/// Check if an origin is a localhost origin (safe from DNS rebinding).
2507/// Drain a caller-supplied notification channel and fan items out to every
2508/// live session's SSE broadcast.
2509///
2510/// No-op when `rx` is `None`. When present, spawns a long-running task that
2511/// runs for the lifetime of the transport (until the channel closes).
2512fn spawn_external_notification_fanout(
2513    rx: Option<NotificationReceiver>,
2514    sessions: Arc<SessionRegistry>,
2515    #[cfg(feature = "stateless")] modern_subscriptions: Arc<ModernSubscriptionRegistry>,
2516) {
2517    let Some(mut rx) = rx else {
2518        return;
2519    };
2520    tokio::spawn(async move {
2521        while let Some(notification) = rx.recv().await {
2522            #[cfg(feature = "stateless")]
2523            modern_subscriptions.publish(&notification);
2524            if let Some(json) = crate::transport::stdio::serialize_notification(&notification) {
2525                sessions.broadcast_to_all(&json).await;
2526            }
2527        }
2528        tracing::debug!("External notification channel closed; fan-out task exiting");
2529    });
2530}
2531
2532fn is_localhost_origin(origin: &str) -> bool {
2533    // Parse the origin to extract the host
2534    if let Some(rest) = origin
2535        .strip_prefix("http://")
2536        .or_else(|| origin.strip_prefix("https://"))
2537    {
2538        is_localhost_host(rest)
2539    } else {
2540        false
2541    }
2542}
2543
2544/// Check if a `host:port` (or `[ipv6]:port`) value refers to localhost.
2545///
2546/// Used by both Origin validation (after stripping the `http(s)://` scheme)
2547/// and Host validation (where there's no scheme to begin with).
2548fn is_localhost_host(host: &str) -> bool {
2549    let host_only = if host.starts_with('[') {
2550        // Bracketed IPv6: [::1]:3000 -> ::1
2551        host.split(']')
2552            .next()
2553            .unwrap_or(host)
2554            .trim_start_matches('[')
2555    } else {
2556        // Strip port if present
2557        host.split(':').next().unwrap_or(host)
2558    };
2559    matches!(host_only, "localhost" | "127.0.0.1" | "::1")
2560}
2561
2562/// Resolve the effective host for validation.
2563///
2564/// Prefers the `Host` header, falling back to the HTTP/2 `:authority`
2565/// pseudo-header (`request.uri().authority()`) when the header is missing.
2566/// This matters behind middleware like `axum::Router::nest`, which can
2567/// strip Hyper's synthesized `Host` before our handler sees it.
2568fn effective_host<'a>(headers: &'a HeaderMap, uri: &'a axum::http::Uri) -> Option<&'a str> {
2569    if let Some(value) = headers.get(header::HOST)
2570        && let Ok(s) = value.to_str()
2571    {
2572        return Some(s);
2573    }
2574    uri.authority().map(|a| a.as_str())
2575}
2576
2577/// Validate the `Host` header (defense-in-depth alongside Origin).
2578///
2579/// Returns Some(Response) if validation fails, None if it passes.
2580fn validate_host(headers: &HeaderMap, uri: &axum::http::Uri, state: &AppState) -> Option<Response> {
2581    if !state.validate_host {
2582        return None;
2583    }
2584
2585    let Some(host) = effective_host(headers, uri) else {
2586        if state.allowed_hosts.is_empty() {
2587            // No Host header and no allowlist: fall back to permissive
2588            // behavior matching pre-validation defaults so we don't break
2589            // existing deployments. (Origin already protects browsers.)
2590            return None;
2591        }
2592        tracing::warn!("Rejecting request: missing Host header and no :authority fallback");
2593        return Some((StatusCode::BAD_REQUEST, "Missing Host header").into_response());
2594    };
2595
2596    if is_localhost_host(host) {
2597        return None;
2598    }
2599
2600    if state.allowed_hosts.is_empty() {
2601        // Non-localhost host with no explicit allowlist: keep accepting it.
2602        // Operators who want strict Host validation must opt in via
2603        // `.allowed_hosts(...)`. This preserves the historical behavior of
2604        // not enforcing Host on non-loopback deployments by default.
2605        return None;
2606    }
2607
2608    if state.allowed_hosts.iter().any(|h| h == host) {
2609        return None;
2610    }
2611
2612    tracing::warn!(host = %host, "Rejecting request: Host not in allowlist");
2613    Some((StatusCode::BAD_REQUEST, "Host not allowed").into_response())
2614}
2615
2616/// Validate Origin header for security.
2617///
2618/// When origin validation is enabled:
2619/// - Requests without an Origin header are allowed (same-origin)
2620/// - Localhost origins are always allowed (DNS rebinding protection)
2621/// - If `allowed_origins` is non-empty, non-localhost origins must match
2622/// - If `allowed_origins` is empty, non-localhost origins are rejected
2623///
2624/// Returns Some(Response) if validation fails, None if it passes.
2625fn validate_origin(headers: &HeaderMap, state: &AppState) -> Option<Response> {
2626    if !state.validate_origin {
2627        return None;
2628    }
2629
2630    if let Some(origin) = headers.get(header::ORIGIN) {
2631        let origin_str = origin.to_str().unwrap_or("");
2632
2633        // Always allow localhost origins (DNS rebinding protection allows these)
2634        if is_localhost_origin(origin_str) {
2635            return None;
2636        }
2637
2638        // Non-localhost origin: check against allowed list
2639        if state.allowed_origins.is_empty() {
2640            tracing::warn!(
2641                origin = %origin_str,
2642                "Rejecting request: cross-origin not allowed (no allowlist configured)"
2643            );
2644            return Some(
2645                (StatusCode::FORBIDDEN, "Cross-origin requests not allowed").into_response(),
2646            );
2647        }
2648
2649        if !state
2650            .allowed_origins
2651            .iter()
2652            .any(|o| o == origin_str || o == "*")
2653        {
2654            tracing::warn!(origin = %origin_str, "Rejecting request: Origin not in allowlist");
2655            return Some((StatusCode::FORBIDDEN, "Origin not allowed").into_response());
2656        }
2657    }
2658
2659    None
2660}
2661
2662/// Extract and validate session ID from headers
2663fn get_session_id(headers: &HeaderMap) -> Option<String> {
2664    headers
2665        .get(MCP_SESSION_ID_HEADER)
2666        .and_then(|v| v.to_str().ok())
2667        .map(|s| s.to_string())
2668}
2669
2670/// Extract protocol version from headers
2671fn get_protocol_version(headers: &HeaderMap) -> Option<String> {
2672    headers
2673        .get(MCP_PROTOCOL_VERSION_HEADER)
2674        .and_then(|v| v.to_str().ok())
2675        .map(|s| s.to_string())
2676}
2677
2678/// Extract Last-Event-ID from headers for SSE stream resumption (SEP-1699)
2679fn get_last_event_id(headers: &HeaderMap) -> Option<u64> {
2680    headers
2681        .get(LAST_EVENT_ID_HEADER)
2682        .and_then(|v| v.to_str().ok())
2683        .and_then(|s| s.parse::<u64>().ok())
2684}
2685
2686/// Check if the request is an initialize request
2687fn is_initialize_request(body: &serde_json::Value) -> bool {
2688    body.get("method")
2689        .and_then(|m| m.as_str())
2690        .map(|m| m == "initialize")
2691        .unwrap_or(false)
2692}
2693
2694/// Check if this is a response to one of our outgoing requests
2695fn is_response(parsed: &serde_json::Value) -> bool {
2696    parsed.get("method").is_none()
2697        && (parsed.get("result").is_some() || parsed.get("error").is_some())
2698}
2699
2700/// Resolve the selected tool's input schema when the transport owns an
2701/// [`McpRouter`]. Pre-built services do not expose their tool registry, so
2702/// supplied custom headers can still be checked there but missing headers
2703/// cannot be inferred before dispatch.
2704fn request_tool_input_schema(
2705    service_source: &ServiceSource,
2706    parsed: &serde_json::Value,
2707) -> Option<serde_json::Value> {
2708    if parsed.get("method").and_then(serde_json::Value::as_str) != Some("tools/call") {
2709        return None;
2710    }
2711    let name = parsed
2712        .get("params")
2713        .and_then(serde_json::Value::as_object)
2714        .and_then(|params| params.get("name"))
2715        .and_then(serde_json::Value::as_str)?;
2716    match service_source {
2717        ServiceSource::Router { router, .. } => router.tool_input_schema(name),
2718        ServiceSource::Service(_) => None,
2719    }
2720}
2721
2722/// Return whether an HTTP request claims the modern, per-request-metadata
2723/// protocol era.
2724///
2725/// The body envelope is authoritative for era detection. The final-version
2726/// header is also treated as a modern claim so a missing or malformed
2727/// envelope receives the specified modern error instead of drifting into the
2728/// legacy session path.
2729fn claims_modern_protocol(headers: &HeaderMap, parsed: &serde_json::Value) -> bool {
2730    get_protocol_version(headers).as_deref() == Some(PROTOCOL_VERSION_2026_07_28)
2731        || parsed
2732            .get("params")
2733            .and_then(serde_json::Value::as_object)
2734            .and_then(|params| params.get("_meta"))
2735            .and_then(serde_json::Value::as_object)
2736            .is_some_and(|meta| meta.contains_key("io.modelcontextprotocol/protocolVersion"))
2737}
2738
2739/// Validate the required modern per-request metadata and return its declared
2740/// protocol version.
2741///
2742/// `clientInfo` is deliberately optional in the final specification.
2743fn validate_modern_request_meta(
2744    parsed: &serde_json::Value,
2745) -> std::result::Result<String, JsonRpcError> {
2746    let params = parsed
2747        .get("params")
2748        .and_then(serde_json::Value::as_object)
2749        .ok_or_else(|| {
2750            JsonRpcError::invalid_params("Modern requests require a params object containing _meta")
2751        })?;
2752    let meta_value = params
2753        .get("_meta")
2754        .ok_or_else(|| JsonRpcError::invalid_params("Modern requests require a _meta object"))?;
2755    crate::protocol::validate_meta_object(meta_value)
2756        .map_err(|error| JsonRpcError::invalid_params(error.to_string()))?;
2757    let meta = meta_value
2758        .as_object()
2759        .expect("validate_meta_object accepted a JSON object");
2760    let protocol_version = meta
2761        .get("io.modelcontextprotocol/protocolVersion")
2762        .and_then(serde_json::Value::as_str)
2763        .ok_or_else(|| {
2764            JsonRpcError::invalid_params(
2765                "Missing or invalid _meta.io.modelcontextprotocol/protocolVersion",
2766            )
2767        })?;
2768    let client_capabilities = meta
2769        .get("io.modelcontextprotocol/clientCapabilities")
2770        .ok_or_else(|| {
2771            JsonRpcError::invalid_params("Missing _meta.io.modelcontextprotocol/clientCapabilities")
2772        })?;
2773    if !client_capabilities.is_object()
2774        || serde_json::from_value::<ClientCapabilities>(client_capabilities.clone()).is_err()
2775    {
2776        return Err(JsonRpcError::invalid_params(
2777            "Invalid _meta.io.modelcontextprotocol/clientCapabilities",
2778        ));
2779    }
2780
2781    Ok(protocol_version.to_string())
2782}
2783
2784/// Methods present in legacy protocol unions but removed from the modern core.
2785fn is_removed_modern_method(method: &str) -> bool {
2786    matches!(
2787        method,
2788        "initialize"
2789            | "notifications/initialized"
2790            | "ping"
2791            | "logging/setLevel"
2792            | "resources/subscribe"
2793            | "resources/unsubscribe"
2794            | "notifications/roots/list_changed"
2795    )
2796}
2797
2798/// Map protocol errors whose final Streamable HTTP binding assigns a
2799/// non-success status. Errors emitted after an SSE stream has opened remain
2800/// in-band because the HTTP status is already committed.
2801#[cfg(feature = "stateless")]
2802fn modern_response_status(response: &JsonRpcResponse) -> StatusCode {
2803    let JsonRpcResponse::Error(error) = response else {
2804        return StatusCode::OK;
2805    };
2806    if error.error.code == ErrorCode::MethodNotFound as i32 {
2807        StatusCode::NOT_FOUND
2808    } else if error.error.code == McpErrorCode::MissingRequiredClientCapability.code() {
2809        StatusCode::BAD_REQUEST
2810    } else {
2811        StatusCode::OK
2812    }
2813}
2814
2815/// Extract request ID from a JSON value
2816fn extract_request_id(parsed: &serde_json::Value) -> Option<RequestId> {
2817    parsed.get("id").and_then(|id| {
2818        if let Some(n) = id.as_i64() {
2819            Some(RequestId::Number(n))
2820        } else {
2821            id.as_str().map(|s| RequestId::String(s.to_string()))
2822        }
2823    })
2824}
2825
2826/// Handle POST requests (JSON-RPC messages from client)
2827async fn handle_post(
2828    State(state): State<Arc<AppState>>,
2829    request: axum::extract::Request,
2830) -> Response {
2831    let (parts, body_bytes) = request.into_parts();
2832    let headers = parts.headers;
2833    let uri = parts.uri.clone();
2834
2835    // Validate Host (DNS rebinding defense, complement to Origin)
2836    if let Some(resp) = validate_host(&headers, &uri, &state) {
2837        return resp;
2838    }
2839
2840    // Validate Origin
2841    if let Some(resp) = validate_origin(&headers, &state) {
2842        return resp;
2843    }
2844
2845    // Bound the body size (rmcp #970 analog). axum's `DefaultBodyLimit`
2846    // doesn't apply here because this handler consumes the raw `Request`
2847    // instead of a body-consuming extractor, so this is the only limit on
2848    // the MCP POST body. A declared Content-Length above the limit is
2849    // rejected without reading; chunked bodies are capped while streaming.
2850    if let Some(declared) = headers
2851        .get(header::CONTENT_LENGTH)
2852        .and_then(|v| v.to_str().ok())
2853        .and_then(|v| v.parse::<usize>().ok())
2854        && declared > state.max_body_size
2855    {
2856        return body_too_large_response(state.max_body_size);
2857    }
2858
2859    let body = match axum::body::to_bytes(body_bytes, state.max_body_size).await {
2860        Ok(bytes) => match String::from_utf8(bytes.to_vec()) {
2861            Ok(s) => s,
2862            Err(e) => {
2863                return json_rpc_error_response(
2864                    None,
2865                    JsonRpcError::parse_error(format!("Invalid UTF-8: {}", e)),
2866                );
2867            }
2868        },
2869        Err(e) if is_length_limit_error(&e) => {
2870            return body_too_large_response(state.max_body_size);
2871        }
2872        Err(e) => {
2873            return json_rpc_error_response(
2874                None,
2875                JsonRpcError::parse_error(format!("Failed to read body: {}", e)),
2876            );
2877        }
2878    };
2879
2880    // Bridge TokenClaims from HTTP extensions to MCP extensions (if present)
2881    #[cfg(feature = "oauth")]
2882    let http_extensions = parts.extensions;
2883    #[cfg(not(feature = "oauth"))]
2884    let _ = parts.extensions;
2885
2886    // Parse the request body
2887    let parsed: serde_json::Value = match serde_json::from_str(&body) {
2888        Ok(v) => v,
2889        Err(e) => {
2890            return json_rpc_error_response(
2891                None,
2892                JsonRpcError::parse_error(format!("Invalid JSON: {}", e)),
2893            );
2894        }
2895    };
2896
2897    // A version header supplies enough exact context to reject a batch before
2898    // any object-only HTTP classification runs. Legacy batches without a
2899    // header are validated against their session revision after lookup below.
2900    if parsed.is_array()
2901        && let Some(version) = get_protocol_version(&headers)
2902    {
2903        let revision = match version.parse::<McpProtocolRevision>() {
2904            Ok(revision) => revision,
2905            Err(_) => {
2906                return json_rpc_error_response(
2907                    None,
2908                    JsonRpcError::unsupported_protocol_version(
2909                        version,
2910                        state.protocol_support.versions().iter().map(String::as_str),
2911                    ),
2912                );
2913            }
2914        };
2915        if let Err(error) = inspect_runtime_value(
2916            &parsed,
2917            revision,
2918            &state.protocol_support,
2919            McpDirection::ClientToServer,
2920        ) {
2921            let status = if revision == McpProtocolRevision::V2026_07_28 {
2922                StatusCode::BAD_REQUEST
2923            } else {
2924                StatusCode::OK
2925            };
2926            return json_rpc_error_response_with_status(None, error, status);
2927        }
2928    }
2929
2930    // Check if this is an initialize request (creates new session)
2931    let is_init = is_initialize_request(&parsed);
2932    let request_method = parsed
2933        .get("method")
2934        .and_then(|method| method.as_str())
2935        .unwrap_or_default()
2936        .to_string();
2937    let tool_input_schema = request_tool_input_schema(&state.service_source, &parsed);
2938    let modern_request = claims_modern_protocol(&headers, &parsed);
2939
2940    // The modern protocol is selected by its per-request `_meta` envelope,
2941    // with the final-version HTTP header also acting as a signal for malformed
2942    // requests whose envelope is missing. Resolve that era before consulting
2943    // any legacy session state so modern traffic cannot accidentally fall
2944    // through to the initialize/session lifecycle.
2945    if modern_request {
2946        let id = extract_request_id(&parsed);
2947        let body_version = match validate_modern_request_meta(&parsed) {
2948            Ok(version) => version,
2949            Err(error) => {
2950                return json_rpc_error_response_with_status(id, error, StatusCode::BAD_REQUEST);
2951            }
2952        };
2953
2954        let Some(header_version) = get_protocol_version(&headers) else {
2955            return json_rpc_error_response_with_status(
2956                id,
2957                JsonRpcError::header_mismatch("MCP-Protocol-Version header is required"),
2958                StatusCode::BAD_REQUEST,
2959            );
2960        };
2961        if header_version != body_version {
2962            return json_rpc_error_response_with_status(
2963                id,
2964                JsonRpcError::header_mismatch(format!(
2965                    "MCP-Protocol-Version header value {header_version:?} does not match \
2966                     request _meta protocol version {body_version:?}"
2967                )),
2968                StatusCode::BAD_REQUEST,
2969            );
2970        }
2971
2972        if !state.protocol_support.contains(&body_version) {
2973            return json_rpc_error_response_with_status(
2974                id,
2975                JsonRpcError::unsupported_protocol_version(
2976                    body_version,
2977                    state.protocol_support.versions().iter().map(String::as_str),
2978                ),
2979                StatusCode::BAD_REQUEST,
2980            );
2981        }
2982
2983        let revision = match body_version.parse::<McpProtocolRevision>() {
2984            Ok(revision) => revision,
2985            Err(_) => {
2986                return json_rpc_error_response_with_status(
2987                    id,
2988                    JsonRpcError::unsupported_protocol_version(
2989                        body_version,
2990                        state.protocol_support.versions().iter().map(String::as_str),
2991                    ),
2992                    StatusCode::BAD_REQUEST,
2993                );
2994            }
2995        };
2996        if let Err(error) = inspect_runtime_value(
2997            &parsed,
2998            revision,
2999            &state.protocol_support,
3000            McpDirection::ClientToServer,
3001        ) {
3002            return json_rpc_error_response_with_status(id, error, StatusCode::BAD_REQUEST);
3003        }
3004
3005        let sep_2243_mode = super::http_headers::mode_for_version(&body_version);
3006        if let Err(error) = super::http_headers::validate_with_tool_schema(
3007            &headers,
3008            &parsed,
3009            sep_2243_mode,
3010            tool_input_schema.as_ref(),
3011        ) {
3012            tracing::warn!(
3013                mode = ?sep_2243_mode,
3014                version = %body_version,
3015                error = %error.message,
3016                "Rejecting modern request: HTTP header validation failed",
3017            );
3018            return json_rpc_error_response_with_status(id, error, StatusCode::BAD_REQUEST);
3019        }
3020
3021        if is_removed_modern_method(&request_method) {
3022            return json_rpc_error_response_with_status(
3023                id,
3024                JsonRpcError::method_not_found(&request_method),
3025                StatusCode::NOT_FOUND,
3026            );
3027        }
3028    }
3029
3030    // SEP-2575 / SEP-2567: version-gated stateless mode for 2026-07-28+ clients.
3031    //
3032    // When the requested (or carried) protocol version is >= 2026-07-28 and the
3033    // request has no mcp-session-id, every request -- including `initialize` --
3034    // is served without creating or looking up a session. Each request is fully
3035    // self-contained; client identity and capabilities flow through per-request
3036    // `_meta` rather than a session handshake.
3037    //
3038    // This block runs before the legacy SEP-1442 stateless path so that
3039    // 2026-07-28 requests are handled here regardless of whether
3040    // `stateless_config` is set on the transport.
3041    #[cfg(feature = "stateless")]
3042    {
3043        let version_in_play: Option<String> = if is_init && !modern_request {
3044            // For `initialize`, read the version the client is requesting from
3045            // the params object.
3046            parsed
3047                .get("params")
3048                .and_then(|p| p.get("protocolVersion"))
3049                .and_then(|v| v.as_str())
3050                .map(|s| s.to_string())
3051        } else {
3052            // For non-init requests, only the HTTP-level `MCP-Protocol-Version`
3053            // header gates stateless mode. Body-level `_meta.protocolVersion` is
3054            // plumbed to handlers via `stash_per_request_meta` in both paths.
3055            get_protocol_version(&headers)
3056        };
3057
3058        if let Some(ref version) = version_in_play
3059            && is_stateless_protocol_version(version)
3060            && state.protocol_support.contains(version)
3061            // `subscriptions/listen` opens an SSE stream; let it fall through to the
3062            // dedicated intercept below rather than handling it as a plain RPC call.
3063            && parsed.get("method").and_then(|m| m.as_str()) != Some("subscriptions/listen")
3064        {
3065            // Notifications and responses are fire-and-forget; no dispatch needed.
3066            if !is_init && (parsed.get("id").is_none() || is_response(&parsed)) {
3067                return StatusCode::ACCEPTED.into_response();
3068            }
3069
3070            // SEP-2243 validation before `parsed` is consumed by deserialization.
3071            // 2026-07-28 falls into strict mode, so missing Mcp-Method is an error.
3072            let sep_2243_mode = super::http_headers::mode_for_version(version);
3073            if let Err(err) = super::http_headers::validate_with_tool_schema(
3074                &headers,
3075                &parsed,
3076                sep_2243_mode,
3077                tool_input_schema.as_ref(),
3078            ) {
3079                tracing::warn!(
3080                    mode = ?sep_2243_mode,
3081                    version = %version,
3082                    error = %err.message,
3083                    "Rejecting stateless request: SEP-2243 header validation failed",
3084                );
3085                let id = extract_request_id(&parsed);
3086                let mut resp = json_rpc_error_response(id, err);
3087                *resp.status_mut() = StatusCode::BAD_REQUEST;
3088                return resp;
3089            }
3090
3091            let request: JsonRpcRequest = match serde_json::from_value(parsed) {
3092                Ok(r) => r,
3093                Err(e) => {
3094                    return json_rpc_error_response(
3095                        None,
3096                        JsonRpcError::parse_error(format!("Invalid request: {}", e)),
3097                    );
3098                }
3099            };
3100
3101            // Ephemeral pre-initialized service -- no session is stored or created.
3102            //
3103            // A per-request notification channel captures anything the handler
3104            // emits during the call (progress, logging). With no session and no
3105            // GET stream on this path, those messages can only reach the client
3106            // on the POST response itself: per the draft Streamable HTTP rules,
3107            // a plain JSON body is only correct when the first outbound message
3108            // is the terminal response; otherwise the response falls back to
3109            // SSE with the notifications delivered ahead of the terminal
3110            // response.
3111            // Captured before the match below borrows `router` into the
3112            // ephemeral session; used to stamp `_meta.serverInfo` on the
3113            // outgoing response (SEP-2575). `None` for a transport built
3114            // from a pre-built service (no router to read identity from).
3115            let server_identity = match &state.service_source {
3116                ServiceSource::Router { router, .. } if state.stamp_server_info => {
3117                    Some(router.implementation())
3118                }
3119                _ => None,
3120            };
3121
3122            let (notif_tx, mut notif_rx) = crate::context::notification_channel(64);
3123            let mut service = match &state.service_source {
3124                ServiceSource::Router { router, factory } => {
3125                    let ephemeral = router
3126                        .with_fresh_session()
3127                        .with_request_notification_sender(notif_tx);
3128                    ephemeral.session().mark_initialized();
3129                    JsonRpcService::new(factory(ephemeral))
3130                }
3131                ServiceSource::Service(mutex) => JsonRpcService::new(mutex.lock().unwrap().clone()),
3132            };
3133
3134            let mut ext = crate::router::Extensions::new();
3135            ext.insert(state.protocol_support.clone());
3136            #[cfg(feature = "oauth")]
3137            if let Some(claims) = http_extensions.get::<crate::oauth::token::TokenClaims>() {
3138                ext.insert(claims.clone());
3139            }
3140            stash_per_request_meta(&request, &mut ext);
3141
3142            // rmcp #967 analog: give the request a cancellation token that
3143            // fires if the client disconnects before the response is
3144            // delivered. The router adopts the token as the
3145            // `RequestContext`'s cancellation source, so handlers observe
3146            // the disconnect via `ctx.is_cancelled()` / `ctx.cancelled()`,
3147            // and spawned work holding a token clone is signalled even
3148            // after the request future itself is dropped. Session-based
3149            // requests are exempt: with stream resumption, a disconnect is
3150            // not a cancellation.
3151            let cancel_token = crate::context::CancellationToken::new();
3152            let mut cancel_guard = CancelOnDisconnect::arm(cancel_token.clone());
3153            ext.insert(cancel_token);
3154
3155            service = service.with_extensions(ext);
3156
3157            let mut call: std::pin::Pin<
3158                Box<dyn std::future::Future<Output = crate::error::Result<JsonRpcResponse>> + Send>,
3159            > = Box::pin(async move {
3160                let mut service = service;
3161                service.call_single(request).await
3162            });
3163
3164            enum FirstOutbound {
3165                Response(crate::error::Result<JsonRpcResponse>),
3166                Notification(crate::context::ServerNotification),
3167            }
3168
3169            // Race the handler against its first notification. A closed
3170            // channel (no sender attached, or all senders dropped) simply
3171            // awaits the handler.
3172            let first = loop {
3173                let outbound = tokio::select! {
3174                    // A handler may enqueue a notification and complete in
3175                    // the same poll. Observe the queued notification first so
3176                    // it is neither dropped nor raced behind the response.
3177                    biased;
3178                    maybe = notif_rx.recv() => match maybe {
3179                        Some(n) => FirstOutbound::Notification(n),
3180                        None => FirstOutbound::Response((&mut call).await),
3181                    },
3182                    result = &mut call => FirstOutbound::Response(result),
3183                };
3184                match outbound {
3185                    FirstOutbound::Notification(notification)
3186                        if state.modern_subscriptions.publish(&notification) =>
3187                    {
3188                        continue;
3189                    }
3190                    outbound => break outbound,
3191                }
3192            };
3193
3194            match first {
3195                FirstOutbound::Response(result) => {
3196                    // `select!` may observe a handler's ready response in the
3197                    // same poll that the handler enqueued notifications.
3198                    // Drain that queue before committing a JSON response.
3199                    while let Ok(notification) = notif_rx.try_recv() {
3200                        if state.modern_subscriptions.publish(&notification) {
3201                            continue;
3202                        }
3203                        let ready_call: std::pin::Pin<
3204                            Box<
3205                                dyn std::future::Future<
3206                                        Output = crate::error::Result<JsonRpcResponse>,
3207                                    > + Send,
3208                            >,
3209                        > = Box::pin(async move { result });
3210                        let mut resp = stateless_sse_with_notifications(
3211                            notification,
3212                            ready_call,
3213                            notif_rx,
3214                            StatelessSseContext {
3215                                version: version.clone(),
3216                                method: request_method.clone(),
3217                                cancel_guard,
3218                                server_identity,
3219                                subscriptions: state.modern_subscriptions.clone(),
3220                            },
3221                        );
3222                        resp.headers_mut().insert(
3223                            MCP_PROTOCOL_VERSION_HEADER,
3224                            HeaderValue::from_str(version).unwrap(),
3225                        );
3226                        return resp;
3227                    }
3228
3229                    // Handler finished; the response is about to be
3230                    // produced, so dropping the connection from here on is
3231                    // no longer a cancellation.
3232                    cancel_guard.disarm();
3233                    let mut response = match result {
3234                        Ok(resp) => resp,
3235                        Err(e) => {
3236                            return json_rpc_error_response(
3237                                None,
3238                                JsonRpcError::internal_error(e.to_string()),
3239                            );
3240                        }
3241                    };
3242
3243                    // Keep the response aligned with the version selected for
3244                    // this sessionless request. The router also receives the
3245                    // runtime allow-list through Extensions.
3246                    if is_init
3247                        && let JsonRpcResponse::Result(ref mut result) = response
3248                        && let Some(pv) = result.result.get_mut("protocolVersion")
3249                    {
3250                        *pv = serde_json::Value::String(version.clone());
3251                    }
3252                    apply_protocol_result_fields(&mut response, &request_method, version);
3253                    if let Some(ref identity) = server_identity {
3254                        stamp_server_info(&mut response, identity);
3255                    }
3256
3257                    let status = modern_response_status(&response);
3258                    let mut resp = if state.sse_responses {
3259                        sse_json_response(&response)
3260                    } else {
3261                        axum::Json(response).into_response()
3262                    };
3263                    *resp.status_mut() = status;
3264                    resp.headers_mut().insert(
3265                        MCP_PROTOCOL_VERSION_HEADER,
3266                        HeaderValue::from_str(version).unwrap(),
3267                    );
3268                    // Intentionally NO `mcp-session-id` header for 2026-07-28+ clients.
3269                    return resp;
3270                }
3271                FirstOutbound::Notification(first_notif) => {
3272                    let mut resp = stateless_sse_with_notifications(
3273                        first_notif,
3274                        call,
3275                        notif_rx,
3276                        StatelessSseContext {
3277                            version: version.clone(),
3278                            method: request_method.clone(),
3279                            cancel_guard,
3280                            server_identity,
3281                            subscriptions: state.modern_subscriptions.clone(),
3282                        },
3283                    );
3284                    resp.headers_mut().insert(
3285                        MCP_PROTOCOL_VERSION_HEADER,
3286                        HeaderValue::from_str(version).unwrap(),
3287                    );
3288                    // Intentionally NO `mcp-session-id` header for 2026-07-28+ clients.
3289                    return resp;
3290                }
3291            }
3292        }
3293    }
3294
3295    // SEP-1442: Handle stateless requests (no session needed).
3296    // Stateless requests have a protocol version but no session ID and are not
3297    // initialize requests. They are processed with an ephemeral service and
3298    // return immediately without storing any session state.
3299    #[cfg(feature = "stateless")]
3300    if !is_init && state.stateless_config.is_some() && get_session_id(&headers).is_none() {
3301        let version_from_header = get_protocol_version(&headers);
3302        let params = parsed.get("params").unwrap_or(&parsed);
3303        let version_from_meta = crate::stateless::StatelessRequestMeta::from_params(params)
3304            .and_then(|m| m.protocol_version);
3305
3306        if let Some(version) = version_from_header.or(version_from_meta) {
3307            if let Err(err) = crate::stateless::validate_protocol_version(&version) {
3308                return json_rpc_error_response(None, err);
3309            }
3310
3311            // Notifications and responses don't make sense without a session
3312            if parsed.get("id").is_none() || is_response(&parsed) {
3313                return StatusCode::ACCEPTED.into_response();
3314            }
3315
3316            let request: JsonRpcRequest = match serde_json::from_value(parsed) {
3317                Ok(r) => r,
3318                Err(e) => {
3319                    return json_rpc_error_response(
3320                        None,
3321                        JsonRpcError::parse_error(format!("Invalid request: {}", e)),
3322                    );
3323                }
3324            };
3325
3326            // Ephemeral pre-initialized service -- no session stored
3327            let mut service = match &state.service_source {
3328                ServiceSource::Router { router, factory } => {
3329                    let ephemeral = router.with_fresh_session();
3330                    ephemeral.session().mark_initialized();
3331                    JsonRpcService::new(factory(ephemeral))
3332                }
3333                ServiceSource::Service(mutex) => JsonRpcService::new(mutex.lock().unwrap().clone()),
3334            };
3335
3336            let mut ext = crate::router::Extensions::new();
3337            ext.insert(state.protocol_support.clone());
3338            #[cfg(feature = "oauth")]
3339            if let Some(claims) = http_extensions.get::<crate::oauth::token::TokenClaims>() {
3340                ext.insert(claims.clone());
3341            }
3342            #[cfg(feature = "stateless")]
3343            stash_per_request_meta(&request, &mut ext);
3344            if !ext.is_empty() {
3345                service = service.with_extensions(ext);
3346            }
3347
3348            let mut response = match service.call_single(request).await {
3349                Ok(resp) => resp,
3350                Err(e) => {
3351                    return json_rpc_error_response(
3352                        None,
3353                        JsonRpcError::internal_error(e.to_string()),
3354                    );
3355                }
3356            };
3357            apply_protocol_result_fields(&mut response, &request_method, &version);
3358
3359            let mut resp = if state.sse_responses {
3360                sse_json_response(&response)
3361            } else {
3362                axum::Json(response).into_response()
3363            };
3364            resp.headers_mut().insert(
3365                MCP_PROTOCOL_VERSION_HEADER,
3366                HeaderValue::from_str(&version).unwrap(),
3367            );
3368            return resp;
3369        }
3370    }
3371
3372    // Final-protocol subscriptions are sessionless long-lived POSTs. They
3373    // must be established before consulting any legacy session state.
3374    #[cfg(feature = "stateless")]
3375    if modern_request && request_method == "subscriptions/listen" {
3376        return handle_modern_subscriptions_listen_sse(state, &parsed).await;
3377    }
3378
3379    // Runtime allowlist enforcement precedes semantic profile validation.
3380    // This is especially important for optional-session traffic: an unknown
3381    // header must not be interpreted under a fallback revision.
3382    if !is_init
3383        && let Some(version) = get_protocol_version(&headers)
3384        && !state.protocol_support.contains(&version)
3385    {
3386        return json_rpc_error_response(
3387            extract_request_id(&parsed),
3388            JsonRpcError::unsupported_protocol_version(
3389                version,
3390                state.protocol_support.versions().iter().map(String::as_str),
3391            ),
3392        );
3393    }
3394
3395    let uses_transient_session = !is_init
3396        && !modern_request
3397        && get_session_id(&headers).is_none()
3398        && state.optional_sessions;
3399
3400    // Get or create session
3401    let session = if is_init {
3402        // Create new session for initialize
3403        let create_result = match &state.service_source {
3404            ServiceSource::Router { router, factory } => {
3405                // Use with_fresh_session() to ensure each session has its own state
3406                state
3407                    .sessions
3408                    .create(router.with_fresh_session(), factory.clone())
3409                    .await
3410            }
3411            ServiceSource::Service(mutex) => {
3412                let service = mutex.lock().unwrap().clone();
3413                state.sessions.create_from_service(service).await
3414            }
3415        };
3416        match create_result {
3417            Some(s) => s,
3418            None => {
3419                return (
3420                    StatusCode::SERVICE_UNAVAILABLE,
3421                    "Maximum session limit reached",
3422                )
3423                    .into_response();
3424            }
3425        }
3426    } else if !modern_request && let Some(session_id) = get_session_id(&headers) {
3427        // Client sent a session ID -- look it up
3428        match state.sessions.get(&session_id).await {
3429            Some(s) => s,
3430            None => {
3431                // Return JSON-RPC error with session info so clients know to re-initialize
3432                return json_rpc_error_response(
3433                    None,
3434                    JsonRpcError::session_not_found_with_id(&session_id),
3435                );
3436            }
3437        }
3438    } else if state.optional_sessions {
3439        // No session ID, but sessions are optional -- create a transient,
3440        // pre-initialized session so the router won't reject the request.
3441        // This supports clients (Codex CLI, Cursor, etc.) that perform
3442        // initialize + tools/list during setup but don't carry the session
3443        // ID forward to subsequent requests.
3444        let create_result = match &state.service_source {
3445            ServiceSource::Router { router, factory } => {
3446                state
3447                    .sessions
3448                    .create_initialized(router.with_fresh_session(), factory.clone())
3449                    .await
3450            }
3451            ServiceSource::Service(mutex) => {
3452                let service = mutex.lock().unwrap().clone();
3453                state
3454                    .sessions
3455                    .create_initialized_from_service(service)
3456                    .await
3457            }
3458        };
3459        match create_result {
3460            Some(s) => s,
3461            None => {
3462                return (
3463                    StatusCode::SERVICE_UNAVAILABLE,
3464                    "Maximum session limit reached",
3465                )
3466                    .into_response();
3467            }
3468        }
3469    } else {
3470        // No session ID and sessions are required
3471        return json_rpc_error_response(None, JsonRpcError::session_required());
3472    };
3473
3474    // Session lookup establishes the exact legacy revision. Validate the raw
3475    // envelope before object-only notification/response routing, then let the
3476    // existing request dispatcher consume the typed shape.
3477    let session_protocol_version = if uses_transient_session {
3478        let version = state
3479            .protocol_support
3480            .versions()
3481            .iter()
3482            .find(|version| {
3483                crate::protocol::SUPPORTED_PROTOCOL_VERSIONS.contains(&version.as_str())
3484            })
3485            .map_or_else(
3486                || state.protocol_support.preferred().to_string(),
3487                Clone::clone,
3488            );
3489        *session.protocol_version.write().await = version.clone();
3490        version
3491    } else {
3492        session.protocol_version.read().await.clone()
3493    };
3494    let session_revision = match session_protocol_version.parse::<McpProtocolRevision>() {
3495        Ok(revision) => revision,
3496        Err(_) => {
3497            return json_rpc_error_response(
3498                extract_request_id(&parsed),
3499                JsonRpcError::unsupported_protocol_version(
3500                    session_protocol_version,
3501                    state.protocol_support.versions().iter().map(String::as_str),
3502                ),
3503            );
3504        }
3505    };
3506    if !is_init
3507        && let Err(error) = inspect_runtime_value(
3508            &parsed,
3509            session_revision,
3510            &state.protocol_support,
3511            McpDirection::ClientToServer,
3512        )
3513    {
3514        return json_rpc_error_response(extract_request_id(&parsed), error);
3515    }
3516
3517    if parsed.is_array() {
3518        if state.strict_initialization
3519            && !session
3520                .initialized_notification_received
3521                .load(Ordering::Acquire)
3522        {
3523            return json_rpc_error_response(
3524                None,
3525                JsonRpcError::invalid_request(
3526                    "Client must send notifications/initialized before making requests",
3527                ),
3528            );
3529        }
3530
3531        let message: JsonRpcMessage = match serde_json::from_value(parsed) {
3532            Ok(message) => message,
3533            Err(error) => {
3534                return json_rpc_error_response(
3535                    None,
3536                    JsonRpcError::invalid_request(format!("Invalid request batch: {error}")),
3537                );
3538            }
3539        };
3540
3541        let mut extensions = crate::router::Extensions::new();
3542        extensions.insert(state.protocol_support.clone());
3543        extensions.insert(session_revision);
3544        #[cfg(feature = "oauth")]
3545        if let Some(claims) = http_extensions.get::<crate::oauth::token::TokenClaims>() {
3546            extensions.insert(claims.clone());
3547        }
3548
3549        let mut service = JsonRpcService::new(session.make_service())
3550            .with_extensions(extensions)
3551            .protocol_support(state.protocol_support.clone())
3552            .with_negotiated_protocol_version(&session_protocol_version);
3553        let response = match service.call_message(message).await {
3554            Ok(response) => response,
3555            Err(error) => {
3556                return json_rpc_error_response(
3557                    None,
3558                    JsonRpcError::internal_error(error.to_string()),
3559                );
3560            }
3561        };
3562        let mut response = axum::Json(response).into_response();
3563        response.headers_mut().insert(
3564            MCP_PROTOCOL_VERSION_HEADER,
3565            HeaderValue::from_str(&session_protocol_version).unwrap(),
3566        );
3567        return response;
3568    }
3569
3570    // SEP-2575 / SEP-2567: intercept `subscriptions/listen` before the standard
3571    // version validation. `subscriptions/listen` is only available when the
3572    // effective protocol version is >= 2026-07-28; otherwise we return a
3573    // proper JSON-RPC error rather than silently falling through to the
3574    // router (which would return `MethodNotFound` anyway, but without the
3575    // protocol-version context).
3576    //
3577    // We check the Mcp-Protocol-Version header first (per-request override),
3578    // falling back to the session-negotiated version. Intercepting here
3579    // also prevents the version-validation guard below from rejecting the
3580    // 2026-07-28 header before we can inspect it.
3581    {
3582        let method_str = parsed.get("method").and_then(|m| m.as_str()).unwrap_or("");
3583        if method_str == "subscriptions/listen" {
3584            let req_id = extract_request_id(&parsed);
3585            let effective_version = if let Some(v) = get_protocol_version(&headers) {
3586                v
3587            } else {
3588                session.protocol_version.read().await.clone()
3589            };
3590            if version_supports_subscriptions_listen(&effective_version, &state.protocol_support) {
3591                return handle_subscriptions_listen_sse(session).await;
3592            } else {
3593                return json_rpc_error_response(
3594                    req_id,
3595                    JsonRpcError::method_not_found("subscriptions/listen"),
3596                );
3597            }
3598        }
3599    }
3600
3601    // SEP-2243: validate the standardized HTTP headers (Mcp-Method,
3602    // Mcp-Name, Mcp-Param-*) against the body. Mode is "strict" only
3603    // when the negotiated protocol version is at or beyond the
3604    // SEP-2243-inclusion version; otherwise present headers are still
3605    // checked for body consistency but missing headers are allowed.
3606    //
3607    // For `initialize` requests the session's protocol version hasn't
3608    // been negotiated yet, so we fall back to the version the client
3609    // requested in the body. For all other requests we use the session's
3610    // negotiated version (which is also reflected back in the response
3611    // `Mcp-Protocol-Version` header).
3612    let sep_2243_version = if is_init {
3613        match parsed
3614            .get("params")
3615            .and_then(|p| p.get("protocolVersion"))
3616            .and_then(|v| v.as_str())
3617        {
3618            Some(v) => v.to_string(),
3619            None => session.protocol_version.read().await.clone(),
3620        }
3621    } else {
3622        session.protocol_version.read().await.clone()
3623    };
3624    let sep_2243_mode = super::http_headers::mode_for_version(&sep_2243_version);
3625    if let Err(err) = super::http_headers::validate_with_tool_schema(
3626        &headers,
3627        &parsed,
3628        sep_2243_mode,
3629        tool_input_schema.as_ref(),
3630    ) {
3631        tracing::warn!(
3632            mode = ?sep_2243_mode,
3633            version = %sep_2243_version,
3634            error = %err.message,
3635            "Rejecting request: SEP-2243 header validation failed",
3636        );
3637        let id = extract_request_id(&parsed);
3638        let mut resp = json_rpc_error_response(id, err);
3639        // Per SEP-2243 §"Error Code" the HTTP status MUST be 400.
3640        *resp.status_mut() = StatusCode::BAD_REQUEST;
3641        return resp;
3642    }
3643
3644    // Check if this is a response to one of our outgoing requests (sampling)
3645    if is_response(&parsed) {
3646        if let Some(id) = extract_request_id(&parsed) {
3647            let result = if let Some(error) = parsed.get("error") {
3648                let code = error.get("code").and_then(|c| c.as_i64()).unwrap_or(-1);
3649                let message = error
3650                    .get("message")
3651                    .and_then(|m| m.as_str())
3652                    .unwrap_or("Unknown error");
3653                Err(Error::Internal(format!(
3654                    "Client error ({}): {}",
3655                    code, message
3656                )))
3657            } else if let Some(result) = parsed.get("result") {
3658                Ok(result.clone())
3659            } else {
3660                Err(Error::Internal(
3661                    "Response has neither result nor error".to_string(),
3662                ))
3663            };
3664
3665            if session.complete_pending_request(&id, result).await {
3666                tracing::debug!(request_id = ?id, "Completed pending request");
3667            } else {
3668                tracing::warn!(request_id = ?id, "Received response for unknown request");
3669            }
3670        }
3671        return StatusCode::ACCEPTED.into_response();
3672    }
3673
3674    // Check if this is a notification (no id field)
3675    if parsed.get("id").is_none() {
3676        // Handle notification
3677        if let Ok(notification) = serde_json::from_value::<JsonRpcNotification>(parsed)
3678            && let Ok(mcp_notification) = McpNotification::from_jsonrpc(&notification)
3679        {
3680            // Per the MCP 2025-11-25 spec, clients MUST send
3681            // `notifications/initialized` after receiving the `initialize`
3682            // response and before sending any other requests. Record the
3683            // receipt so the strict_initialization check below can allow
3684            // subsequent tool/resource/prompt requests.
3685            if matches!(&mcp_notification, McpNotification::Initialized) {
3686                session
3687                    .initialized_notification_received
3688                    .store(true, Ordering::Release);
3689                tracing::debug!(session_id = %session.id, "Received notifications/initialized");
3690            }
3691            session.handle_notification(mcp_notification);
3692        }
3693        return StatusCode::ACCEPTED.into_response();
3694    }
3695
3696    // Enforce `notifications/initialized` before any non-initialize request
3697    // (MCP 2025-11-25 spec requirement). This only applies to the session-based
3698    // path; stateless requests (2026-07-28) are handled above and never reach here.
3699    if !is_init
3700        && state.strict_initialization
3701        && !session
3702            .initialized_notification_received
3703            .load(Ordering::Acquire)
3704    {
3705        let id = extract_request_id(&parsed);
3706        tracing::warn!(
3707            session_id = %session.id,
3708            "Rejecting request: notifications/initialized not yet received"
3709        );
3710        return json_rpc_error_response(
3711            id,
3712            JsonRpcError::invalid_request(
3713                "Client must send notifications/initialized before making requests",
3714            ),
3715        );
3716    }
3717
3718    // For initialize requests, capture the advertised client info /
3719    // capabilities from the raw params before `parsed` is consumed by
3720    // deserialization. These are stashed onto the live `Session` after a
3721    // successful initialize so the persisted SessionRecord faithfully
3722    // describes the client (rather than carrying the defaults set at
3723    // session-create time).
3724    let init_client_metadata: Option<(Option<Implementation>, Option<ClientCapabilities>)> =
3725        if is_init {
3726            let params = parsed.get("params");
3727            let client_info = params
3728                .and_then(|p| p.get("clientInfo"))
3729                .and_then(|v| serde_json::from_value::<Implementation>(v.clone()).ok());
3730            let client_capabilities = params
3731                .and_then(|p| p.get("capabilities"))
3732                .and_then(|v| serde_json::from_value::<ClientCapabilities>(v.clone()).ok());
3733            Some((client_info, client_capabilities))
3734        } else {
3735            None
3736        };
3737
3738    // Handle as JSON-RPC request
3739    let request: JsonRpcRequest = match serde_json::from_value(parsed) {
3740        Ok(r) => r,
3741        Err(e) => {
3742            return json_rpc_error_response(
3743                None,
3744                JsonRpcError::parse_error(format!("Invalid request: {}", e)),
3745            );
3746        }
3747    };
3748
3749    // Process the request through the middleware-wrapped service
3750    let mut service = JsonRpcService::new(session.make_service());
3751
3752    // Bridge per-request data from HTTP into MCP Extensions: OAuth claims,
3753    // SEP-2575 `_meta` (clientInfo, clientCapabilities, etc.). Empty ext is
3754    // skipped to avoid pointless allocation.
3755    #[allow(unused_mut)]
3756    let mut ext = crate::router::Extensions::new();
3757    ext.insert(state.protocol_support.clone());
3758    ext.insert(session_revision);
3759    #[cfg(feature = "oauth")]
3760    if let Some(claims) = http_extensions.get::<crate::oauth::token::TokenClaims>() {
3761        ext.insert(claims.clone());
3762    }
3763    #[cfg(feature = "stateless")]
3764    stash_per_request_meta(&request, &mut ext);
3765
3766    // SEP-2260: legacy server-to-client requests are associated with the
3767    // client POST that caused them. Give this request its own channel while
3768    // drawing IDs from the session-wide allocator so concurrent POSTs cannot
3769    // collide or leak requests onto one another's response streams.
3770    let mut associated_request_rx = if !is_init {
3771        session.request_id_allocator.as_ref().map(|next_id| {
3772            let (request_tx, request_rx) = outgoing_request_channel(32);
3773            let requester: ClientRequesterHandle = Arc::new(
3774                ChannelClientRequester::with_id_allocator(request_tx, next_id.clone()),
3775            );
3776            ext.insert(requester);
3777            request_rx
3778        })
3779    } else {
3780        None
3781    };
3782
3783    if !ext.is_empty() {
3784        service = service.with_extensions(ext);
3785    }
3786
3787    let request_id = request.id.clone();
3788    let mut call: AssociatedCall = Box::pin(async move { service.call_single(request).await });
3789    let mut response = if let Some(mut request_rx) = associated_request_rx.take() {
3790        tokio::select! {
3791            result = &mut call => match result {
3792                Ok(response) => response,
3793                Err(error) => {
3794                    return json_rpc_error_response(
3795                        Some(request_id),
3796                        JsonRpcError::internal_error(error.to_string()),
3797                    );
3798                }
3799            },
3800            outgoing = request_rx.recv() => {
3801                match outgoing {
3802                    Some(outgoing) => {
3803                        let negotiated_version = session.protocol_version.read().await.clone();
3804                        return associated_request_sse_response(
3805                            session,
3806                            call,
3807                            request_rx,
3808                            outgoing,
3809                            request_id,
3810                            request_method,
3811                            negotiated_version,
3812                        );
3813                    }
3814                    None => match call.await {
3815                        Ok(response) => response,
3816                        Err(error) => {
3817                            return json_rpc_error_response(
3818                                Some(request_id),
3819                                JsonRpcError::internal_error(error.to_string()),
3820                            );
3821                        }
3822                    },
3823                }
3824            }
3825        }
3826    } else {
3827        match call.await {
3828            Ok(response) => response,
3829            Err(error) => {
3830                return json_rpc_error_response(
3831                    Some(request_id),
3832                    JsonRpcError::internal_error(error.to_string()),
3833                );
3834            }
3835        }
3836    };
3837
3838    // For successful initialize responses, extract and store the negotiated
3839    // protocol version, stash the client's advertised identity / capabilities
3840    // on the live session, and persist the now-complete record to the session
3841    // store so a restore from a peer instance sees the original client info
3842    // instead of defaults.
3843    if is_init && let JsonRpcResponse::Result(ref result) = response {
3844        if let Some(version) = result
3845            .result
3846            .get("protocolVersion")
3847            .and_then(|v| v.as_str())
3848        {
3849            *session.protocol_version.write().await = version.to_string();
3850        }
3851        if let Some((client_info, client_capabilities)) = init_client_metadata {
3852            *session.client_info.write().await = client_info;
3853            *session.client_capabilities.write().await = client_capabilities;
3854        }
3855        state.sessions.save_record(&session).await;
3856    }
3857
3858    let negotiated_version = session.protocol_version.read().await.clone();
3859    let response_version = if request_method == "server/discover"
3860        && state.protocol_support.contains(PROTOCOL_VERSION_2026_07_28)
3861    {
3862        PROTOCOL_VERSION_2026_07_28
3863    } else {
3864        &negotiated_version
3865    };
3866    apply_protocol_result_fields(&mut response, &request_method, response_version);
3867
3868    // Build response with headers
3869    let mut resp = if state.sse_responses {
3870        sse_json_response(&response)
3871    } else {
3872        axum::Json(response).into_response()
3873    };
3874
3875    if is_init {
3876        resp.headers_mut().insert(
3877            MCP_SESSION_ID_HEADER,
3878            HeaderValue::from_str(&session.id).unwrap(),
3879        );
3880    }
3881
3882    // Always include the negotiated protocol version header
3883    resp.headers_mut().insert(
3884        MCP_PROTOCOL_VERSION_HEADER,
3885        HeaderValue::from_str(&negotiated_version).unwrap(),
3886    );
3887
3888    resp
3889}
3890
3891/// Keep legacy server-to-client requests on the POST response stream that
3892/// caused them. These events deliberately have no SSE IDs and are not written
3893/// to the session event store: their response channels only exist on this
3894/// process and replaying them on another connection would break association.
3895fn associated_request_sse_response(
3896    session: Arc<Session>,
3897    mut call: AssociatedCall,
3898    mut request_rx: OutgoingRequestReceiver,
3899    first_outgoing: OutgoingRequest,
3900    original_request_id: RequestId,
3901    request_method: String,
3902    negotiated_version: String,
3903) -> Response {
3904    let (event_tx, event_rx) =
3905        tokio::sync::mpsc::channel::<std::result::Result<Event, Infallible>>(32);
3906    let call_version = negotiated_version.clone();
3907
3908    tokio::spawn(async move {
3909        let mut pending_ids = Vec::new();
3910        if !send_associated_request(&session, &event_tx, first_outgoing, &mut pending_ids).await {
3911            session
3912                .fail_pending_requests(
3913                    &pending_ids,
3914                    "originating POST disconnected before the client request was delivered",
3915                )
3916                .await;
3917            return;
3918        }
3919
3920        let mut requests_open = true;
3921        loop {
3922            tokio::select! {
3923                _ = event_tx.closed() => {
3924                    session
3925                        .fail_pending_requests(
3926                            &pending_ids,
3927                            "originating POST response stream disconnected",
3928                        )
3929                        .await;
3930                    return;
3931                }
3932                result = &mut call => {
3933                    session
3934                        .fail_pending_requests(
3935                            &pending_ids,
3936                            "originating POST completed before the client request response arrived",
3937                        )
3938                        .await;
3939
3940                    let mut response = match result {
3941                        Ok(response) => response,
3942                        Err(error) => JsonRpcResponse::error(
3943                            Some(original_request_id),
3944                            JsonRpcError::internal_error(error.to_string()),
3945                        ),
3946                    };
3947                    apply_protocol_result_fields(
3948                        &mut response,
3949                        &request_method,
3950                        &call_version,
3951                    );
3952
3953                    match serde_json::to_string(&response) {
3954                        Ok(data) => {
3955                            let _ = event_tx
3956                                .send(Ok(
3957                                    Event::default()
3958                                        .event(SSE_MESSAGE_EVENT)
3959                                        .data(data),
3960                                ))
3961                                .await;
3962                        }
3963                        Err(error) => {
3964                            tracing::error!(
3965                                error = %error,
3966                                "Failed to serialize associated POST response",
3967                            );
3968                        }
3969                    }
3970                    return;
3971                }
3972                outgoing = request_rx.recv(), if requests_open => {
3973                    match outgoing {
3974                        Some(outgoing) => {
3975                            if !send_associated_request(
3976                                &session,
3977                                &event_tx,
3978                                outgoing,
3979                                &mut pending_ids,
3980                            )
3981                            .await
3982                            {
3983                                session
3984                                    .fail_pending_requests(
3985                                        &pending_ids,
3986                                        "originating POST disconnected before the client request was delivered",
3987                                    )
3988                                    .await;
3989                                return;
3990                            }
3991                        }
3992                        None => requests_open = false,
3993                    }
3994                }
3995            }
3996        }
3997    });
3998
3999    let stream = tokio_stream::wrappers::ReceiverStream::new(event_rx);
4000    let mut response = Sse::new(stream)
4001        .keep_alive(
4002            axum::response::sse::KeepAlive::new()
4003                .interval(Duration::from_secs(30))
4004                .text("ping"),
4005        )
4006        .into_response();
4007    response.headers_mut().insert(
4008        MCP_PROTOCOL_VERSION_HEADER,
4009        HeaderValue::from_str(&negotiated_version).unwrap(),
4010    );
4011    response
4012}
4013
4014async fn send_associated_request(
4015    session: &Session,
4016    event_tx: &tokio::sync::mpsc::Sender<std::result::Result<Event, Infallible>>,
4017    outgoing: OutgoingRequest,
4018    pending_ids: &mut Vec<RequestId>,
4019) -> bool {
4020    let id = outgoing.id.clone();
4021    let request = JsonRpcRequest {
4022        jsonrpc: "2.0".to_string(),
4023        id: id.clone(),
4024        method: outgoing.method,
4025        params: Some(outgoing.params),
4026    };
4027    let data = match serde_json::to_string(&request) {
4028        Ok(data) => data,
4029        Err(error) => {
4030            let _ = outgoing.response_tx.send(Err(Error::Internal(format!(
4031                "Failed to serialize associated client request: {error}"
4032            ))));
4033            return true;
4034        }
4035    };
4036
4037    session
4038        .add_pending_request(id.clone(), outgoing.response_tx)
4039        .await;
4040    pending_ids.push(id);
4041
4042    event_tx
4043        .send(Ok(Event::default().event(SSE_MESSAGE_EVENT).data(data)))
4044        .await
4045        .is_ok()
4046}
4047
4048/// Returns `true` when the given protocol version string enables `subscriptions/listen`.
4049///
4050/// `subscriptions/listen` is part of the 2026-07-28 spec (SEP-2575 / SEP-2567).
4051/// Unknown future dates do not opt into behavior that has not been compiled
4052/// and explicitly enabled.
4053fn version_supports_subscriptions_listen(
4054    version: &str,
4055    protocol_support: &ProtocolSupport,
4056) -> bool {
4057    version == PROTOCOL_VERSION_2026_07_28 && protocol_support.contains(version)
4058}
4059
4060/// Returns `true` when the given protocol version string enables stateless
4061/// (sessionless) mode for the HTTP transport.
4062///
4063/// Stateless mode is introduced in the 2026-07-28 protocol (SEP-2575 /
4064/// SEP-2567). Only the exact, compiled-and-enabled version opts in; unknown
4065/// future dates must not silently inherit revision-specific behavior.
4066#[cfg(feature = "stateless")]
4067fn is_stateless_protocol_version(version: &str) -> bool {
4068    version == PROTOCOL_VERSION_2026_07_28
4069}
4070
4071/// Stamp `_meta["io.modelcontextprotocol/serverInfo"]` onto a successful
4072/// response, per SEP-2575: servers SHOULD identify themselves in each
4073/// result's `_meta` unless configured not to (see
4074/// [`HttpTransport::stamp_server_info()`]).
4075///
4076/// A no-op for error responses, and for any result whose top-level JSON
4077/// value isn't an object (defensive; every `McpResponse` variant serializes
4078/// to an object).
4079#[cfg(feature = "stateless")]
4080fn stamp_server_info(response: &mut JsonRpcResponse, implementation: &Implementation) {
4081    let JsonRpcResponse::Result(result) = response else {
4082        return;
4083    };
4084    let Some(obj) = result.result.as_object_mut() else {
4085        return;
4086    };
4087    let meta = obj
4088        .entry("_meta")
4089        .or_insert_with(|| serde_json::Value::Object(Default::default()));
4090    let Some(meta_obj) = meta.as_object_mut() else {
4091        return;
4092    };
4093    if let Ok(value) = serde_json::to_value(implementation) {
4094        meta_obj.insert("io.modelcontextprotocol/serverInfo".to_string(), value);
4095    }
4096}
4097
4098/// Drop guard that cancels a per-request [`CancellationToken`] when the
4099/// request is abandoned before its response is produced.
4100///
4101/// On the sessionless POST path the response future (plain JSON) or the SSE
4102/// response stream is dropped when the client disconnects; holding this
4103/// guard in that future/stream turns the drop into a cancellation signal.
4104/// [`disarm`](Self::disarm) once the handler's terminal response resolves
4105/// so normal completion doesn't signal cancellation.
4106#[cfg(feature = "stateless")]
4107struct CancelOnDisconnect(Option<crate::context::CancellationToken>);
4108
4109#[cfg(feature = "stateless")]
4110impl CancelOnDisconnect {
4111    fn arm(token: crate::context::CancellationToken) -> Self {
4112        Self(Some(token))
4113    }
4114
4115    fn disarm(&mut self) {
4116        self.0 = None;
4117    }
4118}
4119
4120#[cfg(feature = "stateless")]
4121impl Drop for CancelOnDisconnect {
4122    fn drop(&mut self) {
4123        if let Some(token) = self.0.take() {
4124            token.cancel();
4125        }
4126    }
4127}
4128
4129/// Stream a sessionless POST response as SSE: the notifications the handler
4130/// emitted, in order, followed by the terminal JSON-RPC response.
4131///
4132/// Invoked when a handler produced a notification before its terminal
4133/// response on the 2026-07-28 sessionless path. A plain JSON body would drop
4134/// those notifications (there is no session stream to carry them), so the
4135/// response falls back to `text/event-stream`: the buffered first
4136/// notification, any further notifications as they arrive, and finally the
4137/// terminal response, after which the stream ends.
4138#[cfg(feature = "stateless")]
4139struct StatelessSseContext {
4140    version: String,
4141    method: String,
4142    cancel_guard: CancelOnDisconnect,
4143    server_identity: Option<Implementation>,
4144    subscriptions: Arc<ModernSubscriptionRegistry>,
4145}
4146
4147#[cfg(feature = "stateless")]
4148fn stateless_sse_with_notifications(
4149    first: crate::context::ServerNotification,
4150    call: std::pin::Pin<
4151        Box<dyn std::future::Future<Output = crate::error::Result<JsonRpcResponse>> + Send>,
4152    >,
4153    rx: crate::context::NotificationReceiver,
4154    request: StatelessSseContext,
4155) -> Response {
4156    struct Ctx {
4157        call: Option<
4158            std::pin::Pin<
4159                Box<dyn std::future::Future<Output = crate::error::Result<JsonRpcResponse>> + Send>,
4160            >,
4161        >,
4162        rx: crate::context::NotificationReceiver,
4163        rx_open: bool,
4164        queue: std::collections::VecDeque<String>,
4165        terminal: Option<String>,
4166        version: String,
4167        method: String,
4168        /// Cancels the per-request token if the client disconnects (the
4169        /// stream, and with it this state, is dropped) while the handler
4170        /// is still in flight. Disarmed once the handler resolves.
4171        cancel_guard: CancelOnDisconnect,
4172        /// Stamped into `_meta.serverInfo` on the terminal response, if set
4173        /// (see [`HttpTransport::stamp_server_info()`]).
4174        server_identity: Option<Implementation>,
4175        subscriptions: Arc<ModernSubscriptionRegistry>,
4176    }
4177
4178    let mut queue = std::collections::VecDeque::new();
4179    if !request.subscriptions.publish(&first)
4180        && let Some(json) = crate::transport::stdio::serialize_notification(&first)
4181    {
4182        queue.push_back(json);
4183    }
4184    let ctx = Ctx {
4185        call: Some(call),
4186        rx,
4187        rx_open: true,
4188        queue,
4189        terminal: None,
4190        version: request.version,
4191        method: request.method,
4192        cancel_guard: request.cancel_guard,
4193        server_identity: request.server_identity,
4194        subscriptions: request.subscriptions,
4195    };
4196
4197    let stream = futures::stream::unfold(ctx, |mut ctx| async move {
4198        loop {
4199            // Buffered notifications flush first to preserve emission order.
4200            if let Some(json) = ctx.queue.pop_front() {
4201                return Some((
4202                    Ok::<_, Infallible>(Event::default().event(SSE_MESSAGE_EVENT).data(json)),
4203                    ctx,
4204                ));
4205            }
4206            // The terminal response is the last event on the stream.
4207            if let Some(json) = ctx.terminal.take() {
4208                return Some((
4209                    Ok(Event::default().event(SSE_MESSAGE_EVENT).data(json)),
4210                    ctx,
4211                ));
4212            }
4213            let mut call = ctx.call.take()?;
4214            tokio::select! {
4215                result = &mut call => {
4216                    // Handler finished; a later disconnect is no longer a
4217                    // cancellation.
4218                    ctx.cancel_guard.disarm();
4219                    // Drain notifications that were queued before the handler
4220                    // finished so they precede the terminal response.
4221                    while let Ok(n) = ctx.rx.try_recv() {
4222                        if !ctx.subscriptions.publish(&n)
4223                            && let Some(json) =
4224                                crate::transport::stdio::serialize_notification(&n)
4225                        {
4226                            ctx.queue.push_back(json);
4227                        }
4228                    }
4229                    let terminal_json = match result {
4230                        Ok(mut response) => {
4231                            // Same initialize version patch as the JSON path.
4232                            if ctx.method == "initialize"
4233                                && let JsonRpcResponse::Result(ref mut r) = response
4234                                && let Some(pv) = r.result.get_mut("protocolVersion")
4235                            {
4236                                *pv = serde_json::Value::String(ctx.version.clone());
4237                            }
4238                            apply_protocol_result_fields(
4239                                &mut response,
4240                                &ctx.method,
4241                                &ctx.version,
4242                            );
4243                            if let Some(ref identity) = ctx.server_identity {
4244                                stamp_server_info(&mut response, identity);
4245                            }
4246                            serde_json::to_string(&response).ok()
4247                        }
4248                        Err(e) => Some(
4249                            serde_json::json!({
4250                                "jsonrpc": "2.0",
4251                                "id": serde_json::Value::Null,
4252                                "error": JsonRpcError::internal_error(e.to_string()),
4253                            })
4254                            .to_string(),
4255                        ),
4256                    };
4257                    ctx.terminal = terminal_json;
4258                    // `call` is complete and intentionally not restored.
4259                }
4260                maybe = ctx.rx.recv(), if ctx.rx_open => {
4261                    match maybe {
4262                        Some(n) => {
4263                            if !ctx.subscriptions.publish(&n)
4264                                && let Some(json) =
4265                                    crate::transport::stdio::serialize_notification(&n)
4266                            {
4267                                ctx.queue.push_back(json);
4268                            }
4269                        }
4270                        None => ctx.rx_open = false,
4271                    }
4272                    ctx.call = Some(call);
4273                }
4274            }
4275        }
4276    });
4277
4278    Sse::new(stream)
4279        .keep_alive(
4280            axum::response::sse::KeepAlive::new()
4281                .interval(Duration::from_secs(30))
4282                .text("ping"),
4283        )
4284        .into_response()
4285}
4286
4287/// Serve the final, sessionless `subscriptions/listen` protocol over its
4288/// owning POST response.
4289#[cfg(feature = "stateless")]
4290async fn handle_modern_subscriptions_listen_sse(
4291    state: Arc<AppState>,
4292    parsed: &serde_json::Value,
4293) -> Response {
4294    let id = extract_request_id(parsed);
4295    let Some(subscription_id) = id.clone() else {
4296        return json_rpc_error_response_with_status(
4297            None,
4298            JsonRpcError::invalid_request("subscriptions/listen requires a request id"),
4299            StatusCode::BAD_REQUEST,
4300        );
4301    };
4302    let request: JsonRpcRequest = match serde_json::from_value(parsed.clone()) {
4303        Ok(request) => request,
4304        Err(error) => {
4305            return json_rpc_error_response_with_status(
4306                id,
4307                JsonRpcError::invalid_request(format!("Invalid request: {error}")),
4308                StatusCode::BAD_REQUEST,
4309            );
4310        }
4311    };
4312
4313    // Dispatch the request through the per-request service before upgrading,
4314    // so `Service<RouterRequest>` middleware observes accepted and rejected
4315    // listens and the router owns validation and filter negotiation (#1182).
4316    // The service response never reaches the wire: an error is returned as
4317    // the reply, and a success carries the accepted filter this handler
4318    // consumes to register the stream. The stream lifetime stays entirely
4319    // transport-owned.
4320    let service = match &state.service_source {
4321        ServiceSource::Router { router, factory } => {
4322            let ephemeral = router.with_fresh_session();
4323            ephemeral.session().mark_initialized();
4324            JsonRpcService::new(factory(ephemeral))
4325        }
4326        ServiceSource::Service(mutex) => JsonRpcService::new(mutex.lock().unwrap().clone()),
4327    };
4328    let mut ext = crate::router::Extensions::new();
4329    ext.insert(state.protocol_support.clone());
4330    stash_per_request_meta(&request, &mut ext);
4331    if ext
4332        .get::<crate::stateless::StatelessRequestMeta>()
4333        .is_none()
4334    {
4335        // This handler is only reached for effective-final requests, but the
4336        // version can arrive via the HTTP header with no per-request `_meta`.
4337        // Seed the meta so the router classifies the request correctly.
4338        ext.insert(crate::stateless::StatelessRequestMeta {
4339            protocol_version: Some(crate::protocol::PROTOCOL_VERSION_2026_07_28.to_string()),
4340            ..Default::default()
4341        });
4342    }
4343    let mut service = service.with_extensions(ext);
4344
4345    let response = match service.call_single(request).await {
4346        Ok(response) => response,
4347        Err(error) => {
4348            return json_rpc_error_response_with_status(
4349                id,
4350                JsonRpcError::internal_error(error.to_string()),
4351                StatusCode::INTERNAL_SERVER_ERROR,
4352            );
4353        }
4354    };
4355    let accepted = match &response {
4356        JsonRpcResponse::Result(result) => match result
4357            .result
4358            .get("notifications")
4359            .cloned()
4360            .map(serde_json::from_value::<SubscriptionFilter>)
4361        {
4362            Some(Ok(accepted)) => accepted,
4363            _ => {
4364                return json_rpc_error_response_with_status(
4365                    id,
4366                    JsonRpcError::internal_error(
4367                        "subscriptions/listen produced an unrecognized service result",
4368                    ),
4369                    StatusCode::INTERNAL_SERVER_ERROR,
4370                );
4371            }
4372        },
4373        _ => {
4374            // Rejected: middleware already observed the error; reply with it.
4375            let mut resp = axum::Json(&response).into_response();
4376            *resp.status_mut() = StatusCode::BAD_REQUEST;
4377            return resp;
4378        }
4379    };
4380
4381    let (rx, guard) = state
4382        .modern_subscriptions
4383        .register(subscription_id.clone(), accepted.clone());
4384    let acknowledgment = serde_json::json!({
4385        "jsonrpc": "2.0",
4386        "method": "notifications/subscriptions/acknowledged",
4387        "params": {
4388            "_meta": {
4389                "io.modelcontextprotocol/subscriptionId": subscription_id
4390            },
4391            "notifications": accepted
4392        }
4393    })
4394    .to_string();
4395
4396    struct ModernListenStream {
4397        first: Option<String>,
4398        rx: mpsc::UnboundedReceiver<String>,
4399        _guard: ModernSubscriptionGuard,
4400    }
4401
4402    let stream = futures::stream::unfold(
4403        ModernListenStream {
4404            first: Some(acknowledgment),
4405            rx,
4406            _guard: guard,
4407        },
4408        |mut state| async move {
4409            let message = match state.first.take() {
4410                Some(first) => Some(first),
4411                None => state.rx.recv().await,
4412            }?;
4413            Some((
4414                Ok::<_, Infallible>(Event::default().event(SSE_MESSAGE_EVENT).data(message)),
4415                state,
4416            ))
4417        },
4418    );
4419
4420    let mut response = Sse::new(stream)
4421        .keep_alive(
4422            axum::response::sse::KeepAlive::new()
4423                .interval(Duration::from_secs(30))
4424                .text("ping"),
4425        )
4426        .into_response();
4427    response.headers_mut().insert(
4428        MCP_PROTOCOL_VERSION_HEADER,
4429        HeaderValue::from_static(PROTOCOL_VERSION_2026_07_28),
4430    );
4431    response
4432}
4433
4434/// Serve a `subscriptions/listen` request as an SSE stream.
4435///
4436/// Subscribes to the session's notification broadcast channel and returns a
4437/// streaming `text/event-stream` response. The stream closes naturally when:
4438/// - The client disconnects (axum drops the response body).
4439/// - The broadcast channel closes (server shutdown / session expiry).
4440///
4441/// Each notification is assigned a monotonically increasing event ID for
4442/// potential stream resumption (SEP-1699).
4443async fn handle_subscriptions_listen_sse(session: Arc<Session>) -> Response {
4444    let rx = session.notifications_tx.subscribe();
4445    let session_clone = session.clone();
4446
4447    let stream = BroadcastStream::new(rx)
4448        .then(move |result: std::result::Result<String, _>| {
4449            let session = session_clone.clone();
4450            async move {
4451                match result {
4452                    Ok(msg) => {
4453                        let event_id = session.next_event_id();
4454                        // Buffer the event for potential replay (SEP-1699)
4455                        session.buffer_event(event_id, msg.clone()).await;
4456                        Some(Ok::<_, Infallible>(
4457                            Event::default()
4458                                .id(event_id.to_string())
4459                                .event(SSE_MESSAGE_EVENT)
4460                                .data(msg),
4461                        ))
4462                    }
4463                    Err(_) => None,
4464                }
4465            }
4466        })
4467        .filter_map(|x| x);
4468
4469    Sse::new(stream)
4470        .keep_alive(
4471            axum::response::sse::KeepAlive::new()
4472                .interval(Duration::from_secs(30))
4473                .text("ping"),
4474        )
4475        .into_response()
4476}
4477
4478/// Handle GET requests (SSE stream for server notifications and outgoing requests)
4479async fn handle_get(
4480    State(state): State<Arc<AppState>>,
4481    request: axum::extract::Request,
4482) -> Response {
4483    let (parts, _body) = request.into_parts();
4484    let headers = parts.headers;
4485    let uri = parts.uri.clone();
4486
4487    // Validate Host (DNS rebinding defense, complement to Origin)
4488    if let Some(resp) = validate_host(&headers, &uri, &state) {
4489        return resp;
4490    }
4491
4492    // Validate Origin
4493    if let Some(resp) = validate_origin(&headers, &state) {
4494        return resp;
4495    }
4496
4497    // Check Accept header
4498    let accept = headers
4499        .get(header::ACCEPT)
4500        .and_then(|v| v.to_str().ok())
4501        .unwrap_or("");
4502
4503    if !accept.contains("text/event-stream") {
4504        return (
4505            StatusCode::NOT_ACCEPTABLE,
4506            "Accept header must include text/event-stream",
4507        )
4508            .into_response();
4509    }
4510
4511    // Get session
4512    let session_id = match get_session_id(&headers) {
4513        Some(id) => id,
4514        None => {
4515            return json_rpc_error_response(None, JsonRpcError::session_required());
4516        }
4517    };
4518
4519    let session = match state.sessions.get(&session_id).await {
4520        Some(s) => s,
4521        None => {
4522            return json_rpc_error_response(
4523                None,
4524                JsonRpcError::session_not_found_with_id(&session_id),
4525            );
4526        }
4527    };
4528
4529    // Check for Last-Event-ID header for stream resumption (SEP-1699)
4530    let last_event_id = get_last_event_id(&headers);
4531
4532    // GET is the resumable notification stream. Restricted server-to-client
4533    // requests are emitted only on their originating POST response stream.
4534    let rx = session.notifications_tx.subscribe();
4535    let session_clone = session.clone();
4536
4537    // Replay buffered events if Last-Event-ID was provided (SEP-1699)
4538    let replay_events: Vec<_> = if let Some(after_id) = last_event_id {
4539        let events = session.get_events_after(after_id).await;
4540        tracing::debug!(
4541            after_id = after_id,
4542            replay_count = events.len(),
4543            "Replaying buffered events for stream resumption"
4544        );
4545        events
4546            .into_iter()
4547            .map(|e| {
4548                Ok::<_, Infallible>(
4549                    Event::default()
4550                        .id(e.id.to_string())
4551                        .event(SSE_MESSAGE_EVENT)
4552                        .data(e.data),
4553                )
4554            })
4555            .collect()
4556    } else {
4557        Vec::new()
4558    };
4559
4560    // Create replay stream from buffered events
4561    let replay_stream = tokio_stream::iter(replay_events);
4562
4563    // Create live stream for new events
4564    // Use `then` for async processing, then `filter_map` to remove errors
4565    let live_stream = BroadcastStream::new(rx)
4566        .then(move |result: std::result::Result<String, _>| {
4567            let session = session_clone.clone();
4568            async move {
4569                match result {
4570                    Ok(msg) => {
4571                        let event_id = session.next_event_id();
4572                        // Buffer the event for potential replay (SEP-1699)
4573                        session.buffer_event(event_id, msg.clone()).await;
4574                        Some(Ok::<_, Infallible>(
4575                            Event::default()
4576                                .id(event_id.to_string())
4577                                .event(SSE_MESSAGE_EVENT)
4578                                .data(msg),
4579                        ))
4580                    }
4581                    Err(_) => None,
4582                }
4583            }
4584        })
4585        .filter_map(|x| x);
4586
4587    // Chain replay stream with live stream
4588    let stream = replay_stream.chain(live_stream);
4589
4590    Sse::new(stream)
4591        .keep_alive(
4592            axum::response::sse::KeepAlive::new()
4593                .interval(Duration::from_secs(30))
4594                .text("ping"),
4595        )
4596        .into_response()
4597}
4598
4599/// Handle DELETE requests (session termination)
4600async fn handle_delete(
4601    State(state): State<Arc<AppState>>,
4602    request: axum::extract::Request,
4603) -> Response {
4604    let (parts, _body) = request.into_parts();
4605    let headers = parts.headers;
4606    let uri = parts.uri.clone();
4607
4608    // Validate Host (DNS rebinding defense, complement to Origin)
4609    if let Some(resp) = validate_host(&headers, &uri, &state) {
4610        return resp;
4611    }
4612
4613    // Validate Origin
4614    if let Some(resp) = validate_origin(&headers, &state) {
4615        return resp;
4616    }
4617
4618    let session_id = match get_session_id(&headers) {
4619        Some(id) => id,
4620        None => {
4621            return json_rpc_error_response(None, JsonRpcError::session_required());
4622        }
4623    };
4624
4625    if state.sessions.remove(&session_id).await {
4626        tracing::info!(session_id = %session_id, "Session terminated");
4627        StatusCode::OK.into_response()
4628    } else {
4629        // For DELETE, it's okay if the session doesn't exist - it's already gone
4630        // Return OK instead of an error for idempotency
4631        tracing::debug!(session_id = %session_id, "Session already removed or never existed");
4632        StatusCode::OK.into_response()
4633    }
4634}
4635
4636/// Handle GET /health requests
4637///
4638/// Returns a simple 200 OK response for health checks.
4639/// Does not require authentication or session state.
4640async fn handle_health() -> Response {
4641    StatusCode::OK.into_response()
4642}
4643
4644/// Build a synchronous JSON-RPC response wrapped in SSE format.
4645///
4646/// Used when [`AppState::sse_responses`] is `true`. The body is a single SSE
4647/// event followed by the required blank line:
4648///
4649/// ```text
4650/// event: message
4651/// data: <json>
4652///
4653/// ```
4654fn sse_json_response(response: impl serde::Serialize) -> Response {
4655    let json = match serde_json::to_string(&response) {
4656        Ok(s) => s,
4657        Err(e) => {
4658            tracing::error!(error = %e, "Failed to serialize response for SSE wrapping");
4659            return StatusCode::INTERNAL_SERVER_ERROR.into_response();
4660        }
4661    };
4662    let sse_body = format!("event: message\ndata: {json}\n\n");
4663    (
4664        StatusCode::OK,
4665        [
4666            (header::CONTENT_TYPE, "text/event-stream"),
4667            (header::CACHE_CONTROL, "no-cache"),
4668        ],
4669        sse_body,
4670    )
4671        .into_response()
4672}
4673
4674/// Create a JSON-RPC error response
4675fn json_rpc_error_response(
4676    id: Option<crate::protocol::RequestId>,
4677    error: JsonRpcError,
4678) -> Response {
4679    let response = JsonRpcResponse::error(id, error);
4680    axum::Json(response).into_response()
4681}
4682
4683fn json_rpc_error_response_with_status(
4684    id: Option<crate::protocol::RequestId>,
4685    error: JsonRpcError,
4686    status: StatusCode,
4687) -> Response {
4688    let mut response = json_rpc_error_response(id, error);
4689    *response.status_mut() = status;
4690    response
4691}
4692
4693/// HTTP 413 response for a POST body exceeding [`HttpTransport::max_body_size`].
4694fn body_too_large_response(limit: usize) -> Response {
4695    let mut resp = json_rpc_error_response(
4696        None,
4697        JsonRpcError::invalid_request(format!(
4698            "Request body exceeds the maximum size of {} bytes",
4699            limit
4700        )),
4701    );
4702    *resp.status_mut() = StatusCode::PAYLOAD_TOO_LARGE;
4703    resp
4704}
4705
4706/// Returns `true` when the body-read error was caused by exceeding the
4707/// configured length limit (as opposed to a transport-level I/O failure).
4708fn is_length_limit_error(err: &axum::Error) -> bool {
4709    let mut source: Option<&(dyn std::error::Error + 'static)> = Some(err);
4710    while let Some(e) = source {
4711        if e.is::<http_body_util::LengthLimitError>() {
4712            return true;
4713        }
4714        source = e.source();
4715    }
4716    false
4717}
4718
4719#[cfg(test)]
4720mod tests {
4721    use super::*;
4722    use axum::body::Body;
4723    use axum::http::Request;
4724    use proptest::prelude::*;
4725    use tower::ServiceExt;
4726
4727    #[cfg(feature = "oauth")]
4728    fn oauth_test_token(audience: &str, scope: &str) -> String {
4729        jsonwebtoken::encode(
4730            &jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256),
4731            &serde_json::json!({
4732                "sub": "test-user",
4733                "aud": audience,
4734                "scope": scope,
4735            }),
4736            &jsonwebtoken::EncodingKey::from_secret(b"resource-server-test-secret"),
4737        )
4738        .unwrap()
4739    }
4740
4741    #[cfg(feature = "oauth")]
4742    #[tokio::test]
4743    async fn oauth_resource_server_setup_is_path_aware_and_audience_bound() {
4744        let resource = "http://localhost:3000/tenant/mcp";
4745        let metadata = crate::oauth::ProtectedResourceMetadata::new(resource)
4746            .authorization_server("https://auth.example.com")
4747            .scope("mcp:read");
4748        let validator = crate::oauth::JwtValidator::from_secret(b"resource-server-test-secret")
4749            .disable_exp_validation();
4750        let app = HttpTransport::new(create_test_router())
4751            .disable_origin_validation()
4752            .disable_host_validation()
4753            .into_oauth_router_at(
4754                "/tenant/mcp",
4755                validator,
4756                metadata,
4757                crate::oauth::ScopePolicy::new().default_scope("mcp:read"),
4758            )
4759            .unwrap();
4760
4761        let metadata_request = Request::builder()
4762            .uri("/.well-known/oauth-protected-resource/tenant/mcp")
4763            .body(Body::empty())
4764            .unwrap();
4765        let metadata_response = app.clone().oneshot(metadata_request).await.unwrap();
4766        assert_eq!(metadata_response.status(), StatusCode::OK);
4767
4768        let unauthenticated = Request::builder()
4769            .method("POST")
4770            .uri("/tenant/mcp")
4771            .body(Body::empty())
4772            .unwrap();
4773        let response = app.clone().oneshot(unauthenticated).await.unwrap();
4774        assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
4775        assert!(
4776            response
4777                .headers()
4778                .get("WWW-Authenticate")
4779                .unwrap()
4780                .to_str()
4781                .unwrap()
4782                .contains("http://localhost:3000/.well-known/oauth-protected-resource/tenant/mcp")
4783        );
4784
4785        let wrong_audience = oauth_test_token("http://localhost:3000/other", "mcp:read");
4786        let request = Request::builder()
4787            .method("POST")
4788            .uri("/tenant/mcp")
4789            .header("Authorization", format!("Bearer {wrong_audience}"))
4790            .body(Body::empty())
4791            .unwrap();
4792        assert_eq!(
4793            app.clone().oneshot(request).await.unwrap().status(),
4794            StatusCode::UNAUTHORIZED
4795        );
4796
4797        let token = oauth_test_token(resource, "mcp:read");
4798        let request = Request::builder()
4799            .method("POST")
4800            .uri("/tenant/mcp")
4801            .header("Authorization", format!("Bearer {token}"))
4802            .header("Content-Type", "application/json")
4803            .header("Accept", "application/json, text/event-stream")
4804            .body(Body::from(
4805                serde_json::json!({
4806                    "jsonrpc": "2.0",
4807                    "id": 1,
4808                    "method": "initialize",
4809                    "params": {
4810                        "protocolVersion": "2025-11-25",
4811                        "capabilities": {},
4812                        "clientInfo": { "name": "oauth-test", "version": "1.0" }
4813                    }
4814                })
4815                .to_string(),
4816            ))
4817            .unwrap();
4818        let response = app.oneshot(request).await.unwrap();
4819        assert_eq!(response.status(), StatusCode::OK);
4820        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
4821            .await
4822            .unwrap();
4823        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
4824        assert!(json.get("result").is_some(), "unexpected response: {json}");
4825    }
4826
4827    #[cfg(feature = "oauth")]
4828    #[test]
4829    fn oauth_resource_server_setup_validates_metadata() {
4830        let result = HttpTransport::new(create_test_router()).into_oauth_router(
4831            crate::oauth::JwtValidator::from_secret(b"secret"),
4832            crate::oauth::ProtectedResourceMetadata::new("https://mcp.example.com"),
4833            crate::oauth::ScopePolicy::new(),
4834        );
4835        assert!(matches!(
4836            result.unwrap_err(),
4837            crate::oauth::ProtectedResourceMetadataError::MissingAuthorizationServer
4838        ));
4839    }
4840
4841    fn arb_json() -> impl Strategy<Value = serde_json::Value> {
4842        let leaf = prop_oneof![
4843            Just(serde_json::Value::Null),
4844            any::<bool>().prop_map(serde_json::Value::Bool),
4845            any::<i64>().prop_map(|number| serde_json::json!(number)),
4846            prop::collection::vec(any::<char>(), 0..256)
4847                .prop_map(|chars| serde_json::Value::String(chars.into_iter().collect())),
4848        ];
4849        leaf.prop_recursive(6, 128, 10, |inner| {
4850            prop_oneof![
4851                prop::collection::vec(inner.clone(), 0..10).prop_map(serde_json::Value::Array),
4852                prop::collection::hash_map("[a-zA-Z0-9_]{0,24}", inner, 0..10)
4853                    .prop_map(|map| serde_json::Value::Object(map.into_iter().collect())),
4854            ]
4855        })
4856    }
4857
4858    proptest! {
4859        #![proptest_config(ProptestConfig::with_cases(512))]
4860
4861        /// Request-ID extraction runs on untrusted JSON before dispatch and
4862        /// must reject surprising shapes without panicking.
4863        #[test]
4864        fn extract_request_id_never_panics(value in arb_json()) {
4865            let _ = extract_request_id(&value);
4866        }
4867
4868        #[test]
4869        fn extract_request_id_accepts_all_i64_values(id in any::<i64>()) {
4870            prop_assert_eq!(
4871                extract_request_id(&serde_json::json!({ "id": id })),
4872                Some(RequestId::Number(id))
4873            );
4874        }
4875
4876        #[test]
4877        fn extract_request_id_accepts_arbitrary_strings(
4878            chars in prop::collection::vec(any::<char>(), 0..1024)
4879        ) {
4880            let id: String = chars.into_iter().collect();
4881            prop_assert_eq!(
4882                extract_request_id(&serde_json::json!({ "id": id })),
4883                Some(RequestId::String(id))
4884            );
4885        }
4886    }
4887
4888    fn create_test_router() -> McpRouter {
4889        McpRouter::new().server_info("test-server", "1.0.0")
4890    }
4891
4892    #[test]
4893    fn final_result_fields_are_method_and_version_aware() {
4894        for method in [
4895            "server/discover",
4896            "tools/list",
4897            "prompts/list",
4898            "resources/list",
4899            "resources/read",
4900            "resources/templates/list",
4901        ] {
4902            let mut response =
4903                JsonRpcResponse::result(RequestId::Number(1), serde_json::json!({"value": true}));
4904            apply_protocol_result_fields(&mut response, method, PROTOCOL_VERSION_2026_07_28);
4905            let json = serde_json::to_value(response).unwrap();
4906            assert_eq!(json["result"]["resultType"], "complete", "{method}");
4907            assert_eq!(json["result"]["ttlMs"], 0, "{method}");
4908            assert_eq!(json["result"]["cacheScope"], "private", "{method}");
4909        }
4910
4911        let mut ordinary =
4912            JsonRpcResponse::result(RequestId::Number(1), serde_json::json!({"content": []}));
4913        apply_protocol_result_fields(&mut ordinary, "tools/call", PROTOCOL_VERSION_2026_07_28);
4914        let json = serde_json::to_value(ordinary).unwrap();
4915        assert_eq!(json["result"]["resultType"], "complete");
4916        assert!(json["result"].get("ttlMs").is_none());
4917        assert!(json["result"].get("cacheScope").is_none());
4918    }
4919
4920    #[test]
4921    fn final_result_fields_preserve_explicit_values_and_legacy_wire_shape() {
4922        let explicit = serde_json::json!({
4923            "contents": [],
4924            "ttlMs": 42,
4925            "cacheScope": "public"
4926        });
4927        let mut response = JsonRpcResponse::result(RequestId::Number(1), explicit.clone());
4928        apply_protocol_result_fields(&mut response, "resources/read", PROTOCOL_VERSION_2026_07_28);
4929        let json = serde_json::to_value(response).unwrap();
4930        assert_eq!(json["result"]["ttlMs"], 42);
4931        assert_eq!(json["result"]["cacheScope"], "public");
4932
4933        for discriminator in ["input_required", "task"] {
4934            let mut response = JsonRpcResponse::result(
4935                RequestId::Number(1),
4936                serde_json::json!({"resultType": discriminator}),
4937            );
4938            apply_protocol_result_fields(&mut response, "tools/call", PROTOCOL_VERSION_2026_07_28);
4939            let json = serde_json::to_value(response).unwrap();
4940            assert_eq!(json["result"]["resultType"], discriminator);
4941        }
4942
4943        let mut legacy = JsonRpcResponse::result(RequestId::Number(1), explicit);
4944        let before = serde_json::to_value(&legacy).unwrap();
4945        apply_protocol_result_fields(&mut legacy, "resources/read", "2025-11-25");
4946        assert_eq!(serde_json::to_value(legacy).unwrap(), before);
4947    }
4948
4949    #[tokio::test]
4950    #[cfg(feature = "stateless")]
4951    async fn modern_subscription_registry_reports_closes() {
4952        use crate::transport::subscriptions::{
4953            SubscriptionClose, SubscriptionCloseReason, SubscriptionObserver,
4954        };
4955
4956        #[derive(Default)]
4957        struct RecordingObserver {
4958            closes: std::sync::Mutex<Vec<SubscriptionClose>>,
4959        }
4960        impl SubscriptionObserver for RecordingObserver {
4961            fn on_close(&self, close: SubscriptionClose) {
4962                self.closes.lock().unwrap().push(close);
4963            }
4964        }
4965
4966        let observer = Arc::new(RecordingObserver::default());
4967        let registry = Arc::new(ModernSubscriptionRegistry::new(
4968            None,
4969            Some(observer.clone()),
4970        ));
4971
4972        // A dropped guard is a client disconnect.
4973        let (_rx, guard) = registry.register(
4974            RequestId::String("disconnects".into()),
4975            SubscriptionFilter {
4976                tools_list_changed: Some(true),
4977                ..SubscriptionFilter::default()
4978            },
4979        );
4980        drop(guard);
4981
4982        // A drained registry is a graceful server-side close.
4983        let (_rx2, guard2) = registry.register(
4984            RequestId::String("drains".into()),
4985            SubscriptionFilter::default(),
4986        );
4987        assert_eq!(registry.close_all(), 1);
4988        // The entry is already gone, so the later guard drop must not
4989        // produce a second record.
4990        drop(guard2);
4991
4992        let closes = observer.closes.lock().unwrap();
4993        assert_eq!(closes.len(), 2, "one record per stream: {closes:?}");
4994        assert_eq!(
4995            closes[0].subscription_id,
4996            RequestId::String("disconnects".into())
4997        );
4998        assert_eq!(closes[0].reason, SubscriptionCloseReason::Disconnected);
4999        assert_eq!(
5000            closes[1].subscription_id,
5001            RequestId::String("drains".into())
5002        );
5003        assert_eq!(closes[1].reason, SubscriptionCloseReason::Drained);
5004    }
5005
5006    #[tokio::test]
5007    #[cfg(feature = "stateless")]
5008    async fn modern_subscription_registry_filters_and_tags_notifications() {
5009        let registry = Arc::new(ModernSubscriptionRegistry::default());
5010        let (mut rx, guard) = registry.register(
5011            RequestId::String("listen-1".to_string()),
5012            SubscriptionFilter {
5013                tools_list_changed: Some(true),
5014                ..SubscriptionFilter::default()
5015            },
5016        );
5017
5018        assert!(registry.publish(&ServerNotification::PromptsListChanged));
5019        assert!(matches!(
5020            rx.try_recv(),
5021            Err(mpsc::error::TryRecvError::Empty)
5022        ));
5023
5024        assert!(registry.publish(&ServerNotification::ToolsListChanged));
5025        let message = rx.recv().await.expect("matching notification");
5026        let json: serde_json::Value = serde_json::from_str(&message).unwrap();
5027        assert_eq!(json["method"], "notifications/tools/list_changed");
5028        assert_eq!(
5029            json["params"]["_meta"]["io.modelcontextprotocol/subscriptionId"],
5030            "listen-1"
5031        );
5032
5033        drop(guard);
5034        assert!(registry.subscriptions.lock().unwrap().is_empty());
5035    }
5036
5037    #[tokio::test]
5038    async fn runtime_protocol_allowlist_drives_discovery() {
5039        let transport = HttpTransport::new(create_test_router())
5040            .disable_origin_validation()
5041            .protocol_versions(["2025-03-26"])
5042            .unwrap();
5043        let app = transport.into_router();
5044        let request = Request::builder()
5045            .method("POST")
5046            .uri("/")
5047            .header("Content-Type", "application/json")
5048            .header("Accept", "application/json")
5049            .body(Body::from(
5050                serde_json::json!({
5051                    "jsonrpc": "2.0",
5052                    "id": 1,
5053                    "method": "server/discover"
5054                })
5055                .to_string(),
5056            ))
5057            .unwrap();
5058
5059        let response = app.oneshot(request).await.unwrap();
5060        assert_eq!(response.status(), StatusCode::OK);
5061        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
5062            .await
5063            .unwrap();
5064        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
5065        assert_eq!(
5066            json["result"]["supportedVersions"],
5067            serde_json::json!(["2025-03-26"])
5068        );
5069    }
5070
5071    #[tokio::test]
5072    async fn test_oversized_body_rejected_with_413() {
5073        let transport = HttpTransport::new(create_test_router())
5074            .disable_origin_validation()
5075            .max_body_size(1024);
5076        let app = transport.into_router();
5077
5078        // 2 KiB of body against a 1 KiB limit.
5079        let padding = "x".repeat(2048);
5080        let body = format!(
5081            r#"{{"jsonrpc":"2.0","id":1,"method":"ping","params":{{"pad":"{}"}}}}"#,
5082            padding
5083        );
5084        let request = Request::builder()
5085            .method("POST")
5086            .uri("/")
5087            .header("Content-Type", "application/json")
5088            .header("Accept", "application/json, text/event-stream")
5089            .body(Body::from(body))
5090            .unwrap();
5091
5092        let response = app.oneshot(request).await.unwrap();
5093        assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
5094    }
5095
5096    #[tokio::test]
5097    async fn test_oversized_content_length_rejected_without_reading() {
5098        let transport = HttpTransport::new(create_test_router())
5099            .disable_origin_validation()
5100            .max_body_size(1024);
5101        let app = transport.into_router();
5102
5103        // Declared Content-Length above the limit short-circuits even
5104        // though the actual body is tiny.
5105        let request = Request::builder()
5106            .method("POST")
5107            .uri("/")
5108            .header("Content-Type", "application/json")
5109            .header("Accept", "application/json, text/event-stream")
5110            .header("Content-Length", "10485760")
5111            .body(Body::from(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#))
5112            .unwrap();
5113
5114        let response = app.oneshot(request).await.unwrap();
5115        assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
5116    }
5117
5118    #[tokio::test]
5119    async fn test_body_within_limit_accepted() {
5120        let transport = HttpTransport::new(create_test_router())
5121            .disable_origin_validation()
5122            .max_body_size(1024);
5123        let app = transport.into_router();
5124
5125        let request = Request::builder()
5126            .method("POST")
5127            .uri("/")
5128            .header("Content-Type", "application/json")
5129            .header("Accept", "application/json, text/event-stream")
5130            .body(Body::from(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#))
5131            .unwrap();
5132
5133        let response = app.oneshot(request).await.unwrap();
5134        assert_eq!(response.status(), StatusCode::OK);
5135    }
5136
5137    #[tokio::test]
5138    async fn test_initialize_creates_session() {
5139        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
5140        let app = transport.into_router();
5141
5142        let request = Request::builder()
5143            .method("POST")
5144            .uri("/")
5145            .header("Content-Type", "application/json")
5146            .header("Accept", "application/json, text/event-stream")
5147            .body(Body::from(
5148                serde_json::json!({
5149                    "jsonrpc": "2.0",
5150                    "id": 1,
5151                    "method": "initialize",
5152                    "params": {
5153                        "protocolVersion": "2025-11-25",
5154                        "capabilities": {},
5155                        "clientInfo": {
5156                            "name": "test-client",
5157                            "version": "1.0.0"
5158                        }
5159                    }
5160                })
5161                .to_string(),
5162            ))
5163            .unwrap();
5164
5165        let response = app.oneshot(request).await.unwrap();
5166
5167        assert_eq!(response.status(), StatusCode::OK);
5168        assert!(response.headers().contains_key(MCP_SESSION_ID_HEADER));
5169        // Verify protocol version header is present on initialize response
5170        assert_eq!(
5171            response
5172                .headers()
5173                .get(MCP_PROTOCOL_VERSION_HEADER)
5174                .and_then(|v| v.to_str().ok()),
5175            Some("2025-11-25")
5176        );
5177    }
5178
5179    #[tokio::test]
5180    async fn test_protocol_version_header_on_subsequent_requests() {
5181        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
5182        let app = transport.into_router();
5183
5184        // Initialize
5185        let init_request = Request::builder()
5186            .method("POST")
5187            .uri("/")
5188            .header("Content-Type", "application/json")
5189            .header("Accept", "application/json, text/event-stream")
5190            .body(Body::from(
5191                serde_json::json!({
5192                    "jsonrpc": "2.0",
5193                    "id": 1,
5194                    "method": "initialize",
5195                    "params": {
5196                        "protocolVersion": "2025-03-26",
5197                        "capabilities": {},
5198                        "clientInfo": {
5199                            "name": "test-client",
5200                            "version": "1.0.0"
5201                        }
5202                    }
5203                })
5204                .to_string(),
5205            ))
5206            .unwrap();
5207
5208        let init_response = app.clone().oneshot(init_request).await.unwrap();
5209        let session_id = init_response
5210            .headers()
5211            .get(MCP_SESSION_ID_HEADER)
5212            .unwrap()
5213            .to_str()
5214            .unwrap()
5215            .to_string();
5216
5217        // Verify init response has negotiated version (2025-03-26, not latest)
5218        assert_eq!(
5219            init_response
5220                .headers()
5221                .get(MCP_PROTOCOL_VERSION_HEADER)
5222                .and_then(|v| v.to_str().ok()),
5223            Some("2025-03-26")
5224        );
5225
5226        // Send initialized notification
5227        let initialized_request = Request::builder()
5228            .method("POST")
5229            .uri("/")
5230            .header("Content-Type", "application/json")
5231            .header("Accept", "application/json, text/event-stream")
5232            .header(MCP_SESSION_ID_HEADER, &session_id)
5233            .header(MCP_PROTOCOL_VERSION_HEADER, "2025-03-26")
5234            .body(Body::from(
5235                serde_json::json!({
5236                    "jsonrpc": "2.0",
5237                    "method": "notifications/initialized"
5238                })
5239                .to_string(),
5240            ))
5241            .unwrap();
5242
5243        app.clone().oneshot(initialized_request).await.unwrap();
5244
5245        // Send tools/list and check for protocol version header
5246        let list_request = Request::builder()
5247            .method("POST")
5248            .uri("/")
5249            .header("Content-Type", "application/json")
5250            .header("Accept", "application/json, text/event-stream")
5251            .header(MCP_SESSION_ID_HEADER, &session_id)
5252            .header(MCP_PROTOCOL_VERSION_HEADER, "2025-03-26")
5253            .body(Body::from(
5254                serde_json::json!({
5255                    "jsonrpc": "2.0",
5256                    "id": 2,
5257                    "method": "tools/list"
5258                })
5259                .to_string(),
5260            ))
5261            .unwrap();
5262
5263        let response = app.oneshot(list_request).await.unwrap();
5264        assert_eq!(response.status(), StatusCode::OK);
5265        assert_eq!(
5266            response
5267                .headers()
5268                .get(MCP_PROTOCOL_VERSION_HEADER)
5269                .and_then(|v| v.to_str().ok()),
5270            Some("2025-03-26")
5271        );
5272    }
5273
5274    #[tokio::test]
5275    async fn unsupported_protocol_version_returns_spec_shape_error() {
5276        // SEP-2575: requests carrying an unrecognized MCP-Protocol-Version
5277        // header (post-initialize) get a JSON-RPC error with code -32022 and
5278        // data `{ supported: [...], requested: "..." }`.
5279        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
5280        let app = transport.into_router();
5281
5282        // Initialize first so we're past the init exemption.
5283        let init_request = Request::builder()
5284            .method("POST")
5285            .uri("/")
5286            .header("Content-Type", "application/json")
5287            .header("Accept", "application/json, text/event-stream")
5288            .body(Body::from(
5289                serde_json::json!({
5290                    "jsonrpc": "2.0",
5291                    "id": 1,
5292                    "method": "initialize",
5293                    "params": {
5294                        "protocolVersion": "2025-11-25",
5295                        "capabilities": {},
5296                        "clientInfo": { "name": "t", "version": "0" }
5297                    }
5298                })
5299                .to_string(),
5300            ))
5301            .unwrap();
5302        let init_response = app.clone().oneshot(init_request).await.unwrap();
5303        let session_id = init_response
5304            .headers()
5305            .get(MCP_SESSION_ID_HEADER)
5306            .unwrap()
5307            .to_str()
5308            .unwrap()
5309            .to_string();
5310
5311        // Now send a request with a bogus version header.
5312        let bad = Request::builder()
5313            .method("POST")
5314            .uri("/")
5315            .header("Content-Type", "application/json")
5316            .header("Accept", "application/json")
5317            .header(MCP_SESSION_ID_HEADER, &session_id)
5318            .header(MCP_PROTOCOL_VERSION_HEADER, "1999-01-01")
5319            .body(Body::from(
5320                serde_json::json!({
5321                    "jsonrpc": "2.0",
5322                    "id": 99,
5323                    "method": "tools/list"
5324                })
5325                .to_string(),
5326            ))
5327            .unwrap();
5328        let response = app.oneshot(bad).await.unwrap();
5329        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
5330            .await
5331            .unwrap();
5332        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
5333        assert_eq!(json["error"]["code"].as_i64().unwrap(), -32022);
5334        assert_eq!(json["error"]["data"]["requested"], "1999-01-01");
5335        let supported = json["error"]["data"]["supported"]
5336            .as_array()
5337            .expect("supported must be an array");
5338        assert!(supported.contains(&serde_json::json!("2025-11-25")));
5339        // The request id must be echoed (we have one in the body).
5340        assert_eq!(json["id"], 99);
5341        // Field name must be `supported`, NOT `supportedVersions` (SEP-2575 shape).
5342        assert!(
5343            json["error"]["data"].get("supportedVersions").is_none(),
5344            "error data must use 'supported', not 'supportedVersions': {:?}",
5345            json["error"]["data"]
5346        );
5347        // The supported set must exactly match the compiled transport default --
5348        // no extras, none missing.
5349        let expected: Vec<serde_json::Value> = crate::COMPILED_PROTOCOL_VERSIONS
5350            .iter()
5351            .map(|v| serde_json::json!(v))
5352            .collect();
5353        assert_eq!(
5354            supported, &expected,
5355            "data.supported must exactly match COMPILED_PROTOCOL_VERSIONS"
5356        );
5357    }
5358
5359    /// When a request (no session) arrives with an invalid `Mcp-Protocol-Version`
5360    /// header, the transport must return -32022 with the correct SEP-2575 wire
5361    /// shape: `{ supported: [...], requested: "..." }`. This verifies the
5362    /// version-validation path fires without requiring a session.
5363    #[cfg(feature = "stateless")]
5364    #[tokio::test]
5365    async fn stateless_unsupported_protocol_version_returns_spec_shape_error() {
5366        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
5367        let app = transport.into_router();
5368
5369        // A future-looking unknown version must not enter the 2026-07-28
5370        // stateless path merely because its date sorts after 2026-07-28.
5371        let req = Request::builder()
5372            .method("POST")
5373            .uri("/")
5374            .header("Content-Type", "application/json")
5375            .header("Accept", "application/json")
5376            .header(MCP_PROTOCOL_VERSION_HEADER, "2099-01-01")
5377            .body(Body::from(
5378                serde_json::json!({
5379                    "jsonrpc": "2.0",
5380                    "id": 42,
5381                    "method": "tools/list"
5382                })
5383                .to_string(),
5384            ))
5385            .unwrap();
5386        let response = app.oneshot(req).await.unwrap();
5387        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
5388            .await
5389            .unwrap();
5390        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
5391        assert_eq!(
5392            json["error"]["code"].as_i64().unwrap(),
5393            -32022,
5394            "must return UnsupportedProtocolVersion (-32022): {json}"
5395        );
5396        assert_eq!(
5397            json["error"]["data"]["requested"], "2099-01-01",
5398            "data.requested must echo the version: {json}"
5399        );
5400        // Field name must be `supported`, not `supportedVersions`.
5401        assert!(
5402            json["error"]["data"].get("supportedVersions").is_none(),
5403            "error data must use 'supported', not 'supportedVersions': {json}"
5404        );
5405        let supported = json["error"]["data"]["supported"]
5406            .as_array()
5407            .expect("data.supported must be an array");
5408        let expected: Vec<serde_json::Value> = crate::COMPILED_PROTOCOL_VERSIONS
5409            .iter()
5410            .map(|v| serde_json::json!(v))
5411            .collect();
5412        assert_eq!(
5413            supported, &expected,
5414            "data.supported must exactly match COMPILED_PROTOCOL_VERSIONS"
5415        );
5416    }
5417
5418    // =========================================================================
5419    // SEP-2243: HTTP header standardization (Mcp-Method, Mcp-Name, Mcp-Param-*)
5420    // =========================================================================
5421
5422    /// In lenient mode (negotiated protocol version < 2026-07-28) a
5423    /// request without any SEP-2243 headers must still succeed — older
5424    /// clients that haven't opted in must keep working.
5425    #[tokio::test]
5426    async fn sep_2243_lenient_mode_accepts_missing_headers() {
5427        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
5428        let app = transport.into_router();
5429
5430        // Initialize (no SEP-2243 headers) negotiates 2025-11-25 — lenient.
5431        let init = Request::builder()
5432            .method("POST")
5433            .uri("/")
5434            .header("Content-Type", "application/json")
5435            .header("Accept", "application/json, text/event-stream")
5436            .body(Body::from(
5437                serde_json::json!({
5438                    "jsonrpc": "2.0",
5439                    "id": 1,
5440                    "method": "initialize",
5441                    "params": {
5442                        "protocolVersion": "2025-11-25",
5443                        "capabilities": {},
5444                        "clientInfo": { "name": "t", "version": "0" }
5445                    }
5446                })
5447                .to_string(),
5448            ))
5449            .unwrap();
5450        let init_response = app.clone().oneshot(init).await.unwrap();
5451        assert_eq!(init_response.status(), StatusCode::OK);
5452        let session_id = init_response
5453            .headers()
5454            .get(MCP_SESSION_ID_HEADER)
5455            .unwrap()
5456            .to_str()
5457            .unwrap()
5458            .to_string();
5459
5460        // tools/list with no Mcp-Method header — must succeed in lenient mode.
5461        let req = Request::builder()
5462            .method("POST")
5463            .uri("/")
5464            .header("Content-Type", "application/json")
5465            .header("Accept", "application/json, text/event-stream")
5466            .header(MCP_SESSION_ID_HEADER, &session_id)
5467            .body(Body::from(
5468                serde_json::json!({
5469                    "jsonrpc": "2.0",
5470                    "id": 2,
5471                    "method": "tools/list"
5472                })
5473                .to_string(),
5474            ))
5475            .unwrap();
5476        let response = app.oneshot(req).await.unwrap();
5477        assert_eq!(response.status(), StatusCode::OK);
5478    }
5479
5480    /// In lenient mode, if the client opts in by sending Mcp-Method,
5481    /// the server still validates against the body and rejects a
5482    /// mismatch with -32020.
5483    #[tokio::test]
5484    async fn sep_2243_lenient_mode_validates_present_headers() {
5485        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
5486        let app = transport.into_router();
5487
5488        let init = Request::builder()
5489            .method("POST")
5490            .uri("/")
5491            .header("Content-Type", "application/json")
5492            .header("Accept", "application/json, text/event-stream")
5493            .header(MCP_METHOD_HEADER, "initialize")
5494            .body(Body::from(
5495                serde_json::json!({
5496                    "jsonrpc": "2.0",
5497                    "id": 1,
5498                    "method": "initialize",
5499                    "params": {
5500                        "protocolVersion": "2025-11-25",
5501                        "capabilities": {},
5502                        "clientInfo": { "name": "t", "version": "0" }
5503                    }
5504                })
5505                .to_string(),
5506            ))
5507            .unwrap();
5508        let init_response = app.clone().oneshot(init).await.unwrap();
5509        assert_eq!(init_response.status(), StatusCode::OK);
5510        let session_id = init_response
5511            .headers()
5512            .get(MCP_SESSION_ID_HEADER)
5513            .unwrap()
5514            .to_str()
5515            .unwrap()
5516            .to_string();
5517
5518        // tools/list with a deliberately-wrong Mcp-Method header.
5519        let req = Request::builder()
5520            .method("POST")
5521            .uri("/")
5522            .header("Content-Type", "application/json")
5523            .header("Accept", "application/json")
5524            .header(MCP_SESSION_ID_HEADER, &session_id)
5525            .header(MCP_METHOD_HEADER, "ping")
5526            .body(Body::from(
5527                serde_json::json!({
5528                    "jsonrpc": "2.0",
5529                    "id": 2,
5530                    "method": "tools/list"
5531                })
5532                .to_string(),
5533            ))
5534            .unwrap();
5535        let response = app.oneshot(req).await.unwrap();
5536        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
5537        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
5538            .await
5539            .unwrap();
5540        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
5541        assert_eq!(json["error"]["code"].as_i64().unwrap(), -32020);
5542        assert!(
5543            json["error"]["message"]
5544                .as_str()
5545                .unwrap()
5546                .contains("Mcp-Method")
5547        );
5548        assert_eq!(json["id"], 2);
5549    }
5550
5551    /// tools/call with matching Mcp-Method + Mcp-Name passes validation
5552    /// even in strict mode. Driven via initialize with the upcoming
5553    /// 2026-07-28 protocol version so we exercise the strict branch.
5554    ///
5555    /// Gated to `not(stateless)` because with the stateless feature enabled,
5556    /// initialize requests for 2026-07-28 are handled without a session (chunk 5).
5557    /// The stateless-mode equivalent is `stateless_v2026_tools_call_without_session_succeeds`.
5558    #[tokio::test]
5559    #[cfg(not(feature = "stateless"))]
5560    async fn sep_2243_strict_mode_tools_call_with_matching_headers() {
5561        use crate::{CallToolResult, ToolBuilder};
5562
5563        let router = McpRouter::new().server_info("t", "1.0.0").tool(
5564            ToolBuilder::new("echo")
5565                .description("echo")
5566                .handler(|args: serde_json::Value| async move {
5567                    Ok(CallToolResult::text(args.to_string()))
5568                })
5569                .build(),
5570        );
5571        let transport = HttpTransport::new(router).disable_origin_validation();
5572        let app = transport.into_router();
5573
5574        // Initialize requesting 2026-07-28 so the session falls into
5575        // strict mode. The server will negotiate the actual returned
5576        // version against SUPPORTED_PROTOCOL_VERSIONS, but for SEP-2243
5577        // gating on init the requested version is what counts.
5578        let init = Request::builder()
5579            .method("POST")
5580            .uri("/")
5581            .header("Content-Type", "application/json")
5582            .header("Accept", "application/json, text/event-stream")
5583            .header(MCP_METHOD_HEADER, "initialize")
5584            .body(Body::from(
5585                serde_json::json!({
5586                    "jsonrpc": "2.0",
5587                    "id": 1,
5588                    "method": "initialize",
5589                    "params": {
5590                        "protocolVersion": "2026-07-28",
5591                        "capabilities": {},
5592                        "clientInfo": { "name": "t", "version": "0" }
5593                    }
5594                })
5595                .to_string(),
5596            ))
5597            .unwrap();
5598        let init_response = app.clone().oneshot(init).await.unwrap();
5599        assert_eq!(init_response.status(), StatusCode::OK);
5600        let session_id = init_response
5601            .headers()
5602            .get(MCP_SESSION_ID_HEADER)
5603            .unwrap()
5604            .to_str()
5605            .unwrap()
5606            .to_string();
5607        let negotiated_version = init_response
5608            .headers()
5609            .get(MCP_PROTOCOL_VERSION_HEADER)
5610            .unwrap()
5611            .to_str()
5612            .unwrap()
5613            .to_string();
5614
5615        // For subsequent requests, the session's negotiated protocol
5616        // version is what the validator gates on. If the server did
5617        // NOT honor 2026-07-28 (because it isn't in SUPPORTED yet) the
5618        // session will be lenient — which is fine, we just want to
5619        // confirm the happy path works. If it IS honored, the strict
5620        // branch is exercised.
5621        let req = Request::builder()
5622            .method("POST")
5623            .uri("/")
5624            .header("Content-Type", "application/json")
5625            .header("Accept", "application/json")
5626            .header(MCP_SESSION_ID_HEADER, &session_id)
5627            .header(MCP_PROTOCOL_VERSION_HEADER, &negotiated_version)
5628            .header(MCP_METHOD_HEADER, "tools/call")
5629            .header(MCP_NAME_HEADER, "echo")
5630            .body(Body::from(
5631                serde_json::json!({
5632                    "jsonrpc": "2.0",
5633                    "id": 2,
5634                    "method": "tools/call",
5635                    "params": {
5636                        "name": "echo",
5637                        "arguments": {"message": "hi"}
5638                    }
5639                })
5640                .to_string(),
5641            ))
5642            .unwrap();
5643        let response = app.oneshot(req).await.unwrap();
5644        assert_eq!(response.status(), StatusCode::OK);
5645    }
5646
5647    /// tools/call with mismatched Mcp-Name vs body params.name MUST be
5648    /// rejected with -32020 and HTTP 400 even in lenient mode.
5649    #[tokio::test]
5650    async fn sep_2243_tools_call_mcp_name_mismatch_rejected() {
5651        use crate::{CallToolResult, ToolBuilder};
5652
5653        let router = McpRouter::new().server_info("t", "1.0.0").tool(
5654            ToolBuilder::new("echo")
5655                .description("echo")
5656                .handler(|args: serde_json::Value| async move {
5657                    Ok(CallToolResult::text(args.to_string()))
5658                })
5659                .build(),
5660        );
5661        let transport = HttpTransport::new(router).disable_origin_validation();
5662        let app = transport.into_router();
5663
5664        let init = Request::builder()
5665            .method("POST")
5666            .uri("/")
5667            .header("Content-Type", "application/json")
5668            .header("Accept", "application/json, text/event-stream")
5669            .body(Body::from(
5670                serde_json::json!({
5671                    "jsonrpc": "2.0",
5672                    "id": 1,
5673                    "method": "initialize",
5674                    "params": {
5675                        "protocolVersion": "2025-11-25",
5676                        "capabilities": {},
5677                        "clientInfo": { "name": "t", "version": "0" }
5678                    }
5679                })
5680                .to_string(),
5681            ))
5682            .unwrap();
5683        let init_response = app.clone().oneshot(init).await.unwrap();
5684        let session_id = init_response
5685            .headers()
5686            .get(MCP_SESSION_ID_HEADER)
5687            .unwrap()
5688            .to_str()
5689            .unwrap()
5690            .to_string();
5691
5692        let req = Request::builder()
5693            .method("POST")
5694            .uri("/")
5695            .header("Content-Type", "application/json")
5696            .header("Accept", "application/json")
5697            .header(MCP_SESSION_ID_HEADER, &session_id)
5698            .header(MCP_METHOD_HEADER, "tools/call")
5699            .header(MCP_NAME_HEADER, "not-echo")
5700            .body(Body::from(
5701                serde_json::json!({
5702                    "jsonrpc": "2.0",
5703                    "id": 7,
5704                    "method": "tools/call",
5705                    "params": {
5706                        "name": "echo",
5707                        "arguments": {"message": "hi"}
5708                    }
5709                })
5710                .to_string(),
5711            ))
5712            .unwrap();
5713        let response = app.oneshot(req).await.unwrap();
5714        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
5715        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
5716            .await
5717            .unwrap();
5718        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
5719        assert_eq!(json["error"]["code"].as_i64().unwrap(), -32020);
5720        let msg = json["error"]["message"].as_str().unwrap();
5721        assert!(msg.contains("Mcp-Name"), "got: {msg}");
5722        assert_eq!(json["id"], 7);
5723    }
5724
5725    /// Mcp-Param-* with a Base64-encoded value that decodes to the
5726    /// body argument must pass validation.
5727    #[tokio::test]
5728    async fn sep_2243_mcp_param_base64_decoded_and_matched() {
5729        use crate::{CallToolResult, ToolBuilder};
5730
5731        let router = McpRouter::new().server_info("t", "1.0.0").tool(
5732            ToolBuilder::new("echo")
5733                .description("echo")
5734                .handler(|args: serde_json::Value| async move {
5735                    Ok(CallToolResult::text(args.to_string()))
5736                })
5737                .build(),
5738        );
5739        let transport = HttpTransport::new(router).disable_origin_validation();
5740        let app = transport.into_router();
5741
5742        let init = Request::builder()
5743            .method("POST")
5744            .uri("/")
5745            .header("Content-Type", "application/json")
5746            .header("Accept", "application/json, text/event-stream")
5747            .body(Body::from(
5748                serde_json::json!({
5749                    "jsonrpc": "2.0",
5750                    "id": 1,
5751                    "method": "initialize",
5752                    "params": {
5753                        "protocolVersion": "2025-11-25",
5754                        "capabilities": {},
5755                        "clientInfo": { "name": "t", "version": "0" }
5756                    }
5757                })
5758                .to_string(),
5759            ))
5760            .unwrap();
5761        let init_response = app.clone().oneshot(init).await.unwrap();
5762        let session_id = init_response
5763            .headers()
5764            .get(MCP_SESSION_ID_HEADER)
5765            .unwrap()
5766            .to_str()
5767            .unwrap()
5768            .to_string();
5769
5770        // Body argument is "Hello"; header is "=?base64?SGVsbG8=?=".
5771        let req = Request::builder()
5772            .method("POST")
5773            .uri("/")
5774            .header("Content-Type", "application/json")
5775            .header("Accept", "application/json")
5776            .header(MCP_SESSION_ID_HEADER, &session_id)
5777            .header(MCP_METHOD_HEADER, "tools/call")
5778            .header(MCP_NAME_HEADER, "echo")
5779            .header("mcp-param-message", "=?base64?SGVsbG8=?=")
5780            .body(Body::from(
5781                serde_json::json!({
5782                    "jsonrpc": "2.0",
5783                    "id": 5,
5784                    "method": "tools/call",
5785                    "params": {
5786                        "name": "echo",
5787                        "arguments": {"message": "Hello"}
5788                    }
5789                })
5790                .to_string(),
5791            ))
5792            .unwrap();
5793        let response = app.oneshot(req).await.unwrap();
5794        assert_eq!(response.status(), StatusCode::OK);
5795    }
5796
5797    #[cfg(feature = "stateless")]
5798    #[tokio::test]
5799    async fn sep_2243_final_request_requires_schema_annotated_header() {
5800        use crate::extract::RawArgs;
5801        use crate::{CallToolResult, ToolBuilder};
5802
5803        let tool = ToolBuilder::new("route")
5804            .input_schema(serde_json::json!({
5805                "type": "object",
5806                "properties": {
5807                    "tenant_id": {
5808                        "type": "string",
5809                        "x-mcp-header": "Tenant"
5810                    }
5811                }
5812            }))
5813            .extractor_handler((), |RawArgs(args): RawArgs| async move {
5814                Ok(CallToolResult::text(args["tenant_id"].to_string()))
5815            })
5816            .build();
5817        let app = HttpTransport::new(
5818            McpRouter::new()
5819                .server_info("header-test", "1.0.0")
5820                .tool(tool),
5821        )
5822        .disable_origin_validation()
5823        .into_router();
5824
5825        let request = |custom_header: Option<&'static str>| {
5826            let mut builder = Request::builder()
5827                .method("POST")
5828                .uri("/")
5829                .header("Content-Type", "application/json")
5830                .header(MCP_PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION_2026_07_28)
5831                .header(MCP_METHOD_HEADER, "tools/call")
5832                .header(MCP_NAME_HEADER, "route");
5833            if let Some(value) = custom_header {
5834                builder = builder.header("Mcp-Param-Tenant", value);
5835            }
5836            builder
5837                .body(Body::from(
5838                    serde_json::json!({
5839                        "jsonrpc": "2.0",
5840                        "id": 9,
5841                        "method": "tools/call",
5842                        "params": {
5843                            "name": "route",
5844                            "arguments": {"tenant_id": "acme"},
5845                            "_meta": {
5846                                "io.modelcontextprotocol/protocolVersion":
5847                                    PROTOCOL_VERSION_2026_07_28,
5848                                "io.modelcontextprotocol/clientCapabilities": {}
5849                            }
5850                        }
5851                    })
5852                    .to_string(),
5853                ))
5854                .unwrap()
5855        };
5856
5857        let response = app.clone().oneshot(request(None)).await.unwrap();
5858        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
5859        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
5860            .await
5861            .unwrap();
5862        let error: serde_json::Value = serde_json::from_slice(&body).unwrap();
5863        assert_eq!(error["id"], 9);
5864        assert_eq!(error["error"]["code"], -32020);
5865
5866        let response = app.oneshot(request(Some("acme"))).await.unwrap();
5867        assert_eq!(response.status(), StatusCode::OK);
5868    }
5869
5870    /// notifications/initialized still receives ACCEPTED when SEP-2243
5871    /// headers match (regression for the notification fast path).
5872    #[tokio::test]
5873    async fn sep_2243_notification_with_matching_method_header_accepted() {
5874        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
5875        let app = transport.into_router();
5876
5877        let init = Request::builder()
5878            .method("POST")
5879            .uri("/")
5880            .header("Content-Type", "application/json")
5881            .header("Accept", "application/json, text/event-stream")
5882            .body(Body::from(
5883                serde_json::json!({
5884                    "jsonrpc": "2.0",
5885                    "id": 1,
5886                    "method": "initialize",
5887                    "params": {
5888                        "protocolVersion": "2025-11-25",
5889                        "capabilities": {},
5890                        "clientInfo": { "name": "t", "version": "0" }
5891                    }
5892                })
5893                .to_string(),
5894            ))
5895            .unwrap();
5896        let init_response = app.clone().oneshot(init).await.unwrap();
5897        let session_id = init_response
5898            .headers()
5899            .get(MCP_SESSION_ID_HEADER)
5900            .unwrap()
5901            .to_str()
5902            .unwrap()
5903            .to_string();
5904
5905        let req = Request::builder()
5906            .method("POST")
5907            .uri("/")
5908            .header("Content-Type", "application/json")
5909            .header("Accept", "application/json, text/event-stream")
5910            .header(MCP_SESSION_ID_HEADER, &session_id)
5911            .header(MCP_METHOD_HEADER, "notifications/initialized")
5912            .body(Body::from(
5913                serde_json::json!({
5914                    "jsonrpc": "2.0",
5915                    "method": "notifications/initialized"
5916                })
5917                .to_string(),
5918            ))
5919            .unwrap();
5920        let response = app.oneshot(req).await.unwrap();
5921        assert_eq!(response.status(), StatusCode::ACCEPTED);
5922    }
5923
5924    #[tokio::test]
5925    async fn test_request_without_session_fails() {
5926        let transport = HttpTransport::new(create_test_router())
5927            .disable_origin_validation()
5928            .require_sessions();
5929        let app = transport.into_router();
5930
5931        let request = Request::builder()
5932            .method("POST")
5933            .uri("/")
5934            .header("Content-Type", "application/json")
5935            .body(Body::from(
5936                serde_json::json!({
5937                    "jsonrpc": "2.0",
5938                    "id": 1,
5939                    "method": "tools/list"
5940                })
5941                .to_string(),
5942            ))
5943            .unwrap();
5944
5945        let response = app.oneshot(request).await.unwrap();
5946
5947        // We now return JSON-RPC errors for session issues
5948        assert_eq!(response.status(), StatusCode::OK);
5949
5950        // Verify it's a JSON-RPC error response
5951        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
5952            .await
5953            .unwrap();
5954        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
5955        assert!(json.get("error").is_some());
5956        assert_eq!(json["error"]["code"], -32006); // SessionRequired
5957    }
5958
5959    #[tokio::test]
5960    async fn test_delete_session() {
5961        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
5962        let app = transport.into_router();
5963
5964        // First, initialize to get a session
5965        let init_request = Request::builder()
5966            .method("POST")
5967            .uri("/")
5968            .header("Content-Type", "application/json")
5969            .header("Accept", "application/json, text/event-stream")
5970            .body(Body::from(
5971                serde_json::json!({
5972                    "jsonrpc": "2.0",
5973                    "id": 1,
5974                    "method": "initialize",
5975                    "params": {
5976                        "protocolVersion": "2025-11-25",
5977                        "capabilities": {},
5978                        "clientInfo": {
5979                            "name": "test-client",
5980                            "version": "1.0.0"
5981                        }
5982                    }
5983                })
5984                .to_string(),
5985            ))
5986            .unwrap();
5987
5988        let response = app.clone().oneshot(init_request).await.unwrap();
5989        let session_id = response
5990            .headers()
5991            .get(MCP_SESSION_ID_HEADER)
5992            .unwrap()
5993            .to_str()
5994            .unwrap()
5995            .to_string();
5996
5997        // Delete the session
5998        let delete_request = Request::builder()
5999            .method("DELETE")
6000            .uri("/")
6001            .header(MCP_SESSION_ID_HEADER, &session_id)
6002            .body(Body::empty())
6003            .unwrap();
6004
6005        let response = app.clone().oneshot(delete_request).await.unwrap();
6006        assert_eq!(response.status(), StatusCode::OK);
6007
6008        // Verify session is gone
6009        let list_request = Request::builder()
6010            .method("POST")
6011            .uri("/")
6012            .header("Content-Type", "application/json")
6013            .header(MCP_SESSION_ID_HEADER, &session_id)
6014            .body(Body::from(
6015                serde_json::json!({
6016                    "jsonrpc": "2.0",
6017                    "id": 2,
6018                    "method": "tools/list"
6019                })
6020                .to_string(),
6021            ))
6022            .unwrap();
6023
6024        let response = app.oneshot(list_request).await.unwrap();
6025        // We now return JSON-RPC errors for session issues
6026        assert_eq!(response.status(), StatusCode::OK);
6027
6028        // Verify it's a JSON-RPC error response
6029        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
6030            .await
6031            .unwrap();
6032        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
6033        assert!(json.get("error").is_some());
6034        assert_eq!(json["error"]["code"], -32005); // SessionNotFound
6035    }
6036
6037    #[tokio::test]
6038    async fn test_custom_session_store_receives_create_and_delete() {
6039        use crate::session_store::{MemorySessionStore, SessionStore as PublicSessionStore};
6040
6041        let store = Arc::new(MemorySessionStore::new());
6042        let store_dyn: Arc<dyn PublicSessionStore> = store.clone();
6043
6044        let transport = HttpTransport::new(create_test_router())
6045            .disable_origin_validation()
6046            .session_store(store_dyn);
6047        let (app, handle) = transport.into_router_with_handle();
6048
6049        // Initialize to create a session.
6050        let init_request = Request::builder()
6051            .method("POST")
6052            .uri("/")
6053            .header("Content-Type", "application/json")
6054            .header("Accept", "application/json, text/event-stream")
6055            .body(Body::from(
6056                serde_json::json!({
6057                    "jsonrpc": "2.0",
6058                    "id": 1,
6059                    "method": "initialize",
6060                    "params": {
6061                        "protocolVersion": "2025-11-25",
6062                        "capabilities": {},
6063                        "clientInfo": { "name": "test-client", "version": "1.0.0" }
6064                    }
6065                })
6066                .to_string(),
6067            ))
6068            .unwrap();
6069
6070        let response = app.clone().oneshot(init_request).await.unwrap();
6071        assert_eq!(response.status(), StatusCode::OK);
6072        let session_id = response
6073            .headers()
6074            .get(MCP_SESSION_ID_HEADER)
6075            .unwrap()
6076            .to_str()
6077            .unwrap()
6078            .to_string();
6079
6080        // Custom store should have the record.
6081        assert_eq!(store.len().await, 1);
6082        let record = store
6083            .load(&session_id)
6084            .await
6085            .unwrap()
6086            .expect("expected session to be persisted");
6087        assert_eq!(record.id, session_id);
6088
6089        // After initialize completes the record must carry the client's
6090        // advertised identity / capabilities (issue #786). Previously these
6091        // were left as `None` because the record was created before
6092        // initialize ran.
6093        let client_info = record
6094            .client_info
6095            .expect("client_info should be populated after initialize");
6096        assert_eq!(client_info.name, "test-client");
6097        assert_eq!(client_info.version, "1.0.0");
6098        assert!(
6099            record.client_capabilities.is_some(),
6100            "client_capabilities should be populated after initialize"
6101        );
6102
6103        // Terminate session via the handle -- store should be cleared.
6104        assert!(handle.terminate_session(&session_id).await);
6105        assert_eq!(store.len().await, 0);
6106        assert!(store.load(&session_id).await.unwrap().is_none());
6107    }
6108
6109    #[tokio::test]
6110    async fn test_session_store_record_carries_negotiated_protocol_version() {
6111        // Issue #786: stored record should reflect the negotiated protocol
6112        // version (taken from the initialize response), not the default the
6113        // session was created with.
6114        use crate::session_store::{MemorySessionStore, SessionStore as PublicSessionStore};
6115
6116        let store = Arc::new(MemorySessionStore::new());
6117        let store_dyn: Arc<dyn PublicSessionStore> = store.clone();
6118
6119        let transport = HttpTransport::new(create_test_router())
6120            .disable_origin_validation()
6121            .session_store(store_dyn);
6122        let app = transport.into_router();
6123
6124        // Initialize using an older supported protocol version so we can
6125        // tell the persisted version apart from `LATEST_PROTOCOL_VERSION`.
6126        let init_request = Request::builder()
6127            .method("POST")
6128            .uri("/")
6129            .header("Content-Type", "application/json")
6130            .header("Accept", "application/json, text/event-stream")
6131            .body(Body::from(
6132                serde_json::json!({
6133                    "jsonrpc": "2.0",
6134                    "id": 1,
6135                    "method": "initialize",
6136                    "params": {
6137                        "protocolVersion": "2025-03-26",
6138                        "capabilities": {},
6139                        "clientInfo": { "name": "v-client", "version": "2.0.0" }
6140                    }
6141                })
6142                .to_string(),
6143            ))
6144            .unwrap();
6145
6146        let response = app.oneshot(init_request).await.unwrap();
6147        assert_eq!(response.status(), StatusCode::OK);
6148        let session_id = response
6149            .headers()
6150            .get(MCP_SESSION_ID_HEADER)
6151            .unwrap()
6152            .to_str()
6153            .unwrap()
6154            .to_string();
6155
6156        let record = store
6157            .load(&session_id)
6158            .await
6159            .unwrap()
6160            .expect("session should be persisted");
6161        assert_eq!(record.protocol_version, "2025-03-26");
6162        let client_info = record.client_info.expect("client_info should be populated");
6163        assert_eq!(client_info.name, "v-client");
6164    }
6165
6166    #[tokio::test]
6167    async fn test_restored_session_exposes_original_client_info() {
6168        // Issue #786: a session restored from the persistent store on a
6169        // peer instance should retain the original client's identity and
6170        // capabilities, not the synthetic defaults used for auto-reinit.
6171        use crate::session_store::{MemorySessionStore, SessionStore as PublicSessionStore};
6172
6173        let store = Arc::new(MemorySessionStore::new());
6174        let store_dyn: Arc<dyn PublicSessionStore> = store.clone();
6175
6176        // First "instance": initialize, then drop the transport so the
6177        // local registry is gone but the persistent record survives.
6178        let session_id = {
6179            let transport = HttpTransport::new(create_test_router())
6180                .disable_origin_validation()
6181                .session_store(store_dyn.clone());
6182            let app = transport.into_router();
6183
6184            let init_request = Request::builder()
6185                .method("POST")
6186                .uri("/")
6187                .header("Content-Type", "application/json")
6188                .header("Accept", "application/json, text/event-stream")
6189                .body(Body::from(
6190                    serde_json::json!({
6191                        "jsonrpc": "2.0",
6192                        "id": 1,
6193                        "method": "initialize",
6194                        "params": {
6195                            "protocolVersion": "2025-11-25",
6196                            "capabilities": { "roots": {} },
6197                            "clientInfo": {
6198                                "name": "original-client",
6199                                "version": "3.1.4"
6200                            }
6201                        }
6202                    })
6203                    .to_string(),
6204                ))
6205                .unwrap();
6206            let response = app.oneshot(init_request).await.unwrap();
6207            assert_eq!(response.status(), StatusCode::OK);
6208            response
6209                .headers()
6210                .get(MCP_SESSION_ID_HEADER)
6211                .unwrap()
6212                .to_str()
6213                .unwrap()
6214                .to_string()
6215        };
6216
6217        // Sanity check: the persisted record now carries the client info.
6218        let stored = store
6219            .load(&session_id)
6220            .await
6221            .unwrap()
6222            .expect("record should survive transport drop");
6223        assert_eq!(
6224            stored.client_info.as_ref().map(|c| c.name.as_str()),
6225            Some("original-client")
6226        );
6227
6228        // Second "instance": brand new transport, same store. A request
6229        // with the existing session id triggers restore_from_record.
6230        let transport2 = HttpTransport::new(create_test_router())
6231            .disable_origin_validation()
6232            .session_store(store_dyn);
6233        let app2 = transport2.into_router();
6234
6235        let list_request = Request::builder()
6236            .method("POST")
6237            .uri("/")
6238            .header("Content-Type", "application/json")
6239            .header(MCP_SESSION_ID_HEADER, &session_id)
6240            .body(Body::from(
6241                serde_json::json!({
6242                    "jsonrpc": "2.0",
6243                    "id": 1,
6244                    "method": "tools/list"
6245                })
6246                .to_string(),
6247            ))
6248            .unwrap();
6249        let response = app2.oneshot(list_request).await.unwrap();
6250        assert_eq!(response.status(), StatusCode::OK);
6251        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
6252            .await
6253            .unwrap();
6254        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
6255        assert!(
6256            json.get("result").is_some(),
6257            "expected tools/list result, got {json}"
6258        );
6259
6260        // After the restore, the record in the store still carries the
6261        // original client info (refreshed expiry, not a synthetic
6262        // "auto-recovered" identity).
6263        let after_restore = store
6264            .load(&session_id)
6265            .await
6266            .unwrap()
6267            .expect("record should still be present after restore");
6268        let client_info = after_restore
6269            .client_info
6270            .expect("restored record should retain client_info");
6271        assert_eq!(client_info.name, "original-client");
6272        assert_eq!(client_info.version, "3.1.4");
6273        assert!(
6274            after_restore.client_capabilities.is_some(),
6275            "restored record should retain client_capabilities"
6276        );
6277    }
6278
6279    #[tokio::test]
6280    async fn test_auto_reinitialize_marks_synthetic_client_info() {
6281        // Companion to the restored-client-info test: the auto-reinit
6282        // path must continue to flag the client as `"auto-recovered"` so
6283        // the two paths remain distinguishable on inspection of the
6284        // persisted record.
6285        use crate::session_store::{MemorySessionStore, SessionStore as PublicSessionStore};
6286
6287        let store = Arc::new(MemorySessionStore::new());
6288        let store_dyn: Arc<dyn PublicSessionStore> = store.clone();
6289
6290        let transport = HttpTransport::new(create_test_router())
6291            .disable_origin_validation()
6292            .session_store(store_dyn)
6293            .auto_reinitialize_sessions(true);
6294        let app = transport.into_router();
6295
6296        let list_request = Request::builder()
6297            .method("POST")
6298            .uri("/")
6299            .header("Content-Type", "application/json")
6300            .header(MCP_SESSION_ID_HEADER, "made-up-id")
6301            .body(Body::from(
6302                serde_json::json!({
6303                    "jsonrpc": "2.0",
6304                    "id": 1,
6305                    "method": "tools/list"
6306                })
6307                .to_string(),
6308            ))
6309            .unwrap();
6310        let response = app.oneshot(list_request).await.unwrap();
6311        assert_eq!(response.status(), StatusCode::OK);
6312
6313        let record = store
6314            .load("made-up-id")
6315            .await
6316            .unwrap()
6317            .expect("auto-reinitialize should persist a record");
6318        assert_eq!(
6319            record.client_info.as_ref().map(|c| c.name.as_str()),
6320            Some("auto-recovered")
6321        );
6322    }
6323
6324    #[tokio::test]
6325    async fn test_custom_event_store_buffers_and_purges() {
6326        use crate::event_store::{EventStore as PublicEventStore, MemoryEventStore};
6327
6328        let events = Arc::new(MemoryEventStore::new());
6329        let events_dyn: Arc<dyn PublicEventStore> = events.clone();
6330
6331        // Build a session directly so we can exercise buffer_event/get_events_after
6332        // without needing a live SSE subscriber.
6333        let session = Arc::new(Session::new(
6334            create_test_router(),
6335            false,
6336            identity_factory(),
6337            events_dyn,
6338        ));
6339
6340        session.buffer_event(0, "first".to_string()).await;
6341        session.buffer_event(1, "second".to_string()).await;
6342
6343        // Custom store should have both events.
6344        assert_eq!(events.total_events().await, 2);
6345        let replayed = events.replay_after(&session.id, 0).await.unwrap();
6346        assert_eq!(replayed.len(), 1);
6347        assert_eq!(replayed[0].id, 1);
6348        assert_eq!(replayed[0].data, "second");
6349
6350        // Purging should clear the session's log.
6351        events.purge_session(&session.id).await.unwrap();
6352        assert_eq!(events.total_events().await, 0);
6353    }
6354
6355    #[tokio::test]
6356    async fn test_restore_from_store_serves_unknown_session_id() {
6357        use crate::session_store::{MemorySessionStore, SessionRecord, SessionStore};
6358
6359        // Two transports share a single session store (simulating two
6360        // server instances behind a load balancer).
6361        let store = Arc::new(MemorySessionStore::new());
6362        let store_dyn: Arc<dyn SessionStore> = store.clone();
6363
6364        // Seed the store with a record as if a peer instance had created it.
6365        let mut seeded = SessionRecord::new(
6366            "shared-session".to_string(),
6367            "2025-11-25".to_string(),
6368            Duration::from_secs(60),
6369        );
6370        store.create(&mut seeded).await.unwrap();
6371        let seeded_id = seeded.id;
6372
6373        // This transport has never seen the session locally.
6374        let transport = HttpTransport::new(create_test_router())
6375            .disable_origin_validation()
6376            .session_store(store_dyn);
6377        let app = transport.into_router();
6378
6379        let list_request = Request::builder()
6380            .method("POST")
6381            .uri("/")
6382            .header("Content-Type", "application/json")
6383            .header(MCP_SESSION_ID_HEADER, &seeded_id)
6384            .body(Body::from(
6385                serde_json::json!({
6386                    "jsonrpc": "2.0",
6387                    "id": 1,
6388                    "method": "tools/list"
6389                })
6390                .to_string(),
6391            ))
6392            .unwrap();
6393
6394        let response = app.oneshot(list_request).await.unwrap();
6395        // Without restore this would produce a SessionNotFound JSON-RPC
6396        // error; with restore the request is served normally.
6397        assert_eq!(response.status(), StatusCode::OK);
6398
6399        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
6400            .await
6401            .unwrap();
6402        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
6403        assert!(
6404            json.get("result").is_some(),
6405            "expected tools/list result, got {json}"
6406        );
6407    }
6408
6409    #[tokio::test]
6410    async fn test_auto_reinitialize_serves_unknown_session_without_store_record() {
6411        // No seeded store record — the client just shows up with a
6412        // session ID the server has never heard of. With auto-reinit
6413        // enabled the transport spins up a synthetic session.
6414        let transport = HttpTransport::new(create_test_router())
6415            .disable_origin_validation()
6416            .auto_reinitialize_sessions(true);
6417        let app = transport.into_router();
6418
6419        let list_request = Request::builder()
6420            .method("POST")
6421            .uri("/")
6422            .header("Content-Type", "application/json")
6423            .header(MCP_SESSION_ID_HEADER, "client-made-up-id")
6424            .body(Body::from(
6425                serde_json::json!({
6426                    "jsonrpc": "2.0",
6427                    "id": 1,
6428                    "method": "tools/list"
6429                })
6430                .to_string(),
6431            ))
6432            .unwrap();
6433
6434        let response = app.oneshot(list_request).await.unwrap();
6435        assert_eq!(response.status(), StatusCode::OK);
6436        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
6437            .await
6438            .unwrap();
6439        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
6440        assert!(
6441            json.get("result").is_some(),
6442            "expected tools/list result, got {json}"
6443        );
6444    }
6445
6446    #[tokio::test]
6447    async fn test_unknown_session_without_restore_or_auto_reinit_returns_error() {
6448        // Default transport: no store seeded, no auto-reinit.
6449        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
6450        let app = transport.into_router();
6451
6452        let list_request = Request::builder()
6453            .method("POST")
6454            .uri("/")
6455            .header("Content-Type", "application/json")
6456            .header(MCP_SESSION_ID_HEADER, "never-seen-before")
6457            .body(Body::from(
6458                serde_json::json!({
6459                    "jsonrpc": "2.0",
6460                    "id": 1,
6461                    "method": "tools/list"
6462                })
6463                .to_string(),
6464            ))
6465            .unwrap();
6466
6467        let response = app.oneshot(list_request).await.unwrap();
6468        assert_eq!(response.status(), StatusCode::OK);
6469        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
6470            .await
6471            .unwrap();
6472        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
6473        assert!(json.get("error").is_some(), "expected error, got {json}");
6474        assert_eq!(json["error"]["code"], -32005); // SessionNotFound
6475    }
6476
6477    #[tokio::test]
6478    async fn test_session_expiration() {
6479        // Create transport with very short TTL
6480        let config = SessionConfig::with_ttl(Duration::from_millis(50))
6481            .cleanup_interval(Duration::from_millis(10));
6482        let transport = HttpTransport::new(create_test_router())
6483            .disable_origin_validation()
6484            .session_config(config);
6485        let app = transport.into_router();
6486
6487        // Initialize to get a session
6488        let init_request = Request::builder()
6489            .method("POST")
6490            .uri("/")
6491            .header("Content-Type", "application/json")
6492            .header("Accept", "application/json, text/event-stream")
6493            .body(Body::from(
6494                serde_json::json!({
6495                    "jsonrpc": "2.0",
6496                    "id": 1,
6497                    "method": "initialize",
6498                    "params": {
6499                        "protocolVersion": "2025-11-25",
6500                        "capabilities": {},
6501                        "clientInfo": {
6502                            "name": "test-client",
6503                            "version": "1.0.0"
6504                        }
6505                    }
6506                })
6507                .to_string(),
6508            ))
6509            .unwrap();
6510
6511        let response = app.clone().oneshot(init_request).await.unwrap();
6512        assert_eq!(response.status(), StatusCode::OK);
6513        let session_id = response
6514            .headers()
6515            .get(MCP_SESSION_ID_HEADER)
6516            .unwrap()
6517            .to_str()
6518            .unwrap()
6519            .to_string();
6520
6521        // Wait for session to expire and cleanup to run
6522        tokio::time::sleep(Duration::from_millis(100)).await;
6523
6524        // Session should be expired now
6525        let list_request = Request::builder()
6526            .method("POST")
6527            .uri("/")
6528            .header("Content-Type", "application/json")
6529            .header(MCP_SESSION_ID_HEADER, &session_id)
6530            .body(Body::from(
6531                serde_json::json!({
6532                    "jsonrpc": "2.0",
6533                    "id": 2,
6534                    "method": "tools/list"
6535                })
6536                .to_string(),
6537            ))
6538            .unwrap();
6539
6540        let response = app.oneshot(list_request).await.unwrap();
6541        // We now return JSON-RPC errors for session issues
6542        assert_eq!(response.status(), StatusCode::OK);
6543
6544        // Verify it's a JSON-RPC error response
6545        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
6546            .await
6547            .unwrap();
6548        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
6549        assert!(json.get("error").is_some());
6550        assert_eq!(json["error"]["code"], -32005); // SessionNotFound
6551    }
6552
6553    #[tokio::test]
6554    async fn test_layer_with_identity() {
6555        // Verify that .layer() compiles and produces a working transport
6556        // using a no-op layer (tower::layer::Identity)
6557        let transport = HttpTransport::new(create_test_router())
6558            .disable_origin_validation()
6559            .layer(tower::layer::util::Identity::new());
6560        let app = transport.into_router();
6561
6562        let request = Request::builder()
6563            .method("POST")
6564            .uri("/")
6565            .header("Content-Type", "application/json")
6566            .header("Accept", "application/json, text/event-stream")
6567            .body(Body::from(
6568                serde_json::json!({
6569                    "jsonrpc": "2.0",
6570                    "id": 1,
6571                    "method": "initialize",
6572                    "params": {
6573                        "protocolVersion": "2025-11-25",
6574                        "capabilities": {},
6575                        "clientInfo": {
6576                            "name": "test-client",
6577                            "version": "1.0.0"
6578                        }
6579                    }
6580                })
6581                .to_string(),
6582            ))
6583            .unwrap();
6584
6585        let response = app.oneshot(request).await.unwrap();
6586        assert_eq!(response.status(), StatusCode::OK);
6587        assert!(response.headers().contains_key(MCP_SESSION_ID_HEADER));
6588    }
6589
6590    #[tokio::test]
6591    async fn test_layer_with_timeout() {
6592        // Verify that .layer() works with TimeoutLayer
6593        use std::time::Duration;
6594        use tower::timeout::TimeoutLayer;
6595
6596        let transport = HttpTransport::new(create_test_router())
6597            .disable_origin_validation()
6598            .layer(TimeoutLayer::new(Duration::from_secs(30)));
6599        let app = transport.into_router();
6600
6601        let request = Request::builder()
6602            .method("POST")
6603            .uri("/")
6604            .header("Content-Type", "application/json")
6605            .header("Accept", "application/json, text/event-stream")
6606            .body(Body::from(
6607                serde_json::json!({
6608                    "jsonrpc": "2.0",
6609                    "id": 1,
6610                    "method": "initialize",
6611                    "params": {
6612                        "protocolVersion": "2025-11-25",
6613                        "capabilities": {},
6614                        "clientInfo": {
6615                            "name": "test-client",
6616                            "version": "1.0.0"
6617                        }
6618                    }
6619                })
6620                .to_string(),
6621            ))
6622            .unwrap();
6623
6624        let response = app.oneshot(request).await.unwrap();
6625        assert_eq!(response.status(), StatusCode::OK);
6626        assert!(response.headers().contains_key(MCP_SESSION_ID_HEADER));
6627    }
6628
6629    #[tokio::test]
6630    async fn test_layer_middleware_error_produces_jsonrpc_error() {
6631        // Use an extremely short timeout to force an error.
6632        // The CatchError wrapper should convert it to a JSON-RPC error response.
6633        use std::time::Duration;
6634        use tower::timeout::TimeoutLayer;
6635
6636        let slow_tool = crate::tool::ToolBuilder::new("slow")
6637            .description("A slow tool")
6638            .handler(|_: serde_json::Value| async move {
6639                tokio::time::sleep(Duration::from_secs(10)).await;
6640                Ok(crate::CallToolResult::text("done"))
6641            })
6642            .build();
6643
6644        let router = McpRouter::new()
6645            .server_info("test-server", "1.0.0")
6646            .tool(slow_tool);
6647
6648        // 1ms timeout will definitely expire before the tool completes
6649        let transport = HttpTransport::new(router)
6650            .disable_origin_validation()
6651            .layer(TimeoutLayer::new(Duration::from_millis(1)));
6652        let app = transport.into_router();
6653
6654        // Initialize first
6655        let init_request = Request::builder()
6656            .method("POST")
6657            .uri("/")
6658            .header("Content-Type", "application/json")
6659            .header("Accept", "application/json, text/event-stream")
6660            .body(Body::from(
6661                serde_json::json!({
6662                    "jsonrpc": "2.0",
6663                    "id": 1,
6664                    "method": "initialize",
6665                    "params": {
6666                        "protocolVersion": "2025-11-25",
6667                        "capabilities": {},
6668                        "clientInfo": {
6669                            "name": "test-client",
6670                            "version": "1.0.0"
6671                        }
6672                    }
6673                })
6674                .to_string(),
6675            ))
6676            .unwrap();
6677
6678        let response = app.clone().oneshot(init_request).await.unwrap();
6679        let session_id = response
6680            .headers()
6681            .get(MCP_SESSION_ID_HEADER)
6682            .unwrap()
6683            .to_str()
6684            .unwrap()
6685            .to_string();
6686
6687        // Call the slow tool -- should timeout and return a JSON-RPC error
6688        let tool_request = Request::builder()
6689            .method("POST")
6690            .uri("/")
6691            .header("Content-Type", "application/json")
6692            .header(MCP_SESSION_ID_HEADER, &session_id)
6693            .body(Body::from(
6694                serde_json::json!({
6695                    "jsonrpc": "2.0",
6696                    "id": 2,
6697                    "method": "tools/call",
6698                    "params": {
6699                        "name": "slow",
6700                        "arguments": {}
6701                    }
6702                })
6703                .to_string(),
6704            ))
6705            .unwrap();
6706
6707        let response = app.oneshot(tool_request).await.unwrap();
6708        // Should still return 200 with a JSON-RPC error body
6709        assert_eq!(response.status(), StatusCode::OK);
6710
6711        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
6712            .await
6713            .unwrap();
6714        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
6715        assert!(
6716            json.get("error").is_some(),
6717            "Expected JSON-RPC error response, got: {}",
6718            json
6719        );
6720    }
6721
6722    #[tokio::test]
6723    async fn test_max_sessions_limit() {
6724        // Create transport with max 1 session
6725        let config = SessionConfig::default().max_sessions(1);
6726        let transport = HttpTransport::new(create_test_router())
6727            .disable_origin_validation()
6728            .session_config(config);
6729        let app = transport.into_router();
6730
6731        // First initialize should succeed
6732        let init_request1 = Request::builder()
6733            .method("POST")
6734            .uri("/")
6735            .header("Content-Type", "application/json")
6736            .header("Accept", "application/json, text/event-stream")
6737            .body(Body::from(
6738                serde_json::json!({
6739                    "jsonrpc": "2.0",
6740                    "id": 1,
6741                    "method": "initialize",
6742                    "params": {
6743                        "protocolVersion": "2025-11-25",
6744                        "capabilities": {},
6745                        "clientInfo": {
6746                            "name": "test-client",
6747                            "version": "1.0.0"
6748                        }
6749                    }
6750                })
6751                .to_string(),
6752            ))
6753            .unwrap();
6754
6755        let response = app.clone().oneshot(init_request1).await.unwrap();
6756        assert_eq!(response.status(), StatusCode::OK);
6757
6758        // Second initialize should fail (max sessions reached)
6759        let init_request2 = Request::builder()
6760            .method("POST")
6761            .uri("/")
6762            .header("Content-Type", "application/json")
6763            .header("Accept", "application/json, text/event-stream")
6764            .body(Body::from(
6765                serde_json::json!({
6766                    "jsonrpc": "2.0",
6767                    "id": 2,
6768                    "method": "initialize",
6769                    "params": {
6770                        "protocolVersion": "2025-11-25",
6771                        "capabilities": {},
6772                        "clientInfo": {
6773                            "name": "test-client-2",
6774                            "version": "1.0.0"
6775                        }
6776                    }
6777                })
6778                .to_string(),
6779            ))
6780            .unwrap();
6781
6782        let response = app.oneshot(init_request2).await.unwrap();
6783        assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
6784    }
6785
6786    #[tokio::test]
6787    async fn test_session_event_buffering() {
6788        // Test that events are buffered and can be retrieved for replay (SEP-1699)
6789        let session = Session::new(
6790            create_test_router(),
6791            false,
6792            identity_factory(),
6793            Arc::new(crate::event_store::MemoryEventStore::new()),
6794        );
6795
6796        // Buffer some events
6797        session.buffer_event(0, "event0".to_string()).await;
6798        session.buffer_event(1, "event1".to_string()).await;
6799        session.buffer_event(2, "event2".to_string()).await;
6800
6801        // Get events after event 0
6802        let events = session.get_events_after(0).await;
6803        assert_eq!(events.len(), 2);
6804        assert_eq!(events[0].id, 1);
6805        assert_eq!(events[0].data, "event1");
6806        assert_eq!(events[1].id, 2);
6807        assert_eq!(events[1].data, "event2");
6808
6809        // Get events after event 1
6810        let events = session.get_events_after(1).await;
6811        assert_eq!(events.len(), 1);
6812        assert_eq!(events[0].id, 2);
6813
6814        // Get events after event 2 (none)
6815        let events = session.get_events_after(2).await;
6816        assert!(events.is_empty());
6817    }
6818
6819    #[tokio::test]
6820    async fn test_session_event_counter_increments() {
6821        // Test that event IDs increment monotonically (SEP-1699)
6822        let session = Session::new(
6823            create_test_router(),
6824            false,
6825            identity_factory(),
6826            Arc::new(crate::event_store::MemoryEventStore::new()),
6827        );
6828
6829        assert_eq!(session.next_event_id(), 0);
6830        assert_eq!(session.next_event_id(), 1);
6831        assert_eq!(session.next_event_id(), 2);
6832    }
6833
6834    #[tokio::test]
6835    async fn test_session_event_buffer_limit() {
6836        // Test that buffer respects max size limit
6837        // Create a session - buffer limit is DEFAULT_MAX_BUFFERED_EVENTS (1000)
6838        let session = Session::new(
6839            create_test_router(),
6840            false,
6841            identity_factory(),
6842            Arc::new(crate::event_store::MemoryEventStore::new()),
6843        );
6844
6845        // Buffer more events than we can test practically, but verify the mechanism works
6846        // by checking that old events are evicted when we exceed the limit
6847        for i in 0..10 {
6848            session.buffer_event(i, format!("event{}", i)).await;
6849        }
6850
6851        // All 10 events should be present
6852        let events = session.get_events_after(0).await;
6853        // Events after 0 should be 1-9 (9 events)
6854        assert_eq!(events.len(), 9);
6855    }
6856
6857    #[tokio::test]
6858    async fn test_session_handle_count() {
6859        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
6860        let (app, handle) = transport.into_router_with_handle();
6861
6862        // No sessions initially
6863        assert_eq!(handle.session_count().await, 0);
6864
6865        // Initialize to create a session
6866        let request = Request::builder()
6867            .method("POST")
6868            .uri("/")
6869            .header("Content-Type", "application/json")
6870            .header("Accept", "application/json, text/event-stream")
6871            .body(Body::from(
6872                serde_json::json!({
6873                    "jsonrpc": "2.0",
6874                    "id": 1,
6875                    "method": "initialize",
6876                    "params": {
6877                        "protocolVersion": "2025-11-25",
6878                        "capabilities": {},
6879                        "clientInfo": {
6880                            "name": "test-client",
6881                            "version": "1.0.0"
6882                        }
6883                    }
6884                })
6885                .to_string(),
6886            ))
6887            .unwrap();
6888
6889        let response = app.oneshot(request).await.unwrap();
6890        assert_eq!(response.status(), 200);
6891
6892        // Now we should have 1 session
6893        assert_eq!(handle.session_count().await, 1);
6894    }
6895
6896    #[tokio::test]
6897    async fn test_session_handle_list_and_terminate() {
6898        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
6899        let (app, handle) = transport.into_router_with_handle();
6900
6901        // No sessions initially
6902        assert!(handle.list_sessions().await.is_empty());
6903
6904        // Initialize to create a session
6905        let request = Request::builder()
6906            .method("POST")
6907            .uri("/")
6908            .header("Content-Type", "application/json")
6909            .header("Accept", "application/json, text/event-stream")
6910            .body(Body::from(
6911                serde_json::json!({
6912                    "jsonrpc": "2.0",
6913                    "id": 1,
6914                    "method": "initialize",
6915                    "params": {
6916                        "protocolVersion": "2025-11-25",
6917                        "capabilities": {},
6918                        "clientInfo": {
6919                            "name": "test-client",
6920                            "version": "1.0.0"
6921                        }
6922                    }
6923                })
6924                .to_string(),
6925            ))
6926            .unwrap();
6927
6928        let response = app.oneshot(request).await.unwrap();
6929        assert_eq!(response.status(), 200);
6930
6931        // list_sessions should return 1 session with valid metadata
6932        let sessions = handle.list_sessions().await;
6933        assert_eq!(sessions.len(), 1);
6934        assert!(!sessions[0].id.is_empty());
6935
6936        // Terminate the session
6937        let session_id = sessions[0].id.clone();
6938        assert!(handle.terminate_session(&session_id).await);
6939        assert_eq!(handle.session_count().await, 0);
6940
6941        // Terminating again returns false
6942        assert!(!handle.terminate_session(&session_id).await);
6943    }
6944
6945    #[tokio::test]
6946    async fn test_request_without_session_id_rejected() {
6947        let transport = HttpTransport::new(create_test_router())
6948            .disable_origin_validation()
6949            .require_sessions();
6950        let app = transport.into_router();
6951
6952        let request = Request::builder()
6953            .method("POST")
6954            .uri("/")
6955            .header("Content-Type", "application/json")
6956            .header("Accept", "application/json")
6957            // No mcp-session-id header
6958            .body(Body::from(
6959                serde_json::json!({
6960                    "jsonrpc": "2.0",
6961                    "id": 1,
6962                    "method": "tools/list",
6963                    "params": {}
6964                })
6965                .to_string(),
6966            ))
6967            .unwrap();
6968
6969        let response = app.oneshot(request).await.unwrap();
6970        assert_eq!(response.status(), StatusCode::OK); // JSON-RPC errors still 200
6971        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
6972            .await
6973            .unwrap();
6974        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
6975        // Should return session required error
6976        assert!(json["error"].is_object());
6977    }
6978
6979    #[tokio::test]
6980    async fn test_invalid_session_id_returns_error() {
6981        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
6982        let app = transport.into_router();
6983
6984        let request = Request::builder()
6985            .method("POST")
6986            .uri("/")
6987            .header("Content-Type", "application/json")
6988            .header("Accept", "application/json")
6989            .header("mcp-session-id", "nonexistent-session-id")
6990            .body(Body::from(
6991                serde_json::json!({
6992                    "jsonrpc": "2.0",
6993                    "id": 1,
6994                    "method": "tools/list",
6995                    "params": {}
6996                })
6997                .to_string(),
6998            ))
6999            .unwrap();
7000
7001        let response = app.oneshot(request).await.unwrap();
7002        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
7003            .await
7004            .unwrap();
7005        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
7006        assert_eq!(json["error"]["code"].as_i64().unwrap(), -32005); // SessionNotFound
7007    }
7008
7009    #[tokio::test]
7010    async fn test_notification_returns_accepted() {
7011        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
7012        let app = transport.into_router();
7013
7014        // First initialize to get a session
7015        let init_req = Request::builder()
7016            .method("POST")
7017            .uri("/")
7018            .header("Content-Type", "application/json")
7019            .header("Accept", "application/json, text/event-stream")
7020            .body(Body::from(
7021                serde_json::json!({
7022                    "jsonrpc": "2.0",
7023                    "id": 1,
7024                    "method": "initialize",
7025                    "params": {
7026                        "protocolVersion": "2025-11-25",
7027                        "capabilities": {},
7028                        "clientInfo": { "name": "test", "version": "1.0" }
7029                    }
7030                })
7031                .to_string(),
7032            ))
7033            .unwrap();
7034
7035        let resp = app.clone().oneshot(init_req).await.unwrap();
7036        let session_id = resp
7037            .headers()
7038            .get(MCP_SESSION_ID_HEADER)
7039            .unwrap()
7040            .to_str()
7041            .unwrap()
7042            .to_string();
7043
7044        // Send a notification (no id field) -- should return 202 Accepted
7045        let notif = Request::builder()
7046            .method("POST")
7047            .uri("/")
7048            .header("Content-Type", "application/json")
7049            .header("mcp-session-id", &session_id)
7050            .body(Body::from(
7051                serde_json::json!({
7052                    "jsonrpc": "2.0",
7053                    "method": "notifications/initialized"
7054                })
7055                .to_string(),
7056            ))
7057            .unwrap();
7058
7059        let response = app.oneshot(notif).await.unwrap();
7060        assert_eq!(response.status(), StatusCode::ACCEPTED);
7061    }
7062
7063    #[tokio::test]
7064    async fn test_invalid_json_returns_parse_error() {
7065        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
7066        let app = transport.into_router();
7067
7068        let request = Request::builder()
7069            .method("POST")
7070            .uri("/")
7071            .header("Content-Type", "application/json")
7072            .header("Accept", "application/json")
7073            .body(Body::from("not valid json{{{"))
7074            .unwrap();
7075
7076        let response = app.oneshot(request).await.unwrap();
7077        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
7078            .await
7079            .unwrap();
7080        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
7081        tower_mcp_types::testing::assert_jsonrpc_error_response(&json);
7082        assert!(
7083            json["id"].is_null(),
7084            "id must be null on parse error: {json}"
7085        );
7086        assert_eq!(json["error"]["code"].as_i64().unwrap(), -32700);
7087    }
7088
7089    #[tokio::test]
7090    async fn test_session_config_max_sessions() {
7091        let transport = HttpTransport::new(create_test_router())
7092            .disable_origin_validation()
7093            .session_config(SessionConfig::default().max_sessions(1));
7094        let app = transport.into_router();
7095
7096        // First initialize succeeds
7097        let init1 = Request::builder()
7098            .method("POST")
7099            .uri("/")
7100            .header("Content-Type", "application/json")
7101            .header("Accept", "application/json, text/event-stream")
7102            .body(Body::from(
7103                serde_json::json!({
7104                    "jsonrpc": "2.0",
7105                    "id": 1,
7106                    "method": "initialize",
7107                    "params": {
7108                        "protocolVersion": "2025-11-25",
7109                        "capabilities": {},
7110                        "clientInfo": { "name": "test1", "version": "1.0" }
7111                    }
7112                })
7113                .to_string(),
7114            ))
7115            .unwrap();
7116
7117        let resp1 = app.clone().oneshot(init1).await.unwrap();
7118        assert_eq!(resp1.status(), StatusCode::OK);
7119
7120        // Second initialize should fail (max 1 session)
7121        let init2 = Request::builder()
7122            .method("POST")
7123            .uri("/")
7124            .header("Content-Type", "application/json")
7125            .header("Accept", "application/json, text/event-stream")
7126            .body(Body::from(
7127                serde_json::json!({
7128                    "jsonrpc": "2.0",
7129                    "id": 2,
7130                    "method": "initialize",
7131                    "params": {
7132                        "protocolVersion": "2025-11-25",
7133                        "capabilities": {},
7134                        "clientInfo": { "name": "test2", "version": "1.0" }
7135                    }
7136                })
7137                .to_string(),
7138            ))
7139            .unwrap();
7140
7141        let resp2 = app.oneshot(init2).await.unwrap();
7142        assert_eq!(resp2.status(), StatusCode::SERVICE_UNAVAILABLE);
7143    }
7144
7145    #[tokio::test]
7146    async fn test_delete_terminates_session() {
7147        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
7148        let app = transport.into_router();
7149
7150        // Initialize
7151        let init_req = Request::builder()
7152            .method("POST")
7153            .uri("/")
7154            .header("Content-Type", "application/json")
7155            .header("Accept", "application/json, text/event-stream")
7156            .body(Body::from(
7157                serde_json::json!({
7158                    "jsonrpc": "2.0",
7159                    "id": 1,
7160                    "method": "initialize",
7161                    "params": {
7162                        "protocolVersion": "2025-11-25",
7163                        "capabilities": {},
7164                        "clientInfo": { "name": "test", "version": "1.0" }
7165                    }
7166                })
7167                .to_string(),
7168            ))
7169            .unwrap();
7170
7171        let resp = app.clone().oneshot(init_req).await.unwrap();
7172        let session_id = resp
7173            .headers()
7174            .get(MCP_SESSION_ID_HEADER)
7175            .unwrap()
7176            .to_str()
7177            .unwrap()
7178            .to_string();
7179
7180        // DELETE should terminate the session
7181        let delete_req = Request::builder()
7182            .method("DELETE")
7183            .uri("/")
7184            .header("mcp-session-id", &session_id)
7185            .body(Body::empty())
7186            .unwrap();
7187
7188        let resp = app.clone().oneshot(delete_req).await.unwrap();
7189        assert!(resp.status().is_success());
7190
7191        // Subsequent request with that session ID should fail
7192        let list_req = Request::builder()
7193            .method("POST")
7194            .uri("/")
7195            .header("Content-Type", "application/json")
7196            .header("Accept", "application/json")
7197            .header("mcp-session-id", &session_id)
7198            .body(Body::from(
7199                serde_json::json!({
7200                    "jsonrpc": "2.0",
7201                    "id": 2,
7202                    "method": "tools/list",
7203                    "params": {}
7204                })
7205                .to_string(),
7206            ))
7207            .unwrap();
7208
7209        let resp = app.oneshot(list_req).await.unwrap();
7210        let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
7211            .await
7212            .unwrap();
7213        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
7214        assert_eq!(json["error"]["code"].as_i64().unwrap(), -32005);
7215    }
7216
7217    // -----------------------------------------------------------------------
7218    // Origin validation / DNS rebinding protection
7219    // -----------------------------------------------------------------------
7220
7221    #[test]
7222    fn test_is_localhost_origin_http() {
7223        assert!(is_localhost_origin("http://localhost"));
7224        assert!(is_localhost_origin("http://localhost:3000"));
7225        assert!(is_localhost_origin("http://127.0.0.1"));
7226        assert!(is_localhost_origin("http://127.0.0.1:8080"));
7227        assert!(is_localhost_origin("http://[::1]"));
7228        assert!(is_localhost_origin("http://[::1]:3000"));
7229    }
7230
7231    #[test]
7232    fn test_is_localhost_origin_https() {
7233        assert!(is_localhost_origin("https://localhost"));
7234        assert!(is_localhost_origin("https://127.0.0.1:443"));
7235    }
7236
7237    #[test]
7238    fn test_is_not_localhost_origin() {
7239        assert!(!is_localhost_origin("http://example.com"));
7240        assert!(!is_localhost_origin("http://evil-localhost.com"));
7241        assert!(!is_localhost_origin("http://localhost.evil.com"));
7242        assert!(!is_localhost_origin("ftp://localhost"));
7243        assert!(!is_localhost_origin("localhost"));
7244        assert!(!is_localhost_origin(""));
7245    }
7246
7247    #[tokio::test]
7248    async fn test_origin_validation_rejects_cross_origin() {
7249        let transport = HttpTransport::new(create_test_router());
7250        let app = transport.into_router();
7251
7252        let req = Request::builder()
7253            .method("POST")
7254            .uri("/")
7255            .header("Content-Type", "application/json")
7256            .header("Accept", "application/json, text/event-stream")
7257            .header("Origin", "http://evil.com")
7258            .body(Body::from(
7259                serde_json::json!({
7260                    "jsonrpc": "2.0",
7261                    "id": 1,
7262                    "method": "initialize",
7263                    "params": {
7264                        "protocolVersion": "2025-11-25",
7265                        "capabilities": {},
7266                        "clientInfo": { "name": "test", "version": "1.0" }
7267                    }
7268                })
7269                .to_string(),
7270            ))
7271            .unwrap();
7272
7273        let resp = app.oneshot(req).await.unwrap();
7274        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
7275    }
7276
7277    #[tokio::test]
7278    async fn test_origin_validation_allows_localhost() {
7279        let transport = HttpTransport::new(create_test_router());
7280        let app = transport.into_router();
7281
7282        let req = Request::builder()
7283            .method("POST")
7284            .uri("/")
7285            .header("Content-Type", "application/json")
7286            .header("Accept", "application/json, text/event-stream")
7287            .header("Origin", "http://localhost:3000")
7288            .body(Body::from(
7289                serde_json::json!({
7290                    "jsonrpc": "2.0",
7291                    "id": 1,
7292                    "method": "initialize",
7293                    "params": {
7294                        "protocolVersion": "2025-11-25",
7295                        "capabilities": {},
7296                        "clientInfo": { "name": "test", "version": "1.0" }
7297                    }
7298                })
7299                .to_string(),
7300            ))
7301            .unwrap();
7302
7303        let resp = app.oneshot(req).await.unwrap();
7304        assert_eq!(resp.status(), StatusCode::OK);
7305    }
7306
7307    #[tokio::test]
7308    async fn test_origin_validation_allows_configured_origin() {
7309        let transport = HttpTransport::new(create_test_router())
7310            .allowed_origins(vec!["https://my-app.example.com".to_string()]);
7311        let app = transport.into_router();
7312
7313        let req = Request::builder()
7314            .method("POST")
7315            .uri("/")
7316            .header("Content-Type", "application/json")
7317            .header("Accept", "application/json, text/event-stream")
7318            .header("Origin", "https://my-app.example.com")
7319            .body(Body::from(
7320                serde_json::json!({
7321                    "jsonrpc": "2.0",
7322                    "id": 1,
7323                    "method": "initialize",
7324                    "params": {
7325                        "protocolVersion": "2025-11-25",
7326                        "capabilities": {},
7327                        "clientInfo": { "name": "test", "version": "1.0" }
7328                    }
7329                })
7330                .to_string(),
7331            ))
7332            .unwrap();
7333
7334        let resp = app.oneshot(req).await.unwrap();
7335        assert_eq!(resp.status(), StatusCode::OK);
7336    }
7337
7338    #[tokio::test]
7339    async fn test_origin_validation_rejects_unconfigured_origin() {
7340        let transport = HttpTransport::new(create_test_router())
7341            .allowed_origins(vec!["https://my-app.example.com".to_string()]);
7342        let app = transport.into_router();
7343
7344        let req = Request::builder()
7345            .method("POST")
7346            .uri("/")
7347            .header("Content-Type", "application/json")
7348            .header("Accept", "application/json, text/event-stream")
7349            .header("Origin", "https://other-app.example.com")
7350            .body(Body::from(
7351                serde_json::json!({
7352                    "jsonrpc": "2.0",
7353                    "id": 1,
7354                    "method": "initialize",
7355                    "params": {
7356                        "protocolVersion": "2025-11-25",
7357                        "capabilities": {},
7358                        "clientInfo": { "name": "test", "version": "1.0" }
7359                    }
7360                })
7361                .to_string(),
7362            ))
7363            .unwrap();
7364
7365        let resp = app.oneshot(req).await.unwrap();
7366        assert_eq!(resp.status(), StatusCode::FORBIDDEN);
7367    }
7368
7369    #[tokio::test]
7370    async fn test_origin_validation_no_header_allowed() {
7371        // Requests without Origin header should be allowed (same-origin)
7372        let transport = HttpTransport::new(create_test_router());
7373        let app = transport.into_router();
7374
7375        let req = Request::builder()
7376            .method("POST")
7377            .uri("/")
7378            .header("Content-Type", "application/json")
7379            .header("Accept", "application/json, text/event-stream")
7380            // No Origin header
7381            .body(Body::from(
7382                serde_json::json!({
7383                    "jsonrpc": "2.0",
7384                    "id": 1,
7385                    "method": "initialize",
7386                    "params": {
7387                        "protocolVersion": "2025-11-25",
7388                        "capabilities": {},
7389                        "clientInfo": { "name": "test", "version": "1.0" }
7390                    }
7391                })
7392                .to_string(),
7393            ))
7394            .unwrap();
7395
7396        let resp = app.oneshot(req).await.unwrap();
7397        assert_eq!(resp.status(), StatusCode::OK);
7398    }
7399
7400    #[tokio::test]
7401    async fn test_disabled_origin_validation_allows_any() {
7402        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
7403        let app = transport.into_router();
7404
7405        let req = Request::builder()
7406            .method("POST")
7407            .uri("/")
7408            .header("Content-Type", "application/json")
7409            .header("Accept", "application/json, text/event-stream")
7410            .header("Origin", "http://evil.com")
7411            .body(Body::from(
7412                serde_json::json!({
7413                    "jsonrpc": "2.0",
7414                    "id": 1,
7415                    "method": "initialize",
7416                    "params": {
7417                        "protocolVersion": "2025-11-25",
7418                        "capabilities": {},
7419                        "clientInfo": { "name": "test", "version": "1.0" }
7420                    }
7421                })
7422                .to_string(),
7423            ))
7424            .unwrap();
7425
7426        let resp = app.oneshot(req).await.unwrap();
7427        assert_eq!(resp.status(), StatusCode::OK);
7428    }
7429
7430    // =========================================================================
7431    // Host header validation (DNS rebinding defense complement to Origin)
7432    // =========================================================================
7433
7434    fn initialize_body() -> Body {
7435        Body::from(
7436            serde_json::json!({
7437                "jsonrpc": "2.0",
7438                "id": 1,
7439                "method": "initialize",
7440                "params": {
7441                    "protocolVersion": "2025-11-25",
7442                    "capabilities": {},
7443                    "clientInfo": { "name": "test", "version": "1.0" }
7444                }
7445            })
7446            .to_string(),
7447        )
7448    }
7449
7450    #[test]
7451    fn test_is_localhost_host_variants() {
7452        assert!(is_localhost_host("localhost"));
7453        assert!(is_localhost_host("localhost:3000"));
7454        assert!(is_localhost_host("127.0.0.1"));
7455        assert!(is_localhost_host("127.0.0.1:8080"));
7456        assert!(is_localhost_host("[::1]"));
7457        assert!(is_localhost_host("[::1]:3000"));
7458
7459        assert!(!is_localhost_host("evil.com"));
7460        assert!(!is_localhost_host("api.example.com:8443"));
7461        assert!(!is_localhost_host("10.0.0.1"));
7462    }
7463
7464    #[tokio::test]
7465    async fn test_host_validation_allows_localhost() {
7466        let transport = HttpTransport::new(create_test_router())
7467            .allowed_hosts(vec!["api.example.com".to_string()]);
7468        let app = transport.into_router();
7469
7470        let req = Request::builder()
7471            .method("POST")
7472            .uri("/")
7473            .header("Content-Type", "application/json")
7474            .header("Accept", "application/json, text/event-stream")
7475            .header("Host", "127.0.0.1:3000")
7476            .body(initialize_body())
7477            .unwrap();
7478
7479        let resp = app.oneshot(req).await.unwrap();
7480        assert_eq!(resp.status(), StatusCode::OK);
7481    }
7482
7483    #[tokio::test]
7484    async fn test_host_validation_allows_configured_host() {
7485        let transport = HttpTransport::new(create_test_router())
7486            .allowed_hosts(vec!["api.example.com".to_string()]);
7487        let app = transport.into_router();
7488
7489        let req = Request::builder()
7490            .method("POST")
7491            .uri("/")
7492            .header("Content-Type", "application/json")
7493            .header("Accept", "application/json, text/event-stream")
7494            .header("Host", "api.example.com")
7495            .body(initialize_body())
7496            .unwrap();
7497
7498        let resp = app.oneshot(req).await.unwrap();
7499        assert_eq!(resp.status(), StatusCode::OK);
7500    }
7501
7502    #[tokio::test]
7503    async fn test_host_validation_rejects_unconfigured_host() {
7504        let transport = HttpTransport::new(create_test_router())
7505            .allowed_hosts(vec!["api.example.com".to_string()]);
7506        let app = transport.into_router();
7507
7508        let req = Request::builder()
7509            .method("POST")
7510            .uri("/")
7511            .header("Content-Type", "application/json")
7512            .header("Accept", "application/json, text/event-stream")
7513            .header("Host", "evil.com")
7514            .body(initialize_body())
7515            .unwrap();
7516
7517        let resp = app.oneshot(req).await.unwrap();
7518        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
7519    }
7520
7521    #[tokio::test]
7522    async fn test_host_validation_no_allowlist_accepts_any_host() {
7523        // Existing deployments that haven't opted into Host validation
7524        // (no `.allowed_hosts(...)`) should keep accepting non-localhost
7525        // hosts; Origin still protects browsers.
7526        let transport = HttpTransport::new(create_test_router());
7527        let app = transport.into_router();
7528
7529        let req = Request::builder()
7530            .method("POST")
7531            .uri("/")
7532            .header("Content-Type", "application/json")
7533            .header("Accept", "application/json, text/event-stream")
7534            .header("Host", "any.example.com")
7535            .body(initialize_body())
7536            .unwrap();
7537
7538        let resp = app.oneshot(req).await.unwrap();
7539        assert_eq!(resp.status(), StatusCode::OK);
7540    }
7541
7542    #[tokio::test]
7543    async fn test_disabled_host_validation_allows_any_with_allowlist() {
7544        let transport = HttpTransport::new(create_test_router())
7545            .disable_host_validation()
7546            .allowed_hosts(vec!["api.example.com".to_string()]);
7547        let app = transport.into_router();
7548
7549        let req = Request::builder()
7550            .method("POST")
7551            .uri("/")
7552            .header("Content-Type", "application/json")
7553            .header("Accept", "application/json, text/event-stream")
7554            .header("Host", "evil.com")
7555            .body(initialize_body())
7556            .unwrap();
7557
7558        let resp = app.oneshot(req).await.unwrap();
7559        assert_eq!(resp.status(), StatusCode::OK);
7560    }
7561
7562    #[test]
7563    fn test_effective_host_prefers_header() {
7564        let mut headers = HeaderMap::new();
7565        headers.insert(header::HOST, HeaderValue::from_static("api.example.com"));
7566        let uri: axum::http::Uri = "http://other.example.com/path".parse().unwrap();
7567        assert_eq!(effective_host(&headers, &uri), Some("api.example.com"));
7568    }
7569
7570    #[test]
7571    fn test_effective_host_falls_back_to_authority() {
7572        // When Host header is missing (HTTP/2 + middleware that strips it),
7573        // we should fall back to the URI authority.
7574        let headers = HeaderMap::new();
7575        let uri: axum::http::Uri = "http://api.example.com/path".parse().unwrap();
7576        assert_eq!(effective_host(&headers, &uri), Some("api.example.com"));
7577    }
7578
7579    #[test]
7580    fn test_effective_host_returns_none_when_both_missing() {
7581        let headers = HeaderMap::new();
7582        let uri: axum::http::Uri = "/path".parse().unwrap();
7583        assert_eq!(effective_host(&headers, &uri), None);
7584    }
7585
7586    // =========================================================================
7587    // External notification fan-out
7588    // =========================================================================
7589
7590    /// Initialize a session against `app` and return its session id.
7591    async fn init_session(app: &Router) -> String {
7592        let req = Request::builder()
7593            .method("POST")
7594            .uri("/")
7595            .header("Content-Type", "application/json")
7596            .header("Accept", "application/json, text/event-stream")
7597            .body(Body::from(
7598                serde_json::json!({
7599                    "jsonrpc": "2.0",
7600                    "id": 1,
7601                    "method": "initialize",
7602                    "params": {
7603                        "protocolVersion": "2025-11-25",
7604                        "capabilities": {},
7605                        "clientInfo": { "name": "test", "version": "1.0" }
7606                    }
7607                })
7608                .to_string(),
7609            ))
7610            .unwrap();
7611        let resp = app.clone().oneshot(req).await.unwrap();
7612        assert_eq!(resp.status(), StatusCode::OK);
7613        resp.headers()
7614            .get(MCP_SESSION_ID_HEADER)
7615            .and_then(|v| v.to_str().ok())
7616            .map(|s| s.to_string())
7617            .expect("initialize must return a session id")
7618    }
7619
7620    #[tokio::test]
7621    async fn test_external_notification_reaches_single_session() {
7622        let (notif_tx, notif_rx) = notification_channel(8);
7623        let transport = HttpTransport::with_notifications(create_test_router(), notif_rx);
7624        let (app, session_handle) = transport.into_router_with_handle();
7625
7626        let session_id = init_session(&app).await;
7627
7628        // Subscribe to the session's broadcast channel before firing.
7629        let mut rx = {
7630            let sessions = session_handle.store.sessions.read().await;
7631            let session = sessions
7632                .get(&session_id)
7633                .expect("session should be registered");
7634            session.notifications_tx.subscribe()
7635        };
7636
7637        notif_tx
7638            .send(crate::context::ServerNotification::ResourceUpdated {
7639                uri: "claude://chats/abc".to_string(),
7640            })
7641            .await
7642            .unwrap();
7643
7644        let json = tokio::time::timeout(Duration::from_secs(1), rx.recv())
7645            .await
7646            .expect("notification should arrive within timeout")
7647            .expect("broadcast channel closed");
7648        assert!(json.contains("notifications/resources/updated"));
7649        assert!(json.contains("claude://chats/abc"));
7650    }
7651
7652    #[tokio::test]
7653    async fn test_external_notification_fans_out_to_all_sessions() {
7654        let (notif_tx, notif_rx) = notification_channel(8);
7655        let transport = HttpTransport::with_notifications(create_test_router(), notif_rx);
7656        let (app, session_handle) = transport.into_router_with_handle();
7657
7658        let session_a = init_session(&app).await;
7659        let session_b = init_session(&app).await;
7660        assert_ne!(session_a, session_b);
7661
7662        let (mut rx_a, mut rx_b) = {
7663            let sessions = session_handle.store.sessions.read().await;
7664            let a = sessions.get(&session_a).unwrap();
7665            let b = sessions.get(&session_b).unwrap();
7666            (
7667                a.notifications_tx.subscribe(),
7668                b.notifications_tx.subscribe(),
7669            )
7670        };
7671
7672        notif_tx
7673            .send(crate::context::ServerNotification::ResourcesListChanged)
7674            .await
7675            .unwrap();
7676
7677        let json_a = tokio::time::timeout(Duration::from_secs(1), rx_a.recv())
7678            .await
7679            .unwrap()
7680            .unwrap();
7681        let json_b = tokio::time::timeout(Duration::from_secs(1), rx_b.recv())
7682            .await
7683            .unwrap()
7684            .unwrap();
7685        assert!(json_a.contains("notifications/resources/list_changed"));
7686        assert!(json_b.contains("notifications/resources/list_changed"));
7687    }
7688
7689    #[tokio::test]
7690    async fn test_external_notifications_builder_method() {
7691        // `external_notifications` should be equivalent to the constructor.
7692        let (notif_tx, notif_rx) = notification_channel(8);
7693        let transport = HttpTransport::new(create_test_router()).external_notifications(notif_rx);
7694        let (app, session_handle) = transport.into_router_with_handle();
7695
7696        let session_id = init_session(&app).await;
7697        let mut rx = {
7698            let sessions = session_handle.store.sessions.read().await;
7699            sessions
7700                .get(&session_id)
7701                .unwrap()
7702                .notifications_tx
7703                .subscribe()
7704        };
7705
7706        notif_tx
7707            .send(crate::context::ServerNotification::ToolsListChanged)
7708            .await
7709            .unwrap();
7710
7711        let json = tokio::time::timeout(Duration::from_secs(1), rx.recv())
7712            .await
7713            .unwrap()
7714            .unwrap();
7715        assert!(json.contains("notifications/tools/list_changed"));
7716    }
7717
7718    #[tokio::test]
7719    async fn test_default_transport_has_no_external_fanout_task() {
7720        // Smoke test: a transport without external notifications builds and
7721        // serves normally. (Verifying the fan-out task is *not* spawned is
7722        // hard to do directly; this just confirms we didn't accidentally
7723        // gate the happy path on the channel being present.)
7724        let transport = HttpTransport::new(create_test_router());
7725        let (app, _handle) = transport.into_router_with_handle();
7726        let _session_id = init_session(&app).await;
7727    }
7728
7729    // =========================================================================
7730    // Chunk 5: version-gated stateless mode for 2026-07-28+ clients
7731    // =========================================================================
7732
7733    /// 2026-07-28 removed initialize entirely.
7734    #[tokio::test]
7735    #[cfg(feature = "stateless")]
7736    async fn stateless_v2026_initialize_is_method_not_found() {
7737        let transport = HttpTransport::new(create_test_router())
7738            .disable_origin_validation()
7739            .disable_host_validation();
7740        let app = transport.into_router();
7741        let req = Request::builder()
7742            .method("POST")
7743            .uri("/")
7744            .header("Content-Type", "application/json")
7745            .header("Accept", "application/json, text/event-stream")
7746            .header(MCP_METHOD_HEADER, "initialize")
7747            .header(MCP_PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION_2026_07_28)
7748            .body(Body::from(
7749                serde_json::json!({
7750                    "jsonrpc": "2.0",
7751                    "id": 1,
7752                    "method": "initialize",
7753                    "params": {
7754                        "protocolVersion": "2026-07-28",
7755                        "capabilities": {},
7756                        "clientInfo": { "name": "sc", "version": "1.0" },
7757                        "_meta": {
7758                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
7759                            "io.modelcontextprotocol/clientCapabilities": {}
7760                        }
7761                    }
7762                })
7763                .to_string(),
7764            ))
7765            .unwrap();
7766        let response = app.oneshot(req).await.unwrap();
7767        assert_eq!(response.status(), StatusCode::NOT_FOUND);
7768        assert!(
7769            !response.headers().contains_key(MCP_SESSION_ID_HEADER),
7770            "removed final method must not create a session"
7771        );
7772        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
7773            .await
7774            .unwrap();
7775        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
7776        assert_eq!(json["id"], 1);
7777        assert_eq!(json["error"]["code"], ErrorCode::MethodNotFound.code());
7778    }
7779
7780    #[tokio::test]
7781    #[cfg(feature = "stateless")]
7782    async fn stateless_v2026_rejects_missing_required_meta_with_http_400() {
7783        let app = HttpTransport::new(create_test_router())
7784            .disable_origin_validation()
7785            .disable_host_validation()
7786            .into_router();
7787        let request = Request::builder()
7788            .method("POST")
7789            .uri("/")
7790            .header("Content-Type", "application/json")
7791            .header("Accept", "application/json")
7792            .header(MCP_METHOD_HEADER, "server/discover")
7793            .header(MCP_PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION_2026_07_28)
7794            .body(Body::from(
7795                serde_json::json!({
7796                    "jsonrpc": "2.0",
7797                    "id": 101,
7798                    "method": "server/discover",
7799                    "params": {}
7800                })
7801                .to_string(),
7802            ))
7803            .unwrap();
7804
7805        let response = app.oneshot(request).await.unwrap();
7806        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
7807        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
7808            .await
7809            .unwrap();
7810        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
7811        assert_eq!(json["id"], 101);
7812        assert_eq!(json["error"]["code"], ErrorCode::InvalidParams.code());
7813    }
7814
7815    #[tokio::test]
7816    #[cfg(feature = "stateless")]
7817    async fn stateless_v2026_rejects_invalid_meta_and_extension_keys_with_http_400() {
7818        let app = HttpTransport::new(create_test_router())
7819            .disable_origin_validation()
7820            .disable_host_validation()
7821            .into_router();
7822
7823        let build_request =
7824            |id: i64, extra_meta: serde_json::Value, extensions: serde_json::Value| {
7825                let mut meta = serde_json::json!({
7826                    "io.modelcontextprotocol/protocolVersion": PROTOCOL_VERSION_2026_07_28,
7827                    "io.modelcontextprotocol/clientCapabilities": {
7828                        "extensions": extensions
7829                    }
7830                });
7831                meta.as_object_mut()
7832                    .unwrap()
7833                    .extend(extra_meta.as_object().unwrap().clone());
7834                Request::builder()
7835                    .method("POST")
7836                    .uri("/")
7837                    .header("Content-Type", "application/json")
7838                    .header("Accept", "application/json")
7839                    .header(MCP_METHOD_HEADER, "server/discover")
7840                    .header(MCP_PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION_2026_07_28)
7841                    .body(Body::from(
7842                        serde_json::json!({
7843                            "jsonrpc": "2.0",
7844                            "id": id,
7845                            "method": "server/discover",
7846                            "params": { "_meta": meta }
7847                        })
7848                        .to_string(),
7849                    ))
7850                    .unwrap()
7851            };
7852
7853        for request in [
7854            build_request(
7855                111,
7856                serde_json::json!({"com.example/-invalid": true}),
7857                serde_json::json!({}),
7858            ),
7859            build_request(
7860                112,
7861                serde_json::json!({}),
7862                serde_json::json!({"unprefixed": {}}),
7863            ),
7864            build_request(
7865                113,
7866                serde_json::json!({}),
7867                serde_json::json!({"com.example/feature": true}),
7868            ),
7869        ] {
7870            let response = app.clone().oneshot(request).await.unwrap();
7871            assert_eq!(response.status(), StatusCode::BAD_REQUEST);
7872            let body = axum::body::to_bytes(response.into_body(), usize::MAX)
7873                .await
7874                .unwrap();
7875            let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
7876            assert_eq!(json["error"]["code"], ErrorCode::InvalidParams.code());
7877        }
7878    }
7879
7880    #[tokio::test]
7881    #[cfg(feature = "stateless")]
7882    async fn stateless_v2026_rejects_missing_protocol_header_with_http_400() {
7883        let app = HttpTransport::new(create_test_router())
7884            .disable_origin_validation()
7885            .disable_host_validation()
7886            .into_router();
7887        let request = Request::builder()
7888            .method("POST")
7889            .uri("/")
7890            .header("Content-Type", "application/json")
7891            .header("Accept", "application/json")
7892            .header(MCP_METHOD_HEADER, "server/discover")
7893            .body(Body::from(
7894                serde_json::json!({
7895                    "jsonrpc": "2.0",
7896                    "id": 102,
7897                    "method": "server/discover",
7898                    "params": {
7899                        "_meta": {
7900                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
7901                            "io.modelcontextprotocol/clientCapabilities": {}
7902                        }
7903                    }
7904                })
7905                .to_string(),
7906            ))
7907            .unwrap();
7908
7909        let response = app.oneshot(request).await.unwrap();
7910        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
7911        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
7912            .await
7913            .unwrap();
7914        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
7915        assert_eq!(json["id"], 102);
7916        assert_eq!(json["error"]["code"], McpErrorCode::HeaderMismatch.code());
7917    }
7918
7919    #[tokio::test]
7920    #[cfg(feature = "stateless")]
7921    async fn stateless_v2026_unknown_method_is_http_404() {
7922        let app = HttpTransport::new(create_test_router())
7923            .disable_origin_validation()
7924            .disable_host_validation()
7925            .into_router();
7926        let request = Request::builder()
7927            .method("POST")
7928            .uri("/")
7929            .header("Content-Type", "application/json")
7930            .header("Accept", "application/json")
7931            .header(MCP_METHOD_HEADER, "unknown/method")
7932            .header(MCP_PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION_2026_07_28)
7933            .body(Body::from(
7934                serde_json::json!({
7935                    "jsonrpc": "2.0",
7936                    "id": 103,
7937                    "method": "unknown/method",
7938                    "params": {
7939                        "_meta": {
7940                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
7941                            "io.modelcontextprotocol/clientCapabilities": {}
7942                        }
7943                    }
7944                })
7945                .to_string(),
7946            ))
7947            .unwrap();
7948
7949        let response = app.oneshot(request).await.unwrap();
7950        assert_eq!(response.status(), StatusCode::NOT_FOUND);
7951        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
7952            .await
7953            .unwrap();
7954        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
7955        assert_eq!(json["id"], 103);
7956        assert_eq!(json["error"]["code"], ErrorCode::MethodNotFound.code());
7957    }
7958
7959    #[tokio::test]
7960    #[cfg(feature = "stateless")]
7961    async fn stateless_v2026_ignores_legacy_session_and_resumption_headers() {
7962        let app = HttpTransport::new(create_test_router())
7963            .disable_origin_validation()
7964            .disable_host_validation()
7965            .into_router();
7966        let request = Request::builder()
7967            .method("POST")
7968            .uri("/")
7969            .header("Content-Type", "application/json")
7970            .header("Accept", "application/json")
7971            .header(MCP_METHOD_HEADER, "tools/list")
7972            .header(MCP_PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION_2026_07_28)
7973            .header(MCP_SESSION_ID_HEADER, "legacy-session-that-does-not-exist")
7974            .header(LAST_EVENT_ID_HEADER, "legacy-event")
7975            .body(Body::from(
7976                serde_json::json!({
7977                    "jsonrpc": "2.0",
7978                    "id": 104,
7979                    "method": "tools/list",
7980                    "params": {
7981                        "_meta": {
7982                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
7983                            "io.modelcontextprotocol/clientCapabilities": {}
7984                        }
7985                    }
7986                })
7987                .to_string(),
7988            ))
7989            .unwrap();
7990
7991        let response = app.oneshot(request).await.unwrap();
7992        assert_eq!(response.status(), StatusCode::OK);
7993        assert!(!response.headers().contains_key(MCP_SESSION_ID_HEADER));
7994        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
7995            .await
7996            .unwrap();
7997        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
7998        assert!(json["result"]["tools"].is_array());
7999    }
8000
8001    #[tokio::test]
8002    #[cfg(feature = "stateless")]
8003    async fn stateless_v2026_enforces_tool_client_capability_requirements() {
8004        use crate::{CallToolResult, SamplingCapability, ToolBuilder};
8005
8006        let tool = ToolBuilder::new("sample")
8007            .no_params_handler(|| async { Ok(CallToolResult::text("ok")) })
8008            .build()
8009            .require_client_capabilities(ClientCapabilities {
8010                sampling: Some(SamplingCapability::default()),
8011                ..ClientCapabilities::default()
8012            });
8013        let router = McpRouter::new()
8014            .server_info("test-server", "1.0.0")
8015            .tool(tool);
8016        let app = HttpTransport::new(router)
8017            .disable_origin_validation()
8018            .disable_host_validation()
8019            .into_router();
8020
8021        let build_request = |id: i64, capabilities: serde_json::Value| {
8022            Request::builder()
8023                .method("POST")
8024                .uri("/")
8025                .header("Content-Type", "application/json")
8026                .header("Accept", "application/json")
8027                .header(MCP_METHOD_HEADER, "tools/call")
8028                .header(MCP_NAME_HEADER, "sample")
8029                .header(MCP_PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION_2026_07_28)
8030                .body(Body::from(
8031                    serde_json::json!({
8032                        "jsonrpc": "2.0",
8033                        "id": id,
8034                        "method": "tools/call",
8035                        "params": {
8036                            "name": "sample",
8037                            "arguments": {},
8038                            "_meta": {
8039                                "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8040                                "io.modelcontextprotocol/clientCapabilities": capabilities
8041                            }
8042                        }
8043                    })
8044                    .to_string(),
8045                ))
8046                .unwrap()
8047        };
8048
8049        let response = app
8050            .clone()
8051            .oneshot(build_request(105, serde_json::json!({})))
8052            .await
8053            .unwrap();
8054        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
8055        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8056            .await
8057            .unwrap();
8058        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8059        assert_eq!(json["id"], 105);
8060        assert_eq!(
8061            json["error"]["code"],
8062            McpErrorCode::MissingRequiredClientCapability.code()
8063        );
8064        assert_eq!(
8065            json["error"]["data"]["requiredCapabilities"],
8066            serde_json::json!({ "sampling": {} })
8067        );
8068
8069        let response = app
8070            .oneshot(build_request(106, serde_json::json!({ "sampling": {} })))
8071            .await
8072            .unwrap();
8073        assert_eq!(response.status(), StatusCode::OK);
8074        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8075            .await
8076            .unwrap();
8077        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8078        assert_eq!(json["result"]["content"][0]["text"], "ok");
8079    }
8080
8081    /// 2026-07-28 tools/call without session header succeeds.
8082    #[tokio::test]
8083    #[cfg(feature = "stateless")]
8084    async fn stateless_v2026_tools_call_without_session_succeeds() {
8085        use crate::{CallToolResult, ToolBuilder};
8086        let router = McpRouter::new().server_info("t", "1.0.0").tool(
8087            ToolBuilder::new("echo")
8088                .description("echo")
8089                .handler(|args: serde_json::Value| async move {
8090                    Ok(CallToolResult::text(args.to_string()))
8091                })
8092                .build(),
8093        );
8094        let transport = HttpTransport::new(router).disable_origin_validation();
8095        let app = transport.into_router();
8096        let req = Request::builder()
8097            .method("POST")
8098            .uri("/")
8099            .header("Content-Type", "application/json")
8100            .header("Accept", "application/json")
8101            .header(MCP_PROTOCOL_VERSION_HEADER, "2026-07-28")
8102            .header(MCP_METHOD_HEADER, "tools/call")
8103            .header(MCP_NAME_HEADER, "echo")
8104            .body(Body::from(
8105                serde_json::json!({
8106                    "jsonrpc": "2.0",
8107                    "id": 1,
8108                    "method": "tools/call",
8109                    "params": {
8110                        "name": "echo",
8111                        "arguments": {"message": "hello"},
8112                        "_meta": {
8113                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8114                            "io.modelcontextprotocol/clientInfo": {
8115                                "name": "sc", "version": "1.0"
8116                            },
8117                            "io.modelcontextprotocol/clientCapabilities": {}
8118                        }
8119                    }
8120                })
8121                .to_string(),
8122            ))
8123            .unwrap();
8124        let response = app.oneshot(req).await.unwrap();
8125        assert_eq!(response.status(), StatusCode::OK);
8126        assert!(
8127            !response.headers().contains_key(MCP_SESSION_ID_HEADER),
8128            "stateless tools/call must not set mcp-session-id"
8129        );
8130        assert_eq!(
8131            response
8132                .headers()
8133                .get(MCP_PROTOCOL_VERSION_HEADER)
8134                .and_then(|v| v.to_str().ok()),
8135            Some("2026-07-28")
8136        );
8137        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8138            .await
8139            .unwrap();
8140        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8141        assert!(
8142            json.get("result").is_some(),
8143            "expected tools/call result, got: {json}"
8144        );
8145        assert_eq!(json["result"]["resultType"], "complete");
8146    }
8147
8148    /// A stateless 2026-07-28 request body helper for the serverInfo tests below.
8149    #[cfg(feature = "stateless")]
8150    fn stateless_tools_call_request() -> Request<Body> {
8151        Request::builder()
8152            .method("POST")
8153            .uri("/")
8154            .header("Content-Type", "application/json")
8155            .header("Accept", "application/json")
8156            .header(MCP_PROTOCOL_VERSION_HEADER, "2026-07-28")
8157            .header(MCP_METHOD_HEADER, "tools/call")
8158            .header(MCP_NAME_HEADER, "echo")
8159            .body(Body::from(
8160                serde_json::json!({
8161                    "jsonrpc": "2.0",
8162                    "id": 1,
8163                    "method": "tools/call",
8164                    "params": {
8165                        "name": "echo",
8166                        "arguments": {"message": "hello"},
8167                        "_meta": {
8168                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8169                            "io.modelcontextprotocol/clientInfo": {
8170                                "name": "sc", "version": "1.0"
8171                            },
8172                            "io.modelcontextprotocol/clientCapabilities": {}
8173                        }
8174                    }
8175                })
8176                .to_string(),
8177            ))
8178            .unwrap()
8179    }
8180
8181    #[cfg(feature = "stateless")]
8182    fn echo_router() -> McpRouter {
8183        use crate::{CallToolResult, ToolBuilder};
8184        McpRouter::new().server_info("t", "1.0.0").tool(
8185            ToolBuilder::new("echo")
8186                .description("echo")
8187                .handler(|args: serde_json::Value| async move {
8188                    Ok(CallToolResult::text(args.to_string()))
8189                })
8190                .build(),
8191        )
8192    }
8193
8194    /// SEP-2575: 2026-07-28 stateless responses carry server identity in
8195    /// `_meta["io.modelcontextprotocol/serverInfo"]` by default.
8196    #[tokio::test]
8197    #[cfg(feature = "stateless")]
8198    async fn stateless_v2026_response_stamps_server_info_by_default() {
8199        let transport = HttpTransport::new(echo_router()).disable_origin_validation();
8200        let app = transport.into_router();
8201        let response = app.oneshot(stateless_tools_call_request()).await.unwrap();
8202        assert_eq!(response.status(), StatusCode::OK);
8203        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8204            .await
8205            .unwrap();
8206        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8207        assert_eq!(
8208            json["result"]["_meta"]["io.modelcontextprotocol/serverInfo"]["name"], "t",
8209            "expected serverInfo stamped into result._meta, got: {json}"
8210        );
8211        assert_eq!(
8212            json["result"]["_meta"]["io.modelcontextprotocol/serverInfo"]["version"],
8213            "1.0.0"
8214        );
8215    }
8216
8217    /// `.stamp_server_info(false)` opts out of the SEP-2575 `_meta` stamp.
8218    #[tokio::test]
8219    #[cfg(feature = "stateless")]
8220    async fn stateless_v2026_response_omits_server_info_when_disabled() {
8221        let transport = HttpTransport::new(echo_router())
8222            .disable_origin_validation()
8223            .stamp_server_info(false);
8224        let app = transport.into_router();
8225        let response = app.oneshot(stateless_tools_call_request()).await.unwrap();
8226        assert_eq!(response.status(), StatusCode::OK);
8227        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8228            .await
8229            .unwrap();
8230        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8231        assert!(
8232            json["result"].get("_meta").is_none(),
8233            "expected no _meta when stamping is disabled, got: {json}"
8234        );
8235    }
8236
8237    /// 2025-11-25 initialize still returns mcp-session-id (unchanged).
8238    #[tokio::test]
8239    async fn stateless_v2025_initialize_still_gets_session_id() {
8240        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
8241        let app = transport.into_router();
8242        let req = Request::builder()
8243            .method("POST")
8244            .uri("/")
8245            .header("Content-Type", "application/json")
8246            .header("Accept", "application/json, text/event-stream")
8247            .body(Body::from(
8248                serde_json::json!({
8249                    "jsonrpc": "2.0", "id": 1, "method": "initialize",
8250                    "params": {
8251                        "protocolVersion": "2025-11-25",
8252                        "capabilities": {},
8253                        "clientInfo": { "name": "old-client", "version": "1.0" }
8254                    }
8255                })
8256                .to_string(),
8257            ))
8258            .unwrap();
8259        let response = app.oneshot(req).await.unwrap();
8260        assert_eq!(response.status(), StatusCode::OK);
8261        assert!(
8262            response.headers().contains_key(MCP_SESSION_ID_HEADER),
8263            "2025-11-25 initialize must return mcp-session-id"
8264        );
8265        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8266            .await
8267            .unwrap();
8268        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8269        assert!(
8270            json["result"].get("resultType").is_none(),
8271            "legacy result must remain unchanged: {json}"
8272        );
8273    }
8274
8275    /// With require_sessions(), 2025-11-25 tools/list without session
8276    /// header fails with SessionRequired (-32006) -- behavior unchanged.
8277    #[tokio::test]
8278    async fn stateless_v2025_tools_list_without_session_rejected() {
8279        let transport = HttpTransport::new(create_test_router())
8280            .disable_origin_validation()
8281            .require_sessions();
8282        let app = transport.into_router();
8283        let req = Request::builder()
8284            .method("POST")
8285            .uri("/")
8286            .header("Content-Type", "application/json")
8287            .header("Accept", "application/json")
8288            .header(MCP_PROTOCOL_VERSION_HEADER, "2025-11-25")
8289            .body(Body::from(
8290                serde_json::json!({
8291                    "jsonrpc": "2.0", "id": 1, "method": "tools/list"
8292                })
8293                .to_string(),
8294            ))
8295            .unwrap();
8296        let response = app.oneshot(req).await.unwrap();
8297        assert_eq!(response.status(), StatusCode::OK);
8298        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8299            .await
8300            .unwrap();
8301        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8302        assert!(json.get("error").is_some(), "expected error, got: {json}");
8303        assert_eq!(
8304            json["error"]["code"].as_i64().unwrap(),
8305            -32006,
8306            "expected SessionRequired (-32006)"
8307        );
8308    }
8309
8310    /// 2026-07-28 tools/list without session header succeeds (#856).
8311    #[tokio::test]
8312    #[cfg(feature = "stateless")]
8313    async fn stateless_v2026_tools_list_without_session_succeeds() {
8314        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
8315        let app = transport.into_router();
8316        let req = Request::builder()
8317            .method("POST")
8318            .uri("/")
8319            .header("Content-Type", "application/json")
8320            .header("Accept", "application/json")
8321            .header(MCP_PROTOCOL_VERSION_HEADER, "2026-07-28")
8322            .header(MCP_METHOD_HEADER, "tools/list")
8323            .body(Body::from(
8324                serde_json::json!({
8325                    "jsonrpc": "2.0",
8326                    "id": 1,
8327                    "method": "tools/list",
8328                    "params": {
8329                        "_meta": {
8330                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8331                            "io.modelcontextprotocol/clientCapabilities": {}
8332                        }
8333                    }
8334                })
8335                .to_string(),
8336            ))
8337            .unwrap();
8338        let response = app.oneshot(req).await.unwrap();
8339        assert_eq!(response.status(), StatusCode::OK);
8340        assert!(
8341            !response.headers().contains_key(MCP_SESSION_ID_HEADER),
8342            "stateless tools/list must not set mcp-session-id"
8343        );
8344        assert_eq!(
8345            response
8346                .headers()
8347                .get(MCP_PROTOCOL_VERSION_HEADER)
8348                .and_then(|v| v.to_str().ok()),
8349            Some("2026-07-28")
8350        );
8351        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8352            .await
8353            .unwrap();
8354        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8355        assert!(
8356            json["result"]["tools"].is_array(),
8357            "expected tools array in result, got: {json}"
8358        );
8359        assert_eq!(json["result"]["resultType"], "complete");
8360        assert_eq!(json["result"]["ttlMs"], 0);
8361        assert_eq!(json["result"]["cacheScope"], "private");
8362    }
8363
8364    /// A final-protocol cancellation notification returns 202 and creates no
8365    /// session (#857).
8366    #[tokio::test]
8367    #[cfg(feature = "stateless")]
8368    async fn stateless_v2026_notification_returns_202_no_session() {
8369        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
8370        let (app, handle) = transport.into_router_with_handle();
8371        let req = Request::builder()
8372            .method("POST")
8373            .uri("/")
8374            .header("Content-Type", "application/json")
8375            .header("Accept", "application/json")
8376            .header(MCP_PROTOCOL_VERSION_HEADER, "2026-07-28")
8377            .header(MCP_METHOD_HEADER, "notifications/cancelled")
8378            .body(Body::from(
8379                serde_json::json!({
8380                    "jsonrpc": "2.0",
8381                    "method": "notifications/cancelled",
8382                    "params": {
8383                        "requestId": 99,
8384                        "reason": "test",
8385                        "_meta": {
8386                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8387                            "io.modelcontextprotocol/clientCapabilities": {}
8388                        }
8389                    }
8390                })
8391                .to_string(),
8392            ))
8393            .unwrap();
8394        let response = app.oneshot(req).await.unwrap();
8395        assert_eq!(
8396            response.status(),
8397            StatusCode::ACCEPTED,
8398            "stateless notification must return 202 ACCEPTED"
8399        );
8400        assert!(
8401            !response.headers().contains_key(MCP_SESSION_ID_HEADER),
8402            "stateless notification must not set mcp-session-id"
8403        );
8404        assert_eq!(
8405            handle.session_count().await,
8406            0,
8407            "stateless notification must not create a session"
8408        );
8409    }
8410
8411    /// 2026-07-28 stateless request missing Mcp-Method returns -32020 + HTTP 400 (#859).
8412    #[tokio::test]
8413    #[cfg(feature = "stateless")]
8414    async fn stateless_v2026_missing_mcp_method_returns_400() {
8415        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
8416        let app = transport.into_router();
8417        let req = Request::builder()
8418            .method("POST")
8419            .uri("/")
8420            .header("Content-Type", "application/json")
8421            .header("Accept", "application/json")
8422            .header(MCP_PROTOCOL_VERSION_HEADER, "2026-07-28")
8423            // Intentionally NO Mcp-Method header
8424            .body(Body::from(
8425                serde_json::json!({
8426                    "jsonrpc": "2.0",
8427                    "id": 1,
8428                    "method": "tools/list",
8429                    "params": {
8430                        "_meta": {
8431                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8432                            "io.modelcontextprotocol/clientCapabilities": {}
8433                        }
8434                    }
8435                })
8436                .to_string(),
8437            ))
8438            .unwrap();
8439        let response = app.oneshot(req).await.unwrap();
8440        assert_eq!(
8441            response.status(),
8442            StatusCode::BAD_REQUEST,
8443            "missing Mcp-Method must return HTTP 400"
8444        );
8445        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8446            .await
8447            .unwrap();
8448        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8449        assert!(json.get("error").is_some(), "expected error, got: {json}");
8450        assert_eq!(
8451            json["error"]["code"].as_i64().unwrap(),
8452            -32020,
8453            "expected HeaderMismatch (-32020)"
8454        );
8455        assert!(
8456            json["error"]["message"]
8457                .as_str()
8458                .unwrap_or("")
8459                .contains("Mcp-Method"),
8460            "error message must mention Mcp-Method, got: {json}"
8461        );
8462    }
8463
8464    #[tokio::test]
8465    async fn sse_responses_false_returns_application_json() {
8466        // Default behavior: synchronous responses use Content-Type: application/json
8467        let transport = HttpTransport::new(create_test_router())
8468            .disable_origin_validation()
8469            .sse_responses(false);
8470        let app = transport.into_router();
8471
8472        let request = Request::builder()
8473            .method("POST")
8474            .uri("/")
8475            .header("Content-Type", "application/json")
8476            .header("Accept", "application/json, text/event-stream")
8477            .body(Body::from(
8478                serde_json::json!({
8479                    "jsonrpc": "2.0",
8480                    "id": 1,
8481                    "method": "initialize",
8482                    "params": {
8483                        "protocolVersion": "2025-11-25",
8484                        "capabilities": {},
8485                        "clientInfo": {"name": "test", "version": "0.1"}
8486                    }
8487                })
8488                .to_string(),
8489            ))
8490            .unwrap();
8491
8492        let response = app.oneshot(request).await.unwrap();
8493        assert_eq!(response.status(), StatusCode::OK);
8494        let ct = response
8495            .headers()
8496            .get(header::CONTENT_TYPE)
8497            .and_then(|v| v.to_str().ok())
8498            .unwrap_or("");
8499        assert!(
8500            ct.contains("application/json"),
8501            "sse_responses(false) should return application/json, got: {ct}"
8502        );
8503    }
8504
8505    #[tokio::test]
8506    async fn sse_responses_true_returns_text_event_stream_with_valid_json() {
8507        // When sse_responses is enabled, synchronous responses use SSE format
8508        let transport = HttpTransport::new(create_test_router())
8509            .disable_origin_validation()
8510            .sse_responses(true);
8511        let app = transport.into_router();
8512
8513        let init_body = serde_json::json!({
8514            "jsonrpc": "2.0",
8515            "id": 1,
8516            "method": "initialize",
8517            "params": {
8518                "protocolVersion": "2025-11-25",
8519                "capabilities": {},
8520                "clientInfo": {"name": "test", "version": "0.1"}
8521            }
8522        })
8523        .to_string();
8524
8525        let request = Request::builder()
8526            .method("POST")
8527            .uri("/")
8528            .header("Content-Type", "application/json")
8529            .header("Accept", "application/json, text/event-stream")
8530            .body(Body::from(init_body))
8531            .unwrap();
8532
8533        let response = app.oneshot(request).await.unwrap();
8534        assert_eq!(response.status(), StatusCode::OK);
8535
8536        let ct = response
8537            .headers()
8538            .get(header::CONTENT_TYPE)
8539            .and_then(|v| v.to_str().ok())
8540            .unwrap_or("");
8541        assert!(
8542            ct.contains("text/event-stream"),
8543            "sse_responses(true) should return text/event-stream, got: {ct}"
8544        );
8545
8546        let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
8547            .await
8548            .unwrap();
8549        let body_text = String::from_utf8_lossy(&bytes);
8550
8551        // SSE body must contain the event type line and data line
8552        assert!(
8553            body_text.contains("event: message"),
8554            "SSE body missing 'event: message': {body_text}"
8555        );
8556        assert!(
8557            body_text.contains("data: "),
8558            "SSE body missing 'data: ' line: {body_text}"
8559        );
8560
8561        // Extract and validate the JSON from the data: line
8562        let data_line = body_text
8563            .lines()
8564            .find(|l| l.starts_with("data: "))
8565            .expect("no data: line in SSE body");
8566        let json_str = data_line.trim_start_matches("data: ");
8567        let val: serde_json::Value =
8568            serde_json::from_str(json_str).expect("data: line is not valid JSON");
8569
8570        // Verify it's a well-formed JSON-RPC response with the expected result
8571        assert_eq!(val["jsonrpc"], "2.0", "jsonrpc version mismatch: {val}");
8572        assert_eq!(val["id"], 1, "id mismatch: {val}");
8573        assert!(
8574            val["result"].is_object(),
8575            "result should be an object: {val}"
8576        );
8577        // The initialize result must contain protocolVersion
8578        assert_eq!(
8579            val["result"]["protocolVersion"].as_str(),
8580            Some("2025-11-25"),
8581            "protocolVersion missing or wrong: {val}"
8582        );
8583    }
8584
8585    #[tokio::test]
8586    async fn sse_responses_true_tools_list_returns_valid_sse() {
8587        // Verify tools/list (non-init request) also returns SSE when enabled
8588        let transport = HttpTransport::new(create_test_router())
8589            .disable_origin_validation()
8590            .sse_responses(true);
8591        let app = transport.into_router();
8592
8593        // Initialize first to get a session ID
8594        let init_request = Request::builder()
8595            .method("POST")
8596            .uri("/")
8597            .header("Content-Type", "application/json")
8598            .header("Accept", "application/json, text/event-stream")
8599            .body(Body::from(
8600                serde_json::json!({
8601                    "jsonrpc": "2.0",
8602                    "id": 1,
8603                    "method": "initialize",
8604                    "params": {
8605                        "protocolVersion": "2025-11-25",
8606                        "capabilities": {},
8607                        "clientInfo": {"name": "test", "version": "0.1"}
8608                    }
8609                })
8610                .to_string(),
8611            ))
8612            .unwrap();
8613
8614        let init_response = app.clone().oneshot(init_request).await.unwrap();
8615        assert_eq!(init_response.status(), StatusCode::OK);
8616        let session_id = init_response
8617            .headers()
8618            .get(MCP_SESSION_ID_HEADER)
8619            .and_then(|v| v.to_str().ok())
8620            .map(|s| s.to_string())
8621            .expect("missing session ID from initialize");
8622
8623        // Send notifications/initialized to complete the MCP handshake.
8624        let notif_request = Request::builder()
8625            .method("POST")
8626            .uri("/")
8627            .header("Content-Type", "application/json")
8628            .header("Accept", "application/json, text/event-stream")
8629            .header(MCP_SESSION_ID_HEADER, &session_id)
8630            .body(Body::from(
8631                serde_json::json!({
8632                    "jsonrpc": "2.0",
8633                    "method": "notifications/initialized"
8634                })
8635                .to_string(),
8636            ))
8637            .unwrap();
8638        app.clone().oneshot(notif_request).await.unwrap();
8639
8640        // Now call tools/list
8641        let list_request = Request::builder()
8642            .method("POST")
8643            .uri("/")
8644            .header("Content-Type", "application/json")
8645            .header("Accept", "application/json, text/event-stream")
8646            .header(MCP_SESSION_ID_HEADER, &session_id)
8647            .body(Body::from(
8648                serde_json::json!({
8649                    "jsonrpc": "2.0",
8650                    "id": 2,
8651                    "method": "tools/list",
8652                    "params": {}
8653                })
8654                .to_string(),
8655            ))
8656            .unwrap();
8657
8658        let list_response = app.oneshot(list_request).await.unwrap();
8659        assert_eq!(list_response.status(), StatusCode::OK);
8660
8661        let ct = list_response
8662            .headers()
8663            .get(header::CONTENT_TYPE)
8664            .and_then(|v| v.to_str().ok())
8665            .unwrap_or("");
8666        assert!(
8667            ct.contains("text/event-stream"),
8668            "tools/list with sse_responses(true) should return text/event-stream, got: {ct}"
8669        );
8670
8671        let bytes = axum::body::to_bytes(list_response.into_body(), usize::MAX)
8672            .await
8673            .unwrap();
8674        let body_text = String::from_utf8_lossy(&bytes);
8675        let data_line = body_text
8676            .lines()
8677            .find(|l| l.starts_with("data: "))
8678            .expect("no data: line in SSE body for tools/list");
8679        let json_str = data_line.trim_start_matches("data: ");
8680        let val: serde_json::Value =
8681            serde_json::from_str(json_str).expect("tools/list data: line is not valid JSON");
8682
8683        assert_eq!(val["jsonrpc"], "2.0");
8684        assert_eq!(val["id"], 2);
8685        // tools/list result has a "tools" array (may be empty for create_test_router())
8686        assert!(
8687            val["result"]["tools"].is_array(),
8688            "tools/list result.tools should be an array: {val}"
8689        );
8690    }
8691
8692    // =========================================================================
8693    // notifications/initialized enforcement (#901)
8694    // =========================================================================
8695
8696    /// Helper: do the `initialize` handshake and return the session ID.
8697    async fn do_initialize(app: &axum::Router) -> String {
8698        do_initialize_for_revision(app, "2025-11-25").await
8699    }
8700
8701    async fn do_initialize_for_revision(app: &axum::Router, revision: &str) -> String {
8702        let init_request = Request::builder()
8703            .method("POST")
8704            .uri("/")
8705            .header("Content-Type", "application/json")
8706            .header("Accept", "application/json, text/event-stream")
8707            .body(Body::from(
8708                serde_json::json!({
8709                    "jsonrpc": "2.0",
8710                    "id": 1,
8711                    "method": "initialize",
8712                    "params": {
8713                        "protocolVersion": revision,
8714                        "capabilities": {},
8715                        "clientInfo": { "name": "test-client", "version": "1.0.0" }
8716                    }
8717                })
8718                .to_string(),
8719            ))
8720            .unwrap();
8721
8722        let response = app.clone().oneshot(init_request).await.unwrap();
8723        response
8724            .headers()
8725            .get(MCP_SESSION_ID_HEADER)
8726            .unwrap()
8727            .to_str()
8728            .unwrap()
8729            .to_string()
8730    }
8731
8732    async fn send_initialized(app: &axum::Router, session_id: &str) {
8733        let request = Request::builder()
8734            .method("POST")
8735            .uri("/")
8736            .header("Content-Type", "application/json")
8737            .header("Accept", "application/json, text/event-stream")
8738            .header(MCP_SESSION_ID_HEADER, session_id)
8739            .body(Body::from(
8740                serde_json::json!({
8741                    "jsonrpc": "2.0",
8742                    "method": "notifications/initialized"
8743                })
8744                .to_string(),
8745            ))
8746            .unwrap();
8747        let response = app.clone().oneshot(request).await.unwrap();
8748        assert_eq!(response.status(), StatusCode::ACCEPTED);
8749    }
8750
8751    async fn post_legacy_batch(app: &axum::Router, session_id: &str) -> serde_json::Value {
8752        let request = Request::builder()
8753            .method("POST")
8754            .uri("/")
8755            .header("Content-Type", "application/json")
8756            .header("Accept", "application/json")
8757            .header(MCP_SESSION_ID_HEADER, session_id)
8758            .body(Body::from(
8759                serde_json::json!([
8760                    {"jsonrpc": "2.0", "id": 2, "method": "ping"},
8761                    {"jsonrpc": "2.0", "id": 3, "method": "tools/list"}
8762                ])
8763                .to_string(),
8764            ))
8765            .unwrap();
8766        let response = app.clone().oneshot(request).await.unwrap();
8767        assert_eq!(response.status(), StatusCode::OK);
8768        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8769            .await
8770            .unwrap();
8771        serde_json::from_slice(&body).unwrap()
8772    }
8773
8774    #[tokio::test]
8775    async fn http_batch_policy_uses_exact_session_revision() {
8776        let march_app = HttpTransport::new(create_test_router())
8777            .disable_origin_validation()
8778            .protocol_versions(["2025-03-26"])
8779            .unwrap()
8780            .into_router();
8781        let march_session = do_initialize_for_revision(&march_app, "2025-03-26").await;
8782        send_initialized(&march_app, &march_session).await;
8783        let march_response = post_legacy_batch(&march_app, &march_session).await;
8784        assert_eq!(march_response.as_array().map(Vec::len), Some(2));
8785
8786        let november_app = HttpTransport::new(create_test_router())
8787            .disable_origin_validation()
8788            .into_router();
8789        let november_session = do_initialize(&november_app).await;
8790        send_initialized(&november_app, &november_session).await;
8791        let november_response = post_legacy_batch(&november_app, &november_session).await;
8792        assert_eq!(november_response["error"]["code"], -32600);
8793        assert!(
8794            november_response["error"]["message"]
8795                .as_str()
8796                .unwrap()
8797                .contains("does not permit top-level JSON-RPC batches")
8798        );
8799    }
8800
8801    #[cfg(feature = "stateless")]
8802    #[tokio::test]
8803    async fn http_final_batch_is_rejected_before_object_routing() {
8804        let app = HttpTransport::new(create_test_router())
8805            .disable_origin_validation()
8806            .into_router();
8807        let request = Request::builder()
8808            .method("POST")
8809            .uri("/")
8810            .header("Content-Type", "application/json")
8811            .header("Accept", "application/json")
8812            .header(MCP_PROTOCOL_VERSION_HEADER, PROTOCOL_VERSION_2026_07_28)
8813            .body(Body::from(
8814                serde_json::json!([{
8815                    "jsonrpc": "2.0",
8816                    "id": 1,
8817                    "method": "tools/list",
8818                    "params": {
8819                        "_meta": {
8820                            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
8821                            "io.modelcontextprotocol/clientCapabilities": {}
8822                        }
8823                    }
8824                }])
8825                .to_string(),
8826            ))
8827            .unwrap();
8828        let response = app.oneshot(request).await.unwrap();
8829        assert_eq!(response.status(), StatusCode::BAD_REQUEST);
8830        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8831            .await
8832            .unwrap();
8833        let response: serde_json::Value = serde_json::from_slice(&body).unwrap();
8834        assert_eq!(response["error"]["code"], -32600);
8835        assert!(
8836            response["error"]["message"]
8837                .as_str()
8838                .unwrap()
8839                .contains("does not permit top-level JSON-RPC batches")
8840        );
8841    }
8842
8843    #[tokio::test]
8844    async fn tools_list_before_initialized_notification_returns_error() {
8845        // Spec: clients MUST send notifications/initialized before any other
8846        // request. Skipping it should yield -32600 InvalidRequest.
8847        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
8848        let app = transport.into_router();
8849
8850        let session_id = do_initialize(&app).await;
8851
8852        // Send tools/list WITHOUT sending notifications/initialized first.
8853        let list_request = Request::builder()
8854            .method("POST")
8855            .uri("/")
8856            .header("Content-Type", "application/json")
8857            .header("Accept", "application/json, text/event-stream")
8858            .header(MCP_SESSION_ID_HEADER, &session_id)
8859            .body(Body::from(
8860                serde_json::json!({
8861                    "jsonrpc": "2.0",
8862                    "id": 2,
8863                    "method": "tools/list"
8864                })
8865                .to_string(),
8866            ))
8867            .unwrap();
8868
8869        let response = app.oneshot(list_request).await.unwrap();
8870        assert_eq!(response.status(), StatusCode::OK);
8871        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8872            .await
8873            .unwrap();
8874        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8875        assert!(
8876            json.get("error").is_some(),
8877            "expected error when notifications/initialized not sent, got: {json}"
8878        );
8879        assert_eq!(
8880            json["error"]["code"].as_i64().unwrap(),
8881            -32600,
8882            "expected InvalidRequest (-32600), got: {json}"
8883        );
8884        assert!(
8885            json["error"]["message"]
8886                .as_str()
8887                .unwrap_or("")
8888                .contains("notifications/initialized"),
8889            "error message should mention notifications/initialized, got: {json}"
8890        );
8891    }
8892
8893    #[tokio::test]
8894    async fn tools_list_after_initialized_notification_succeeds() {
8895        // After sending notifications/initialized, tool requests should succeed.
8896        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
8897        let app = transport.into_router();
8898
8899        let session_id = do_initialize(&app).await;
8900
8901        // Send notifications/initialized.
8902        let notif_request = Request::builder()
8903            .method("POST")
8904            .uri("/")
8905            .header("Content-Type", "application/json")
8906            .header("Accept", "application/json, text/event-stream")
8907            .header(MCP_SESSION_ID_HEADER, &session_id)
8908            .body(Body::from(
8909                serde_json::json!({
8910                    "jsonrpc": "2.0",
8911                    "method": "notifications/initialized"
8912                })
8913                .to_string(),
8914            ))
8915            .unwrap();
8916        app.clone().oneshot(notif_request).await.unwrap();
8917
8918        // Now tools/list should succeed.
8919        let list_request = Request::builder()
8920            .method("POST")
8921            .uri("/")
8922            .header("Content-Type", "application/json")
8923            .header("Accept", "application/json, text/event-stream")
8924            .header(MCP_SESSION_ID_HEADER, &session_id)
8925            .body(Body::from(
8926                serde_json::json!({
8927                    "jsonrpc": "2.0",
8928                    "id": 2,
8929                    "method": "tools/list"
8930                })
8931                .to_string(),
8932            ))
8933            .unwrap();
8934
8935        let response = app.oneshot(list_request).await.unwrap();
8936        assert_eq!(response.status(), StatusCode::OK);
8937        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
8938            .await
8939            .unwrap();
8940        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
8941        assert!(
8942            json.get("result").is_some(),
8943            "expected success after notifications/initialized, got: {json}"
8944        );
8945    }
8946
8947    #[tokio::test]
8948    async fn notifications_initialized_itself_always_accepted() {
8949        // The notifications/initialized notification itself must always be
8950        // accepted (202 ACCEPTED) regardless of the initialization flag.
8951        let transport = HttpTransport::new(create_test_router()).disable_origin_validation();
8952        let app = transport.into_router();
8953
8954        let session_id = do_initialize(&app).await;
8955
8956        let notif_request = Request::builder()
8957            .method("POST")
8958            .uri("/")
8959            .header("Content-Type", "application/json")
8960            .header("Accept", "application/json, text/event-stream")
8961            .header(MCP_SESSION_ID_HEADER, &session_id)
8962            .body(Body::from(
8963                serde_json::json!({
8964                    "jsonrpc": "2.0",
8965                    "method": "notifications/initialized"
8966                })
8967                .to_string(),
8968            ))
8969            .unwrap();
8970
8971        let response = app.oneshot(notif_request).await.unwrap();
8972        assert_eq!(
8973            response.status(),
8974            StatusCode::ACCEPTED,
8975            "notifications/initialized must return 202 ACCEPTED"
8976        );
8977    }
8978
8979    #[tokio::test]
8980    async fn strict_initialization_false_allows_tools_list_without_notification() {
8981        // When strict_initialization is disabled, tool requests must succeed
8982        // even if the client skips notifications/initialized.
8983        let config = SessionConfig {
8984            strict_initialization: false,
8985            ..Default::default()
8986        };
8987        let transport = HttpTransport::new(create_test_router())
8988            .disable_origin_validation()
8989            .session_config(config);
8990        let app = transport.into_router();
8991
8992        let session_id = do_initialize(&app).await;
8993
8994        // No notifications/initialized -- should still succeed.
8995        let list_request = Request::builder()
8996            .method("POST")
8997            .uri("/")
8998            .header("Content-Type", "application/json")
8999            .header("Accept", "application/json, text/event-stream")
9000            .header(MCP_SESSION_ID_HEADER, &session_id)
9001            .body(Body::from(
9002                serde_json::json!({
9003                    "jsonrpc": "2.0",
9004                    "id": 2,
9005                    "method": "tools/list"
9006                })
9007                .to_string(),
9008            ))
9009            .unwrap();
9010
9011        let response = app.oneshot(list_request).await.unwrap();
9012        assert_eq!(response.status(), StatusCode::OK);
9013        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
9014            .await
9015            .unwrap();
9016        let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
9017        assert!(
9018            json.get("result").is_some(),
9019            "expected success with strict_initialization=false, got: {json}"
9020        );
9021    }
9022}