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