//! MCP server implementation for FastMCP.
//!
//! This crate provides the server-side implementation:
//! - Server builder pattern
//! - Tool, resource, and prompt registration
//! - Request routing and dispatching
//! - Session management
//!
//! MCP 2026-07-28 support is under implementation and remains unverified. The
//! public protocol constant is still `2024-11-05`; server source presence is
//! not aggregate conformance or release evidence.
//!
//! # Example
//!
//! ```ignore
//! use asupersync::runtime::{RuntimeBuilder, reactor::create_reactor};
//! use fastmcp_rust::{modern::ServerBuilder, prelude::*};
//!
//! #[tool]
//! async fn greet(ctx: &McpContext, name: String) -> McpResult<String> {
//! ctx.checkpoint()?;
//! Ok(format!("Hello, {name}!"))
//! }
//!
//! fn main() {
//! let runtime = RuntimeBuilder::current_thread()
//! .with_reactor(create_reactor().expect("create I/O reactor"))
//! .blocking_threads(0, 16)
//! .build()
//! .expect("create application runtime");
//! runtime.block_on(async {
//! let cx = Cx::current().expect("application context");
//! ServerBuilder::new("my-server", "1.0.0")
//! .tool(Greet)
//! .build()
//! .run_stdio_with_cx(&cx)
//! .await
//! });
//! }
//! ```
//!
//! # Role in the System
//!
//! `fastmcp-server` is the **execution engine** for MCP servers. It ties
//! together:
//! - Protocol types (`fastmcp-protocol`) for requests and responses
//! - Transports (`fastmcp-transport`) for stdio/SSE/WebSocket/HTTP/memory I/O
//! - Core context + cancellation (`fastmcp-core`) for budgets and checkpoints
//! - Console output (`fastmcp-console`) for human-friendly stderr rendering
//!
//! The façade package `fastmcp-rust` re-exports this API, so most users
//! interact with `Server` via `fastmcp_rust::prelude::*`.
//!
//! # Extension panic-containment boundary
//!
//! Unwinding extension callback panics (including handler, auth, middleware,
//! and lifecycle callbacks) are caught at their framework boundaries.
//! Request-path failures are mapped to a fixed payload-free peer error. FastMCP
//! also installs a redacting panic hook for local diagnostics, but Rust's hook
//! is process-global and replaceable: an embedding application that installs
//! another hook afterward controls subsequent panic diagnostics. Such
//! replacement does not change the fixed peer error, but FastMCP cannot promise
//! payload-free local diagnostics after its hook has been replaced.
// Raised for the trait solver: proving `Send` for the nested async blocks in
// `serve_http_connection` overflows the default limit on newer rustc.
#![recursion_limit = "512"]
#![forbid(unsafe_code)]
// Proc-macros (fastmcp-derive) reference this crate by its external name
// (`fastmcp_server::...`). This alias makes those macros usable inside this crate too
// (including in unit tests).
extern crate self as fastmcp_server;
mod async_stdio;
mod auth;
pub mod bidirectional;
mod builder;
pub mod caching;
// FND-01: Docket/Redis is not part of the FND-01 production surface. Source bytes
// remain on disk (no-deletion) and are package-excluded; do not re-export.
pub mod extensions;
mod handler;
pub mod http_admission;
#[cfg(feature = "legacy-2024-11-05")]
pub mod legacy_2024;
mod middleware;
pub mod oauth;
pub mod oidc;
pub mod providers;
#[cfg(feature = "proxy")]
mod proxy;
pub mod rate_limiting;
mod router;
mod session;
#[cfg(feature = "tasks")]
mod tasks;
pub mod transform;
#[cfg(test)]
mod tests;
pub use auth::{
AllowAllAuthProvider, AuthProvider, AuthRequest, StaticTokenVerifier, TokenAuthProvider,
TokenVerifier,
};
pub use builder::{
RefusedRegistration, RegistrationKind, ServerBuildError, ServerBuilder, ServerLaunchPolicyError,
};
pub use extensions::{
ExtensionHandler, ExtensionHandlerInvocationError, ExtensionHandlerKey,
ExtensionHandlerLookupError, ExtensionHandlerRegistrationError, ExtensionHandlerRegistry,
};
pub use fastmcp_console::config::{BannerStyle, ConsoleConfig, TrafficVerbosity};
pub use fastmcp_console::stats::{ServerStats, StatsSnapshot};
use fastmcp_protocol::{
MAX_CLIENT_REGISTRATION_BYTES, SecurityDocumentKind, admit_security_document,
};
/// Cancellation-aware server-side WebSocket message adapter.
///
/// [`BoundWebSocketServer`] owns the listener and HTTP Upgrade boundary; this
/// lower-level adapter remains available for embedders that already own an
/// upgraded asupersync byte stream.
#[cfg(feature = "websocket")]
pub use fastmcp_transport::websocket::AsyncWsServerTransport;
pub use handler::{
BidirectionalSenders, BoxFuture, CompletionHandler, FinalElicitation,
FinalElicitationContextExt, FinalMethodOutcome, FinalResourceReadCacheHintProvenance,
FinalRoots, FinalRootsContextExt, FinalSampling, FinalSamplingContextExt, FinalToolOutcome,
FinalToolSchemaAuthority, ProgressNotificationSender, PromptHandler, ResourceHandler,
ToolErrorKind, ToolExecutionMode, ToolHandler, UriParams, create_context_with_progress,
create_context_with_progress_and_senders, promote_legacy_prompt_messages,
promote_legacy_resource_contents, promote_legacy_tool_content,
};
pub use middleware::continuation_replay::{
ContinuationReplayAuthority, ContinuationReplayLimits, ContinuationReplayMiddleware,
};
pub use middleware::{Middleware, MiddlewareDecision};
use oauth::{
AuthorizationRequest, CodeChallengeMethod, NativePublicClientRegistrationRequest, OAuthError,
OAuthHttpRoutes, OAuthParameterAdmission, OAuthParameterEndpoint, OAuthParameterName,
TokenRequest,
};
#[cfg(feature = "proxy")]
pub use proxy::{
FinalProgressCallback, ProgressCallback, ProxyAsyncRequest, ProxyBackend, ProxyCatalog,
ProxyCatalogCacheHint, ProxyClient, ProxyFinalCatalog, ProxyLegacyPeerNotifications,
ProxyPromptCatalog, ProxyResourceCatalog, ProxyResourceTemplateCatalog, ProxyToolCatalog,
ProxyTypedCatalog, ProxyUpstreamAdapter, ProxyUpstreamBinding, ProxyUpstreamBindingRegistry,
};
#[cfg(all(feature = "proxy", feature = "tasks"))]
use proxy::{ProxyCatalogListener, ProxyCatalogListenerEvent, ProxyFinalTaskRelay};
#[cfg(all(feature = "proxy", feature = "tasks"))]
pub use proxy::{ProxyFinalTaskListener, ProxyFinalTaskListenerEvent};
pub use router::{
InboundRequestContext, InboundRequestTransport, ModernConnection, MountResult,
NotificationSender, Router, TagFilters,
};
use router::{RouterPromptCaller, RouterResourceReader, RouterToolCaller, TransportAuthorization};
pub use session::Session;
#[cfg(feature = "legacy-2024-11-05")]
use session::SessionPrincipalBinding;
use session::{
InitializationSnapshot, MAX_RESOURCE_SUBSCRIPTION_BYTES_PER_SESSION, SubscriptionAdmission,
SubscriptionAdmissionError, SubscriptionRemoval, SubscriptionRemovalError,
};
#[cfg(feature = "tasks")]
pub use tasks::{
ApplicationTaskSupervisor, AuthorizedTaskServiceRunner, DEFAULT_IN_MEMORY_FINAL_TASKS,
FinalTaskAcceptedInput, FinalTaskExecutionBudget, FinalTaskExecutionLimits,
FinalTaskInitialWork, FinalTaskNotificationEmitter, FinalTaskRetentionAuthority,
FinalTaskRetentionDeadline, FinalTaskRuntime, FinalTaskRuntimeConfig, FinalTaskSnapshot,
FinalTaskStore, FinalTaskSupervisorFuture, FinalTaskSupervisorHandoff, FinalTaskWorkDescriptor,
InMemoryFinalTaskStore, MAX_IN_MEMORY_FINAL_TASK_INPUT_KEY_BYTES,
MAX_IN_MEMORY_FINAL_TASK_INPUT_KEYS,
};
#[cfg(all(test, feature = "tasks"))]
pub(crate) use tasks::{SharedTaskManager, TaskManager};
// Re-export bidirectional communication types
pub use bidirectional::{
PendingRequests, RequestSender, TransportElicitationSender, TransportRootsProvider,
TransportSamplingSender,
};
use std::any::Any;
use std::cell::Cell;
#[cfg(all(feature = "proxy", feature = "tasks"))]
use std::collections::BTreeSet;
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::future::Future;
use std::io::Write;
use std::net::SocketAddr;
#[cfg(feature = "websocket")]
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex, Once, OnceLock};
#[cfg(feature = "websocket")]
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
#[cfg(feature = "legacy-2024-11-05")]
use fastmcp_transport::http::{
DualEraHttpEndpoint, DualEraHttpEndpointConfig, DualEraHttpEndpointError,
DualEraHttpEndpointResponse, DualEraHttpLegacyLifecycle, DualEraHttpLegacySseResponse,
DualEraHttpSession, DualEraHttpSseResponse, Legacy2024HttpPostEnvelope,
admit_legacy_2024_http_post,
};
use fastmcp_transport::http::{
HttpError, HttpHandlerConfig, HttpMethod, HttpRequest, HttpRequestHandler, HttpResponse,
HttpSessionError, HttpStatus, StreamableHttpRequestCancellation,
StreamableHttpRequestResponseSender,
};
use fastmcp_transport::sse::SseEvent;
#[cfg(not(feature = "legacy-2024-11-05"))]
use modern_http_only::{
DualEraHttpEndpoint, DualEraHttpEndpointConfig, DualEraHttpEndpointError,
DualEraHttpEndpointResponse, DualEraHttpSession, DualEraHttpSseResponse,
};
// The normal no-legacy graph deliberately omits the transport's dual-era
// adapter. The server still needs the modern request-owned HTTP primitives, so
// provide a private modern-only composition with the same internal seam. It
// has no `/sse` or legacy-message admission branch.
#[cfg(not(feature = "legacy-2024-11-05"))]
mod modern_http_only {
use std::sync::Arc;
use asupersync::Cx;
use fastmcp_protocol::{JsonRpcMessage, JsonRpcRequest, JsonRpcResponse, RequestId};
use fastmcp_transport::http::{
HttpError, HttpRequest, HttpRequestHandler, HttpResponse, HttpResponseRepresentation,
HttpStatus, StreamableHttpRequestCancellation, StreamableHttpRequestResponseSender,
StreamableHttpRequestResponseStream, StreamableHttpResponseStream, StreamableHttpTransport,
};
use fastmcp_transport::sse::SseEvent;
use fastmcp_transport::{Transport, TransportError};
#[derive(Debug)]
pub enum DualEraHttpEndpointError {
InvalidConfiguration(String),
Http(HttpError),
Transport(TransportError),
Closed,
}
impl std::fmt::Display for DualEraHttpEndpointError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidConfiguration(message) => formatter.write_str(message),
Self::Http(error) => error.fmt(formatter),
Self::Transport(error) => error.fmt(formatter),
Self::Closed => formatter.write_str("modern HTTP session is closed"),
}
}
}
impl std::error::Error for DualEraHttpEndpointError {}
impl From<HttpError> for DualEraHttpEndpointError {
fn from(error: HttpError) -> Self {
Self::Http(error)
}
}
impl From<TransportError> for DualEraHttpEndpointError {
fn from(error: TransportError) -> Self {
Self::Transport(error)
}
}
pub struct DualEraHttpEndpointConfig {
pub request_capacity: usize,
}
impl DualEraHttpEndpointConfig {
pub fn new() -> Self {
Self {
request_capacity: 64,
}
}
}
pub struct DualEraHttpEndpoint {
handler: Arc<HttpRequestHandler>,
capacity: usize,
}
impl DualEraHttpEndpoint {
pub fn new(
handler: HttpRequestHandler,
config: DualEraHttpEndpointConfig,
) -> Result<Self, DualEraHttpEndpointError> {
if config.request_capacity == 0 {
return Err(DualEraHttpEndpointError::InvalidConfiguration(
"modern HTTP request capacity must be nonzero".to_owned(),
));
}
Ok(Self {
handler: Arc::new(handler),
capacity: config.request_capacity,
})
}
pub fn open_session(&self) -> Result<DualEraHttpSession, DualEraHttpEndpointError> {
let mut transport = StreamableHttpTransport::with_capacity(self.capacity)?;
let ingress = transport.request_ingress()?;
let responses = transport.response_stream()?;
Ok(DualEraHttpSession {
handler: Arc::clone(&self.handler),
transport,
ingress,
responses,
closed: false,
})
}
}
pub enum DualEraHttpEndpointResponse {
Immediate(HttpResponse),
ModernJson(DualEraHttpJsonResponse),
ModernSse(DualEraHttpSseResponse),
}
pub struct DualEraHttpJsonResponse {
handler: Arc<HttpRequestHandler>,
responses: StreamableHttpResponseStream,
request_id: RequestId,
origin: Option<String>,
}
impl DualEraHttpJsonResponse {
pub fn try_response(&self) -> Result<Option<HttpResponse>, DualEraHttpEndpointError> {
let Some(response) = self.responses.pop_response(Some(&self.request_id))? else {
return Ok(None);
};
Ok(Some(
self.handler
.try_create_response(&response, self.origin.as_deref())?,
))
}
}
pub struct DualEraHttpSseResponse {
response: HttpResponse,
body: StreamableHttpRequestResponseStream,
}
impl DualEraHttpSseResponse {
pub fn response(&self) -> &HttpResponse {
&self.response
}
pub fn cancellation(&self) -> StreamableHttpRequestCancellation {
self.body.cancellation()
}
pub fn sender(&self) -> StreamableHttpRequestResponseSender {
self.body.sender()
}
pub fn is_finished(&self) -> bool {
self.body.is_finished()
}
pub fn pop_event(&self) -> Result<Option<SseEvent>, DualEraHttpEndpointError> {
self.body
.pop_message()
.map(|message| message.map(frame_message))
.map_err(Into::into)
}
}
fn frame_message(
message: fastmcp_transport::http::StreamableHttpRequestResponseMessage,
) -> SseEvent {
let value = match message {
fastmcp_transport::http::StreamableHttpRequestResponseMessage::Notification(
request,
)
| fastmcp_transport::http::StreamableHttpRequestResponseMessage::Request(request) => {
serde_json::to_string(&request)
}
fastmcp_transport::http::StreamableHttpRequestResponseMessage::Response(response) => {
serde_json::to_string(&response)
}
}
.expect("validated JSON-RPC message must serialize for a modern HTTP SSE body");
SseEvent::message(value)
}
pub struct DualEraHttpSession {
handler: Arc<HttpRequestHandler>,
transport: StreamableHttpTransport,
ingress: fastmcp_transport::http::StreamableHttpRequestIngress,
responses: StreamableHttpResponseStream,
closed: bool,
}
impl DualEraHttpSession {
pub fn handle(
&mut self,
cx: &Cx,
request: HttpRequest,
) -> Result<DualEraHttpEndpointResponse, DualEraHttpEndpointError> {
if self.closed {
return Err(DualEraHttpEndpointError::Closed);
}
if request.path != self.handler.config().base_path {
return Ok(DualEraHttpEndpointResponse::Immediate(HttpResponse::new(
HttpStatus::NOT_FOUND,
)));
}
let origin = request.header("origin").map(str::to_owned);
let admission = self.handler.admit_modern_request(&request)?;
let json_rpc = admission.request().clone();
match admission.response_representation() {
HttpResponseRepresentation::Json => {
self.ingress.push_request(cx, json_rpc.clone())?;
let Some(request_id) = json_rpc.id else {
return Ok(DualEraHttpEndpointResponse::Immediate(HttpResponse::new(
HttpStatus::ACCEPTED,
)));
};
Ok(DualEraHttpEndpointResponse::ModernJson(
DualEraHttpJsonResponse {
handler: Arc::clone(&self.handler),
responses: self.responses.clone(),
request_id,
origin,
},
))
}
HttpResponseRepresentation::Sse => {
let body = admission.bind_sse_response_body(&self.responses)?;
self.ingress.push_request(cx, json_rpc)?;
Ok(DualEraHttpEndpointResponse::ModernSse(
DualEraHttpSseResponse {
response: HttpResponse::new(HttpStatus::OK)
.with_header("content-type", "text/event-stream")
.with_header("cache-control", "no-cache")
.with_header("connection", "keep-alive")
.with_body(Vec::new()),
body,
},
))
}
}
}
pub fn recv_modern_request(
&mut self,
cx: &Cx,
) -> Result<JsonRpcRequest, DualEraHttpEndpointError> {
match self.transport.recv(cx)? {
JsonRpcMessage::Request(request) => Ok(request),
JsonRpcMessage::Response(_) => {
Err(DualEraHttpEndpointError::Transport(TransportError::Io(
std::io::Error::other("modern HTTP ingress yielded a response"),
)))
}
}
}
pub fn send_modern_json_response(
&mut self,
cx: &Cx,
response: JsonRpcResponse,
) -> Result<(), DualEraHttpEndpointError> {
self.transport
.send(cx, &JsonRpcMessage::Response(response))?;
Ok(())
}
pub fn send_modern_sse_response(
&mut self,
cx: &Cx,
cancellation: &StreamableHttpRequestCancellation,
response: JsonRpcResponse,
) -> Result<(), DualEraHttpEndpointError> {
self.transport
.send_response_for_request(cx, cancellation, response)?;
Ok(())
}
pub fn send_modern_sse_notification(
&mut self,
cx: &Cx,
cancellation: &StreamableHttpRequestCancellation,
notification: JsonRpcRequest,
) -> Result<(), DualEraHttpEndpointError> {
self.transport
.send_notification_for_request(cx, cancellation, notification)?;
Ok(())
}
pub fn close(&mut self) {
if self.closed {
return;
}
self.closed = true;
self.transport.terminate();
}
}
impl Drop for DualEraHttpSession {
fn drop(&mut self) {
self.close();
}
}
}
use crate::http_admission::{
AdmittedModernPost, HttpAdmissionLimits, HttpEndpointConfig, ModernPostRejection,
ResponseRepresentation, admit_modern_post,
};
use asupersync::bytes::BytesMut;
#[cfg(any(feature = "legacy-2024-11-05", feature = "websocket"))]
use asupersync::channel::mpsc as asupersync_mpsc;
use asupersync::codec::{Decoder, Encoder, Framed};
#[cfg(feature = "legacy-2024-11-05")]
use asupersync::cx::ChildRegionSpec;
use asupersync::http::h1::{
Http1Codec, HttpError as Http1DecodeError, Method as Http1Method, Response as Http1Response,
};
#[cfg(feature = "websocket")]
use asupersync::io::{AsyncRead, AsyncWrite, ReadBuf};
use asupersync::io::{AsyncReadExt, AsyncWriteExt};
use asupersync::net::{TcpListener as AsyncTcpListener, TcpStream as AsyncTcpStream};
use asupersync::stream::StreamExt;
use asupersync::{Budget, CancelKind, Cx, RegionId, channel::oneshot};
use fastmcp_console::RequestResponseRenderer;
use fastmcp_console::banner::StartupBanner;
use fastmcp_console::console::FastMcpConsole;
use fastmcp_console::logging::RichLoggerBuilder;
#[cfg(test)]
use fastmcp_core::block_on;
use fastmcp_core::logging::{debug, error, info, targets};
use fastmcp_core::{
AuthContext, ClientCapabilityInfo, McpContext, McpContextLeaseGuard, McpError, McpErrorCode,
McpLogLevel, McpRequestCancellation, McpResult, ServerCapabilityInfo, SessionState,
Sha256Digest, sha256_bounded,
};
#[cfg(any(feature = "apps", feature = "tasks"))]
use fastmcp_protocol::ExtensionDescriptorRegistry;
use fastmcp_protocol::common_types::OpenMetadata;
use fastmcp_protocol::common_types::{Implementation, LoggingLevel};
#[cfg(feature = "apps")]
use fastmcp_protocol::extensions::McpAppsNegotiationResolver;
#[cfg(feature = "tasks")]
use fastmcp_protocol::extensions::TasksNegotiationResolver;
use fastmcp_protocol::extensions::{
ExtensionLocalEnablement, ExtensionNegotiationError, ExtensionSettingsCompatibilityResolver,
ExtensionSettingsResolution,
};
#[cfg(feature = "legacy-2024-11-05")]
use fastmcp_protocol::methods::decode_legacy_2024_11_05_client_capabilities;
#[cfg(feature = "legacy-2024-11-05")]
use fastmcp_protocol::methods::{
Legacy2024ListChangedCapability, Legacy2024ResourcesCapability, Legacy2024ServerCapabilities,
};
use fastmcp_protocol::methods::{SUBSCRIPTIONS_LISTEN, final_2026_07_28_method};
use fastmcp_protocol::protocol_policy::{
LEGACY_PROTOCOL_VERSION, MODERN_PROTOCOL_VERSION, ModernVersionSupport, ProtocolEra,
ProtocolPolicy, StdioEraClassifier, StdioEraDecision, StdioOpeningFrame,
};
#[cfg(feature = "tasks")]
use fastmcp_protocol::task_subscription_ids;
#[cfg(feature = "tasks")]
use fastmcp_protocol::tasks_extension::{
TaskStatusNotification as FinalTaskStatusNotification, set_task_subscription_ids,
};
use fastmcp_protocol::{
CallToolParams, CancellationSender, CancellationWireMessage, CancelledParams,
ClientCapabilities, ClientExtensionDiscovery, ClientNotification, CoreRequest, CoreResult,
CorrelationKey, DiscoveryCacheHints, ExtensionDescriptor, ExtensionId, ExtensionRegistryError,
ExtensionRegistryReceipt, ExtensionSettings, FINAL_CLIENT_CAPABILITIES_META_KEY,
FINAL_SERVER_INFO_META_KEY, FINAL_SUBSCRIPTION_ID_META_KEY, FinalCancelledNotificationParams,
FinalCoreRequest, FinalCoreResult, FinalLogMessageParams, FinalResultMetadataSeal,
FinalSubscriptionsAcknowledgedNotificationParams, FinalSubscriptionsListenParams,
GetPromptParams, InitializeParams, JsonRpcError, JsonRpcMessage, JsonRpcRequest,
JsonRpcResponse, ListPromptsParams, ListResourceTemplatesParams, ListResourcesParams,
ListToolsParams, LogLevel, LogMessageParams, MAX_SERVER_INSTRUCTIONS_BYTES,
MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE, MissingRequiredClientCapabilityError,
ProgressMarker, Prompt, ReadResourceParams, RequestId, Resource, ResourceTemplate,
SERVER_DISCOVER_METHOD, ServerCapabilities, ServerDiscoverCapabilities, ServerDiscoverRequest,
ServerDiscoverResult, ServerExtensionDiscovery, ServerInfo, ServerInstructions,
ServerNotification, SetLogLevelParams, SubscribeResourceParams, SubscriptionFilter, Tool,
UnsubscribeResourceParams,
};
use fastmcp_protocol::{CompleteResult, FinalSubscriptionsListenResult, ResultMeta};
#[cfg(feature = "legacy-2024-11-05")]
use legacy_2024::{
Legacy2024AdapterError, Legacy2024Handler, Legacy2024HandlerError, Legacy2024Outbound,
Legacy2024ServerAdapter, Legacy2024ServerConfig, Legacy2024ServerInfo, Legacy2024StateSnapshot,
LegacyAuthenticatedPeerPartition, LegacyPeerBinding,
};
const REDACTED_EXTENSION_PANIC_INCIDENT: &[u8] =
b"fastmcp extension callback panicked; panic payload redacted\n";
/// Server-local error code for a bounded resource-subscription admission
/// failure. MCP does not currently assign a standard `ResourceExhausted` code.
const RESOURCE_EXHAUSTED_ERROR_CODE: i32 = -32006;
const RESOURCE_SUBSCRIPTION_CAPACITY_MESSAGE: &str = "Resource subscription capacity exhausted";
const MAX_DISPATCH_QUEUE_DEPTH: usize = 64;
/// Anti-hang watchdog for the test helpers that drive a whole returning
/// transport lifecycle on a blocking thread and wait for its result.
///
/// This is NOT a latency assertion. Nothing about the protocol or the server
/// promises to finish inside it, and no test asserts on it; its only job is to
/// fail loudly instead of hanging the suite when a pump never produces a
/// result. Each per-test assertion on the returned value is unaffected.
///
/// It must therefore be far larger than any legitimate completion, because the
/// bound starts before the pump has been scheduled. `cargo test -p
/// fastmcp-server --lib` discovers 2307 tests and, at default parallelism,
/// leaves hundreds of threads competing for CPU, so a pump can sit runnable
/// while the clock runs. The previous 2-second bound measured that contention
/// rather than the server: it produced a rotating set of ~12 failures across
/// `legacy_application_content_*`, `live_runtime_*` and
/// `public_stdio_legacy_progress_*`, all reporting the same
/// "legacy returning test timed out", while the identical tests passed under
/// `--test-threads=1`. A genuine stall still trips this bound and is still
/// reported as a failure.
#[cfg(test)]
const RETURNING_PUMP_WATCHDOG_NANOS: u64 = 30_000_000_000;
const MAX_DISPATCH_QUEUE_BYTES: usize = 16 * 1024 * 1024;
const DISPATCH_QUEUE_CAPACITY_MESSAGE: &str = "Server request queue capacity exhausted";
const DISCOVERY_CACHE_MAX_AGE_SECONDS: u32 = 60;
const MODERN_ONLY_INITIALIZE_MESSAGE: &str = "Initialization-based MCP is not enabled";
/// Typed refusal for a modern (discovery-era) request on a `LegacyOnly`
/// endpoint. Plan §1.7: a `LegacyOnly` endpoint exposes no modern method, so
/// the JSON-RPC class is `-32601` (method not found), the same admitted
/// legacy-refusal signal the Auto stdio client uses to fall back to the
/// exact 2024-11-05 lifecycle. `-32600` stays reserved for a genuine
/// mid-connection era mismatch on an already negotiated connection.
const LEGACY_ONLY_MODERN_MESSAGE: &str = "Discovery-based MCP is not enabled";
const STDIO_OUTPUT_COMMIT_TIMEOUT: Duration = Duration::from_secs(2);
const DISPATCH_WORKER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
const HTTP_TERMINAL_DRAIN_TIMEOUT: Duration = Duration::from_secs(2);
/// Wake `accept` often enough that a cancelled listener Cx can leave the
/// loop without waiting for the next inbound connection. Two live SSE
/// listens otherwise keep their writers parked until fixture teardown
/// burns `HTTP_TERMINAL_DRAIN_TIMEOUT`.
const HTTP_ACCEPT_CANCEL_POLL: Duration = Duration::from_millis(20);
/// Default bound on receiving one complete HTTP request (head and body). A
/// peer that connects and then sends nothing, or trickles bytes, would
/// otherwise hold one of the listener's `max_connections` slots forever.
const DEFAULT_HTTP_REQUEST_READ_TIMEOUT: Duration = Duration::from_secs(30);
/// Bound the public HTTP lifecycle's cooperative connection drain. A
/// synchronous handler which ignores cancellation cannot be preempted, so a
/// timeout returns a caller-owned handle retaining its still-live child.
const HTTP_CONNECTION_SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(500);
/// Maximum time a modern request may retain one coalesced progress update
/// while its handler is still running.
const FINAL_PROGRESS_FLUSH_INTERVAL: Duration = Duration::from_millis(50);
/// The response commit gate is transiently contended by final progress, logs,
/// and the terminal response. Retry that contention, but never spin a server
/// worker forever when a transport cannot make progress.
const MAX_HTTP_SSE_COMMIT_RETRIES: usize = 64;
static INSTALL_EXTENSION_PANIC_HOOK: Once = Once::new();
#[cfg(test)]
static REDACTED_EXTENSION_PANIC_COUNT: AtomicUsize = AtomicUsize::new(0);
#[cfg(test)]
thread_local! {
/// Test-only fault injection consumed by the public HTTP endpoint path.
/// Keeping this thread-local prevents an intentionally stalled endpoint
/// from perturbing unrelated tests running in parallel.
static FORCED_HTTP_SSE_WOULD_BLOCK_COMMITS: Cell<usize> = const { Cell::new(0) };
}
#[cfg(test)]
fn set_forced_http_sse_would_block_commits(commits: usize) {
FORCED_HTTP_SSE_WOULD_BLOCK_COMMITS.with(|remaining| remaining.set(commits));
}
#[cfg(test)]
fn take_forced_http_sse_would_block_commit() -> bool {
FORCED_HTTP_SSE_WOULD_BLOCK_COMMITS.with(|remaining| {
let commits = remaining.get();
if commits == 0 {
false
} else {
remaining.set(commits - 1);
true
}
})
}
#[cfg(test)]
fn forced_http_sse_would_block_commits_remaining() -> usize {
FORCED_HTTP_SSE_WOULD_BLOCK_COMMITS.with(|remaining| remaining.get())
}
#[cfg(test)]
struct StdioProgressCommitInterlock {
request_id: u64,
state: Mutex<StdioProgressCommitInterlockState>,
changed: Condvar,
}
#[cfg(test)]
#[derive(Default)]
struct StdioProgressCommitInterlockState {
entered: bool,
released: bool,
}
#[cfg(test)]
impl StdioProgressCommitInterlock {
fn new(request_id: u64) -> Self {
Self {
request_id,
state: Mutex::new(StdioProgressCommitInterlockState::default()),
changed: Condvar::new(),
}
}
fn wait_until_entered(&self, timeout: Duration) -> bool {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (state, _) = self
.changed
.wait_timeout_while(state, timeout, |state| !state.entered)
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.entered
}
fn release(&self) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.released = true;
self.changed.notify_all();
}
}
#[cfg(test)]
static STDIO_PROGRESS_COMMIT_INTERLOCK: std::sync::OnceLock<
Mutex<Option<Arc<StdioProgressCommitInterlock>>>,
> = std::sync::OnceLock::new();
#[cfg(test)]
fn install_stdio_progress_commit_interlock(request_id: u64) -> Arc<StdioProgressCommitInterlock> {
let interlock = Arc::new(StdioProgressCommitInterlock::new(request_id));
let mut installed = STDIO_PROGRESS_COMMIT_INTERLOCK
.get_or_init(|| Mutex::new(None))
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(
installed.is_none(),
"only one stdio progress interlock may be active"
);
*installed = Some(Arc::clone(&interlock));
interlock
}
#[cfg(test)]
fn release_stdio_progress_commit_interlock() {
let Some(interlock) = STDIO_PROGRESS_COMMIT_INTERLOCK.get() else {
return;
};
let interlock = interlock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
if let Some(interlock) = interlock {
interlock.release();
}
}
#[cfg(test)]
fn wait_for_stdio_progress_commit_fence(request_id: u64, notification: &JsonRpcRequest) {
if notification.method != "notifications/progress" {
return;
}
let Some(interlock) = STDIO_PROGRESS_COMMIT_INTERLOCK.get().and_then(|installed| {
installed
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.filter(|interlock| interlock.request_id == request_id)
.cloned()
}) else {
return;
};
// A test that stops before releasing the interlock must fail rather than
// leave this progress writer blocked for the rest of the process.
const RELEASE_BOUND: Duration = Duration::from_secs(60);
let mut state = interlock
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.entered = true;
interlock.changed.notify_all();
let (_state, waited) = interlock
.changed
.wait_timeout_while(state, RELEASE_BOUND, |state| !state.released)
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(
!waited.timed_out(),
"stdio progress commit interlock was not released within {RELEASE_BOUND:?}; its test stopped before releasing it"
);
}
/// Failure while installing the server-owned extension registry.
#[derive(Debug)]
pub enum ServerExtensionConfigurationError {
/// A builder accepts exactly one caller-supplied extension registry.
AlreadyInstalled,
/// Final Tasks was configured more than once for the same server.
FinalTasksAlreadyInstalled,
/// Official MCP Apps was configured more than once for the same server.
OfficialMcpAppsAlreadyInstalled,
/// Caller discovery contradicts descriptor-bound server metadata.
ConflictingServerMetadata(String),
/// Discovery attempted to advertise an identifier absent from the registry.
UnregisteredAdvertisedExtension(String),
/// The descriptor registry could not be frozen into a canonical receipt.
Registry(ExtensionRegistryError),
/// The server could not bind a typed final Tasks handler.
Handler(ExtensionHandlerRegistrationError),
/// Builder-time resolver composition found a poisoned resolver lock.
ResolverPoisoned,
}
impl std::fmt::Display for ServerExtensionConfigurationError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AlreadyInstalled => {
formatter.write_str("a server extension registry is already installed")
}
Self::FinalTasksAlreadyInstalled => {
formatter.write_str("the final Tasks extension is already installed")
}
Self::OfficialMcpAppsAlreadyInstalled => {
formatter.write_str("the official MCP Apps extension is already installed")
}
Self::ConflictingServerMetadata(id) => {
write!(
formatter,
"server discovery contradicts registered metadata: {id}"
)
}
Self::UnregisteredAdvertisedExtension(id) => {
write!(
formatter,
"server discovery advertises an unregistered extension: {id}"
)
}
Self::Registry(error) => {
write!(formatter, "server extension registry is invalid: {error}")
}
Self::Handler(error) => {
write!(formatter, "server extension handler is invalid: {error}")
}
Self::ResolverPoisoned => {
formatter.write_str("the server extension resolver is unavailable")
}
}
}
}
impl std::error::Error for ServerExtensionConfigurationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Registry(error) => Some(error),
Self::Handler(error) => Some(error),
Self::AlreadyInstalled
| Self::FinalTasksAlreadyInstalled
| Self::OfficialMcpAppsAlreadyInstalled
| Self::ConflictingServerMetadata(_)
| Self::UnregisteredAdvertisedExtension(_)
| Self::ResolverPoisoned => None,
}
}
}
/// Failure while deriving a current-exchange extension capability set.
#[derive(Debug)]
pub enum ServerExtensionNegotiationError {
/// This server has no extension registry installed.
NotConfigured,
/// The installed resolver lock was poisoned by an earlier caller panic.
ResolverPoisoned,
/// The frozen protocol registry rejected the current peer settings.
Protocol(ExtensionNegotiationError),
}
impl std::fmt::Display for ServerExtensionNegotiationError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotConfigured => formatter.write_str("the server has no extension registry"),
Self::ResolverPoisoned => {
formatter.write_str("the server extension resolver is unavailable")
}
Self::Protocol(error) => {
write!(formatter, "server extension negotiation failed: {error}")
}
}
}
}
impl std::error::Error for ServerExtensionNegotiationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Protocol(error) => Some(error),
Self::NotConfigured | Self::ResolverPoisoned => None,
}
}
}
/// Sized bridge for a caller-provided dynamic extension settings resolver.
struct BoxedExtensionSettingsResolver(Box<dyn ExtensionSettingsCompatibilityResolver + Send>);
impl ExtensionSettingsCompatibilityResolver for BoxedExtensionSettingsResolver {
fn resolve(
&mut self,
descriptor: &ExtensionDescriptor,
client: &ExtensionSettings,
server: &ExtensionSettings,
) -> Result<ExtensionSettings, ExtensionNegotiationError> {
self.0.resolve(descriptor, client, server)
}
fn resolve_with_disposition(
&mut self,
descriptor: &ExtensionDescriptor,
client: &ExtensionSettings,
server: &ExtensionSettings,
) -> Result<ExtensionSettingsResolution, ExtensionNegotiationError> {
self.0.resolve_with_disposition(descriptor, client, server)
}
}
/// Immutable server-owned extension runtime retained after builder freeze.
pub(crate) struct ServerExtensionRuntime {
handlers: ExtensionHandlerRegistry,
local_enablement: ExtensionLocalEnablement,
server_discovery: ServerExtensionDiscovery,
resolver: Mutex<BoxedExtensionSettingsResolver>,
}
impl ServerExtensionRuntime {
pub(crate) fn new<R>(
handlers: ExtensionHandlerRegistry,
mut server_discovery: ServerExtensionDiscovery,
resolver: R,
) -> Result<Self, ServerExtensionConfigurationError>
where
R: ExtensionSettingsCompatibilityResolver + Send + 'static,
{
let registered_discovery = handlers.configured_server_discovery();
for (id, settings) in registered_discovery.extensions {
match server_discovery.extensions.get(&id) {
Some(existing) if existing != &settings => {
return Err(
ServerExtensionConfigurationError::ConflictingServerMetadata(
id.to_string(),
),
);
}
Some(_) => {}
None => {
server_discovery.extensions.insert(id, settings);
}
}
}
for id in server_discovery.extensions.keys() {
if handlers.descriptor_registry().descriptor(id).is_none() {
return Err(
ServerExtensionConfigurationError::UnregisteredAdvertisedExtension(
id.to_string(),
),
);
}
}
// `ServerBuilder::build` has no failure channel. Freeze a clone now so
// its canonical descriptor digest is known-valid before construction.
let mut preflight = handlers.descriptor_registry().clone();
preflight
.freeze()
.map_err(ServerExtensionConfigurationError::Registry)?;
let mut local_enablement = ExtensionLocalEnablement::default();
for id in server_discovery.extensions.keys() {
local_enablement.enable(id.clone());
}
let resolver = BoxedExtensionSettingsResolver(Box::new(resolver));
#[cfg(feature = "apps")]
let resolver = {
let apps_id = fastmcp_protocol::extensions::official_mcp_apps_extension_id();
if server_discovery.extensions.contains_key(&apps_id) {
BoxedExtensionSettingsResolver(Box::new(McpAppsNegotiationResolver::with_fallback(
resolver,
)))
} else {
resolver
}
};
Ok(Self {
handlers,
local_enablement,
server_discovery,
resolver: Mutex::new(resolver),
})
}
#[cfg(feature = "apps")]
pub(crate) fn with_official_mcp_apps() -> Result<Self, ServerExtensionConfigurationError> {
let handlers = ExtensionHandlerRegistry::new(ExtensionDescriptorRegistry::new());
let mut runtime = Self::new(
handlers,
ServerExtensionDiscovery::default(),
|descriptor: &ExtensionDescriptor,
_client: &ExtensionSettings,
_server: &ExtensionSettings|
-> Result<ExtensionSettings, ExtensionNegotiationError> {
Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
descriptor.id.to_string(),
))
},
)?;
runtime.install_official_mcp_apps()?;
Ok(runtime)
}
#[cfg(feature = "tasks")]
pub(crate) fn with_final_tasks(
task_runtime: &FinalTaskRuntime,
) -> Result<Self, ServerExtensionConfigurationError> {
let handlers = ExtensionHandlerRegistry::new(ExtensionDescriptorRegistry::new());
let mut runtime = Self::new(
handlers,
ServerExtensionDiscovery::default(),
|descriptor: &ExtensionDescriptor,
_client: &ExtensionSettings,
_server: &ExtensionSettings|
-> Result<ExtensionSettings, ExtensionNegotiationError> {
Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
descriptor.id.to_string(),
))
},
)?;
runtime.install_final_tasks(task_runtime)?;
Ok(runtime)
}
#[cfg(all(feature = "proxy", feature = "tasks"))]
pub(crate) fn with_proxy_final_tasks(
task_relay: Arc<ProxyFinalTaskRelay>,
) -> Result<Self, ServerExtensionConfigurationError> {
let handlers = ExtensionHandlerRegistry::new(ExtensionDescriptorRegistry::new());
let mut runtime = Self::new(
handlers,
ServerExtensionDiscovery::default(),
|descriptor: &ExtensionDescriptor,
_client: &ExtensionSettings,
_server: &ExtensionSettings|
-> Result<ExtensionSettings, ExtensionNegotiationError> {
Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
descriptor.id.to_string(),
))
},
)?;
runtime.install_proxy_final_tasks(task_relay)?;
Ok(runtime)
}
/// Merges the official final Tasks descriptor, handlers, discovery
/// settings, and resolver branch into this still-mutable registry.
///
/// The caller-supplied registry keeps ownership of every unrelated
/// descriptor and resolver path. A pre-existing official Tasks descriptor
/// is a real duplicate ownership conflict because the final runtime owns
/// all three Tasks request handlers as one atomic surface.
#[cfg(feature = "tasks")]
pub(crate) fn install_final_tasks(
&mut self,
task_runtime: &FinalTaskRuntime,
) -> Result<(), ServerExtensionConfigurationError> {
let tasks_id = fastmcp_protocol::extensions::official_tasks_extension_id();
if self
.handlers
.descriptor_registry()
.descriptor(&tasks_id)
.is_some()
{
return Err(ServerExtensionConfigurationError::FinalTasksAlreadyInstalled);
}
fastmcp_protocol::extensions::register_official_tasks_extension(
self.handlers
.descriptor_registry_mut()
.map_err(ServerExtensionConfigurationError::Handler)?,
)
.map_err(ServerExtensionConfigurationError::Registry)?;
let task_settings = fastmcp_protocol::extensions::official_tasks_empty_settings();
if self
.server_discovery
.extensions
.insert(tasks_id.clone(), task_settings)
.is_some()
{
return Err(ServerExtensionConfigurationError::FinalTasksAlreadyInstalled);
}
self.local_enablement.enable(tasks_id.clone());
let get_runtime = task_runtime.clone();
self.handlers
.register(
tasks_id.clone(),
fastmcp_protocol::tasks_extension::TASK_GET,
move |context: &McpContext, parameters: serde_json::Value| {
tasks::dispatch_final_tasks_get(&get_runtime, context, parameters)
},
)
.map_err(ServerExtensionConfigurationError::Handler)?;
let update_runtime = task_runtime.clone();
self.handlers
.register(
tasks_id.clone(),
fastmcp_protocol::tasks_extension::TASK_UPDATE,
move |context: &McpContext, parameters: serde_json::Value| {
tasks::dispatch_final_tasks_update(&update_runtime, context, parameters)
},
)
.map_err(ServerExtensionConfigurationError::Handler)?;
let cancel_runtime = task_runtime.clone();
self.handlers
.register(
tasks_id.clone(),
fastmcp_protocol::tasks_extension::TASK_CANCEL,
move |context: &McpContext, parameters: serde_json::Value| {
tasks::dispatch_final_tasks_cancel(&cancel_runtime, context, parameters)
},
)
.map_err(ServerExtensionConfigurationError::Handler)?;
let resolver = self
.resolver
.get_mut()
.map_err(|_| ServerExtensionConfigurationError::ResolverPoisoned)?;
let previous = std::mem::replace(
resolver,
BoxedExtensionSettingsResolver(Box::new(
|descriptor: &ExtensionDescriptor,
_client: &ExtensionSettings,
_server: &ExtensionSettings|
-> Result<ExtensionSettings, ExtensionNegotiationError> {
Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
descriptor.id.to_string(),
))
},
)),
);
*resolver = BoxedExtensionSettingsResolver(Box::new(
TasksNegotiationResolver::with_fallback(previous),
));
Ok(())
}
/// Installs the official final Tasks descriptor around one selected
/// upstream relay. The handlers deliberately construct fresh upstream
/// request metadata through that selected client; downstream `_meta` is
/// admission data and is never replayed across a proxy boundary.
#[cfg(all(feature = "proxy", feature = "tasks"))]
pub(crate) fn install_proxy_final_tasks(
&mut self,
task_relay: Arc<ProxyFinalTaskRelay>,
) -> Result<(), ServerExtensionConfigurationError> {
let tasks_id = fastmcp_protocol::extensions::official_tasks_extension_id();
if self
.handlers
.descriptor_registry()
.descriptor(&tasks_id)
.is_some()
{
return Err(ServerExtensionConfigurationError::FinalTasksAlreadyInstalled);
}
fastmcp_protocol::extensions::register_official_tasks_extension(
self.handlers
.descriptor_registry_mut()
.map_err(ServerExtensionConfigurationError::Handler)?,
)
.map_err(ServerExtensionConfigurationError::Registry)?;
let task_settings = fastmcp_protocol::extensions::official_tasks_empty_settings();
if self
.server_discovery
.extensions
.insert(tasks_id.clone(), task_settings)
.is_some()
{
return Err(ServerExtensionConfigurationError::FinalTasksAlreadyInstalled);
}
self.local_enablement.enable(tasks_id.clone());
let get_relay = Arc::clone(&task_relay);
self.handlers
.register_async(
tasks_id.clone(),
fastmcp_protocol::tasks_extension::TASK_GET,
move |context: &McpContext, parameters: serde_json::Value| {
let relay = Arc::clone(&get_relay);
Box::pin(async move { relay.dispatch_get(context, parameters).await })
},
)
.map_err(ServerExtensionConfigurationError::Handler)?;
let update_relay = Arc::clone(&task_relay);
self.handlers
.register_async(
tasks_id.clone(),
fastmcp_protocol::tasks_extension::TASK_UPDATE,
move |context: &McpContext, parameters: serde_json::Value| {
let relay = Arc::clone(&update_relay);
Box::pin(async move { relay.dispatch_update(context, parameters).await })
},
)
.map_err(ServerExtensionConfigurationError::Handler)?;
self.handlers
.register_async(
tasks_id.clone(),
fastmcp_protocol::tasks_extension::TASK_CANCEL,
move |context: &McpContext, parameters: serde_json::Value| {
let relay = Arc::clone(&task_relay);
Box::pin(async move { relay.dispatch_cancel(context, parameters).await })
},
)
.map_err(ServerExtensionConfigurationError::Handler)?;
let resolver = self
.resolver
.get_mut()
.map_err(|_| ServerExtensionConfigurationError::ResolverPoisoned)?;
let previous = std::mem::replace(
resolver,
BoxedExtensionSettingsResolver(Box::new(
|descriptor: &ExtensionDescriptor,
_client: &ExtensionSettings,
_server: &ExtensionSettings|
-> Result<ExtensionSettings, ExtensionNegotiationError> {
Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
descriptor.id.to_string(),
))
},
)),
);
*resolver = BoxedExtensionSettingsResolver(Box::new(
TasksNegotiationResolver::with_fallback(previous),
));
Ok(())
}
/// Merges the official MCP Apps descriptor, its exact empty discovery
/// marker, and its disposition-preserving resolver branch into this
/// still-mutable runtime.
#[cfg(feature = "apps")]
pub(crate) fn install_official_mcp_apps(
&mut self,
) -> Result<(), ServerExtensionConfigurationError> {
let apps_id = fastmcp_protocol::extensions::official_mcp_apps_extension_id();
if self
.handlers
.descriptor_registry()
.descriptor(&apps_id)
.is_some()
|| self.server_discovery.extensions.contains_key(&apps_id)
{
return Err(ServerExtensionConfigurationError::OfficialMcpAppsAlreadyInstalled);
}
self.handlers
.install_official_mcp_apps()
.map_err(ServerExtensionConfigurationError::Handler)?;
self.server_discovery.extensions.insert(
apps_id.clone(),
fastmcp_protocol::extensions::official_mcp_apps_empty_server_settings(),
);
self.local_enablement.enable(apps_id);
let resolver = self
.resolver
.get_mut()
.map_err(|_| ServerExtensionConfigurationError::ResolverPoisoned)?;
let previous = std::mem::replace(
resolver,
BoxedExtensionSettingsResolver(Box::new(
|descriptor: &ExtensionDescriptor,
_client: &ExtensionSettings,
_server: &ExtensionSettings|
-> Result<ExtensionSettings, ExtensionNegotiationError> {
Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
descriptor.id.to_string(),
))
},
)),
);
*resolver = BoxedExtensionSettingsResolver(Box::new(
McpAppsNegotiationResolver::with_fallback(previous),
));
Ok(())
}
pub(crate) fn freeze(&mut self) -> Result<ExtensionRegistryReceipt, ExtensionRegistryError> {
self.handlers.freeze()
}
fn discovery_capabilities(&self) -> BTreeMap<String, serde_json::Value> {
self.server_discovery
.extensions
.iter()
.map(|(id, settings)| {
(
id.as_str().to_owned(),
serde_json::Value::Object(settings.as_object().clone()),
)
})
.collect()
}
fn negotiate(
&self,
client: &ClientExtensionDiscovery,
) -> Result<fastmcp_protocol::extensions::NegotiatedExtensionSet, ServerExtensionNegotiationError>
{
let mut resolver = self
.resolver
.lock()
.map_err(|_| ServerExtensionNegotiationError::ResolverPoisoned)?;
self.handlers
.descriptor_registry()
.negotiate(
ProtocolEra::Modern2026,
&self.local_enablement,
client,
&self.server_discovery,
&mut *resolver,
)
.map_err(ServerExtensionNegotiationError::Protocol)
}
}
/// Request-owned state retained until the exact legacy response reaches its
/// stdio commit boundary.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacy2024ActiveRequest {
cancellation: McpRequestCancellation,
active_guard: ActiveRequestGuard,
budget: Budget,
}
/// Result of a live exact-2024 dispatch together with its still-active
/// cancellation authority.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacy2024Dispatch {
result: McpResult<serde_json::Value>,
active_request: LiveLegacy2024ActiveRequest,
}
/// Shared handler-facing state for one live exact-2024 stdio connection.
///
/// The lifecycle adapter owns exact wire admission, while this runtime owns
/// the server facilities a dispatched handler can use. Keeping these values
/// together prevents an admitted legacy request from receiving a fresh state
/// bag or a disconnected outbound request registry.
#[derive(Clone)]
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacy2024ConnectionRuntime {
session_state: SessionState,
notification_sender: NotificationSender,
request_sender: Option<RequestSender>,
supports_sampling: Arc<AtomicBool>,
supports_roots: Arc<AtomicBool>,
client_info: Arc<Mutex<Option<fastmcp_protocol::ClientInfo>>>,
resource_subscriptions: Arc<Mutex<Vec<String>>>,
log_level: Arc<Mutex<Option<LogLevel>>>,
logging_ceiling: LevelFilter,
}
#[cfg(feature = "legacy-2024-11-05")]
fn map_legacy_log_level(level: &str, ceiling: LevelFilter) -> Option<LogLevel> {
let mapped = match level {
"debug" => LogLevel::Debug,
"info" => LogLevel::Info,
"notice" => LogLevel::Notice,
"warning" => LogLevel::Warning,
"error" => LogLevel::Error,
"critical" => LogLevel::Critical,
"alert" => LogLevel::Alert,
"emergency" => LogLevel::Emergency,
_ => return None,
};
let requested = match mapped {
LogLevel::Debug => LevelFilter::Debug,
LogLevel::Info | LogLevel::Notice => LevelFilter::Info,
LogLevel::Warning => LevelFilter::Warn,
LogLevel::Error | LogLevel::Critical | LogLevel::Alert | LogLevel::Emergency => {
LevelFilter::Error
}
};
if ceiling == LevelFilter::Off {
return None;
}
let effective = requested.min(ceiling);
if effective == requested {
return Some(mapped);
}
Some(match effective {
LevelFilter::Debug => LogLevel::Debug,
LevelFilter::Info => LogLevel::Info,
LevelFilter::Warn => LogLevel::Warning,
LevelFilter::Error | LevelFilter::Trace => LogLevel::Error,
LevelFilter::Off => return None,
})
}
#[cfg(feature = "legacy-2024-11-05")]
impl LiveLegacy2024ConnectionRuntime {
fn new(
session_state: SessionState,
notification_sender: NotificationSender,
request_sender: Option<RequestSender>,
logging_ceiling: LevelFilter,
) -> Self {
Self {
session_state,
notification_sender,
request_sender,
supports_sampling: Arc::new(AtomicBool::new(false)),
supports_roots: Arc::new(AtomicBool::new(false)),
client_info: Arc::new(Mutex::new(None)),
resource_subscriptions: Arc::new(Mutex::new(Vec::new())),
log_level: Arc::new(Mutex::new(None)),
logging_ceiling,
}
}
fn bidirectional_senders(
&self,
server: &Server,
request_cancellation: &McpRequestCancellation,
request_context: &McpContext,
) -> Option<BidirectionalSenders> {
let request_sender = self.request_sender.as_ref()?;
server.create_bidirectional_senders_from_capabilities(
self.supports_sampling.load(Ordering::Acquire),
false,
self.supports_roots.load(Ordering::Acquire),
request_sender,
request_cancellation,
request_context,
)
}
fn log_level(&self) -> Option<LogLevel> {
*self
.log_level
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn sync_from_adapter_snapshot(&self, snapshot: &Legacy2024StateSnapshot) {
let supports_sampling =
serde_json::from_slice::<serde_json::Value>(&snapshot.client_capabilities_bytes)
.ok()
.and_then(|capabilities| {
decode_legacy_2024_11_05_client_capabilities(capabilities).ok()
})
.is_some_and(|capabilities| capabilities.sampling.is_some());
self.supports_sampling
.store(supports_sampling, Ordering::Release);
let supports_roots =
serde_json::from_slice::<serde_json::Value>(&snapshot.client_capabilities_bytes)
.ok()
.and_then(|capabilities| {
decode_legacy_2024_11_05_client_capabilities(capabilities).ok()
})
.is_some_and(|capabilities| capabilities.roots.is_some());
self.supports_roots.store(supports_roots, Ordering::Release);
*self
.resource_subscriptions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = snapshot.subscriptions.clone();
let log_level = snapshot
.logging_level
.as_deref()
.and_then(|level| map_legacy_log_level(level, self.logging_ceiling));
*self
.log_level
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = log_level;
}
}
/// Bridges an admitted exact-2024 operation to the server's legacy result
/// surface while retaining the peer's original request identity.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacy2024RuntimeHandler<'a> {
server: &'a Server,
cx: Cx,
session_id: u64,
session_principal: SessionPrincipalBinding,
runtime: LiveLegacy2024ConnectionRuntime,
queue_state: Option<Arc<DispatchQueueState>>,
active_request: Arc<Mutex<Option<LiveLegacy2024ActiveRequest>>>,
connection_auth: Option<AuthDispatchCustody>,
}
/// The live exact-2024 runtime handlers are driven only through
/// `Legacy2024ServerAdapter::receive_async`. Their synchronous hook would have
/// to block on the request future, so it refuses instead of dispatching.
#[cfg(feature = "legacy-2024-11-05")]
const LIVE_LEGACY_SYNC_DISPATCH_REFUSED: &str =
"live exact-2024 runtime handlers dispatch only through receive_async";
#[cfg(feature = "legacy-2024-11-05")]
fn live_legacy_sync_dispatch_refused() -> Legacy2024HandlerError {
Legacy2024HandlerError::new(LIVE_LEGACY_SYNC_DISPATCH_REFUSED)
}
#[cfg(feature = "legacy-2024-11-05")]
fn combine_legacy_dispatch_and_close<T>(
dispatch: Result<T, Legacy2024HandlerError>,
close: Result<(), Legacy2024HandlerError>,
) -> Result<T, Legacy2024HandlerError> {
match (dispatch, close) {
(Ok(value), Ok(())) => Ok(value),
(Err(dispatch_error), Ok(())) => Err(dispatch_error),
(Ok(_), Err(close_error)) => Err(close_error),
(Err(dispatch_error), Err(close_error)) => Err(Legacy2024HandlerError::with_code(
dispatch_error.code().clone(),
format!("{}; {}", dispatch_error.message(), close_error.message()),
)),
}
}
#[cfg(feature = "legacy-2024-11-05")]
impl Legacy2024Handler for LiveLegacy2024RuntimeHandler<'_> {
fn handle_legacy_2024(
&mut self,
method: &'static str,
params: Option<&serde_json::Value>,
) -> Result<serde_json::Value, Legacy2024HandlerError> {
self.handle_legacy_2024_with_request_id(&serde_json::json!(0), method, params)
}
fn handle_legacy_2024_with_request_id(
&mut self,
_request_id: &serde_json::Value,
_method: &'static str,
_params: Option<&serde_json::Value>,
) -> Result<serde_json::Value, Legacy2024HandlerError> {
Err(live_legacy_sync_dispatch_refused())
}
fn handle_legacy_2024_with_request_id_async<'a>(
&'a mut self,
request_id: &'a serde_json::Value,
method: &'static str,
params: Option<&'a serde_json::Value>,
) -> BoxFuture<'a, Result<serde_json::Value, Legacy2024HandlerError>> {
Box::pin(async move {
let request_id =
serde_json::from_value::<RequestId>(request_id.clone()).map_err(|_| {
Legacy2024HandlerError::new("legacy adapter supplied an invalid request ID")
})?;
let request_cancellation = self
.queue_state
.as_ref()
.and_then(|queue| queue.admitted_request_cancellation(&request_id));
let request = JsonRpcRequest::new(method, params.cloned(), request_id.clone());
// Exact-2024 custom transports are intentionally serial at the
// wire boundary, but each admitted request still needs its own
// structured cancellation owner. Without this child region the
// active-request registry records the caller/pump region for
// every request, so request shutdown and sibling reuse share one
// ownership domain.
let request_budget = self.server.create_request_budget(&self.cx);
let request_region = self
.cx
.open_child_region(ChildRegionSpec::inherit().with_budget(request_budget))
.await
.map_err(|_| {
Legacy2024HandlerError::new("legacy request region could not be opened")
})?;
let request_cx = request_region.cx().clone();
let dispatch = self
.server
.dispatch_legacy_2024(
&request_cx,
self.session_id,
&self.session_principal,
Some(&self.runtime),
request_cancellation,
&request,
None,
self.connection_auth.as_ref(),
)
.await
.map_err(|error| {
legacy_handler_error_from_mcp(error, self.server.mask_error_details)
});
let close_result = request_region
.close()
.await
.map_err(|_| Legacy2024HandlerError::new("legacy request region close failed"));
let dispatch = combine_legacy_dispatch_and_close(dispatch, close_result)?;
let LiveLegacy2024Dispatch {
result,
active_request,
} = dispatch;
let mut retained = self
.active_request
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if retained.is_some() {
return Err(Legacy2024HandlerError::new(
"legacy runtime attempted to overlap exact request finalization",
));
}
*retained = Some(active_request);
drop(retained);
result.map_err(|error| {
legacy_handler_error_from_mcp(error, self.server.mask_error_details)
})
})
}
}
/// Owns a server reference for an exact-2024 HTTP session. The session updates
/// the request context before every adapter call, while the adapter retains the
/// lifecycle and original wire identity across independent HTTP requests.
#[cfg(feature = "legacy-2024-11-05")]
struct HttpLegacy2024RuntimeHandler {
server: Arc<Server>,
session_id: u64,
session_principal: SessionPrincipalBinding,
active_request: Arc<Mutex<Option<LiveLegacy2024ActiveRequest>>>,
request_cx: Arc<Mutex<Cx>>,
runtime: LiveLegacy2024ConnectionRuntime,
/// The live HTTP connection admits a correlated request before waiting
/// for the session mutex. Reusing that authority here closes the gap
/// between a separate cancellation POST and active-request registration.
legacy_admissions: Arc<HttpLegacyRequestAdmissions>,
/// Native HTTP admission receipt for the in-flight exact-2024 POST.
auth_receipt: Arc<Mutex<Option<AuthAdmissionReceipt>>>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl Legacy2024Handler for HttpLegacy2024RuntimeHandler {
fn handle_legacy_2024(
&mut self,
method: &'static str,
params: Option<&serde_json::Value>,
) -> Result<serde_json::Value, Legacy2024HandlerError> {
self.handle_legacy_2024_with_request_id(&serde_json::json!(0), method, params)
}
fn handle_legacy_2024_with_request_id(
&mut self,
_request_id: &serde_json::Value,
_method: &'static str,
_params: Option<&serde_json::Value>,
) -> Result<serde_json::Value, Legacy2024HandlerError> {
Err(live_legacy_sync_dispatch_refused())
}
fn handle_legacy_2024_with_request_id_async<'a>(
&'a mut self,
request_id: &'a serde_json::Value,
method: &'static str,
params: Option<&'a serde_json::Value>,
) -> BoxFuture<'a, Result<serde_json::Value, Legacy2024HandlerError>> {
Box::pin(async move {
let request_id =
serde_json::from_value::<RequestId>(request_id.clone()).map_err(|_| {
Legacy2024HandlerError::new("legacy adapter supplied an invalid request ID")
})?;
let request = JsonRpcRequest::new(method, params.cloned(), request_id.clone());
let request_cx = self
.request_cx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let request_cancellation = self
.legacy_admissions
.admitted_request_cancellation(&request_id);
let auth_receipt = self
.auth_receipt
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
let dispatch = self
.server
.dispatch_legacy_2024(
&request_cx,
self.session_id,
&self.session_principal,
Some(&self.runtime),
request_cancellation,
&request,
auth_receipt.as_ref(),
None,
)
.await
.map_err(|error| {
legacy_handler_error_from_mcp(error, self.server.mask_error_details)
})?;
let LiveLegacy2024Dispatch {
result,
active_request,
} = dispatch;
let mut retained = self
.active_request
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if retained.is_some() {
return Err(Legacy2024HandlerError::new(
"legacy runtime attempted to overlap exact request finalization",
));
}
*retained = Some(active_request);
drop(retained);
result.map_err(|error| {
legacy_handler_error_from_mcp(error, self.server.mask_error_details)
})
})
}
}
fn final_client_extension_discovery(
request: &JsonRpcRequest,
) -> McpResult<ClientExtensionDiscovery> {
let Some(params) = request.params.as_ref() else {
return Ok(ClientExtensionDiscovery::default());
};
let Some(parameters) = params.as_object() else {
return Err(McpError::invalid_params(
"Invalid final client extension capabilities",
));
};
let Some(metadata) = parameters.get("_meta") else {
return Ok(ClientExtensionDiscovery::default());
};
let Some(metadata) = metadata.as_object() else {
return Err(McpError::invalid_params(
"Invalid final client extension capabilities",
));
};
let Some(capabilities) = metadata.get(FINAL_CLIENT_CAPABILITIES_META_KEY) else {
return Ok(ClientExtensionDiscovery::default());
};
let Some(capabilities) = capabilities.as_object() else {
return Err(McpError::invalid_params(
"Invalid final client extension capabilities",
));
};
let Some(extensions) = capabilities.get("extensions") else {
return Ok(ClientExtensionDiscovery::default());
};
let Some(extensions) = extensions.as_object() else {
return Err(McpError::invalid_params(
"Invalid final client extension capabilities",
));
};
let mut parsed = BTreeMap::new();
for (raw_id, raw_settings) in extensions {
let id = ExtensionId::parse(raw_id.clone())
.map_err(|_| McpError::invalid_params("Invalid final client extension capabilities"))?;
let settings = ExtensionSettings::new(raw_settings.clone())
.map_err(|_| McpError::invalid_params("Invalid final client extension capabilities"))?;
parsed.insert(id, settings);
}
Ok(ClientExtensionDiscovery { extensions: parsed })
}
fn require_exact_modern_extension_metadata(request: &JsonRpcRequest) -> McpResult<()> {
if modern_protocol_version(request) == Some(MODERN_PROTOCOL_VERSION) {
Ok(())
} else {
Err(McpError::invalid_request(
"Extension requests require the admitted MCP 2026-07-28 protocol version",
))
}
}
#[derive(Default)]
struct DispatchRequestByteCounter {
bytes: usize,
}
impl Write for DispatchRequestByteCounter {
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
let prospective = self
.bytes
.checked_add(buffer.len())
.ok_or_else(|| std::io::Error::other(DISPATCH_QUEUE_CAPACITY_MESSAGE))?;
if prospective > MAX_DISPATCH_QUEUE_BYTES {
return Err(std::io::Error::other(DISPATCH_QUEUE_CAPACITY_MESSAGE));
}
self.bytes = prospective;
Ok(buffer.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn measure_dispatch_request(request: &JsonRpcRequest) -> Option<usize> {
let mut counter = DispatchRequestByteCounter::default();
serde_json::to_writer(&mut counter, request)
.ok()
.map(|()| counter.bytes)
}
const MODERN_PROTOCOL_VERSION_METADATA_KEY: &str = "io.modelcontextprotocol/protocolVersion";
fn modern_protocol_version(request: &JsonRpcRequest) -> Option<&str> {
request
.params
.as_ref()?
.get("_meta")?
.get(MODERN_PROTOCOL_VERSION_METADATA_KEY)?
.as_str()
}
/// Decodes an active final core request before middleware can short-circuit
/// normal dispatch. Unknown methods remain eligible for extension fallback.
fn decode_final_core_request_for_middleware(
request: &JsonRpcRequest,
) -> McpResult<Option<CoreRequest>> {
if request.id.is_none() || final_2026_07_28_method(&request.method).is_none() {
return Ok(None);
}
CoreRequest::decode(
ProtocolEra::Modern2026,
&request.method,
request.params.as_ref(),
)
.map(Some)
.map_err(|error| McpError::invalid_params(error.to_string()))
}
/// Admits the sole client-originated final notification before an ID-less
/// registered final frame can enter authentication or application dispatch.
///
/// Final registry membership is intentionally checked first: extension
/// notifications remain eligible for their registered extension path, while a
/// final request missing its id and every server-originated final notification
/// is rejected by the typed client union.
fn admit_final_client_notification_ingress(request: &JsonRpcRequest) -> McpResult<()> {
if request.id.is_some() || final_2026_07_28_method(&request.method).is_none() {
return Ok(());
}
ClientNotification::decode(request)
.map(|_| ())
.map_err(|error| McpError::invalid_request(error.to_string()))
}
/// Rejects a final client-forbidden notification from raw native-HTTP input
/// before strict POST admission rejects its missing request id. That keeps
/// HTTP on the same typed direction authority as stateful transports without
/// treating an untyped malformed document as a notification.
fn rejects_final_client_notification_http_ingress(body: &[u8], document_byte_limit: usize) -> bool {
JsonRpcRequest::decode_strict_with_raw_params(body, document_byte_limit)
.is_ok_and(|(request, _)| admit_final_client_notification_ingress(&request).is_err())
}
/// Projects admitted final client capabilities into the handler context.
///
/// Final handlers use the context capability view to decide whether an
/// embedded MRTR input may be emitted. This is intentionally independent of
/// legacy reverse-request senders: final elicitation is carried by the
/// router-owned `input_required` registry, not by a server-to-client request.
fn admitted_final_client_capability_info(
request: Option<&CoreRequest>,
) -> McpResult<Option<ClientCapabilityInfo>> {
let Some(CoreRequest::Final(request)) = request else {
return Ok(None);
};
let metadata = match request {
FinalCoreRequest::Discover(params) => ¶ms.meta,
FinalCoreRequest::Completion(params) => ¶ms.meta,
FinalCoreRequest::ToolsList(params)
| FinalCoreRequest::ResourcesList(params)
| FinalCoreRequest::ResourceTemplatesList(params)
| FinalCoreRequest::PromptsList(params) => ¶ms.meta,
FinalCoreRequest::ToolsCall(params) => ¶ms.meta,
FinalCoreRequest::ResourcesRead(params) => ¶ms.meta,
FinalCoreRequest::PromptsGet(params) => ¶ms.meta,
FinalCoreRequest::SubscriptionsListen(params) => ¶ms.meta,
};
let capability_members = metadata
.client_capabilities()
.map_err(|error| McpError::invalid_params(error.to_string()))?
.ok_or_else(|| McpError::invalid_params("final client capabilities are required"))?
.clone();
#[cfg(feature = "tasks")]
let supports_tasks = capability_members
.get("extensions")
.and_then(serde_json::Value::as_object)
.and_then(|extensions| extensions.get(fastmcp_protocol::TASKS_EXTENSION))
.and_then(serde_json::Value::as_object)
.is_some_and(serde_json::Map::is_empty);
let capability_value = serde_json::Value::Object(capability_members);
let capabilities: ClientCapabilities = serde_json::from_value(capability_value)
.map_err(|error| McpError::invalid_params(error.to_string()))?;
let (elicitation_form, elicitation_url) = capabilities
.elicitation
.as_ref()
.map_or((false, false), |elicitation| {
(elicitation.supports_form(), elicitation.supports_url())
});
let capability_info =
ClientCapabilityInfo::new().with_elicitation(elicitation_form, elicitation_url);
let capability_info = if capabilities.sampling.is_some() {
capability_info.with_sampling()
} else {
capability_info
};
let capability_info = match capabilities.roots.as_ref() {
Some(roots) => capability_info.with_roots(roots.list_changed),
None => capability_info,
};
#[cfg(feature = "tasks")]
let capability_info = if supports_tasks {
capability_info.with_tasks()
} else {
capability_info
};
Ok(Some(capability_info))
}
fn admitted_final_client_implementation(
request: Option<&CoreRequest>,
) -> McpResult<Option<fastmcp_core::ClientImplementationInfo>> {
let Some(CoreRequest::Final(request)) = request else {
return Ok(None);
};
let metadata = match request {
FinalCoreRequest::Discover(params) => ¶ms.meta,
FinalCoreRequest::Completion(params) => ¶ms.meta,
FinalCoreRequest::ToolsList(params)
| FinalCoreRequest::ResourcesList(params)
| FinalCoreRequest::ResourceTemplatesList(params)
| FinalCoreRequest::PromptsList(params) => ¶ms.meta,
FinalCoreRequest::ToolsCall(params) => ¶ms.meta,
FinalCoreRequest::ResourcesRead(params) => ¶ms.meta,
FinalCoreRequest::PromptsGet(params) => ¶ms.meta,
FinalCoreRequest::SubscriptionsListen(params) => ¶ms.meta,
};
let Some(implementation) = metadata
.client_info()
.map_err(|error| McpError::invalid_params(error.to_string()))?
else {
return Ok(None);
};
Ok(Some(fastmcp_core::ClientImplementationInfo {
name: implementation.name,
version: implementation.version,
title: implementation.title,
description: implementation.description,
website_url: implementation
.website_url
.map(|uri| uri.as_str().to_owned()),
icon_sources: implementation
.icons
.iter()
.map(|icon| icon.src.as_str().to_owned())
.collect(),
}))
}
/// A raw parameter sidecar is usable only while authentication and admission
/// have left the materialized parameter value untouched. In particular,
/// stripping a recognized credential must sever the raw source before router
/// MRTR decoding can observe it. The sidecar was admitted as the source of
/// `admitted`, so comparing that value with the current one replaces decoding
/// the source again; final core decoding still verifies the source against
/// the value it receives.
fn retained_raw_params<'a>(
raw_params: Option<&'a str>,
admitted: Option<&serde_json::Value>,
current: Option<&serde_json::Value>,
) -> Option<&'a str> {
raw_params.filter(|_| admitted == current)
}
/// Re-admits a middleware-produced final core response through the request's
/// exact result algebra and returns its canonical wire representation.
/// A response-cache hit may complete a final core method. Arbitrary
/// middleware still cannot invent a typed final result. Only the sealed
/// continuation replay journal may also answer with the router-minted
/// `input_required` reply it captured, and only with a nonempty requestState.
fn middleware_may_complete_final_core_from_cache(
ctx: &McpContext,
middleware: &dyn crate::Middleware,
value: &serde_json::Value,
) -> bool {
if !ctx.response_was_served_from_cache() {
return false;
}
match value.get("resultType").and_then(serde_json::Value::as_str) {
Some("complete") => true,
Some("input_required") => {
middleware.replays_router_minted_continuation(crate::middleware::seal::Sealed)
&& value
.get("requestState")
.and_then(serde_json::Value::as_str)
.is_some_and(|state| !state.is_empty())
}
_ => false,
}
}
fn validate_final_core_middleware_response(
core_request: Option<&CoreRequest>,
request: &JsonRpcRequest,
value: serde_json::Value,
) -> McpResult<serde_json::Value> {
let Some(core_request) = core_request else {
return Ok(value);
};
if core_request.method() != SERVER_DISCOVER_METHOD && value.get("resultType").is_none() {
return Err(McpError::internal_error(
"middleware response omits the required final result discriminator",
));
}
let response_id = request.id.clone().ok_or_else(|| {
McpError::internal_error("final core middleware response is missing a request id")
})?;
let response = JsonRpcResponse::success(response_id, value);
let result = core_request.decode_response(&response).map_err(|_| {
McpError::internal_error("middleware response violates the final result contract")
})?;
let encoded = result.encode().map_err(|_| {
McpError::internal_error("middleware response violates the final result contract")
})?;
serde_json::from_str(&encoded).map_err(McpError::from)
}
/// Captures server-owned final metadata before response middleware receives a
/// core response. Unknown methods deliberately have no core seal.
fn final_core_middleware_metadata_seal(
core_request: Option<&CoreRequest>,
request: &JsonRpcRequest,
value: &serde_json::Value,
) -> McpResult<Option<FinalResultMetadataSeal>> {
let Some(core_request) = core_request else {
return Ok(None);
};
let response_id = request.id.clone().ok_or_else(|| {
McpError::internal_error("final core middleware response is missing a request id")
})?;
let response = JsonRpcResponse::success(response_id, value.clone());
let CoreResult::Final(result) = core_request.decode_response(&response).map_err(|_| {
McpError::internal_error("middleware response violates the final result contract")
})?
else {
return Err(McpError::internal_error(
"final core middleware response selected a non-final result",
));
};
result
.protected_metadata_seal()
.map(Some)
.map_err(|_| McpError::internal_error("middleware response violates final metadata"))
}
/// Rejects middleware changes to server-owned final response metadata while
/// leaving every open metadata entry available for ordinary middleware use.
fn validate_final_core_middleware_metadata_seal(
expected: Option<&FinalResultMetadataSeal>,
core_request: Option<&CoreRequest>,
request: &JsonRpcRequest,
value: &serde_json::Value,
) -> McpResult<()> {
let Some(expected) = expected else {
return Ok(());
};
let actual = final_core_middleware_metadata_seal(core_request, request, value)?
.ok_or_else(|| McpError::internal_error("final core middleware metadata seal is absent"))?;
if &actual != expected {
return Err(McpError::internal_error(
"middleware changed protected final response metadata",
));
}
Ok(())
}
fn stdio_opening_frame(request: &JsonRpcRequest) -> StdioOpeningFrame {
if let Some(protocol_version) = modern_protocol_version(request) {
return if request.method == "initialize" {
StdioOpeningFrame::MixedInitializeAndModernMetadata {
protocol_version: protocol_version.to_owned(),
}
} else {
StdioOpeningFrame::ModernRequest {
protocol_version: protocol_version.to_owned(),
}
};
}
if request.id.is_none() {
return StdioOpeningFrame::Notification;
}
// The proposed version is negotiated by initialize, not an era marker.
// Modern metadata was checked above; malformed params belong to dispatch.
if request.method == "initialize" {
StdioOpeningFrame::LegacyInitialize
} else {
StdioOpeningFrame::RequestWithoutModernMetadata
}
}
/// Classifies the first complete stdio envelope before any envelope-specific
/// side effect can run. Auto selection is one-shot: a response, an invalid
/// request, or a notification without final metadata is a terminal opening
/// rejection rather than a frame the pump may ignore and retry after.
fn classify_initial_stdio_envelope(
classifier: &mut StdioEraClassifier,
message: &JsonRpcMessage,
) -> Result<ProtocolEra, StdioEraDecision> {
let opening = match message {
JsonRpcMessage::Response(_) => StdioOpeningFrame::Response,
JsonRpcMessage::Request(request) if request.validate().is_err() => {
StdioOpeningFrame::Malformed
}
JsonRpcMessage::Request(request) => stdio_opening_frame(request),
};
let decision = classifier.classify_opening(opening);
#[cfg(not(feature = "legacy-2024-11-05"))]
if matches!(
&decision,
StdioEraDecision::Selected {
era: ProtocolEra::Legacy2024,
..
}
) {
// No-legacy profiles must reject at the envelope boundary, before an
// adapter, session, request authority, or handler can be installed.
return Err(decision);
}
match decision {
StdioEraDecision::Selected {
era: ProtocolEra::Modern2026,
modern_version: Some(ModernVersionSupport::Supported),
} => Ok(ProtocolEra::Modern2026),
StdioEraDecision::Selected {
era: ProtocolEra::Legacy2024,
modern_version: None,
} => Ok(ProtocolEra::Legacy2024),
decision => Err(decision),
}
}
/// Preserves the configured runtime classifier policy.
///
/// A no-legacy build rejects an exact legacy selection at the envelope
/// boundary in [`classify_initial_stdio_envelope`]. It must not silently turn
/// `LegacyOnly` into `ModernOnly`: the former is an unavailable configuration,
/// while the latter is an explicit final-era admission policy.
fn runtime_stdio_policy(policy: ProtocolPolicy) -> ProtocolPolicy {
policy
}
/// Rejects a malformed receive failure as an Auto connection's terminal
/// opening frame. Later malformed frames retain ordinary JSON-RPC recovery
/// behavior because their era was already selected.
#[cfg(feature = "legacy-2024-11-05")]
fn reject_initial_stdio_malformed(classifier: &mut StdioEraClassifier) -> StdioEraDecision {
classifier.classify_opening(StdioOpeningFrame::Malformed)
}
#[cfg(feature = "legacy-2024-11-05")]
fn runtime_legacy_binding(generation: u64) -> LegacyPeerBinding {
let mut partition = [0_u8; LegacyAuthenticatedPeerPartition::BYTE_LEN];
partition[..8].copy_from_slice(&generation.to_be_bytes());
partition[8..16].copy_from_slice(&(!generation).to_be_bytes());
LegacyPeerBinding::from_authenticated_transport(
LegacyAuthenticatedPeerPartition::from_authenticated_transport(partition),
generation,
)
}
/// Polls `future` on `cx` instead of the process-global `block_on` runtime.
///
/// Stdio workers and sequential pumps already own a request/connection `Cx`.
/// Driving the handler future here keeps cancellation, budget, and nested
/// `ctx.call_tool` / `ctx.read_resource` on that same context. A 1ms park
/// timeout is the same bound the multiplexed worker already uses for an
/// empty dispatch queue, so a missed wake cannot deadlock the pump.
fn poll_on_cx<F: Future>(cx: &Cx, future: F) -> F::Output {
use std::task::{Context as TaskContext, Poll, Wake, Waker};
struct ThreadWake(std::thread::Thread);
impl Wake for ThreadWake {
fn wake(self: Arc<Self>) {
self.0.unpark();
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.unpark();
}
}
let _guard = Cx::set_current(Some(cx.clone()));
let waker = Waker::from(Arc::new(ThreadWake(std::thread::current())));
let mut task_cx = TaskContext::from_waker(&waker);
let mut future = std::pin::pin!(future);
loop {
if let Poll::Ready(output) = future.as_mut().poll(&mut task_cx) {
return output;
}
if cx.is_cancel_requested()
&& let Poll::Ready(output) = future.as_mut().poll(&mut task_cx)
{
return output;
}
std::thread::park_timeout(Duration::from_millis(1));
}
}
/// Establishes or verifies the connection owner at an ordinary request's
/// admission boundary.
///
/// The live stdio/WebSocket modern path and the exact-2024 adapter both commit
/// anonymous request authentication internally. Binding that same principal
/// before queue hand-off lets a following control notification prove ownership
/// even when it races the first request's dispatch. Cancellation itself still
/// uses `verify_existing`, so a control frame can never claim an unbound
/// connection.
#[cfg(feature = "legacy-2024-11-05")]
fn bind_anonymous_connection_principal(
principal_binding: &SessionPrincipalBinding,
) -> McpResult<()> {
bind_connection_principal(principal_binding, auth::principal_fingerprint(None)?)
}
#[cfg(feature = "legacy-2024-11-05")]
fn bind_connection_principal(
principal_binding: &SessionPrincipalBinding,
fingerprint: Sha256Digest,
) -> McpResult<()> {
if principal_binding.bind_or_verify(fingerprint) {
Ok(())
} else {
Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authenticated principal does not own this session",
))
}
}
#[cfg(feature = "legacy-2024-11-05")]
async fn legacy_adapter_response_async<H: Legacy2024Handler>(
adapter: &mut Legacy2024ServerAdapter<H>,
binding: LegacyPeerBinding,
request: &JsonRpcRequest,
) -> Result<Option<JsonRpcResponse>, Legacy2024AdapterError> {
let wire = serde_json::to_value(request)
.expect("a validated JSON-RPC request must serialize for the legacy adapter");
match adapter.receive_async(binding, wire).await? {
Legacy2024Outbound::Response(response) => {
let response = serde_json::from_value(response)
.expect("the legacy adapter must emit a valid JSON-RPC response");
Ok(Some(response))
}
Legacy2024Outbound::NoResponse => Ok(None),
Legacy2024Outbound::ReverseRequest(_) | Legacy2024Outbound::ReverseNotification(_) => {
unreachable!("receiving a client frame cannot create a legacy reverse frame")
}
}
}
#[cfg(feature = "legacy-2024-11-05")]
fn sync_live_legacy_runtime_from_adapter<H: Legacy2024Handler>(
runtime: &LiveLegacy2024ConnectionRuntime,
adapter: &Legacy2024ServerAdapter<H>,
) {
runtime.sync_from_adapter_snapshot(&adapter.snapshot());
*runtime
.client_info
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = adapter.client_info().cloned();
}
/// Applies an exact legacy client response to the adapter that allocated its
/// reverse-request ID. A response cannot produce peer output, so any other
/// adapter result is a protocol failure.
#[cfg(feature = "legacy-2024-11-05")]
fn legacy_adapter_accept_response<H: Legacy2024Handler>(
adapter: &mut Legacy2024ServerAdapter<H>,
binding: LegacyPeerBinding,
response: &JsonRpcResponse,
) -> bool {
let wire = match serde_json::to_value(response) {
Ok(wire) => wire,
Err(_) => return false,
};
matches!(
adapter.receive(binding, wire),
Ok(Legacy2024Outbound::NoResponse)
)
}
#[cfg(feature = "legacy-2024-11-05")]
fn take_live_legacy_active_request(
active_request: &Arc<Mutex<Option<LiveLegacy2024ActiveRequest>>>,
) -> Option<LiveLegacy2024ActiveRequest> {
active_request
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
}
#[cfg(feature = "legacy-2024-11-05")]
fn legacy_handled_response(
response: JsonRpcResponse,
active_request: Option<LiveLegacy2024ActiveRequest>,
cx: &Cx,
) -> HandledRequest {
match active_request {
Some(LiveLegacy2024ActiveRequest {
cancellation,
active_guard,
budget,
}) => HandledRequest::tracked(
response,
cancellation,
Some(active_guard),
None,
cx.clone(),
budget,
)
.suppress_cancelled_response(),
None => HandledRequest::untracked(response),
}
}
fn remove_modern_protocol_metadata(request: &mut JsonRpcRequest) {
let Some(params) = request.params.as_mut() else {
return;
};
let Some(params) = params.as_object_mut() else {
return;
};
let remove_meta = params
.get_mut("_meta")
.and_then(serde_json::Value::as_object_mut)
.is_some_and(|metadata| {
metadata.remove(MODERN_PROTOCOL_VERSION_METADATA_KEY);
metadata.is_empty()
});
if remove_meta {
params.remove("_meta");
}
}
fn protocol_era_refusal(request: &JsonRpcRequest) -> Option<JsonRpcResponse> {
request.id.clone().map(|id| {
JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: McpErrorCode::InvalidRequest.into(),
message: "Request does not match the connection's negotiated MCP protocol era"
.to_owned(),
data: Some(serde_json::json!({
"supported": [MODERN_PROTOCOL_VERSION, LEGACY_PROTOCOL_VERSION],
})),
},
)
})
}
/// Methods MCP 2026-07-28 removed. On a modern connection they are unknown
/// methods (`-32601`), never era-selection signals.
fn is_removed_legacy_method(method: &str) -> bool {
matches!(
method,
"initialize"
| "ping"
| "logging/setLevel"
| "resources/subscribe"
| "resources/unsubscribe"
)
}
/// Stateless per-request refusal for a request on a modern stdio connection
/// that does not carry exact MCP 2026-07-28 metadata.
///
/// Every modern request is admitted on its own `_meta` (SEP-2575), so one
/// invalid request is answered and the connection keeps serving: an
/// unsupported version is `-32022` with the supported list, a removed method
/// is `-32601`, and a request missing the required protocol version is
/// `-32602`. Notifications receive no response.
fn modern_request_version_refusal(request: &JsonRpcRequest) -> Option<JsonRpcResponse> {
let id = request.id.clone()?;
let error = match modern_protocol_version(request) {
Some(requested) => JsonRpcError {
code: fastmcp_protocol::UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE.into(),
message: "Unsupported MCP protocol version".to_owned(),
data: Some(serde_json::json!({
"supported": fastmcp_protocol::SUPPORTED_FINAL_PROTOCOL_VERSIONS,
"requested": requested,
})),
},
None if is_removed_legacy_method(&request.method) => JsonRpcError {
code: McpErrorCode::MethodNotFound.into(),
message: "Method not found".to_owned(),
data: None,
},
None => JsonRpcError {
code: McpErrorCode::InvalidParams.into(),
message: format!(
"Invalid params: request _meta must carry {MODERN_PROTOCOL_VERSION_METADATA_KEY}"
),
data: None,
},
};
Some(JsonRpcResponse::error(Some(id), error))
}
/// Whether an opening-frame rejection left the connection in the modern era.
///
/// `ModernOnly` fixes the era before the first frame, and `Auto` selects it
/// from a modern request even when that request names an unsupported
/// version. In both cases the rejected frame is answered statelessly instead
/// of ending the connection.
fn opening_rejection_selected_modern(decision: &StdioEraDecision) -> bool {
matches!(
decision,
StdioEraDecision::Selected {
era: ProtocolEra::Modern2026,
modern_version: Some(ModernVersionSupport::Unsupported { .. }),
} | StdioEraDecision::RejectedUnderSelectedEra {
era: ProtocolEra::Modern2026,
..
}
)
}
/// `-32601` refusal for a modern request that reaches a `LegacyOnly` policy.
fn legacy_only_modern_refusal(request: &JsonRpcRequest) -> Option<JsonRpcResponse> {
request.id.clone().map(|id| {
JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: (-32601).into(),
message: LEGACY_ONLY_MODERN_MESSAGE.to_owned(),
data: Some(serde_json::json!({ "supported": [LEGACY_PROTOCOL_VERSION] })),
},
)
})
}
/// Selects the refusal shape for a request that failed era admission:
/// a modern request under `LegacyOnly` is a method-not-found style typed
/// refusal, every other mismatch is the negotiated-era `-32600` refusal.
#[cfg(feature = "legacy-2024-11-05")]
fn era_admission_refusal(
policy: ProtocolPolicy,
request: &JsonRpcRequest,
) -> Option<JsonRpcResponse> {
if matches!(policy, ProtocolPolicy::LegacyOnly) && modern_protocol_version(request).is_some() {
return legacy_only_modern_refusal(request);
}
protocol_era_refusal(request)
}
// Exact-2024 era: era_admission_refusal serves only the dual-era loop.
#[cfg(all(test, feature = "legacy-2024-11-05"))]
mod era_admission_refusal_tests {
use super::*;
fn modern_discover() -> JsonRpcRequest {
JsonRpcRequest::new(
"server/discover",
Some(serde_json::json!({
"_meta": {
fastmcp_protocol::FINAL_PROTOCOL_VERSION_META_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
}
})),
7,
)
}
fn legacy_initialize() -> JsonRpcRequest {
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "probe", "version": "1"},
})),
7,
)
}
fn refusal(policy: ProtocolPolicy, request: &JsonRpcRequest) -> JsonRpcError {
era_admission_refusal(policy, request)
.expect("an id-bearing request receives a refusal response")
.error
.expect("the refusal is a JSON-RPC error")
}
/// Positive: a modern probe against `LegacyOnly` is the typed `-32601`
/// signal the Auto stdio client admits as a legacy refusal.
#[test]
fn legacy_only_refuses_a_modern_probe_with_method_not_found() {
let error = refusal(ProtocolPolicy::LegacyOnly, &modern_discover());
assert_eq!(error.code, fastmcp_protocol::JsonInteger::from(-32601));
assert_eq!(error.message, LEGACY_ONLY_MODERN_MESSAGE);
assert_eq!(
error.data,
Some(serde_json::json!({ "supported": [LEGACY_PROTOCOL_VERSION] }))
);
}
/// Planted negative (policy differs): the same probe on an Auto
/// connection that already failed era admission keeps the negotiated-era
/// `-32600` refusal.
#[test]
fn auto_keeps_the_negotiated_era_refusal_for_a_modern_probe() {
let error = refusal(ProtocolPolicy::Auto, &modern_discover());
assert_eq!(error.code, fastmcp_protocol::JsonInteger::from(-32600));
assert_eq!(
error.data,
Some(serde_json::json!({
"supported": [MODERN_PROTOCOL_VERSION, LEGACY_PROTOCOL_VERSION],
}))
);
}
/// Planted negative (request differs): a legacy-shaped request that
/// fails admission under `LegacyOnly` is not a modern method and keeps
/// the `-32600` refusal.
#[test]
fn legacy_only_keeps_the_negotiated_era_refusal_for_a_legacy_request() {
let error = refusal(ProtocolPolicy::LegacyOnly, &legacy_initialize());
assert_eq!(error.code, fastmcp_protocol::JsonInteger::from(-32600));
assert_ne!(error.message, LEGACY_ONLY_MODERN_MESSAGE);
}
/// A notification (no id) never receives a refusal response.
#[test]
fn notifications_receive_no_refusal_response() {
let mut request = modern_discover();
request.id = None;
assert!(era_admission_refusal(ProtocolPolicy::LegacyOnly, &request).is_none());
}
}
#[cfg(test)]
mod modern_request_version_refusal_tests {
use super::*;
fn request(method: &str, meta: Option<serde_json::Value>) -> JsonRpcRequest {
let mut params = serde_json::json!({});
if let Some(meta) = meta {
params["_meta"] = meta;
}
JsonRpcRequest::new(method, Some(params), 7_i64)
}
fn meta(version: &str) -> serde_json::Value {
serde_json::json!({
MODERN_PROTOCOL_VERSION_METADATA_KEY: version,
"io.modelcontextprotocol/clientCapabilities": {},
})
}
fn refusal_error(request: &JsonRpcRequest) -> JsonRpcError {
let response =
modern_request_version_refusal(request).expect("a request receives a refusal");
assert_eq!(response.id, request.id, "a refusal echoes the request id");
response.error.expect("a refusal is a JSON-RPC error")
}
#[test]
fn unsupported_version_gets_the_official_unsupported_version_shape() {
let error = refusal_error(&request("tools/list", Some(meta("2099-01-01"))));
assert_eq!(error.code, fastmcp_protocol::JsonInteger::from(-32022));
assert_eq!(
error.data,
Some(serde_json::json!({
"supported": [MODERN_PROTOCOL_VERSION],
"requested": "2099-01-01",
}))
);
}
#[test]
fn missing_protocol_version_is_invalid_params() {
let without_meta = request("tools/list", None);
let without_version = request(
"tools/list",
Some(serde_json::json!({"io.modelcontextprotocol/clientCapabilities": {}})),
);
for request in [without_meta, without_version] {
assert_eq!(
refusal_error(&request).code,
fastmcp_protocol::JsonInteger::from(-32602)
);
}
}
#[test]
fn removed_methods_are_method_not_found_not_invalid_params() {
for method in [
"initialize",
"ping",
"logging/setLevel",
"resources/subscribe",
"resources/unsubscribe",
] {
assert_eq!(
refusal_error(&request(method, None)).code,
fastmcp_protocol::JsonInteger::from(-32601),
"{method} was removed in MCP 2026-07-28"
);
}
// Planted negative: a current method without metadata stays -32602.
assert_eq!(
refusal_error(&request("server/discover", None)).code,
fastmcp_protocol::JsonInteger::from(-32602)
);
}
#[test]
fn notifications_receive_no_response() {
let mut notification = request("notifications/cancelled", None);
notification.id = None;
assert!(modern_request_version_refusal(¬ification).is_none());
}
#[test]
fn only_modern_selecting_opening_rejections_keep_the_connection() {
let mut modern_only = StdioEraClassifier::new(ProtocolPolicy::ModernOnly);
let missing = modern_only.classify_opening(StdioOpeningFrame::RequestWithoutModernMetadata);
assert!(opening_rejection_selected_modern(&missing));
let mut auto = StdioEraClassifier::new(ProtocolPolicy::Auto);
let unsupported = auto.classify_opening(StdioOpeningFrame::ModernRequest {
protocol_version: "2099-01-01".to_owned(),
});
assert!(opening_rejection_selected_modern(&unsupported));
assert_eq!(
auto.state(),
&fastmcp_protocol::protocol_policy::StdioEraState::Selected(ProtocolEra::Modern2026),
"the unsupported modern opening still selects the modern era"
);
// Planted negatives: ambiguous Auto openings select no era and still
// close, and a supported opening is not a rejection at all.
let mut auto = StdioEraClassifier::new(ProtocolPolicy::Auto);
let notification = auto.classify_opening(StdioOpeningFrame::Notification);
assert!(!opening_rejection_selected_modern(¬ification));
let mut auto = StdioEraClassifier::new(ProtocolPolicy::Auto);
let supported = auto.classify_opening(StdioOpeningFrame::ModernRequest {
protocol_version: MODERN_PROTOCOL_VERSION.to_owned(),
});
assert!(!opening_rejection_selected_modern(&supported));
}
}
fn is_quarantined_task_rpc(method: &str) -> bool {
// Legacy 2024-era names. Official MCP 2026-07-28 Tasks methods are
// tasks/get, tasks/update, and tasks/cancel; with the `tasks` feature a
// built server serves them from a process-local in-memory store unless
// ServerBuilder::final_tasks (or a proxy relay) supplies the runtime.
matches!(method, "tasks/list" | "tasks/submit")
}
fn is_notification_only_method(method: &str) -> bool {
method.starts_with("notifications/")
}
fn is_request_only_method(method: &str) -> bool {
matches!(
method,
"server/discover"
| "initialize"
| "ping"
| "logging/setLevel"
| "completion/complete"
| "tools/list"
| "tools/call"
| "resources/list"
| "resources/templates/list"
| "resources/read"
| "resources/subscribe"
| "resources/unsubscribe"
| "prompts/list"
| "prompts/get"
| SUBSCRIPTIONS_LISTEN
| "tasks/list"
| "tasks/get"
| "tasks/update"
| "tasks/cancel"
| "tasks/submit"
)
}
fn is_session_mutation(method: &str) -> bool {
matches!(
method,
"initialize" | "logging/setLevel" | "resources/subscribe" | "resources/unsubscribe"
)
}
enum SessionMutationRollback {
RemoveResourceSubscription(String),
RestoreResourceSubscription(String),
RestoreInitialization(InitializationSnapshot),
RestoreLogLevel(Option<LogLevel>),
}
impl SessionMutationRollback {
fn apply(self, session: &mut Session) {
match self {
Self::RemoveResourceSubscription(uri) => {
session.rollback_resource_subscription(&uri);
}
Self::RestoreResourceSubscription(uri) => {
session.restore_resource_subscription(uri);
}
Self::RestoreInitialization(snapshot) => {
session.restore_initialization(snapshot);
}
Self::RestoreLogLevel(level) => {
session.restore_log_level(level);
}
}
}
}
#[cfg(feature = "legacy-2024-11-05")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DispatchPrincipalAdmissionState {
Pending,
Admitted,
Rejected,
}
#[cfg(feature = "legacy-2024-11-05")]
struct DispatchPrincipalAdmission {
state: Mutex<DispatchPrincipalAdmissionState>,
changed: Condvar,
}
#[cfg(feature = "legacy-2024-11-05")]
impl DispatchPrincipalAdmission {
fn pending() -> Self {
Self {
state: Mutex::new(DispatchPrincipalAdmissionState::Pending),
changed: Condvar::new(),
}
}
fn admitted() -> Self {
Self {
state: Mutex::new(DispatchPrincipalAdmissionState::Admitted),
changed: Condvar::new(),
}
}
fn resolve(&self, admitted: bool) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
debug_assert_eq!(*state, DispatchPrincipalAdmissionState::Pending);
*state = if admitted {
DispatchPrincipalAdmissionState::Admitted
} else {
DispatchPrincipalAdmissionState::Rejected
};
self.changed.notify_all();
}
fn wait_timeout(&self, timeout: Duration) -> Option<bool> {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (state, result) = self
.changed
.wait_timeout_while(state, timeout, |state| {
matches!(state, DispatchPrincipalAdmissionState::Pending)
})
.unwrap_or_else(std::sync::PoisonError::into_inner);
match *state {
DispatchPrincipalAdmissionState::Admitted => Some(true),
DispatchPrincipalAdmissionState::Rejected => Some(false),
DispatchPrincipalAdmissionState::Pending => {
debug_assert!(result.timed_out());
None
}
}
}
}
#[derive(Default)]
struct DispatchQueueState {
inner: Mutex<DispatchQueueStateInner>,
drained: Condvar,
}
#[derive(Default)]
struct DispatchQueueStateInner {
/// Request IDs retain this reservation from admission until the response
/// attempt completes. Keeping it across the queued-to-active transition
/// closes both admission TOCTOU and post-commit ABA races with ID reuse.
reserved: HashSet<CorrelationKey>,
/// Initialization requests remain cancellable only by the server-owned
/// connection shutdown path. Peer cancellation cannot erase the request
/// that establishes the selected legacy lifecycle.
peer_cancellation_protected: HashSet<CorrelationKey>,
/// Every correlated request retains one cancellation authority from queue
/// admission through active dispatch and response finalization. The
/// modern child and exact-2024 adapter must both receive this same handle
/// so a control notification cannot be lost in the queued-to-active gap.
admitted_cancellations: HashMap<CorrelationKey, McpRequestCancellation>,
/// Reserved requests that a worker has begun dispatching.
dispatching: HashSet<CorrelationKey>,
cancelled: HashSet<CorrelationKey>,
modern_cancellations: HashMap<u64, McpRequestCancellation>,
/// The subset of [`Self::modern_cancellations`] belonging to id-less
/// (notification) children. These have no response to commit, so shutdown
/// cancels them before the pre-stop drain window instead of waiting it
/// out on their behalf.
uncorrelated_modern_cancellations: HashSet<u64>,
next_modern_cancellation_id: u64,
queued_bytes: usize,
modern_in_flight: usize,
stopping: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ModernDispatchStart {
Ready,
Cancelled,
Stopping,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DispatchCancellationDisposition {
NotOwned,
Protected,
Accepted,
AlreadySettled,
}
#[cfg(feature = "legacy-2024-11-05")]
struct QueuedDispatchRequest {
request: JsonRpcRequest,
era: ProtocolEra,
serialized_bytes: usize,
principal_admission: Arc<DispatchPrincipalAdmission>,
}
#[cfg(feature = "legacy-2024-11-05")]
enum QueuedDispatchMessage {
Request(QueuedDispatchRequest),
/// Exact-2024 client responses are serialized through the same adapter
/// that allocated their reverse-request IDs. Generic bidirectional
/// responses never enter this variant: they are consumed directly by the
/// generic pending-request registry.
LegacyResponse(JsonRpcResponse),
}
impl DispatchQueueState {
fn admit(&self, id: &RequestId, peer_cancellation_allowed: bool) -> bool {
let Ok(key) = id.correlation_key() else {
return false;
};
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if inner.stopping || !inner.reserved.insert(key.clone()) {
return false;
}
let previous = inner
.admitted_cancellations
.insert(key.clone(), McpRequestCancellation::new());
debug_assert!(previous.is_none());
if !peer_cancellation_allowed {
inner.peer_cancellation_protected.insert(key);
}
true
}
fn discard(&self, id: &RequestId) {
let Ok(key) = id.correlation_key() else {
return;
};
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let removed = inner.reserved.remove(&key);
inner.peer_cancellation_protected.remove(&key);
inner.admitted_cancellations.remove(&key);
inner.dispatching.remove(&key);
inner.cancelled.remove(&key);
if removed && inner.reserved.is_empty() {
self.drained.notify_all();
}
}
fn admitted_request_cancellation(&self, id: &RequestId) -> Option<McpRequestCancellation> {
let key = id.correlation_key().ok()?;
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.admitted_cancellations
.get(&key)
.cloned()
}
fn reserve_queued_bytes(&self, serialized_bytes: usize) -> bool {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if inner.stopping {
return false;
}
let Some(prospective) = inner.queued_bytes.checked_add(serialized_bytes) else {
return false;
};
if prospective > MAX_DISPATCH_QUEUE_BYTES {
return false;
}
inner.queued_bytes = prospective;
true
}
fn release_queued_bytes(&self, serialized_bytes: usize) {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.queued_bytes = inner.queued_bytes.saturating_sub(serialized_bytes);
}
/// Registers the request-local cancellation domain retained by a modern
/// child. This covers notifications as well as correlated requests: the
/// latter are also indexed by `Server::active_requests`, while a
/// notification has no wire id to index there.
fn register_modern_cancellation(
&self,
cancellation: McpRequestCancellation,
correlated: bool,
) -> u64 {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let cancellation_id = inner.next_modern_cancellation_id;
inner.next_modern_cancellation_id = inner.next_modern_cancellation_id.wrapping_add(1);
if inner.stopping {
cancellation.cancel();
} else {
let previous = inner
.modern_cancellations
.insert(cancellation_id, cancellation);
debug_assert!(previous.is_none());
if !correlated {
inner
.uncorrelated_modern_cancellations
.insert(cancellation_id);
}
}
cancellation_id
}
fn unregister_modern_cancellation(&self, cancellation_id: u64) {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.modern_cancellations.remove(&cancellation_id);
inner
.uncorrelated_modern_cancellations
.remove(&cancellation_id);
}
/// Cancels id-less (notification) modern children ahead of the pre-stop
/// drain: the drain window exists to let response commits finish, and a
/// notification has no response to protect.
fn cancel_uncorrelated_modern_children(&self) {
let cancellations = {
let inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner
.uncorrelated_modern_cancellations
.iter()
.filter_map(|id| inner.modern_cancellations.get(id).cloned())
.collect::<Vec<_>>()
};
for cancellation in cancellations {
cancellation.cancel();
}
}
/// Reserves one bounded modern request slot before it is submitted to the
/// caller's runtime. The reservation persists until the request-owned
/// child finishes, so mailbox admission cannot turn into unbounded work.
fn reserve_modern_slot(&self) -> bool {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if inner.stopping || inner.modern_in_flight >= MAX_DISPATCH_QUEUE_DEPTH {
return false;
}
inner.modern_in_flight += 1;
true
}
fn release_modern_slot(&self) {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.modern_in_flight = inner.modern_in_flight.saturating_sub(1);
if inner.modern_in_flight == 0 {
self.drained.notify_all();
}
}
fn wait_for_modern_drain(&self, timeout: Duration) -> bool {
let inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (inner, _) = self
.drained
.wait_timeout_while(inner, timeout, |inner| inner.modern_in_flight != 0)
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.modern_in_flight == 0
}
/// Waits for every admitted id-bearing request to finish its terminal
/// response attempt. This spans modern children and the exact-2024
/// adapter; `reserved` is retained from queue admission through the send
/// attempt, so an ingress EOF cannot strand a reply by stopping the shared
/// dispatch worker too early.
fn wait_for_correlated_response_drain(&self, timeout: Duration) -> bool {
let inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (inner, _) = self
.drained
.wait_timeout_while(inner, timeout, |inner| !inner.reserved.is_empty())
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.reserved.is_empty()
}
fn wait_for_modern_drain_unbounded(&self) {
let inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _quiesced_guard = self
.drained
.wait_while(inner, |inner| inner.modern_in_flight != 0)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
fn cancel_reserved(&self, id: &RequestId) -> DispatchCancellationDisposition {
let Ok(key) = id.correlation_key() else {
return DispatchCancellationDisposition::NotOwned;
};
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !inner.reserved.contains(&key) {
return DispatchCancellationDisposition::NotOwned;
}
if inner.peer_cancellation_protected.contains(&key) {
return DispatchCancellationDisposition::Protected;
}
if let Some(cancellation) = inner.admitted_cancellations.get(&key).cloned() {
if !inner.dispatching.contains(&key) {
inner.cancelled.insert(key);
}
return if cancellation.cancel() {
DispatchCancellationDisposition::Accepted
} else {
DispatchCancellationDisposition::AlreadySettled
};
}
if inner.dispatching.contains(&key) {
return DispatchCancellationDisposition::NotOwned;
}
if inner.cancelled.insert(key) {
DispatchCancellationDisposition::Accepted
} else {
DispatchCancellationDisposition::AlreadySettled
}
}
fn begin_dispatch(&self, id: &RequestId) -> bool {
let Ok(key) = id.correlation_key() else {
return false;
};
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
debug_assert!(inner.reserved.contains(&key));
inner.dispatching.insert(key.clone());
inner.cancelled.remove(&key) || inner.stopping
}
fn begin_modern_dispatch(&self, id: Option<&RequestId>) -> ModernDispatchStart {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if inner.stopping {
return ModernDispatchStart::Stopping;
}
let Some(id) = id else {
return ModernDispatchStart::Ready;
};
let Ok(key) = id.correlation_key() else {
return ModernDispatchStart::Stopping;
};
debug_assert!(inner.reserved.contains(&key));
inner.dispatching.insert(key.clone());
if inner.cancelled.remove(&key) {
ModernDispatchStart::Cancelled
} else {
ModernDispatchStart::Ready
}
}
#[cfg(feature = "legacy-2024-11-05")]
fn is_stopping(&self) -> bool {
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.stopping
}
fn stop(&self) {
let cancellations = {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
inner.stopping = true;
let queued = inner
.reserved
.difference(&inner.dispatching)
.cloned()
.collect::<Vec<_>>();
inner.cancelled.extend(queued);
inner
.modern_cancellations
.values()
.chain(inner.admitted_cancellations.values())
.cloned()
.collect::<Vec<_>>()
};
for cancellation in cancellations {
cancellation.cancel();
}
}
}
/// Marks an unexpected dispatch-worker exit before the worker stack unwinds.
///
/// The latch is disarmed only after a stop requested by the receive pump. A
/// returned send failure, disconnected worker queue, or panic therefore closes
/// admission and makes the pump report failure. Its drop path contains queue
/// wake-up failures so a worker panic cannot turn into a double-panic abort.
#[cfg(feature = "legacy-2024-11-05")]
struct DispatchWorkerFailureLatch {
failed: Arc<AtomicBool>,
queue: Arc<DispatchQueueState>,
armed: bool,
}
#[cfg(feature = "legacy-2024-11-05")]
impl DispatchWorkerFailureLatch {
fn new(failed: Arc<AtomicBool>, queue: Arc<DispatchQueueState>) -> Self {
Self {
failed,
queue,
armed: true,
}
}
fn disarm(&mut self) {
self.armed = false;
}
}
#[cfg(feature = "legacy-2024-11-05")]
impl Drop for DispatchWorkerFailureLatch {
fn drop(&mut self) {
if !self.armed {
return;
}
self.failed.store(true, Ordering::Release);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.queue.stop()));
}
}
#[cfg(feature = "legacy-2024-11-05")]
struct DispatchWorkerCompletionSignal(Option<std::sync::mpsc::Sender<()>>);
#[cfg(feature = "legacy-2024-11-05")]
impl Drop for DispatchWorkerCompletionSignal {
fn drop(&mut self) {
if let Some(sender) = self.0.take() {
let _ = sender.send(());
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum PumpIoMode {
Split,
#[cfg(not(feature = "legacy-2024-11-05"))]
Unsplit,
}
/// Longest an unsplit pump holds off its next `recv` for a listen that has
/// neither acknowledged nor finished. Past it the pump reads again, and the
/// acknowledgement is written when that `recv` returns.
#[cfg(not(feature = "legacy-2024-11-05"))]
const UNSPLIT_LISTEN_OPEN_TIMEOUT: Duration = Duration::from_secs(5);
/// Set once a listen dispatched off an unsplit pump has written its
/// acknowledgement or finished (bd-4crkf).
#[cfg(not(feature = "legacy-2024-11-05"))]
#[derive(Default)]
struct UnsplitListenOpened {
opened: Mutex<bool>,
changed: Condvar,
}
#[cfg(not(feature = "legacy-2024-11-05"))]
impl UnsplitListenOpened {
fn fire(&self) {
*self
.opened
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = true;
self.changed.notify_all();
}
/// Waits until fired, `timeout` elapses, or the pump is cancelled.
fn wait(&self, cx: &Cx, timeout: Duration) {
let deadline = Instant::now() + timeout;
let mut opened = self
.opened
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while !*opened && cx.checkpoint().is_ok() {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return;
}
opened = self
.changed
.wait_timeout(opened, remaining.min(Duration::from_millis(10)))
.unwrap_or_else(std::sync::PoisonError::into_inner)
.0;
}
}
}
#[cfg(not(feature = "legacy-2024-11-05"))]
struct UnsplitListenOpenedOnDrop(Arc<UnsplitListenOpened>);
#[cfg(not(feature = "legacy-2024-11-05"))]
impl Drop for UnsplitListenOpenedOnDrop {
fn drop(&mut self) {
self.0.fire();
}
}
/// Owns every admission-side resource for one modern stdio request until its
/// caller-owned child task has finished. The reservation is deliberately held
/// by the task closure itself: a runtime that rejects or cancels a task before
/// first poll still releases its capacity without leaving an orphaned request
/// ID or byte charge behind.
struct ModernDispatchReservation {
queue: Arc<DispatchQueueState>,
request_id: Option<RequestId>,
serialized_bytes: usize,
cancellation: McpRequestCancellation,
cancellation_id: u64,
failed: Arc<AtomicBool>,
failure_latched: bool,
}
impl ModernDispatchReservation {
fn new(
queue: Arc<DispatchQueueState>,
request_id: Option<RequestId>,
serialized_bytes: usize,
failed: Arc<AtomicBool>,
) -> Self {
let cancellation = request_id.as_ref().map_or_else(
McpRequestCancellation::new,
|id| {
queue.admitted_request_cancellation(id).expect(
"a correlated modern child must retain its queue-admission cancellation authority",
)
},
);
let cancellation_id =
queue.register_modern_cancellation(cancellation.clone(), request_id.is_some());
Self {
queue,
request_id,
serialized_bytes,
cancellation,
cancellation_id,
failed,
failure_latched: false,
}
}
fn begin(&mut self) {
self.failure_latched = true;
}
fn disarm_failure(&mut self) {
self.failure_latched = false;
}
fn cancellation(&self) -> McpRequestCancellation {
self.cancellation.clone()
}
}
impl DispatchQueueState {
fn admit_modern_request(
self: &Arc<Self>,
request: &JsonRpcRequest,
failed: Arc<AtomicBool>,
) -> Result<ModernDispatchReservation, JsonRpcError> {
if let Some(id) = request.id.as_ref()
&& !self.admit(id, request.method != "initialize")
{
return Err(JsonRpcError {
code: McpErrorCode::InvalidRequest.into(),
message: "Request id is already active".to_owned(),
data: None,
});
}
let slot = self.reserve_modern_slot();
let bytes = measure_dispatch_request(request);
if slot
&& let Some(bytes) = bytes
&& self.reserve_queued_bytes(bytes)
{
return Ok(ModernDispatchReservation::new(
Arc::clone(self),
request.id.clone(),
bytes,
failed,
));
}
if slot {
self.release_modern_slot();
}
if let Some(id) = request.id.as_ref() {
self.discard(id);
}
Err(JsonRpcError {
code: RESOURCE_EXHAUSTED_ERROR_CODE.into(),
message: DISPATCH_QUEUE_CAPACITY_MESSAGE.to_owned(),
data: None,
})
}
}
impl Drop for ModernDispatchReservation {
fn drop(&mut self) {
if self.failure_latched {
self.failed.store(true, Ordering::Release);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.queue.stop();
}));
}
if let Some(id) = self.request_id.as_ref() {
self.queue.discard(id);
}
self.queue
.unregister_modern_cancellation(self.cancellation_id);
self.queue.release_queued_bytes(self.serialized_bytes);
self.queue.release_modern_slot();
}
}
thread_local! {
static REDACT_EXTENSION_PANIC: Cell<bool> = const { Cell::new(false) };
}
struct ExtensionPanicRedactionGuard {
previous: bool,
}
impl ExtensionPanicRedactionGuard {
fn enter() -> Self {
let previous = REDACT_EXTENSION_PANIC.with(|redact| redact.replace(true));
Self { previous }
}
}
impl Drop for ExtensionPanicRedactionGuard {
fn drop(&mut self) {
REDACT_EXTENSION_PANIC.with(|redact| redact.set(self.previous));
}
}
/// Installs FastMCP's payload-redacting diagnostic hook once.
///
/// Rust exposes one process-global, replaceable panic hook. An embedding
/// application can call `std::panic::set_hook` after this function returns and
/// replace FastMCP's hook; FastMCP cannot prevent or reliably detect that. The
/// fixed peer-facing error produced after `catch_unwind` remains payload-free,
/// but payload-free process diagnostics require the application to preserve or
/// correctly chain the installed hook.
fn install_extension_panic_hook() {
INSTALL_EXTENSION_PANIC_HOOK.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let redact = REDACT_EXTENSION_PANIC.try_with(Cell::get).unwrap_or(false);
if redact {
// This branch deliberately cannot observe `panic_info`. Keep
// the local diagnostic constant-sized and payload-free.
#[cfg(test)]
REDACTED_EXTENSION_PANIC_COUNT.fetch_add(1, Ordering::Relaxed);
let _ = std::io::stderr().write_all(REDACTED_EXTENSION_PANIC_INCIDENT);
} else {
previous(panic_info);
}
}));
});
}
pub(crate) fn catch_extension_unwind<R>(
callback: impl FnOnce() -> R,
) -> Result<R, Box<dyn Any + Send>> {
install_extension_panic_hook();
let _redaction = ExtensionPanicRedactionGuard::enter();
std::panic::catch_unwind(std::panic::AssertUnwindSafe(callback))
}
fn extension_panic_error(extension_class: &'static str) -> McpError {
static NEXT_EXTENSION_INCIDENT_ID: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(1);
let incident_id = NEXT_EXTENSION_INCIDENT_ID.fetch_add(1, Ordering::Relaxed);
error!(
target: targets::SERVER,
"extension callback terminated unexpectedly; incident_id={incident_id}; class={extension_class}; detail=panic_payload_redacted"
);
McpError::internal_error("Internal server error")
}
fn resource_subscription_capacity_error() -> McpError {
McpError::new(
McpErrorCode::Custom(RESOURCE_EXHAUSTED_ERROR_CODE),
RESOURCE_SUBSCRIPTION_CAPACITY_MESSAGE,
)
}
#[cfg(feature = "tasks")]
fn missing_final_tasks_capability_error() -> McpResult<McpError> {
let mut extensions = serde_json::Map::new();
extensions.insert(
fastmcp_protocol::TASKS_EXTENSION.to_owned(),
serde_json::json!({}),
);
let missing = MissingRequiredClientCapabilityError::new(serde_json::json!({
"extensions": serde_json::Value::Object(extensions),
}))
.map_err(|_| McpError::internal_error("failed to encode required Tasks capability"))?;
Ok(McpError::with_data(
McpErrorCode::Custom(missing.jsonrpc_error_code()),
"Required client capability is missing",
missing.canonical_error_data(),
))
}
fn has_canonical_missing_required_client_capability_payload(
data: Option<&serde_json::Value>,
) -> bool {
let Some(data) = data else {
return false;
};
let Some(payload) = data.as_object() else {
return false;
};
if payload.len() != 1 {
return false;
}
let Some(required_capabilities) = payload.get("requiredCapabilities") else {
return false;
};
MissingRequiredClientCapabilityError::new(required_capabilities.clone())
.is_ok_and(|error| error.canonical_error_data() == *data)
}
fn is_canonical_missing_required_client_capability_error(error: &McpError) -> bool {
error.code == McpErrorCode::Custom(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE)
&& error.message == "Required client capability is missing"
&& has_canonical_missing_required_client_capability_payload(error.data.as_ref())
}
fn mask_peer_error(error: McpError, mask_error_details: bool) -> McpError {
// Preserve only the fixed, reviewed missing-capability error shape while
// continuing to mask arbitrary custom extension errors, including
// lookalikes that reuse its code but alter its message or payload.
if is_canonical_missing_required_client_capability_error(&error)
|| error.code == McpErrorCode::Custom(RESOURCE_EXHAUSTED_ERROR_CODE)
&& error.message == RESOURCE_SUBSCRIPTION_CAPACITY_MESSAGE
&& error.data.is_none()
|| error.code == McpErrorCode::Custom(fastmcp_protocol::HEADER_MISMATCH_ERROR_CODE)
&& error.message == http_admission::HEADER_MISMATCH_MESSAGE
&& error.data.is_none()
{
error
} else {
error.masked(mask_error_details)
}
}
#[cfg(feature = "legacy-2024-11-05")]
fn legacy_handler_error_from_mcp(
error: McpError,
mask_error_details: bool,
) -> Legacy2024HandlerError {
let masked = mask_peer_error(error, mask_error_details);
Legacy2024HandlerError::with_code(i64::from(i32::from(masked.code)), masked.message)
}
fn is_canonical_missing_required_client_capability_response(response: &JsonRpcResponse) -> bool {
response.error.as_ref().is_some_and(|error| {
error.code.as_i32() == Some(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE)
&& error.message == "Required client capability is missing"
&& has_canonical_missing_required_client_capability_payload(error.data.as_ref())
})
}
/// Whether a recognized `Mcp-Param-*` mirror refused a modern HTTP
/// `tools/call` after its tool was resolved (HTTP-05).
fn is_parameter_header_mismatch_response(response: &JsonRpcResponse) -> bool {
response.error.as_ref().is_some_and(|error| {
error.code.as_i32() == Some(fastmcp_protocol::HEADER_MISMATCH_ERROR_CODE)
&& error.message == http_admission::HEADER_MISMATCH_MESSAGE
&& error.data.is_none()
})
}
/// Whether a final outcome is JSON-RPC Method not found (`-32601`).
fn is_method_not_found_response(response: &JsonRpcResponse) -> bool {
response
.error
.as_ref()
.is_some_and(|error| error.code.as_i32() == Some(-32_601))
}
/// Final outcomes whose canonical modern HTTP status is not 200, each carried
/// as the JSON-RPC error body:
///
/// - 400 for a missing required client capability (`-32021`) or a
/// parameter-header mismatch (`-32020`);
/// - 404 for a method the server does not implement (`-32601`), including
/// methods MCP 2026-07-28 removed such as `initialize` and `ping`.
fn canonical_http_error_status_response(response: &JsonRpcResponse) -> Option<HttpResponse> {
if is_canonical_missing_required_client_capability_response(response)
|| is_parameter_header_mismatch_response(response)
{
return Some(HttpResponse::new(HttpStatus::BAD_REQUEST).with_json(response));
}
is_method_not_found_response(response)
.then(|| HttpResponse::new(HttpStatus::NOT_FOUND).with_json(response))
}
#[cfg(feature = "legacy-2024-11-05")]
use fastmcp_transport::sse::SseServerTransport;
use fastmcp_transport::{
AsyncStdout, Codec, StdioTransport, Transport, TransportError, TransportRecvHalf,
TransportSendHalf,
};
#[cfg(test)]
use log::Level;
use log::LevelFilter;
#[derive(Debug, Clone, PartialEq, Eq)]
enum ReceiveErrorDisposition {
ReplyWithParseError,
ReplyWithInvalidRequest(Option<RequestId>),
Terminate,
}
fn classify_receive_error(error: &TransportError) -> ReceiveErrorDisposition {
match error {
// A transport timeout does not prove that the byte stream is still at
// a message boundary. Some receive paths report it after consuming a
// partial or complete frame, while a context deadline remains
// exhausted even when no bytes were consumed. Retrying is therefore
// neither framing-safe nor capable of making progress in general.
TransportError::Timeout => ReceiveErrorDisposition::Terminate,
// An explicit bounded-receive deadline may have interrupted a partial
// frame. This server dispatcher does not own the transport-specific
// closed-state proof needed to retry it safely.
TransportError::ReceiveDeadlineExceeded => ReceiveErrorDisposition::Terminate,
// JSON decoding begins only after a transport has isolated one
// complete message. Reply with the fixed uncorrelated JSON-RPC error,
// then admit the next complete message.
TransportError::Codec(fastmcp_transport::CodecError::Json(_)) => {
ReceiveErrorDisposition::ReplyWithParseError
}
TransportError::Codec(fastmcp_transport::CodecError::InvalidMessage {
kind: fastmcp_transport::InvalidMessageKind::Request,
request_id,
..
}) => ReceiveErrorDisposition::ReplyWithInvalidRequest(request_id.clone()),
TransportError::Codec(fastmcp_transport::CodecError::InvalidMessage {
kind: fastmcp_transport::InvalidMessageKind::Response,
..
}) => ReceiveErrorDisposition::Terminate,
// A bounded line reader can detect an oversized frame before it has
// consumed the delimiter. The byte stream may still point inside the
// rejected frame, so retrying would reinterpret its suffix.
TransportError::Codec(fastmcp_transport::CodecError::MessageTooLarge(_)) => {
ReceiveErrorDisposition::Terminate
}
// I/O errors include HTTP/WebSocket framing and protocol violations.
// Continuing on the same byte stream could reinterpret attacker-owned
// suffix bytes after the parser has lost synchronization.
TransportError::Io(_)
| TransportError::Closed
| TransportError::ControlFrameTooLarge { .. }
| TransportError::Cancelled => ReceiveErrorDisposition::Terminate,
}
}
fn transport_run_error(stage: &'static str, error: &TransportError) -> McpError {
let kind = match error {
TransportError::Io(_) => "io",
TransportError::Closed => "closed",
TransportError::Codec(_) => "codec",
TransportError::Timeout => "timeout",
TransportError::ReceiveDeadlineExceeded => "receive_deadline_exceeded",
TransportError::ControlFrameTooLarge { .. } => "control_frame_too_large",
TransportError::Cancelled => "cancelled",
};
McpError::with_data(
McpErrorCode::InternalError,
format!("Server transport failed during {stage}"),
serde_json::json!({
"stage": stage,
"kind": kind,
}),
)
}
fn returning_send_result(error: &TransportError) -> McpResult<()> {
if error.is_cancelled() {
Ok(())
} else {
Err(transport_run_error("send", error))
}
}
fn server_run_error(stage: &'static str, kind: &'static str, message: &'static str) -> McpError {
McpError::with_data(
McpErrorCode::InternalError,
message,
serde_json::json!({
"stage": stage,
"kind": kind,
}),
)
}
fn returning_send_result_with_connection_failure(
error: &TransportError,
connection_failure: &Option<Arc<AtomicBool>>,
) -> McpResult<()> {
if connection_failure
.as_ref()
.is_some_and(|failed| failed.load(Ordering::Acquire))
{
Err(server_run_error(
"notification",
"send_failure",
"Server notification send failed",
))
} else {
returning_send_result(error)
}
}
fn combined_run_and_close_error(run_error: McpError, close_error: McpError) -> McpError {
McpError::with_data(
McpErrorCode::InternalError,
"Server transport run and close both failed",
serde_json::json!({
"stage": "run_and_close",
"kind": "multiple_failures",
"run": run_error,
"close": close_error,
}),
)
}
fn combined_operation_and_cleanup_error(
operation_error: McpError,
cleanup: ShutdownCleanupOutcome,
) -> McpError {
let Some(cleanup_error) = cleanup.into_error() else {
return operation_error;
};
McpError::with_data(
McpErrorCode::InternalError,
"Server transport operation and shutdown cleanup both failed",
serde_json::json!({
"stage": "run_and_cleanup",
"kind": "multiple_failures",
"run": operation_error,
"cleanup": cleanup_error,
}),
)
}
fn send_uncorrelated_parse_error<S>(send: &Arc<Mutex<S>>, cx: &Cx) -> Result<(), TransportError>
where
S: FnMut(&Cx, &JsonRpcMessage) -> Result<(), TransportError>,
{
send_jsonrpc_error(send, cx, None, McpErrorCode::ParseError, "Parse error")
}
fn send_invalid_request<S>(
send: &Arc<Mutex<S>>,
cx: &Cx,
request_id: Option<RequestId>,
) -> Result<(), TransportError>
where
S: FnMut(&Cx, &JsonRpcMessage) -> Result<(), TransportError>,
{
send_jsonrpc_error(
send,
cx,
request_id,
McpErrorCode::InvalidRequest,
"Invalid Request",
)
}
fn send_jsonrpc_error<S>(
send: &Arc<Mutex<S>>,
cx: &Cx,
request_id: Option<RequestId>,
code: McpErrorCode,
message: &'static str,
) -> Result<(), TransportError>
where
S: FnMut(&Cx, &JsonRpcMessage) -> Result<(), TransportError>,
{
let response = JsonRpcResponse::error(
request_id,
JsonRpcError {
code: code.into(),
message: message.to_string(),
data: None,
},
);
let mut guard = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
guard(cx, &JsonRpcMessage::Response(response))
}
/// Type alias for startup hook function.
pub type StartupHook =
Box<dyn FnOnce() -> Result<(), Box<dyn std::error::Error + Send + Sync>> + Send>;
/// Type alias for shutdown hook function.
pub type ShutdownHook = Box<dyn FnOnce() + Send>;
/// Lifecycle hooks for server startup and shutdown.
///
/// These hooks allow custom initialization and cleanup logic to run
/// at well-defined points in the server lifecycle:
///
/// - `on_startup`: Called before the server starts accepting connections
/// - `on_shutdown`: Called when the server is shutting down
///
/// # Example
///
/// ```ignore
/// use fastmcp_rust::{modern::ServerBuilder, prelude::*};
///
/// // Inside the application's async entry point, with its supplied `cx`.
/// ServerBuilder::new("demo", "1.0.0")
/// .on_startup(|| {
/// eprintln!("Initializing...");
/// // Initialize database, caches, etc.
/// Ok::<(), std::io::Error>(())
/// })
/// .on_shutdown(|| {
/// eprintln!("Cleaning up...");
/// // Close connections, flush buffers, etc.
/// })
/// .build()
/// .run_stdio_with_cx(cx)
/// .await;
/// ```
#[derive(Default)]
pub struct LifespanHooks {
/// Hook called before the server starts accepting connections.
pub on_startup: Option<StartupHook>,
/// Hook called when the server is shutting down.
pub on_shutdown: Option<ShutdownHook>,
}
impl LifespanHooks {
/// Creates empty lifecycle hooks.
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
/// Logging configuration for the server.
#[derive(Debug, Clone)]
pub struct LoggingConfig {
/// Maximum enabled log verbosity (default: INFO).
///
/// Set this to [`LevelFilter::Off`] to disable server-managed logging.
pub level: LevelFilter,
/// Show timestamps in logs (default: true).
pub timestamps: bool,
/// Show module targets in logs (default: true).
pub targets: bool,
/// Show file:line in logs (default: false).
pub file_line: bool,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: LevelFilter::Info,
timestamps: true,
targets: true,
file_line: false,
}
}
}
impl LoggingConfig {
/// Create logging config from environment variables.
///
/// Respects:
/// - `FASTMCP_LOG`: Log filter (`off`, `error`, `warn`, `info`, `debug`, or `trace`)
/// - `FASTMCP_LOG_TIMESTAMPS`: Show timestamps (0/false to disable)
/// - `FASTMCP_LOG_TARGETS`: Show targets (0/false to disable)
/// - `FASTMCP_LOG_FILE_LINE`: Show file:line (1/true to enable)
#[must_use]
pub fn from_env() -> Self {
Self::from(&ConsoleConfig::from_env())
}
}
impl From<&ConsoleConfig> for LoggingConfig {
fn from(config: &ConsoleConfig) -> Self {
Self {
level: config.log_level,
timestamps: config.log_timestamps,
targets: config.log_targets,
file_line: config.log_file_line,
}
}
}
/// Behavior when registering a component with a name that already exists.
///
/// This setting controls how the server handles duplicate tool, resource,
/// or prompt names during registration.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DuplicateBehavior {
/// Raise an error and fail registration.
///
/// Use this for strict validation in production environments.
Error,
/// Log a warning and keep the original component.
///
/// This is the default behavior, providing visibility into duplicates
/// while maintaining backwards compatibility.
#[default]
Warn,
/// Replace the original component with the new one.
///
/// Use this when you want later registrations to override earlier ones.
Replace,
/// Silently keep the original component.
///
/// Use this when duplicates are expected and should be ignored.
Ignore,
}
/// Configuration for the live HTTP listener used by [`Server::run_http`].
///
/// The listener serves modern Streamable HTTP at [`Self::mcp_path`]. Enabling
/// the dated legacy feature additionally exposes the exact MCP 2024-11-05 SSE
/// and message POST routes. It runs on the caller-owned asupersync context and
/// does not create a runtime of its own.
///
/// # Example
///
/// ```ignore
/// use fastmcp_server::HttpServerConfig;
///
/// let config = HttpServerConfig::new()
/// .mcp_path("/api/mcp")
/// .health_path("/healthz")
/// .max_connections(128);
/// ```
#[derive(Debug, Clone)]
pub struct HttpServerConfig {
/// Path for the health-check endpoint (default: `"/health"`).
pub health_path: String,
/// Maximum number of concurrent connections (default: 64).
pub max_connections: usize,
/// Time allowed to receive one complete request on an accepted
/// connection before it is closed (default: 30 seconds).
pub request_read_timeout: Duration,
/// Inner HTTP handler configuration (endpoint path, CORS, and body size).
pub handler_config: HttpHandlerConfig,
/// Maximum requests buffered for one live HTTP session.
pub request_capacity: usize,
/// GET path for the exact MCP 2024-11-05 SSE stream.
#[cfg(feature = "legacy-2024-11-05")]
pub legacy_sse_path: String,
/// POST path advertised to an exact MCP 2024-11-05 SSE client.
#[cfg(feature = "legacy-2024-11-05")]
pub legacy_message_path: String,
}
// These match the transport's fixed HTTP/1 header-block ceiling. The body
// ceiling remains deployment-configurable through `HttpHandlerConfig` and is
// passed to the strict admission authority for every endpoint session.
const MODERN_HTTP_ADMISSION_MAX_HEADER_COUNT: usize = 64;
const MODERN_HTTP_ADMISSION_MAX_HEADER_BLOCK_BYTES: usize = 64 * 1024;
fn modern_post_rejection_response(
rejection: ModernPostRejection,
body: &[u8],
max_body_size: usize,
) -> HttpResponse {
match rejection {
ModernPostRejection::EndpointMismatch => HttpResponse::new(HttpStatus::NOT_FOUND),
ModernPostRejection::MethodNotAllowed => HttpResponse::new(HttpStatus::METHOD_NOT_ALLOWED),
ModernPostRejection::NotAcceptable => HttpResponse::new(HttpStatus::NOT_ACCEPTABLE),
ModernPostRejection::FinalAdmission(admission) => {
protocol_admission_error_response(body, admission, max_body_size)
}
ModernPostRejection::InvalidRequestMeta { issue } => {
let response = JsonRpcResponse::error(
admitted_request_id(body, max_body_size),
JsonRpcError {
code: McpErrorCode::InvalidParams.into(),
message: format!("Invalid params: {issue}"),
data: None,
},
);
HttpResponse::new(HttpStatus::BAD_REQUEST).with_json(&response)
}
// A strictly decoded JSON-RPC notification is accepted with 202 and no
// body (plan HTTP-02 "successful notification acceptance"). Modern
// HTTP is stateless per POST: a request is cancelled by closing its
// own response stream, so a separately POSTed notification carries no
// authority over another connection's request and has no effect.
ModernPostRejection::NotARequest => HttpResponse::new(HttpStatus::ACCEPTED),
ModernPostRejection::TooManyHeaders { .. }
| ModernPostRejection::HeaderBlockTooLarge { .. }
| ModernPostRejection::DuplicateSingletonHeader { .. }
| ModernPostRejection::UnsupportedMediaType
| ModernPostRejection::UnsupportedContentCoding
| ModernPostRejection::Raw(_)
| ModernPostRejection::InvalidEnvelope => HttpResponse::bad_request(),
}
}
fn admit_modern_http_post(
handler_config: &HttpHandlerConfig,
method: &str,
path: &str,
headers: &[(String, String)],
body: &[u8],
) -> Result<AdmittedModernPost, HttpResponse> {
let limits = HttpAdmissionLimits::new(
MODERN_HTTP_ADMISSION_MAX_HEADER_COUNT,
MODERN_HTTP_ADMISSION_MAX_HEADER_BLOCK_BYTES,
handler_config.max_body_size,
)
.expect("fixed modern HTTP admission limits are nonzero");
let config = HttpEndpointConfig::new(handler_config.base_path.clone(), limits)
.ok_or_else(HttpResponse::bad_request)?;
admit_modern_post(&config, method, path, headers, body).map_err(|rejection| {
modern_post_rejection_response(rejection, body, handler_config.max_body_size)
})
}
impl Default for HttpServerConfig {
fn default() -> Self {
Self {
health_path: "/health".to_string(),
max_connections: 64,
request_read_timeout: DEFAULT_HTTP_REQUEST_READ_TIMEOUT,
handler_config: HttpHandlerConfig {
base_path: "/mcp".to_string(),
..HttpHandlerConfig::default()
},
request_capacity: 64,
#[cfg(feature = "legacy-2024-11-05")]
legacy_sse_path: "/sse".to_string(),
#[cfg(feature = "legacy-2024-11-05")]
legacy_message_path: "/messages".to_string(),
}
}
}
impl HttpServerConfig {
/// Creates a new configuration with default values.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Sets the MCP endpoint path.
#[must_use]
pub fn mcp_path(mut self, path: impl Into<String>) -> Self {
self.handler_config.base_path = path.into();
self
}
/// Sets the health-check endpoint path.
#[must_use]
pub fn health_path(mut self, path: impl Into<String>) -> Self {
self.health_path = path.into();
self
}
/// Sets the maximum number of concurrent connections.
#[must_use]
pub fn max_connections(mut self, max: usize) -> Self {
self.max_connections = max;
self
}
/// Sets the time allowed to receive one complete request before the
/// connection is closed and its slot released.
#[must_use]
pub fn request_read_timeout(mut self, timeout: Duration) -> Self {
self.request_read_timeout = timeout;
self
}
/// Sets the inner HTTP handler configuration.
#[must_use]
pub fn handler_config(mut self, config: HttpHandlerConfig) -> Self {
self.handler_config = config;
self
}
/// Sets the per-session HTTP request queue capacity.
#[must_use]
pub fn request_capacity(mut self, capacity: usize) -> Self {
self.request_capacity = capacity;
self
}
/// Sets the exact MCP 2024-11-05 SSE stream path.
#[must_use]
#[cfg(feature = "legacy-2024-11-05")]
pub fn legacy_sse_path(mut self, path: impl Into<String>) -> Self {
self.legacy_sse_path = path.into();
self
}
/// Sets the exact MCP 2024-11-05 POST path advertised through SSE.
#[must_use]
#[cfg(feature = "legacy-2024-11-05")]
pub fn legacy_message_path(mut self, path: impl Into<String>) -> Self {
self.legacy_message_path = path.into();
self
}
}
/// Failure while constructing or operating a public server HTTP endpoint.
///
/// This is the stable server API for both ModernOnly and legacy-enabled
/// builds; transport-internal endpoint errors never cross the server boundary.
#[derive(Debug)]
pub enum ServerHttpEndpointError {
/// The endpoint configuration is invalid.
InvalidConfiguration(String),
/// Modern HTTP admission failed.
Http(HttpError),
/// A bounded transport operation failed.
Transport(TransportError),
/// A bounded HTTP session could not be opened.
Session(HttpSessionError),
/// The endpoint session has closed.
Closed,
}
impl std::fmt::Display for ServerHttpEndpointError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidConfiguration(message) => {
write!(
formatter,
"invalid server HTTP endpoint configuration: {message}"
)
}
Self::Http(error) => write!(formatter, "modern HTTP admission failed: {error}"),
Self::Transport(error) => write!(formatter, "HTTP transport failed: {error}"),
Self::Session(error) => write!(formatter, "HTTP session setup failed: {error}"),
Self::Closed => formatter.write_str("server HTTP endpoint session is closed"),
}
}
}
impl std::error::Error for ServerHttpEndpointError {}
impl ServerHttpEndpointError {
fn from_internal(error: DualEraHttpEndpointError) -> Self {
match error {
DualEraHttpEndpointError::InvalidConfiguration(message) => {
Self::InvalidConfiguration(message)
}
DualEraHttpEndpointError::Http(error) => Self::Http(error),
DualEraHttpEndpointError::Transport(error) => Self::Transport(error),
#[cfg(feature = "legacy-2024-11-05")]
DualEraHttpEndpointError::Session(error) => Self::Session(error),
DualEraHttpEndpointError::Closed => Self::Closed,
}
}
}
fn http_request_accepts_sse(request: &HttpRequest) -> bool {
request.header("accept").is_some_and(|value| {
fastmcp_transport::http::HttpResponsePreferences::from_headers([("accept", value)])
.is_ok_and(|preferences| {
preferences.accepts(fastmcp_transport::http::HttpResponseRepresentation::Sse)
})
})
}
/// A live server composition over a bounded HTTP endpoint.
///
/// Modern Streamable HTTP POSTs, including discovery, are independently
/// dispatched and neither emit nor accept `MCP-Session-Id`. A final
/// `subscriptions/listen` SSE body retains its request-owned dispatch only for
/// that response body. A retained [`ServerHttpSession`] is an embedding
/// lifecycle handle whose repeated [`ServerHttpSession::handle_async`] calls share
/// modern mutable component state; it does not own a private MRTR namespace.
/// Turnkey live ingress opens a fresh embedding session per stateless POST.
/// Eligible stateless MRTR continuations are owned by the router; listener
/// shutdown additionally closes them for turnkey live ingress. Exact MCP
/// 2024-11-05 requests retain the transport-issued session identifier,
/// lifecycle adapter, and SSE response stream for this one session when the
/// legacy feature is enabled.
pub struct ServerHttpEndpoint {
server: Arc<Server>,
#[cfg(feature = "legacy-2024-11-05")]
legacy_origin: String,
}
fn validate_server_http_route_configuration(
server: &Server,
) -> Result<(), ServerHttpEndpointError> {
let Some(routes) = server.oauth_http_routes.as_ref() else {
return Ok(());
};
let occupied = vec![
server.http_config.handler_config.base_path.as_str(),
server.http_config.health_path.as_str(),
];
#[cfg(feature = "legacy-2024-11-05")]
let occupied = {
let mut occupied = occupied;
occupied.push(server.http_config.legacy_sse_path.as_str());
occupied.push(server.http_config.legacy_message_path.as_str());
occupied
};
routes
.validate_non_overlapping_paths(occupied)
.map_err(|error| ServerHttpEndpointError::InvalidConfiguration(error.to_string()))
}
/// A server-owned session opened through [`ServerHttpEndpoint`].
///
/// Call [`ServerHttpSession::close`] before dropping a session that admitted
/// modern request work. Async close is the only API that joins those tasks;
/// `Drop` can cancel their exact authorities but cannot synchronously drive an
/// asupersync runtime to quiescence.
#[must_use = "a ServerHttpSession with admitted work must be closed asynchronously"]
pub struct ServerHttpSession {
server: Arc<Server>,
endpoint_session: Arc<Mutex<DualEraHttpSession>>,
#[cfg(feature = "legacy-2024-11-05")]
legacy_session_id: String,
#[cfg(feature = "legacy-2024-11-05")]
legacy_lifecycle: DualEraHttpLegacyLifecycle,
#[cfg(feature = "legacy-2024-11-05")]
legacy_session: Session,
#[cfg(feature = "legacy-2024-11-05")]
legacy_binding: LegacyPeerBinding,
#[cfg(feature = "legacy-2024-11-05")]
legacy_adapter: Option<Legacy2024ServerAdapter<HttpLegacy2024RuntimeHandler>>,
#[cfg(feature = "legacy-2024-11-05")]
legacy_active_request: Arc<Mutex<Option<LiveLegacy2024ActiveRequest>>>,
#[cfg(feature = "legacy-2024-11-05")]
legacy_request_cx: Arc<Mutex<Cx>>,
#[cfg(feature = "legacy-2024-11-05")]
legacy_admissions: Arc<HttpLegacyRequestAdmissions>,
#[cfg(feature = "legacy-2024-11-05")]
legacy_auth_receipt: Arc<Mutex<Option<AuthAdmissionReceipt>>>,
/// Principal admitted on the GET `/sse` that opened this generation.
///
/// GET has no JSON-RPC body, so its receipt cannot be committed onto a
/// later POST. The fingerprint must still bind the session: otherwise the
/// first POST wins ownership and can stream results to a different opener.
#[cfg(feature = "legacy-2024-11-05")]
legacy_sse_open_fingerprint: Option<Sha256Digest>,
#[cfg(feature = "legacy-2024-11-05")]
legacy_pending_requests: Arc<PendingRequests>,
#[cfg(feature = "legacy-2024-11-05")]
legacy_runtime: LiveLegacy2024ConnectionRuntime,
/// Embedding-session-owned modern mutable component state.
///
/// Repeated calls to this public session share the value. Turnkey live
/// ingress instead creates a fresh public session per stateless POST.
/// Eligible MRTR continuation state lives in the router-owned registry
/// rather than in a session-specific namespace here. Listener shutdown
/// closes that registry for turnkey live ingress.
modern_connection: Arc<ModernConnection>,
/// Owned modern listen dispatches whose SSE bodies were returned to the
/// embedding caller. Handles are retained because dropping an asupersync
/// task handle detaches it from the session lifecycle.
modern_dispatches: Arc<Mutex<Vec<OwnedModernHttpDispatch>>>,
/// The first recognized, policy-admitted wire era for this public session.
/// A session never bridges state across the two incompatible HTTP eras.
selected_era: Option<ProtocolEra>,
closed: bool,
}
struct OwnedModernHttpDispatch {
owner_generation: u64,
request_cancellation: McpRequestCancellation,
task: asupersync::runtime::TaskHandle<()>,
}
type ModernHttpDispatchRegistry = Arc<Mutex<Vec<OwnedModernHttpDispatch>>>;
fn take_modern_http_dispatches(
registry: &ModernHttpDispatchRegistry,
owner_generation: Option<u64>,
) -> Vec<OwnedModernHttpDispatch> {
let mut registry = registry
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut retained = Vec::with_capacity(registry.len());
let mut removed = Vec::new();
for dispatch in std::mem::take(&mut *registry) {
if owner_generation.is_none_or(|owner| dispatch.owner_generation == owner) {
removed.push(dispatch);
} else {
retained.push(dispatch);
}
}
*registry = retained;
removed
}
fn cancel_modern_http_dispatches(
server: &Server,
registry: &ModernHttpDispatchRegistry,
owner_generation: Option<u64>,
) -> Vec<asupersync::runtime::TaskHandle<()>> {
let dispatches = take_modern_http_dispatches(registry, owner_generation);
for dispatch in &dispatches {
server
.final_subscriptions
.cancel_modern_http_owner(dispatch.owner_generation);
dispatch.request_cancellation.cancel();
dispatch.task.abort();
}
dispatches
.into_iter()
.map(|dispatch| dispatch.task)
.collect()
}
/// A server-bound modern SSE response body.
///
/// The transport body owns peer-drop cancellation. A graceful HTTP
/// `subscriptions/listen` ends with its correlated final response (normally a
/// complete result, or a canonical post-admission error); it never emits an
/// MCP `notifications/cancelled` control.
pub struct ServerHttpSseResponse {
inner: DualEraHttpSseResponse,
request_cancellation: McpRequestCancellation,
terminal_delivery: Arc<FinalSubscriptionTerminalDelivery>,
}
const FINAL_TERMINAL_OPEN: u8 = 0;
const FINAL_TERMINAL_ENQUEUED: u8 = 1;
const FINAL_TERMINAL_DRAINED: u8 = 2;
const FINAL_TERMINAL_FAILED: u8 = 3;
#[derive(Default)]
struct FinalSubscriptionTerminalDelivery {
state: AtomicU8,
completion_state: AtomicU8,
}
impl FinalSubscriptionTerminalDelivery {
fn mark_drained(&self) {
let mut state = self.state.load(Ordering::Acquire);
while matches!(state, FINAL_TERMINAL_OPEN | FINAL_TERMINAL_ENQUEUED) {
match self.state.compare_exchange_weak(
state,
FINAL_TERMINAL_DRAINED,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return,
Err(observed) => state = observed,
}
}
}
/// Modern HTTP ends a graceful `subscriptions/listen` body without an MCP
/// `notifications/cancelled` control. Its terminal response may be the
/// correlated complete result or a canonical post-admission error, so the
/// control half of this receipt is satisfied before that response queues.
fn mark_control_not_required(&self) {
let _ = self.state.compare_exchange(
FINAL_TERMINAL_OPEN,
FINAL_TERMINAL_DRAINED,
Ordering::AcqRel,
Ordering::Acquire,
);
}
fn mark_completion_enqueued(&self) {
let _ = self.completion_state.compare_exchange(
FINAL_TERMINAL_OPEN,
FINAL_TERMINAL_ENQUEUED,
Ordering::AcqRel,
Ordering::Acquire,
);
}
fn mark_completion_drained(&self) {
let mut state = self.completion_state.load(Ordering::Acquire);
// The writer must never settle a response merely because it can parse
// it. Only the dispatcher marks a selected terminal response
// `ENQUEUED`; this makes a post-admission substituted error wait for
// its actual pop and flush just like a complete result.
while state == FINAL_TERMINAL_ENQUEUED {
match self.completion_state.compare_exchange_weak(
state,
FINAL_TERMINAL_DRAINED,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return,
Err(observed) => state = observed,
}
}
}
fn mark_failed(&self) {
let _ = self.state.compare_exchange(
FINAL_TERMINAL_OPEN,
FINAL_TERMINAL_FAILED,
Ordering::AcqRel,
Ordering::Acquire,
);
let _ = self.completion_state.compare_exchange(
FINAL_TERMINAL_OPEN,
FINAL_TERMINAL_FAILED,
Ordering::AcqRel,
Ordering::Acquire,
);
let _ = self.completion_state.compare_exchange(
FINAL_TERMINAL_ENQUEUED,
FINAL_TERMINAL_FAILED,
Ordering::AcqRel,
Ordering::Acquire,
);
let _ = self.state.compare_exchange(
FINAL_TERMINAL_ENQUEUED,
FINAL_TERMINAL_FAILED,
Ordering::AcqRel,
Ordering::Acquire,
);
}
fn is_committed(&self) -> bool {
matches!(
self.state.load(Ordering::Acquire),
FINAL_TERMINAL_ENQUEUED | FINAL_TERMINAL_DRAINED
)
}
#[cfg(test)]
fn is_drained(&self) -> bool {
self.state.load(Ordering::Acquire) == FINAL_TERMINAL_DRAINED
}
fn is_settled(&self) -> bool {
let terminal_state = self.state.load(Ordering::Acquire);
let completion_state = self.completion_state.load(Ordering::Acquire);
terminal_state == FINAL_TERMINAL_FAILED
|| completion_state == FINAL_TERMINAL_FAILED
|| terminal_state == FINAL_TERMINAL_DRAINED
&& completion_state == FINAL_TERMINAL_DRAINED
}
}
/// Cancellation view shared by one public HTTP response body and its MCP
/// request authority.
#[derive(Clone)]
pub struct ServerHttpRequestCancellation {
transport: StreamableHttpRequestCancellation,
request: McpRequestCancellation,
terminal_delivery: Arc<FinalSubscriptionTerminalDelivery>,
}
impl ServerHttpRequestCancellation {
/// Returns whether peer body closure or MCP request cancellation won.
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.transport.is_cancelled()
|| self.terminal_delivery.is_settled()
|| self.request.is_cancel_requested() && !self.terminal_delivery.is_committed()
}
/// Returns the exact JSON-RPC request ID owned by this response body.
#[must_use]
pub fn request_id(&self) -> &RequestId {
self.transport.request_id()
}
/// Checks caller, transport-body, and MCP request cancellation together.
///
/// # Errors
///
/// Returns the transport's cancellation error, or [`TransportError::Cancelled`]
/// when MCP cancellation has closed the response body.
pub fn checkpoint(&self, cx: &Cx) -> Result<(), TransportError> {
self.transport.checkpoint(cx)?;
// A committed terminal sequence keeps the cancelled request live
// until its required HTTP completion frame settles.
if self.terminal_delivery.is_settled()
|| self.request.is_cancel_requested() && !self.terminal_delivery.is_committed()
{
return Err(TransportError::Cancelled);
}
Ok(())
}
}
impl ServerHttpSseResponse {
fn new(
inner: DualEraHttpSseResponse,
request_cancellation: McpRequestCancellation,
terminal_delivery: Arc<FinalSubscriptionTerminalDelivery>,
) -> Self {
Self {
inner,
request_cancellation,
terminal_delivery,
}
}
/// Returns the HTTP response head for this SSE body.
#[must_use]
pub fn response(&self) -> &HttpResponse {
self.inner.response()
}
/// Returns the cancellation view shared by the transport body and MCP request.
#[must_use]
pub fn cancellation(&self) -> ServerHttpRequestCancellation {
ServerHttpRequestCancellation {
transport: self.inner.cancellation(),
request: self.request_cancellation.clone(),
terminal_delivery: Arc::clone(&self.terminal_delivery),
}
}
/// Returns the producer bound to this exact response body.
#[must_use]
pub fn sender(&self) -> StreamableHttpRequestResponseSender {
self.inner.sender()
}
/// Returns whether either the transport body or the graceful terminal
/// sequence reached its completed state.
#[must_use]
pub fn is_finished(&self) -> bool {
self.inner.is_finished()
|| self.terminal_delivery.is_settled()
|| self.request_cancellation.is_cancel_requested()
&& !self.terminal_delivery.is_committed()
}
/// Pops the next queued event, then reports a fully drained terminal
/// sequence as a closed body rather than an indefinitely pending queue.
///
/// # Errors
///
/// Returns the underlying transport failure, or [`ServerHttpEndpointError::Closed`]
/// after the final terminal response has drained.
pub fn pop_event(&self) -> Result<Option<SseEvent>, ServerHttpEndpointError> {
if self.terminal_delivery.is_settled() {
return Err(ServerHttpEndpointError::Closed);
}
let popped = match pop_sse_body_event(&self.inner) {
// A peer/session close cancels the transport body directly; with
// no committed terminal sequence that is this body's clean close,
// not a transport failure the reader must distinguish.
Err(DualEraHttpEndpointError::Transport(TransportError::Cancelled))
if !self.terminal_delivery.is_committed() =>
{
self.terminal_delivery.mark_failed();
return Err(ServerHttpEndpointError::Closed);
}
popped => popped.map_err(ServerHttpEndpointError::from_internal)?,
};
match popped {
Some(event) => {
if final_subscription_terminal_event(&event) {
self.terminal_delivery.mark_drained();
}
if final_subscription_terminal_response_event(&event) {
self.terminal_delivery.mark_completion_drained();
}
Ok(Some(event))
}
None if self.request_cancellation.is_cancel_requested() => {
if self.terminal_delivery.is_committed() {
Ok(None)
} else {
self.terminal_delivery.mark_failed();
Err(ServerHttpEndpointError::Closed)
}
}
None => Ok(None),
}
}
/// Receives the next event while observing both transport and MCP terminal
/// cancellation.
///
/// # Errors
///
/// Returns the underlying transport failure, or [`ServerHttpEndpointError::Closed`]
/// when the response body, MCP request, or caller context is terminal.
pub fn recv_event(&self, cx: &Cx) -> Result<SseEvent, ServerHttpEndpointError> {
loop {
match self.pop_event()? {
Some(event) => return Ok(event),
None if cx.checkpoint().is_err() => {
return Err(ServerHttpEndpointError::Closed);
}
None => std::thread::sleep(Duration::from_millis(1)),
}
}
}
}
fn retryable_http_sse_transport_error(error: &TransportError) -> bool {
matches!(
error,
TransportError::Io(error) if error.kind() == std::io::ErrorKind::WouldBlock
)
}
fn retryable_http_sse_endpoint_error(error: &DualEraHttpEndpointError) -> bool {
matches!(
error,
DualEraHttpEndpointError::Transport(error) if retryable_http_sse_transport_error(error)
)
}
/// Observes one event of a request-owned SSE body without blocking. Every
/// server-side reader (the public response and both live writers) uses this.
fn pop_sse_body_event(
response: &DualEraHttpSseResponse,
) -> Result<Option<SseEvent>, DualEraHttpEndpointError> {
sse_body_observation(response.pop_event())
}
/// A producer may hold the response mailbox for the few instructions needed to
/// commit an event. That contention means "nothing observable yet", not that
/// the body failed; a writer that treated it as fatal dropped a live
/// `subscriptions/listen` connection mid-stream.
fn sse_body_observation(
popped: Result<Option<SseEvent>, DualEraHttpEndpointError>,
) -> Result<Option<SseEvent>, DualEraHttpEndpointError> {
match popped {
Err(error) if retryable_http_sse_endpoint_error(&error) => Ok(None),
popped => popped,
}
}
/// Serializes a request-owned modern HTTP SSE commit through transient
/// transport contention. `WouldBlock` represents a bounded commit gate or
/// mailbox being busy, not peer cancellation. The retry remains bounded so a
/// permanently stalled transport reports its own error instead of pinning a
/// server worker.
fn retry_http_sse_commit<T, E>(
cx: &Cx,
request_cancellation: &McpRequestCancellation,
mut commit: impl FnMut() -> Result<T, E>,
is_retryable: impl Fn(&E) -> bool,
) -> Result<T, E> {
let mut retries = 0;
loop {
match commit() {
Ok(value) => return Ok(value),
Err(error)
if is_retryable(&error)
&& !request_cancellation.is_cancel_requested()
&& cx.checkpoint().is_ok() =>
{
if retries >= MAX_HTTP_SSE_COMMIT_RETRIES {
return Err(error);
}
retries += 1;
std::thread::yield_now();
}
Err(error) => return Err(error),
}
}
}
/// Drives one modern request while retaining its final-progress runtime in the
/// same request owner. The timer is polled alongside the dispatch future, so
/// it neither creates detached work nor waits for handler completion before
/// delivering the newest coalesced update.
async fn await_final_progress_rate_tick<T, F, SendFn>(
cx: &Cx,
request_cancellation: &McpRequestCancellation,
runtime: &handler::FinalProgressRuntime<SendFn>,
future: F,
) -> T
where
F: std::future::Future<Output = T>,
SendFn: Fn(JsonRpcRequest) + Send + Sync,
{
let mut future = std::pin::pin!(future);
let mut tick = Box::pin(asupersync::time::sleep(
cx.now(),
FINAL_PROGRESS_FLUSH_INTERVAL,
));
std::future::poll_fn(|task_cx| {
if let std::task::Poll::Ready(result) = std::future::Future::poll(future.as_mut(), task_cx)
{
return std::task::Poll::Ready(result);
}
if !request_cancellation.is_terminal()
&& matches!(
std::future::Future::poll(tick.as_mut(), task_cx),
std::task::Poll::Ready(())
)
{
runtime.flush_pending();
tick.as_mut().set(asupersync::time::sleep(
cx.now(),
FINAL_PROGRESS_FLUSH_INTERVAL,
));
// Poll the replacement on the next turn instead of discarding an
// immediately-ready result and later repolling a completed Sleep.
// Arrange that turn even when the handler has no wake of its own.
task_cx.waker().wake_by_ref();
}
std::task::Poll::Pending
})
.await
}
fn final_subscription_terminal_event(event: &SseEvent) -> bool {
let Ok(notification) = serde_json::from_str::<JsonRpcRequest>(&event.data) else {
return false;
};
final_subscription_terminal_notification(¬ification)
}
/// Returns whether this request-owned SSE event is its terminal JSON-RPC
/// response. A graceful subscription normally ends in a complete result, but
/// response middleware may substitute the canonical required-capability error
/// after the stream has already been admitted. Both must settle the delivery
/// receipt only after the writer actually consumes the frame.
fn final_subscription_terminal_response_event(event: &SseEvent) -> bool {
serde_json::from_str::<JsonRpcResponse>(&event.data).is_ok()
}
fn final_subscription_completion_response(response: &JsonRpcResponse) -> bool {
response.error.is_none()
&& response.result.as_ref().is_some_and(|result| {
final_subscription_completion_result(result, response.id.as_ref())
})
}
fn final_subscription_completion_result(
result: &serde_json::Value,
response_id: Option<&RequestId>,
) -> bool {
result.get("resultType") == Some(&serde_json::json!("complete"))
&& result
.get("_meta")
.and_then(|metadata| metadata.get(FINAL_SUBSCRIPTION_ID_META_KEY))
.and_then(|subscription_id| {
serde_json::from_value::<RequestId>(subscription_id.clone()).ok()
})
.is_some_and(|subscription_id| response_id == Some(&subscription_id))
}
fn final_subscription_terminal_notification(notification: &JsonRpcRequest) -> bool {
let Ok(ServerNotification::Cancelled(params)) = ServerNotification::decode(notification) else {
return false;
};
params
.meta
.as_ref()
.and_then(|metadata| metadata.get(FINAL_SUBSCRIPTION_ID_META_KEY))
.cloned()
.and_then(|subscription_id| serde_json::from_value::<RequestId>(subscription_id).ok())
.is_some_and(|subscription_id| subscription_id == params.request_id)
}
fn final_subscription_acknowledgement_notification(notification: &JsonRpcRequest) -> bool {
matches!(
ServerNotification::decode(notification),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
)
}
/// A response emitted by [`ServerHttpSession::handle_async`].
pub enum ServerHttpEndpointResponse {
/// A complete response, including modern JSON and legacy POST acknowledgement.
Immediate(HttpResponse),
/// A finite modern request-scoped SSE response body.
ModernSse(ServerHttpSseResponse),
/// A live exact MCP 2024-11-05 SSE stream.
#[cfg(feature = "legacy-2024-11-05")]
LegacySse(DualEraHttpLegacySseResponse),
}
/// Upper bound for live final subscription streams owned by one server.
///
/// Each entry retains only a selected filter, request ID, and a transport
/// notification callback. The callback remains owned by the originating
/// request transport; this registry never creates a background runtime.
const MAX_FINAL_SUBSCRIPTION_STREAMS: usize = 64;
/// Per-subscription bounds for events retained by the registry, including its
/// sole active callback. Concurrent publishers enqueue without waiting for
/// that callback; exceeding either bound retires only that subscription.
const MAX_FINAL_SUBSCRIPTION_QUEUED_EVENTS: usize = 64;
const MAX_FINAL_SUBSCRIPTION_QUEUED_BYTES: usize = 256 * 1024;
/// Modern Streamable HTTP has no protocol session identifier. A listen
/// response nevertheless owns one connection-local cancellation domain, so
/// give its active-request entry a process-local generation rather than
/// collapsing unrelated HTTP response streams that happen to reuse a JSON-RPC
/// request ID.
static NEXT_MODERN_HTTP_STREAM_GENERATION: AtomicU64 = AtomicU64::new(1);
/// Opaque endpoint-local ownership for live modern response bodies. This is
/// intentionally unrelated to JSON-RPC request IDs and never appears on the
/// wire, so listener teardown can own every active SSE dispatch.
static NEXT_LIVE_MODERN_HTTP_RESPONSE_BODY_GENERATION: AtomicU64 = AtomicU64::new(1);
/// Bridges handler catalog mutations onto live `subscriptions/listen` streams.
struct FinalCatalogPublisher {
registry: Arc<FinalSubscriptionRegistry>,
}
impl fastmcp_core::CatalogChangePublisher for FinalCatalogPublisher {
fn publish_catalog_changed(&self, kind: fastmcp_core::McpCatalogKind) -> bool {
let notification = match kind {
fastmcp_core::McpCatalogKind::Tools => ServerNotification::ToolsListChanged(None),
fastmcp_core::McpCatalogKind::Resources => {
ServerNotification::ResourcesListChanged(None)
}
fastmcp_core::McpCatalogKind::Prompts => ServerNotification::PromptsListChanged(None),
};
self.registry.publish(notification).unwrap_or(0) > 0
}
fn publish_resource_updated(&self, uri: &str) -> bool {
let Ok(uri) = fastmcp_protocol::common_types::AbsoluteUri::parse(uri) else {
return false;
};
self.registry
.publish(ServerNotification::ResourceUpdated(
fastmcp_protocol::FinalResourceUpdatedNotificationParams {
uri,
meta: None,
additional: BTreeMap::new(),
},
))
.unwrap_or(0)
> 0
}
}
/// A server-wide registry of request-owned final subscription streams.
///
/// Stdio and modern HTTP both reserve the same bounded entry while sending the
/// mandatory acknowledgement, then make it publishable. Teardown can remove
/// and cancel that opening reservation without waiting on an external sender.
/// Events are cloned and tagged per entry so a notification can never escape
/// its accepted filter or correlation ID.
#[derive(Default)]
struct FinalSubscriptionRegistry {
inner: Mutex<FinalSubscriptionRegistryState>,
terminating: AtomicBool,
}
#[derive(Default)]
struct FinalSubscriptionRegistryState {
next_key: usize,
entries: HashMap<usize, FinalSubscriptionEntry>,
/// Request-body ownership, rather than the JSON-RPC ID, isolates modern
/// Streamable HTTP listens. Different HTTP bodies may legitimately reuse
/// the same JSON-RPC request ID without sharing cancellation authority.
modern_http_owners: HashMap<u64, usize>,
}
#[derive(Clone)]
struct FinalSubscriptionEntry {
registry_key: usize,
subscription_id: RequestId,
modern_http_owner: Option<u64>,
accepted_filter: SubscriptionFilter,
#[cfg(feature = "tasks")]
task_principal: Option<Sha256Digest>,
notification_sender: NotificationSender,
request_cancellation: McpRequestCancellation,
terminal_delivery: Option<Arc<FinalSubscriptionTerminalDelivery>>,
election: Arc<FinalSubscriptionElection>,
}
#[derive(Default)]
struct FinalSubscriptionTerminationReceipt {
terminated: usize,
terminal_deliveries: Vec<Arc<FinalSubscriptionTerminalDelivery>>,
/// Elections the server won. One won while an acknowledgement or event
/// callback was in flight is left `ServerTerminationPending`; that
/// callback's owner completes it, and only while its request is not yet
/// cancelled.
won_elections: Vec<(Arc<FinalSubscriptionElection>, McpRequestCancellation)>,
}
impl FinalSubscriptionTerminationReceipt {
fn is_settled(&self) -> bool {
self.terminal_deliveries
.iter()
.all(|delivery| delivery.is_settled())
}
/// Every path that finishes a won election leaves the pending phase and
/// cancels its request last, so both together mean its owner is done.
fn elections_settled(&self) -> bool {
self.won_elections.iter().all(|(election, cancellation)| {
cancellation.is_cancel_requested()
&& !matches!(
*election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
FinalSubscriptionPhase::ServerTerminationPending(_)
)
})
}
/// Waits, bounded, for the callbacks that own pending elections to finish
/// them. Generic shutdown cancellation must not run before this: it is
/// indistinguishable from a peer cancel and would defeat the server's
/// already-won graceful completion (bd-81cct).
fn wait_for_pending_elections(&self, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while !self.elections_settled() {
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(1));
}
true
}
fn fail_pending(&self) {
for delivery in &self.terminal_deliveries {
if !delivery.is_settled() {
delivery.mark_failed();
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum FinalSubscriptionPhase {
Opening,
/// The acknowledgement has returned and one queued event callback is in
/// flight. New events still join the opening FIFO so they cannot overtake
/// the callback already selected for delivery.
OpeningDelivery,
Active,
/// Exactly one notification callback owns the FIFO drain. The count is
/// retained for the shared terminal-election representation.
EventDelivery(usize),
/// Server termination won and is waiting for the acknowledgement callback
/// (`0`) or this many already-admitted event callbacks (`> 0`) to finish.
ServerTerminationPending(usize),
PeerTerminated,
ServerTerminated,
}
struct FinalSubscriptionElection {
phase: Mutex<FinalSubscriptionPhase>,
opening_events: Mutex<FinalSubscriptionEventQueue>,
graceful_completion: AtomicBool,
}
#[derive(Default)]
struct FinalSubscriptionEventQueue {
events: VecDeque<(JsonRpcRequest, usize)>,
retained_events: usize,
retained_bytes: usize,
}
impl FinalSubscriptionEventQueue {
fn try_push(&mut self, notification: JsonRpcRequest, bytes: usize) -> bool {
if self.retained_events >= MAX_FINAL_SUBSCRIPTION_QUEUED_EVENTS
|| bytes > MAX_FINAL_SUBSCRIPTION_QUEUED_BYTES.saturating_sub(self.retained_bytes)
{
return false;
}
self.retained_events += 1;
self.retained_bytes += bytes;
self.events.push_back((notification, bytes));
true
}
fn pop_front(&mut self) -> Option<(JsonRpcRequest, usize)> {
// Keep the popped event charged until its callback returns. A blocked
// callback cannot permit another full queue in addition to its bytes.
self.events.pop_front()
}
fn finish_delivery(&mut self, bytes: usize) {
self.retained_events = self.retained_events.saturating_sub(1);
self.retained_bytes = self.retained_bytes.saturating_sub(bytes);
}
fn clear(&mut self) {
self.events.clear();
self.retained_events = 0;
self.retained_bytes = 0;
}
}
/// Measure without allocating a second encoded copy or accepting an
/// arbitrarily large application-authored event into a listener's queue.
fn final_subscription_event_bytes<T: serde::Serialize>(notification: &T) -> Option<usize> {
struct BoundedSize(usize);
impl std::io::Write for BoundedSize {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
if bytes.len() > MAX_FINAL_SUBSCRIPTION_QUEUED_BYTES.saturating_sub(self.0) {
return Err(std::io::Error::other(
"subscription event byte limit exceeded",
));
}
self.0 += bytes.len();
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut size = BoundedSize(0);
serde_json::to_writer(&mut size, notification).ok()?;
Some(size.0)
}
impl FinalSubscriptionElection {
fn opening() -> Self {
Self {
phase: Mutex::new(FinalSubscriptionPhase::Opening),
opening_events: Mutex::new(FinalSubscriptionEventQueue::default()),
graceful_completion: AtomicBool::new(false),
}
}
}
/// Caller-owned in-process `subscriptions/listen` stream.
///
/// Keep this handle alive for as long as events should be delivered. Drop
/// unregisters the listener from the server.
pub struct SubscriptionListenHandle {
_lease: FinalSubscriptionLease,
}
#[cfg(test)]
impl SubscriptionListenHandle {
/// The registration this handle keeps alive, for tests that inspect it.
fn lease(&self) -> &FinalSubscriptionLease {
let Self { _lease: lease } = self;
lease
}
}
/// Removes one subscription entry when its request exits for any reason.
struct FinalSubscriptionLease {
registry: Arc<FinalSubscriptionRegistry>,
key: usize,
election: Arc<FinalSubscriptionElection>,
}
impl FinalSubscriptionLease {
fn has_graceful_completion(&self) -> bool {
self.election.graceful_completion.load(Ordering::Acquire)
}
fn is_peer_terminated(&self) -> bool {
matches!(
*self
.election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
FinalSubscriptionPhase::PeerTerminated
)
}
}
impl Drop for FinalSubscriptionLease {
fn drop(&mut self) {
let mut state = self
.registry
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(entry) = state.entries.remove(&self.key) {
if let Some(owner) = entry.modern_http_owner {
state.modern_http_owners.remove(&owner);
}
drop(state);
let mut phase = entry
.election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if matches!(
*phase,
FinalSubscriptionPhase::Opening
| FinalSubscriptionPhase::OpeningDelivery
| FinalSubscriptionPhase::Active
| FinalSubscriptionPhase::EventDelivery(_)
) {
*phase = FinalSubscriptionPhase::PeerTerminated;
entry
.election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
}
}
}
}
fn send_final_subscription_termination_control(
subscription_id: &RequestId,
modern_http_owner: Option<u64>,
terminal_delivery: Option<&Arc<FinalSubscriptionTerminalDelivery>>,
notification_sender: &NotificationSender,
) -> bool {
if modern_http_owner.is_some() {
if let Some(delivery) = terminal_delivery {
delivery.mark_control_not_required();
}
return true;
}
match subscription_cancellation_notification(subscription_id) {
Ok(notification) => {
let sent = catch_extension_unwind(|| notification_sender(notification)).is_ok();
if !sent {
let _ = extension_panic_error("final_subscription_termination_sender");
}
sent
}
Err(_) => false,
}
}
fn complete_final_subscription_server_termination(
election: &FinalSubscriptionElection,
subscription_id: &RequestId,
modern_http_owner: Option<u64>,
terminal_delivery: Option<&Arc<FinalSubscriptionTerminalDelivery>>,
notification_sender: &NotificationSender,
request_cancellation: &McpRequestCancellation,
) -> bool {
let sent = send_final_subscription_termination_control(
subscription_id,
modern_http_owner,
terminal_delivery,
notification_sender,
);
let terminal_committed = terminal_delivery.is_some_and(|delivery| delivery.is_committed());
if !sent || request_cancellation.is_cancel_requested() && !terminal_committed {
if let Some(delivery) = terminal_delivery {
delivery.mark_failed();
}
request_cancellation.cancel();
return false;
}
election.graceful_completion.store(true, Ordering::Release);
request_cancellation.cancel();
true
}
impl FinalSubscriptionRegistry {
#[cfg(all(test, feature = "proxy", feature = "tasks"))]
fn snapshot_for_test(&self) -> serde_json::Value {
let state = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut entries = state
.entries
.values()
.map(|entry| {
serde_json::json!({
"subscriptionId": entry.subscription_id.clone(),
"filter": entry.accepted_filter.clone(),
"modernHttpOwner": entry.modern_http_owner,
})
})
.collect::<Vec<_>>();
entries.sort_by_key(|entry| entry.to_string());
serde_json::json!({
"terminating": self.terminating.load(Ordering::Acquire),
"entries": entries,
})
}
fn remove_entry(&self, key: usize) {
let mut state = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(entry) = state.entries.remove(&key)
&& let Some(owner) = entry.modern_http_owner
{
state.modern_http_owners.remove(&owner);
}
}
fn open(
self: &Arc<Self>,
subscription_id: RequestId,
requested: SubscriptionFilter,
accept_tasks: bool,
modern_http_owner: Option<u64>,
request_cancellation: McpRequestCancellation,
terminal_delivery: Option<Arc<FinalSubscriptionTerminalDelivery>>,
notification_sender: NotificationSender,
) -> McpResult<FinalSubscriptionLease> {
self.open_with_task_principal(
subscription_id,
requested,
accept_tasks,
modern_http_owner,
request_cancellation,
terminal_delivery,
notification_sender,
None,
)
}
fn open_with_task_principal(
self: &Arc<Self>,
subscription_id: RequestId,
requested: SubscriptionFilter,
accept_tasks: bool,
modern_http_owner: Option<u64>,
request_cancellation: McpRequestCancellation,
terminal_delivery: Option<Arc<FinalSubscriptionTerminalDelivery>>,
notification_sender: NotificationSender,
task_principal: Option<Sha256Digest>,
) -> McpResult<FinalSubscriptionLease> {
#[cfg(not(feature = "tasks"))]
let _ = task_principal;
if subscription_id.validate().is_err() {
return Err(McpError::invalid_request(
"subscriptions/listen requires a valid JSON-RPC request id",
));
}
if self.terminating.load(Ordering::Acquire) {
return Err(McpError::request_cancelled());
}
#[cfg(feature = "tasks")]
let accepted_filter = accepted_subscription_filter(&requested, accept_tasks)?;
#[cfg(not(feature = "tasks"))]
let accepted_filter = accepted_subscription_filter(&requested, accept_tasks);
let acknowledgement =
subscription_acknowledgement(subscription_id.clone(), accepted_filter.clone())?;
let acknowledgement = acknowledgement.encode().map_err(|error| {
McpError::internal_error(format!(
"failed to encode subscriptions/listen acknowledgement: {error}"
))
})?;
let mut state = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.terminating.load(Ordering::Acquire) {
return Err(McpError::request_cancelled());
}
if state.entries.len() >= MAX_FINAL_SUBSCRIPTION_STREAMS {
return Err(McpError::new(
McpErrorCode::Custom(RESOURCE_EXHAUSTED_ERROR_CODE),
"final subscription stream capacity exhausted",
));
}
if let Some(owner) = modern_http_owner
&& state.modern_http_owners.contains_key(&owner)
{
return Err(McpError::invalid_request(
"a modern HTTP subscriptions/listen response body is already active",
));
}
let mut key = state.next_key;
while state.entries.contains_key(&key) {
key = key.checked_add(1).unwrap_or(0);
}
state.next_key = key.checked_add(1).unwrap_or(0);
let election = Arc::new(FinalSubscriptionElection::opening());
let previous = state.entries.insert(
key,
FinalSubscriptionEntry {
registry_key: key,
subscription_id: subscription_id.clone(),
modern_http_owner,
accepted_filter,
#[cfg(feature = "tasks")]
task_principal,
notification_sender: notification_sender.clone(),
request_cancellation: request_cancellation.clone(),
terminal_delivery: terminal_delivery.clone(),
election: Arc::clone(&election),
},
);
debug_assert!(previous.is_none());
if let Some(owner) = modern_http_owner {
let previous = state.modern_http_owners.insert(owner, key);
debug_assert!(previous.is_none());
}
drop(state);
// Application-supplied senders are never called under the registry or
// election mutex. A concurrent server shutdown records
// `ServerTerminationPending`; this opener then emits the terminal
// control after the acknowledgement callback returns.
let acknowledgement_sent =
catch_extension_unwind(|| notification_sender(acknowledgement)).is_ok();
if !acknowledgement_sent {
self.remove_entry(key);
let mut phase = election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if matches!(
*phase,
FinalSubscriptionPhase::Opening
| FinalSubscriptionPhase::OpeningDelivery
| FinalSubscriptionPhase::Active
| FinalSubscriptionPhase::ServerTerminationPending(_)
) {
*phase = FinalSubscriptionPhase::PeerTerminated;
}
election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
if let Some(delivery) = &terminal_delivery {
delivery.mark_failed();
}
request_cancellation.cancel();
return Err(extension_panic_error(
"final_subscription_acknowledgement_sender",
));
}
// Publishers append to the bounded FIFO while the acknowledgement or
// an earlier queued callback is in flight. Drain one callback at a
// time while retaining the opening phase so a concurrent publisher
// cannot overtake an already-admitted event.
loop {
let queued_notification;
let queued_bytes;
let mut phase = election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match *phase {
FinalSubscriptionPhase::Opening if !request_cancellation.is_cancel_requested() => {
let mut opening_events = election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some((notification, bytes)) = opening_events.pop_front() {
*phase = FinalSubscriptionPhase::OpeningDelivery;
queued_notification = notification;
queued_bytes = bytes;
} else {
*phase = FinalSubscriptionPhase::Active;
break;
}
}
FinalSubscriptionPhase::ServerTerminationPending(0)
if !request_cancellation.is_cancel_requested() =>
{
election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
*phase = FinalSubscriptionPhase::ServerTerminated;
drop(phase);
if !complete_final_subscription_server_termination(
&election,
&subscription_id,
modern_http_owner,
terminal_delivery.as_ref(),
¬ification_sender,
&request_cancellation,
) {
*election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
FinalSubscriptionPhase::PeerTerminated;
return Err(McpError::request_cancelled());
}
return Ok(FinalSubscriptionLease {
registry: Arc::clone(self),
key,
election,
});
}
FinalSubscriptionPhase::Opening
| FinalSubscriptionPhase::OpeningDelivery
| FinalSubscriptionPhase::Active
| FinalSubscriptionPhase::EventDelivery(_)
| FinalSubscriptionPhase::ServerTerminationPending(_)
| FinalSubscriptionPhase::PeerTerminated
| FinalSubscriptionPhase::ServerTerminated => {
*phase = FinalSubscriptionPhase::PeerTerminated;
election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
drop(phase);
self.remove_entry(key);
if let Some(delivery) = &terminal_delivery {
delivery.mark_failed();
}
request_cancellation.cancel();
return Err(McpError::request_cancelled());
}
}
drop(phase);
let sent = catch_extension_unwind(|| notification_sender(queued_notification)).is_ok();
if !sent {
let _ = extension_panic_error("final_subscription_event_sender");
}
let mut phase = election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.finish_delivery(queued_bytes);
match *phase {
FinalSubscriptionPhase::OpeningDelivery
if sent && !request_cancellation.is_cancel_requested() =>
{
*phase = FinalSubscriptionPhase::Opening;
}
FinalSubscriptionPhase::ServerTerminationPending(1)
if sent && !request_cancellation.is_cancel_requested() =>
{
*phase = FinalSubscriptionPhase::ServerTerminated;
drop(phase);
if !complete_final_subscription_server_termination(
&election,
&subscription_id,
modern_http_owner,
terminal_delivery.as_ref(),
¬ification_sender,
&request_cancellation,
) {
*election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
FinalSubscriptionPhase::PeerTerminated;
return Err(McpError::request_cancelled());
}
return Ok(FinalSubscriptionLease {
registry: Arc::clone(self),
key,
election,
});
}
FinalSubscriptionPhase::OpeningDelivery
| FinalSubscriptionPhase::ServerTerminationPending(_)
| FinalSubscriptionPhase::PeerTerminated
| FinalSubscriptionPhase::ServerTerminated
| FinalSubscriptionPhase::Opening
| FinalSubscriptionPhase::Active
| FinalSubscriptionPhase::EventDelivery(_) => {
*phase = FinalSubscriptionPhase::PeerTerminated;
election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
drop(phase);
self.remove_entry(key);
if let Some(delivery) = &terminal_delivery {
delivery.mark_failed();
}
request_cancellation.cancel();
return Err(McpError::request_cancelled());
}
}
}
Ok(FinalSubscriptionLease {
registry: Arc::clone(self),
key,
election,
})
}
fn publish(&self, notification: ServerNotification) -> McpResult<usize> {
if !is_final_subscription_event(¬ification) {
return Err(McpError::invalid_params(
"only final catalog or resource change notifications may use subscriptions/listen",
));
}
// Bound application-owned data before cloning it into recipient-
// specific metadata and wire objects. The exact tagged frame is
// measured again at each queue, including its subscription ID.
let bounded = match ¬ification {
ServerNotification::ResourceUpdated(params) => {
final_subscription_event_bytes(params).is_some()
}
ServerNotification::ResourcesListChanged(params)
| ServerNotification::ToolsListChanged(params)
| ServerNotification::PromptsListChanged(params) => params
.as_ref()
.is_none_or(|params| final_subscription_event_bytes(params).is_some()),
_ => unreachable!("only subscription events passed admission above"),
};
if !bounded {
return Err(McpError::new(
McpErrorCode::Custom(RESOURCE_EXHAUSTED_ERROR_CODE),
"subscription event byte limit exceeded",
));
}
let targets = {
let state = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state
.entries
.values()
.filter(|entry| subscription_filter_accepts(&entry.accepted_filter, ¬ification))
.cloned()
.collect::<Vec<_>>()
};
// Author all recipient-specific metadata before sending the first
// callback. If one entry cannot be represented within the bounded
// metadata contract, no recipient observes a partial publication.
let mut deliveries = Vec::with_capacity(targets.len());
for entry in targets {
let event = tag_subscription_notification(¬ification, &entry.subscription_id)?;
let wire = event.encode().map_err(|error| {
McpError::internal_error(format!(
"failed to encode final subscription notification: {error}"
))
})?;
deliveries.push((entry, wire));
}
Ok(self.publish_prepared(deliveries, "final_subscription_event_sender"))
}
#[cfg(feature = "tasks")]
fn publish_task(&self, notification: FinalTaskStatusNotification) -> McpResult<usize> {
self.publish_owned_task(notification, None)
}
#[cfg(feature = "tasks")]
fn publish_owned_task(
&self,
notification: FinalTaskStatusNotification,
principal: Option<Sha256Digest>,
) -> McpResult<usize> {
if final_subscription_event_bytes(¬ification).is_none() {
return Err(McpError::new(
McpErrorCode::Custom(RESOURCE_EXHAUSTED_ERROR_CODE),
"subscription event byte limit exceeded",
));
}
let task_id = notification.params.task.base().task_id.as_str();
let targets = {
let state = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state
.entries
.values()
.filter(|entry| {
entry.task_principal == principal
&& task_subscription_ids(&entry.accepted_filter)
.ok()
.flatten()
.is_some_and(|task_ids| {
task_ids
.iter()
.any(|candidate| candidate.as_str() == task_id)
})
})
.cloned()
.collect::<Vec<_>>()
};
let mut deliveries = Vec::with_capacity(targets.len());
for entry in targets {
let event = tag_task_subscription_notification(¬ification, &entry.subscription_id)?;
deliveries.push((entry, event));
}
Ok(self.publish_prepared(deliveries, "final_task_subscription_sender"))
}
fn publish_prepared(
&self,
deliveries: Vec<(FinalSubscriptionEntry, JsonRpcRequest)>,
panic_boundary: &'static str,
) -> usize {
let mut count = 0;
for (entry, notification) in deliveries {
let bytes = final_subscription_event_bytes(¬ification);
let mut phase = entry
.election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match *phase {
FinalSubscriptionPhase::ServerTerminationPending(_)
| FinalSubscriptionPhase::PeerTerminated
| FinalSubscriptionPhase::ServerTerminated => continue,
FinalSubscriptionPhase::Opening
| FinalSubscriptionPhase::OpeningDelivery
| FinalSubscriptionPhase::Active
| FinalSubscriptionPhase::EventDelivery(_) => {}
}
let mut events = entry
.election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if entry.request_cancellation.is_cancel_requested()
|| !bytes.is_some_and(|bytes| events.try_push(notification, bytes))
{
events.clear();
*phase = FinalSubscriptionPhase::PeerTerminated;
drop(events);
drop(phase);
self.retire_failed_subscription(&entry);
continue;
}
if *phase != FinalSubscriptionPhase::Active {
// Opening and active callbacks share the same bounded FIFO.
// A reentrant publisher returns after admission, without
// recursively entering the sender or waiting for itself.
count += 1;
continue;
}
let first = events
.pop_front()
.expect("the active publisher just admitted its first event");
*phase = FinalSubscriptionPhase::EventDelivery(1);
drop(events);
drop(phase);
count += usize::from(self.drain_subscription_events(&entry, first, panic_boundary));
}
count
}
fn retire_failed_subscription(&self, entry: &FinalSubscriptionEntry) {
if let Some(delivery) = &entry.terminal_delivery {
delivery.mark_failed();
}
entry.request_cancellation.cancel();
self.remove_entry(entry.registry_key);
}
/// The publisher that claims an idle subscription drives it synchronously.
/// Concurrent publishers only append; they never invoke its callback in
/// parallel. The return value counts this publisher's first event alone,
/// since each queued event was already counted by its admitting publisher.
fn drain_subscription_events(
&self,
entry: &FinalSubscriptionEntry,
mut next: (JsonRpcRequest, usize),
panic_boundary: &'static str,
) -> bool {
let mut first = true;
let mut first_delivered = false;
loop {
let (notification, bytes) = next;
let sent = catch_extension_unwind(|| (entry.notification_sender)(notification)).is_ok();
if !sent {
let _ = extension_panic_error(panic_boundary);
}
let mut phase = entry
.election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut events = entry
.election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
events.finish_delivery(bytes);
match *phase {
FinalSubscriptionPhase::EventDelivery(1)
if sent && !entry.request_cancellation.is_cancel_requested() =>
{
if first {
first_delivered = true;
}
let Some(queued) = events.pop_front() else {
*phase = FinalSubscriptionPhase::Active;
return first_delivered;
};
next = queued;
first = false;
}
FinalSubscriptionPhase::ServerTerminationPending(1)
if sent && !entry.request_cancellation.is_cancel_requested() =>
{
events.clear();
*phase = FinalSubscriptionPhase::ServerTerminated;
drop(events);
drop(phase);
if complete_final_subscription_server_termination(
&entry.election,
&entry.subscription_id,
entry.modern_http_owner,
entry.terminal_delivery.as_ref(),
&entry.notification_sender,
&entry.request_cancellation,
) {
first_delivered |= first;
} else {
*entry
.election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
FinalSubscriptionPhase::PeerTerminated;
}
return first_delivered;
}
FinalSubscriptionPhase::EventDelivery(_)
| FinalSubscriptionPhase::ServerTerminationPending(_)
| FinalSubscriptionPhase::Opening
| FinalSubscriptionPhase::OpeningDelivery
| FinalSubscriptionPhase::Active
| FinalSubscriptionPhase::PeerTerminated
| FinalSubscriptionPhase::ServerTerminated => {
*phase = FinalSubscriptionPhase::PeerTerminated;
events.clear();
drop(events);
drop(phase);
self.retire_failed_subscription(entry);
return first_delivered;
}
}
}
}
fn terminate(&self) -> usize {
self.terminate_with_receipt().terminated
}
fn terminate_with_receipt(&self) -> FinalSubscriptionTerminationReceipt {
self.terminating.store(true, Ordering::Release);
let entries = {
let mut state = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let entries = std::mem::take(&mut state.entries);
state.modern_http_owners.clear();
entries.into_values().collect::<Vec<_>>()
};
let mut receipt = FinalSubscriptionTerminationReceipt::default();
for entry in entries {
let election = Arc::clone(&entry.election);
let request_cancellation = entry.request_cancellation.clone();
let (terminated, terminal_delivery) = Self::terminate_removed_entry(entry);
if terminated {
receipt.terminated += 1;
receipt.won_elections.push((election, request_cancellation));
}
if let Some(terminal_delivery) = terminal_delivery {
receipt.terminal_deliveries.push(terminal_delivery);
}
}
receipt
}
fn terminate_removed_entry(
entry: FinalSubscriptionEntry,
) -> (bool, Option<Arc<FinalSubscriptionTerminalDelivery>>) {
let mut phase = entry
.election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match *phase {
FinalSubscriptionPhase::Opening => {
if entry.request_cancellation.is_cancel_requested() {
*phase = FinalSubscriptionPhase::PeerTerminated;
entry
.election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
if let Some(delivery) = &entry.terminal_delivery {
delivery.mark_failed();
}
return (false, None);
}
// The acknowledgement callback owns the only ordering point
// at which terminal delivery can safely begin. Record the
// server's winning election and let `open` finish it after the
// callback returns, without waiting under a callback-spanning
// lock.
entry
.election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
*phase = FinalSubscriptionPhase::ServerTerminationPending(0);
(true, entry.terminal_delivery)
}
FinalSubscriptionPhase::OpeningDelivery => {
entry
.election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
if entry.request_cancellation.is_cancel_requested() {
*phase = FinalSubscriptionPhase::PeerTerminated;
if let Some(delivery) = &entry.terminal_delivery {
delivery.mark_failed();
}
return (false, None);
}
*phase = FinalSubscriptionPhase::ServerTerminationPending(1);
(true, entry.terminal_delivery)
}
FinalSubscriptionPhase::Active => {
if entry.request_cancellation.is_cancel_requested() {
*phase = FinalSubscriptionPhase::PeerTerminated;
return (false, None);
}
*phase = FinalSubscriptionPhase::ServerTerminated;
drop(phase);
// A terminal stdio listen queues its correlated cancellation
// control before waking its dispatch to produce the matching
// complete result. Modern HTTP has no in-band cancellation
// notification: its body closure is client cancellation and
// graceful server shutdown sends one terminal response below.
if !complete_final_subscription_server_termination(
&entry.election,
&entry.subscription_id,
entry.modern_http_owner,
entry.terminal_delivery.as_ref(),
&entry.notification_sender,
&entry.request_cancellation,
) {
*entry
.election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
FinalSubscriptionPhase::PeerTerminated;
return (false, None);
}
// The parked dispatch only observes the election through its
// request cancellation; the graceful_completion flag above is
// ordered before this wake so the dispatch converts to the
// terminal completion response instead of a cancelled error.
(true, entry.terminal_delivery)
}
FinalSubscriptionPhase::EventDelivery(in_flight) => {
entry
.election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
if entry.request_cancellation.is_cancel_requested() {
*phase = FinalSubscriptionPhase::PeerTerminated;
if let Some(delivery) = &entry.terminal_delivery {
delivery.mark_failed();
}
return (false, None);
}
*phase = FinalSubscriptionPhase::ServerTerminationPending(in_flight);
(true, entry.terminal_delivery)
}
FinalSubscriptionPhase::ServerTerminationPending(_)
| FinalSubscriptionPhase::PeerTerminated
| FinalSubscriptionPhase::ServerTerminated => (false, None),
}
}
/// Cancels the exact live modern HTTP listen owned by one response body.
/// A missing or already-finished owner is deliberately a no-op and cannot
/// affect another body that reused the same JSON-RPC request ID.
fn cancel_modern_http_owner(&self, owner: u64) -> bool {
let entry = {
let mut state = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let Some(entry_key) = state.modern_http_owners.remove(&owner) else {
return false;
};
state.entries.remove(&entry_key)
};
let Some(entry) = entry else {
return false;
};
let mut phase = entry
.election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if matches!(
*phase,
FinalSubscriptionPhase::Opening
| FinalSubscriptionPhase::OpeningDelivery
| FinalSubscriptionPhase::Active
| FinalSubscriptionPhase::EventDelivery(_)
) {
*phase = FinalSubscriptionPhase::PeerTerminated;
entry
.election
.opening_events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
entry.request_cancellation.cancel();
true
} else {
false
}
}
fn is_terminating(&self) -> bool {
self.terminating.load(Ordering::Acquire)
}
}
fn core_accepted_subscription_filter(requested: &SubscriptionFilter) -> SubscriptionFilter {
SubscriptionFilter {
prompts_list_changed: requested.prompts_list_changed.filter(|accepted| *accepted),
resource_subscriptions: requested.resource_subscriptions.clone(),
resources_list_changed: requested
.resources_list_changed
.filter(|accepted| *accepted),
tools_list_changed: requested.tools_list_changed.filter(|accepted| *accepted),
// Unknown extension filter keys remain schema-open but are never
// activated by the core server execution path.
additional: BTreeMap::new(),
}
}
#[cfg(feature = "tasks")]
fn accepted_subscription_filter(
requested: &SubscriptionFilter,
accept_tasks: bool,
) -> McpResult<SubscriptionFilter> {
let mut accepted = core_accepted_subscription_filter(requested);
if let Some(task_ids) = task_subscription_ids(requested)
.map_err(|_| McpError::invalid_params("invalid Tasks subscription filter"))?
{
if !accept_tasks {
return Err(McpError::invalid_params(
"Tasks subscription filter was not negotiated",
));
}
let mut seen = HashSet::new();
let canonical = task_ids
.into_iter()
.filter(|task_id| seen.insert(task_id.clone()))
.collect();
set_task_subscription_ids(&mut accepted, canonical)
.map_err(|_| McpError::invalid_params("invalid Tasks subscription filter"))?;
}
Ok(accepted)
}
#[cfg(not(feature = "tasks"))]
fn accepted_subscription_filter(
requested: &SubscriptionFilter,
_accept_tasks: bool,
) -> SubscriptionFilter {
core_accepted_subscription_filter(requested)
}
/// Compares the filters that this server can actually admit, rather than their
/// raw wire shapes. In particular, disabled categories and unactivated
/// extension members cannot make an upstream acknowledgement appear narrower;
/// every admitted category plus the resource URI and Tasks ID sets must still
/// match.
#[cfg(all(feature = "proxy", feature = "tasks"))]
fn subscription_filter_admission_matches(
expected: &SubscriptionFilter,
acknowledged: &SubscriptionFilter,
) -> McpResult<bool> {
let expected = accepted_subscription_filter(expected, true)?;
let acknowledged = accepted_subscription_filter(acknowledged, true)?;
let expected_resource_subscriptions = expected
.resource_subscriptions
.as_deref()
.map(|uris| uris.iter().map(String::as_str).collect::<BTreeSet<_>>());
let acknowledged_resource_subscriptions = acknowledged
.resource_subscriptions
.as_deref()
.map(|uris| uris.iter().map(String::as_str).collect::<BTreeSet<_>>());
#[cfg(feature = "tasks")]
let expected_task_ids = task_subscription_ids(&expected)
.map_err(|_| McpError::invalid_params("invalid Tasks subscription filter"))?
.map(|task_ids| task_ids.into_iter().collect::<BTreeSet<_>>());
#[cfg(feature = "tasks")]
let acknowledged_task_ids = task_subscription_ids(&acknowledged)
.map_err(|_| McpError::invalid_params("invalid Tasks subscription filter"))?
.map(|task_ids| task_ids.into_iter().collect::<BTreeSet<_>>());
let core_filters_match = expected.prompts_list_changed == acknowledged.prompts_list_changed
&& expected_resource_subscriptions == acknowledged_resource_subscriptions
&& expected.resources_list_changed == acknowledged.resources_list_changed
&& expected.tools_list_changed == acknowledged.tools_list_changed;
#[cfg(feature = "tasks")]
{
Ok(core_filters_match && expected_task_ids == acknowledged_task_ids)
}
#[cfg(not(feature = "tasks"))]
{
Ok(core_filters_match)
}
}
fn subscription_metadata(
existing: Option<OpenMetadata>,
subscription_id: &RequestId,
) -> McpResult<OpenMetadata> {
let mut entries = existing.map_or_else(BTreeMap::new, |metadata| metadata.entries().clone());
entries.insert(
FINAL_SUBSCRIPTION_ID_META_KEY.to_owned(),
serde_json::to_value(subscription_id).map_err(McpError::from)?,
);
OpenMetadata::try_from_entries(entries)
.map_err(|_| McpError::invalid_params("invalid final subscription metadata"))
}
fn subscription_acknowledgement(
subscription_id: RequestId,
accepted_filter: SubscriptionFilter,
) -> McpResult<ServerNotification> {
Ok(ServerNotification::SubscriptionsAcknowledged(
FinalSubscriptionsAcknowledgedNotificationParams {
meta: Some(subscription_metadata(None, &subscription_id)?),
notifications: accepted_filter,
additional: BTreeMap::new(),
},
))
}
fn subscription_cancellation_notification(
subscription_id: &RequestId,
) -> McpResult<JsonRpcRequest> {
ServerNotification::Cancelled(FinalCancelledNotificationParams {
request_id: subscription_id.clone(),
reason: None,
meta: Some(subscription_metadata(None, subscription_id)?),
additional: BTreeMap::new(),
})
.encode()
.map_err(|error| {
McpError::internal_error(format!(
"failed to encode final subscription cancellation: {error}"
))
})
}
#[cfg(all(feature = "proxy", feature = "tasks"))]
fn catalog_filter_requests_resource_updates(filter: &SubscriptionFilter) -> bool {
filter
.resource_subscriptions
.as_ref()
.is_some_and(|uris| !uris.is_empty())
}
fn is_final_subscription_event(notification: &ServerNotification) -> bool {
matches!(
notification,
ServerNotification::ResourcesListChanged(_)
| ServerNotification::ToolsListChanged(_)
| ServerNotification::PromptsListChanged(_)
| ServerNotification::ResourceUpdated(_)
)
}
fn subscription_filter_accepts(
filter: &SubscriptionFilter,
notification: &ServerNotification,
) -> bool {
match notification {
ServerNotification::ResourcesListChanged(_) => filter.resources_list_changed == Some(true),
ServerNotification::ToolsListChanged(_) => filter.tools_list_changed == Some(true),
ServerNotification::PromptsListChanged(_) => filter.prompts_list_changed == Some(true),
ServerNotification::ResourceUpdated(params) => filter
.resource_subscriptions
.as_ref()
.is_some_and(|uris| uris.iter().any(|uri| uri == params.uri.as_str())),
ServerNotification::Cancelled(_)
| ServerNotification::Progress(_)
| ServerNotification::Message(_)
| ServerNotification::SubscriptionsAcknowledged(_) => false,
}
}
fn tag_subscription_notification(
notification: &ServerNotification,
subscription_id: &RequestId,
) -> McpResult<ServerNotification> {
match notification {
ServerNotification::ResourcesListChanged(params) => {
let mut params = params.clone().unwrap_or_default();
params.meta = Some(subscription_metadata(params.meta, subscription_id)?);
Ok(ServerNotification::ResourcesListChanged(Some(params)))
}
ServerNotification::ToolsListChanged(params) => {
let mut params = params.clone().unwrap_or_default();
params.meta = Some(subscription_metadata(params.meta, subscription_id)?);
Ok(ServerNotification::ToolsListChanged(Some(params)))
}
ServerNotification::PromptsListChanged(params) => {
let mut params = params.clone().unwrap_or_default();
params.meta = Some(subscription_metadata(params.meta, subscription_id)?);
Ok(ServerNotification::PromptsListChanged(Some(params)))
}
ServerNotification::ResourceUpdated(params) => {
let mut params = params.clone();
params.meta = Some(subscription_metadata(params.meta, subscription_id)?);
Ok(ServerNotification::ResourceUpdated(params))
}
ServerNotification::Cancelled(_)
| ServerNotification::Progress(_)
| ServerNotification::Message(_)
| ServerNotification::SubscriptionsAcknowledged(_) => Err(McpError::invalid_params(
"notification does not belong on a final subscription stream",
)),
}
}
#[cfg(feature = "tasks")]
fn tag_task_subscription_notification(
notification: &FinalTaskStatusNotification,
subscription_id: &RequestId,
) -> McpResult<JsonRpcRequest> {
let mut notification = notification.clone();
notification.params.meta = Some(subscription_metadata(
notification.params.meta,
subscription_id,
)?);
serde_json::from_value(serde_json::to_value(notification).map_err(McpError::from)?)
.map_err(McpError::from)
}
/// One exact-2024 HTTP request admitted before it waits for the session mutex.
///
/// The generation binds cleanup to this precise admission. It prevents a late
/// response finalizer from removing a future admission that reused the same
/// wire ID.
#[cfg(feature = "legacy-2024-11-05")]
struct HttpLegacyRequestAdmission {
generation: u64,
cancellation: McpRequestCancellation,
peer_cancellation_protected: bool,
}
#[derive(Default)]
#[cfg(feature = "legacy-2024-11-05")]
struct HttpLegacyRequestAdmissionsInner {
next_generation: u64,
entries: HashMap<CorrelationKey, HttpLegacyRequestAdmission>,
}
/// Session-scoped legacy HTTP cancellation authorities.
///
/// The target POST enters this registry before it waits for the serialized
/// adapter/session path. A separate cancellation POST can therefore select the
/// exact authority throughout admission, mutex wait, active dispatch, and
/// response finalization without taking the session mutex.
#[derive(Default)]
#[cfg(feature = "legacy-2024-11-05")]
struct HttpLegacyRequestAdmissions {
inner: Mutex<HttpLegacyRequestAdmissionsInner>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg(feature = "legacy-2024-11-05")]
enum HttpLegacyAdmissionCancellationDisposition {
NotOwned,
Protected,
Accepted,
AlreadySettled,
}
/// Retains an HTTP admission until its target POST has completed response
/// finalization. Dropping the guard releases only its matching generation.
#[cfg(feature = "legacy-2024-11-05")]
struct HttpLegacyRequestAdmissionGuard {
admissions: Arc<HttpLegacyRequestAdmissions>,
key: CorrelationKey,
generation: u64,
}
#[cfg(feature = "legacy-2024-11-05")]
impl HttpLegacyRequestAdmissions {
fn admit(
self: &Arc<Self>,
request: &JsonRpcRequest,
principal_binding: &SessionPrincipalBinding,
fingerprint: Sha256Digest,
) -> Result<Option<HttpLegacyRequestAdmissionGuard>, ()> {
if request.validate().is_err()
|| request.id.is_none()
|| request.method == "notifications/cancelled"
{
return Ok(None);
}
let request_id = request
.id
.as_ref()
.expect("a correlated legacy HTTP request retains its ID");
let key = request_id.correlation_key().map_err(|_| ())?;
let peer_cancellation_protected = request.method == "initialize";
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// Establish the owner while the cancellation control is excluded from
// both authentication and map lookup. A control POST therefore cannot
// observe a bound owner without the matching per-ID authority, or an
// authority whose owner it cannot yet verify.
if !principal_binding.bind_or_verify(fingerprint) {
return Err(());
}
if inner.entries.contains_key(&key) {
return Err(());
}
let generation = inner.next_generation;
inner.next_generation = inner.next_generation.wrapping_add(1);
let previous = inner.entries.insert(
key.clone(),
HttpLegacyRequestAdmission {
generation,
cancellation: McpRequestCancellation::new(),
peer_cancellation_protected,
},
);
debug_assert!(previous.is_none());
drop(inner);
Ok(Some(HttpLegacyRequestAdmissionGuard {
admissions: Arc::clone(self),
key,
generation,
}))
}
fn admitted_request_cancellation(
&self,
request_id: &RequestId,
) -> Option<McpRequestCancellation> {
let key = request_id.correlation_key().ok()?;
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.get(&key)
.map(|entry| entry.cancellation.clone())
}
fn cancel_locked(
inner: &HttpLegacyRequestAdmissionsInner,
request_id: &RequestId,
) -> HttpLegacyAdmissionCancellationDisposition {
let Ok(key) = request_id.correlation_key() else {
return HttpLegacyAdmissionCancellationDisposition::NotOwned;
};
let Some(entry) = inner.entries.get(&key) else {
return HttpLegacyAdmissionCancellationDisposition::NotOwned;
};
if entry.peer_cancellation_protected {
return HttpLegacyAdmissionCancellationDisposition::Protected;
}
if entry.cancellation.cancel() {
HttpLegacyAdmissionCancellationDisposition::Accepted
} else {
HttpLegacyAdmissionCancellationDisposition::AlreadySettled
}
}
fn cancel_all(&self) {
let cancellations = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.values()
.map(|entry| entry.cancellation.clone())
.collect::<Vec<_>>();
for cancellation in cancellations {
cancellation.cancel();
}
}
#[cfg(test)]
fn contains(&self, request_id: &RequestId) -> bool {
let Ok(key) = request_id.correlation_key() else {
return false;
};
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.contains_key(&key)
}
fn release(&self, key: &CorrelationKey, generation: u64) {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if inner
.entries
.get(key)
.is_some_and(|entry| entry.generation == generation)
{
inner.entries.remove(key);
}
}
}
#[cfg(feature = "legacy-2024-11-05")]
impl Drop for HttpLegacyRequestAdmissionGuard {
fn drop(&mut self) {
self.admissions.release(&self.key, self.generation);
}
}
/// Cancellation authority deliberately kept outside the session mutex.
///
/// A legacy client sends cancellation on a separate POST connection while the
/// request it targets may be queued for or running under the session lock. The
/// control frame authenticates first, then resolves the session admission map
/// before falling back to the server active-request registry.
#[derive(Clone)]
#[cfg(feature = "legacy-2024-11-05")]
struct HttpLegacyCancellationControl {
server: Arc<Server>,
session_id: u64,
http_session_id: String,
legacy_message_path: String,
legacy_lifecycle: DualEraHttpLegacyLifecycle,
session_principal: SessionPrincipalBinding,
max_body_size: usize,
admissions: Arc<HttpLegacyRequestAdmissions>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl HttpLegacyCancellationControl {
/// Revokes every exact-2024 request admitted by this SSE peer without
/// taking the serialized `ServerHttpSession` mutex. The transport must
/// use this on peer close because an admitted synchronous handler may be
/// holding that mutex while it waits to observe cancellation.
fn cancel_all_admitted_requests(&self) {
self.admissions.cancel_all();
}
fn handle(&self, cx: &Cx, request: &HttpRequest) -> Option<HttpResponse> {
let mut notification = match admit_legacy_2024_http_post(
request,
&self.legacy_message_path,
&self.http_session_id,
self.max_body_size,
) {
Ok(Legacy2024HttpPostEnvelope::ClientMessage(notification)) => notification,
Ok(Legacy2024HttpPostEnvelope::Response(_)) => return None,
Err(response) => return Some(response),
};
if notification.id.is_some() || notification.method != "notifications/cancelled" {
return None;
}
Some(
self.legacy_lifecycle
.commit_if_live(|| {
let (cancellation, disposition) = {
// The target admission binds its owner and inserts its
// exact authority under this same lock. Holding it through
// authentication makes that transition one ordered operation.
let admissions = self
.admissions
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let authorization = match transport_authorization_from_http_request(request)
{
Ok(authorization) => authorization,
Err(response) => return response,
};
let receipt = match self.server.preauthenticate_http_request(
cx,
¬ification,
&authorization,
) {
Ok(receipt) => receipt,
Err(response) => return response,
};
let cancellation =
match self.server.authenticate_cancelled_control_notification(
cx,
&self.session_principal,
ProtocolEra::Legacy2024,
&mut notification,
Some(&receipt),
) {
Ok(cancellation) => cancellation,
Err(_) => return HttpResponse::bad_request(),
};
let request_id = Server::cancellation_wire_request_id(&cancellation);
let disposition =
HttpLegacyRequestAdmissions::cancel_locked(&admissions, request_id);
(cancellation, disposition)
};
if matches!(
disposition,
HttpLegacyAdmissionCancellationDisposition::NotOwned
) {
self.server
.handle_cancellation_wire_notification(self.session_id, cancellation);
}
HttpResponse::new(HttpStatus::ACCEPTED)
})
.unwrap_or_else(|| HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE)),
)
}
}
#[cfg(feature = "legacy-2024-11-05")]
struct LiveHttpSession {
/// `None` while the owning connection child is driving request-owned
/// handler futures. Cancellation and reverse-response routing stay
/// outside this slot so they never wait on that exclusive borrow.
session: Mutex<Option<ServerHttpSession>>,
session_available: Condvar,
/// Listener or peer teardown has removed this session from admission.
///
/// A request may temporarily own `session` outside the mutex. Marking the
/// shared shell before registry evacuation ensures that request closes the
/// owned session instead of restoring a detached live SSE body.
closing: AtomicBool,
cancellation: HttpLegacyCancellationControl,
/// Reverse request correlation is intentionally outside `session`.
///
/// SAFETY: an exact-2024 handler may synchronously wait for a
/// `sampling/createMessage` or `roots/list` response while its originating
/// POST owns the serialized session mutex. The matching response POST must
/// therefore route through this independently synchronized registry before
/// attempting session mutation. Every nonmatching response continues
/// through `ServerHttpSession::handle_async`, preserving serialized adapter and
/// session-state mutation.
legacy_pending_requests: Arc<PendingRequests>,
}
#[cfg(feature = "legacy-2024-11-05")]
enum LiveHttpSessionTake {
Acquired(Box<ServerHttpSession>),
Cancelled,
Unavailable,
}
#[cfg(feature = "legacy-2024-11-05")]
fn take_live_http_session(
cx: &Cx,
session: &LiveHttpSession,
request_cancellation: Option<&McpRequestCancellation>,
) -> LiveHttpSessionTake {
let mut slot = session
.session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
loop {
if request_cancellation.is_some_and(McpRequestCancellation::is_cancel_requested) {
return LiveHttpSessionTake::Cancelled;
}
if session.closing.load(Ordering::Acquire)
|| cx.is_cancel_requested()
|| !session.cancellation.legacy_lifecycle.is_live()
{
return LiveHttpSessionTake::Unavailable;
}
if let Some(owned) = slot.take() {
return LiveHttpSessionTake::Acquired(Box::new(owned));
}
(slot, _) = session
.session_available
.wait_timeout(slot, HTTP_ACCEPT_CANCEL_POLL)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
#[cfg(feature = "legacy-2024-11-05")]
async fn take_live_http_session_async(
cx: &Cx,
session: &LiveHttpSession,
request_cancellation: Option<&McpRequestCancellation>,
) -> LiveHttpSessionTake {
loop {
{
let mut slot = session
.session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if request_cancellation.is_some_and(McpRequestCancellation::is_cancel_requested) {
return LiveHttpSessionTake::Cancelled;
}
if session.closing.load(Ordering::Acquire)
|| cx.is_cancel_requested()
|| !session.cancellation.legacy_lifecycle.is_live()
{
return LiveHttpSessionTake::Unavailable;
}
if let Some(owned) = slot.take() {
return LiveHttpSessionTake::Acquired(Box::new(owned));
}
}
// Caller-owned async handlers must stay on the inbound runtime, but
// waiting for this serialized session must yield that reactor so a
// reverse response or cancellation connection can still run.
asupersync::time::sleep(cx.now(), HTTP_ACCEPT_CANCEL_POLL).await;
}
}
#[cfg(feature = "legacy-2024-11-05")]
fn restore_live_http_session(
session: &LiveHttpSession,
mut owned: ServerHttpSession,
) -> Vec<asupersync::runtime::TaskHandle<()>> {
let mut slot = session
.session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
debug_assert!(slot.is_none());
if session.closing.load(Ordering::Acquire) {
drop(slot);
let dispatches = owned.begin_close();
session.session_available.notify_all();
return dispatches;
}
*slot = Some(owned);
drop(slot);
session.session_available.notify_one();
Vec::new()
}
#[cfg(feature = "legacy-2024-11-05")]
type LiveHttpSessionRegistry = Arc<Mutex<HashMap<String, Arc<LiveHttpSession>>>>;
const MODERN_HTTP_RESPONSE_BODY_TTL: Duration = Duration::from_mins(15);
const MODERN_HTTP_SESSION_REAP_INTERVAL: Duration = Duration::from_secs(30);
/// Process-wide bounds for synchronous handler dispatch bridges.
///
/// `Cx::spawn_blocking` deliberately runs inline when an embedding runtime has
/// no blocking pool. That deterministic fallback is useful in the lab, but a
/// request must not let one synchronous handler occupy the reactor that
/// accepts its cancellation or reverse response. This lazy Asupersync
/// pool supplies only that missing blocking authority; it does not own an
/// async runtime or any transport task.
const MAX_BLOCKING_DISPATCH_THREADS: usize = 64;
const MAX_BLOCKING_DISPATCHES: usize = 256;
static BLOCKING_DISPATCHES: AtomicUsize = AtomicUsize::new(0);
struct BlockingDispatchPermit;
impl Drop for BlockingDispatchPermit {
fn drop(&mut self) {
BLOCKING_DISPATCHES.fetch_sub(1, Ordering::AcqRel);
}
}
fn try_reserve_blocking_dispatch() -> Option<BlockingDispatchPermit> {
BLOCKING_DISPATCHES
.try_update(Ordering::AcqRel, Ordering::Acquire, |current| {
(current < MAX_BLOCKING_DISPATCHES).then_some(current + 1)
})
.ok()
.map(|_| BlockingDispatchPermit)
}
fn blocking_dispatch_pool() -> asupersync::runtime::BlockingPoolHandle {
static POOL: std::sync::OnceLock<asupersync::runtime::BlockingPool> =
std::sync::OnceLock::new();
POOL.get_or_init(|| asupersync::runtime::BlockingPool::new(0, MAX_BLOCKING_DISPATCH_THREADS))
.handle()
}
struct BlockingTaskGuard(asupersync::runtime::BlockingTaskHandle);
impl Drop for BlockingTaskGuard {
fn drop(&mut self) {
self.0.cancel();
}
}
#[cfg(feature = "legacy-2024-11-05")]
enum LegacyHttpBlockingDispatchOutcome {
Response(HttpResponse),
Panicked,
}
#[cfg(feature = "legacy-2024-11-05")]
async fn quarantine_panicked_live_http_session(
cx: &Cx,
sessions: &LiveHttpSessionRegistry,
session_id: &str,
) {
let session = sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(session_id);
if let Some(session) = session {
session.closing.store(true, Ordering::Release);
session.session_available.notify_all();
session.cancellation.cancel_all_admitted_requests();
session.legacy_pending_requests.cancel_all();
let owned = session
.session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
if let Some(mut owned) = owned {
owned.close(cx).await;
}
}
}
#[cfg(feature = "legacy-2024-11-05")]
async fn run_live_http_legacy_blocking_dispatch<F>(
cx: &Cx,
sessions: &LiveHttpSessionRegistry,
panic_session_id: Option<String>,
blocking_dispatch_permit: BlockingDispatchPermit,
dispatch: F,
) -> Result<HttpResponse, ()>
where
F: FnOnce(Cx) -> HttpResponse + Send + 'static,
{
let panic_sessions = Arc::clone(sessions);
let mut dispatch = cx
.spawn(move |dispatch_cx| async move {
let blocking_cx = dispatch_cx.clone();
let (dispatch_sender, mut dispatch_receiver) = asupersync::channel::oneshot::channel();
let blocking_dispatch = blocking_dispatch_pool().spawn(move || {
let _blocking_dispatch_permit = blocking_dispatch_permit;
// A pool thread is never the async driver (bd-6rfrg).
let _lane = fastmcp_core::runtime::enter_blocking_lane();
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
dispatch(blocking_cx)
}))
.map_or_else(
|_| LegacyHttpBlockingDispatchOutcome::Panicked,
LegacyHttpBlockingDispatchOutcome::Response,
);
let _ = dispatch_sender.send_blocking(outcome);
});
let blocking_dispatch_guard = BlockingTaskGuard(blocking_dispatch);
// A claimed blocking task cannot be force-stopped. Keep this
// region child alive until the pool records terminal completion,
// even after its Cx has been cancelled. The worker sends before
// it returns, so observing `is_done` establishes that the final
// channel drain cannot race an in-flight send.
while !blocking_dispatch_guard.0.is_done() {
asupersync::time::sleep(dispatch_cx.now(), Duration::from_millis(1)).await;
// Sleep becomes ready immediately after cancellation. Yield
// explicitly so retained blocking work cannot monopolize a
// current-thread runtime while the listener transfers it.
if dispatch_cx.is_cancel_requested() {
asupersync::runtime::yield_now().await;
}
}
let outcome = dispatch_receiver.try_recv().ok();
drop(blocking_dispatch_guard);
match outcome {
Some(LegacyHttpBlockingDispatchOutcome::Response(response)) => response,
Some(LegacyHttpBlockingDispatchOutcome::Panicked) => {
if let Some(session_id) = panic_session_id.as_deref() {
quarantine_panicked_live_http_session(
&dispatch_cx,
&panic_sessions,
session_id,
)
.await;
}
HttpResponse::internal_error()
}
None => HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE),
}
})
.map_err(|_| ())?;
dispatch.join(cx).await.map_err(|_| ())
}
/// One response-body-owned modern HTTP session.
///
/// Modern Streamable HTTP has no listener-visible session identifier. This
/// owner exists only while an SSE response body needs its request-local
/// dispatch lifecycle. The endpoint, rather than this body, owns continuation
/// state across later stateless POSTs.
struct LiveModernHttpSession {
server: Arc<Server>,
session: Mutex<ServerHttpSession>,
expires_at: Mutex<Instant>,
modern_dispatches: ModernHttpDispatchRegistry,
/// Phase one starts listener shutdown and rejects new work without
/// interrupting an already-enqueued terminal SSE control frame.
closing: AtomicBool,
/// Phase two owns destructive endpoint/dispatch teardown after the
/// listener's bounded terminal-control drain.
finalized: AtomicBool,
}
struct LiveModernHttpSessionRegistryState {
/// Every admitted response body is keyed only by a local opaque
/// generation, never by a client-visible modern session identifier.
sessions: Mutex<HashMap<u64, Arc<LiveModernHttpSession>>>,
/// Aborted session dispatches remain listener-owned until their task
/// completion is observed. Dropping a task handle would detach it.
retired_dispatches: Mutex<Vec<asupersync::runtime::TaskHandle<()>>>,
/// Closes response-body admission before phase one evacuates the current
/// map, so a concurrent SSE POST cannot escape listener-owned teardown.
closing: AtomicBool,
}
impl LiveModernHttpSessionRegistryState {
fn new() -> Self {
Self {
sessions: Mutex::new(HashMap::new()),
retired_dispatches: Mutex::new(Vec::new()),
closing: AtomicBool::new(false),
}
}
fn retain_retired_dispatches(&self, dispatches: Vec<asupersync::runtime::TaskHandle<()>>) {
self.retired_dispatches
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.extend(dispatches);
}
fn register_response_body(
&self,
generation: u64,
session: Arc<LiveModernHttpSession>,
) -> Result<(), Arc<LiveModernHttpSession>> {
if self.closing.load(Ordering::Acquire) {
return Err(session);
}
let mut sessions = self
.sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.closing.load(Ordering::Acquire) || sessions.contains_key(&generation) {
return Err(session);
}
sessions.insert(generation, session);
Ok(())
}
fn take_response_body(&self, generation: u64) -> Option<Arc<LiveModernHttpSession>> {
self.sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&generation)
}
fn reap_retired_dispatches(&self) {
let mut dispatches = self
.retired_dispatches
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut active = Vec::with_capacity(dispatches.len());
for mut dispatch in std::mem::take(&mut *dispatches) {
if matches!(dispatch.try_join(), Ok(None)) {
active.push(dispatch);
}
}
*dispatches = active;
}
fn take_retired_dispatches(&self) -> Vec<asupersync::runtime::TaskHandle<()>> {
std::mem::take(
&mut *self
.retired_dispatches
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
)
}
}
type LiveModernHttpSessionRegistry = Arc<LiveModernHttpSessionRegistryState>;
impl LiveModernHttpSession {
fn new(session: ServerHttpSession) -> Self {
let modern_dispatches = Arc::clone(&session.modern_dispatches);
Self {
server: Arc::clone(&session.server),
session: Mutex::new(session),
expires_at: Mutex::new(Instant::now() + MODERN_HTTP_RESPONSE_BODY_TTL),
modern_dispatches,
closing: AtomicBool::new(false),
finalized: AtomicBool::new(false),
}
}
fn is_expired(&self, now: Instant) -> bool {
*self
.expires_at
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
<= now
}
#[cfg(test)]
fn is_closing(&self) -> bool {
self.closing.load(Ordering::Acquire)
}
/// Starts listener/TTL shutdown without closing the response queues that
/// still own a server-elected terminal subscription frame.
fn begin_shutdown_invalidation(&self) {
self.closing.store(true, Ordering::Release);
}
fn register_modern_dispatch(
&self,
dispatch: OwnedModernHttpDispatch,
) -> Result<(), OwnedModernHttpDispatch> {
if self.closing.load(Ordering::Acquire) {
return Err(dispatch);
}
let mut dispatches = self
.modern_dispatches
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if self.closing.load(Ordering::Acquire) {
drop(dispatches);
return Err(dispatch);
}
dispatches.push(dispatch);
Ok(())
}
fn cancel_modern_dispatch(
&self,
owner_generation: u64,
) -> Vec<asupersync::runtime::TaskHandle<()>> {
cancel_modern_http_dispatches(
&self.server,
&self.modern_dispatches,
Some(owner_generation),
)
}
fn reap_modern_dispatches(&self) {
let mut dispatches = self
.modern_dispatches
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut active = Vec::with_capacity(dispatches.len());
for mut dispatch in std::mem::take(&mut *dispatches) {
if matches!(dispatch.task.try_join(), Ok(None)) {
active.push(dispatch);
}
}
*dispatches = active;
}
/// Performs phase two after the bounded terminal-control drain, or
/// immediately for TTL/peer-close teardown where no drain is required.
fn finish_close(&self) -> Vec<asupersync::runtime::TaskHandle<()>> {
self.begin_shutdown_invalidation();
if self.finalized.swap(true, Ordering::AcqRel) {
return Vec::new();
}
let dispatches = cancel_modern_http_dispatches(&self.server, &self.modern_dispatches, None);
match self.session.try_lock() {
Ok(mut session) => {
let _ = session.begin_close();
}
Err(std::sync::TryLockError::Poisoned(poisoned)) => {
let _ = poisoned.into_inner().begin_close();
}
Err(std::sync::TryLockError::WouldBlock) => {
// The detached request still owns this `Arc`; it will finish
// its synchronous section and fail closed on session drop.
}
}
dispatches
}
}
fn close_detached_modern_http_session(
registry: &LiveModernHttpSessionRegistry,
session: Arc<LiveModernHttpSession>,
) {
registry.retain_retired_dispatches(session.finish_close());
}
fn expire_live_modern_http_sessions(registry: &LiveModernHttpSessionRegistry) {
let now = Instant::now();
let expired = {
let mut sessions = registry
.sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let expired_ids = sessions
.iter()
.filter_map(|(id, session)| session.is_expired(now).then_some(*id))
.collect::<Vec<_>>();
expired_ids
.into_iter()
.filter_map(|id| sessions.remove(&id))
.collect::<Vec<_>>()
};
for session in expired {
close_detached_modern_http_session(registry, session);
}
registry.reap_retired_dispatches();
}
struct HttpConnectionLimiter {
limit: usize,
active: AtomicUsize,
}
impl HttpConnectionLimiter {
const fn new(limit: usize) -> Self {
Self {
limit,
active: AtomicUsize::new(0),
}
}
fn try_acquire(self: &Arc<Self>) -> Option<HttpConnectionPermit> {
let mut active = self.active.load(Ordering::Acquire);
loop {
if active >= self.limit {
return None;
}
match self.active.compare_exchange_weak(
active,
active + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => {
return Some(HttpConnectionPermit {
limiter: Arc::clone(self),
});
}
Err(observed) => active = observed,
}
}
}
}
struct HttpConnectionPermit {
limiter: Arc<HttpConnectionLimiter>,
}
impl Drop for HttpConnectionPermit {
fn drop(&mut self) {
self.limiter.active.fetch_sub(1, Ordering::AcqRel);
}
}
/// Tracks every connection child admitted by one live HTTP listener.
///
/// `TaskHandle` drop intentionally detaches in asupersync. The listener must
/// therefore retain every handle until it has observed normal completion or
/// the listener has reported an explicit nonquiescent outcome. The enclosing
/// caller-owned `Cx` region remains the runtime owner in the latter case;
/// shutdown never transfers unrelated work to a process-global registry.
#[derive(Default)]
struct HttpConnectionChildren {
tasks: Vec<asupersync::runtime::TaskHandle<()>>,
/// Panic and handle-consumption failures already observed by a nonblocking
/// settlement poll. Cancellation is expected after listener shutdown and
/// therefore is deliberately not recorded as a failed child.
terminal_failures: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HttpConnectionShutdown {
Quiescent,
Nonquiescent { remaining: usize },
}
/// The state of a caller-owned nonquiescent HTTP shutdown handle.
///
/// A `Pending` result means the listener's bounded shutdown drain expired,
/// but the connection task remains owned by this handle and its enclosing
/// asupersync region. It was neither detached nor transferred to global state.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HttpShutdownSettlement {
/// Every retained connection child has completed and been observed.
Settled,
/// Every retained child has reached a terminal state, but one or more
/// children panicked or violated the handle's single-consumption contract.
Failed { failures: usize },
/// The retained children are still live.
Pending { remaining: usize },
}
/// Caller-owned settlement authority for a bounded, nonquiescent HTTP stop.
///
/// This handle retains every connection child that did not cooperate with the
/// listener's shutdown cancellation before its bounded drain elapsed. Call
/// [`Self::poll_settlement`] or [`Self::settle_for`] for bounded observation,
/// or [`Self::settle`] when the caller deliberately elects to await eventual
/// quiescence. Dropping the handle does not create FastMCP global retention;
/// the caller's asupersync region remains the task owner.
#[must_use = "a nonquiescent HTTP shutdown owns child handles that must be settled"]
pub struct HttpNonquiescentShutdown {
children: HttpConnectionChildren,
listener_error: Option<McpError>,
}
impl HttpNonquiescentShutdown {
/// Returns the listener failure that accompanied this shutdown, if any.
#[must_use]
pub fn listener_error(&self) -> Option<&McpError> {
self.listener_error.as_ref()
}
/// Reaps completed children without waiting.
#[must_use]
pub fn poll_settlement(&mut self) -> HttpShutdownSettlement {
self.children.reap_finished();
if self.children.tasks.is_empty() {
if self.children.terminal_failures.is_empty() {
HttpShutdownSettlement::Settled
} else {
HttpShutdownSettlement::Failed {
failures: self.children.terminal_failures.len(),
}
}
} else {
HttpShutdownSettlement::Pending {
remaining: self.children.tasks.len(),
}
}
}
/// Waits no longer than `timeout` for cooperative completion.
pub async fn settle_for(&mut self, timeout: Duration) -> HttpShutdownSettlement {
let deadline = Instant::now() + timeout;
loop {
let settlement = self.poll_settlement();
if matches!(
settlement,
HttpShutdownSettlement::Settled | HttpShutdownSettlement::Failed { .. }
) || Instant::now() >= deadline
{
return settlement;
}
asupersync::runtime::yield_now().await;
}
}
/// Waits until each retained child has reached a terminal state.
///
/// This is intentionally an explicit caller choice: synchronous code can
/// ignore cancellation indefinitely, so FastMCP never performs this
/// unbounded join inside the public listener shutdown path.
pub async fn settle(mut self, cx: &Cx) -> McpResult<()> {
self.children.reap_finished();
let mut failures = std::mem::take(&mut self.children.terminal_failures);
for mut child in std::mem::take(&mut self.children.tasks) {
match child.join(cx).await {
Ok(()) | Err(asupersync::runtime::JoinError::Cancelled(_)) => {}
Err(error) => failures.push(error.to_string()),
}
}
if failures.is_empty() {
Ok(())
} else {
Err(McpError::internal_error(format!(
"nonquiescent HTTP child settlement failed: {}",
failures.join("; ")
)))
}
}
}
/// The complete outcome of [`BoundHttpServer::serve`].
///
/// Normal shutdown returns [`Self::Quiescent`]. A bounded timeout returns
/// [`Self::Nonquiescent`] and transfers the still-live child handles directly
/// to the caller, preserving structured ownership without a global registry.
#[must_use = "a nonquiescent HTTP shutdown transfers child handles that must be settled"]
pub enum HttpServerShutdown {
/// The listener stopped and every connection child was joined.
Quiescent,
/// The listener stopped, but this caller now owns explicit settlement of
/// the retained connection children.
Nonquiescent(HttpNonquiescentShutdown),
}
impl HttpConnectionChildren {
fn reap_finished(&mut self) {
let mut active = Vec::with_capacity(self.tasks.len());
for mut task in std::mem::take(&mut self.tasks) {
match task.try_join() {
Ok(None) => active.push(task),
Ok(Some(())) | Err(asupersync::runtime::JoinError::Cancelled(_)) => {}
Err(error) => self.terminal_failures.push(error.to_string()),
}
}
self.tasks = active;
}
/// Waits only for children which cooperate with the caller's cancellation.
///
/// Do not abort the remaining children when the deadline elapses.
/// `Cx::spawn_blocking` implements soft cancellation, so aborting its
/// wrapper would discard the result while the synchronous closure still
/// runs. Their caller-owned region remains responsible for eventual
/// quiescence; this listener reports the bounded nonquiescent outcome
/// rather than creating a global orphan-retention path.
async fn drain_cooperative_shutdown(&mut self) -> HttpConnectionShutdown {
let deadline = Instant::now() + HTTP_CONNECTION_SHUTDOWN_TIMEOUT;
loop {
self.reap_finished();
if self.tasks.is_empty() {
return HttpConnectionShutdown::Quiescent;
}
if Instant::now() >= deadline {
return HttpConnectionShutdown::Nonquiescent {
remaining: self.tasks.len(),
};
}
asupersync::runtime::yield_now().await;
}
}
async fn drain_terminal_controls(&mut self, receipt: &FinalSubscriptionTerminationReceipt) {
let deadline = Instant::now() + HTTP_TERMINAL_DRAIN_TIMEOUT;
while !receipt.is_settled() && Instant::now() < deadline {
self.reap_finished();
if self.tasks.is_empty() {
break;
}
asupersync::runtime::yield_now().await;
}
receipt.fail_pending();
}
}
/// Closes every exact-2024 session still retained by the live listener.
///
/// A connection abort can interrupt `serve_http_connection` before its normal
/// post-SSE removal path runs. Evacuating the registry prevents a late legacy
/// POST from observing a detached-but-still-routable session. A busy legacy
/// handler may hold its session mutex while observing the server's earlier
/// cancellation signal, so this cleanup never waits on that mutex.
#[cfg(feature = "legacy-2024-11-05")]
async fn close_live_http_sessions(cx: &Cx, sessions: &LiveHttpSessionRegistry) {
let sessions = {
let mut sessions = sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
std::mem::take(&mut *sessions)
};
let mut dispatches = Vec::new();
for session in sessions.into_values() {
session.closing.store(true, Ordering::Release);
session.session_available.notify_all();
// A busy exact-2024 handler may own `session.session`. Cancellation
// authority is deliberately independent so listener shutdown never
// waits on that serialized mutex before revoking admitted work.
session.cancellation.cancel_all_admitted_requests();
match session.session.try_lock() {
Ok(mut session) => {
if let Some(session) = session.as_mut() {
dispatches.extend(session.begin_close());
}
}
Err(std::sync::TryLockError::Poisoned(poisoned)) => {
if let Some(session) = poisoned.into_inner().as_mut() {
dispatches.extend(session.begin_close());
}
}
Err(std::sync::TryLockError::WouldBlock) => {
// The owning connection child is cancelled and joined below.
// Its local `ServerHttpSession` then closes on drop without
// reintroducing this registry entry.
}
}
}
for mut dispatch in dispatches {
let _ = dispatch.join(cx).await;
}
}
/// Evacuates the listener-owned modern response-body registry before waiting
/// for terminal SSE control frames to flush. Listener shutdown has already
/// invalidated MRTR continuations separately.
fn detach_live_modern_http_sessions(
sessions: &LiveModernHttpSessionRegistry,
) -> Vec<Arc<LiveModernHttpSession>> {
sessions.closing.store(true, Ordering::Release);
let sessions = {
let mut sessions = sessions
.sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
std::mem::take(&mut *sessions)
.into_values()
.collect::<Vec<_>>()
};
for session in &sessions {
session.begin_shutdown_invalidation();
}
sessions
}
/// Completes modern HTTP session teardown after the terminal-result drain.
///
/// Phase one already removed the response bodies from admission. This phase
/// may now cancel live SSE dispatches without suppressing the complete result
/// that was granted the listener's bounded drain window.
async fn finish_live_modern_http_sessions(
registry: &LiveModernHttpSessionRegistry,
sessions: Vec<Arc<LiveModernHttpSession>>,
) -> Vec<asupersync::runtime::TaskHandle<()>> {
let mut dispatches = registry.take_retired_dispatches();
for session in sessions {
dispatches.extend(session.finish_close());
}
let deadline = Instant::now() + HTTP_TERMINAL_DRAIN_TIMEOUT;
let mut pending = dispatches;
while !pending.is_empty() && Instant::now() < deadline {
let mut active = Vec::with_capacity(pending.len());
for mut dispatch in pending {
if matches!(dispatch.try_join(), Ok(None)) {
active.push(dispatch);
}
}
pending = active;
if !pending.is_empty() {
asupersync::runtime::yield_now().await;
}
}
// A dispatch which outlives this bounded terminal-control drain remains
// caller-owned shutdown debt. Do not abort and join it here: an abort is
// cooperative for a blocking wrapper, and joining after the deadline
// would turn the advertised bounded listener shutdown into an unbounded
// wait. `BoundHttpServer::serve` transfers these handles into its
// `HttpNonquiescentShutdown` outcome instead.
pending
}
/// Collects dispatches retired concurrently with listener shutdown.
///
/// The reaper and connection children are drained before this runs, so no
/// task can subsequently add another listener-owned dispatch to this queue.
/// Completed handles are observed here; live ones remain attached to the
/// caller-owned nonquiescent settlement outcome.
fn take_unsettled_retired_modern_http_dispatches(
registry: &LiveModernHttpSessionRegistry,
) -> Vec<asupersync::runtime::TaskHandle<()>> {
let mut pending = Vec::new();
for mut dispatch in registry.take_retired_dispatches() {
if matches!(dispatch.try_join(), Ok(None)) {
pending.push(dispatch);
}
}
pending
}
/// Clone-shared authority for one live HTTP listener shutdown.
///
/// The listener's caller-owned `Cx` makes cancellation visible immediately;
/// the atomic retains the shutdown fence after the accept loop has observed
/// that cancellation (or exited for an error). Checking both closes the race
/// where a connection could commit a response between root cancellation and
/// the accept loop publishing its terminal state.
#[derive(Clone)]
struct HttpListenerShutdown {
listener_cx: Cx,
requested: Arc<AtomicBool>,
}
impl HttpListenerShutdown {
fn new(listener_cx: &Cx) -> Self {
Self {
listener_cx: listener_cx.clone(),
requested: Arc::new(AtomicBool::new(false)),
}
}
fn request(&self) {
self.requested.store(true, Ordering::Release);
}
fn is_requested(&self) -> bool {
self.requested.load(Ordering::Acquire) || self.listener_cx.is_cancel_requested()
}
}
/// A bound, caller-owned HTTP server lifecycle.
///
/// The listener and every accepted connection stay in the caller's [`Cx`]
/// region. Dropping the lifecycle closes its listener; cancelling that region
/// cancels the accept loop and all of its connection children together.
pub struct BoundHttpServer {
listener: AsyncTcpListener,
endpoint: Arc<ServerHttpEndpoint>,
#[cfg(feature = "legacy-2024-11-05")]
legacy_sessions: LiveHttpSessionRegistry,
modern_sessions: LiveModernHttpSessionRegistry,
connection_limiter: Arc<HttpConnectionLimiter>,
}
impl BoundHttpServer {
/// Returns the address selected by the operating system for this listener.
pub fn local_addr(&self) -> McpResult<SocketAddr> {
self.listener.local_addr().map_err(|error| {
McpError::internal_error(format!("HTTP listener address unavailable: {error}"))
})
}
/// Accepts real HTTP/1.1 loopback or network connections until the caller
/// cancels the owning context.
///
/// Cooperative connection children are joined during shutdown. If a
/// synchronous handler ignores cancellation, this method returns
/// [`HttpServerShutdown::Nonquiescent`] after
/// the bounded connection-shutdown wait instead of claiming a clean
/// shutdown. The caller owns the returned settlement handle and chooses a
/// further bounded observation or eventual join.
///
/// The returned future is `Send`; this crate verifies that contract so
/// downstream runtimes need not expand the listener's internal task graph.
#[expect(
clippy::manual_async_fn,
reason = "explicit Send bounds keep the listener's proof inside this crate"
)]
pub fn serve(self, cx: &Cx) -> impl Future<Output = McpResult<HttpServerShutdown>> + Send + '_ {
async move {
let server = Arc::clone(&self.endpoint.server);
server.init_rich_logging();
if let Some(stats) = &server.stats {
stats.connection_opened();
}
if !server.run_startup_hook() {
server.graceful_shutdown_returning();
return Err(McpError::internal_error("HTTP server startup hook failed"));
}
#[cfg(feature = "tasks")]
let hosted_task_service = match server.start_hosted_task_service(cx).await {
Ok(hosted) => hosted,
Err(error) => {
server.graceful_shutdown_returning();
return Err(error);
}
};
let connection_scope = cx.scope();
let connection_shutdown = HttpListenerShutdown::new(cx);
// Expired modern sessions are reaped from this accept loop, which
// already wakes every HTTP_ACCEPT_CANCEL_POLL. A dedicated reaper
// task had to be aborted and joined during shutdown, and that join
// parked the whole listener shutdown (bd-f2ndd).
let reap_interval_nanos =
u64::try_from(MODERN_HTTP_SESSION_REAP_INTERVAL.as_nanos()).unwrap_or(u64::MAX);
let mut last_session_reap = cx.now();
let mut connection_children = HttpConnectionChildren::default();
let result = loop {
connection_children.reap_finished();
if cx.checkpoint().is_err() {
break Ok(());
}
#[cfg(feature = "tasks")]
if let Some(hosted) = hosted_task_service.as_ref()
&& let Err(error) = hosted.check_running()
{
break Err(error);
}
let now = cx.now();
if now.duration_since(last_session_reap) >= reap_interval_nanos {
last_session_reap = now;
expire_live_modern_http_sessions(&self.modern_sessions);
}
#[cfg(test)]
lib_unit_tests::record_live_http_listener_wait();
let accepted = match asupersync::time::timeout(
cx.now(),
HTTP_ACCEPT_CANCEL_POLL,
self.listener.accept(),
)
.await
{
Ok(accepted) => accepted,
Err(_) => continue,
};
let (stream, _peer_addr) = match accepted {
Ok(connection) => connection,
Err(_error) if cx.checkpoint().is_err() => break Ok(()),
Err(error) => {
break Err(McpError::internal_error(format!(
"HTTP listener accept failed: {error}"
)));
}
};
let Some(permit) = self.connection_limiter.try_acquire() else {
// A capacity rejection must not wait for a slow peer to read
// an HTTP error response. Dropping the socket applies the
// configured bound without creating an unbounded population
// of rejection tasks.
drop(stream);
continue;
};
let endpoint = Arc::clone(&self.endpoint);
#[cfg(feature = "legacy-2024-11-05")]
let legacy_sessions = Arc::clone(&self.legacy_sessions);
let modern_sessions = Arc::clone(&self.modern_sessions);
let listener_shutdown = connection_shutdown.clone();
let connection = cx
.spawn_in(&connection_scope, move |connection_cx| async move {
let _permit = permit;
#[cfg(feature = "legacy-2024-11-05")]
{
// Keep the nested connection future's Send proof in
// this crate instead of exhausting downstream crates'
// default trait-recursion limit during code generation.
let connection: std::pin::Pin<
Box<dyn Future<Output = ()> + Send + '_>,
> = Box::pin(serve_http_connection(
&connection_cx,
stream,
endpoint,
legacy_sessions,
modern_sessions,
listener_shutdown,
));
connection.await;
}
#[cfg(not(feature = "legacy-2024-11-05"))]
serve_modern_http_connection(
&connection_cx,
stream,
endpoint,
modern_sessions,
listener_shutdown,
)
.await;
})
.map_err(|error| {
McpError::internal_error(format!(
"HTTP connection task admission failed: {error}"
))
});
match connection {
Ok(connection) => connection_children.tasks.push(connection),
Err(error) => break Err(error),
}
};
connection_shutdown.request();
// Closing stateless issuance is a latched registry fence, not merely a
// purge. A handler racing shutdown therefore cannot mint a new
// cross-POST continuation after the existing set has been invalidated.
let _ = server.router.close_stateless_mrtr_exchanges();
// Final subscription teardown must commit its correlated cancellation
// controls while the response writers are still owned. Give those
// children a bounded scheduling window to flush and close before
// aborting any unrelated or uncooperative connection.
let terminal_receipt = server.final_subscriptions.terminate_with_receipt();
#[cfg(feature = "legacy-2024-11-05")]
close_live_http_sessions(cx, &self.legacy_sessions).await;
// Phase one closes response-body admission before any uninterruptible
// connection-child join can begin. Leave the SSE queues alive until
// the elected terminal response has had its bounded opportunity to
// flush.
let closing_modern_sessions = detach_live_modern_http_sessions(&self.modern_sessions);
connection_children
.drain_terminal_controls(&terminal_receipt)
.await;
let unsettled_modern_dispatches =
finish_live_modern_http_sessions(&self.modern_sessions, closing_modern_sessions)
.await;
// Revoke request-owned authority before waiting for the connection
// children. Cooperative handlers observe this and join structurally;
// an ignored cancellation becomes a reported, retained shutdown debt.
server.cancel_active_requests(CancelKind::Shutdown, false);
connection_children
.tasks
.extend(unsettled_modern_dispatches);
let connection_shutdown = connection_children.drain_cooperative_shutdown().await;
connection_children
.tasks
.extend(take_unsettled_retired_modern_http_dispatches(
&self.modern_sessions,
));
connection_children.reap_finished();
#[cfg(feature = "tasks")]
let task_service_result =
Server::settle_hosted_task_service(hosted_task_service, cx).await;
#[cfg(not(feature = "tasks"))]
let task_service_result: McpResult<()> = Ok(());
server.graceful_shutdown_returning();
let result = result.and(task_service_result);
match (connection_shutdown, connection_children.tasks.len()) {
(_, 0) if connection_children.terminal_failures.is_empty() => {
result?;
Ok(HttpServerShutdown::Quiescent)
}
(_, 0) => Err(McpError::internal_error(format!(
"HTTP connection shutdown observed terminal child failure(s): {}",
connection_children.terminal_failures.join("; ")
))),
(_, remaining) => {
debug_assert!(remaining > 0);
Ok(HttpServerShutdown::Nonquiescent(HttpNonquiescentShutdown {
children: connection_children,
listener_error: result.err(),
}))
}
}
}
}
}
// The WebSocket adapter owns one read/write object, while the established
// dual-era server dispatcher owns independent synchronous receive and send
// halves. Keep their bridge bounded in both directions; this preserves the
// dispatcher's mature cancellation and reverse-request semantics without
// giving a slow socket an unbounded in-memory queue.
#[cfg(feature = "websocket")]
const WEBSOCKET_BRIDGE_QUEUE_CAPACITY: usize = 64;
#[cfg(feature = "websocket")]
const WEBSOCKET_HANDSHAKE_MAX_BYTES: usize = 64 * 1024;
#[cfg(feature = "websocket")]
const WEBSOCKET_DRIVER_POLL_INTERVAL: Duration = Duration::from_millis(5);
#[cfg(feature = "websocket")]
const WEBSOCKET_CONNECTION_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
/// Wake `accept` often enough that a cancelled listener Cx can leave the
/// accept loop without waiting for the next inbound connection.
#[cfg(feature = "websocket")]
const WEBSOCKET_ACCEPT_CANCEL_POLL: Duration = Duration::from_millis(20);
#[cfg(feature = "websocket")]
static NEXT_WEBSOCKET_AUTH_CONNECTION_GENERATION: AtomicU64 = AtomicU64::new(1);
/// A bound, caller-owned WebSocket listener.
///
/// Each accepted connection completes HTTP Upgrade before dispatch. A
/// ModernOnly listener drives independently owned native read/write halves
/// and request child regions without a blocking worker. Auto and LegacyOnly
/// retain the split dual-era dispatcher used by synchronous stdio. Protocol
/// binding, cancellation, and authentication remain connection scoped.
#[cfg(feature = "websocket")]
pub struct BoundWebSocketServer {
listener: AsyncTcpListener,
server: Arc<Server>,
path: String,
connection_limiter: Arc<HttpConnectionLimiter>,
}
/// Result of a WebSocket listener shutdown.
#[cfg(feature = "websocket")]
#[must_use = "a nonquiescent WebSocket shutdown retains connection children that must be settled"]
pub enum WebSocketServerShutdown {
/// The listener stopped and every accepted socket task joined.
Quiescent,
/// The listener stopped, but the caller retains explicit connection work.
Nonquiescent(WebSocketNonquiescentShutdown),
}
/// Caller-owned WebSocket connection work that outlived bounded shutdown.
#[cfg(feature = "websocket")]
#[must_use = "settle retained WebSocket connection children before discarding this outcome"]
pub struct WebSocketNonquiescentShutdown {
children: WebSocketConnectionChildren,
server: Arc<Server>,
listener_error: Option<McpError>,
shutdown_complete: bool,
}
#[cfg(feature = "websocket")]
impl WebSocketNonquiescentShutdown {
/// Number of accepted connections that still require caller-owned settlement.
#[must_use]
pub const fn remaining_connections(&self) -> usize {
self.children.tasks.len()
}
/// Returns the listener failure that ended acceptance, when one occurred.
#[must_use]
pub fn listener_error(&self) -> Option<&McpError> {
self.listener_error.as_ref()
}
/// Observes retained children for at most `timeout`, without detaching them.
///
/// Returns `Ok(true)` only when every child completed successfully. A
/// recorded child failure takes precedence over a listener failure after
/// all children have been observed and cleanup has run; `Ok(false)`
/// retains caller-owned work.
pub async fn settle_for(&mut self, timeout: Duration) -> McpResult<bool> {
let deadline = Instant::now() + timeout;
while !self.children.tasks.is_empty() && Instant::now() < deadline {
self.children.reap_finished();
if !self.children.tasks.is_empty() {
asupersync::runtime::yield_now().await;
}
}
if self.children.tasks.is_empty() {
let terminal_failure = self.children.terminal_failure();
let listener_error = self.listener_error.clone();
if !self.shutdown_complete {
self.server.graceful_shutdown_returning();
self.shutdown_complete = true;
}
if let Some(error) = terminal_failure {
Err(error)
} else if let Some(error) = listener_error {
Err(error)
} else {
Ok(true)
}
} else {
Ok(false)
}
}
/// Joins every retained connection in the caller's context.
pub async fn settle(mut self, cx: &Cx) -> McpResult<()> {
self.children.reap_finished();
while let Some(mut child) = self.children.tasks.pop() {
match child.join(cx).await {
Ok(Ok(())) | Err(asupersync::runtime::JoinError::Cancelled(_)) => {}
Ok(Err(error)) => self.children.record_failure(error.to_string()),
Err(error) => self.children.record_failure(error.to_string()),
}
}
let terminal_failure = self.children.terminal_failure();
if !self.shutdown_complete {
self.server.graceful_shutdown_returning();
self.shutdown_complete = true;
}
if let Some(error) = terminal_failure {
return Err(error);
}
if let Some(error) = self.listener_error {
return Err(error);
}
Ok(())
}
}
#[cfg(feature = "websocket")]
impl BoundWebSocketServer {
/// Returns the address selected by the operating system for this listener.
pub fn local_addr(&self) -> McpResult<SocketAddr> {
self.listener.local_addr().map_err(|error| {
McpError::internal_error(format!("WebSocket listener address unavailable: {error}"))
})
}
/// Serves HTTP Upgrade and WebSocket MCP connections until `cx` is cancelled.
pub async fn serve(self, cx: &Cx) -> McpResult<WebSocketServerShutdown> {
self.server.init_rich_logging();
if let Some(stats) = &self.server.stats {
stats.connection_opened();
}
if !self.server.run_startup_hook() {
self.server.graceful_shutdown_returning();
return Err(McpError::internal_error(
"WebSocket server startup hook failed",
));
}
#[cfg(feature = "tasks")]
let hosted_task_service = match self.server.start_hosted_task_service(cx).await {
Ok(hosted) => hosted,
Err(error) => {
self.server.graceful_shutdown_returning();
return Err(error);
}
};
let connection_scope = cx.scope();
let mut children = WebSocketConnectionChildren::default();
let accept_result = loop {
children.reap_finished();
if cx.checkpoint().is_err() {
break Ok(());
}
#[cfg(feature = "tasks")]
if let Some(hosted) = hosted_task_service.as_ref()
&& let Err(error) = hosted.check_running()
{
break Err(error);
}
let accepted = match asupersync::time::timeout(
cx.now(),
WEBSOCKET_ACCEPT_CANCEL_POLL,
self.listener.accept(),
)
.await
{
Ok(accepted) => accepted,
Err(_) => continue,
};
let (stream, _) = match accepted {
Ok(connection) => connection,
Err(_) if cx.checkpoint().is_err() => break Ok(()),
Err(error) => {
break Err(McpError::internal_error(format!(
"WebSocket listener accept failed: {error}"
)));
}
};
// Acquire before spawning so an overflow peer cannot create a
// retained connection child. The permit moves into that child
// and therefore covers handshake, upgraded socket, dispatch, and
// its caller-owned nonquiescent settlement lifetime.
let Some(permit) = self.connection_limiter.try_acquire() else {
drop(stream);
continue;
};
let server = Arc::clone(&self.server);
let path = self.path.clone();
let terminal_failure = Arc::clone(&children.terminal_failure);
match cx.spawn_in(&connection_scope, move |connection_cx| async move {
let _permit = permit;
let result =
serve_websocket_connection(&connection_cx, stream, server, &path).await;
if let Err(error) = &result {
terminal_failure
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get_or_insert_with(|| error.to_string());
}
result
}) {
Ok(child) => children.tasks.push(child),
Err(error) => {
break Err(McpError::internal_error(format!(
"WebSocket connection task admission failed: {error}"
)));
}
}
};
// Capture failures that completed while `accept` was parked before
// aborting live children. Otherwise an abort could turn an already
// terminal task into an indistinguishable cancellation outcome.
children.reap_finished();
for child in &mut children.tasks {
child.abort();
}
let deadline = Instant::now() + WEBSOCKET_CONNECTION_SHUTDOWN_TIMEOUT;
while !children.tasks.is_empty() && Instant::now() < deadline {
children.reap_finished();
if !children.tasks.is_empty() {
asupersync::runtime::yield_now().await;
}
}
#[cfg(feature = "tasks")]
let task_service_result = Server::settle_hosted_task_service(hosted_task_service, cx).await;
#[cfg(not(feature = "tasks"))]
let task_service_result: McpResult<()> = Ok(());
let accept_result = accept_result.and(task_service_result);
if children.tasks.is_empty() {
let terminal_failure = children.terminal_failure();
self.server.graceful_shutdown_returning();
if let Some(error) = terminal_failure {
return Err(error);
}
accept_result?;
Ok(WebSocketServerShutdown::Quiescent)
} else {
Ok(WebSocketServerShutdown::Nonquiescent(
WebSocketNonquiescentShutdown {
children,
server: Arc::clone(&self.server),
listener_error: accept_result.err(),
shutdown_complete: false,
},
))
}
}
}
#[cfg(feature = "websocket")]
#[derive(Default)]
struct WebSocketConnectionChildren {
tasks: Vec<asupersync::runtime::TaskHandle<McpResult<()>>>,
/// The first connection failure observed by a nonblocking reaper. Later
/// failures are still drained, but cannot overwrite the original causal
/// failure presented to the lifecycle caller.
terminal_failure: Arc<Mutex<Option<String>>>,
}
#[cfg(feature = "websocket")]
impl WebSocketConnectionChildren {
fn record_failure(&self, failure: String) {
self.terminal_failure
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get_or_insert(failure);
}
fn terminal_failure(&self) -> Option<McpError> {
self.terminal_failure
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.map(|failure| {
McpError::internal_error(format!(
"WebSocket connection child settlement failure: {failure}"
))
})
}
#[cfg(test)]
fn has_terminal_failure(&self) -> bool {
self.terminal_failure
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_some()
}
fn reap_finished(&mut self) {
let mut active = Vec::with_capacity(self.tasks.len());
for mut child in std::mem::take(&mut self.tasks) {
match child.try_join() {
Ok(None) => active.push(child),
Ok(Some(Ok(()))) | Err(asupersync::runtime::JoinError::Cancelled(_)) => {}
Ok(Some(Err(error))) => self.record_failure(error.to_string()),
Err(error) => self.record_failure(error.to_string()),
}
}
self.tasks = active;
}
}
#[cfg(feature = "websocket")]
struct PrefixedWebSocketStream {
prefix: Vec<u8>,
stream: AsyncTcpStream,
}
#[cfg(feature = "websocket")]
impl AsyncRead for PrefixedWebSocketStream {
fn poll_read(
mut self: Pin<&mut Self>,
task_cx: &mut Context<'_>,
buffer: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
if !self.prefix.is_empty() {
let count = self.prefix.len().min(buffer.unfilled().len());
buffer.put_slice(&self.prefix[..count]);
self.prefix.drain(..count);
return Poll::Ready(Ok(()));
}
Pin::new(&mut self.stream).poll_read(task_cx, buffer)
}
}
#[cfg(feature = "websocket")]
impl AsyncWrite for PrefixedWebSocketStream {
fn poll_write(
mut self: Pin<&mut Self>,
task_cx: &mut Context<'_>,
buffer: &[u8],
) -> Poll<std::io::Result<usize>> {
Pin::new(&mut self.stream).poll_write(task_cx, buffer)
}
fn poll_flush(
mut self: Pin<&mut Self>,
task_cx: &mut Context<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.stream).poll_flush(task_cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
task_cx: &mut Context<'_>,
) -> Poll<std::io::Result<()>> {
Pin::new(&mut self.stream).poll_shutdown(task_cx)
}
}
#[cfg(feature = "websocket")]
struct WebSocketBridgeRecv {
receiver: asupersync_mpsc::Receiver<JsonRpcMessage>,
}
#[cfg(feature = "websocket")]
impl TransportRecvHalf for WebSocketBridgeRecv {
fn recv(&mut self, cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
loop {
cx.checkpoint().map_err(|_| TransportError::Cancelled)?;
match self.receiver.try_recv() {
Ok(message) => return Ok(message),
Err(asupersync_mpsc::RecvError::Empty) => {
// This bridge is polled only by the dispatcher blocking
// worker. The owned async socket task retains admission
// and cancellation authority; a short yield keeps this
// compatibility boundary from parking a global thread.
std::thread::sleep(WEBSOCKET_DRIVER_POLL_INTERVAL);
}
Err(asupersync_mpsc::RecvError::Disconnected) => {
return Err(TransportError::Closed);
}
Err(asupersync_mpsc::RecvError::Cancelled) => {
return Err(TransportError::Cancelled);
}
}
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
self.receiver.close();
Ok(())
}
}
#[cfg(feature = "websocket")]
struct WebSocketBridgeSend {
sender: asupersync_mpsc::Sender<JsonRpcMessage>,
}
#[cfg(feature = "websocket")]
impl TransportSendHalf for WebSocketBridgeSend {
fn send(&mut self, cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
cx.checkpoint().map_err(|_| TransportError::Cancelled)?;
let result = match self.sender.try_reserve() {
Ok(permit) => permit.try_send(message.clone()),
Err(asupersync_mpsc::SendError::Full(())) => {
return Err(TransportError::Io(std::io::Error::from(
std::io::ErrorKind::WouldBlock,
)));
}
Err(
asupersync_mpsc::SendError::Disconnected(())
| asupersync_mpsc::SendError::Cancelled(()),
) => return Err(TransportError::Closed),
};
result.map_err(|error| match error {
asupersync_mpsc::SendError::Full(_) => {
TransportError::Io(std::io::Error::from(std::io::ErrorKind::WouldBlock))
}
asupersync_mpsc::SendError::Disconnected(_)
| asupersync_mpsc::SendError::Cancelled(_) => TransportError::Closed,
})
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
#[cfg(feature = "websocket")]
async fn serve_websocket_connection(
cx: &Cx,
mut stream: AsyncTcpStream,
server: Arc<Server>,
expected_path: &str,
) -> McpResult<()> {
let read_timeout = server.http_config.request_read_timeout;
let (request, trailing) = match read_websocket_handshake(cx, &mut stream, read_timeout).await {
Ok(handshake) => handshake,
Err(error) => {
let rejection = if error.message == "WebSocket Authorization admission failed" {
b"HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Bearer\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
.to_vec()
} else {
asupersync::net::websocket::ServerHandshake::reject(400, "Bad Request")
};
let _ = stream.write_all(&rejection).await;
return Ok(());
}
};
let path = request
.path
.split_once('?')
.map_or(request.path.as_str(), |(path, _)| path);
if path != expected_path {
stream
.write_all(&asupersync::net::websocket::ServerHandshake::reject(
404,
"Not Found",
))
.await
.map_err(|_| {
McpError::internal_error("WebSocket route rejection could not be written")
})?;
return Ok(());
}
let transport_authorization =
TransportAuthorization::from_singleton_header(request.header("authorization"));
let connection_generation =
NEXT_WEBSOCKET_AUTH_CONNECTION_GENERATION.fetch_add(1, Ordering::AcqRel);
let auth_custody = match server.authenticate_websocket_upgrade(
cx,
&transport_authorization,
connection_generation,
) {
Ok(receipt) => receipt,
Err(_) => {
stream
.write_all(
b"HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: Bearer\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
)
.await
.map_err(|_| {
McpError::internal_error("WebSocket authorization rejection could not be written")
})?;
return Ok(());
}
};
let response = match asupersync::net::websocket::ServerHandshake::new().accept(&request) {
Ok(response) => response,
Err(_) => {
stream
.write_all(&asupersync::net::websocket::ServerHandshake::reject(
400,
"Bad Request",
))
.await
.map_err(|_| {
McpError::internal_error("WebSocket handshake rejection could not be written")
})?;
return Ok(());
}
};
stream
.write_all(&response.response_bytes())
.await
.map_err(|_| McpError::internal_error("WebSocket upgrade response could not be written"))?;
stream
.flush()
.await
.map_err(|_| McpError::internal_error("WebSocket upgrade response could not be flushed"))?;
let transport = AsyncWsServerTransport::from_upgraded(PrefixedWebSocketStream {
prefix: trailing,
stream,
});
if server.protocol_policy == ProtocolPolicy::ModernOnly {
return server
.serve_modern_websocket(
cx,
transport,
transport_authorization,
auth_custody,
connection_generation,
)
.await;
}
run_websocket_dispatch_bridge(
cx,
server,
transport,
transport_authorization,
auth_custody,
connection_generation,
)
.await
}
#[cfg(feature = "websocket")]
async fn read_websocket_handshake(
cx: &Cx,
stream: &mut AsyncTcpStream,
read_timeout: Duration,
) -> McpResult<(asupersync::net::websocket::HttpRequest, Vec<u8>)> {
let mut bytes = Vec::with_capacity(1024);
let mut chunk = [0_u8; 1024];
// The upgrade is an HTTP request: the listener's request-read deadline
// keeps an idle or trickling peer from holding a connection slot.
let deadline = cx
.now()
.saturating_add_nanos(u64::try_from(read_timeout.as_nanos()).unwrap_or(u64::MAX));
loop {
cx.checkpoint()
.map_err(|_| McpError::internal_error("WebSocket handshake cancelled"))?;
let now = cx.now();
if now >= deadline {
return Err(McpError::invalid_request(
"WebSocket handshake was not received in time",
));
}
let wait = HTTP_ACCEPT_CANCEL_POLL.min(Duration::from_nanos(deadline.duration_since(now)));
let Ok(read) = asupersync::time::timeout(now, wait, stream.read(&mut chunk)).await else {
continue;
};
let read =
read.map_err(|_| McpError::invalid_request("WebSocket handshake could not be read"))?;
if read == 0 {
return Err(McpError::invalid_request(
"WebSocket handshake ended before completion",
));
}
if bytes
.len()
.checked_add(read)
.is_none_or(|length| length > WEBSOCKET_HANDSHAKE_MAX_BYTES)
{
return Err(McpError::invalid_request(
"WebSocket handshake headers exceed the limit",
));
}
bytes.extend_from_slice(&chunk[..read]);
if let Some(header_end) = bytes.windows(4).position(|window| window == b"\r\n\r\n") {
let header_block = &bytes[..header_end];
if raw_websocket_upgrade_authorization_is_admissible(header_block).is_err() {
return Err(McpError::invalid_request(
"WebSocket Authorization admission failed",
));
}
if raw_websocket_upgrade_headers_are_well_formed(header_block).is_err() {
return Err(McpError::invalid_request("WebSocket handshake is invalid"));
}
if !raw_websocket_upgrade_origin_is_admissible(header_block) {
return Err(McpError::invalid_request("WebSocket Origin is not allowed"));
}
}
match asupersync::net::websocket::HttpRequest::parse_with_trailing(&bytes) {
Ok((request, trailing)) => return Ok((request, trailing.to_vec())),
Err(_) if !bytes.windows(4).any(|window| window == b"\r\n\r\n") => {}
Err(_) => return Err(McpError::invalid_request("WebSocket handshake is invalid")),
}
}
}
#[cfg(feature = "websocket")]
fn raw_websocket_upgrade_headers_are_well_formed(head: &[u8]) -> Result<(), ()> {
let head = std::str::from_utf8(head).map_err(|_| ())?;
let mut lines = head.split("\r\n");
if lines.next().is_none_or(str::is_empty) {
return Err(());
}
for line in lines {
let (name, _) = line.split_once(':').ok_or(())?;
if name.is_empty()
|| !name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&byte))
{
return Err(());
}
}
Ok(())
}
/// Cross-site WebSocket hijacking protection: a browser always sends
/// `Origin` on a WebSocket upgrade, and the server-side transport has no CORS
/// allowlist, so a present `Origin` must be a loopback origin (the same
/// admission policy the HTTP routes apply by default). Non-browser clients
/// that send no `Origin` are unaffected.
#[cfg(feature = "websocket")]
fn raw_websocket_upgrade_origin_is_admissible(head: &[u8]) -> bool {
let Ok(head) = std::str::from_utf8(head) else {
// Left to the handshake parser's generic bounded 400 path.
return true;
};
let mut origins = head.split("\r\n").skip(1).filter_map(|line| {
line.split_once(':')
.filter(|(name, _)| name.eq_ignore_ascii_case("origin"))
.map(|(_, value)| value.trim())
});
match (origins.next(), origins.next()) {
(None, _) => true,
(Some(origin), None) => HttpHandlerConfig::default().admits_origin(origin),
// A repeated Origin is ambiguous; refuse rather than pick one.
(Some(_), Some(_)) => false,
}
}
#[cfg(feature = "websocket")]
fn raw_websocket_upgrade_authorization_is_admissible(head: &[u8]) -> Result<(), ()> {
let Ok(head) = std::str::from_utf8(head) else {
// This is not an Authorization-specific admission failure; leave it
// to the handshake parser's generic bounded 400 path.
return Ok(());
};
let mut authorization = None;
for line in head.split("\r\n").skip(1) {
let malformed_authorization_intent = line
.split_once(':')
.map(|(name, _)| name.trim())
.or_else(|| line.split_ascii_whitespace().next())
.is_some_and(|name| name.eq_ignore_ascii_case("authorization"));
let Some((name, value)) = line.split_once(':') else {
if malformed_authorization_intent {
return Err(());
}
continue;
};
let normalized_name = name.trim();
if normalized_name.eq_ignore_ascii_case("authorization") {
// OWS belongs after the colon, never in an HTTP field-name.
// Treat an Authorization-shaped malformed line as an auth denial
// so it cannot fall through to the generic Upgrade 400 path.
if name != normalized_name
|| value.trim().is_empty()
|| authorization.replace(value).is_some()
{
return Err(());
}
}
}
Ok(())
}
#[cfg(feature = "websocket")]
async fn run_websocket_dispatch_bridge(
cx: &Cx,
server: Arc<Server>,
mut transport: AsyncWsServerTransport<PrefixedWebSocketStream>,
transport_authorization: TransportAuthorization,
auth_custody: Arc<WebSocketAuthCustody>,
connection_generation: u64,
) -> McpResult<()> {
let (incoming_sender, incoming_receiver) =
asupersync_mpsc::channel(WEBSOCKET_BRIDGE_QUEUE_CAPACITY);
let (outgoing_sender, mut outgoing_receiver) =
asupersync_mpsc::channel(WEBSOCKET_BRIDGE_QUEUE_CAPACITY);
let dispatch_cx = cx.clone();
let dispatcher_custody = Arc::clone(&auth_custody);
let mut dispatcher = cx
.spawn_blocking(move |pump_cx| {
server.run_split_transport_returning_shared_with_dispatch_cx(
&pump_cx,
&dispatch_cx,
WebSocketBridgeRecv {
receiver: incoming_receiver,
},
WebSocketBridgeSend {
sender: outgoing_sender,
},
transport_authorization,
AuthDispatchCustody::WebSocket(dispatcher_custody),
connection_generation,
)
})
.map_err(|error| {
McpError::internal_error(format!("WebSocket dispatch admission failed: {error}"))
})?;
let driver_result = 'driver: loop {
loop {
match outgoing_receiver.try_recv() {
Ok(message) => transport.send(cx, &message).await.map_err(|_| {
McpError::internal_error("WebSocket response could not be committed")
})?,
Err(asupersync_mpsc::RecvError::Empty) => break,
Err(asupersync_mpsc::RecvError::Disconnected) => break 'driver Ok(()),
Err(asupersync_mpsc::RecvError::Cancelled) => break 'driver Ok(()),
}
}
if cx.checkpoint().is_err() {
break Ok(());
}
let deadline = cx.now() + WEBSOCKET_DRIVER_POLL_INTERVAL;
match asupersync::time::timeout_at(deadline, transport.recv(cx)).await {
Ok(Ok(mut message)) => {
if let JsonRpcMessage::Request(request) = &mut message {
// Strict frame decode is the last point raw in-band
// credentials exist. Sanitize before traffic rendering,
// logs, queues, or router visibility; custody remembers a
// rejected mixed-source frame for its later auth fence.
sanitize_websocket_decoded_request(&auth_custody, request);
}
let permit = match incoming_sender.reserve(cx).await {
Ok(permit) => permit,
Err(
asupersync_mpsc::SendError::Disconnected(())
| asupersync_mpsc::SendError::Cancelled(()),
) => break Ok(()),
Err(asupersync_mpsc::SendError::Full(())) => {
break Err(McpError::internal_error("WebSocket ingress queue is full"));
}
};
if let Err(error) = permit.try_send(message) {
break match error {
asupersync_mpsc::SendError::Full(_) => {
Err(McpError::internal_error("WebSocket ingress queue is full"))
}
asupersync_mpsc::SendError::Disconnected(_)
| asupersync_mpsc::SendError::Cancelled(_) => Ok(()),
};
}
}
Ok(Err(TransportError::Closed | TransportError::Cancelled)) => break Ok(()),
Ok(Err(_)) => break Err(McpError::invalid_request("WebSocket frame was rejected")),
Err(_) => {}
}
};
drop(incoming_sender);
let _ = transport.close(cx).await;
let dispatch_result = dispatcher.join(cx).await.map_err(|error| {
McpError::internal_error(format!("WebSocket dispatch worker failed: {error}"))
})?;
match (driver_result, dispatch_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
(Err(driver), Err(dispatch)) => Err(combined_run_and_close_error(driver, dispatch)),
}
}
#[cfg(feature = "websocket")]
impl Server {
/// Binds a turnkey WebSocket MCP listener on the caller-owned context.
///
/// The upgrade route is the configured [`HttpServerConfig`] MCP path, so
/// one server configuration cannot accidentally expose different HTTP and
/// WebSocket entry points. This method only binds; call
/// [`BoundWebSocketServer::serve`] to accept connections.
pub async fn bind_websocket(
self,
cx: &Cx,
addr: impl Into<String>,
) -> McpResult<BoundWebSocketServer> {
cx.checkpoint()
.map_err(|_| McpError::internal_error("WebSocket bind was cancelled"))?;
let listener = AsyncTcpListener::bind(addr.into()).await.map_err(|error| {
McpError::internal_error(format!("WebSocket listener bind failed: {error}"))
})?;
Ok(BoundWebSocketServer {
listener,
path: self.http_config.handler_config.base_path.clone(),
connection_limiter: Arc::new(HttpConnectionLimiter::new(
self.http_config.max_connections,
)),
server: Arc::new(self),
})
}
/// Binds and serves a turnkey WebSocket MCP bridge until `cx` is cancelled.
pub async fn serve_websocket(
self,
cx: &Cx,
addr: impl Into<String>,
) -> McpResult<WebSocketServerShutdown> {
self.bind_websocket(cx, addr).await?.serve(cx).await
}
/// Alias for [`Self::serve_websocket`] for callers that use `run_*` APIs.
pub async fn run_websocket(
self,
cx: &Cx,
addr: impl Into<String>,
) -> McpResult<WebSocketServerShutdown> {
self.serve_websocket(cx, addr).await
}
}
impl Server {
/// Consumes this server into a live HTTP endpoint.
///
/// Modern Streamable HTTP remains at the configured MCP path.
#[cfg(not(feature = "legacy-2024-11-05"))]
pub fn into_http_endpoint(self) -> Result<ServerHttpEndpoint, ServerHttpEndpointError> {
validate_server_http_route_configuration(&self)?;
let endpoint = ServerHttpEndpoint {
server: Arc::new(self),
};
let _ = endpoint
.transport_endpoint()
.map_err(ServerHttpEndpointError::from_internal)?;
Ok(endpoint)
}
/// Consumes this server into a live dual-era HTTP endpoint.
///
/// `legacy_origin` is used only for the exact MCP 2024-11-05 SSE endpoint
/// event. Modern Streamable HTTP remains at the configured MCP path.
#[cfg(feature = "legacy-2024-11-05")]
pub fn into_http_endpoint(
self,
legacy_origin: impl Into<String>,
) -> Result<ServerHttpEndpoint, ServerHttpEndpointError> {
validate_server_http_route_configuration(&self)?;
let endpoint = ServerHttpEndpoint {
server: Arc::new(self),
legacy_origin: legacy_origin.into(),
};
let _ = endpoint
.transport_endpoint(&endpoint.legacy_origin)
.map_err(ServerHttpEndpointError::from_internal)?;
Ok(endpoint)
}
/// Binds the HTTP listener on the caller-owned [`Cx`].
///
/// This performs only listener setup. Call [`BoundHttpServer::serve`] to
/// accept connections, or use [`Self::serve_http`] for the turnkey
/// lifecycle. In a legacy-enabled build, the exact SSE endpoint derives
/// its advertised authority from the client's `Host` header, with the
/// resolved local address as the fallback for direct embeddings.
pub async fn bind_http(self, cx: &Cx, addr: impl Into<String>) -> McpResult<BoundHttpServer> {
if cx.checkpoint().is_err() {
return Err(McpError::request_cancelled());
}
let listener = AsyncTcpListener::bind(addr.into()).await.map_err(|error| {
McpError::internal_error(format!("HTTP listener bind failed: {error}"))
})?;
let max_connections = self.http_config.max_connections;
#[cfg(feature = "legacy-2024-11-05")]
let local_addr = listener.local_addr().map_err(|error| {
McpError::internal_error(format!("HTTP listener address unavailable: {error}"))
})?;
#[cfg(feature = "legacy-2024-11-05")]
let endpoint = self
.into_http_endpoint(format!("http://{local_addr}"))
.map_err(|error| {
McpError::internal_error(format!("HTTP endpoint setup failed: {error}"))
})?;
#[cfg(not(feature = "legacy-2024-11-05"))]
let endpoint = self.into_http_endpoint().map_err(|error| {
McpError::internal_error(format!("HTTP endpoint setup failed: {error}"))
})?;
Ok(BoundHttpServer {
listener,
endpoint: Arc::new(endpoint),
#[cfg(feature = "legacy-2024-11-05")]
legacy_sessions: Arc::new(Mutex::new(HashMap::new())),
modern_sessions: Arc::new(LiveModernHttpSessionRegistryState::new()),
connection_limiter: Arc::new(HttpConnectionLimiter::new(max_connections)),
})
}
/// Binds and accepts a turnkey HTTP server on the caller's context. This
/// method never creates or owns an async runtime.
pub async fn serve_http(
self,
cx: &Cx,
addr: impl Into<String>,
) -> McpResult<HttpServerShutdown> {
self.bind_http(cx, addr).await?.serve(cx).await
}
}
impl ServerHttpEndpoint {
/// Opens one independently bounded live HTTP session.
///
/// Repeated calls through the returned embedding handle share modern
/// mutable component state. Turnkey live ingress calls this method afresh
/// for each stateless POST. Eligible MRTR continuations are represented by
/// router-owned opaque, single-use state and may be resumed by a later
/// stateless POST only when its operation and principal bindings match.
pub fn open_session(&self, cx: &Cx) -> Result<ServerHttpSession, ServerHttpEndpointError> {
#[cfg(feature = "legacy-2024-11-05")]
let session = self.open_session_with_legacy_origin(cx, &self.legacy_origin);
#[cfg(not(feature = "legacy-2024-11-05"))]
let session = self.open_session_modern(cx);
session.map_err(ServerHttpEndpointError::from_internal)
}
#[cfg(feature = "legacy-2024-11-05")]
fn transport_endpoint(
&self,
legacy_origin: &str,
) -> Result<DualEraHttpEndpoint, DualEraHttpEndpointError> {
let mut config = DualEraHttpEndpointConfig::new(
self.server.http_config.legacy_sse_path.clone(),
self.server.http_config.legacy_message_path.clone(),
legacy_origin,
);
config.legacy_request_capacity = self.server.http_config.request_capacity;
DualEraHttpEndpoint::new(
HttpRequestHandler::with_config(self.server.http_config.handler_config.clone()),
config,
)
}
#[cfg(not(feature = "legacy-2024-11-05"))]
fn transport_endpoint(&self) -> Result<DualEraHttpEndpoint, DualEraHttpEndpointError> {
let mut config = DualEraHttpEndpointConfig::new();
config.request_capacity = self.server.http_config.request_capacity;
DualEraHttpEndpoint::new(
HttpRequestHandler::with_config(self.server.http_config.handler_config.clone()),
config,
)
}
#[cfg(feature = "legacy-2024-11-05")]
fn open_session_with_legacy_origin(
&self,
cx: &Cx,
legacy_origin: &str,
) -> Result<ServerHttpSession, DualEraHttpEndpointError> {
let endpoint = self.transport_endpoint(legacy_origin)?;
self.open_session_from_transport(cx, endpoint)
}
#[cfg(not(feature = "legacy-2024-11-05"))]
fn open_session_modern(&self, cx: &Cx) -> Result<ServerHttpSession, DualEraHttpEndpointError> {
let endpoint = self.transport_endpoint()?;
self.open_session_from_transport(cx, endpoint)
}
fn open_session_from_transport(
&self,
cx: &Cx,
endpoint: DualEraHttpEndpoint,
) -> Result<ServerHttpSession, DualEraHttpEndpointError> {
let _ = cx;
let endpoint_session = Arc::new(Mutex::new(endpoint.open_session()?));
#[cfg(feature = "legacy-2024-11-05")]
let (legacy_session_id, legacy_lifecycle) = {
let session = endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(session.session_id().to_owned(), session.legacy_lifecycle())
};
#[cfg(feature = "legacy-2024-11-05")]
let legacy_session =
Session::new(self.server.info.clone(), self.server.capabilities.clone());
#[cfg(feature = "legacy-2024-11-05")]
let legacy_binding = runtime_legacy_binding(legacy_session.id());
#[cfg(feature = "legacy-2024-11-05")]
let legacy_pending_requests = Arc::new(
PendingRequests::with_max_in_flight_for_exact_legacy(
self.server.max_bidirectional_requests_per_connection,
)
.expect("ServerBuilder validates the bidirectional request limit"),
);
#[cfg(feature = "legacy-2024-11-05")]
let notification_sender: NotificationSender = {
let endpoint_session = Arc::clone(&endpoint_session);
Arc::new(move |notification| {
let _ = endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.publish_legacy_message(&JsonRpcMessage::Request(notification));
})
};
#[cfg(feature = "legacy-2024-11-05")]
let legacy_request_sender = RequestSender::new(Arc::clone(&legacy_pending_requests), {
let endpoint_session = Arc::clone(&endpoint_session);
Arc::new(move |message| {
endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.publish_legacy_message(message)
.map_err(|error| error.to_string())?;
Ok(())
})
});
#[cfg(feature = "legacy-2024-11-05")]
let legacy_runtime = LiveLegacy2024ConnectionRuntime::new(
legacy_session.state().clone(),
notification_sender,
Some(legacy_request_sender),
self.server.logging.level,
);
Ok(ServerHttpSession {
server: Arc::clone(&self.server),
endpoint_session,
#[cfg(feature = "legacy-2024-11-05")]
legacy_session_id,
#[cfg(feature = "legacy-2024-11-05")]
legacy_lifecycle,
#[cfg(feature = "legacy-2024-11-05")]
legacy_session,
#[cfg(feature = "legacy-2024-11-05")]
legacy_binding,
#[cfg(feature = "legacy-2024-11-05")]
legacy_adapter: None,
#[cfg(feature = "legacy-2024-11-05")]
legacy_active_request: Arc::new(Mutex::new(None)),
#[cfg(feature = "legacy-2024-11-05")]
legacy_request_cx: Arc::new(Mutex::new(cx.clone())),
#[cfg(feature = "legacy-2024-11-05")]
legacy_admissions: Arc::new(HttpLegacyRequestAdmissions::default()),
#[cfg(feature = "legacy-2024-11-05")]
legacy_auth_receipt: Arc::new(Mutex::new(None)),
#[cfg(feature = "legacy-2024-11-05")]
legacy_sse_open_fingerprint: None,
#[cfg(feature = "legacy-2024-11-05")]
legacy_pending_requests,
#[cfg(feature = "legacy-2024-11-05")]
legacy_runtime,
modern_connection: Arc::new(ModernConnection::new_request_local()),
modern_dispatches: Arc::new(Mutex::new(Vec::new())),
selected_era: None,
closed: false,
})
}
#[cfg(feature = "legacy-2024-11-05")]
fn open_session_for_legacy_sse(
&self,
cx: &Cx,
request: &HttpRequest,
) -> Result<ServerHttpSession, DualEraHttpEndpointError> {
let legacy_origin = request
.header("host")
.and_then(legacy_origin_from_host)
.unwrap_or_else(|| self.legacy_origin.clone());
self.open_session_with_legacy_origin(cx, &legacy_origin)
}
}
/// Private proof that native ingress evaluated authentication for one exact,
/// strict-admitted modern request. It is intentionally neither serializable
/// nor publicly constructible: only this module can carry it from admission
/// to the request context that dispatch owns.
#[derive(Clone)]
struct AuthAdmissionReceipt {
method: String,
request_id: Option<RequestId>,
sanitized_params: Option<serde_json::Value>,
fingerprint: Sha256Digest,
authenticated: Option<AuthContext>,
}
impl AuthAdmissionReceipt {
fn commit(
&self,
ctx: &McpContext,
inbound: &InboundRequestContext,
request: &mut JsonRpcRequest,
) -> Result<Sha256Digest, McpError> {
if self.method != request.method
|| request.id != self.request_id
|| !inbound
.auth_request(&request.method, request.params.as_ref())
.credential_sources_are_admissible()
{
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authentication failed",
));
}
request.params = self.sanitized_params.clone();
let committed = match &self.authenticated {
Some(auth) => ctx.set_auth(auth.clone()),
None => ctx.commit_anonymous_auth(),
};
if committed {
Ok(self.fingerprint.clone())
} else {
Err(Server::request_context_error(ctx).unwrap_or_else(|| {
McpError::internal_error("authentication admission was already committed")
}))
}
}
/// Commits this receipt onto an exact-2024 request context.
///
/// Legacy HTTP has no [`InboundRequestContext`]; method and wire-id
/// equality are the binding between pre-admission and dispatch.
#[cfg(feature = "legacy-2024-11-05")]
fn commit_legacy(
&self,
ctx: &McpContext,
request: &JsonRpcRequest,
) -> Result<Sha256Digest, McpError> {
if self.method != request.method || request.id != self.request_id {
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authentication failed",
));
}
let committed = match &self.authenticated {
Some(auth) => ctx.set_auth(auth.clone()),
None => ctx.commit_anonymous_auth(),
};
if committed {
Ok(self.fingerprint.clone())
} else {
Err(Server::request_context_error(ctx).unwrap_or_else(|| {
McpError::internal_error("authentication admission was already committed")
}))
}
}
}
/// Immutable, connection-owned WebSocket authentication facts. Unlike an
/// HTTP receipt, this is never an arbitrary request receipt: the exact
/// accepted connection generation must be supplied at every frame dispatch.
#[cfg(feature = "websocket")]
struct WebSocketAuthCustody {
connection_generation: u64,
fingerprint: Sha256Digest,
authenticated: Option<AuthContext>,
rejected_inband_request_ids: Mutex<HashSet<u64>>,
}
// The WebSocket arm is an Arc handle by design; the Http receipt is the
// wide variant and both are moved once per admission.
#[allow(clippy::large_enum_variant)]
#[derive(Clone)]
enum AuthDispatchCustody {
Http(AuthAdmissionReceipt),
#[cfg(feature = "websocket")]
WebSocket(Arc<WebSocketAuthCustody>),
}
impl AuthDispatchCustody {
fn commit(
&self,
ctx: &McpContext,
inbound: &InboundRequestContext,
request: &mut JsonRpcRequest,
// Underscore-prefixed because non-websocket builds never read it; the
// websocket arm does, so the binding stays named for that arm.
#[allow(clippy::used_underscore_binding)] _websocket_connection_generation: Option<u64>,
) -> Result<Sha256Digest, McpError> {
match self {
Self::Http(receipt) => receipt.commit(ctx, inbound, request),
#[cfg(feature = "websocket")]
Self::WebSocket(custody) => {
if _websocket_connection_generation != Some(custody.connection_generation)
|| custody
.rejected_inband_request_ids
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&request_id_to_u64(request.id.as_ref()))
|| !inbound
.auth_request(&request.method, request.params.as_ref())
.credential_sources_are_admissible()
{
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authentication failed",
));
}
// Frame decode/admission has just produced this request;
// strip before traffic rendering, middleware, logs, or router.
auth::strip_recognized_access_credentials(&mut request.params);
let committed = match &custody.authenticated {
Some(auth) => ctx.set_auth(auth.clone()),
None => ctx.commit_anonymous_auth(),
};
if committed {
Ok(custody.fingerprint)
} else {
Err(Server::request_context_error(ctx).unwrap_or_else(|| {
McpError::internal_error("authentication admission was already committed")
}))
}
}
}
}
#[cfg(feature = "legacy-2024-11-05")]
fn commit_legacy_connection(&self, ctx: &McpContext) -> Result<Sha256Digest, McpError> {
let (authenticated, fingerprint) = match self {
Self::Http(receipt) => (receipt.authenticated.clone(), receipt.fingerprint.clone()),
#[cfg(feature = "websocket")]
Self::WebSocket(custody) => {
(custody.authenticated.clone(), custody.fingerprint.clone())
}
};
let committed = match authenticated {
Some(auth) => ctx.set_auth(auth),
None => ctx.commit_anonymous_auth(),
};
if committed {
Ok(fingerprint)
} else {
Err(Server::request_context_error(ctx).unwrap_or_else(|| {
McpError::internal_error("authentication admission was already committed")
}))
}
}
#[cfg(feature = "legacy-2024-11-05")]
fn connection_fingerprint(&self) -> Sha256Digest {
match self {
Self::Http(receipt) => receipt.fingerprint.clone(),
#[cfg(feature = "websocket")]
Self::WebSocket(custody) => custody.fingerprint.clone(),
}
}
}
#[cfg(feature = "websocket")]
fn sanitize_websocket_decoded_request(
custody: &WebSocketAuthCustody,
request: &mut JsonRpcRequest,
) {
let original = request.params.clone();
auth::strip_recognized_access_credentials(&mut request.params);
if request.params != original {
custody
.rejected_inband_request_ids
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(request_id_to_u64(request.id.as_ref()));
}
}
impl ServerHttpSession {
/// Returns the exact opaque session value required by the legacy POST URI.
#[must_use]
#[cfg(feature = "legacy-2024-11-05")]
pub fn legacy_session_id(&self) -> &str {
&self.legacy_session_id
}
/// Routes and dispatches one modern or exact-2024 HTTP request on the caller's `Cx`.
///
/// The caller drives this future on its runtime. This entry does not
/// construct or re-enter a runtime, so request-owned children can progress
/// while the handler awaits them.
pub async fn handle_async(
&mut self,
cx: &Cx,
request: HttpRequest,
) -> Result<ServerHttpEndpointResponse, ServerHttpEndpointError> {
self.handle_with_modern_request_cancellation_async(cx, request, None, false, None)
.await
.map_err(ServerHttpEndpointError::from_internal)
}
#[cfg(feature = "legacy-2024-11-05")]
async fn handle_preclassified_unmatched_legacy_response(
&mut self,
cx: &Cx,
request: HttpRequest,
_verified: IngressVerifiedLegacyResponse,
) -> Result<ServerHttpEndpointResponse, ServerHttpEndpointError> {
self.handle_with_modern_request_cancellation_async(cx, request, None, true, None)
.await
.map_err(ServerHttpEndpointError::from_internal)
}
async fn handle_with_modern_request_cancellation_async(
&mut self,
cx: &Cx,
request: HttpRequest,
modern_request_cancellation: Option<McpRequestCancellation>,
legacy_response_preclassified_unmatched: bool,
legacy_auth_receipt: Option<AuthAdmissionReceipt>,
) -> Result<ServerHttpEndpointResponse, DualEraHttpEndpointError> {
let transport_authorization = match transport_authorization_from_http_request(&request) {
Ok(authorization) => authorization,
Err(response) => return Ok(ServerHttpEndpointResponse::Immediate(response)),
};
self.handle_with_modern_request_cancellation_and_transport_authorization_async(
cx,
request,
transport_authorization,
modern_request_cancellation,
legacy_response_preclassified_unmatched,
legacy_auth_receipt,
)
.await
}
#[cfg_attr(
not(feature = "legacy-2024-11-05"),
allow(clippy::unused_async, clippy::unused_async_trait_impl)
)]
async fn handle_with_modern_request_cancellation_and_transport_authorization_async(
&mut self,
cx: &Cx,
request: HttpRequest,
transport_authorization: TransportAuthorization,
modern_request_cancellation: Option<McpRequestCancellation>,
legacy_response_preclassified_unmatched: bool,
legacy_auth_receipt: Option<AuthAdmissionReceipt>,
) -> Result<ServerHttpEndpointResponse, DualEraHttpEndpointError> {
#[cfg(not(feature = "legacy-2024-11-05"))]
let _ = (legacy_response_preclassified_unmatched, legacy_auth_receipt);
self.reap_modern_dispatches();
let mut request = request;
if request.path == self.server.http_config.health_path && request.method == HttpMethod::Get
{
return Ok(ServerHttpEndpointResponse::Immediate(
HttpResponse::ok().with_json(&serde_json::json!({"status": "ok"})),
));
}
// DNS-rebinding protection for every MCP route, modern and exact-2024
// alike: a present `Origin` must be loopback or explicitly allowed,
// otherwise HTTP 403 before any parsing, authentication, or dispatch.
if let Some(origin) = request.header("origin")
&& !self.server.http_config.handler_config.admits_origin(origin)
{
return Ok(ServerHttpEndpointResponse::Immediate(HttpResponse::new(
HttpStatus::FORBIDDEN,
)));
}
let is_modern = request.method == HttpMethod::Post
&& request.path == self.server.http_config.handler_config.base_path;
let mut raw_params = None;
let mut http_parameter_headers = None;
let mut auth_receipt = None;
#[cfg(feature = "legacy-2024-11-05")]
let is_legacy = (request.method == HttpMethod::Get
&& request.path == self.server.http_config.legacy_sse_path)
|| (request.method == HttpMethod::Post
&& request.path == self.server.http_config.legacy_message_path);
#[cfg(not(feature = "legacy-2024-11-05"))]
if matches!(self.server.protocol_policy, ProtocolPolicy::LegacyOnly) {
return Ok(ServerHttpEndpointResponse::Immediate(HttpResponse::new(
HttpStatus::BAD_REQUEST,
)));
}
#[cfg(feature = "legacy-2024-11-05")]
if (is_modern && matches!(self.server.protocol_policy, ProtocolPolicy::LegacyOnly))
|| (is_legacy && matches!(self.server.protocol_policy, ProtocolPolicy::ModernOnly))
{
return Ok(ServerHttpEndpointResponse::Immediate(HttpResponse::new(
HttpStatus::BAD_REQUEST,
)));
}
#[cfg(feature = "legacy-2024-11-05")]
let legacy_post_admission = if request.method == HttpMethod::Post
&& request.path == self.server.http_config.legacy_message_path
{
match admit_legacy_2024_http_post(
&request,
&self.server.http_config.legacy_message_path,
self.legacy_session_id(),
self.server.http_config.handler_config.max_body_size,
) {
Ok(admitted) => Some(admitted),
Err(response) => {
return Ok(ServerHttpEndpointResponse::Immediate(response));
}
}
} else {
None
};
if is_modern {
if request.header("mcp-session-id").is_some() {
return Ok(ServerHttpEndpointResponse::Immediate(
HttpResponse::bad_request(),
));
}
let (prepared_request, admitted_request, admitted_raw_params) =
match self.prepare_modern_http_request(request) {
Ok(prepared) => prepared,
Err(response) => return Ok(ServerHttpEndpointResponse::Immediate(response)),
};
request = prepared_request;
raw_params = admitted_raw_params;
http_parameter_headers = Some(http_admission::http_parameter_headers(&request.headers));
auth_receipt = match self.preauthenticate_modern_http_request(
cx,
&admitted_request,
&transport_authorization,
) {
Ok(receipt) => Some(AuthDispatchCustody::Http(receipt)),
Err(response) => return Ok(ServerHttpEndpointResponse::Immediate(response)),
};
}
#[cfg(feature = "legacy-2024-11-05")]
let ingress_verified_legacy_response = legacy_response_preclassified_unmatched
&& matches!(
legacy_post_admission,
Some(Legacy2024HttpPostEnvelope::Response(_))
);
#[cfg(feature = "legacy-2024-11-05")]
if is_legacy && !ingress_verified_legacy_response {
let admitted_request = match &legacy_post_admission {
Some(Legacy2024HttpPostEnvelope::ClientMessage(request)) => Some(request),
_ => None,
};
let receipt = match legacy_auth_receipt {
Some(receipt) => {
// Recheck the exact admitted payload after waiting for
// exclusive session ownership. A receipt never authorizes
// a different method, request, or notification.
if !admitted_request.is_some_and(|request| {
receipt.method == request.method
&& receipt.request_id == request.id
&& receipt.sanitized_params == request.params
}) {
return Ok(ServerHttpEndpointResponse::Immediate(
native_http_authentication_rejection(),
));
}
receipt
}
None => match self.preauthenticate_legacy_http_request(
cx,
admitted_request,
&transport_authorization,
) {
Ok(receipt) => receipt,
Err(response) => return Ok(ServerHttpEndpointResponse::Immediate(response)),
},
};
if matches!(
legacy_post_admission,
Some(Legacy2024HttpPostEnvelope::Response(_))
) && !self
.legacy_session
.principal_binding()
.verify_existing(receipt.fingerprint.clone())
{
return Ok(ServerHttpEndpointResponse::Immediate(
native_http_authentication_rejection(),
));
}
// GET `/sse` has no JSON-RPC body, so the receipt cannot be
// committed onto a later POST. Retain the opener fingerprint so
// `install_legacy_generation` can bind the session owner. POST
// `/messages` (including reverse responses) must not overwrite
// or claim that opener identity.
if request.method == HttpMethod::Get
&& request.path == self.server.http_config.legacy_sse_path
{
self.legacy_sse_open_fingerprint = Some(receipt.fingerprint.clone());
}
let mut stored = self
.legacy_auth_receipt
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*stored = admitted_request.is_some().then_some(receipt);
}
#[cfg(feature = "legacy-2024-11-05")]
let request_era = if is_modern {
Some(ProtocolEra::Modern2026)
} else if is_legacy {
Some(ProtocolEra::Legacy2024)
} else {
None
};
#[cfg(not(feature = "legacy-2024-11-05"))]
let request_era = is_modern.then_some(ProtocolEra::Modern2026);
if request_era.is_some_and(|era| self.selected_era.is_some_and(|selected| selected != era))
{
// Reject before modern admission or legacy adapter dispatch: a
// retained public session is one era's mutable namespace, never
// a bridge between exact-2024 and final HTTP state.
return Ok(ServerHttpEndpointResponse::Immediate(
HttpResponse::bad_request(),
));
}
// The first admitted HTTP era irrevocably owns this public session.
// Modern requests retain the existing recognition-time pin; exact
// legacy POSTs pin only after strict admission, and legacy GET pins
// only after it successfully creates the response body below.
#[cfg(feature = "legacy-2024-11-05")]
let defer_legacy_era_selection = matches!(request_era, Some(ProtocolEra::Legacy2024));
#[cfg(not(feature = "legacy-2024-11-05"))]
let defer_legacy_era_selection = false;
if let Some(era) = request_era.filter(|_| !defer_legacy_era_selection) {
self.selected_era.get_or_insert(era);
}
#[cfg(feature = "legacy-2024-11-05")]
if let Some(Legacy2024HttpPostEnvelope::Response(response)) = legacy_post_admission {
let lifecycle = self.legacy_lifecycle.clone();
let result = lifecycle
.commit_if_live(|| {
self.selected_era.get_or_insert(ProtocolEra::Legacy2024);
if legacy_response_preclassified_unmatched {
self.handle_unmatched_legacy_reverse_response(response)
} else {
self.handle_legacy_reverse_response(response)
}
})
.unwrap_or_else(|| {
ServerHttpEndpointResponse::Immediate(HttpResponse::new(
HttpStatus::SERVICE_UNAVAILABLE,
))
});
return Ok(result);
}
let endpoint_response = self
.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.handle(cx, request)?;
#[cfg(feature = "legacy-2024-11-05")]
if defer_legacy_era_selection
&& (matches!(
&endpoint_response,
DualEraHttpEndpointResponse::LegacySse(_)
) || matches!(
&endpoint_response,
DualEraHttpEndpointResponse::Immediate(response)
if response.status == HttpStatus::ACCEPTED
))
{
self.selected_era.get_or_insert(ProtocolEra::Legacy2024);
}
if is_modern {
return self
.handle_modern(
cx,
endpoint_response,
transport_authorization,
raw_params,
http_parameter_headers,
auth_receipt,
modern_request_cancellation,
)
.await;
}
#[cfg(feature = "legacy-2024-11-05")]
return self.handle_legacy(cx, endpoint_response).await;
#[cfg(not(feature = "legacy-2024-11-05"))]
{
let _ = endpoint_response;
Ok(ServerHttpEndpointResponse::Immediate(HttpResponse::new(
HttpStatus::NOT_FOUND,
)))
}
}
fn prepare_modern_http_request(
&self,
mut request: HttpRequest,
) -> Result<(HttpRequest, JsonRpcRequest, Option<Arc<str>>), HttpResponse> {
if rejects_final_client_notification_http_ingress(
&request.body,
self.server.http_config.handler_config.max_body_size,
) {
return Err(HttpResponse::bad_request());
}
let admitted = self.admit_modern_http_request(&request)?;
let raw_params = admitted.raw_params().map(Arc::<str>::from);
if admit_final_client_notification_ingress(admitted.request()).is_err() {
return Err(HttpResponse::bad_request());
}
if let Some(response) = self
.server
.preflight_modern_http_required_capability(admitted.request())
{
return Err(HttpResponse::new(HttpStatus::BAD_REQUEST).with_json(&response));
}
let request_requires_sse = admitted.request().method == SUBSCRIPTIONS_LISTEN
|| self
.server
.modern_request_requires_owned_sse(admitted.request());
let request_accepts_sse = http_request_accepts_sse(&request);
// The strict admission authority has chosen the request-local
// representation. Preserve that decision when the transport admits
// the already-validated request a second time.
request.headers.insert(
"accept".to_owned(),
match admitted.representation() {
ResponseRepresentation::Json => "application/json",
ResponseRepresentation::RequestScopedSse => "text/event-stream",
}
.to_owned(),
);
if !request_requires_sse {
return Ok((request, admitted.request().clone(), raw_params));
}
if !request_accepts_sse {
return Err(HttpResponse::new(HttpStatus::NOT_ACCEPTABLE));
}
// The transport defaults a dual-acceptable request to JSON.
// Request-scoped final notifications instead require the
// connection-owned SSE sender, so select that accepted form before
// the request enters the transport ingress queue.
request
.headers
.insert("accept".to_owned(), "text/event-stream".to_owned());
Ok((request, admitted.request().clone(), raw_params))
}
/// Authenticates a strict modern HTTP request before it can pin a public
/// session era, allocate an SSE body, or enter dispatch. The returned
/// receipt carries the sole successful provider evaluation into the fresh
/// request context that dispatch owns; failed admission has no session,
/// handler, middleware, or log side effects.
fn preauthenticate_modern_http_request(
&self,
cx: &Cx,
request: &JsonRpcRequest,
transport_authorization: &TransportAuthorization,
) -> Result<AuthAdmissionReceipt, HttpResponse> {
self.server
.preauthenticate_http_request(cx, request, transport_authorization)
}
/// Authenticates an exact-2024 HTTP GET `/sse` or POST `/messages`
/// request before the session era can pin or dispatch can run. GET uses a
/// synthetic request identity because the SSE open has no JSON-RPC body.
#[cfg(feature = "legacy-2024-11-05")]
fn preauthenticate_legacy_http_request(
&self,
cx: &Cx,
request: Option<&JsonRpcRequest>,
transport_authorization: &TransportAuthorization,
) -> Result<AuthAdmissionReceipt, HttpResponse> {
let synthetic = request
.is_none()
.then(|| JsonRpcRequest::new("legacy/sse", None, RequestId::Number(0)));
let admitted = request
.or(synthetic.as_ref())
.expect("synthetic fills GET /sse");
self.preauthenticate_modern_http_request(cx, admitted, transport_authorization)
}
fn admit_modern_http_request(
&self,
request: &HttpRequest,
) -> Result<AdmittedModernPost, HttpResponse> {
let headers = request
.headers
.iter()
.map(|(name, value)| (name.clone(), value.clone()))
.collect::<Vec<_>>();
admit_modern_http_post(
&self.server.http_config.handler_config,
request.method.as_str(),
&request.path,
&headers,
&request.body,
)
}
/// Installs every exact-2024 server-issued capability for one live SSE
/// body generation. Reconnect must replace, rather than revive, these
/// objects: retained providers and request senders otherwise close over
/// the mutable transport session and could publish into the fresh body.
#[cfg(feature = "legacy-2024-11-05")]
fn install_legacy_generation(&mut self, cx: &Cx) {
self.legacy_admissions.cancel_all();
self.legacy_pending_requests.cancel_all();
*self
.legacy_auth_receipt
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
if let Some(active_request) = take_live_legacy_active_request(&self.legacy_active_request) {
active_request.cancellation.cancel();
}
if let Some(adapter) = self.legacy_adapter.as_mut() {
let _ = adapter.close(self.legacy_binding);
}
self.legacy_adapter = None;
self.legacy_session =
Session::new(self.server.info.clone(), self.server.capabilities.clone());
self.legacy_binding = runtime_legacy_binding(self.legacy_session.id());
self.legacy_active_request = Arc::new(Mutex::new(None));
self.legacy_request_cx = Arc::new(Mutex::new(cx.clone()));
self.legacy_admissions = Arc::new(HttpLegacyRequestAdmissions::default());
let pending_requests = Arc::new(
PendingRequests::with_max_in_flight_for_exact_legacy(
self.server.max_bidirectional_requests_per_connection,
)
.expect("ServerBuilder validates the bidirectional request limit"),
);
let lifecycle = self.legacy_lifecycle.clone();
let notification_sender: NotificationSender = {
let endpoint_session = Arc::clone(&self.endpoint_session);
let lifecycle = lifecycle.clone();
Arc::new(move |notification| {
let _ = lifecycle.commit_if_live(|| {
endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.publish_legacy_message_committed(&JsonRpcMessage::Request(notification))
});
})
};
let request_sender = RequestSender::new(Arc::clone(&pending_requests), {
let endpoint_session = Arc::clone(&self.endpoint_session);
Arc::new(move |message| {
lifecycle
.commit_if_live(|| {
endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.publish_legacy_message_committed(message)
})
.ok_or_else(|| "Legacy SSE generation is closed".to_owned())?
.map_err(|error| error.to_string())
})
});
self.legacy_pending_requests = pending_requests;
self.legacy_runtime = LiveLegacy2024ConnectionRuntime::new(
self.legacy_session.state().clone(),
notification_sender,
Some(request_sender),
self.server.logging.level,
);
}
/// Returns a transport-backed roots provider after exact-2024 roots
/// capability negotiation has completed for this HTTP connection.
#[must_use]
#[cfg(feature = "legacy-2024-11-05")]
pub fn legacy_roots_provider(&self) -> Option<TransportRootsProvider> {
if !self.legacy_adapter.as_ref().is_some_and(|adapter| {
let snapshot = adapter.snapshot();
snapshot.operating_transition_count == 1 && snapshot.close_release_count == 0
}) {
return None;
}
self.legacy_runtime
.supports_roots
.load(Ordering::Acquire)
.then(|| self.legacy_runtime.request_sender.clone())
.flatten()
.map(|sender| {
TransportRootsProvider::new(
McpContext::new(
self.legacy_request_cx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone(),
0,
),
sender,
)
})
}
/// Delivers an exact `notifications/resources/updated` message to this
/// live legacy HTTP connection when it subscribed to `uri`.
///
/// Returns `false` without emitting a message when the connection has not
/// subscribed to the URI, has not completed legacy initialization, or its
/// SSE stream is not live.
#[cfg(feature = "legacy-2024-11-05")]
pub fn notify_legacy_resource_updated(&mut self, uri: &str) -> bool {
let Some(adapter) = self.legacy_adapter.as_mut() else {
return false;
};
if adapter
.snapshot()
.subscriptions
.binary_search_by(|item| item.as_str().cmp(uri))
.is_err()
{
return false;
}
let Ok(Legacy2024Outbound::ReverseNotification(notification)) = adapter.make_notification(
self.legacy_binding,
"notifications/resources/updated",
Some(serde_json::json!({ "uri": uri })),
) else {
return false;
};
let Ok(notification) = serde_json::from_value::<JsonRpcRequest>(notification) else {
return false;
};
self.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.publish_legacy_message(&JsonRpcMessage::Request(notification))
.is_ok()
}
#[cfg(feature = "legacy-2024-11-05")]
fn handle_legacy_reverse_response(
&mut self,
response: JsonRpcResponse,
) -> ServerHttpEndpointResponse {
let disposition = self
.legacy_pending_requests
.route_response_with_disposition(&response);
if matches!(
disposition,
bidirectional::PendingResponseDisposition::Delivered
| bidirectional::PendingResponseDisposition::RetiredGeneric
) {
return ServerHttpEndpointResponse::Immediate(HttpResponse::new(HttpStatus::ACCEPTED));
}
self.handle_unmatched_legacy_reverse_response(response)
}
#[cfg(feature = "legacy-2024-11-05")]
fn handle_unmatched_legacy_reverse_response(
&mut self,
response: JsonRpcResponse,
) -> ServerHttpEndpointResponse {
let accepted_by_adapter = self.legacy_adapter.as_mut().is_some_and(|adapter| {
legacy_adapter_accept_response(adapter, self.legacy_binding, &response)
});
if !accepted_by_adapter {
return ServerHttpEndpointResponse::Immediate(HttpResponse::bad_request());
}
if let Some(adapter) = self.legacy_adapter.as_ref() {
sync_live_legacy_runtime_from_adapter(&self.legacy_runtime, adapter);
self.legacy_session
.restore_log_level(self.legacy_runtime.log_level());
}
ServerHttpEndpointResponse::Immediate(HttpResponse::new(HttpStatus::ACCEPTED))
}
async fn handle_modern(
&mut self,
cx: &Cx,
endpoint_response: DualEraHttpEndpointResponse,
transport_authorization: TransportAuthorization,
raw_params: Option<Arc<str>>,
http_parameter_headers: Option<Arc<[(String, String)]>>,
auth_receipt: Option<AuthDispatchCustody>,
modern_request_cancellation: Option<McpRequestCancellation>,
) -> Result<ServerHttpEndpointResponse, DualEraHttpEndpointError> {
let request = self
.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.recv_modern_request(cx)?;
// Final Streamable HTTP cancellation is response-body closure. A
// separate notifications/cancelled POST has no request-body authority
// and is rejected instead of being correlated by JSON-RPC ID.
if request.method == "notifications/cancelled" {
return Ok(ServerHttpEndpointResponse::Immediate(
HttpResponse::bad_request(),
));
}
if request.method == SUBSCRIPTIONS_LISTEN {
let DualEraHttpEndpointResponse::ModernSse(sse) = endpoint_response else {
return Ok(ServerHttpEndpointResponse::Immediate(
HttpResponse::bad_request(),
));
};
let sender = sse.sender();
let request_cancellation = sender.request_cancellation();
let owner_generation = next_modern_http_stream_generation();
let terminal_delivery = Arc::new(FinalSubscriptionTerminalDelivery::default());
let inbound = InboundRequestContext::with_modern_connection_and_transport_authorization(
cx.clone(),
request_id_to_u64(request.id.as_ref()),
InboundRequestTransport::Http,
&self.modern_connection,
transport_authorization,
);
let dispatch = spawn_modern_sse_dispatch(
cx,
Arc::clone(&self.server),
owner_generation,
inbound,
request,
raw_params,
auth_receipt,
sender,
Arc::clone(&terminal_delivery),
None,
)?;
self.modern_dispatches
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(OwnedModernHttpDispatch {
owner_generation,
request_cancellation: request_cancellation.clone(),
task: dispatch,
});
return Ok(ServerHttpEndpointResponse::ModernSse(
ServerHttpSseResponse::new(sse, request_cancellation, terminal_delivery),
));
}
let inbound = InboundRequestContext::with_modern_connection_and_transport_authorization(
cx.clone(),
request_id_to_u64(request.id.as_ref()),
InboundRequestTransport::Http,
&self.modern_connection,
transport_authorization,
);
let inbound = match http_parameter_headers {
Some(headers) => inbound.with_http_parameter_headers(headers),
None => inbound,
};
match endpoint_response {
DualEraHttpEndpointResponse::ModernJson(pending) => {
if self.server.modern_request_requires_owned_sse(&request) {
return Ok(ServerHttpEndpointResponse::Immediate(HttpResponse::new(
HttpStatus::NOT_ACCEPTABLE,
)));
}
// JSON selects the response representation, not a synchronous
// handler contract. Use the same owned dispatcher as SSE so
// async extensions (including proxy Tasks controls) can yield
// on the caller's runtime and core handlers own their region.
let response = Arc::clone(&self.server)
.dispatch_with_protocol_policy_owned(
self.server.protocol_policy,
&inbound,
request,
raw_params,
auth_receipt,
None,
None,
modern_request_cancellation.unwrap_or_default(),
None,
Arc::new(|_| {}),
)
.await;
if let Some(response) = response {
if let Some(rejection) = canonical_http_error_status_response(&response) {
return Ok(ServerHttpEndpointResponse::Immediate(rejection));
}
self.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.send_modern_json_response(cx, response)?;
let response = pending
.try_response()?
.expect("a committed modern JSON response must be retrievable");
Ok(ServerHttpEndpointResponse::Immediate(response))
} else {
Ok(ServerHttpEndpointResponse::Immediate(HttpResponse::new(
HttpStatus::ACCEPTED,
)))
}
}
DualEraHttpEndpointResponse::ModernSse(sse) => {
let request_cancellation = sse.sender().request_cancellation();
let transport_cancellation = sse.cancellation();
// Progress finalization and the terminal response share this
// request-owned serializer. It keeps a transient transport
// commit gate collision from being mistaken for peer closure
// or allowing the response to overtake the coalesced update.
let terminal_commit = Arc::new(Mutex::new(()));
// A notification callback has no result channel into router
// dispatch. Retain its commit failure explicitly so an
// exhausted `WouldBlock` cannot be mistaken for a harmless
// dropped progress/log frame and followed by a terminal
// response.
let notification_commit_failed = Arc::new(AtomicBool::new(false));
let terminal_delivery = Arc::new(FinalSubscriptionTerminalDelivery::default());
// The final outcome is the sole authority for the terminal
// HTTP status. Retain request-scoped progress and log frames
// until that outcome is known, so any canonical -32021 from
// middleware, a tool, or another final gate becomes a 400
// without committing an SSE event first.
let deferred_notifications = Arc::new(Mutex::new(Vec::new()));
let committed_notification_sender: NotificationSender = {
let endpoint_session = Arc::clone(&self.endpoint_session);
let notification_cx = cx.clone();
let transport_cancellation = transport_cancellation.clone();
let request_cancellation = request_cancellation.clone();
let terminal_commit = Arc::clone(&terminal_commit);
let notification_commit_failed = Arc::clone(¬ification_commit_failed);
let terminal_delivery = Arc::clone(&terminal_delivery);
Arc::new(move |notification| {
if notification_commit_failed.load(Ordering::Acquire) {
return;
}
let _terminal_commit = terminal_commit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let sent = retry_http_sse_commit(
¬ification_cx,
&request_cancellation,
|| {
#[cfg(test)]
if take_forced_http_sse_would_block_commit() {
return Err(DualEraHttpEndpointError::Transport(
TransportError::Io(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"forced final HTTP commit contention",
)),
));
}
endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.send_modern_sse_notification(
¬ification_cx,
&transport_cancellation,
notification.clone(),
)
},
retryable_http_sse_endpoint_error,
);
if sent.is_err() {
notification_commit_failed.store(true, Ordering::Release);
terminal_delivery.mark_failed();
request_cancellation.cancel();
}
})
};
let notification_sender: NotificationSender = {
let deferred_notifications = Arc::clone(&deferred_notifications);
Arc::new(move |notification| {
deferred_notifications
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
})
};
let response = Arc::clone(&self.server)
.dispatch_with_protocol_policy_owned(
self.server.protocol_policy,
&inbound,
request,
None,
auth_receipt,
None,
None,
request_cancellation.clone(),
None,
notification_sender,
)
.await;
if let Some(rejection) = response
.as_ref()
.and_then(canonical_http_error_status_response)
{
return Ok(ServerHttpEndpointResponse::Immediate(rejection));
}
let deferred_notifications = std::mem::take(
&mut *deferred_notifications
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
for notification in deferred_notifications {
committed_notification_sender(notification);
}
if notification_commit_failed.load(Ordering::Acquire) {
terminal_delivery.mark_failed();
return Err(DualEraHttpEndpointError::Transport(TransportError::Io(
std::io::Error::other(
"modern HTTP progress or log commit failed before terminal response",
),
)));
}
if let Some(response) = response {
let _terminal_commit = terminal_commit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
retry_http_sse_commit(
cx,
&request_cancellation,
|| {
self.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.send_modern_sse_response(
cx,
&transport_cancellation,
response.clone(),
)
},
retryable_http_sse_endpoint_error,
)?;
}
Ok(ServerHttpEndpointResponse::ModernSse(
ServerHttpSseResponse::new(sse, request_cancellation, terminal_delivery),
))
}
DualEraHttpEndpointResponse::Immediate(response) => {
debug_assert!(request.id.is_none());
Ok(ServerHttpEndpointResponse::Immediate(response))
}
#[cfg(feature = "legacy-2024-11-05")]
DualEraHttpEndpointResponse::LegacySse(_) => {
unreachable!("the modern route cannot create a legacy SSE response")
}
}
}
/// Admits a modern SSE body without synchronously dispatching its request.
///
/// The socket writer owns this body and its request-owned dispatch child.
/// Long-lived subscriptions can stream after pre-admission, while ordinary
/// request-scoped SSE defers its wire representation until the final
/// outcome has elected it.
///
/// `modern_request_cancellation` is the connection-owned cancellation
/// domain for a request whose representation is elected as JSON here. That
/// dispatch completes inline, before any SSE response owner exists, so the
/// caller must observe its peer while awaiting this method (#76).
async fn begin_modern_sse(
&mut self,
cx: &Cx,
request: HttpRequest,
transport_authorization: TransportAuthorization,
modern_request_cancellation: Option<McpRequestCancellation>,
) -> Result<
Result<
(
JsonRpcRequest,
DualEraHttpSseResponse,
Option<Arc<str>>,
AuthDispatchCustody,
),
// Boxed so this Result stays small (bd-cmvwm): the response enum
// is hundreds of bytes and only the refusal path pays the box.
Box<ServerHttpEndpointResponse>,
>,
DualEraHttpEndpointError,
> {
if matches!(self.server.protocol_policy, ProtocolPolicy::LegacyOnly) {
return Ok(Err(Box::new(ServerHttpEndpointResponse::Immediate(
HttpResponse::new(HttpStatus::BAD_REQUEST),
))));
}
let (request, admitted_request, raw_params) =
match self.prepare_modern_http_request(request) {
Ok(request) => request,
Err(response) => {
return Ok(Err(Box::new(ServerHttpEndpointResponse::Immediate(
response,
))));
}
};
let auth_receipt = match self.preauthenticate_modern_http_request(
cx,
&admitted_request,
&transport_authorization,
) {
Ok(receipt) => AuthDispatchCustody::Http(receipt),
Err(response) => {
return Ok(Err(Box::new(ServerHttpEndpointResponse::Immediate(
response,
))));
}
};
let http_parameter_headers = http_admission::http_parameter_headers(&request.headers);
let endpoint_response = match self
.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.handle(cx, request)
{
Ok(endpoint_response) => endpoint_response,
// The transport refuses to construct an SSE body for a frame
// whose shape cannot own one (an id-less notification, or a
// request that already selected a JSON response) before the
// endpoint's own rejection arm can run; peers get the same
// graceful 400 the non-streaming arm produces.
Err(DualEraHttpEndpointError::Transport(TransportError::Io(error)))
if error.kind() == std::io::ErrorKind::InvalidInput =>
{
return Ok(Err(Box::new(ServerHttpEndpointResponse::Immediate(
HttpResponse::bad_request(),
))));
}
Err(error) => return Err(error),
};
let DualEraHttpEndpointResponse::ModernSse(sse) = endpoint_response else {
return self
.handle_modern(
cx,
endpoint_response,
transport_authorization,
raw_params,
Some(http_parameter_headers),
Some(auth_receipt),
modern_request_cancellation,
)
.await
.map(|response| Err(Box::new(response)));
};
let request = self
.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.recv_modern_request(cx)?;
if request.method == "notifications/cancelled" {
return Ok(Err(Box::new(ServerHttpEndpointResponse::Immediate(
HttpResponse::bad_request(),
))));
}
Ok(Ok((request, sse, raw_params, auth_receipt)))
}
#[cfg(feature = "legacy-2024-11-05")]
async fn handle_legacy(
&mut self,
cx: &Cx,
endpoint_response: DualEraHttpEndpointResponse,
) -> Result<ServerHttpEndpointResponse, DualEraHttpEndpointError> {
let response = match endpoint_response {
DualEraHttpEndpointResponse::LegacySse(sse) => {
let (session_id, lifecycle) = {
let endpoint = self
.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(
endpoint.session_id().to_owned(),
endpoint.legacy_lifecycle(),
)
};
self.legacy_session_id = session_id;
self.legacy_lifecycle = lifecycle;
self.install_legacy_generation(cx);
if let Some(fingerprint) = self.legacy_sse_open_fingerprint.take()
&& !self
.legacy_session
.principal_binding()
.bind_or_verify(fingerprint)
{
return Ok(ServerHttpEndpointResponse::Immediate(HttpResponse::new(
HttpStatus::UNAUTHORIZED,
)));
}
return Ok(ServerHttpEndpointResponse::LegacySse(sse));
}
DualEraHttpEndpointResponse::Immediate(response) => response,
DualEraHttpEndpointResponse::ModernJson(_)
| DualEraHttpEndpointResponse::ModernSse(_) => {
unreachable!("the legacy route cannot create a modern response")
}
};
let Some(request) = self
.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take_legacy_request()
else {
return Ok(ServerHttpEndpointResponse::Immediate(response));
};
*self
.legacy_request_cx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = cx.clone();
let adapter = match self.legacy_adapter.as_mut() {
Some(adapter) => adapter,
None => {
let adapter = self
.server
.install_legacy_2024_adapter(
self.legacy_binding,
HttpLegacy2024RuntimeHandler {
server: Arc::clone(&self.server),
session_id: self.legacy_binding.generation(),
session_principal: self.legacy_session.principal_binding(),
active_request: Arc::clone(&self.legacy_active_request),
request_cx: Arc::clone(&self.legacy_request_cx),
runtime: self.legacy_runtime.clone(),
legacy_admissions: Arc::clone(&self.legacy_admissions),
auth_receipt: Arc::clone(&self.legacy_auth_receipt),
},
)
.map_err(|error| {
DualEraHttpEndpointError::Transport(TransportError::Io(
std::io::Error::other(error.to_string()),
))
})?;
self.legacy_adapter.insert(adapter)
}
};
let legacy_response = legacy_adapter_response_async(adapter, self.legacy_binding, &request)
.await
.map_err(|error| {
DualEraHttpEndpointError::Transport(TransportError::Io(std::io::Error::other(
error.to_string(),
)))
})?;
sync_live_legacy_runtime_from_adapter(&self.legacy_runtime, adapter);
self.legacy_session
.restore_log_level(self.legacy_runtime.log_level());
let active_request = take_live_legacy_active_request(&self.legacy_active_request);
if request.method == "notifications/cancelled" && request.id.is_none() {
let fingerprint = self
.legacy_auth_receipt
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.map(|receipt| receipt.fingerprint.clone());
// Control frames must not cancel unless they prove the already-bound
// SSE opener. An unbound session or a different principal is ignored.
if fingerprint.is_some_and(|fingerprint| {
self.legacy_session
.principal_binding()
.verify_existing(fingerprint)
}) && let Ok(params) = parse_params::<CancelledParams>(request.params.clone())
{
self.server
.handle_cancelled_notification(self.legacy_binding.generation(), params);
}
}
let Some(legacy_response) = legacy_response else {
return Ok(ServerHttpEndpointResponse::Immediate(response));
};
let handled = legacy_handled_response(legacy_response, active_request, cx);
handled
.send_with(&mut self.legacy_session, |response| {
self.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.publish_legacy_message(&JsonRpcMessage::Response(response.clone()))
.map_err(|error| {
TransportError::Io(std::io::Error::other(error.to_string()))
})?;
Ok(())
})
.map_err(DualEraHttpEndpointError::from)?;
Ok(ServerHttpEndpointResponse::Immediate(response))
}
/// Closes this session, joins every owned modern dispatch, and releases its
/// exact legacy lifecycle state.
///
/// This is the structured settlement boundary. Callers that admitted a
/// modern SSE dispatch must await it before dropping the session.
pub async fn close(&mut self, cx: &Cx) {
let dispatches = self.begin_close();
for mut task in dispatches {
let _ = task.join(cx).await;
}
}
fn begin_close(&mut self) -> Vec<asupersync::runtime::TaskHandle<()>> {
if self.closed {
return Vec::new();
}
self.closed = true;
if !self.modern_connection.is_ephemeral() {
self.modern_connection.disconnect();
}
let dispatches = cancel_modern_http_dispatches(&self.server, &self.modern_dispatches, None);
#[cfg(feature = "legacy-2024-11-05")]
self.legacy_admissions.cancel_all();
#[cfg(feature = "legacy-2024-11-05")]
if let Some(adapter) = self.legacy_adapter.as_mut() {
let _ = adapter.close(self.legacy_binding);
}
self.endpoint_session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.close();
dispatches
}
fn reap_modern_dispatches(&mut self) {
let mut dispatches = self
.modern_dispatches
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut active = Vec::with_capacity(dispatches.len());
for mut dispatch in std::mem::take(&mut *dispatches) {
if matches!(dispatch.task.try_join(), Ok(None)) {
active.push(dispatch);
}
}
*dispatches = active;
}
#[cfg(test)]
fn has_modern_dispatches(&self) -> bool {
!self
.modern_dispatches
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
}
#[cfg(feature = "legacy-2024-11-05")]
fn cancellation_control(&self) -> HttpLegacyCancellationControl {
HttpLegacyCancellationControl {
server: Arc::clone(&self.server),
session_id: self.legacy_binding.generation(),
http_session_id: self.legacy_session_id.clone(),
legacy_message_path: self.server.http_config.legacy_message_path.clone(),
legacy_lifecycle: self.legacy_lifecycle.clone(),
session_principal: self.legacy_session.principal_binding(),
max_body_size: self.server.http_config.handler_config.max_body_size,
admissions: Arc::clone(&self.legacy_admissions),
}
}
}
impl Drop for ServerHttpSession {
fn drop(&mut self) {
// A synchronous destructor cannot drive the caller-owned asupersync
// runtime. Fail closed by revoking every exact transport/MCP authority
// in `begin_close`, aborting its task, and consuming any terminal
// result already available. Pending cancelled tasks remain owned by
// their structured runtime region; unlike the former process-global
// queue, this path retains no cross-session work and claims no join
// guarantee. `close(&Cx).await` is the required joined boundary.
for mut task in self.begin_close() {
let _ = task.try_join();
}
}
}
#[cfg(feature = "legacy-2024-11-05")]
fn legacy_origin_from_host(host: &str) -> Option<String> {
if host.is_empty()
|| host.bytes().any(|byte| {
byte.is_ascii_whitespace()
|| matches!(byte, b'/' | b'?' | b'#' | b'\\' | b'\r' | b'\n' | b'\0')
})
{
return None;
}
Some(format!("http://{host}"))
}
fn h1_request_to_transport(
request: &asupersync::http::h1::Request,
) -> Result<HttpRequest, HttpResponse> {
let method = match &request.method {
Http1Method::Get => HttpMethod::Get,
Http1Method::Post => HttpMethod::Post,
Http1Method::Put => HttpMethod::Put,
Http1Method::Delete => HttpMethod::Delete,
Http1Method::Options => HttpMethod::Options,
Http1Method::Head => HttpMethod::Head,
Http1Method::Patch => HttpMethod::Patch,
Http1Method::Connect | Http1Method::Trace | Http1Method::Extension(_) => {
return Err(HttpResponse::new(HttpStatus::METHOD_NOT_ALLOWED));
}
};
let (path, query) = request
.uri
.split_once('?')
.map_or((request.uri.as_str(), ""), |(path, query)| (path, query));
if !path.starts_with('/')
|| path
.bytes()
.any(|byte| matches!(byte, b'\r' | b'\n' | b'\0'))
{
return Err(HttpResponse::bad_request());
}
let mut transport = HttpRequest::new(method, path).with_body(request.body.clone());
for (name, value) in &request.headers {
if name
.bytes()
.chain(value.bytes())
.any(|byte| matches!(byte, b'\r' | b'\n' | b'\0'))
{
return Err(HttpResponse::bad_request());
}
let normalized_name = name.to_ascii_lowercase();
if normalized_name == "accept" {
if let Some(accept) = transport.headers.get_mut(&normalized_name) {
accept.push(',');
accept.push_str(value);
} else {
transport = transport.with_header(name, value);
}
} else if transport.headers.contains_key(&normalized_name) {
// Only list-valued Accept fields are merged. Every routing,
// security, and singleton header remains cardinality-strict.
return Err(HttpResponse::bad_request());
} else {
transport = transport.with_header(name, value);
}
}
if !query.is_empty() {
for pair in query.split('&') {
let Some((name, value)) = pair.split_once('=') else {
return Err(HttpResponse::bad_request());
};
if name.is_empty()
|| name
.bytes()
.chain(value.bytes())
.any(|byte| matches!(byte, b'\r' | b'\n' | b'\0'))
|| transport.query.contains_key(name)
{
return Err(HttpResponse::bad_request());
}
transport = transport.with_query(name, value);
}
}
Ok(transport)
}
/// Captures exactly one native `Authorization` field before the H1 request is
/// decoded into the transport representation. The raw H1 field sequence is
/// the only ingress that can represent duplicate case variants, so reject it
/// here rather than silently dropping custody before transport authorization.
fn h1_transport_authorization(
request: &asupersync::http::h1::Request,
) -> Result<TransportAuthorization, HttpResponse> {
if let Some((_, query)) = request.uri.split_once('?')
&& auth::query_has_access_credentials(query)
{
return Err(native_http_credential_location_rejection());
}
let mut singleton = None;
for (name, value) in &request.headers {
if name.eq_ignore_ascii_case("authorization") {
if singleton.replace(value.as_str()).is_some() {
return Err(native_http_authentication_rejection());
}
}
}
Ok(TransportAuthorization::from_singleton_header(singleton))
}
/// Retains the same singleton rule for direct public `HttpRequest` ingress.
/// Native H1 performs this check before conversion; direct callers can build a
/// header map themselves and must receive the identical case-insensitive
/// rejection rather than an order-dependent credential choice.
fn transport_authorization_from_http_request(
request: &HttpRequest,
) -> Result<TransportAuthorization, HttpResponse> {
if request
.query
.keys()
.any(|name| auth::query_has_access_credentials(name))
{
return Err(native_http_credential_location_rejection());
}
let mut singleton = None;
for (name, value) in &request.headers {
if name.eq_ignore_ascii_case("authorization") {
if singleton.replace(value.as_str()).is_some() {
return Err(native_http_authentication_rejection());
}
}
}
Ok(TransportAuthorization::from_singleton_header(singleton))
}
/// Produces the bounded native HTTP authentication challenge used before any
/// public-session or SSE-body mutation. It intentionally carries no provider
/// detail and never reflects a credential.
fn native_http_authentication_rejection() -> HttpResponse {
HttpResponse::new(HttpStatus::UNAUTHORIZED).with_header("www-authenticate", "Bearer")
}
/// A fixed migration diagnostic: neither the source name nor its value is
/// reflected, and no provider has been invoked for this credential location.
fn native_http_credential_location_rejection() -> HttpResponse {
native_http_authentication_rejection().with_json(&serde_json::json!({
"error": "invalid_request",
"message": "HTTP credentials must use the Authorization header"
}))
}
fn oauth_http_no_store(response: HttpResponse) -> HttpResponse {
response
.with_header("cache-control", "no-store")
.with_header("pragma", "no-cache")
}
fn oauth_http_error(error: OAuthError) -> HttpResponse {
let status = match &error {
OAuthError::ServerError(_) | OAuthError::TemporarilyUnavailable(_) => {
HttpStatus::SERVICE_UNAVAILABLE
}
_ => HttpStatus::BAD_REQUEST,
};
// OAuth error descriptions may contain implementation detail. The public
// route exposes only the standardized error code.
oauth_http_no_store(
HttpResponse::new(status).with_json(&serde_json::json!({ "error": error.error_code() })),
)
}
/// RFC 6749 section 5.2 requires a matching challenge only when failed client
/// authentication used the Authorization header. Form and public-client
/// failures remain HTTP 400 without inviting a different authentication method.
fn oauth_http_client_authentication_error(
error: OAuthError,
used_basic_authorization: bool,
) -> HttpResponse {
match &error {
OAuthError::InvalidClient(_) if used_basic_authorization => {
oauth_http_client_authentication_rejection()
}
_ => oauth_http_error(error),
}
}
fn oauth_http_invalid_request() -> HttpResponse {
oauth_http_no_store(
HttpResponse::bad_request().with_json(&serde_json::json!({ "error": "invalid_request" })),
)
}
fn oauth_http_method_not_allowed(allow: &'static str) -> HttpResponse {
oauth_http_no_store(
HttpResponse::new(HttpStatus::METHOD_NOT_ALLOWED).with_header("allow", allow),
)
}
fn oauth_http_client_authentication_rejection() -> HttpResponse {
oauth_http_no_store(
HttpResponse::new(HttpStatus::UNAUTHORIZED)
.with_header("www-authenticate", "Basic realm=\"oauth\"")
.with_json(&serde_json::json!({ "error": "invalid_client" })),
)
}
/// Selects one credential location before any OAuth state mutation. Basic
/// username/password components use OAuth form decoding, not raw HTTP Basic
/// text (RFC 6749 section 2.3.1). Decoding is bounded before allocation and all
/// intermediate credential buffers are zeroized on success and rejection.
fn oauth_http_client_credentials(
admission: &mut OAuthParameterAdmission,
authorization: Option<&str>,
) -> Result<(String, Option<String>, oauth::TokenEndpointAuthMethod), HttpResponse> {
let Some(authorization) = authorization else {
let client_id = oauth_required_parameter(admission, OAuthParameterName::ClientId)
.map_err(oauth_http_error)?;
let client_secret_present = admission
.parameters()
.iter()
.any(|parameter| parameter.is_defined() && parameter.name() == "client_secret");
let client_secret = admission
.take_defined_value(OAuthParameterName::ClientSecret)
.map(|value| value.into_string())
.or_else(|| client_secret_present.then(String::new));
let method = if client_secret.is_some() {
oauth::TokenEndpointAuthMethod::ClientSecretPost
} else {
oauth::TokenEndpointAuthMethod::None
};
return Ok((client_id, client_secret, method));
};
// Keep the raw parameter inventory: empty client_id/client_secret fields
// are omitted from the taking surface but still conflict with Basic.
if admission.parameters().iter().any(|parameter| {
parameter.is_defined() && matches!(parameter.name(), "client_id" | "client_secret")
}) {
return Err(oauth_http_invalid_request());
}
let authorization = authorization.trim_matches([' ', '\t']);
let scheme = authorization.split_ascii_whitespace().next().unwrap_or("");
if !scheme.eq_ignore_ascii_case("basic") {
return Err(oauth_http_invalid_request());
}
let (scheme, encoded) = authorization
.split_once(' ')
.ok_or_else(oauth_http_client_authentication_rejection)?;
if !scheme.eq_ignore_ascii_case("basic") {
return Err(oauth_http_client_authentication_rejection());
}
let encoded = encoded.trim_start_matches(' ');
// Every UTF-8 byte can expand to three percent-encoded bytes; add the
// Basic colon and padded base64 expansion. This also bounds malformed
// headers without allocating their decoded representation.
const MAX_BASIC_DECODED_BYTES: usize =
3 * (oauth::MAX_OAUTH_CLIENT_ID_BYTES + oauth::MAX_OAUTH_CLIENT_CREDENTIAL_BYTES) + 1;
const MAX_BASIC_ENCODED_BYTES: usize = 4 * MAX_BASIC_DECODED_BYTES.div_ceil(3);
if encoded.is_empty() || encoded.len() > MAX_BASIC_ENCODED_BYTES {
return Err(oauth_http_client_authentication_rejection());
}
use base64::Engine as _;
let mut decoded = zeroize::Zeroizing::new(Vec::with_capacity(encoded.len().div_ceil(4) * 3));
base64::engine::general_purpose::STANDARD
.decode_vec(encoded, &mut decoded)
.map_err(|_| oauth_http_client_authentication_rejection())?;
if decoded.len() > MAX_BASIC_DECODED_BYTES {
return Err(oauth_http_client_authentication_rejection());
}
let colon = decoded
.iter()
.position(|byte| *byte == b':')
.ok_or_else(oauth_http_client_authentication_rejection)?;
let client_id = zeroize::Zeroizing::new(
oauth::decode_oauth_form_component(&decoded[..colon], oauth::MAX_OAUTH_CLIENT_ID_BYTES)
.map_err(|_| oauth_http_client_authentication_rejection())?,
);
let client_secret = zeroize::Zeroizing::new(
oauth::decode_oauth_form_component(
&decoded[colon + 1..],
oauth::MAX_OAUTH_CLIENT_CREDENTIAL_BYTES,
)
.map_err(|_| oauth_http_client_authentication_rejection())?,
);
if client_id.is_empty()
|| client_id.chars().any(char::is_control)
|| client_secret.chars().any(char::is_control)
{
return Err(oauth_http_client_authentication_rejection());
}
// An empty Basic password stays Some(""): it cannot downgrade to a
// public client's no-secret authentication in the underlying verifier.
Ok((
client_id.to_string(),
Some(client_secret.to_string()),
oauth::TokenEndpointAuthMethod::ClientSecretBasic,
))
}
fn h1_oauth_singleton_header<'a>(
request: &'a asupersync::http::h1::Request,
expected_name: &str,
) -> Result<Option<&'a str>, HttpResponse> {
let mut singleton = None;
for (name, value) in &request.headers {
if name.eq_ignore_ascii_case(expected_name) {
if singleton.replace(value.as_str()).is_some() {
return Err(oauth_http_invalid_request());
}
}
}
Ok(singleton)
}
fn oauth_json_content_type(request: &asupersync::http::h1::Request) -> Result<(), HttpResponse> {
let Some(content_type) = h1_oauth_singleton_header(request, "content-type")? else {
return Err(HttpResponse::new(HttpStatus(415)));
};
if !content_type
.trim_matches([' ', '\t'])
.eq_ignore_ascii_case("application/json")
{
return Err(HttpResponse::new(HttpStatus(415)));
}
Ok(())
}
fn oauth_form_content_type(request: &asupersync::http::h1::Request) -> Result<(), HttpResponse> {
let Some(content_type) = h1_oauth_singleton_header(request, "content-type")? else {
return Err(HttpResponse::new(HttpStatus(415)));
};
if !content_type
.trim_matches([' ', '\t'])
.eq_ignore_ascii_case("application/x-www-form-urlencoded")
{
return Err(HttpResponse::new(HttpStatus(415)));
}
Ok(())
}
fn oauth_required_parameter(
admission: &mut OAuthParameterAdmission,
name: OAuthParameterName,
) -> Result<String, OAuthError> {
admission
.take_defined_value(name)
.map(|value| value.into_string())
.ok_or_else(|| {
OAuthError::InvalidRequest("required OAuth parameter is missing".to_string())
})
}
fn oauth_authorization_request(
admission: &mut OAuthParameterAdmission,
) -> Result<AuthorizationRequest, OAuthError> {
let code_challenge_method =
oauth_required_parameter(admission, OAuthParameterName::CodeChallengeMethod)?;
let code_challenge_method =
CodeChallengeMethod::parse(&code_challenge_method).ok_or_else(|| {
OAuthError::InvalidRequest("unsupported code_challenge_method".to_string())
})?;
let scopes = admission
.take_defined_value(OAuthParameterName::Scope)
.map(|value| {
value
.into_string()
.split_ascii_whitespace()
.map(str::to_owned)
.collect()
})
.unwrap_or_default();
Ok(AuthorizationRequest {
response_type: oauth_required_parameter(admission, OAuthParameterName::ResponseType)?,
client_id: oauth_required_parameter(admission, OAuthParameterName::ClientId)?,
redirect_uri: oauth_required_parameter(admission, OAuthParameterName::RedirectUri)?,
resource: admission
.take_defined_value(OAuthParameterName::Resource)
.map(|value| value.into_string()),
scopes,
state: admission
.take_defined_value(OAuthParameterName::State)
.map(|value| value.into_string()),
code_challenge: oauth_required_parameter(admission, OAuthParameterName::CodeChallenge)?,
code_challenge_method,
})
}
fn oauth_token_request(
admission: &mut OAuthParameterAdmission,
client_id: String,
client_secret: Option<String>,
client_authentication_method: oauth::TokenEndpointAuthMethod,
development_client_credentials_enabled: bool,
) -> Result<TokenRequest, OAuthError> {
let grant_type = oauth_required_parameter(admission, OAuthParameterName::GrantType)?;
let machine_grant =
development_client_credentials_enabled && grant_type == "client_credentials";
if machine_grant
&& admission.parameters().iter().any(|parameter| {
matches!(
parameter.name(),
"code"
| "redirect_uri"
| "code_verifier"
| "refresh_token"
| "client_assertion"
| "client_assertion_type"
)
})
{
return Err(OAuthError::InvalidRequest(
"machine requests must use only the configured client authentication method"
.to_string(),
));
}
let scopes = admission
.take_defined_value(OAuthParameterName::Scope)
.map(|value| {
let value = value.into_string();
if machine_grant {
// RFC 6749 scope is a nonempty SP-separated sequence. Do not
// normalize tabs, repeated spaces or leading/trailing spaces
// into an authorized machine request.
if value.split(' ').any(|scope| {
scope.is_empty()
|| !scope
.bytes()
.all(|byte| matches!(byte, 0x21 | 0x23..=0x5B | 0x5D..=0x7E))
}) {
return Err(OAuthError::InvalidScope(
"invalid machine scope syntax".to_string(),
));
}
}
Ok(value.split_ascii_whitespace().map(str::to_owned).collect())
})
.transpose()?;
Ok(TokenRequest {
grant_type,
code: admission
.take_defined_value(OAuthParameterName::Code)
.map(|value| value.into_string()),
redirect_uri: admission
.take_defined_value(OAuthParameterName::RedirectUri)
.map(|value| value.into_string()),
client_id,
client_secret,
client_authentication_method,
code_verifier: admission
.take_defined_value(OAuthParameterName::CodeVerifier)
.map(|value| value.into_string()),
refresh_token: admission
.take_defined_value(OAuthParameterName::RefreshToken)
.map(|value| value.into_string()),
scopes,
resource: admission
.take_defined_value(OAuthParameterName::Resource)
.map(|value| value.into_string()),
})
}
/// OIDC Core 5.3 UserInfo: the discovery document's `userinfo_endpoint`
/// (bd-vej30). The access token is accepted only as an RFC 6750 Bearer
/// `Authorization` header; failures carry the RFC 6750 challenge and never
/// reflect the credential.
#[cfg(feature = "builtin-auth-server")]
fn oidc_userinfo_response(
oidc: &crate::oauth::OidcHttpRoutes,
request: &asupersync::http::h1::Request,
authorization_header: Option<&str>,
raw_query: &str,
) -> HttpResponse {
if !matches!(request.method, Http1Method::Get | Http1Method::Post) {
return oauth_http_method_not_allowed("GET, POST");
}
if !raw_query.is_empty() || !request.body.is_empty() {
return oauth_http_invalid_request();
}
let bearer = authorization_header.and_then(|header| {
let (scheme, token) = header.trim().split_once(' ')?;
let token = token.trim_start();
(scheme.eq_ignore_ascii_case("bearer")
&& !token.is_empty()
&& !token.contains(char::is_whitespace))
.then_some(token)
});
let Some(token) = bearer else {
return oidc_userinfo_challenge(HttpStatus::UNAUTHORIZED, None);
};
match oidc.provider().userinfo(token) {
Ok(claims) => oauth_http_no_store(HttpResponse::ok().with_json(&claims)),
Err(oidc::OidcError::MissingOpenIdScope) => {
oidc_userinfo_challenge(HttpStatus::FORBIDDEN, Some("insufficient_scope"))
}
Err(_) => oidc_userinfo_challenge(HttpStatus::UNAUTHORIZED, Some("invalid_token")),
}
}
#[cfg(feature = "builtin-auth-server")]
fn oidc_userinfo_challenge(status: HttpStatus, error: Option<&'static str>) -> HttpResponse {
let Some(error) = error else {
return oauth_http_no_store(
HttpResponse::new(status).with_header("www-authenticate", "Bearer"),
);
};
oauth_http_no_store(
HttpResponse::new(status)
.with_header("www-authenticate", format!("Bearer error=\"{error}\""))
.with_json(&serde_json::json!({ "error": error })),
)
}
fn dispatch_oauth_h1_request(
routes: &OAuthHttpRoutes,
request: &asupersync::http::h1::Request,
raw_path: &str,
raw_query: &str,
) -> HttpResponse {
let authorization_header = match h1_oauth_singleton_header(request, "authorization") {
Ok(header) => header,
Err(response) => return response,
};
if raw_path == routes.metadata_path() {
if authorization_header.is_some() || request.uri.contains('?') || !request.body.is_empty() {
return oauth_http_invalid_request();
}
if !matches!(request.method, Http1Method::Get) {
return oauth_http_method_not_allowed("GET");
}
return match routes.authorization_server_metadata() {
Ok(metadata) => oauth_http_no_store(HttpResponse::ok().with_json(&metadata)),
Err(_) => oauth_http_no_store(HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE)),
};
}
#[cfg(feature = "builtin-auth-server")]
if let Some(oidc) = routes.oidc_routes() {
if raw_path == oidc.userinfo_path() {
return oidc_userinfo_response(oidc, request, authorization_header, raw_query);
}
if raw_path == oidc.discovery_path() || raw_path == oidc.jwks_path() {
if authorization_header.is_some() || !raw_query.is_empty() || !request.body.is_empty() {
return oauth_http_invalid_request();
}
if !matches!(request.method, Http1Method::Get) {
return oauth_http_method_not_allowed("GET");
}
if raw_path == oidc.discovery_path() {
return oauth_http_no_store(
HttpResponse::ok().with_json(
&oidc
.provider()
.discovery_document(routes.public_endpoint_base()),
),
);
}
return oidc
.provider()
.published_jwks_document(oidc.jwks_uri())
.map(|body| {
oauth_http_no_store(
HttpResponse::ok()
.with_header("content-type", "application/jwk-set+json")
.with_body(body),
)
})
.unwrap_or_else(|_| HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE));
}
}
if raw_path == routes.authorization_path() {
if authorization_header.is_some() {
return oauth_http_invalid_request();
}
if !matches!(request.method, Http1Method::Get) {
return oauth_http_method_not_allowed("GET");
}
if !request.body.is_empty() {
return oauth_http_invalid_request();
}
let mut admission = match OAuthParameterAdmission::admit(
OAuthParameterEndpoint::AuthorizationQuery,
raw_query.as_bytes(),
) {
Ok(admission) => admission,
Err(_) => return oauth_http_invalid_request(),
};
let authorization = match oauth_authorization_request(&mut admission) {
Ok(authorization) => authorization,
Err(error) => return oauth_http_error(error),
};
return match routes.server().authorize(&authorization) {
Ok((_code, redirect)) => oauth_http_no_store(
HttpResponse::new(HttpStatus(303)).with_header("location", redirect),
),
Err(error) => routes
.server()
.authorization_error_redirect(&authorization, &error)
.map(|redirect| {
oauth_http_no_store(
HttpResponse::new(HttpStatus(303)).with_header("location", redirect),
)
})
.unwrap_or_else(|| oauth_http_error(error)),
};
}
if routes.registration_path() == Some(raw_path) {
if authorization_header.is_some() || !raw_query.is_empty() {
return oauth_http_invalid_request();
}
if !matches!(request.method, Http1Method::Post) {
return oauth_http_method_not_allowed("POST");
}
if let Err(response) = oauth_json_content_type(request) {
return oauth_http_no_store(response);
}
if admit_security_document(SecurityDocumentKind::ClientRegistration, &request.body).is_err()
{
return oauth_http_invalid_request();
}
let registration =
match serde_json::from_slice::<NativePublicClientRegistrationRequest>(&request.body) {
Ok(registration) => registration,
Err(_) => return oauth_http_invalid_request(),
};
return match routes.server().register_native_public_client(registration) {
Ok(response) => {
oauth_http_no_store(HttpResponse::new(HttpStatus(201)).with_json(&response))
}
Err(error) => oauth_http_error(error),
};
}
if !raw_query.is_empty() {
return oauth_http_invalid_request();
}
if raw_path == routes.token_path() {
if !matches!(request.method, Http1Method::Post) {
return oauth_http_method_not_allowed("POST");
}
if let Err(response) = oauth_form_content_type(request) {
return oauth_http_no_store(response);
}
let mut admission = match OAuthParameterAdmission::admit(
OAuthParameterEndpoint::TokenForm,
&request.body,
) {
Ok(admission) => admission,
Err(_) => return oauth_http_invalid_request(),
};
let (client_id, client_secret, client_authentication_method) =
match oauth_http_client_credentials(&mut admission, authorization_header) {
Ok(credentials) => credentials,
Err(response) => return response,
};
let token = match oauth_token_request(
&mut admission,
client_id,
client_secret,
client_authentication_method,
routes.server().development_client_credentials_enabled(),
) {
Ok(token) => token,
Err(error) => return oauth_http_error(error),
};
return match routes.server().token(&token) {
Ok(response) => oauth_http_no_store(HttpResponse::ok().with_json(&response)),
Err(error) => {
oauth_http_client_authentication_error(error, authorization_header.is_some())
}
};
}
if !matches!(request.method, Http1Method::Post) {
return oauth_http_method_not_allowed("POST");
}
if let Err(response) = oauth_form_content_type(request) {
return oauth_http_no_store(response);
}
let mut admission =
match OAuthParameterAdmission::admit(OAuthParameterEndpoint::RevocationForm, &request.body)
{
Ok(admission) => admission,
Err(_) => return oauth_http_invalid_request(),
};
let token = match oauth_required_parameter(&mut admission, OAuthParameterName::Token) {
Ok(token) => token,
Err(error) => return oauth_http_error(error),
};
let (client_id, client_secret, client_authentication_method) =
match oauth_http_client_credentials(&mut admission, authorization_header) {
Ok(credentials) => credentials,
Err(response) => return response,
};
match routes.server().revoke(
&token,
&client_id,
client_secret.as_deref(),
client_authentication_method,
) {
Ok(()) => oauth_http_no_store(HttpResponse::ok()),
Err(error) => oauth_http_client_authentication_error(error, authorization_header.is_some()),
}
}
/// Immutable route facts needed before the generic HTTP/1 decoder accepts a
/// request body. Keeping this separate from the OAuth server prevents the
/// listener's MCP-sized body allowance from becoming an OAuth body allowance.
#[derive(Clone)]
struct OAuthNativeH1RouteLimits {
metadata: String,
authorization: String,
token: String,
revocation: String,
registration: Option<String>,
#[cfg(feature = "builtin-auth-server")]
oidc_discovery: Option<String>,
#[cfg(feature = "builtin-auth-server")]
oidc_jwks: Option<String>,
#[cfg(feature = "builtin-auth-server")]
oidc_userinfo: Option<String>,
}
impl OAuthNativeH1RouteLimits {
fn from_routes(routes: &OAuthHttpRoutes) -> Self {
Self {
metadata: routes.metadata_path().to_owned(),
authorization: routes.authorization_path().to_owned(),
token: routes.token_path().to_owned(),
revocation: routes.revocation_path().to_owned(),
registration: routes.registration_path().map(str::to_owned),
#[cfg(feature = "builtin-auth-server")]
oidc_discovery: routes
.oidc_routes()
.map(|oidc| oidc.discovery_path().to_owned()),
#[cfg(feature = "builtin-auth-server")]
oidc_jwks: routes.oidc_routes().map(|oidc| oidc.jwks_path().to_owned()),
#[cfg(feature = "builtin-auth-server")]
oidc_userinfo: routes
.oidc_routes()
.map(|oidc| oidc.userinfo_path().to_owned()),
}
}
fn body_limit_for_path(&self, path: &str) -> Option<usize> {
if path == self.metadata || path == self.authorization || {
#[cfg(feature = "builtin-auth-server")]
{
self.oidc_discovery.as_deref() == Some(path)
|| self.oidc_jwks.as_deref() == Some(path)
|| self.oidc_userinfo.as_deref() == Some(path)
}
#[cfg(not(feature = "builtin-auth-server"))]
{
false
}
} {
Some(0)
} else if path == self.token || path == self.revocation {
Some(oauth::MAX_OAUTH_FORM_BODY_BYTES)
} else if self.registration.as_deref() == Some(path) {
Some(MAX_CLIENT_REGISTRATION_BYTES)
} else {
None
}
}
}
/// Pre-body routing state for [`NativeHttp1Codec`].
#[derive(Clone, Copy, PartialEq, Eq)]
enum NativeHttp1PreBodyAdmission {
AwaitingHead,
Complete,
}
/// Native HTTP/1 codec with a route-aware OAuth pre-body limit.
///
/// The generic codec still owns all HTTP parsing and MCP's configured body
/// limit. This wrapper only examines a complete unconsumed request head before
/// delegating, so OAuth `Content-Length` and transfer-encoding admission can
/// fail before the generic decoder allocates a body at the larger MCP limit.
struct NativeHttp1Codec {
inner: Http1Codec,
oauth_limits: Option<OAuthNativeH1RouteLimits>,
pre_body_admission: NativeHttp1PreBodyAdmission,
}
impl NativeHttp1Codec {
fn new(max_body_size: usize, routes: Option<&OAuthHttpRoutes>) -> Self {
Self {
inner: Http1Codec::new().max_body_size(max_body_size),
oauth_limits: routes.map(OAuthNativeH1RouteLimits::from_routes),
pre_body_admission: NativeHttp1PreBodyAdmission::AwaitingHead,
}
}
fn pre_admit_oauth_body(&mut self, source: &BytesMut) -> Result<(), Http1DecodeError> {
if self.pre_body_admission == NativeHttp1PreBodyAdmission::Complete {
return Ok(());
}
let Some(head_end) = source.windows(4).position(|window| window == b"\r\n\r\n") else {
return Ok(());
};
// The underlying codec remains the sole authority for malformed HTTP.
// A non-UTF-8 or malformed head is left for it to reject.
let Ok(head) = std::str::from_utf8(&source[..head_end]) else {
return Ok(());
};
let Some(request_line) = head.split("\r\n").next() else {
return Ok(());
};
let mut request_line_parts = request_line.split(' ');
let Some(_method) = request_line_parts.next() else {
return Ok(());
};
let Some(target) = request_line_parts.next() else {
return Ok(());
};
let raw_path = target.split_once('?').map_or(target, |(path, _)| path);
let Some(limits) = self.oauth_limits.as_ref() else {
self.pre_body_admission = NativeHttp1PreBodyAdmission::Complete;
return Ok(());
};
let Some(body_limit) = limits.body_limit_for_path(raw_path) else {
self.pre_body_admission = NativeHttp1PreBodyAdmission::Complete;
return Ok(());
};
if raw_path == limits.authorization {
let query = target.split_once('?').map_or("", |(_, query)| query);
if query.len() > oauth::MAX_OAUTH_AUTHORIZATION_QUERY_BYTES {
return Err(Http1DecodeError::BodyTooLarge);
}
}
for line in head.split("\r\n").skip(1) {
let Some((name, value)) = line.split_once(':') else {
continue;
};
if name.eq_ignore_ascii_case("transfer-encoding") {
// An OAuth route has a small, fixed body budget. Rejecting a
// streaming transfer before the generic decoder starts
// buffering preserves that budget without constraining MCP.
return Err(Http1DecodeError::BodyTooLarge);
}
if name.eq_ignore_ascii_case("content-length")
&& value
.trim()
.parse::<usize>()
.is_ok_and(|length| length > body_limit)
{
return Err(Http1DecodeError::BodyTooLarge);
}
}
self.pre_body_admission = NativeHttp1PreBodyAdmission::Complete;
Ok(())
}
}
impl Decoder for NativeHttp1Codec {
type Item = asupersync::http::h1::Request;
type Error = Http1DecodeError;
fn decode(&mut self, source: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
self.pre_admit_oauth_body(source)?;
let request = self.inner.decode(source)?;
if request.is_some() {
self.pre_body_admission = NativeHttp1PreBodyAdmission::AwaitingHead;
}
Ok(request)
}
}
impl Encoder<Http1Response> for NativeHttp1Codec {
type Error = Http1DecodeError;
fn encode(
&mut self,
response: Http1Response,
destination: &mut BytesMut,
) -> Result<(), Self::Error> {
self.inner.encode(response, destination)
}
}
fn native_http1_codec(endpoint: &ServerHttpEndpoint) -> NativeHttp1Codec {
NativeHttp1Codec::new(
endpoint.server.http_config.handler_config.max_body_size,
endpoint.server.oauth_http_routes.as_ref(),
)
}
async fn next_native_http1_request(
cx: &Cx,
listener_shutdown: &HttpListenerShutdown,
framed: &mut Framed<AsyncTcpStream, NativeHttp1Codec>,
read_timeout: Duration,
) -> Option<Result<asupersync::http::h1::Request, Http1DecodeError>> {
// Every wait below is bounded by this one deadline, so an idle or
// trickling peer is closed and its connection slot released.
let deadline = cx
.now()
.saturating_add_nanos(u64::try_from(read_timeout.as_nanos()).unwrap_or(u64::MAX));
loop {
if listener_shutdown.is_requested() || cx.checkpoint().is_err() {
return None;
}
let now = cx.now();
if now >= deadline {
return None;
}
let wait = HTTP_ACCEPT_CANCEL_POLL.min(Duration::from_nanos(deadline.duration_since(now)));
if let Ok(request) = asupersync::time::timeout(now, wait, framed.next()).await {
return request;
}
}
}
/// Handles an OAuth-only route before MCP's transport conversion can merge
/// query fields or discard raw header cardinality. Returns true only when the
/// request path was one of the installed immutable OAuth routes.
async fn serve_oauth_h1_request<T>(
cx: &Cx,
listener_shutdown: &HttpListenerShutdown,
framed: &mut Framed<T, NativeHttp1Codec>,
endpoint: &ServerHttpEndpoint,
request: &asupersync::http::h1::Request,
) -> bool
where
T: asupersync::io::AsyncWrite + Unpin,
{
let Some(routes) = endpoint.server.oauth_http_routes.as_ref() else {
return false;
};
let (raw_path, raw_query) = request
.uri
.split_once('?')
.map_or((request.uri.as_str(), ""), |(path, query)| (path, query));
if !routes.has_path(raw_path) {
return false;
}
if cx.checkpoint().is_err() {
let _ = send_h1_response(
cx,
listener_shutdown,
framed,
HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE),
)
.await;
return true;
}
// RFC 8414 metadata is derived only from immutable, bounded configuration.
// It never invokes consent, authentication, signing, or issuer state, so
// discovery remains available without occupying a blocking worker.
if raw_path == routes.metadata_path() {
let response = dispatch_oauth_h1_request(routes, request, raw_path, raw_query);
let _ = send_h1_response(cx, listener_shutdown, framed, response).await;
return true;
}
let routes = routes.clone();
let request = request.clone();
let raw_path = raw_path.to_owned();
let raw_query = raw_query.to_owned();
let response = match cx.spawn_blocking(move |route_cx| {
if route_cx.checkpoint().is_err() {
return HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE);
}
dispatch_oauth_h1_request(&routes, &request, &raw_path, &raw_query)
}) {
Ok(mut dispatch) => match dispatch.join(cx).await {
Ok(response) => response,
Err(_) => HttpResponse::internal_error(),
},
Err(_) => HttpResponse::internal_error(),
};
let _ = send_h1_response(cx, listener_shutdown, framed, response).await;
true
}
fn h1_response(mut response: HttpResponse) -> Http1Response {
response
.headers
.retain(|name, _| !name.eq_ignore_ascii_case("connection"));
response = response.with_header("connection", "close");
let mut h1 = Http1Response::new(response.status.0, "", response.body);
for (name, value) in response.headers {
h1 = h1.with_header(name, value);
}
h1
}
async fn send_h1_response<T>(
cx: &Cx,
listener_shutdown: &HttpListenerShutdown,
framed: &mut Framed<T, NativeHttp1Codec>,
response: HttpResponse,
) -> Result<(), ()>
where
T: asupersync::io::AsyncWrite + Unpin,
{
// The listener's shutdown cancellation owns every ordinary H1 connection.
// Unlike SSE terminal controls, an immediate response has no protected
// drain phase and must never begin a new wire write after cancellation.
if listener_shutdown.is_requested() {
return Err(());
}
cx.checkpoint().map_err(|_| ())?;
framed.send(h1_response(response)).map_err(|_| ())?;
// `Framed::send` only accepts the response into its codec buffer; the
// socket commit happens while closing. Re-check so a shutdown that wins
// between encoding and that commit suppresses the buffered response.
if listener_shutdown.is_requested() {
return Err(());
}
cx.checkpoint().map_err(|_| ())?;
std::future::poll_fn(|task_cx| {
if listener_shutdown.is_requested() || cx.is_cancel_requested() {
// Drop the framed connection with its buffered response rather
// than allowing a shutdown-cancelled task to commit it.
return std::task::Poll::Ready(Ok(()));
}
framed.poll_close(task_cx)
})
.await
.map_err(|_| ())
}
/// Writes the one pre-representation rejection that a modern SSE request can
/// elect after dispatch. This path deliberately owns a raw write half: the
/// request body has already been split so peer closure can cancel dispatch
/// while the outcome is still unknown.
async fn send_h1_bad_request_response<W>(
cx: &Cx,
listener_shutdown: &HttpListenerShutdown,
writer: &mut W,
response: &HttpResponse,
) -> Result<(), ()>
where
W: asupersync::io::AsyncWrite + Unpin,
{
if response.status != HttpStatus::BAD_REQUEST {
return Err(());
}
let mut head = b"HTTP/1.1 400 Bad Request\r\n".to_vec();
for (name, value) in &response.headers {
if name.eq_ignore_ascii_case("connection")
|| name.eq_ignore_ascii_case("content-length")
|| name.eq_ignore_ascii_case("transfer-encoding")
{
continue;
}
if name
.bytes()
.chain(value.bytes())
.any(|byte| matches!(byte, b'\r' | b'\n' | b'\0'))
{
return Err(());
}
head.extend_from_slice(name.as_bytes());
head.extend_from_slice(b": ");
head.extend_from_slice(value.as_bytes());
head.extend_from_slice(b"\r\n");
}
head.extend_from_slice(format!("content-length: {}\r\n", response.body.len()).as_bytes());
head.extend_from_slice(b"connection: close\r\n\r\n");
head.extend_from_slice(&response.body);
if listener_shutdown.is_requested() {
return Err(());
}
cx.checkpoint().map_err(|_| ())?;
writer.write_all(&head).await.map_err(|_| ())?;
writer.flush().await.map_err(|_| ())
}
fn sse_response_head(response: &HttpResponse) -> Result<Vec<u8>, ()> {
let mut head = format!("HTTP/1.1 {} OK\r\n", response.status.0).into_bytes();
for (name, value) in &response.headers {
if name.eq_ignore_ascii_case("connection") {
continue;
}
if name
.bytes()
.chain(value.bytes())
.any(|byte| matches!(byte, b'\r' | b'\n' | b'\0'))
{
return Err(());
}
head.extend_from_slice(name.as_bytes());
head.extend_from_slice(b": ");
head.extend_from_slice(value.as_bytes());
head.extend_from_slice(b"\r\n");
}
head.extend_from_slice(b"transfer-encoding: chunked\r\nconnection: close\r\n\r\n");
Ok(head)
}
#[cfg(feature = "legacy-2024-11-05")]
async fn send_legacy_sse_stream(
cx: &Cx,
stream: AsyncTcpStream,
response: DualEraHttpLegacySseResponse,
) -> Result<(), ()> {
let (mut peer_reader, mut stream) = stream.into_split();
let (peer_closed_sender, mut peer_closed_receiver) = oneshot::channel();
let mut peer_watch = cx
.spawn(move |_peer_cx| async move {
let mut byte = [0_u8; 1];
// The SSE GET request is fully decoded before this point. Any
// later read completion (EOF, error, or unexpected bytes) closes
// this one-way body without waiting for an application event to
// cause a write-side error.
let _ = peer_reader.read(&mut byte).await;
let _ = peer_closed_sender.send_blocking(());
})
.map_err(|_| ())?;
let result = async {
stream
.write_all(&sse_response_head(response.response())?)
.await
.map_err(|_| ())?;
stream.flush().await.map_err(|_| ())?;
// Park on the channel's registered receive waker so publication wakes
// this writer directly. Race peer closure through its own wake-driven
// signal so an idle stream still tears down promptly.
let mut response = response;
loop {
let next = {
let mut next_event = Box::pin(response.recv_event_async(cx));
let mut peer_closed = Box::pin(peer_closed_receiver.recv(cx));
std::future::poll_fn(|task_cx| {
if peer_closed.as_mut().poll(task_cx).is_ready() {
return std::task::Poll::Ready(None);
}
match next_event.as_mut().poll(task_cx) {
std::task::Poll::Ready(event) => std::task::Poll::Ready(Some(event)),
std::task::Poll::Pending => std::task::Poll::Pending,
}
})
.await
};
let Some(event) = next else {
break;
};
let event = match event {
Ok(event) => event,
Err(_) => break,
};
let bytes = event.to_bytes().map_err(|_| ())?;
let prefix = format!("{:X}\r\n", bytes.len());
stream.write_all(prefix.as_bytes()).await.map_err(|_| ())?;
stream.write_all(&bytes).await.map_err(|_| ())?;
stream.write_all(b"\r\n").await.map_err(|_| ())?;
stream.flush().await.map_err(|_| ())?;
}
let _ = stream.write_all(b"0\r\n\r\n").await;
let _ = stream.flush().await;
Ok(())
}
.await;
peer_watch.abort();
let _ = peer_watch.join(cx).await;
result
}
enum ModernSseDispatchElection {
Stream,
Immediate(HttpResponse),
Failed,
}
enum ModernSseNotificationDelivery {
Pending(Vec<JsonRpcRequest>),
Admitted,
Streaming,
}
/// A transport-owned signal invoked only after final router handler admission.
/// Application contexts cannot construct or invoke this HTTP representation gate.
pub(crate) type FinalHandlerAdmission = Arc<dyn Fn() + Send + Sync>;
struct ModernSseNotificationGate {
delivery: Mutex<ModernSseNotificationDelivery>,
subscription_request: bool,
outcome_gate: Option<Arc<ModernSseOutcomeGate>>,
terminal_delivery: Arc<FinalSubscriptionTerminalDelivery>,
committed_sender: NotificationSender,
commit_failed: Arc<AtomicBool>,
stream_admitted: Arc<AtomicBool>,
}
impl ModernSseNotificationGate {
fn publish(&self, notification: JsonRpcRequest) {
if final_subscription_terminal_notification(¬ification) {
self.terminal_delivery.mark_control_not_required();
return;
}
if self.commit_failed.load(Ordering::Acquire) {
return;
}
let mut delivery = self
.delivery
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match &mut *delivery {
ModernSseNotificationDelivery::Streaming => {
drop(delivery);
(self.committed_sender)(notification);
return;
}
ModernSseNotificationDelivery::Admitted => {
(self.committed_sender)(notification);
}
ModernSseNotificationDelivery::Pending(pending) => {
let acknowledgement = self.subscription_request
&& final_subscription_acknowledgement_notification(¬ification);
if !acknowledgement {
pending.push(notification);
return;
}
let deferred = std::mem::take(pending);
// Keep concurrent publishers behind the acknowledgement and
// the frames already waiting for that subscription boundary.
(self.committed_sender)(notification);
if !self.finish_stream_admission(&mut delivery) {
return;
}
for notification in deferred {
(self.committed_sender)(notification);
}
return;
}
}
self.finish_stream_admission(&mut delivery);
}
fn admit_handler(&self) {
if self.subscription_request || self.commit_failed.load(Ordering::Acquire) {
return;
}
let mut delivery = self
.delivery
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let ModernSseNotificationDelivery::Pending(pending) = &mut *delivery else {
return;
};
if pending.is_empty() {
// Admission alone does not commit an HTTP representation. A
// canonical error can still elect JSON 400 until a frame exists.
*delivery = ModernSseNotificationDelivery::Admitted;
return;
}
let mut deferred = std::mem::take(pending).into_iter();
if let Some(notification) = deferred.next() {
(self.committed_sender)(notification);
}
// The writer must be able to drain its bounded queue before the
// remainder is committed. Admission still follows one real frame.
if !self.finish_stream_admission(&mut delivery) {
return;
}
for notification in deferred {
(self.committed_sender)(notification);
}
}
fn finish_stream_admission(&self, delivery: &mut ModernSseNotificationDelivery) -> bool {
if self.commit_failed.load(Ordering::Acquire) {
self.terminal_delivery.mark_failed();
if let Some(outcome_gate) = &self.outcome_gate {
outcome_gate.elect(ModernSseDispatchElection::Failed);
}
return false;
}
*delivery = ModernSseNotificationDelivery::Streaming;
let elected = self
.outcome_gate
.as_ref()
.is_none_or(|gate| gate.elect(ModernSseDispatchElection::Stream));
if elected {
// HTTP has no in-band cancellation control. Once the body starts,
// its one terminal response owns the remaining settlement.
if self.outcome_gate.is_some() {
self.terminal_delivery.mark_control_not_required();
}
self.stream_admitted.store(true, Ordering::Release);
}
elected
}
fn take_pending(&self) -> Vec<JsonRpcRequest> {
match &mut *self
.delivery
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
{
ModernSseNotificationDelivery::Pending(pending) => std::mem::take(pending),
ModernSseNotificationDelivery::Admitted | ModernSseNotificationDelivery::Streaming => {
Vec::new()
}
}
}
}
enum ModernSseOutcomeGateState {
AwaitingElection(oneshot::Sender<ModernSseDispatchElection>),
Elected { stream: bool },
}
/// A one-time dispatch-to-writer handoff for one live modern SSE request.
///
/// The sender is taken exactly once by the request-owned dispatcher. The
/// writer waits on the paired cancel-safe oneshot rather than polling shared
/// state. Peer body closure wins before an election, while an already-ready
/// election remains authoritative when cancellation becomes ready alongside
/// the writer's poll.
struct ModernSseOutcomeGate {
state: Mutex<ModernSseOutcomeGateState>,
}
impl ModernSseOutcomeGate {
fn new() -> (Arc<Self>, oneshot::Receiver<ModernSseDispatchElection>) {
let (sender, receiver) = oneshot::channel();
(
Arc::new(Self {
state: Mutex::new(ModernSseOutcomeGateState::AwaitingElection(sender)),
}),
receiver,
)
}
/// Delivers one final representation election and reports whether this
/// caller linearized it. A failed send still counts as won: the receiver
/// may already have observed peer cancellation, but no later caller may
/// substitute a different representation.
fn elect(&self, election: ModernSseDispatchElection) -> bool {
let sender = {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if matches!(&*state, ModernSseOutcomeGateState::Elected { .. }) {
return false;
}
let stream = matches!(&election, ModernSseDispatchElection::Stream);
let ModernSseOutcomeGateState::AwaitingElection(sender) =
std::mem::replace(&mut *state, ModernSseOutcomeGateState::Elected { stream })
else {
return false;
};
sender
};
let _ = sender.send_blocking(election);
true
}
fn stream_admitted(&self) -> bool {
matches!(
&*self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
ModernSseOutcomeGateState::Elected { stream: true }
)
}
}
async fn await_modern_sse_dispatch_election(
cx: &Cx,
request_cancellation: &McpRequestCancellation,
receiver: &mut oneshot::Receiver<ModernSseDispatchElection>,
) -> Result<ModernSseDispatchElection, ()> {
let mut election = std::pin::pin!(receiver.recv(cx));
let mut cancelled = std::pin::pin!(request_cancellation.cancelled());
std::future::poll_fn(|task_cx| match election.as_mut().poll(task_cx) {
std::task::Poll::Ready(Ok(election)) => std::task::Poll::Ready(Ok(election)),
std::task::Poll::Ready(Err(_)) => std::task::Poll::Ready(Err(())),
std::task::Poll::Pending if cancelled.as_mut().poll(task_cx).is_ready() => {
std::task::Poll::Ready(Err(()))
}
std::task::Poll::Pending => std::task::Poll::Pending,
})
.await
}
fn spawn_modern_sse_dispatch(
cx: &Cx,
server: Arc<Server>,
http_stream_generation: u64,
inbound: InboundRequestContext,
request: JsonRpcRequest,
raw_params: Option<Arc<str>>,
auth_receipt: Option<AuthDispatchCustody>,
response_sender: StreamableHttpRequestResponseSender,
terminal_delivery: Arc<FinalSubscriptionTerminalDelivery>,
outcome_gate: Option<Arc<ModernSseOutcomeGate>>,
) -> Result<asupersync::runtime::TaskHandle<()>, DualEraHttpEndpointError> {
let policy = server.protocol_policy;
let notification_response_sender = response_sender.clone();
let notification_terminal_delivery = Arc::clone(&terminal_delivery);
let terminal_commit = Arc::new(Mutex::new(()));
cx.spawn(move |request_cx| async move {
let inbound = inbound.with_cx(request_cx.clone());
let cancellation = response_sender.request_cancellation();
let subscription_request = request.method == SUBSCRIPTIONS_LISTEN;
let mut duplicate_response = None;
let _active_request = match request.id.clone() {
Some(id) => match ActiveRequestGuard::try_new_with_cancellation(
Arc::clone(&server.active_requests),
http_stream_generation,
id.clone(),
request_cx.clone(),
cancellation.clone(),
) {
Ok(guard) => Some(guard),
Err(_) => {
duplicate_response = Some(JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: McpErrorCode::InvalidRequest.into(),
message: "Request id is already active".to_owned(),
data: None,
},
));
None
}
},
None => None,
};
let notification_cx = request_cx.clone();
let notification_cancellation = response_sender.request_cancellation();
let notification_terminal_commit = Arc::clone(&terminal_commit);
let notification_commit_failed = Arc::new(AtomicBool::new(false));
let notification_commit_failed_for_sender = Arc::clone(¬ification_commit_failed);
let committed_notification_sender: NotificationSender = Arc::new(move |notification| {
let terminal_control = final_subscription_terminal_notification(¬ification);
// Modern HTTP never puts `notifications/cancelled` on its SSE
// body. The registry elects a response-only terminal sequence
// for this request body, but keep the transport boundary strict
// if a future caller reaches this sender with such a control
// frame.
if terminal_control {
notification_terminal_delivery.mark_control_not_required();
return;
}
if notification_commit_failed_for_sender.load(Ordering::Acquire) {
return;
}
let _terminal_commit = notification_terminal_commit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let sent = retry_http_sse_commit(
¬ification_cx,
¬ification_cancellation,
|| {
notification_response_sender
.send_notification(¬ification_cx, notification.clone())
},
retryable_http_sse_transport_error,
);
if sent.is_err() {
// Exhausted transient contention is a delivery failure just as a
// hard transport error is. Mark the response body terminal and
// suppress every later log/response frame rather than silently
// dropping progress and violating the final-frame ordering.
notification_commit_failed_for_sender.store(true, Ordering::Release);
notification_terminal_delivery.mark_failed();
notification_cancellation.cancel();
}
});
// `Stream` is the live writer's linearized admission point. A later
// canonical -32021 may replace the terminal response only after this
// point; before it, the same error still elects an HTTP JSON 400.
let stream_admitted = Arc::new(AtomicBool::new(false));
// Subscriptions admit at their acknowledgement. Ordinary live HTTP
// requests admit at the router's handler boundary, keeping middleware
// and parameter-header rejections ahead of the first SSE frame.
let deferred_notifications = (subscription_request || outcome_gate.is_some()).then(|| {
Arc::new(ModernSseNotificationGate {
delivery: Mutex::new(ModernSseNotificationDelivery::Pending(Vec::new())),
subscription_request,
outcome_gate: outcome_gate.clone(),
terminal_delivery: Arc::clone(&terminal_delivery),
committed_sender: Arc::clone(&committed_notification_sender),
commit_failed: Arc::clone(¬ification_commit_failed),
stream_admitted: Arc::clone(&stream_admitted),
})
});
let notification_sender: NotificationSender = match &deferred_notifications {
Some(deferred_notifications) => {
let deferred_notifications = Arc::clone(deferred_notifications);
Arc::new(move |notification| {
deferred_notifications.publish(notification);
})
}
None => Arc::clone(&committed_notification_sender),
};
let handler_admission = deferred_notifications
.as_ref()
.filter(|_| outcome_gate.is_some() && !subscription_request)
.map(|gate| {
let gate = Arc::clone(gate);
Arc::new(move || gate.admit_handler()) as FinalHandlerAdmission
});
let response = match duplicate_response {
Some(response) => Some(response),
None => {
Arc::clone(&server)
.dispatch_with_protocol_policy_owned_and_handler_admission(
policy,
&inbound,
request,
raw_params,
auth_receipt,
None,
Some(http_stream_generation),
cancellation.clone(),
Some(Arc::clone(&terminal_delivery)),
notification_sender,
handler_admission,
)
.await
}
};
let mut canonical_after_stream_admission = false;
if let (Some(outcome_gate), Some(rejection)) = (
outcome_gate.as_ref(),
response
.as_ref()
.and_then(canonical_http_error_status_response),
) {
if outcome_gate.elect(ModernSseDispatchElection::Immediate(rejection)) {
return;
}
if outcome_gate.stream_admitted() {
canonical_after_stream_admission = true;
} else {
// An earlier failed election cannot safely commit a response
// after its body has retired.
return;
}
}
if outcome_gate.is_none()
&& subscription_request
&& response
.as_ref()
.is_some_and(is_canonical_missing_required_client_capability_response)
&& stream_admitted.load(Ordering::Acquire)
{
// The typed in-process endpoint has no alternate HTTP
// representation to elect, but after acknowledgement it has the
// same one-terminal-response obligation as a live Stream body.
canonical_after_stream_admission = true;
}
if let Some(deferred_notifications) = deferred_notifications {
for notification in deferred_notifications.take_pending() {
committed_notification_sender(notification);
}
}
if notification_commit_failed.load(Ordering::Acquire) {
terminal_delivery.mark_failed();
if let Some(outcome_gate) = &outcome_gate {
outcome_gate.elect(ModernSseDispatchElection::Failed);
}
return;
}
let mut terminal_commit_failed = false;
if let Some(response) = response {
let graceful_completion = final_subscription_completion_response(&response);
let elected_terminal_response = graceful_completion || canonical_after_stream_admission;
// The server-side graceful election cancels this request as its
// dispatch wake, which claims the finalization race; its elected
// terminal response is the sanctioned final frame and must still
// flush. A canonical error substituted after `Stream` has the
// same delivery contract. Every other response drops once
// cancellation won.
if cancellation.begin_finalization() || elected_terminal_response {
let _terminal_commit = terminal_commit
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if elected_terminal_response {
// This is deliberately before the queue commit: graceful
// shutdown waits for the writer to flush this terminal
// response, and a failed commit converts it to failed
// delivery rather than pretending it settled.
terminal_delivery.mark_completion_enqueued();
}
let sent = if elected_terminal_response {
// A server-owned shutdown may have cancelled the
// listener's root context. Its elected terminal response
// is the only final HTTP frame, so mask the commit while
// the peer still owns the body.
request_cx.masked(|| {
retry_http_sse_commit(
&request_cx,
&cancellation,
|| response_sender.send_response(&request_cx, response.clone()),
retryable_http_sse_transport_error,
)
})
} else {
retry_http_sse_commit(
&request_cx,
&cancellation,
|| response_sender.send_response(&request_cx, response.clone()),
retryable_http_sse_transport_error,
)
};
if sent.is_err() {
// The terminal response has no error channel after the
// SSE body is admitted. Retire the body explicitly so a
// bounded `WouldBlock` does not leave it pending forever.
terminal_delivery.mark_failed();
cancellation.cancel();
terminal_commit_failed = true;
}
} else {
// Peer cancel after Stream admission drops the JSON-RPC
// result. Settle the HTTP receipt here so listener drain
// does not wait for a complete that this path never sends.
terminal_delivery.mark_failed();
terminal_commit_failed = true;
}
}
if let Some(outcome_gate) = &outcome_gate {
outcome_gate.elect(if terminal_commit_failed {
ModernSseDispatchElection::Failed
} else {
ModernSseDispatchElection::Stream
});
}
})
.map_err(|error| {
DualEraHttpEndpointError::Transport(TransportError::Io(std::io::Error::other(format!(
"modern HTTP request dispatch admission failed: {error}"
))))
})
}
async fn send_modern_sse_stream(
cx: &Cx,
listener_shutdown: &HttpListenerShutdown,
stream: AsyncTcpStream,
server: Arc<Server>,
live_session: &LiveModernHttpSession,
modern_sessions: &LiveModernHttpSessionRegistry,
http_stream_generation: u64,
inbound: InboundRequestContext,
request: JsonRpcRequest,
raw_params: Option<Arc<str>>,
auth_receipt: Option<AuthDispatchCustody>,
response: DualEraHttpSseResponse,
) -> Result<(), ()> {
let sender = response.sender();
let ordinary_request = request.method != SUBSCRIPTIONS_LISTEN;
let request_cancellation = sender.request_cancellation();
let terminal_delivery = Arc::new(FinalSubscriptionTerminalDelivery::default());
// Every modern SSE representation waits for this request-owned election.
// A durable subscription elects `Stream` only after its dynamic middleware
// and admission acknowledgement succeed; ordinary requests elect after
// handler admission commits a notification or their final response is queued.
let (outcome_gate, mut outcome_receiver) = ModernSseOutcomeGate::new();
// H1 request input is complete before this request-owned response starts.
// A clean peer write-half EOF is therefore ordinary request completion,
// not cancellation of the still-owned response body. TCP cannot tell that
// half-close from a full close through this read side alone; output writes
// are the only definitive disconnect signal for this SSE response.
let (_reader, mut writer) = stream.into_split();
let dispatch = match spawn_modern_sse_dispatch(
cx,
Arc::clone(&server),
http_stream_generation,
inbound,
request,
raw_params,
auth_receipt,
sender,
Arc::clone(&terminal_delivery),
Some(Arc::clone(&outcome_gate)),
) {
Ok(dispatch) => dispatch,
Err(_) => return Err(()),
};
let dispatch = OwnedModernHttpDispatch {
owner_generation: http_stream_generation,
request_cancellation: request_cancellation.clone(),
task: dispatch,
};
if let Err(dispatch) = live_session.register_modern_dispatch(dispatch) {
server
.final_subscriptions
.cancel_modern_http_owner(dispatch.owner_generation);
dispatch.request_cancellation.cancel();
dispatch.task.abort();
modern_sessions.retain_retired_dispatches(vec![dispatch.task]);
return Err(());
}
let result = async {
match await_modern_sse_dispatch_election(cx, &request_cancellation, &mut outcome_receiver)
.await?
{
ModernSseDispatchElection::Stream => {}
ModernSseDispatchElection::Immediate(response) => {
return send_h1_bad_request_response(cx, listener_shutdown, &mut writer, &response)
.await;
}
ModernSseDispatchElection::Failed => return Err(()),
}
let response_head = sse_response_head(response.response())?;
writer.write_all(&response_head).await.map_err(|_| ())?;
writer.flush().await.map_err(|_| ())?;
let mut last_frame_write = cx.now();
loop {
// A failed request-scoped progress/log/terminal commit retires
// this body even when response finalization had already claimed
// the request cancellation token.
if terminal_delivery.is_settled() {
return Err(());
}
match pop_sse_body_event(&response) {
Ok(Some(event)) => {
let terminal_control = final_subscription_terminal_event(&event);
let terminal_response = final_subscription_terminal_response_event(&event);
let bytes = event.to_bytes().map_err(|_| ())?;
let prefix = format!("{:X}\r\n", bytes.len());
writer.write_all(prefix.as_bytes()).await.map_err(|_| ())?;
writer.write_all(&bytes).await.map_err(|_| ())?;
writer.write_all(b"\r\n").await.map_err(|_| ())?;
writer.flush().await.map_err(|_| ())?;
last_frame_write = cx.now();
if terminal_control {
terminal_delivery.mark_drained();
}
if terminal_response {
terminal_delivery.mark_completion_drained();
}
if terminal_delivery.is_settled() {
break;
}
}
Ok(None) if response.is_finished() => break,
Ok(None)
if request_cancellation.is_cancel_requested()
&& !terminal_delivery.is_committed() =>
{
// Peer/session cancel with no graceful complete in flight.
// Stream election marks control not-required, so do not
// wait for a control frame that modern HTTP never writes.
// Fail the receipt now so two live as_proxy catalog+Tasks
// SSE POSTs do not burn the full drain budget.
terminal_delivery.mark_failed();
break;
}
Ok(None) => {
// A cancelled listener Cx must not park on its timer: two
// live catalog+Tasks writers would busy-loop or hang and
// starve the dispatch that still owes the elected complete.
if cx.checkpoint().is_err() {
if !terminal_delivery.is_settled() {
// The connection region is already cancelled, so
// dispatch cannot flush an unelected complete or
// a RequestCancelled that already left the body.
// Fail now instead of burning the drain budget.
terminal_delivery.mark_failed();
break;
}
asupersync::runtime::yield_now().await;
} else {
if ordinary_request
&& cx.now().duration_since(last_frame_write) >= 2_000_000_000
{
// A quiet handler still needs an output write to
// observe a peer that has closed its response body.
// One inert comment line every two idle seconds
// detects that closure without treating a legal
// request write-half EOF as cancellation. No blank
// line is added: this consumes one caller keepalive
// line and cannot create an MCP event or reset an
// ordinary response's progress-based idle deadline.
// Subscriptions retain their separate long-lived
// activity and keepalive policies.
writer.write_all(b"2\r\n:\n\r\n").await.map_err(|_| ())?;
writer.flush().await.map_err(|_| ())?;
last_frame_write = cx.now();
}
asupersync::time::sleep(cx.now(), Duration::from_millis(1)).await;
}
}
Err(DualEraHttpEndpointError::Transport(TransportError::Closed))
if response.is_finished() =>
{
break;
}
Err(_) => return Err(()),
}
}
writer.write_all(b"0\r\n\r\n").await.map_err(|_| ())?;
writer.flush().await.map_err(|_| ())
}
.await;
if result.is_err() {
terminal_delivery.mark_failed();
modern_sessions
.retain_retired_dispatches(live_session.cancel_modern_dispatch(http_stream_generation));
} else if !terminal_delivery.is_settled() {
// Writing the chunked trailer can succeed after the peer already
// dropped. That must not leave a graceful-election receipt open for
// the full HTTP_TERMINAL_DRAIN_TIMEOUT — two live SSE POSTs would
// then blow the fixture teardown bound.
terminal_delivery.mark_failed();
}
live_session.reap_modern_dispatches();
result
}
fn next_modern_http_stream_generation() -> u64 {
loop {
let generation = NEXT_MODERN_HTTP_STREAM_GENERATION.fetch_add(1, Ordering::Relaxed);
if generation != 0 {
return generation;
}
}
}
fn next_live_modern_http_response_body_generation() -> u64 {
loop {
let generation =
NEXT_LIVE_MODERN_HTTP_RESPONSE_BODY_GENERATION.fetch_add(1, Ordering::Relaxed);
if generation != 0 {
return generation;
}
}
}
fn http_endpoint_response_to_static(cx: &Cx, response: ServerHttpEndpointResponse) -> HttpResponse {
match response {
ServerHttpEndpointResponse::Immediate(response) => response,
ServerHttpEndpointResponse::ModernSse(sse) => {
let mut response = sse.response().clone();
match sse.recv_event(cx).and_then(|event| {
event
.to_bytes()
.map_err(DualEraHttpEndpointError::from)
.map_err(ServerHttpEndpointError::from_internal)
}) {
Ok(body) => response = response.with_body(body),
Err(_) => response = HttpResponse::internal_error(),
}
response
}
#[cfg(feature = "legacy-2024-11-05")]
ServerHttpEndpointResponse::LegacySse(_) => HttpResponse::internal_error(),
}
}
#[cfg(feature = "legacy-2024-11-05")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct IngressVerifiedLegacyResponse;
#[cfg(feature = "legacy-2024-11-05")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LiveHttpLegacyDispatchMode {
Blocking,
BlockingPreclassifiedResponse(IngressVerifiedLegacyResponse),
CallerOwnedAsync,
}
#[cfg(feature = "legacy-2024-11-05")]
struct HttpLegacyIngressAdmission {
// Notifications have no correlated cancellation guard, but still carry
// their authenticated decision across the session-ownership wait.
_request_guard: Option<HttpLegacyRequestAdmissionGuard>,
auth_receipt: AuthAdmissionReceipt,
}
#[cfg(feature = "legacy-2024-11-05")]
enum LiveHttpLegacyIngress {
Dispatch {
admission: Option<Box<HttpLegacyIngressAdmission>>,
mode: LiveHttpLegacyDispatchMode,
},
Immediate(HttpResponse),
}
#[cfg(feature = "legacy-2024-11-05")]
fn admit_live_http_legacy_request(
cx: &Cx,
endpoint: &ServerHttpEndpoint,
legacy_sessions: &LiveHttpSessionRegistry,
request: &HttpRequest,
transport_authorization: &TransportAuthorization,
) -> Result<LiveHttpLegacyIngress, HttpResponse> {
if request.method != HttpMethod::Post
|| request.path != endpoint.server.http_config.legacy_message_path
{
return Ok(LiveHttpLegacyIngress::Dispatch {
admission: None,
mode: LiveHttpLegacyDispatchMode::Blocking,
});
}
let Some(session_id) = request.query.get("session_id") else {
return Err(HttpResponse::new(HttpStatus::NOT_FOUND));
};
let session = legacy_sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(session_id)
.cloned()
.ok_or_else(|| HttpResponse::new(HttpStatus::NOT_FOUND))?;
let admitted = admit_legacy_2024_http_post(
request,
&session.cancellation.legacy_message_path,
&session.cancellation.http_session_id,
session.cancellation.max_body_size,
)?;
let Legacy2024HttpPostEnvelope::ClientMessage(message) = admitted else {
let Legacy2024HttpPostEnvelope::Response(response) = admitted else {
unreachable!("legacy POST admission has exactly two envelope variants");
};
let auth_probe = JsonRpcRequest::new("legacy/sse", None, RequestId::Number(0));
let receipt = session.cancellation.server.preauthenticate_http_request(
cx,
&auth_probe,
transport_authorization,
)?;
if !session
.cancellation
.session_principal
.verify_existing(receipt.fingerprint)
{
return Err(native_http_authentication_rejection());
}
// Route a correlated response on the connection reactor before it can
// queue behind the blocking handler that emitted the reverse request.
// An unmatched response must still enter the serialized fallback, but
// that condvar wait belongs on the bounded bridge pool rather than the
// caller-owned reactor; otherwise the peer cannot post cancellation.
let disposition = session
.cancellation
.legacy_lifecycle
.commit_if_live(|| {
session
.legacy_pending_requests
.route_response_with_disposition(&response)
})
.ok_or_else(|| HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE))?;
return if matches!(
disposition,
bidirectional::PendingResponseDisposition::Delivered
| bidirectional::PendingResponseDisposition::RetiredGeneric
) {
Ok(LiveHttpLegacyIngress::Immediate(HttpResponse::new(
HttpStatus::ACCEPTED,
)))
} else {
Ok(LiveHttpLegacyIngress::Dispatch {
admission: None,
mode: LiveHttpLegacyDispatchMode::BlockingPreclassifiedResponse(
IngressVerifiedLegacyResponse,
),
})
};
};
if message.id.is_none() && message.method == "notifications/cancelled" {
return Ok(LiveHttpLegacyIngress::Immediate(
session
.cancellation
.handle(cx, request)
.unwrap_or_else(HttpResponse::bad_request),
));
}
let dispatch_mode = if endpoint
.server
.router
.legacy_request_uses_transport_owned_async_dispatch(&message)
{
LiveHttpLegacyDispatchMode::CallerOwnedAsync
} else {
LiveHttpLegacyDispatchMode::Blocking
};
let receipt = session.cancellation.server.preauthenticate_http_request(
cx,
&message,
transport_authorization,
)?;
let admission = session
.cancellation
.legacy_lifecycle
.commit_if_live(|| {
session.cancellation.admissions.admit(
&message,
&session.cancellation.session_principal,
receipt.fingerprint,
)
})
.ok_or_else(|| HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE))?
.map_err(|()| HttpResponse::bad_request())?;
Ok(LiveHttpLegacyIngress::Dispatch {
admission: Some(Box::new(HttpLegacyIngressAdmission {
_request_guard: admission,
auth_receipt: receipt,
})),
mode: dispatch_mode,
})
}
/// Re-decodes an already-admitted body to recover its request ID for an
/// admission error response.
///
/// Admission rejections after envelope decode are returned only once the
/// strict, bounded JSON-RPC request decode has succeeded. Re-decoding with the
/// same configured bound retains the admitted request ID without widening the
/// listener's parsing surface.
fn admitted_request_id(body: &[u8], max_body_size: usize) -> Option<RequestId> {
let mut codec = Codec::new();
codec.set_max_message_size(max_body_size);
codec
.decode_complete_message(body)
.ok()
.and_then(|message| match message {
JsonRpcMessage::Request(request) => request.id,
JsonRpcMessage::Response(_) => None,
})
}
fn protocol_admission_error_response(
body: &[u8],
admission: fastmcp_protocol::RequestAdmissionError,
max_body_size: usize,
) -> HttpResponse {
let id = admitted_request_id(body, max_body_size);
let (message, data) = match &admission {
fastmcp_protocol::RequestAdmissionError::HeaderMismatch(error) => (
http_admission::HEADER_MISMATCH_MESSAGE,
error.canonical_error_data(),
),
fastmcp_protocol::RequestAdmissionError::UnsupportedProtocolVersion(error) => (
"Unsupported MCP protocol version",
Some(error.canonical_error_data()),
),
};
let response = JsonRpcResponse::error(
id,
JsonRpcError {
code: admission.jsonrpc_error_code().into(),
message: message.to_owned(),
data,
},
);
HttpResponse::new(HttpStatus(admission.http_status())).with_json(&response)
}
fn http_endpoint_error_response(
request: &HttpRequest,
error: ServerHttpEndpointError,
max_body_size: usize,
) -> HttpResponse {
match error {
ServerHttpEndpointError::Http(HttpError::ProtocolAdmission(admission)) => {
protocol_admission_error_response(&request.body, admission, max_body_size)
}
_ => HttpResponse::bad_request(),
}
}
async fn dispatch_modern_http_request(
cx: &Cx,
endpoint: &ServerHttpEndpoint,
modern_sessions: &LiveModernHttpSessionRegistry,
request: HttpRequest,
) -> HttpResponse {
let transport_authorization = match transport_authorization_from_http_request(&request) {
Ok(authorization) => authorization,
Err(response) => return response,
};
dispatch_modern_http_request_with_cancellation_and_transport_authorization(
cx,
endpoint,
modern_sessions,
request,
transport_authorization,
None,
)
.await
}
async fn dispatch_modern_http_request_with_cancellation_and_transport_authorization(
cx: &Cx,
endpoint: &ServerHttpEndpoint,
modern_sessions: &LiveModernHttpSessionRegistry,
request: HttpRequest,
transport_authorization: TransportAuthorization,
request_cancellation: Option<McpRequestCancellation>,
) -> HttpResponse {
let error_request = request.clone();
if request.header("mcp-session-id").is_some() {
return HttpResponse::bad_request();
}
modern_sessions.reap_retired_dispatches();
let mut session = match endpoint.open_session(cx) {
Ok(session) => session,
Err(_) => return HttpResponse::internal_error(),
};
session
.handle_with_modern_request_cancellation_and_transport_authorization_async(
cx,
request,
transport_authorization,
request_cancellation,
false,
None,
)
.await
.map_err(ServerHttpEndpointError::from_internal)
.map(|response| http_endpoint_response_to_static(cx, response))
.unwrap_or_else(|error| {
http_endpoint_error_response(
&error_request,
error,
endpoint.server.http_config.handler_config.max_body_size,
)
})
}
/// Dispatches one ordinary modern JSON request while its connection owns the
/// cancellation authority. The request has already been fully decoded.
///
/// A clean write-half EOF is ordinary H1 request completion: the peer is
/// waiting to read this response. Unexpected extra bytes (pipelining into a
/// single-request connection) abandon the response body. Full close is
/// observed when the later response write fails, matching modern SSE.
async fn serve_modern_json_http_connection(
cx: &Cx,
stream: AsyncTcpStream,
endpoint: Arc<ServerHttpEndpoint>,
modern_sessions: LiveModernHttpSessionRegistry,
listener_shutdown: HttpListenerShutdown,
request: HttpRequest,
transport_authorization: TransportAuthorization,
) {
let (mut peer_reader, writer) = stream.into_split();
let request_cancellation = McpRequestCancellation::new();
let peer_cancellation = request_cancellation.clone();
let mut peer_watch = match cx.spawn(move |peer_cx| async move {
let mut probe = [0_u8; 1];
match peer_reader.read(&mut probe).await {
Ok(0) => {}
Ok(_) => {
peer_cancellation.cancel();
}
Err(_) if !peer_cx.is_cancel_requested() => {
peer_cancellation.cancel();
}
Err(_) => {}
}
}) {
Ok(peer_watch) => peer_watch,
Err(_) => return,
};
// Keep the dispatch under a joined connection child, but let it yield.
// spawn_blocking can run inline without a caller-owned blocking pool;
// blocking there would prevent the owned handler's children from running.
let request_endpoint = Arc::clone(&endpoint);
let request_modern_sessions = Arc::clone(&modern_sessions);
let dispatch_cancellation = request_cancellation.clone();
let mut dispatch = match cx.spawn(move |request_cx| async move {
dispatch_modern_http_request_with_cancellation_and_transport_authorization(
&request_cx,
&request_endpoint,
&request_modern_sessions,
request,
transport_authorization,
Some(dispatch_cancellation),
)
.await
}) {
Ok(dispatch) => dispatch,
Err(_) => {
request_cancellation.cancel();
peer_watch.abort();
let _ = peer_watch.join(cx).await;
return;
}
};
let response = match dispatch.join(cx).await {
Ok(response) => response,
Err(_) => HttpResponse::internal_error(),
};
peer_watch.abort();
let _ = peer_watch.join(cx).await;
let mut response_framed = Framed::new(writer, native_http1_codec(&endpoint));
let _ = send_h1_response(cx, &listener_shutdown, &mut response_framed, response).await;
}
#[cfg(feature = "legacy-2024-11-05")]
fn dispatch_http_request(
cx: &Cx,
endpoint: &ServerHttpEndpoint,
legacy_sessions: &LiveHttpSessionRegistry,
modern_sessions: &LiveModernHttpSessionRegistry,
request: HttpRequest,
legacy_admission: Option<Box<HttpLegacyIngressAdmission>>,
legacy_dispatch_mode: LiveHttpLegacyDispatchMode,
) -> HttpResponse {
// Drive the dispatch on the caller's runtime handles, as modern owned
// dispatch does, and not inside `fastmcp_core::block_on`. The live arm
// runs this on a blocking-pool thread, and the synchronous tool it reaches
// runs inline here. A handler that bridges `ctx.sample` with `block_on`
// would otherwise nest a second bridge on this thread, which panics, so
// the reverse request was never sent and the peer waited on SSE for it
// (bd-f2ndd, bd-6rfrg).
poll_on_cx(
cx,
dispatch_http_request_async(
cx,
endpoint,
legacy_sessions,
modern_sessions,
request,
legacy_admission,
legacy_dispatch_mode,
),
)
}
#[cfg(feature = "legacy-2024-11-05")]
async fn dispatch_http_request_async(
cx: &Cx,
endpoint: &ServerHttpEndpoint,
legacy_sessions: &LiveHttpSessionRegistry,
modern_sessions: &LiveModernHttpSessionRegistry,
request: HttpRequest,
legacy_admission: Option<Box<HttpLegacyIngressAdmission>>,
legacy_dispatch_mode: LiveHttpLegacyDispatchMode,
) -> HttpResponse {
let is_legacy_message = request.method == HttpMethod::Post
&& request.path == endpoint.server.http_config.legacy_message_path;
if is_legacy_message {
let Some(session_id) = request.query.get("session_id") else {
return HttpResponse::new(HttpStatus::NOT_FOUND);
};
let session = legacy_sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(session_id)
.cloned();
let Some(session) = session else {
return HttpResponse::new(HttpStatus::NOT_FOUND);
};
let admitted = match admit_legacy_2024_http_post(
&request,
&session.cancellation.legacy_message_path,
&session.cancellation.http_session_id,
session.cancellation.max_body_size,
) {
Ok(admitted) => admitted,
Err(response) => return response,
};
if matches!(
&admitted,
Legacy2024HttpPostEnvelope::ClientMessage(notification)
if notification.id.is_none()
&& notification.method == "notifications/cancelled"
) {
return session
.cancellation
.handle(cx, &request)
.unwrap_or_else(HttpResponse::bad_request);
}
// The connection creates this authority before queueing the blocking
// dispatch. Direct embedding callers create it here instead. In both
// cases it remains live through the serialized adapter/session mutex,
// active guard, and response finalization.
let admission = match legacy_admission {
Some(admission) => Some(admission),
None => match &admitted {
Legacy2024HttpPostEnvelope::ClientMessage(message) => {
let transport_authorization =
match transport_authorization_from_http_request(&request) {
Ok(authorization) => authorization,
Err(response) => return response,
};
let receipt = match session.cancellation.server.preauthenticate_http_request(
cx,
message,
&transport_authorization,
) {
Ok(receipt) => receipt,
Err(response) => return response,
};
let Some(admission) =
session.cancellation.legacy_lifecycle.commit_if_live(|| {
session.cancellation.admissions.admit(
message,
&session.cancellation.session_principal,
receipt.fingerprint,
)
})
else {
return HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE);
};
match admission {
Ok(admission) => Some(Box::new(HttpLegacyIngressAdmission {
_request_guard: admission,
auth_receipt: receipt,
})),
Err(()) => return HttpResponse::bad_request(),
}
}
Legacy2024HttpPostEnvelope::Response(_) => None,
},
};
let request_cancellation = match &admitted {
Legacy2024HttpPostEnvelope::ClientMessage(message) => {
message.id.as_ref().and_then(|request_id| {
session
.cancellation
.admissions
.admitted_request_cancellation(request_id)
})
}
Legacy2024HttpPostEnvelope::Response(_) => None,
};
let session_take = if matches!(
legacy_dispatch_mode,
LiveHttpLegacyDispatchMode::CallerOwnedAsync
) {
take_live_http_session_async(cx, &session, request_cancellation.as_ref()).await
} else {
take_live_http_session(cx, &session, request_cancellation.as_ref())
};
let mut owned = match session_take {
LiveHttpSessionTake::Acquired(owned) => *owned,
LiveHttpSessionTake::Cancelled => {
return HttpResponse::new(HttpStatus::ACCEPTED);
}
LiveHttpSessionTake::Unavailable => {
return HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE);
}
};
let response = if let LiveHttpLegacyDispatchMode::BlockingPreclassifiedResponse(verified) =
legacy_dispatch_mode
{
owned
.handle_preclassified_unmatched_legacy_response(cx, request, verified)
.await
} else {
owned
.handle_with_modern_request_cancellation_async(
cx,
request,
None,
false,
admission
.as_ref()
.map(|admission| admission.auth_receipt.clone()),
)
.await
.map_err(ServerHttpEndpointError::from_internal)
}
.map_err(|_| HttpResponse::bad_request());
let closing_dispatches = restore_live_http_session(&session, owned);
for mut dispatch in closing_dispatches {
let _ = dispatch.join(cx).await;
}
return response.map_or_else(
|response| response,
|response| http_endpoint_response_to_static(cx, response),
);
}
if request.method == HttpMethod::Post
&& request.path == endpoint.server.http_config.handler_config.base_path
{
return dispatch_modern_http_request(cx, endpoint, modern_sessions, request).await;
}
let mut session = match endpoint.open_session(cx) {
Ok(session) => session,
Err(_) => return HttpResponse::internal_error(),
};
let error_request = request.clone();
session
.handle_async(cx, request)
.await
.map(|response| http_endpoint_response_to_static(cx, response))
.unwrap_or_else(|error| {
http_endpoint_error_response(
&error_request,
error,
endpoint.server.http_config.handler_config.max_body_size,
)
})
}
#[cfg(feature = "legacy-2024-11-05")]
async fn serve_http_connection(
cx: &Cx,
stream: AsyncTcpStream,
endpoint: Arc<ServerHttpEndpoint>,
legacy_sessions: LiveHttpSessionRegistry,
modern_sessions: LiveModernHttpSessionRegistry,
listener_shutdown: HttpListenerShutdown,
) {
let http_config = &endpoint.server.http_config.handler_config;
let mut framed = Framed::new(stream, native_http1_codec(&endpoint));
#[cfg(test)]
lib_unit_tests::record_live_http_connection_read_wait();
let read_timeout = endpoint.server.http_config.request_read_timeout;
let Some(request) =
next_native_http1_request(cx, &listener_shutdown, &mut framed, read_timeout).await
else {
return;
};
let request = match request {
Ok(request) => request,
Err(_) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::bad_request(),
)
.await;
return;
}
};
if !framed.read_buffer().is_empty() {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::bad_request(),
)
.await;
return;
}
if serve_oauth_h1_request(cx, &listener_shutdown, &mut framed, &endpoint, &request).await {
return;
}
let transport_authorization = match h1_transport_authorization(&request) {
Ok(authorization) => authorization,
Err(response) => {
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
};
let raw_path = request
.uri
.split_once('?')
.map_or(request.uri.as_str(), |(path, _)| path);
if matches!(&request.method, Http1Method::Post) && raw_path == http_config.base_path.as_str() {
// Preserve wire-field cardinality for strict admission: conversion to
// `HttpRequest` intentionally merges list-valued Accept fields.
let headers = request
.headers
.iter()
.map(|(name, value)| (name.clone(), value.clone()))
.collect::<Vec<_>>();
if let Err(response) =
admit_modern_http_post(http_config, "POST", raw_path, &headers, &request.body)
{
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
}
let request = match h1_request_to_transport(&request) {
Ok(request) => request,
Err(response) => {
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
};
let is_legacy_sse = request.method == HttpMethod::Get
&& request.path == endpoint.server.http_config.legacy_sse_path;
let is_modern_sse = request.method == HttpMethod::Post
&& request.path == endpoint.server.http_config.handler_config.base_path
&& http_request_accepts_sse(&request);
if is_modern_sse {
if request.header("mcp-session-id").is_some() {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::bad_request(),
)
.await;
return;
}
let mut session = match endpoint.open_session(cx) {
Ok(session) => session,
Err(_) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::internal_error(),
)
.await;
return;
}
};
let http_parameter_headers = http_admission::http_parameter_headers(&request.headers);
let response = {
match session
.begin_modern_sse(cx, request.clone(), transport_authorization.clone(), None)
.await
{
Ok(Ok((request, response, raw_params, auth_receipt))) => Ok(Ok((
InboundRequestContext::with_modern_connection_and_transport_authorization(
cx.clone(),
request_id_to_u64(request.id.as_ref()),
InboundRequestTransport::Http,
&session.modern_connection,
transport_authorization.clone(),
)
.with_http_parameter_headers(http_parameter_headers),
request,
raw_params,
auth_receipt,
response,
))),
Ok(Err(response)) => Ok(Err(response)),
Err(error) => Err(ServerHttpEndpointError::from_internal(error)),
}
};
let live_session = Arc::new(LiveModernHttpSession::new(session));
match response {
Ok(Ok((inbound, request, raw_params, auth_receipt, response))) => {
let response_body_generation = next_live_modern_http_response_body_generation();
let live_session = match modern_sessions
.register_response_body(response_body_generation, Arc::clone(&live_session))
{
Ok(()) => live_session,
Err(live_session) => {
close_detached_modern_http_session(&modern_sessions, live_session);
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE),
)
.await;
return;
}
};
let stream = framed.into_inner();
let _ = send_modern_sse_stream(
cx,
&listener_shutdown,
stream,
Arc::clone(&endpoint.server),
&live_session,
&modern_sessions,
next_modern_http_stream_generation(),
inbound,
request,
raw_params,
Some(auth_receipt),
response,
)
.await;
if let Some(live_session) =
modern_sessions.take_response_body(response_body_generation)
{
close_detached_modern_http_session(&modern_sessions, live_session);
}
return;
}
Ok(Err(response)) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
http_endpoint_response_to_static(cx, *response),
)
.await;
return;
}
Err(error) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
http_endpoint_error_response(&request, error, http_config.max_body_size),
)
.await;
return;
}
}
}
if request.method == HttpMethod::Post
&& request.path == endpoint.server.http_config.handler_config.base_path
{
let stream = framed.into_inner();
serve_modern_json_http_connection(
cx,
stream,
endpoint,
modern_sessions,
listener_shutdown.clone(),
request,
transport_authorization,
)
.await;
return;
}
if !is_legacy_sse {
// Admit correlated legacy work before dispatch. The captured guard
// keeps this authority live while ordinary requests use synchronous
// isolation. Matching reverse responses are consumed during ingress;
// cancellation is handled there too. Handlers frozen as async remain
// on the caller runtime, with a yielding session-acquisition path.
let ingress = match admit_live_http_legacy_request(
cx,
&endpoint,
&legacy_sessions,
&request,
&transport_authorization,
) {
Ok(admission) => admission,
Err(response) => {
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
};
let (legacy_admission, legacy_dispatch_mode) = match ingress {
LiveHttpLegacyIngress::Dispatch { admission, mode } => (admission, mode),
LiveHttpLegacyIngress::Immediate(response) => {
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
};
if request.method == HttpMethod::Post
&& request.path == endpoint.server.http_config.legacy_message_path
{
if matches!(
legacy_dispatch_mode,
LiveHttpLegacyDispatchMode::CallerOwnedAsync
) {
let response = dispatch_http_request_async(
cx,
&endpoint,
&legacy_sessions,
&modern_sessions,
request,
legacy_admission,
legacy_dispatch_mode,
)
.await;
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
let dispatch_endpoint = Arc::clone(&endpoint);
let dispatch_legacy_sessions = Arc::clone(&legacy_sessions);
let dispatch_modern_sessions = Arc::clone(&modern_sessions);
let Some(blocking_dispatch_permit) = try_reserve_blocking_dispatch() else {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE),
)
.await;
return;
};
// Keep the direct pool handle inside a real request-region child.
// This avoids `Cx::spawn_blocking`'s documented inline fallback,
// while the outer TaskHandle still binds listener quiescence to
// the pool task's actual terminal state.
let panic_session_id = request.query.get("session_id").cloned();
let response = match run_live_http_legacy_blocking_dispatch(
cx,
&legacy_sessions,
panic_session_id,
blocking_dispatch_permit,
move |blocking_cx| {
dispatch_http_request(
&blocking_cx,
&dispatch_endpoint,
&dispatch_legacy_sessions,
&dispatch_modern_sessions,
request,
legacy_admission,
legacy_dispatch_mode,
)
},
)
.await
{
Ok(response) => response,
Err(()) => HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE),
};
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
let response = dispatch_http_request_async(
cx,
&endpoint,
&legacy_sessions,
&modern_sessions,
request,
legacy_admission,
LiveHttpLegacyDispatchMode::Blocking,
)
.await;
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
let mut session = match endpoint.open_session_for_legacy_sse(cx, &request) {
Ok(session) => session,
Err(_) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::internal_error(),
)
.await;
return;
}
};
let session_id = session.legacy_session_id().to_owned();
let response = match session.handle_async(cx, request).await {
Ok(ServerHttpEndpointResponse::LegacySse(response)) => response,
Ok(response) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
http_endpoint_response_to_static(cx, response),
)
.await;
return;
}
Err(_) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::bad_request(),
)
.await;
return;
}
};
let session = Arc::new(LiveHttpSession {
cancellation: session.cancellation_control(),
legacy_pending_requests: Arc::clone(&session.legacy_pending_requests),
session: Mutex::new(Some(session)),
session_available: Condvar::new(),
closing: AtomicBool::new(false),
});
legacy_sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(session_id.clone(), Arc::clone(&session));
let stream = framed.into_inner();
let _ = send_legacy_sse_stream(cx, stream, response).await;
// Peer close must revoke active exact-2024 requests before acquiring the
// serialized session slot below. Mark the detached shell first so an
// in-flight handler closes its owned session instead of restoring it
// after this listener has removed the public admission route.
session.closing.store(true, Ordering::Release);
session.session_available.notify_all();
session.cancellation.cancel_all_admitted_requests();
let removed = legacy_sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&session_id);
if let Some(session) = removed {
let dispatches = session
.session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_mut()
.map(ServerHttpSession::begin_close)
.unwrap_or_default();
for mut task in dispatches {
let _ = task.join(cx).await;
}
}
}
/// Production HTTP/1 connection path for a feature-off server.
///
/// This is intentionally separate from the dual-era listener: the only
/// request route admitted here is final Streamable HTTP. Historical `/sse`
/// and `/messages` traffic reaches `ServerHttpSession::handle_async` as a 404 and
/// can neither allocate a legacy session nor pin a legacy era.
#[cfg(not(feature = "legacy-2024-11-05"))]
async fn serve_modern_http_connection(
cx: &Cx,
stream: AsyncTcpStream,
endpoint: Arc<ServerHttpEndpoint>,
modern_sessions: LiveModernHttpSessionRegistry,
listener_shutdown: HttpListenerShutdown,
) {
let http_config = &endpoint.server.http_config.handler_config;
let mut framed = Framed::new(stream, native_http1_codec(&endpoint));
#[cfg(test)]
lib_unit_tests::record_live_http_connection_read_wait();
let read_timeout = endpoint.server.http_config.request_read_timeout;
let Some(request) =
next_native_http1_request(cx, &listener_shutdown, &mut framed, read_timeout).await
else {
return;
};
let request = match request {
Ok(request) => request,
Err(_) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::bad_request(),
)
.await;
return;
}
};
if !framed.read_buffer().is_empty() {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::bad_request(),
)
.await;
return;
}
if serve_oauth_h1_request(cx, &listener_shutdown, &mut framed, &endpoint, &request).await {
return;
}
let transport_authorization = match h1_transport_authorization(&request) {
Ok(authorization) => authorization,
Err(response) => {
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
};
let raw_path = request
.uri
.split_once('?')
.map_or(request.uri.as_str(), |(path, _)| path);
if matches!(&request.method, Http1Method::Post) && raw_path == http_config.base_path.as_str() {
let headers = request
.headers
.iter()
.map(|(name, value)| (name.clone(), value.clone()))
.collect::<Vec<_>>();
if let Err(response) =
admit_modern_http_post(http_config, "POST", raw_path, &headers, &request.body)
{
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
}
let request = match h1_request_to_transport(&request) {
Ok(request) => request,
Err(response) => {
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
return;
}
};
let is_modern_sse = request.method == HttpMethod::Post
&& request.path == endpoint.server.http_config.handler_config.base_path
&& http_request_accepts_sse(&request);
if is_modern_sse {
if request.header("mcp-session-id").is_some() {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::bad_request(),
)
.await;
return;
}
let mut session = match endpoint.open_session(cx) {
Ok(session) => session,
Err(_) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::internal_error(),
)
.await;
return;
}
};
let http_parameter_headers = http_admission::http_parameter_headers(&request.headers);
let response = {
match session
.begin_modern_sse(cx, request.clone(), transport_authorization.clone(), None)
.await
{
Ok(Ok((request, response, raw_params, auth_receipt))) => Ok(Ok((
InboundRequestContext::with_modern_connection_and_transport_authorization(
cx.clone(),
request_id_to_u64(request.id.as_ref()),
InboundRequestTransport::Http,
&session.modern_connection,
transport_authorization.clone(),
)
.with_http_parameter_headers(http_parameter_headers),
request,
raw_params,
auth_receipt,
response,
))),
Ok(Err(response)) => Ok(Err(response)),
Err(error) => Err(ServerHttpEndpointError::from_internal(error)),
}
};
let live_session = Arc::new(LiveModernHttpSession::new(session));
match response {
Ok(Ok((inbound, request, raw_params, auth_receipt, response))) => {
let response_body_generation = next_live_modern_http_response_body_generation();
let live_session = match modern_sessions
.register_response_body(response_body_generation, Arc::clone(&live_session))
{
Ok(()) => live_session,
Err(live_session) => {
close_detached_modern_http_session(&modern_sessions, live_session);
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::new(HttpStatus::SERVICE_UNAVAILABLE),
)
.await;
return;
}
};
let stream = framed.into_inner();
let _ = send_modern_sse_stream(
cx,
&listener_shutdown,
stream,
Arc::clone(&endpoint.server),
&live_session,
&modern_sessions,
next_modern_http_stream_generation(),
inbound,
request,
raw_params,
Some(auth_receipt),
response,
)
.await;
if let Some(live_session) =
modern_sessions.take_response_body(response_body_generation)
{
close_detached_modern_http_session(&modern_sessions, live_session);
}
return;
}
Ok(Err(response)) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
http_endpoint_response_to_static(cx, *response),
)
.await;
return;
}
Err(error) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
http_endpoint_error_response(&request, error, http_config.max_body_size),
)
.await;
return;
}
}
}
if request.method == HttpMethod::Post
&& request.path == endpoint.server.http_config.handler_config.base_path
{
let stream = framed.into_inner();
serve_modern_json_http_connection(
cx,
stream,
endpoint,
modern_sessions,
listener_shutdown.clone(),
request,
transport_authorization,
)
.await;
return;
}
let response = if request.method == HttpMethod::Post
&& request.path == endpoint.server.http_config.handler_config.base_path
{
dispatch_modern_http_request(cx, &endpoint, &modern_sessions, request).await
} else {
let mut session = match endpoint.open_session(cx) {
Ok(session) => session,
Err(_) => {
let _ = send_h1_response(
cx,
&listener_shutdown,
&mut framed,
HttpResponse::internal_error(),
)
.await;
return;
}
};
let error_request = request.clone();
session
.handle_async(cx, request)
.await
.map(|response| http_endpoint_response_to_static(cx, response))
.unwrap_or_else(|error| {
http_endpoint_error_response(&error_request, error, http_config.max_body_size)
})
};
let _ = send_h1_response(cx, &listener_shutdown, &mut framed, response).await;
}
/// An MCP server instance.
///
/// Servers are built using [`ServerBuilder`] and can run on various
/// transports (stdio, SSE, WebSocket).
pub struct Server {
info: ServerInfo,
title: Option<String>,
description: Option<String>,
website_url: Option<String>,
icons: Vec<fastmcp_protocol::common_types::RawIcon>,
capabilities: ServerCapabilities,
router: Arc<Router>,
instructions: Option<String>,
/// Server-owned request ceiling in seconds (0 = no additional ceiling).
request_timeout_secs: u64,
/// Runtime statistics collector (None = disabled).
stats: Option<ServerStats>,
/// Whether to mask internal error details in responses.
mask_error_details: bool,
/// Logging configuration.
logging: LoggingConfig,
/// Console configuration for rich output.
console_config: ConsoleConfig,
/// Server-owned console resolved from `console_config`.
console: FastMcpConsole,
/// Lifecycle hooks (wrapped in Option so they can be taken once).
lifespan: Mutex<Option<LifespanHooks>>,
/// Optional authentication provider.
auth_provider: Option<Arc<dyn AuthProvider>>,
/// Registered middleware.
middleware: Arc<Vec<Box<dyn crate::Middleware>>>,
/// Active requests by connection/session identity and JSON-RPC request ID.
active_requests: Arc<Mutex<HashMap<ActiveRequestKey, ActiveRequest>>>,
/// Test-only legacy task manager retained while the task subsystem is
/// rebuilt. It is absent from production library builds.
#[cfg(all(test, feature = "tasks"))]
task_manager: Option<SharedTaskManager>,
/// Per-connection ceiling for pending server-to-client requests.
#[cfg(feature = "legacy-2024-11-05")]
max_bidirectional_requests_per_connection: usize,
/// Immutable protocol-era admission policy selected by [`ServerBuilder`].
protocol_policy: ProtocolPolicy,
#[cfg(feature = "legacy-2024-11-05")]
legacy_application_tool_content: bool,
/// Immutable configuration for the live dual-era HTTP endpoint.
http_config: HttpServerConfig,
/// Optional OAuth-only public routes admitted before MCP transport conversion.
oauth_http_routes: Option<OAuthHttpRoutes>,
/// Frozen modern-only extension descriptors, handlers, and resolver.
extension_runtime: Option<Arc<ServerExtensionRuntime>>,
/// Application-owned final Tasks state retained for the caller's supervisor.
#[cfg(feature = "tasks")]
final_task_runtime: Option<FinalTaskRuntime>,
/// Application service entered and settled by the owning serve lifetime.
#[cfg(feature = "tasks")]
task_service_host: Option<tasks::TaskServiceHost>,
/// One route-bound upstream final Tasks relay, distinct from local durable
/// task state because its identifiers and lifecycle remain upstream-owned.
#[cfg(all(feature = "proxy", feature = "tasks"))]
final_task_relay: Option<Arc<ProxyFinalTaskRelay>>,
/// Request-owned final subscription streams shared by stdio and HTTP.
final_subscriptions: Arc<FinalSubscriptionRegistry>,
}
impl Server {
/// Returns the local subscription/notification delivery state for unit
/// tests that prove a rejected request produced no local side effect.
#[cfg(all(test, feature = "proxy", feature = "tasks"))]
pub(crate) fn final_subscription_snapshot_for_test(&self) -> serde_json::Value {
self.final_subscriptions.snapshot_for_test()
}
/// Creates a new server builder.
#[must_use]
#[allow(clippy::new_ret_no_self)]
pub fn new(name: impl Into<String>, version: impl Into<String>) -> ServerBuilder {
ServerBuilder::new(name, version)
}
/// Returns the server info.
#[must_use]
pub fn info(&self) -> &ServerInfo {
&self.info
}
#[cfg(feature = "legacy-2024-11-05")]
fn new_pending_requests_for_connection(&self) -> Arc<bidirectional::PendingRequests> {
Arc::new(
bidirectional::PendingRequests::with_max_in_flight(
self.max_bidirectional_requests_per_connection,
)
.expect("ServerBuilder validates the bidirectional request limit"),
)
}
/// Returns the server capabilities.
#[must_use]
pub fn capabilities(&self) -> &ServerCapabilities {
&self.capabilities
}
/// Returns the immutable protocol-era policy selected by the builder.
#[must_use]
pub const fn protocol_policy(&self) -> ProtocolPolicy {
self.protocol_policy
}
/// Returns the installed frozen extension handler registry, if configured.
#[must_use]
pub fn extension_handler_registry(&self) -> Option<&ExtensionHandlerRegistry> {
self.extension_runtime
.as_deref()
.map(|runtime| &runtime.handlers)
}
/// Returns the canonical descriptor receipt for the installed registry.
#[must_use]
pub fn extension_registry_receipt(&self) -> Option<&ExtensionRegistryReceipt> {
self.extension_handler_registry()
.and_then(|registry| registry.descriptor_registry().receipt())
}
/// Returns final Tasks state when the application configured the official extension.
///
/// The returned state machine creates no runtime and no detached task. A
/// caller-owned structured supervisor drives worker transitions through
/// this handle while the server routes only the negotiated wire methods.
#[must_use]
#[cfg(feature = "tasks")]
pub fn final_task_runtime(&self) -> Option<&FinalTaskRuntime> {
self.final_task_runtime.as_ref()
}
#[cfg(feature = "tasks")]
async fn start_hosted_task_service(
&self,
cx: &Cx,
) -> McpResult<Option<tasks::HostedTaskService>> {
match self.task_service_host.as_ref() {
Some(host) => host.start_ready(cx).await.map(Some),
None => Ok(None),
}
}
#[cfg(feature = "tasks")]
fn start_hosted_task_service_blocking(
&self,
cx: &Cx,
owns_server_lifecycle: bool,
) -> McpResult<Option<tasks::HostedTaskService>> {
match self
.task_service_host
.as_ref()
.filter(|_| owns_server_lifecycle)
{
Some(host) => host.start_ready_blocking(cx).map(Some),
None => Ok(None),
}
}
#[cfg(feature = "tasks")]
async fn settle_hosted_task_service(
hosted: Option<tasks::HostedTaskService>,
cx: &Cx,
) -> McpResult<()> {
match hosted {
Some(hosted) => hosted.settle(cx).await,
None => Ok(()),
}
}
#[cfg(feature = "tasks")]
fn settle_hosted_task_service_blocking(
hosted: Option<tasks::HostedTaskService>,
) -> McpResult<()> {
match hosted {
Some(hosted) => hosted.settle_blocking(),
None => Ok(()),
}
}
/// Publishes one final catalog or resource change notification to every
/// live `subscriptions/listen` request whose accepted filter matches it.
///
/// The server tags each delivered notification with that request's exact
/// subscription ID. Request-scoped notifications such as progress and
/// messages are rejected rather than being broadcast.
///
/// Each listener retains at most 64 events and 256 KiB of encoded event
/// data, including its active callback. Concurrent and reentrant publishes
/// queue in admission order behind that callback; their return count means
/// queue admission. Overflow cancels and unregisters only that listener.
/// The caller that starts a drain invokes callbacks synchronously, so an
/// application callback must still bound its own work.
///
/// # Errors
///
/// Returns `InvalidParams` when `notification` is not a final
/// subscription event, a capacity error for oversized application event
/// data, or an internal error when it cannot be encoded.
pub fn publish_subscription_notification(
&self,
notification: ServerNotification,
) -> McpResult<usize> {
self.final_subscriptions.publish(notification)
}
/// Opens one in-process `subscriptions/listen` stream.
///
/// The returned handle keeps the stream registered. Dropping it
/// unregisters the listener. Events are delivered through
/// `notification_sender`; this method does not block on stream lifetime.
/// Wire transports should keep using their owned listen dispatch.
/// This is a trusted embedding control without caller authentication;
/// never expose it as a request adapter for protected Tasks.
///
/// # Errors
///
/// Returns `InvalidRequest` when `subscription_id` is not a valid JSON-RPC
/// id, `InvalidParams` when the filter cannot be admitted, or a capacity
/// error when the server already holds the maximum number of streams.
pub fn open_subscription_listen(
&self,
subscription_id: RequestId,
notifications: SubscriptionFilter,
notification_sender: NotificationSender,
) -> McpResult<SubscriptionListenHandle> {
#[cfg(feature = "tasks")]
let accept_tasks = task_subscription_ids(¬ifications)
.ok()
.flatten()
.is_some();
#[cfg(not(feature = "tasks"))]
let accept_tasks = false;
let lease = self.final_subscriptions.open(
subscription_id,
notifications,
accept_tasks,
None,
McpRequestCancellation::new(),
None,
notification_sender,
)?;
Ok(SubscriptionListenHandle { _lease: lease })
}
/// Publishes one typed final Tasks status notification to matching streams.
///
/// Only subscriptions whose acknowledged `taskIds` set contains the
/// notification's exact opaque task identifier receive it. The server adds
/// the request's subscription ID without changing Task state.
/// This embedding publisher has no authenticated event custody and reaches
/// only unprotected subscriptions. Authenticated task transitions publish
/// internally with their committed principal.
#[cfg(feature = "tasks")]
pub fn publish_task_status_notification(
&self,
notification: FinalTaskStatusNotification,
) -> McpResult<usize> {
self.final_subscriptions.publish_task(notification)
}
/// Begins graceful termination for all live final subscription streams.
///
/// Stdio `subscriptions/listen` streams receive one correlated final
/// `notifications/cancelled` control. Modern HTTP streams instead end
/// gracefully with their correlated terminal response; client
/// cancellation is response-body closure, never an MCP cancellation
/// notification.
#[must_use]
pub fn terminate_subscription_streams(&self) -> usize {
self.final_subscriptions.terminate()
}
/// Shutdown's subscription teardown. A listen still inside its
/// acknowledgement (or an event) callback when the server wins its election
/// is completed by that callback's owner, and only if its request is not
/// cancelled yet. So this waits, bounded, for those owners before the
/// caller's generic `cancel_active_requests`, which a peer cancel and a
/// shutdown cancel look identical to (bd-81cct).
fn terminate_subscription_streams_for_shutdown(&self) -> usize {
let receipt = self.final_subscriptions.terminate_with_receipt();
if !receipt.wait_for_pending_elections(SHUTDOWN_CLEANUP_TIMEOUT) {
error!(
target: targets::SESSION,
"Final subscription callbacks did not finish their graceful completion before shutdown cancellation"
);
}
receipt.terminated
}
/// Negotiates currently advertised client extension settings against this server.
///
/// The resulting set is bound to this server's frozen descriptor receipt and
/// can only admit modern extension calls through
/// [`Self::dispatch_negotiated_extension`]. Exact MCP 2024-11-05 uses no
/// extension registry path.
pub fn negotiate_extensions(
&self,
client: &ClientExtensionDiscovery,
) -> Result<fastmcp_protocol::extensions::NegotiatedExtensionSet, ServerExtensionNegotiationError>
{
let runtime = self
.extension_runtime
.as_deref()
.ok_or(ServerExtensionNegotiationError::NotConfigured)?;
runtime.negotiate(client)
}
#[cfg(feature = "legacy-2024-11-05")]
fn legacy_2024_server_config(&self) -> Legacy2024ServerConfig {
Legacy2024ServerConfig {
capabilities: Legacy2024ServerCapabilities {
logging: self.capabilities.logging.as_ref().map(|_| BTreeMap::new()),
prompts: self.capabilities.prompts.as_ref().map(|capability| {
Legacy2024ListChangedCapability {
list_changed: capability.list_changed,
..Legacy2024ListChangedCapability::default()
}
}),
resources: self.capabilities.resources.as_ref().map(|capability| {
Legacy2024ResourcesCapability {
subscribe: capability.subscribe,
list_changed: capability.list_changed,
..Legacy2024ResourcesCapability::default()
}
}),
tools: self.capabilities.tools.as_ref().map(|capability| {
Legacy2024ListChangedCapability {
list_changed: capability.list_changed,
..Legacy2024ListChangedCapability::default()
}
}),
extensions: self
.capabilities
.completions
.as_ref()
.map(|_| {
BTreeMap::from([(
"completions".to_owned(),
serde_json::Value::Object(serde_json::Map::new()),
)])
})
.unwrap_or_default(),
..Legacy2024ServerCapabilities::default()
},
server_info: Legacy2024ServerInfo {
name: self.info.name.clone(),
version: self.info.version.clone(),
},
instructions: self.instructions.clone(),
}
}
#[cfg(feature = "legacy-2024-11-05")]
fn install_legacy_2024_adapter<H: Legacy2024Handler>(
&self,
binding: LegacyPeerBinding,
handler: H,
) -> Result<Legacy2024ServerAdapter<H>, Legacy2024AdapterError> {
Legacy2024ServerAdapter::install(binding, self.legacy_2024_server_config(), handler).map(
|adapter| adapter.with_application_tool_content(self.legacy_application_tool_content),
)
}
/// Returns the final discovery result for this constructed server.
///
/// Discovery is explicitly unauthenticated at this server boundary, just
/// like the rest of the modern stateless surface: transports authenticate
/// raw credentials before constructing [`InboundRequestContext`], while
/// this method receives only already-sanitized request facts. The result
/// derives from the immutable router catalog and cannot grant access to a
/// method or notification path that is not installed.
pub fn server_discovery(&self) -> McpResult<ServerDiscoverResult> {
let instructions = match self.instructions.as_deref() {
Some(value) if value.len() > MAX_SERVER_INSTRUCTIONS_BYTES => {
return Err(McpError::internal_error(
"Server discovery instructions exceed the configured limit",
));
}
Some(value) => Some(ServerInstructions::new(value.to_owned()).map_err(|_| {
McpError::internal_error("Server discovery instructions are invalid")
})?),
None => None,
};
let registry = self.router.server_discovery_behavior_registry();
let extension_capabilities = self.extension_runtime.as_deref().map_or_else(
BTreeMap::new,
ServerExtensionRuntime::discovery_capabilities,
);
let capabilities =
ServerDiscoverCapabilities::from_registry(®istry, extension_capabilities).map_err(
|_| McpError::internal_error("Server discovery capabilities are invalid"),
)?;
let discovery = ServerDiscoverResult::new(
capabilities,
self.info.clone(),
instructions,
DiscoveryCacheHints::private_ttl_ms(u64::from(DISCOVERY_CACHE_MAX_AGE_SECONDS) * 1_000),
);
let Some(mut implementation) = fastmcp_protocol::common_types::Implementation::try_new(
self.info.name.clone(),
self.info.version.clone(),
)
.ok() else {
return Ok(discovery);
};
implementation.title = self.title.clone();
implementation.description = self.description.clone();
if let Some(website_url) = self.website_url.as_deref()
&& let Ok(uri) = fastmcp_protocol::common_types::AbsoluteUri::parse(website_url)
{
implementation.website_url = Some(uri);
}
implementation.icons = self.icons.clone();
if implementation.title.is_some()
|| implementation.description.is_some()
|| implementation.website_url.is_some()
|| !implementation.icons.is_empty()
{
return Ok(discovery.with_implementation(implementation));
}
Ok(discovery)
}
/// Lists all registered tools.
#[must_use]
pub fn tools(&self) -> Vec<Tool> {
self.router.tools()
}
/// Lists all registered resources.
#[must_use]
pub fn resources(&self) -> Vec<Resource> {
self.router.resources()
}
/// Lists all registered resource templates.
#[must_use]
pub fn resource_templates(&self) -> Vec<ResourceTemplate> {
self.router.resource_templates()
}
/// Lists all registered prompts.
#[must_use]
pub fn prompts(&self) -> Vec<Prompt> {
self.router.prompts()
}
fn registered_extension_handler_id(&self, method: &str) -> McpResult<Option<&ExtensionId>> {
let Some(runtime) = self.extension_runtime.as_deref() else {
return Ok(None);
};
for id in runtime.server_discovery.extensions.keys() {
match runtime.handlers.lookup(id, method) {
Ok(_) => return Ok(Some(id)),
Err(ExtensionHandlerLookupError::HandlerNotFound(_)) => {}
Err(ExtensionHandlerLookupError::RegistryNotFrozen) => {
return Err(McpError::internal_error(
"server extension handlers were not frozen",
));
}
}
}
Ok(None)
}
fn invoke_negotiated_extension(
&self,
request_ctx: &McpContext,
negotiated: &fastmcp_protocol::extensions::NegotiatedExtensionSet,
extension_id: &ExtensionId,
request: &JsonRpcRequest,
) -> McpResult<serde_json::Value> {
// Keep the exact metadata available for modern admission and
// negotiation, but never expose the transport-owned version marker to
// extension handlers.
let mut request = request.clone();
remove_modern_protocol_metadata(&mut request);
let runtime = self
.extension_runtime
.as_deref()
.ok_or_else(|| McpError::method_not_found(&request.method))?;
runtime
.handlers
.invoke(
request_ctx,
negotiated,
ProtocolEra::Modern2026,
extension_id,
&request,
)
.map_err(|error| match error {
ExtensionHandlerInvocationError::Handler(error) => error,
ExtensionHandlerInvocationError::RegistryNotFrozen => {
McpError::internal_error("server extension handlers were not frozen")
}
ExtensionHandlerInvocationError::Protocol(_)
| ExtensionHandlerInvocationError::HandlerNotFound(_)
| ExtensionHandlerInvocationError::RequestEnvelopeRequired(_) => {
McpError::invalid_params("Extension request is not admitted")
}
})
}
/// Dispatches an already-negotiated modern extension request.
///
/// This public seam preserves the caller's request-owned context and only
/// invokes a handler when its descriptor is active in the supplied frozen
/// current-exchange capability set.
pub fn dispatch_negotiated_extension(
&self,
request_ctx: &McpContext,
negotiated: &fastmcp_protocol::extensions::NegotiatedExtensionSet,
request: &JsonRpcRequest,
) -> McpResult<serde_json::Value> {
require_exact_modern_extension_metadata(request)?;
let Some(extension_id) = self.registered_extension_handler_id(&request.method)? else {
return Err(McpError::method_not_found(&request.method));
};
if negotiated.active(extension_id).is_none() {
return Err(McpError::invalid_params(
"Extension request is not admitted",
));
}
self.invoke_negotiated_extension(request_ctx, negotiated, extension_id, request)
}
/// Dispatches a negotiated extension on the caller's runtime, including
/// handlers that suspend for upstream I/O. The synchronous dispatch API
/// remains suitable only for synchronous extension handlers.
pub async fn dispatch_negotiated_extension_async(
&self,
request_ctx: &McpContext,
negotiated: &fastmcp_protocol::extensions::NegotiatedExtensionSet,
request: &JsonRpcRequest,
) -> McpResult<serde_json::Value> {
require_exact_modern_extension_metadata(request)?;
let Some(extension_id) = self.registered_extension_handler_id(&request.method)? else {
return Err(McpError::method_not_found(&request.method));
};
if negotiated.active(extension_id).is_none() {
return Err(McpError::invalid_params(
"Extension request is not admitted",
));
}
let mut request = request.clone();
remove_modern_protocol_metadata(&mut request);
let runtime = self
.extension_runtime
.as_deref()
.ok_or_else(|| McpError::method_not_found(&request.method))?;
runtime
.handlers
.invoke_async(
request_ctx,
negotiated,
ProtocolEra::Modern2026,
extension_id,
&request,
)
.await
.map_err(|error| match error {
ExtensionHandlerInvocationError::Handler(error) => error,
ExtensionHandlerInvocationError::RegistryNotFrozen => {
McpError::internal_error("server extension handlers were not frozen")
}
ExtensionHandlerInvocationError::Protocol(_)
| ExtensionHandlerInvocationError::HandlerNotFound(_)
| ExtensionHandlerInvocationError::RequestEnvelopeRequired(_) => {
McpError::invalid_params("Extension request is not admitted")
}
})
}
async fn dispatch_extension_fallback_async(
&self,
request_ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<serde_json::Value> {
require_exact_modern_extension_metadata(request)?;
if self
.registered_extension_handler_id(&request.method)?
.is_none()
{
return Err(McpError::method_not_found(&request.method));
}
let client = final_client_extension_discovery(request)?;
let negotiated = self
.negotiate_extensions(&client)
.map_err(|_| McpError::invalid_params("Extension request negotiation was rejected"))?;
self.dispatch_negotiated_extension_async(request_ctx, &negotiated, request)
.await
}
fn dispatch_extension_fallback(
&self,
request_ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<serde_json::Value> {
// Extension fallback is a modern-only escape hatch after the frozen
// core router rejects an unknown method. Do not let a request merely
// shaped like an extension call reach either negotiation or an
// extension handler: admission requires the exact final-era marker
// carried by the current request.
require_exact_modern_extension_metadata(request)?;
let Some(extension_id) = self.registered_extension_handler_id(&request.method)? else {
return Err(McpError::method_not_found(&request.method));
};
let client = final_client_extension_discovery(request)?;
let negotiated = self
.negotiate_extensions(&client)
.map_err(|_| McpError::invalid_params("Extension request negotiation was rejected"))?;
self.invoke_negotiated_extension(request_ctx, &negotiated, extension_id, request)
}
fn maybe_dispatch_extension_fallback(
&self,
request_ctx: &McpContext,
request: &JsonRpcRequest,
result: McpResult<serde_json::Value>,
) -> McpResult<serde_json::Value> {
match result {
Err(error)
if error.code == McpErrorCode::MethodNotFound
&& modern_protocol_version(request) == Some(MODERN_PROTOCOL_VERSION) =>
{
match self.registered_extension_handler_id(&request.method) {
Ok(Some(_)) => self.dispatch_extension_fallback(request_ctx, request),
Ok(None) => Err(error),
Err(lookup) => Err(lookup),
}
}
other => other,
}
}
/// Returns the test-only legacy task manager, if configured.
#[cfg(all(test, feature = "tasks"))]
#[must_use]
pub(crate) fn task_manager(&self) -> Option<&SharedTaskManager> {
self.task_manager.as_ref()
}
/// Consumes the server and returns its router.
///
/// This is used for mounting one server's components into another.
///
#[must_use]
pub fn into_router(self) -> Router {
Arc::into_inner(self.router)
.expect("the server is the sole strong owner of its router outside dispatch")
}
/// Returns the capabilities this server provides.
///
/// This is useful when determining what components a server has
/// before mounting.
#[must_use]
pub fn has_tools(&self) -> bool {
self.capabilities.tools.is_some()
}
/// Returns whether this server has resources.
#[must_use]
pub fn has_resources(&self) -> bool {
self.capabilities.resources.is_some()
}
/// Returns whether this server has prompts.
#[must_use]
pub fn has_prompts(&self) -> bool {
self.capabilities.prompts.is_some()
}
/// Returns a point-in-time snapshot of server statistics.
///
/// Returns `None` if statistics collection is disabled.
#[must_use]
pub fn stats(&self) -> Option<StatsSnapshot> {
self.stats.as_ref().map(ServerStats::snapshot)
}
/// Returns the raw statistics collector.
///
/// Useful for advanced scenarios where you need direct access.
/// Returns `None` if statistics collection is disabled.
#[must_use]
pub fn stats_collector(&self) -> Option<&ServerStats> {
self.stats.as_ref()
}
/// Renders a stats panel to stderr, if stats are enabled.
pub fn display_stats(&self) {
let Some(stats) = self.stats.as_ref() else {
return;
};
let snapshot = stats.snapshot();
let renderer =
fastmcp_console::stats::StatsRenderer::new(self.console_config.resolve_context());
renderer.render_panel(&snapshot, &self.console);
}
fn configured_traffic_renderer(&self) -> Option<RequestResponseRenderer> {
let show_bodies = match self.console_config.traffic_verbosity {
TrafficVerbosity::None => return None,
TrafficVerbosity::Summary => false,
TrafficVerbosity::Full => true,
};
let mut renderer = RequestResponseRenderer::new(self.console_config.resolve_context());
renderer.truncate_at = self.console_config.truncate_at;
renderer.max_json_depth = self.console_config.max_json_depth;
renderer.show_params = show_bodies;
renderer.show_result = show_bodies;
Some(renderer)
}
/// Returns the console configuration.
#[must_use]
pub fn console_config(&self) -> &ConsoleConfig {
&self.console_config
}
/// Renders the startup banner based on console configuration.
fn render_startup_banner(&self, transport: &str) {
let render = || {
let mut banner = StartupBanner::new(&self.info.name, &self.info.version)
.tools(self.router.tools_count())
.resources(self.router.resources_count())
.prompts(self.router.prompts_count())
.transport(transport)
.show_capabilities(self.console_config.show_capabilities);
if let Some(desc) = self.instructions.as_deref().filter(|d| !d.is_empty()) {
banner = banner.description(desc);
}
// Apply banner style from config
match self.console_config.banner_style {
BannerStyle::Full => banner.render(&self.console),
BannerStyle::Compact => {
banner.no_logo().render(&self.console);
}
BannerStyle::Minimal => banner.minimal().render(&self.console),
BannerStyle::None => {} // Already checked show_banner, but be safe
}
};
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(render)).is_err() {
self.console
.print_plain("Warning: startup banner rendering failed");
}
}
/// Initializes rich logging based on server configuration.
///
/// This should be called early in the startup sequence, before any
/// log output is generated. If initialization fails (e.g., logger
/// already set), a warning is printed to stderr.
fn init_rich_logging(&self) {
let result = RichLoggerBuilder::new()
.level_filter(self.logging.level)
.with_timestamps(self.logging.timestamps)
.with_targets(self.logging.targets)
.with_file_line(self.logging.file_line)
.with_max_width(Some(self.console_config.truncate_at))
.with_context(self.console_config.resolve_context())
.init();
if result.is_err() {
// Logger already initialized (likely by user code), not an error
self.console
.print_plain("Note: Rich logging not initialized (logger already set)");
}
}
/// Processes one modern request through the final dispatch surface.
///
/// The transport owns raw headers and retains a native authorization field
/// only in crate-private inbound custody. Each call creates a fresh request
/// authority, authenticates before extension middleware, and delegates only
/// to the router's final dispatch surface. It neither reads nor mutates a
/// legacy [`Session`]. A transport that supplies a connection-bound
/// [`InboundRequestContext`] also carries retained-continuation authority.
/// Durable connections contribute their exact MRTR partition; ephemeral
/// HTTP contributes an eligibility partition that the router replaces with
/// its stateless domain.
///
/// Requests that require connection-local lifecycle state remain available
/// exclusively through the legacy adapter. Modern list results still come
/// from the immutable router catalog and never depend on previous request
/// data.
#[must_use]
pub async fn dispatch_stateless(
&self,
inbound: &InboundRequestContext,
request: &JsonRpcRequest,
) -> Option<JsonRpcResponse> {
self.dispatch_stateless_with_cancellation(inbound, request, None, None, None, None)
.await
}
async fn dispatch_stateless_with_cancellation(
&self,
inbound: &InboundRequestContext,
request: &JsonRpcRequest,
request_cancellation: Option<McpRequestCancellation>,
raw_params: Option<&str>,
auth_receipt: Option<&AuthDispatchCustody>,
websocket_connection_generation: Option<u64>,
) -> Option<JsonRpcResponse> {
let method = request.method.clone();
let response_id = request.id.clone();
let is_notification = response_id.is_none();
let started_at = Instant::now();
let response_for_error = |error: McpError| {
response_id.clone().map(|id| {
let masked = mask_peer_error(error, self.mask_error_details);
JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: masked.code.into(),
message: masked.message,
data: masked.data,
},
)
})
};
if request.validate().is_err() {
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), false);
}
return response_for_error(McpError::invalid_request("Invalid JSON-RPC request"));
}
if !is_notification && is_notification_only_method(&method) {
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), false);
}
return response_for_error(McpError::invalid_request(
"MCP notification method must not carry a request id",
));
}
if admit_final_client_notification_ingress(request).is_err() {
return None;
}
if is_notification && is_request_only_method(&method) {
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), false);
}
return None;
}
if inbound.request_id() != request_id_to_u64(response_id.as_ref()) {
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), false);
}
return response_for_error(McpError::invalid_request(
"Inbound request identity does not match the JSON-RPC request id",
));
}
let request_ctx = match request_cancellation.as_ref() {
Some(cancellation) => inbound
.request_context()
.with_request_cancellation(cancellation.clone()),
None => inbound.request_context(),
};
let request_ctx = self
.attach_request_scoped_component_access(self.with_final_catalog_publisher(request_ctx));
let budget = self.create_request_budget(request_ctx.cx());
if let Some(error) = Self::request_budget_error(request_ctx.cx(), budget) {
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), false);
}
return response_for_error(error);
}
let Some((request_ctx, _request_lease_guard)) = request_ctx
.with_budget_ceiling(budget)
.begin_request_scope()
else {
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), false);
}
return response_for_error(McpError::internal_error(
"request scope could not be established",
));
};
// The value the raw sidecar was admitted with; `request` is shadowed
// below by a metadata-stripped copy.
let admitted_params = request.params.as_ref();
let mut admission_request = request.clone();
if let Err(error) = self.authenticate_modern_request(
&request_ctx,
inbound,
&mut admission_request,
auth_receipt,
websocket_connection_generation,
) {
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), false);
}
return response_for_error(error);
}
let final_core_request = match decode_final_core_request_for_middleware(&admission_request)
{
Ok(request) => request,
Err(error) => {
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), false);
}
return response_for_error(error);
}
};
let request_ctx = match admitted_final_client_capability_info(final_core_request.as_ref()) {
Ok(Some(capabilities)) => request_ctx.with_client_capabilities(capabilities),
Ok(None) => request_ctx,
Err(error) => {
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), false);
}
return response_for_error(error);
}
};
let request_ctx = match admitted_final_client_implementation(final_core_request.as_ref()) {
Ok(Some(identity)) => request_ctx.with_client_implementation(identity),
Ok(None) => request_ctx,
Err(error) => {
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), false);
}
return response_for_error(error);
}
};
// The protocol-version marker is part of typed modern admission. Once
// that admission has completed, keep the marker out of application
// middleware and handler parameter decoding just as stdio does.
let mut request = admission_request.clone();
remove_modern_protocol_metadata(&mut request);
let mut entered_middleware: Vec<&dyn crate::Middleware> = Vec::new();
let result: McpResult<serde_json::Value> = async {
for middleware in self.middleware.iter() {
Self::enforce_request_context(&request_ctx)?;
entered_middleware.push(middleware.as_ref());
match catch_extension_unwind(|| middleware.on_request(&request_ctx, &request)) {
Ok(Ok(MiddlewareDecision::Continue)) => {}
Ok(Ok(MiddlewareDecision::Respond(value))) => {
if final_core_request.is_some()
&& !middleware_may_complete_final_core_from_cache(
&request_ctx,
middleware.as_ref(),
&value,
)
{
return Err(McpError::internal_error(
"middleware cannot short-circuit a final core response",
));
}
return Ok(value);
}
Ok(Err(error)) => return Err(error),
Err(_payload) => return Err(extension_panic_error("middleware_on_request")),
}
}
if request.method == SERVER_DISCOVER_METHOD {
let params = admission_request
.params
.clone()
.unwrap_or_else(|| serde_json::json!({}));
serde_json::from_value::<ServerDiscoverRequest>(params)
.map_err(|error| McpError::invalid_params(error.to_string()))?;
serde_json::to_value(self.server_discovery()?).map_err(McpError::from)
} else if request.method == SUBSCRIPTIONS_LISTEN {
Err(McpError::invalid_request(
"subscriptions/listen is a long-lived stream; use Server::open_subscription_listen or an owned transport dispatch",
))
} else {
let continuation_cancellation =
inbound.mrtr_continuation_cancellation().unwrap_or_default();
match self
.router
.dispatch_stateless_with_continuation_cancellation_and_raw_params(
&request_ctx,
&admission_request,
retained_raw_params(
raw_params,
admitted_params,
admission_request.params.as_ref(),
),
&continuation_cancellation,
)
.await
{
Err(error) if error.code == McpErrorCode::MethodNotFound => {
self.dispatch_extension_fallback(&request_ctx, &admission_request)
}
result => result,
}
}
}
.await;
let result = match result {
Ok(value) => {
let metadata_seal = final_core_middleware_metadata_seal(
final_core_request.as_ref(),
&request,
&value,
);
metadata_seal
.and_then(|metadata_seal| {
self.apply_middleware_response(
&entered_middleware,
&request_ctx,
&request,
value,
)
.and_then(|value| {
validate_final_core_middleware_response(
final_core_request.as_ref(),
&request,
value,
)
})
.and_then(|value| {
validate_final_core_middleware_metadata_seal(
metadata_seal.as_ref(),
final_core_request.as_ref(),
&request,
&value,
)?;
Ok(value)
})
})
.map_err(|error| {
self.apply_middleware_error(
&entered_middleware,
&request_ctx,
&request,
error,
)
})
}
Err(error) => {
Err(self.apply_middleware_error(&entered_middleware, &request_ctx, &request, error))
}
};
let result = Self::request_context_error(&request_ctx).map_or(result, Err);
let succeeded = result.is_ok();
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), succeeded);
}
if is_notification {
if let Err(error) = result {
error!(
target: targets::HANDLER,
"Stateless notification failed; method_key={:016x}; code={:?}",
stable_hash_request_id(&method),
error.code
);
}
return None;
}
// A listen stream terminates through its request-owned cancellation
// authority. For both a peer-close and server-initiated teardown,
// the stream has no usable response channel after that terminal
// signal; emitting an error response would create a second terminal
// outcome. Other request methods retain ordinary cancellation
// responses.
if request.method == SUBSCRIPTIONS_LISTEN
&& request_cancellation
.as_ref()
.is_some_and(McpRequestCancellation::is_cancel_requested)
{
return None;
}
let response_id = response_id.expect("non-notification requests have an id");
Some(match result {
Ok(value) => JsonRpcResponse::success(response_id, value),
Err(error) => {
let masked = mask_peer_error(error, self.mask_error_details);
JsonRpcResponse::error(
Some(response_id),
JsonRpcError {
code: masked.code.into(),
message: masked.message,
data: masked.data,
},
)
}
})
}
async fn dispatch_stateless_owned_with_cancellation(
self: Arc<Self>,
inbound: &InboundRequestContext,
mut request: JsonRpcRequest,
raw_params: Option<Arc<str>>,
auth_receipt: Option<AuthDispatchCustody>,
websocket_connection_generation: Option<u64>,
modern_http_owner: Option<u64>,
request_cancellation: McpRequestCancellation,
terminal_delivery: Option<Arc<FinalSubscriptionTerminalDelivery>>,
notification_sender: NotificationSender,
handler_admission: Option<FinalHandlerAdmission>,
) -> Option<JsonRpcResponse> {
// The value the raw sidecar was admitted with, before authentication
// may strip credentials from this request.
let admitted_params = raw_params.as_ref().and_then(|_| request.params.clone());
let method = request.method.clone();
let response_id = request.id.clone();
let is_notification = response_id.is_none();
let started_at = Instant::now();
let response_for_error = |error: McpError| {
response_id.clone().map(|id| {
let masked = mask_peer_error(error, self.mask_error_details);
JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: masked.code.into(),
message: masked.message,
data: masked.data,
},
)
})
};
if request.validate().is_err() {
return response_for_error(McpError::invalid_request("Invalid JSON-RPC request"));
}
if !is_notification && is_notification_only_method(&method) {
return response_for_error(McpError::invalid_request(
"MCP notification method must not carry a request id",
));
}
if admit_final_client_notification_ingress(&request).is_err() {
return None;
}
if is_notification && is_request_only_method(&method) {
return None;
}
if inbound.request_id() != request_id_to_u64(response_id.as_ref()) {
return response_for_error(McpError::invalid_request(
"Inbound request identity does not match the JSON-RPC request id",
));
}
let request_ctx = self.attach_request_scoped_component_access(
self.with_final_catalog_publisher(
inbound
.request_context()
.with_request_cancellation(request_cancellation.clone()),
),
);
let budget = self.create_owned_modern_request_budget(request_ctx.cx(), &method);
if let Some(error) = Self::request_budget_error(request_ctx.cx(), budget) {
return response_for_error(error);
}
let Some((mut request_ctx, _request_lease_guard)) = request_ctx
.with_budget_ceiling(budget)
.begin_request_scope()
else {
return response_for_error(McpError::internal_error(
"request scope could not be established",
));
};
if let Err(error) = self.authenticate_modern_request(
&request_ctx,
inbound,
&mut request,
auth_receipt.as_ref(),
websocket_connection_generation,
) {
return response_for_error(error);
}
let final_core_request = match decode_final_core_request_for_middleware(&request) {
Ok(request) => request,
Err(error) => return response_for_error(error),
};
match admitted_final_client_capability_info(final_core_request.as_ref()) {
Ok(Some(capabilities)) => {
request_ctx = request_ctx.with_client_capabilities(capabilities);
}
Ok(None) => {}
Err(error) => return response_for_error(error),
}
match admitted_final_client_implementation(final_core_request.as_ref()) {
Ok(Some(identity)) => {
request_ctx = request_ctx.with_client_implementation(identity);
}
Ok(None) => {}
Err(error) => return response_for_error(error),
}
if let Some(level) = self.final_request_log_level(&request) {
request_ctx =
request_ctx.with_min_log_level(Some(Self::mcp_log_level_from_final(level)));
}
{
let sender = notification_sender.clone();
request_ctx =
request_ctx.with_log_sender(Arc::new(crate::handler::LogNotificationSender::new(
move |notification| {
sender(notification);
},
ProtocolEra::Modern2026,
)));
}
// The outer request owner retains the sole final-progress runtime.
// Router/handler derivation sees this reporter and reuses it instead
// of constructing a second sender, so the coalescing slot and
// terminal race remain request-scoped across the whole call chain.
let final_progress_runtime = final_core_request
.as_ref()
.and_then(|_| Self::request_progress_marker(&request))
.map(|marker| {
let sender = notification_sender.clone();
let frame_context = request_ctx.clone();
let frame_cancellation = request_cancellation.clone();
Arc::new(handler::FinalProgressRuntime::new(
marker,
move |notification| {
// A final response finalization prevents a later
// cancellation from winning, but an ordinary peer or
// body-drop cancellation must suppress every queued
// progress frame at the callback boundary.
if !frame_cancellation.is_cancel_requested()
&& frame_context.ensure_live().is_ok()
{
sender(notification);
}
},
))
});
if let Some(runtime) = &final_progress_runtime {
request_ctx = request_ctx.with_progress_reporter(Arc::clone(runtime).into_reporter());
}
let mut entered_middleware: Vec<&dyn crate::Middleware> = Vec::new();
let mut middleware_result = None;
for middleware in self.middleware.iter() {
if let Err(error) = Self::enforce_request_context(&request_ctx) {
middleware_result = Some(Err(error));
break;
}
entered_middleware.push(middleware.as_ref());
let decision = catch_extension_unwind(|| middleware.on_request(&request_ctx, &request))
.map_err(|_payload| extension_panic_error("middleware_on_request"))
.and_then(|result| result);
match decision {
Ok(MiddlewareDecision::Continue) => {}
Ok(MiddlewareDecision::Respond(value)) => {
middleware_result = Some(
if final_core_request.is_some()
&& !middleware_may_complete_final_core_from_cache(
&request_ctx,
middleware.as_ref(),
&value,
)
{
Err(McpError::internal_error(
"middleware cannot short-circuit a final core response",
))
} else {
Ok(value)
},
);
break;
}
Err(error) => {
middleware_result = Some(Err(error));
break;
}
}
}
// Bound the dispatch state and its Send proof before the progress
// timer and transport futures wrap it. The same owner still polls
// and drops the request; no extra task or cancellation scope is added.
let dispatch: std::pin::Pin<
Box<dyn Future<Output = McpResult<serde_json::Value>> + Send + '_>,
> = Box::pin(async {
match middleware_result {
Some(result) => result,
None if request.method == SERVER_DISCOVER_METHOD => {
let params = request
.params
.clone()
.unwrap_or_else(|| serde_json::json!({}));
match serde_json::from_value::<ServerDiscoverRequest>(params)
.map_err(|error| McpError::invalid_params(error.to_string()))
{
Ok(_) => self.server_discovery().and_then(|result| {
serde_json::to_value(result).map_err(McpError::from)
}),
Err(error) => Err(error),
}
}
None if request.method == SUBSCRIPTIONS_LISTEN => {
self.dispatch_final_subscriptions_listen(
&request_ctx,
&request,
modern_http_owner,
request_cancellation.clone(),
terminal_delivery,
Arc::clone(¬ification_sender),
)
.await
}
None => match Arc::clone(&self.router)
.dispatch_stateless_owned_with_handler_admission(
request_ctx.clone(),
request.clone(),
retained_raw_params(
raw_params.as_deref(),
admitted_params.as_ref(),
request.params.as_ref(),
)
.map(Arc::<str>::from),
inbound.mrtr_continuation_cancellation().unwrap_or_default(),
handler_admission,
)
.await
{
Err(error) if error.code == McpErrorCode::MethodNotFound => {
self.dispatch_extension_fallback_async(&request_ctx, &request)
.await
}
result => result,
},
}
});
let result = match final_progress_runtime.as_ref() {
Some(runtime) => {
await_final_progress_rate_tick(
request_ctx.cx(),
&request_cancellation,
runtime,
dispatch,
)
.await
}
None => dispatch.await,
};
// The server-side graceful election cancels the listen as its
// dispatch wake; liveness gating would convert the sanctioned
// terminal completion into a cancellation error, so that exact
// response is exempt while middleware hooks still run.
let raw_graceful_completion = request.method == SUBSCRIPTIONS_LISTEN
&& result.as_ref().is_ok_and(|value| {
final_subscription_completion_result(value, request.id.as_ref())
});
let result = match result {
Ok(value) => {
let metadata_seal = final_core_middleware_metadata_seal(
final_core_request.as_ref(),
&request,
&value,
);
metadata_seal
.and_then(|metadata_seal| {
self.apply_middleware_response_with_liveness(
&entered_middleware,
&request_ctx,
&request,
value,
!raw_graceful_completion,
)
.and_then(|value| {
validate_final_core_middleware_response(
final_core_request.as_ref(),
&request,
value,
)
})
.and_then(|value| {
validate_final_core_middleware_metadata_seal(
metadata_seal.as_ref(),
final_core_request.as_ref(),
&request,
&value,
)?;
Ok(value)
})
})
.map_err(|error| {
self.apply_middleware_error(
&entered_middleware,
&request_ctx,
&request,
error,
)
})
}
Err(error) => {
Err(self.apply_middleware_error(&entered_middleware, &request_ctx, &request, error))
}
};
let graceful_subscription_completion = request.method == SUBSCRIPTIONS_LISTEN
&& result.as_ref().is_ok_and(|value| {
final_subscription_completion_result(value, request.id.as_ref())
});
// Response middleware may replace the server-elected complete result
// with the one canonical missing-capability error. Once a live SSE
// listen has admitted, that replacement is its terminal SSE response
// rather than a request-cancellation loser; preserve it through the
// liveness fence below.
let graceful_subscription_canonical_substitution = raw_graceful_completion
&& result
.as_ref()
.is_err_and(is_canonical_missing_required_client_capability_error);
let result =
if graceful_subscription_completion || graceful_subscription_canonical_substitution {
result
} else {
Self::request_context_error(&request_ctx).map_or(result, Err)
};
// Elect terminal ownership before final progress, logging, or a
// response writer can run. A cancellation result, or cancellation
// already elected by response-body drop, suppresses every trailing
// request-scoped notification instead of letting a final log escape
// after the cancelled tool call.
let finalization_won = !result
.as_ref()
.is_err_and(|error| error.code == McpErrorCode::RequestCancelled)
&& request_cancellation.begin_finalization();
if let Some(runtime) = &final_progress_runtime {
if finalization_won {
runtime.finalize();
} else {
runtime.cancel();
}
}
if let Some(stats) = &self.stats {
stats.record_request(&method, started_at.elapsed(), result.is_ok());
}
if finalization_won {
self.maybe_emit_final_log_notification(&request, ¬ification_sender, result.is_ok());
}
if is_notification {
if let Err(error) = result {
error!(
target: targets::HANDLER,
"Stateless notification failed; method_key={:016x}; code={:?}",
stable_hash_request_id(&method),
error.code
);
}
return None;
}
if request.method == SUBSCRIPTIONS_LISTEN
&& request_cancellation.is_cancel_requested()
&& !graceful_subscription_completion
&& !graceful_subscription_canonical_substitution
{
return None;
}
let response_id = response_id.expect("non-notification requests have an id");
Some(match result {
Ok(value) => JsonRpcResponse::success(response_id, value),
Err(error) => {
let masked = mask_peer_error(error, self.mask_error_details);
JsonRpcResponse::error(
Some(response_id),
JsonRpcError {
code: masked.code.into(),
message: masked.message,
data: masked.data,
},
)
}
})
}
#[cfg(feature = "tasks")]
fn admit_final_task_subscription(
&self,
notifications: &SubscriptionFilter,
client_capabilities: &serde_json::Map<String, serde_json::Value>,
) -> McpResult<bool> {
let tasks_requested = task_subscription_ids(notifications)
.map_err(|_| McpError::invalid_params("invalid Tasks subscription filter"))?
.is_some();
if !tasks_requested {
return Ok(false);
}
let has_task_service = self.final_task_runtime.is_some() || {
#[cfg(feature = "proxy")]
{
self.final_task_relay.is_some()
}
#[cfg(not(feature = "proxy"))]
{
false
}
};
if !has_task_service {
return Err(McpError::invalid_params(
"Tasks subscription filter is unavailable on this server",
));
}
let tasks_declared = client_capabilities
.get("extensions")
.and_then(serde_json::Value::as_object)
.and_then(|extensions| extensions.get(fastmcp_protocol::TASKS_EXTENSION))
.and_then(serde_json::Value::as_object)
.is_some_and(serde_json::Map::is_empty);
if !tasks_declared {
return Err(missing_final_tasks_capability_error()?);
}
Ok(true)
}
/// Executes one final `subscriptions/listen` request until the server
/// begins graceful stream termination or the request transport is
/// cancelled. The registry lease is intentionally scoped to this future:
/// cancellation, disconnect, and every error path remove the entry before
/// the request response path can run.
async fn dispatch_final_subscriptions_listen(
self: &Arc<Self>,
request_ctx: &McpContext,
request: &JsonRpcRequest,
modern_http_owner: Option<u64>,
request_cancellation: McpRequestCancellation,
terminal_delivery: Option<Arc<FinalSubscriptionTerminalDelivery>>,
notification_sender: NotificationSender,
) -> McpResult<serde_json::Value> {
let subscription_id = request.id.clone().ok_or_else(|| {
McpError::invalid_request("subscriptions/listen must carry a JSON-RPC request id")
})?;
let params = request
.params
.clone()
.ok_or_else(|| McpError::invalid_params("subscriptions/listen requires parameters"))
.and_then(|params| {
serde_json::from_value::<FinalSubscriptionsListenParams>(params)
.map_err(|error| McpError::invalid_params(error.to_string()))
})?;
let protocol_version = params.meta.protocol_version().map_err(|_| {
McpError::invalid_params("subscriptions/listen has invalid protocol metadata")
})?;
let client_capabilities = params.meta.client_capabilities().map_err(|_| {
McpError::invalid_params("subscriptions/listen has invalid client capabilities")
})?;
if client_capabilities.is_none() {
return Err(McpError::invalid_params(
"subscriptions/listen requires exact final request metadata",
));
}
#[cfg(feature = "tasks")]
let client_capabilities = client_capabilities.expect("presence checked above");
if protocol_version != Some(MODERN_PROTOCOL_VERSION) {
return Err(McpError::invalid_params(
"subscriptions/listen requires exact final request metadata",
));
}
#[cfg(feature = "tasks")]
let tasks_requested =
self.admit_final_task_subscription(¶ms.notifications, client_capabilities)?;
#[cfg(feature = "tasks")]
let notification_sender =
if tasks_requested && let Some(runtime) = self.final_task_runtime.as_ref() {
let mut admitted_tasks = BTreeMap::new();
for task_id in task_subscription_ids(¶ms.notifications)
.map_err(|_| McpError::invalid_params("invalid Tasks subscription filter"))?
.unwrap_or_default()
{
let snapshot = runtime.task_for_request(request_ctx, &task_id)?;
admitted_tasks.insert(task_id, snapshot);
}
let runtime = runtime.clone();
let guarded: NotificationSender = Arc::new(move |notification| {
if notification.method == fastmcp_protocol::TASK_STATUS_NOTIFICATION {
let event = serde_json::to_value(¬ification).ok().and_then(|wire| {
serde_json::from_value::<FinalTaskStatusNotification>(wire).ok()
});
let Some(event) = event else {
return;
};
let Some(admitted) = admitted_tasks.get(&event.params.task.base().task_id)
else {
return;
};
if !runtime.notification_matches_task_owner(admitted, &event) {
return;
}
}
notification_sender(notification);
});
guarded
} else {
notification_sender
};
#[cfg(not(feature = "tasks"))]
let tasks_requested = false;
if request_cancellation.is_cancel_requested() || request_ctx.ensure_live().is_err() {
return Err(McpError::request_cancelled());
}
// A route-bound relay owns a distinct upstream listen request. Require
// its acknowledgement before acknowledging downstream, and refuse a
// narrowed upstream filter rather than advertising task IDs we cannot
// later deliver. The listener itself remains owned by this downstream
// request future; dropping it closes its upstream response body.
#[cfg(all(feature = "proxy", feature = "tasks"))]
let mut relay_listener: Option<Box<dyn ProxyFinalTaskListener>> = None;
#[cfg(all(feature = "proxy", feature = "tasks"))]
if tasks_requested && let Some(relay) = self.final_task_relay.as_ref() {
let mut listener = relay
.open_listener_async(request_ctx, params.notifications.clone())
.await?;
match listener
.next_async(request_ctx.cx(), &request_cancellation)
.await?
{
ProxyFinalTaskListenerEvent::Acknowledged(accepted) => {
if !subscription_filter_admission_matches(¶ms.notifications, &accepted)? {
return Err(McpError::invalid_params(
"Proxy upstream narrowed the final Tasks subscription filter",
));
}
}
ProxyFinalTaskListenerEvent::Notification(_)
| ProxyFinalTaskListenerEvent::Terminal => {
return Err(McpError::invalid_request(
"Proxy upstream Tasks listener did not acknowledge before events",
));
}
}
relay_listener = Some(listener);
}
#[cfg(all(feature = "proxy", feature = "tasks"))]
let mut catalog_relay_listener: Option<Box<dyn ProxyCatalogListener>> = None;
#[cfg(all(feature = "proxy", feature = "tasks"))]
if !tasks_requested
&& catalog_filter_requests_resource_updates(¶ms.notifications)
&& let Some(relay) = self.final_task_relay.as_ref()
{
let mut listener = relay
.open_catalog_listener_async(request_ctx, params.notifications.clone())
.await?;
match listener
.next_async(request_ctx.cx(), &request_cancellation)
.await?
{
ProxyCatalogListenerEvent::Acknowledged(accepted) => {
if !subscription_filter_admission_matches(¶ms.notifications, &accepted)? {
return Err(McpError::invalid_params(
"Proxy upstream narrowed the catalog subscription filter",
));
}
}
ProxyCatalogListenerEvent::Notification(_)
| ProxyCatalogListenerEvent::Terminal => {
return Err(McpError::invalid_request(
"Proxy upstream catalog listener did not acknowledge before events",
));
}
}
catalog_relay_listener = Some(listener);
}
#[cfg(feature = "tasks")]
let task_principal = if tasks_requested && self.final_task_runtime.is_some() {
request_ctx
.auth()
.as_ref()
.map(|auth| auth::principal_fingerprint(Some(auth)))
.transpose()?
} else {
None
};
#[cfg(not(feature = "tasks"))]
let task_principal = None;
let lease = self.final_subscriptions.open_with_task_principal(
subscription_id.clone(),
params.notifications,
tasks_requested,
modern_http_owner,
request_cancellation.clone(),
terminal_delivery.clone(),
notification_sender,
task_principal,
)?;
while !self.final_subscriptions.is_terminating() {
if request_cancellation.is_cancel_requested() || request_ctx.ensure_live().is_err() {
if lease.has_graceful_completion() {
return self.final_subscription_complete_result(&subscription_id);
}
if let Some(delivery) = &terminal_delivery {
delivery.mark_failed();
}
return Err(McpError::request_cancelled());
}
#[cfg(all(feature = "proxy", feature = "tasks"))]
if let Some(listener) = relay_listener.as_mut() {
match listener
.next_async(request_ctx.cx(), &request_cancellation)
.await
{
Ok(ProxyFinalTaskListenerEvent::Notification(notification)) => {
self.final_subscriptions.publish_task(notification)?;
}
Ok(ProxyFinalTaskListenerEvent::Terminal) => {
return self.final_subscription_complete_result(&subscription_id);
}
Ok(ProxyFinalTaskListenerEvent::Acknowledged(_)) => {
return Err(McpError::invalid_request(
"Proxy upstream Tasks listener acknowledged more than once",
));
}
Err(error) if error.code == McpErrorCode::RequestCancelled => {
// Shutdown elects graceful completion by cancelling
// this wait. The `?` path would drop that elected
// terminal response because cancellation already won
// the request token, and HTTP teardown would wait out
// both drain budgets.
if lease.has_graceful_completion() {
return self.final_subscription_complete_result(&subscription_id);
}
if let Some(delivery) = &terminal_delivery {
delivery.mark_failed();
}
return Err(error);
}
Err(error) => return Err(error),
}
}
#[cfg(all(feature = "proxy", feature = "tasks"))]
if let Some(listener) = catalog_relay_listener.as_mut() {
match listener
.next_async(request_ctx.cx(), &request_cancellation)
.await
{
Ok(ProxyCatalogListenerEvent::Notification(
notification @ ServerNotification::ResourceUpdated(_),
)) => {
self.final_subscriptions.publish(notification)?;
}
Ok(ProxyCatalogListenerEvent::Notification(_)) => {}
Ok(ProxyCatalogListenerEvent::Terminal) => {
return self.final_subscription_complete_result(&subscription_id);
}
Ok(ProxyCatalogListenerEvent::Acknowledged(_)) => {
return Err(McpError::invalid_request(
"Proxy upstream catalog listener acknowledged more than once",
));
}
Err(error) if error.code == McpErrorCode::RequestCancelled => {
if lease.has_graceful_completion() {
return self.final_subscription_complete_result(&subscription_id);
}
if let Some(delivery) = &terminal_delivery {
delivery.mark_failed();
}
return Err(error);
}
Err(error) => return Err(error),
}
}
if request_cancellation.is_cancel_requested() {
// A server-side graceful election cancels this request as its
// wake; the elected terminal completion outranks the plain
// cancellation error a peer-initiated cancel produces.
if lease.has_graceful_completion() {
return self.final_subscription_complete_result(&subscription_id);
}
if let Some(delivery) = &terminal_delivery {
delivery.mark_failed();
}
return Err(McpError::request_cancelled());
}
if request_ctx.ensure_live().is_err() {
// Keep the lease registered until the owning server lifecycle
// performs its global terminal election. Exiting here would
// let `Drop` remove the entry before `BoundHttpServer::serve`
// can obtain the delivery receipt it must await. An embedding
// session without that lifecycle still releases this wait by
// cancelling the exact response-body request in `close`.
while !self.final_subscriptions.is_terminating()
&& !request_cancellation.is_cancel_requested()
{
asupersync::runtime::yield_now().await;
}
if request_cancellation.is_cancel_requested()
&& !self.final_subscriptions.is_terminating()
&& !lease.has_graceful_completion()
{
return Err(McpError::request_cancelled());
}
break;
}
// The registry is intentionally transport-neutral. Polling uses
// the caller-owned clock and never creates a detached wake task;
// a request cancellation wakes its transport-owned dispatch task.
// Race the poll against `cancelled()` so two live HTTP listens
// wake as soon as `terminate_with_receipt` elects completion,
// instead of remaining parked on a cancelled-Cx timer.
if request_ctx.ensure_live().is_err() {
asupersync::runtime::yield_now().await;
} else {
let mut sleep = std::pin::pin!(asupersync::time::sleep(
request_ctx.cx().now(),
Duration::from_millis(1),
));
let cancellation = request_cancellation.clone();
let mut cancelled = std::pin::pin!(async move {
cancellation.cancelled().await;
});
std::future::poll_fn(|task_cx| {
if self.final_subscriptions.is_terminating()
|| request_cancellation.is_cancel_requested()
{
return std::task::Poll::Ready(());
}
if cancelled.as_mut().poll(task_cx).is_ready() {
return std::task::Poll::Ready(());
}
if sleep.as_mut().poll(task_cx).is_ready() {
return std::task::Poll::Ready(());
}
std::task::Poll::Pending
})
.await;
}
}
// `terminate_with_receipt` publishes its global terminating bit before
// it acquires each lease election. Do not let that brief ordering
// window turn a server-owned graceful close into peer cancellation:
// wait until this exact lease records which terminal owner won.
while self.final_subscriptions.is_terminating()
&& !lease.has_graceful_completion()
&& !lease.is_peer_terminated()
&& !request_cancellation.is_cancel_requested()
{
asupersync::runtime::yield_now().await;
}
if lease.has_graceful_completion() {
return self.final_subscription_complete_result(&subscription_id);
}
Err(McpError::request_cancelled())
}
fn final_subscription_complete_result(
&self,
subscription_id: &RequestId,
) -> McpResult<serde_json::Value> {
let server_info =
Implementation::try_new(self.info.name.clone(), self.info.version.clone())
.map_err(|_| McpError::internal_error("server identity is invalid"))?;
let mut metadata = BTreeMap::new();
metadata.insert(
FINAL_SERVER_INFO_META_KEY.to_owned(),
serde_json::to_value(&server_info).map_err(McpError::from)?,
);
metadata.insert(
FINAL_SUBSCRIPTION_ID_META_KEY.to_owned(),
serde_json::to_value(subscription_id).map_err(McpError::from)?,
);
let metadata = OpenMetadata::try_from_entries(metadata)
.map_err(|_| McpError::internal_error("final subscription metadata is invalid"))?;
let result = CompleteResult::new(
FinalSubscriptionsListenResult {},
ResultMeta::server_generated(server_info).with_metadata(metadata),
);
let wire = CoreResult::Final(FinalCoreResult::SubscriptionsListen {
result,
subscription_id: subscription_id.clone(),
diagnostic: None,
})
.encode()
.map_err(|error| {
McpError::internal_error(format!(
"failed to encode final subscriptions/listen completion: {error}"
))
})?;
serde_json::from_str(&wire).map_err(McpError::from)
}
/// Runs one modern stdio request. `subscriptions/listen` runs in a
/// caller-owned blocking task so the receive pump can keep accepting
/// requests while shutdown retains the subscription's active ownership.
/// On an unsplit transport, output queued while `recv` owns the I/O
/// handle flushes when that receive returns. Waiting for a blocked
/// middleware callback here would also block the input that releases it;
/// callers needing independent output must use the split transport API.
///
/// `cx` is the RECEIVE PUMP's context and owns transport I/O. `dispatch_cx`
/// is the caller runtime's context and owns the request future: when the
/// pump runs as a blocking child (the stdio arrangement), a handler that
/// admits an async child cannot make progress under the pump's blocking
/// context, and the request future stays `Pending` forever — the process
/// answers `server/discover` and then silently stops responding
/// (GitHub #65). Callers whose pump and runtime are the same context pass
/// the same `Cx` twice.
fn dispatch_or_schedule_stdio_modern_request<S>(
server: Arc<Self>,
cx: &Cx,
dispatch_cx: &Cx,
session_id: u64,
inbound: InboundRequestContext,
request: JsonRpcRequest,
auth_receipt: Option<AuthDispatchCustody>,
auth_custody_generation: Option<u64>,
notification_sender: NotificationSender,
send: Arc<Mutex<S>>,
background_send_failure: Arc<Mutex<Option<McpError>>>,
queue: Arc<DispatchQueueState>,
) -> Option<JsonRpcResponse>
where
S: FnMut(&Cx, &JsonRpcMessage) -> Result<(), TransportError> + Send + 'static,
{
let policy = server.protocol_policy;
if request.id.is_none() && request.method == "notifications/cancelled" {
let mut request = request;
if admit_final_client_notification_ingress(&request).is_ok()
&& let Ok(cancellation) = server.authenticate_modern_cancelled_control(
&inbound,
&mut request,
auth_receipt.as_ref(),
auth_custody_generation,
)
{
let _writer = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
server.handle_cancellation_wire_notification(session_id, cancellation);
}
return None;
}
// Reserve count and retained bytes before authentication or task
// submission. The owner moves into the task, including while it is
// queued or still blocked in application middleware.
let reservation =
match queue.admit_modern_request(&request, Arc::new(AtomicBool::new(false))) {
Ok(reservation) => reservation,
Err(error) => return request.id.map(|id| JsonRpcResponse::error(Some(id), error)),
};
let request_cancellation = reservation.cancellation();
// Establish the connection principal before receiving another frame.
// A cancellation of this opening request must work even if its child
// has not started. The private receipt prevents a second provider call.
let auth_receipt = match server.admit_modern_pump_authentication(
&inbound,
&request,
auth_receipt.as_ref(),
auth_custody_generation,
) {
Ok(receipt) => Some(receipt),
Err(error) => {
let error = mask_peer_error(error, server.mask_error_details);
return request.id.map(|id| {
JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: error.code.into(),
message: error.message,
data: error.data,
},
)
});
}
};
if request.method == SUBSCRIPTIONS_LISTEN {
// `inbound`/`request` move into the worker closure below;
// `InboundRequestContext` is deliberately non-Clone, so a failed
// spawn cannot fall back to inline dispatch — it answers with an
// internal error instead of freezing the receive pump.
let send_cx = cx.clone();
let request_id = request.id.clone();
// Never let spawn_blocking fall back to polling a blocking
// middleware inline on the caller's async worker.
if dispatch_cx.blocking_pool_handle().is_none() {
return Some(JsonRpcResponse::error(
request_id,
JsonRpcError {
code: McpErrorCode::InternalError.into(),
message: "Subscriptions require a caller-owned blocking pool".to_owned(),
data: None,
},
));
}
// Reserve ownership before admission: shutdown can race a queued
// task before its caller-owned child Cx has been created.
let active_request = request
.id
.clone()
.map(|id| {
ActiveRequestGuard::try_reserve(
Arc::clone(&server.active_requests),
session_id,
id,
dispatch_cx.region_id(),
request_cancellation.clone(),
)
})
.transpose();
let active_request = match active_request {
Ok(guard) => guard,
Err(_) => {
return Some(JsonRpcResponse::error(
request_id,
JsonRpcError {
code: McpErrorCode::InvalidRequest.into(),
message: "Request id is already active".to_owned(),
data: None,
},
));
}
};
let submitted = dispatch_cx.spawn_blocking({
let server = Arc::clone(&server);
let notification_sender = Arc::clone(¬ification_sender);
let request_cancellation = request_cancellation.clone();
move |request_cx| {
let _reservation = reservation;
let active_guard = active_request;
if active_guard
.as_ref()
.is_some_and(|guard| !guard.activate(request_cx.clone()))
{
return;
}
let inbound = inbound.with_cx(request_cx.clone());
let response = poll_on_cx(
&request_cx,
server.dispatch_with_protocol_policy_owned(
policy,
&inbound,
request,
None,
auth_receipt,
auth_custody_generation,
None,
request_cancellation,
None,
notification_sender,
),
);
if let Some(response) = response {
let mut send_guard = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Err(error) =
send_guard(&send_cx, &JsonRpcMessage::Response(response))
&& let Err(error) = returning_send_result(&error)
{
background_send_failure
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get_or_insert(error);
}
}
drop(active_guard);
}
});
return match submitted {
Ok(_) => None,
Err(_) => Some(JsonRpcResponse::error(
request_id,
JsonRpcError {
code: (-32603).into(),
message: "Caller runtime could not admit the subscription task".to_owned(),
data: None,
},
)),
};
}
poll_on_cx(
dispatch_cx,
server.dispatch_with_protocol_policy_owned(
policy,
&inbound,
request,
None,
auth_receipt,
auth_custody_generation,
None,
request_cancellation,
None,
notification_sender,
),
)
}
async fn dispatch_with_protocol_policy_owned(
self: Arc<Self>,
policy: ProtocolPolicy,
inbound: &InboundRequestContext,
request: JsonRpcRequest,
raw_params: Option<Arc<str>>,
auth_receipt: Option<AuthDispatchCustody>,
websocket_connection_generation: Option<u64>,
modern_http_owner: Option<u64>,
request_cancellation: McpRequestCancellation,
terminal_delivery: Option<Arc<FinalSubscriptionTerminalDelivery>>,
notification_sender: NotificationSender,
) -> Option<JsonRpcResponse> {
self.dispatch_with_protocol_policy_owned_and_handler_admission(
policy,
inbound,
request,
raw_params,
auth_receipt,
websocket_connection_generation,
modern_http_owner,
request_cancellation,
terminal_delivery,
notification_sender,
None,
)
.await
}
async fn dispatch_with_protocol_policy_owned_and_handler_admission(
self: Arc<Self>,
policy: ProtocolPolicy,
inbound: &InboundRequestContext,
request: JsonRpcRequest,
raw_params: Option<Arc<str>>,
auth_receipt: Option<AuthDispatchCustody>,
websocket_connection_generation: Option<u64>,
modern_http_owner: Option<u64>,
request_cancellation: McpRequestCancellation,
terminal_delivery: Option<Arc<FinalSubscriptionTerminalDelivery>>,
notification_sender: NotificationSender,
handler_admission: Option<FinalHandlerAdmission>,
) -> Option<JsonRpcResponse> {
if matches!(policy, ProtocolPolicy::ModernOnly)
&& request.method == "initialize"
&& request.validate().is_ok()
&& inbound.request_id() == request_id_to_u64(request.id.as_ref())
{
return request.id.clone().map(|id| {
JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: (-32601).into(),
message: MODERN_ONLY_INITIALIZE_MESSAGE.to_owned(),
data: Some(serde_json::json!({ "supported": ["2026-07-28"] })),
},
)
});
}
self.dispatch_stateless_owned_with_cancellation(
inbound,
request,
raw_params,
auth_receipt,
websocket_connection_generation,
modern_http_owner,
request_cancellation,
terminal_delivery,
notification_sender,
handler_admission,
)
.await
}
/// Dispatches one adapter-admitted exact-2024 request without applying the
/// modern complete-result package.
///
/// The exact adapter owns lifecycle admission and final JSON-RPC response
/// construction. This seam owns the request authority for a real wire ID,
/// including cooperative cancellation, middleware ordering, and raw
/// method-specific result serialization. In particular, it must never
/// reuse [`Self::dispatch_stateless`]: that modern surface adds
/// `resultType: "complete"`, which has no exact-2024 representation.
#[cfg(feature = "legacy-2024-11-05")]
async fn dispatch_legacy_2024(
&self,
cx: &Cx,
session_id: u64,
session_principal: &SessionPrincipalBinding,
runtime: Option<&LiveLegacy2024ConnectionRuntime>,
admitted_cancellation: Option<McpRequestCancellation>,
request: &JsonRpcRequest,
auth_receipt: Option<&AuthAdmissionReceipt>,
connection_auth: Option<&AuthDispatchCustody>,
) -> McpResult<LiveLegacy2024Dispatch> {
let request_id = request
.id
.clone()
.ok_or_else(|| McpError::invalid_request("exact legacy requests require an id"))?;
let budget = self.create_request_budget(cx);
Self::enforce_request_budget(cx, budget)?;
// Hold this guard through handler and middleware completion so a
// concurrently received notifications/cancelled frame targets the
// exact legacy wire ID rather than an internal synthetic ID.
let active_guard = ActiveRequestGuard::try_new_with_cancellation(
Arc::clone(&self.active_requests),
session_id,
request_id,
cx.clone(),
admitted_cancellation.unwrap_or_default(),
)
.map_err(|_| {
McpError::invalid_request(
"Request id is already active; wait for the earlier request to finish before reusing it",
)
})?;
let request_cancellation = active_guard.cancellation();
let result = async {
let session_state =
runtime.map_or_else(SessionState::new, |runtime| runtime.session_state.clone());
let (mut request_ctx, _request_lease_guard) = self.request_context(
cx,
request_id_to_u64(request.id.as_ref()),
session_state.clone(),
request_cancellation.clone(),
budget,
)?;
request_ctx = Self::attach_session_log_floor(
request_ctx,
runtime.and_then(|runtime| runtime.log_level()),
);
if let Some(runtime) = runtime {
let subscriptions = runtime
.resource_subscriptions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
request_ctx = Self::attach_session_resource_subscriptions(
request_ctx,
subscriptions.iter().map(String::as_str),
);
}
if let Some(runtime) = runtime {
let mut info = ClientCapabilityInfo::new();
if runtime.supports_sampling.load(Ordering::Acquire) {
info = info.with_sampling();
}
if runtime.supports_roots.load(Ordering::Acquire) {
info = info.with_roots(false);
}
request_ctx = request_ctx.with_client_capabilities(info);
let identity = runtime
.client_info
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
request_ctx =
Self::attach_session_client_implementation(request_ctx, identity.as_ref());
}
if let Some(runtime) = runtime {
let sender = Arc::clone(&runtime.notification_sender);
request_ctx = request_ctx.with_log_sender(Arc::new(
crate::handler::LogNotificationSender::new(
move |notification| {
sender(notification);
},
ProtocolEra::Legacy2024,
),
));
}
if let (Some(marker), Some(runtime)) = (Self::request_progress_marker(request), runtime)
{
let sender = Arc::clone(&runtime.notification_sender);
request_ctx = request_ctx.with_progress_reporter(
ProgressNotificationSender::new(marker, move |notification| {
sender(notification);
})
.into_reporter(),
);
}
let bidirectional_senders = runtime.and_then(|runtime| {
runtime.bidirectional_senders(self, &request_cancellation, &request_ctx)
});
Self::enforce_request_context(&request_ctx)?;
// Clone so in-band credentials can be stripped after admission.
// Exact-2024 stdio has no transport custody; it must still evaluate
// AuthProvider the same way modern stdio does, or negotiating
// 2024-11-05 would skip every installed verifier.
let mut request = request.clone();
let fingerprint = if let Some(receipt) = auth_receipt {
let fingerprint = receipt.commit_legacy(&request_ctx, &request)?;
request.params = receipt.sanitized_params.clone();
fingerprint
} else if let Some(connection_auth) = connection_auth {
let fingerprint = connection_auth.commit_legacy_connection(&request_ctx)?;
auth::strip_recognized_access_credentials(&mut request.params);
fingerprint
} else {
let auth_request = AuthRequest {
method: request.method.as_str(),
params: request.params.as_ref(),
transport_authorization: None,
request_id: request_id_to_u64(request.id.as_ref()),
};
let fingerprint = self.authenticate_request(&request_ctx, auth_request)?;
auth::strip_recognized_access_credentials(&mut request.params);
fingerprint
};
if !session_principal.bind_or_verify(fingerprint) {
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authenticated principal does not own an admitted session",
));
}
let mut entered_middleware: Vec<&dyn crate::Middleware> = Vec::new();
let result: McpResult<serde_json::Value> = async {
for middleware in self.middleware.iter() {
Self::enforce_request_context(&request_ctx)?;
entered_middleware.push(middleware.as_ref());
match catch_extension_unwind(|| middleware.on_request(&request_ctx, &request)) {
Ok(Ok(MiddlewareDecision::Continue)) => {}
Ok(Ok(MiddlewareDecision::Respond(value))) => return Ok(value),
Ok(Err(error)) => return Err(error),
Err(_payload) => {
return Err(extension_panic_error("middleware_on_request"));
}
}
}
let params = request.params.clone();
match request.method.as_str() {
"completion/complete" => {
self.router
.dispatch_legacy_completion_in_request(&request_ctx, cx, &request)
.await
}
"tools/list" => {
let params: ListToolsParams = parse_params_or_default(params)?;
serde_json::to_value(self.router.handle_tools_list(
&request_ctx,
params,
Some(&session_state),
)?)
.map_err(McpError::from)
}
"tools/call" => {
let params: CallToolParams = parse_params(params)?;
if self.legacy_application_tool_content {
return serde_json::to_value(
self.router
.handle_application_tools_call_in_request(
&request_ctx,
cx,
params,
session_state.clone(),
runtime.map(|runtime| &runtime.notification_sender),
bidirectional_senders.as_ref(),
)
.await?,
)
.map_err(McpError::from);
}
serde_json::to_value(
self.router
.handle_tools_call_in_request(
&request_ctx,
cx,
params,
session_state.clone(),
runtime.map(|runtime| &runtime.notification_sender),
bidirectional_senders.as_ref(),
)
.await?,
)
.map_err(McpError::from)
}
"resources/list" => {
let params: ListResourcesParams = parse_params_or_default(params)?;
serde_json::to_value(self.router.handle_resources_list(
&request_ctx,
params,
Some(&session_state),
)?)
.map_err(McpError::from)
}
"resources/templates/list" => {
let params: ListResourceTemplatesParams = parse_params_or_default(params)?;
serde_json::to_value(self.router.handle_resource_templates_list(
&request_ctx,
params,
Some(&session_state),
)?)
.map_err(McpError::from)
}
"resources/read" => {
let params: ReadResourceParams = parse_params(params)?;
serde_json::to_value(
self.router
.handle_resources_read_in_request(
&request_ctx,
cx,
¶ms,
session_state.clone(),
runtime.map(|runtime| &runtime.notification_sender),
bidirectional_senders.as_ref(),
)
.await?,
)
.map_err(McpError::from)
}
"prompts/list" => {
let params: ListPromptsParams = parse_params_or_default(params)?;
serde_json::to_value(self.router.handle_prompts_list(
&request_ctx,
params,
Some(&session_state),
)?)
.map_err(McpError::from)
}
"prompts/get" => {
let params: GetPromptParams = parse_params(params)?;
serde_json::to_value(
self.router
.handle_prompts_get_in_request(
&request_ctx,
cx,
params,
session_state,
runtime.map(|runtime| &runtime.notification_sender),
bidirectional_senders.as_ref(),
)
.await?,
)
.map_err(McpError::from)
}
"resources/subscribe" => {
let params: SubscribeResourceParams = parse_params(params)?;
let uri = params.uri;
if uri.len() > MAX_RESOURCE_SUBSCRIPTION_BYTES_PER_SESSION {
return Err(resource_subscription_capacity_error());
}
if !self.router.resource_exists(&uri) {
return Err(McpError::resource_not_found(&uri));
}
self.router
.notify_resource_subscribed_async(&request_ctx, &uri)
.await?;
Ok(serde_json::json!({}))
}
"resources/unsubscribe" => {
let params: UnsubscribeResourceParams = parse_params(params)?;
self.router
.notify_resource_unsubscribed_async(&request_ctx, ¶ms.uri)
.await?;
Ok(serde_json::json!({}))
}
_ => Err(McpError::method_not_found(&request.method)),
}
}
.await;
let result = self.finalize_middleware_result(
&request_cancellation,
&entered_middleware,
&request_ctx,
&request,
result,
);
if let Some(runtime) = runtime {
self.maybe_emit_log_notification_for_level(
runtime.log_level(),
&runtime.notification_sender,
&request.method,
&result,
);
}
match result {
Ok(value) if value.get("resultType").is_some() => Err(mask_peer_error(
McpError::internal_error(
"modern result discriminator cannot cross the exact legacy bridge",
),
self.mask_error_details,
)),
Ok(value) => Ok(value),
Err(error) => Err(mask_peer_error(error, self.mask_error_details)),
}
}
.await;
Ok(LiveLegacy2024Dispatch {
result,
active_request: LiveLegacy2024ActiveRequest {
cancellation: request_cancellation,
active_guard,
budget,
},
})
}
/// Rejects a final Tasks subscription request before the admitted HTTP
/// response representation can commit or dispatch request-scoped SSE
/// output. Tool outcome capability admission stays in router dispatch so
/// disabled-tool, schema, and complete-outcome precedence remains exact.
fn preflight_modern_http_required_capability(
&self,
request: &JsonRpcRequest,
) -> Option<JsonRpcResponse> {
#[cfg(feature = "tasks")]
{
let admission = self.preflight_final_http_subscription_required_capability(request);
if let Err(error) = admission
&& is_canonical_missing_required_client_capability_error(&error)
{
return request.id.clone().map(|id| {
JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: error.code.into(),
message: error.message,
data: error.data,
},
)
});
}
}
#[cfg(not(feature = "tasks"))]
let _ = request;
None
}
#[cfg(feature = "tasks")]
fn preflight_final_http_subscription_required_capability(
&self,
request: &JsonRpcRequest,
) -> McpResult<()> {
if request.method != SUBSCRIPTIONS_LISTEN
|| modern_protocol_version(request) != Some(MODERN_PROTOCOL_VERSION)
{
return Ok(());
}
let Some(params) = request.params.clone() else {
return Ok(());
};
let Ok(params) = serde_json::from_value::<FinalSubscriptionsListenParams>(params) else {
return Ok(());
};
let Ok(Some(client_capabilities)) = params.meta.client_capabilities() else {
return Ok(());
};
self.admit_final_task_subscription(¶ms.notifications, client_capabilities)
.map(|_| ())
}
/// Processes a modern request under an explicit protocol policy.
///
/// `ModernOnly` declines every request that lacks the exact final-era
/// marker before parameter decoding or any legacy session lookup. Final
/// `server/discover` remains a valid first request when it carries that
/// marker.
#[must_use]
pub async fn dispatch_with_protocol_policy(
&self,
policy: ProtocolPolicy,
inbound: &InboundRequestContext,
request: &JsonRpcRequest,
) -> Option<JsonRpcResponse> {
self.dispatch_with_protocol_policy_and_cancellation(
policy, inbound, request, None, None, None, None,
)
.await
}
async fn dispatch_with_protocol_policy_and_cancellation(
&self,
policy: ProtocolPolicy,
inbound: &InboundRequestContext,
request: &JsonRpcRequest,
request_cancellation: Option<McpRequestCancellation>,
raw_params: Option<&str>,
auth_receipt: Option<&AuthDispatchCustody>,
websocket_connection_generation: Option<u64>,
) -> Option<JsonRpcResponse> {
if matches!(policy, ProtocolPolicy::LegacyOnly)
&& modern_protocol_version(request).is_some()
{
return legacy_only_modern_refusal(request);
}
if matches!(policy, ProtocolPolicy::ModernOnly)
&& modern_protocol_version(request) != Some(MODERN_PROTOCOL_VERSION)
{
if request.method != "initialize"
|| request.validate().is_err()
|| inbound.request_id() != request_id_to_u64(request.id.as_ref())
{
return protocol_era_refusal(request);
}
return request.id.clone().map(|id| {
JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: (-32601).into(),
message: MODERN_ONLY_INITIALIZE_MESSAGE.to_owned(),
data: Some(serde_json::json!({ "supported": ["2026-07-28"] })),
},
)
});
}
self.dispatch_stateless_with_cancellation(
inbound,
request,
request_cancellation,
raw_params,
auth_receipt,
websocket_connection_generation,
)
.await
}
/// Maps an explicit-policy modern dispatch result onto the HTTP response
/// status required for an explicit-policy cross-era refusal.
///
/// The transport still owns raw HTTP parsing, origin checks, and
/// authentication before it constructs [`InboundRequestContext`]. This
/// helper owns only the transport-neutral JSON-RPC result and its final
/// HTTP status mapping.
#[must_use]
pub async fn dispatch_http_with_protocol_policy(
&self,
policy: ProtocolPolicy,
inbound: &InboundRequestContext,
request: &JsonRpcRequest,
) -> HttpResponse {
let is_modern_http_request =
modern_protocol_version(request) == Some(MODERN_PROTOCOL_VERSION);
let is_cross_era_request = (matches!(policy, ProtocolPolicy::LegacyOnly)
&& modern_protocol_version(request).is_some())
|| (matches!(policy, ProtocolPolicy::ModernOnly)
&& modern_protocol_version(request) != Some(MODERN_PROTOCOL_VERSION));
match self
.dispatch_with_protocol_policy(policy, inbound, request)
.await
{
Some(response)
if is_cross_era_request
|| is_modern_http_request
&& (is_canonical_missing_required_client_capability_response(
&response,
) || is_parameter_header_mismatch_response(&response)) =>
{
HttpResponse::new(HttpStatus::BAD_REQUEST).with_json(&response)
}
Some(response) if is_modern_http_request && is_method_not_found_response(&response) => {
HttpResponse::new(HttpStatus::NOT_FOUND).with_json(&response)
}
Some(response) => HttpResponse::ok().with_json(&response),
None => HttpResponse::new(HttpStatus::ACCEPTED),
}
}
/// Processes a single JSON-RPC request through the full server dispatch
/// pipeline (initialization checks, middleware, routing, tool/resource/prompt
/// execution, error masking, and statistics recording).
///
/// This is the public equivalent of the internal `handle_request` method that
/// the stdio and custom transport paths use. It allows
/// external code to drive the server from a custom transport or embedding
/// without going through a `Transport` abstraction.
///
/// # Parameters
///
/// - `cx` — The cancellation / budget context for this request.
/// - `session` — Mutable reference to the session for this connection.
/// The caller is responsible for session lifecycle (creation, sharing,
/// locking if shared across threads).
/// - `request` — The incoming JSON-RPC request (or notification).
/// - `notification_sender` — Callback used to push server-initiated
/// notifications (e.g. progress) back to the client.
/// - `request_sender` — Sender for server-to-client requests
/// (sampling, elicitation, roots).
///
/// # Returns
///
/// `Some(JsonRpcResponse)` for normal requests, or `None` for
/// notifications (JSON-RPC messages without an `id`).
///
/// # Example
///
/// ```ignore
/// use std::sync::Arc;
/// use fastmcp_rust::{
/// Server, Session, JsonRpcRequest, NotificationSender,
/// bidirectional::RequestSender,
/// };
/// use fastmcp_core::Cx;
///
/// let server = Arc::new(
/// Server::new("my-server", "1.0.0").build(),
/// );
/// let mut session = Session::new();
/// let cx = Cx::for_request();
/// let notify: NotificationSender = Arc::new(|_| {});
/// let req_sender = RequestSender::noop();
///
/// let request: JsonRpcRequest = /* ... */;
/// let response = server
/// .dispatch_request(&cx, &mut session, request, ¬ify, &req_sender)
/// .await;
/// ```
pub async fn dispatch_request(
&self,
cx: &Cx,
session: &mut Session,
request: JsonRpcRequest,
notification_sender: &NotificationSender,
request_sender: &bidirectional::RequestSender,
) -> Option<JsonRpcResponse> {
self.handle_request(cx, session, request, notification_sender, request_sender)
.await
}
/// Runs the server on stdio with a provided Cx.
///
/// The application owns the runtime and must configure a blocking pool
/// for the receive pump. This method does not create or re-enter a runtime.
///
/// On Unix, the receive pump uses readiness polling so cancellation from a
/// failed dispatch worker remains observable while stdin is silent or a
/// pipe frame is incomplete. Unix responses use nonblocking writes with a
/// bounded commit deadline for ordinary pipes and sockets; regular files
/// and some devices may ignore `O_NONBLOCK`. Other targets use the
/// sequential server loop: that avoids a worker failing while the receive
/// side is blocked, but generic blocking stdin reads and stdout writes
/// remain observable only at frame boundaries or I/O completion.
///
/// If an arbitrary handler does not quiesce within the bounded worker
/// shutdown deadline, the process exits unsuccessfully without running the
/// shutdown hook; running a hook concurrently with a live handler would
/// violate the hook's quiescence contract.
pub async fn run_stdio_with_cx(self, cx: &Cx) -> ! {
let dispatch_cx = cx.clone();
let mut pump =
match cx.spawn_blocking(move |pump_cx| self.run_stdio_pump(&pump_cx, &dispatch_cx)) {
Ok(pump) => pump,
Err(error) => {
error!(
target: targets::SERVER,
"Could not admit the stdio pump into the caller runtime: {error}"
);
std::process::exit(1);
}
};
let exit_code = match pump.join(cx).await {
Ok(exit_code) => exit_code,
Err(error) => {
error!(
target: targets::SERVER,
"Caller-owned stdio pump ended without a final status: {error:?}"
);
1
}
};
std::process::exit(exit_code)
}
fn run_stdio_pump(self, cx: &Cx, dispatch_cx: &Cx) -> i32 {
// Initialize rich logging first, before any log output
self.init_rich_logging();
let mut transport = StdioTransport::stdio();
let mut stdout = AsyncStdout::new();
let codec = Codec::new();
// Create a notification sender that writes to a separate stdout handle.
// This allows progress notifications to be sent during handler execution
// while the main transport is blocked on recv().
let fatal_notification_output = Arc::new(AtomicBool::new(false));
let notification_sender =
create_notification_sender(Arc::clone(&fatal_notification_output));
#[cfg(unix)]
{
let receive_fatal_output = Arc::clone(&fatal_notification_output);
let send_fatal_output = Arc::clone(&fatal_notification_output);
self.run_loop_with_dispatch_cx(
cx,
dispatch_cx,
move |cx, worker_failed| {
transport.recv_until_or_stopped(cx, None, || {
if receive_fatal_output.load(Ordering::Acquire) {
worker_failed.store(true, Ordering::Release);
true
} else {
worker_failed.load(Ordering::Acquire)
}
})
},
move |cx, message| {
if send_fatal_output.load(Ordering::Acquire) || cx.checkpoint().is_err() {
return Err(TransportError::Cancelled);
}
let bytes = match message {
JsonRpcMessage::Request(request) => codec.encode_request(request)?,
JsonRpcMessage::Response(response) => codec.encode_response(response)?,
};
stdout.write_all_bounded(&bytes, STDIO_OUTPUT_COMMIT_TIMEOUT)?;
Ok(())
},
notification_sender,
Arc::clone(&fatal_notification_output),
"stdio",
)
}
#[cfg(not(unix))]
{
let receive_fatal_output = Arc::clone(&fatal_notification_output);
let send_fatal_output = Arc::clone(&fatal_notification_output);
self.run_loop_with_dispatch_cx(
cx,
dispatch_cx,
move |cx, worker_failed| {
if receive_fatal_output.load(Ordering::Acquire) {
worker_failed.store(true, Ordering::Release);
return Err(TransportError::Io(std::io::Error::other(
"notification output failed",
)));
}
let result = transport.recv(cx);
if receive_fatal_output.load(Ordering::Acquire) {
// Generic non-Unix reads cannot be interrupted once
// blocked, but the fatal latch must still dominate a
// concurrently completed frame or clean EOF.
worker_failed.store(true, Ordering::Release);
Err(TransportError::Io(std::io::Error::other(
"notification output failed",
)))
} else {
result
}
},
move |cx, message| {
if send_fatal_output.load(Ordering::Acquire) || cx.checkpoint().is_err() {
return Err(TransportError::Cancelled);
}
let bytes = match message {
JsonRpcMessage::Request(request) => codec.encode_request(request)?,
JsonRpcMessage::Response(response) => codec.encode_response(response)?,
};
stdout.write_all_unchecked(&bytes)?;
stdout.flush_unchecked()?;
Ok(())
},
notification_sender,
Arc::clone(&fatal_notification_output),
"stdio",
)
}
}
/// Runs the server on a custom transport with a provided Cx.
///
/// This allows integration with a real asupersync runtime.
///
/// ```compile_fail
/// use fastmcp_server::Server;
/// use fastmcp_transport::Transport;
///
/// fn omitted_context<T>(server: Server, transport: T)
/// where
/// T: Transport + Send + 'static,
/// {
/// server.run_transport(transport);
/// }
/// ```
pub fn run_transport_with_cx<T>(self, cx: &Cx, transport: T) -> !
where
T: Transport + Send + 'static,
{
self.run_transport_with_label(cx, transport, "custom")
}
fn run_transport_with_label<T>(self, cx: &Cx, transport: T, label: &'static str) -> !
where
T: Transport + Send + 'static,
{
self.init_rich_logging();
let notification_failure = Arc::new(AtomicBool::new(false));
let shared =
SharedTransport::with_output_failure(transport, Arc::clone(¬ification_failure));
let notification_sender = create_transport_notification_sender(
shared.clone(),
cx.clone(),
Arc::clone(¬ification_failure),
);
let shared_recv = shared.clone();
let shared_send = shared.clone();
#[cfg(feature = "legacy-2024-11-05")]
let run_result = self.run_loop_returning(
cx,
move |cx| shared_recv.recv(cx),
move |cx, message| shared_send.send(cx, message),
notification_sender,
Some(notification_failure),
label,
);
#[cfg(not(feature = "legacy-2024-11-05"))]
let run_result = match Arc::new(self).run_loop_pump_with_policy(
cx,
cx,
move |cx, _worker_failed| shared_recv.recv(cx),
move |cx, message| shared_send.send(cx, message),
notification_sender,
label,
true,
Some(notification_failure),
true,
true,
None,
None,
None,
PumpIoMode::Unsplit,
) {
0 => Ok(()),
_ => Err(server_run_error(
"transport",
"pump_failure",
"Server transport loop failed",
)),
};
let close_result = shared
.close(cx)
.map_err(|error| transport_run_error("close", &error));
let result = match (run_result, close_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
(Err(run_error), Err(close_error)) => {
Err(combined_run_and_close_error(run_error, close_error))
}
};
let exit_code = match result {
Ok(()) => 0,
Err(error) => {
error!(
target: targets::TRANSPORT,
"Server transport terminated with error: {error:?}"
);
1
}
};
std::process::exit(exit_code)
}
/// Runs the server on a custom transport until clean closure, cancellation,
/// or failure.
///
/// Unlike [`run_transport_with_cx`](Self::run_transport_with_cx), this does not call
/// `std::process::exit` on shutdown. This is useful for tests and embedding where you need
/// the server loop to be joinable. Clean EOF and cancellation return
/// `Ok(())` when request cleanup quiesces; bounded non-quiescent cleanup,
/// startup, protocol, fatal receive, and fatal send failures return an
/// error.
/// Transport failures carry fixed `stage` and `kind` fields in the error
/// data without copying peer-controlled I/O or codec text.
///
/// This entry point and [`Transport::recv`] are synchronous. When the
/// caller uses an async runtime, run this loop through [`Cx::spawn_blocking`],
/// pass the closure's child context here, and await its task handle from
/// the caller. Calling the loop directly on a cooperative runtime worker
/// can prevent that same runtime from polling the request children.
///
/// An unsplit transport cannot write while its `recv` blocks. Output that
/// request workers produce in that window (subscription acknowledgements,
/// notifications, responses) is queued in order and written as soon as
/// `recv` returns. Use [`Server::run_split_transport_returning_with_cx`]
/// when that output must not wait for the next inbound frame.
///
/// # Errors
///
/// Returns an error when startup fails, a fatal receive/protocol failure is
/// observed, request cleanup remains non-quiescent at its finite bound,
/// the server cannot send a required response, or transport close fails.
/// Simultaneous operation, cleanup, and close failures retain structured
/// nested evidence under `data.run` and `data.close`.
///
/// ```compile_fail
/// use fastmcp_server::Server;
/// use fastmcp_transport::Transport;
///
/// fn omitted_context<T>(server: Server, transport: T)
/// where
/// T: Transport + Send + 'static,
/// {
/// server.run_transport_returning(transport);
/// }
/// ```
pub fn run_transport_returning_with_cx<T>(self, cx: &Cx, transport: T) -> McpResult<()>
where
T: Transport + Send + 'static,
{
self.init_rich_logging();
let notification_failure = Arc::new(AtomicBool::new(false));
let shared =
SharedTransport::with_output_failure(transport, Arc::clone(¬ification_failure));
let notification_sender = create_transport_notification_sender(
shared.clone(),
cx.clone(),
Arc::clone(¬ification_failure),
);
let shared_recv = shared.clone();
let shared_send = shared.clone();
let run_result = self.run_loop_returning(
cx,
move |cx| shared_recv.recv(cx),
move |cx, message| shared_send.send(cx, message),
notification_sender,
Some(notification_failure),
"custom",
);
let close_result = shared
.close(cx)
.map_err(|error| transport_run_error("close", &error));
match (run_result, close_result) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
(Err(run_error), Err(close_error)) => {
Err(combined_run_and_close_error(run_error, close_error))
}
}
}
/// Runs independently owned receive and send halves under the dual-era dispatcher.
///
/// Modern requests receive bounded request-owned child contexts and may
/// progress while the receive half blocks for another frame. Exact
/// MCP 2024-11-05 traffic remains serialized through its lifecycle adapter.
/// Use this entry point for genuinely full-duplex transports; an unsplit
/// [`Transport`] cannot safely promise concurrent receive and response I/O.
///
/// Shutdown retains ownership of the dispatch worker. If an arbitrary
/// handler ignores cancellation past the bounded shutdown deadline, this
/// returning API continues waiting for that worker to quiesce instead of
/// orphaning it merely to return. Once it does quiesce, the call reports
/// the timeout failure and then performs the normal owned cleanup.
pub fn run_split_transport_returning_with_cx<R, S>(
self,
cx: &Cx,
recv_half: R,
send_half: S,
) -> McpResult<()>
where
R: TransportRecvHalf + Send + 'static,
S: TransportSendHalf + 'static,
{
self.run_split_transport_returning_with_dispatch_cx(cx, cx, recv_half, send_half)
}
/// Runs independently owned receive and send halves with an explicit
/// caller-owned context for modern request dispatch.
///
/// A split receive pump may itself be placed on the caller's blocking
/// pool. In that arrangement `pump_cx` owns ingress cancellation, while
/// `dispatch_cx` must remain the runtime context that owns the blocking
/// pool used by concurrent modern request children.
pub fn run_split_transport_returning_with_dispatch_cx<R, S>(
self,
pump_cx: &Cx,
dispatch_cx: &Cx,
recv_half: R,
send_half: S,
) -> McpResult<()>
where
R: TransportRecvHalf + Send + 'static,
S: TransportSendHalf + 'static,
{
self.init_rich_logging();
let recv_half = SharedRecvHalf::new(recv_half);
let send_half = SharedSendHalf::new(send_half);
let notification_failure = Arc::new(AtomicBool::new(false));
let notification_sender = create_split_transport_notification_sender(
send_half.clone(),
pump_cx.clone(),
Arc::clone(¬ification_failure),
);
let recv_for_run = recv_half.clone();
let send_for_run = send_half.clone();
let run_result = self.run_loop_returning_with_dispatch_cx(
pump_cx,
dispatch_cx,
move |cx, _worker_failed| recv_for_run.recv(cx),
move |cx, message| send_for_run.send(cx, message),
notification_sender,
Some(notification_failure),
"split-custom",
);
let recv_close = recv_half
.close(pump_cx)
.map_err(|error| transport_run_error("receive_close", &error));
let send_close = send_half
.close(pump_cx)
.map_err(|error| transport_run_error("send_close", &error));
combine_split_transport_results(run_result, recv_close, send_close)
}
#[cfg(feature = "websocket")]
fn run_split_transport_returning_shared_with_dispatch_cx<R, S>(
self: Arc<Self>,
pump_cx: &Cx,
dispatch_cx: &Cx,
recv_half: R,
send_half: S,
transport_authorization: TransportAuthorization,
auth_receipt: AuthDispatchCustody,
websocket_connection_generation: u64,
) -> McpResult<()>
where
R: TransportRecvHalf + Send + 'static,
S: TransportSendHalf + 'static,
{
let recv_half = SharedRecvHalf::new(recv_half);
let send_half = SharedSendHalf::new(send_half);
let notification_failure = Arc::new(AtomicBool::new(false));
let notification_sender = create_split_transport_notification_sender(
send_half.clone(),
pump_cx.clone(),
Arc::clone(¬ification_failure),
);
let recv_for_run = recv_half.clone();
let send_for_run = send_half.clone();
let run_result = self.run_loop_returning_shared_with_dispatch_cx(
pump_cx,
dispatch_cx,
move |cx, _worker_failed| recv_for_run.recv(cx),
move |cx, message| send_for_run.send(cx, message),
notification_sender,
Some(notification_failure),
"websocket",
transport_authorization,
auth_receipt,
websocket_connection_generation,
);
let recv_close = recv_half
.close(pump_cx)
.map_err(|error| transport_run_error("receive_close", &error));
let send_close = send_half
.close(pump_cx)
.map_err(|error| transport_run_error("send_close", &error));
combine_split_transport_results(run_result, recv_close, send_close)
}
#[cfg(feature = "legacy-2024-11-05")]
async fn run_split_transport_with_label<R, S>(
self,
cx: &Cx,
recv_half: R,
send_half: S,
transport_label: &'static str,
) -> !
where
R: TransportRecvHalf + Send + 'static,
S: TransportSendHalf + 'static,
{
let dispatch_cx = cx.clone();
let mut pump = match cx.spawn_blocking(move |pump_cx| {
self.run_split_transport_returning_with_dispatch_cx(
&pump_cx,
&dispatch_cx,
recv_half,
send_half,
)
}) {
Ok(pump) => pump,
Err(error) => {
error!(
target: targets::SERVER,
"Could not admit the {transport_label} split transport pump into the caller runtime: {error}"
);
std::process::exit(1);
}
};
let exit_code = match pump.join(cx).await {
Ok(Ok(())) => 0,
Ok(Err(error)) => {
error!(
target: targets::SERVER,
"{transport_label} split transport pump failed: {error}"
);
1
}
Err(error) => {
error!(
target: targets::SERVER,
"Caller-owned {transport_label} split transport pump ended without a final status: {error:?}"
);
1
}
};
std::process::exit(exit_code)
}
/// Runs the server using SSE transport with a provided Cx.
///
/// ```compile_fail
/// use fastmcp_server::Server;
/// use std::io::Write;
///
/// fn omitted_context<W, R>(server: Server, writer: W, requests: R)
/// where
/// W: Write + Send + 'static,
/// R: Iterator<Item = fastmcp_protocol::JsonRpcRequest> + Send + 'static,
/// {
/// server.run_sse(writer, requests, "http://localhost/events");
/// }
/// ```
#[cfg(feature = "legacy-2024-11-05")]
pub async fn run_sse_with_cx<W, R>(
self,
cx: &Cx,
writer: W,
request_source: R,
endpoint_url: impl Into<String>,
) -> !
where
W: Write + Send + 'static,
R: Iterator<Item = JsonRpcRequest> + Send + 'static,
{
let (recv_half, send_half) =
SseServerTransport::new(writer, request_source, endpoint_url).into_split();
self.run_split_transport_with_label(cx, recv_half, send_half, "sse")
.await
}
// =========================================================================
// HTTP Server — caller-owned asupersync HTTP/1.1 lifecycle
// =========================================================================
/// Runs the turnkey dual-era HTTP server on a caller-owned [`Cx`].
///
/// This is an async lifecycle rather than a runtime constructor: callers
/// drive it from their existing asupersync region and retain cancellation,
/// deadlines, and task ownership throughout socket acceptance.
pub async fn run_http(self, cx: &Cx, addr: impl Into<String>) -> McpResult<HttpServerShutdown> {
self.serve_http(cx, addr).await
}
/// Named compatibility entry point for callers that already pass a
/// context. It is exactly [`Self::run_http`] and never creates a runtime.
pub async fn run_http_with_cx(
self,
cx: &Cx,
addr: impl Into<String>,
) -> McpResult<HttpServerShutdown> {
self.serve_http(cx, addr).await
}
/// Returning form of [`Self::run_http`] for embedders that select the
/// shutdown boundary themselves.
pub async fn run_http_returning(
self,
cx: &Cx,
addr: impl Into<String>,
) -> McpResult<HttpServerShutdown> {
self.serve_http(cx, addr).await
}
/// Returning form with an explicit caller-owned context.
pub async fn run_http_returning_with_cx(
self,
cx: &Cx,
addr: impl Into<String>,
) -> McpResult<HttpServerShutdown> {
self.serve_http(cx, addr).await
}
fn configured_http_request_handler(&self) -> HttpRequestHandler {
HttpRequestHandler::with_config(self.http_config.handler_config.clone())
}
/// Runs the startup lifecycle hook, if configured.
///
/// Returns `true` if startup succeeded (or no hook was configured),
/// `false` if the hook returned an error.
pub(crate) fn run_startup_hook(&self) -> bool {
let hook = {
let mut guard = self.lifespan.lock().unwrap_or_else(|poisoned| {
error!(target: targets::SERVER, "lifespan lock poisoned in run_startup_hook, recovering");
poisoned.into_inner()
});
guard.as_mut().and_then(|h| h.on_startup.take())
};
if let Some(hook) = hook {
debug!(target: targets::SERVER, "Running startup hook");
match catch_extension_unwind(hook) {
Ok(Ok(())) => {
debug!(target: targets::SERVER, "Startup hook completed successfully");
true
}
Ok(Err(e)) => {
error!(target: targets::SERVER, "Startup hook failed: {}", e);
false
}
Err(_payload) => {
let _ = extension_panic_error("startup_hook");
false
}
}
} else {
true
}
}
/// Runs the shutdown lifecycle hook, if configured.
pub(crate) fn run_shutdown_hook(&self) {
let hook = {
let mut guard = self.lifespan.lock().unwrap_or_else(|poisoned| {
error!(target: targets::SERVER, "lifespan lock poisoned in run_shutdown_hook, recovering");
poisoned.into_inner()
});
guard.as_mut().and_then(|h| h.on_shutdown.take())
};
if let Some(hook) = hook {
debug!(target: targets::SERVER, "Running shutdown hook");
if catch_extension_unwind(hook).is_err() {
let _ = extension_panic_error("shutdown_hook");
} else {
debug!(target: targets::SERVER, "Shutdown hook completed");
}
}
}
/// Performs graceful shutdown without exiting the process.
///
/// This is intended for embedding/testing scenarios where the server loop is
/// running on a thread and the caller wants to `join()` it.
fn graceful_shutdown_returning(&self) -> ShutdownCleanupOutcome {
// Preserve the same graceful subscription completion semantics for
// embedded servers as for the process-exiting lifecycle.
let _ = self.terminate_subscription_streams_for_shutdown();
let cleanup = self.cancel_active_requests(CancelKind::Shutdown, true);
if matches!(cleanup, ShutdownCleanupOutcome::Quiescent) {
self.run_shutdown_hook();
} else {
error!(
target: targets::SESSION,
"Skipping shutdown hook because active request cleanup did not quiesce"
);
}
if let Some(ref stats) = self.stats {
stats.connection_closed();
}
cleanup
}
/// Runs a continuous receive pump. Exact-2024 frames retain the one
/// lifecycle-preserving worker; modern frames use bounded children of the
/// caller context and may progress independently.
fn run_loop_with_dispatch_cx<R, S>(
self,
pump_cx: &Cx,
dispatch_cx: &Cx,
recv: R,
send: S,
notification_sender: NotificationSender,
connection_failure: Arc<AtomicBool>,
transport_label: &'static str,
) -> i32
where
R: FnMut(&Cx, &AtomicBool) -> Result<JsonRpcMessage, TransportError>,
S: FnMut(&Cx, &JsonRpcMessage) -> Result<(), TransportError> + Send + Sync + 'static,
{
Arc::new(self).run_loop_pump_with_policy(
pump_cx,
dispatch_cx,
recv,
send,
notification_sender,
transport_label,
true,
Some(connection_failure),
true,
true,
None,
None,
None,
PumpIoMode::Split,
)
}
/// Returning counterpart of [`Self::run_loop_with_dispatch_cx`].
///
/// Clean closure or cancellation returns `Ok(())`; any failing pump status
/// is preserved as a local server-loop error. A receive implementation
/// that can block must include the supplied worker-failure flag in its
/// readiness/stop predicate. Modern requests establish a fresh stateless
/// request scope; exact legacy traffic retains its isolated session path.
/// Unlike the process-exiting stdio entry point, this returning helper
/// never detaches a non-quiescent handler: after logging the bounded
/// shutdown deadline it waits without a second deadline for the owned
/// worker before running lifecycle hooks or returning to the caller.
fn run_loop_returning_with_dispatch_cx<R, S>(
self,
pump_cx: &Cx,
dispatch_cx: &Cx,
mut recv: R,
send: S,
notification_sender: NotificationSender,
connection_failure: Option<Arc<AtomicBool>>,
transport_label: &'static str,
) -> McpResult<()>
where
R: FnMut(&Cx, &AtomicBool) -> Result<JsonRpcMessage, TransportError>,
S: FnMut(&Cx, &JsonRpcMessage) -> Result<(), TransportError> + Send + Sync + 'static,
{
match Arc::new(self).run_loop_pump_with_policy(
pump_cx,
dispatch_cx,
move |cx, worker_failed| recv(cx, worker_failed),
send,
notification_sender,
transport_label,
false,
connection_failure,
true,
true,
None,
None,
None,
PumpIoMode::Split,
) {
0 => Ok(()),
_ => Err(server_run_error(
"transport",
"pump_failure",
"Server transport loop failed",
)),
}
}
#[cfg(feature = "websocket")]
fn run_loop_returning_shared_with_dispatch_cx<R, S>(
self: Arc<Self>,
pump_cx: &Cx,
dispatch_cx: &Cx,
mut recv: R,
send: S,
notification_sender: NotificationSender,
connection_failure: Option<Arc<AtomicBool>>,
transport_label: &'static str,
transport_authorization: TransportAuthorization,
auth_receipt: AuthDispatchCustody,
websocket_connection_generation: u64,
) -> McpResult<()>
where
R: FnMut(&Cx, &AtomicBool) -> Result<JsonRpcMessage, TransportError>,
S: FnMut(&Cx, &JsonRpcMessage) -> Result<(), TransportError> + Send + Sync + 'static,
{
match self.run_loop_pump_with_policy(
pump_cx,
dispatch_cx,
move |cx, worker_failed| recv(cx, worker_failed),
send,
notification_sender,
transport_label,
false,
connection_failure,
true,
false,
Some(transport_authorization),
Some(auth_receipt),
Some(websocket_connection_generation),
PumpIoMode::Split,
) {
0 => Ok(()),
_ => Err(server_run_error(
"transport",
"pump_failure",
"Server transport loop failed",
)),
}
}
/// Feature-off stdio runner: only final-era envelopes can reach dispatch.
///
/// Uses the shared bounded request admission and retains each child until
/// it settles, without compiling a legacy adapter or reverse registry.
/// The dual-era implementation below remains compiled only with the dated
/// feature.
#[cfg(not(feature = "legacy-2024-11-05"))]
#[allow(clippy::too_many_arguments)]
fn run_loop_pump_with_policy<R, S>(
self: Arc<Self>,
cx: &Cx,
dispatch_cx: &Cx,
mut recv: R,
send: S,
_notification_sender: NotificationSender,
transport_label: &'static str,
detach_on_worker_timeout: bool,
connection_failure: Option<Arc<AtomicBool>>,
enforce_runtime_era: bool,
owns_server_lifecycle: bool,
transport_authorization: Option<TransportAuthorization>,
auth_receipt: Option<AuthDispatchCustody>,
websocket_connection_generation: Option<u64>,
io_mode: PumpIoMode,
) -> i32
where
R: FnMut(&Cx, &AtomicBool) -> Result<JsonRpcMessage, TransportError>,
S: FnMut(&Cx, &JsonRpcMessage) -> Result<(), TransportError> + Send + Sync + 'static,
{
let server = self;
let auth_custody_generation = websocket_connection_generation;
if let Some(stats) = &server.stats {
stats.connection_opened();
}
if server.console_config.show_banner && !banner_suppressed() {
server.render_startup_banner(transport_label);
}
if owns_server_lifecycle && !server.run_startup_hook() {
server.graceful_shutdown_returning();
return 1;
}
#[cfg(feature = "tasks")]
let hosted_task_service =
match server.start_hosted_task_service_blocking(dispatch_cx, owns_server_lifecycle) {
Ok(hosted) => hosted,
Err(error) => {
error!(target: targets::SERVER, "Hosted Task service startup failed: {error}");
server.graceful_shutdown_returning();
return 1;
}
};
let modern_connection = ModernConnection::new();
let send = Arc::new(Mutex::new(send));
let mut classifier = StdioEraClassifier::new(runtime_stdio_policy(server.protocol_policy));
#[cfg(feature = "tasks")]
let worker_failed = hosted_task_service.as_ref().map_or_else(
|| Arc::new(AtomicBool::new(false)),
tasks::HostedTaskService::failure_signal,
);
#[cfg(not(feature = "tasks"))]
let worker_failed = Arc::new(AtomicBool::new(false));
let queue = Arc::new(DispatchQueueState::default());
let mut children: Vec<asupersync::runtime::TaskHandle<()>> = Vec::new();
let mut blocking_children: Vec<BlockingTaskGuard> = Vec::new();
// A synchronous embedding can call this loop on the runtime's only
// async worker. It cannot join a task scheduled on that same worker.
// Use the existing bounded blocking bridge in that arrangement; the
// split/stdio pump supplies a distinct dispatch Cx and uses async
// children instead. Both paths retain the same admission ownership.
let synchronous_dispatch =
io_mode == PumpIoMode::Split && cx.task_id() == dispatch_cx.task_id();
let mut negotiated_era = None;
let mut exit_code = 0;
let mut drain_responses = false;
loop {
blocking_children.retain(|child| !child.0.is_done());
// Retain ownership until completion, but do not accumulate handles
// over the lifetime of a long-running connection.
for index in (0..children.len()).rev() {
if children[index].is_finished() {
let mut child = children.swap_remove(index);
match poll_on_cx(cx, child.join(cx)) {
Ok(()) | Err(asupersync::runtime::JoinError::Cancelled(_)) => {}
Err(_) => worker_failed.store(true, Ordering::Release),
}
}
}
if cx.checkpoint().is_err()
|| worker_failed.load(Ordering::Acquire)
|| connection_failure
.as_ref()
.is_some_and(|failed| failed.load(Ordering::Acquire))
{
break;
}
let message = match recv(cx, &worker_failed) {
Ok(message) => message,
Err(TransportError::Closed) => {
drain_responses = true;
break;
}
Err(TransportError::Cancelled) => break,
Err(_) => {
exit_code = 1;
break;
}
};
// A request child can fail its write while ingress is blocked.
// Recheck before authenticating or admitting the returned frame.
if worker_failed.load(Ordering::Acquire)
|| connection_failure
.as_ref()
.is_some_and(|failed| failed.load(Ordering::Acquire))
{
exit_code = 1;
break;
}
let JsonRpcMessage::Request(request) = message else {
continue;
};
if request.validate().is_err() {
exit_code = 1;
break;
}
if enforce_runtime_era && negotiated_era.is_none() {
match classify_initial_stdio_envelope(
&mut classifier,
&JsonRpcMessage::Request(request.clone()),
) {
Ok(ProtocolEra::Modern2026) => negotiated_era = Some(ProtocolEra::Modern2026),
Err(decision) if opening_rejection_selected_modern(&decision) => {
negotiated_era = Some(ProtocolEra::Modern2026);
}
Ok(ProtocolEra::Legacy2024) | Err(_) => {
if let Some(response) = protocol_era_refusal(&request) {
let _ = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
);
}
exit_code = i32::from(owns_server_lifecycle);
drain_responses = true;
break;
}
}
}
if negotiated_era == Some(ProtocolEra::Modern2026)
&& modern_protocol_version(&request) != Some(MODERN_PROTOCOL_VERSION)
{
// Stateless per-request admission: answer this request and
// keep serving the stream.
if let Some(response) = modern_request_version_refusal(&request)
&& send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
.is_err()
{
exit_code = 1;
break;
}
continue;
}
if negotiated_era != Some(ProtocolEra::Modern2026) {
if let Some(response) = protocol_era_refusal(&request) {
let _ = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
);
}
exit_code = i32::from(owns_server_lifecycle);
drain_responses = true;
break;
}
if admit_final_client_notification_ingress(&request).is_err() {
continue;
}
// The request context is the DISPATCH runtime's, not the receive
// pump's: under the stdio arrangement the pump is a blocking child
// and cannot drive an async request child (GitHub #65).
let inbound = InboundRequestContext::with_modern_connection_and_transport_authorization(
dispatch_cx.clone(),
request_id_to_u64(request.id.as_ref()),
InboundRequestTransport::Stdio,
&modern_connection,
transport_authorization.clone().unwrap_or_default(),
);
if request.id.is_none() && request.method == "notifications/cancelled" {
let mut request = request;
if let Ok(cancellation) = server.authenticate_modern_cancelled_control(
&inbound,
&mut request,
auth_receipt.as_ref(),
auth_custody_generation,
) {
// Cancellation and final output elect their winner under
// the same writer fence. An unrelated ID changes nothing.
let _writer = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
queue.cancel_reserved(Self::cancellation_wire_request_id(&cancellation));
}
continue;
}
let admission = queue.admit_modern_request(&request, Arc::clone(&worker_failed));
let mut reservation = match admission {
Ok(reservation) => reservation,
Err(error) => {
if let Some(id) = request.id {
let response = JsonRpcResponse::error(Some(id), error);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
.is_err()
{
exit_code = 1;
break;
}
}
continue;
}
};
// A listen does not finish until it is terminated, so on an
// unsplit transport it cannot run inline: the pump would stop
// reading and never see the cancellation that ends it (bd-4crkf).
let unsplit_listen =
io_mode == PumpIoMode::Unsplit && request.method == SUBSCRIPTIONS_LISTEN;
let blocking_permit =
(synchronous_dispatch || unsplit_listen).then(try_reserve_blocking_dispatch);
if matches!(blocking_permit, Some(None)) {
if let Some(id) = request.id {
let response = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: RESOURCE_EXHAUSTED_ERROR_CODE.into(),
message: DISPATCH_QUEUE_CAPACITY_MESSAGE.to_owned(),
data: None,
},
);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
.is_err()
{
exit_code = 1;
break;
}
}
continue;
}
// Establish ordinary-request ownership before reading the next
// frame. A following cancellation must not depend on whether the
// runtime has polled this child yet. Dispatch consumes the exact
// private receipt, so the provider is evaluated only once.
let task_auth_receipt = match server.admit_modern_pump_authentication(
&inbound,
&request,
auth_receipt.as_ref(),
auth_custody_generation,
) {
Ok(receipt) => receipt,
Err(error) => {
if let Some(id) = request.id {
let response = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: error.code.into(),
message: error.message,
data: error.data,
},
);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
.is_err()
{
exit_code = 1;
break;
}
}
continue;
}
};
reservation.begin();
let request_server = Arc::clone(&server);
let request_send = Arc::clone(&send);
let listen_opened = unsplit_listen.then(|| Arc::new(UnsplitListenOpened::default()));
let listen_opened_by_dispatch = listen_opened.clone();
let dispatch = move |request_cx: Cx| async move {
let mut reservation = reservation;
let cancellation = reservation.cancellation();
let subscription_request = request.method == SUBSCRIPTIONS_LISTEN;
let inbound = inbound.with_cx(request_cx.clone());
let notification_send = Arc::clone(&request_send);
let notification_cx = request_cx.clone();
let notification_cancellation = cancellation.clone();
let notification_failed = Arc::clone(&reservation.failed);
let notification_opened = listen_opened_by_dispatch;
#[cfg(test)]
let notification_request_id = request_id_to_u64(request.id.as_ref());
let committed_notifications: NotificationSender = Arc::new(move |notification| {
#[cfg(test)]
wait_for_stdio_progress_commit_fence(notification_request_id, ¬ification);
let acknowledgement = notification.method
== fastmcp_protocol::methods::NOTIFICATIONS_SUBSCRIPTIONS_ACKNOWLEDGED;
let mut writer = notification_send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !notification_cancellation.is_cancel_requested()
&& writer(¬ification_cx, &JsonRpcMessage::Request(notification)).is_err()
{
notification_failed.store(true, Ordering::Release);
notification_cancellation.cancel();
}
drop(writer);
if acknowledgement && let Some(opened) = ¬ification_opened {
opened.fire();
}
});
let response = match reservation.queue.begin_modern_dispatch(request.id.as_ref()) {
ModernDispatchStart::Stopping => None,
ModernDispatchStart::Cancelled => request.id.clone().map(|id| {
JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: McpErrorCode::RequestCancelled.into(),
message: "Request cancelled before dispatch".to_owned(),
data: None,
},
)
}),
ModernDispatchStart::Ready => {
Arc::clone(&request_server)
.dispatch_with_protocol_policy_owned(
request_server.protocol_policy,
&inbound,
request,
None,
Some(task_auth_receipt),
auth_custody_generation,
None,
cancellation.clone(),
None,
committed_notifications,
)
.await
}
};
if let Some(mut response) = response {
let mut writer = request_send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !(subscription_request && final_subscription_completion_response(&response)
|| cancellation.begin_finalization())
{
response = JsonRpcResponse::error(
response.id,
JsonRpcError {
code: McpErrorCode::RequestCancelled.into(),
message: "Request cancelled before response finalization"
.to_owned(),
data: None,
},
);
}
if writer(&request_cx, &JsonRpcMessage::Response(response)).is_err() {
return;
}
}
reservation.disarm_failure();
};
if io_mode == PumpIoMode::Unsplit && !unsplit_listen {
// An unsplit Transport holds one mutex during recv. Finish
// its response before receiving again, otherwise that read
// can lock out the response the peer is waiting for.
poll_on_cx(dispatch_cx, dispatch(dispatch_cx.clone()));
continue;
}
if let Some(Some(permit)) = blocking_permit {
let request_cx = dispatch_cx.clone();
// Fires when the task finishes or is dropped unrun, so the
// pump never waits on a listen that ended before it
// acknowledged.
let listen_finished = listen_opened.clone().map(UnsplitListenOpenedOnDrop);
blocking_children.push(BlockingTaskGuard(blocking_dispatch_pool().spawn(
move || {
let _permit = permit;
let _listen_finished = listen_finished;
// A pool thread is never the async driver (bd-6rfrg).
let _lane = fastmcp_core::runtime::enter_blocking_lane();
poll_on_cx(&request_cx, dispatch(request_cx.clone()));
},
)));
if let Some(opened) = &listen_opened {
// Output written while `recv` blocks waits for that recv
// to return, so let the listen's acknowledgement (or its
// early answer) go out before reading again.
opened.wait(cx, UNSPLIT_LISTEN_OPEN_TIMEOUT);
}
continue;
}
let submitted = dispatch_cx.spawn(dispatch);
match submitted {
Ok(child) => children.push(child),
Err(_) => {
exit_code = 1;
break;
}
}
}
if owns_server_lifecycle {
let _ = server.terminate_subscription_streams_for_shutdown();
}
queue.cancel_uncorrelated_modern_children();
if drain_responses
&& !queue.wait_for_correlated_response_drain(DISPATCH_WORKER_SHUTDOWN_TIMEOUT)
{
exit_code = 1;
}
queue.stop();
modern_connection.disconnect();
if owns_server_lifecycle {
server.cancel_active_requests(CancelKind::Shutdown, false);
}
let mut quiescent = queue.wait_for_modern_drain(DISPATCH_WORKER_SHUTDOWN_TIMEOUT);
if !quiescent {
exit_code = 1;
if !detach_on_worker_timeout {
queue.wait_for_modern_drain_unbounded();
quiescent = true;
}
}
#[cfg(feature = "tasks")]
let task_service_succeeded =
Self::settle_hosted_task_service_blocking(hosted_task_service).is_ok();
#[cfg(not(feature = "tasks"))]
let task_service_succeeded = true;
if !task_service_succeeded {
error!(target: targets::SERVER, "Hosted Task service did not settle successfully");
exit_code = 1;
}
if quiescent {
for child in blocking_children {
child.0.wait();
}
for mut child in children {
match poll_on_cx(cx, child.join(cx)) {
Ok(()) | Err(asupersync::runtime::JoinError::Cancelled(_)) => {}
Err(_) => exit_code = 1,
}
}
if owns_server_lifecycle {
server.run_shutdown_hook();
}
}
if worker_failed.load(Ordering::Acquire)
|| connection_failure
.as_ref()
.is_some_and(|failed| failed.load(Ordering::Acquire))
{
exit_code = 1;
}
if let Some(stats) = &server.stats {
stats.connection_closed();
}
exit_code
}
#[cfg(feature = "legacy-2024-11-05")]
#[allow(clippy::too_many_lines)]
fn run_loop_pump_with_policy<R, S>(
self: Arc<Self>,
cx: &Cx,
dispatch_cx: &Cx,
mut recv: R,
send: S,
notification_sender: NotificationSender,
transport_label: &'static str,
detach_on_worker_timeout: bool,
connection_failure: Option<Arc<AtomicBool>>,
enforce_runtime_era: bool,
owns_server_lifecycle: bool,
transport_authorization: Option<TransportAuthorization>,
auth_receipt: Option<AuthDispatchCustody>,
websocket_connection_generation: Option<u64>,
_io_mode: PumpIoMode,
) -> i32
where
R: FnMut(&Cx, &AtomicBool) -> Result<JsonRpcMessage, TransportError>,
S: FnMut(&Cx, &JsonRpcMessage) -> Result<(), TransportError> + Send + Sync + 'static,
{
if let Some(ref stats) = self.stats {
stats.connection_opened();
}
let auth_custody_generation = websocket_connection_generation;
if self.console_config.show_banner && !banner_suppressed() {
self.render_startup_banner(transport_label);
}
if owns_server_lifecycle && !self.run_startup_hook() {
error!(target: targets::SERVER, "Startup hook failed, stopping");
self.graceful_shutdown_returning();
return 1;
}
#[cfg(feature = "tasks")]
let hosted_task_service =
match self.start_hosted_task_service_blocking(dispatch_cx, owns_server_lifecycle) {
Ok(hosted) => hosted,
Err(error) => {
error!(target: targets::SERVER, "Hosted Task service startup failed: {error}");
self.graceful_shutdown_returning();
return 1;
}
};
let traffic_renderer = self.configured_traffic_renderer();
let server = self;
let session = Session::new(server.info.clone(), server.capabilities.clone());
let session_id = session.id();
let session_principal = session.principal_binding();
let send = Arc::new(Mutex::new(send));
let queue_state = Arc::new(DispatchQueueState::default());
#[cfg(feature = "tasks")]
let worker_failed = hosted_task_service.as_ref().map_or_else(
|| Arc::new(AtomicBool::new(false)),
tasks::HostedTaskService::failure_signal,
);
#[cfg(not(feature = "tasks"))]
let worker_failed = Arc::new(AtomicBool::new(false));
let pending_requests = Arc::new(
PendingRequests::with_max_in_flight_for_exact_legacy(
server.max_bidirectional_requests_per_connection,
)
.expect("ServerBuilder validates the bidirectional request limit"),
);
let legacy_request_sender = RequestSender::new(Arc::clone(&pending_requests), {
let send = Arc::clone(&send);
let request_cx = dispatch_cx.clone();
Arc::new(move |message| {
let mut send_guard = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
send_guard(&request_cx, message).map_err(|error| error.to_string())
})
});
let legacy_runtime = LiveLegacy2024ConnectionRuntime::new(
SessionState::new(),
notification_sender,
Some(legacy_request_sender),
server.logging.level,
);
let modern_connection = ModernConnection::new();
let worker_modern_connection = modern_connection.request_context();
let dispatch_modern_connection = modern_connection.request_context();
let (dispatch_sender, mut dispatch_receiver) =
asupersync_mpsc::channel::<QueuedDispatchMessage>(MAX_DISPATCH_QUEUE_DEPTH);
let worker_server = Arc::clone(&server);
let worker_send = Arc::clone(&send);
let worker_queue_state = Arc::clone(&queue_state);
let worker_failed_flag = Arc::clone(&worker_failed);
let worker_cx = cx.clone();
let worker_renderer = traffic_renderer.clone();
let worker_session_principal = session_principal.clone();
let worker_legacy_runtime = legacy_runtime.clone();
let worker_transport_authorization = transport_authorization.clone().unwrap_or_default();
let worker_auth_receipt = auth_receipt.clone();
let worker_auth_custody_generation = auth_custody_generation;
let (worker_completion_sender, worker_completion_receiver) = std::sync::mpsc::channel();
let worker = std::thread::spawn(move || {
// This dedicated thread is never the async driver: the receive
// pump routes matched reverse responses into `pending_requests` on
// its own thread. A handler that bridges sampling, elicitation or
// roots here therefore cannot starve its own completion, so declare
// the lane or bd-6rfrg's detection rejects a working call.
let _lane = fastmcp_core::runtime::enter_blocking_lane();
let _completion = DispatchWorkerCompletionSignal(Some(worker_completion_sender));
let mut failure_latch = DispatchWorkerFailureLatch::new(
Arc::clone(&worker_failed_flag),
Arc::clone(&worker_queue_state),
);
let mut session = session;
let legacy_binding = runtime_legacy_binding(session.id());
let mut legacy_adapter: Option<
Legacy2024ServerAdapter<LiveLegacy2024RuntimeHandler<'_>>,
> = None;
let legacy_active_request = Arc::new(Mutex::new(None));
let mut clean_exit = false;
loop {
if worker_queue_state.is_stopping() {
clean_exit = true;
break;
}
let queued_message = match dispatch_receiver.try_recv() {
Ok(request) => request,
Err(asupersync_mpsc::RecvError::Empty) => {
if worker_queue_state.is_stopping() {
clean_exit = true;
break;
}
std::thread::sleep(Duration::from_millis(1));
continue;
}
Err(
asupersync_mpsc::RecvError::Disconnected
| asupersync_mpsc::RecvError::Cancelled,
) => {
clean_exit = worker_queue_state.is_stopping();
break;
}
};
let queued_request = match queued_message {
QueuedDispatchMessage::LegacyResponse(response) => {
let Some(adapter) = legacy_adapter.as_mut() else {
break;
};
if !legacy_adapter_accept_response(adapter, legacy_binding, &response) {
break;
}
continue;
}
QueuedDispatchMessage::Request(request) => request,
};
worker_queue_state.release_queued_bytes(queued_request.serialized_bytes);
let era = queued_request.era;
let request = queued_request.request;
match queued_request
.principal_admission
.wait_timeout(DISPATCH_WORKER_SHUTDOWN_TIMEOUT)
{
Some(true) => {}
Some(false) => {
if let Some(id) = request.id.as_ref() {
worker_queue_state.discard(id);
}
continue;
}
None => {
if let Some(id) = request.id.as_ref() {
worker_queue_state.discard(id);
}
break;
}
}
// The hand-off from the receive queue to the worker is the
// linearization point for cancellation. Mark it before any
// further branch so a later cancellation targets an active
// request rather than silently cancelling a dequeued one.
let cancelled_before_dispatch = request
.id
.as_ref()
.is_some_and(|id| worker_queue_state.begin_dispatch(id));
if worker_queue_state.is_stopping() {
if let Some(id) = request.id.as_ref() {
worker_queue_state.discard(id);
}
clean_exit = true;
break;
}
if cancelled_before_dispatch {
if let Some(id) = request.id.as_ref() {
if matches!(era, ProtocolEra::Legacy2024) {
// Exact MCP 2024-11-05 treats an accepted
// cancellation as the peer relinquishing its
// response. The queued request never executes and
// no synthetic RequestCancelled response is sent.
worker_queue_state.discard(id);
continue;
}
let cancelled = JsonRpcResponse::error(
Some(id.clone()),
JsonRpcError {
code: McpErrorCode::RequestCancelled.into(),
message: "Request cancelled before dispatch".to_string(),
data: None,
},
);
let mut send_guard = worker_send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let send_result =
send_guard(&worker_cx, &JsonRpcMessage::Response(cancelled));
drop(send_guard);
worker_queue_state.discard(id);
if let Err(send_error) = send_result {
if send_error.is_cancelled() {
worker_queue_state.stop();
clean_exit = true;
}
break;
}
}
continue;
}
let start_time = Instant::now();
if let Some(renderer) = &worker_renderer {
renderer.render_request(&request, &worker_server.console);
}
let request_id = request.id.clone();
if matches!(era, ProtocolEra::Modern2026) {
let inbound = InboundRequestContext::with_modern_connection_context(
worker_cx.clone(),
request_id_to_u64(request.id.as_ref()),
InboundRequestTransport::Stdio,
&worker_modern_connection,
worker_transport_authorization.clone(),
);
let request_cancellation = McpRequestCancellation::new();
let notification_send = Arc::clone(&worker_send);
let notification_cx = worker_cx.clone();
let notification_cancellation = request_cancellation.clone();
let notification_sender: NotificationSender = Arc::new(move |notification| {
let mut send_guard = notification_send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// The receive side claims cancellation through this
// same writer lock. Once it has won, never commit a
// trailing progress or final-log notification.
if !notification_cancellation.is_cancel_requested()
&& send_guard(¬ification_cx, &JsonRpcMessage::Request(notification))
.is_err()
{
notification_cancellation.cancel();
}
});
let response = poll_on_cx(
&worker_cx,
Arc::clone(&worker_server).dispatch_with_protocol_policy_owned(
worker_server.protocol_policy,
&inbound,
request.clone(),
None,
worker_auth_receipt.clone(),
worker_auth_custody_generation,
None,
request_cancellation.clone(),
None,
notification_sender,
),
);
let send_result = response.map_or(Ok(()), |response| {
let mut send_guard = worker_send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
send_guard(&worker_cx, &JsonRpcMessage::Response(response))
});
if let Some(id) = request_id.as_ref() {
worker_queue_state.discard(id);
}
if let Err(send_error) = &send_result {
if send_error.is_cancelled() {
worker_queue_state.stop();
clean_exit = true;
}
break;
}
continue;
}
let adapter = match legacy_adapter.as_mut() {
Some(adapter) => adapter,
None => {
let adapter = worker_server.install_legacy_2024_adapter(
legacy_binding,
LiveLegacy2024RuntimeHandler {
server: worker_server.as_ref(),
cx: worker_cx.clone(),
session_id: legacy_binding.generation(),
session_principal: worker_session_principal.clone(),
runtime: worker_legacy_runtime.clone(),
queue_state: Some(Arc::clone(&worker_queue_state)),
active_request: Arc::clone(&legacy_active_request),
connection_auth: worker_auth_receipt.clone(),
},
);
let Ok(adapter) = adapter else {
if let Some(id) = request_id.as_ref() {
worker_queue_state.discard(id);
}
break;
};
legacy_adapter.insert(adapter)
}
};
let legacy_response = poll_on_cx(
&worker_cx,
legacy_adapter_response_async(adapter, legacy_binding, &request),
);
sync_live_legacy_runtime_from_adapter(&worker_legacy_runtime, adapter);
let active_request = take_live_legacy_active_request(&legacy_active_request);
let handled = match legacy_response {
Ok(response) => response.map(|response| {
legacy_handled_response(response, active_request, &worker_cx)
}),
// Peer-fault rejections of id-less frames are dropped
// rather than terminating the dispatch worker.
Err(error) if matches!(error.code().as_i32(), Some(-32602..=-32600)) => {
debug!(
target: targets::SESSION,
"Dropped invalid exact-2024 notification; code={}",
error.code()
);
None
}
Err(_) => {
if let Some(id) = request_id.as_ref() {
worker_queue_state.discard(id);
}
break;
}
};
let duration = start_time.elapsed();
let Some(handled) = handled else {
if let Some(id) = request_id.as_ref() {
worker_queue_state.discard(id);
}
continue;
};
// Acquire exclusive output ownership before closing the
// cancellation race. This is the strongest reservation the
// current synchronous writer surface can provide; the later
// write/flush can still fail and therefore remains fallible.
let mut send_guard = worker_send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let send_result = handled.send_with(&mut session, |response| {
send_guard(&worker_cx, &JsonRpcMessage::Response(response.clone()))
});
drop(send_guard);
if let Ok(Some(response)) = &send_result {
if let Some(renderer) = &worker_renderer {
renderer.render_response(response, Some(duration), &worker_server.console);
}
if let Some(ref stats) = worker_server.stats
&& let Ok(json) = serde_json::to_string(response)
{
stats.add_bytes_sent(json.len() as u64 + 1);
}
}
if let Some(id) = request_id.as_ref() {
worker_queue_state.discard(id);
}
if let Err(send_error) = &send_result {
// The send callback already maps a pre-commit context stop
// to `Cancelled`. Once it reports I/O or codec failure,
// that concrete output failure wins over any concurrent
// EOF, queue stop, or ambient cancellation.
if send_error.is_cancelled() {
worker_queue_state.stop();
clean_exit = true;
}
break;
}
}
let adapter_closed = legacy_adapter
.as_mut()
.is_none_or(|adapter| adapter.close(legacy_binding).is_ok());
if clean_exit && adapter_closed {
failure_latch.disarm();
}
});
let mut era_classifier =
StdioEraClassifier::new(runtime_stdio_policy(server.protocol_policy));
let mut negotiated_era = None;
let mut exit_code = 0;
let mut drain_correlated_before_stop = false;
'receive: loop {
if worker_failed.load(Ordering::Acquire)
|| connection_failure
.as_ref()
.is_some_and(|failed| failed.load(Ordering::Acquire))
{
exit_code = 1;
break;
}
if cx.checkpoint().is_err() {
break;
}
let message = match recv(cx, &worker_failed) {
Ok(message) => message,
Err(TransportError::Closed) => {
// A full-duplex peer may half-close ingress while the
// independently owned egress remains writable. Give
// every already-admitted id-bearing request a bounded
// opportunity to finish its response commit before
// shutdown cancellation begins, regardless of era.
drain_correlated_before_stop = true;
break;
}
Err(TransportError::Cancelled) => break,
Err(error) => {
let disposition = classify_receive_error(&error);
if enforce_runtime_era
&& negotiated_era.is_none()
&& matches!(
&disposition,
ReceiveErrorDisposition::ReplyWithParseError
| ReceiveErrorDisposition::ReplyWithInvalidRequest(_)
)
{
let _ = reject_initial_stdio_malformed(&mut era_classifier);
let _ = match disposition {
ReceiveErrorDisposition::ReplyWithParseError => {
send_uncorrelated_parse_error(&send, cx)
}
ReceiveErrorDisposition::ReplyWithInvalidRequest(request_id) => {
send_invalid_request(&send, cx, request_id)
}
ReceiveErrorDisposition::Terminate => unreachable!(
"only recoverable malformed input reaches the Auto opening rejection"
),
};
exit_code = 1;
break;
}
match disposition {
ReceiveErrorDisposition::ReplyWithParseError => {
if send_uncorrelated_parse_error(&send, cx).is_err() {
exit_code = 1;
break;
}
continue;
}
ReceiveErrorDisposition::ReplyWithInvalidRequest(request_id) => {
if send_invalid_request(&send, cx, request_id).is_err() {
exit_code = 1;
break;
}
continue;
}
ReceiveErrorDisposition::Terminate => {
exit_code = 1;
break;
}
}
}
};
// The receive implementation rechecks its stop predicate after a
// completed frame, but a worker failure can still race the final
// closure return. Never admit or mutate state for a frame after a
// connection-fatal worker exit has become observable.
if worker_failed.load(Ordering::Acquire) {
exit_code = 1;
break;
}
if enforce_runtime_era && negotiated_era.is_none() {
match classify_initial_stdio_envelope(&mut era_classifier, &message) {
Ok(era) => negotiated_era = Some(era),
Err(decision) if opening_rejection_selected_modern(&decision) => {
negotiated_era = Some(ProtocolEra::Modern2026);
if let JsonRpcMessage::Request(request) = &message {
let sent = if request.validate().is_err() {
send_invalid_request(&send, cx, request.id.clone())
} else if let Some(response) = modern_request_version_refusal(request) {
send.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
} else {
Ok(())
};
if sent.is_err() {
exit_code = 1;
break;
}
}
continue;
}
Err(_) => {
if let JsonRpcMessage::Request(request) = &message {
let _ = if request.validate().is_err() {
send_invalid_request(&send, cx, request.id.clone())
} else if let Some(response) =
era_admission_refusal(server.protocol_policy, request)
{
send.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
} else {
Ok(())
};
}
exit_code = i32::from(owns_server_lifecycle);
break;
}
}
}
// Account at the receive boundary so queued, rejected, cancelled,
// and bidirectional-response frames are not omitted from traffic
// totals merely because they never reach a dispatch worker.
if let Some(ref stats) = server.stats
&& let Ok(json) = serde_json::to_string(&message)
{
stats.add_bytes_received(json.len() as u64 + 1);
}
match message {
JsonRpcMessage::Response(response) => {
if response.validate().is_err() {
exit_code = 1;
break;
}
let disposition = pending_requests.route_response_with_disposition(&response);
if matches!(
disposition,
bidirectional::PendingResponseDisposition::Delivered
) {
debug!(target: targets::SERVER, "Routed response to pending request");
} else if matches!(
disposition,
bidirectional::PendingResponseDisposition::Unmatched
) && matches!(negotiated_era, Some(ProtocolEra::Legacy2024))
{
match dispatch_sender
.try_send(QueuedDispatchMessage::LegacyResponse(response))
{
Ok(()) => {}
Err(
asupersync_mpsc::SendError::Full(_)
| asupersync_mpsc::SendError::Disconnected(_)
| asupersync_mpsc::SendError::Cancelled(_),
) => {
exit_code = 1;
break;
}
}
} else if matches!(
disposition,
bidirectional::PendingResponseDisposition::Unmatched
) {
debug!(target: targets::SERVER, "Received unmatched JSON-RPC response");
}
}
JsonRpcMessage::Request(mut request)
if request.id.is_none() && request.method == "notifications/cancelled" =>
{
let Some(era) = negotiated_era else {
debug!(
target: targets::SESSION,
"Ignoring cancellation notification before protocol-era negotiation"
);
continue;
};
if request.validate().is_err() {
debug!(
target: targets::SESSION,
"Ignoring invalid cancellation notification before request-state mutation"
);
continue;
}
if matches!(era, ProtocolEra::Modern2026)
&& admit_final_client_notification_ingress(&request).is_err()
{
debug!(
target: targets::SESSION,
"Ignoring final cancellation notification not admitted from the client"
);
continue;
}
match server.authenticate_cancelled_control_notification(
cx,
&session_principal,
era,
&mut request,
None,
) {
Ok(cancellation) => {
let cancellation_accepted = if matches!(era, ProtocolEra::Modern2026) {
// Progress, final logs, and the terminal
// response all commit under this writer lock.
// Claim peer cancellation under the same
// authority so a winning cancellation cannot
// be followed by a queued request-scoped
// frame. Exact-2024 retains its adapter-owned
// cancellation ordering below.
let _writer_fence = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let queue_disposition = queue_state.cancel_reserved(
Server::cancellation_wire_request_id(&cancellation),
);
match queue_disposition {
DispatchCancellationDisposition::Accepted => true,
DispatchCancellationDisposition::Protected
| DispatchCancellationDisposition::AlreadySettled => false,
DispatchCancellationDisposition::NotOwned => server
.handle_cancellation_wire_notification(
session_id,
cancellation,
),
}
} else {
let queue_disposition = queue_state.cancel_reserved(
Server::cancellation_wire_request_id(&cancellation),
);
match queue_disposition {
DispatchCancellationDisposition::Accepted => true,
DispatchCancellationDisposition::Protected
| DispatchCancellationDisposition::AlreadySettled => false,
DispatchCancellationDisposition::NotOwned => server
.handle_cancellation_wire_notification(
session_id,
cancellation,
),
}
};
if cancellation_accepted {
let interrupted = pending_requests.cancel_cancelled();
if interrupted > 0 {
debug!(
target: targets::SESSION,
"Interrupted {interrupted} pending server-to-client request(s) owned by the cancelled request"
);
}
}
}
Err(error) => {
error!(
target: targets::SERVER,
"Rejected cancellation notification before mutation; code={:?}",
error.code
);
}
}
}
JsonRpcMessage::Request(request) => {
if request.validate().is_err() {
if send_invalid_request(&send, cx, request.id).is_err() {
exit_code = 1;
break 'receive;
}
continue;
}
let era = if !enforce_runtime_era {
ProtocolEra::Legacy2024
} else {
match negotiated_era {
Some(ProtocolEra::Modern2026) => {
if modern_protocol_version(&request)
== Some(MODERN_PROTOCOL_VERSION)
{
ProtocolEra::Modern2026
} else {
// Stateless per-request admission: answer
// this request and keep serving the stream.
if let Some(response) = modern_request_version_refusal(&request)
&& send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
.is_err()
{
exit_code = 1;
break 'receive;
}
continue;
}
}
Some(ProtocolEra::Legacy2024) => {
if modern_protocol_version(&request).is_some() {
if let Some(response) = protocol_era_refusal(&request) {
let _ = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
);
}
exit_code = i32::from(owns_server_lifecycle);
break 'receive;
}
ProtocolEra::Legacy2024
}
None => match era_classifier
.classify_opening(stdio_opening_frame(&request))
{
StdioEraDecision::Selected {
era: ProtocolEra::Modern2026,
modern_version: Some(ModernVersionSupport::Supported),
} => {
negotiated_era = Some(ProtocolEra::Modern2026);
ProtocolEra::Modern2026
}
StdioEraDecision::Selected {
era: ProtocolEra::Legacy2024,
modern_version: None,
} => {
negotiated_era = Some(ProtocolEra::Legacy2024);
ProtocolEra::Legacy2024
}
decision if opening_rejection_selected_modern(&decision) => {
negotiated_era = Some(ProtocolEra::Modern2026);
if let Some(response) = modern_request_version_refusal(&request)
&& send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
.is_err()
{
exit_code = 1;
break 'receive;
}
continue;
}
_ => {
if let Some(response) = protocol_era_refusal(&request) {
let _ = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
);
}
exit_code = i32::from(owns_server_lifecycle);
break 'receive;
}
},
}
};
if matches!(era, ProtocolEra::Modern2026)
&& admit_final_client_notification_ingress(&request).is_err()
{
debug!(
target: targets::SESSION,
"Ignoring final notification not admitted from the client before dispatch; method={}",
request.method
);
continue;
}
let request_id = request.id.clone();
if let Some(id) = request_id.as_ref()
&& (server.request_id_is_active(session_id, id)
|| !queue_state.admit(id, request.method != "initialize"))
{
let duplicate = JsonRpcResponse::error(
Some(id.clone()),
JsonRpcError {
code: McpErrorCode::InvalidRequest.into(),
message: "Request id is already active".to_string(),
data: None,
},
);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(duplicate),
)
.is_err()
{
exit_code = 1;
break;
}
continue;
}
let modern_slot = matches!(era, ProtocolEra::Modern2026);
if modern_slot && !queue_state.reserve_modern_slot() {
if let Some(id) = request.id {
queue_state.discard(&id);
let overloaded = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: RESOURCE_EXHAUSTED_ERROR_CODE.into(),
message: DISPATCH_QUEUE_CAPACITY_MESSAGE.to_string(),
data: None,
},
);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(overloaded),
)
.is_err()
{
exit_code = 1;
break;
}
}
continue;
}
let serialized_bytes = measure_dispatch_request(&request);
if serialized_bytes.is_none_or(|bytes| !queue_state.reserve_queued_bytes(bytes))
{
if modern_slot {
queue_state.release_modern_slot();
}
if let Some(id) = request.id {
queue_state.discard(&id);
let overloaded = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: RESOURCE_EXHAUSTED_ERROR_CODE.into(),
message: DISPATCH_QUEUE_CAPACITY_MESSAGE.to_string(),
data: None,
},
);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(overloaded),
)
.is_err()
{
exit_code = 1;
break;
}
}
continue;
}
let serialized_bytes = serialized_bytes
.expect("a dispatch byte reservation requires a measured request");
if modern_slot {
let response_id = request.id.clone();
let principal_admission = Arc::new(if response_id.is_some() {
DispatchPrincipalAdmission::pending()
} else {
DispatchPrincipalAdmission::admitted()
});
let reservation = ModernDispatchReservation::new(
Arc::clone(&queue_state),
response_id.clone(),
serialized_bytes,
Arc::clone(&worker_failed),
);
let request_server = Arc::clone(&server);
let request_send = Arc::clone(&send);
let request_renderer = traffic_renderer.clone();
let task_principal_admission = Arc::clone(&principal_admission);
let task_modern_connection = dispatch_modern_connection.clone();
let task_transport_authorization =
transport_authorization.clone().unwrap_or_default();
let task_auth_receipt = auth_receipt.clone();
let task_auth_custody_generation = auth_custody_generation;
let submitted = dispatch_cx.spawn_blocking(move |request_cx| {
let mut reservation = reservation;
match task_principal_admission
.wait_timeout(DISPATCH_WORKER_SHUTDOWN_TIMEOUT)
{
Some(true) => {}
Some(false) => return,
None => {
reservation.begin();
return;
}
}
reservation.begin();
let dispatched = std::panic::catch_unwind(
std::panic::AssertUnwindSafe(|| {
if let Some(renderer) = &request_renderer {
renderer.render_request(&request, &request_server.console);
}
let request_cancellation = reservation.cancellation();
let _active_guard = match request.id.clone() {
Some(id) => match ActiveRequestGuard::try_new_with_cancellation(
Arc::clone(&request_server.active_requests),
session_id,
id.clone(),
request_cx.clone(),
request_cancellation.clone(),
) {
Ok(guard) => Some(guard),
Err(_) => {
let response = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: McpErrorCode::InvalidRequest.into(),
message: "Request id is already active"
.to_string(),
data: None,
},
);
let mut send_guard = request_send
.lock()
.unwrap_or_else(
std::sync::PoisonError::into_inner,
);
return send_guard(
&request_cx,
&JsonRpcMessage::Response(response),
);
}
},
None => None,
};
match reservation
.queue
.begin_modern_dispatch(request.id.as_ref())
{
ModernDispatchStart::Stopping => Ok(()),
ModernDispatchStart::Cancelled => {
let Some(id) = request.id.clone() else {
return Ok(());
};
let response = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: McpErrorCode::RequestCancelled.into(),
message: "Request cancelled before dispatch"
.to_string(),
data: None,
},
);
let mut send_guard = request_send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
send_guard(
&request_cx,
&JsonRpcMessage::Response(response),
)
}
ModernDispatchStart::Ready => {
let inbound = InboundRequestContext::with_modern_connection_context(
request_cx.clone(),
request_id_to_u64(request.id.as_ref()),
InboundRequestTransport::Stdio,
&task_modern_connection,
task_transport_authorization.clone(),
);
let started = Instant::now();
let notification_send = Arc::clone(&request_send);
let notification_cx = request_cx.clone();
let notification_cancellation =
request_cancellation.clone();
let notification_failed =
Arc::clone(&reservation.failed);
#[cfg(test)]
let notification_request_id =
request_id_to_u64(request.id.as_ref());
let notification_sender: NotificationSender = Arc::new(
move |notification| {
#[cfg(test)]
wait_for_stdio_progress_commit_fence(
notification_request_id,
¬ification,
);
let mut send_guard = notification_send
.lock()
.unwrap_or_else(
std::sync::PoisonError::into_inner,
);
// Peer cancellation
// acquires this same
// writer fence before it
// changes request state.
// The post-lock check
// makes that election
// authoritative for
// progress and final logs.
if !notification_cancellation
.is_cancel_requested()
&& send_guard(
¬ification_cx,
&JsonRpcMessage::Request(notification),
)
.is_err()
{
// A later successful cancellation response
// must not erase this failed notification.
notification_failed
.store(true, Ordering::Release);
notification_cancellation.cancel();
}
},
);
let response = poll_on_cx(
&request_cx,
Arc::clone(&request_server)
.dispatch_with_protocol_policy_owned(
request_server.protocol_policy,
&inbound,
request.clone(),
None,
task_auth_receipt.clone(),
task_auth_custody_generation,
None,
request_cancellation.clone(),
None,
notification_sender,
),
);
response.map_or(Ok(()), |mut response| {
let mut send_guard = request_send
.lock()
.unwrap_or_else(
std::sync::PoisonError::into_inner,
);
// Server shutdown elects a final subscription
// completion before cancelling its request to
// wake this worker. That completion is the
// request's one sanctioned terminal response;
// do not overwrite it with RequestCancelled at
// the stdio writer fence.
let graceful_subscription_completion =
final_subscription_completion_response(&response);
if !graceful_subscription_completion
&& !request_cancellation.begin_finalization()
{
response = JsonRpcResponse::error(
response.id.clone(),
JsonRpcError {
code: McpErrorCode::RequestCancelled
.into(),
message: "Request cancelled before response finalization"
.to_string(),
data: None,
},
);
}
send_guard(
&request_cx,
&JsonRpcMessage::Response(response.clone()),
)?;
if let Some(renderer) = &request_renderer {
renderer.render_response(
&response,
Some(started.elapsed()),
&request_server.console,
);
}
if let Some(stats) = &request_server.stats
&& let Ok(json) = serde_json::to_string(&response)
{
stats.add_bytes_sent(json.len() as u64 + 1);
}
Ok(())
})
}
}
}),
);
match dispatched {
Ok(Ok(())) => reservation.disarm_failure(),
Ok(Err(error))
if error.is_cancelled()
&& reservation.queue.is_stopping() =>
{
reservation.disarm_failure();
}
Ok(Err(_)) | Err(_) => {}
}
});
if submitted.is_ok() {
let principal_result = response_id.as_ref().map_or(Ok(()), |_| {
bind_anonymous_connection_principal(&session_principal)
});
// Id-less messages construct the admission already
// admitted; resolving again would trip the
// single-transition invariant.
if response_id.is_some() {
principal_admission.resolve(principal_result.is_ok());
}
if let Err(error) = principal_result
&& let Some(id) = response_id
{
let rejected = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: error.code.into(),
message: error.message,
data: error.data,
},
);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(rejected),
)
.is_err()
{
exit_code = 1;
break;
}
}
} else if let Some(id) = response_id {
let unavailable = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: McpErrorCode::InternalError.into(),
message: "Caller runtime could not admit request dispatch"
.to_string(),
data: None,
},
);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(unavailable),
)
.is_err()
{
exit_code = 1;
break;
}
}
continue;
}
let permit = match dispatch_sender.try_reserve() {
Ok(permit) => permit,
Err(asupersync_mpsc::SendError::Full(())) => {
queue_state.release_queued_bytes(serialized_bytes);
if let Some(id) = request.id {
queue_state.discard(&id);
let overloaded = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: RESOURCE_EXHAUSTED_ERROR_CODE.into(),
message: DISPATCH_QUEUE_CAPACITY_MESSAGE.to_string(),
data: None,
},
);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(overloaded),
)
.is_err()
{
exit_code = 1;
break;
}
}
continue;
}
Err(
asupersync_mpsc::SendError::Disconnected(())
| asupersync_mpsc::SendError::Cancelled(()),
) => {
queue_state.release_queued_bytes(serialized_bytes);
if let Some(id) = request.id {
queue_state.discard(&id);
}
exit_code = 1;
break;
}
};
let response_id = request.id.clone();
let principal_admission = Arc::new(if response_id.is_some() {
DispatchPrincipalAdmission::pending()
} else {
DispatchPrincipalAdmission::admitted()
});
let queued = QueuedDispatchMessage::Request(QueuedDispatchRequest {
request,
era,
serialized_bytes,
principal_admission: Arc::clone(&principal_admission),
});
match permit.try_send(queued) {
Ok(()) => {
let principal_result =
response_id
.as_ref()
.map_or(Ok(()), |_| match &auth_receipt {
Some(custody) => bind_connection_principal(
&session_principal,
custody.connection_fingerprint(),
),
None => {
bind_anonymous_connection_principal(&session_principal)
}
});
// Id-less messages construct the admission already
// admitted; resolving again would trip the
// single-transition invariant.
if response_id.is_some() {
principal_admission.resolve(principal_result.is_ok());
}
if let Err(error) = principal_result
&& let Some(id) = response_id
{
let rejected = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: error.code.into(),
message: error.message,
data: error.data,
},
);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(rejected),
)
.is_err()
{
exit_code = 1;
break;
}
}
}
Err(asupersync_mpsc::SendError::Full(QueuedDispatchMessage::Request(
request,
))) => {
queue_state.release_queued_bytes(request.serialized_bytes);
if let Some(id) = request.request.id {
queue_state.discard(&id);
let overloaded = JsonRpcResponse::error(
Some(id),
JsonRpcError {
code: RESOURCE_EXHAUSTED_ERROR_CODE.into(),
message: DISPATCH_QUEUE_CAPACITY_MESSAGE.to_string(),
data: None,
},
);
if send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(overloaded),
)
.is_err()
{
exit_code = 1;
break;
}
}
}
Err(
asupersync_mpsc::SendError::Disconnected(
QueuedDispatchMessage::Request(request),
)
| asupersync_mpsc::SendError::Cancelled(QueuedDispatchMessage::Request(
request,
)),
) => {
queue_state.release_queued_bytes(request.serialized_bytes);
if let Some(id) = request.request.id {
queue_state.discard(&id);
}
exit_code = 1;
break;
}
Err(
asupersync_mpsc::SendError::Full(
QueuedDispatchMessage::LegacyResponse(_),
)
| asupersync_mpsc::SendError::Disconnected(
QueuedDispatchMessage::LegacyResponse(_),
)
| asupersync_mpsc::SendError::Cancelled(
QueuedDispatchMessage::LegacyResponse(_),
),
) => unreachable!("only a request is sent on this queue path"),
}
}
}
}
// Elect graceful final-subscription completion while the writer and
// dispatch worker still own their response path. The election sends
// its correlated stdio control, then wakes the listen future by
// cancellation; cancelling children first would erase that completion
// before the writer can commit it.
if owns_server_lifecycle {
let _ = server.terminate_subscription_streams_for_shutdown();
}
// Notification children have no response commit to protect, so they
// are cancelled before the drain windows instead of being waited out.
queue_state.cancel_uncorrelated_modern_children();
if drain_correlated_before_stop
&& !queue_state.wait_for_correlated_response_drain(DISPATCH_WORKER_SHUTDOWN_TIMEOUT)
{
exit_code = 1;
}
queue_state.stop();
drop(dispatch_sender);
if owns_server_lifecycle {
server.cancel_active_requests(CancelKind::Shutdown, false);
}
pending_requests.cancel_all();
let modern_children_quiesced = if queue_state
.wait_for_modern_drain(DISPATCH_WORKER_SHUTDOWN_TIMEOUT)
{
true
} else {
error!(
target: targets::SERVER,
"Modern request children did not drain within the bounded shutdown deadline"
);
exit_code = 1;
if detach_on_worker_timeout {
false
} else {
error!(
target: targets::SERVER,
"Returning server pump is waiting for modern-child quiescence before cleanup"
);
queue_state.wait_for_modern_drain_unbounded();
true
}
};
let worker_quiesced = match worker_completion_receiver
.recv_timeout(DISPATCH_WORKER_SHUTDOWN_TIMEOUT)
{
Ok(()) => {
if worker.join().is_err() {
exit_code = 1;
}
true
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
if worker.join().is_err() {
exit_code = 1;
}
true
}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
error!(
target: targets::SERVER,
"Dispatch worker did not stop within the bounded shutdown deadline"
);
exit_code = 1;
if detach_on_worker_timeout {
// `run_stdio_with_cx` exits the process immediately
// after this return. Rust cannot safely preempt an
// arbitrary blocking handler; detaching is the only
// bounded process-exit fallback here.
drop(worker);
false
} else {
error!(
target: targets::SERVER,
"Returning server pump is waiting for dispatch-worker quiescence before running shutdown hooks"
);
let _ = worker_completion_receiver.recv();
if worker.join().is_err() {
exit_code = 1;
}
true
}
}
};
// Recheck all producer-owned failure latches after the dispatch worker
// has quiesced. This closes the final-load race where notification
// output fails immediately after recv classifies concurrent EOF as a
// clean close.
if worker_failed.load(Ordering::Acquire)
|| connection_failure
.as_ref()
.is_some_and(|failed| failed.load(Ordering::Acquire))
{
exit_code = 1;
}
#[cfg(feature = "tasks")]
let task_service_succeeded =
Self::settle_hosted_task_service_blocking(hosted_task_service).is_ok();
#[cfg(not(feature = "tasks"))]
let task_service_succeeded = true;
if !task_service_succeeded {
error!(target: targets::SERVER, "Hosted Task service did not settle successfully");
exit_code = 1;
}
if owns_server_lifecycle {
if worker_quiesced && modern_children_quiesced {
server.run_shutdown_hook();
} else {
error!(
target: targets::SERVER,
"Skipping shutdown hook because dispatch-child quiescence was not established"
);
}
}
if let Some(ref stats) = server.stats {
stats.connection_closed();
}
exit_code
}
/// Shared server loop for embedding/testing, returning on shutdown instead of exiting.
///
/// This is intentionally separate from
/// [`run_loop_with_dispatch_cx`](Self::run_loop_with_dispatch_cx) because the primary server
/// entrypoints use `std::process::exit` on shutdown for subprocess use-cases. Clean EOF and
/// cancellation return success; startup, protocol, and fatal transport failures return errors.
#[allow(clippy::too_many_lines)]
fn run_loop_returning<R, S>(
self,
cx: &Cx,
mut recv: R,
send: S,
notification_sender: NotificationSender,
connection_failure: Option<Arc<AtomicBool>>,
transport_label: &'static str,
) -> McpResult<()>
where
R: FnMut(&Cx) -> Result<JsonRpcMessage, TransportError>,
S: FnMut(&Cx, &JsonRpcMessage) -> Result<(), TransportError> + Send + Sync + 'static,
{
let server = Arc::new(self);
#[cfg(feature = "legacy-2024-11-05")]
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
#[cfg(feature = "legacy-2024-11-05")]
let connection_id = session.id();
// This returning API consumes its Server and owns exactly one
// connection. Its active-request registry is already isolated from
// other Server values; no legacy Session is needed for that key.
#[cfg(not(feature = "legacy-2024-11-05"))]
let connection_id = 0;
#[cfg(feature = "legacy-2024-11-05")]
let legacy_binding = runtime_legacy_binding(session.id());
#[cfg(feature = "legacy-2024-11-05")]
let mut legacy_adapter: Option<
Legacy2024ServerAdapter<LiveLegacy2024RuntimeHandler<'_>>,
> = None;
#[cfg(feature = "legacy-2024-11-05")]
let legacy_active_request = Arc::new(Mutex::new(None));
let mut era_classifier =
StdioEraClassifier::new(runtime_stdio_policy(server.protocol_policy));
let mut negotiated_era = None;
// Keep response output and inbound pending-response routing connection-scoped.
let send = Arc::new(Mutex::new(send));
let background_send_failure = Arc::new(Mutex::new(None));
let modern_queue = Arc::new(DispatchQueueState::default());
#[cfg(feature = "legacy-2024-11-05")]
let pending_requests = server.new_pending_requests_for_connection();
#[cfg(feature = "legacy-2024-11-05")]
let legacy_runtime = LiveLegacy2024ConnectionRuntime::new(
SessionState::new(),
Arc::clone(¬ification_sender),
None,
server.logging.level,
);
let modern_connection = ModernConnection::new();
// Track connection opened
if let Some(ref stats) = server.stats {
stats.connection_opened();
}
// Render startup banner if enabled (respects both config and legacy env var)
if server.console_config.show_banner && !banner_suppressed() {
server.render_startup_banner(transport_label);
}
// Complete the operation first so temporary output guards are released
// before shutdown waits for active request owners. Every exit retains
// cleanup failure, including protocol and response-send failures.
#[cfg(feature = "tasks")]
let mut hosted_task_service = None;
let run_result = (|| {
// Run startup hook
if !server.run_startup_hook() {
error!(target: targets::SERVER, "Startup hook failed, stopping");
return Err(server_run_error(
"startup",
"hook_failure",
"Server startup hook failed",
));
}
#[cfg(feature = "tasks")]
{
hosted_task_service = server.start_hosted_task_service_blocking(cx, true)?;
}
// Create traffic renderer if enabled
let traffic_renderer = server.configured_traffic_renderer();
// Main request loop
loop {
#[cfg(feature = "tasks")]
if let Some(hosted) = hosted_task_service.as_ref() {
hosted.check_running()?;
}
if let Some(error) = background_send_failure
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
{
return Err(error);
}
if connection_failure
.as_ref()
.is_some_and(|failed| failed.load(Ordering::Acquire))
{
return Err(server_run_error(
"notification",
"send_failure",
"Server notification send failed",
));
}
// Check for cancellation
if cx.checkpoint().is_err() {
info!(target: targets::SERVER, "Cancellation requested, stopping");
return Ok(());
}
// Receive next message
let receive_result = recv(cx);
if let Some(error) = background_send_failure
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
{
return Err(error);
}
if connection_failure
.as_ref()
.is_some_and(|failed| failed.load(Ordering::Acquire))
{
return Err(server_run_error(
"notification",
"send_failure",
"Server notification send failed",
));
}
let message = match receive_result {
Ok(msg) => msg,
Err(TransportError::Closed) => {
return Ok(());
}
Err(TransportError::Cancelled) => {
info!(target: targets::SERVER, "Transport cancelled");
return Ok(());
}
Err(error) => match classify_receive_error(&error) {
ReceiveErrorDisposition::ReplyWithParseError => {
// A JSON syntax failure is a transport-isolated complete
// frame, so JSON-RPC requires the fixed uncorrelated parse
// error even before MCP protocol-era negotiation. The
// response shape is era-independent and the following
// complete message remains admissible.
error!(target: targets::TRANSPORT, "Rejected malformed transport message");
if let Err(send_error) = send_uncorrelated_parse_error(&send, cx) {
error!(target: targets::TRANSPORT, "Failed to send parse-error response; terminating transport");
return returning_send_result_with_connection_failure(
&send_error,
&connection_failure,
);
}
continue;
}
ReceiveErrorDisposition::ReplyWithInvalidRequest(request_id) => {
error!(target: targets::TRANSPORT, "Rejected invalid JSON-RPC request");
if let Err(send_error) = send_invalid_request(&send, cx, request_id) {
error!(target: targets::TRANSPORT, "Failed to send invalid-request response; terminating transport");
return returning_send_result_with_connection_failure(
&send_error,
&connection_failure,
);
}
continue;
}
ReceiveErrorDisposition::Terminate => {
error!(target: targets::TRANSPORT, "Fatal transport receive failure; terminating transport");
return Err(transport_run_error("receive", &error));
}
},
};
// Log request traffic
if let Some(renderer) = &traffic_renderer {
if let JsonRpcMessage::Request(req) = &message {
renderer.render_request(req, &server.console);
}
}
let start_time = Instant::now();
// Handle the message
let response_opt = match message {
JsonRpcMessage::Request(request) => {
if request.validate().is_err() {
if let Err(send_error) = send_invalid_request(&send, cx, request.id) {
return returning_send_result_with_connection_failure(
&send_error,
&connection_failure,
);
}
continue;
}
// Track bytes received (approximate from serialized request size)
if let Some(ref stats) = server.stats {
// Estimate request size by serializing back to JSON
// This is approximate but accurate enough for statistics
if let Ok(json) = serde_json::to_string(&request) {
stats.add_bytes_received(json.len() as u64 + 1); // +1 for newline
}
}
let era = match negotiated_era {
Some(ProtocolEra::Modern2026)
if modern_protocol_version(&request)
== Some(MODERN_PROTOCOL_VERSION) =>
{
ProtocolEra::Modern2026
}
#[cfg(feature = "legacy-2024-11-05")]
Some(ProtocolEra::Legacy2024)
if modern_protocol_version(&request).is_none() =>
{
ProtocolEra::Legacy2024
}
Some(ProtocolEra::Modern2026) => {
// Stateless per-request admission: answer this
// request and keep serving the stream.
if let Some(response) = modern_request_version_refusal(&request)
&& let Err(send_error) = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
{
return returning_send_result_with_connection_failure(
&send_error,
&connection_failure,
);
}
continue;
}
Some(_) => {
if let Some(response) = protocol_era_refusal(&request)
&& let Err(send_error) = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
{
return returning_send_result_with_connection_failure(
&send_error,
&connection_failure,
);
}
return Err(server_run_error(
"protocol",
"era_admission",
"Request does not match the negotiated MCP protocol era",
));
}
None => {
match era_classifier.classify_opening(stdio_opening_frame(&request))
{
StdioEraDecision::Selected {
era: ProtocolEra::Modern2026,
modern_version: Some(ModernVersionSupport::Supported),
} => {
negotiated_era = Some(ProtocolEra::Modern2026);
ProtocolEra::Modern2026
}
#[cfg(feature = "legacy-2024-11-05")]
StdioEraDecision::Selected {
era: ProtocolEra::Legacy2024,
modern_version: None,
} => {
negotiated_era = Some(ProtocolEra::Legacy2024);
ProtocolEra::Legacy2024
}
decision if opening_rejection_selected_modern(&decision) => {
negotiated_era = Some(ProtocolEra::Modern2026);
if let Some(response) =
modern_request_version_refusal(&request)
&& let Err(send_error) = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
{
return returning_send_result_with_connection_failure(
&send_error,
&connection_failure,
);
}
continue;
}
_ => {
if let Some(response) = protocol_era_refusal(&request)
&& let Err(send_error) = send
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)(
cx,
&JsonRpcMessage::Response(response),
)
{
return returning_send_result_with_connection_failure(
&send_error,
&connection_failure,
);
}
return Err(server_run_error(
"protocol",
"era_admission",
"Request does not match the negotiated MCP protocol era",
));
}
}
}
};
if matches!(era, ProtocolEra::Modern2026) {
// Stdio is a local trusted pipe with no transport-layer
// authorization; dispatch carries no auth custody for it.
let inbound =
InboundRequestContext::with_modern_connection_and_transport_authorization(
cx.clone(),
request_id_to_u64(request.id.as_ref()),
InboundRequestTransport::Stdio,
&modern_connection,
TransportAuthorization::default(),
);
let response = Self::dispatch_or_schedule_stdio_modern_request(
Arc::clone(&server),
cx,
cx,
connection_id,
inbound,
request,
None,
None,
Arc::clone(¬ification_sender),
Arc::clone(&send),
Arc::clone(&background_send_failure),
Arc::clone(&modern_queue),
);
#[cfg(feature = "legacy-2024-11-05")]
let response = response.map(HandledRequest::untracked);
response
} else {
#[cfg(not(feature = "legacy-2024-11-05"))]
return Err(server_run_error(
"protocol",
"era_admission",
"Request does not match the negotiated MCP protocol era",
));
#[cfg(feature = "legacy-2024-11-05")]
{
let adapter = match legacy_adapter.as_mut() {
Some(adapter) => adapter,
None => {
let adapter = server.install_legacy_2024_adapter(
legacy_binding,
LiveLegacy2024RuntimeHandler {
server: server.as_ref(),
cx: cx.clone(),
session_id: legacy_binding.generation(),
session_principal: session.principal_binding(),
runtime: legacy_runtime.clone(),
queue_state: None,
active_request: Arc::clone(&legacy_active_request),
connection_auth: None,
},
);
let Ok(adapter) = adapter else {
return Err(server_run_error(
"protocol",
"legacy_adapter_install",
"Legacy MCP 2024-11-05 adapter could not be installed",
));
};
legacy_adapter.insert(adapter)
}
};
let legacy_response = poll_on_cx(
cx,
legacy_adapter_response_async(
adapter,
legacy_binding,
&request,
),
);
sync_live_legacy_runtime_from_adapter(&legacy_runtime, adapter);
let active_request =
take_live_legacy_active_request(&legacy_active_request);
match legacy_response {
Ok(response) => response.map(|response| {
legacy_handled_response(response, active_request, cx)
}),
// A peer-fault rejection of an id-less frame has no
// response channel; JSON-RPC drops it without
// advancing lifecycle, and one malformed peer
// notification must not terminate the connection.
Err(error)
if matches!(
error.code().as_i32(),
Some(-32602..=-32600)
) =>
{
debug!(
target: targets::SESSION,
"Dropped invalid exact-2024 notification; code={}",
error.code()
);
None
}
Err(_) => {
return Err(server_run_error(
"protocol",
"legacy_adapter",
"Legacy MCP 2024-11-05 adapter rejected a notification",
));
}
}
}
}
}
JsonRpcMessage::Response(response) => {
if response.validate().is_err() {
return Err(server_run_error(
"receive",
"invalid_response",
"Received invalid JSON-RPC response",
));
}
#[cfg(feature = "legacy-2024-11-05")]
{
// Preserve generic pending responses first; otherwise an
// exact-2024 response completes the adapter-owned reverse
// request for this one selected legacy connection.
let disposition =
pending_requests.route_response_with_disposition(&response);
if matches!(
disposition,
bidirectional::PendingResponseDisposition::Delivered
) {
debug!(target: targets::SERVER, "Routed response to pending request");
} else if matches!(
disposition,
bidirectional::PendingResponseDisposition::Unmatched
) && matches!(negotiated_era, Some(ProtocolEra::Legacy2024))
{
let Some(adapter) = legacy_adapter.as_mut() else {
return Err(server_run_error(
"protocol",
"legacy_adapter",
"Legacy MCP 2024-11-05 adapter is unavailable for a client response",
));
};
if !legacy_adapter_accept_response(
adapter,
legacy_binding,
&response,
) {
return Err(server_run_error(
"protocol",
"legacy_adapter",
"Legacy MCP 2024-11-05 adapter rejected a client response",
));
}
} else if matches!(
disposition,
bidirectional::PendingResponseDisposition::Unmatched
) {
let request_key = response.id.as_ref().map(request_id_log_key);
debug!(
target: targets::SERVER,
"Received unexpected response (id_present={}, request_key={:016x})",
response.id.is_some(),
request_key.unwrap_or_default()
);
}
}
#[cfg(not(feature = "legacy-2024-11-05"))]
{
// Modern requests do not create reverse JSON-RPC
// requests. A well-formed unmatched response has
// no recipient, but invalid responses above must
// still terminate the transport before more input.
let request_key = response.id.as_ref().map(request_id_log_key);
debug!(
target: targets::SERVER,
"Received unexpected response (id_present={}, request_key={:016x})",
response.id.is_some(),
request_key.unwrap_or_default()
);
}
continue;
}
};
if connection_failure
.as_ref()
.is_some_and(|failed| failed.load(Ordering::Acquire))
{
return Err(server_run_error(
"notification",
"send_failure",
"Server notification send failed",
));
}
let duration = start_time.elapsed();
if let Some(response) = response_opt {
let mut guard = match send.lock() {
Ok(guard) => guard,
Err(poisoned) => {
error!(
target: targets::TRANSPORT,
"Send channel lock poisoned; continuing with inner guard"
);
poisoned.into_inner()
}
};
#[cfg(feature = "legacy-2024-11-05")]
let send_result = response.send_with(&mut session, |response| {
guard(cx, &JsonRpcMessage::Response(response.clone()))
});
#[cfg(not(feature = "legacy-2024-11-05"))]
let send_result = guard(cx, &JsonRpcMessage::Response(response.clone()))
.map(|()| Some(response));
drop(guard);
if let Ok(Some(response)) = &send_result {
if let Some(renderer) = &traffic_renderer {
renderer.render_response(response, Some(duration), &server.console);
}
if let Some(ref stats) = server.stats
&& let Ok(json) = serde_json::to_string(response)
{
stats.add_bytes_sent(json.len() as u64 + 1);
}
}
if let Err(send_error) = send_result {
error!(target: targets::TRANSPORT, "Failed to send response; terminating transport");
return returning_send_result_with_connection_failure(
&send_error,
&connection_failure,
);
}
}
}
})();
#[cfg(feature = "tasks")]
let task_service_result = Self::settle_hosted_task_service_blocking(hosted_task_service);
#[cfg(not(feature = "tasks"))]
let task_service_result: McpResult<()> = Ok(());
let cleanup = server.graceful_shutdown_returning();
let run_result = run_result.and(task_service_result);
// A worker can finish its response attempt during the shutdown drain.
// Read its failure after ownership retirement, including on clean EOF.
let send_error = background_send_failure
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
let run_result = match (run_result, send_error) {
(Ok(()), Some(error)) => Err(error),
(Err(run_error), Some(send_error)) => Err(McpError::with_data(
McpErrorCode::InternalError,
"Server operation and background response send both failed",
serde_json::json!({
"stage": "run_and_send",
"kind": "multiple_failures",
"run": run_error,
"send": send_error,
}),
)),
(result, None) => result,
};
match run_result {
Ok(()) => cleanup.into_error().map_or(Ok(()), Err),
Err(error) => Err(combined_operation_and_cleanup_error(error, cleanup)),
}
}
/// Handles a single JSON-RPC request.
async fn handle_request(
&self,
cx: &Cx,
session: &mut Session,
request: JsonRpcRequest,
notification_sender: &NotificationSender,
request_sender: &bidirectional::RequestSender,
) -> Option<JsonRpcResponse> {
self.handle_request_internal(
cx,
session,
request,
notification_sender,
request_sender,
None,
None,
)
.await
.map(|handled| handled.finalize_for_return(session))
}
async fn handle_request_internal(
&self,
cx: &Cx,
session: &mut Session,
request: JsonRpcRequest,
notification_sender: &NotificationSender,
request_sender: &bidirectional::RequestSender,
dispatch_queue: Option<&DispatchQueueState>,
transport_authorization: Option<&str>,
) -> Option<HandledRequest> {
let id = request.id.clone();
let method = request.method.clone();
let is_notification = id.is_none();
// Start timing for stats
let start_time = Instant::now();
if !is_notification && is_notification_only_method(&method) {
let deferred_stats = DeferredRequestStats::new(
self.stats.as_ref(),
&method,
start_time,
DeferredRequestOutcome::Failure,
);
return Some(
HandledRequest::untracked(JsonRpcResponse::error(
id,
JsonRpcError {
code: McpErrorCode::InvalidRequest.into(),
message: "MCP notification method must not carry a request id".to_string(),
data: None,
},
))
.with_deferred_stats(deferred_stats),
);
}
if is_notification && is_request_only_method(&method) {
if let Some(ref stats) = self.stats {
stats.record_request(&method, start_time.elapsed(), false);
}
error!(
target: targets::SERVER,
"Rejected request-only MCP method sent without an id; method_key={:016x}",
stable_hash_request_id(&method)
);
return None;
}
// Generate internal request ID for tracing
let request_id = request_id_to_u64(id.as_ref());
// Create a budget for this request based on timeout configuration
let budget = self.create_request_budget(cx);
// Reject an already-cancelled or exhausted request before it acquires
// request tracking or enters authentication/middleware.
if let Some(error) = Self::request_budget_error(cx, budget) {
// If it's a notification, we don't send an error response
let outcome = if error.code == McpErrorCode::RequestCancelled {
DeferredRequestOutcome::Cancelled
} else {
DeferredRequestOutcome::Failure
};
let Some(response_id) = id.clone() else {
if let Some(stats) =
DeferredRequestStats::new(self.stats.as_ref(), &method, start_time, outcome)
{
stats.record();
}
return None;
};
let deferred_stats =
DeferredRequestStats::new(self.stats.as_ref(), &method, start_time, outcome);
return Some(
HandledRequest::untracked(JsonRpcResponse::error(
Some(response_id),
JsonRpcError {
code: error.code.into(),
message: error.message,
data: error.data,
},
))
.with_deferred_stats(deferred_stats),
);
}
let request_cx = cx.clone();
let active_guard = match id.clone() {
Some(request_id) => {
// Reuse the queue-admitted cancellation token when this
// request came through the dispatch queue. `cancel_reserved`
// cancels the admitted token, so an in-band cancellation that
// arrives while the request is dispatching must act on the
// same token the dispatch observes and finalizes against.
let admitted_cancellation = dispatch_queue
.and_then(|queue| queue.admitted_request_cancellation(&request_id))
.unwrap_or_default();
match ActiveRequestGuard::try_new_with_cancellation(
Arc::clone(&self.active_requests),
session.id(),
request_id.clone(),
request_cx.clone(),
admitted_cancellation,
) {
Ok(guard) => Some(guard),
Err(_duplicate_id) => {
let message = "Request id is already active; wait for the earlier request to finish before reusing it".to_string();
let deferred_stats = DeferredRequestStats::new(
self.stats.as_ref(),
&method,
start_time,
DeferredRequestOutcome::Failure,
);
return Some(
HandledRequest::untracked(JsonRpcResponse::error(
Some(request_id),
JsonRpcError {
code: McpErrorCode::InvalidRequest.into(),
message,
data: None,
},
))
.with_deferred_stats(deferred_stats),
);
}
}
}
None => None,
};
let request_cancellation = active_guard.as_ref().map_or_else(
McpRequestCancellation::new,
ActiveRequestGuard::cancellation,
);
if let (Some(queue), Some(request_id)) = (dispatch_queue, id.as_ref())
&& queue.begin_dispatch(request_id)
{
let _ = request_cancellation.cancel();
}
// Dispatch based on method, passing the budget, notification sender, and request sender
let mut session_mutation_rollback = None;
let mut result = self
.dispatch_method(
&request_cx,
session,
request,
request_id,
&request_cancellation,
&budget,
notification_sender,
request_sender,
&mut session_mutation_rollback,
transport_authorization,
)
.await;
result = Self::enforce_post_dispatch_liveness(
&request_cancellation,
&request_cx,
budget,
result,
);
if result.is_err()
&& let Some(rollback) = session_mutation_rollback.take()
{
rollback.apply(session);
}
let stats_outcome = match &result {
Ok(_) => DeferredRequestOutcome::Success,
Err(e) if e.code == McpErrorCode::RequestCancelled => DeferredRequestOutcome::Cancelled,
Err(_) => DeferredRequestOutcome::Failure,
};
// If it's a notification (no ID), we must not reply
if is_notification {
if let Err(e) = result {
fastmcp_core::logging::error!(
target: targets::HANDLER,
"Notification method={} failed with code={:?}",
safe_peer_log_key(&method),
e.code
);
}
if let Some(stats) =
DeferredRequestStats::new(self.stats.as_ref(), &method, start_time, stats_outcome)
{
stats.record();
}
return None;
}
// We only reach here if `is_notification` is false, which implies `id` is present.
// Use `?` to avoid `unwrap()` and keep the control-flow explicit.
let response_id = id.clone()?;
let response = match result {
Ok(value) => JsonRpcResponse::success(response_id, value),
Err(e) => {
// Log full error before masking if this is an internal error
if self.mask_error_details && e.is_internal() {
fastmcp_core::logging::error!(
target: targets::HANDLER,
"Request method={} failed with masked internal code={:?}",
safe_peer_log_key(&method),
e.code
);
}
// Apply masking if enabled
let masked = mask_peer_error(e, self.mask_error_details);
JsonRpcResponse::error(
id,
JsonRpcError {
code: masked.code.into(),
message: masked.message,
data: masked.data,
},
)
}
};
let deferred_stats =
DeferredRequestStats::new(self.stats.as_ref(), &method, start_time, stats_outcome);
Some(
HandledRequest::tracked(
response,
request_cancellation,
active_guard,
session_mutation_rollback,
request_cx,
budget,
)
// An accepted in-band notifications/cancelled must suppress the
// request's response entirely (2024-11-05 cancellation contract);
// ambient cancellation and deadline expiry still produce their
// ordinary terminal error responses.
.suppress_cancelled_response()
.with_deferred_stats(deferred_stats),
)
}
/// Creates a budget for a new request based on server configuration.
fn create_request_budget(&self, cx: &Cx) -> Budget {
if self.request_timeout_secs == 0 {
// No timeout - unlimited budget
Budget::INFINITE
} else {
// Keep the ceiling in the caller context's clock domain so router
// admission and post-completion checks compare like with like.
// The legacy synchronous handler dispatcher uses a private runtime
// for its timeout future and therefore does not itself drive a
// foreign virtual clock.
let now = cx.now();
let timeout_ns = self.request_timeout_secs.saturating_mul(1_000_000_000);
let deadline = now.saturating_add_nanos(timeout_ns);
Budget::new().with_deadline(deadline)
}
}
fn request_budget_error(cx: &Cx, budget: Budget) -> Option<McpError> {
// The runtime also cancels a Cx whose deadline passed, so classify the
// deadline first; a passed deadline is not reported as a plain
// cancellation. Explicit request cancellation is checked by callers.
let effective_budget = cx.budget().meet(budget);
if effective_budget.is_past_deadline(cx.now()) {
return Some(McpError::new(
McpErrorCode::RequestCancelled,
"Request timeout exceeded",
));
}
if cx.is_cancel_requested() {
return Some(McpError::request_cancelled());
}
None
}
fn enforce_request_budget(cx: &Cx, budget: Budget) -> McpResult<()> {
match Self::request_budget_error(cx, budget) {
Some(error) => Err(error),
None => Ok(()),
}
}
fn enforce_post_dispatch_liveness(
request_cancellation: &McpRequestCancellation,
cx: &Cx,
budget: Budget,
mut result: McpResult<serde_json::Value>,
) -> McpResult<serde_json::Value> {
if request_cancellation.is_cancel_requested() {
return Err(McpError::request_cancelled());
}
if let Some(error) = Self::request_budget_error(cx, budget) {
result = Err(error);
}
result
}
fn request_context_error(ctx: &McpContext) -> Option<McpError> {
if ctx.ensure_live().is_err() {
if ctx.deadline_expired() {
return Some(McpError::new(
McpErrorCode::RequestCancelled,
"Request timeout exceeded",
));
}
return Some(McpError::request_cancelled());
}
None
}
fn enforce_request_context(ctx: &McpContext) -> McpResult<()> {
match Self::request_context_error(ctx) {
Some(error) => Err(error),
None => Ok(()),
}
}
/// Builds the single shared authority object for one request.
///
/// Authentication, middleware, top-level handlers, and any nested
/// tool/resource dispatch all derive from this context so request-local
/// budget accounting remains shared and the committed authenticated
/// principal cannot be replaced between layers.
fn request_context(
&self,
cx: &Cx,
request_id: u64,
state: SessionState,
request_cancellation: McpRequestCancellation,
budget: Budget,
) -> McpResult<(McpContext, McpContextLeaseGuard)> {
let ctx = McpContext::with_state(cx.clone(), request_id, state)
.with_request_cancellation(request_cancellation)
.with_budget_ceiling(budget);
// A handler that mutates the catalog on this path must still reach the
// `subscriptions/listen` publisher; dropping it here silently stopped
// every handler-driven `list_changed` from reaching modern listeners.
self.attach_request_scoped_component_access(self.with_final_catalog_publisher(ctx))
.begin_request_scope()
.ok_or_else(|| McpError::internal_error("request scope could not be established"))
}
/// Attaches request-scoped nested tool/resource access to one context.
///
/// Modern HTTP dispatch used to build the inbound context without these
/// callers, so `ctx.call_tool` / `ctx.read_resource` failed as a masked
/// internal error even after the nested router path awaited instead of
/// `block_on`.
fn attach_request_scoped_component_access(&self, ctx: McpContext) -> McpContext {
let state = ctx
.session_state()
.cloned()
.unwrap_or_else(SessionState::ephemeral);
// Request contexts must not extend the server router's lifetime. The
// Server remains its strong owner throughout dispatch, while escaped
// context clones lose both their lease and their ability to upgrade
// this weak access once the Server is consumed or dropped.
ctx.with_tool_caller(Arc::new(RouterToolCaller::request_scoped(
Arc::downgrade(&self.router),
state.clone(),
)))
.with_resource_reader(Arc::new(RouterResourceReader::request_scoped(
Arc::downgrade(&self.router),
state.clone(),
)))
.with_prompt_caller(Arc::new(RouterPromptCaller::request_scoped(
Arc::downgrade(&self.router),
state,
)))
.with_server_capabilities(self.handler_visible_server_capabilities())
}
/// Handler-visible slice of the advertised server capability document.
fn handler_visible_server_capabilities(&self) -> ServerCapabilityInfo {
let advertised = &self.capabilities;
let mut info = ServerCapabilityInfo::new();
if advertised.tools.is_some() {
info = info.with_tools();
}
if let Some(resources) = advertised.resources.as_ref() {
info = info.with_resources(resources.subscribe);
}
if advertised.prompts.is_some() {
info = info.with_prompts();
}
if advertised.logging.is_some() {
info = info.with_logging();
}
info
}
/// Handler-visible slice of a negotiated client capability document.
fn handler_visible_client_capabilities(caps: &ClientCapabilities) -> ClientCapabilityInfo {
let (elicitation_form, elicitation_url) = caps
.elicitation
.as_ref()
.map_or((false, false), |elicitation| {
(elicitation.supports_form(), elicitation.supports_url())
});
let mut info =
ClientCapabilityInfo::new().with_elicitation(elicitation_form, elicitation_url);
if caps.sampling.is_some() {
info = info.with_sampling();
}
if let Some(roots) = caps.roots.as_ref() {
info = info.with_roots(roots.list_changed);
}
info
}
fn attach_session_client_capabilities(
ctx: McpContext,
capabilities: Option<&ClientCapabilities>,
) -> McpContext {
match capabilities {
Some(capabilities) => ctx
.with_client_capabilities(Self::handler_visible_client_capabilities(capabilities)),
None => ctx,
}
}
fn handler_visible_legacy_client_implementation(
info: &fastmcp_protocol::ClientInfo,
) -> fastmcp_core::ClientImplementationInfo {
fastmcp_core::ClientImplementationInfo {
name: info.name.clone(),
version: info.version.clone(),
title: None,
description: None,
website_url: None,
icon_sources: Vec::new(),
}
}
fn attach_session_client_implementation(
ctx: McpContext,
client_info: Option<&fastmcp_protocol::ClientInfo>,
) -> McpContext {
match client_info {
Some(info) => ctx.with_client_implementation(
Self::handler_visible_legacy_client_implementation(info),
),
None => ctx,
}
}
fn with_final_catalog_publisher(&self, ctx: McpContext) -> McpContext {
ctx.with_catalog_publisher(Arc::new(FinalCatalogPublisher {
registry: Arc::clone(&self.final_subscriptions),
}))
}
fn attach_session_log_floor(ctx: McpContext, level: Option<LogLevel>) -> McpContext {
ctx.with_min_log_level(level.map(crate::handler::mcp_log_level))
}
fn attach_session_resource_subscriptions<'a>(
ctx: McpContext,
uris: impl IntoIterator<Item = &'a str>,
) -> McpContext {
ctx.with_resource_subscriptions(uris)
}
/// Dispatches a request to the appropriate handler.
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
async fn dispatch_method(
&self,
cx: &Cx,
session: &mut Session,
mut request: JsonRpcRequest,
request_id: u64,
request_cancellation: &McpRequestCancellation,
budget: &Budget,
notification_sender: &NotificationSender,
request_sender: &bidirectional::RequestSender,
session_mutation_rollback: &mut Option<SessionMutationRollback>,
transport_authorization: Option<&str>,
) -> Result<serde_json::Value, McpError> {
let (mw_ctx, _request_lease_guard) = self.request_context(
cx,
request_id,
session.state().clone(),
request_cancellation.clone(),
*budget,
)?;
let mw_ctx = Self::attach_session_log_floor(mw_ctx, session.log_level());
let mw_ctx =
Self::attach_session_client_capabilities(mw_ctx, session.client_capabilities());
let mw_ctx = Self::attach_session_client_implementation(mw_ctx, session.client_info());
let mw_ctx =
Self::attach_session_resource_subscriptions(mw_ctx, session.subscribed_resource_uris());
if let Err(error) = Self::enforce_request_context(&mw_ctx) {
let result =
Self::enforce_post_dispatch_liveness(request_cancellation, cx, *budget, Err(error));
self.maybe_emit_log_notification(
session,
notification_sender,
&request.method,
&result,
);
return result;
}
// Reject before authentication or extension middleware can claim the
// method. Quarantine must be invariant under server configuration.
if is_quarantined_task_rpc(&request.method) {
let result = Self::enforce_post_dispatch_liveness(
request_cancellation,
cx,
*budget,
Err(McpError::method_not_found(&request.method)),
);
self.maybe_emit_log_notification(
session,
notification_sender,
&request.method,
&result,
);
return result;
}
// Check initialization state
if !session.is_initialized() && request.method != "initialize" && request.method != "ping" {
let result = Self::enforce_post_dispatch_liveness(
request_cancellation,
cx,
*budget,
Err(McpError::invalid_request(
"Server not initialized. Client must send 'initialize' first.",
)),
);
self.maybe_emit_log_notification(
session,
notification_sender,
&request.method,
&result,
);
return result;
}
let auth_result = {
let auth_request = AuthRequest {
method: &request.method,
params: request.params.as_ref(),
transport_authorization,
request_id,
};
self.authenticate_request(&mw_ctx, auth_request)
.and_then(|fingerprint| {
if session.principal_binding().bind_or_verify(fingerprint) {
Self::enforce_request_context(&mw_ctx)
} else {
Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authenticated principal does not own this session",
))
}
})
};
auth::strip_recognized_access_credentials(&mut request.params);
if let Err(err) = auth_result {
let err = Self::request_context_error(&mw_ctx).unwrap_or(err);
let err =
self.finalize_global_middleware_error(request_cancellation, &mw_ctx, &request, err);
let result = Err(err);
self.maybe_emit_log_notification(
session,
notification_sender,
&request.method,
&result,
);
return result;
}
// Middleware: on_request
// We use a temporary context derived from the request context for middleware
// so they can access session state, request auth, and share the request's lifecycle.
let mut entered_middleware: Vec<&dyn crate::Middleware> = Vec::new();
for m in self.middleware.iter() {
if let Some(error) = Self::request_context_error(&mw_ctx) {
let result = self.finalize_middleware_result(
request_cancellation,
&entered_middleware,
&mw_ctx,
&request,
Err(error),
);
self.maybe_emit_log_notification(
session,
notification_sender,
&request.method,
&result,
);
return result;
}
entered_middleware.push(m.as_ref());
let decision = match catch_extension_unwind(|| m.on_request(&mw_ctx, &request)) {
Ok(decision) => decision,
Err(_payload) => Err(extension_panic_error("middleware_on_request")),
};
if let Some(error) = Self::request_context_error(&mw_ctx) {
let result = self.finalize_middleware_result(
request_cancellation,
&entered_middleware,
&mw_ctx,
&request,
Err(error),
);
self.maybe_emit_log_notification(
session,
notification_sender,
&request.method,
&result,
);
return result;
}
match decision {
Ok(crate::MiddlewareDecision::Continue) => {}
Ok(crate::MiddlewareDecision::Respond(v)) => {
let short_circuit_result = if is_session_mutation(&request.method) {
Err(McpError::internal_error(
"Middleware cannot short-circuit session mutations",
))
} else {
Ok(v)
};
let result = self.finalize_middleware_result(
request_cancellation,
&entered_middleware,
&mw_ctx,
&request,
short_circuit_result,
);
self.maybe_emit_log_notification(
session,
notification_sender,
&request.method,
&result,
);
return result;
}
Err(e) => {
let result = self.finalize_middleware_result(
request_cancellation,
&entered_middleware,
&mw_ctx,
&request,
Err(e),
);
self.maybe_emit_log_notification(
session,
notification_sender,
&request.method,
&result,
);
return result;
}
}
}
// Everything after middleware entry must flow through `result` so that:
// - `on_response` runs for successes in reverse middleware order
// - `on_error` runs for handler/middleware errors in reverse middleware order
//
// Without this, `?` would early-return from `dispatch_method` and bypass middleware error
// rewriting, contradicting the ordering semantics documented in `middleware.rs`.
let result: Result<serde_json::Value, McpError> = async {
Self::enforce_request_context(&mw_ctx)?;
let method = &request.method;
let params = request.params.clone();
// Create bidirectional senders based on client capabilities
let bidirectional_senders = self.create_bidirectional_senders(
session,
request_sender,
request_cancellation,
&mw_ctx,
);
match method.as_str() {
"initialize" => {
let params: InitializeParams = parse_params(params)?;
*session_mutation_rollback =
Some(SessionMutationRollback::RestoreInitialization(
session.initialization_snapshot(),
));
let result = self.router.handle_initialize(
&mw_ctx,
session,
params,
self.instructions.as_deref(),
)?;
Ok(serde_json::to_value(result).map_err(McpError::from)?)
}
"notifications/initialized" => {
// Notification, no response needed (but we send empty ok)
Ok(serde_json::Value::Null)
}
"notifications/cancelled" => {
let params: CancelledParams = parse_params(params)?;
self.handle_cancelled_notification(session.id(), params);
Ok(serde_json::Value::Null)
}
"logging/setLevel" => {
let params: SetLogLevelParams = parse_params(params)?;
*session_mutation_rollback = Some(SessionMutationRollback::RestoreLogLevel(
session.log_level(),
));
self.handle_set_log_level(session, params);
Ok(serde_json::Value::Null)
}
"tools/list" => {
let params: ListToolsParams = parse_params_or_default(params)?;
let result =
self.router
.handle_tools_list(&mw_ctx, params, Some(session.state()))?;
Ok(serde_json::to_value(result).map_err(McpError::from)?)
}
"tools/call" => {
let params: CallToolParams = parse_params(params)?;
let result = self
.router
.handle_tools_call(
&mw_ctx,
params,
session.state().clone(),
Some(notification_sender),
bidirectional_senders.as_ref(),
)
.await?;
Ok(serde_json::to_value(result).map_err(McpError::from)?)
}
"resources/list" => {
let params: ListResourcesParams = parse_params_or_default(params)?;
let result = self.router.handle_resources_list(
&mw_ctx,
params,
Some(session.state()),
)?;
Ok(serde_json::to_value(result).map_err(McpError::from)?)
}
"resources/templates/list" => {
let params: ListResourceTemplatesParams = parse_params_or_default(params)?;
let result = self.router.handle_resource_templates_list(
&mw_ctx,
params,
Some(session.state()),
)?;
Ok(serde_json::to_value(result).map_err(McpError::from)?)
}
"resources/read" => {
let params: ReadResourceParams = parse_params(params)?;
let result = self
.router
.handle_resources_read(
&mw_ctx,
¶ms,
session.state().clone(),
Some(notification_sender),
bidirectional_senders.as_ref(),
)
.await?;
Ok(serde_json::to_value(result).map_err(McpError::from)?)
}
"resources/subscribe" => {
let params: SubscribeResourceParams = parse_params(params)?;
let uri = params.uri;
// Enforce the individual retention bound before router
// lookup/template matching can hash or scan an impossible
// peer-controlled URI.
if uri.len() > MAX_RESOURCE_SUBSCRIPTION_BYTES_PER_SESSION {
return Err(resource_subscription_capacity_error());
}
if !self.router.resource_exists(&uri) {
return Err(McpError::resource_not_found(&uri));
}
match session.subscribe_resource(&mw_ctx, uri.clone()) {
Ok(SubscriptionAdmission::Accepted) => {
*session_mutation_rollback = Some(
SessionMutationRollback::RemoveResourceSubscription(uri.clone()),
);
self.router.notify_resource_subscribed(&mw_ctx, &uri)?;
Ok(serde_json::json!({}))
}
Ok(SubscriptionAdmission::Duplicate) => {
self.router.notify_resource_subscribed(&mw_ctx, &uri)?;
Ok(serde_json::json!({}))
}
Err(SubscriptionAdmissionError::CapacityExceeded) => {
Err(resource_subscription_capacity_error())
}
Err(SubscriptionAdmissionError::RequestNotLive) => {
Err(McpError::request_cancelled())
}
}
}
"resources/unsubscribe" => {
let params: UnsubscribeResourceParams = parse_params(params)?;
let uri = params.uri;
match session.unsubscribe_resource(&mw_ctx, &uri) {
Ok(SubscriptionRemoval::Removed) => {
*session_mutation_rollback = Some(
SessionMutationRollback::RestoreResourceSubscription(uri.clone()),
);
self.router.notify_resource_unsubscribed(&mw_ctx, &uri)?;
Ok(serde_json::json!({}))
}
Ok(SubscriptionRemoval::NotSubscribed) => Ok(serde_json::json!({})),
Err(SubscriptionRemovalError::RequestNotLive) => {
Err(McpError::request_cancelled())
}
}
}
"prompts/list" => {
let params: ListPromptsParams = parse_params_or_default(params)?;
let result =
self.router
.handle_prompts_list(&mw_ctx, params, Some(session.state()))?;
Ok(serde_json::to_value(result).map_err(McpError::from)?)
}
"prompts/get" => {
let params: GetPromptParams = parse_params(params)?;
let result = self
.router
.handle_prompts_get(
&mw_ctx,
params,
session.state().clone(),
Some(notification_sender),
bidirectional_senders.as_ref(),
)
.await?;
Ok(serde_json::to_value(result).map_err(McpError::from)?)
}
"ping" => {
// Simple ping-pong for health checks
Ok(serde_json::json!({}))
}
"completion/complete" => {
self.router
.dispatch_legacy_completion(&mw_ctx, &request)
.await
}
_ => Err(McpError::method_not_found(method)),
}
}
.await;
let result = self.maybe_dispatch_extension_fallback(&mw_ctx, &request, result);
let final_result = self.finalize_middleware_result(
request_cancellation,
&entered_middleware,
&mw_ctx,
&request,
result,
);
if final_result.is_err()
&& let Some(rollback) = session_mutation_rollback.take()
{
rollback.apply(session);
}
self.maybe_emit_log_notification(
session,
notification_sender,
&request.method,
&final_result,
);
final_result
}
fn apply_middleware_response(
&self,
stack: &[&dyn crate::Middleware],
ctx: &McpContext,
request: &JsonRpcRequest,
value: serde_json::Value,
) -> Result<serde_json::Value, McpError> {
self.apply_middleware_response_with_liveness(stack, ctx, request, value, true)
}
fn apply_middleware_response_with_liveness(
&self,
stack: &[&dyn crate::Middleware],
ctx: &McpContext,
request: &JsonRpcRequest,
value: serde_json::Value,
enforce_liveness: bool,
) -> Result<serde_json::Value, McpError> {
if enforce_liveness && let Some(error) = Self::request_context_error(ctx) {
return Err(error);
}
let mut response = value;
for m in stack.iter().rev() {
let invocation = catch_extension_unwind(|| m.on_response(ctx, request, response));
match invocation {
Ok(Ok(next)) => {
response = next;
if enforce_liveness && let Some(error) = Self::request_context_error(ctx) {
return Err(error);
}
}
Ok(Err(err)) => return Err(err),
Err(_payload) => {
return Err(extension_panic_error("middleware_on_response"));
}
}
}
Ok(response)
}
fn finalize_middleware_result(
&self,
request_cancellation: &McpRequestCancellation,
stack: &[&dyn crate::Middleware],
ctx: &McpContext,
request: &JsonRpcRequest,
result: McpResult<serde_json::Value>,
) -> McpResult<serde_json::Value> {
let initial = Self::request_context_error(ctx).map_or(result, Err);
let mut terminal_cancellation = initial
.as_ref()
.is_err_and(|error| error.code == McpErrorCode::RequestCancelled);
let (result, error_hooks_applied) = match initial {
Ok(value) => match self.apply_middleware_response(stack, ctx, request, value) {
Ok(value) => (Ok(value), false),
Err(error) => {
terminal_cancellation |= error.code == McpErrorCode::RequestCancelled;
(
Err(self.apply_middleware_error(stack, ctx, request, error)),
true,
)
}
},
Err(error) => (
Err(self.apply_middleware_error(stack, ctx, request, error)),
true,
),
};
// Response/error middleware is part of request dispatch. Cancellation
// remains eligible throughout those callbacks and until the caller's
// actual response-commit boundary. Error hooks may observe and clean
// up a cancellation, but may not rewrite its terminal wire class.
let cancellation_observed = terminal_cancellation
|| Self::request_context_error(ctx).is_some()
|| request_cancellation.is_cancel_requested();
if cancellation_observed {
if !error_hooks_applied {
let _ =
self.apply_middleware_error(stack, ctx, request, McpError::request_cancelled());
}
return Err(McpError::request_cancelled());
}
result
}
fn apply_middleware_error(
&self,
stack: &[&dyn crate::Middleware],
ctx: &McpContext,
request: &JsonRpcRequest,
error: McpError,
) -> McpError {
let mut err = error;
for m in stack.iter().rev() {
err = match catch_extension_unwind(|| m.on_error(ctx, request, err)) {
Ok(next) => next,
// A panicking error hook must not prevent earlier middleware
// from running their reverse-order cleanup. Replace the
// in-flight error with the fixed peer-safe failure and keep
// unwinding the entered middleware stack.
Err(_payload) => extension_panic_error("middleware_on_error"),
};
}
err
}
fn apply_global_middleware_error(
&self,
ctx: &McpContext,
request: &JsonRpcRequest,
error: McpError,
) -> McpError {
let mut err = error;
for m in self.middleware.iter().rev() {
err = match catch_extension_unwind(|| m.on_error(ctx, request, err)) {
Ok(next) => next,
// Auth and other pre-entry failures use the full registered
// stack. Preserve reverse cleanup even if one hook panics.
Err(_payload) => extension_panic_error("middleware_on_error"),
};
}
err
}
fn finalize_global_middleware_error(
&self,
request_cancellation: &McpRequestCancellation,
ctx: &McpContext,
request: &JsonRpcRequest,
error: McpError,
) -> McpError {
let terminal_cancellation = error.code == McpErrorCode::RequestCancelled;
let mapped = self.apply_global_middleware_error(ctx, request, error);
if terminal_cancellation
|| Self::request_context_error(ctx).is_some()
|| request_cancellation.is_cancel_requested()
{
return McpError::request_cancelled();
}
mapped
}
/// Creates bidirectional senders based on client capabilities.
///
/// Returns `Some(BidirectionalSenders)` if the client supports any
/// bidirectional feature (sampling, elicitation, roots), or `None` if it
/// supports none.
fn create_bidirectional_senders(
&self,
session: &Session,
request_sender: &bidirectional::RequestSender,
request_cancellation: &McpRequestCancellation,
request_context: &McpContext,
) -> Option<handler::BidirectionalSenders> {
self.create_bidirectional_senders_from_capabilities(
session.supports_sampling(),
session.supports_elicitation(),
session.supports_roots(),
request_sender,
request_cancellation,
request_context,
)
}
fn create_bidirectional_senders_from_capabilities(
&self,
supports_sampling: bool,
supports_elicitation: bool,
supports_roots: bool,
request_sender: &bidirectional::RequestSender,
request_cancellation: &McpRequestCancellation,
request_context: &McpContext,
) -> Option<handler::BidirectionalSenders> {
if !supports_sampling && !supports_elicitation && !supports_roots {
return None;
}
let mut senders = handler::BidirectionalSenders::new();
let request_sender = request_sender.for_request(request_cancellation.clone());
if supports_sampling {
let sampling_sender: Arc<dyn fastmcp_core::SamplingSender> =
Arc::new(bidirectional::TransportSamplingSender::new(
request_context.clone(),
request_sender.clone(),
));
senders = senders.with_sampling(sampling_sender);
}
if supports_elicitation {
let elicitation_sender: Arc<dyn fastmcp_core::ElicitationSender> =
Arc::new(bidirectional::TransportElicitationSender::new(
request_context.clone(),
request_sender.clone(),
));
senders = senders.with_elicitation(elicitation_sender);
}
if supports_roots {
let roots_provider: Arc<dyn fastmcp_core::RootsProvider> = Arc::new(
bidirectional::TransportRootsProvider::new(request_context.clone(), request_sender),
);
senders = senders.with_roots(roots_provider);
}
Some(senders)
}
/// Authenticates a native HTTP request before session pin, SSE-body
/// mutation, or exact-2024 admission can observe it.
fn preauthenticate_http_request(
&self,
cx: &Cx,
request: &JsonRpcRequest,
transport_authorization: &TransportAuthorization,
) -> Result<AuthAdmissionReceipt, HttpResponse> {
let auth_request = transport_authorization.auth_request(
&request.method,
request.params.as_ref(),
request_id_to_u64(request.id.as_ref()),
);
if auth_request.has_in_band_credential_source() {
return Err(native_http_credential_location_rejection());
}
if self.auth_provider.is_some() && auth_request.transport_authorization.is_none() {
return Err(native_http_authentication_rejection());
}
let budget = self.create_request_budget(cx);
Self::enforce_request_budget(cx, budget)
.map_err(|_| native_http_authentication_rejection())?;
let (request_ctx, _request_lease_guard) =
McpContext::new(cx.clone(), request_id_to_u64(request.id.as_ref()))
.with_budget_ceiling(budget)
.begin_request_scope()
.ok_or_else(native_http_authentication_rejection)?;
let admitted = self.authenticate_request_without_commit(&request_ctx, auth_request);
Self::enforce_request_context(&request_ctx)
.map_err(|_| native_http_authentication_rejection())?;
admitted
.map(|(fingerprint, authenticated)| {
let mut sanitized = request.clone();
auth::strip_recognized_access_credentials(&mut sanitized.params);
AuthAdmissionReceipt {
method: sanitized.method,
request_id: sanitized.id,
sanitized_params: sanitized.params,
fingerprint,
authenticated,
}
})
.map_err(|_| native_http_authentication_rejection())
}
/// Authenticates one modern ingress request before extension middleware
/// sees it. Native authorization remains in the inbound custody object;
/// recognized JSON credentials are removed after authentication and before
/// any extension callback. Connection-backed ingress additionally fences
/// retained state to the first authenticated principal.
fn authenticate_modern_request(
&self,
ctx: &McpContext,
inbound: &InboundRequestContext,
request: &mut JsonRpcRequest,
auth_receipt: Option<&AuthDispatchCustody>,
websocket_connection_generation: Option<u64>,
) -> McpResult<()> {
let fingerprint = match auth_receipt {
Some(receipt) => {
receipt.commit(ctx, inbound, request, websocket_connection_generation)?
}
None => {
let auth_request = inbound.auth_request(&request.method, request.params.as_ref());
let fingerprint = self.authenticate_request(ctx, auth_request)?;
auth::strip_recognized_access_credentials(&mut request.params);
fingerprint
}
};
if !inbound.bind_or_verify_principal(fingerprint) {
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authenticated principal does not own an admitted session",
));
}
Self::enforce_request_context(ctx)
}
fn admit_modern_pump_authentication(
&self,
inbound: &InboundRequestContext,
request: &JsonRpcRequest,
auth_receipt: Option<&AuthDispatchCustody>,
websocket_connection_generation: Option<u64>,
) -> McpResult<AuthDispatchCustody> {
let context = inbound.request_context();
let budget = self.create_request_budget(context.cx());
let (context, _lease) = context
.with_budget_ceiling(budget)
.begin_request_scope()
.ok_or_else(|| McpError::internal_error("request scope could not be established"))?;
Self::enforce_request_context(&context)?;
let mut sanitized = request.clone();
let (fingerprint, authenticated) = match auth_receipt {
Some(receipt) => (
receipt.commit(
&context,
inbound,
&mut sanitized,
websocket_connection_generation,
)?,
context.auth(),
),
None => self.authenticate_request_without_commit(
&context,
inbound.auth_request(&request.method, request.params.as_ref()),
)?,
};
Self::enforce_request_context(&context)?;
auth::strip_recognized_access_credentials(&mut sanitized.params);
if !inbound.bind_or_verify_principal(fingerprint) {
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authenticated principal does not own an admitted connection",
));
}
Ok(AuthDispatchCustody::Http(AuthAdmissionReceipt {
method: sanitized.method,
request_id: sanitized.id,
sanitized_params: sanitized.params,
fingerprint,
authenticated,
}))
}
/// A control message may cancel an existing principal's request, but may
/// never establish that principal's connection ownership itself.
fn authenticate_modern_cancelled_control(
&self,
inbound: &InboundRequestContext,
request: &mut JsonRpcRequest,
auth_receipt: Option<&AuthDispatchCustody>,
websocket_connection_generation: Option<u64>,
) -> McpResult<CancellationWireMessage> {
let context = inbound.request_context();
let budget = self.create_request_budget(context.cx());
let (context, _lease) = context
.with_budget_ceiling(budget)
.begin_request_scope()
.ok_or_else(|| McpError::internal_error("request scope could not be established"))?;
Self::enforce_request_context(&context)?;
let fingerprint = match auth_receipt {
Some(receipt) => {
receipt.commit(&context, inbound, request, websocket_connection_generation)?
}
None => {
let fingerprint = self.authenticate_request(
&context,
inbound.auth_request(&request.method, request.params.as_ref()),
)?;
auth::strip_recognized_access_credentials(&mut request.params);
fingerprint
}
};
let cancellation = CancellationWireMessage::decode(
ProtocolEra::Modern2026,
CancellationSender::Client,
request,
)
.map_err(|error| McpError::invalid_params(error.to_string()))?;
if !inbound.verify_existing_principal(fingerprint) {
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authenticated principal does not own an admitted connection",
));
}
Self::enforce_request_context(&context)?;
Ok(cancellation)
}
fn authenticate_request(
&self,
ctx: &McpContext,
request: AuthRequest<'_>,
) -> Result<Sha256Digest, McpError> {
let (fingerprint, authenticated) =
self.authenticate_request_without_commit(ctx, request)?;
Self::enforce_request_context(ctx)?;
match authenticated {
Some(auth) => {
if ctx.set_auth(auth) {
Ok(fingerprint)
} else if let Some(error) = Self::request_context_error(ctx) {
Err(error)
} else {
Err(McpError::internal_error(
"authentication context was already committed",
))
}
}
None if ctx.commit_anonymous_auth() => Ok(fingerprint),
None => Err(Self::request_context_error(ctx).unwrap_or_else(|| {
McpError::internal_error("authentication admission was already committed")
})),
}
}
/// Evaluates authentication without mutating the caller's request context.
/// Native HTTP and WebSocket ingress turn these private facts into an
/// [`AuthAdmissionReceipt`] before acquiring session or response-body
/// ownership; dispatch commits that receipt without re-evaluating a
/// provider.
fn authenticate_request_without_commit(
&self,
ctx: &McpContext,
request: AuthRequest<'_>,
) -> Result<(Sha256Digest, Option<AuthContext>), McpError> {
if !request.credential_sources_are_admissible() {
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authentication failed",
));
}
let credential_present = request.has_any_credential_source();
let Some(provider) = &self.auth_provider else {
if credential_present {
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authentication failed",
));
}
return auth::principal_fingerprint(None).map(|fingerprint| (fingerprint, None));
};
let auth = {
let staged_ctx = ctx.clone().with_isolated_auth();
let result = catch_extension_unwind(|| provider.authenticate(&staged_ctx, request))
.map_err(|_payload| extension_panic_error("auth_provider"))?;
result.map_err(|provider_error| {
debug!(
target: targets::SERVER,
"Authentication provider denied request; code={:?}",
provider_error.code
);
McpError::new(McpErrorCode::ResourceForbidden, "Authentication failed")
})?
};
if credential_present && auth.subject.as_deref().is_none_or(str::is_empty) {
error!(
target: targets::SERVER,
"Authentication provider admitted a credential without a stable subject"
);
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authentication failed",
));
}
let fingerprint = auth::principal_fingerprint(Some(&auth)).map_err(|provider_error| {
debug!(
target: targets::SERVER,
"Authentication provider returned inadmissible facts; code={:?}",
provider_error.code
);
McpError::new(McpErrorCode::ResourceForbidden, "Authentication failed")
})?;
Ok((fingerprint, Some(auth)))
}
/// Validates Upgrade authorization before the WebSocket `101` commits.
/// The resulting transport-private field is carried to every connection
/// request, where normal request-scoped authentication still strips any
/// recognized in-band fields before middleware can observe them.
#[cfg(feature = "websocket")]
fn authenticate_websocket_upgrade(
&self,
cx: &Cx,
transport_authorization: &TransportAuthorization,
connection_generation: u64,
) -> McpResult<Arc<WebSocketAuthCustody>> {
let request = transport_authorization.auth_request("websocket/upgrade", None, 0);
if self.auth_provider.is_some() && !request.has_any_credential_source() {
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authentication failed",
));
}
self.authenticate_request_without_commit(&McpContext::new(cx.clone(), 0), request)
.map(|(fingerprint, authenticated)| {
Arc::new(WebSocketAuthCustody {
connection_generation,
fingerprint,
authenticated,
rejected_inband_request_ids: Mutex::new(HashSet::new()),
})
})
}
/// Authenticates and parses an out-of-band cancellation before any queue,
/// active-request, or bidirectional waiter state is mutated.
#[cfg(feature = "legacy-2024-11-05")]
fn authenticate_cancelled_control_notification(
&self,
cx: &Cx,
principal_binding: &SessionPrincipalBinding,
era: ProtocolEra,
request: &mut JsonRpcRequest,
http_receipt: Option<&AuthAdmissionReceipt>,
) -> McpResult<CancellationWireMessage> {
let budget = self.create_request_budget(cx);
Self::enforce_request_budget(cx, budget)?;
let (request_ctx, _request_lease_guard) =
McpContext::new(cx.clone(), request_id_to_u64(request.id.as_ref()))
.with_budget_ceiling(budget)
.begin_request_scope()
.ok_or_else(|| {
McpError::internal_error("request scope could not be established")
})?;
let fingerprint = match http_receipt {
Some(receipt) => receipt.commit_legacy(&request_ctx, request)?,
None => self.authenticate_request(
&request_ctx,
AuthRequest {
method: &request.method,
params: request.params.as_ref(),
transport_authorization: None,
request_id: request_id_to_u64(request.id.as_ref()),
},
)?,
};
auth::strip_recognized_access_credentials(&mut request.params);
let cancellation =
CancellationWireMessage::decode(era, CancellationSender::Client, request)
.map_err(|error| McpError::invalid_params(error.to_string()))?;
if !principal_binding.verify_existing(fingerprint) {
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"Authenticated principal does not own an admitted session",
));
}
Self::enforce_request_context(&request_ctx)?;
Ok(cancellation)
}
#[cfg(feature = "legacy-2024-11-05")]
fn request_id_is_active(&self, session_id: u64, request_id: &RequestId) -> bool {
let Ok(key) = ActiveRequestKey::new(session_id, request_id) else {
return false;
};
self.active_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.contains_key(&key)
}
/// Requests cancellation of exactly one active request in the originating
/// session.
///
/// A cancellation notification is advisory: malformed IDs, IDs not owned
/// by this session, and IDs whose request has already left the registry
/// are all ignored. In particular, none of those inputs may trigger
/// cleanup of unrelated bidirectional waiters or alter connection-wide
/// state.
fn handle_cancelled_notification(&self, session_id: u64, params: CancelledParams) -> bool {
self.handle_request_cancellation(session_id, ¶ms.request_id, params.reason.as_deref())
}
fn cancellation_wire_request_id(cancellation: &CancellationWireMessage) -> &RequestId {
match cancellation {
CancellationWireMessage::Legacy2024 { params, .. } => ¶ms.request_id,
CancellationWireMessage::Modern2026 { params, .. } => ¶ms.request_id,
}
}
fn handle_cancellation_wire_notification(
&self,
session_id: u64,
cancellation: CancellationWireMessage,
) -> bool {
match cancellation {
CancellationWireMessage::Legacy2024 { params, .. } => {
self.handle_cancelled_notification(session_id, params)
}
CancellationWireMessage::Modern2026 { params, .. } => self.handle_request_cancellation(
session_id,
¶ms.request_id,
params.reason.as_deref(),
),
}
}
fn handle_request_cancellation(
&self,
session_id: u64,
request_id: &RequestId,
reason: Option<&str>,
) -> bool {
let reason_present = reason.is_some();
let reason = reason.unwrap_or("unspecified");
info!(
target: targets::SESSION,
"Cancellation requested for request_key={:016x} (reason_present={}, reason_bytes={})",
request_id_log_key(request_id),
reason_present,
reason.len()
);
let Ok(active_key) = ActiveRequestKey::new(session_id, request_id) else {
debug!(
target: targets::SESSION,
"Ignoring cancellation with invalid request key={:016x}",
request_id_log_key(request_id)
);
return false;
};
// Keep registry membership and the cancellation transition in the
// same critical section. A finishing request removes itself under this
// lock, so a late notification cannot race from an observed entry into
// a completed request.
let accepted = {
let guard = self.active_requests.lock().unwrap_or_else(|poisoned| {
error!(target: targets::SERVER, "active_requests lock poisoned, recovering");
poisoned.into_inner()
});
guard
.get(&active_key)
.map(|entry| entry.cancellation.cancel())
};
let Some(accepted) = accepted else {
debug!(
target: targets::SESSION,
"Ignoring cancellation for unknown or completed request_key={:016x}",
request_id_log_key(request_id)
);
return false;
};
if !accepted {
debug!(
target: targets::SESSION,
"Ignoring duplicate or finalized cancellation for request_key={:016x}",
request_id_log_key(request_id)
);
}
accepted
}
/// Cancels every active request as part of server-owned shutdown.
///
/// `wait_for_shutdown_cleanup` is an internal lifecycle control and is
/// deliberately unrelated to MCP cancellation notification parameters.
fn cancel_active_requests(
&self,
kind: CancelKind,
wait_for_shutdown_cleanup: bool,
) -> ShutdownCleanupOutcome {
let active: Vec<(
ActiveRequestKey,
RegionId,
Arc<OnceLock<Cx>>,
McpRequestCancellation,
Arc<RequestCompletion>,
)> = {
let guard = self.active_requests.lock().unwrap_or_else(|poisoned| {
error!(target: targets::SERVER, "active_requests lock poisoned in cancel_active_requests, recovering");
poisoned.into_inner()
});
guard
.iter()
.map(|(key, entry)| {
(
key.clone(),
entry.region_id,
entry.cx.clone(),
entry.cancellation.clone(),
entry.completion.clone(),
)
})
.collect()
};
if active.is_empty() {
return ShutdownCleanupOutcome::Quiescent;
}
info!(
target: targets::SESSION,
"Cancelling {} active request(s) (kind={:?}, wait_for_shutdown_cleanup={})",
active.len(),
kind,
wait_for_shutdown_cleanup
);
for (_, _, cx, cancellation, _) in &active {
cancellation.cancel();
if let Some(cx) = cx.get() {
cx.cancel_with(kind, None);
}
}
if wait_for_shutdown_cleanup {
let cleanup_started = Instant::now();
let mut remaining = 0;
for (key, region_id, _cx, _cancellation, completion) in active {
let timeout = SHUTDOWN_CLEANUP_TIMEOUT.saturating_sub(cleanup_started.elapsed());
let completed = completion.wait_timeout(timeout);
if !completed {
remaining += 1;
fastmcp_core::logging::warn!(
target: targets::SESSION,
"Shutdown cancel timed out for session={} request_key={:016x} (ambient_region={:?})",
key.session_id,
key.log_key(),
region_id
);
}
}
let still_active = self
.active_requests
.lock()
.unwrap_or_else(|poisoned| {
error!(target: targets::SERVER, "active_requests lock poisoned after shutdown cleanup, recovering");
poisoned.into_inner()
})
.len();
if remaining != 0 || still_active != 0 {
return ShutdownCleanupOutcome::TimedOut {
remaining: remaining.max(still_active),
};
}
}
ShutdownCleanupOutcome::Quiescent
}
fn handle_set_log_level(&self, session: &mut Session, params: SetLogLevelParams) {
let requested = match params.level {
LogLevel::Debug => LevelFilter::Debug,
LogLevel::Info | LogLevel::Notice => LevelFilter::Info,
LogLevel::Warning => LevelFilter::Warn,
LogLevel::Error | LogLevel::Critical | LogLevel::Alert | LogLevel::Emergency => {
LevelFilter::Error
}
};
let configured = self.logging.level;
let effective = if requested > configured {
configured
} else {
requested
};
if effective == LevelFilter::Off {
session.restore_log_level(None);
fastmcp_core::logging::warn!(
target: targets::SESSION,
"Client requested log level {:?}; server logging is disabled",
params.level
);
return;
}
let effective_level = if effective == requested {
params.level
} else {
match effective {
LevelFilter::Debug => LogLevel::Debug,
LevelFilter::Info => LogLevel::Info,
LevelFilter::Warn => LogLevel::Warning,
LevelFilter::Error => LogLevel::Error,
// A client cannot request Trace through MCP; defensively clamp a
// future/internal Trace value to the most verbose protocol level.
LevelFilter::Trace => LogLevel::Debug,
// Handled by the early return above.
LevelFilter::Off => LogLevel::Error,
}
};
session.set_log_level(effective_level);
if effective != requested {
fastmcp_core::logging::warn!(
target: targets::SESSION,
"Client requested log level {:?}; provisional session level clamped to server level {:?}",
params.level,
effective
);
} else {
info!(
target: targets::SESSION,
"Provisional session log level set to {:?}",
params.level
);
}
}
fn log_level_rank(level: LogLevel) -> u8 {
match level {
LogLevel::Debug => 1,
LogLevel::Info => 2,
LogLevel::Notice => 3,
LogLevel::Warning => 4,
LogLevel::Error => 5,
LogLevel::Critical => 6,
LogLevel::Alert => 7,
LogLevel::Emergency => 8,
}
}
fn request_progress_marker(request: &JsonRpcRequest) -> Option<ProgressMarker> {
request
.params
.as_ref()?
.get("_meta")?
.get("progressToken")
.cloned()
.and_then(|marker| serde_json::from_value(marker).ok())
}
fn final_log_level_rank(level: LoggingLevel) -> u8 {
match level {
LoggingLevel::Debug => 1,
LoggingLevel::Info => 2,
LoggingLevel::Notice => 3,
LoggingLevel::Warning => 4,
LoggingLevel::Error => 5,
LoggingLevel::Critical => 6,
LoggingLevel::Alert => 7,
LoggingLevel::Emergency => 8,
}
}
fn final_logging_ceiling(&self) -> Option<LoggingLevel> {
match self.logging.level {
LevelFilter::Off => None,
LevelFilter::Trace | LevelFilter::Debug => Some(LoggingLevel::Debug),
LevelFilter::Info => Some(LoggingLevel::Info),
LevelFilter::Warn => Some(LoggingLevel::Warning),
LevelFilter::Error => Some(LoggingLevel::Error),
}
}
fn mcp_log_level_from_final(level: LoggingLevel) -> McpLogLevel {
match level {
LoggingLevel::Debug => McpLogLevel::Debug,
LoggingLevel::Info => McpLogLevel::Info,
LoggingLevel::Notice => McpLogLevel::Notice,
LoggingLevel::Warning => McpLogLevel::Warning,
LoggingLevel::Error => McpLogLevel::Error,
LoggingLevel::Critical => McpLogLevel::Critical,
LoggingLevel::Alert => McpLogLevel::Alert,
LoggingLevel::Emergency => McpLogLevel::Emergency,
}
}
fn final_request_log_level(&self, request: &JsonRpcRequest) -> Option<LoggingLevel> {
if modern_protocol_version(request) != Some(MODERN_PROTOCOL_VERSION) {
return None;
}
let entries: BTreeMap<String, serde_json::Value> = request
.params
.as_ref()?
.get("_meta")?
.as_object()?
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
let requested = OpenMetadata::try_from_entries(entries)
.ok()?
.log_level()
.ok()??;
let ceiling = self.final_logging_ceiling()?;
Some(
if Self::final_log_level_rank(requested) < Self::final_log_level_rank(ceiling) {
ceiling
} else {
requested
},
)
}
fn modern_request_requires_owned_sse(&self, request: &JsonRpcRequest) -> bool {
if request.id.is_none() || request.method.starts_with("notifications/") {
return false;
}
if Self::request_progress_marker(request).is_some() {
return true;
}
self.final_request_log_level(request)
.is_some_and(|minimum| {
Self::final_log_level_rank(LoggingLevel::Error)
>= Self::final_log_level_rank(minimum)
})
}
fn final_log_notification_metadata(&self) -> Option<OpenMetadata> {
let server_info =
Implementation::try_new(self.info.name.clone(), self.info.version.clone()).ok()?;
OpenMetadata::try_from_entries([(
FINAL_SERVER_INFO_META_KEY.to_owned(),
serde_json::to_value(server_info).ok()?,
)])
.ok()
}
fn maybe_emit_final_log_notification(
&self,
request: &JsonRpcRequest,
sender: &NotificationSender,
result_is_ok: bool,
) {
if request.method.starts_with("notifications/") {
return;
}
let Some(min_level) = self.final_request_log_level(request) else {
return;
};
let level = if result_is_ok {
LoggingLevel::Info
} else {
LoggingLevel::Error
};
if Self::final_log_level_rank(level) < Self::final_log_level_rank(min_level) {
return;
}
let Some(meta) = self.final_log_notification_metadata() else {
return;
};
let method_key = safe_peer_log_key(&request.method);
let message = if result_is_ok {
format!("Handled method={method_key}")
} else {
format!("Error handling method={method_key}")
};
let notification = ServerNotification::Message(FinalLogMessageParams {
level,
logger: Some("fastmcp_rust::server".to_owned()),
data: serde_json::Value::String(message),
meta: Some(meta),
additional: BTreeMap::new(),
})
.encode();
let Ok(notification) = notification else {
return;
};
if catch_extension_unwind(|| sender(notification)).is_err() {
let _ = extension_panic_error("final_log_notification_sender");
}
}
fn emit_log_notification_for_level(
&self,
min_level: Option<LogLevel>,
sender: &NotificationSender,
level: LogLevel,
message: impl Into<String>,
) {
let Some(min_level) = min_level else {
return;
};
if Self::log_level_rank(level) < Self::log_level_rank(min_level) {
return;
}
let ts = chrono::Utc::now().to_rfc3339();
let text = format!("{ts} {}", message.into());
let params = LogMessageParams {
level,
logger: Some("fastmcp_rust::server".to_string()),
data: serde_json::Value::String(text),
};
let payload = match serde_json::to_value(params) {
Ok(value) => value,
Err(err) => {
fastmcp_core::logging::warn!(
target: targets::SESSION,
"Failed to serialize log message notification: {}",
err
);
return;
}
};
let notification = JsonRpcRequest::notification("notifications/message", Some(payload));
if catch_extension_unwind(|| sender(notification)).is_err() {
// Notification delivery is an application-supplied extension
// boundary. A broken callback must not unwind across dispatch and
// discard the already-computed JSON-RPC response. Keep both this
// diagnostic and the panic hook payload-free.
let _ = extension_panic_error("log_notification_sender");
}
}
fn emit_log_notification(
&self,
session: &Session,
sender: &NotificationSender,
level: LogLevel,
message: impl Into<String>,
) {
self.emit_log_notification_for_level(session.log_level(), sender, level, message);
}
#[cfg(feature = "legacy-2024-11-05")]
fn maybe_emit_log_notification_for_level(
&self,
min_level: Option<LogLevel>,
sender: &NotificationSender,
method: &str,
result: &McpResult<serde_json::Value>,
) {
if method.starts_with("notifications/") || method == "logging/setLevel" {
return;
}
let level = if result.is_ok() {
LogLevel::Info
} else {
LogLevel::Error
};
let method_key = safe_peer_log_key(method);
let message = if result.is_ok() {
format!("Handled method={method_key}")
} else {
format!("Error handling method={method_key}")
};
self.emit_log_notification_for_level(min_level, sender, level, message);
}
fn maybe_emit_log_notification(
&self,
session: &Session,
sender: &NotificationSender,
method: &str,
result: &McpResult<serde_json::Value>,
) {
if method.starts_with("notifications/") || method == "logging/setLevel" {
return;
}
let level = if result.is_ok() {
LogLevel::Info
} else {
LogLevel::Error
};
let method_key = safe_peer_log_key(method);
let message = if result.is_ok() {
format!("Handled method={method_key}")
} else {
format!("Error handling method={method_key}")
};
self.emit_log_notification(session, sender, level, message);
}
}
/// Upper bound for server-owned shutdown settlement after cancellation.
const SHUTDOWN_CLEANUP_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ShutdownCleanupOutcome {
Quiescent,
TimedOut { remaining: usize },
}
impl ShutdownCleanupOutcome {
fn into_error(self) -> Option<McpError> {
match self {
Self::Quiescent => None,
Self::TimedOut { remaining } => Some(McpError::with_data(
McpErrorCode::InternalError,
"Server shutdown cleanup did not quiesce within its bound",
serde_json::json!({
"stage": "shutdown",
"kind": "non_quiescent",
"remaining": remaining,
}),
)),
}
}
}
struct RequestCompletion {
done: Mutex<bool>,
cv: Condvar,
}
impl RequestCompletion {
fn new() -> Self {
Self {
done: Mutex::new(false),
cv: Condvar::new(),
}
}
fn mark_done(&self) {
let mut done = self
.done
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !*done {
*done = true;
self.cv.notify_all();
}
}
fn wait_timeout(&self, timeout: Duration) -> bool {
let mut done = self
.done
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if *done {
return true;
}
let start = Instant::now();
let mut remaining = timeout;
loop {
let (guard, result) = self
.cv
.wait_timeout(done, remaining)
.unwrap_or_else(std::sync::PoisonError::into_inner);
done = guard;
if *done {
return true;
}
if result.timed_out() {
return false;
}
let elapsed = start.elapsed();
remaining = match timeout.checked_sub(elapsed) {
Some(left) if !left.is_zero() => left,
_ => return false,
};
}
}
#[cfg(test)]
fn is_done(&self) -> bool {
let done = self
.done
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*done
}
}
struct ActiveRequest {
cx: Arc<OnceLock<Cx>>,
cancellation: McpRequestCancellation,
region_id: RegionId,
completion: Arc<RequestCompletion>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct ActiveRequestKey {
session_id: u64,
correlation_key: CorrelationKey,
}
impl ActiveRequestKey {
fn new(session_id: u64, request_id: &RequestId) -> Result<Self, &'static str> {
Ok(Self {
session_id,
correlation_key: request_id.correlation_key()?,
})
}
fn log_key(&self) -> u64 {
let (domain, value) = match &self.correlation_key {
CorrelationKey::String(value) => ("string:", value),
CorrelationKey::Integer(value) => ("integer:", value),
};
stable_hash_request_id(domain) ^ stable_hash_request_id(value)
}
}
impl ActiveRequest {
#[cfg(test)]
fn new(cx: Cx, completion: Arc<RequestCompletion>) -> Self {
Self::with_cancellation(cx, completion, McpRequestCancellation::new())
}
fn with_cancellation(
cx: Cx,
completion: Arc<RequestCompletion>,
cancellation: McpRequestCancellation,
) -> Self {
let region_id = cx.region_id();
Self {
cx: Arc::new(OnceLock::from(cx)),
cancellation,
region_id,
completion,
}
}
}
struct ActiveRequestGuard {
map: Arc<Mutex<HashMap<ActiveRequestKey, ActiveRequest>>>,
key: ActiveRequestKey,
cx: Arc<OnceLock<Cx>>,
cancellation: McpRequestCancellation,
completion: Arc<RequestCompletion>,
}
impl ActiveRequestGuard {
#[cfg(test)]
fn try_new(
map: Arc<Mutex<HashMap<ActiveRequestKey, ActiveRequest>>>,
session_id: u64,
id: RequestId,
cx: Cx,
) -> Result<Self, RequestId> {
Self::try_new_with_cancellation(map, session_id, id, cx, McpRequestCancellation::new())
}
fn try_new_with_cancellation(
map: Arc<Mutex<HashMap<ActiveRequestKey, ActiveRequest>>>,
session_id: u64,
id: RequestId,
cx: Cx,
cancellation: McpRequestCancellation,
) -> Result<Self, RequestId> {
let completion = Arc::new(RequestCompletion::new());
let entry = ActiveRequest::with_cancellation(cx, completion, cancellation);
Self::try_insert(map, session_id, id, entry)
}
fn try_reserve(
map: Arc<Mutex<HashMap<ActiveRequestKey, ActiveRequest>>>,
session_id: u64,
id: RequestId,
owner_region_id: RegionId,
cancellation: McpRequestCancellation,
) -> Result<Self, RequestId> {
Self::try_insert(
map,
session_id,
id,
ActiveRequest {
cx: Arc::new(OnceLock::new()),
cancellation,
region_id: owner_region_id,
completion: Arc::new(RequestCompletion::new()),
},
)
}
fn try_insert(
map: Arc<Mutex<HashMap<ActiveRequestKey, ActiveRequest>>>,
session_id: u64,
id: RequestId,
entry: ActiveRequest,
) -> Result<Self, RequestId> {
let key = ActiveRequestKey::new(session_id, &id).map_err(|_| id.clone())?;
let cx = Arc::clone(&entry.cx);
let completion = Arc::clone(&entry.completion);
let cancellation = entry.cancellation.clone();
let mut guard = map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if guard.contains_key(&key) {
fastmcp_core::logging::warn!(
target: targets::SESSION,
"Duplicate active request_key={:016x} rejected while an earlier request is still running",
request_id_log_key(&id)
);
return Err(id);
}
guard.insert(key.clone(), entry);
drop(guard);
Ok(Self {
map,
key,
cx,
cancellation,
completion,
})
}
fn activate(&self, cx: Cx) -> bool {
// Publish before checking the token. Shutdown cancels the token before
// reading this same cell, so either it sees the child or activation
// observes cancellation. Never cancel the receive pump's parent Cx.
if self.cx.set(cx.clone()).is_err() {
return false;
}
if self.cancellation.is_cancel_requested() {
cx.cancel_with(CancelKind::Shutdown, None);
return false;
}
true
}
fn cancellation(&self) -> McpRequestCancellation {
self.cancellation.clone()
}
}
impl Drop for ActiveRequestGuard {
fn drop(&mut self) {
{
let mut guard = self
.map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match guard.get(&self.key) {
Some(entry) if Arc::ptr_eq(&entry.completion, &self.completion) => {
guard.remove(&self.key);
}
Some(_) => {
fastmcp_core::logging::warn!(
target: targets::SESSION,
"Active request replaced before drop for session={} request_key={:016x}",
self.key.session_id,
self.key.log_key()
);
}
None => {
fastmcp_core::logging::warn!(
target: targets::SESSION,
"Active request missing on drop for session={} request_key={:016x}",
self.key.session_id,
self.key.log_key()
);
}
}
}
self.completion.mark_done();
}
}
/// Keeps request cancellation ownership and reversible session mutations alive
/// until a direct caller accepts the response or the transport path has
/// acquired exclusive output ownership for one response attempt.
///
/// Exclusive ownership prevents another frame from overtaking the response,
/// but synchronous `Write`/`flush` remain fallible and cannot provide an atomic
/// wire-commit primitive. A failed attempt therefore still rolls back reversible
/// session mutations and is never counted as a sent response.
struct HandledRequest {
response: JsonRpcResponse,
cancellation: Option<McpRequestCancellation>,
suppress_cancelled_response: bool,
_active_guard: Option<ActiveRequestGuard>,
session_mutation_rollback: Option<SessionMutationRollback>,
deferred_stats: Option<DeferredRequestStats>,
commit_liveness: Option<CommitLiveness>,
}
struct CommitLiveness {
cx: Cx,
budget: Budget,
}
#[derive(Clone, Copy)]
enum DeferredRequestOutcome {
Success,
Failure,
Cancelled,
}
struct DeferredRequestStats {
collector: ServerStats,
method: String,
started_at: Instant,
outcome: DeferredRequestOutcome,
}
impl DeferredRequestStats {
fn new(
collector: Option<&ServerStats>,
method: &str,
started_at: Instant,
outcome: DeferredRequestOutcome,
) -> Option<Self> {
collector.map(|collector| Self {
collector: collector.clone(),
method: method.to_string(),
started_at,
outcome,
})
}
fn record(self) {
let latency = self.started_at.elapsed();
match self.outcome {
DeferredRequestOutcome::Success => {
self.collector.record_request(&self.method, latency, true);
}
DeferredRequestOutcome::Failure => {
self.collector.record_request(&self.method, latency, false);
}
DeferredRequestOutcome::Cancelled => {
self.collector.record_cancelled(&self.method, latency);
}
}
}
}
impl HandledRequest {
fn untracked(response: JsonRpcResponse) -> Self {
Self {
response,
cancellation: None,
suppress_cancelled_response: false,
_active_guard: None,
session_mutation_rollback: None,
deferred_stats: None,
commit_liveness: None,
}
}
fn tracked(
response: JsonRpcResponse,
cancellation: McpRequestCancellation,
active_guard: Option<ActiveRequestGuard>,
session_mutation_rollback: Option<SessionMutationRollback>,
commit_cx: Cx,
commit_budget: Budget,
) -> Self {
Self {
response,
cancellation: Some(cancellation),
suppress_cancelled_response: false,
_active_guard: active_guard,
session_mutation_rollback,
deferred_stats: None,
commit_liveness: Some(CommitLiveness {
cx: commit_cx,
budget: commit_budget,
}),
}
}
fn with_deferred_stats(mut self, deferred_stats: Option<DeferredRequestStats>) -> Self {
self.deferred_stats = deferred_stats;
self
}
fn suppress_cancelled_response(mut self) -> Self {
self.suppress_cancelled_response = true;
self
}
fn commit_liveness_error(&self) -> Option<McpError> {
self.commit_liveness
.as_ref()
.and_then(|liveness| Server::request_budget_error(&liveness.cx, liveness.budget))
}
fn replace_with_terminal_error(&mut self, session: &mut Session, error: McpError) {
if let Some(rollback) = self.session_mutation_rollback.take() {
rollback.apply(session);
}
if let Some(stats) = self.deferred_stats.as_mut() {
stats.outcome = DeferredRequestOutcome::Cancelled;
}
self.response = JsonRpcResponse::error(
self.response.id.clone(),
JsonRpcError {
code: error.code.into(),
message: error.message,
data: error.data,
},
);
}
/// Resolves the final response/cancellation race and reports whether an
/// explicit request cancellation won. Ambient cancellation and deadline
/// expiry still produce their ordinary terminal error response.
fn resolve_commit_race(&mut self, session: &mut Session) -> bool {
// An explicit in-band cancellation that already linearized wins
// outright: its suppression contract holds even when ambient shutdown
// or deadline expiry lands concurrently with response finalization.
// This must not eagerly CAS to FINALIZING, because ambient death below
// still needs to propagate into the token for `cancelled()` waiters.
let explicit_cancellation_linearized = self
.cancellation
.as_ref()
.is_some_and(McpRequestCancellation::is_cancel_requested);
if explicit_cancellation_linearized {
// A server-side graceful election cancels its listen as the
// dispatch wake; the elected terminal completion is that
// request's sanctioned final frame, not a response that lost a
// cancellation race.
if final_subscription_completion_response(&self.response) {
return false;
}
self.replace_with_terminal_error(session, McpError::request_cancelled());
return true;
}
if let Some(error) = self.commit_liveness_error() {
if let Some(cancellation) = &self.cancellation {
let _ = cancellation.cancel();
}
self.replace_with_terminal_error(session, error);
return false;
}
let cancellation_won = self
.cancellation
.as_ref()
.is_some_and(|cancellation| !cancellation.begin_finalization());
if cancellation_won {
if final_subscription_completion_response(&self.response) {
return false;
}
self.replace_with_terminal_error(session, McpError::request_cancelled());
return true;
}
// The token CAS is the explicit-cancellation linearization point. A
// second ambient/deadline check closes the interval between the first
// liveness snapshot and that CAS. Cancellation after this snapshot
// loses to response finalization.
if let Some(error) = self.commit_liveness_error() {
self.replace_with_terminal_error(session, error);
}
false
}
fn finalize_for_return(mut self, session: &mut Session) -> JsonRpcResponse {
let _ = self.resolve_commit_race(session);
self.session_mutation_rollback.take();
if let Some(stats) = self.deferred_stats.take() {
stats.record();
}
self.response
}
#[cfg(feature = "legacy-2024-11-05")]
fn send_with<F>(
mut self,
session: &mut Session,
send: F,
) -> Result<Option<JsonRpcResponse>, TransportError>
where
F: FnOnce(&JsonRpcResponse) -> Result<(), TransportError>,
{
// Callers must acquire exclusive output ownership before entering this
// method. Finalization then linearizes against cancellation immediately
// before the single fallible write/flush attempt.
let cancellation_won = self.resolve_commit_race(session);
if cancellation_won && self.suppress_cancelled_response {
if let Some(stats) = self.deferred_stats.take() {
stats.record();
}
return Ok(None);
}
match send(&self.response) {
Ok(()) => {
self.session_mutation_rollback.take();
if let Some(stats) = self.deferred_stats.take() {
stats.record();
}
Ok(Some(self.response))
}
Err(error) => {
if let Some(rollback) = self.session_mutation_rollback.take() {
rollback.apply(session);
}
Err(error)
}
}
}
}
/// Checks if banner should be suppressed via environment variable.
///
/// This is a legacy check. Prefer using `ConsoleConfig` for banner control.
fn banner_suppressed() -> bool {
std::env::var("FASTMCP_NO_BANNER")
.map(|value| matches!(value.to_lowercase().as_str(), "1" | "true" | "yes"))
.unwrap_or(false)
}
/// Parses required parameters from JSON.
fn parse_params<T: serde::de::DeserializeOwned>(
params: Option<serde_json::Value>,
) -> Result<T, McpError> {
let value = params.ok_or_else(|| McpError::invalid_params("Missing required parameters"))?;
serde_json::from_value(value).map_err(|e| McpError::invalid_params(e.to_string()))
}
/// Parses optional parameters from JSON, using default if not provided.
fn parse_params_or_default<T: serde::de::DeserializeOwned + Default>(
params: Option<serde_json::Value>,
) -> Result<T, McpError> {
match params {
Some(value) => {
serde_json::from_value(value).map_err(|e| McpError::invalid_params(e.to_string()))
}
None => Ok(T::default()),
}
}
/// Converts a JSON-RPC RequestId to a u64 for internal tracking.
///
/// If the ID is representable as an `i64`, uses that number. Arbitrary-
/// precision integer and string IDs use a stable hash; absent IDs use zero.
fn request_id_to_u64(id: Option<&RequestId>) -> u64 {
match id {
Some(RequestId::Number(n)) => *n as u64,
Some(RequestId::Integer(integer)) => stable_hash_request_id(integer),
Some(RequestId::String(s)) => stable_hash_request_id(s),
None => 0,
}
}
fn request_id_log_key(id: &RequestId) -> u64 {
request_id_to_u64(Some(id))
}
const PEER_LOG_KEY_INPUT_BYTES: usize = 4 * 1024;
const PEER_LOG_KEY_PREFIX_BYTES: usize = 8;
#[derive(Clone, Copy)]
struct SafePeerLogKey {
byte_len: usize,
hashed_bytes: usize,
digest_prefix: [u8; PEER_LOG_KEY_PREFIX_BYTES],
}
impl std::fmt::Display for SafePeerLogKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "bytes={},sha256_prefix=", self.byte_len)?;
for byte in self.digest_prefix {
write!(f, "{byte:02x}")?;
}
if self.hashed_bytes < self.byte_len {
write!(f, ",hashed_prefix_bytes={}", self.hashed_bytes)?;
}
Ok(())
}
}
fn safe_peer_log_key(value: &str) -> SafePeerLogKey {
let bytes = value.as_bytes();
let hashed_bytes = bytes.len().min(PEER_LOG_KEY_INPUT_BYTES);
let mut digest_prefix = [0_u8; PEER_LOG_KEY_PREFIX_BYTES];
if let Ok(digest) = sha256_bounded(&bytes[..hashed_bytes], PEER_LOG_KEY_INPUT_BYTES) {
digest_prefix.copy_from_slice(&digest.as_bytes()[..PEER_LOG_KEY_PREFIX_BYTES]);
}
SafePeerLogKey {
byte_len: bytes.len(),
hashed_bytes,
digest_prefix,
}
}
fn stable_hash_request_id(value: &str) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = FNV_OFFSET;
for byte in value.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
if hash == 0 { FNV_OFFSET } else { hash }
}
/// An unsplit [`Transport`] shared by the receive pump and every writer.
///
/// `recv` and `send` both need `&mut T`, so the pump owns the I/O handle for as
/// long as a `recv` blocks. A send from another thread in that window (a listen
/// acknowledgement, a notification, a worker's response) is queued in
/// [`UnsplitOutput`] instead of failing, and the owner writes the queue in
/// order before it gives the handle up. Output produced while `recv` blocks
/// therefore reaches the peer as soon as that `recv` returns, ahead of anything
/// written later (bd-8bcfq).
struct SharedTransport<T> {
inner: Arc<Mutex<Option<T>>>,
output: Arc<UnsplitOutput>,
}
/// Output queued behind one owner of an unsplit transport is bounded; past
/// this a send fails instead of growing without limit.
const UNSPLIT_OUTPUT_QUEUE_LIMIT: usize = 1024;
struct UnsplitOutput {
state: Mutex<UnsplitOutputState>,
released: Condvar,
/// Latched when queued output cannot be written. Its senders were already
/// told `Ok`, so the pump must fail the connection rather than lose it.
failure: Arc<AtomicBool>,
}
#[derive(Default)]
struct UnsplitOutputState {
owned: bool,
queue: VecDeque<JsonRpcMessage>,
failed: bool,
}
impl UnsplitOutput {
fn new(failure: Arc<AtomicBool>) -> Self {
Self {
state: Mutex::new(UnsplitOutputState::default()),
released: Condvar::new(),
failure,
}
}
fn state(&self) -> std::sync::MutexGuard<'_, UnsplitOutputState> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
/// Takes the handle for a receive or close, waiting out a writer.
fn claim(&self) -> UnsplitOwnership<'_> {
let mut state = self.state();
while state.owned {
state = self
.released
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
state.owned = true;
UnsplitOwnership {
output: self,
released: false,
}
}
/// Takes the handle for a send, or queues `message` behind the current
/// owner, which writes it before releasing the handle (`Ok(None)`).
fn claim_or_queue(
&self,
message: &JsonRpcMessage,
) -> Result<Option<UnsplitOwnership<'_>>, TransportError> {
let mut state = self.state();
if state.failed {
return Err(TransportError::Io(std::io::Error::other(
"an earlier queued send on this unsplit transport failed",
)));
}
if !state.owned {
state.owned = true;
return Ok(Some(UnsplitOwnership {
output: self,
released: false,
}));
}
if state.queue.len() >= UNSPLIT_OUTPUT_QUEUE_LIMIT {
return Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"unsplit transport output queue is full while another caller owns the I/O handle",
)));
}
state.queue.push_back(message.clone());
Ok(None)
}
fn fail(&self, error: &TransportError) {
self.state().failed = true;
self.failure.store(true, Ordering::Release);
log::error!(
target: targets::TRANSPORT,
"Failed to send queued unsplit transport output: {}",
error
);
}
}
/// Ownership of an unsplit transport's I/O handle. Releasing it writes all
/// output queued behind it first; an unwind releases it without writing.
struct UnsplitOwnership<'a> {
output: &'a UnsplitOutput,
released: bool,
}
impl UnsplitOwnership<'_> {
fn release<T: Transport>(mut self, cx: &Cx, mut transport: Option<&mut T>) {
loop {
let batch = {
let mut state = self.output.state();
if state.queue.is_empty() {
// Checked and released under one lock, so a sender either
// queued before this point or finds the handle free.
state.owned = false;
drop(state);
self.released = true;
self.output.released.notify_all();
return;
}
std::mem::take(&mut state.queue)
};
let Some(writer) = transport.as_deref_mut() else {
// Closed (or already failed): nothing queued can be written.
continue;
};
for message in &batch {
if let Err(error) = writer.send(cx, message) {
if !error.is_cancelled() {
self.output.fail(&error);
}
transport = None;
break;
}
}
}
}
}
impl Drop for UnsplitOwnership<'_> {
fn drop(&mut self) {
if self.released {
return;
}
let mut state = self.output.state();
state.owned = false;
if !state.queue.is_empty() {
state.queue.clear();
state.failed = true;
self.output.failure.store(true, Ordering::Release);
}
drop(state);
self.output.released.notify_all();
}
}
struct SharedRecvHalf<R> {
inner: Arc<Mutex<R>>,
}
impl<R> Clone for SharedRecvHalf<R> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<R: TransportRecvHalf> SharedRecvHalf<R> {
fn new(inner: R) -> Self {
Self {
inner: Arc::new(Mutex::new(inner)),
}
}
fn recv(&self, cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
self.inner
.lock()
.map_err(|_| transport_lock_error())?
.recv(cx)
}
fn close(&self, cx: &Cx) -> Result<(), TransportError> {
self.inner
.lock()
.map_err(|_| transport_lock_error())?
.close(cx)
}
}
struct SharedSendHalf<S> {
inner: Arc<Mutex<S>>,
}
impl<S> Clone for SharedSendHalf<S> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl<S: TransportSendHalf> SharedSendHalf<S> {
fn new(inner: S) -> Self {
Self {
inner: Arc::new(Mutex::new(inner)),
}
}
fn send(&self, cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
self.inner
.lock()
.map_err(|_| transport_lock_error())?
.send(cx, message)
}
fn close(&self, cx: &Cx) -> Result<(), TransportError> {
self.inner
.lock()
.map_err(|_| transport_lock_error())?
.close(cx)
}
}
impl<T> Clone for SharedTransport<T> {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
output: Arc::clone(&self.output),
}
}
}
impl<T: Transport> SharedTransport<T> {
#[cfg(test)]
fn new(transport: T) -> Self {
Self::with_output_failure(transport, Arc::new(AtomicBool::new(false)))
}
/// `output_failure` is latched when output queued during another owner's
/// I/O cannot be written; the pump checks it after every `recv`.
fn with_output_failure(transport: T, output_failure: Arc<AtomicBool>) -> Self {
Self {
inner: Arc::new(Mutex::new(Some(transport))),
output: Arc::new(UnsplitOutput::new(output_failure)),
}
}
fn recv(&self, cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let ownership = self.output.claim();
let mut guard = self.inner.lock().map_err(|_| transport_lock_error())?;
let result = guard
.as_mut()
.map_or(Err(TransportError::Closed), |transport| transport.recv(cx));
// Output queued while this `recv` blocked goes out before the pump
// dispatches what it returned.
ownership.release(cx, guard.as_mut());
result
}
fn send(&self, cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
let Some(ownership) = self.output.claim_or_queue(message)? else {
return Ok(());
};
let mut guard = self.inner.lock().map_err(|_| transport_lock_error())?;
let result = guard
.as_mut()
.map_or(Err(TransportError::Closed), |transport| {
transport.send(cx, message)
});
ownership.release(cx, guard.as_mut());
result
}
fn close(&self, cx: &Cx) -> Result<(), TransportError> {
let ownership = self.output.claim();
let transport = {
let mut guard = self.inner.lock().map_err(|_| transport_lock_error())?;
ownership.release(cx, guard.as_mut());
guard.take()
};
let Some(mut transport) = transport else {
return Ok(());
};
transport.close(cx)
}
}
fn transport_lock_error() -> TransportError {
TransportError::Io(std::io::Error::other("transport lock poisoned"))
}
fn create_transport_notification_sender<T>(
transport: SharedTransport<T>,
cx: Cx,
failure: Arc<AtomicBool>,
) -> NotificationSender
where
T: Transport + Send + 'static,
{
Arc::new(move |request: JsonRpcRequest| {
let message = JsonRpcMessage::Request(request);
if let Err(e) = transport.send(&cx, &message) {
if !e.is_cancelled() {
failure.store(true, Ordering::Release);
}
log::error!(
target: targets::TRANSPORT,
"Failed to send notification: {}",
e
);
}
})
}
fn create_split_transport_notification_sender<S>(
transport: SharedSendHalf<S>,
cx: Cx,
failure: Arc<AtomicBool>,
) -> NotificationSender
where
S: TransportSendHalf + 'static,
{
Arc::new(move |request: JsonRpcRequest| {
if let Err(error) = transport.send(&cx, &JsonRpcMessage::Request(request))
&& !error.is_cancelled()
{
failure.store(true, Ordering::Release);
}
})
}
fn combine_split_transport_results(
run: McpResult<()>,
recv_close: McpResult<()>,
send_close: McpResult<()>,
) -> McpResult<()> {
[recv_close, send_close]
.into_iter()
.fold(run, |result, close| match (result, close) {
(Ok(()), Ok(())) => Ok(()),
(Err(error), Ok(())) | (Ok(()), Err(error)) => Err(error),
(Err(run_error), Err(close_error)) => {
Err(combined_run_and_close_error(run_error, close_error))
}
})
}
/// Creates a notification sender that writes JSON-RPC notifications to stdout.
///
/// This creates a separate stdout handle for sending notifications, allowing
/// notifications (like progress updates) to be sent during handler execution
/// independently of the main transport.
///
/// The sender uses NDJSON format (newline-delimited JSON) to match the
/// standard MCP transport format.
fn create_notification_sender(fatal_output: Arc<AtomicBool>) -> NotificationSender {
use std::sync::Mutex;
// Use AsyncStdout so notifications share the global stdout lock used by
// the transport writer, preventing interleaved NDJSON writes.
let stdout = Mutex::new(AsyncStdout::new());
let codec = Codec::new();
Arc::new(move |request: JsonRpcRequest| {
let bytes = match codec.encode_request(&request) {
Ok(b) => b,
Err(e) => {
log::error!(target: targets::SERVER, "Failed to encode notification: {}", e);
fatal_output.store(true, Ordering::Release);
return;
}
};
let Ok(mut stdout) = stdout.lock() else {
log::error!(target: targets::SERVER, "Failed to acquire stdout lock for notification");
fatal_output.store(true, Ordering::Release);
return;
};
#[cfg(unix)]
let write_result = stdout.write_all_bounded(&bytes, STDIO_OUTPUT_COMMIT_TIMEOUT);
#[cfg(not(unix))]
let write_result = stdout.write_all_unchecked(&bytes);
if let Err(e) = write_result {
log::error!(target: targets::TRANSPORT, "Failed to send notification: {}", e);
fatal_output.store(true, Ordering::Release);
#[cfg(not(unix))]
return;
}
#[cfg(not(unix))]
if let Err(e) = stdout.flush_unchecked() {
log::error!(target: targets::TRANSPORT, "Failed to flush notification: {}", e);
fatal_output.store(true, Ordering::Release);
}
})
}
/// Test-only construction of this build's live HTTP endpoint.
///
/// The dual-era build takes the origin its exact-legacy SSE route advertises;
/// a no-legacy build has no such route and takes none. A modern test names an
/// origin once and runs in both feature lanes.
#[cfg(test)]
pub(crate) trait TestHttpEndpoint {
fn test_http_endpoint(
self,
legacy_origin: &str,
) -> Result<ServerHttpEndpoint, ServerHttpEndpointError>;
}
#[cfg(test)]
impl TestHttpEndpoint for Server {
fn test_http_endpoint(
self,
legacy_origin: &str,
) -> Result<ServerHttpEndpoint, ServerHttpEndpointError> {
#[cfg(feature = "legacy-2024-11-05")]
{
self.into_http_endpoint(legacy_origin)
}
#[cfg(not(feature = "legacy-2024-11-05"))]
{
let _ = legacy_origin;
self.into_http_endpoint()
}
}
}
#[cfg(test)]
impl TestHttpEndpoint for ServerBuilder {
fn test_http_endpoint(
self,
legacy_origin: &str,
) -> Result<ServerHttpEndpoint, ServerHttpEndpointError> {
#[cfg(feature = "legacy-2024-11-05")]
{
self.build_http_endpoint(legacy_origin)
}
#[cfg(not(feature = "legacy-2024-11-05"))]
{
let _ = legacy_origin;
self.build_http_endpoint()
}
}
}
#[cfg(test)]
mod lib_unit_tests {
use super::*;
use asupersync::io::AsyncReadExt;
use asupersync::runtime::RuntimeBuilder;
use asupersync::runtime::reactor::create_reactor;
use fastmcp_derive::tool;
#[cfg(feature = "tasks")]
use fastmcp_protocol::ExtensionDescriptorRegistry;
use fastmcp_protocol::extensions::ExtensionNegotiationError;
#[cfg(all(feature = "apps", feature = "tasks"))]
use fastmcp_protocol::extensions::{ExtensionInactiveReason, McpAppsClientSettings};
#[cfg(feature = "tasks")]
use fastmcp_protocol::extensions::{
official_tasks_empty_settings, register_official_tasks_extension,
};
use fastmcp_protocol::{
CallToolResult, CompletionValues, Content, FinalCompletionParams, LegacyCompletionParams,
LegacyContent,
};
use fastmcp_protocol::{
CoreResultDiscriminatorPolicy, DecodedResult, ResultPeerEra, decode_peer_result,
};
use std::future::Future;
#[cfg(feature = "builtin-auth-server")]
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::mpsc::{Receiver, TryRecvError, sync_channel};
use std::sync::{Condvar, OnceLock};
use std::thread;
use std::time::Duration;
#[derive(Debug, Default)]
struct HttpOverlapMetrics {
current: AtomicUsize,
max: AtomicUsize,
}
static HTTP_OVERLAP_METRICS: OnceLock<HttpOverlapMetrics> = OnceLock::new();
static HTTP_OVERLAP_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
static HTTP_OVERLAP_CONTROL: OnceLock<HttpOverlapControl> = OnceLock::new();
static LIVE_HTTP_LISTENER_WAITS: AtomicUsize = AtomicUsize::new(0);
static LIVE_HTTP_CONNECTION_READ_WAITS: AtomicUsize = AtomicUsize::new(0);
// Live loopback probes include listener admission, socket I/O, request
// dispatch, cancellation, and structured shutdown. Two seconds was below
// the observed floor on loaded RCH workers and converted otherwise useful
// assertions into harness timeouts. Keep this bounded, but leave enough
// room for the scenario itself to report the precise failed invariant.
const LIVE_HTTP_TEST_TIMEOUT_NANOS: u64 = 10_000_000_000;
// Blocking fixtures and individual setup steps must expire before the
// outer test watchdog so their exact invariant failure is observable.
const LIVE_HTTP_STEP_TIMEOUT_NANOS: u64 = LIVE_HTTP_TEST_TIMEOUT_NANOS / 2;
/// A same-run control for host load. A heartbeat thread asks to sleep
/// [`Self::TICK`] at a time, and whatever it oversleeps is time a runnable
/// thread of this process waited for a CPU. A bound measured in runnable
/// time (wall time less that lag) stretches under host load but still
/// expires on a hang, where the process could run and made no progress.
/// Ten times the bound in wall time expires it regardless, so no amount of
/// lag removes a bound. Reports state the lag, which tells the two apart.
#[derive(Clone)]
pub(crate) struct RunnableClock {
lag_micros: Arc<std::sync::atomic::AtomicU64>,
}
impl RunnableClock {
const TICK: Duration = Duration::from_millis(5);
/// Starts the heartbeat; it stops once every clone is dropped.
pub(crate) fn start() -> Self {
let lag_micros = Arc::new(std::sync::atomic::AtomicU64::new(0));
let heartbeat = Arc::downgrade(&lag_micros);
thread::spawn(move || {
loop {
let asked = Instant::now();
thread::sleep(Self::TICK);
let late = asked.elapsed().saturating_sub(Self::TICK);
let Some(lag) = heartbeat.upgrade() else {
break;
};
lag.fetch_add(
u64::try_from(late.as_micros()).unwrap_or(u64::MAX),
Ordering::AcqRel,
);
}
});
Self { lag_micros }
}
fn lag(&self) -> Duration {
Duration::from_micros(self.lag_micros.load(Ordering::Acquire))
}
/// A point to measure from: the wall instant and the lag so far.
pub(crate) fn mark(&self) -> (Instant, Duration) {
(Instant::now(), self.lag())
}
/// Wall time from `mark` to `now`, less the lag accrued between them.
fn runnable_between(mark: (Instant, Duration), now: (Instant, Duration)) -> Duration {
now.0
.saturating_duration_since(mark.0)
.saturating_sub(now.1.saturating_sub(mark.1))
}
/// Whether `bound` of runnable time, or ten times it of wall time,
/// passed from `mark` to `now`.
fn expired_between(
mark: (Instant, Duration),
now: (Instant, Duration),
bound: Duration,
) -> bool {
Self::runnable_between(mark, now) >= bound
|| now.0.saturating_duration_since(mark.0) >= bound.saturating_mul(10)
}
pub(crate) fn expired(&self, mark: (Instant, Duration), bound: Duration) -> bool {
Self::expired_between(mark, self.mark(), bound)
}
#[cfg(all(unix, feature = "proxy", feature = "legacy-2024-11-05"))]
pub(crate) fn remaining(&self, mark: (Instant, Duration), bound: Duration) -> Duration {
bound.saturating_sub(Self::runnable_between(mark, self.mark()))
}
pub(crate) fn describe(&self, mark: (Instant, Duration)) -> String {
let now = self.mark();
format!(
"{:?} runnable ({:?} wall, {:?} host lag)",
Self::runnable_between(mark, now),
now.0.saturating_duration_since(mark.0),
now.1.saturating_sub(mark.1)
)
}
}
/// Awaits `future` for at most `bound` of runnable time from `started`.
/// Only the short wall-clock slice around it is re-armed, never the
/// future itself, so a slice expiring cancels nothing. `None` means the
/// bound expired and the future is still pending.
// Exact-2024 era: used only by legacy HTTP SSE tests.
#[cfg(feature = "legacy-2024-11-05")]
async fn within_runnable<F: Future>(
cx: &Cx,
host: &RunnableClock,
started: (Instant, Duration),
bound: Duration,
future: F,
) -> Option<F::Output> {
let mut future = std::pin::pin!(future);
loop {
match asupersync::time::timeout(cx.now(), Duration::from_millis(50), future.as_mut())
.await
{
Ok(output) => return Some(output),
Err(_) if host.expired(started, bound) => return None,
Err(_) => {}
}
}
}
/// One second of wall time between two marks; only the lag accrued in
/// it differs between the arms.
#[test]
fn runnable_clock_discounts_host_lag_and_keeps_a_wall_backstop() {
let start = Instant::now();
let mark = (start, Duration::from_millis(40));
let later = |lag_ms: u64| {
(
start + Duration::from_secs(1),
Duration::from_millis(40 + lag_ms),
)
};
let bound = Duration::from_millis(500);
// Unloaded: the whole second was runnable, so the bound expired.
assert_eq!(
RunnableClock::runnable_between(mark, later(0)),
Duration::from_secs(1)
);
assert!(RunnableClock::expired_between(mark, later(0), bound));
// Loaded: 900 ms of it was host lag, so the same bound has not.
assert_eq!(
RunnableClock::runnable_between(mark, later(900)),
Duration::from_millis(100)
);
assert!(!RunnableClock::expired_between(mark, later(900), bound));
// Nearly all lag: 10 ms runnable is under a 90 ms bound, but the
// second of wall time is past ten times it, so the backstop expires.
assert_eq!(
RunnableClock::runnable_between(mark, later(990)),
Duration::from_millis(10)
);
assert!(RunnableClock::expired_between(
mark,
later(990),
Duration::from_millis(90)
));
assert!(!RunnableClock::expired_between(
mark,
later(990),
Duration::from_millis(101)
));
}
#[cfg(feature = "builtin-auth-server")]
const OIDC_TEST_PUBLIC_MODULUS: &str = "jlHZ9nzuIuM4aiAQSAgEJMBaYS7qm7Z_3mtGYDdzReIkzxPHHr21oeXQyUJI89eQG13fsUdyoodcuh5kmndPCrODJekfr_zgor6sNspcB88iQEqEc9yf9YAf5v-cNH1Evh82KABuWb26LMaNAzZFR3BMhMEQ1FD6fLFGAbX76Drd5_UZ-1xcU07IXEc_9zvQvOwXckhO7P5Yil1fVzLTrHye_6zTbGWvdqi45095bKPnSqjrLBCTVrUW8o02Gi6mt7Ls9pZeWx2DXV8SqV06DdlqiovtKWRooQ1zV-v7BGsLsVk6T6d-8mNMGNrh0fpNb_5kdaHphAt_Ji6eE1wQPw";
#[cfg(feature = "builtin-auth-server")]
const OIDC_TEST_CANARY_COMPACT_JWS: &str = concat!(
"eyJhbGciOiJSUzI1NiIsImtpZCI6ImZpeGVkLXJzMjU2In0.",
"eyJzdWIiOiJmaXhlZC12ZWN0b3IiLCJhdWQiOiJzZXJ2ZXItcG9saWN5LWxhdGVyIn0.",
"Oak9UDEtrL-pNcPIFw31uzuCoCTyXywF5i3jxDixd0gHonZYPFfSlyPwhNSTrqmlzPsL-wNFcDn1zFlug6Ae1vK_QaL-bZBSxq-lOrMDUI_5_3P_HUrngtZaNk8ru88-wdGByGm1jRZa-LfeoSkESHVKPIcQ_WT7wqhq1RX3ZrPiq9QkHFE8nWIgiIesu8DFOXsdN05rmOxHheCbDGRpf8cQAG0ZENpJvYugD-SX9Sg9Kds5HOlOt6csIQBexCeKM2rIrN0r7qCp6jx_0aevqU6rNr6oxCxCGoH3UZGJa5xRh2KeJ6NVBE9BpPW3Kdi3dEfKlKldjzlUW-zEREdeEw"
);
#[cfg(feature = "builtin-auth-server")]
struct OidcPublicCanaryBackend;
#[cfg(feature = "builtin-auth-server")]
impl fastmcp_protocol::jose::ExternalRs256SignerBackend for OidcPublicCanaryBackend {
fn sign<'a>(
&'a self,
_: &'a Cx,
request: fastmcp_protocol::jose::ExternalRs256SigningRequest,
) -> Pin<
Box<
dyn Future<Output = fastmcp_protocol::jose::ExternalRs256SignDisposition>
+ Send
+ 'a,
>,
> {
Box::pin(async move {
use base64::Engine as _;
use fastmcp_protocol::jose::{
ExternalRs256OperationReceipt, ExternalRs256SignDisposition, RawRs256Signature,
RedactedSignerProvenance,
};
let (input, signature) = OIDC_TEST_CANARY_COMPACT_JWS
.rsplit_once('.')
.expect("retained public test canary has a signature");
assert!(
request
.input()
.with_bytes(|bytes| bytes == input.as_bytes())
);
let signature = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(signature)
.expect("retained public canary signature decodes");
let receipt = ExternalRs256OperationReceipt::new(
request.binding(),
1,
RedactedSignerProvenance::new("oidc-live-public-canary")
.expect("bounded redacted provenance"),
)
.expect("valid dispatched operation receipt");
ExternalRs256SignDisposition::Dispatched(
RawRs256Signature::from_bytes(signature)
.expect("retained RS256 signature has exact length"),
receipt,
)
})
}
}
#[cfg(feature = "builtin-auth-server")]
fn oidc_public_canary_signer() -> Arc<fastmcp_protocol::jose::ExternalRs256Signer> {
use base64::Engine as _;
use fastmcp_protocol::jose::{
AttestedRs256PublicKey, ExternalRs256Signer, RedactedSignerProvenance,
Rs256SigningBinding,
};
let modulus = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(OIDC_TEST_PUBLIC_MODULUS)
.expect("retained public verification modulus");
let binding = Rs256SigningBinding::new(71, 72, 73, 74)
.expect("well-formed external signer generations");
let key = AttestedRs256PublicKey::admit(
"fixed-rs256",
modulus,
binding,
RedactedSignerProvenance::new("oidc-live-public-test")
.expect("bounded redacted provenance"),
)
.expect("retained public key admits");
Arc::new(ExternalRs256Signer::new(
Arc::new(OidcPublicCanaryBackend),
key,
))
}
/// Test-only embedding verifier: it performs a fresh loopback HTTP fetch
/// of the shipped JWKS route and returns evidence bound to the configured
/// public HTTPS URI/origin. It never reads the provider's route buffer.
#[cfg(feature = "builtin-auth-server")]
struct LiveLoopbackOidcReadBackVerifier {
address: Mutex<Option<SocketAddr>>,
generation: u64,
}
#[cfg(feature = "builtin-auth-server")]
impl LiveLoopbackOidcReadBackVerifier {
fn set_address(&self, address: SocketAddr) {
*self.address.lock().expect("loopback verifier address lock") = Some(address);
}
}
#[cfg(feature = "builtin-auth-server")]
impl oidc::OidcJwksReadBackVerifier for LiveLoopbackOidcReadBackVerifier {
fn read_back<'a>(
&'a self,
_: &'a Cx,
endpoints: &'a [String],
) -> Pin<
Box<
dyn Future<
Output = Result<
Vec<fastmcp_protocol::jose::JwksEndpointReadBack>,
oidc::OidcError,
>,
> + Send
+ 'a,
>,
> {
let address = *self.address.lock().expect("loopback verifier address lock");
let generation = self.generation;
Box::pin(async move {
let address = address.ok_or_else(|| {
oidc::OidcError::SigningError(
"loopback read-back endpoint is not bound".to_string(),
)
})?;
let mut evidence = Vec::with_capacity(endpoints.len());
for endpoint in endpoints {
if endpoint != "https://fastmcp.invalid/oidc/jwks" {
return Err(oidc::OidcError::SigningError(
"loopback verifier was asked for an unconfigured public JWKS URI"
.to_string(),
));
}
let response = live_http_exchange(
address,
b"GET /oidc/jwks HTTP/1.1\r\nHost: loopback\r\nConnection: close\r\n\r\n"
.to_vec(),
)
.await
.map_err(oidc::OidcError::SigningError)?;
if !response.starts_with(b"HTTP/1.1 200") {
return Err(oidc::OidcError::SigningError(
"loopback JWKS read-back did not return HTTP 200".to_string(),
));
}
let body = live_http_response_body(&response)
.map_err(oidc::OidcError::SigningError)?
.to_vec();
evidence.push(
fastmcp_protocol::jose::JwksEndpointReadBack::new(
endpoint.clone(),
"https://fastmcp.invalid",
body,
generation,
)
.map_err(|_| {
oidc::OidcError::SigningError(
"loopback JWKS evidence is outside admission bounds".to_string(),
)
})?,
);
}
Ok(evidence)
})
}
}
#[cfg(feature = "builtin-auth-server")]
#[derive(Default)]
struct LiveOidcActivationStore {
record: Mutex<Option<oidc::OidcSigningActivationStoreRecord>>,
}
#[cfg(feature = "builtin-auth-server")]
impl oidc::OidcSigningActivationStore for LiveOidcActivationStore {
fn load(
&self,
_: &Cx,
issuer: &str,
) -> Result<Option<oidc::OidcSigningActivationStoreRecord>, oidc::OidcError> {
Ok(self
.record
.lock()
.map_err(|_| {
oidc::OidcError::SigningError("live OIDC store unavailable".to_string())
})?
.clone()
.filter(|record| record.issuer() == issuer))
}
fn compare_and_set(
&self,
_: &Cx,
expected_generation: Option<u64>,
next: oidc::OidcSigningActivationStoreRecord,
) -> Result<oidc::OidcSigningActivationStoreRecord, oidc::OidcError> {
let mut record = self.record.lock().map_err(|_| {
oidc::OidcError::SigningError("live OIDC store unavailable".to_string())
})?;
if record
.as_ref()
.map(oidc::OidcSigningActivationStoreRecord::activation_generation)
!= expected_generation
{
return Err(oidc::OidcError::SigningError(
"live OIDC activation CAS lost".to_string(),
));
}
*record = Some(next.clone());
Ok(next)
}
}
#[test]
fn websocket_profile_exposes_turnkey_server_endpoint_behind_its_feature() {
let source = include_str!("lib.rs").replace("\r\n", "\n");
assert!(source.contains("#[cfg(feature = \"websocket\")]\nimpl Server"));
assert!(source.contains("pub async fn bind_websocket"));
assert!(source.contains("pub async fn serve_websocket"));
assert!(source.contains("pub async fn run_http"));
}
#[cfg(feature = "websocket")]
async fn websocket_upgrade(
cx: &Cx,
address: std::net::SocketAddr,
request: &[u8],
) -> Result<(AsyncTcpStream, Vec<u8>), String> {
use asupersync::io::{AsyncReadExt, AsyncWriteExt};
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut stream = asupersync::time::timeout_at(deadline, AsyncTcpStream::connect(address))
.await
.map_err(|_| "WebSocket client connect timed out".to_owned())?
.map_err(|error| format!("WebSocket client connect failed: {error}"))?;
asupersync::time::timeout_at(deadline, stream.write_all(request))
.await
.map_err(|_| "WebSocket handshake write timed out".to_owned())?
.map_err(|error| format!("WebSocket handshake write failed: {error}"))?;
asupersync::time::timeout_at(deadline, stream.flush())
.await
.map_err(|_| "WebSocket handshake flush timed out".to_owned())?
.map_err(|error| format!("WebSocket handshake flush failed: {error}"))?;
let mut received = Vec::new();
while !received.windows(4).any(|window| window == b"\r\n\r\n") {
let mut chunk = [0_u8; 512];
let read = asupersync::time::timeout_at(deadline, stream.read(&mut chunk))
.await
.map_err(|_| "WebSocket handshake response timed out".to_owned())?
.map_err(|error| format!("WebSocket handshake response failed: {error}"))?;
if read == 0 {
break;
}
received.extend_from_slice(&chunk[..read]);
}
Ok((stream, received))
}
#[cfg(feature = "websocket")]
#[test]
fn live_websocket_successful_child_reports_quiescent_before_shutdown_cleanup() {
run_live_http_test(|cx| async move {
use asupersync::io::{AsyncReadExt, AsyncWriteExt};
let peer_closed = Arc::new(AtomicBool::new(false));
let cleanup_after_peer_close = Arc::new(AtomicBool::new(false));
let shutdown_calls = Arc::new(AtomicUsize::new(0));
let observed_peer_close = Arc::clone(&peer_closed);
let observed_cleanup = Arc::clone(&cleanup_after_peer_close);
let observed_shutdown_calls = Arc::clone(&shutdown_calls);
let listener = Server::new("live-websocket-successful-child", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern-only policy is available")
.on_shutdown(move || {
observed_shutdown_calls.fetch_add(1, Ordering::AcqRel);
observed_cleanup.store(
observed_peer_close.load(Ordering::Acquire),
Ordering::Release,
);
})
.build()
.bind_websocket(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("successful-child WebSocket bind failed: {error}"))?;
let address = listener
.local_addr()
.map_err(|error| format!("successful-child WebSocket address failed: {error}"))?;
let scope = cx.scope();
let mut listener_task = cx
.spawn_in(&scope, move |listener_cx| async move {
listener.serve(&listener_cx).await
})
.map_err(|error| {
format!("successful-child WebSocket listener admission failed: {error}")
})?;
let opening = concat!(
"GET /mcp HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\n",
"Connection: Upgrade\r\nSec-WebSocket-Version: 13\r\n",
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"
);
let (mut client, accepted) =
websocket_upgrade(&cx, address, opening.as_bytes()).await?;
if !accepted.starts_with(b"HTTP/1.1 101") {
return Err("successful WebSocket child was not upgraded".to_owned());
}
client
.write_all(&masked_websocket_frame(0x08, &[]))
.await
.map_err(|error| format!("successful WebSocket close write failed: {error}"))?;
client
.flush()
.await
.map_err(|error| format!("successful WebSocket close flush failed: {error}"))?;
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut close_reply = [0_u8; 32];
let close_read = asupersync::time::timeout_at(deadline, client.read(&mut close_reply))
.await
.map_err(|_| "successful WebSocket close reply timed out".to_owned())?
.map_err(|error| format!("successful WebSocket close reply failed: {error}"))?;
if close_read < 2 || close_reply[0] & 0x0f != 0x08 {
return Err("successful WebSocket child did not complete a close reply".to_owned());
}
peer_closed.store(true, Ordering::Release);
drop(client);
listener_task.abort();
let join_cx = Cx::for_testing();
let shutdown = listener_task
.join(&join_cx)
.await
.map_err(|error| format!("successful WebSocket listener join failed: {error:?}"))?
.map_err(|error| format!("successful WebSocket listener failed: {error}"))?;
if !matches!(shutdown, WebSocketServerShutdown::Quiescent)
|| shutdown_calls.load(Ordering::Acquire) != 1
|| !cleanup_after_peer_close.load(Ordering::Acquire)
{
return Err(
"successful WebSocket child did not settle before quiescent cleanup".to_owned(),
);
}
Ok(())
});
}
#[cfg(feature = "websocket")]
#[test]
fn rh5_websocket_rejects_server_notification_without_dispatch_or_connection_loss() {
run_live_http_test(|cx| async move {
use asupersync::io::{AsyncReadExt, AsyncWriteExt};
let handler_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let saw_credential = Arc::new(AtomicBool::new(false));
let listener = Server::new("websocket-wrong-direction-notification", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern-only policy is available")
.middleware(ModernHttpAuthMiddleware {
calls: Arc::clone(&middleware_calls),
saw_credential: Arc::clone(&saw_credential),
})
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&handler_calls),
})
.build()
.bind_websocket(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("wrong-direction WebSocket bind failed: {error}"))?;
let address = listener
.local_addr()
.map_err(|error| format!("wrong-direction WebSocket address failed: {error}"))?;
let scope = cx.scope();
let mut listener_task = cx
.spawn_in(&scope, move |listener_cx| async move {
listener.serve(&listener_cx).await
})
.map_err(|error| {
format!("wrong-direction WebSocket listener admission failed: {error}")
})?;
let opening = concat!(
"GET /mcp HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\n",
"Connection: Upgrade\r\nSec-WebSocket-Version: 13\r\n",
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"
);
let (mut client, accepted) =
websocket_upgrade(&cx, address, opening.as_bytes()).await?;
if !accepted.starts_with(b"HTTP/1.1 101") {
return Err("wrong-direction WebSocket was not upgraded".to_owned());
}
let server_notification = JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/progress",
Some(serde_json::json!({
"progressToken": "server-only",
"progress": 1,
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
},
})),
));
let resumed_client_request = JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "modern_http_auth_counter",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
7_402_i64,
));
let mut frames = masked_websocket_message(modern_discovery_request(7_401));
frames.extend(masked_websocket_message(server_notification));
frames.extend(masked_websocket_message(resumed_client_request));
client
.write_all(&frames)
.await
.map_err(|error| format!("wrong-direction WebSocket write failed: {error}"))?;
client
.flush()
.await
.map_err(|error| format!("wrong-direction WebSocket flush failed: {error}"))?;
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut responses = Vec::new();
while !(responses
.windows(b"serverInfo".len())
.any(|window| window == b"serverInfo")
&& responses
.windows(b"authenticated modern HTTP dispatch".len())
.any(|window| window == b"authenticated modern HTTP dispatch"))
{
let mut chunk = [0_u8; 1024];
let read = asupersync::time::timeout_at(deadline, client.read(&mut chunk))
.await
.map_err(|_| "wrong-direction WebSocket response timed out".to_owned())?
.map_err(|error| {
format!("wrong-direction WebSocket response failed: {error}")
})?;
if read == 0 {
return Err(
"wrong-direction notification closed the WebSocket connection".to_owned(),
);
}
responses.extend_from_slice(&chunk[..read]);
}
if handler_calls.load(Ordering::Acquire) != 1
|| middleware_calls.load(Ordering::Acquire) != 2
|| saw_credential.load(Ordering::Acquire)
{
return Err(
"wrong-direction notification reached middleware or handler before the resumed request"
.to_owned(),
);
}
client
.write_all(&masked_websocket_frame(0x08, &[]))
.await
.map_err(|error| {
format!("wrong-direction WebSocket close write failed: {error}")
})?;
client.flush().await.map_err(|error| {
format!("wrong-direction WebSocket close flush failed: {error}")
})?;
drop(client);
listener_task.abort();
let join_cx = Cx::for_testing();
let shutdown = listener_task
.join(&join_cx)
.await
.map_err(|error| {
format!("wrong-direction WebSocket listener join failed: {error:?}")
})?
.map_err(|error| format!("wrong-direction WebSocket listener failed: {error}"))?;
if !matches!(shutdown, WebSocketServerShutdown::Quiescent) {
return Err("wrong-direction WebSocket listener did not quiesce".to_owned());
}
Ok(())
});
}
#[cfg(feature = "websocket")]
#[test]
fn live_websocket_failing_child_is_reported_after_cleanup_observes_terminal_peer() {
run_live_http_test(|cx| async move {
use asupersync::io::{AsyncReadExt, AsyncWriteExt};
let peer_terminal = Arc::new(AtomicBool::new(false));
let cleanup_after_peer_terminal = Arc::new(AtomicBool::new(false));
let shutdown_calls = Arc::new(AtomicUsize::new(0));
let observed_peer_terminal = Arc::clone(&peer_terminal);
let observed_cleanup = Arc::clone(&cleanup_after_peer_terminal);
let observed_shutdown_calls = Arc::clone(&shutdown_calls);
let listener = Server::new("live-websocket-failing-child", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern-only policy is available")
.on_shutdown(move || {
observed_shutdown_calls.fetch_add(1, Ordering::AcqRel);
observed_cleanup.store(
observed_peer_terminal.load(Ordering::Acquire),
Ordering::Release,
);
})
.build()
.bind_websocket(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("failing-child WebSocket bind failed: {error}"))?;
let address = listener
.local_addr()
.map_err(|error| format!("failing-child WebSocket address failed: {error}"))?;
let scope = cx.scope();
let mut listener_task = cx
.spawn_in(&scope, move |listener_cx| async move {
listener.serve(&listener_cx).await
})
.map_err(|error| {
format!("failing-child WebSocket listener admission failed: {error}")
})?;
let opening = concat!(
"GET /mcp HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\n",
"Connection: Upgrade\r\nSec-WebSocket-Version: 13\r\n",
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"
);
let (mut client, accepted) =
websocket_upgrade(&cx, address, opening.as_bytes()).await?;
if !accepted.starts_with(b"HTTP/1.1 101") {
return Err("failing WebSocket child was not upgraded".to_owned());
}
// Reserved opcode 0x03 is a decoded-frame failure, not a clean
// peer close. The real connection child must return that failure
// to the listener-owned TaskHandle.
client
.write_all(&masked_websocket_frame(0x03, &[]))
.await
.map_err(|error| format!("failing WebSocket frame write failed: {error}"))?;
client
.flush()
.await
.map_err(|error| format!("failing WebSocket frame flush failed: {error}"))?;
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut terminal = [0_u8; 32];
let terminal_read = asupersync::time::timeout_at(deadline, client.read(&mut terminal))
.await
.map_err(|_| "failing WebSocket terminal response timed out".to_owned())?
.map_err(|error| format!("failing WebSocket terminal response failed: {error}"))?;
if terminal_read != 0 && (terminal_read < 2 || terminal[0] & 0x0f != 0x08) {
return Err(
"failing WebSocket child did not terminate the invalid frame".to_owned(),
);
}
peer_terminal.store(true, Ordering::Release);
drop(client);
// A second accepted socket makes the listener reap the first
// terminal child on its real accept-loop path before shutdown.
let reaper_trigger = opening.replace("Connection: Upgrade", "Connection: keep-alive");
let (_trigger, rejected) =
websocket_upgrade(&cx, address, reaper_trigger.as_bytes()).await?;
if !rejected.starts_with(b"HTTP/1.1 400") {
return Err("failing-child reaper trigger did not reach the listener".to_owned());
}
listener_task.abort();
let join_cx = Cx::for_testing();
let listener_result = listener_task
.join(&join_cx)
.await
.map_err(|error| format!("failing WebSocket listener join failed: {error:?}"))?;
let error = match listener_result {
Err(error) => error,
Ok(_) => {
return Err(
"failing WebSocket child unexpectedly produced a quiescent success result"
.to_owned(),
);
}
};
if !error
.message
.contains("WebSocket connection child settlement failure")
|| shutdown_calls.load(Ordering::Acquire) != 1
|| !cleanup_after_peer_terminal.load(Ordering::Acquire)
{
return Err(
"failing WebSocket child was hidden or cleanup preceded terminal settlement"
.to_owned(),
);
}
Ok(())
});
}
#[cfg(feature = "websocket")]
#[test]
fn live_websocket_upgrade_owns_connection_teardown_and_preserves_peer_isolation() {
run_live_http_test(|cx| async move {
use asupersync::io::{AsyncReadExt, AsyncWriteExt};
let listener = Server::new("live-websocket", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern-only policy is available")
.http_config(HttpServerConfig::new().mcp_path("/qualified/mcp"))
.build()
.bind_websocket(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("WebSocket bind failed: {error}"))?;
let address = listener
.local_addr()
.map_err(|error| format!("WebSocket local address failed: {error}"))?;
let scope = cx.scope();
let mut listener_task = cx
.spawn_in(&scope, move |listener_cx| async move {
listener.serve(&listener_cx).await
})
.map_err(|error| format!("WebSocket listener task admission failed: {error}"))?;
let valid = concat!(
"GET /qualified/mcp?ignored=query HTTP/1.1\r\n",
"Host: localhost\r\n",
"Upgrade: websocket\r\n",
"Connection: Upgrade\r\n",
"Sec-WebSocket-Version: 13\r\n",
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"
);
// The opening frame intentionally shares the Upgrade write. This
// proves that HTTP header parsing hands pre-read WebSocket bytes
// to the server-role frame decoder instead of losing them.
let mut opening = valid.as_bytes().to_vec();
opening.extend_from_slice(&masked_websocket_message(modern_discovery_request(7_101)));
let (mut client, accepted) = websocket_upgrade(&cx, address, &opening).await?;
if !accepted.starts_with(b"HTTP/1.1 101") {
return Err(format!(
"valid WebSocket upgrade was not accepted: {accepted:?}"
));
}
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let header_end = accepted
.windows(4)
.position(|window| window == b"\r\n\r\n")
.map_or(accepted.len(), |position| position + 4);
let mut response = accepted[header_end..].to_vec();
while !response.windows(10).any(|window| window == b"serverInfo") {
let mut chunk = [0_u8; 1024];
let read = asupersync::time::timeout_at(deadline, client.read(&mut chunk))
.await
.map_err(|_| "modern WebSocket response timed out".to_owned())?
.map_err(|error| format!("modern WebSocket response failed: {error}"))?;
if read == 0 {
return Err("modern WebSocket closed before its response".to_owned());
}
response.extend_from_slice(&chunk[..read]);
}
client
.write_all(&masked_websocket_frame(0x08, &[]))
.await
.map_err(|error| format!("WebSocket close write failed: {error}"))?;
client
.flush()
.await
.map_err(|error| format!("WebSocket close flush failed: {error}"))?;
let mut close_reply = [0_u8; 32];
let close_read = asupersync::time::timeout_at(deadline, client.read(&mut close_reply))
.await
.map_err(|_| "WebSocket close reply timed out".to_owned())?
.map_err(|error| format!("WebSocket close reply failed: {error}"))?;
if close_read < 2 || close_reply[0] & 0x0f != 0x08 || close_reply[1] & 0x80 != 0 {
return Err("server did not return an unmasked WebSocket close reply".to_owned());
}
let malformed_header = valid.replace("Connection: Upgrade", "Connection: keep-alive");
let (_bad_client, rejected) =
websocket_upgrade(&cx, address, malformed_header.as_bytes()).await?;
if !rejected.starts_with(b"HTTP/1.1 400") {
return Err(format!("bad Upgrade header was not rejected: {rejected:?}"));
}
let (mut era_client, era_accepted) =
websocket_upgrade(&cx, address, valid.as_bytes()).await?;
if !era_accepted.starts_with(b"HTTP/1.1 101") {
return Err("era-negative WebSocket handshake was not accepted".to_owned());
}
era_client
.write_all(&masked_websocket_message(exact_legacy_initialize_request(
7_102,
serde_json::json!("1.0.0"),
)))
.await
.map_err(|error| format!("legacy-era WebSocket frame write failed: {error}"))?;
era_client
.flush()
.await
.map_err(|error| format!("legacy-era WebSocket frame flush failed: {error}"))?;
let mut rejection = [0_u8; 1024];
let read = asupersync::time::timeout_at(deadline, era_client.read(&mut rejection))
.await
.map_err(|_| "legacy-era WebSocket rejection timed out".to_owned())?
.map_err(|error| format!("legacy-era WebSocket rejection failed: {error}"))?;
if read == 0
|| !rejection[..read]
.windows(5)
.any(|window| window == b"error")
{
return Err("wrong-era frame did not receive a JSON-RPC refusal".to_owned());
}
// RH-5 negative neighbor: a rejected legacy opening must not
// poison independent modern state or terminate its listener.
let (mut independent_client, independent_accepted) =
websocket_upgrade(&cx, address, valid.as_bytes()).await?;
if !independent_accepted.starts_with(b"HTTP/1.1 101") {
return Err("wrong-era peer changed listener admission state".to_owned());
}
independent_client
.write_all(&masked_websocket_message(modern_discovery_request(7_103)))
.await
.map_err(|error| format!("isolated modern frame write failed: {error}"))?;
independent_client
.flush()
.await
.map_err(|error| format!("isolated modern frame flush failed: {error}"))?;
let mut independent_response = Vec::new();
while !independent_response
.windows(10)
.any(|window| window == b"serverInfo")
{
let mut chunk = [0_u8; 1024];
let read =
asupersync::time::timeout_at(deadline, independent_client.read(&mut chunk))
.await
.map_err(|_| "isolated modern WebSocket response timed out".to_owned())?
.map_err(|error| {
format!("isolated modern WebSocket response failed: {error}")
})?;
if read == 0 {
return Err("wrong-era peer closed an independent modern connection".to_owned());
}
independent_response.extend_from_slice(&chunk[..read]);
}
independent_client
.write_all(&masked_websocket_frame(0x08, &[]))
.await
.map_err(|error| format!("isolated WebSocket close write failed: {error}"))?;
drop(era_client);
// TaskHandle::abort requests cooperative cancellation; the
// serving future still owns child settlement and returns its
// first-class shutdown result.
listener_task.abort();
let join_cx = Cx::for_testing();
let shutdown = listener_task
.join(&join_cx)
.await
.map_err(|error| format!("WebSocket listener join failed: {error:?}"))?
.map_err(|error| format!("WebSocket listener failed: {error}"))?;
if !matches!(shutdown, WebSocketServerShutdown::Quiescent) {
return Err("cooperative WebSocket shutdown retained a child".to_owned());
}
Ok(())
});
}
#[cfg(all(feature = "websocket", feature = "legacy-2024-11-05"))]
#[test]
fn live_websocket_auto_accepts_fresh_eras_and_fences_cross_era_traffic() {
run_live_http_test(|cx| async move {
use asupersync::io::{AsyncReadExt, AsyncWriteExt};
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
haystack
.windows(needle.len())
.any(|window| window == needle)
}
let handler_calls = Arc::new(AtomicUsize::new(0));
let listener = Server::new("live-websocket-auto-era-isolation", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto policy is available to the public WebSocket listener")
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&handler_calls),
})
.build()
.bind_websocket(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("Auto WebSocket bind failed: {error}"))?;
let address = listener
.local_addr()
.map_err(|error| format!("Auto WebSocket address failed: {error}"))?;
let scope = cx.scope();
let mut listener_task = cx
.spawn_in(&scope, move |listener_cx| async move {
listener.serve(&listener_cx).await
})
.map_err(|error| format!("Auto WebSocket listener admission failed: {error}"))?;
let opening = concat!(
"GET /mcp HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\n",
"Connection: Upgrade\r\nSec-WebSocket-Version: 13\r\n",
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"
);
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
// Positive: a final-era client selects final routing on its own
// fresh public WebSocket connection.
let (mut modern, modern_upgrade) =
websocket_upgrade(&cx, address, opening.as_bytes()).await?;
if !modern_upgrade.starts_with(b"HTTP/1.1 101") {
return Err("Auto WebSocket did not accept the final-era client".to_owned());
}
modern
.write_all(&masked_websocket_message(modern_discovery_request(7_201)))
.await
.map_err(|error| format!("Auto final-era discovery write failed: {error}"))?;
modern
.flush()
.await
.map_err(|error| format!("Auto final-era discovery flush failed: {error}"))?;
let mut modern_response = Vec::new();
while !contains_bytes(&modern_response, b"serverInfo") {
let mut chunk = [0_u8; 1024];
let read = asupersync::time::timeout_at(deadline, modern.read(&mut chunk))
.await
.map_err(|_| "Auto final-era discovery timed out".to_owned())?
.map_err(|error| format!("Auto final-era discovery read failed: {error}"))?;
if read == 0 {
return Err("Auto final-era client closed before discovery response".to_owned());
}
modern_response.extend_from_slice(&chunk[..read]);
}
modern
.write_all(&masked_websocket_frame(0x08, &[]))
.await
.map_err(|error| format!("Auto final-era close write failed: {error}"))?;
modern
.flush()
.await
.map_err(|error| format!("Auto final-era close flush failed: {error}"))?;
drop(modern);
// Positive: a separate exact-2024 peer may initialize, confirm
// initialization, and make an ordinary request on the same public
// listener without sharing the final-era connection namespace.
let (mut legacy, legacy_upgrade) =
websocket_upgrade(&cx, address, opening.as_bytes()).await?;
if !legacy_upgrade.starts_with(b"HTTP/1.1 101") {
return Err("Auto WebSocket did not accept the exact-2024 client".to_owned());
}
let legacy_initialize =
exact_legacy_initialize_request(7_202, serde_json::json!("1.0.0"));
let legacy_initialized = JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
));
let legacy_list = JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({})),
7_203_i64,
));
let mut legacy_frames = masked_websocket_message(legacy_initialize);
legacy_frames.extend(masked_websocket_message(legacy_initialized));
legacy_frames.extend(masked_websocket_message(legacy_list));
legacy
.write_all(&legacy_frames)
.await
.map_err(|error| format!("Auto exact-2024 transcript write failed: {error}"))?;
legacy
.flush()
.await
.map_err(|error| format!("Auto exact-2024 transcript flush failed: {error}"))?;
let mut legacy_response = Vec::new();
while !(contains_bytes(&legacy_response, b"\"id\":7202")
&& contains_bytes(&legacy_response, b"\"id\":7203")
&& contains_bytes(&legacy_response, b"\"tools\""))
{
let mut chunk = [0_u8; 1024];
let read = asupersync::time::timeout_at(deadline, legacy.read(&mut chunk))
.await
.map_err(|_| "Auto exact-2024 transcript timed out".to_owned())?
.map_err(|error| format!("Auto exact-2024 transcript read failed: {error}"))?;
if read == 0 {
return Err(
"Auto exact-2024 client closed before its ordinary response".to_owned()
);
}
legacy_response.extend_from_slice(&chunk[..read]);
}
legacy
.write_all(&masked_websocket_frame(0x08, &[]))
.await
.map_err(|error| format!("Auto exact-2024 close write failed: {error}"))?;
legacy
.flush()
.await
.map_err(|error| format!("Auto exact-2024 close flush failed: {error}"))?;
drop(legacy);
// RH-5 neighbor: the only changed dimension is a legacy opening
// after this peer already selected final routing. Its refusal is
// connection-local and cannot poison a later independent final
// client on the listener.
let (mut mixed, mixed_upgrade) =
websocket_upgrade(&cx, address, opening.as_bytes()).await?;
if !mixed_upgrade.starts_with(b"HTTP/1.1 101") {
return Err("Auto WebSocket did not admit the cross-era probe peer".to_owned());
}
let mut mixed_frames = masked_websocket_message(modern_discovery_request(7_204));
mixed_frames.extend(masked_websocket_message(exact_legacy_initialize_request(
7_205,
serde_json::json!("1.0.0"),
)));
mixed
.write_all(&mixed_frames)
.await
.map_err(|error| format!("Auto cross-era frame write failed: {error}"))?;
mixed
.flush()
.await
.map_err(|error| format!("Auto cross-era frame flush failed: {error}"))?;
let mut mixed_response = Vec::new();
while !(contains_bytes(&mixed_response, b"\"id\":7205")
&& contains_bytes(&mixed_response, b"\"error\""))
{
let mut chunk = [0_u8; 1024];
let read = asupersync::time::timeout_at(deadline, mixed.read(&mut chunk))
.await
.map_err(|_| "Auto cross-era refusal timed out".to_owned())?
.map_err(|error| format!("Auto cross-era refusal read failed: {error}"))?;
if read == 0 {
return Err("Auto cross-era peer closed before its refusal".to_owned());
}
mixed_response.extend_from_slice(&chunk[..read]);
}
if handler_calls.load(Ordering::Acquire) != 0 {
return Err(
"cross-era refusal dispatched a handler before closing the offending peer"
.to_owned(),
);
}
loop {
let mut terminal = [0_u8; 64];
let read = asupersync::time::timeout_at(deadline, mixed.read(&mut terminal))
.await
.map_err(|_| "Auto cross-era peer close timed out".to_owned())?
.map_err(|error| format!("Auto cross-era peer close failed: {error}"))?;
if read == 0 {
break;
}
}
drop(mixed);
let (mut isolated, isolated_upgrade) =
websocket_upgrade(&cx, address, opening.as_bytes()).await?;
if !isolated_upgrade.starts_with(b"HTTP/1.1 101") {
return Err("cross-era probe poisoned later listener admission".to_owned());
}
isolated
.write_all(&masked_websocket_message(modern_discovery_request(7_206)))
.await
.map_err(|error| format!("isolated final-era discovery write failed: {error}"))?;
isolated
.flush()
.await
.map_err(|error| format!("isolated final-era discovery flush failed: {error}"))?;
let mut isolated_response = Vec::new();
while !contains_bytes(&isolated_response, b"serverInfo") {
let mut chunk = [0_u8; 1024];
let read = asupersync::time::timeout_at(deadline, isolated.read(&mut chunk))
.await
.map_err(|_| "isolated final-era discovery timed out".to_owned())?
.map_err(|error| {
format!("isolated final-era discovery read failed: {error}")
})?;
if read == 0 {
return Err("cross-era probe poisoned later final-era traffic".to_owned());
}
isolated_response.extend_from_slice(&chunk[..read]);
}
isolated
.write_all(&masked_websocket_frame(0x08, &[]))
.await
.map_err(|error| format!("isolated final-era close write failed: {error}"))?;
isolated
.flush()
.await
.map_err(|error| format!("isolated final-era close flush failed: {error}"))?;
drop(isolated);
listener_task.abort();
let join_cx = Cx::for_testing();
let shutdown = listener_task
.join(&join_cx)
.await
.map_err(|error| format!("Auto WebSocket listener join failed: {error:?}"))?
.map_err(|error| format!("Auto WebSocket listener failed: {error}"))?;
if !matches!(shutdown, WebSocketServerShutdown::Quiescent) {
return Err("Auto WebSocket listener retained a child after era proof".to_owned());
}
Ok(())
});
}
#[cfg(feature = "websocket")]
#[test]
fn live_websocket_connection_limit_rejects_before_child_and_releases_its_permit() {
run_live_http_test(|cx| async move {
let listener = Server::new("live-websocket-limit", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern-only policy is available")
.http_config(HttpServerConfig::new().max_connections(1))
.build()
.bind_websocket(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("limited WebSocket bind failed: {error}"))?;
let address = listener
.local_addr()
.map_err(|error| format!("limited WebSocket address failed: {error}"))?;
let scope = cx.scope();
let mut listener_task = cx
.spawn_in(&scope, move |listener_cx| async move {
listener.serve(&listener_cx).await
})
.map_err(|error| format!("limited WebSocket listener admission failed: {error}"))?;
let opening = concat!(
"GET /mcp HTTP/1.1\r\n",
"Host: localhost\r\n",
"Upgrade: websocket\r\n",
"Connection: Upgrade\r\n",
"Sec-WebSocket-Version: 13\r\n",
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"
);
let (first, accepted) = websocket_upgrade(&cx, address, opening.as_bytes()).await?;
if !accepted.starts_with(b"HTTP/1.1 101") {
return Err("the first limited WebSocket connection was not accepted".to_owned());
}
let (_overflow, rejected) = websocket_upgrade(&cx, address, opening.as_bytes()).await?;
if rejected.starts_with(b"HTTP/1.1 101") {
return Err(
"N+1 WebSocket connection created a child despite the limiter".to_owned(),
);
}
drop(first);
// The permit belongs to the accepted child's full lifetime. A
// later handshake proves it was released when that child ended.
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut replacement = None;
while cx.now() < deadline {
let (stream, response) =
websocket_upgrade(&cx, address, opening.as_bytes()).await?;
if response.starts_with(b"HTTP/1.1 101") {
replacement = Some(stream);
break;
}
asupersync::time::sleep(cx.now(), Duration::from_millis(1)).await;
}
drop(replacement.ok_or_else(|| {
"WebSocket permit did not release with its connection child".to_owned()
})?);
listener_task.abort();
let join_cx = Cx::for_testing();
let shutdown = listener_task
.join(&join_cx)
.await
.map_err(|error| format!("limited WebSocket listener join failed: {error:?}"))?
.map_err(|error| format!("limited WebSocket listener failed: {error}"))?;
if !matches!(shutdown, WebSocketServerShutdown::Quiescent) {
return Err("limited WebSocket shutdown retained a child".to_owned());
}
Ok(())
});
}
#[cfg(feature = "websocket")]
#[test]
fn live_websocket_upgrade_authentication_rejects_before_101_and_allows_bearer() {
run_live_http_test(|cx| async move {
use asupersync::io::{AsyncReadExt, AsyncWriteExt};
let provider_calls = Arc::new(AtomicUsize::new(0));
let handler_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let saw_credential = Arc::new(AtomicBool::new(false));
let listener = Server::new("live-websocket-auth", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern-only policy is available")
.auth_provider(OneShotNativeAuthProvider {
calls: Arc::clone(&provider_calls),
})
.middleware(ModernHttpAuthMiddleware {
calls: Arc::clone(&middleware_calls),
saw_credential: Arc::clone(&saw_credential),
})
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&handler_calls),
})
.build()
.bind_websocket(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("WebSocket auth bind failed: {error}"))?;
let address = listener
.local_addr()
.map_err(|error| format!("WebSocket auth address failed: {error}"))?;
let scope = cx.scope();
let mut listener_task = cx
.spawn_in(&scope, move |listener_cx| async move {
listener.serve(&listener_cx).await
})
.map_err(|error| format!("WebSocket auth listener admission failed: {error}"))?;
let unsigned = concat!(
"GET /mcp HTTP/1.1\r\nHost: localhost\r\nUpgrade: websocket\r\n",
"Connection: Upgrade\r\nSec-WebSocket-Version: 13\r\n",
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\r\n"
);
let (_missing, missing) = websocket_upgrade(&cx, address, unsigned.as_bytes()).await?;
if !missing.starts_with(b"HTTP/1.1 401")
|| !String::from_utf8_lossy(&missing)
.to_ascii_lowercase()
.contains("www-authenticate: bearer")
{
return Err(
"missing WebSocket bearer committed a 101 or omitted challenge".to_owned(),
);
}
let duplicate = unsigned.replace(
"\r\n\r\n",
"\r\nAuthorization: Bearer alpha\r\naUtHoRiZaTiOn: Bearer beta\r\n\r\n",
);
let (_duplicate, duplicate_response) =
websocket_upgrade(&cx, address, duplicate.as_bytes()).await?;
if !duplicate_response.starts_with(b"HTTP/1.1 401")
|| !String::from_utf8_lossy(&duplicate_response)
.to_ascii_lowercase()
.contains("www-authenticate: bearer")
{
return Err(
"duplicate WebSocket Authorization did not receive 401 before 101".to_owned(),
);
}
for malformed in [
unsigned.replace("\r\n\r\n", "\r\nAuthorization Bearer alpha\r\n\r\n"),
unsigned.replace("\r\n\r\n", "\r\nAuthorization : Bearer alpha\r\n\r\n"),
] {
let (_malformed, response) =
websocket_upgrade(&cx, address, malformed.as_bytes()).await?;
if !response.starts_with(b"HTTP/1.1 401")
|| !String::from_utf8_lossy(&response)
.to_ascii_lowercase()
.contains("www-authenticate: bearer")
{
return Err(
"malformed Authorization intent did not receive the bounded challenge"
.to_owned(),
);
}
}
let unrelated_malformed = unsigned.replace("\r\n\r\n", "\r\nX-Test malformed\r\n\r\n");
let (_unrelated, unrelated_response) =
websocket_upgrade(&cx, address, unrelated_malformed.as_bytes()).await?;
if !unrelated_response.starts_with(b"HTTP/1.1 400") {
return Err("unrelated malformed Upgrade header was not retained as 400".to_owned());
}
if provider_calls.load(Ordering::Acquire) != 0
|| handler_calls.load(Ordering::Acquire) != 0
|| middleware_calls.load(Ordering::Acquire) != 0
{
return Err(
"pre-101 missing, duplicate, or malformed admission evaluated provider or dispatch"
.to_owned(),
);
}
let valid = unsigned.replace("\r\n\r\n", "\r\nAuthorization: Bearer alpha\r\n\r\n");
let (mut accepted, response) =
websocket_upgrade(&cx, address, valid.as_bytes()).await?;
if !response.starts_with(b"HTTP/1.1 101") {
return Err("valid WebSocket bearer did not pass Upgrade admission".to_owned());
}
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "modern_http_auth_counter",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
7_104_i64,
);
accepted
.write_all(&masked_websocket_message(JsonRpcMessage::Request(request)))
.await
.map_err(|error| format!("valid WebSocket tool write failed: {error}"))?;
accepted
.flush()
.await
.map_err(|error| format!("valid WebSocket tool flush failed: {error}"))?;
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut response = Vec::new();
while !response
.windows("authenticated modern HTTP dispatch".len())
.any(|window| window == b"authenticated modern HTTP dispatch")
{
let mut chunk = [0_u8; 1024];
let read = asupersync::time::timeout_at(deadline, accepted.read(&mut chunk))
.await
.map_err(|_| "valid WebSocket tool response timed out".to_owned())?
.map_err(|error| format!("valid WebSocket tool response failed: {error}"))?;
if read == 0 {
return Err("valid WebSocket bearer closed before handler dispatch".to_owned());
}
response.extend_from_slice(&chunk[..read]);
}
if handler_calls.load(Ordering::Acquire) != 1
|| provider_calls.load(Ordering::Acquire) != 1
|| middleware_calls.load(Ordering::Acquire) != 1
|| saw_credential.load(Ordering::Acquire)
{
return Err(
"valid WebSocket bearer did not consume exactly one custody evaluation"
.to_owned(),
);
}
// The first provider evaluation above was the accepted bearer.
// This second evaluation must be counted and denied before 101;
// it must not produce another custody or dispatch mutation.
let invalid = unsigned.replace(
"\r\n\r\n",
"\r\nAuthorization: Bearer invalid-never-echo\r\n\r\n",
);
let (_invalid, invalid_response) =
websocket_upgrade(&cx, address, invalid.as_bytes()).await?;
if !invalid_response.starts_with(b"HTTP/1.1 401")
|| String::from_utf8_lossy(&invalid_response).contains("invalid-never-echo")
|| provider_calls.load(Ordering::Acquire) != 2
|| handler_calls.load(Ordering::Acquire) != 1
|| middleware_calls.load(Ordering::Acquire) != 1
{
return Err(
"second provider evaluation did not reject invalid WebSocket bearer before 101"
.to_owned(),
);
}
let mixed = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"authorization": "Bearer never-log-or-handle",
"name": "modern_http_auth_counter",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
7_105_i64,
);
accepted
.write_all(&masked_websocket_message(JsonRpcMessage::Request(mixed)))
.await
.map_err(|error| format!("mixed WebSocket credential write failed: {error}"))?;
accepted
.flush()
.await
.map_err(|error| format!("mixed WebSocket credential flush failed: {error}"))?;
let mut rejection = [0_u8; 1024];
let rejected = asupersync::time::timeout_at(deadline, accepted.read(&mut rejection))
.await
.map_err(|_| "mixed WebSocket credential response timed out".to_owned())?
.map_err(|error| format!("mixed WebSocket credential response failed: {error}"))?;
if rejected == 0
|| !rejection[..rejected]
.windows(5)
.any(|window| window == b"error")
|| String::from_utf8_lossy(&rejection[..rejected]).contains("never-log-or-handle")
|| handler_calls.load(Ordering::Acquire) != 1
|| provider_calls.load(Ordering::Acquire) != 2
|| middleware_calls.load(Ordering::Acquire) != 1
|| saw_credential.load(Ordering::Acquire)
{
return Err(
"mixed WebSocket credential escaped custody or reached dispatch".to_owned(),
);
}
drop(accepted);
listener_task.abort();
let join_cx = Cx::for_testing();
let shutdown = listener_task
.join(&join_cx)
.await
.map_err(|error| format!("WebSocket auth listener join failed: {error:?}"))?
.map_err(|error| format!("WebSocket auth listener failed: {error}"))?;
if !matches!(shutdown, WebSocketServerShutdown::Quiescent) {
return Err("WebSocket authentication shutdown retained a child".to_owned());
}
Ok(())
});
}
#[cfg(feature = "websocket")]
#[test]
fn websocket_nonquiescent_settle_for_drains_successful_retained_child_once() {
run_live_http_test(|cx| async move {
let shutdown_calls = Arc::new(AtomicUsize::new(0));
let shutdown_observer = Arc::clone(&shutdown_calls);
let server = Arc::new(
Server::new("websocket-settlement-success", "1.0.0")
.on_shutdown(move || {
shutdown_observer.fetch_add(1, Ordering::AcqRel);
})
.build(),
);
let release_child = Arc::new(AtomicBool::new(false));
let child_release = Arc::clone(&release_child);
let child_started = Arc::new(AtomicBool::new(false));
let child_started_observer = Arc::clone(&child_started);
let scope = cx.scope();
let child = cx
.spawn_in(&scope, move |_child_cx| async move {
child_started_observer.store(true, Ordering::Release);
while !child_release.load(Ordering::Acquire) {
asupersync::runtime::yield_now().await;
}
Ok(())
})
.map_err(|error| format!("successful retained child admission failed: {error}"))?;
let deadline = Instant::now() + Duration::from_secs(1);
while !child_started.load(Ordering::Acquire) && Instant::now() < deadline {
asupersync::runtime::yield_now().await;
}
if !child_started.load(Ordering::Acquire) {
return Err("successful retained child did not begin".to_owned());
}
let mut retained = WebSocketNonquiescentShutdown {
children: WebSocketConnectionChildren {
tasks: vec![child],
terminal_failure: Arc::default(),
},
server,
listener_error: None,
shutdown_complete: false,
};
if retained.remaining_connections() != 1 {
return Err("successful retained child was not caller-owned".to_owned());
}
release_child.store(true, Ordering::Release);
if !retained
.settle_for(Duration::from_secs(1))
.await
.map_err(|error| format!("successful retained child settlement failed: {error}"))?
|| retained.remaining_connections() != 0
|| shutdown_calls.load(Ordering::Acquire) != 1
{
return Err(
"successful retained child did not drain to one cleanup-backed settlement"
.to_owned(),
);
}
Ok(())
});
}
#[cfg(feature = "websocket")]
#[test]
fn websocket_nonquiescent_settle_for_retains_abort_race_and_reaped_panic() {
run_live_http_test(|cx| async move {
let shutdown_calls = Arc::new(AtomicUsize::new(0));
let shutdown_observer = Arc::clone(&shutdown_calls);
let server = Arc::new(
Server::new("websocket-settlement-drain", "1.0.0")
.on_shutdown(move || {
shutdown_observer.fetch_add(1, Ordering::AcqRel);
})
.build(),
);
let scope = cx.scope();
let release_live_child = Arc::new(AtomicBool::new(false));
let live_child_release = Arc::clone(&release_live_child);
let live_child_started = Arc::new(AtomicBool::new(false));
let live_child_started_observer = Arc::clone(&live_child_started);
let retained_live = cx
.spawn_in(&scope, move |_child_cx| async move {
live_child_started_observer.store(true, Ordering::Release);
while !live_child_release.load(Ordering::Acquire) {
// Deliberately ignore the child Cx cancellation. This
// is the shutdown-debt shape that must remain caller
// owned after the listener aborts its child.
asupersync::runtime::yield_now().await;
}
Ok(())
})
.map_err(|error| format!("retained WebSocket child admission failed: {error}"))?;
let started_deadline = Instant::now() + Duration::from_secs(1);
while !live_child_started.load(Ordering::Acquire) && Instant::now() < started_deadline {
asupersync::runtime::yield_now().await;
}
if !live_child_started.load(Ordering::Acquire) {
return Err("WebSocket retained child did not begin before abort".to_owned());
}
let failed = cx
.spawn_in(&scope, move |_child_cx| async move {
panic!("forced WebSocket child failure")
})
.map_err(|error| format!("failed WebSocket child admission failed: {error}"))?;
retained_live.abort();
let mut children = WebSocketConnectionChildren {
tasks: vec![retained_live, failed],
terminal_failure: Arc::default(),
};
let deadline = Instant::now() + Duration::from_secs(1);
while !children.has_terminal_failure() && Instant::now() < deadline {
children.reap_finished();
if !children.has_terminal_failure() {
asupersync::runtime::yield_now().await;
}
}
if !children.has_terminal_failure() {
return Err("WebSocket child reaper did not retain the forced panic".to_owned());
}
let retained = WebSocketServerShutdown::Nonquiescent(WebSocketNonquiescentShutdown {
children,
server,
listener_error: None,
shutdown_complete: false,
});
let WebSocketServerShutdown::Nonquiescent(retained) = retained else {
return Err("forced WebSocket retained work did not report Nonquiescent".to_owned());
};
if retained.remaining_connections() != 1 {
return Err(
"WebSocket abort race did not retain exactly one live caller-owned child"
.to_owned(),
);
}
let mut retained = retained;
if retained
.settle_for(Duration::ZERO)
.await
.map_err(|error| format!("WebSocket retained-child probe failed: {error}"))?
{
return Err(
"WebSocket settle_for falsely reported an abort-race child as settled"
.to_owned(),
);
}
if retained.remaining_connections() != 1 {
return Err("WebSocket settle_for detached or lost the abort-race child".to_owned());
}
release_live_child.store(true, Ordering::Release);
let error = retained
.settle_for(Duration::from_secs(1))
.await
.expect_err("a real child failure must be returned after all children drain");
if !error
.message
.contains("WebSocket connection child settlement failure")
|| shutdown_calls.load(Ordering::Acquire) != 1
{
return Err(
"WebSocket settlement skipped cleanup or hid its recorded child failure"
.to_owned(),
);
}
Ok(())
});
}
#[cfg(feature = "websocket")]
#[test]
fn websocket_settle_for_returns_listener_failure_after_cleanup() {
run_live_http_test(|_cx| async move {
let shutdown_calls = Arc::new(AtomicUsize::new(0));
let shutdown_observer = Arc::clone(&shutdown_calls);
let server = Arc::new(
Server::new("websocket-settle-for-listener-error", "1.0.0")
.on_shutdown(move || {
shutdown_observer.fetch_add(1, Ordering::AcqRel);
})
.build(),
);
let mut retained = WebSocketNonquiescentShutdown {
children: WebSocketConnectionChildren::default(),
server,
listener_error: Some(McpError::internal_error("forced accept-loop failure")),
shutdown_complete: false,
};
let error = retained
.settle_for(Duration::ZERO)
.await
.expect_err("settle_for must not hide a recorded listener failure");
if !error.message.contains("forced accept-loop failure")
|| shutdown_calls.load(Ordering::Acquire) != 1
{
return Err(
"WebSocket settle_for did not preserve listener error after cleanup".to_owned(),
);
}
Ok(())
});
}
fn live_http_test_timeout(operation: &str) -> String {
format!("Timeout while waiting for {operation}")
}
#[test]
fn boxed_extension_settings_resolver_preserves_inactive_disposition() {
struct InactiveResolver;
impl ExtensionSettingsCompatibilityResolver for InactiveResolver {
fn resolve(
&mut self,
descriptor: &ExtensionDescriptor,
_client: &ExtensionSettings,
_server: &ExtensionSettings,
) -> Result<ExtensionSettings, ExtensionNegotiationError> {
Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
descriptor.id.to_string(),
))
}
fn resolve_with_disposition(
&mut self,
_descriptor: &ExtensionDescriptor,
_client: &ExtensionSettings,
_server: &ExtensionSettings,
) -> Result<ExtensionSettingsResolution, ExtensionNegotiationError> {
Ok(ExtensionSettingsResolution::Inactive)
}
}
let settings = ExtensionSettings::new(serde_json::json!({}))
.expect("empty extension settings must be valid");
let mut resolver = BoxedExtensionSettingsResolver(Box::new(InactiveResolver));
let disposition = resolver
.resolve_with_disposition(
&fastmcp_protocol::extensions::official_mcp_apps_descriptor(),
&settings,
&settings,
)
.expect("boxed resolver must retain an inactive disposition");
assert_eq!(disposition, ExtensionSettingsResolution::Inactive);
}
#[cfg(all(feature = "apps", feature = "tasks"))]
#[test]
fn builder_mcp_apps_composes_with_final_tasks_and_preserves_inactive_apps() {
let delivered = Arc::new(Mutex::new(Vec::new()));
let server = Server::new("apps-and-tasks-server", "1.0.0")
.mcp_apps()
.expect("official Apps installs before final Tasks")
.final_tasks(final_tasks_test_runtime(Arc::clone(&delivered)))
.expect("final Tasks composes with official Apps")
.build();
let apps_id = fastmcp_protocol::official_mcp_apps_extension_id();
let tasks_id = fastmcp_protocol::official_tasks_extension_id();
assert_eq!(
server
.extension_registry_receipt()
.expect("Apps and Tasks registry is frozen")
.descriptor_count(),
2
);
let discovery = serde_json::to_value(
server
.server_discovery()
.expect("Apps and Tasks discovery is available"),
)
.expect("Apps and Tasks discovery serializes");
assert_eq!(
discovery.pointer("/capabilities/extensions/io.modelcontextprotocol~1ui"),
Some(&serde_json::json!({}))
);
assert_eq!(
discovery.pointer("/capabilities/extensions/io.modelcontextprotocol~1tasks"),
Some(&serde_json::json!({}))
);
let client = ClientExtensionDiscovery {
extensions: BTreeMap::from([
(
apps_id.clone(),
McpAppsClientSettings::new(vec!["text/plain".to_owned()])
.expect("another bounded MIME type is valid Apps settings")
.to_extension_settings(),
),
(tasks_id.clone(), official_tasks_empty_settings()),
]),
};
let negotiated = server
.negotiate_extensions(&client)
.expect("valid inactive Apps settings do not reject Tasks negotiation");
assert_eq!(
negotiated.inactive_reason(&apps_id),
Some(ExtensionInactiveReason::SettingsInactiveFallback)
);
assert!(negotiated.active(&tasks_id).is_some());
}
#[cfg(feature = "apps")]
#[test]
fn builder_mcp_apps_ui_resource_binds_a_final_only_ui_catalog_entry() {
let resource = crate::providers::McpAppsUiResource::try_new(
fastmcp_protocol::common_types::AbsoluteUri::parse("ui://weather/dashboard")
.expect("valid ui URI"),
"weather-dashboard",
"<main>weather</main>",
)
.expect("valid Apps UI resource");
let server = Server::new("apps-ui-resource", "1.0")
.mcp_apps()
.expect("Apps capability installs")
.mcp_apps_ui_resource(resource)
.expect("Apps UI resource binds after Apps opt-in")
.build();
let inbound =
InboundRequestContext::new(Cx::for_testing(), 901, InboundRequestTransport::Memory);
let response = block_on(server.dispatch_stateless(
&inbound,
&JsonRpcRequest::new(
"resources/list",
Some(serde_json::json!({
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
},
})),
901_i64,
),
))
.expect("modern resource catalog dispatch succeeds");
let resources = response.result.expect("modern resource list has a result");
assert_eq!(resources["resources"][0]["uri"], "ui://weather/dashboard");
assert_eq!(
resources["resources"][0]["mimeType"],
fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE
);
let read_inbound =
InboundRequestContext::new(Cx::for_testing(), 902, InboundRequestTransport::Memory);
let read_request = JsonRpcRequest::new(
"resources/read",
Some(serde_json::json!({
"uri": "ui://weather/dashboard",
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {},
},
})),
902_i64,
);
let read = block_on(server.dispatch_stateless(&read_inbound, &read_request))
.expect("modern Apps UI read reaches its final-only resource");
assert!(
read.error.is_none(),
"modern Apps UI read must succeed: {:?}",
read.error
);
let contents = read.result.expect("modern UI read has a result");
assert_eq!(contents["contents"][0]["text"], "<main>weather</main>");
assert_eq!(
contents["contents"][0]["mimeType"],
fastmcp_protocol::MCP_APPS_HTML_MIME_TYPE
);
assert!(
server.resources().is_empty(),
"the Apps UI document must remain absent from the exact-2024 catalog"
);
}
#[cfg(feature = "apps")]
#[test]
fn builder_mcp_apps_ui_resource_rejects_only_missing_apps_opt_in_without_mutation() {
let resource = crate::providers::McpAppsUiResource::try_new(
fastmcp_protocol::common_types::AbsoluteUri::parse("ui://weather/dashboard")
.expect("valid ui URI"),
"weather-dashboard",
"<main>weather</main>",
)
.expect("valid Apps UI resource");
let error = match Server::new("apps-ui-resource", "1.0").mcp_apps_ui_resource(resource) {
Ok(_) => panic!("changing only the missing Apps opt-in must reject the UI resource"),
Err(error) => error,
};
assert_eq!(error.code, McpErrorCode::InvalidRequest);
}
#[cfg(all(feature = "apps", feature = "tasks"))]
#[test]
fn builder_final_tasks_then_mcp_apps_preserves_both_discovery_markers() {
let server = Server::new("tasks-then-apps-server", "1.0.0")
.final_tasks(final_tasks_test_runtime(Arc::new(Mutex::new(Vec::new()))))
.expect("final Tasks configures before official Apps")
.mcp_apps()
.expect("official Apps composes after final Tasks")
.build();
let discovery = serde_json::to_value(
server
.server_discovery()
.expect("Apps and Tasks discovery is available"),
)
.expect("Apps and Tasks discovery serializes");
assert_eq!(
discovery.pointer("/capabilities/extensions/io.modelcontextprotocol~1ui"),
Some(&serde_json::json!({}))
);
assert_eq!(
discovery.pointer("/capabilities/extensions/io.modelcontextprotocol~1tasks"),
Some(&serde_json::json!({}))
);
}
#[derive(Debug, Default)]
struct HttpOverlapControlState {
enabled: bool,
entries: usize,
permits: usize,
}
#[derive(Debug, Default)]
struct HttpOverlapControl {
state: Mutex<HttpOverlapControlState>,
changed: Condvar,
}
impl HttpOverlapControl {
fn begin(&self) -> HttpOverlapControlGuard<'_> {
let mut state = self.state.lock().expect("HTTP overlap control poisoned");
state.enabled = true;
state.entries = 0;
state.permits = 0;
HttpOverlapControlGuard { control: self }
}
fn enter_and_wait(&self) {
let mut state = self.state.lock().expect("HTTP overlap control poisoned");
if !state.enabled {
return;
}
state.entries += 1;
self.changed.notify_all();
while state.enabled && state.permits == 0 {
state = self
.changed
.wait(state)
.expect("HTTP overlap control poisoned while waiting");
}
state.permits = state.permits.saturating_sub(1);
}
fn wait_for_entries(&self, target: usize, timeout: Duration) -> bool {
let state = self.state.lock().expect("HTTP overlap control poisoned");
let (state, _) = self
.changed
.wait_timeout_while(state, timeout, |state| state.entries < target)
.expect("HTTP overlap control poisoned while observing entries");
state.entries >= target
}
fn release_one(&self) {
let mut state = self.state.lock().expect("HTTP overlap control poisoned");
state.permits = state.permits.saturating_add(1);
self.changed.notify_all();
}
fn disable(&self) {
let mut state = self.state.lock().expect("HTTP overlap control poisoned");
state.enabled = false;
state.permits = 0;
self.changed.notify_all();
}
}
struct HttpOverlapControlGuard<'a> {
control: &'a HttpOverlapControl,
}
impl Drop for HttpOverlapControlGuard<'_> {
fn drop(&mut self) {
self.control.disable();
}
}
fn http_overlap_metrics() -> &'static HttpOverlapMetrics {
HTTP_OVERLAP_METRICS.get_or_init(HttpOverlapMetrics::default)
}
fn http_overlap_lock() -> &'static Mutex<()> {
HTTP_OVERLAP_LOCK.get_or_init(|| Mutex::new(()))
}
fn http_overlap_control() -> &'static HttpOverlapControl {
HTTP_OVERLAP_CONTROL.get_or_init(HttpOverlapControl::default)
}
pub(super) fn record_live_http_listener_wait() {
LIVE_HTTP_LISTENER_WAITS.fetch_add(1, Ordering::AcqRel);
}
pub(super) fn record_live_http_connection_read_wait() {
LIVE_HTTP_CONNECTION_READ_WAITS.fetch_add(1, Ordering::AcqRel);
}
fn live_http_listener_wait_count() -> usize {
LIVE_HTTP_LISTENER_WAITS.load(Ordering::Acquire)
}
fn live_http_connection_read_wait_count() -> usize {
LIVE_HTTP_CONNECTION_READ_WAITS.load(Ordering::Acquire)
}
async fn wait_for_live_http_probe(
cx: &Cx,
before: usize,
probe: fn() -> usize,
) -> Result<(), String> {
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while probe() <= before {
cx.checkpoint()
.map_err(|error| format!("live HTTP probe caller stopped: {error}"))?;
asupersync::time::timeout_at(
deadline,
asupersync::time::sleep(cx.now(), Duration::from_millis(1)),
)
.await
.map_err(|_| live_http_test_timeout("live HTTP probe"))?;
}
Ok(())
}
async fn wait_for_live_http_flag(
cx: &Cx,
flag: &AtomicBool,
operation: &str,
) -> Result<(), String> {
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while !flag.load(Ordering::Acquire) {
cx.checkpoint()
.map_err(|error| format!("{operation} caller stopped: {error}"))?;
asupersync::time::timeout_at(
deadline,
asupersync::time::sleep(cx.now(), Duration::from_millis(1)),
)
.await
.map_err(|_| live_http_test_timeout(operation))?;
}
Ok(())
}
async fn wait_for_live_http_test_result<T>(receiver: Receiver<T>) -> Result<T, String> {
let cx = Cx::current().expect("the test runtime must install an ambient Cx while polling");
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
loop {
match receiver.try_recv() {
Ok(result) => return Ok(result),
Err(TryRecvError::Disconnected) => {
panic!("live HTTP test task exited without reporting its result");
}
Err(TryRecvError::Empty) => {
asupersync::time::timeout_at(
deadline,
asupersync::time::sleep(cx.now(), Duration::from_millis(1)),
)
.await
.map_err(|_| live_http_test_timeout("live HTTP test result"))?;
}
}
}
}
/// Runnable time one whole live HTTP test may take.
///
/// Each client step has its own [`LIVE_HTTP_TEST_TIMEOUT_NANOS`] bound,
/// but `serve` returns only once its caller `Cx` is cancelled, and most
/// tests cancel it only on their client's success path. A client that
/// failed first therefore left `serve` waiting forever. In a no-legacy test
/// build that forced the legacy loop, four tests hung the process this way
/// until the runner killed it:
/// `live_http_legacy_initialize_is_peer_cancellation_protected`,
/// `live_http_legacy_sse_advertises_the_request_host_on_wildcard_bind`,
/// `live_http_legacy_sse_peer_close_cancels_its_busy_request` and
/// `live_http_legacy_sse_peer_close_does_not_cancel_another_session`.
const LIVE_HTTP_WHOLE_TEST_BOUND: Duration = Duration::from_secs(60);
fn run_live_http_test<F, Fut>(operation: F)
where
F: FnOnce(Cx) -> Fut + Send + 'static,
Fut: Future<Output = Result<(), String>> + Send + 'static,
{
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("live HTTP test reactor must initialize"))
.blocking_threads(4, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("live HTTP test runtime must initialize");
let host = RunnableClock::start();
let (result, expired) = runtime.block_on(async move {
let cx = Cx::current().expect("the live HTTP test runtime must install an ambient Cx");
// On expiry the watchdog cancels the caller `Cx`, which is what
// lets a `serve` with no remaining canceller return, so the test
// fails with this bound's message instead of hanging.
let (finished, finished_signal) = sync_channel::<()>(0);
let watchdog_cx = cx.clone();
let watchdog = thread::spawn(move || {
let started = host.mark();
while !host.expired(started, LIVE_HTTP_WHOLE_TEST_BOUND) {
if !matches!(
finished_signal.recv_timeout(Duration::from_millis(50)),
Err(std::sync::mpsc::RecvTimeoutError::Timeout)
) {
return None;
}
}
watchdog_cx.cancel_with(
CancelKind::Deadline,
Some("live HTTP test exceeded its whole-test bound"),
);
Some(host.describe(started))
});
let result = operation(cx).await;
drop(finished);
let expired = watchdog
.join()
.expect("the live HTTP test watchdog must not panic");
(result, expired)
});
if let Some(elapsed) = expired {
panic!(
"live HTTP test exceeded its {LIVE_HTTP_WHOLE_TEST_BOUND:?} whole-test bound after {elapsed}; the watchdog cancelled its caller Cx so a serve left without a canceller could return. Test result: {result:?}"
);
}
result.unwrap();
}
async fn require_quiescent_http_shutdown(
shutdown: HttpServerShutdown,
probe: &str,
) -> Result<(), String> {
match shutdown {
HttpServerShutdown::Quiescent => Ok(()),
HttpServerShutdown::Nonquiescent(mut shutdown) => {
let initial = shutdown.poll_settlement();
let settled = shutdown.settle_for(Duration::from_secs(1)).await;
Err(format!(
"{probe} unexpectedly retained caller-owned HTTP children; initial={initial:?}, after bounded settlement={settled:?}"
))
}
}
}
fn run_live_modern_pump<R>(server: Server, recv: R, responses: Arc<LiveModernResponses>) -> i32
where
R: FnMut(&Cx, &AtomicBool) -> Result<JsonRpcMessage, TransportError> + Send + 'static,
{
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("live modern test reactor must initialize"))
.blocking_threads(4, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("live modern test runtime must initialize");
let (sender, receiver) = sync_channel(1);
runtime
.block_on(async move {
let cx =
Cx::current().expect("the live modern test runtime must install an ambient Cx");
let dispatch_cx = cx.clone();
match cx.spawn_blocking(move |pump_cx| {
let result = Arc::new(server).run_loop_pump_with_policy(
&pump_cx,
&dispatch_cx,
recv,
move |_send_cx, message| {
responses.record(message.clone());
Ok(())
},
Arc::new(|_| {}),
"live-modern-test",
false,
None,
true,
true,
None,
None,
None,
PumpIoMode::Split,
);
let _ = sender.send(result);
}) {
// The blocking closure owns the product return value.
// Joining its cancellation-dominant wrapper can instead
// report shutdown when the pump closes the server region
// immediately after producing that value.
Ok(_pump) => wait_for_live_http_test_result(receiver).await,
Err(error) => Err(format!("live modern pump admission failed: {error}")),
}
})
.expect("live modern pump must complete")
}
fn run_live_split_transport<R, S>(
completion_timeout: Duration,
server: Server,
recv: R,
send: S,
) -> Result<(), String>
where
R: TransportRecvHalf + Send + 'static,
S: TransportSendHalf + 'static,
{
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("live split transport reactor must initialize"))
.blocking_threads(4, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("live split transport runtime must initialize");
let (sender, receiver) = sync_channel(1);
runtime.block_on(async move {
let cx =
Cx::current().expect("the live split transport runtime must install an ambient Cx");
let dispatch_cx = cx.clone();
match cx.spawn_blocking(move |pump_cx| {
let result = server.run_split_transport_returning_with_dispatch_cx(
&pump_cx,
&dispatch_cx,
recv,
send,
);
let _ = sender.send(result);
}) {
// Read the value from the completed blocking closure itself;
// the wrapper join is cancellation-dominant during shutdown.
Ok(_pump) => {
// Counted in runnable time, so a loaded full suite cannot
// expire the bound while a stalled transport still does.
let host = RunnableClock::start();
let started = host.mark();
loop {
match receiver.try_recv() {
Ok(result) => break result.map_err(|error| error.to_string()),
Err(TryRecvError::Disconnected) => {
panic!(
"live split transport task exited without reporting its result"
);
}
Err(TryRecvError::Empty) => {
if host.expired(started, completion_timeout) {
return Err(format!(
"{} after {}",
live_http_test_timeout("live split transport result"),
host.describe(started)
));
}
asupersync::time::sleep(cx.now(), Duration::from_millis(1)).await;
}
}
}
}
Err(error) => Err(format!("live split transport admission failed: {error}")),
}
})
}
async fn live_http_exchange(address: SocketAddr, request: Vec<u8>) -> Result<Vec<u8>, String> {
let cx = Cx::current()
.expect("the test runtime must install an ambient Cx for live HTTP exchange");
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut stream = asupersync::time::timeout_at(deadline, AsyncTcpStream::connect(address))
.await
.map_err(|_| live_http_test_timeout("live HTTP client connection"))?
.map_err(|error| format!("live HTTP client connect failed: {error}"))?;
asupersync::time::timeout_at(deadline, stream.write_all(&request))
.await
.map_err(|_| live_http_test_timeout("live HTTP client request write"))?
.map_err(|error| format!("live HTTP client write failed: {error}"))?;
asupersync::time::timeout_at(deadline, stream.flush())
.await
.map_err(|_| live_http_test_timeout("live HTTP client request flush"))?
.map_err(|error| format!("live HTTP client flush failed: {error}"))?;
let mut response = Vec::new();
read_live_http_to_end(&mut stream, &mut response, "live HTTP client response EOF").await?;
Ok(response)
}
fn live_http_post(
path_and_query: &str,
body: &[u8],
extra_headers: &[(&str, &str)],
) -> Vec<u8> {
let mut request = format!(
"POST {path_and_query} HTTP/1.1\r\nHost: loopback\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n",
body.len()
);
for (name, value) in extra_headers {
request.push_str(name);
request.push_str(": ");
request.push_str(value);
request.push_str("\r\n");
}
request.push_str("\r\n");
let mut request = request.into_bytes();
request.extend_from_slice(body);
request
}
async fn live_http_strict_admission_exchange(
cx: &Cx,
mirrored_method_header: &'static str,
) -> Result<Vec<u8>, String> {
live_http_admission_exchange(
cx,
vec![
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", mirrored_method_header),
],
)
.await
}
async fn live_http_admission_exchange(
cx: &Cx,
headers: Vec<(&'static str, &'static str)>,
) -> Result<Vec<u8>, String> {
let bound = Server::new("live-http-strict-admission", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build()
.bind_http(cx, "127.0.0.1:0")
.await
.map_err(|error| format!("strict admission HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("strict admission HTTP address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
803_i64,
);
let body = serde_json::to_vec(&request).map_err(|error| {
format!("strict admission request did not serialize: {error}")
})?;
let response =
live_http_exchange(address, live_http_post("/mcp", &body, &headers)).await;
caller_cx.cancel_with(CancelKind::User, Some("strict admission exchange complete"));
response
})
.map_err(|error| format!("strict admission client admission failed: {error}"))?;
let serve = bound.serve(cx).await;
let response = client
.join(cx)
.await
.map_err(|error| format!("strict admission client failed: {error:?}"))??;
let shutdown = serve.map_err(|error| format!("strict admission server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "strict admission").await?;
Ok(response)
}
fn live_http_response_body(response: &[u8]) -> Result<&[u8], String> {
response
.windows(4)
.position(|window| window == b"\r\n\r\n")
.map(|offset| &response[offset + 4..])
.ok_or_else(|| "live HTTP response omitted its header terminator".to_string())
}
fn live_http_sse_jsonrpc_response(response: &[u8]) -> Result<JsonRpcResponse, String> {
let body = std::str::from_utf8(live_http_response_body(response)?)
.map_err(|error| format!("live SSE body was not UTF-8: {error}"))?;
let data = body
.rsplit_once("data: ")
.map(|(_, data)| data)
.and_then(|data| data.split_once("\n\n").map(|(data, _)| data))
.ok_or_else(|| "live SSE body omitted a JSON-RPC event".to_owned())?;
serde_json::from_str(data)
.map_err(|error| format!("live SSE JSON-RPC event was invalid: {error}"))
}
/// Parses the full chunked H1 body rather than searching its text. This
/// keeps live SSE assertions honest about frame boundaries, ordering, and
/// terminal EOF: an extra notification after the response is a fourth
/// parsed frame, not an overlooked substring.
fn live_http_chunked_sse_messages(response: &[u8]) -> Result<Vec<JsonRpcMessage>, String> {
let mut remaining = live_http_response_body(response)?;
let mut messages = Vec::new();
loop {
let Some(line_end) = remaining.windows(2).position(|window| window == b"\r\n") else {
return Err("live SSE chunk omitted its size terminator".to_owned());
};
let chunk_size = std::str::from_utf8(&remaining[..line_end])
.map_err(|error| format!("live SSE chunk size was not UTF-8: {error}"))?;
let chunk_size = usize::from_str_radix(chunk_size, 16)
.map_err(|error| format!("live SSE chunk size was invalid: {error}"))?;
remaining = &remaining[line_end + 2..];
if chunk_size == 0 {
if remaining != b"\r\n" {
return Err("live SSE terminal chunk had trailing bytes".to_owned());
}
return Ok(messages);
}
if remaining.len() < chunk_size + 2 {
return Err("live SSE chunk was truncated".to_owned());
}
let (chunk, suffix) = remaining.split_at(chunk_size);
if &suffix[..2] != b"\r\n" {
return Err("live SSE chunk omitted its data terminator".to_owned());
}
remaining = &suffix[2..];
let event = std::str::from_utf8(chunk)
.map_err(|error| format!("live SSE event was not UTF-8: {error}"))?;
if !event.starts_with("event: message\n") || !event.ends_with("\n\n") {
return Err(format!(
"live SSE chunk was not one message event: {event:?}"
));
}
let data = event
.lines()
.filter_map(|line| line.strip_prefix("data: "))
.collect::<Vec<_>>();
if data.len() != 1 {
return Err(
"live SSE message event did not carry one JSON-RPC data line".to_owned(),
);
}
messages.push(
Codec::new()
.decode_complete_message(data[0].as_bytes())
.map_err(|error| format!("live SSE JSON-RPC event was invalid: {error}"))?,
);
}
}
fn live_http_response_header(response: &[u8], name: &str) -> Result<String, String> {
let headers = std::str::from_utf8(
response
.windows(4)
.position(|window| window == b"\r\n\r\n")
.map(|offset| &response[..offset])
.ok_or_else(|| "live HTTP response omitted its header terminator".to_string())?,
)
.map_err(|error| format!("live HTTP response headers were not UTF-8: {error}"))?;
headers
.split("\r\n")
.skip(1)
.find_map(|header| {
let (header_name, value) = header.split_once(':')?;
header_name
.eq_ignore_ascii_case(name)
.then(|| value.trim().to_owned())
})
.ok_or_else(|| format!("live HTTP response omitted {name} header"))
}
fn live_http_response_has_header(response: &[u8], name: &str) -> Result<bool, String> {
let headers = std::str::from_utf8(
response
.windows(4)
.position(|window| window == b"\r\n\r\n")
.map(|offset| &response[..offset])
.ok_or_else(|| "live HTTP response omitted its header terminator".to_string())?,
)
.map_err(|error| format!("live HTTP response headers were not UTF-8: {error}"))?;
Ok(headers.split("\r\n").skip(1).any(|header| {
header
.split_once(':')
.is_some_and(|(header_name, _)| header_name.eq_ignore_ascii_case(name))
}))
}
async fn read_live_http_until(
stream: &mut AsyncTcpStream,
received: &mut Vec<u8>,
needle: &[u8],
) -> Result<(), String> {
let cx = Cx::current()
.expect("the test runtime must install an ambient Cx for live HTTP SSE reads");
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while !received
.windows(needle.len())
.any(|window| window == needle)
{
if received.len() > 64 * 1024 {
return Err("live HTTP SSE response exceeded the test bound".to_string());
}
let mut chunk = [0_u8; 2048];
let read = asupersync::time::timeout_at(deadline, stream.read(&mut chunk))
.await
.map_err(|_| live_http_test_timeout("live HTTP SSE response"))?
.map_err(|error| format!("live HTTP SSE read failed: {error}"))?;
if read == 0 {
return Err("live HTTP SSE connection closed before expected event".to_string());
}
received.extend_from_slice(&chunk[..read]);
}
Ok(())
}
async fn read_live_http_to_end(
stream: &mut AsyncTcpStream,
received: &mut Vec<u8>,
operation: &str,
) -> Result<(), String> {
let cx = Cx::current()
.expect("the test runtime must install an ambient Cx for live HTTP EOF reads");
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
asupersync::time::timeout_at(deadline, stream.read_to_end(received))
.await
.map_err(|_| live_http_test_timeout(operation))?
.map_err(|error| format!("{operation} failed: {error}"))?;
Ok(())
}
// Exact-2024 era: opens a legacy HTTP SSE session.
#[cfg(feature = "legacy-2024-11-05")]
async fn open_live_legacy_http_session(
address: SocketAddr,
headers: &[(&str, &str)],
) -> Result<(AsyncTcpStream, String, Vec<u8>), String> {
let cx = Cx::current()
.expect("the test runtime must install an ambient Cx for legacy HTTP SSE setup");
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut stream = asupersync::time::timeout_at(deadline, AsyncTcpStream::connect(address))
.await
.map_err(|_| live_http_test_timeout("legacy HTTP SSE connection"))?
.map_err(|error| format!("legacy HTTP SSE connect failed: {error}"))?;
let mut extra_headers = String::new();
for (name, value) in headers {
extra_headers.push_str(name);
extra_headers.push_str(": ");
extra_headers.push_str(value);
extra_headers.push_str("\r\n");
}
let request = format!(
"GET /sse HTTP/1.1\r\nHost: {address}\r\nAccept: text/event-stream\r\nConnection: keep-alive\r\n{extra_headers}\r\n"
);
asupersync::time::timeout_at(deadline, stream.write_all(request.as_bytes()))
.await
.map_err(|_| live_http_test_timeout("legacy HTTP SSE request write"))?
.map_err(|error| format!("legacy HTTP SSE write failed: {error}"))?;
asupersync::time::timeout_at(deadline, stream.flush())
.await
.map_err(|_| live_http_test_timeout("legacy HTTP SSE request flush"))?
.map_err(|error| format!("legacy HTTP SSE flush failed: {error}"))?;
let mut received = Vec::new();
let endpoint_prefix = format!("data: http://{address}/messages?session_id=");
read_live_http_until(&mut stream, &mut received, endpoint_prefix.as_bytes()).await?;
let received_text = std::str::from_utf8(&received)
.map_err(|error| format!("legacy HTTP SSE endpoint was not UTF-8: {error}"))?;
let session_id = received_text
.split_once(&endpoint_prefix)
.and_then(|(_, remainder)| remainder.split_whitespace().next())
.ok_or_else(|| "legacy HTTP SSE endpoint omitted its session identifier".to_string())?
.to_owned();
Ok((stream, session_id, received))
}
// Exact-2024 era: waits on a legacy HTTP session's admissions.
#[cfg(feature = "legacy-2024-11-05")]
async fn wait_for_live_http_legacy_admission(
cx: &Cx,
session: &LiveHttpSession,
request_id: &RequestId,
) -> Result<(), String> {
let deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while !session.cancellation.admissions.contains(request_id) {
cx.checkpoint()
.map_err(|error| format!("legacy HTTP admission wait stopped: {error}"))?;
asupersync::time::timeout_at(
deadline,
asupersync::time::sleep(cx.now(), Duration::from_millis(1)),
)
.await
.map_err(|_| live_http_test_timeout("legacy HTTP request admission"))?;
}
Ok(())
}
fn reset_http_overlap_metrics() {
let metrics = http_overlap_metrics();
metrics.current.store(0, Ordering::SeqCst);
metrics.max.store(0, Ordering::SeqCst);
}
fn test_request_sender() -> RequestSender {
let pending = Arc::new(PendingRequests::new());
let send_fn: bidirectional::TransportSendFn =
Arc::new(|message| Err(format!("unexpected outbound message in test: {message:?}")));
RequestSender::new(pending, send_fn)
}
fn fixed_test_subject_for_credential(token: &str) -> McpResult<&'static str> {
match token {
"alpha" => Ok("principal-alpha"),
"beta" => Ok("principal-beta"),
_ => Err(McpError::invalid_request("unrecognized auth token")),
}
}
fn initialized_test_session(server: &Server) -> Session {
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "panic-containment-test-client".to_string(),
version: "1.0.0".to_string(),
},
fastmcp_protocol::ClientCapabilities::default(),
"2024-11-05".to_string(),
);
session
}
fn initialize_test_request(
id: i64,
client_name: &str,
capabilities: fastmcp_protocol::ClientCapabilities,
) -> JsonRpcRequest {
JsonRpcRequest::new(
"initialize",
Some(
serde_json::to_value(InitializeParams {
protocol_version: "2024-11-05".to_string(),
capabilities,
client_info: fastmcp_protocol::ClientInfo {
name: client_name.to_string(),
version: "1.0.0".to_string(),
},
})
.expect("serialize initialize request"),
),
id,
)
}
#[test]
fn resource_exhausted_masking_preserves_only_the_fixed_server_contract() {
let fixed = mask_peer_error(resource_subscription_capacity_error(), true);
assert_eq!(
fixed.code,
McpErrorCode::Custom(RESOURCE_EXHAUSTED_ERROR_CODE)
);
assert_eq!(fixed.message, RESOURCE_SUBSCRIPTION_CAPACITY_MESSAGE);
assert!(fixed.data.is_none());
let forged = mask_peer_error(
McpError::with_data(
McpErrorCode::Custom(RESOURCE_EXHAUSTED_ERROR_CODE),
"credential-canary-must-be-masked",
serde_json::json!({"token": "secret-canary"}),
),
true,
);
assert_eq!(forged.message, "Internal server error");
assert!(forged.data.is_none());
}
fn dispatch_test_request(
server: &Server,
session: &mut Session,
method: &str,
) -> JsonRpcResponse {
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
block_on(server.dispatch_request(
&Cx::for_testing(),
session,
JsonRpcRequest::new(method, Some(serde_json::json!({})), 1_i64),
¬ification_sender,
&request_sender,
))
.expect("test request should produce a JSON-RPC response")
}
#[cfg(feature = "tasks")]
fn extension_registry_test_server(calls: Arc<AtomicUsize>) -> Server {
let mut descriptors = ExtensionDescriptorRegistry::new();
let tasks_id = register_official_tasks_extension(&mut descriptors)
.expect("official Tasks descriptor must register");
let mut handlers = ExtensionHandlerRegistry::new(descriptors);
let handler_calls = Arc::clone(&calls);
handlers
.register(
tasks_id.clone(),
"tasks/get",
move |context: &McpContext, request: serde_json::Value| {
assert_eq!(context.request_id(), 71);
assert!(
request
.pointer("/_meta/io.modelcontextprotocol~1protocolVersion")
.is_none(),
"extension handlers must not receive transport protocol metadata"
);
handler_calls.fetch_add(1, Ordering::SeqCst);
let value = request
.get("value")
.and_then(serde_json::Value::as_u64)
.ok_or_else(|| McpError::invalid_params("missing value"))?;
Ok(serde_json::json!({ "next": value + 1 }))
},
)
.expect("official Tasks handler must register");
Server::new("extension-registry-test", "1.0.0")
.extension_registry(
handlers,
ServerExtensionDiscovery {
extensions: std::collections::BTreeMap::from([(
tasks_id,
official_tasks_empty_settings(),
)]),
},
|_descriptor: &fastmcp_protocol::ExtensionDescriptor,
_client: &fastmcp_protocol::ExtensionSettings,
_server: &fastmcp_protocol::ExtensionSettings|
-> Result<fastmcp_protocol::ExtensionSettings, ExtensionNegotiationError> {
Ok(official_tasks_empty_settings())
},
)
.expect("extension registry must install")
.build()
}
#[cfg(feature = "tasks")]
fn extension_tasks_get_request(settings: serde_json::Value) -> JsonRpcRequest {
JsonRpcRequest::new(
"tasks/get",
Some(serde_json::json!({
"taskId": "task-extension-71",
"value": 41,
"_meta": {
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": settings,
},
},
},
})),
71_i64,
)
}
#[cfg(feature = "tasks")]
fn extension_tasks_get_http_request(protocol_version: &str) -> HttpRequest {
let mut request = extension_tasks_get_request(serde_json::json!({}));
*request
.params
.as_mut()
.and_then(|value| value.pointer_mut("/_meta/io.modelcontextprotocol~1protocolVersion"))
.expect("extension request has modern protocol metadata") =
serde_json::json!(protocol_version);
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json")
.with_header("mcp-protocol-version", protocol_version)
.with_header("mcp-method", "tasks/get")
.with_header("mcp-name", "task-extension-71")
.with_body(
serde_json::to_vec(&request).expect("modern extension request must serialize"),
)
}
#[cfg(feature = "tasks")]
#[test]
fn server_extension_registry_freezes_and_advertises_installed_extension() {
let server = extension_registry_test_server(Arc::new(AtomicUsize::new(0)));
assert!(
server
.extension_handler_registry()
.expect("installed registry must be retained")
.is_frozen()
);
let receipt = server
.extension_registry_receipt()
.expect("installed registry must expose its canonical receipt");
assert_eq!(receipt.descriptor_count(), 1);
let discovery = serde_json::to_value(
server
.server_discovery()
.expect("installed extension must be discoverable"),
)
.expect("server discovery must serialize");
assert_eq!(
discovery["capabilities"]["extensions"]["io.modelcontextprotocol/tasks"],
serde_json::json!({})
);
}
#[cfg(feature = "tasks")]
#[test]
fn modern_extension_fallback_rejects_one_variable_incompatible_settings() {
let calls = Arc::new(AtomicUsize::new(0));
let server = extension_registry_test_server(Arc::clone(&calls));
let inbound =
InboundRequestContext::new(Cx::for_testing(), 71, InboundRequestTransport::Memory);
let admitted = block_on(server.dispatch_stateless(
&inbound,
&extension_tasks_get_request(serde_json::json!({})),
))
.expect("modern extension request must have a response");
assert_eq!(admitted.result, Some(serde_json::json!({ "next": 42 })));
assert!(admitted.error.is_none());
let negotiated = server
.negotiate_extensions(
&final_client_extension_discovery(&extension_tasks_get_request(serde_json::json!(
{}
)))
.expect("extension settings must decode"),
)
.expect("extension settings must negotiate");
assert_eq!(
server
.dispatch_negotiated_extension(
&inbound.request_context(),
&negotiated,
&extension_tasks_get_request(serde_json::json!({})),
)
.expect("public negotiated extension dispatch must admit exact modern metadata"),
serde_json::json!({ "next": 42 })
);
let mut rejected = extension_tasks_get_request(serde_json::json!({}));
*rejected
.params
.as_mut()
.and_then(|value| {
value.pointer_mut(
"/_meta/io.modelcontextprotocol~1clientCapabilities/extensions/io.modelcontextprotocol~1tasks",
)
})
.expect("test request must contain its extension settings") =
serde_json::json!({ "unexpected": true });
let rejected = block_on(server.dispatch_stateless(&inbound, &rejected))
.expect("rejected extension request must have an error response");
let error = rejected
.error
.expect("incompatible extension settings must fail closed");
assert_eq!(error.code, i32::from(McpErrorCode::InvalidParams).into());
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[cfg(feature = "tasks")]
#[test]
fn modern_extension_fallback_rejects_wrong_protocol_metadata_before_invocation() {
let calls = Arc::new(AtomicUsize::new(0));
let server = extension_registry_test_server(Arc::clone(&calls));
let inbound =
InboundRequestContext::new(Cx::for_testing(), 71, InboundRequestTransport::Memory);
let admitted = block_on(server.dispatch_stateless(
&inbound,
&extension_tasks_get_request(serde_json::json!({})),
))
.expect("modern extension request must have a response");
assert!(admitted.error.is_none());
let negotiated = server
.negotiate_extensions(
&final_client_extension_discovery(&extension_tasks_get_request(serde_json::json!(
{}
)))
.expect("extension settings must decode"),
)
.expect("extension settings must negotiate");
let mut rejected = extension_tasks_get_request(serde_json::json!({}));
*rejected
.params
.as_mut()
.and_then(|value| value.pointer_mut("/_meta/io.modelcontextprotocol~1protocolVersion"))
.expect("test request must contain its protocol version") =
serde_json::json!(LEGACY_PROTOCOL_VERSION);
let rejected = block_on(server.dispatch_stateless(&inbound, &rejected))
.expect("wrong-era extension request must have an error response");
assert_eq!(
rejected.error.map(|error| error.code),
Some(i32::from(McpErrorCode::InvalidRequest).into())
);
let mut public_rejected = extension_tasks_get_request(serde_json::json!({}));
*public_rejected
.params
.as_mut()
.and_then(|value| value.pointer_mut("/_meta/io.modelcontextprotocol~1protocolVersion"))
.expect("test request must contain its protocol version") =
serde_json::json!(LEGACY_PROTOCOL_VERSION);
assert_eq!(
server
.dispatch_negotiated_extension(
&inbound.request_context(),
&negotiated,
&public_rejected,
)
.expect_err("public extension dispatch must reject the changed era metadata")
.code,
McpErrorCode::InvalidRequest
);
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[cfg(feature = "tasks")]
#[test]
fn final_core_middleware_short_circuit_cannot_bypass_request_admission() {
struct ShortCircuitMiddleware;
impl Middleware for ShortCircuitMiddleware {
fn on_request(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
Ok(MiddlewareDecision::Respond(serde_json::json!({})))
}
}
let server = Server::new("final-core-short-circuit", "1.0.0")
.middleware(ShortCircuitMiddleware)
.build();
let inbound =
InboundRequestContext::new(Cx::for_testing(), 72, InboundRequestTransport::Memory);
let request = JsonRpcRequest::new("tools/list", Some(serde_json::json!({})), 72_i64);
let response = block_on(server.dispatch_stateless(&inbound, &request))
.expect("a malformed final request with an id receives an error response");
assert_eq!(
response.error.map(|error| error.code),
Some(i32::from(McpErrorCode::InvalidParams).into())
);
assert!(response.result.is_none());
}
#[cfg(feature = "tasks")]
#[test]
fn final_core_middleware_short_circuit_cannot_author_metadata() {
struct ShortCircuitMiddleware;
impl Middleware for ShortCircuitMiddleware {
fn on_request(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
Ok(MiddlewareDecision::Respond(serde_json::json!({
"resultType": "complete",
"tools": [],
"ttlMs": 0,
"cacheScope": "private",
"_meta": {
"io.modelcontextprotocol/serverInfo": {
"name": "middleware",
"version": "1.0.0",
},
},
})))
}
}
let server = Server::new("final-core-short-circuit", "1.0.0")
.middleware(ShortCircuitMiddleware)
.build();
let inbound =
InboundRequestContext::new(Cx::for_testing(), 721, InboundRequestTransport::Memory);
let request = JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({
"_meta": {
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {},
},
})),
721_i64,
);
let response = block_on(server.dispatch_stateless(&inbound, &request))
.expect("short-circuited final request receives an error response");
assert_eq!(
response.error.map(|error| error.code),
Some(i32::from(McpErrorCode::InternalError).into())
);
assert!(response.result.is_none());
}
#[cfg(feature = "tasks")]
#[test]
fn final_core_middleware_rejects_one_field_result_type_mutation() {
struct ResultTypeMutationMiddleware;
impl Middleware for ResultTypeMutationMiddleware {
fn on_response(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
mut response: serde_json::Value,
) -> McpResult<serde_json::Value> {
assert_eq!(response["resultType"], "complete");
response["resultType"] = serde_json::json!("input_required");
Ok(response)
}
}
let server = Server::new("final-core-result-type", "1.0.0")
.middleware(ResultTypeMutationMiddleware)
.build();
let inbound =
InboundRequestContext::new(Cx::for_testing(), 73, InboundRequestTransport::Memory);
let request = JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({
"_meta": {
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {},
},
})),
73_i64,
);
let response = block_on(server.dispatch_stateless(&inbound, &request))
.expect("a malformed middleware result with an id receives an error response");
assert_eq!(
response.error.map(|error| error.code),
Some(i32::from(McpErrorCode::InternalError).into())
);
assert!(response.result.is_none());
}
#[cfg(feature = "tasks")]
#[test]
fn final_core_middleware_preserves_open_response_metadata() {
struct OpenMetadataMiddleware;
impl Middleware for OpenMetadataMiddleware {
fn on_response(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
mut response: serde_json::Value,
) -> McpResult<serde_json::Value> {
response["_meta"]["com.example/trace"] = serde_json::json!("retained");
Ok(response)
}
}
let server = Server::new("final-core-open-metadata", "1.0.0")
.middleware(OpenMetadataMiddleware)
.build();
let inbound =
InboundRequestContext::new(Cx::for_testing(), 74, InboundRequestTransport::Memory);
let request = JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({
"_meta": {
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {},
},
})),
74_i64,
);
let response = block_on(server.dispatch_stateless(&inbound, &request))
.expect("final request has a response");
assert!(response.error.is_none());
assert_eq!(
response
.result
.as_ref()
.and_then(|result| result.pointer("/_meta/com.example~1trace")),
Some(&serde_json::json!("retained")),
"middleware may still add open metadata"
);
}
#[cfg(feature = "tasks")]
#[test]
fn final_core_middleware_rejects_one_field_server_info_injection() {
struct ServerInfoInjectionMiddleware;
impl Middleware for ServerInfoInjectionMiddleware {
fn on_response(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
mut response: serde_json::Value,
) -> McpResult<serde_json::Value> {
response["_meta"][FINAL_SERVER_INFO_META_KEY] = serde_json::json!({
"name": "middleware",
"version": "1.0.0",
});
Ok(response)
}
}
let server = Server::new("final-core-server-info", "1.0.0")
.middleware(ServerInfoInjectionMiddleware)
.build();
let inbound =
InboundRequestContext::new(Cx::for_testing(), 75, InboundRequestTransport::Memory);
let request = JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({
"_meta": {
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {},
},
})),
75_i64,
);
let response = block_on(server.dispatch_stateless(&inbound, &request))
.expect("sealed metadata mutation receives an error response");
assert_eq!(
response.error.map(|error| error.code),
Some(i32::from(McpErrorCode::InternalError).into())
);
assert!(response.result.is_none());
}
#[cfg(feature = "tasks")]
#[test]
fn final_discovery_middleware_rejects_one_field_server_info_mutation() {
struct DiscoveryServerInfoMutationMiddleware;
impl Middleware for DiscoveryServerInfoMutationMiddleware {
fn on_response(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
mut response: serde_json::Value,
) -> McpResult<serde_json::Value> {
response["_meta"][FINAL_SERVER_INFO_META_KEY]["name"] =
serde_json::json!("middleware");
Ok(response)
}
}
let server = Server::new("final-discovery-server-info", "1.0.0")
.middleware(DiscoveryServerInfoMutationMiddleware)
.build();
let inbound =
InboundRequestContext::new(Cx::for_testing(), 76, InboundRequestTransport::Memory);
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {},
},
})),
76_i64,
);
let response = block_on(server.dispatch_stateless(&inbound, &request))
.expect("sealed discovery mutation receives an error response");
assert_eq!(
response.error.map(|error| error.code),
Some(i32::from(McpErrorCode::InternalError).into())
);
assert!(response.result.is_none());
}
#[cfg(feature = "tasks")]
#[test]
fn legacy_middleware_retains_open_result_metadata() {
struct LegacyMetadataMiddleware;
impl Middleware for LegacyMetadataMiddleware {
fn on_response(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
mut response: serde_json::Value,
) -> McpResult<serde_json::Value> {
response["_meta"][FINAL_SERVER_INFO_META_KEY] = serde_json::json!({
"name": "middleware",
"version": "1.0.0",
});
Ok(response)
}
}
let server = Server::new("legacy-open-result-metadata", "1.0.0")
.middleware(LegacyMetadataMiddleware)
.build();
let mut session = initialized_test_session(&server);
let response = dispatch_test_request(&server, &mut session, "tools/list");
assert!(response.error.is_none());
assert_eq!(
response
.result
.as_ref()
.and_then(|result| result.pointer("/_meta/io.modelcontextprotocol~1serverInfo")),
Some(&serde_json::json!({
"name": "middleware",
"version": "1.0.0",
})),
"the exact legacy result metadata surface remains open to middleware"
);
}
#[cfg(feature = "tasks")]
#[test]
fn legacy_dispatch_does_not_fall_through_to_extension_registry() {
let calls = Arc::new(AtomicUsize::new(0));
let server = extension_registry_test_server(Arc::clone(&calls));
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tasks/get",
Some(serde_json::json!({ "value": 41 })),
71_i64,
),
¬ification_sender,
&request_sender,
))
.expect("legacy request must have a response");
let error = response
.error
.expect("legacy request must not reach the extension handler");
assert_eq!(error.code, i32::from(McpErrorCode::MethodNotFound).into());
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
#[cfg(feature = "tasks")]
#[derive(Default)]
struct ServerFinalTaskState {
tasks: std::collections::BTreeMap<fastmcp_protocol::FinalTaskId, fastmcp_protocol::Task>,
generations: std::collections::BTreeMap<fastmcp_protocol::FinalTaskId, u64>,
next_generation: u64,
next_dispatch_fence: u64,
work_descriptors:
std::collections::BTreeMap<fastmcp_protocol::FinalTaskId, FinalTaskWorkDescriptor>,
initial_work:
std::collections::BTreeMap<fastmcp_protocol::FinalTaskId, FinalTaskWorkDescriptor>,
accepted_inputs: std::collections::BTreeMap<
fastmcp_protocol::FinalTaskId,
fastmcp_protocol::TaskInputResponses,
>,
handoff_leases:
std::collections::BTreeMap<fastmcp_protocol::FinalTaskId, ServerFinalTaskHandoffLease>,
cancellation_requests: std::collections::BTreeSet<fastmcp_protocol::FinalTaskId>,
notifications: Vec<fastmcp_protocol::TaskStatusNotification>,
expires_at: std::collections::BTreeMap<fastmcp_protocol::FinalTaskId, Instant>,
execution_budgets: std::collections::BTreeMap<
fastmcp_protocol::FinalTaskId,
crate::tasks::FinalTaskExecutionBudget,
>,
}
#[cfg(feature = "tasks")]
#[derive(Clone, Copy, PartialEq, Eq)]
enum ServerFinalTaskHandoffKind {
Initial,
Resumed,
}
#[cfg(feature = "tasks")]
struct ServerFinalTaskHandoffLease {
generation: u64,
kind: ServerFinalTaskHandoffKind,
owner_id: String,
dispatch_fence: Option<u64>,
expires_at: Instant,
}
#[cfg(feature = "tasks")]
const SERVER_FINAL_TASK_HANDOFF_LEASE: Duration = Duration::from_secs(30);
#[cfg(feature = "tasks")]
const SERVER_FINAL_TASK_HANDOFF_HEARTBEAT: Duration = Duration::from_secs(10);
#[cfg(feature = "tasks")]
struct ServerFinalTaskStore {
clock: Arc<dyn Fn() -> Instant + Send + Sync>,
execution_limits: crate::tasks::FinalTaskExecutionLimits,
state: Mutex<ServerFinalTaskState>,
}
#[cfg(feature = "tasks")]
impl Default for ServerFinalTaskStore {
fn default() -> Self {
Self::with_clock(Arc::new(Instant::now))
}
}
#[cfg(feature = "tasks")]
impl ServerFinalTaskStore {
fn with_clock(clock: Arc<dyn Fn() -> Instant + Send + Sync>) -> Self {
Self {
clock,
execution_limits: crate::tasks::FinalTaskExecutionLimits::default(),
state: Mutex::new(ServerFinalTaskState::default()),
}
}
fn now(&self) -> Instant {
(self.clock)()
}
}
#[cfg(feature = "tasks")]
fn next_server_final_task_generation(state: &mut ServerFinalTaskState) -> McpResult<u64> {
let generation = state.next_generation.checked_add(1).ok_or_else(|| {
McpError::internal_error("Server final task test-store generation space is exhausted")
})?;
state.next_generation = generation;
Ok(generation)
}
#[cfg(feature = "tasks")]
fn next_server_final_task_dispatch_fence(state: &mut ServerFinalTaskState) -> McpResult<u64> {
let fence = state.next_dispatch_fence.checked_add(1).ok_or_else(|| {
McpError::internal_error(
"Server final task test-store dispatch fence space is exhausted",
)
})?;
state.next_dispatch_fence = fence;
Ok(fence)
}
#[cfg(feature = "tasks")]
fn server_final_task_handoff_expiry(now: Instant) -> McpResult<Instant> {
now.checked_add(SERVER_FINAL_TASK_HANDOFF_LEASE)
.ok_or_else(|| {
McpError::internal_error(
"Server final task test-store handoff lease exceeds clock range",
)
})
}
/// Validates every unbounded wire duration before a store operation may
/// reclaim, replace, or otherwise mutate retained state. The returned TTL
/// is the one bounded duration that the store needs for its expiry index.
#[cfg(feature = "tasks")]
fn validate_server_final_task_durations(
task: &fastmcp_protocol::Task,
) -> McpResult<Option<u64>> {
let ttl_ms = task
.base()
.ttl_ms
.as_ref()
.map(fastmcp_protocol::TaskDuration::try_as_millis)
.transpose()
.map_err(|_| {
McpError::invalid_params(
"Server final task test-store ttlMs is outside milliseconds",
)
})?;
task.base()
.poll_interval_ms
.as_ref()
.map(fastmcp_protocol::TaskDuration::try_as_millis)
.transpose()
.map_err(|_| {
McpError::invalid_params(
"Server final task test-store pollIntervalMs is outside milliseconds",
)
})?;
Ok(ttl_ms)
}
#[cfg(feature = "tasks")]
fn server_final_task_expiry(ttl_ms: Option<u64>, now: Instant) -> McpResult<Option<Instant>> {
let Some(ttl_ms) = ttl_ms else {
return Ok(None);
};
now.checked_add(Duration::from_millis(ttl_ms))
.map(Some)
.ok_or_else(|| {
McpError::internal_error("Server final task test-store TTL exceeds clock range")
})
}
#[cfg(feature = "tasks")]
fn new_server_final_task_execution_budget(
limits: crate::tasks::FinalTaskExecutionLimits,
now: Instant,
) -> McpResult<crate::tasks::FinalTaskExecutionBudget> {
let total_deadline = now.checked_add(limits.total_runtime()).ok_or_else(|| {
McpError::internal_error("Server final task execution exceeds clock range")
})?;
crate::tasks::FinalTaskExecutionBudget::new(total_deadline, None, 0, limits)
}
#[cfg(feature = "tasks")]
fn server_final_task_execution_exhaustion(
state: &ServerFinalTaskState,
task_id: &fastmcp_protocol::FinalTaskId,
now: Instant,
) -> McpResult<Option<&'static str>> {
let Some(task) = state.tasks.get(task_id) else {
return Ok(None);
};
if !matches!(
task,
fastmcp_protocol::Task::Working(_) | fastmcp_protocol::Task::InputRequired { .. }
) {
return Ok(None);
}
let budget = state.execution_budgets.get(task_id).ok_or_else(|| {
McpError::internal_error("Server final task is missing execution accounting")
})?;
if now >= budget.total_deadline() {
return Ok(Some("Task total execution deadline exceeded"));
}
if budget
.attempt_deadline()
.is_some_and(|deadline| now >= deadline)
{
return Ok(Some("Task attempt execution deadline exceeded"));
}
if budget.attempts() == budget.limits().max_attempts()
&& !state.handoff_leases.contains_key(task_id)
&& matches!(task, fastmcp_protocol::Task::Working(_))
&& (state.initial_work.contains_key(task_id)
|| state.accepted_inputs.contains_key(task_id))
{
return Ok(Some("Task execution attempt limit exceeded"));
}
Ok(None)
}
#[cfg(feature = "tasks")]
fn released_server_final_task_execution_budget(
state: &ServerFinalTaskState,
task_id: &fastmcp_protocol::FinalTaskId,
) -> McpResult<crate::tasks::FinalTaskExecutionBudget> {
let budget = state.execution_budgets.get(task_id).ok_or_else(|| {
McpError::internal_error("Server final task is missing execution accounting")
})?;
crate::tasks::FinalTaskExecutionBudget::new(
budget.total_deadline(),
None,
budget.attempts(),
budget.limits(),
)
}
#[cfg(feature = "tasks")]
fn prepare_server_final_task_execution_release(
state: &ServerFinalTaskState,
task_id: &fastmcp_protocol::FinalTaskId,
now: Instant,
) -> McpResult<crate::tasks::FinalTaskExecutionBudget> {
if let Some(reason) = server_final_task_execution_exhaustion(state, task_id, now)? {
return Err(McpError::invalid_params(reason));
}
released_server_final_task_execution_budget(state, task_id)
}
#[cfg(feature = "tasks")]
fn server_final_task_is_working(
state: &ServerFinalTaskState,
task_id: &fastmcp_protocol::FinalTaskId,
generation: u64,
) -> bool {
state.generations.get(task_id) == Some(&generation)
&& state
.tasks
.get(task_id)
.is_some_and(|task| matches!(task, fastmcp_protocol::Task::Working(_)))
&& !state.cancellation_requests.contains(task_id)
}
/// Retires durable cancellation intent after its elected service lease
/// expires. This test-store transition mirrors the production in-memory
/// store: it must not drop the only retirement fence and leave a working
/// task with cancellation intent.
#[cfg(feature = "tasks")]
fn terminalize_expired_server_final_task_cancellation(
state: &mut ServerFinalTaskState,
task_id: &fastmcp_protocol::FinalTaskId,
) -> McpResult<()> {
let Some(fastmcp_protocol::Task::Working(base)) = state.tasks.get(task_id).cloned() else {
return Err(McpError::internal_error(
"Expired cancellation lease no longer owns a working server final task",
));
};
let task =
fastmcp_protocol::Task::Cancelled(crate::tasks::transition_terminal_final_task_base(
base,
fastmcp_protocol::FinalTaskStatus::Cancelled,
None,
)?);
let notification = crate::tasks::final_task_notification(&task);
let execution_budget = released_server_final_task_execution_budget(state, task_id)?;
let generation = next_server_final_task_generation(state)?;
state.tasks.insert(task_id.clone(), task);
state.generations.insert(task_id.clone(), generation);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.accepted_inputs.remove(task_id);
state.initial_work.remove(task_id);
state.handoff_leases.remove(task_id);
state.cancellation_requests.remove(task_id);
state.notifications.push(notification);
Ok(())
}
#[cfg(feature = "tasks")]
fn reclaim_expired_server_final_task_handoffs(state: &mut ServerFinalTaskState, now: Instant) {
let expired = state
.handoff_leases
.iter()
.filter(|(_, lease)| lease.expires_at <= now)
.map(|(task_id, _)| task_id.clone())
.collect::<Vec<_>>();
for task_id in expired {
// Expired execution retains its exact fence until the explicit
// expiry operation commits a failure and matching notification.
if !matches!(
server_final_task_execution_exhaustion(state, &task_id, now),
Ok(None)
) {
continue;
}
let Some(lease_generation) = state
.handoff_leases
.get(&task_id)
.map(|lease| lease.generation)
else {
continue;
};
let cancellation_requires_retirement = state.cancellation_requests.contains(&task_id)
&& state
.tasks
.get(&task_id)
.is_some_and(|task| matches!(task, fastmcp_protocol::Task::Working(_)));
if cancellation_requires_retirement {
// If checked terminalization cannot allocate a generation,
// retain this exact lease as the cancellation-retirement
// fence. Releasing it would strand working+cancellation.
let _ = terminalize_expired_server_final_task_cancellation(state, &task_id);
continue;
}
let recoverable = server_final_task_is_working(state, &task_id, lease_generation);
if recoverable {
let Ok(execution_budget) =
released_server_final_task_execution_budget(state, &task_id)
else {
continue;
};
if let Ok(generation) = next_server_final_task_generation(state) {
state.handoff_leases.remove(&task_id);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.generations.insert(task_id, generation);
}
} else {
state.handoff_leases.remove(&task_id);
}
}
let expired_task_ids = state
.expires_at
.iter()
.filter(|(_, expires_at)| **expires_at <= now)
.map(|(task_id, _)| task_id.clone())
.collect::<Vec<_>>();
for task_id in expired_task_ids {
state.expires_at.remove(&task_id);
state.execution_budgets.remove(&task_id);
state.tasks.remove(&task_id);
state.generations.remove(&task_id);
state.work_descriptors.remove(&task_id);
state.initial_work.remove(&task_id);
state.accepted_inputs.remove(&task_id);
state.handoff_leases.remove(&task_id);
state.cancellation_requests.remove(&task_id);
state
.notifications
.retain(|notification| notification.params.task.base().task_id != task_id);
}
}
#[cfg(feature = "tasks")]
fn insert_server_final_task_handoff_lease(
state: &mut ServerFinalTaskState,
task_id: fastmcp_protocol::FinalTaskId,
generation: u64,
kind: ServerFinalTaskHandoffKind,
owner_id: &str,
now: Instant,
) -> McpResult<()> {
if let Some(reason) = server_final_task_execution_exhaustion(state, &task_id, now)? {
return Err(McpError::invalid_params(reason));
}
let expires_at = server_final_task_handoff_expiry(now)?;
if state.handoff_leases.contains_key(&task_id) {
return Err(McpError::internal_error(
"Server final task test-store overwrote a live handoff lease",
));
}
let budget = state.execution_budgets.get(&task_id).ok_or_else(|| {
McpError::internal_error("Server final task is missing execution accounting")
})?;
let attempts = budget
.attempts()
.checked_add(1)
.filter(|attempts| *attempts <= budget.limits().max_attempts())
.ok_or_else(|| McpError::invalid_params("Task execution attempt limit exceeded"))?;
let attempt_deadline = now
.checked_add(budget.limits().attempt_runtime())
.ok_or_else(|| {
McpError::internal_error("Server final task attempt exceeds clock range")
})?
.min(budget.total_deadline());
let execution_budget = crate::tasks::FinalTaskExecutionBudget::new(
budget.total_deadline(),
Some(attempt_deadline),
attempts,
budget.limits(),
)?;
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.handoff_leases.insert(
task_id,
ServerFinalTaskHandoffLease {
generation,
kind,
owner_id: owner_id.to_owned(),
dispatch_fence: None,
expires_at,
},
);
Ok(())
}
#[cfg(feature = "tasks")]
fn next_server_final_task_recovery_id<'a>(
task_ids: impl Iterator<Item = &'a fastmcp_protocol::FinalTaskId>,
after_task_id: Option<&fastmcp_protocol::FinalTaskId>,
mut eligible: impl FnMut(&fastmcp_protocol::FinalTaskId) -> McpResult<bool>,
) -> McpResult<Option<fastmcp_protocol::FinalTaskId>> {
let task_ids = task_ids.collect::<Vec<_>>();
for task_id in task_ids.iter().copied() {
if after_task_id.is_none_or(|after| task_id > after) && eligible(task_id)? {
return Ok(Some(task_id.clone()));
}
}
for task_id in task_ids {
if after_task_id.is_some_and(|after| task_id <= after) && eligible(task_id)? {
return Ok(Some(task_id.clone()));
}
}
Ok(None)
}
#[cfg(feature = "tasks")]
fn record_server_final_task_cancellation(
state: &mut ServerFinalTaskState,
task_id: &fastmcp_protocol::FinalTaskId,
now: Instant,
) -> McpResult<()> {
let execution_budget = prepare_server_final_task_execution_release(state, task_id, now)?;
let generation = state.generations.get(task_id).copied().ok_or_else(|| {
McpError::internal_error("Server final task test-store is missing a task generation")
})?;
let dispatch_elected = state
.handoff_leases
.get(task_id)
.is_some_and(|lease| lease.generation == generation && lease.dispatch_fence.is_some());
let next_generation = (!dispatch_elected && !state.cancellation_requests.contains(task_id))
.then(|| next_server_final_task_generation(state))
.transpose()?;
state.accepted_inputs.remove(task_id);
state.initial_work.remove(task_id);
if !dispatch_elected {
state.handoff_leases.remove(task_id);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
}
state.cancellation_requests.insert(task_id.clone());
if let Some(next_generation) = next_generation {
state.generations.insert(task_id.clone(), next_generation);
}
Ok(())
}
#[cfg(feature = "tasks")]
fn ensure_server_final_task_notification_matches_task(
task: &fastmcp_protocol::Task,
notification: &fastmcp_protocol::TaskStatusNotification,
) -> McpResult<()> {
let retained_task = serde_json::to_value(task).map_err(|error| {
McpError::internal_error(format!(
"Could not encode retained final task for validation: {error}"
))
})?;
let notified_task = serde_json::to_value(¬ification.params.task).map_err(|error| {
McpError::internal_error(format!(
"Could not encode final task notification for validation: {error}"
))
})?;
if notified_task != retained_task {
return Err(McpError::invalid_params(
"Final task notification must contain exactly the retained task",
));
}
Ok(())
}
#[cfg(feature = "tasks")]
impl FinalTaskStore for ServerFinalTaskStore {
fn execution_limits(&self) -> McpResult<crate::tasks::FinalTaskExecutionLimits> {
Ok(self.execution_limits)
}
fn task_execution_budget_if_current(
&self,
expected: &FinalTaskSnapshot,
) -> McpResult<Option<crate::tasks::FinalTaskExecutionBudget>> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
reclaim_expired_server_final_task_handoffs(&mut state, self.now());
let task_id = &expected.task().base().task_id;
if state.generations.get(task_id) != Some(&expected.generation()) {
return Ok(None);
}
state
.execution_budgets
.get(task_id)
.copied()
.map(Some)
.ok_or_else(|| {
McpError::internal_error("Server final task is missing execution accounting")
})
}
fn expire_task_execution_if_current(
&self,
expected: &FinalTaskSnapshot,
) -> McpResult<Option<FinalTaskSnapshot>> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
let task_id = &expected.task().base().task_id;
if state.generations.get(task_id) != Some(&expected.generation()) {
return Ok(None);
}
let Some(reason) = server_final_task_execution_exhaustion(&state, task_id, now)? else {
return Ok(None);
};
let retained = state.tasks.get(task_id).ok_or_else(|| {
McpError::internal_error("Server final task expiry lost its retained task")
})?;
let task = fastmcp_protocol::Task::Failed {
base: crate::tasks::transition_terminal_final_task_base(
retained.base().clone(),
fastmcp_protocol::FinalTaskStatus::Failed,
None,
)?,
error: fastmcp_protocol::FinalTaskError {
code: (-32000).into(),
message: reason.to_owned(),
data: None,
additional: BTreeMap::new(),
},
};
let notification = crate::tasks::final_task_notification(&task);
let execution_budget = released_server_final_task_execution_budget(&state, task_id)?;
let generation = next_server_final_task_generation(&mut state)?;
state.tasks.insert(task_id.clone(), task.clone());
state.generations.insert(task_id.clone(), generation);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.initial_work.remove(task_id);
state.accepted_inputs.remove(task_id);
state.handoff_leases.remove(task_id);
state.cancellation_requests.remove(task_id);
state.notifications.push(notification);
Ok(Some(FinalTaskSnapshot::new(task, generation)))
}
fn next_expired_execution_snapshot(&self) -> McpResult<Option<FinalTaskSnapshot>> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
for (task_id, task) in &state.tasks {
if server_final_task_execution_exhaustion(&state, task_id, now)?.is_some() {
let generation = state.generations.get(task_id).copied().ok_or_else(|| {
McpError::internal_error("Server final task expiry lost its generation")
})?;
return Ok(Some(FinalTaskSnapshot::new(task.clone(), generation)));
}
}
Ok(None)
}
fn create_task(
&self,
task: fastmcp_protocol::Task,
notification: fastmcp_protocol::TaskStatusNotification,
) -> McpResult<()> {
let task_id = task.base().task_id.clone();
let ttl_ms = validate_server_final_task_durations(&task)?;
ensure_server_final_task_notification_matches_task(&task, ¬ification)?;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
let expires_at = server_final_task_expiry(ttl_ms, now)?;
let execution_budget =
new_server_final_task_execution_budget(self.execution_limits, now)?;
reclaim_expired_server_final_task_handoffs(&mut state, now);
if state.tasks.contains_key(&task_id) {
return Err(McpError::invalid_params("Task already exists"));
}
let generation = next_server_final_task_generation(&mut state)?;
state.tasks.insert(task_id.clone(), task);
state.generations.insert(task_id.clone(), generation);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.notifications.push(notification);
if let Some(expires_at) = expires_at {
state.expires_at.insert(task_id, expires_at);
}
Ok(())
}
fn create_task_with_work(
&self,
task: fastmcp_protocol::Task,
notification: fastmcp_protocol::TaskStatusNotification,
work_descriptor: FinalTaskWorkDescriptor,
) -> McpResult<FinalTaskSnapshot> {
let task_id = task.base().task_id.clone();
let ttl_ms = validate_server_final_task_durations(&task)?;
ensure_server_final_task_notification_matches_task(&task, ¬ification)?;
if !matches!(task, fastmcp_protocol::Task::Working(_)) {
return Err(McpError::invalid_params(
"Initial application work requires a working final task",
));
}
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
let expires_at = server_final_task_expiry(ttl_ms, now)?;
let execution_budget =
new_server_final_task_execution_budget(self.execution_limits, now)?;
reclaim_expired_server_final_task_handoffs(&mut state, now);
if state.tasks.contains_key(&task_id) {
return Err(McpError::invalid_params("Task already exists"));
}
let generation = next_server_final_task_generation(&mut state)?;
state.tasks.insert(task_id.clone(), task);
state.generations.insert(task_id.clone(), generation);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state
.work_descriptors
.insert(task_id.clone(), work_descriptor.clone());
state.initial_work.insert(task_id.clone(), work_descriptor);
state.notifications.push(notification);
let committed = FinalTaskSnapshot::new(state.tasks[&task_id].clone(), generation);
if let Some(expires_at) = expires_at {
state.expires_at.insert(task_id, expires_at);
}
Ok(committed)
}
fn get_task(
&self,
task_id: &fastmcp_protocol::FinalTaskId,
) -> McpResult<Option<fastmcp_protocol::Task>> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
reclaim_expired_server_final_task_handoffs(&mut state, self.now());
Ok(state.tasks.get(task_id).cloned())
}
fn get_task_snapshot(
&self,
task_id: &fastmcp_protocol::FinalTaskId,
) -> McpResult<Option<FinalTaskSnapshot>> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
reclaim_expired_server_final_task_handoffs(&mut state, self.now());
let Some(task) = state.tasks.get(task_id).cloned() else {
return Ok(None);
};
let generation = state.generations.get(task_id).copied().ok_or_else(|| {
McpError::internal_error("Server final task test store is missing a generation")
})?;
Ok(Some(FinalTaskSnapshot::new(task, generation)))
}
fn replace_task(
&self,
task: fastmcp_protocol::Task,
notification: fastmcp_protocol::TaskStatusNotification,
) -> McpResult<()> {
let task_id = task.base().task_id.clone();
validate_server_final_task_durations(&task)?;
ensure_server_final_task_notification_matches_task(&task, ¬ification)?;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if !state.tasks.contains_key(&task_id) {
return Err(McpError::invalid_params("Task not found"));
}
let execution_budget =
prepare_server_final_task_execution_release(&state, &task_id, now)?;
let generation = next_server_final_task_generation(&mut state)?;
let terminal = matches!(
&task,
fastmcp_protocol::Task::Completed { .. }
| fastmcp_protocol::Task::Failed { .. }
| fastmcp_protocol::Task::Cancelled(_)
);
state.tasks.insert(task_id.clone(), task);
state.generations.insert(task_id.clone(), generation);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.accepted_inputs.remove(&task_id);
state.handoff_leases.remove(&task_id);
state.initial_work.remove(&task_id);
state.notifications.push(notification);
if terminal {
state.cancellation_requests.remove(&task_id);
}
Ok(())
}
fn replace_task_if_current(
&self,
expected: &FinalTaskSnapshot,
task: fastmcp_protocol::Task,
notification: fastmcp_protocol::TaskStatusNotification,
) -> McpResult<bool> {
let task_id = task.base().task_id.clone();
if expected.task().base().task_id != task_id {
return Err(McpError::invalid_params(
"Expected and replacement final task IDs must match",
));
}
validate_server_final_task_durations(&task)?;
ensure_server_final_task_notification_matches_task(&task, ¬ification)?;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if state.generations.get(&task_id) != Some(&expected.generation()) {
return Ok(false);
}
let execution_budget =
prepare_server_final_task_execution_release(&state, &task_id, now)?;
let generation = next_server_final_task_generation(&mut state)?;
let terminal = matches!(
&task,
fastmcp_protocol::Task::Completed { .. }
| fastmcp_protocol::Task::Failed { .. }
| fastmcp_protocol::Task::Cancelled(_)
);
state.tasks.insert(task_id.clone(), task);
state.generations.insert(task_id.clone(), generation);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.accepted_inputs.remove(&task_id);
state.handoff_leases.remove(&task_id);
state.initial_work.remove(&task_id);
state.notifications.push(notification);
if terminal {
state.cancellation_requests.remove(&task_id);
}
Ok(true)
}
fn replace_task_and_append_input_if_current(
&self,
expected: &FinalTaskSnapshot,
task: fastmcp_protocol::Task,
notification: fastmcp_protocol::TaskStatusNotification,
input_responses: fastmcp_protocol::TaskInputResponses,
) -> McpResult<Option<FinalTaskSnapshot>> {
let task_id = task.base().task_id.clone();
if expected.task().base().task_id != task_id {
return Err(McpError::invalid_params(
"Expected and replacement final task IDs must match",
));
}
validate_server_final_task_durations(&task)?;
ensure_server_final_task_notification_matches_task(&task, ¬ification)?;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if state.generations.get(&task_id) != Some(&expected.generation()) {
return Ok(None);
}
let execution_budget =
prepare_server_final_task_execution_release(&state, &task_id, now)?;
let generation = next_server_final_task_generation(&mut state)?;
state.tasks.insert(task_id.clone(), task);
state.generations.insert(task_id.clone(), generation);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.handoff_leases.remove(&task_id);
state.initial_work.remove(&task_id);
let committed = FinalTaskSnapshot::new(state.tasks[&task_id].clone(), generation);
if !input_responses.is_empty() {
state
.accepted_inputs
.entry(task_id)
.or_default()
.extend(input_responses);
}
state.notifications.push(notification);
Ok(Some(committed))
}
fn replace_task_and_clear_input_if_current(
&self,
expected: &FinalTaskSnapshot,
task: fastmcp_protocol::Task,
notification: fastmcp_protocol::TaskStatusNotification,
) -> McpResult<Option<FinalTaskSnapshot>> {
let task_id = task.base().task_id.clone();
if expected.task().base().task_id != task_id {
return Err(McpError::invalid_params(
"Expected and replacement final task IDs must match",
));
}
validate_server_final_task_durations(&task)?;
ensure_server_final_task_notification_matches_task(&task, ¬ification)?;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if state.generations.get(&task_id) != Some(&expected.generation()) {
return Ok(None);
}
let execution_budget =
prepare_server_final_task_execution_release(&state, &task_id, now)?;
let generation = next_server_final_task_generation(&mut state)?;
let terminal = matches!(
&task,
fastmcp_protocol::Task::Completed { .. }
| fastmcp_protocol::Task::Failed { .. }
| fastmcp_protocol::Task::Cancelled(_)
);
state.tasks.insert(task_id.clone(), task);
state.generations.insert(task_id.clone(), generation);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.accepted_inputs.remove(&task_id);
state.handoff_leases.remove(&task_id);
state.initial_work.remove(&task_id);
state.notifications.push(notification);
if terminal {
state.cancellation_requests.remove(&task_id);
}
Ok(Some(FinalTaskSnapshot::new(
state.tasks[&task_id].clone(),
generation,
)))
}
fn replace_task_and_clear_input_for_handoff_if_current(
&self,
expected: &FinalTaskSnapshot,
owner_id: &str,
dispatch_fence: u64,
cancellation_required: bool,
task: fastmcp_protocol::Task,
notification: fastmcp_protocol::TaskStatusNotification,
) -> McpResult<Option<FinalTaskSnapshot>> {
if owner_id.is_empty() {
return Err(McpError::invalid_params(
"Final task handoff owner must be non-empty",
));
}
let task_id = task.base().task_id.clone();
if expected.task().base().task_id != task_id {
return Err(McpError::invalid_params(
"Expected and replacement final task IDs must match",
));
}
if matches!(&task, fastmcp_protocol::Task::Cancelled(_)) != cancellation_required {
return Err(McpError::invalid_params(
"Fenced final task cancellation disposition does not match the replacement task",
));
}
validate_server_final_task_durations(&task)?;
ensure_server_final_task_notification_matches_task(&task, ¬ification)?;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
let owns_exact_dispatch = state.handoff_leases.get(&task_id).is_some_and(|lease| {
lease.generation == expected.generation()
&& lease.owner_id == owner_id
&& lease.dispatch_fence == Some(dispatch_fence)
});
if state.generations.get(&task_id) != Some(&expected.generation())
|| !owns_exact_dispatch
|| state.cancellation_requests.contains(&task_id) != cancellation_required
{
return Ok(None);
}
let execution_budget =
prepare_server_final_task_execution_release(&state, &task_id, now)?;
let generation = next_server_final_task_generation(&mut state)?;
let terminal = matches!(
&task,
fastmcp_protocol::Task::Completed { .. }
| fastmcp_protocol::Task::Failed { .. }
| fastmcp_protocol::Task::Cancelled(_)
);
state.tasks.insert(task_id.clone(), task);
state.generations.insert(task_id.clone(), generation);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.accepted_inputs.remove(&task_id);
state.handoff_leases.remove(&task_id);
state.initial_work.remove(&task_id);
state.notifications.push(notification);
if terminal {
state.cancellation_requests.remove(&task_id);
}
Ok(Some(FinalTaskSnapshot::new(
state.tasks[&task_id].clone(),
generation,
)))
}
fn take_input_if_current(
&self,
_expected: &FinalTaskSnapshot,
) -> McpResult<Option<fastmcp_protocol::TaskInputResponses>> {
Err(McpError::internal_error(
"Raw final task input claims require an authorized service owner",
))
}
fn take_input_for_owner_if_current(
&self,
expected: &FinalTaskSnapshot,
owner_id: &str,
) -> McpResult<Option<fastmcp_protocol::TaskInputResponses>> {
if owner_id.is_empty() {
return Err(McpError::invalid_params(
"Final task handoff owner must be non-empty",
));
}
let task_id = &expected.task().base().task_id;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if !server_final_task_is_working(&state, task_id, expected.generation())
|| state.handoff_leases.contains_key(task_id)
|| server_final_task_execution_exhaustion(&state, task_id, now)?.is_some()
{
return Ok(None);
}
let Some(input_responses) = state.accepted_inputs.get(task_id).cloned() else {
return Ok(None);
};
insert_server_final_task_handoff_lease(
&mut state,
task_id.clone(),
expected.generation(),
ServerFinalTaskHandoffKind::Resumed,
owner_id,
now,
)?;
Ok(Some(input_responses))
}
fn work_descriptor_if_current(
&self,
expected: &FinalTaskSnapshot,
) -> McpResult<Option<FinalTaskWorkDescriptor>> {
let task_id = &expected.task().base().task_id;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
reclaim_expired_server_final_task_handoffs(&mut state, self.now());
if !server_final_task_is_working(&state, task_id, expected.generation()) {
return Ok(None);
}
state
.work_descriptors
.get(task_id)
.cloned()
.map(Some)
.ok_or_else(|| {
McpError::internal_error(
"Server final task test-store is missing a working task descriptor",
)
})
}
fn next_initial_work_snapshot_after(
&self,
after_task_id: Option<&fastmcp_protocol::FinalTaskId>,
) -> McpResult<Option<FinalTaskSnapshot>> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
let Some(task_id) = next_server_final_task_recovery_id(
state.initial_work.keys(),
after_task_id,
|task_id| {
Ok(state.generations.get(task_id).is_some_and(|generation| {
server_final_task_is_working(&state, task_id, *generation)
}) && !state.handoff_leases.contains_key(task_id)
&& server_final_task_execution_exhaustion(&state, task_id, now)?.is_none())
},
)?
else {
return Ok(None);
};
let task = state.tasks.get(&task_id).cloned().ok_or_else(|| {
McpError::internal_error(
"Server final task test-store retained initial work for a missing task",
)
})?;
let generation = state.generations.get(&task_id).copied().ok_or_else(|| {
McpError::internal_error(
"Server final task test-store retained initial work without a generation",
)
})?;
Ok(Some(FinalTaskSnapshot::new(task, generation)))
}
fn next_initial_work_snapshot(&self) -> McpResult<Option<FinalTaskSnapshot>> {
self.next_initial_work_snapshot_after(None)
}
fn take_initial_work_for_owner_if_current(
&self,
expected: &FinalTaskSnapshot,
owner_id: &str,
) -> McpResult<Option<FinalTaskWorkDescriptor>> {
if owner_id.is_empty() {
return Err(McpError::invalid_params(
"Final task handoff owner must be non-empty",
));
}
let task_id = &expected.task().base().task_id;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if !server_final_task_is_working(&state, task_id, expected.generation())
|| state.handoff_leases.contains_key(task_id)
|| server_final_task_execution_exhaustion(&state, task_id, now)?.is_some()
{
return Ok(None);
}
let Some(work_descriptor) = state.initial_work.get(task_id).cloned() else {
return Ok(None);
};
insert_server_final_task_handoff_lease(
&mut state,
task_id.clone(),
expected.generation(),
ServerFinalTaskHandoffKind::Initial,
owner_id,
now,
)?;
Ok(Some(work_descriptor))
}
fn restore_initial_work_for_owner_if_current(
&self,
task_id: &fastmcp_protocol::FinalTaskId,
generation: u64,
owner_id: &str,
dispatch_fence: Option<u64>,
work_descriptor: FinalTaskWorkDescriptor,
) -> McpResult<bool> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
let owns_lease = state.handoff_leases.get(task_id).is_some_and(|lease| {
lease.generation == generation
&& lease.kind == ServerFinalTaskHandoffKind::Initial
&& lease.owner_id == owner_id
&& lease.dispatch_fence == dispatch_fence
});
if !owns_lease {
return Ok(false);
}
// The elected owner must retain its fence until it converts a
// concurrent cancellation request into the terminal task state.
// Removing it here would strand a working task with cancellation
// intent after a supervisor error or dropped future.
if state.cancellation_requests.contains(task_id)
|| server_final_task_execution_exhaustion(&state, task_id, now)?.is_some()
|| !server_final_task_is_working(&state, task_id, generation)
|| state.initial_work.get(task_id) != Some(&work_descriptor)
{
return Ok(false);
}
let execution_budget = released_server_final_task_execution_budget(&state, task_id)?;
state.handoff_leases.remove(task_id);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
Ok(true)
}
fn next_accepted_input_snapshot_after(
&self,
after_task_id: Option<&fastmcp_protocol::FinalTaskId>,
) -> McpResult<Option<FinalTaskSnapshot>> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
let Some(task_id) = next_server_final_task_recovery_id(
state.accepted_inputs.keys(),
after_task_id,
|task_id| {
Ok(state.generations.get(task_id).is_some_and(|generation| {
server_final_task_is_working(&state, task_id, *generation)
}) && !state.handoff_leases.contains_key(task_id)
&& server_final_task_execution_exhaustion(&state, task_id, now)?.is_none())
},
)?
else {
return Ok(None);
};
let task = state.tasks.get(&task_id).cloned().ok_or_else(|| {
McpError::internal_error(
"Server final task test-store retained input for a missing task",
)
})?;
let generation = state.generations.get(&task_id).copied().ok_or_else(|| {
McpError::internal_error(
"Server final task test-store retained input without a generation",
)
})?;
Ok(Some(FinalTaskSnapshot::new(task, generation)))
}
fn next_accepted_input_snapshot(&self) -> McpResult<Option<FinalTaskSnapshot>> {
self.next_accepted_input_snapshot_after(None)
}
fn restore_input_for_owner_if_current(
&self,
task_id: &fastmcp_protocol::FinalTaskId,
generation: u64,
owner_id: &str,
dispatch_fence: Option<u64>,
input_responses: fastmcp_protocol::TaskInputResponses,
) -> McpResult<bool> {
if input_responses.is_empty() {
return Err(McpError::internal_error(
"Cannot restore an empty accepted-input handoff",
));
}
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
let owns_lease = state.handoff_leases.get(task_id).is_some_and(|lease| {
lease.generation == generation
&& lease.kind == ServerFinalTaskHandoffKind::Resumed
&& lease.owner_id == owner_id
&& lease.dispatch_fence == dispatch_fence
});
if !owns_lease {
return Ok(false);
}
// See initial-work restoration: cancellation must retain the
// elected fence until the runner records terminal cancellation.
if state.cancellation_requests.contains(task_id)
|| server_final_task_execution_exhaustion(&state, task_id, now)?.is_some()
|| !server_final_task_is_working(&state, task_id, generation)
|| state.accepted_inputs.get(task_id) != Some(&input_responses)
{
return Ok(false);
}
let execution_budget = released_server_final_task_execution_budget(&state, task_id)?;
state.handoff_leases.remove(task_id);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
Ok(true)
}
fn begin_handoff_dispatch_for_owner_if_current(
&self,
task_id: &fastmcp_protocol::FinalTaskId,
generation: u64,
owner_id: &str,
) -> McpResult<Option<u64>> {
if owner_id.is_empty() {
return Err(McpError::invalid_params(
"Final task handoff owner must be non-empty",
));
}
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if !server_final_task_is_working(&state, task_id, generation)
|| server_final_task_execution_exhaustion(&state, task_id, now)?.is_some()
|| !state.handoff_leases.get(task_id).is_some_and(|lease| {
lease.generation == generation
&& lease.owner_id == owner_id
&& lease.dispatch_fence.is_none()
&& lease.expires_at > now
})
{
return Ok(None);
}
let dispatch_fence = next_server_final_task_dispatch_fence(&mut state)?;
let expires_at = server_final_task_handoff_expiry(now)?;
let lease = state.handoff_leases.get_mut(task_id).ok_or_else(|| {
McpError::internal_error("Server final task test-store lost a handoff lease")
})?;
lease.dispatch_fence = Some(dispatch_fence);
lease.expires_at = expires_at;
Ok(Some(dispatch_fence))
}
fn renew_handoff_dispatch_if_current(
&self,
task_id: &fastmcp_protocol::FinalTaskId,
generation: u64,
owner_id: &str,
dispatch_fence: u64,
) -> McpResult<bool> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if !server_final_task_is_working(&state, task_id, generation)
|| server_final_task_execution_exhaustion(&state, task_id, now)?.is_some()
|| !state.handoff_leases.get(task_id).is_some_and(|lease| {
lease.generation == generation
&& lease.owner_id == owner_id
&& lease.dispatch_fence == Some(dispatch_fence)
})
{
return Ok(false);
}
let expires_at = server_final_task_handoff_expiry(now)?;
let lease = state.handoff_leases.get_mut(task_id).ok_or_else(|| {
McpError::internal_error("Server final task test-store lost a handoff lease")
})?;
lease.expires_at = expires_at;
Ok(true)
}
fn handoff_dispatch_lease_heartbeat_interval(&self) -> McpResult<Duration> {
Ok(SERVER_FINAL_TASK_HANDOFF_HEARTBEAT)
}
fn finish_handoff_dispatch_for_owner_if_current(
&self,
task_id: &fastmcp_protocol::FinalTaskId,
generation: u64,
owner_id: &str,
dispatch_fence: u64,
) -> McpResult<bool> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
let Some(lease) = state.handoff_leases.get(task_id) else {
return Ok(false);
};
if lease.generation != generation
|| lease.owner_id != owner_id
|| lease.dispatch_fence != Some(dispatch_fence)
{
return Ok(false);
}
// A cancellation race after the supervisor returns must preserve
// this elected fence for fenced terminal retirement.
if state.cancellation_requests.contains(task_id)
|| server_final_task_execution_exhaustion(&state, task_id, now)?.is_some()
{
return Ok(false);
}
let kind = lease.kind;
let still_dispatchable = server_final_task_is_working(&state, task_id, generation);
let execution_budget = released_server_final_task_execution_budget(&state, task_id)?;
state.handoff_leases.remove(task_id);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
if still_dispatchable {
match kind {
ServerFinalTaskHandoffKind::Initial => {
state.initial_work.remove(task_id);
}
ServerFinalTaskHandoffKind::Resumed => {
state.accepted_inputs.remove(task_id);
}
}
}
Ok(still_dispatchable)
}
fn request_cancellation(&self, task_id: &fastmcp_protocol::FinalTaskId) -> McpResult<()> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if !state.tasks.contains_key(task_id) {
return Err(McpError::invalid_params("Task not found"));
}
record_server_final_task_cancellation(&mut state, task_id, now)
}
fn request_cancellation_if_current(&self, expected: &FinalTaskSnapshot) -> McpResult<bool> {
let task_id = &expected.task().base().task_id;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if state.generations.get(task_id) != Some(&expected.generation()) {
return Ok(false);
}
record_server_final_task_cancellation(&mut state, task_id, now)?;
Ok(true)
}
fn request_cancellation_and_clear_input_if_current(
&self,
expected: &FinalTaskSnapshot,
cancelled_task: fastmcp_protocol::Task,
cancelled_notification: fastmcp_protocol::TaskStatusNotification,
) -> McpResult<Option<FinalTaskSnapshot>> {
let task_id = &expected.task().base().task_id;
if &cancelled_task.base().task_id != task_id {
return Err(McpError::invalid_params(
"Expected and cancelled final task IDs must match",
));
}
if !matches!(&cancelled_task, fastmcp_protocol::Task::Cancelled(_)) {
return Err(McpError::invalid_params(
"Atomic task cancellation requires a cancelled final task",
));
}
validate_server_final_task_durations(&cancelled_task)?;
ensure_server_final_task_notification_matches_task(
&cancelled_task,
&cancelled_notification,
)?;
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let now = self.now();
reclaim_expired_server_final_task_handoffs(&mut state, now);
if state.generations.get(task_id) != Some(&expected.generation()) {
return Ok(None);
}
let execution_budget =
prepare_server_final_task_execution_release(&state, task_id, now)?;
let dispatch_elected = state.handoff_leases.get(task_id).is_some_and(|lease| {
lease.generation == expected.generation() && lease.dispatch_fence.is_some()
});
if dispatch_elected {
record_server_final_task_cancellation(&mut state, task_id, now)?;
let task = state.tasks.get(task_id).cloned().ok_or_else(|| {
McpError::internal_error(
"Server final task test store lost an elected task during cancellation",
)
})?;
let generation = state.generations.get(task_id).copied().ok_or_else(|| {
McpError::internal_error(
"Server final task test store lost an elected task generation during cancellation",
)
})?;
return Ok(Some(FinalTaskSnapshot::new(task, generation)));
}
let generation = next_server_final_task_generation(&mut state)?;
state.tasks.insert(task_id.clone(), cancelled_task.clone());
state.generations.insert(task_id.clone(), generation);
state
.execution_budgets
.insert(task_id.clone(), execution_budget);
state.accepted_inputs.remove(task_id);
state.initial_work.remove(task_id);
state.handoff_leases.remove(task_id);
state.cancellation_requests.remove(task_id);
state.notifications.push(cancelled_notification);
Ok(Some(FinalTaskSnapshot::new(cancelled_task, generation)))
}
fn is_cancellation_requested(
&self,
task_id: &fastmcp_protocol::FinalTaskId,
) -> McpResult<bool> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
reclaim_expired_server_final_task_handoffs(&mut state, self.now());
if !state.tasks.contains_key(task_id) {
return Err(McpError::invalid_params("Task not found"));
}
Ok(state.cancellation_requests.contains(task_id))
}
fn retention_clock_now(&self) -> Instant {
self.now()
}
fn task_retention_deadline_if_current(
&self,
task_id: &fastmcp_protocol::FinalTaskId,
generation: u64,
) -> McpResult<Option<FinalTaskRetentionDeadline>> {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.generations.get(task_id) != Some(&generation) {
return Ok(None);
}
if let Some(expires_at) = state.expires_at.get(task_id).copied() {
Ok(Some(FinalTaskRetentionDeadline::Finite(expires_at)))
} else if state.tasks.contains_key(task_id) {
Ok(Some(FinalTaskRetentionDeadline::Unlimited))
} else {
Ok(None)
}
}
}
#[cfg(feature = "tasks")]
#[test]
fn server_final_task_store_execution_budget_survives_renewal_and_reclaim() {
let start = Instant::now();
let clock_now = Arc::new(Mutex::new(start));
let clock = {
let clock_now = Arc::clone(&clock_now);
Arc::new(move || {
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
})
};
let mut backend = ServerFinalTaskStore::with_clock(clock);
backend.execution_limits = crate::tasks::FinalTaskExecutionLimits::new(
Duration::from_secs(120),
Duration::from_secs(60),
2,
)
.expect("finite backend execution policy");
let store = Arc::new(backend);
let runtime = server_final_task_store_runtime(Arc::clone(&store));
let mut service = start_final_tasks_test_service(&runtime);
let healthy = runtime
.create_task_with_work(final_tasks_test_work_descriptor(), None)
.expect("public admission accepts a bounded backend");
let mut poll_cx = std::task::Context::from_waker(std::task::Waker::noop());
assert!(Future::poll(service.as_mut(), &mut poll_cx).is_pending());
assert!(matches!(
runtime
.get_task(&healthy.task.base().task_id)
.expect("read service completion")
.task,
fastmcp_protocol::Task::Completed { .. }
));
let created = runtime
.create_task_with_work(final_tasks_test_work_descriptor(), None)
.expect("public admission stamps execution accounting");
let task_id = created.task.base().task_id.clone();
let snapshot = store.get_task_snapshot(&task_id).unwrap().unwrap();
let original = store
.task_execution_budget_if_current(&snapshot)
.unwrap()
.unwrap();
assert_eq!(original.total_deadline(), start + Duration::from_secs(120));
assert_eq!(original.attempt_deadline(), None);
assert_eq!(original.attempts(), 0);
let work = store
.take_initial_work_for_owner_if_current(&snapshot, "first-owner")
.unwrap()
.unwrap();
let claimed = store
.task_execution_budget_if_current(&snapshot)
.unwrap()
.unwrap();
assert_eq!(claimed.attempts(), 1);
assert_eq!(
claimed.attempt_deadline(),
Some(start + Duration::from_secs(60))
);
assert!(
store
.take_initial_work_for_owner_if_current(&snapshot, "other-owner")
.unwrap()
.is_none()
);
assert_eq!(
store.task_execution_budget_if_current(&snapshot).unwrap(),
Some(claimed)
);
let fence = store
.begin_handoff_dispatch_for_owner_if_current(
&task_id,
snapshot.generation(),
"first-owner",
)
.unwrap()
.unwrap();
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = start + Duration::from_secs(10);
assert!(
store
.renew_handoff_dispatch_if_current(
&task_id,
snapshot.generation(),
"first-owner",
fence,
)
.unwrap()
);
assert_eq!(
store.task_execution_budget_if_current(&snapshot).unwrap(),
Some(claimed)
);
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = start + Duration::from_secs(40);
let recovered = store.get_task_snapshot(&task_id).unwrap().unwrap();
assert!(recovered.generation() > snapshot.generation());
assert!(
store
.task_execution_budget_if_current(&snapshot)
.unwrap()
.is_none()
);
let released = store
.task_execution_budget_if_current(&recovered)
.unwrap()
.unwrap();
assert_eq!(released.total_deadline(), original.total_deadline());
assert_eq!(released.attempt_deadline(), None);
assert_eq!(released.attempts(), 1);
assert!(
!store
.restore_initial_work_for_owner_if_current(
&task_id,
snapshot.generation(),
"first-owner",
Some(fence),
work.clone(),
)
.unwrap()
);
assert_eq!(
store.task_execution_budget_if_current(&recovered).unwrap(),
Some(released)
);
assert!(
store
.take_initial_work_for_owner_if_current(&recovered, "second-owner")
.unwrap()
.is_some()
);
let retried = store
.task_execution_budget_if_current(&recovered)
.unwrap()
.unwrap();
assert_eq!(retried.total_deadline(), original.total_deadline());
assert_eq!(
retried.attempt_deadline(),
Some(start + Duration::from_secs(100))
);
assert_eq!(retried.attempts(), 2);
assert!(
store
.restore_initial_work_for_owner_if_current(
&task_id,
recovered.generation(),
"second-owner",
None,
work,
)
.unwrap()
);
assert!(
store
.take_initial_work_for_owner_if_current(&recovered, "third-owner")
.unwrap()
.is_none()
);
let fastmcp_protocol::Task::Failed { error, .. } = runtime
.get_task(&task_id)
.expect("public read retires exhausted claims")
.task
else {
panic!("claim exhaustion must retain a failed task");
};
assert_eq!(error.message, "Task execution attempt limit exceeded");
}
#[cfg(feature = "tasks")]
#[test]
fn server_final_task_store_execution_expiry_fences_completion_and_retains_identity() {
let start = Instant::now();
let clock_now = Arc::new(Mutex::new(start));
let clock = {
let clock_now = Arc::clone(&clock_now);
Arc::new(move || {
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
})
};
let mut backend = ServerFinalTaskStore::with_clock(clock);
backend.execution_limits = crate::tasks::FinalTaskExecutionLimits::new(
Duration::from_millis(100),
Duration::from_millis(10),
2,
)
.expect("finite backend execution policy");
let store = Arc::new(backend);
let runtime = server_final_task_store_runtime(Arc::clone(&store));
let _service = start_final_tasks_test_service(&runtime);
let created = runtime
.create_task_with_work(final_tasks_test_work_descriptor(), None)
.unwrap();
let sibling = runtime
.create_task_with_work(final_tasks_test_work_descriptor(), None)
.unwrap();
let task_id = created.task.base().task_id.clone();
let snapshot = store.get_task_snapshot(&task_id).unwrap().unwrap();
let sibling_snapshot = store
.get_task_snapshot(&sibling.task.base().task_id)
.unwrap()
.unwrap();
store
.take_initial_work_for_owner_if_current(&snapshot, "owner")
.unwrap()
.unwrap();
let fence = store
.begin_handoff_dispatch_for_owner_if_current(&task_id, snapshot.generation(), "owner")
.unwrap()
.unwrap();
let claimed = store
.task_execution_budget_if_current(&snapshot)
.unwrap()
.unwrap();
let retained_ttl = snapshot.task().base().ttl_ms.clone();
let retained_created_at = snapshot.task().base().created_at.clone();
assert!(
store
.expire_task_execution_if_current(&snapshot)
.unwrap()
.is_none()
);
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = start + Duration::from_millis(10);
assert!(
!store
.renew_handoff_dispatch_if_current(&task_id, snapshot.generation(), "owner", fence)
.unwrap()
);
let completed = fastmcp_protocol::Task::Completed {
base: crate::tasks::transition_terminal_final_task_base(
snapshot.task().base().clone(),
fastmcp_protocol::FinalTaskStatus::Completed,
None,
)
.unwrap(),
result: serde_json::from_value(serde_json::json!({"content": []})).unwrap(),
};
let error = store
.replace_task_and_clear_input_for_handoff_if_current(
&snapshot,
"owner",
fence,
false,
completed.clone(),
crate::tasks::final_task_notification(&completed),
)
.expect_err("late fenced completion cannot replace the pending timeout");
assert_eq!(error.code, McpErrorCode::InvalidParams);
assert_eq!(
store
.get_task_snapshot(&task_id)
.unwrap()
.unwrap()
.generation(),
snapshot.generation()
);
assert_eq!(
store.task_execution_budget_if_current(&snapshot).unwrap(),
Some(claimed)
);
{
let state = store
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(state.notifications.len(), 2);
assert_eq!(state.handoff_leases[&task_id].dispatch_fence, Some(fence));
}
assert_eq!(
store
.next_expired_execution_snapshot()
.unwrap()
.unwrap()
.task()
.base()
.task_id,
task_id
);
let mut forged_base = snapshot.task().base().clone();
forged_base.ttl_ms = None;
let forged = FinalTaskSnapshot::new(
fastmcp_protocol::Task::Working(forged_base),
snapshot.generation(),
);
let expired = store
.expire_task_execution_if_current(&forged)
.unwrap()
.unwrap();
assert_eq!(expired.task().base().ttl_ms, retained_ttl);
assert_eq!(expired.task().base().created_at, retained_created_at);
assert!(expired.generation() > snapshot.generation());
let fastmcp_protocol::Task::Failed { error, .. } = expired.task() else {
panic!("attempt timeout must commit a typed failure");
};
assert_eq!(error.code, (-32000).into());
assert_eq!(error.message, "Task attempt execution deadline exceeded");
assert!(
store
.expire_task_execution_if_current(&snapshot)
.unwrap()
.is_none()
);
assert!(matches!(
runtime.get_task(&task_id).unwrap().task,
fastmcp_protocol::Task::Failed { .. }
));
assert_eq!(
store
.get_task_snapshot(&sibling.task.base().task_id)
.unwrap()
.unwrap()
.generation(),
sibling_snapshot.generation()
);
{
let state = store
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(state.notifications.len(), 3);
assert!(!state.handoff_leases.contains_key(&task_id));
assert!(!state.initial_work.contains_key(&task_id));
assert_eq!(state.execution_budgets[&task_id].attempt_deadline(), None);
assert_eq!(state.execution_budgets[&task_id].attempts(), 1);
}
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
start + Duration::from_millis(100);
assert!(store.next_initial_work_snapshot().unwrap().is_none());
let fastmcp_protocol::Task::Failed { error, .. } =
runtime.get_task(&sibling.task.base().task_id).unwrap().task
else {
panic!("queued work must expire without first acquiring a worker");
};
assert_eq!(error.message, "Task total execution deadline exceeded");
let state = store
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(
state.execution_budgets[&sibling.task.base().task_id].attempts(),
0
);
assert_eq!(state.notifications.len(), 4);
}
#[cfg(feature = "tasks")]
struct FinalTasksTestSupervisor;
#[cfg(feature = "tasks")]
impl ApplicationTaskSupervisor for FinalTasksTestSupervisor {
fn resume<'a>(
&'a self,
_cx: &'a Cx,
handoff: FinalTaskSupervisorHandoff,
) -> FinalTaskSupervisorFuture<'a> {
Box::pin(async move {
let result = serde_json::from_value(serde_json::json!({"content": []}))
.expect("typed terminal task result");
match handoff {
FinalTaskSupervisorHandoff::Initial(initial) => {
initial
.complete_task(result, Some("completed by test service".to_owned()))?;
}
FinalTaskSupervisorHandoff::Resumed(accepted) => {
accepted
.complete_task(result, Some("completed by test service".to_owned()))?;
}
}
Ok(())
})
}
}
/// Starts an owned Tasks runner and advances it to its receive boundary.
///
/// Callers retain the returned future for the server lifetime. Tests that
/// claim handoff consumption drive that same future after a durable wakeup
/// rather than treating readiness alone as execution evidence.
#[cfg(feature = "tasks")]
fn start_final_tasks_test_service(
runtime: &FinalTaskRuntime,
) -> std::pin::Pin<Box<dyn Future<Output = McpResult<()>>>> {
let runner = runtime
.install_task_service(1, Arc::new(FinalTasksTestSupervisor))
.expect("test final Tasks service must install");
let service_cx = Cx::for_testing();
let mut service = Box::pin(async move { runner.run(&service_cx).await });
let mut poll_cx = std::task::Context::from_waker(std::task::Waker::noop());
assert!(
Future::poll(service.as_mut(), &mut poll_cx).is_pending(),
"test final Tasks service must publish readiness and remain owned",
);
service
}
#[cfg(feature = "tasks")]
fn final_tasks_test_work_descriptor() -> FinalTaskWorkDescriptor {
FinalTaskWorkDescriptor::new(serde_json::json!({
"operation": "server-final-tasks-test",
}))
.expect("test final Tasks work descriptor must be valid")
}
#[cfg(feature = "tasks")]
struct RecordingServerFinalTaskSupervisor {
started: Arc<Mutex<Vec<fastmcp_protocol::FinalTaskId>>>,
}
#[cfg(feature = "tasks")]
impl ApplicationTaskSupervisor for RecordingServerFinalTaskSupervisor {
fn resume<'a>(
&'a self,
cx: &'a Cx,
handoff: FinalTaskSupervisorHandoff,
) -> FinalTaskSupervisorFuture<'a> {
let started = Arc::clone(&self.started);
Box::pin(async move {
let FinalTaskSupervisorHandoff::Initial(initial) = handoff else {
return Err(McpError::internal_error(
"server final task test supervisor expected initial work",
));
};
started
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(initial.task_id().clone());
let result = serde_json::from_value(serde_json::json!({"content": []}))
.expect("typed terminal task result");
initial.complete_task(result, None)?;
cx.cancel_with(asupersync::CancelKind::User, None);
Ok(())
})
}
}
#[cfg(feature = "tasks")]
fn server_final_task_store_runtime(store: Arc<ServerFinalTaskStore>) -> FinalTaskRuntime {
FinalTaskRuntime::new(
store,
FinalTaskRuntimeConfig::new(60_000, None)
.expect("server final task test policy is valid"),
Arc::new(|_| {}),
)
}
#[cfg(feature = "tasks")]
fn poll_final_task_service_once<F>(
service: std::pin::Pin<&mut F>,
) -> std::task::Poll<McpResult<()>>
where
F: Future<Output = McpResult<()>>,
{
let mut task_cx = std::task::Context::from_waker(std::task::Waker::noop());
Future::poll(service, &mut task_cx)
}
#[cfg(feature = "tasks")]
#[test]
fn server_final_task_store_service_consumes_durable_initial_work() {
let store = Arc::new(ServerFinalTaskStore::default());
let runtime = server_final_task_store_runtime(Arc::clone(&store));
let started = Arc::new(Mutex::new(Vec::new()));
let runner = runtime
.install_task_service(
1,
Arc::new(RecordingServerFinalTaskSupervisor {
started: Arc::clone(&started),
}),
)
.expect("test task service installs");
let service_cx = Cx::for_testing();
let mut service = Box::pin(runner.run(&service_cx));
assert!(matches!(
poll_final_task_service_once(service.as_mut()),
std::task::Poll::Pending
));
let created = runtime
.create_task_with_work(final_tasks_test_work_descriptor(), None)
.expect("ready service admits durable initial work");
let task_id = created.task.base().task_id.clone();
assert!(matches!(
poll_final_task_service_once(service.as_mut()),
std::task::Poll::Ready(Ok(()))
));
assert_eq!(
started
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_slice(),
&[task_id],
"the service consumes exactly the initial descriptor retained with task creation"
);
assert!(
FinalTaskStore::next_initial_work_snapshot(&*store)
.expect("completed initial-work scan is valid")
.is_none(),
"successful owner-fenced dispatch consumes the initial handoff"
);
}
#[cfg(feature = "tasks")]
#[test]
fn server_final_task_store_cancellation_before_wakeup_fences_initial_work() {
let store = Arc::new(ServerFinalTaskStore::default());
let runtime = server_final_task_store_runtime(Arc::clone(&store));
let started = Arc::new(Mutex::new(Vec::new()));
let runner = runtime
.install_task_service(
1,
Arc::new(RecordingServerFinalTaskSupervisor {
started: Arc::clone(&started),
}),
)
.expect("test task service installs");
let service_cx = Cx::for_testing();
let mut service = Box::pin(runner.run(&service_cx));
assert!(matches!(
poll_final_task_service_once(service.as_mut()),
std::task::Poll::Pending
));
let created = runtime
.create_task_with_work(final_tasks_test_work_descriptor(), None)
.expect("ready service admits durable initial work");
let task_id = created.task.base().task_id.clone();
runtime
.cancel_task(&task_id)
.expect("changing only cancellation records intent before service polling");
assert!(matches!(
poll_final_task_service_once(service.as_mut()),
std::task::Poll::Pending
));
assert!(
started
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty(),
"cancellation before dispatch prevents application initial work"
);
assert!(
FinalTaskStore::next_initial_work_snapshot(&*store)
.expect("cancelled initial-work scan is valid")
.is_none(),
"cancellation fences the durable initial handoff instead of allowing recovery"
);
}
#[cfg(feature = "tasks")]
#[test]
fn server_final_task_store_expired_elected_cancellation_lease_retires_task() {
let start = Instant::now();
let clock_now = Arc::new(Mutex::new(start));
let clock = {
let clock_now = Arc::clone(&clock_now);
Arc::new(move || {
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
})
};
let store = ServerFinalTaskStore::with_clock(clock);
let task_id = fastmcp_protocol::FinalTaskId::parse("server-expired-elected-cancellation")
.expect("fixed final task ID must be valid");
let timestamp = fastmcp_protocol::TaskTimestamp::parse("2026-07-28T12:00:00.000Z")
.expect("fixed final task timestamp must be valid");
let task = fastmcp_protocol::Task::Working(fastmcp_protocol::TaskBase {
task_id: task_id.clone(),
status: fastmcp_protocol::FinalTaskStatus::Working,
status_message: None,
created_at: timestamp.clone(),
last_updated_at: timestamp,
ttl_ms: None,
poll_interval_ms: None,
});
let notification = crate::tasks::final_task_notification(&task);
store
.create_task_with_work(task, notification, final_tasks_test_work_descriptor())
.expect("initial work is atomically retained");
let snapshot = store
.get_task_snapshot(&task_id)
.expect("elected task snapshot is readable")
.expect("elected task remains retained");
let owner_id = "server-expired-cancelled-owner";
assert!(
store
.take_initial_work_for_owner_if_current(&snapshot, owner_id)
.expect("initial work claim is valid")
.is_some()
);
store
.begin_handoff_dispatch_for_owner_if_current(&task_id, snapshot.generation(), owner_id)
.expect("dispatch election is valid")
.expect("claimed owner becomes elected");
store
.request_cancellation(&task_id)
.expect("elected task records durable cancellation intent");
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = start
.checked_add(SERVER_FINAL_TASK_HANDOFF_LEASE)
.expect("fixed server handoff lease fits the monotonic clock");
let retired = store
.get_task_snapshot(&task_id)
.expect("expired lease reclamation is readable")
.expect("unlimited-retention task remains stored after lease expiry");
assert!(matches!(
retired.task(),
fastmcp_protocol::Task::Cancelled(_)
));
assert!(
!store
.is_cancellation_requested(&task_id)
.expect("terminal retirement consumes cancellation intent")
);
let state = store
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(
!state.handoff_leases.contains_key(&task_id)
&& !state.initial_work.contains_key(&task_id)
&& !state.accepted_inputs.contains_key(&task_id),
"changing only cancellation from normal server-lease expiry leaves no stranded work"
);
}
#[cfg(feature = "tasks")]
#[test]
fn server_final_task_store_reclaims_finite_ttl_task_and_work_at_creation_expiry() {
let start = Instant::now();
let clock_now = Arc::new(Mutex::new(start));
let clock = {
let clock_now = Arc::clone(&clock_now);
Arc::new(move || {
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
})
};
let store = ServerFinalTaskStore::with_clock(clock);
let task_id = fastmcp_protocol::FinalTaskId::parse("server-store-ttl-test")
.expect("fixed final task ID must be valid");
let timestamp = fastmcp_protocol::TaskTimestamp::parse("2026-07-28T12:00:00.000Z")
.expect("fixed final task timestamp must be valid");
let task = fastmcp_protocol::Task::Working(fastmcp_protocol::TaskBase {
task_id: task_id.clone(),
status: fastmcp_protocol::FinalTaskStatus::Working,
status_message: None,
created_at: timestamp.clone(),
last_updated_at: timestamp,
ttl_ms: Some(
serde_json::from_value(serde_json::json!(10_u64))
.expect("positive finite task TTL must be valid"),
),
poll_interval_ms: None,
});
let notification = fastmcp_protocol::TaskStatusNotification::new(
fastmcp_protocol::tasks_extension::TaskStatusNotificationParams {
task: task.clone(),
meta: None,
additional: BTreeMap::new(),
},
);
store
.create_task_with_work(task, notification, final_tasks_test_work_descriptor())
.expect("finite task creation atomically retains task and initial work");
let pre_expiry_snapshot = store
.get_task_snapshot(&task_id)
.expect("pre-expiry task snapshot is readable")
.expect("finite task remains readable before its creation-time TTL expires");
assert!(
store
.work_descriptor_if_current(&pre_expiry_snapshot)
.expect("pre-expiry work lookup is valid")
.is_some(),
"the initial descriptor remains readable before expiry"
);
assert!(
store
.next_initial_work_snapshot()
.expect("pre-expiry initial-work recovery scan is valid")
.is_some(),
"the initial descriptor remains recoverable before expiry"
);
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = start
.checked_add(Duration::from_millis(10))
.expect("test monotonic clock must represent task expiry");
assert!(
store
.get_task(&task_id)
.expect("post-expiry task lookup is valid")
.is_none(),
"changing only the clock to the finite creation-time expiry reclaims the task"
);
assert!(
store
.get_task_snapshot(&task_id)
.expect("post-expiry task snapshot lookup is valid")
.is_none(),
"expiry leaves no readable task generation"
);
assert!(
store
.next_initial_work_snapshot()
.expect("post-expiry initial-work recovery scan is valid")
.is_none(),
"expiry leaves no recoverable initial work"
);
let state = store
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(
!state.work_descriptors.contains_key(&task_id)
&& !state.initial_work.contains_key(&task_id)
&& !state.expires_at.contains_key(&task_id),
"post-expiry reclamation clears only the expired task's private work and expiry state"
);
}
#[cfg(feature = "tasks")]
#[test]
fn server_final_task_store_rejects_out_of_range_ttl_before_mutation() {
let store = ServerFinalTaskStore::default();
let task_id = fastmcp_protocol::FinalTaskId::parse("server-store-oversized-ttl")
.expect("fixed final task ID must be valid");
let timestamp = fastmcp_protocol::TaskTimestamp::parse("2026-07-28T12:00:00.000Z")
.expect("fixed final task timestamp must be valid");
let task = fastmcp_protocol::Task::Working(fastmcp_protocol::TaskBase {
task_id: task_id.clone(),
status: fastmcp_protocol::FinalTaskStatus::Working,
status_message: None,
created_at: timestamp.clone(),
last_updated_at: timestamp,
ttl_ms: None,
poll_interval_ms: None,
});
let mut wire = serde_json::to_value(task).expect("serialize valid task baseline");
wire["ttlMs"] = serde_json::from_str("18446744073709551616")
.expect("retain the oversized mathematical JSON integer");
let error = serde_json::from_value::<fastmcp_protocol::Task>(wire)
.expect_err("out-of-range task TTL must be rejected during typed admission");
assert_eq!(error.classify(), serde_json::error::Category::Data);
let state = store
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(
state.tasks.is_empty()
&& state.generations.is_empty()
&& state.notifications.is_empty()
&& state.expires_at.is_empty(),
"typed TTL admission fails before task-store mutation"
);
assert_eq!(state.next_generation, 0);
assert!(!state.tasks.contains_key(&task_id));
}
#[cfg(feature = "tasks")]
#[test]
fn server_final_task_store_rejects_out_of_range_poll_before_reclaiming_expired_state() {
let start = Instant::now();
let clock_now = Arc::new(Mutex::new(start));
let clock = {
let clock_now = Arc::clone(&clock_now);
Arc::new(move || {
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
})
};
let store = ServerFinalTaskStore::with_clock(clock);
let timestamp = fastmcp_protocol::TaskTimestamp::parse("2026-07-28T12:00:00.000Z")
.expect("fixed final task timestamp must be valid");
let expired_id = fastmcp_protocol::FinalTaskId::parse("server-store-expired-before-poll")
.expect("fixed expired task ID must be valid");
let expired_task = fastmcp_protocol::Task::Working(fastmcp_protocol::TaskBase {
task_id: expired_id.clone(),
status: fastmcp_protocol::FinalTaskStatus::Working,
status_message: None,
created_at: timestamp.clone(),
last_updated_at: timestamp.clone(),
ttl_ms: Some(
serde_json::from_value(serde_json::json!(1_u64))
.expect("finite expiry TTL must be valid"),
),
poll_interval_ms: None,
});
let expired_notification = fastmcp_protocol::TaskStatusNotification::new(
fastmcp_protocol::tasks_extension::TaskStatusNotificationParams {
task: expired_task.clone(),
meta: None,
additional: BTreeMap::new(),
},
);
store
.create_task(expired_task, expired_notification)
.expect("baseline expired task must enter the store");
*clock_now
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = start
.checked_add(Duration::from_millis(1))
.expect("test monotonic clock must represent expiry");
let rejected_id = fastmcp_protocol::FinalTaskId::parse("server-store-oversized-poll")
.expect("fixed rejected task ID must be valid");
let rejected_task = fastmcp_protocol::Task::Working(fastmcp_protocol::TaskBase {
task_id: rejected_id.clone(),
status: fastmcp_protocol::FinalTaskStatus::Working,
status_message: None,
created_at: timestamp.clone(),
last_updated_at: timestamp,
ttl_ms: None,
poll_interval_ms: None,
});
let mut wire = serde_json::to_value(rejected_task).expect("serialize valid task baseline");
wire["pollIntervalMs"] = serde_json::from_str("18446744073709551616")
.expect("retain the oversized mathematical JSON integer");
let error = serde_json::from_value::<fastmcp_protocol::Task>(wire)
.expect_err("out-of-range task poll interval must fail typed admission");
assert_eq!(error.classify(), serde_json::error::Category::Data);
let state = store
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(
state.tasks.contains_key(&expired_id)
&& state.expires_at.contains_key(&expired_id)
&& state.generations.contains_key(&expired_id)
&& !state.tasks.contains_key(&rejected_id),
"typed poll admission rejects before reclaiming unrelated expired state"
);
assert_eq!(state.notifications.len(), 1);
assert_eq!(state.next_generation, 1);
}
#[cfg(feature = "tasks")]
#[test]
fn server_final_task_store_rejects_same_id_notification_base_drift_without_mutation() {
let store = Arc::new(ServerFinalTaskStore::default());
let timestamp = fastmcp_protocol::TaskTimestamp::parse("2026-07-28T12:00:00.000Z")
.expect("fixed final task timestamp must be valid");
let task = fastmcp_protocol::Task::Working(fastmcp_protocol::TaskBase {
task_id: fastmcp_protocol::FinalTaskId::parse("server-store-drift-test")
.expect("fixed final task ID must be valid"),
status: fastmcp_protocol::FinalTaskStatus::Working,
status_message: None,
created_at: timestamp.clone(),
last_updated_at: timestamp,
ttl_ms: None,
poll_interval_ms: None,
});
let notification = fastmcp_protocol::TaskStatusNotification::new(
fastmcp_protocol::tasks_extension::TaskStatusNotificationParams {
task: task.clone(),
meta: None,
additional: BTreeMap::new(),
},
);
store
.create_task(task.clone(), notification)
.expect("matching task and notification create");
let task_id = task.base().task_id.clone();
let snapshot_before = store
.get_task_snapshot(&task_id)
.expect("stored task snapshot is readable")
.expect("created task is retained");
let task_before = serde_json::to_value(snapshot_before.task())
.expect("serialize retained task before rejection");
let notifications_before = {
let state = store
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
serde_json::to_value(&state.notifications)
.expect("serialize retained notifications before rejection")
};
let mut drifted_notification = {
let state = store
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state
.notifications
.last()
.cloned()
.expect("created task retains its notification")
};
let fastmcp_protocol::Task::Working(base) = &mut drifted_notification.params.task else {
panic!("baseline notification contains the working task");
};
base.status_message = Some("only the notification task base drifted".to_owned());
let error = store
.replace_task_if_current(&snapshot_before, task, drifted_notification)
.expect_err("same-ID notification base drift must be rejected");
assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidParams);
let snapshot_after = store
.get_task_snapshot(&task_id)
.expect("stored task snapshot remains readable")
.expect("rejection preserves the retained task");
assert_eq!(
serde_json::to_value(snapshot_after.task())
.expect("serialize retained task after rejection"),
task_before,
"rejection preserves the retained task"
);
assert_eq!(
snapshot_after.generation(),
snapshot_before.generation(),
"rejection preserves the compare-and-swap generation"
);
let notifications_after = {
let state = store
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
serde_json::to_value(&state.notifications)
.expect("serialize retained notifications after rejection")
};
assert_eq!(
notifications_after, notifications_before,
"rejection preserves the retained notification history"
);
}
#[cfg(feature = "tasks")]
fn final_tasks_test_runtime(
delivered: Arc<Mutex<Vec<fastmcp_protocol::TaskStatusNotification>>>,
) -> FinalTaskRuntime {
let emitter: FinalTaskNotificationEmitter = Arc::new(move |notification| {
delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
});
FinalTaskRuntime::in_memory(
FinalTaskRuntimeConfig::new(60_000, Some(5_000))
.expect("valid final Tasks timing policy"),
emitter,
)
}
#[cfg(feature = "tasks")]
fn final_tasks_test_server(
delivered: Arc<Mutex<Vec<fastmcp_protocol::TaskStatusNotification>>>,
) -> (
Server,
std::pin::Pin<Box<dyn Future<Output = McpResult<()>>>>,
) {
let runtime = final_tasks_test_runtime(delivered);
let service = start_final_tasks_test_service(&runtime);
let server = Server::new("final-tasks-server", "1.0.0")
.final_tasks(runtime)
.expect("final Tasks must install")
.build();
(server, service)
}
#[cfg(feature = "tasks")]
#[test]
fn final_tasks_test_server_drives_retained_service_after_task_creation() {
let delivered = Arc::new(Mutex::new(Vec::new()));
let (server, mut service) = final_tasks_test_server(Arc::clone(&delivered));
let runtime = server
.final_task_runtime()
.expect("configured final Tasks runtime must be retained");
let task_id = runtime
.create_task_with_work(final_tasks_test_work_descriptor(), None)
.expect("ready service accepts durable task creation")
.task
.base()
.task_id
.clone();
let mut poll_cx = std::task::Context::from_waker(std::task::Waker::noop());
assert!(
Future::poll(service.as_mut(), &mut poll_cx).is_pending(),
"the retained service continues waiting after committing its task transition",
);
assert!(matches!(
runtime
.get_task(&task_id)
.expect("completed task remains readable")
.task,
fastmcp_protocol::Task::Completed { .. }
));
assert_eq!(
delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
2,
"task creation and the fenced terminal completion each emit one notification",
);
}
#[cfg(feature = "tasks")]
struct FinalTaskCreatingTool;
#[cfg(feature = "tasks")]
impl ToolHandler for FinalTaskCreatingTool {
fn definition(&self) -> Tool {
Tool {
name: "durable_final_task".to_owned(),
description: Some("Creates one negotiated final task".to_owned()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<Vec<Content>> {
Ok(vec![Content::text("exact legacy completion")])
}
fn call_final_outcome(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<FinalToolOutcome> {
Ok(FinalToolOutcome::CreateTask {
work_descriptor: FinalTaskWorkDescriptor::new(serde_json::json!({
"operation": "durable-final-task-tool",
}))?,
status_message: Some("accepted by negotiated tool".to_owned()),
})
}
fn declares_final_tasks(&self) -> bool {
true
}
}
#[cfg(feature = "tasks")]
fn final_task_creating_tool_request(declare_tasks: bool) -> JsonRpcRequest {
let capabilities = if declare_tasks {
serde_json::json!({
"extensions": { "io.modelcontextprotocol/tasks": {} }
})
} else {
serde_json::json!({})
};
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"_meta": {
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": capabilities,
},
"name": "durable_final_task",
"arguments": {},
})),
81_i64,
)
}
#[cfg(feature = "tasks")]
struct HttpConditionalTaskOutcomeTool {
calls: Arc<AtomicUsize>,
}
#[cfg(feature = "tasks")]
impl ToolHandler for HttpConditionalTaskOutcomeTool {
fn definition(&self) -> Tool {
Tool {
name: "durable_final_task".to_owned(),
description: Some("Creates one negotiated final task".to_owned()),
input_schema: serde_json::json!({
"type": "object",
"properties": {"createTask": {"type": "boolean"}},
"required": ["createTask"],
"unevaluatedProperties": false,
}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<Vec<Content>> {
Ok(vec![Content::text("exact legacy completion")])
}
fn call_final_outcome(
&self,
ctx: &McpContext,
arguments: serde_json::Value,
) -> McpResult<FinalToolOutcome> {
self.calls.fetch_add(1, Ordering::SeqCst);
ctx.report_progress(1.0, Some("before task capability admission"));
if arguments
.get("createTask")
.and_then(serde_json::Value::as_bool)
.is_some_and(|create_task| !create_task)
{
return Ok(FinalToolOutcome::Complete(
crate::handler::promote_legacy_tool_content(vec![Content::text(
"exact final non-task completion",
)])?,
));
}
Ok(FinalToolOutcome::CreateTask {
work_descriptor: FinalTaskWorkDescriptor::new(serde_json::json!({
"operation": "durable-final-task-http-outcome",
}))?,
status_message: Some("requires negotiated Tasks capability".to_owned()),
})
}
fn declares_final_tasks(&self) -> bool {
true
}
}
#[cfg(feature = "tasks")]
fn final_http_task_outcome_request(
create_task: bool,
include_progress_marker: bool,
) -> JsonRpcRequest {
let metadata = if include_progress_marker {
serde_json::json!({
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {},
"progressToken": "missing-task-capability",
})
} else {
serde_json::json!({
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {},
})
};
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"_meta": metadata,
"name": "durable_final_task",
"arguments": {"createTask": create_task},
})),
81_i64,
)
}
#[cfg(feature = "tasks")]
fn public_http_missing_required_capability_response(
accept: &str,
create_task: bool,
) -> (HttpResponse, usize, usize) {
let cx = Cx::for_testing();
let calls = Arc::new(AtomicUsize::new(0));
let delivered = Arc::new(Mutex::new(Vec::new()));
let endpoint = Server::new("missing-capability-http", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.tool(HttpConditionalTaskOutcomeTool {
calls: Arc::clone(&calls),
})
.final_tasks(final_tasks_test_runtime(Arc::clone(&delivered)))
.expect("final Tasks runtime must install for HTTP outcome admission")
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let request = final_http_task_outcome_request(create_task, accept == "text/event-stream");
let response = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", accept)
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/call")
.with_header("mcp-name", "durable_final_task")
.with_body(
serde_json::to_vec(&request)
.expect("missing-capability request must serialize"),
),
),
)
.expect("missing-capability request must be handled");
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("missing capability must reject before committing a response stream");
};
let delivered = delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len();
(response, calls.load(Ordering::SeqCst), delivered)
}
#[cfg(feature = "tasks")]
fn assert_public_http_missing_required_capability(
response: HttpResponse,
handler_calls: usize,
delivered_task_notifications: usize,
) {
assert_eq!(response.status, HttpStatus::BAD_REQUEST);
assert_eq!(
response.headers.get("content-type").map(String::as_str),
Some("application/json")
);
let response: JsonRpcResponse =
serde_json::from_slice(&response.body).expect("rejection body must be JSON-RPC");
assert_eq!(response.id, Some(81_i64.into()));
assert_eq!(
response
.error
.as_ref()
.and_then(|error| error.code.as_i32()),
Some(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE)
);
assert_eq!(
response.error.and_then(|error| error.data),
Some(serde_json::json!({
"requiredCapabilities": {
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
}))
);
assert_eq!(
handler_calls, 1,
"the declared task-capable handler must choose CreateTask before capability admission",
);
assert_eq!(
delivered_task_notifications, 0,
"rejected task creation must preserve runtime state before either endpoint representation commits",
);
}
#[cfg(feature = "tasks")]
#[test]
fn public_http_json_missing_required_capability_maps_to_400_before_response_commit() {
let (response, handler_calls, delivered_task_notifications) =
public_http_missing_required_capability_response("application/json", true);
assert_public_http_missing_required_capability(
response,
handler_calls,
delivered_task_notifications,
);
}
#[cfg(feature = "tasks")]
#[test]
fn public_http_sse_missing_required_capability_maps_to_400_before_stream_commit() {
let (response, handler_calls, delivered_task_notifications) =
public_http_missing_required_capability_response("text/event-stream", true);
assert_public_http_missing_required_capability(
response,
handler_calls,
delivered_task_notifications,
);
}
#[cfg(feature = "tasks")]
#[test]
fn public_http_task_capable_tool_create_task_false_preserves_complete_outcome_precedence() {
let (response, handler_calls, delivered_task_notifications) =
public_http_missing_required_capability_response("application/json", false);
assert_eq!(response.status, HttpStatus::OK);
let response: JsonRpcResponse =
serde_json::from_slice(&response.body).expect("complete result must be JSON-RPC");
assert!(response.error.is_none());
assert_eq!(
response
.result
.as_ref()
.and_then(|result| result.get("resultType")),
Some(&serde_json::json!("complete")),
);
assert_eq!(
handler_calls, 1,
"createTask:false must reach the declared task-capable handler",
);
assert_eq!(
delivered_task_notifications, 0,
"a complete outcome must not create or notify a Task",
);
}
#[derive(Debug, Clone)]
struct HttpCapabilityErrorMiddleware {
data: serde_json::Value,
emit_progress: bool,
}
impl Middleware for HttpCapabilityErrorMiddleware {
fn on_request(
&self,
ctx: &McpContext,
_request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
if self.emit_progress {
ctx.report_progress(1.0, Some("before canonical capability error"));
}
Err(McpError::with_data(
McpErrorCode::Custom(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE),
"Required client capability is missing",
self.data.clone(),
))
}
}
#[derive(Debug, Clone, Default)]
struct HttpSubscriptionProgressMiddleware;
impl Middleware for HttpSubscriptionProgressMiddleware {
fn on_request(
&self,
ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
if request.method == SUBSCRIPTIONS_LISTEN {
ctx.report_progress(1.0, Some("before subscription acknowledgement"));
}
Ok(MiddlewareDecision::Continue)
}
}
#[derive(Debug, Clone)]
struct HttpSubscriptionResponseCapabilityErrorMiddleware {
data: serde_json::Value,
}
impl Middleware for HttpSubscriptionResponseCapabilityErrorMiddleware {
fn on_response(
&self,
_ctx: &McpContext,
request: &JsonRpcRequest,
response: serde_json::Value,
) -> McpResult<serde_json::Value> {
if request.method == SUBSCRIPTIONS_LISTEN {
return Err(McpError::with_data(
McpErrorCode::Custom(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE),
"Required client capability is missing",
self.data.clone(),
));
}
Ok(response)
}
}
#[test]
fn modern_sse_elected_representation_survives_losing_elections() {
let (stream, _receiver) = ModernSseOutcomeGate::new();
assert!(!stream.stream_admitted());
assert!(stream.elect(ModernSseDispatchElection::Stream));
assert!(stream.stream_admitted());
assert!(
!stream.elect(ModernSseDispatchElection::Immediate(HttpResponse::new(
HttpStatus::BAD_REQUEST
),))
);
assert!(!stream.elect(ModernSseDispatchElection::Failed));
assert!(
stream.stream_admitted(),
"the elected stream owns its terminal response"
);
let (immediate, _receiver) = ModernSseOutcomeGate::new();
assert!(
immediate.elect(ModernSseDispatchElection::Immediate(HttpResponse::new(
HttpStatus::BAD_REQUEST
),))
);
assert!(!immediate.elect(ModernSseDispatchElection::Stream));
assert!(
!immediate.stream_admitted(),
"a losing Stream cannot authorize SSE output"
);
}
#[test]
fn modern_sse_admission_elects_before_draining_bounded_pending_notifications() {
let (outcome_gate, _receiver) = ModernSseOutcomeGate::new();
let failed = Arc::new(AtomicBool::new(false));
let delivered = Arc::new(Mutex::new(Vec::new()));
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
let receiver = Arc::new(Mutex::new(receiver));
let committed_sender: NotificationSender = {
let outcome_gate = Arc::clone(&outcome_gate);
let failed = Arc::clone(&failed);
let delivered = Arc::clone(&delivered);
let receiver = Arc::clone(&receiver);
Arc::new(move |notification| {
// Model a capacity-one body whose writer can drain only once
// it receives the real Stream election.
if outcome_gate.stream_admitted()
&& let Ok(previous) = receiver.lock().unwrap().try_recv()
{
delivered.lock().unwrap().push(previous);
}
if sender.try_send(notification).is_err() {
failed.store(true, Ordering::Release);
}
})
};
let gate = ModernSseNotificationGate {
delivery: Mutex::new(ModernSseNotificationDelivery::Pending(Vec::new())),
subscription_request: false,
outcome_gate: Some(Arc::clone(&outcome_gate)),
terminal_delivery: Arc::new(FinalSubscriptionTerminalDelivery::default()),
committed_sender,
commit_failed: Arc::clone(&failed),
stream_admitted: Arc::new(AtomicBool::new(false)),
};
let first = JsonRpcRequest::notification(
"notifications/progress",
Some(serde_json::json!({
"progressToken": "bounded-gate", "progress": 1
})),
);
let second = JsonRpcRequest::notification(
"notifications/progress",
Some(serde_json::json!({
"progressToken": "bounded-gate", "progress": 2
})),
);
gate.publish(first.clone());
gate.publish(second.clone());
assert!(receiver.lock().unwrap().try_recv().is_err());
assert!(
!outcome_gate.stream_admitted(),
"middleware cannot open the body"
);
gate.admit_handler();
assert!(outcome_gate.stream_admitted());
assert!(
!failed.load(Ordering::Acquire),
"the admitted writer must drain before the second commit"
);
delivered
.lock()
.unwrap()
.push(receiver.lock().unwrap().try_recv().unwrap());
assert_eq!(
serde_json::to_string(&*delivered.lock().unwrap()).unwrap(),
serde_json::to_string(&[first, second]).unwrap(),
);
}
#[test]
fn modern_sse_ready_election_wins_over_simultaneous_request_cancellation() {
let cx = Cx::for_testing();
let request_cancellation = McpRequestCancellation::new();
let (outcome_gate, mut receiver) = ModernSseOutcomeGate::new();
assert!(outcome_gate.elect(ModernSseDispatchElection::Stream));
request_cancellation.cancel();
assert!(matches!(
block_on(await_modern_sse_dispatch_election(
&cx,
&request_cancellation,
&mut receiver,
)),
Ok(ModernSseDispatchElection::Stream)
));
assert!(
!outcome_gate.elect(ModernSseDispatchElection::Failed),
"the ready Stream election must remain the one linearized outcome"
);
let pre_election_cancellation = McpRequestCancellation::new();
let (_unelected_gate, mut unelected_receiver) = ModernSseOutcomeGate::new();
pre_election_cancellation.cancel();
assert!(
block_on(await_modern_sse_dispatch_election(
&cx,
&pre_election_cancellation,
&mut unelected_receiver,
))
.is_err(),
"peer body-drop before an election must still cancel the pending writer"
);
}
fn public_http_capability_error_test_server(data: serde_json::Value) -> Server {
Server::new("http-capability-error-shape", "1.0.0")
.mask_error_details(false)
.middleware(HttpCapabilityErrorMiddleware {
data,
emit_progress: false,
})
.build()
}
#[test]
fn public_http_sse_middleware_missing_capability_has_zero_events_and_maps_to_400() {
let canonical_data = serde_json::json!({
"requiredCapabilities": {
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
});
let cx = Cx::for_testing();
let endpoint = Server::new("http-middleware-capability-sse", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.mask_error_details(false)
.middleware(HttpCapabilityErrorMiddleware {
data: canonical_data.clone(),
emit_progress: true,
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "middleware-capability-progress",
},
})),
84_i64,
);
let response = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "text/event-stream")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", SERVER_DISCOVER_METHOD)
.with_body(
serde_json::to_vec(&request)
.expect("middleware capability request must serialize"),
),
),
)
.expect("middleware capability request must be handled");
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("canonical middleware error must commit neither an SSE event nor stream");
};
assert_eq!(response.status, HttpStatus::BAD_REQUEST);
assert_eq!(
response.headers.get("content-type").map(String::as_str),
Some("application/json")
);
let response: JsonRpcResponse =
serde_json::from_slice(&response.body).expect("rejection body must be JSON-RPC");
let error = response.error.expect("middleware error must be retained");
assert_eq!(
error.code.as_i32(),
Some(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE)
);
assert_eq!(error.data, Some(canonical_data));
}
#[test]
fn public_http_modern_malformed_missing_capability_error_stays_json_rpc_ok() {
let malformed_data = serde_json::json!({"requiredCapabilities": []});
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
82_i64,
);
let inbound =
InboundRequestContext::new(Cx::for_testing(), 82, InboundRequestTransport::Http);
let response = block_on(
public_http_capability_error_test_server(malformed_data.clone())
.dispatch_http_with_protocol_policy(ProtocolPolicy::ModernOnly, &inbound, &request),
);
assert_eq!(response.status, HttpStatus::OK);
let response: JsonRpcResponse =
serde_json::from_slice(&response.body).expect("lookalike response must be JSON-RPC");
let error = response
.error
.expect("middleware must preserve its lookalike error");
assert_eq!(
error.code.as_i32(),
Some(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE)
);
assert_eq!(error.data, Some(malformed_data));
}
#[test]
fn public_http_legacy_only_missing_capability_error_stays_json_rpc_ok() {
let canonical_data = serde_json::json!({
"requiredCapabilities": {
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
});
let request = JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-http-client", "version": "1.0.0"},
})),
83_i64,
);
let inbound =
InboundRequestContext::new(Cx::for_testing(), 83, InboundRequestTransport::Http);
let response = block_on(
public_http_capability_error_test_server(canonical_data.clone())
.dispatch_http_with_protocol_policy(ProtocolPolicy::LegacyOnly, &inbound, &request),
);
assert_eq!(response.status, HttpStatus::OK);
let response: JsonRpcResponse =
serde_json::from_slice(&response.body).expect("legacy response must be JSON-RPC");
let error = response
.error
.expect("middleware must preserve its canonical error");
assert_eq!(
error.code.as_i32(),
Some(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE)
);
assert_eq!(error.data, Some(canonical_data));
}
#[cfg(feature = "tasks")]
fn unrelated_extension_registry(
method: &str,
) -> (ExtensionHandlerRegistry, ServerExtensionDiscovery) {
let extension_id = fastmcp_protocol::ExtensionId::parse("com.example/echo")
.expect("test extension identifier must be valid");
let mut descriptors = ExtensionDescriptorRegistry::new();
descriptors
.register(fastmcp_protocol::ExtensionDescriptor {
id: extension_id.clone(),
client_settings: fastmcp_protocol::ExtensionSettingsSchema {
schema_id: "example-client-v1".to_owned(),
codec_id: "example-client-codec-v1".to_owned(),
},
server_settings: fastmcp_protocol::ExtensionSettingsSchema {
schema_id: "example-server-v1".to_owned(),
codec_id: "example-server-codec-v1".to_owned(),
},
resolver: fastmcp_protocol::ExtensionNegotiationResolver {
id: "example-compatibility-v1".to_owned(),
version: 1,
fallback: fastmcp_protocol::ExtensionFallbackPolicy::RejectOneSided,
},
method: Some(fastmcp_protocol::ExtensionMethodDescriptor {
name: method.to_owned(),
direction: fastmcp_protocol::ExtensionDirection::ClientToServer,
http_era_disposition: Some(
fastmcp_protocol::ExtensionHttpEraDisposition::ModernExclusive,
),
legacy_fallback: false,
}),
notification: None,
result_discriminator: Some("example-result".to_owned()),
routing_headers: Vec::new(),
stdio_correlation: None,
})
.expect("unrelated extension descriptor must register");
let mut handlers = ExtensionHandlerRegistry::new(descriptors);
handlers
.register(
extension_id.clone(),
method,
|_context: &McpContext, parameters: serde_json::Value| {
Ok::<serde_json::Value, McpError>(parameters)
},
)
.expect("unrelated extension handler must register");
(
handlers,
ServerExtensionDiscovery {
extensions: BTreeMap::from([(
extension_id,
fastmcp_protocol::ExtensionSettings::new(serde_json::json!({}))
.expect("empty unrelated extension settings must be valid"),
)]),
},
)
}
#[cfg(feature = "tasks")]
fn final_tasks_params(
task_id: &fastmcp_protocol::FinalTaskId,
settings: serde_json::Value,
) -> serde_json::Value {
serde_json::json!({
"taskId": task_id.as_str(),
"_meta": {
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": settings,
},
},
},
})
}
#[cfg(feature = "tasks")]
fn final_tasks_get_params(
task_id: &fastmcp_protocol::FinalTaskId,
settings: serde_json::Value,
) -> serde_json::Value {
serde_json::json!({
"taskId": task_id.as_str(),
"_meta": {
"io.modelcontextprotocol/protocolVersion": MODERN_PROTOCOL_VERSION,
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": settings,
},
},
},
})
}
#[cfg(feature = "tasks")]
#[test]
fn configured_final_tasks_route_methods_and_typed_notifications() {
let delivered = Arc::new(Mutex::new(Vec::new()));
let (server, _service) = final_tasks_test_server(Arc::clone(&delivered));
let runtime = server
.final_task_runtime()
.expect("configured final Tasks runtime must be retained");
let task_id = runtime
.create_task_with_work(
final_tasks_test_work_descriptor(),
Some("accepted".to_owned()),
)
.expect("application store must accept task before reply")
.task
.base()
.task_id
.clone();
let notification = delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.first()
.cloned()
.expect("durable task creation must route a typed notification");
assert_eq!(
serde_json::to_value(notification).expect("notification must serialize")["method"],
fastmcp_protocol::TASK_STATUS_NOTIFICATION
);
let inbound =
InboundRequestContext::new(Cx::for_testing(), 71, InboundRequestTransport::Memory);
let get = block_on(server.dispatch_stateless(
&inbound,
&JsonRpcRequest::new(
fastmcp_protocol::TASK_GET,
Some(final_tasks_get_params(&task_id, serde_json::json!({}))),
71_i64,
),
))
.expect("tasks/get must respond");
assert_eq!(
get.result.as_ref().map(|result| &result["resultType"]),
Some(&serde_json::json!("complete"))
);
assert_eq!(
get.result.as_ref().map(|result| &result["taskId"]),
Some(&serde_json::json!(task_id.as_str()))
);
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let session_get = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
fastmcp_protocol::TASK_GET,
Some(final_tasks_get_params(&task_id, serde_json::json!({}))),
72_i64,
),
¬ification_sender,
&test_request_sender(),
))
.expect("session tasks/get must produce a JSON-RPC response");
assert_eq!(
session_get.result.as_ref().map(|result| &result["taskId"]),
Some(&serde_json::json!(task_id.as_str())),
"stdio/session dispatch must serve official tasks/get"
);
let missing = fastmcp_protocol::FinalTaskId::parse("missing-task-id-0001")
.expect("planted missing task id");
let session_missing = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
fastmcp_protocol::TASK_GET,
Some(final_tasks_get_params(&missing, serde_json::json!({}))),
73_i64,
),
¬ification_sender,
&test_request_sender(),
))
.expect("session tasks/get of an unknown id must respond");
let missing_error = session_missing
.error
.as_ref()
.expect("unknown task id must fail");
assert_ne!(
missing_error.code,
i32::from(McpErrorCode::MethodNotFound).into(),
"unknown task id is not MethodNotFound after official Tasks is installed"
);
let mut requests = fastmcp_protocol::TaskInputRequests::new();
requests.insert(
"roots".to_owned(),
serde_json::from_value(serde_json::json!({ "method": "roots/list" }))
.expect("typed roots request"),
);
runtime
.require_input(&task_id, requests, Some("awaiting roots".to_owned()))
.expect("caller-owned supervisor can enter input_required");
let mut update_params = final_tasks_params(&task_id, serde_json::json!({}));
update_params
.as_object_mut()
.expect("final Tasks parameters are an object")
.insert(
"inputResponses".to_owned(),
serde_json::json!({ "roots": { "roots": [] } }),
);
let update = block_on(server.dispatch_stateless(
&inbound,
&JsonRpcRequest::new(
fastmcp_protocol::tasks_extension::TASK_UPDATE,
Some(update_params),
71_i64,
),
))
.expect("tasks/update must respond");
assert_eq!(
update.result,
Some(serde_json::json!({ "resultType": "complete" }))
);
let cancel = block_on(server.dispatch_stateless(
&inbound,
&JsonRpcRequest::new(
fastmcp_protocol::TASK_CANCEL,
Some(final_tasks_get_params(&task_id, serde_json::json!({}))),
71_i64,
),
))
.expect("tasks/cancel must respond");
assert_eq!(
cancel.result,
Some(serde_json::json!({ "resultType": "complete" }))
);
assert!(matches!(
runtime
.get_task(&task_id)
.expect("final cancellation must remain in application storage")
.task,
fastmcp_protocol::Task::Cancelled(_)
));
assert!(
!runtime
.is_cancellation_requested(&task_id)
.expect("terminal task has no outstanding cooperative intent")
);
}
#[cfg(feature = "tasks")]
#[test]
fn default_built_server_serves_official_tasks_get() {
let server = Server::new("default-tasks-server", "1.0.0").build();
let runtime = server
.final_task_runtime()
.expect("default build must install an in-memory official Tasks runtime");
let _service = start_final_tasks_test_service(runtime);
let task_id = runtime
.create_task_with_work(
final_tasks_test_work_descriptor(),
Some("accepted".to_owned()),
)
.expect("default store must accept a task once its service is ready")
.task
.base()
.task_id
.clone();
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let session_get = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
fastmcp_protocol::TASK_GET,
Some(final_tasks_get_params(&task_id, serde_json::json!({}))),
80_i64,
),
¬ification_sender,
&test_request_sender(),
))
.expect("default session tasks/get must produce a JSON-RPC response");
assert_eq!(
session_get.result.as_ref().map(|result| &result["taskId"]),
Some(&serde_json::json!(task_id.as_str())),
"Server::new().build() must serve official tasks/get without final_tasks()"
);
let missing = fastmcp_protocol::FinalTaskId::parse("missing-default-task-0001")
.expect("planted missing task id");
let session_missing = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
fastmcp_protocol::TASK_GET,
Some(final_tasks_get_params(&missing, serde_json::json!({}))),
81_i64,
),
¬ification_sender,
&test_request_sender(),
))
.expect("unknown default-runtime task id must still be a Tasks response");
let missing_error = session_missing
.error
.as_ref()
.expect("unknown task id must fail");
assert_ne!(
missing_error.code,
i32::from(McpErrorCode::MethodNotFound).into(),
"unknown id is not MethodNotFound once default official Tasks is installed"
);
}
#[cfg(feature = "tasks")]
#[test]
fn default_built_server_advertises_official_tasks_in_discovery() {
let discovery = Server::new("default-tasks-discovery", "1.0.0")
.build()
.server_discovery()
.expect("default server must produce discovery");
let capabilities = serde_json::to_value(discovery.capabilities())
.expect("discovery capabilities serialize");
assert!(
capabilities
.pointer("/extensions/io.modelcontextprotocol~1tasks")
.is_some(),
"default build must advertise official Tasks so clients can negotiate it"
);
}
// Exact-2024 era: exercises the exact-legacy completion route.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn session_dispatch_serves_legacy_completion_complete() {
let server = Server::new("session-completion", "1.0.0")
.completion_handler(LiveLegacyCompletionHandler)
.build();
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"completion/complete",
Some(serde_json::json!({
"ref": {"type": "ref/prompt", "name": "deploy"},
"argument": {"name": "environment", "value": "sta"},
})),
90_i64,
),
¬ification_sender,
&test_request_sender(),
))
.expect("session completion/complete must produce a JSON-RPC response");
let values = response
.result
.as_ref()
.and_then(|result| result.pointer("/completion/values"))
.cloned()
.expect("legacy completion must return values");
assert_eq!(values, serde_json::json!(["legacy:sta"]));
let missing_handler = Server::new("session-completion-missing", "1.0.0").build();
let mut missing_session = initialized_test_session(&missing_handler);
let missing = block_on(missing_handler.dispatch_request(
&Cx::for_testing(),
&mut missing_session,
JsonRpcRequest::new(
"completion/complete",
Some(serde_json::json!({
"ref": {"type": "ref/prompt", "name": "deploy"},
"argument": {"name": "environment", "value": "sta"},
})),
91_i64,
),
¬ification_sender,
&test_request_sender(),
))
.expect("missing completion handler must still produce a JSON-RPC response");
let error = missing
.error
.as_ref()
.expect("no completion handler is MethodNotFound");
assert_eq!(error.code, i32::from(McpErrorCode::MethodNotFound).into());
}
#[test]
fn completion_handler_is_advertised_on_initialize() {
let with_handler = Server::new("advertise-completion", "1.0.0")
.completion_handler(LiveLegacyCompletionHandler)
.build();
assert!(with_handler.capabilities().completions.is_some());
let mut session = Session::new(
with_handler.info.clone(),
with_handler.capabilities().clone(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let response = block_on(with_handler.dispatch_request(
&Cx::for_testing(),
&mut session,
initialize_test_request(1, "client", ClientCapabilities::default()),
¬ification_sender,
&test_request_sender(),
))
.expect("initialize must respond");
assert_eq!(
response
.result
.as_ref()
.and_then(|result| result.pointer("/capabilities/completions")),
Some(&serde_json::json!({})),
"initialize must advertise completions when a handler is installed"
);
let without = Server::new("no-completion", "1.0.0").build();
assert!(without.capabilities().completions.is_none());
}
// Exact-2024 era: legacy initialize completion capability.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_completion_handler_is_advertised_on_initialize_only() {
let server = Server::new("legacy-advertise-completion", "1.0.0")
.legacy_completion_handler(LiveLegacyCompletionHandler)
.build();
assert!(server.capabilities().completions.is_some());
let mut session = Session::new(server.info.clone(), server.capabilities().clone());
let notification_sender: NotificationSender = Arc::new(|_| {});
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
initialize_test_request(1, "client", ClientCapabilities::default()),
¬ification_sender,
&test_request_sender(),
))
.expect("initialize must respond");
assert_eq!(
response
.result
.as_ref()
.and_then(|result| result.pointer("/capabilities/completions")),
Some(&serde_json::json!({})),
"initialize must advertise completions when only the 2024 handler is installed"
);
let discovery = server
.server_discovery()
.expect("legacy-only completion server must still produce discovery");
let capabilities = serde_json::to_value(discovery.capabilities())
.expect("discovery capabilities serialize");
assert!(
capabilities.pointer("/completions").is_none(),
"legacy-only completion must not appear on final server/discover"
);
let mut session = initialized_test_session(&server);
let served = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"completion/complete",
Some(serde_json::json!({
"ref": {"type": "ref/prompt", "name": "deploy"},
"argument": {"name": "environment", "value": "sta"},
})),
92_i64,
),
¬ification_sender,
&test_request_sender(),
))
.expect("session completion/complete must produce a JSON-RPC response");
let values = served
.result
.as_ref()
.and_then(|result| result.pointer("/completion/values"))
.cloned()
.expect("legacy-only completion must return values");
assert_eq!(values, serde_json::json!(["legacy:sta"]));
}
#[test]
fn registered_resource_advertises_subscribe_on_initialize() {
struct SubscribeResource;
impl ResourceHandler for SubscribeResource {
fn definition(&self) -> Resource {
Resource {
uri: "file:///subscribe.txt".to_string(),
name: "subscribe".to_string(),
description: None,
mime_type: None,
icon: None,
version: None,
tags: vec![],
}
}
fn read(&self, _ctx: &McpContext) -> McpResult<Vec<fastmcp_protocol::ResourceContent>> {
Ok(vec![fastmcp_protocol::ResourceContent {
uri: "file:///subscribe.txt".to_string(),
mime_type: None,
text: Some("ok".to_string()),
blob: None,
}])
}
}
let server = Server::new("advertise-subscribe", "1.0.0")
.resource(SubscribeResource)
.build();
assert!(
server
.capabilities()
.resources
.as_ref()
.is_some_and(|resources| resources.subscribe)
);
let mut session = Session::new(server.info.clone(), server.capabilities().clone());
let notification_sender: NotificationSender = Arc::new(|_| {});
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
initialize_test_request(1, "client", ClientCapabilities::default()),
¬ification_sender,
&test_request_sender(),
))
.expect("initialize must respond");
assert_eq!(
response
.result
.as_ref()
.and_then(|result| result.pointer("/capabilities/resources/subscribe")),
Some(&serde_json::json!(true)),
"initialize must advertise resources.subscribe once a resource is registered"
);
let mut session = initialized_test_session(&server);
let subscribed = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"resources/subscribe",
Some(serde_json::json!({ "uri": "file:///subscribe.txt" })),
93_i64,
),
¬ification_sender,
&test_request_sender(),
))
.expect("session resources/subscribe must produce a JSON-RPC response");
assert!(
subscribed.error.is_none(),
"registered resource must be subscribable without resource_subscriptions(): {subscribed:?}"
);
}
#[cfg(feature = "tasks")]
#[test]
fn public_tasks_update_requires_an_id_before_the_extension_handler_can_mutate_state() {
let delivered = Arc::new(Mutex::new(Vec::new()));
let (server, _service) = final_tasks_test_server(Arc::clone(&delivered));
let runtime = server
.final_task_runtime()
.expect("configured final Tasks runtime must be retained");
let task_id = runtime
.create_task_with_work(
final_tasks_test_work_descriptor(),
Some("accepted".to_owned()),
)
.expect("application store must accept task before the update")
.task
.base()
.task_id
.clone();
let mut requests = fastmcp_protocol::TaskInputRequests::new();
requests.insert(
"roots".to_owned(),
serde_json::from_value(serde_json::json!({ "method": "roots/list" }))
.expect("typed roots request"),
);
runtime
.require_input(&task_id, requests, Some("awaiting roots".to_owned()))
.expect("caller-owned supervisor can enter input_required");
let mut parameters = final_tasks_params(&task_id, serde_json::json!({}));
parameters
.as_object_mut()
.expect("final Tasks parameters are an object")
.insert(
"inputResponses".to_owned(),
serde_json::json!({ "roots": { "roots": [] } }),
);
let task_before = serde_json::to_vec(
&runtime
.get_task(&task_id)
.expect("serialize the complete task before rejected update"),
)
.expect("complete task serializes before rejected update");
let notifications_before = serde_json::to_vec(
&*delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
)
.expect("notification sequence serializes before rejected update");
let notification = JsonRpcRequest::notification(
fastmcp_protocol::tasks_extension::TASK_UPDATE,
Some(parameters.clone()),
);
let no_response = block_on(server.dispatch_stateless(
&InboundRequestContext::new(Cx::for_testing(), 0, InboundRequestTransport::Memory),
¬ification,
));
assert!(
no_response.is_none(),
"an id-less extension request cannot produce a JSON-RPC response"
);
let task_after = serde_json::to_vec(
&runtime
.get_task(&task_id)
.expect("rejected update must leave the durable task readable"),
)
.expect("complete task serializes after rejected update");
let notifications_after = serde_json::to_vec(
&*delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
)
.expect("notification sequence serializes after rejected update");
assert_eq!(
task_after, task_before,
"the id-less rejected update cannot alter any serialized task field"
);
assert_eq!(
notifications_after, notifications_before,
"the id-less rejected update cannot append, replace, or alter notifications"
);
let update = block_on(server.dispatch_stateless(
&InboundRequestContext::new(Cx::for_testing(), 72, InboundRequestTransport::Memory),
&JsonRpcRequest::new(
fastmcp_protocol::tasks_extension::TASK_UPDATE,
Some(parameters),
72_i64,
),
))
.expect("adding only a request id admits the same Tasks update");
assert_eq!(
update.result,
Some(serde_json::json!({ "resultType": "complete" }))
);
assert!(matches!(
runtime
.get_task(&task_id)
.expect("admitted update retains its durable result")
.task,
fastmcp_protocol::Task::Working(_)
));
}
#[cfg(feature = "tasks")]
#[test]
fn final_tool_task_result_mutates_only_after_capability_admission_and_legacy_stays_exact() {
let delivered = Arc::new(Mutex::new(Vec::new()));
let delivered_for_emitter = Arc::clone(&delivered);
let runtime = FinalTaskRuntime::in_memory(
FinalTaskRuntimeConfig::new(60_000, Some(5_000))
.expect("valid final Tasks timing policy"),
Arc::new(move |notification| {
delivered_for_emitter
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
}),
);
let _service = start_final_tasks_test_service(&runtime);
let server = Server::new("final-tool-task-server", "1.0.0")
.tool(FinalTaskCreatingTool)
.mask_error_details(true)
.final_tasks(runtime.clone())
.expect("final Tasks must install before task-capable tool dispatch")
.build();
let inbound =
InboundRequestContext::new(Cx::for_testing(), 81, InboundRequestTransport::Memory);
let admitted =
block_on(server.dispatch_stateless(&inbound, &final_task_creating_tool_request(true)))
.expect("declared final Tasks tool request must respond");
assert!(admitted.error.is_none());
assert_eq!(
admitted.result.as_ref().map(|result| &result["resultType"]),
Some(&serde_json::json!("task"))
);
assert_eq!(
admitted.result.as_ref().map(|result| &result["status"]),
Some(&serde_json::json!("working"))
);
let task_id = admitted
.result
.as_ref()
.and_then(|result| result["taskId"].as_str())
.and_then(|task_id| fastmcp_protocol::FinalTaskId::parse(task_id).ok())
.expect("the task result must expose its durable task ID");
assert_eq!(
runtime
.get_task(&task_id)
.expect("the application task store must remain readable")
.task
.base()
.task_id,
task_id,
"the admitted result must name a task already durable in the application store"
);
assert_eq!(
delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"the admitted request publishes exactly one durable task transition"
);
let rejected =
block_on(server.dispatch_stateless(&inbound, &final_task_creating_tool_request(false)))
.expect("missing-capability final tool request must respond");
let error = rejected
.error
.expect("removing only the Tasks declaration must reject task creation");
assert_eq!(
error.code,
MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE.into()
);
assert_eq!(
error.data,
Some(serde_json::json!({
"requiredCapabilities": {
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
}))
);
assert_eq!(
delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"rejected task capability must leave durable state unchanged"
);
let mut legacy_session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let legacy = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut legacy_session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "durable_final_task",
"arguments": {},
})),
81_i64,
),
¬ification_sender,
&test_request_sender(),
))
.expect("exact legacy tool call must respond through its original handler surface");
assert!(legacy.error.is_none());
assert_eq!(
legacy
.result
.as_ref()
.and_then(|result| result.pointer("/content/0/text")),
Some(&serde_json::json!("exact legacy completion"))
);
assert_eq!(
delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"exact MCP 2024-11-05 must never enter the final task producer"
);
}
#[cfg(feature = "tasks")]
#[test]
fn final_tasks_compose_with_an_existing_extension_registry_before_freeze() {
let delivered = Arc::new(Mutex::new(Vec::new()));
let (handlers, discovery) = unrelated_extension_registry("example/echo");
let runtime = final_tasks_test_runtime(Arc::clone(&delivered));
let _service = start_final_tasks_test_service(&runtime);
let server = Server::new("composed-final-tasks-server", "1.0.0")
.extension_registry(
handlers,
discovery,
|_descriptor: &fastmcp_protocol::ExtensionDescriptor,
client: &fastmcp_protocol::ExtensionSettings,
_server: &fastmcp_protocol::ExtensionSettings|
-> Result<fastmcp_protocol::ExtensionSettings, ExtensionNegotiationError> {
Ok(client.clone())
},
)
.expect("unrelated extension registry must install")
.final_tasks(runtime)
.expect("final Tasks must merge into the existing registry")
.build();
assert_eq!(
server
.extension_registry_receipt()
.expect("composed registry must be frozen")
.descriptor_count(),
2,
"the caller extension and official Tasks share one receipt"
);
let discovery = serde_json::to_value(
server
.server_discovery()
.expect("composed discovery must be valid"),
)
.expect("composed discovery must serialize");
assert_eq!(
discovery.pointer("/capabilities/extensions/com.example~1echo"),
Some(&serde_json::json!({}))
);
assert_eq!(
discovery.pointer("/capabilities/extensions/io.modelcontextprotocol~1tasks"),
Some(&serde_json::json!({}))
);
let runtime = server
.final_task_runtime()
.expect("composed final Tasks runtime must be retained");
let task_id = runtime
.create_task_with_work(final_tasks_test_work_descriptor(), None)
.expect("application-owned task store must accept task")
.task
.base()
.task_id
.clone();
// The caller extension registered with RejectOneSided, so the client
// must declare it alongside the official Tasks extension or the whole
// per-request negotiation (correctly) rejects.
let mut get_params = final_tasks_get_params(&task_id, serde_json::json!({}));
get_params
.pointer_mut("/_meta/io.modelcontextprotocol~1clientCapabilities/extensions")
.and_then(serde_json::Value::as_object_mut)
.expect("baseline request declares client extensions")
.insert("com.example/echo".to_owned(), serde_json::json!({}));
let response = block_on(server.dispatch_stateless(
&InboundRequestContext::new(Cx::for_testing(), 71, InboundRequestTransport::Memory),
&JsonRpcRequest::new(fastmcp_protocol::TASK_GET, Some(get_params), 71_i64),
))
.expect("merged final Tasks handler must route requests");
assert!(response.error.is_none());
}
#[cfg(feature = "tasks")]
#[test]
fn final_tasks_rejects_only_a_true_owned_method_collision() {
let delivered = Arc::new(Mutex::new(Vec::new()));
let (handlers, discovery) = unrelated_extension_registry(fastmcp_protocol::TASK_GET);
let result = Server::new("conflicting-final-tasks-server", "1.0.0")
.extension_registry(
handlers,
discovery,
|_descriptor: &fastmcp_protocol::ExtensionDescriptor,
client: &fastmcp_protocol::ExtensionSettings,
_server: &fastmcp_protocol::ExtensionSettings|
-> Result<fastmcp_protocol::ExtensionSettings, ExtensionNegotiationError> {
Ok(client.clone())
},
)
.expect("unrelated registry must install before the conflicting method is merged")
.final_tasks(final_tasks_test_runtime(delivered));
let error = match result {
Ok(_) => panic!("only the duplicated tasks/get ownership must be rejected"),
Err(error) => error,
};
assert!(matches!(
error,
ServerExtensionConfigurationError::Registry(
fastmcp_protocol::ExtensionRegistryError::OwnershipCollision {
field: "method",
value
}
) if value == fastmcp_protocol::TASK_GET
));
}
#[cfg(feature = "tasks")]
#[test]
fn final_tasks_reject_one_variable_incompatible_extension_settings() {
let delivered = Arc::new(Mutex::new(Vec::new()));
let (server, _service) = final_tasks_test_server(Arc::clone(&delivered));
let runtime = server
.final_task_runtime()
.expect("configured final Tasks runtime must be retained");
let task_id = runtime
.create_task_with_work(final_tasks_test_work_descriptor(), None)
.expect("task must persist")
.task
.base()
.task_id
.clone();
let inbound =
InboundRequestContext::new(Cx::for_testing(), 71, InboundRequestTransport::Memory);
let admitted = block_on(server.dispatch_stateless(
&inbound,
&JsonRpcRequest::new(
fastmcp_protocol::TASK_GET,
Some(final_tasks_get_params(&task_id, serde_json::json!({}))),
71_i64,
),
))
.expect("baseline final Tasks request must respond");
assert!(admitted.error.is_none());
let notification_count = delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len();
let mut rejected_params = final_tasks_get_params(&task_id, serde_json::json!({}));
*rejected_params
.pointer_mut(
"/_meta/io.modelcontextprotocol~1clientCapabilities/extensions/io.modelcontextprotocol~1tasks",
)
.expect("baseline request must contain Tasks settings") =
serde_json::json!({ "unexpected": true });
let rejected = block_on(server.dispatch_stateless(
&inbound,
&JsonRpcRequest::new(fastmcp_protocol::TASK_GET, Some(rejected_params), 71_i64),
))
.expect("rejected final Tasks request must respond");
let error = rejected
.error
.expect("changing only Tasks settings must reject the request");
assert_eq!(error.code, i32::from(McpErrorCode::InvalidParams).into());
assert_eq!(
delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
notification_count,
"rejected settings must not invoke the final Tasks runtime"
);
}
#[cfg(feature = "tasks")]
#[test]
fn legacy_dispatch_remains_isolated_from_configured_final_tasks() {
let (server, _service) = final_tasks_test_server(Arc::new(Mutex::new(Vec::new())));
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
fastmcp_protocol::TASK_GET,
Some(serde_json::json!({ "taskId": "legacy-task" })),
71_i64,
),
¬ification_sender,
&request_sender,
))
.expect("legacy request must respond");
assert_eq!(
response.error.map(|error| error.code),
Some(i32::from(McpErrorCode::MethodNotFound).into())
);
}
#[test]
fn http_server_uses_its_configured_request_handler_policy() {
let handler_config = HttpHandlerConfig {
base_path: "/private/mcp".to_string(),
allow_cors: false,
cors_origins: vec!["https://trusted.example".to_string()],
max_body_size: 321,
};
let server = Server::new("http-policy-test", "1.0.0")
.http_config(
HttpServerConfig::new()
.mcp_path("/private/mcp")
.handler_config(handler_config),
)
.build();
let handler = server.configured_http_request_handler();
let actual = handler.config();
assert_eq!(actual.base_path, "/private/mcp");
assert!(!actual.allow_cors);
assert_eq!(
actual.cors_origins,
vec!["https://trusted.example".to_string()]
);
assert_eq!(actual.max_body_size, 321);
let options = HttpRequest::new(HttpMethod::Options, "/private/mcp")
.with_header("origin", "https://trusted.example");
assert_eq!(
handler.handle_options(&options).status,
HttpStatus::METHOD_NOT_ALLOWED
);
let oversized = HttpRequest::new(HttpMethod::Post, "/private/mcp")
.with_header("content-type", "application/json")
.with_body(vec![b'x'; 322]);
assert!(matches!(
handler.parse_request(&oversized),
Err(fastmcp_transport::http::HttpError::BodyTooLarge {
size: 322,
max: 321
})
));
}
#[test]
fn http_server_mcp_path_updates_the_authoritative_handler_path() {
let config = HttpServerConfig::new().mcp_path("/single-source");
assert_eq!(config.handler_config.base_path, "/single-source");
let server = Server::new("http-path-test", "1.0.0")
.http_config(config)
.build();
let handler = server.configured_http_request_handler();
assert_eq!(handler.config().base_path, "/single-source");
}
#[tool(
name = "http_overlap_tool",
description = "Records concurrent overlap for HTTP tests",
annotations(read_only)
)]
fn http_overlap_tool(_ctx: &McpContext) -> String {
let metrics = http_overlap_metrics();
let current = metrics.current.fetch_add(1, Ordering::SeqCst) + 1;
metrics.max.fetch_max(current, Ordering::SeqCst);
http_overlap_control().enter_and_wait();
metrics.current.fetch_sub(1, Ordering::SeqCst);
"overlap-ok".to_string()
}
#[tool(
name = "http_auth_echo_tool_runtime",
description = "Returns the request-scoped auth subject while recording overlap",
annotations(read_only)
)]
fn http_auth_echo_tool_runtime(ctx: &McpContext) -> String {
ctx.auth()
.and_then(|auth| auth.subject)
.unwrap_or_else(|| "anonymous".to_string())
}
#[tool(
name = "http_stateful_increment_tool",
description = "Increments a session counter across HTTP requests"
)]
fn http_stateful_increment_tool(ctx: &McpContext) -> String {
let count: i32 = ctx.get_state("http_counter").unwrap_or(0);
let next = count + 1;
assert!(ctx.set_state("http_counter", next));
format!("Counter: {next}")
}
#[tool(
name = "http_current_auth_subject_tool",
description = "Returns the current request auth subject",
annotations(read_only)
)]
fn http_current_auth_subject_tool(ctx: &McpContext) -> String {
ctx.auth()
.and_then(|auth| auth.subject)
.unwrap_or_else(|| "anonymous".to_string())
}
#[tool(
name = "http_current_auth_subject_exclusive_tool",
description = "Returns the current request auth subject from the exclusive path"
)]
fn http_current_auth_subject_exclusive_tool(ctx: &McpContext) -> String {
ctx.auth()
.and_then(|auth| auth.subject)
.unwrap_or_else(|| "anonymous".to_string())
}
#[derive(Debug, Clone)]
struct CapturingAuthMiddleware {
seen: Arc<Mutex<Vec<(String, Option<String>)>>>,
}
impl Middleware for CapturingAuthMiddleware {
fn on_request(
&self,
ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
self.seen
.lock()
.expect("captured auth middleware mutex should not be poisoned")
.push((
request.method.clone(),
ctx.auth().and_then(|auth| auth.subject),
));
Ok(MiddlewareDecision::Continue)
}
}
#[derive(Debug, Clone)]
struct OverridingAuthMiddleware {
subject: &'static str,
}
impl Middleware for OverridingAuthMiddleware {
fn on_request(
&self,
ctx: &McpContext,
_request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
if ctx.set_auth(AuthContext::with_subject(self.subject)) {
return Err(McpError::internal_error(
"middleware replaced a committed authenticated principal",
));
}
Ok(MiddlewareDecision::Continue)
}
}
#[derive(Debug)]
struct AlwaysFailAuthProvider;
impl AuthProvider for AlwaysFailAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
_request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
Err(McpError::invalid_request("auth failed"))
}
}
#[derive(Debug, Clone)]
struct RewritingErrorMiddleware;
impl Middleware for RewritingErrorMiddleware {
fn on_error(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
error: McpError,
) -> McpError {
McpError::new(error.code, format!("rewritten: {}", error.message))
}
}
#[derive(Debug, Clone)]
struct ResponseCancellationRewriter;
impl Middleware for ResponseCancellationRewriter {
fn on_response(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
_response: serde_json::Value,
) -> McpResult<serde_json::Value> {
Err(McpError::request_cancelled())
}
fn on_error(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
_error: McpError,
) -> McpError {
McpError::internal_error("hostile cancellation rewrite")
}
}
#[derive(Debug, Clone)]
struct RejectResponseMiddleware;
impl Middleware for RejectResponseMiddleware {
fn on_response(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
_response: serde_json::Value,
) -> McpResult<serde_json::Value> {
Err(McpError::internal_error(
"response middleware rejected provisional session mutation",
))
}
}
const EXTENSION_PANIC_CANARY: &str =
"EXTENSION_PANIC_CANARY Bearer peer-secret\n\u{001b}[31mpayload\u{001b}[0m";
#[derive(Debug)]
struct PanickingAuthProvider;
impl AuthProvider for PanickingAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
_request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
panic!("{EXTENSION_PANIC_CANARY}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PanickingMiddlewareHook {
Request,
Response,
Error,
}
type MiddlewareEvent = (&'static str, &'static str);
#[derive(Debug, Clone)]
struct RecordingPanicMiddleware {
name: &'static str,
panic_at: Option<PanickingMiddlewareHook>,
events: Arc<Mutex<Vec<MiddlewareEvent>>>,
}
impl RecordingPanicMiddleware {
fn new(
name: &'static str,
panic_at: Option<PanickingMiddlewareHook>,
events: &Arc<Mutex<Vec<MiddlewareEvent>>>,
) -> Self {
Self {
name,
panic_at,
events: Arc::clone(events),
}
}
fn record(&self, hook: &'static str) {
self.events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push((self.name, hook));
}
fn should_panic(&self, hook: PanickingMiddlewareHook) -> bool {
self.panic_at == Some(hook)
}
}
impl Middleware for RecordingPanicMiddleware {
fn on_request(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
self.record("request");
if self.should_panic(PanickingMiddlewareHook::Request) {
panic!("{EXTENSION_PANIC_CANARY}");
}
Ok(MiddlewareDecision::Continue)
}
fn on_response(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
response: serde_json::Value,
) -> McpResult<serde_json::Value> {
self.record("response");
if self.should_panic(PanickingMiddlewareHook::Response) {
panic!("{EXTENSION_PANIC_CANARY}");
}
Ok(response)
}
fn on_error(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
error: McpError,
) -> McpError {
self.record("error");
if self.should_panic(PanickingMiddlewareHook::Error) {
panic!("{EXTENSION_PANIC_CANARY}");
}
error
}
}
fn recorded_middleware_events(
events: &Arc<Mutex<Vec<MiddlewareEvent>>>,
) -> Vec<MiddlewareEvent> {
events
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
fn assert_peer_extension_error_is_sanitized(response: &JsonRpcResponse) {
let error = response
.error
.as_ref()
.expect("extension panic must produce a JSON-RPC error");
assert_eq!(error.message, "Internal server error");
assert_eq!(error.data, None);
let wire = serde_json::to_string(response).expect("JSON-RPC response should serialize");
for forbidden in [
"EXTENSION_PANIC_CANARY",
"Bearer",
"peer-secret",
"payload",
"\\u001b",
] {
assert!(
!wire.contains(forbidden),
"peer response exposed panic payload fragment {forbidden:?}: {wire}"
);
}
}
// ── parse_params ────────────────────────────────────────────────
#[cfg(feature = "tasks")]
#[test]
fn configured_task_manager_does_not_advertise_or_enable_task_rpc_methods() {
let server = Server::new("quarantined-task-rpc-test", "1.0.0")
.with_task_manager(TaskManager::new().into_shared())
.build();
assert!(server.task_manager().is_some());
assert!(server.capabilities().tasks.is_none());
let mut session = initialized_test_session(&server);
for method in [
"tasks/list",
"tasks/get",
"tasks/update",
"tasks/cancel",
"tasks/submit",
] {
let response = dispatch_test_request(&server, &mut session, method);
let error = response
.error
.as_ref()
.unwrap_or_else(|| panic!("{method} must fail closed"));
assert_eq!(error.code, i32::from(McpErrorCode::MethodNotFound).into());
assert!(response.result.is_none());
}
}
#[test]
fn task_rpc_quarantine_matches_exact_legacy_task_method_names() {
assert!(is_quarantined_task_rpc("tasks/list"));
assert!(is_quarantined_task_rpc("tasks/submit"));
assert!(
!is_quarantined_task_rpc("tasks/get"),
"official final Tasks methods are not quarantined"
);
assert!(
!is_quarantined_task_rpc("tasks/lists"),
"changing only the owned method name must not quarantine an unrelated extension"
);
}
#[test]
fn notification_only_methods_reject_ids_and_bare_initialized_is_unknown() {
let server = Server::new("notification-envelope-test", "1.0.0").build();
let mut session = initialized_test_session(&server);
for method in ["notifications/initialized", "notifications/cancelled"] {
let response = dispatch_test_request(&server, &mut session, method);
let error = response
.error
.unwrap_or_else(|| panic!("{method} with an id must be rejected"));
assert_eq!(error.code, i32::from(McpErrorCode::InvalidRequest).into());
assert!(response.result.is_none());
}
let response = dispatch_test_request(&server, &mut session, "initialized");
let error = response
.error
.expect("the legacy bare initialized spelling must not be routed");
assert_eq!(error.code, i32::from(McpErrorCode::MethodNotFound).into());
}
#[test]
fn log_level_is_session_local_and_does_not_mutate_process_filter() {
let server = Server::new("session-log-level-test", "1.0.0")
.log_level(Level::Debug)
.build();
let mut first = initialized_test_session(&server);
let mut second = initialized_test_session(&server);
let process_level = log::max_level();
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
for (session, id, level) in [
(&mut first, 1_i64, LogLevel::Debug),
(&mut second, 2_i64, LogLevel::Error),
] {
let request = JsonRpcRequest::new(
"logging/setLevel",
Some(
serde_json::to_value(SetLogLevelParams { level })
.expect("serialize log-level request"),
),
id,
);
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
session,
request,
¬ification_sender,
&request_sender,
))
.expect("setLevel request must have a response");
assert!(
response.error.is_none(),
"unexpected response: {response:?}"
);
}
assert_eq!(first.log_level(), Some(LogLevel::Debug));
assert_eq!(second.log_level(), Some(LogLevel::Error));
assert_eq!(log::max_level(), process_level);
}
#[test]
fn disabled_server_logging_does_not_enable_session_notifications() {
let server = Server::new("disabled-session-log-level-test", "1.0.0")
.log_level_filter(LevelFilter::Off)
.build();
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let request = JsonRpcRequest::new(
"logging/setLevel",
Some(
serde_json::to_value(SetLogLevelParams {
level: LogLevel::Debug,
})
.expect("serialize log-level request"),
),
3_i64,
);
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
request,
¬ification_sender,
&request_sender,
))
.expect("setLevel request must have a response");
assert!(
response.error.is_none(),
"unexpected response: {response:?}"
);
assert_eq!(session.log_level(), None);
}
#[test]
fn panicking_log_sender_cannot_discard_response_or_leak_active_request() {
let server = Server::new("log-sender-panic-test", "1.0.0")
.log_level(Level::Debug)
.build();
let mut session = initialized_test_session(&server);
session.set_log_level(LogLevel::Debug);
let sender: NotificationSender =
Arc::new(|_| panic!("LOG-SENDER-PANIC-CANARY Bearer secret\r\nforged-line"));
let request_sender = test_request_sender();
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new("ping", Some(serde_json::json!({})), 77_i64),
&sender,
&request_sender,
))
.expect("ping must retain its response despite sender panic");
assert!(
response.error.is_none(),
"unexpected response: {response:?}"
);
assert!(
server
.active_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
);
let failing_server = Server::new("auth-failure-log-sender-panic-test", "1.0.0")
.log_level(Level::Debug)
.auth_provider(AlwaysFailAuthProvider)
.build();
let mut failing_session = initialized_test_session(&failing_server);
failing_session.set_log_level(LogLevel::Debug);
let response = block_on(failing_server.dispatch_request(
&Cx::for_testing(),
&mut failing_session,
JsonRpcRequest::new("tools/list", Some(serde_json::json!({})), 78_i64),
&sender,
&request_sender,
))
.expect("auth failure must retain its response despite sender panic");
assert!(response.error.is_some());
assert!(
failing_server
.active_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
);
}
#[test]
fn credential_is_visible_only_to_auth_then_stripped_before_middleware() {
#[derive(Debug, Clone)]
struct CaptureRawAuth {
seen: Arc<Mutex<Option<serde_json::Value>>>,
}
impl AuthProvider for CaptureRawAuth {
fn authenticate(
&self,
_ctx: &McpContext,
request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
assert!(request.access_token().is_some());
*self
.seen
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = request.params.cloned();
Ok(AuthContext::with_subject("fixed-test-principal"))
}
}
#[derive(Debug, Clone)]
struct CaptureSanitizedParams {
seen: Arc<Mutex<Option<serde_json::Value>>>,
}
impl Middleware for CaptureSanitizedParams {
fn on_request(
&self,
_ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
*self
.seen
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = request.params.clone();
Ok(MiddlewareDecision::Continue)
}
}
let raw = Arc::new(Mutex::new(None));
let sanitized = Arc::new(Mutex::new(None));
let server = Server::new("credential-custody-test", "1.0.0")
.auth_provider(CaptureRawAuth {
seen: Arc::clone(&raw),
})
.middleware(CaptureSanitizedParams {
seen: Arc::clone(&sanitized),
})
.build();
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
// The fail-closed credential grammar rejects a request carrying more
// than one recognized credential source, so each recognized location
// proves its visible-to-auth-then-stripped custody in its own
// single-source dispatch.
let cases = [
(
91_i64,
serde_json::json!({
"authorization": "Bearer top-level-secret",
"arguments": {
"token": "domain-value",
"nested": {"authorization": "domain-authorization"}
}
}),
),
(
92_i64,
serde_json::json!({
"_meta": {
"accessToken": "Bearer metadata-secret",
"trace": "preserved-metadata"
}
}),
),
(
93_i64,
serde_json::json!({
"headers": {
"Authorization": "Bearer header-secret",
"x-preserved": "yes"
}
}),
),
];
for (id, params) in cases {
let request = JsonRpcRequest::new("tools/list", Some(params.clone()), id);
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
request,
¬ification_sender,
&request_sender,
))
.expect("authenticated tools/list must respond");
assert!(
response.error.is_none(),
"unexpected response for id {id}: {response:?}"
);
let raw = raw
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
.expect("auth provider must receive raw params");
assert_eq!(raw, params, "auth must see the unstripped params");
let sanitized = sanitized
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
.expect("middleware must receive sanitized params");
match id {
91 => {
assert!(sanitized.get("authorization").is_none());
assert_eq!(sanitized["arguments"]["token"], "domain-value");
assert_eq!(
sanitized["arguments"]["nested"]["authorization"],
"domain-authorization"
);
}
92 => {
assert!(sanitized["_meta"].get("accessToken").is_none());
assert_eq!(sanitized["_meta"]["trace"], "preserved-metadata");
}
_ => {
assert!(sanitized["headers"].get("Authorization").is_none());
assert_eq!(sanitized["headers"]["x-preserved"], "yes");
}
}
}
}
// Exact-2024 era: exact-2024 dispatch.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn exact_2024_dispatch_without_transport_custody_still_evaluates_auth_provider() {
let verifier = StaticTokenVerifier::new([(
"good-token",
AuthContext::with_subject("legacy-stdio-owner"),
)])
.expect("valid verifier configuration")
.with_allowed_schemes(["Bearer"])
.expect("valid scheme configuration");
let server = Server::new("legacy-stdio-auth", "1.0.0")
.auth_provider(TokenAuthProvider::new(verifier))
.build();
let session_principal = crate::session::SessionPrincipalBinding::default();
let missing = JsonRpcRequest::new("tools/list", None, 1_i64);
let missing_dispatch = fastmcp_core::block_on(server.dispatch_legacy_2024(
&Cx::for_testing(),
1,
&session_principal,
None,
None,
&missing,
None,
None,
))
.expect("missing-token admission still returns a dispatch record");
let missing_error = missing_dispatch
.result
.expect_err("exact-2024 tools/list without a token must fail closed");
assert_eq!(missing_error.code, McpErrorCode::ResourceForbidden);
let wrong = JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({"authorization": "Bearer wrong-token"})),
2_i64,
);
let wrong_dispatch = fastmcp_core::block_on(server.dispatch_legacy_2024(
&Cx::for_testing(),
1,
&session_principal,
None,
None,
&wrong,
None,
None,
))
.expect("wrong-token admission still returns a dispatch record");
let wrong_error = wrong_dispatch
.result
.expect_err("exact-2024 tools/list with the wrong token must fail closed");
assert_eq!(wrong_error.code, McpErrorCode::ResourceForbidden);
let admitted = JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({"authorization": "Bearer good-token"})),
3_i64,
);
let dispatch = fastmcp_core::block_on(server.dispatch_legacy_2024(
&Cx::for_testing(),
1,
&session_principal,
None,
None,
&admitted,
None,
None,
))
.expect("exact-2024 tools/list with the admitted token must dispatch");
dispatch
.result
.expect("admitted exact-2024 tools/list must succeed");
}
#[test]
fn auth_provider_covers_initialize_ping_and_control_notifications() {
#[derive(Debug, Clone)]
struct RecordingAuthProvider {
methods: Arc<Mutex<Vec<String>>>,
}
impl AuthProvider for RecordingAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
self.methods
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(request.method.to_string());
Ok(AuthContext::with_subject("connection-owner"))
}
}
let methods = Arc::new(Mutex::new(Vec::new()));
let server = Server::new("auth-all-methods-test", "1.0.0")
.auth_provider(RecordingAuthProvider {
methods: Arc::clone(&methods),
})
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let initialize = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
initialize_test_request(
101,
"authenticated-client",
fastmcp_protocol::ClientCapabilities::default(),
),
¬ification_sender,
&request_sender,
))
.expect("initialize request must respond");
assert!(initialize.error.is_none());
assert!(
block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::notification("notifications/initialized", None),
¬ification_sender,
&request_sender,
))
.is_none()
);
let ping = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new("ping", Some(serde_json::json!({})), 102_i64),
¬ification_sender,
&request_sender,
))
.expect("ping request must respond");
assert!(ping.error.is_none());
assert!(
block_on(
server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::notification(
"notifications/cancelled",
Some(
serde_json::to_value(CancelledParams {
request_id: RequestId::Number(999),
reason: None,
meta: None,
})
.expect("serialize cancellation"),
),
),
¬ification_sender,
&request_sender,
)
)
.is_none()
);
assert_eq!(
*methods
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec![
"initialize",
"notifications/initialized",
"ping",
"notifications/cancelled",
]
);
}
#[test]
fn malformed_or_ambiguous_credentials_never_reach_the_provider() {
#[derive(Debug, Clone)]
struct CountingAuthProvider {
calls: Arc<AtomicUsize>,
}
impl AuthProvider for CountingAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
_request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(AuthContext::with_subject("must-not-run"))
}
}
#[derive(Debug, Clone)]
struct CapturingAdmissionError {
seen: Arc<Mutex<Vec<(Option<serde_json::Value>, McpError)>>>,
}
impl Middleware for CapturingAdmissionError {
fn on_error(
&self,
_ctx: &McpContext,
request: &JsonRpcRequest,
error: McpError,
) -> McpError {
self.seen
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push((request.params.clone(), error.clone()));
error
}
}
let calls = Arc::new(AtomicUsize::new(0));
let seen = Arc::new(Mutex::new(Vec::new()));
let server = Server::new("credential-admission-test", "1.0.0")
.auth_provider(CountingAuthProvider {
calls: Arc::clone(&calls),
})
.middleware(CapturingAdmissionError {
seen: Arc::clone(&seen),
})
.build();
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let cases = [
(
Some("Bearer native"),
serde_json::json!({
"authorization": "Bearer in-band",
"cursor": "preserved"
}),
),
(
None,
serde_json::json!({
"authorization": "Bearer first",
"_meta": {"accessToken": "Bearer second"},
"cursor": "preserved"
}),
),
(Some("Bearer"), serde_json::json!({"cursor": "preserved"})),
];
for (index, (native, params)) in cases.into_iter().enumerate() {
let response = block_on(server.handle_request_internal(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/list",
Some(params),
i64::try_from(index + 1).expect("bounded test request ID"),
),
¬ification_sender,
&request_sender,
None,
native,
))
.map(|handled| handled.finalize_for_return(&mut session))
.expect("request must receive an authentication error");
let error = response.error.expect("authentication must fail");
assert_eq!(
error.code,
i32::from(McpErrorCode::ResourceForbidden).into()
);
assert_eq!(error.message, "Authentication failed");
assert!(error.data.is_none());
}
assert_eq!(calls.load(Ordering::SeqCst), 0);
let seen = seen
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(seen.len(), 3);
for (params, error) in seen.iter() {
assert_eq!(error.message, "Authentication failed");
assert!(error.data.is_none());
let params = params.as_ref().expect("non-credential params remain");
assert_eq!(params["cursor"], "preserved");
let wire = serde_json::to_string(params).expect("serialize sanitized params");
assert!(!wire.contains("Bearer"));
assert!(!wire.contains("first"));
assert!(!wire.contains("second"));
assert!(!wire.contains("native"));
}
}
// Exact-2024 era: legacy cancellation control-notification authentication.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn cancellation_control_cannot_claim_an_unbound_session_principal() {
let server = Server::new("cancel-control-owner-test", "1.0.0").build();
let session = Session::new(server.info.clone(), server.capabilities.clone());
let binding = session.principal_binding();
let mut cancellation = JsonRpcRequest::notification(
"notifications/cancelled",
Some(
serde_json::to_value(CancelledParams {
request_id: RequestId::Number(17),
reason: None,
meta: None,
})
.expect("serialize cancellation"),
),
);
let error = server
.authenticate_cancelled_control_notification(
&Cx::for_testing(),
&binding,
ProtocolEra::Legacy2024,
&mut cancellation,
None,
)
.expect_err("a control frame must not establish session ownership");
assert_eq!(error.code, McpErrorCode::ResourceForbidden);
let admitted_anonymous = auth::principal_fingerprint(None).expect("bounded fingerprint");
assert!(binding.bind_or_verify(admitted_anonymous));
let mut cancellation = JsonRpcRequest::notification(
"notifications/cancelled",
Some(
serde_json::to_value(CancelledParams {
request_id: RequestId::Number(17),
reason: None,
meta: None,
})
.expect("serialize cancellation"),
),
);
assert!(
server
.authenticate_cancelled_control_notification(
&Cx::for_testing(),
&binding,
ProtocolEra::Legacy2024,
&mut cancellation,
None,
)
.is_ok()
);
}
#[test]
fn failed_initialize_auth_does_not_initialize_session() {
let server = Server::new("initialize-auth-failure-test", "1.0.0")
.auth_provider(AlwaysFailAuthProvider)
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
initialize_test_request(
103,
"unauthenticated-client",
fastmcp_protocol::ClientCapabilities::default(),
),
¬ification_sender,
&request_sender,
))
.expect("initialize auth failure must respond");
assert!(response.error.is_some());
assert!(!session.is_initialized());
}
// Extension panic-containment regressions.
#[test]
fn auth_provider_panic_is_counted_sanitized_and_runs_global_reverse_cleanup() {
let events = Arc::new(Mutex::new(Vec::new()));
let server = Server::new("auth-panic-containment-test", "1.0.0")
.auth_provider(PanickingAuthProvider)
.middleware(RecordingPanicMiddleware::new("first", None, &events))
.middleware(RecordingPanicMiddleware::new("second", None, &events))
.middleware(RecordingPanicMiddleware::new("third", None, &events))
.build();
let mut session = initialized_test_session(&server);
let panic_count_before = REDACTED_EXTENSION_PANIC_COUNT.load(Ordering::Relaxed);
let response = dispatch_test_request(&server, &mut session, "tools/list");
let panic_count_after = REDACTED_EXTENSION_PANIC_COUNT.load(Ordering::Relaxed);
assert!(
panic_count_after > panic_count_before,
"the installed redaction hook must count the contained auth-provider panic"
);
assert_peer_extension_error_is_sanitized(&response);
assert_eq!(
recorded_middleware_events(&events),
vec![("third", "error"), ("second", "error"), ("first", "error"),]
);
}
#[test]
fn middleware_on_request_panic_runs_entered_stack_reverse_cleanup() {
let events = Arc::new(Mutex::new(Vec::new()));
let server = Server::new("middleware-request-panic-test", "1.0.0")
.middleware(RecordingPanicMiddleware::new("first", None, &events))
.middleware(RecordingPanicMiddleware::new(
"second",
Some(PanickingMiddlewareHook::Request),
&events,
))
.middleware(RecordingPanicMiddleware::new("third", None, &events))
.build();
let mut session = initialized_test_session(&server);
let response = dispatch_test_request(&server, &mut session, "ping");
assert_peer_extension_error_is_sanitized(&response);
assert_eq!(
recorded_middleware_events(&events),
vec![
("first", "request"),
("second", "request"),
("second", "error"),
("first", "error"),
]
);
}
#[test]
fn middleware_on_response_panic_runs_full_entered_stack_reverse_cleanup() {
let events = Arc::new(Mutex::new(Vec::new()));
let server = Server::new("middleware-response-panic-test", "1.0.0")
.middleware(RecordingPanicMiddleware::new("first", None, &events))
.middleware(RecordingPanicMiddleware::new(
"second",
Some(PanickingMiddlewareHook::Response),
&events,
))
.middleware(RecordingPanicMiddleware::new("third", None, &events))
.build();
let mut session = initialized_test_session(&server);
let response = dispatch_test_request(&server, &mut session, "ping");
assert_peer_extension_error_is_sanitized(&response);
assert_eq!(
recorded_middleware_events(&events),
vec![
("first", "request"),
("second", "request"),
("third", "request"),
("third", "response"),
("second", "response"),
("third", "error"),
("second", "error"),
("first", "error"),
]
);
}
#[test]
fn middleware_response_cancellation_cannot_be_rewritten_by_error_hook() {
let server = Server::new("middleware-response-cancellation-test", "1.0.0")
.middleware(ResponseCancellationRewriter)
.build();
let mut session = initialized_test_session(&server);
let response = dispatch_test_request(&server, &mut session, "ping");
let error = response
.error
.expect("response-stage cancellation must remain an error");
assert_eq!(error.code, i32::from(McpErrorCode::RequestCancelled).into());
assert_eq!(error.message, "Request cancelled");
assert!(!error.message.contains("hostile"));
}
#[test]
fn middleware_on_error_panic_does_not_skip_remaining_reverse_cleanup() {
let events = Arc::new(Mutex::new(Vec::new()));
let server = Server::new("middleware-error-panic-test", "1.0.0")
.middleware(RecordingPanicMiddleware::new("first", None, &events))
.middleware(RecordingPanicMiddleware::new(
"second",
Some(PanickingMiddlewareHook::Error),
&events,
))
.middleware(RecordingPanicMiddleware::new("third", None, &events))
.build();
let mut session = initialized_test_session(&server);
let response = dispatch_test_request(&server, &mut session, "unknown/test-method");
assert_peer_extension_error_is_sanitized(&response);
assert_eq!(
recorded_middleware_events(&events),
vec![
("first", "request"),
("second", "request"),
("third", "request"),
("third", "error"),
("second", "error"),
("first", "error"),
]
);
}
// Parameter parsing regressions.
#[test]
fn parse_params_none_returns_error() {
let result = parse_params::<serde_json::Value>(None);
let err = result.unwrap_err();
assert!(err.message.contains("Missing required parameters"));
}
#[test]
fn parse_params_invalid_json_returns_error() {
// Pass a string where a struct is expected
let result = parse_params::<ListToolsParams>(Some(serde_json::json!("not_an_object")));
assert!(result.is_err());
}
#[test]
fn parse_params_valid_json_succeeds() {
let result = parse_params::<ReadResourceParams>(Some(serde_json::json!({"uri": "x://y"})));
let params = result.unwrap();
assert_eq!(params.uri, "x://y");
}
// ── parse_params_or_default ─────────────────────────────────────
#[test]
fn parse_params_or_default_none_returns_default() {
let result = parse_params_or_default::<ListToolsParams>(None);
let params = result.unwrap();
assert!(params.cursor.is_none());
}
#[test]
fn parse_params_or_default_invalid_json_returns_error() {
let result =
parse_params_or_default::<ListToolsParams>(Some(serde_json::json!("bad_input")));
assert!(result.is_err());
}
#[test]
fn parse_params_or_default_valid_json_succeeds() {
let result =
parse_params_or_default::<ListToolsParams>(Some(serde_json::json!({"cursor": "abc"})));
let params = result.unwrap();
assert_eq!(params.cursor.as_deref(), Some("abc"));
}
// ── request_id_to_u64 ───────────────────────────────────────────
#[test]
fn request_id_to_u64_number() {
let id = RequestId::Number(42);
assert_eq!(request_id_to_u64(Some(&id)), 42);
}
#[test]
fn request_id_to_u64_string() {
let id = RequestId::String("req-123".to_string());
let result = request_id_to_u64(Some(&id));
assert_ne!(result, 0);
}
#[test]
fn request_id_to_u64_none() {
assert_eq!(request_id_to_u64(None), 0);
}
// ── stable_hash_request_id ──────────────────────────────────────
#[test]
fn stable_hash_is_deterministic() {
let h1 = stable_hash_request_id("test");
let h2 = stable_hash_request_id("test");
assert_eq!(h1, h2);
}
#[test]
fn stable_hash_never_returns_zero() {
// Empty string and various inputs should never produce 0
assert_ne!(stable_hash_request_id(""), 0);
assert_ne!(stable_hash_request_id("a"), 0);
}
#[test]
fn stable_hash_different_inputs_differ() {
let h1 = stable_hash_request_id("alpha");
let h2 = stable_hash_request_id("beta");
assert_ne!(h1, h2);
}
#[test]
fn dispatch_queue_reservation_spans_dispatch_until_response_completion() {
let queue = DispatchQueueState::default();
let request_id = RequestId::String("linearized-request".to_string());
assert!(queue.admit(&request_id, true));
let cancellation = queue
.admitted_request_cancellation(&request_id)
.expect("admitted requests retain their cancellation authority");
assert!(!queue.begin_dispatch(&request_id));
assert!(
!queue.admit(&request_id, true),
"an active request must retain its queue reservation"
);
assert_eq!(
queue.cancel_reserved(&request_id),
DispatchCancellationDisposition::Accepted,
"an active request must retain the authority admitted at queue entry"
);
assert!(cancellation.is_cancel_requested());
queue.discard(&request_id);
assert!(
queue.admit(&request_id, true),
"the id may be reused only after response completion releases it"
);
}
#[test]
fn correlated_response_drain_completes_only_after_the_reservation_is_discarded() {
let queue = DispatchQueueState::default();
let request_id = RequestId::String("pending-response".to_owned());
assert!(queue.admit(&request_id, true));
assert!(
!queue.wait_for_correlated_response_drain(Duration::ZERO),
"an admitted response reservation prevents an immediate drain"
);
queue.discard(&request_id);
assert!(
queue.wait_for_correlated_response_drain(Duration::ZERO),
"discarding the response reservation completes the drain"
);
}
#[test]
fn queued_initialize_ignores_peer_cancellation_and_remains_dispatchable() {
let queue = DispatchQueueState::default();
let initialize_id = RequestId::Number(6);
assert!(queue.admit(&initialize_id, false));
assert_eq!(
queue.cancel_reserved(&initialize_id),
DispatchCancellationDisposition::Protected,
"peer cancellation must not erase a queued initialize request"
);
assert!(
!queue.begin_dispatch(&initialize_id),
"the unchanged initialize request must remain ready to dispatch"
);
queue.discard(&initialize_id);
}
#[derive(Clone, Copy)]
enum QueuedToActiveProbeEra {
Legacy2024,
Modern2026,
}
fn queued_to_active_cancellation_probe(
era: QueuedToActiveProbeEra,
cancelled_id: RequestId,
) -> (DispatchCancellationDisposition, bool, bool) {
let queue = Arc::new(DispatchQueueState::default());
let request_id = RequestId::Integer("7e0".to_owned());
assert!(queue.admit(&request_id, true));
// The modern child must share the request authority created at queue
// admission; exact-2024 obtains that authority through its adapter.
let modern_reservation = matches!(era, QueuedToActiveProbeEra::Modern2026).then(|| {
assert!(queue.reserve_modern_slot());
ModernDispatchReservation::new(
Arc::clone(&queue),
Some(request_id.clone()),
0,
Arc::new(AtomicBool::new(false)),
)
});
let cancellation = modern_reservation.as_ref().map_or_else(
|| {
queue
.admitted_request_cancellation(&RequestId::Number(7))
.expect("numeric aliases must select the admitted cancellation authority")
},
ModernDispatchReservation::cancellation,
);
let (started_sender, started_receiver) = sync_channel(1);
let (release_sender, release_receiver) = sync_channel(1);
let (result_sender, result_receiver) = sync_channel(1);
let worker_queue = Arc::clone(&queue);
let worker_request_id = request_id.clone();
let worker = thread::spawn(move || {
started_sender
.send(())
.expect("queued-to-active worker must report its gate");
release_receiver
.recv_timeout(Duration::from_secs(2))
.expect("queued-to-active worker release must be bounded");
let cancelled = match era {
QueuedToActiveProbeEra::Legacy2024 => {
worker_queue.begin_dispatch(&worker_request_id)
}
QueuedToActiveProbeEra::Modern2026 => matches!(
worker_queue.begin_modern_dispatch(Some(&worker_request_id)),
ModernDispatchStart::Cancelled
),
};
result_sender
.send(cancelled)
.expect("queued-to-active worker must report its dispatch outcome");
});
started_receiver
.recv_timeout(Duration::from_secs(2))
.expect("queued-to-active worker must reach the cancellation gate");
let disposition = queue.cancel_reserved(&cancelled_id);
release_sender
.send(())
.expect("queued-to-active worker must remain available for release");
let cancelled_before_dispatch = result_receiver
.recv_timeout(Duration::from_secs(2))
.expect("queued-to-active dispatch result must be bounded");
worker
.join()
.expect("queued-to-active worker must not panic");
(
disposition,
cancelled_before_dispatch,
cancellation.is_cancel_requested(),
)
}
#[test]
fn queued_to_active_cancellation_admits_the_exact_id_for_modern_and_legacy() {
for era in [
QueuedToActiveProbeEra::Legacy2024,
QueuedToActiveProbeEra::Modern2026,
] {
let (disposition, cancelled_before_dispatch, cancellation_observed) =
queued_to_active_cancellation_probe(era, RequestId::Integer("7.0".to_owned()));
assert_eq!(disposition, DispatchCancellationDisposition::Accepted);
assert!(cancelled_before_dispatch);
assert!(cancellation_observed);
}
}
#[test]
fn queued_to_active_cancellation_rejects_one_id_negative_for_modern_and_legacy() {
for era in [
QueuedToActiveProbeEra::Legacy2024,
QueuedToActiveProbeEra::Modern2026,
] {
let (disposition, cancelled_before_dispatch, cancellation_observed) =
queued_to_active_cancellation_probe(era, RequestId::Integer("8e0".to_owned()));
assert_eq!(disposition, DispatchCancellationDisposition::NotOwned);
assert!(!cancelled_before_dispatch);
assert!(!cancellation_observed);
}
}
// Exact-2024 era: the legacy loop's dispatch-queue stop state.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn dispatch_queue_stop_rejects_admission_and_cancels_queued_start() {
let queue = DispatchQueueState::default();
let queued = RequestId::Number(7);
assert!(queue.admit(&queued, true));
queue.stop();
assert!(queue.is_stopping());
assert!(queue.begin_dispatch(&queued));
assert!(!queue.admit(&RequestId::Number(8), true));
}
#[test]
fn dispatch_queue_enforces_and_releases_aggregate_byte_budget() {
let queue = DispatchQueueState::default();
assert!(queue.reserve_queued_bytes(MAX_DISPATCH_QUEUE_BYTES));
assert!(!queue.reserve_queued_bytes(1));
queue.release_queued_bytes(MAX_DISPATCH_QUEUE_BYTES / 2);
assert!(queue.reserve_queued_bytes(MAX_DISPATCH_QUEUE_BYTES / 2));
assert!(!queue.reserve_queued_bytes(usize::MAX));
queue.release_queued_bytes(MAX_DISPATCH_QUEUE_BYTES);
queue.stop();
assert!(!queue.reserve_queued_bytes(1));
}
#[test]
fn dispatch_request_measurement_matches_wire_serialization() {
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({"payload": "bounded"})),
7_i64,
);
assert_eq!(
measure_dispatch_request(&request),
Some(serde_json::to_vec(&request).unwrap().len())
);
}
// ── RequestCompletion ───────────────────────────────────────────
#[test]
fn request_completion_new_is_not_done() {
let rc = RequestCompletion::new();
assert!(!rc.is_done());
}
#[test]
fn request_completion_mark_done_sets_done() {
let rc = RequestCompletion::new();
rc.mark_done();
assert!(rc.is_done());
}
#[test]
fn request_completion_mark_done_idempotent() {
let rc = RequestCompletion::new();
rc.mark_done();
rc.mark_done(); // should not panic
assert!(rc.is_done());
}
#[test]
fn request_completion_wait_timeout_returns_true_if_done() {
let rc = RequestCompletion::new();
rc.mark_done();
assert!(rc.wait_timeout(Duration::from_millis(10)));
}
#[test]
fn request_completion_wait_timeout_returns_false_if_not_done() {
let rc = RequestCompletion::new();
assert!(!rc.wait_timeout(Duration::from_millis(10)));
}
// ── DuplicateBehavior ───────────────────────────────────────────
#[test]
fn duplicate_behavior_default_is_warn() {
assert_eq!(DuplicateBehavior::default(), DuplicateBehavior::Warn);
}
#[test]
fn duplicate_behavior_debug_and_clone() {
let b = DuplicateBehavior::Error;
let debug = format!("{:?}", b);
assert!(debug.contains("Error"));
let cloned = b;
assert_eq!(cloned, DuplicateBehavior::Error);
}
#[test]
fn duplicate_behavior_all_variants_are_distinct() {
assert_ne!(DuplicateBehavior::Error, DuplicateBehavior::Warn);
assert_ne!(DuplicateBehavior::Warn, DuplicateBehavior::Replace);
assert_ne!(DuplicateBehavior::Replace, DuplicateBehavior::Ignore);
}
// ── LoggingConfig ───────────────────────────────────────────────
#[test]
fn logging_config_default_values() {
let config = LoggingConfig::default();
assert_eq!(config.level, LevelFilter::Info);
assert!(config.timestamps);
assert!(config.targets);
assert!(!config.file_line);
}
// Exact-2024 era: legacy logging/setLevel level mapping.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_logging_maps_all_eight_levels_without_losing_severity() {
for (wire, ceiling, expected) in [
("debug", LevelFilter::Debug, LogLevel::Debug),
("info", LevelFilter::Info, LogLevel::Info),
("notice", LevelFilter::Info, LogLevel::Notice),
("warning", LevelFilter::Warn, LogLevel::Warning),
("error", LevelFilter::Error, LogLevel::Error),
("critical", LevelFilter::Error, LogLevel::Critical),
("alert", LevelFilter::Error, LogLevel::Alert),
("emergency", LevelFilter::Error, LogLevel::Emergency),
] {
assert_eq!(map_legacy_log_level(wire, ceiling), Some(expected));
}
}
#[test]
fn legacy_set_log_level_preserves_all_eight_levels_when_unclamped() {
let server = Server::new("legacy-log-levels", "1.0.0")
.log_level(Level::Debug)
.build();
let mut session = initialized_test_session(&server);
for level in [
LogLevel::Debug,
LogLevel::Info,
LogLevel::Notice,
LogLevel::Warning,
LogLevel::Error,
LogLevel::Critical,
LogLevel::Alert,
LogLevel::Emergency,
] {
server.handle_set_log_level(&mut session, SetLogLevelParams { level });
assert_eq!(session.log_level(), Some(level));
}
}
// Exact-2024 era: legacy logging/setLevel level mapping.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_logging_clamps_to_server_ceiling_and_off() {
assert_eq!(
map_legacy_log_level("notice", LevelFilter::Warn),
Some(LogLevel::Warning)
);
assert_eq!(
map_legacy_log_level("error", LevelFilter::Warn),
Some(LogLevel::Error)
);
assert_eq!(map_legacy_log_level("emergency", LevelFilter::Off), None);
assert_eq!(
map_legacy_log_level("not-a-level", LevelFilter::Error),
None
);
}
// ── LifespanHooks ───────────────────────────────────────────────
#[test]
fn lifespan_hooks_new_has_no_hooks() {
let hooks = LifespanHooks::new();
assert!(hooks.on_startup.is_none());
assert!(hooks.on_shutdown.is_none());
}
// ── log_level_rank ──────────────────────────────────────────────
#[test]
fn log_level_rank_ordering() {
for (level, expected_rank) in [
(LogLevel::Debug, 1),
(LogLevel::Info, 2),
(LogLevel::Notice, 3),
(LogLevel::Warning, 4),
(LogLevel::Error, 5),
(LogLevel::Critical, 6),
(LogLevel::Alert, 7),
(LogLevel::Emergency, 8),
] {
assert_eq!(Server::log_level_rank(level), expected_rank);
}
}
// ── ActiveRequestGuard ──────────────────────────────────────────
#[test]
fn active_request_pending_activation_preserves_parent_and_sibling() {
active_request_pending_case(None);
}
#[test]
fn active_request_pending_cancel_before_activation_preserves_parent_and_sibling() {
active_request_pending_case(Some(true));
}
#[test]
fn active_request_pending_cancel_after_activation_preserves_parent_and_sibling() {
active_request_pending_case(Some(false));
}
fn active_request_pending_case(cancel_before_activation: Option<bool>) {
let runtime = RuntimeBuilder::current_thread()
.build()
.expect("ownership test runtime must initialize");
let parent = runtime.request_cx_with_budget(Budget::INFINITE);
let child = runtime.request_cx_with_budget(Budget::INFINITE);
let sibling = runtime.request_cx_with_budget(Budget::INFINITE);
let server = Server::new("pending-request-owner", "1.0.0").build();
let cancellation = McpRequestCancellation::new();
let guard = ActiveRequestGuard::try_reserve(
Arc::clone(&server.active_requests),
11,
RequestId::Number(7),
parent.region_id(),
cancellation.clone(),
)
.expect("pending request should reserve its wire ID");
let completion = Arc::clone(&guard.completion);
assert_eq!(server.active_requests.lock().unwrap().len(), 1);
assert!(!completion.is_done());
assert!(
ActiveRequestGuard::try_new(
Arc::clone(&server.active_requests),
11,
RequestId::Integer("7e0".to_owned()),
sibling.clone(),
)
.is_err(),
"queued ownership must reject an equivalent duplicate without replacement"
);
if cancel_before_activation == Some(true) {
server.cancel_active_requests(CancelKind::Shutdown, false);
}
assert_eq!(
guard.activate(child.clone()),
cancel_before_activation != Some(true)
);
if cancel_before_activation == Some(false) {
server.cancel_active_requests(CancelKind::Shutdown, false);
}
assert_eq!(
child.is_cancel_requested(),
cancel_before_activation.is_some()
);
assert_eq!(
cancellation.is_cancel_requested(),
cancel_before_activation.is_some()
);
assert!(!parent.is_cancel_requested());
assert!(!sibling.is_cancel_requested());
assert!(!completion.is_done());
assert_eq!(server.active_requests.lock().unwrap().len(), 1);
drop(guard);
assert!(completion.is_done());
assert!(server.active_requests.lock().unwrap().is_empty());
let replacement = ActiveRequestGuard::try_new(
Arc::clone(&server.active_requests),
11,
RequestId::Number(7),
sibling.clone(),
)
.expect("retirement must permit exact ID reuse");
assert!(!replacement.cancellation().is_cancel_requested());
assert!(!sibling.is_cancel_requested());
drop(replacement);
assert!(server.active_requests.lock().unwrap().is_empty());
}
#[test]
fn active_request_guard_removes_on_drop() {
let map = Arc::new(Mutex::new(HashMap::new()));
let cx = Cx::for_testing();
let id = RequestId::Number(1);
{
let _guard = ActiveRequestGuard::try_new(Arc::clone(&map), 11, id.clone(), cx)
.expect("insert guard");
assert_eq!(map.lock().unwrap().len(), 1);
}
// After drop, the entry should be removed
assert_eq!(map.lock().unwrap().len(), 0);
}
#[test]
fn active_request_guard_rejects_duplicate_request_id() {
let map = Arc::new(Mutex::new(HashMap::new()));
let first = ActiveRequestGuard::try_new(
Arc::clone(&map),
11,
RequestId::Number(7),
Cx::for_testing(),
)
.expect("first request should register");
let duplicate = ActiveRequestGuard::try_new(
Arc::clone(&map),
11,
RequestId::Integer("7e0".to_owned()),
Cx::for_testing(),
);
assert!(
duplicate.is_err(),
"equivalent numeric active request IDs must be rejected"
);
drop(first);
assert!(map.lock().unwrap().is_empty());
}
#[test]
fn active_request_guard_allows_same_request_id_in_distinct_sessions() {
let map = Arc::new(Mutex::new(HashMap::new()));
let first = ActiveRequestGuard::try_new(
Arc::clone(&map),
11,
RequestId::Number(7),
Cx::for_testing(),
)
.expect("first session should register");
let second = ActiveRequestGuard::try_new(
Arc::clone(&map),
12,
RequestId::Number(7),
Cx::for_testing(),
)
.expect("second session may reuse the same wire request id");
assert_eq!(map.lock().unwrap().len(), 2);
drop((first, second));
assert!(map.lock().unwrap().is_empty());
}
#[test]
fn cancellation_notification_is_bound_to_originating_session() {
let server = Server::new("cancel-owner-test", "1.0.0").build();
let first_cx = Cx::for_testing();
let second_cx = Cx::for_testing();
let request_id = RequestId::Number(7);
let first = ActiveRequestGuard::try_new(
Arc::clone(&server.active_requests),
11,
request_id.clone(),
first_cx.clone(),
)
.expect("first session should register");
let second = ActiveRequestGuard::try_new(
Arc::clone(&server.active_requests),
12,
request_id.clone(),
second_cx.clone(),
)
.expect("second session should register");
let first_cancellation = first.cancellation();
let second_cancellation = second.cancellation();
server.handle_cancelled_notification(
11,
CancelledParams {
request_id,
reason: Some("owner requested cancellation".to_string()),
meta: None,
},
);
assert!(first_cancellation.is_cancel_requested());
assert!(!second_cancellation.is_cancel_requested());
assert!(!first_cx.is_cancel_requested());
assert!(!second_cx.is_cancel_requested());
drop((first, second));
}
#[test]
fn cancelled_notification_does_not_block_the_receive_path() {
let server = Server::new("nonblocking-cancellation-test", "1.0.0").build();
let request_id = RequestId::Number(41);
let guard = ActiveRequestGuard::try_new(
Arc::clone(&server.active_requests),
11,
request_id.clone(),
Cx::for_testing(),
)
.expect("request should register");
let cancellation = guard.cancellation();
server.handle_cancelled_notification(
11,
CancelledParams {
request_id,
reason: None,
meta: None,
},
);
// bd-mvpne: an `assert!(start.elapsed() < Duration::from_secs(1))` stood here and was
// removed, with its `Instant::now()` binding. It ran AFTER the call it bounded, so a
// genuine block never reached it -- it could not detect the defect its own message
// named -- while still being able to fail because the machine was merely slow. The
// two assertions below carry everything recoverable. Do not re-add a wall-clock
// bound here; the property is structural, not temporal.
assert!(cancellation.is_cancel_requested());
assert!(!guard.completion.is_done());
}
#[test]
fn duplicate_cancellation_is_not_misclassified_as_finalization() {
let server = Server::new("duplicate-cancellation-state-test", "1.0.0").build();
let request_id = RequestId::Number(42);
let guard = ActiveRequestGuard::try_new(
Arc::clone(&server.active_requests),
11,
request_id.clone(),
Cx::for_testing(),
)
.expect("request should register");
let cancellation = guard.cancellation();
for _ in 0..2 {
server.handle_cancelled_notification(
11,
CancelledParams {
request_id: request_id.clone(),
reason: None,
meta: None,
},
);
}
assert!(cancellation.is_cancel_requested());
assert!(!cancellation.is_finalizing());
assert!(!guard.completion.is_done());
}
#[test]
fn cancellation_notification_accepts_arbitrary_precision_integer_request_ids() {
let server = Server::new("large-cancellation-id-test", "1.0.0").build();
let request_id = RequestId::Integer(
"9223372036854775808922337203685477580892233720368547758089".to_string(),
);
let guard = ActiveRequestGuard::try_new(
Arc::clone(&server.active_requests),
11,
request_id.clone(),
Cx::for_testing(),
)
.expect("an arbitrary-precision mathematical integer must register");
let cancellation = guard.cancellation();
assert!(server.handle_cancelled_notification(
11,
CancelledParams {
request_id,
reason: Some("large-id cancellation".to_string()),
meta: None,
},
));
assert!(cancellation.is_cancel_requested());
}
// Exact-2024 era: legacy active-request registry.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn invalid_or_unknown_cancellation_does_not_mutate_an_active_request() {
let server = Server::new("ignored-cancellation-test", "1.0.0").build();
let active_request_id = RequestId::Number(42);
let guard = ActiveRequestGuard::try_new(
Arc::clone(&server.active_requests),
11,
active_request_id.clone(),
Cx::for_testing(),
)
.expect("active request must register");
let cancellation = guard.cancellation();
assert!(!server.handle_cancelled_notification(
11,
CancelledParams {
request_id: RequestId::Integer("1.5".to_string()),
reason: None,
meta: None,
},
));
assert!(!server.handle_cancelled_notification(
11,
CancelledParams {
request_id: RequestId::Number(43),
reason: None,
meta: None,
},
));
assert!(!cancellation.is_cancel_requested());
assert!(server.request_id_is_active(11, &active_request_id));
assert_eq!(
server
.active_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"ignored cancellation notifications must leave the request registry unchanged"
);
}
#[test]
fn completed_request_cancellation_is_ignored_without_registry_mutation() {
let server = Server::new("completed-cancellation-test", "1.0.0").build();
let request_id = RequestId::Number(44);
let cancellation = {
let guard = ActiveRequestGuard::try_new(
Arc::clone(&server.active_requests),
11,
request_id.clone(),
Cx::for_testing(),
)
.expect("active request must register");
guard.cancellation()
};
assert!(!server.handle_cancelled_notification(
11,
CancelledParams {
request_id,
reason: None,
meta: None,
},
));
assert!(!cancellation.is_cancel_requested());
assert!(
server
.active_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty(),
"a completed request must remain absent after a late cancellation"
);
}
#[test]
fn request_local_cancellation_dominates_a_late_success_without_cancelling_ambient_cx() {
let cx = Cx::for_testing();
let cancellation = McpRequestCancellation::new();
cancellation.cancel();
let result = Server::enforce_post_dispatch_liveness(
&cancellation,
&cx,
Budget::INFINITE,
Ok(serde_json::json!({"late": "success"})),
)
.expect_err("late request cancellation must dominate a successful handler result");
assert_eq!(result.code, McpErrorCode::RequestCancelled);
assert!(!cx.is_cancel_requested());
}
#[test]
fn post_dispatch_success_remains_cancellable_until_response_commit() {
let cx = Cx::for_testing();
let cancellation = McpRequestCancellation::new();
let result = Server::enforce_post_dispatch_liveness(
&cancellation,
&cx,
Budget::INFINITE,
Ok(serde_json::json!({"committed": true})),
)
.expect("active request should preserve its successful result");
assert_eq!(result, serde_json::json!({"committed": true}));
assert!(!cancellation.is_finalizing());
assert!(cancellation.cancel());
assert!(cancellation.is_cancel_requested());
assert!(!cx.is_cancel_requested());
}
#[test]
fn initialize_response_middleware_failure_restores_exact_prior_state() {
let server = Server::new("initialize-middleware-rollback-test", "1.0.0")
.middleware(RejectResponseMiddleware)
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "original-client".to_string(),
version: "0.9.0".to_string(),
},
fastmcp_protocol::ClientCapabilities {
sampling: Some(fastmcp_protocol::SamplingCapability::default()),
elicitation: None,
roots: None,
..Default::default()
},
"original-protocol".to_string(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
initialize_test_request(
81,
"replacement-client",
fastmcp_protocol::ClientCapabilities {
sampling: None,
elicitation: Some(fastmcp_protocol::ElicitationCapability::form()),
roots: None,
..Default::default()
},
),
¬ification_sender,
&test_request_sender(),
))
.expect("initialize middleware failure must produce a response");
assert!(response.error.is_some());
assert!(session.is_initialized());
assert_eq!(
session.client_info().map(|info| info.name.as_str()),
Some("original-client")
);
assert_eq!(session.protocol_version(), Some("original-protocol"));
assert!(session.supports_sampling());
assert!(!session.supports_elicitation());
}
#[test]
fn late_initialize_cancellation_restores_uninitialized_state() {
let server = Server::new("initialize-cancellation-rollback-test", "1.0.0").build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let handled = block_on(server.handle_request_internal(
&Cx::for_testing(),
&mut session,
initialize_test_request(
82,
"cancelled-client",
fastmcp_protocol::ClientCapabilities::default(),
),
¬ification_sender,
&request_sender,
None,
None,
))
.expect("initialize must produce a provisional response");
let cancellation = handled
.cancellation
.as_ref()
.expect("initialize request must remain cancellable")
.clone();
assert!(session.is_initialized());
assert!(cancellation.cancel());
let response = handled.finalize_for_return(&mut session);
assert_eq!(
response
.error
.as_ref()
.and_then(|error| error.code.as_i32()),
Some(i32::from(McpErrorCode::RequestCancelled))
);
assert!(!session.is_initialized());
assert!(session.client_info().is_none());
assert!(session.client_capabilities().is_none());
assert!(session.protocol_version().is_none());
}
// Exact-2024 era: legacy initialize response commit.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn initialize_encode_failure_restores_exact_prior_state() {
let server = Server::new("initialize-encode-rollback-test", "1.0.0").build();
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let handled = block_on(server.handle_request_internal(
&Cx::for_testing(),
&mut session,
initialize_test_request(
83,
"unsent-replacement-client",
fastmcp_protocol::ClientCapabilities::default(),
),
¬ification_sender,
&request_sender,
None,
None,
))
.expect("initialize must produce a provisional response");
assert_eq!(
session.client_info().map(|info| info.name.as_str()),
Some("unsent-replacement-client")
);
let json_error = serde_json::from_str::<serde_json::Value>("{")
.expect_err("malformed JSON must create a codec error");
let error = handled
.send_with(&mut session, |_response| {
Err(TransportError::Codec(fastmcp_transport::CodecError::Json(
json_error,
)))
})
.expect_err("encode failure must propagate");
assert!(matches!(error, TransportError::Codec(_)));
assert_eq!(
session.client_info().map(|info| info.name.as_str()),
Some("panic-containment-test-client")
);
assert_eq!(session.protocol_version(), Some("2024-11-05"));
}
// Exact-2024 era: legacy logging/setLevel response commit.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn log_level_write_failure_restores_previous_level() {
let server = Server::new("log-level-write-rollback-test", "1.0.0")
.log_level(Level::Debug)
.build();
let mut session = initialized_test_session(&server);
session.set_log_level(LogLevel::Warning);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let request = JsonRpcRequest::new(
"logging/setLevel",
Some(
serde_json::to_value(SetLogLevelParams {
level: LogLevel::Debug,
})
.expect("serialize logging/setLevel request"),
),
84_i64,
);
let handled = block_on(server.handle_request_internal(
&Cx::for_testing(),
&mut session,
request,
¬ification_sender,
&request_sender,
None,
None,
))
.expect("logging/setLevel must produce a provisional response");
assert_eq!(session.log_level(), Some(LogLevel::Debug));
let error = handled
.send_with(&mut session, |_response| {
Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"test logging response write failure",
)))
})
.expect_err("write failure must propagate");
assert!(matches!(error, TransportError::Io(_)));
assert_eq!(session.log_level(), Some(LogLevel::Warning));
}
// Exact-2024 era: legacy handled-response commit.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn handled_response_retains_active_request_until_commit_and_late_cancellation_wins() {
let server = Server::new("commit-race-test", "1.0.0").build();
let mut session = initialized_test_session(&server);
let request_id = RequestId::String("commit-race".to_string());
let request_cx = Cx::for_testing();
let guard = ActiveRequestGuard::try_new(
Arc::clone(&server.active_requests),
session.id(),
request_id.clone(),
request_cx.clone(),
)
.expect("register active request");
let cancellation = guard.cancellation();
let handled = HandledRequest::tracked(
JsonRpcResponse::success(request_id.clone(), serde_json::json!({"ok": true})),
cancellation.clone(),
Some(guard),
None,
request_cx,
Budget::INFINITE,
)
.with_deferred_stats(DeferredRequestStats::new(
server.stats.as_ref(),
"tools/call",
Instant::now(),
DeferredRequestOutcome::Success,
));
assert!(server.request_id_is_active(session.id(), &request_id));
assert!(cancellation.cancel());
let response = handled.finalize_for_return(&mut session);
assert_eq!(response.id, Some(request_id.clone()));
assert_eq!(
response
.error
.as_ref()
.and_then(|error| error.code.as_i32()),
Some(i32::from(McpErrorCode::RequestCancelled))
);
assert!(!server.request_id_is_active(session.id(), &request_id));
let stats = server.stats().expect("stats enabled by default");
assert_eq!(stats.total_requests, 1);
assert_eq!(stats.successful_requests, 0);
assert_eq!(stats.cancelled_requests, 1);
}
#[test]
fn ambient_cancellation_before_commit_rolls_back_and_replaces_success() {
let mut session = Session::new(
ServerInfo {
name: "ambient-commit-test".to_string(),
version: "1.0.0".to_string(),
},
ServerCapabilities::default(),
);
session.set_log_level(LogLevel::Debug);
let cx = Cx::for_testing();
let cancellation = McpRequestCancellation::new();
let handled = HandledRequest::tracked(
JsonRpcResponse::success(RequestId::Number(91), serde_json::json!({"ok": true})),
cancellation.clone(),
None,
Some(SessionMutationRollback::RestoreLogLevel(Some(
LogLevel::Warning,
))),
cx.clone(),
Budget::INFINITE,
);
cx.set_cancel_requested(true);
let response = handled.finalize_for_return(&mut session);
assert_eq!(
response
.error
.as_ref()
.and_then(|error| error.code.as_i32()),
Some(i32::from(McpErrorCode::RequestCancelled))
);
assert_eq!(session.log_level(), Some(LogLevel::Warning));
assert!(cancellation.is_cancel_requested());
}
#[test]
fn deadline_expiring_while_response_is_pending_prevents_commit() {
let mut session = Session::new(
ServerInfo {
name: "deadline-commit-test".to_string(),
version: "1.0.0".to_string(),
},
ServerCapabilities::default(),
);
let cx = Cx::for_testing();
// Keep the precondition well away from scheduler jitter. The previous
// one-millisecond window could expire while the test thread was
// descheduled before the assertion below.
let budget = Budget::new().with_deadline(cx.now().saturating_add_nanos(1_000_000_000));
assert!(Server::request_budget_error(&cx, budget).is_none());
let handled = HandledRequest::tracked(
JsonRpcResponse::success(RequestId::Number(92), serde_json::json!({"ok": true})),
McpRequestCancellation::new(),
None,
None,
cx,
budget,
);
std::thread::sleep(Duration::from_millis(1_100));
let response = handled.finalize_for_return(&mut session);
assert_eq!(
response
.error
.as_ref()
.and_then(|error| error.code.as_i32()),
Some(i32::from(McpErrorCode::RequestCancelled))
);
assert_eq!(
response.error.as_ref().map(|error| error.message.as_str()),
Some("Request timeout exceeded")
);
}
// Exact-2024 era: legacy handled-response commit.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn failed_response_write_rolls_back_reversible_session_mutation() {
let server = Server::new("commit-rollback-test", "1.0.0").build();
let mut session = initialized_test_session(&server);
let uri = "test://response-write-rollback".to_string();
session.restore_resource_subscription(uri.clone());
let handled = HandledRequest::tracked(
JsonRpcResponse::success(RequestId::Number(9), serde_json::json!({})),
McpRequestCancellation::new(),
None,
Some(SessionMutationRollback::RemoveResourceSubscription(
uri.clone(),
)),
Cx::for_testing(),
Budget::INFINITE,
)
.with_deferred_stats(DeferredRequestStats::new(
server.stats.as_ref(),
"resources/subscribe",
Instant::now(),
DeferredRequestOutcome::Success,
));
let error = handled
.send_with(&mut session, |_response| {
Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"test write failure",
)))
})
.expect_err("failed response write must propagate");
assert!(matches!(error, TransportError::Io(_)));
assert!(!session.is_resource_subscribed(&uri));
assert_eq!(
server
.stats()
.expect("stats enabled by default")
.total_requests,
0,
"a response that never reached the transport must not be counted as sent"
);
}
/// bd-0vcz4: the runtime also cancels a Cx whose deadline passed; that
/// cancellation is the timeout, not a plain cancellation.
#[test]
fn cancelled_cx_past_its_deadline_reports_request_timeout() {
let cx = Cx::for_testing_with_budget(Budget::new().with_deadline(asupersync::Time::ZERO));
cx.set_cancel_requested(true);
let error = Server::request_budget_error(&cx, Budget::INFINITE)
.expect("an expired, cancelled request is refused");
assert_eq!(error.code, McpErrorCode::RequestCancelled);
assert_eq!(error.message, "Request timeout exceeded");
}
/// Near-identical negative: the same cancelled Cx without a passed deadline
/// is a plain cancellation.
#[test]
fn cancelled_cx_without_a_passed_deadline_reports_request_cancelled() {
let cx = Cx::for_testing();
cx.set_cancel_requested(true);
let error = Server::request_budget_error(&cx, Budget::INFINITE)
.expect("a cancelled request is refused");
assert_eq!(error.code, McpErrorCode::RequestCancelled);
assert_eq!(error.message, McpError::request_cancelled().message);
}
#[test]
fn expired_ambient_deadline_is_enforced_before_and_after_dispatch() {
let cx = Cx::for_testing_with_budget(Budget::new().with_deadline(asupersync::Time::ZERO));
let admission = Server::request_budget_error(&cx, Budget::INFINITE)
.expect("expired ambient deadline must fail pre-admission");
assert_eq!(admission.code, McpErrorCode::RequestCancelled);
let result = Server::enforce_post_dispatch_liveness(
&McpRequestCancellation::new(),
&cx,
Budget::INFINITE,
Ok(serde_json::json!({"late": "success"})),
)
.expect_err("expired ambient deadline must dominate a late success");
assert_eq!(result.code, McpErrorCode::RequestCancelled);
}
// =========================================================================
// Additional coverage tests (bd-cd79)
// =========================================================================
#[test]
fn logging_config_debug_and_clone() {
let config = LoggingConfig::default();
let debug = format!("{config:?}");
assert!(debug.contains("LoggingConfig"));
assert!(debug.contains("Info"));
let cloned = config.clone();
assert_eq!(cloned.level, LevelFilter::Info);
assert_eq!(cloned.timestamps, config.timestamps);
}
#[test]
fn transport_lock_error_is_io() {
let err = transport_lock_error();
match err {
TransportError::Io(io) => {
assert!(io.to_string().contains("poisoned"));
}
other => panic!("expected Io variant, got: {other:?}"),
}
}
fn wait_for_dispatch_worker_failure(worker_failed: &AtomicBool) -> TransportError {
let deadline = Instant::now() + Duration::from_secs(2);
while !worker_failed.load(Ordering::Acquire) && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(1));
}
if worker_failed.load(Ordering::Acquire) {
TransportError::Cancelled
} else {
// Let the pump perform its normal stop-and-join cleanup. The
// callback-entry assertion then fails the test without detaching
// its worker thread through an unwind from inside `recv`.
TransportError::Timeout
}
}
#[test]
fn dispatch_worker_send_failure_returns_failure_status() {
let emitted = Arc::new(AtomicBool::new(false));
let emitted_for_receive = Arc::clone(&emitted);
let send_attempted = Arc::new(AtomicBool::new(false));
let send_attempted_by_worker = Arc::clone(&send_attempted);
let cx = Cx::for_testing();
let exit_code = Arc::new(Server::new("worker-send-failure-test", "1.0.0").build())
.run_loop_pump_with_policy(
&cx,
&cx,
move |_receive_cx, worker_failed| {
if !emitted_for_receive.swap(true, Ordering::AcqRel) {
return Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"ping", None, 701_i64,
)));
}
Err(wait_for_dispatch_worker_failure(worker_failed))
},
move |_send_cx, _message| {
send_attempted_by_worker.store(true, Ordering::Release);
Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"test response send failure",
)))
},
Arc::new(|_| {}),
"test",
true,
None,
true,
true,
None,
None,
None,
PumpIoMode::Split,
);
assert_eq!(exit_code, 1);
assert!(send_attempted.load(Ordering::Acquire));
assert!(!cx.is_cancel_requested());
}
#[test]
fn reality_check_regression_worker_io_failure_wins_concurrent_clean_eof() {
let emitted = Arc::new(AtomicBool::new(false));
let emitted_for_receive = Arc::clone(&emitted);
let send_entered = Arc::new(AtomicBool::new(false));
let send_entered_for_receive = Arc::clone(&send_entered);
let send_entered_by_worker = Arc::clone(&send_entered);
let cx = Cx::for_testing();
let exit_code = Arc::new(Server::new("worker-send-eof-race-test", "1.0.0").build())
.run_loop_pump_with_policy(
&cx,
&cx,
move |_receive_cx, _worker_failed| {
if !emitted_for_receive.swap(true, Ordering::AcqRel) {
return Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"ping", None, 706_i64,
)));
}
let deadline = Instant::now() + Duration::from_secs(2);
while !send_entered_for_receive.load(Ordering::Acquire)
&& Instant::now() < deadline
{
std::thread::sleep(Duration::from_millis(1));
}
Err(TransportError::Closed)
},
move |send_cx, _message| {
send_entered_by_worker.store(true, Ordering::Release);
let deadline = Instant::now() + Duration::from_secs(2);
while send_cx.checkpoint().is_ok() && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(1));
}
Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"test response failure after concurrent EOF",
)))
},
Arc::new(|_| {}),
"test",
true,
None,
true,
true,
None,
None,
None,
PumpIoMode::Split,
);
assert!(send_entered.load(Ordering::Acquire));
assert_eq!(exit_code, 1);
}
// Exact-2024 era: dispatches an un-negotiated ping, which only the dual-era pump admits.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn dispatch_worker_panic_trips_failure_latch_and_returns_failure_status() {
let emitted = Arc::new(AtomicBool::new(false));
let emitted_for_receive = Arc::clone(&emitted);
let send_entered = Arc::new(AtomicBool::new(false));
let send_entered_by_worker = Arc::clone(&send_entered);
let cx = Cx::for_testing();
let exit_code = Arc::new(Server::new("worker-panic-latch-test", "1.0.0").build())
.run_loop_pump_with_policy(
&cx,
&cx,
move |_receive_cx, worker_failed| {
if !emitted_for_receive.swap(true, Ordering::AcqRel) {
return Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"ping", None, 702_i64,
)));
}
Err(wait_for_dispatch_worker_failure(worker_failed))
},
move |_send_cx, _message| -> Result<(), TransportError> {
send_entered_by_worker.store(true, Ordering::Release);
panic!("dispatch worker send callback panic")
},
Arc::new(|_| {}),
"test",
true,
None,
true,
true,
None,
None,
None,
PumpIoMode::Split,
);
assert_eq!(exit_code, 1);
assert!(send_entered.load(Ordering::Acquire));
assert!(!cx.is_cancel_requested());
}
// Exact-2024 era: the non-quiescent handler is reached through a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn stdio_pump_returns_failure_and_skips_hooks_for_non_quiescent_legacy_handler() {
let control = Arc::new(NonQuiescentLegacyControl::default());
let control_for_receive = Arc::clone(&control);
let phase = Arc::new(AtomicUsize::new(0));
let phase_for_receive = Arc::clone(&phase);
let shutdown_called = Arc::new(AtomicBool::new(false));
let shutdown_observer = Arc::clone(&shutdown_called);
let server = Server::new("bounded-legacy-worker-shutdown-test", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(NonQuiescentLegacyTool {
control: Arc::clone(&control),
})
.on_shutdown(move || shutdown_observer.store(true, Ordering::Release))
.build();
// The legacy dispatch worker also requires the caller-owned runtime
// to admit its request child. A detached testing Cx rejects admission
// before the deliberately non-cooperative handler can start.
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("stdio pump reactor must initialize"))
.blocking_threads(4, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("stdio pump runtime must initialize");
let (sender, receiver) = sync_channel(1);
runtime
.handle()
.try_spawn_with_cx(move |cx| async move {
let dispatch_cx = cx.clone();
let pump_sender = sender.clone();
if let Err(error) = cx.spawn_blocking(move |pump_cx| {
let code = Arc::new(server).run_loop_pump_with_policy(
&pump_cx,
&dispatch_cx,
move |_receive_cx, _worker_failed| match phase_for_receive
.fetch_add(1, Ordering::AcqRel)
{
0 => Ok(exact_legacy_initialize_request(
705,
serde_json::json!("1.0.0"),
)),
1 => Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
))),
2 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "non_quiescent_legacy_tool",
"arguments": {},
})),
706_i64,
))),
3 if control_for_receive.wait_for_started(Duration::from_secs(2)) => {
Err(TransportError::Closed)
}
3 => Err(TransportError::Timeout),
_ => Err(TransportError::Closed),
},
move |_send_cx, _message| Ok(()),
Arc::new(|_| {}),
"stdio-test",
true,
None,
true,
true,
None,
None,
None,
PumpIoMode::Split,
);
let _ = pump_sender.send(Ok(code));
}) {
let _ = sender.send(Err(format!("stdio pump admission failed: {error}")));
}
})
.expect("stdio pump task must be admitted");
// The pump waits through bounded worker-shutdown windows before
// detaching the non-quiescent legacy worker; poll its result without
// joining the task while the handler is intentionally parked.
let pump_result: Result<Result<i32, String>, String> = runtime.block_on(async {
let cx = Cx::current().expect("the test runtime installs an ambient Cx");
let deadline = cx.now().saturating_add_nanos(30_000_000_000);
loop {
match receiver.try_recv() {
Ok(result) => break Ok(result),
Err(TryRecvError::Disconnected) => {
break Err("stdio pump task exited without reporting its result".to_owned());
}
Err(TryRecvError::Empty) => {
if asupersync::time::timeout_at(
deadline,
asupersync::time::sleep(cx.now(), Duration::from_millis(5)),
)
.await
.is_err()
{
break Err("stdio pump did not settle within its bound".to_owned());
}
}
}
}
});
// Capture observations before releasing the parked legacy handler so
// an assertion failure cannot strand the worker during runtime drop.
let started = control.has_started();
let finished_before_release = control.has_finished();
let shutdown_before_release = shutdown_called.load(Ordering::Acquire);
control.release();
let finished_after_release = control.wait_for_finished(Duration::from_secs(2));
let exit_code = pump_result
.expect("stdio pump must report a result")
.expect("stdio pump must complete");
assert_eq!(exit_code, 1);
assert!(started);
assert!(!finished_before_release);
assert!(!shutdown_before_release);
assert!(finished_after_release);
}
// Exact-2024 era: the non-quiescent handler is reached through a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn returning_split_waits_for_non_quiescent_legacy_handler_before_owned_cleanup() {
let control = Arc::new(NonQuiescentLegacyControl::default());
let shutdown_called = Arc::new(AtomicBool::new(false));
let shutdown_observer = Arc::clone(&shutdown_called);
let release_control = Arc::clone(&control);
let release_thread = std::thread::spawn(move || {
let started = release_control.wait_for_started(Duration::from_secs(2));
if started {
std::thread::sleep(DISPATCH_WORKER_SHUTDOWN_TIMEOUT + Duration::from_millis(500));
}
// Always release before reporting a failed start observation. A
// panic in this helper would otherwise strand the intentionally
// non-cooperative handler and hang the entire test process.
release_control.release();
started
});
let started = Instant::now();
let run_result = run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
Server::new("returning-legacy-worker-shutdown-test", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(NonQuiescentLegacyTool {
control: Arc::clone(&control),
})
.on_shutdown(move || shutdown_observer.store(true, Ordering::Release))
.build(),
NonQuiescentLegacySplitRecv {
phase: 0,
control: Arc::clone(&control),
},
NonQuiescentLegacySplitSend,
);
let elapsed = started.elapsed();
let release_observed_start = release_thread.join().expect("release thread must finish");
assert!(
release_observed_start,
"legacy handler must start before its delayed release"
);
assert!(control.has_started());
assert!(control.has_finished());
assert!(elapsed >= DISPATCH_WORKER_SHUTDOWN_TIMEOUT);
assert!(shutdown_called.load(Ordering::Acquire));
assert!(run_result.is_err(), "worker timeout must remain a failure");
}
#[test]
fn returning_split_waits_for_non_quiescent_modern_child_before_cleanup() {
let control = Arc::new(NonQuiescentLegacyControl::default());
let shutdown_called = Arc::new(AtomicBool::new(false));
let shutdown_observer = Arc::clone(&shutdown_called);
let release_control = Arc::clone(&control);
// Scheduling the modern child can take longer than a couple of wall
// seconds in a loaded full suite, so its start bound counts runnable
// time; a child that never starts still fails.
const START_BOUND: Duration = Duration::from_secs(5);
let release_thread = std::thread::spawn(move || {
let host = RunnableClock::start();
let waiting = host.mark();
let started = loop {
if release_control.wait_for_started(Duration::from_millis(20)) {
break Ok(());
}
if host.expired(waiting, START_BOUND) {
break Err(host.describe(waiting));
}
};
if started.is_ok() {
std::thread::sleep(
DISPATCH_WORKER_SHUTDOWN_TIMEOUT * 2 + Duration::from_millis(500),
);
}
// Preserve teardown even when the start observation misses its
// bound. The assertion belongs after release so a failed harness
// precondition cannot leave the runtime joining a parked child.
release_control.release();
started
});
let started = Instant::now();
// The handler deliberately outlives both five-second product drain
// windows. Include its start bound and two seconds for joined cleanup
// instead of expiring the harness before release.
let completion_timeout = DISPATCH_WORKER_SHUTDOWN_TIMEOUT * 2
+ Duration::from_millis(500)
+ START_BOUND
+ Duration::from_secs(2);
let run_result = run_live_split_transport(
completion_timeout,
Server::new("returning-modern-worker-shutdown-test", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.tool(NonQuiescentModernTool {
control: Arc::clone(&control),
})
.on_shutdown(move || shutdown_observer.store(true, Ordering::Release))
.build(),
NonQuiescentModernSplitRecv { phase: 0 },
NonQuiescentLegacySplitSend,
);
let elapsed = started.elapsed();
if let Err(waited) = release_thread.join().expect("release thread must finish") {
panic!("modern child must start before its delayed release: waited {waited}");
}
assert!(control.has_started());
assert!(control.has_finished());
assert!(elapsed >= DISPATCH_WORKER_SHUTDOWN_TIMEOUT);
assert!(shutdown_called.load(Ordering::Acquire));
assert!(
run_result.is_err(),
"modern worker timeout must remain a failure"
);
assert_eq!(
run_result.as_ref().map_err(String::as_str),
Err("[-32603] Server transport loop failed"),
"the product must report its drain failure after cleanup; a harness timeout is not that failure"
);
}
#[test]
fn stdio_pump_returns_failure_and_skips_hooks_for_non_quiescent_modern_child() {
let control = Arc::new(NonQuiescentLegacyControl::default());
let shutdown_called = Arc::new(AtomicBool::new(false));
let shutdown_observer = Arc::clone(&shutdown_called);
let phase = Arc::new(AtomicUsize::new(0));
let phase_for_receive = Arc::clone(&phase);
let control_for_receive = Arc::clone(&control);
let server = Server::new("stdio-modern-worker-shutdown-test", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.tool(NonQuiescentModernTool {
control: Arc::clone(&control),
})
.on_shutdown(move || shutdown_observer.store(true, Ordering::Release))
.build();
// The modern dispatch arm admits requests through spawn_blocking, so
// the pump must run under a runtime with blocking threads; a bare
// testing Cx rejects every modern dispatch before the tool starts.
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("stdio pump reactor must initialize"))
.blocking_threads(4, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("stdio pump runtime must initialize");
let (sender, receiver) = sync_channel(1);
runtime
.handle()
.try_spawn_with_cx(move |cx| async move {
let dispatch_cx = cx.clone();
let pump_sender = sender.clone();
// The pump reports its exit code through the channel from the
// blocking closure itself rather than via join(): joining the
// pump task can park forever while the deliberately
// non-quiescent modern child still occupies its blocking
// thread, and the harness only needs the exit code.
if let Err(error) = cx.spawn_blocking(move |pump_cx| {
let code = Arc::new(server).run_loop_pump_with_policy(
&pump_cx,
&dispatch_cx,
move |_receive_cx, _worker_failed| match phase_for_receive
.fetch_add(1, Ordering::AcqRel)
{
0 => Ok(modern_discovery_opening_request()),
1 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "non_quiescent_modern_tool",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
710_i64,
))),
2 if control_for_receive.wait_for_started(Duration::from_secs(2)) => {
Err(TransportError::Closed)
}
2 => Err(TransportError::Timeout),
_ => Err(TransportError::Closed),
},
move |_send_cx, _message| Ok(()),
Arc::new(|_| {}),
"stdio-modern-test",
true,
None,
true,
true,
None,
None,
None,
PumpIoMode::Split,
);
let _ = pump_sender.send(Ok(code));
}) {
let _ = sender.send(Err(format!("stdio pump admission failed: {error}")));
}
})
.expect("stdio pump task must be admitted");
// The pump legitimately holds several bounded worker-shutdown windows
// (5s each) before detaching the non-quiescent child, so the result
// wait must exceed their sum; the shared live-HTTP helper's 2s bound
// is far too tight here.
let pump_result: Result<Result<i32, String>, String> = runtime.block_on(async {
let cx = Cx::current().expect("the test runtime installs an ambient Cx");
let deadline = cx.now().saturating_add_nanos(30_000_000_000);
loop {
match receiver.try_recv() {
Ok(result) => break Ok(result),
Err(TryRecvError::Disconnected) => {
break Err("stdio pump task exited without reporting its result".to_owned());
}
Err(TryRecvError::Empty) => {
if asupersync::time::timeout_at(
deadline,
asupersync::time::sleep(cx.now(), Duration::from_millis(5)),
)
.await
.is_err()
{
break Err("stdio pump did not settle within its bound".to_owned());
}
}
}
}
});
// Capture the pre-release observations, then release the parked tool
// BEFORE any panic: a failed assert (or a pump timeout) would
// otherwise unwind into the runtime drop, which joins the
// still-parked blocking thread forever.
let started = control.has_started();
let finished_before_release = control.has_finished();
let shutdown_before_release = shutdown_called.load(Ordering::Acquire);
control.release();
let finished_after_release = control.wait_for_finished(Duration::from_secs(2));
let exit_code = pump_result
.expect("stdio pump must report a result")
.expect("stdio pump must complete");
assert_eq!(exit_code, 1);
assert!(started);
assert!(!finished_before_release);
assert!(!shutdown_before_release);
assert!(finished_after_release);
}
#[test]
fn reality_check_regression_notification_failure_wins_concurrent_clean_eof() {
let notification_failure = Arc::new(AtomicBool::new(false));
let failure_from_receive = Arc::clone(¬ification_failure);
let cx = Cx::for_testing();
let exit_code = Arc::new(Server::new("notification-eof-race-test", "1.0.0").build())
.run_loop_pump_with_policy(
&cx,
&cx,
move |_receive_cx, _worker_failed| {
// Model notification output failing immediately after a
// stop-aware receiver's final predicate check but before
// it returns the concurrently observed EOF.
failure_from_receive.store(true, Ordering::Release);
Err(TransportError::Closed)
},
move |_send_cx, _message| Ok(()),
Arc::new(|_| {}),
"stdio-test",
true,
Some(Arc::clone(¬ification_failure)),
false,
true,
None,
None,
None,
PumpIoMode::Split,
);
assert!(notification_failure.load(Ordering::Acquire));
assert_eq!(exit_code, 1);
}
// Exact-2024 era: dispatches an un-negotiated ping, which only the dual-era pump admits.
#[cfg(all(unix, feature = "legacy-2024-11-05"))]
#[test]
fn reality_check_regression_dispatch_worker_send_failure_wakes_unix_stdio_receive() {
use std::os::unix::net::UnixStream;
let (mut peer, server_stream) =
UnixStream::pair().expect("Unix stream fixture must be available");
peer.write_all(b"{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":703}\n")
.expect("request fixture must reach the server stream");
peer.flush().expect("request fixture must be flushed");
let receive_calls = Arc::new(AtomicUsize::new(0));
let receive_count = Arc::clone(&receive_calls);
let receive_count_for_worker = Arc::clone(&receive_calls);
let send_attempted = Arc::new(AtomicBool::new(false));
let send_attempted_by_worker = Arc::clone(&send_attempted);
let mut transport = StdioTransport::new(server_stream, Vec::<u8>::new());
let cx = Cx::for_testing();
let exit_code = cx.masked(|| {
Arc::new(Server::new("unix-stdio-worker-failure-test", "1.0.0").build())
.run_loop_pump_with_policy(
&cx,
&cx,
move |receive_cx, worker_failed| {
receive_count.fetch_add(1, Ordering::AcqRel);
transport.recv_until_or_stopped(receive_cx, None, || {
worker_failed.load(Ordering::Acquire)
})
},
move |_send_cx, _message| {
let deadline = Instant::now() + Duration::from_secs(2);
while receive_count_for_worker.load(Ordering::Acquire) < 2
&& Instant::now() < deadline
{
std::thread::sleep(Duration::from_millis(1));
}
send_attempted_by_worker.store(true, Ordering::Release);
Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"test response send failure",
)))
},
Arc::new(|_| {}),
"stdio-test",
true,
None,
true,
true,
None,
None,
None,
PumpIoMode::Split,
)
});
assert_eq!(exit_code, 1);
assert!(send_attempted.load(Ordering::Acquire));
assert_eq!(receive_calls.load(Ordering::Acquire), 2);
}
#[derive(Clone, Copy)]
enum ReturningProbeReceive {
Closed,
Cancelled,
Timeout,
// Exact-2024 era: an un-negotiated ping only the dual-era loop admits.
#[cfg(feature = "legacy-2024-11-05")]
PingThenClosed,
}
struct ReturningProbeTransport {
receive: ReturningProbeReceive,
fail_send: bool,
fail_close: bool,
close_calls: Arc<AtomicUsize>,
}
struct SharedTransportCountingTransport {
recv_calls: Arc<AtomicUsize>,
send_calls: Arc<AtomicUsize>,
close_calls: Arc<AtomicUsize>,
fail_close: bool,
}
struct ProtocolPolicyProbeTransport {
next: Option<JsonRpcMessage>,
sent: Arc<Mutex<Vec<JsonRpcMessage>>>,
receive_calls: Arc<AtomicUsize>,
}
// Exact-2024 era: used only by legacy HTTP SSE lifecycle tests.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveRuntimeListedTool;
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for LiveRuntimeListedTool {
fn definition(&self) -> Tool {
Tool {
name: "live_runtime_listed_tool".to_owned(),
description: Some("Proves legacy adapter runtime handler wiring".to_owned()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
Ok(vec![Content::text(format!(
"live runtime legacy adapter request {}",
ctx.request_id()
))])
}
}
struct StdioCatalogMutatingTool;
impl ToolHandler for StdioCatalogMutatingTool {
fn definition(&self) -> Tool {
Tool {
name: "stdio_catalog_mutating_tool".to_owned(),
description: Some(
"Disables itself so listen streams observe list_changed".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
if !ctx.disable_tool("stdio_catalog_mutating_tool") {
return Err(McpError::internal_error(
"stdio catalog mutation requires session state",
));
}
Ok(vec![Content::text("disabled")])
}
}
// Exact-2024 era: used only by legacy runtime-context tests.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyRuntimeConnectionTool;
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for LiveLegacyRuntimeConnectionTool {
fn definition(&self) -> Tool {
Tool {
name: "live_legacy_runtime_connection_tool".to_owned(),
description: Some(
"Proves legacy stdio handlers retain their connection runtime".to_owned(),
),
input_schema: serde_json::json!({
"type": "object",
"required": ["sample"],
"properties": {"sample": {"type": "boolean"}},
}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
let counter = ctx.get_state::<u64>("legacy-runtime-counter").unwrap_or(0) + 1;
if !ctx.set_state("legacy-runtime-counter", counter) {
return Err(McpError::internal_error(
"legacy connection state could not retain a counter",
));
}
ctx.report_progress(counter as f64, Some("legacy connection progress"));
let sample = arguments
.get("sample")
.and_then(serde_json::Value::as_bool)
.ok_or_else(|| McpError::invalid_params("sample must be a boolean"))?;
let text = if sample {
let response = block_on(ctx.sample("legacy runtime sample", 16))?;
format!("legacy-runtime-{counter}-{}", response.text)
} else {
format!("legacy-runtime-{counter}-without-sampling")
};
Ok(vec![Content::text(text)])
}
}
/// Same tool NAME and schema as `LiveLegacyRuntimeConnectionTool`, but
/// declares `ToolExecutionMode::Async` and awaits `ctx.sample` directly
/// instead of bridging it with `block_on`, so legacy HTTP dispatches it on
/// the caller-owned path rather than the blocking pool.
///
/// Note `call_async`'s trait default delegates to `call`, so declaring the
/// mode without supplying this hook would route straight back into the
/// `block_on` and be polled on the caller's runtime instead.
// Exact-2024 era: used only by legacy HTTP SSE reverse-response tests.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyReverseResponseAsyncTool;
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for LiveLegacyReverseResponseAsyncTool {
fn definition(&self) -> Tool {
LiveLegacyRuntimeConnectionTool.definition()
}
fn execution_mode(&self) -> ToolExecutionMode {
ToolExecutionMode::Async
}
fn call(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<Vec<Content>> {
Err(McpError::internal_error(
"the async reverse-response tool requires asynchronous caller-owned dispatch",
))
}
fn call_async<'a>(
&'a self,
ctx: &'a McpContext,
arguments: serde_json::Value,
) -> BoxFuture<'a, fastmcp_core::McpOutcome<Vec<Content>>> {
Box::pin(async move {
let counter = ctx.get_state::<u64>("legacy-runtime-counter").unwrap_or(0) + 1;
if !ctx.set_state("legacy-runtime-counter", counter) {
return asupersync::Outcome::Err(McpError::internal_error(
"legacy connection state could not retain a counter",
));
}
ctx.report_progress(counter as f64, Some("legacy connection progress"));
let Some(sample) = arguments.get("sample").and_then(serde_json::Value::as_bool)
else {
return asupersync::Outcome::Err(McpError::invalid_params(
"sample must be a boolean",
));
};
let text = if sample {
match ctx.sample("legacy runtime sample", 16).await {
Ok(response) => format!("legacy-runtime-{counter}-{}", response.text),
Err(error) => return asupersync::Outcome::Err(error),
}
} else {
format!("legacy-runtime-{counter}-without-sampling")
};
asupersync::Outcome::Ok(vec![Content::text(text)])
})
}
}
// Exact-2024 era: used only by legacy runtime-context tests.
#[cfg(feature = "legacy-2024-11-05")]
struct LegacyRootsContextTool;
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for LegacyRootsContextTool {
fn definition(&self) -> Tool {
Tool {
name: "legacy_roots_context_tool".to_owned(),
description: Some(
"Proves exact-2024 handlers receive only negotiated roots authority".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
let text = if ctx.can_list_roots() {
let roots = block_on(ctx.list_roots())?;
format!(
"negotiated roots: {}",
roots.first().map_or("<none>", |root| root.uri.as_str())
)
} else {
"roots authority unavailable".to_owned()
};
Ok(vec![Content::text(text)])
}
}
fn final_progress_probe_number(source: &str) -> serde_json::Number {
serde_json::from_str(source).expect("final progress probe number must parse")
}
fn report_final_progress_probe_updates(ctx: &McpContext) {
// Signed values and values beyond total are valid final progress. The
// final update differs only by a regressive progress value, so it
// must not replace the latest monotonic pending notification.
ctx.report_progress_exact(
final_progress_probe_number("-2"),
Some(final_progress_probe_number("-3")),
Some("negative"),
);
ctx.report_progress_exact(
final_progress_probe_number("12000"),
Some(final_progress_probe_number("11999")),
Some("latest"),
);
ctx.report_progress_exact(
final_progress_probe_number("11999"),
Some(final_progress_probe_number("11000")),
Some("regressive"),
);
}
struct HttpRequestScopedProgressTool {
calls: Arc<AtomicUsize>,
}
impl ToolHandler for HttpRequestScopedProgressTool {
fn definition(&self) -> Tool {
Tool {
name: "http_request_scoped_progress".to_owned(),
description: Some(
"Proves final HTTP notifications remain request-scoped".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
self.calls.fetch_add(1, Ordering::AcqRel);
report_final_progress_probe_updates(ctx);
Ok(vec![Content::text("HTTP progress completed")])
}
}
struct HttpFinalProgressCancellationTool;
impl ToolHandler for HttpFinalProgressCancellationTool {
fn definition(&self) -> Tool {
Tool {
name: "http_final_progress_cancellation".to_owned(),
description: Some(
"Proves cancellation discards staged final HTTP progress".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
report_final_progress_probe_updates(ctx);
Ok(vec![Content::text("cancellation requested")])
}
fn call_final_outcome_async_in_request<'a>(
&'a self,
ctx: &'a McpContext,
_request_cx: &'a Cx,
_arguments: serde_json::Value,
) -> BoxFuture<'a, fastmcp_core::McpOutcome<FinalToolOutcome>> {
Box::pin(async move {
report_final_progress_probe_updates(ctx);
fastmcp_core::Outcome::Cancelled(asupersync::CancelReason::user(
"final HTTP progress cancellation probe",
))
})
}
}
fn live_http_mrtr_input_required() -> fastmcp_protocol::InputRequiredResult {
let encoded = serde_json::json!({
"resultType": "input_required",
"inputRequests": {"roots": {"method": "roots/list"}},
"requestState": "handler-forged-state",
})
.to_string();
let (decoded, diagnostic) = decode_peer_result(
&encoded,
ResultPeerEra::Modern,
&CoreResultDiscriminatorPolicy,
)
.expect("test input-required result must decode");
assert!(diagnostic.is_none());
let DecodedResult::InputRequired(result) = decoded else {
panic!("test result must be input_required");
};
result
}
struct LiveHttpMrtrTool {
name: &'static str,
calls: Arc<AtomicUsize>,
}
#[derive(Clone, Copy)]
enum PublicFinalElicitationMode {
Form,
Url,
}
struct PublicFinalElicitationTool {
name: &'static str,
mode: PublicFinalElicitationMode,
calls: Arc<AtomicUsize>,
}
impl ToolHandler for PublicFinalElicitationTool {
fn definition(&self) -> Tool {
Tool {
name: self.name.to_owned(),
description: Some(
"Proves public final elicitation capability propagation".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<Vec<Content>> {
Ok(vec![Content::text("exact legacy result")])
}
fn declares_final_mrtr(&self) -> bool {
true
}
fn call_final_outcome(
&self,
ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<FinalToolOutcome> {
self.calls.fetch_add(1, Ordering::AcqRel);
let elicitation = match self.mode {
PublicFinalElicitationMode::Form => ctx.final_elicitation_form(
"approval",
"Approve this operation",
serde_json::json!({
"type": "object",
"properties": {"approved": {"type": "boolean"}},
"required": ["approved"],
}),
)?,
PublicFinalElicitationMode::Url => ctx.final_elicitation_url(
"approval",
"Approve this operation",
"https://example.com/approve",
)?,
};
Ok(FinalToolOutcome::InputRequired(
elicitation.into_input_required()?,
))
}
}
struct PublicFinalSamplingTool {
calls: Arc<AtomicUsize>,
}
impl ToolHandler for PublicFinalSamplingTool {
fn definition(&self) -> Tool {
Tool {
name: "public-final-sampling".to_owned(),
description: Some("Proves public final sampling capability propagation".to_owned()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<Vec<Content>> {
Ok(vec![Content::text("exact legacy result")])
}
fn declares_final_mrtr(&self) -> bool {
true
}
fn call_final_outcome(
&self,
ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<FinalToolOutcome> {
let sampling = ctx.final_sampling(
"sample",
serde_json::from_value(serde_json::json!({
"messages": [{
"role": "assistant",
"content": {
"type": "tool_use",
"id": "weather-request",
"name": "weather",
"input": {"city": "Boston"},
},
}],
"maxTokens": 16,
"tools": [{
"name": "weather",
"inputSchema": {"type": "object"},
}],
"toolChoice": {"mode": "required"},
}))
.map_err(|error| McpError::internal_error(error.to_string()))?,
)?;
self.calls.fetch_add(1, Ordering::AcqRel);
Ok(FinalToolOutcome::InputRequired(
sampling.into_input_required()?,
))
}
fn call_final_outcome_async_resuming_in_request<'a>(
&'a self,
ctx: &'a McpContext,
_request_cx: &'a Cx,
arguments: serde_json::Value,
resume_inputs: Option<&'a bidirectional::MrtrCompletedInputs>,
) -> BoxFuture<'a, fastmcp_core::McpOutcome<FinalToolOutcome>> {
Box::pin(async move {
let Some(inputs) = resume_inputs else {
return self
.call_final_outcome(ctx, arguments)
.map_or_else(fastmcp_core::Outcome::Err, fastmcp_core::Outcome::Ok);
};
match inputs.sampling("sample") {
Ok(Some(fastmcp_protocol::FinalCreateMessageResult {
content:
fastmcp_protocol::FinalSamplingMessageContent::Block(
fastmcp_protocol::FinalSamplingMessageContentBlock::ToolUse {
..
},
),
..
})) => {
self.calls.fetch_add(1, Ordering::AcqRel);
crate::handler::promote_legacy_tool_content(vec![Content::text(
"final sampling resumed with tool use",
)])
.map(FinalToolOutcome::Complete)
.map_or_else(fastmcp_core::Outcome::Err, fastmcp_core::Outcome::Ok)
}
Ok(Some(_)) => fastmcp_core::Outcome::Err(McpError::invalid_params(
"public final sampling test requires a tool-use result",
)),
Ok(None) => fastmcp_core::Outcome::Err(McpError::invalid_params(
"public final sampling retry lost its admitted input",
)),
Err(error) => fastmcp_core::Outcome::Err(error),
}
})
}
}
impl ToolHandler for LiveHttpMrtrTool {
fn definition(&self) -> Tool {
Tool {
name: self.name.to_owned(),
description: Some(
"Proves bound MRTR resume across stateless HTTP POSTs".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<Vec<Content>> {
Ok(vec![Content::text("live HTTP MRTR resumed")])
}
fn call_final_outcome_async_resuming_in_request<'a>(
&'a self,
ctx: &'a McpContext,
_request_cx: &'a Cx,
arguments: serde_json::Value,
resume_inputs: Option<&'a bidirectional::MrtrCompletedInputs>,
) -> BoxFuture<'a, fastmcp_core::McpOutcome<FinalToolOutcome>> {
Box::pin(async move {
self.calls.fetch_add(1, Ordering::AcqRel);
match resume_inputs {
None => asupersync::Outcome::Ok(FinalToolOutcome::InputRequired(
live_http_mrtr_input_required(),
)),
Some(inputs) => match inputs.roots("roots") {
Ok(Some(_)) => self
.call_final(ctx, arguments)
.map(FinalToolOutcome::Complete)
.map_or_else(asupersync::Outcome::Err, asupersync::Outcome::Ok),
Ok(None) => asupersync::Outcome::Err(McpError::internal_error(
"live HTTP MRTR retry lost its roots input",
)),
Err(error) => asupersync::Outcome::Err(error),
},
}
})
}
}
struct HttpSessionDisablingTool {
calls: Arc<AtomicUsize>,
}
impl ToolHandler for HttpSessionDisablingTool {
fn definition(&self) -> Tool {
Tool {
name: "http_session_disabling_tool".to_owned(),
description: None,
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
self.calls.fetch_add(1, Ordering::AcqRel);
if !ctx.disable_tool("http_session_disabling_tool") {
return Err(McpError::internal_error(
"modern HTTP tool dispatch must have session state",
));
}
Ok(vec![Content::text("disabled for this HTTP session")])
}
}
struct ModernHttpAuthCounterTool {
calls: Arc<AtomicUsize>,
}
impl ToolHandler for ModernHttpAuthCounterTool {
fn definition(&self) -> Tool {
Tool {
name: "modern_http_auth_counter".to_owned(),
description: Some("Counts authenticated modern HTTP dispatches".to_owned()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<Vec<Content>> {
self.calls.fetch_add(1, Ordering::AcqRel);
Ok(vec![Content::text("authenticated modern HTTP dispatch")])
}
}
#[derive(Clone)]
struct ModernHttpAuthMiddleware {
calls: Arc<AtomicUsize>,
saw_credential: Arc<AtomicBool>,
}
impl Middleware for ModernHttpAuthMiddleware {
fn on_request(
&self,
_ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
self.calls.fetch_add(1, Ordering::AcqRel);
let serialized = serde_json::to_string(request)
.map_err(|error| McpError::internal_error(error.to_string()))?;
if serialized.contains("Bearer alpha") || serialized.contains("Bearer beta") {
self.saw_credential.store(true, Ordering::Release);
}
Ok(MiddlewareDecision::Continue)
}
}
struct ModernHttpAuthProvider;
impl AuthProvider for ModernHttpAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
let access = request
.access_token()
.ok_or_else(|| McpError::new(McpErrorCode::ResourceForbidden, "missing bearer"))?;
match access.token.as_str() {
"alpha" | "beta" => Ok(AuthContext::with_subject(access.token)),
_ => Err(McpError::new(
McpErrorCode::ResourceForbidden,
"unrecognized bearer",
)),
}
}
}
#[derive(Clone)]
struct OneShotNativeAuthProvider {
calls: Arc<AtomicUsize>,
}
impl AuthProvider for OneShotNativeAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
// This is deliberately an attempt counter, not an accepted-token
// counter: receipt custody must prove every provider evaluation.
let call = self.calls.fetch_add(1, Ordering::AcqRel) + 1;
let access = request
.access_token()
.ok_or_else(|| McpError::new(McpErrorCode::ResourceForbidden, "missing bearer"))?;
if call == 1 && access.token.as_str() == "alpha" {
Ok(AuthContext::with_subject("one-shot-alpha"))
} else {
Err(McpError::new(
McpErrorCode::ResourceForbidden,
"provider must not be evaluated twice",
))
}
}
}
// Exact-2024 era: used only by legacy HTTP SSE auth tests.
#[cfg(feature = "legacy-2024-11-05")]
#[derive(Clone)]
struct CountingLegacyHttpAuthProvider {
calls: Arc<AtomicUsize>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl AuthProvider for CountingLegacyHttpAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
self.calls.fetch_add(1, Ordering::AcqRel);
let access = request
.access_token()
.ok_or_else(|| McpError::new(McpErrorCode::ResourceForbidden, "missing bearer"))?;
if access.token.as_str() == "alpha" {
Ok(AuthContext::with_subject("alice"))
} else {
Err(McpError::new(
McpErrorCode::ResourceForbidden,
"unrecognized bearer",
))
}
}
}
fn modern_http_json_tool_request(name: &str, id: i64) -> HttpRequest {
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": name,
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
id,
);
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/call")
.with_header("mcp-name", name)
.with_body(serde_json::to_vec(&request).expect("modern tool request must encode"))
}
fn auth_00_mrtr_provider(stable_owner: bool) -> TokenAuthProvider {
let contexts = ["alpha", "beta"].map(|token| {
let mut auth = AuthContext::with_subject(token);
if stable_owner {
auth = auth.with_session_owner(
fastmcp_core::sha256_bounded(token.as_bytes(), 32)
.expect("bounded configured owner"),
);
}
(token, auth)
});
TokenAuthProvider::new(
StaticTokenVerifier::new(contexts).expect("valid static token registration"),
)
}
async fn auth_00_mrtr_http_call(
cx: &Cx,
endpoint: &ServerHttpEndpoint,
state: Option<&str>,
id: i64,
) -> JsonRpcResponse {
let mut request = modern_http_json_tool_request("live_http_mrtr", id)
.with_header("authorization", "Bearer alpha");
if let Some(state) = state {
let mut rpc: JsonRpcRequest = serde_json::from_slice(&request.body).unwrap();
let params = rpc.params.as_mut().unwrap();
params["requestState"] = serde_json::json!(state);
params["inputResponses"] = serde_json::json!({
"roots": bidirectional::MrtrInputResponse::roots(
fastmcp_protocol::ListRootsResult::empty(),
).expect("valid roots response"),
});
request.body = serde_json::to_vec(&rpc).unwrap();
}
let mut session = endpoint.open_session(cx).expect("fresh HTTP session");
let response = asupersync::time::timeout(
cx.now(),
Duration::from_secs(5),
Box::pin(session.handle_async(cx, request)),
)
.await
.expect("MRTR HTTP handling must finish before the harness deadline")
.expect("HTTP handling result");
session.close(cx).await;
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("ordinary MRTR request must return an immediate JSON response");
};
assert_eq!(response.status, HttpStatus::OK);
let response: JsonRpcResponse = serde_json::from_slice(&response.body).unwrap();
assert_eq!(response.id, Some(id.into()));
response
}
/// How the MRTR test server's provider supplies the session owner.
#[derive(Clone, Copy)]
enum Auth00Owner {
/// The verifier configures an explicit provider-scoped owner.
Configured,
/// A subject-only verified context. `TokenAuthProvider` derives its
/// owner in a process-local namespace (756719c3).
Derived,
/// The same verified subject-only context, with no owner at all.
Absent,
}
/// Verifies the same credentials through `TokenAuthProvider`, then
/// rebuilds the admitted context without the owner that provider
/// derives. Owner admission therefore sees a verified subject that
/// really has no stable owner.
struct OwnerlessAuthProvider(TokenAuthProvider);
impl AuthProvider for OwnerlessAuthProvider {
fn authenticate(
&self,
ctx: &McpContext,
request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
let admitted = self.0.authenticate(ctx, request)?;
let subject = admitted
.subject
.clone()
.ok_or_else(|| McpError::internal_error("admitted context has no subject"))?;
let mut ownerless = AuthContext::with_subject(subject);
ownerless.scopes = admitted.scopes;
ownerless.claims = admitted.claims;
Ok(ownerless)
}
}
fn auth_00_mrtr_server(owner: Auth00Owner, calls: &Arc<AtomicUsize>) -> Server {
let builder = Server::new("auth-00-mrtr-owner", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern policy");
let builder = match owner {
Auth00Owner::Configured => builder.auth_provider(auth_00_mrtr_provider(true)),
Auth00Owner::Derived => builder.auth_provider(auth_00_mrtr_provider(false)),
Auth00Owner::Absent => {
builder.auth_provider(OwnerlessAuthProvider(auth_00_mrtr_provider(false)))
}
};
builder
.tool(LiveHttpMrtrTool {
name: "live_http_mrtr",
calls: Arc::clone(calls),
})
.build()
}
fn assert_auth_00_mrtr_complete(response: &JsonRpcResponse) {
assert!(response.error.is_none());
let result = response.result.as_ref().expect("completed MRTR result");
assert_eq!(result["resultType"], "complete");
assert_eq!(
result["content"],
serde_json::json!([{"type": "text", "text": "live HTTP MRTR resumed"}]),
);
}
#[test]
fn auth_00_http_mrtr_stable_owner_completes_two_rounds() {
assert_auth_00_http_mrtr_completes_two_rounds(Auth00Owner::Configured);
}
/// Pins 756719c3: `TokenAuthProvider` derives an owner for a subject-only
/// verified context, so the same two rounds complete without a
/// verifier-configured owner.
#[test]
fn auth_00_http_mrtr_derived_owner_completes_two_rounds() {
assert_auth_00_http_mrtr_completes_two_rounds(Auth00Owner::Derived);
}
fn assert_auth_00_http_mrtr_completes_two_rounds(owner: Auth00Owner) {
run_live_http_test(move |cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = auth_00_mrtr_server(owner, &calls)
.test_http_endpoint("http://auth-owner.test")
.unwrap();
let initial = auth_00_mrtr_http_call(&cx, &endpoint, None, 981).await;
assert!(initial.error.is_none());
let result = initial.result.as_ref().unwrap();
assert_eq!(result["resultType"], "input_required");
assert_eq!(result["inputRequests"]["roots"]["method"], "roots/list");
let state = result["requestState"].as_str().unwrap();
assert!(!state.is_empty());
assert_ne!(state, "handler-forged-state");
assert_eq!(endpoint.server.router.test_active_mrtr_exchange_count(), 1);
assert_eq!(calls.load(Ordering::Acquire), 1);
let retry = auth_00_mrtr_http_call(&cx, &endpoint, Some(state), 982).await;
assert_auth_00_mrtr_complete(&retry);
assert_eq!(calls.load(Ordering::Acquire), 2);
assert_eq!(endpoint.server.router.test_active_mrtr_exchange_count(), 0);
Ok(())
});
}
#[test]
fn auth_00_http_mrtr_subject_without_owner_rejects_without_allocating() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
// Same valid credential, subject, request and handler as the
// positive; only the provider's stable owner is absent.
let endpoint = auth_00_mrtr_server(Auth00Owner::Absent, &calls)
.test_http_endpoint("http://auth-owner.test")
.unwrap();
let response = auth_00_mrtr_http_call(&cx, &endpoint, None, 981).await;
assert!(response.result.is_none());
let error = response.error.as_ref().expect("owner admission rejection");
assert_eq!(error.code, McpErrorCode::InvalidParams.into());
assert_eq!(calls.load(Ordering::Acquire), 1);
assert_eq!(endpoint.server.router.test_active_mrtr_exchange_count(), 0);
Ok(())
});
}
#[test]
fn auth_00_http_mrtr_ownerless_leaked_handle_preserves_owner_state() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let owner_server = auth_00_mrtr_server(Auth00Owner::Configured, &calls);
let mut ownerless_server = auth_00_mrtr_server(Auth00Owner::Absent, &calls);
// Both public endpoints use the same real exchange registry.
// Independent registries would reject even without owner checks.
ownerless_server.router = Arc::clone(&owner_server.router);
let owner = owner_server
.test_http_endpoint("http://auth-owner.test")
.unwrap();
let ownerless = ownerless_server
.test_http_endpoint("http://auth-owner.test")
.unwrap();
let initial = auth_00_mrtr_http_call(&cx, &owner, None, 991).await;
assert!(initial.error.is_none());
let state = initial.result.as_ref().unwrap()["requestState"]
.as_str()
.unwrap();
assert_eq!(owner.server.router.test_active_mrtr_exchange_count(), 1);
let denied = auth_00_mrtr_http_call(&cx, &ownerless, Some(state), 992).await;
let unknown = auth_00_mrtr_http_call(&cx, &ownerless, Some("unknown"), 993).await;
assert!(denied.result.is_none());
assert!(unknown.result.is_none());
let denied_error = denied.error.as_ref().unwrap();
assert_eq!(denied_error.code, McpErrorCode::InvalidParams.into());
assert_eq!(denied_error, unknown.error.as_ref().unwrap());
assert_eq!(calls.load(Ordering::Acquire), 1);
assert_eq!(owner.server.router.test_active_mrtr_exchange_count(), 1);
let retry = auth_00_mrtr_http_call(&cx, &owner, Some(state), 994).await;
assert_auth_00_mrtr_complete(&retry);
assert_eq!(calls.load(Ordering::Acquire), 2);
assert_eq!(owner.server.router.test_active_mrtr_exchange_count(), 0);
Ok(())
});
}
#[test]
fn auth_00_http_mrtr_anonymous_complete_remains_available() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = Server::new("auth-00-anonymous-complete", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.unwrap()
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&calls),
})
.test_http_endpoint("http://auth-owner.test")
.unwrap();
let request = modern_http_json_tool_request("modern_http_auth_counter", 995);
let mut session = endpoint.open_session(&cx).unwrap();
let response = asupersync::time::timeout(
cx.now(),
Duration::from_secs(5),
Box::pin(session.handle_async(&cx, request)),
)
.await
.expect("anonymous complete call must finish before the harness deadline")
.unwrap();
session.close(&cx).await;
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("anonymous complete call must return JSON");
};
assert_eq!(response.status, HttpStatus::OK);
let rpc: JsonRpcResponse = serde_json::from_slice(&response.body).unwrap();
assert_eq!(rpc.id, Some(995_i64.into()));
assert!(rpc.error.is_none());
let result = rpc.result.as_ref().unwrap();
assert_eq!(result["resultType"], "complete");
assert_eq!(
result["content"],
serde_json::json!([{
"type": "text",
"text": "authenticated modern HTTP dispatch",
}]),
);
assert_eq!(calls.load(Ordering::Acquire), 1);
assert_eq!(endpoint.server.router.test_active_mrtr_exchange_count(), 0);
Ok(())
});
}
async fn auth_00_handle_mrtr_http_request(
cx: &Cx,
session: &mut ServerHttpSession,
request: HttpRequest,
) -> JsonRpcResponse {
let response = asupersync::time::timeout(
cx.now(),
Duration::from_secs(5),
Box::pin(session.handle_async(cx, request)),
)
.await
.expect("MRTR owner lookup must finish before the harness deadline")
.expect("MRTR HTTP handling must succeed");
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("MRTR owner lookup must return immediate JSON");
};
assert_eq!(response.status, HttpStatus::OK);
serde_json::from_slice(&response.body).expect("MRTR response must be JSON-RPC")
}
fn auth_00_http_mrtr_cancelled_state_probe(foreign_owner: bool) {
run_live_http_test(move |cx| async move {
for state_only in [false, true] {
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = auth_00_mrtr_server(Auth00Owner::Configured, &calls)
.test_http_endpoint("http://auth-owner.test")
.unwrap();
let mut issuer = endpoint.open_session(&cx).unwrap();
let initial = auth_00_handle_mrtr_http_request(
&cx,
&mut issuer,
modern_http_json_tool_request("live_http_mrtr", 1001)
.with_header("authorization", "Bearer alpha"),
)
.await;
assert_eq!(initial.id, Some(1001_i64.into()));
assert!(initial.error.is_none());
let result = initial.result.as_ref().unwrap();
assert_eq!(result["resultType"], "input_required");
assert_eq!(result["inputRequests"]["roots"]["method"], "roots/list");
let state = result["requestState"].as_str().unwrap();
assert!(!state.is_empty());
assert_ne!(state, "handler-forged-state");
assert_eq!(calls.load(Ordering::Acquire), 1);
// Cancel the real continuation owner, which is distinct from
// the already-finalized JSON-RPC request cancellation token.
issuer.modern_connection.disconnect();
let retry = |request_state: &str, bearer: &str, id: i64| {
let mut request = modern_http_json_tool_request("live_http_mrtr", id)
.with_header("authorization", format!("Bearer {bearer}"));
let mut rpc: JsonRpcRequest = serde_json::from_slice(&request.body).unwrap();
let params = rpc.params.as_mut().unwrap();
params["requestState"] = serde_json::json!(request_state);
if !state_only {
params["inputResponses"] = serde_json::json!({
"roots": bidirectional::MrtrInputResponse::roots(
fastmcp_protocol::ListRootsResult::empty(),
).unwrap(),
});
}
request.body = serde_json::to_vec(&rpc).unwrap();
request
};
let bearer = if foreign_owner { "beta" } else { "alpha" };
let mut caller = endpoint.open_session(&cx).unwrap();
let observed =
auth_00_handle_mrtr_http_request(&cx, &mut caller, retry(state, bearer, 1002))
.await;
assert_eq!(observed.id, Some(1002_i64.into()));
assert!(observed.result.is_none());
let observed_error = observed.error.as_ref().unwrap();
assert_eq!(
observed_error.code,
if foreign_owner {
McpErrorCode::InvalidParams.into()
} else {
McpErrorCode::RequestCancelled.into()
}
);
let unknown = auth_00_handle_mrtr_http_request(
&cx,
&mut caller,
retry("unknown", bearer, 1003),
)
.await;
assert_eq!(unknown.id, Some(1003_i64.into()));
assert!(unknown.result.is_none());
let unknown_error = unknown.error.as_ref().unwrap();
assert_eq!(unknown_error.code, McpErrorCode::InvalidParams.into());
if foreign_owner {
assert_eq!(observed_error, unknown_error);
}
assert_eq!(calls.load(Ordering::Acquire), 1);
let mut owner = endpoint.open_session(&cx).unwrap();
let retirement =
auth_00_handle_mrtr_http_request(&cx, &mut owner, retry(state, "alpha", 1004))
.await;
assert_eq!(retirement.id, Some(1004_i64.into()));
assert!(retirement.result.is_none());
assert_eq!(
retirement.error.as_ref().unwrap().code,
if foreign_owner {
// The foreign lookup must not erase the cancelled
// record before its rightful owner observes it.
McpErrorCode::RequestCancelled.into()
} else {
McpErrorCode::InvalidParams.into()
}
);
let repeated =
auth_00_handle_mrtr_http_request(&cx, &mut owner, retry(state, "alpha", 1005))
.await;
assert_eq!(repeated.id, Some(1005_i64.into()));
assert!(repeated.result.is_none());
assert_eq!(repeated.error.as_ref().unwrap(), unknown_error);
assert_eq!(calls.load(Ordering::Acquire), 1);
assert!(cx.checkpoint().is_ok());
// Do not use active_len() here: it purges cancelled records
// and would erase the evidence before the authorized retry.
owner.close(&cx).await;
caller.close(&cx).await;
issuer.close(&cx).await;
}
Ok(())
});
}
#[test]
fn auth_00_http_mrtr_cancelled_state_owner_retires_positive() {
auth_00_http_mrtr_cancelled_state_probe(false);
}
#[test]
fn auth_00_http_mrtr_cancelled_state_foreign_owner_preserves_negative() {
auth_00_http_mrtr_cancelled_state_probe(true);
}
struct Auth00GrantMrtrTool {
calls: Arc<AtomicUsize>,
state_only: bool,
}
impl ToolHandler for Auth00GrantMrtrTool {
fn definition(&self) -> Tool {
Tool {
name: "auth_00_grant_mrtr".to_owned(),
description: None,
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
if !ctx
.auth()
.is_some_and(|auth| auth.scopes.iter().any(|scope| scope == "private:read"))
{
return Err(McpError::new(
McpErrorCode::ResourceForbidden,
"The private read grant is required",
));
}
let subject = arguments["subject"]
.as_str()
.ok_or_else(|| McpError::invalid_params("A subject is required"))?;
Ok(vec![Content::text(subject)])
}
fn declares_final_mrtr(&self) -> bool {
true
}
fn call_final_outcome_async_resuming_in_request<'a>(
&'a self,
ctx: &'a McpContext,
_request_cx: &'a Cx,
arguments: serde_json::Value,
resume_inputs: Option<&'a bidirectional::MrtrCompletedInputs>,
) -> BoxFuture<'a, fastmcp_core::McpOutcome<FinalToolOutcome>> {
Box::pin(async move {
self.calls.fetch_add(1, Ordering::AcqRel);
let result = (|| {
let content = self.call(ctx, arguments)?;
let Some(inputs) = resume_inputs else {
let requests = if self.state_only {
None
} else {
live_http_mrtr_input_required().input_requests().cloned()
};
return Ok(FinalToolOutcome::InputRequired(
fastmcp_protocol::InputRequiredResult::new(
requests,
Some("grant-bound-private-state".to_owned()),
fastmcp_protocol::ResultMeta::empty(),
)
.expect("real state-only or roots continuation"),
));
};
assert_eq!(
inputs.handler_request_state(),
Some("grant-bound-private-state"),
);
if self.state_only {
assert!(inputs.responses().is_empty());
} else {
assert_eq!(inputs.responses().len(), 1);
assert_eq!(
serde_json::to_value(
inputs.roots("roots")?.expect("admitted roots input"),
)
.expect("roots response serializes"),
serde_json::json!({"roots": []}),
);
}
crate::handler::promote_legacy_tool_content(content)
.map(FinalToolOutcome::Complete)
})();
result.map_or_else(asupersync::Outcome::Err, asupersync::Outcome::Ok)
})
}
}
fn auth_00_http_mrtr_grant_snapshot_probe(reduced_grants: bool) {
run_live_http_test(move |cx| async move {
for state_only in [false, true] {
let calls = Arc::new(AtomicUsize::new(0));
let owner = fastmcp_core::sha256_bounded(b"grant-snapshot-owner", 32).unwrap();
let mut initial_auth =
AuthContext::with_subject("same-owner").with_session_owner(owner);
initial_auth.scopes = vec!["private:read".to_owned(), "public:list".to_owned()];
let mut retry_auth = initial_auth.clone();
// Both tests rotate alpha to beta. Only the required grant
// differs; reordering and duplicate grants remain equivalent.
retry_auth.scopes = vec!["public:list".to_owned(), "public:list".to_owned()];
if !reduced_grants {
retry_auth.scopes.push("private:read".to_owned());
}
let provider = TokenAuthProvider::new(
StaticTokenVerifier::new([("alpha", initial_auth), ("beta", retry_auth)])
.expect("real same-owner token registrations"),
);
let endpoint = Server::new("auth-00-grants", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.unwrap()
.auth_provider(provider)
.tool(Auth00GrantMrtrTool {
calls: Arc::clone(&calls),
state_only,
})
.test_http_endpoint("http://auth-grants.test")
.unwrap();
let subject = format!("private-result-{}-{state_only}", cx.now().as_nanos());
let request = |request_state: Option<&str>, bearer: &str, id: i64| {
let mut request = modern_http_json_tool_request("auth_00_grant_mrtr", id)
.with_header("authorization", format!("Bearer {bearer}"));
let mut rpc: JsonRpcRequest = serde_json::from_slice(&request.body).unwrap();
let params = rpc.params.as_mut().unwrap();
params["arguments"] = serde_json::json!({"subject": subject});
if let Some(state) = request_state {
params["requestState"] = serde_json::json!(state);
if !state_only {
params["inputResponses"] = serde_json::json!({
"roots": bidirectional::MrtrInputResponse::roots(
fastmcp_protocol::ListRootsResult::empty(),
).unwrap(),
});
}
}
request.body = serde_json::to_vec(&rpc).unwrap();
request
};
let mut issuer = endpoint.open_session(&cx).unwrap();
let initial = auth_00_handle_mrtr_http_request(
&cx,
&mut issuer,
request(None, "alpha", 1011),
)
.await;
assert_eq!(initial.id, Some(1011_i64.into()));
assert!(initial.error.is_none());
let initial_result = initial.result.as_ref().unwrap();
assert_eq!(initial_result["resultType"], "input_required");
if state_only {
assert!(initial_result.get("inputRequests").is_none());
} else {
assert_eq!(
initial_result["inputRequests"],
serde_json::json!({"roots": {"method": "roots/list"}}),
);
}
let state = initial_result["requestState"].as_str().unwrap();
assert!(!state.is_empty());
assert_ne!(state, "grant-bound-private-state");
assert_eq!(calls.load(Ordering::Acquire), 1);
issuer.close(&cx).await;
let mut caller = endpoint.open_session(&cx).unwrap();
let retry = auth_00_handle_mrtr_http_request(
&cx,
&mut caller,
request(Some(state), "beta", 1012),
)
.await;
assert_eq!(retry.id, Some(1012_i64.into()));
let unknown = auth_00_handle_mrtr_http_request(
&cx,
&mut caller,
request(Some("unknown"), "beta", 1013),
)
.await;
assert_eq!(unknown.id, Some(1013_i64.into()));
assert!(unknown.result.is_none());
let unknown_error = unknown.error.as_ref().unwrap();
assert_eq!(unknown_error.code, McpErrorCode::InvalidParams.into());
let completed = if reduced_grants {
assert!(retry.result.is_none());
assert_eq!(retry.error.as_ref().unwrap(), unknown_error);
assert_eq!(calls.load(Ordering::Acquire), 1);
let mut rightful = endpoint.open_session(&cx).unwrap();
let response = auth_00_handle_mrtr_http_request(
&cx,
&mut rightful,
request(Some(state), "alpha", 1014),
)
.await;
assert_eq!(response.id, Some(1014_i64.into()));
rightful.close(&cx).await;
response
} else {
retry
};
assert!(completed.error.is_none());
let result = completed.result.as_ref().unwrap();
assert_eq!(result["resultType"], "complete");
assert_eq!(
result["content"],
serde_json::json!([{"type": "text", "text": subject}]),
);
assert_eq!(calls.load(Ordering::Acquire), 2);
let replay = auth_00_handle_mrtr_http_request(
&cx,
&mut caller,
request(Some(state), "beta", 1015),
)
.await;
assert_eq!(replay.id, Some(1015_i64.into()));
assert!(replay.result.is_none());
assert_eq!(replay.error.as_ref().unwrap(), unknown_error);
assert_eq!(calls.load(Ordering::Acquire), 2);
assert!(cx.checkpoint().is_ok());
caller.close(&cx).await;
}
Ok(())
});
}
#[test]
fn auth_00_http_mrtr_unchanged_grants_rotation_completes_positive() {
auth_00_http_mrtr_grant_snapshot_probe(false);
}
#[test]
fn auth_00_http_mrtr_reduced_grants_preserve_state_negative() {
auth_00_http_mrtr_grant_snapshot_probe(true);
}
async fn run_live_http_mrtr_retry(
cx: &Cx,
request_state_suffix: &str,
) -> Result<(JsonRpcResponse, JsonRpcResponse, usize), String> {
let calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-http-mrtr-retry", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(auth_00_mrtr_provider(true))
.tool(LiveHttpMrtrTool {
name: "live_http_mrtr",
calls: Arc::clone(&calls),
})
.build()
.bind_http(cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live HTTP MRTR retry bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live HTTP MRTR retry address failed: {error}"))?;
let request_state_suffix = request_state_suffix.to_owned();
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let result = async {
let initial = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_http_mrtr",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "live-http-mrtr-sse",
},
})),
922_i64,
);
let initial_body = serde_json::to_vec(&initial).map_err(|error| {
format!("MRTR initial request did not serialize: {error}")
})?;
let initial = live_http_exchange(
address,
live_http_post(
"/mcp",
&initial_body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_http_mrtr"),
("Authorization", "Bearer alpha"),
],
),
)
.await?;
let initial = live_http_sse_jsonrpc_response(&initial)?;
let request_state = initial
.result
.as_ref()
.and_then(|result| result.get("requestState"))
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
"MRTR input-required response omitted requestState".to_owned()
})?;
let roots = serde_json::to_value(
bidirectional::MrtrInputResponse::roots(
fastmcp_protocol::ListRootsResult::empty(),
)
.map_err(|error| format!("MRTR roots response failed: {error}"))?,
)
.map_err(|error| format!("MRTR roots response did not serialize: {error}"))?;
// The retry leg reads its result from an ordinary JSON
// response; a progress token would demand an owned SSE
// body and turn this Accept into a 406.
let retry = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_http_mrtr",
"arguments": {},
"inputResponses": {"roots": roots},
"requestState": format!("{request_state}{request_state_suffix}"),
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
923_i64,
);
let retry_body = serde_json::to_vec(&retry).map_err(|error| {
format!("MRTR retry request did not serialize: {error}")
})?;
let retry = live_http_exchange(
address,
live_http_post(
"/mcp",
&retry_body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_http_mrtr"),
("Authorization", "Bearer alpha"),
],
),
)
.await?;
let retry = serde_json::from_slice(live_http_response_body(&retry)?).map_err(
|error| format!("MRTR retry response was not valid JSON-RPC: {error}"),
)?;
Ok::<_, String>((initial, retry))
}
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("live HTTP cross-POST MRTR retry complete"),
);
result
})
.map_err(|error| format!("live HTTP MRTR retry client admission failed: {error}"))?;
let serve = bound.serve(cx).await;
let (initial, retry) = client
.join(cx)
.await
.map_err(|error| format!("live HTTP MRTR retry client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("live HTTP MRTR retry server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live HTTP MRTR retry").await?;
Ok((initial, retry, calls.load(Ordering::Acquire)))
}
async fn run_live_http_public_final_sampling_json(
cx: &Cx,
) -> Result<(JsonRpcResponse, usize), String> {
let calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-http-public-final-sampling", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(auth_00_mrtr_provider(true))
.tool(PublicFinalSamplingTool {
calls: Arc::clone(&calls),
})
.build()
.bind_http(cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live HTTP public final sampling bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live HTTP public final sampling address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let result = async {
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "public-final-sampling",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {"sampling": {}},
},
})),
940_i64,
);
let body = serde_json::to_vec(&request).map_err(|error| {
format!("public final sampling request did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "public-final-sampling"),
("Authorization", "Bearer alpha"),
],
),
)
.await?;
let response = serde_json::from_slice(live_http_response_body(&response)?)
.map_err(|error| {
format!(
"public final sampling response was not valid JSON-RPC: {error}"
)
})?;
Ok::<_, String>(response)
}
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("live HTTP public final sampling complete"),
);
result
})
.map_err(|error| {
format!("live HTTP public final sampling client admission failed: {error}")
})?;
let serve = bound.serve(cx).await;
let response = client.join(cx).await.map_err(|error| {
format!("live HTTP public final sampling client failed: {error:?}")
})??;
let shutdown = serve
.map_err(|error| format!("live HTTP public final sampling server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live HTTP public final sampling").await?;
Ok((response, calls.load(Ordering::Acquire)))
}
struct LiveLegacyCompletionHandler;
impl CompletionHandler for LiveLegacyCompletionHandler {
fn complete_legacy(
&self,
_ctx: &McpContext,
params: LegacyCompletionParams,
) -> McpResult<CompletionValues> {
Ok(CompletionValues {
values: vec![format!("legacy:{}", params.argument.value)],
total: Some(1),
has_more: Some(false),
})
}
fn complete_final(
&self,
_ctx: &McpContext,
_params: FinalCompletionParams,
) -> McpResult<fastmcp_protocol::FinalCompletionValues> {
Err(McpError::method_not_found("completion/complete"))
}
}
// Exact-2024 era: used only by legacy completion tests.
#[cfg(feature = "legacy-2024-11-05")]
struct CountingLegacyCompletionHandler {
calls: Arc<AtomicUsize>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl CompletionHandler for CountingLegacyCompletionHandler {
fn complete_legacy(
&self,
_ctx: &McpContext,
params: LegacyCompletionParams,
) -> McpResult<CompletionValues> {
self.calls.fetch_add(1, Ordering::AcqRel);
Ok(CompletionValues {
values: vec![format!("legacy:{}", params.argument.value)],
total: Some(1),
has_more: Some(false),
})
}
fn complete_final(
&self,
_ctx: &McpContext,
_params: FinalCompletionParams,
) -> McpResult<fastmcp_protocol::FinalCompletionValues> {
Err(McpError::method_not_found("completion/complete"))
}
}
// Exact-2024 era: used only by legacy HTTP SSE cancellation tests.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyCancellationTool {
started: Arc<AtomicBool>,
observed_cancellation: Arc<AtomicBool>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for LiveLegacyCancellationTool {
fn definition(&self) -> Tool {
Tool {
name: "live_legacy_cancellation_tool".to_owned(),
description: Some(
"Proves exact legacy cancellation reaches the live handler".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
self.started.store(true, Ordering::Release);
let deadline = Instant::now() + Duration::from_nanos(LIVE_HTTP_STEP_TIMEOUT_NANOS);
while !ctx.is_cancelled() && Instant::now() < deadline {
std::thread::yield_now();
}
if ctx.is_cancelled() {
self.observed_cancellation.store(true, Ordering::Release);
return Err(McpError::request_cancelled());
}
Err(McpError::internal_error(
"exact legacy cancellation did not reach the live handler",
))
}
}
struct LiveModernHttpPeerCancellationTool {
started: Arc<AtomicBool>,
release: Arc<AtomicBool>,
observed_cancellation: Arc<AtomicBool>,
}
impl ToolHandler for LiveModernHttpPeerCancellationTool {
fn definition(&self) -> Tool {
Tool {
name: "live_modern_http_peer_cancellation_tool".to_owned(),
description: Some(
"Proves ordinary modern JSON peer close reaches only its handler".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
self.started.store(true, Ordering::Release);
let deadline = Instant::now() + Duration::from_nanos(LIVE_HTTP_STEP_TIMEOUT_NANOS);
while !self.release.load(Ordering::Acquire)
&& !ctx.is_cancelled()
&& Instant::now() < deadline
{
std::thread::yield_now();
}
if ctx.is_cancelled() {
self.observed_cancellation.store(true, Ordering::Release);
return Err(McpError::request_cancelled());
}
if !self.release.load(Ordering::Acquire) {
return Err(McpError::internal_error(
"ordinary modern JSON peer cancellation did not reach the handler",
));
}
Ok(vec![Content::text(
"ordinary modern JSON request completed",
)])
}
}
// Exact-2024 era: used only by the legacy HTTP SSE shutdown probe.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveHttpShutdownTool {
name: &'static str,
cooperate_with_cancellation: bool,
started: Arc<AtomicBool>,
cancellation_observed: Arc<AtomicBool>,
release: Arc<AtomicBool>,
finished: Arc<AtomicBool>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for LiveHttpShutdownTool {
fn definition(&self) -> Tool {
Tool {
name: self.name.to_owned(),
description: Some(
"Proves live HTTP shutdown ownership for a synchronous handler".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
self.started.store(true, Ordering::Release);
loop {
if ctx.is_cancelled() {
self.cancellation_observed.store(true, Ordering::Release);
if self.cooperate_with_cancellation {
self.finished.store(true, Ordering::Release);
return Err(McpError::request_cancelled());
}
}
if self.release.load(Ordering::Acquire) {
self.finished.store(true, Ordering::Release);
return Ok(Vec::new());
}
thread::yield_now();
thread::sleep(Duration::from_millis(1));
}
}
}
// Exact-2024 era: gates legacy HTTP request handlers.
#[cfg(feature = "legacy-2024-11-05")]
#[derive(Default)]
struct LiveLegacyRequestGate {
entered: AtomicBool,
released: AtomicBool,
}
#[cfg(feature = "legacy-2024-11-05")]
impl LiveLegacyRequestGate {
fn enter(&self, ctx: &McpContext) -> McpResult<()> {
self.entered.store(true, Ordering::Release);
let deadline = Instant::now() + Duration::from_nanos(LIVE_HTTP_STEP_TIMEOUT_NANOS);
while !self.released.load(Ordering::Acquire) && Instant::now() < deadline {
if ctx.is_cancelled() {
return Err(McpError::request_cancelled());
}
thread::yield_now();
thread::sleep(Duration::from_millis(1));
}
if self.released.load(Ordering::Acquire) {
Ok(())
} else {
Err(McpError::internal_error(
"bounded legacy HTTP request gate was not released",
))
}
}
fn release(&self) {
self.released.store(true, Ordering::Release);
}
}
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyRequestGateRelease {
gate: Arc<LiveLegacyRequestGate>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl Drop for LiveLegacyRequestGateRelease {
fn drop(&mut self) {
self.gate.release();
}
}
// Exact-2024 era: holds a legacy HTTP session's mutex.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveHttpSessionMutexHold {
release: Option<std::sync::mpsc::SyncSender<()>>,
worker: Option<thread::JoinHandle<Result<(), String>>>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl LiveHttpSessionMutexHold {
fn new(session: Arc<LiveHttpSession>) -> Result<Self, String> {
let (started_sender, started_receiver) = sync_channel(1);
let (release_sender, release_receiver) = sync_channel(1);
let worker = thread::spawn(move || {
let _session = session
.session
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
started_sender
.send(())
.map_err(|_| "legacy HTTP session holder lost its test owner".to_string())?;
release_receiver
.recv_timeout(Duration::from_nanos(LIVE_HTTP_STEP_TIMEOUT_NANOS))
.map_err(|_| "legacy HTTP session holder release timed out".to_string())?;
Ok(())
});
let mut hold = Self {
release: Some(release_sender),
worker: Some(worker),
};
if started_receiver
.recv_timeout(Duration::from_nanos(LIVE_HTTP_STEP_TIMEOUT_NANOS))
.is_err()
{
let _ = hold.release();
return Err("legacy HTTP session holder did not acquire the mutex".to_string());
}
Ok(hold)
}
fn release(&mut self) -> Result<(), String> {
if let Some(sender) = self.release.take() {
sender.send(()).map_err(|_| {
"legacy HTTP session holder release was disconnected".to_string()
})?;
}
let Some(worker) = self.worker.take() else {
return Ok(());
};
worker
.join()
.map_err(|_| "legacy HTTP session holder panicked".to_string())?
}
}
#[cfg(feature = "legacy-2024-11-05")]
impl Drop for LiveHttpSessionMutexHold {
fn drop(&mut self) {
let _ = self.release();
}
}
// Exact-2024 era: used only by legacy HTTP admission tests.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyAdmissionBlocker {
gate: Arc<LiveLegacyRequestGate>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for LiveLegacyAdmissionBlocker {
fn definition(&self) -> Tool {
Tool {
name: "live_legacy_admission_blocker".to_owned(),
description: Some(
"Holds the exact legacy HTTP session mutex at a bounded test gate".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
self.gate.enter(ctx)?;
Ok(vec![Content::text("legacy admission blocker released")])
}
}
// Exact-2024 era: used only by legacy HTTP admission tests.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyAdmissionTarget {
calls: Arc<AtomicUsize>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for LiveLegacyAdmissionTarget {
fn definition(&self) -> Tool {
Tool {
name: "live_legacy_admission_target".to_owned(),
description: Some(
"Records whether a queued exact legacy HTTP request entered its handler"
.to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
self.calls.fetch_add(1, Ordering::AcqRel);
ctx.checkpoint()
.map_err(|_| McpError::request_cancelled())?;
Ok(vec![Content::text("legacy admission target entered")])
}
}
#[derive(Default)]
struct NonQuiescentLegacyState {
started: bool,
released: bool,
finished: bool,
}
#[derive(Default)]
struct NonQuiescentLegacyControl {
state: Mutex<NonQuiescentLegacyState>,
changed: Condvar,
}
impl NonQuiescentLegacyControl {
/// How long a deliberately non-cooperative handler waits for its test
/// to release it. A test that fails before releasing would otherwise
/// leave the handler thread blocked and hang the process.
const RELEASE_BOUND: Duration = Duration::from_secs(60);
fn wait_until_released(&self) {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut state = state;
state.started = true;
self.changed.notify_all();
let (mut state, waited) = self
.changed
.wait_timeout_while(state, Self::RELEASE_BOUND, |state| !state.released)
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert!(
!waited.timed_out(),
"non-quiescent test handler was not released within {:?}; its test stopped before releasing it",
Self::RELEASE_BOUND
);
state.finished = true;
self.changed.notify_all();
}
fn wait_for_started(&self, timeout: Duration) -> bool {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (state, _) = self
.changed
.wait_timeout_while(state, timeout, |state| !state.started)
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.started
}
fn has_started(&self) -> bool {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.started
}
fn has_finished(&self) -> bool {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.finished
}
fn release(&self) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.released = true;
self.changed.notify_all();
}
fn wait_for_finished(&self, timeout: Duration) -> bool {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (state, _) = self
.changed
.wait_timeout_while(state, timeout, |state| !state.finished)
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.finished
}
}
// Exact-2024 era: reached only through a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
struct NonQuiescentLegacyTool {
control: Arc<NonQuiescentLegacyControl>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for NonQuiescentLegacyTool {
fn definition(&self) -> Tool {
Tool {
name: "non_quiescent_legacy_tool".to_owned(),
description: Some(
"Test-only legacy handler that deliberately ignores cancellation".to_owned(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<Vec<Content>> {
self.control.wait_until_released();
Ok(vec![Content::text("non-quiescent legacy handler released")])
}
}
struct NonQuiescentModernTool {
control: Arc<NonQuiescentLegacyControl>,
}
impl ToolHandler for NonQuiescentModernTool {
fn definition(&self) -> Tool {
Tool {
name: "non_quiescent_modern_tool".to_owned(),
description: Some("Test-only non-cooperative modern handler".to_owned()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<Vec<Content>> {
self.control.wait_until_released();
Ok(vec![Content::text("non-quiescent modern child released")])
}
}
// Exact-2024 era: scripts a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
struct NonQuiescentLegacySplitRecv {
phase: usize,
control: Arc<NonQuiescentLegacyControl>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl TransportRecvHalf for NonQuiescentLegacySplitRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(exact_legacy_initialize_request(
707,
serde_json::json!("1.0.0"),
)),
1 => Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
))),
2 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "non_quiescent_legacy_tool",
"arguments": {},
})),
708_i64,
))),
3 if self.control.wait_for_started(Duration::from_secs(2)) => {
Err(TransportError::Closed)
}
3 => Err(TransportError::Timeout),
_ => Err(TransportError::Closed),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
struct NonQuiescentLegacySplitSend;
impl TransportSendHalf for NonQuiescentLegacySplitSend {
fn send(&mut self, _cx: &Cx, _message: &JsonRpcMessage) -> Result<(), TransportError> {
Ok(())
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
struct NonQuiescentModernSplitRecv {
phase: usize,
}
impl TransportRecvHalf for NonQuiescentModernSplitRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "non_quiescent_modern_tool",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
709_i64,
))),
_ => Err(TransportError::Closed),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
#[derive(Default)]
struct LiveModernControlState {
active: usize,
max_active: usize,
started: HashSet<u64>,
cancelled: HashSet<u64>,
released: HashSet<u64>,
release_all: bool,
}
#[derive(Default)]
struct LiveModernControl {
state: Mutex<LiveModernControlState>,
changed: Condvar,
shutdown_requested: AtomicBool,
shutdown_cx: Mutex<Option<Cx>>,
}
impl LiveModernControl {
fn call(&self, ctx: &McpContext) -> McpResult<Vec<Content>> {
let request_id = ctx.request_id();
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.active += 1;
state.max_active = state.max_active.max(state.active);
state.started.insert(request_id);
self.changed.notify_all();
loop {
if ctx.is_cancelled() {
state.active = state.active.saturating_sub(1);
state.cancelled.insert(request_id);
self.changed.notify_all();
return Err(McpError::request_cancelled());
}
if state.release_all || state.released.contains(&request_id) {
state.active = state.active.saturating_sub(1);
self.changed.notify_all();
return Ok(vec![Content::text(format!("modern request {request_id}"))]);
}
let (next, _) = self
.changed
.wait_timeout(state, Duration::from_millis(1))
.unwrap_or_else(std::sync::PoisonError::into_inner);
state = next;
}
}
fn wait_for_started(&self, count: usize, timeout: Duration) -> bool {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (state, _) = self
.changed
.wait_timeout_while(state, timeout, |state| state.started.len() < count)
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.started.len() >= count
}
fn has_started(&self, request_id: u64) -> bool {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.started
.contains(&request_id)
}
fn wait_for_cancellation(&self, request_id: u64, timeout: Duration) -> bool {
let state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (state, _) = self
.changed
.wait_timeout_while(state, timeout, |state| {
!state.cancelled.contains(&request_id)
})
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.cancelled.contains(&request_id)
}
fn release(&self, request_id: u64) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.released.insert(request_id);
self.changed.notify_all();
}
fn request_shutdown(&self) {
self.shutdown_requested.store(true, Ordering::Release);
self.changed.notify_all();
}
fn install_shutdown_cx(&self, cx: Cx) {
*self
.shutdown_cx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(cx);
}
fn apply_shutdown(&self) {
if let Some(cx) = self
.shutdown_cx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.cloned()
{
cx.cancel_with(
CancelKind::Shutdown,
Some("controlled modern tool requested server shutdown"),
);
}
}
fn release_all(&self) {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.release_all = true;
self.changed.notify_all();
}
fn max_active(&self) -> usize {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.max_active
}
fn was_cancelled(&self, request_id: u64) -> bool {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.cancelled
.contains(&request_id)
}
}
struct LiveModernControlledTool {
control: Arc<LiveModernControl>,
}
impl ToolHandler for LiveModernControlledTool {
fn definition(&self) -> Tool {
Tool {
name: "live_modern_controlled_tool".to_owned(),
description: Some("Deterministic live modern dispatcher probe".to_owned()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
report_final_progress_probe_updates(ctx);
self.control.call(ctx)
}
// The request-owned modern dispatch polls this handler on the
// runtime's async worker, so a controlled tool that parks must be
// COOPERATIVE: a synchronous condvar wait inside poll would
// monopolize the single current_thread worker and starve every other
// request-owned task (the exact strand the overlap tests then
// misreport as lost dispatches).
fn call_final_outcome_async_in_request<'a>(
&'a self,
ctx: &'a McpContext,
request_cx: &'a Cx,
_arguments: serde_json::Value,
) -> BoxFuture<'a, fastmcp_core::McpOutcome<FinalToolOutcome>> {
// Cancellation may drop the pending future before another body poll.
struct ActiveCallGuard<'ctx> {
control: &'ctx LiveModernControl,
ctx: &'ctx McpContext,
request_cx: &'ctx Cx,
request_id: u64,
cancellation_observed: bool,
}
impl Drop for ActiveCallGuard<'_> {
fn drop(&mut self) {
let mut state = self
.control
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.active = state.active.saturating_sub(1);
if self.cancellation_observed
|| self.ctx.request_cancellation().is_cancel_requested()
|| self.request_cx.is_cancel_requested()
|| self.control.shutdown_requested.load(Ordering::Acquire)
{
state.cancelled.insert(self.request_id);
}
self.control.changed.notify_all();
}
}
Box::pin(async move {
report_final_progress_probe_updates(ctx);
let request_id = ctx.request_id();
{
let mut state = self
.control
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.active += 1;
state.max_active = state.max_active.max(state.active);
state.started.insert(request_id);
self.control.changed.notify_all();
}
let mut active_call = ActiveCallGuard {
control: &self.control,
ctx,
request_cx,
request_id,
cancellation_observed: false,
};
loop {
if self.control.shutdown_requested.load(Ordering::Acquire) {
self.control.apply_shutdown();
return fastmcp_core::Outcome::Cancelled(asupersync::CancelReason::user(
"controlled modern tool released for server shutdown",
));
}
if ctx.checkpoint().is_err() || request_cx.is_cancel_requested() {
active_call.cancellation_observed = true;
return fastmcp_core::Outcome::Cancelled(asupersync::CancelReason::user(
"request cancellation observed by controlled modern tool",
));
}
{
let state = self
.control
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.release_all || state.released.contains(&request_id) {
drop(state);
return match crate::handler::promote_legacy_tool_content(vec![
Content::text(format!("modern request {request_id}")),
]) {
Ok(result) => {
fastmcp_core::Outcome::Ok(FinalToolOutcome::Complete(result))
}
Err(error) => fastmcp_core::Outcome::Err(error),
};
}
}
// A fair yield keeps this cooperative: spawned task
// timers do not fire while the harness main thread owns
// block_on (a timer park would freeze the tool forever),
// and a hand-rolled immediate self-wake can starve
// sibling tasks of the runtime.
asupersync::runtime::yield_now().await;
}
})
}
}
#[derive(Default)]
struct LiveModernNotificationControl {
started: Mutex<bool>,
cancelled: AtomicBool,
changed: Condvar,
}
impl LiveModernNotificationControl {
fn wait_for_started(&self, timeout: Duration) -> bool {
let started = self
.started
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (started, _) = self
.changed
.wait_timeout_while(started, timeout, |started| !*started)
.unwrap_or_else(std::sync::PoisonError::into_inner);
*started
}
fn was_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Acquire)
}
}
struct LiveModernBlockingNotificationMiddleware {
control: Arc<LiveModernNotificationControl>,
}
impl Middleware for LiveModernBlockingNotificationMiddleware {
fn on_request(
&self,
ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
if request.method != "notifications/initialized" {
return Ok(MiddlewareDecision::Continue);
}
{
let mut started = self
.control
.started
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*started = true;
self.control.changed.notify_all();
}
let deadline = Instant::now() + Duration::from_secs(1);
while !ctx.is_cancelled() && Instant::now() < deadline {
std::thread::yield_now();
}
if ctx.is_cancelled() {
self.control.cancelled.store(true, Ordering::Release);
return Err(McpError::request_cancelled());
}
Err(McpError::internal_error(
"modern notification did not receive shutdown cancellation",
))
}
}
#[derive(Default)]
struct LiveModernResponses {
messages: Mutex<Vec<JsonRpcMessage>>,
changed: Condvar,
}
impl LiveModernResponses {
fn record(&self, message: JsonRpcMessage) {
let mut messages = self
.messages
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
messages.push(message);
self.changed.notify_all();
}
fn response_count(&self, id: i64) -> usize {
self.messages
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.filter(|message| {
matches!(message, JsonRpcMessage::Response(response) if response.id == Some(id.into()))
})
.count()
}
fn response(&self, id: i64) -> Option<JsonRpcResponse> {
self.messages
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(id.into()) => {
Some(response.clone())
}
_ => None,
})
}
fn messages(&self) -> Vec<JsonRpcMessage> {
self.messages
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
fn wait_for_notification(&self, method: &str, timeout: Duration) -> bool {
let messages = self
.messages
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (messages, _) = self
.changed
.wait_timeout_while(messages, timeout, |messages| {
!messages.iter().any(|message| {
matches!(message, JsonRpcMessage::Request(request) if request.method == method)
})
})
.unwrap_or_else(std::sync::PoisonError::into_inner);
messages.iter().any(|message| {
matches!(message, JsonRpcMessage::Request(request) if request.method == method)
})
}
fn wait_for_responses(&self, ids: &[i64], timeout: Duration) -> bool {
let messages = self
.messages
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (messages, _) = self
.changed
.wait_timeout_while(messages, timeout, |messages| {
ids.iter().any(|id| {
!messages.iter().any(|message| {
matches!(message, JsonRpcMessage::Response(response) if response.id == Some((*id).into()))
})
})
})
.unwrap_or_else(std::sync::PoisonError::into_inner);
ids.iter().all(|id| {
messages.iter().any(|message| {
matches!(message, JsonRpcMessage::Response(response) if response.id == Some((*id).into()))
})
})
}
}
#[derive(Default)]
struct BoundedTestSignal {
raised: Mutex<bool>,
changed: Condvar,
}
impl BoundedTestSignal {
fn raise(&self) {
let mut raised = self
.raised
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*raised = true;
self.changed.notify_all();
}
fn wait(&self, timeout: Duration) -> bool {
let raised = self
.raised
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let (raised, _) = self
.changed
.wait_timeout_while(raised, timeout, |raised| !*raised)
.unwrap_or_else(std::sync::PoisonError::into_inner);
*raised
}
}
struct LiveModernSplitRecv {
phase: usize,
control: Arc<LiveModernControl>,
responses: Arc<LiveModernResponses>,
}
impl TransportRecvHalf for LiveModernSplitRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_controlled_tool_request(1200)),
2 => {
if self.control.wait_for_started(1, Duration::from_secs(2)) {
Ok(modern_controlled_tool_request(1201))
} else {
Err(TransportError::Timeout)
}
}
3 => {
if !self.control.wait_for_started(2, Duration::from_secs(2)) {
return Err(TransportError::Timeout);
}
self.control.release(1200);
self.control.release(1201);
if self
.responses
.wait_for_responses(&[1200, 1201], Duration::from_secs(2))
{
Err(TransportError::Closed)
} else {
Err(TransportError::Timeout)
}
}
_ => Err(TransportError::Closed),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
struct LiveModernSplitSend {
responses: Arc<LiveModernResponses>,
}
impl TransportSendHalf for LiveModernSplitSend {
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
self.responses.record(message.clone());
Ok(())
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
// Exact-2024 era: scripts a legacy runtime connection.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyRuntimeSplitRecv {
phase: u8,
supports_sampling: bool,
outbound: Receiver<JsonRpcMessage>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl LiveLegacyRuntimeSplitRecv {
fn wait_for_response(&self, id: i64) -> Result<(), TransportError> {
loop {
match self.outbound.recv_timeout(Duration::from_secs(2)) {
Ok(JsonRpcMessage::Response(response)) if response.id == Some(id.into()) => {
return Ok(());
}
Ok(_) => {}
Err(_) => return Err(TransportError::Timeout),
}
}
}
fn sampling_response(&self) -> Result<JsonRpcMessage, TransportError> {
loop {
match self.outbound.recv_timeout(Duration::from_secs(2)) {
Ok(JsonRpcMessage::Request(request))
if request.method == "sampling/createMessage" =>
{
let id = request.id.expect("sampling request must carry an id");
let result =
serde_json::to_value(fastmcp_protocol::CreateMessageResult::text(
"sampled-value",
"legacy-test-model",
))
.expect("sampling result must serialize");
return Ok(JsonRpcMessage::Response(JsonRpcResponse::success(
id, result,
)));
}
Ok(_) => {}
Err(_) => return Err(TransportError::Timeout),
}
}
}
}
#[cfg(feature = "legacy-2024-11-05")]
impl TransportRecvHalf for LiveLegacyRuntimeSplitRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": if self.supports_sampling {
serde_json::json!({"sampling": {}})
} else {
serde_json::json!({})
},
"clientInfo": {"name": "legacy-runtime-client", "version": "1.0.0"},
})),
701_i64,
))),
1 => Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
))),
2 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"logging/setLevel",
Some(serde_json::json!({"level": "debug"})),
702_i64,
))),
3 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_legacy_runtime_connection_tool",
"arguments": {"sample": true},
"_meta": {"progressToken": "legacy-runtime-progress"},
})),
703_i64,
))),
4 if self.supports_sampling => self.sampling_response(),
4 => {
self.wait_for_response(703)?;
Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_legacy_runtime_connection_tool",
"arguments": {"sample": false},
"_meta": {"progressToken": "legacy-runtime-progress"},
})),
704_i64,
)))
}
5 if self.supports_sampling => {
self.wait_for_response(703)?;
Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_legacy_runtime_connection_tool",
"arguments": {"sample": false},
"_meta": {"progressToken": "legacy-runtime-progress"},
})),
704_i64,
)))
}
5 | 6 => {
self.wait_for_response(704)?;
Err(TransportError::Closed)
}
_ => Err(TransportError::Closed),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
// Exact-2024 era: scripts a legacy runtime connection.
#[cfg(feature = "legacy-2024-11-05")]
struct LegacyRootsContextSplitRecv {
phase: u8,
supports_roots: bool,
outbound: Receiver<JsonRpcMessage>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl LegacyRootsContextSplitRecv {
fn wait_for_response(&self, id: i64) -> Result<(), TransportError> {
loop {
match self.outbound.recv_timeout(Duration::from_secs(2)) {
Ok(JsonRpcMessage::Response(response)) if response.id == Some(id.into()) => {
return Ok(());
}
Ok(_) => {}
Err(_) => return Err(TransportError::Timeout),
}
}
}
fn roots_response(&self) -> Result<JsonRpcMessage, TransportError> {
loop {
match self.outbound.recv_timeout(Duration::from_secs(2)) {
Ok(JsonRpcMessage::Request(request)) if request.method == "roots/list" => {
let id = request.id.expect("roots request must carry an id");
return Ok(JsonRpcMessage::Response(JsonRpcResponse::success(
id,
serde_json::json!({
"roots": [{"uri": "file:///workspace", "name": "workspace"}],
}),
)));
}
Ok(_) => {}
Err(_) => return Err(TransportError::Timeout),
}
}
}
}
#[cfg(feature = "legacy-2024-11-05")]
impl TransportRecvHalf for LegacyRootsContextSplitRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": if self.supports_roots {
serde_json::json!({"roots": {}})
} else {
serde_json::json!({})
},
"clientInfo": {"name": "legacy-roots-context", "version": "1.0.0"},
})),
731_i64,
))),
1 => Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
))),
2 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "legacy_roots_context_tool",
"arguments": {},
})),
732_i64,
))),
3 if self.supports_roots => self.roots_response(),
3 => {
self.wait_for_response(732)?;
Err(TransportError::Closed)
}
4 if self.supports_roots => {
self.wait_for_response(732)?;
Err(TransportError::Closed)
}
_ => Err(TransportError::Closed),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
// Exact-2024 era: scripts legacy server-to-client sampling.
#[cfg(feature = "legacy-2024-11-05")]
struct LegacyCancelLateRecv {
phase: u8,
outbound: Receiver<JsonRpcMessage>,
reverse_id: Option<RequestId>,
stale_offset: i64,
cancellation_request_id: RequestId,
}
#[cfg(feature = "legacy-2024-11-05")]
impl TransportRecvHalf for LegacyCancelLateRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {"sampling": {}},
"clientInfo": {"name": "cancel-late", "version": "1.0.0"},
})),
801_i64,
))),
1 => Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
))),
2 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(
serde_json::json!({"name": "live_legacy_runtime_connection_tool", "arguments": {"sample": true}}),
),
802_i64,
))),
3 => loop {
match self.outbound.recv_timeout(Duration::from_secs(2)) {
Ok(JsonRpcMessage::Request(request))
if request.method == "sampling/createMessage" =>
{
let id = request.id.expect("sampling request id");
assert_eq!(id, RequestId::Number(-1));
self.reverse_id = Some(id.clone());
return Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": self.cancellation_request_id.clone(),
"reason": "test cancellation",
})),
)));
}
Ok(_) => {}
Err(_) => return Err(TransportError::Timeout),
}
},
4 => {
let id = self.reverse_id.clone().expect("captured reverse id");
let id = match id {
RequestId::Number(value) => RequestId::Number(value + self.stale_offset),
other => other,
};
Ok(JsonRpcMessage::Response(JsonRpcResponse::success(
id,
serde_json::json!({"text": "late"}),
)))
}
5 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(
serde_json::json!({"name": "live_legacy_runtime_connection_tool", "arguments": {"sample": false}}),
),
803_i64,
))),
6 => loop {
match self.outbound.recv_timeout(Duration::from_secs(2)) {
Ok(JsonRpcMessage::Response(response))
if response.id == Some(803_i64.into()) =>
{
return Err(TransportError::Closed);
}
Ok(_) => {}
Err(_) => return Err(TransportError::Timeout),
}
},
_ => Err(TransportError::Closed),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
// Exact-2024 era: records a legacy runtime connection's output.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyRuntimeSplitSend {
sent: Arc<Mutex<Vec<JsonRpcMessage>>>,
outbound: std::sync::mpsc::SyncSender<JsonRpcMessage>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl TransportSendHalf for LiveLegacyRuntimeSplitSend {
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
self.sent
.lock()
.expect("legacy runtime sent-message mutex must not be poisoned")
.push(message.clone());
self.outbound
.send(message.clone())
.map_err(|_| TransportError::Closed)
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
// Exact-2024 era: scripts a legacy runtime connection.
#[cfg(feature = "legacy-2024-11-05")]
fn live_legacy_runtime_connection_transcript(supports_sampling: bool) -> Vec<JsonRpcMessage> {
let sent = Arc::new(Mutex::new(Vec::new()));
let (outbound, inbound) = sync_channel(32);
run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
Server::new("live-legacy-runtime-connection", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(LiveLegacyRuntimeConnectionTool)
.build(),
LiveLegacyRuntimeSplitRecv {
phase: 0,
supports_sampling,
outbound: inbound,
},
LiveLegacyRuntimeSplitSend {
sent: Arc::clone(&sent),
outbound,
},
)
.expect("public split transport must complete the legacy runtime script");
sent.lock()
.expect("legacy runtime sent-message mutex must not be poisoned")
.clone()
}
// Exact-2024 era: scripts a legacy runtime connection.
#[cfg(feature = "legacy-2024-11-05")]
fn legacy_roots_context_transcript(supports_roots: bool) -> Vec<JsonRpcMessage> {
let sent = Arc::new(Mutex::new(Vec::new()));
let (outbound, inbound) = sync_channel(16);
run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
Server::new("legacy-roots-context", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(LegacyRootsContextTool)
.build(),
LegacyRootsContextSplitRecv {
phase: 0,
supports_roots,
outbound: inbound,
},
LiveLegacyRuntimeSplitSend {
sent: Arc::clone(&sent),
outbound,
},
)
.expect("public split transport must complete the legacy roots context script");
sent.lock()
.expect("legacy roots context sent-message mutex must not be poisoned")
.clone()
}
// Exact-2024 era: scripts legacy progress over a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
fn legacy_stdio_progress_transcript(include_progress_token: bool) -> Vec<JsonRpcMessage> {
let sent = Arc::new(Mutex::new(Vec::new()));
let receive_calls = Arc::new(AtomicUsize::new(0));
let mut params = serde_json::json!({
"name": "live_legacy_runtime_connection_tool",
"arguments": {"sample": false},
});
if include_progress_token {
params
.as_object_mut()
.expect("legacy tool parameters must be an object")
.insert(
"_meta".to_owned(),
serde_json::json!({"progressToken": "legacy-stdio-progress"}),
);
}
run_returning_transport_with_test_runtime(
Server::new("legacy-stdio-progress", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(LiveLegacyRuntimeConnectionTool)
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
exact_legacy_initialize_request(711, serde_json::json!("1.0.0")),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(params),
712_i64,
)),
]),
sent: Arc::clone(&sent),
receive_calls,
},
)
.expect("public stdio transport must complete the legacy progress script");
sent.lock()
.expect("legacy stdio sent-message mutex must not be poisoned")
.clone()
}
#[cfg(feature = "websocket")]
fn masked_websocket_frame(opcode: u8, payload: &[u8]) -> Vec<u8> {
let mask = [0x41, 0x73, 0x19, 0xC7];
let mut frame = Vec::with_capacity(payload.len() + 14);
frame.push(0x80 | opcode);
match payload.len() {
length @ 0..=125 => frame.push(0x80 | length as u8),
length @ 126..=65_535 => {
frame.push(0x80 | 126);
frame.extend_from_slice(&(length as u16).to_be_bytes());
}
length => {
frame.push(0x80 | 127);
frame.extend_from_slice(&(length as u64).to_be_bytes());
}
}
frame.extend_from_slice(&mask);
frame.extend(
payload
.iter()
.enumerate()
.map(|(index, byte)| byte ^ mask[index % mask.len()]),
);
frame
}
#[cfg(feature = "websocket")]
fn masked_websocket_message(message: JsonRpcMessage) -> Vec<u8> {
let payload = serde_json::to_vec(&message)
.expect("live WebSocket request fixture must serialize to JSON-RPC");
masked_websocket_frame(0x01, &payload)
}
/// The policy a server of this build selects by default: `Auto` with the
/// exact legacy adapter, `ModernOnly` without it. A test of modern
/// behaviour that does not depend on era classification selects it, so
/// each feature lane exercises the loop its own shipped build runs.
const fn build_default_protocol_policy() -> ProtocolPolicy {
if cfg!(feature = "legacy-2024-11-05") {
ProtocolPolicy::Auto
} else {
ProtocolPolicy::ModernOnly
}
}
struct ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque<JsonRpcMessage>,
sent: Arc<Mutex<Vec<JsonRpcMessage>>>,
receive_calls: Arc<AtomicUsize>,
}
fn modern_discovery_request(id: i64) -> JsonRpcMessage {
JsonRpcMessage::Request(JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
id,
))
}
fn modern_discovery_opening_request() -> JsonRpcMessage {
modern_discovery_request(905)
}
fn modern_controlled_tool_request(id: i64) -> JsonRpcMessage {
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_modern_controlled_tool",
"arguments": {},
// Modern REQUESTS must carry clientCapabilities alongside
// protocolVersion or reserved-value validation rejects them
// with -32602 before dispatch (notifications stay
// protocolVersion-only by contract).
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
id,
))
}
fn modern_controlled_tool_progress_request(id: i64) -> JsonRpcMessage {
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_modern_controlled_tool",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "public-stdio-final-progress",
"io.modelcontextprotocol/logLevel": "info",
},
})),
id,
))
}
// Exact-2024 era: builds a legacy tools/call request.
#[cfg(feature = "legacy-2024-11-05")]
fn legacy_controlled_tool_request(id: i64) -> JsonRpcMessage {
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_modern_controlled_tool",
"arguments": {},
})),
id,
))
}
fn modern_initialized_notification() -> JsonRpcMessage {
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
},
})),
))
}
fn modern_cancelled_notification(request_id: u64) -> JsonRpcMessage {
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": request_id,
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
},
})),
))
}
fn modern_catalog_mutating_tool_request(id: i64) -> JsonRpcMessage {
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "stdio_catalog_mutating_tool",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
id,
))
}
fn modern_subscriptions_listen_request(id: i64) -> JsonRpcMessage {
JsonRpcMessage::Request(JsonRpcRequest::new(
SUBSCRIPTIONS_LISTEN,
Some(serde_json::json!({
"notifications": {
"toolsListChanged": true,
},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
id,
))
}
struct PublicStdioSubscriptionRecv {
inbound: std::collections::VecDeque<JsonRpcMessage>,
sent: Arc<Mutex<Vec<JsonRpcMessage>>>,
receive_calls: Arc<AtomicUsize>,
termination_attempted: Arc<AtomicBool>,
}
impl TransportRecvHalf for PublicStdioSubscriptionRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
self.receive_calls.fetch_add(1, Ordering::AcqRel);
if let Some(message) = self.inbound.pop_front() {
return Ok(message);
}
let deadline = Instant::now() + Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while Instant::now() < deadline {
let acknowledged = self
.sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.any(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if request.method
== "notifications/subscriptions/acknowledged"
)
});
if acknowledged {
while Instant::now() < deadline {
let completion_committed = self
.sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.any(|message| {
matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(906_i64.into())
&& final_subscription_completion_response(response)
)
});
if self.termination_attempted.load(Ordering::Acquire)
&& completion_committed
{
return Err(TransportError::Closed);
}
thread::sleep(Duration::from_millis(1));
}
return Err(TransportError::Timeout);
}
thread::sleep(Duration::from_millis(1));
}
Err(TransportError::Timeout)
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
fn public_stdio_final_subscription_transcript(
cancellation: Option<JsonRpcMessage>,
) -> Vec<JsonRpcMessage> {
const DISCOVERY_ID: i64 = 905;
const SUBSCRIPTION_ID: i64 = 906;
let sent = Arc::new(Mutex::new(Vec::new()));
let receive_calls = Arc::new(AtomicUsize::new(0));
let termination_attempted = Arc::new(AtomicBool::new(false));
let server = Server::new("public-stdio-final-subscription", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build();
let subscriptions = Arc::clone(&server.final_subscriptions);
let sent_for_terminator = Arc::clone(&sent);
let termination_attempted_by_terminator = Arc::clone(&termination_attempted);
let terminator = thread::spawn(move || {
let deadline = Instant::now() + Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while Instant::now() < deadline {
let acknowledged = sent_for_terminator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.any(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if request.method == "notifications/subscriptions/acknowledged"
)
});
if acknowledged {
let terminated = subscriptions.terminate() == 1;
termination_attempted_by_terminator.store(true, Ordering::Release);
return terminated;
}
thread::sleep(Duration::from_millis(1));
}
termination_attempted_by_terminator.store(true, Ordering::Release);
false
});
let mut inbound =
std::collections::VecDeque::from([modern_discovery_request(DISCOVERY_ID)]);
if let Some(cancellation) = cancellation {
inbound.push_back(cancellation);
}
inbound.push_back(modern_subscriptions_listen_request(SUBSCRIPTION_ID));
run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
server,
PublicStdioSubscriptionRecv {
inbound,
sent: Arc::clone(&sent),
receive_calls,
termination_attempted,
},
PublicSplitSubscriptionSend {
sent: Arc::clone(&sent),
},
)
.expect("public modern stdio transport must settle the subscription stream");
assert!(
terminator
.join()
.expect("subscription terminator must not panic"),
"the public listener must acknowledge before server termination"
);
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
fn public_stdio_listen_observes_handler_catalog_mutation() -> Vec<JsonRpcMessage> {
const DISCOVERY_ID: i64 = 925;
const SUBSCRIPTION_ID: i64 = 926;
const TOOL_ID: i64 = 927;
let sent = Arc::new(Mutex::new(Vec::new()));
let receive_calls = Arc::new(AtomicUsize::new(0));
let termination_attempted = Arc::new(AtomicBool::new(false));
let server = Server::new("public-stdio-listen-catalog", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.tool(StdioCatalogMutatingTool)
.build();
let subscriptions = Arc::clone(&server.final_subscriptions);
let sent_for_terminator = Arc::clone(&sent);
let termination_attempted_by_terminator = Arc::clone(&termination_attempted);
let terminator = thread::spawn(move || {
let deadline = Instant::now() + Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while Instant::now() < deadline {
let saw_list_changed = sent_for_terminator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.any(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if request.method == "notifications/tools/list_changed"
)
});
if saw_list_changed {
let terminated = subscriptions.terminate() == 1;
termination_attempted_by_terminator.store(true, Ordering::Release);
return terminated;
}
thread::sleep(Duration::from_millis(1));
}
termination_attempted_by_terminator.store(true, Ordering::Release);
false
});
struct WaitForAckThenMutate {
phase: u8,
sent: Arc<Mutex<Vec<JsonRpcMessage>>>,
termination_attempted: Arc<AtomicBool>,
}
impl TransportRecvHalf for WaitForAckThenMutate {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(modern_discovery_request(DISCOVERY_ID)),
1 => Ok(modern_subscriptions_listen_request(SUBSCRIPTION_ID)),
2 => {
let deadline =
Instant::now() + Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while Instant::now() < deadline {
let acknowledged = self
.sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.any(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if request.method
== "notifications/subscriptions/acknowledged"
)
});
if acknowledged {
return Ok(modern_catalog_mutating_tool_request(TOOL_ID));
}
thread::sleep(Duration::from_millis(1));
}
Err(TransportError::Timeout)
}
_ => {
let deadline =
Instant::now() + Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while Instant::now() < deadline {
let completion_committed = self
.sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.any(|message| {
matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(SUBSCRIPTION_ID.into())
&& final_subscription_completion_response(response)
)
});
if self.termination_attempted.load(Ordering::Acquire)
&& completion_committed
{
return Err(TransportError::Closed);
}
thread::sleep(Duration::from_millis(1));
}
Err(TransportError::Timeout)
}
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
let _ = receive_calls;
run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
server,
WaitForAckThenMutate {
phase: 0,
sent: Arc::clone(&sent),
termination_attempted,
},
PublicSplitSubscriptionSend {
sent: Arc::clone(&sent),
},
)
.expect("public modern stdio transport must settle listen plus catalog mutation");
assert!(
terminator
.join()
.expect("catalog-mutation terminator must not panic"),
"the public listener must observe handler-driven tools/list_changed"
);
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
#[test]
fn dispatch_stateless_listen_is_not_method_not_found() {
let server = Server::new("stateless-listen-error", "1.0.0").build();
let inbound =
InboundRequestContext::new(Cx::for_testing(), 926, InboundRequestTransport::Memory);
let JsonRpcMessage::Request(request) = modern_subscriptions_listen_request(926) else {
panic!("listen fixture must be a request");
};
let response = block_on(server.dispatch_stateless(&inbound, &request))
.expect("listen with an id must produce a JSON-RPC response");
let error = response
.error
.expect("sync dispatch_stateless cannot own a listen stream");
assert_ne!(
error.code,
i32::from(McpErrorCode::MethodNotFound).into(),
"listen must not look unimplemented: {error:?}"
);
assert!(
error.message.contains("open_subscription_listen"),
"error must name the in-process listen API: {error:?}"
);
}
#[test]
fn open_subscription_listen_receives_stateless_catalog_mutation() {
let server = Server::new("in-process-listen", "1.0.0")
.tool(StdioCatalogMutatingTool)
.build();
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
});
let _listen = server
.open_subscription_listen(
RequestId::String("in-process-listen".to_owned()),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
sender,
)
.expect("in-process listen must open");
assert!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.any(|request| request.method == "notifications/subscriptions/acknowledged"),
"open_subscription_listen must acknowledge immediately"
);
let connection = ModernConnection::new();
let inbound = InboundRequestContext::with_modern_connection(
Cx::for_testing(),
927,
InboundRequestTransport::Memory,
&connection,
);
let JsonRpcMessage::Request(request) = modern_catalog_mutating_tool_request(927) else {
panic!("catalog mutation fixture must be a request");
};
let response = block_on(server.dispatch_stateless(&inbound, &request))
.expect("stateless tools/call must respond");
assert!(
response.error.is_none(),
"catalog mutation tool must succeed: {response:?}"
);
assert!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.any(|request| request.method == "notifications/tools/list_changed"),
"in-process listen must observe handler-driven list_changed"
);
}
#[test]
fn dropped_in_process_listen_does_not_receive_later_catalog_events() {
let server = Server::new("dropped-in-process-listen", "1.0.0")
.tool(StdioCatalogMutatingTool)
.build();
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
});
let listen = server
.open_subscription_listen(
RequestId::String("dropped-listen".to_owned()),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
sender,
)
.expect("in-process listen must open");
drop(listen);
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clear();
let connection = ModernConnection::new();
let inbound = InboundRequestContext::with_modern_connection(
Cx::for_testing(),
928,
InboundRequestTransport::Memory,
&connection,
);
let JsonRpcMessage::Request(request) = modern_catalog_mutating_tool_request(928) else {
panic!("catalog mutation fixture must be a request");
};
let _ = block_on(server.dispatch_stateless(&inbound, &request))
.expect("stateless tools/call must respond");
assert!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.all(|request| request.method != "notifications/tools/list_changed"),
"a dropped listen handle must not receive later catalog events"
);
}
#[test]
fn public_stdio_listen_receives_handler_driven_list_changed() {
let transcript = public_stdio_listen_observes_handler_catalog_mutation();
assert!(
transcript.iter().any(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if request.method == "notifications/subscriptions/acknowledged"
)
}),
"listen must acknowledge before catalog events: {transcript:?}"
);
assert!(
transcript.iter().any(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if request.method == "notifications/tools/list_changed"
)
}),
"detached stdio listen must receive handler-driven list_changed: {transcript:?}"
);
}
#[derive(Clone, Copy)]
enum PublicSplitSubscriptionTerminal {
Eof,
Shutdown,
}
struct PublicSplitSubscriptionRecv {
phase: u8,
sent: Arc<Mutex<Vec<JsonRpcMessage>>>,
terminal: PublicSplitSubscriptionTerminal,
}
impl TransportRecvHalf for PublicSplitSubscriptionRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(modern_discovery_request(915)),
1 => Ok(modern_subscriptions_listen_request(916)),
2 => {
let deadline = Instant::now() + Duration::from_secs(2);
while Instant::now() < deadline {
let acknowledged = self
.sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
.any(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if request.method
== "notifications/subscriptions/acknowledged"
)
});
if acknowledged {
return Err(match self.terminal {
PublicSplitSubscriptionTerminal::Eof => TransportError::Closed,
PublicSplitSubscriptionTerminal::Shutdown => {
TransportError::Cancelled
}
});
}
thread::sleep(Duration::from_millis(1));
}
Err(TransportError::Timeout)
}
_ => Err(TransportError::Closed),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
struct PublicSplitSubscriptionSend {
sent: Arc<Mutex<Vec<JsonRpcMessage>>>,
}
impl TransportSendHalf for PublicSplitSubscriptionSend {
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
self.sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(message.clone());
Ok(())
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
fn public_split_stdio_final_subscription_transcript(
terminal: PublicSplitSubscriptionTerminal,
) -> Result<Vec<JsonRpcMessage>, String> {
let sent = Arc::new(Mutex::new(Vec::new()));
run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
Server::new("public-split-stdio-final-subscription", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build(),
PublicSplitSubscriptionRecv {
phase: 0,
sent: Arc::clone(&sent),
terminal,
},
PublicSplitSubscriptionSend {
sent: Arc::clone(&sent),
},
)?;
Ok(sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone())
}
fn assert_public_split_stdio_subscription_shutdown(terminal: PublicSplitSubscriptionTerminal) {
let sent = public_split_stdio_final_subscription_transcript(terminal)
.expect("public split stdio shutdown must preserve the listener completion");
assert_eq!(
sent.len(),
4,
"the public split transport must emit only discovery, acknowledgement, cancellation, and completion: {sent:?}"
);
assert!(sent.iter().any(|message| {
matches!(
message,
JsonRpcMessage::Response(response) if response.id == Some(915_i64.into())
)
}));
let acknowledgement = sent
.iter()
.position(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if matches!(
ServerNotification::decode(request),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
)
)
})
.expect("public split transport must acknowledge subscriptions/listen");
let cancellation = sent
.iter()
.position(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if matches!(
ServerNotification::decode(request),
Ok(ServerNotification::Cancelled(params))
if params.request_id == RequestId::Number(916)
)
)
})
.expect("public split transport must cancel the final subscription");
let completion = sent
.iter()
.position(|message| {
matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(916_i64.into())
&& final_subscription_completion_response(response)
)
})
.expect("public split transport must preserve graceful subscription completion");
assert!(
acknowledgement < cancellation && cancellation < completion,
"subscription acknowledgement, cancellation, and completion must retain their graceful order: {sent:?}"
);
}
#[test]
fn public_split_stdio_eof_elects_subscription_completion_before_cancellation_drain() {
assert_public_split_stdio_subscription_shutdown(PublicSplitSubscriptionTerminal::Eof);
}
#[test]
fn public_split_stdio_shutdown_elects_subscription_completion_before_cancellation_drain() {
assert_public_split_stdio_subscription_shutdown(PublicSplitSubscriptionTerminal::Shutdown);
}
/// Runs a two-message stdio script against a server built with the
/// no-legacy default policy.
#[cfg(not(feature = "legacy-2024-11-05"))]
fn no_legacy_stdio_script(
opening: JsonRpcMessage,
follow_up: JsonRpcMessage,
) -> (McpResult<()>, usize, Vec<JsonRpcMessage>) {
let sent = Arc::new(Mutex::new(Vec::new()));
let receive_calls = Arc::new(AtomicUsize::new(0));
let server = Server::new("no-legacy-stdio-route", "1.0.0").build();
assert_eq!(server.protocol_policy(), ProtocolPolicy::ModernOnly);
let result = server.run_transport_returning_with_cx(
&Cx::for_testing(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([opening, follow_up]),
sent: Arc::clone(&sent),
receive_calls: Arc::clone(&receive_calls),
},
);
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
(result, receive_calls.load(Ordering::Acquire), sent)
}
#[cfg(not(feature = "legacy-2024-11-05"))]
#[test]
fn no_legacy_stdio_refuses_the_opening_envelope_before_a_second_request_or_dispatch() {
// A no-legacy build cannot select Auto, so the legacy opening below
// meets the ModernOnly default rather than a dual-era classifier.
assert!(matches!(
Server::new("no-legacy-stdio-route", "1.0.0").protocol_policy(ProtocolPolicy::Auto),
Err(ServerLaunchPolicyError::FeatureUnavailable)
));
let (result, receive_calls, sent) = no_legacy_stdio_script(
exact_legacy_initialize_request(917, serde_json::json!("1.0.0")),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({})),
918_i64,
)),
);
assert!(
result.is_err(),
"a no-legacy stdio opening must be terminal"
);
assert_eq!(
receive_calls, 1,
"the legacy opening must not admit a follow-up request into dispatch"
);
assert!(
sent.iter().all(|message| !matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(917_i64.into()) && response.error.is_none()
)),
"the exact legacy initialize response proves adapter admission and must never be emitted"
);
}
#[cfg(not(feature = "legacy-2024-11-05"))]
#[test]
fn no_legacy_stdio_admits_a_modern_opening_and_its_follow_up_request() {
// Differs from the refusal above only in the era of the opening.
let (result, receive_calls, sent) =
no_legacy_stdio_script(modern_discovery_request(917), modern_discovery_request(918));
result.expect("a modern opening must keep the no-legacy stdio connection open");
assert_eq!(
receive_calls, 3,
"both requests and the closing receive must reach the loop"
);
for id in [917_i64, 918] {
assert!(
sent.iter().any(|message| matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(id.into()) && response.error.is_none()
)),
"request {id} must be dispatched and answered: {sent:?}"
);
}
}
#[cfg(not(feature = "legacy-2024-11-05"))]
#[test]
fn no_legacy_source_gates_standalone_sse_and_keeps_modern_sse_framing() {
let source = include_str!("lib.rs").replace("\r\n", "\n");
assert!(source.contains(
"#[cfg(feature = \"legacy-2024-11-05\")]\nuse fastmcp_transport::sse::SseServerTransport;"
));
assert!(!source.contains("#[cfg(feature = \"legacy-2024-11-05\")]\n pub fn run_sse"));
assert!(
source.contains(
"#[cfg(feature = \"legacy-2024-11-05\")]\n pub async fn run_sse_with_cx"
)
);
assert!(source.contains("fn sse_response_head(response: &HttpResponse)"));
// Split so this assertion's own text cannot satisfy the scan.
assert!(!source.contains(concat!("legacy_", "sse_response_head")));
}
#[test]
fn no_context_transport_and_sse_runtime_wrappers_are_absent() {
let source = include_str!("lib.rs");
let has_public_signature = |signature: &str| {
source
.lines()
.any(|line| line.trim_start().starts_with(signature))
};
assert!(!has_public_signature("pub fn run_transport<T>"));
assert!(!has_public_signature("pub fn run_transport_returning<T>"));
assert!(!has_public_signature("pub fn run_sse<W, R>"));
assert!(has_public_signature("pub fn run_transport_with_cx<T>"));
assert!(has_public_signature(
"pub fn run_transport_returning_with_cx<T>"
));
assert!(has_public_signature("pub async fn run_sse_with_cx<W, R>"));
}
#[cfg(not(feature = "legacy-2024-11-05"))]
#[test]
fn no_legacy_source_exposes_only_the_public_modern_http_contract() {
let source = include_str!("lib.rs").replace("\r\n", "\n");
assert!(source.contains("pub enum ServerHttpEndpointError"));
assert!(source.contains(
"pub fn into_http_endpoint(self) -> Result<ServerHttpEndpoint, ServerHttpEndpointError>"
));
assert!(source.contains(
"pub fn open_session(&self, cx: &Cx) -> Result<ServerHttpSession, ServerHttpEndpointError>"
));
assert!(
source.contains(") -> Result<ServerHttpEndpointResponse, ServerHttpEndpointError>")
);
assert!(source.contains(
"pub fn pop_event(&self) -> Result<Option<SseEvent>, ServerHttpEndpointError>"
));
assert!(source.contains("pub request_capacity: usize,"));
assert!(
source.contains(
"#[cfg(feature = \"legacy-2024-11-05\")]\n pub legacy_sse_path: String,"
)
);
assert!(source.contains(
"#[cfg(feature = \"legacy-2024-11-05\")]\n pub legacy_message_path: String,"
));
// Split so this assertion's own text cannot satisfy the scan.
assert!(!source.contains(concat!("pub legacy_", "request_capacity")));
}
#[cfg(not(feature = "legacy-2024-11-05"))]
fn no_legacy_http_session(name: &str) -> (Cx, ServerHttpEndpoint, ServerHttpSession) {
let cx = Cx::for_testing();
let endpoint = Server::new(name, "1.0.0")
.build()
.into_http_endpoint()
.expect("modern-only HTTP endpoint must construct without legacy transport support");
let session = endpoint
.open_session(&cx)
.expect("modern-only HTTP session must open");
(cx, endpoint, session)
}
#[cfg(not(feature = "legacy-2024-11-05"))]
#[test]
fn no_legacy_http_routes_are_not_found_without_pinning_or_adapter_admission() {
// A no-legacy build cannot select Auto, so the legacy routes below
// meet the ModernOnly default rather than a dual-era classifier.
assert!(matches!(
Server::new("no-legacy-http-routes", "1.0.0").protocol_policy(ProtocolPolicy::Auto),
Err(ServerLaunchPolicyError::FeatureUnavailable)
));
// This build has no legacy adapter field to populate, so the former
// `legacy_adapter.is_none()` check now holds at compile time.
let (cx, endpoint, mut session) = no_legacy_http_session("no-legacy-http-routes");
for request in [
HttpRequest::new(HttpMethod::Get, "/sse"),
HttpRequest::new(HttpMethod::Post, "/messages"),
] {
let response = block_on(session.handle_async(&cx, request))
.expect("a disabled legacy route is an ordinary zero-route response");
assert!(matches!(
response,
ServerHttpEndpointResponse::Immediate(HttpResponse {
status: HttpStatus::NOT_FOUND,
..
})
));
assert_eq!(session.selected_era, None);
}
assert!(
endpoint
.server
.active_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty(),
"zero-route HTTP rejection must not create request authority"
);
}
#[cfg(not(feature = "legacy-2024-11-05"))]
#[test]
fn no_legacy_http_modern_route_is_admitted_and_pins_the_modern_era() {
// Differs from the refusal above only in the route and era of the request.
let (cx, _endpoint, mut session) = no_legacy_http_session("no-legacy-http-routes");
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
919_i64,
);
let response = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", SERVER_DISCOVER_METHOD)
.with_body(
serde_json::to_vec(&request)
.expect("modern discovery request must serialize"),
),
),
)
.expect("a modern request must be admitted by the no-legacy endpoint");
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("modern JSON negotiation must produce an immediate HTTP response");
};
assert_eq!(response.status, HttpStatus::OK);
let response: JsonRpcResponse =
serde_json::from_slice(&response.body).expect("modern response must be JSON-RPC");
assert_eq!(response.id, Some(919_i64.into()));
assert!(response.error.is_none());
assert_eq!(session.selected_era, Some(ProtocolEra::Modern2026));
}
#[test]
fn public_stdio_final_subscription_listener_acknowledges_then_cancels_and_completes() {
let sent = public_stdio_final_subscription_transcript(None);
assert_public_stdio_final_subscription_transcript(&sent);
}
#[test]
fn public_stdio_final_subscription_wrong_cancellation_id_preserves_listener_correlation() {
let sent =
public_stdio_final_subscription_transcript(Some(modern_cancelled_notification(907)));
assert_public_stdio_final_subscription_transcript(&sent);
}
fn assert_public_stdio_final_subscription_transcript(sent: &[JsonRpcMessage]) {
assert_eq!(
sent.len(),
4,
"discovery plus one acknowledgement, cancellation, and completion are exact: {sent:?}"
);
assert!(sent.iter().any(|message| {
matches!(
message,
JsonRpcMessage::Response(response) if response.id == Some(905_i64.into())
)
}));
let acknowledgement = sent
.iter()
.position(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if matches!(
ServerNotification::decode(request),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
)
)
})
.expect("the listener must emit one acknowledgement");
let cancellation = sent
.iter()
.position(|message| {
matches!(
message,
JsonRpcMessage::Request(request)
if matches!(
ServerNotification::decode(request),
Ok(ServerNotification::Cancelled(params))
if params.request_id == RequestId::Number(906)
)
)
})
.expect("server termination must cancel the exact listener");
let completion = sent
.iter()
.position(|message| {
matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(906_i64.into())
&& final_subscription_completion_response(response)
)
})
.expect("server termination must complete the exact listener");
assert!(
acknowledgement < cancellation && cancellation < completion,
"the listener must acknowledge, cancel, and complete in causal order: {sent:?}"
);
}
fn exact_legacy_initialize_request(
id: i64,
client_version: serde_json::Value,
) -> JsonRpcMessage {
JsonRpcMessage::Request(JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {
"name": "live-legacy-client",
"version": client_version,
},
})),
id,
))
}
struct CancelledNotificationTransport;
impl Transport for CancelledNotificationTransport {
fn send(&mut self, _cx: &Cx, _message: &JsonRpcMessage) -> Result<(), TransportError> {
Err(TransportError::Cancelled)
}
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
Err(TransportError::Closed)
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
#[test]
fn reality_check_regression_transport_notification_sender_latches_send_failure() {
let failure = Arc::new(AtomicBool::new(false));
let sender = create_transport_notification_sender(
SharedTransport::new(ReturningProbeTransport {
receive: ReturningProbeReceive::Closed,
fail_send: true,
fail_close: false,
close_calls: Arc::new(AtomicUsize::new(0)),
}),
Cx::for_testing(),
Arc::clone(&failure),
);
sender(JsonRpcRequest::notification("notifications/progress", None));
assert!(failure.load(Ordering::Acquire));
}
#[test]
fn reality_check_regression_notification_sender_does_not_promote_cancellation() {
let failure = Arc::new(AtomicBool::new(false));
let sender = create_transport_notification_sender(
SharedTransport::new(CancelledNotificationTransport),
Cx::for_testing(),
Arc::clone(&failure),
);
sender(JsonRpcRequest::notification("notifications/progress", None));
assert!(!failure.load(Ordering::Acquire));
}
#[test]
fn reality_check_regression_returning_loop_preserves_notification_failure() {
let failure = Arc::new(AtomicBool::new(false));
let failure_from_receive = Arc::clone(&failure);
let error = Server::new("returning-notification-failure-test", "1.0.0")
.build()
.run_loop_returning(
&Cx::for_testing(),
move |_receive_cx| {
failure_from_receive.store(true, Ordering::Release);
Err(TransportError::Closed)
},
move |_send_cx, _message| Ok(()),
Arc::new(|_| {}),
Some(failure),
"custom-test",
)
.expect_err("notification failure must dominate concurrent clean EOF");
assert_eq!(error.code, McpErrorCode::InternalError);
assert_eq!(
error.data.as_ref().and_then(|data| data["stage"].as_str()),
Some("notification")
);
assert_eq!(
error.data.as_ref().and_then(|data| data["kind"].as_str()),
Some("send_failure")
);
}
#[test]
fn reality_check_regression_notification_failure_wins_cancelled_response_send() {
let failure = Arc::new(AtomicBool::new(false));
let failure_from_send = Arc::clone(&failure);
let emitted = Arc::new(AtomicBool::new(false));
let emitted_from_receive = Arc::clone(&emitted);
let error = Server::new("returning-notification-send-race-test", "1.0.0")
.build()
.run_loop_returning(
&Cx::for_testing(),
move |_receive_cx| {
if emitted_from_receive.swap(true, Ordering::AcqRel) {
Err(TransportError::Closed)
} else {
Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"ping", None, 707_i64,
)))
}
},
move |_send_cx, _message| {
failure_from_send.store(true, Ordering::Release);
Err(TransportError::Cancelled)
},
Arc::new(|_| {}),
Some(failure),
"custom-test",
)
.expect_err("latched notification I/O failure must win cancelled response send");
assert_eq!(
error.data.as_ref().and_then(|data| data["stage"].as_str()),
Some("notification")
);
assert_eq!(
error.data.as_ref().and_then(|data| data["kind"].as_str()),
Some("send_failure")
);
}
#[test]
fn reality_check_regression_parse_error_send_preserves_notification_failure() {
let failure = Arc::new(AtomicBool::new(false));
let failure_from_send = Arc::clone(&failure);
let error = Server::new("returning-notification-parse-error-race-test", "1.0.0")
.build()
.run_loop_returning(
&Cx::for_testing(),
move |_receive_cx| {
Err(TransportError::Codec(fastmcp_transport::CodecError::Json(
serde_json::from_str::<serde_json::Value>("{")
.expect_err("fixture must be invalid JSON"),
)))
},
move |_send_cx, message| {
let JsonRpcMessage::Response(response) = message else {
panic!("recoverable JSON syntax failure must emit a response");
};
assert!(response.id.is_none());
assert_eq!(
response
.error
.as_ref()
.and_then(|response_error| response_error.code.as_i32()),
Some(-32700)
);
failure_from_send.store(true, Ordering::Release);
Err(TransportError::Cancelled)
},
Arc::new(|_| {}),
Some(failure),
"custom-test",
)
.expect_err("latched notification failure must win cancelled parse-error reply");
assert_eq!(
error.data.as_ref().and_then(|data| data["stage"].as_str()),
Some("notification")
);
assert_eq!(
error.data.as_ref().and_then(|data| data["kind"].as_str()),
Some("send_failure")
);
}
impl Transport for ReturningProbeTransport {
fn send(&mut self, _cx: &Cx, _message: &JsonRpcMessage) -> Result<(), TransportError> {
if self.fail_send {
Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"test returning-transport send failure",
)))
} else {
Ok(())
}
}
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
match self.receive {
ReturningProbeReceive::Closed => Err(TransportError::Closed),
ReturningProbeReceive::Cancelled => Err(TransportError::Cancelled),
ReturningProbeReceive::Timeout => Err(TransportError::Timeout),
#[cfg(feature = "legacy-2024-11-05")]
ReturningProbeReceive::PingThenClosed => {
self.receive = ReturningProbeReceive::Closed;
Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"ping", None, 704_i64,
)))
}
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
self.close_calls.fetch_add(1, Ordering::AcqRel);
if self.fail_close {
Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"test returning-transport close failure",
)))
} else {
Ok(())
}
}
}
impl Transport for SharedTransportCountingTransport {
fn send(&mut self, _cx: &Cx, _message: &JsonRpcMessage) -> Result<(), TransportError> {
self.send_calls.fetch_add(1, Ordering::AcqRel);
Ok(())
}
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
self.recv_calls.fetch_add(1, Ordering::AcqRel);
Err(TransportError::Closed)
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
self.close_calls.fetch_add(1, Ordering::AcqRel);
if self.fail_close {
Err(TransportError::Io(std::io::Error::other(
"counting transport close failure",
)))
} else {
Ok(())
}
}
}
impl Transport for ProtocolPolicyProbeTransport {
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
self.sent
.lock()
.expect("protocol-policy test sent-message mutex must not be poisoned")
.push(message.clone());
Ok(())
}
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
self.receive_calls.fetch_add(1, Ordering::AcqRel);
self.next.take().ok_or(TransportError::Closed)
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
impl Transport for ProtocolPolicyScriptTransport {
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
self.sent
.lock()
.expect("protocol-policy test sent-message mutex must not be poisoned")
.push(message.clone());
Ok(())
}
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
self.receive_calls.fetch_add(1, Ordering::AcqRel);
self.inbound.pop_front().ok_or(TransportError::Closed)
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
#[test]
fn invalid_launch_policy_rejects_server_builder_before_transport_setup() {
let builder = ServerBuilder::from_launch_protocol_policy(
"invalid-launch-policy",
"1.0.0",
Err(ServerLaunchPolicyError::InvalidValue),
);
assert!(matches!(
builder,
Err(ServerLaunchPolicyError::InvalidValue)
));
}
#[test]
fn server_builder_policy_admits_selected_modern_runtime_era() {
let sent = Arc::new(Mutex::new(Vec::new()));
let receive_calls = Arc::new(AtomicUsize::new(0));
let server = Server::new("selected-modern-runtime-policy", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build();
assert_eq!(server.protocol_policy(), ProtocolPolicy::ModernOnly);
server
.run_transport_returning_with_cx(
&Cx::for_testing(),
ProtocolPolicyProbeTransport {
next: Some(modern_discovery_opening_request()),
sent: Arc::clone(&sent),
receive_calls: Arc::clone(&receive_calls),
},
)
.expect("the selected modern policy must admit a modern discovery opener");
assert_eq!(receive_calls.load(Ordering::Acquire), 2);
let sent = sent
.lock()
.expect("protocol-policy test sent-message mutex must not be poisoned");
let [JsonRpcMessage::Response(response)] = sent.as_slice() else {
panic!("admitted modern discovery must emit exactly one response");
};
assert_eq!(response.id, Some(905_i64.into()));
assert!(response.error.is_none());
assert!(response.result.is_some());
}
// Exact-2024 era: selects LegacyOnly, which a no-legacy build refuses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn server_builder_policy_rejects_same_modern_runtime_era_when_legacy_only() {
let sent = Arc::new(Mutex::new(Vec::new()));
let receive_calls = Arc::new(AtomicUsize::new(0));
let server = Server::new("selected-modern-runtime-policy", "1.0.0")
.protocol_policy(ProtocolPolicy::LegacyOnly)
.expect("LegacyOnly must be available to this test build")
.build();
assert_eq!(server.protocol_policy(), ProtocolPolicy::LegacyOnly);
let error = server
.run_transport_returning_with_cx(
&Cx::for_testing(),
ProtocolPolicyProbeTransport {
next: Some(modern_discovery_opening_request()),
sent: Arc::clone(&sent),
receive_calls: Arc::clone(&receive_calls),
},
)
.expect_err("changing only the policy must reject the modern opener");
assert_eq!(
error.data.as_ref().and_then(|data| data["stage"].as_str()),
Some("protocol")
);
assert_eq!(
error.data.as_ref().and_then(|data| data["kind"].as_str()),
Some("era_admission")
);
assert_eq!(receive_calls.load(Ordering::Acquire), 1);
let sent = sent
.lock()
.expect("protocol-policy test sent-message mutex must not be poisoned");
let [JsonRpcMessage::Response(response)] = sent.as_slice() else {
panic!("rejected modern discovery must emit exactly one refusal");
};
assert_eq!(response.id, Some(905_i64.into()));
assert!(response.result.is_none());
assert_eq!(
response
.error
.as_ref()
.and_then(|response_error| response_error.code.as_i32()),
Some(-32600)
);
}
// Exact-2024 era: tests Auto classification, which a no-legacy build refuses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn auto_runtime_keeps_a_modern_opening_on_the_modern_dispatch_path() {
let sent = Arc::new(Mutex::new(Vec::new()));
let receive_calls = Arc::new(AtomicUsize::new(0));
Server::new("auto-modern-runtime-policy", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.build()
.run_transport_returning_with_cx(
&Cx::for_testing(),
ProtocolPolicyProbeTransport {
next: Some(modern_discovery_opening_request()),
sent: Arc::clone(&sent),
receive_calls: Arc::clone(&receive_calls),
},
)
.expect("Auto must keep a supported modern opener on the modern path");
assert_eq!(receive_calls.load(Ordering::Acquire), 2);
let sent = sent
.lock()
.expect("protocol-policy test sent-message mutex must not be poisoned");
let [JsonRpcMessage::Response(response)] = sent.as_slice() else {
panic!("modern Auto selection must emit exactly one discovery response");
};
assert_eq!(response.id, Some(905_i64.into()));
assert!(response.error.is_none());
assert!(response.result.is_some());
}
// Exact-2024 era: negotiates a legacy initialize.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn returning_transport_negotiates_initialize_proposals_before_tools_list() {
for proposal in ["2024-11-05", "2025-03-26", "2025-06-18", "2099-01-01"] {
let sent = Arc::new(Mutex::new(Vec::new()));
let mut initialize = exact_legacy_initialize_request(711, serde_json::json!("1.0.0"));
let JsonRpcMessage::Request(request) = &mut initialize else {
panic!("initialize fixture must be a request");
};
request.params.as_mut().expect("initialize params")["protocolVersion"] =
serde_json::json!(proposal);
run_returning_transport_with_test_runtime(
Server::new("proposal-negotiation", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto policy")
.tool(LiveLegacyRuntimeConnectionTool)
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
initialize,
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
JsonRpcMessage::Request(JsonRpcRequest::new("tools/list", None, 712_i64)),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect("proposal must negotiate and retain a usable session");
let sent = sent.lock().expect("sent messages");
let responses: Vec<_> = sent
.iter()
.filter_map(|message| match message {
JsonRpcMessage::Response(response) => Some(response),
JsonRpcMessage::Request(_) => None,
})
.collect();
assert_eq!(responses.len(), 2, "{proposal}");
assert!(
responses.iter().all(|response| response.error.is_none()),
"{responses:?}"
);
assert_eq!(
responses[0].result.as_ref().expect("initialize result")["protocolVersion"],
"2024-11-05"
);
let tools = responses[1].result.as_ref().expect("tools result")["tools"]
.as_array()
.expect("tools array");
assert!(
tools
.iter()
.any(|tool| tool["name"] == "live_legacy_runtime_connection_tool")
);
}
}
// Exact-2024 era: tests Auto classification, which a no-legacy build refuses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn stdio_initialize_proposals_select_legacy_negotiation() {
for proposal in ["2024-11-05", "2025-03-26", "2025-06-18", "2099-01-01"] {
let request: JsonRpcRequest = serde_json::from_value(serde_json::json!({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": proposal, "capabilities": {},
"clientInfo": {"name": "negotiation-test", "version": "1"}}
}))
.expect("valid initialize envelope");
let mut classifier = StdioEraClassifier::new(ProtocolPolicy::Auto);
assert_eq!(
classify_initial_stdio_envelope(&mut classifier, &JsonRpcMessage::Request(request)),
Ok(ProtocolEra::Legacy2024),
"{proposal} must reach initialize negotiation"
);
}
}
// Exact-2024 era: tests Auto classification, which a no-legacy build refuses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn stdio_auto_classifies_the_first_envelope_before_legacy_fallback() {
let mut accepted = StdioEraClassifier::new(ProtocolPolicy::Auto);
assert_eq!(
classify_initial_stdio_envelope(&mut accepted, &modern_discovery_opening_request()),
Ok(ProtocolEra::Modern2026),
"the exact modern opening selects modern exactly once"
);
let mut rejected = StdioEraClassifier::new(ProtocolPolicy::Auto);
let response = JsonRpcMessage::Response(JsonRpcResponse::success(
906_i64.into(),
serde_json::json!({}),
));
assert!(matches!(
classify_initial_stdio_envelope(&mut rejected, &response),
Err(StdioEraDecision::RejectedAndClosed {
reason:
fastmcp_protocol::protocol_policy::StdioEraRejection::ResponseCannotClassify,
})
));
assert!(matches!(
rejected.state(),
fastmcp_protocol::protocol_policy::StdioEraState::TerminalWithoutEra
));
assert!(matches!(
rejected.classify_opening(StdioOpeningFrame::LegacyInitialize),
StdioEraDecision::AlreadyTerminal
));
let mut malformed = StdioEraClassifier::new(ProtocolPolicy::Auto);
assert!(matches!(
reject_initial_stdio_malformed(&mut malformed),
StdioEraDecision::RejectedAndClosed {
reason: fastmcp_protocol::protocol_policy::StdioEraRejection::MalformedOpeningFrame,
}
));
assert!(matches!(
malformed.state(),
fastmcp_protocol::protocol_policy::StdioEraState::TerminalWithoutEra
));
}
#[test]
fn cancellation_codec_uses_negotiated_era_and_modern_metadata_is_optional() {
let metadata_free = JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": 907,
})),
);
assert!(matches!(
CancellationWireMessage::decode(
ProtocolEra::Legacy2024,
CancellationSender::Client,
&metadata_free,
),
Ok(CancellationWireMessage::Legacy2024 { .. })
));
assert!(matches!(
CancellationWireMessage::decode(
ProtocolEra::Modern2026,
CancellationSender::Client,
&metadata_free,
),
Ok(CancellationWireMessage::Modern2026 { .. })
));
let with_metadata = JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": 907,
"_meta": {
"trace": "optional-final-metadata",
},
})),
);
assert!(matches!(
CancellationWireMessage::decode(
ProtocolEra::Modern2026,
CancellationSender::Client,
&with_metadata,
),
Ok(CancellationWireMessage::Modern2026 { .. })
));
assert!(
CancellationWireMessage::decode(
ProtocolEra::Legacy2024,
CancellationSender::Client,
&with_metadata,
)
.is_ok()
);
let with_final_reserved_metadata = JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": 907,
"_meta": {
"io.modelcontextprotocol/subscriptionId": 907,
},
})),
);
assert!(
CancellationWireMessage::decode(
ProtocolEra::Legacy2024,
CancellationSender::Client,
&with_final_reserved_metadata,
)
.is_err()
);
}
// Exact-2024 era: used only by legacy application content tests.
#[cfg(feature = "legacy-2024-11-05")]
struct ApplicationContentTool {
content: Vec<Content>,
calls: Arc<AtomicUsize>,
fail: bool,
}
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for ApplicationContentTool {
fn definition(&self) -> Tool {
let mut definition = LiveRuntimeListedTool.definition();
definition.name = "application_content".to_owned();
definition
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
ctx.checkpoint()?;
self.calls.fetch_add(1, Ordering::SeqCst);
if self.fail {
Err(McpError::tool_error("application tool failed"))
} else {
Ok(self.content.clone())
}
}
}
// Exact-2024 era: used only by legacy application content tests.
#[cfg(feature = "legacy-2024-11-05")]
struct ApplicationContentResultMiddleware(serde_json::Value);
#[cfg(feature = "legacy-2024-11-05")]
impl Middleware for ApplicationContentResultMiddleware {
fn on_response(
&self,
_ctx: &McpContext,
request: &JsonRpcRequest,
response: serde_json::Value,
) -> McpResult<serde_json::Value> {
Ok(if request.method == "tools/call" {
self.0.clone()
} else {
response
})
}
}
// Exact-2024 era: used only by legacy application content tests.
#[cfg(feature = "legacy-2024-11-05")]
fn application_content_transcript(builder: ServerBuilder) -> Vec<JsonRpcMessage> {
let sent = Arc::new(Mutex::new(Vec::new()));
let call = |id| {
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({"name": "application_content", "arguments": {}})),
id,
))
};
// Finite transport fixture; all parsing, lifecycle admission,
// middleware, request-owned handler execution, and result validation
// use the actual returning server entry point.
run_returning_transport_with_test_runtime(
builder
.protocol_policy(ProtocolPolicy::LegacyOnly)
.expect("legacy test profile")
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
exact_legacy_initialize_request(41, serde_json::json!("1.0.0")),
call(40_i64),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
call(42_i64),
JsonRpcMessage::Request(JsonRpcRequest::new("ping", None, 43_i64)),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect("finite legacy transport closes cleanly");
let messages = sent.lock().expect("captured server responses").clone();
let response = |id: i64| {
messages
.iter()
.find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(id.into()) => {
Some(response)
}
_ => None,
})
.expect("correlated response")
};
assert_eq!(
response(40_i64)
.error
.as_ref()
.map(|error| error.code.clone()),
Some((-32600).into())
);
assert!(response(40_i64).result.is_none());
assert!(response(43_i64).error.is_none(), "connection stays usable");
messages
}
// Exact-2024 era: used only by legacy application content tests.
#[cfg(feature = "legacy-2024-11-05")]
fn application_content_response(messages: &[JsonRpcMessage]) -> &JsonRpcResponse {
messages
.iter()
.find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(42_i64.into()) => {
Some(response)
}
_ => None,
})
.expect("tool call has its original response ID")
}
// Exact-2024 era: legacy application tool content option.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_application_content_roundtrips_and_strict_mode_rejects_extensions() {
for payload in [
serde_json::json!({"type": "audio", "data": "YXVkaW8=", "mimeType": "audio/wav"}),
serde_json::json!({"type": "resource", "resource": {"uri": "file:///empty"}}),
serde_json::json!({"type": "resource", "resource": {
"uri": "file:///both", "text": "text", "blob": "Ynl0ZXM="
}}),
serde_json::json!({"type": "text", "text": "ordinary"}),
] {
for enabled in [false, true] {
let calls = Arc::new(AtomicUsize::new(0));
let mut builder =
Server::new("application-content", "1").tool(ApplicationContentTool {
content: vec![serde_json::from_value(payload.clone()).unwrap()],
calls: Arc::clone(&calls),
fail: false,
});
if enabled {
builder = builder.legacy_application_tool_content(true);
}
let messages = application_content_transcript(builder);
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"uninitialized call is denied; admitted call executes once"
);
let response = application_content_response(&messages);
if enabled || payload["type"] == "text" {
assert!(response.error.is_none(), "{response:?}");
assert_eq!(
response.result,
Some(serde_json::json!({"content": [payload.clone()]}))
);
} else {
assert_eq!(
response.error.as_ref().map(|error| error.code.clone()),
Some((-32603).into())
);
assert!(response.result.is_none());
}
}
}
}
// Exact-2024 era: legacy application tool content option.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_application_content_rejects_malformed_results_and_forbidden_envelopes() {
let mut rejected = vec![
serde_json::json!({"content": [{"type": "text", "text": 3}]}),
serde_json::json!({"content": [{"type": "audio", "data": "YQ=="}]}),
serde_json::json!({"content": [{"type": "resource", "resource": {"text": "missing URI"}}]}),
serde_json::json!({"content": [{"type": "resource", "resource": {"uri": "file:///both", "text": "ok", "blob": 42}}]}),
serde_json::json!({"content": [{"type": "audio", "data": "YQ==", "mimeType": "audio/wav", "extension": {"kept": true}}]}),
serde_json::json!({"content": [{"type": "resource", "resource": {"uri": "file:///both", "text": "text", "blob": "Yg==", "extension": 7}}]}),
serde_json::json!({"content": null}),
serde_json::json!({"content": [], "isError": null}),
serde_json::json!({"content": [], "_meta": null}),
serde_json::json!({"content": [], "resultType": "complete"}),
];
for key in [
"io.modelcontextprotocol/protocolVersion",
"io.modelcontextprotocol/clientCapabilities",
"io.modelcontextprotocol/clientInfo",
"io.modelcontextprotocol/logLevel",
"io.modelcontextprotocol/serverInfo",
"io.modelcontextprotocol/subscriptionId",
] {
rejected.push(serde_json::json!({"content": [], "_meta": {key: "forbidden"}}));
}
for result in rejected {
let calls = Arc::new(AtomicUsize::new(0));
let builder = Server::new("application-invalid-content", "1")
.legacy_application_tool_content(true)
.tool(ApplicationContentTool {
content: vec![Content::text("ordinary handler result")],
calls: Arc::clone(&calls),
fail: false,
})
.middleware(ApplicationContentResultMiddleware(result.clone()));
let messages = application_content_transcript(builder);
assert_eq!(calls.load(Ordering::SeqCst), 1);
let response = application_content_response(&messages);
assert!(
response.result.is_none(),
"invalid result escaped: {result}"
);
assert_eq!(
response.error.as_ref().map(|error| error.code.clone()),
Some((-32603).into())
);
}
}
// Exact-2024 era: legacy application tool content option.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_application_content_preserves_open_metadata_and_content_fields() {
let result = serde_json::json!({
"content": [
{"type": "audio", "data": "YQ==", "mimeType": "audio/wav"},
{"type": "resource", "resource": {"uri": "file:///both", "text": "text", "blob": "Yg=="}}
],
"isError": false,
"_meta": {"caller-note": {"kept": true}}
});
let calls = Arc::new(AtomicUsize::new(0));
let messages = application_content_transcript(
Server::new("application-open-content", "1")
.legacy_application_tool_content(true)
.tool(ApplicationContentTool {
content: Vec::new(),
calls: Arc::clone(&calls),
fail: false,
})
.middleware(ApplicationContentResultMiddleware(result.clone())),
);
assert_eq!(calls.load(Ordering::SeqCst), 1);
let response = application_content_response(&messages);
assert!(response.error.is_none(), "{response:?}");
assert_eq!(response.result, Some(result));
}
// Exact-2024 era: legacy application tool content option.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_application_content_does_not_bypass_authentication() {
let calls = Arc::new(AtomicUsize::new(0));
let messages = application_content_transcript(
Server::new("application-denied-content", "1")
.legacy_application_tool_content(true)
.auth_provider(AlwaysFailAuthProvider)
.tool(ApplicationContentTool {
content: vec![Content::text("must not execute")],
calls: Arc::clone(&calls),
fail: false,
}),
);
assert_eq!(calls.load(Ordering::SeqCst), 0);
let response = application_content_response(&messages);
assert!(response.result.is_none());
// Authentication admission deliberately normalizes provider errors to
// ResourceForbidden; the provider's own invalid-request code is private.
assert_eq!(
response.error.as_ref().map(|error| error.code.clone()),
Some((-32002).into())
);
assert_eq!(
response.error.as_ref().map(|error| error.message.as_str()),
Some("Authentication failed")
);
}
// Exact-2024 era: legacy application tool content option.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_application_content_preserves_tool_errors() {
let calls = Arc::new(AtomicUsize::new(0));
let messages = application_content_transcript(
Server::new("application-tool-error", "1")
.legacy_application_tool_content(true)
.tool(ApplicationContentTool {
content: Vec::new(),
calls: Arc::clone(&calls),
fail: true,
}),
);
assert_eq!(calls.load(Ordering::SeqCst), 1);
let response = application_content_response(&messages);
assert!(response.error.is_none());
assert_eq!(
response.result,
Some(serde_json::json!({
"content": [{"type": "text", "text": "application tool failed"}], "isError": true
}))
);
}
// Exact-2024 era: routes legacy frames through the 2024 adapter.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_runtime_routes_exact_legacy_frames_through_the_2024_adapter() {
let sent = Arc::new(Mutex::new(Vec::new()));
let receive_calls = Arc::new(AtomicUsize::new(0));
run_returning_transport_with_test_runtime(
Server::new("live-legacy-runtime", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(LiveRuntimeListedTool)
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
exact_legacy_initialize_request(41, serde_json::json!("1.0.0")),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_runtime_listed_tool",
"arguments": {},
})),
42_i64,
)),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::clone(&receive_calls),
},
)
.expect("an exact legacy lifecycle must complete through the live adapter");
assert_eq!(receive_calls.load(Ordering::Acquire), 4);
let sent = sent
.lock()
.expect("protocol-policy test sent-message mutex must not be poisoned");
let [
JsonRpcMessage::Response(initialize),
JsonRpcMessage::Response(call),
] = sent.as_slice()
else {
panic!("legacy initialize and tools/call must be the only adapter responses");
};
assert_eq!(initialize.id, Some(41_i64.into()));
assert_eq!(
initialize
.result
.as_ref()
.and_then(|result| result["protocolVersion"].as_str()),
Some(LEGACY_PROTOCOL_VERSION)
);
assert_eq!(call.id, Some(42_i64.into()));
assert_eq!(
call.result
.as_ref()
.and_then(|result| result["content"].as_array())
.and_then(|content| content.first())
.and_then(|content| content["text"].as_str()),
Some("live runtime legacy adapter request 42")
);
assert!(
call.result
.as_ref()
.is_some_and(|result| result.get("resultType").is_none())
);
}
// Exact-2024 era: legacy cancellation on a legacy connection.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_legacy_runtime_ignores_invalid_cancellation_without_closing_the_connection() {
let sent = Arc::new(Mutex::new(Vec::new()));
let receive_calls = Arc::new(AtomicUsize::new(0));
run_returning_transport_with_test_runtime(
Server::new("legacy-invalid-cancellation", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
exact_legacy_initialize_request(71, serde_json::json!("1.0.0")),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": 72,
"unexpected": true,
})),
)),
JsonRpcMessage::Request(JsonRpcRequest::new("ping", None, 72_i64)),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::clone(&receive_calls),
},
)
.expect("an invalid cancellation must leave the legacy connection usable");
assert_eq!(receive_calls.load(Ordering::Acquire), 5);
let sent = sent
.lock()
.expect("protocol-policy test sent-message mutex must not be poisoned");
let [
JsonRpcMessage::Response(initialize),
JsonRpcMessage::Response(ping),
] = sent.as_slice()
else {
panic!(
"the invalid notification must not respond or prevent the following legacy request"
);
};
assert_eq!(initialize.id, Some(71_i64.into()));
assert!(initialize.error.is_none());
assert_eq!(ping.id, Some(72_i64.into()));
assert!(ping.error.is_none());
}
#[test]
fn rh5_stdio_rejects_server_notification_without_dispatch_or_connection_loss() {
let sent = Arc::new(Mutex::new(Vec::new()));
let receive_calls = Arc::new(AtomicUsize::new(0));
let handler_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let saw_credential = Arc::new(AtomicBool::new(false));
let server_notification = JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/progress",
Some(serde_json::json!({
"progressToken": "server-only",
"progress": 1,
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
},
})),
));
let valid_call = JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "modern_http_auth_counter",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
73_i64,
));
Server::new("stdio-wrong-direction-notification", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.middleware(ModernHttpAuthMiddleware {
calls: Arc::clone(&middleware_calls),
saw_credential: Arc::clone(&saw_credential),
})
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&handler_calls),
})
.build()
.run_transport_returning_with_cx(
&Cx::for_testing(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
modern_discovery_request(72),
server_notification,
valid_call,
]),
sent: Arc::clone(&sent),
receive_calls: Arc::clone(&receive_calls),
},
)
.expect("wrong-direction stdio notification must not close the connection");
assert_eq!(receive_calls.load(Ordering::Acquire), 4);
assert_eq!(middleware_calls.load(Ordering::Acquire), 2);
assert_eq!(handler_calls.load(Ordering::Acquire), 1);
assert!(!saw_credential.load(Ordering::Acquire));
let sent = sent
.lock()
.expect("stdio sent-message mutex must not be poisoned");
assert!(matches!(
sent.as_slice(),
[JsonRpcMessage::Response(discovery), JsonRpcMessage::Response(call)]
if discovery.id == Some(72_i64.into())
&& discovery.error.is_none()
&& call.id == Some(73_i64.into())
&& call.error.is_none()
));
}
// Exact-2024 era: legacy runtime context after a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_split_transport_legacy_runtime_retains_state_notifications_and_sampling() {
let sent = live_legacy_runtime_connection_transcript(true);
assert!(
sent.iter().any(|message| {
matches!(message, JsonRpcMessage::Request(request) if request.method == "sampling/createMessage")
}),
"an advertised legacy sampling capability must install the connection request sender"
);
assert_eq!(
sent.iter()
.filter(|message| {
matches!(message, JsonRpcMessage::Request(request) if request.method == "notifications/progress")
})
.count(),
2,
"both handler calls must use the connection notification sender for progress"
);
assert_eq!(
sent.iter()
.filter(|message| {
matches!(message, JsonRpcMessage::Request(request) if request.method == "notifications/message")
})
.count(),
2,
"the negotiated legacy log level must use the same notification sender"
);
let first = sent.iter().find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(703_i64.into()) => {
response.result.as_ref()
}
_ => None,
});
assert_eq!(
first
.and_then(|result| result["content"].as_array())
.and_then(|content| content.first())
.and_then(|content| content["text"].as_str()),
Some("legacy-runtime-1-sampled-value")
);
let second = sent.iter().find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(704_i64.into()) => {
response.result.as_ref()
}
_ => None,
});
assert_eq!(
second
.and_then(|result| result["content"].as_array())
.and_then(|content| content.first())
.and_then(|content| content["text"].as_str()),
Some("legacy-runtime-2-without-sampling"),
"the second public-path call must observe the first call's SessionState mutation"
);
}
// Exact-2024 era: legacy runtime context after a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_split_transport_legacy_context_installs_roots_provider_after_negotiation() {
let sent = legacy_roots_context_transcript(true);
assert!(
sent.iter().any(|message| {
matches!(message, JsonRpcMessage::Request(request) if request.method == "roots/list")
}),
"a negotiated roots capability must install a request-scoped roots provider"
);
let response = sent.iter().find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(732_i64.into()) => {
response.result.as_ref()
}
_ => None,
});
assert_eq!(
response
.and_then(|result| result["content"].as_array())
.and_then(|content| content.first())
.and_then(|content| content["text"].as_str()),
Some("negotiated roots: file:///workspace")
);
}
// Exact-2024 era: legacy runtime context after a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_split_transport_legacy_context_withholds_roots_provider_without_capability() {
let sent = legacy_roots_context_transcript(false);
assert!(
!sent.iter().any(|message| {
matches!(message, JsonRpcMessage::Request(request) if request.method == "roots/list")
}),
"changing only the negotiated roots capability must not emit a roots request"
);
let response = sent.iter().find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(732_i64.into()) => {
response.result.as_ref()
}
_ => None,
});
assert_eq!(
response
.and_then(|result| result["content"].as_array())
.and_then(|content| content.first())
.and_then(|content| content["text"].as_str()),
Some("roots authority unavailable")
);
}
// Exact-2024 era: legacy progress after a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_stdio_legacy_progress_token_installs_f64_notification_sender() {
let sent = legacy_stdio_progress_transcript(true);
let progress = sent.iter().find_map(|message| match message {
JsonRpcMessage::Request(request) if request.method == "notifications/progress" => {
Some(request)
}
_ => None,
});
let params = progress
.and_then(|request| request.params.as_ref())
.expect("the public legacy stdio request must emit progress");
assert_eq!(
params.get("progressToken"),
Some(&serde_json::json!("legacy-stdio-progress"))
);
assert_eq!(
params.get("progress").and_then(serde_json::Value::as_f64),
Some(1.0)
);
assert_eq!(
params.get("message"),
Some(&serde_json::json!("legacy connection progress")),
"legacy progress must retain its f64 ProgressParams wire model"
);
}
// Exact-2024 era: legacy progress after a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_stdio_legacy_progress_sender_is_absent_when_only_token_is_removed() {
let sent = legacy_stdio_progress_transcript(false);
assert!(
sent.iter().any(|message| matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(712_i64.into()) && response.error.is_none()
)),
"removing only the progress token must retain normal legacy handler dispatch"
);
assert!(
!sent.iter().any(|message| matches!(
message,
JsonRpcMessage::Request(request) if request.method == "notifications/progress"
)),
"removing only the progress token must leave the legacy progress sender absent"
);
}
// Exact-2024 era: legacy server-to-client sampling.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_split_legacy_sampling_cancel_late_reply_preserves_connection() {
let sent = Arc::new(Mutex::new(Vec::new()));
let (outbound, inbound) = sync_channel(32);
let result = run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
Server::new("legacy-cancel-late", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(LiveLegacyRuntimeConnectionTool)
.build(),
LegacyCancelLateRecv {
phase: 0,
outbound: inbound,
reverse_id: None,
stale_offset: 0,
cancellation_request_id: RequestId::Number(802),
},
LiveLegacyRuntimeSplitSend {
sent: Arc::clone(&sent),
outbound,
},
);
assert!(
result.is_ok(),
"retired late response must preserve the connection: {result:?}; sent={:?}",
sent.lock().expect("sent messages mutex")
);
assert!(
sent.lock()
.expect("sent messages mutex")
.iter()
.any(|message| matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(803_i64.into())
&& response
.result
.as_ref()
.and_then(|result| result.get("content"))
.is_some_and(|content| content.to_string().contains("legacy-runtime-2-without-sampling"))
))
);
}
// Exact-2024 era: legacy server-to-client sampling.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_split_legacy_sampling_numeric_cancellation_alias_retires_late_reply() {
let sent = Arc::new(Mutex::new(Vec::new()));
let (outbound, inbound) = sync_channel(32);
let result = run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
Server::new("legacy-cancel-numeric-alias", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(LiveLegacyRuntimeConnectionTool)
.build(),
LegacyCancelLateRecv {
phase: 0,
outbound: inbound,
reverse_id: None,
stale_offset: 0,
cancellation_request_id: RequestId::Integer("802e0".to_owned()),
},
LiveLegacyRuntimeSplitSend {
sent: Arc::clone(&sent),
outbound,
},
);
result.expect(
"numeric cancellation aliases must retire the late reply and preserve the connection",
);
assert!(
sent.lock()
.expect("sent messages mutex")
.iter()
.any(|message| matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(803_i64.into())
&& response
.result
.as_ref()
.and_then(|result| result.get("content"))
.is_some_and(|content| content.to_string().contains("legacy-runtime-2-without-sampling"))
)),
"changing only the cancellation ID spelling must retain the successful follow-up request"
);
}
// Exact-2024 era: legacy server-to-client sampling.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_split_legacy_sampling_next_negative_reply_remains_invalid() {
let sent = Arc::new(Mutex::new(Vec::new()));
let (outbound, inbound) = sync_channel(32);
let result = run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
Server::new("legacy-cancel-next-negative", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(LiveLegacyRuntimeConnectionTool)
.build(),
LegacyCancelLateRecv {
phase: 0,
outbound: inbound,
reverse_id: None,
stale_offset: -1,
cancellation_request_id: RequestId::Number(802),
},
LiveLegacyRuntimeSplitSend {
sent: Arc::clone(&sent),
outbound,
},
);
result.expect_err("next unissued negative response must remain invalid");
assert!(
!sent
.lock()
.expect("sent messages mutex")
.iter()
.any(|message| matches!(
message,
JsonRpcMessage::Response(response) if response.id == Some(803_i64.into())
))
);
}
// Exact-2024 era: legacy runtime context after a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_split_transport_legacy_runtime_rejects_sampling_when_only_capability_is_removed() {
let sent = live_legacy_runtime_connection_transcript(false);
assert!(
!sent.iter().any(|message| {
matches!(message, JsonRpcMessage::Request(request) if request.method == "sampling/createMessage")
}),
"removing only the advertised sampling capability must prevent a reverse request"
);
assert_eq!(
sent.iter()
.filter(|message| {
matches!(message, JsonRpcMessage::Request(request) if request.method == "notifications/progress")
})
.count(),
2,
"the negative must retain the same connection state and notification path"
);
let rejected = sent.iter().find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(703_i64.into()) => {
Some(response)
}
_ => None,
});
let rejected_result = rejected
.and_then(|response| response.result.as_ref())
.expect("the tool-level sampling refusal must use a legacy tools/call result");
assert_eq!(
rejected_result.get("isError"),
Some(&serde_json::json!(true))
);
assert!(
rejected_result
.get("content")
.and_then(serde_json::Value::as_array)
.and_then(|content| content.first())
.and_then(|content| content.get("text"))
.and_then(serde_json::Value::as_str)
.is_some_and(|message| message.contains("Sampling not available")),
"the otherwise identical handler call must fail at the advertised-capability gate"
);
let second = sent.iter().find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(704_i64.into()) => {
response.result.as_ref()
}
_ => None,
});
assert_eq!(
second
.and_then(|result| result["content"].as_array())
.and_then(|content| content.first())
.and_then(|content| content["text"].as_str()),
Some("legacy-runtime-2-without-sampling"),
"removing only sampling capability must not replace the retained SessionState"
);
}
// Exact-2024 era: legacy tools/call argument validation.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_runtime_rejects_only_non_object_legacy_tool_arguments_without_advancing_lifecycle() {
let sent = Arc::new(Mutex::new(Vec::new()));
run_returning_transport_with_test_runtime(
Server::new("live-legacy-runtime-negative", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(LiveRuntimeListedTool)
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
exact_legacy_initialize_request(51, serde_json::json!("1.0.0")),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_runtime_listed_tool",
"arguments": [],
})),
52_i64,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_runtime_listed_tool",
"arguments": {},
})),
53_i64,
)),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect("only the changed invalid arguments field must be rejected");
let sent = sent
.lock()
.expect("protocol-policy test sent-message mutex must not be poisoned");
let [
JsonRpcMessage::Response(initialize),
JsonRpcMessage::Response(rejected),
JsonRpcMessage::Response(call),
] = sent.as_slice()
else {
panic!(
"a rejected tools/call must leave the exact lifecycle ready for the unchanged valid call"
);
};
assert_eq!(initialize.id, Some(51_i64.into()));
assert_eq!(
initialize
.result
.as_ref()
.and_then(|result| result["protocolVersion"].as_str()),
Some(LEGACY_PROTOCOL_VERSION)
);
assert_eq!(rejected.id, Some(52_i64.into()));
assert_eq!(
rejected
.error
.as_ref()
.and_then(|error| error.code.as_i32()),
Some(-32602)
);
assert_eq!(call.id, Some(53_i64.into()));
assert_eq!(
call.result
.as_ref()
.and_then(|result| result["content"].as_array())
.and_then(|content| content.first())
.and_then(|content| content["text"].as_str()),
Some("live runtime legacy adapter request 53")
);
assert!(
call.result
.as_ref()
.is_some_and(|result| result.get("resultType").is_none())
);
}
// Exact-2024 era: legacy completion/complete routing.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_runtime_routes_exact_legacy_completion_through_the_legacy_router_surface() {
let sent = Arc::new(Mutex::new(Vec::new()));
run_returning_transport_with_test_runtime(
Server::new("live-legacy-completion", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.completion_handler(LiveLegacyCompletionHandler)
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
exact_legacy_initialize_request(61, serde_json::json!("1.0.0")),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"completion/complete",
Some(serde_json::json!({
"ref": {"type": "ref/prompt", "name": "deploy"},
"argument": {"name": "environment", "value": "sta"},
})),
62_i64,
)),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect("the live exact legacy completion route must return its legacy result");
let sent = sent
.lock()
.expect("legacy completion sent-message mutex must not be poisoned");
let [
JsonRpcMessage::Response(initialize),
JsonRpcMessage::Response(completion),
] = sent.as_slice()
else {
panic!("exact legacy completion must emit initialize and completion responses");
};
assert_eq!(initialize.id, Some(61_i64.into()));
assert_eq!(completion.id, Some(62_i64.into()));
assert_eq!(
completion
.result
.as_ref()
.and_then(|result| result["completion"]["values"].as_array())
.and_then(|values| values.first())
.and_then(serde_json::Value::as_str),
Some("legacy:sta")
);
assert!(
completion
.result
.as_ref()
.is_some_and(|result| result.get("resultType").is_none())
);
}
// Exact-2024 era: a legacy completion notification after a legacy initialize.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_runtime_rejects_completion_notification_without_invoking_handler() {
let sent = Arc::new(Mutex::new(Vec::new()));
let calls = Arc::new(AtomicUsize::new(0));
let completion_params = serde_json::json!({
"ref": {"type": "ref/prompt", "name": "deploy"},
"argument": {"name": "environment", "value": "sta"},
});
run_returning_transport_with_test_runtime(
Server::new("live-legacy-completion-id", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.completion_handler(CountingLegacyCompletionHandler {
calls: Arc::clone(&calls),
})
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
exact_legacy_initialize_request(63, serde_json::json!("1.0.0")),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"completion/complete",
Some(completion_params.clone()),
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"completion/complete",
Some(completion_params),
64_i64,
)),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect("the rejected notification must leave the valid request executable");
assert_eq!(calls.load(Ordering::Acquire), 1);
let sent = sent
.lock()
.expect("completion notification sent-message mutex must not be poisoned");
let [
JsonRpcMessage::Response(initialize),
JsonRpcMessage::Response(completion),
] = sent.as_slice()
else {
panic!("only initialize and the ID-bearing completion may emit responses");
};
assert_eq!(initialize.id, Some(63_i64.into()));
assert_eq!(completion.id, Some(64_i64.into()));
assert_eq!(
completion
.result
.as_ref()
.and_then(|result| result["completion"]["values"].as_array())
.and_then(|values| values.first())
.and_then(serde_json::Value::as_str),
Some("legacy:sta")
);
}
#[test]
fn builder_http_endpoint_dispatches_a_modern_request_end_to_end() {
let cx = Cx::for_testing();
let endpoint = Server::new("modern-http-endpoint", "1.0.0")
.test_http_endpoint("http://legacy.test")
.expect("builder must construct the configured dual-era endpoint");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded live session");
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
201_i64,
);
let response = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", SERVER_DISCOVER_METHOD)
.with_body(
serde_json::to_vec(&request)
.expect("modern discovery request must serialize"),
),
),
)
.expect("modern request must be admitted and dispatched");
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("modern JSON negotiation must produce an immediate HTTP response");
};
assert_eq!(response.status, HttpStatus::OK);
let response: JsonRpcResponse =
serde_json::from_slice(&response.body).expect("modern response must be JSON-RPC");
assert_eq!(response.id, Some(201_i64.into()));
assert!(response.error.is_none());
assert_eq!(
response.result.as_ref().and_then(|result| {
result["_meta"][fastmcp_protocol::SERVER_DISCOVER_SERVER_INFO_META_KEY]["name"]
.as_str()
}),
Some("modern-http-endpoint")
);
}
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_http_endpoint_routes_a_shared_target_by_method() {
let cx = Cx::for_testing();
let endpoint = Server::new("http-shared-target", "1.0.0")
.http_config(
HttpServerConfig::new()
.mcp_path("/bridge")
.legacy_sse_path("/bridge")
.legacy_message_path("/messages"),
)
.build_http_endpoint("http://legacy.test")
.expect("a shared modern POST and legacy GET target is valid");
let mut legacy_session = endpoint
.open_session(&cx)
.expect("legacy session opens before shared-target GET");
let rejected = block_on(legacy_session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Get, "/bridge").with_body(b"{}".to_vec()),
))
.expect("nonempty shared-target GET becomes an HTTP rejection");
assert!(matches!(
rejected,
ServerHttpEndpointResponse::Immediate(response)
if response.status == HttpStatus::BAD_REQUEST
));
assert_eq!(legacy_session.selected_era, None);
let legacy = block_on(
legacy_session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/bridge")),
)
.expect("shared GET target selects legacy SSE");
assert!(matches!(legacy, ServerHttpEndpointResponse::LegacySse(_)));
assert_eq!(legacy_session.selected_era, Some(ProtocolEra::Legacy2024));
let mut modern_session = endpoint
.open_session(&cx)
.expect("modern session opens before shared-target POST");
let mut modern = modern_http_json_tool_request("missing", 2_020);
modern.path = "/bridge".to_owned();
let modern = block_on(modern_session.handle_async(&cx, modern))
.expect("shared POST target selects final modern admission");
assert!(matches!(modern, ServerHttpEndpointResponse::Immediate(_)));
assert_eq!(modern_session.selected_era, Some(ProtocolEra::Modern2026));
}
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_http_legacy_reconnect_rotates_the_advertised_post_capability() {
let cx = Cx::for_testing();
let endpoint = Server::new("http-legacy-reconnect", "1.0.0")
.build_http_endpoint("http://legacy.test")
.expect("dual-era endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("legacy session must open");
let ServerHttpEndpointResponse::LegacySse(first) =
block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("first legacy GET must open")
else {
panic!("first legacy GET must return an SSE body");
};
let first_session_id = session.legacy_session_id().to_owned();
drop(first);
let ServerHttpEndpointResponse::LegacySse(mut second) = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Get, "/sse")
.with_header("last-event-id", "non-replayable"),
),
)
.expect("reconnect must open a fresh legacy body") else {
panic!("reconnect must return an SSE body");
};
assert_ne!(session.legacy_session_id(), first_session_id);
assert!(
session.legacy_lifecycle.commit_if_live(|| ()).is_some(),
"server fast paths must refresh to the reconnect generation"
);
assert_eq!(
second
.recv_event(&cx)
.expect("fresh body must advertise its endpoint")
.data,
format!(
"http://legacy.test/messages?session_id={}",
session.legacy_session_id()
)
);
let initialize = JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "reconnect-client", "version": "1.0.0"},
})),
2_024_i64,
);
let old_capability = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", first_session_id)
.with_body(
serde_json::to_vec(&initialize).expect("legacy initialize must serialize"),
),
),
)
.expect("old POST capability must become an HTTP rejection");
assert!(matches!(
old_capability,
ServerHttpEndpointResponse::Immediate(response)
if response.status == HttpStatus::NOT_FOUND
));
let current_session_id = session.legacy_session_id().to_owned();
let current_capability = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", current_session_id)
.with_body(
serde_json::to_vec(&initialize).expect("legacy initialize must serialize"),
),
),
)
.expect("fresh POST capability must remain usable");
assert!(matches!(
current_capability,
ServerHttpEndpointResponse::Immediate(response)
if response.status == HttpStatus::ACCEPTED
));
}
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_http_legacy_reconnect_revokes_server_issued_generation_capabilities() {
let cx = Cx::for_testing();
let endpoint = Server::new("http-legacy-capability-generation", "1.0.0")
.build_http_endpoint("http://legacy.test")
.expect("dual-era endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("legacy session must open");
let ServerHttpEndpointResponse::LegacySse(mut first_stream) =
block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("first legacy GET must open")
else {
panic!("first legacy GET must return an SSE body");
};
let _first_endpoint = first_stream
.recv_event(&cx)
.expect("first body advertises its exact POST capability");
let first_session_id = session.legacy_session_id().to_owned();
let first_post = |message: JsonRpcMessage| {
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", first_session_id.clone())
.with_body(serde_json::to_vec(&message).expect("legacy message must serialize"))
};
for message in [
JsonRpcMessage::Request(JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {"roots": {}},
"clientInfo": {"name": "first-generation", "version": "1.0.0"},
})),
2_041_i64,
)),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
] {
assert!(matches!(
block_on(session.handle_async(&cx, first_post(message))),
Ok(ServerHttpEndpointResponse::Immediate(response))
if response.status == HttpStatus::ACCEPTED
));
}
let _first_initialize = first_stream
.recv_event(&cx)
.expect("first generation initializes before exposing roots");
let stale_provider = session
.legacy_roots_provider()
.expect("first generation advertises roots");
let stale_pending = Arc::clone(&session.legacy_pending_requests);
let stale_binding_generation = session.legacy_binding.generation();
assert!(session.legacy_adapter.as_ref().is_some_and(|adapter| {
let snapshot = adapter.snapshot();
snapshot.operating_transition_count == 1 && snapshot.close_release_count == 0
}));
let stale_task = std::thread::spawn(move || block_on(stale_provider.list_roots()));
let stale_request = first_stream
.recv_event(&cx)
.expect("old provider publishes only into its own live generation");
let JsonRpcMessage::Request(stale_request) = Codec::new()
.decode_complete_message(stale_request.data.as_bytes())
.expect("old roots request remains JSON-RPC")
else {
panic!("old provider must emit a roots request");
};
let stale_request_id = stale_request.id.expect("old roots request has an ID");
assert_eq!(stale_pending.in_flight_len(), 1);
drop(first_stream);
let ServerHttpEndpointResponse::LegacySse(mut fresh_stream) = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Get, "/sse")
.with_header("last-event-id", "old-generation"),
),
)
.expect("reconnect opens a fresh generation") else {
panic!("reconnect must return an SSE body");
};
let _fresh_endpoint = fresh_stream
.recv_event(&cx)
.expect("fresh body advertises its new POST capability");
assert_ne!(
session.legacy_binding.generation(),
stale_binding_generation
);
assert!(
session.legacy_adapter.is_none(),
"the old adapter cannot remain authoritative in the fresh generation"
);
assert_eq!(stale_pending.in_flight_len(), 0);
assert!(
stale_task
.join()
.expect("stale provider task must not panic")
.is_err(),
"body drop plus reconnect cancels the old provider's pending request"
);
assert!(
fresh_stream
.try_recv_event(&cx)
.expect("fresh stream remains readable")
.is_none(),
"the stale runtime sender cannot publish into the fresh body"
);
let fresh_session_id = session.legacy_session_id().to_owned();
let fresh_post = |message: JsonRpcMessage| {
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", fresh_session_id.clone())
.with_body(serde_json::to_vec(&message).expect("legacy message must serialize"))
};
for message in [
JsonRpcMessage::Request(JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {"roots": {}},
"clientInfo": {"name": "fresh-generation", "version": "1.0.0"},
})),
2_042_i64,
)),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
] {
assert!(matches!(
block_on(session.handle_async(&cx, fresh_post(message))),
Ok(ServerHttpEndpointResponse::Immediate(response))
if response.status == HttpStatus::ACCEPTED
));
}
let _fresh_initialize = fresh_stream
.recv_event(&cx)
.expect("fresh generation initializes independently");
let fresh_provider = session
.legacy_roots_provider()
.expect("fresh generation exposes its own roots provider");
let fresh_task = std::thread::spawn(move || block_on(fresh_provider.list_roots()));
let fresh_request = fresh_stream
.recv_event(&cx)
.expect("fresh provider publishes into the fresh body");
let JsonRpcMessage::Request(fresh_request) = Codec::new()
.decode_complete_message(fresh_request.data.as_bytes())
.expect("fresh roots request remains JSON-RPC")
else {
panic!("fresh provider must emit a roots request");
};
let fresh_request_id = fresh_request.id.expect("fresh roots request has an ID");
assert_eq!(fresh_request_id, stale_request_id);
assert_eq!(session.legacy_pending_requests.in_flight_len(), 1);
let stale_completion = JsonRpcResponse::success(
stale_request_id,
serde_json::json!({"roots": [{"uri": "file:///stale"}]}),
);
assert!(
!stale_pending.route_response(&stale_completion),
"the retained old PendingRequests authority cannot deliver after rotation"
);
assert_eq!(
session.legacy_pending_requests.in_flight_len(),
1,
"stale completion leaves the fresh same-ID waiter unchanged"
);
let fresh_completion = JsonRpcResponse::success(
fresh_request_id,
serde_json::json!({"roots": [{"uri": "file:///fresh", "name": "fresh"}]}),
);
assert!(matches!(
block_on(session.handle_async(
&cx,
fresh_post(JsonRpcMessage::Response(fresh_completion))
)),
Ok(ServerHttpEndpointResponse::Immediate(response))
if response.status == HttpStatus::ACCEPTED
));
let roots = fresh_task
.join()
.expect("fresh provider task must not panic")
.expect("fresh response completes only the fresh provider");
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].uri, "file:///fresh");
}
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_http_session_rejects_legacy_after_modern_admission_without_mutation() {
let cx = Cx::for_testing();
let endpoint = Server::new("http-era-pin-modern", "1.0.0")
.build_http_endpoint("http://legacy.test")
.expect("dual-era endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("session must open before first-era admission");
let modern =
block_on(session.handle_async(&cx, modern_http_json_tool_request("missing", 2_021)))
.expect("an admitted modern request may return a JSON-RPC handler error");
assert!(matches!(modern, ServerHttpEndpointResponse::Immediate(_)));
let before_era = session.selected_era;
let before_dispatches = session
.modern_dispatches
.lock()
.expect("modern dispatch registry must not be poisoned")
.len();
assert_eq!(before_era, Some(ProtocolEra::Modern2026));
assert!(session.legacy_adapter.is_none());
let rejected =
block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("cross-era request must become an HTTP rejection");
assert!(matches!(
rejected,
ServerHttpEndpointResponse::Immediate(response) if response.status == HttpStatus::BAD_REQUEST
));
assert_eq!(session.selected_era, before_era);
assert!(session.legacy_adapter.is_none());
assert_eq!(
session
.modern_dispatches
.lock()
.expect("rejection must not poison modern dispatch state")
.len(),
before_dispatches,
"changing only the HTTP era must not allocate legacy or modern session work",
);
}
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_http_session_rejects_modern_after_legacy_admission_without_mutation() {
let cx = Cx::for_testing();
let endpoint = Server::new("http-era-pin-legacy", "1.0.0")
.build_http_endpoint("http://legacy.test")
.expect("dual-era endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("session must open before first-era admission");
let legacy = block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("legacy SSE opening request must be admitted");
assert!(matches!(legacy, ServerHttpEndpointResponse::LegacySse(_)));
let before_era = session.selected_era;
let before_session_id = session.legacy_session_id().to_owned();
let before_dispatches = session
.modern_dispatches
.lock()
.expect("modern dispatch registry must not be poisoned")
.len();
assert_eq!(before_era, Some(ProtocolEra::Legacy2024));
let rejected =
block_on(session.handle_async(&cx, modern_http_json_tool_request("missing", 2_022)))
.expect("cross-era request must become an HTTP rejection");
assert!(matches!(
rejected,
ServerHttpEndpointResponse::Immediate(response) if response.status == HttpStatus::BAD_REQUEST
));
assert_eq!(session.selected_era, before_era);
assert_eq!(session.legacy_session_id(), before_session_id);
assert_eq!(
session
.modern_dispatches
.lock()
.expect("rejection must not poison modern dispatch state")
.len(),
before_dispatches,
"changing only the HTTP era must not allocate modern session work",
);
}
#[cfg(feature = "tasks")]
#[test]
fn builder_http_endpoint_dispatches_admitted_extension_without_metadata_leakage() {
let cx = Cx::for_testing();
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = extension_registry_test_server(Arc::clone(&calls))
.test_http_endpoint("http://legacy.test")
.expect("extension server must construct the configured HTTP endpoint");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded live session");
let response = block_on(session.handle_async(
&cx,
extension_tasks_get_http_request(MODERN_PROTOCOL_VERSION),
))
.expect("exact modern extension request must be admitted and dispatched");
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("modern JSON extension dispatch must produce an immediate HTTP response");
};
assert_eq!(response.status, HttpStatus::OK);
let response: JsonRpcResponse =
serde_json::from_slice(&response.body).expect("modern response must be JSON-RPC");
assert_eq!(response.id, Some(71_i64.into()));
assert_eq!(response.result, Some(serde_json::json!({ "next": 42 })));
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
// Exact-2024 era: drives the dual-era internal HTTP dispatcher.
#[cfg(all(feature = "tasks", feature = "legacy-2024-11-05"))]
#[test]
fn builder_http_endpoint_rejects_one_variable_wrong_extension_version() {
let cx = Cx::for_testing();
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = extension_registry_test_server(Arc::clone(&calls))
.into_http_endpoint("http://legacy.test")
.expect("extension server must construct the configured HTTP endpoint");
let legacy_sessions: LiveHttpSessionRegistry = Arc::new(Mutex::new(HashMap::new()));
let modern_sessions: LiveModernHttpSessionRegistry =
Arc::new(LiveModernHttpSessionRegistryState::new());
let response = dispatch_http_request(
&cx,
&endpoint,
&legacy_sessions,
&modern_sessions,
extension_tasks_get_http_request(LEGACY_PROTOCOL_VERSION),
None,
LiveHttpLegacyDispatchMode::Blocking,
);
assert_eq!(response.status, HttpStatus::BAD_REQUEST);
assert_eq!(calls.load(Ordering::SeqCst), 0);
}
#[cfg(feature = "tasks")]
#[test]
fn live_modern_stdio_extension_preserves_metadata_through_admission() {
let calls = Arc::new(AtomicUsize::new(0));
let sent = Arc::new(Mutex::new(Vec::new()));
extension_registry_test_server(Arc::clone(&calls))
.run_transport_returning_with_cx(
&Cx::for_testing(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
modern_discovery_opening_request(),
JsonRpcMessage::Request(extension_tasks_get_request(serde_json::json!({}))),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect("exact modern metadata must reach extension admission before sanitization");
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
#[cfg(feature = "tasks")]
#[test]
fn live_modern_stdio_extension_rejects_one_variable_wrong_version() {
let calls = Arc::new(AtomicUsize::new(0));
let sent = Arc::new(Mutex::new(Vec::new()));
let mut wrong_version = extension_tasks_get_request(serde_json::json!({}));
*wrong_version
.params
.as_mut()
.and_then(|value| value.pointer_mut("/_meta/io.modelcontextprotocol~1protocolVersion"))
.expect("extension request has modern protocol metadata") =
serde_json::json!(LEGACY_PROTOCOL_VERSION);
extension_registry_test_server(Arc::clone(&calls))
.run_transport_returning_with_cx(
&Cx::for_testing(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
modern_discovery_opening_request(),
JsonRpcMessage::Request(wrong_version),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::new(AtomicUsize::new(0)),
},
)
// MCP 2026-07-28 (c7f4717b): a wrong version is a per-request
// -32022 refusal and the stdio runtime keeps serving to EOF.
.expect("only the extension protocol version differs from the admitted positive");
assert_eq!(calls.load(Ordering::SeqCst), 0, "the handler never runs");
let refused = sent.lock().unwrap().iter().any(|message| {
matches!(message, JsonRpcMessage::Response(response)
if response.error.as_ref().map(|error| error.code.clone())
== Some(fastmcp_protocol::UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE.into()))
});
assert!(
refused,
"the wrong version receives the typed -32022 refusal"
);
}
// Exact-2024 era: selects LegacyOnly, which a no-legacy build refuses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_http_protocol_policy_rejects_opposite_era_requests_both_ways() {
let modern_request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
},
})),
218_i64,
);
let modern_inbound =
InboundRequestContext::new(Cx::for_testing(), 218, InboundRequestTransport::Http);
let legacy_only = Server::new("legacy-only-http-policy", "1.0.0")
.protocol_policy(ProtocolPolicy::LegacyOnly)
.expect("LegacyOnly must be available to this test build")
.build();
let modern_response = block_on(legacy_only.dispatch_http_with_protocol_policy(
ProtocolPolicy::LegacyOnly,
&modern_inbound,
&modern_request,
));
assert_eq!(modern_response.status, HttpStatus::BAD_REQUEST);
let legacy_request = JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-http-client", "version": "1.0.0"},
})),
219_i64,
);
let legacy_inbound =
InboundRequestContext::new(Cx::for_testing(), 219, InboundRequestTransport::Http);
let modern_only = Server::new("modern-only-http-policy", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build();
let legacy_response = block_on(modern_only.dispatch_http_with_protocol_policy(
ProtocolPolicy::ModernOnly,
&legacy_inbound,
&legacy_request,
));
assert_eq!(legacy_response.status, HttpStatus::BAD_REQUEST);
}
#[test]
fn public_http_protocol_policy_admits_matching_modern_metadata() {
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
220_i64,
);
let inbound =
InboundRequestContext::new(Cx::for_testing(), 220, InboundRequestTransport::Http);
let server = Server::new("modern-http-policy", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build();
let response = block_on(server.dispatch_http_with_protocol_policy(
ProtocolPolicy::ModernOnly,
&inbound,
&request,
));
assert_eq!(response.status, HttpStatus::OK);
let response: JsonRpcResponse =
serde_json::from_slice(&response.body).expect("modern response must be JSON-RPC");
assert_eq!(response.id, Some(220_i64.into()));
assert!(response.error.is_none());
}
#[test]
fn public_http_protocol_policy_rejects_metadata_free_modern_request() {
// This differs from the matching-metadata positive only by omitting
// the final-era protocol marker. Public dispatch must not infer the
// modern era merely because the caller selected ModernOnly.
let request =
JsonRpcRequest::new(SERVER_DISCOVER_METHOD, Some(serde_json::json!({})), 220_i64);
let inbound =
InboundRequestContext::new(Cx::for_testing(), 220, InboundRequestTransport::Http);
let server = Server::new("modern-http-policy", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build();
let response = block_on(server.dispatch_http_with_protocol_policy(
ProtocolPolicy::ModernOnly,
&inbound,
&request,
));
assert_eq!(response.status, HttpStatus::BAD_REQUEST);
let response: JsonRpcResponse =
serde_json::from_slice(&response.body).expect("rejection must be JSON-RPC");
assert_eq!(response.id, Some(220_i64.into()));
assert!(response.result.is_none());
assert_eq!(
response
.error
.as_ref()
.and_then(|error| error.code.as_i32()),
Some(McpErrorCode::InvalidRequest.into())
);
}
#[test]
fn live_http_loopback_dispatches_modern_json_over_a_real_socket() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-modern-http", "1.0.0")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live modern HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live modern HTTP address failed: {error}"))?;
let caller_cx = cx.clone();
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
801_i64,
);
let body = serde_json::to_vec(&request)
.map_err(|error| format!("modern wire request did not serialize: {error}"))?;
let request = live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SERVER_DISCOVER_METHOD),
],
);
let mut client = cx
.spawn(move |_client_cx| async move {
let response = live_http_exchange(address, request).await;
caller_cx.cancel_with(CancelKind::User, Some("modern loopback complete"));
response
})
.map_err(|error| format!("modern loopback client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let response = client
.join(&cx)
.await
.map_err(|error| format!("modern loopback client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("modern loopback server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "modern loopback").await?;
let response = std::str::from_utf8(&response)
.map_err(|error| format!("modern loopback response was not UTF-8: {error}"))?;
if !response.starts_with("HTTP/1.1 200") {
return Err(format!(
"modern loopback response status was unexpected: {response}"
));
}
if !response
.to_ascii_lowercase()
.contains("\r\nconnection: close\r\n")
{
return Err(format!(
"single-request HTTP response did not declare connection closure: {response}"
));
}
if live_http_response_header(response.as_bytes(), "mcp-session-id").is_ok() {
return Err("modern discovery response issued MCP-Session-Id".to_owned());
}
let response: JsonRpcResponse = serde_json::from_slice(live_http_response_body(
response.as_bytes(),
)?)
.map_err(|error| format!("modern loopback JSON-RPC response was invalid: {error}"))?;
if response.id != Some(801_i64.into()) || response.error.is_some() {
return Err(format!(
"modern loopback request was not dispatched: {response:?}"
));
}
if response.result.as_ref().and_then(|result| {
result["_meta"][fastmcp_protocol::SERVER_DISCOVER_SERVER_INFO_META_KEY]["name"]
.as_str()
}) != Some("live-modern-http")
{
return Err(format!(
"modern loopback discovery result was unexpected: {response:?}"
));
}
Ok(())
});
}
/// Explicit test consent backend installed through the production API;
/// native Basic tests do not depend on the cfg(test) default approval.
struct NativeBasicConsent;
impl oauth::AuthorizationApprovalBackend for NativeBasicConsent {
fn generation(&self) -> oauth::AuthorizationApprovalGeneration {
oauth::AuthorizationApprovalGeneration::from_bytes([0x42; 32])
}
fn approve(
&self,
request: &oauth::AuthorizationApprovalRequest,
) -> oauth::AuthorizationApprovalDisposition {
oauth::AuthorizationApprovalDisposition::Approved(
request
.approve(
"native-basic-owner".to_owned(),
request.scopes().to_vec(),
request.resource().map(str::to_owned),
self.generation(),
)
.expect("approve the exact bounded client request"),
)
}
}
fn native_oauth_basic_header(encoded_components: &str) -> String {
use base64::Engine as _;
format!(
"Basic {}",
base64::engine::general_purpose::STANDARD.encode(encoded_components),
)
}
fn native_oauth_basic_fixture() -> (OAuthHttpRoutes, String, String) {
native_oauth_client_method_fixture(oauth::TokenEndpointAuthMethod::ClientSecretBasic)
}
fn native_oauth_client_method_fixture(
method: oauth::TokenEndpointAuthMethod,
) -> (OAuthHttpRoutes, String, String) {
let oauth = Arc::new(
oauth::OAuthServer::try_with_approval_backend(
oauth::OAuthServerConfig::default(),
Arc::new(NativeBasicConsent),
)
.expect("valid explicit consent configuration"),
);
oauth
.register_client(
oauth::OAuthClient::builder("native:basic+client")
.secret("S3cret:+ &=%")
.token_endpoint_auth_method(method)
.redirect_uri("https://client.example.test/callback")
.scope("mcp")
.build()
.expect("bounded confidential client"),
)
.expect("register confidential client");
let (code, _) = oauth
.authorize(&oauth::AuthorizationRequest {
response_type: "code".to_owned(),
client_id: "native:basic+client".to_owned(),
redirect_uri: "https://client.example.test/callback".to_owned(),
scopes: vec!["mcp".to_owned()],
resource: None,
state: None,
code_challenge: "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM".to_owned(),
code_challenge_method: oauth::CodeChallengeMethod::S256,
})
.expect("issue a code through the explicit consent backend");
let form = format!(
"grant_type=authorization_code&code={code}&redirect_uri=https%3A%2F%2Fclient.example.test%2Fcallback&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
);
// These literal OAuth components exercise encoded colons, plus signs,
// spaces, ampersands, equals signs and percent signs independently of
// the production percent decoder.
let basic = native_oauth_basic_header("native%3Abasic%2Bclient:S3cret%3A%2B+%26%3D%25");
let routes = OAuthHttpRoutes::new(oauth, "https://fastmcp.invalid/oauth")
.expect("native OAuth routes");
(routes, form, basic)
}
fn native_oauth_form_request(
path: &str,
form: &str,
authorization: Option<&str>,
) -> asupersync::http::h1::Request {
let mut headers = vec![(
"Content-Type".to_owned(),
"application/x-www-form-urlencoded".to_owned(),
)];
if let Some(authorization) = authorization {
headers.push(("Authorization".to_owned(), authorization.to_owned()));
}
asupersync::http::h1::Request {
method: Http1Method::Post,
uri: path.to_owned(),
version: asupersync::http::h1::Version::Http11,
headers,
body: form.as_bytes().to_vec(),
trailers: Vec::new(),
peer_addr: None,
}
}
#[cfg(all(feature = "builtin-auth-server", feature = "oauth-client-credentials"))]
fn native_development_machine_fixture(enabled: bool) -> (OAuthHttpRoutes, String) {
let oauth = Arc::new(
oauth::OAuthServer::try_new(oauth::OAuthServerConfig {
allow_development_client_credentials: enabled,
..oauth::OAuthServerConfig::default()
})
.unwrap(),
);
let endpoint =
fastmcp_core::CanonicalHttpUrl::parse("https://resource.example/api").unwrap();
let resource = fastmcp_core::CanonicalResourceId::parse_for_endpoint(
endpoint.as_str(),
&endpoint,
fastmcp_core::CanonicalResourceIdPolicy::DEFAULT,
)
.unwrap();
oauth
.register_client(
oauth::OAuthClient::builder("native-machine")
.secret("machine-secret")
.scope("browser:admin")
.development_client_credentials(
oauth::DevelopmentClientCredentialsGrant::new(
resource,
["machine:read", "machine:write"],
)
.unwrap(),
)
.build()
.unwrap(),
)
.unwrap();
(
OAuthHttpRoutes::new(oauth, "https://fastmcp.invalid/oauth").unwrap(),
native_oauth_basic_header("native-machine:machine-secret"),
)
}
#[cfg(all(feature = "builtin-auth-server", feature = "oauth-client-credentials"))]
#[test]
fn native_development_client_credentials_issue_revoke_and_advertise_exact_enabled_grant() {
let (routes, basic) = native_development_machine_fixture(true);
let form = "grant_type=client_credentials&resource=https%3A%2F%2Fresource.example%2Fapi&scope=machine%3Awrite+machine%3Aread";
let request = native_oauth_form_request(routes.token_path(), form, Some(&basic));
let response = dispatch_oauth_h1_request(&routes, &request, routes.token_path(), "");
assert_eq!(response.status, HttpStatus::OK);
assert_eq!(
response.headers.get("cache-control").map(String::as_str),
Some("no-store")
);
assert_eq!(
response.headers.get("pragma").map(String::as_str),
Some("no-cache")
);
let issued: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
assert_eq!(issued["scope"], "machine:read machine:write");
assert_eq!(issued["expires_in"], 900);
assert!(issued.get("refresh_token").is_none());
assert!(issued.get("id_token").is_none());
let token = issued["access_token"].as_str().unwrap();
let retained = routes.server().validate_access_token(token).unwrap();
assert_eq!(retained.client_id, "native-machine");
assert!(retained.subject.is_none());
assert_eq!(
retained.resource.as_deref(),
Some("https://resource.example/api")
);
let mut metadata_request = native_oauth_form_request(routes.metadata_path(), "", None);
metadata_request.method = Http1Method::Get;
metadata_request.headers.clear();
let metadata =
dispatch_oauth_h1_request(&routes, &metadata_request, routes.metadata_path(), "");
assert_eq!(metadata.status, HttpStatus::OK);
let metadata: serde_json::Value = serde_json::from_slice(&metadata.body).unwrap();
assert_eq!(
metadata["grant_types_supported"],
serde_json::json!(["authorization_code", "refresh_token", "client_credentials",])
);
assert!(
metadata["token_endpoint_auth_methods_supported"]
.as_array()
.unwrap()
.iter()
.any(|method| method == "client_secret_basic")
);
let revoke = native_oauth_form_request(
routes.revocation_path(),
&format!("token={token}"),
Some(&basic),
);
let response = dispatch_oauth_h1_request(&routes, &revoke, routes.revocation_path(), "");
assert_eq!(response.status, HttpStatus::OK);
assert!(routes.server().validate_access_token(token).is_none());
assert_eq!(routes.server().stats().refresh_tokens, 0);
}
#[cfg(all(feature = "builtin-auth-server", feature = "oauth-client-credentials"))]
#[test]
fn native_development_client_credentials_reject_method_resource_scope_and_mixed_grants() {
let (routes, basic) = native_development_machine_fixture(true);
let base = "grant_type=client_credentials&resource=https%3A%2F%2Fresource.example%2Fapi&scope=machine%3Aread";
let baseline = native_oauth_form_request(routes.token_path(), base, Some(&basic));
let response = dispatch_oauth_h1_request(&routes, &baseline, routes.token_path(), "");
assert_eq!(response.status, HttpStatus::OK);
let issued: serde_json::Value = serde_json::from_slice(&response.body).unwrap();
let token = issued["access_token"].as_str().unwrap();
for (form, authorization, status, error, challenge) in [
(
base.to_string(),
Some(native_oauth_basic_header("native-machine:wrong-secret")),
HttpStatus::UNAUTHORIZED,
"invalid_client",
Some("Basic realm=\"oauth\""),
),
(
format!("{base}&client_id=native-machine&client_secret=machine-secret"),
None,
HttpStatus::BAD_REQUEST,
"invalid_client",
None,
),
(
base.replace("resource.example%2Fapi", "resource.example%2Fother"),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_request",
None,
),
(
base.replace("machine%3Aread", "browser%3Aadmin"),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_scope",
None,
),
(
base.replace("machine%3Aread", "machine%3Aread%09machine%3Awrite"),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_request",
None,
),
(
base.replace("machine%3Aread", "+machine%3Aread"),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_scope",
None,
),
(
base.replace("machine%3Aread", "machine%3Aread++machine%3Awrite"),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_scope",
None,
),
(
base.replace("&scope=machine%3Aread", ""),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_scope",
None,
),
(
format!("{base}&scope=machine%3Aread"),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_request",
None,
),
(
format!("{base}&resource=https%3A%2F%2Fresource.example%2Fapi"),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_request",
None,
),
(
format!("{base}&code="),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_request",
None,
),
(
format!("{base}&client_assertion=ignored"),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_request",
None,
),
(
format!("{base}&client_assertion_type=ignored"),
Some(basic.clone()),
HttpStatus::BAD_REQUEST,
"invalid_request",
None,
),
] {
let request =
native_oauth_form_request(routes.token_path(), &form, authorization.as_deref());
let response = dispatch_oauth_h1_request(&routes, &request, routes.token_path(), "");
assert_native_oauth_error(&response, status, error, challenge);
assert_eq!(routes.server().stats().access_tokens, 1);
assert_eq!(routes.server().stats().refresh_tokens, 0);
assert_eq!(routes.server().stats().revoked_tokens, 0);
assert!(routes.server().validate_access_token(token).is_some());
}
}
#[cfg(all(feature = "builtin-auth-server", feature = "oauth-client-credentials"))]
#[test]
fn native_development_client_credentials_opt_in_does_not_change_disabled_issuer_behavior() {
let (routes, basic) = native_development_machine_fixture(false);
assert_eq!(
routes
.authorization_server_metadata()
.unwrap()
.grant_types_supported,
["authorization_code", "refresh_token"]
);
let base = "grant_type=client_credentials&resource=https%3A%2F%2Fresource.example%2Fapi&scope=machine%3Aread";
for form in [
base.to_string(),
format!("{base}&code="),
format!("{base}&client_assertion=ignored"),
base.replace("machine%3Aread", "machine%3Aread++machine%3Awrite"),
] {
let request = native_oauth_form_request(routes.token_path(), &form, Some(&basic));
let response = dispatch_oauth_h1_request(&routes, &request, routes.token_path(), "");
assert_native_oauth_error(
&response,
HttpStatus::BAD_REQUEST,
"unsupported_grant_type",
None,
);
assert_eq!(routes.server().stats().access_tokens, 0);
assert_eq!(routes.server().stats().refresh_tokens, 0);
}
}
fn assert_native_oauth_error(
response: &HttpResponse,
status: HttpStatus,
error: &str,
challenge: Option<&str>,
) {
assert_eq!(response.status, status);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&response.body).expect("OAuth error JSON"),
serde_json::json!({ "error": error }),
);
assert_eq!(
response.headers.get("www-authenticate").map(String::as_str),
challenge,
);
assert_eq!(
response.headers.get("cache-control").map(String::as_str),
Some("no-store"),
);
assert_eq!(
response.headers.get("pragma").map(String::as_str),
Some("no-cache"),
);
}
#[test]
fn native_oauth_basic_rejections_preserve_code_and_live_grant() {
let (routes, form, basic) = native_oauth_basic_fixture();
let token_path = routes.token_path();
let bad_secret = native_oauth_basic_header("native%3Abasic%2Bclient:wrong-secret");
let unknown_client = native_oauth_basic_header("unknown-client:wrong-secret");
for authorization in [
bad_secret.as_str(),
unknown_client.as_str(),
"Basic malformed!",
"Basic",
"Basic \tY2xpZW50OnNlY3JldA==",
] {
let request = native_oauth_form_request(token_path, &form, Some(authorization));
assert_native_oauth_error(
&dispatch_oauth_h1_request(&routes, &request, token_path, ""),
HttpStatus::UNAUTHORIZED,
"invalid_client",
Some("Basic realm=\"oauth\""),
);
}
for extra in [
"client_id=native%3Abasic%2Bclient",
"client_id=",
"client_secret=wrong-secret",
"client_secret=",
"client%5Fsecret=",
] {
let mixed_form = format!("{form}&{extra}");
let request = native_oauth_form_request(token_path, &mixed_form, Some(&basic));
assert_native_oauth_error(
&dispatch_oauth_h1_request(&routes, &request, token_path, ""),
HttpStatus::BAD_REQUEST,
"invalid_request",
None,
);
}
let mut duplicate_header = native_oauth_form_request(token_path, &form, Some(&basic));
duplicate_header
.headers
.push(("authorization".to_owned(), basic.clone()));
assert_native_oauth_error(
&dispatch_oauth_h1_request(&routes, &duplicate_header, token_path, ""),
HttpStatus::BAD_REQUEST,
"invalid_request",
None,
);
let request = native_oauth_form_request(token_path, &form, Some(&basic));
let issued = dispatch_oauth_h1_request(&routes, &request, token_path, "");
assert_eq!(
issued.status,
HttpStatus::OK,
"rejections must preserve the code",
);
let issued: serde_json::Value = serde_json::from_slice(&issued.body).expect("token JSON");
let access = issued["access_token"].as_str().expect("access token");
let refresh = issued["refresh_token"].as_str().expect("refresh token");
assert!(routes.server().validate_access_token(access).is_some());
let refresh_form = format!("grant_type=refresh_token&refresh_token={refresh}");
let request = native_oauth_form_request(token_path, &refresh_form, Some(&bad_secret));
assert_native_oauth_error(
&dispatch_oauth_h1_request(&routes, &request, token_path, ""),
HttpStatus::UNAUTHORIZED,
"invalid_client",
Some("Basic realm=\"oauth\""),
);
let revoke_path = routes.revocation_path();
let revoke_form = format!("token={refresh}");
let request = native_oauth_form_request(revoke_path, &revoke_form, Some(&bad_secret));
assert_native_oauth_error(
&dispatch_oauth_h1_request(&routes, &request, revoke_path, ""),
HttpStatus::UNAUTHORIZED,
"invalid_client",
Some("Basic realm=\"oauth\""),
);
let request = native_oauth_form_request(
revoke_path,
&format!("{revoke_form}&client_secret="),
Some(&basic),
);
assert_native_oauth_error(
&dispatch_oauth_h1_request(&routes, &request, revoke_path, ""),
HttpStatus::BAD_REQUEST,
"invalid_request",
None,
);
assert!(routes.server().validate_access_token(access).is_some());
let request = native_oauth_form_request(token_path, &refresh_form, Some(&basic));
let refreshed = dispatch_oauth_h1_request(&routes, &request, token_path, "");
assert_eq!(
refreshed.status,
HttpStatus::OK,
"rejections must preserve refresh",
);
let refreshed: serde_json::Value =
serde_json::from_slice(&refreshed.body).expect("refreshed token JSON");
let successor = refreshed["access_token"].as_str().expect("successor token");
assert!(routes.server().validate_access_token(successor).is_some());
let request =
native_oauth_form_request(revoke_path, &format!("token={successor}"), Some(&basic));
assert_eq!(
dispatch_oauth_h1_request(&routes, &request, revoke_path, "").status,
HttpStatus::OK,
);
assert!(routes.server().validate_access_token(successor).is_none());
}
#[test]
fn native_oauth_basic_decoding_is_bounded_and_preserves_empty_password_authentication() {
let client_id = "%61".repeat(oauth::MAX_OAUTH_CLIENT_ID_BYTES);
let secret = "%62".repeat(oauth::MAX_OAUTH_CLIENT_CREDENTIAL_BYTES);
let basic = native_oauth_basic_header(&format!("{client_id}:{secret}"));
let mut admission =
OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, b"").unwrap();
let (id, password, method) = oauth_http_client_credentials(&mut admission, Some(&basic))
.expect("exact maximum percent-expanded credentials");
assert_eq!(method, oauth::TokenEndpointAuthMethod::ClientSecretBasic);
assert_eq!(id, "a".repeat(oauth::MAX_OAUTH_CLIENT_ID_BYTES));
assert_eq!(
password,
Some("b".repeat(oauth::MAX_OAUTH_CLIENT_CREDENTIAL_BYTES)),
);
for components in [
format!("{client_id}a:{secret}"),
format!("{client_id}:{secret}b"),
format!("{client_id}a:secret"),
format!("client:{secret}b"),
"client:%".to_owned(),
"client:%GG".to_owned(),
"client:%ff".to_owned(),
"client:%0a".to_owned(),
"client-without-colon".to_owned(),
":secret".to_owned(),
] {
let basic = native_oauth_basic_header(&components);
let mut admission =
OAuthParameterAdmission::admit(OAuthParameterEndpoint::TokenForm, b"").unwrap();
let error = oauth_http_client_credentials(&mut admission, Some(&basic))
.expect_err("invalid Basic input must fail before authentication");
assert_native_oauth_error(
&error,
HttpStatus::UNAUTHORIZED,
"invalid_client",
Some("Basic realm=\"oauth\""),
);
}
let oauth = Arc::new(oauth::OAuthServer::with_defaults());
oauth
.register_client(
oauth::OAuthClient::builder("public-client")
.redirect_uri("https://client.example.test/callback")
.build()
.expect("public client"),
)
.unwrap();
let routes = OAuthHttpRoutes::new(oauth, "https://fastmcp.invalid/oauth").unwrap();
let empty_password = native_oauth_basic_header("public-client:");
let request = native_oauth_form_request(
routes.revocation_path(),
"token=unknown-token",
Some(&empty_password),
);
assert_native_oauth_error(
&dispatch_oauth_h1_request(&routes, &request, routes.revocation_path(), ""),
HttpStatus::UNAUTHORIZED,
"invalid_client",
Some("Basic realm=\"oauth\""),
);
for secret_field in ["client_secret=", "client%5Fsecret="] {
let request = native_oauth_form_request(
routes.revocation_path(),
&format!("token=unknown-token&client_id=public-client&{secret_field}"),
None,
);
assert_native_oauth_error(
&dispatch_oauth_h1_request(&routes, &request, routes.revocation_path(), ""),
HttpStatus::BAD_REQUEST,
"invalid_client",
None,
);
}
let request = native_oauth_form_request(
routes.revocation_path(),
"token=unknown-token&client_id=public-client",
None,
);
assert_eq!(
dispatch_oauth_h1_request(&routes, &request, routes.revocation_path(), "").status,
HttpStatus::OK,
"the same public client remains usable with none authentication",
);
}
#[test]
fn native_oauth_registered_authentication_method_controls_code_refresh_and_revoke() {
for registered in [
oauth::TokenEndpointAuthMethod::ClientSecretBasic,
oauth::TokenEndpointAuthMethod::ClientSecretPost,
] {
let (routes, code_form, basic) = native_oauth_client_method_fixture(registered);
let token_path = routes.token_path();
let revoke_path = routes.revocation_path();
let basic_registered = registered == oauth::TokenEndpointAuthMethod::ClientSecretBasic;
// The decoded client ID and secret are identical in both requests.
// Only their credential location differs from the registration.
let request = |path: &str, form: &str, use_basic: bool| {
if use_basic {
native_oauth_form_request(path, form, Some(&basic))
} else {
native_oauth_form_request(
path,
&format!(
"{form}&client_id=native%3Abasic%2Bclient&client_secret=S3cret%3A%2B+%26%3D%25"
),
None,
)
}
};
let assert_method_rejection = |response: &HttpResponse| {
assert_native_oauth_error(
response,
if basic_registered {
HttpStatus::BAD_REQUEST
} else {
HttpStatus::UNAUTHORIZED
},
"invalid_client",
if basic_registered {
None
} else {
Some("Basic realm=\"oauth\"")
},
);
};
let denied = request(token_path, &code_form, !basic_registered);
assert_method_rejection(&dispatch_oauth_h1_request(&routes, &denied, token_path, ""));
let accepted = request(token_path, &code_form, basic_registered);
let issued = dispatch_oauth_h1_request(&routes, &accepted, token_path, "");
assert_eq!(
issued.status,
HttpStatus::OK,
"same code remains redeemable"
);
let issued: serde_json::Value = serde_json::from_slice(&issued.body).unwrap();
let access = issued["access_token"].as_str().unwrap();
let refresh = issued["refresh_token"].as_str().unwrap();
let refresh_form = format!("grant_type=refresh_token&refresh_token={refresh}");
let denied = request(token_path, &refresh_form, !basic_registered);
assert_method_rejection(&dispatch_oauth_h1_request(&routes, &denied, token_path, ""));
let denied = request(revoke_path, &format!("token={refresh}"), !basic_registered);
assert_method_rejection(&dispatch_oauth_h1_request(
&routes,
&denied,
revoke_path,
"",
));
assert!(routes.server().validate_access_token(access).is_some());
let accepted = request(token_path, &refresh_form, basic_registered);
let successor = dispatch_oauth_h1_request(&routes, &accepted, token_path, "");
assert_eq!(
successor.status,
HttpStatus::OK,
"same refresh remains usable"
);
let successor: serde_json::Value = serde_json::from_slice(&successor.body).unwrap();
let access = successor["access_token"].as_str().unwrap();
let refresh = successor["refresh_token"].as_str().unwrap();
let accepted = request(revoke_path, &format!("token={refresh}"), basic_registered);
assert_eq!(
dispatch_oauth_h1_request(&routes, &accepted, revoke_path, "").status,
HttpStatus::OK,
);
assert!(routes.server().validate_access_token(access).is_none());
}
}
#[test]
fn live_http_oauth_basic_authorization_code_refresh_and_revocation() {
run_live_http_test(|cx| async move {
let (routes, code_form, basic) = native_oauth_basic_fixture();
let oauth = Arc::clone(routes.server());
let bound = Server::new("live-oauth-basic", "1.0.0")
.oauth_http_routes(routes)
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("Basic OAuth listener failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("Basic OAuth address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let result = async {
let request = |path: &str, form: &str, authorization: &str| {
format!(
"POST {path} HTTP/1.1\r\nHost: loopback\r\nAuthorization: {authorization}\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{form}",
form.len(),
)
.into_bytes()
};
let wrong_basic =
native_oauth_basic_header("native%3Abasic%2Bclient:wrong-secret");
// RH-5: only the Basic password differs from the
// succeeding wire request; its code must survive.
let rejected = live_http_exchange(
address,
request("/oauth/token", &code_form, &wrong_basic),
)
.await?;
if !rejected.starts_with(b"HTTP/1.1 401")
|| live_http_response_header(&rejected, "www-authenticate")?
!= "Basic realm=\"oauth\""
|| serde_json::from_slice::<serde_json::Value>(
live_http_response_body(&rejected)?,
)
.map_err(|_| "Basic rejection was not JSON".to_owned())?
!= serde_json::json!({ "error": "invalid_client" })
{
return Err(
"Basic wrong-secret rejection changed its contract".to_owned(),
);
}
let issued = live_http_exchange(
address,
request("/oauth/token", &code_form, &basic),
)
.await?;
if !issued.starts_with(b"HTTP/1.1 200")
|| live_http_response_header(&issued, "cache-control")? != "no-store"
|| live_http_response_header(&issued, "pragma")? != "no-cache"
{
return Err(
"Basic authorization-code exchange did not succeed".to_owned(),
);
}
let issued: serde_json::Value =
serde_json::from_slice(live_http_response_body(&issued)?)
.map_err(|_| "Basic token response was not JSON".to_owned())?;
let access = issued["access_token"]
.as_str()
.ok_or_else(|| "Basic token response omitted access token".to_owned())?;
if oauth.validate_access_token(access).is_none() {
return Err("Basic access token is not usable".to_owned());
}
let refresh = issued["refresh_token"]
.as_str()
.ok_or_else(|| "Basic token response omitted refresh token".to_owned())?;
let refreshed = live_http_exchange(
address,
request(
"/oauth/token",
&format!("grant_type=refresh_token&refresh_token={refresh}"),
&basic,
),
)
.await?;
if !refreshed.starts_with(b"HTTP/1.1 200") {
return Err("Basic refresh exchange did not succeed".to_owned());
}
let refreshed: serde_json::Value =
serde_json::from_slice(live_http_response_body(&refreshed)?)
.map_err(|_| "Basic refresh response was not JSON".to_owned())?;
let successor = refreshed["access_token"]
.as_str()
.ok_or_else(|| "Basic refresh omitted access token".to_owned())?;
if successor == access || oauth.validate_access_token(successor).is_none() {
return Err("Basic refresh did not issue a usable successor".to_owned());
}
let revoked = live_http_exchange(
address,
request("/oauth/revoke", &format!("token={successor}"), &basic),
)
.await?;
if !revoked.starts_with(b"HTTP/1.1 200")
|| oauth.validate_access_token(successor).is_some()
{
return Err("Basic revocation did not invalidate its token".to_owned());
}
Ok(())
}
.await;
caller_cx.cancel_with(CancelKind::User, Some("Basic OAuth lifecycle complete"));
result
})
.map_err(|error| format!("Basic OAuth client spawn failed: {error}"))?;
let serve = bound.serve(&cx).await;
client
.join(&cx)
.await
.map_err(|error| format!("Basic OAuth client failed: {error:?}"))??;
let shutdown = serve.map_err(|error| format!("Basic OAuth server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "Basic OAuth lifecycle").await
});
}
/// Native OAuth routes with OIDC under `base`, plus an access token for a
/// client registered and authorized with `scopes`. A Pending signer
/// activation is all `with_oidc` needs to fix the OIDC paths.
#[cfg(feature = "builtin-auth-server")]
fn native_oidc_userinfo_fixture(scopes: &[&str], base: &str) -> (OAuthHttpRoutes, String) {
const CLIENT_ID: &str = "native-userinfo-client";
const REDIRECT_URI: &str = "http://127.0.0.1/userinfo-callback";
// RFC 7636 appendix B.
const CODE_VERIFIER: &str = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
const CODE_CHALLENGE: &str = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM";
let oauth = Arc::new(oauth::OAuthServer::with_defaults());
oauth
.register_client(
oauth::OAuthClient::builder(CLIENT_ID)
.redirect_uri(REDIRECT_URI)
.scopes(scopes.iter().copied())
.build()
.expect("valid userinfo client"),
)
.expect("register userinfo client");
let (code, _) = oauth
.authorize(&oauth::AuthorizationRequest {
response_type: "code".to_string(),
client_id: CLIENT_ID.to_string(),
redirect_uri: REDIRECT_URI.to_string(),
scopes: scopes.iter().map(|scope| (*scope).to_string()).collect(),
resource: None,
state: Some("userinfo-state".to_string()),
code_challenge: CODE_CHALLENGE.to_string(),
code_challenge_method: oauth::CodeChallengeMethod::S256,
})
.expect("authorize userinfo client");
let access_token = oauth
.token(&oauth::TokenRequest {
grant_type: "authorization_code".to_string(),
code: Some(code),
redirect_uri: Some(REDIRECT_URI.to_string()),
client_id: CLIENT_ID.to_string(),
client_secret: None,
client_authentication_method: oauth::TokenEndpointAuthMethod::None,
code_verifier: Some(CODE_VERIFIER.to_string()),
refresh_token: None,
scopes: None,
resource: None,
})
.expect("exchange userinfo access token")
.access_token;
let provider = native_pending_oidc_provider(&oauth, "https://fastmcp.invalid/oidc/jwks");
let routes = OAuthHttpRoutes::new(oauth, base)
.expect("OAuth route base")
.with_oidc(provider)
.expect("OIDC routes");
(routes, access_token)
}
/// An OIDC provider over `oauth` whose signer activation is Pending on
/// `jwks_uri`, which fixes the JWKS route `with_oidc` will serve.
#[cfg(feature = "builtin-auth-server")]
fn native_pending_oidc_provider(
oauth: &Arc<oauth::OAuthServer>,
jwks_uri: &str,
) -> Arc<oidc::OidcProvider> {
let signer = oidc_public_canary_signer();
let provider = Arc::new(
oidc::OidcProvider::with_defaults(Arc::clone(oauth)).expect("default OIDC provider"),
);
provider
.set_id_token_signing_activation_dependencies(
Arc::new(LiveLoopbackOidcReadBackVerifier {
address: Mutex::new(None),
generation: signer.binding().ring_generation(),
}) as Arc<dyn oidc::OidcJwksReadBackVerifier>,
Arc::new(LiveOidcActivationStore::default())
as Arc<dyn oidc::OidcSigningActivationStore>,
)
.expect("OIDC activation dependencies");
provider
.begin_id_token_signing_activation(signer, jwks_uri)
.expect("OIDC Pending activation");
provider
}
/// The UserInfo route shares the OAuth route prefix, so a JWKS URI that
/// lands on it would make one path answer two documents. `with_oidc` must
/// refuse that configuration; a JWKS path beside it is accepted.
#[cfg(feature = "builtin-auth-server")]
#[test]
fn native_oidc_routes_refuse_a_jwks_path_on_the_userinfo_route() {
for (jwks_uri, accepted) in [
("https://fastmcp.invalid/oauth/jwks", true),
("https://fastmcp.invalid/oauth/userinfo", false),
] {
let oauth = Arc::new(oauth::OAuthServer::with_defaults());
let provider = native_pending_oidc_provider(&oauth, jwks_uri);
let routes = OAuthHttpRoutes::new(oauth, "https://fastmcp.invalid/oauth")
.expect("OAuth route base")
.with_oidc(provider);
if accepted {
let routes = routes.expect("a distinct JWKS path is accepted");
let oidc = routes.oidc_routes().expect("OIDC routes configured");
assert_eq!(oidc.jwks_path(), "/oauth/jwks");
assert_eq!(oidc.userinfo_path(), "/oauth/userinfo");
} else {
assert_eq!(
routes.err(),
Some(oauth::OAuthHttpRouteConfigurationError::InvalidPublicEndpointBase)
);
}
}
}
#[cfg(feature = "builtin-auth-server")]
fn native_oidc_userinfo_request(
routes: &OAuthHttpRoutes,
method: Http1Method,
authorization: Option<&str>,
) -> HttpResponse {
let path = routes
.oidc_routes()
.expect("OIDC routes configured")
.userinfo_path()
.to_owned();
let request = asupersync::http::h1::Request {
method,
uri: path.clone(),
version: asupersync::http::h1::Version::Http11,
headers: authorization
.map(|value| vec![("Authorization".to_owned(), value.to_owned())])
.unwrap_or_default(),
body: Vec::new(),
trailers: Vec::new(),
peer_addr: None,
};
dispatch_oauth_h1_request(routes, &request, &path, "")
}
/// bd-vej30: discovery advertises `userinfo_endpoint`, so a native route
/// must serve it, under either spelling of the endpoint base.
#[cfg(feature = "builtin-auth-server")]
#[test]
fn native_oidc_userinfo_serves_claims_for_an_openid_token_positive() {
for base in [
"https://fastmcp.invalid/oauth",
"https://fastmcp.invalid/oauth/",
] {
let (routes, access_token) = native_oidc_userinfo_fixture(&["openid"], base);
let oidc = routes.oidc_routes().expect("OIDC routes configured");
let discovery = oidc
.provider()
.discovery_document(routes.public_endpoint_base());
assert_eq!(
discovery.userinfo_endpoint,
Some(format!("https://fastmcp.invalid{}", oidc.userinfo_path())),
"{base}"
);
assert!(routes.has_path(oidc.userinfo_path()), "{base}");
for method in [Http1Method::Get, Http1Method::Post] {
let response = native_oidc_userinfo_request(
&routes,
method,
Some(&format!("Bearer {access_token}")),
);
assert_eq!(response.status, HttpStatus::OK, "{base} {response:?}");
assert_eq!(
response.headers.get("cache-control").map(String::as_str),
Some("no-store")
);
let claims: serde_json::Value =
serde_json::from_slice(&response.body).expect("userinfo JSON");
assert!(
claims["sub"].as_str().is_some_and(|sub| !sub.is_empty()),
"{claims}"
);
}
}
}
#[cfg(feature = "builtin-auth-server")]
#[test]
fn native_oidc_userinfo_refuses_a_token_without_openid_scope_negative() {
let (routes, access_token) =
native_oidc_userinfo_fixture(&["mcp"], "https://fastmcp.invalid/oauth");
let response = native_oidc_userinfo_request(
&routes,
Http1Method::Get,
Some(&format!("Bearer {access_token}")),
);
assert_eq!(response.status, HttpStatus::FORBIDDEN, "{response:?}");
assert_eq!(
response.headers.get("www-authenticate").map(String::as_str),
Some("Bearer error=\"insufficient_scope\"")
);
let body: serde_json::Value = serde_json::from_slice(&response.body).expect("error JSON");
assert_eq!(body, serde_json::json!({ "error": "insufficient_scope" }));
}
#[cfg(feature = "builtin-auth-server")]
#[test]
fn native_oidc_userinfo_challenges_missing_and_unknown_bearer_tokens() {
let (routes, _) =
native_oidc_userinfo_fixture(&["openid"], "https://fastmcp.invalid/oauth");
for authorization in [None, Some("Basic dXNlcjpwYXNz"), Some("Bearer")] {
let response = native_oidc_userinfo_request(&routes, Http1Method::Get, authorization);
assert_eq!(
response.status,
HttpStatus::UNAUTHORIZED,
"{authorization:?}"
);
assert_eq!(
response.headers.get("www-authenticate").map(String::as_str),
Some("Bearer"),
"{authorization:?}"
);
}
let response = native_oidc_userinfo_request(
&routes,
Http1Method::Get,
Some("Bearer forged-opaque-credential"),
);
assert_eq!(response.status, HttpStatus::UNAUTHORIZED);
assert_eq!(
response.headers.get("www-authenticate").map(String::as_str),
Some("Bearer error=\"invalid_token\"")
);
let response = native_oidc_userinfo_request(&routes, Http1Method::Put, None);
assert_eq!(response.status, HttpStatus::METHOD_NOT_ALLOWED);
}
#[cfg(feature = "builtin-auth-server")]
#[test]
fn live_http_oidc_jwks_read_back_then_discovery_activate_exact_public_bytes() {
run_live_http_test(|cx| async move {
use fastmcp_protocol::jose::ExternalRs256SigningDeadline;
let oauth = Arc::new(oauth::OAuthServer::with_defaults());
let signer = oidc_public_canary_signer();
let expected_jwks = signer
.canonical_public_jwks()
.map_err(|error| format!("OIDC canonical JWKS failed: {error}"))?
.as_bytes()
.to_vec();
let provider = Arc::new(
oidc::OidcProvider::with_defaults(Arc::clone(&oauth))
.map_err(|error| format!("OIDC provider construction failed: {error}"))?,
);
let verifier = Arc::new(LiveLoopbackOidcReadBackVerifier {
address: Mutex::new(None),
generation: signer.binding().ring_generation(),
});
let store = Arc::new(LiveOidcActivationStore::default());
provider
.set_id_token_signing_activation_dependencies(
Arc::clone(&verifier) as Arc<dyn oidc::OidcJwksReadBackVerifier>,
Arc::clone(&store) as Arc<dyn oidc::OidcSigningActivationStore>,
)
.map_err(|error| format!("OIDC external dependencies failed: {error}"))?;
provider
.begin_id_token_signing_activation(
Arc::clone(&signer),
"https://fastmcp.invalid/oidc/jwks",
)
.map_err(|error| format!("OIDC Pending admission failed: {error}"))?;
provider
.publish_id_token_signing_jwks(
signer.canonical_public_jwks().map_err(|error| {
format!("OIDC JWKS publication generation failed: {error}")
})?,
)
.map_err(|error| format!("OIDC canonical JWKS publication failed: {error}"))?;
let routes = OAuthHttpRoutes::new(Arc::clone(&oauth), "https://fastmcp.invalid/oauth")
.map_err(|error| format!("OIDC OAuth route base failed: {error}"))?
.with_oidc(Arc::clone(&provider))
.map_err(|error| format!("OIDC public route configuration failed: {error}"))?;
let bound = Server::new("live-oidc-jwks", "1.0.0")
.oauth_http_routes(routes)
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("OIDC public HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("OIDC public HTTP address failed: {error}"))?;
verifier.set_address(address);
let provider_for_client = Arc::clone(&provider);
let caller_cx = cx.clone();
let expected_for_client = expected_jwks.clone();
let mut client = cx
.spawn(move |client_cx| async move {
let jwks = live_http_exchange(
address,
b"GET /oidc/jwks HTTP/1.1\r\nHost: loopback\r\nConnection: close\r\n\r\n"
.to_vec(),
)
.await?;
if !jwks.starts_with(b"HTTP/1.1 200")
|| live_http_response_body(&jwks)? != expected_for_client.as_slice()
{
return Err(format!(
"OIDC public JWKS did not return exact canonical bytes: {}",
String::from_utf8_lossy(&jwks)
));
}
let deadline = ExternalRs256SigningDeadline::new(Duration::from_secs(1))
.map_err(|error| format!("OIDC canary deadline invalid: {error}"))?;
provider_for_client
.activate_id_token_signing(&client_cx, deadline)
.await
.map_err(|error| format!("OIDC active admission failed: {error}"))?;
let discovery = live_http_exchange(
address,
b"GET /.well-known/openid-configuration HTTP/1.1\r\nHost: loopback\r\nConnection: close\r\n\r\n"
.to_vec(),
)
.await?;
caller_cx.cancel_with(CancelKind::User, Some("live OIDC publication complete"));
Ok::<_, String>(discovery)
})
.map_err(|error| format!("OIDC public client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let discovery = client
.join(&cx)
.await
.map_err(|error| format!("OIDC public client failed: {error:?}"))??;
let shutdown = serve.map_err(|error| format!("OIDC public server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live OIDC public JWKS").await?;
if !discovery.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"OIDC discovery did not become publicly reachable: {}",
String::from_utf8_lossy(&discovery)
));
}
let discovery: serde_json::Value =
serde_json::from_slice(live_http_response_body(&discovery)?)
.map_err(|error| format!("OIDC discovery was not JSON: {error}"))?;
if discovery["issuer"] != "https://fastmcp.invalid/"
|| discovery["jwks_uri"] != "https://fastmcp.invalid/oidc/jwks"
{
return Err(format!(
"OIDC discovery mismatched active publication: {discovery}"
));
}
Ok(())
});
}
#[test]
fn live_http_oauth_pkce_authorize_then_token_is_publicly_reachable() {
run_live_http_test(|cx| async move {
let oauth = Arc::new(oauth::OAuthServer::with_defaults());
oauth
.register_client(
oauth::OAuthClient::builder("live-pkce-client")
.redirect_uri("https://client.example.test/callback")
.scope("mcp")
.build()
.map_err(|error| {
format!("live OAuth client registration failed: {error}")
})?,
)
.map_err(|error| format!("live OAuth client retention failed: {error}"))?;
let routes = OAuthHttpRoutes::new(Arc::clone(&oauth), "https://fastmcp.invalid/oauth")
.map_err(|error| format!("live OAuth HTTP routes were invalid: {error}"))?;
let bound = Server::new("live-oauth-pkce", "1.0.0")
.oauth_http_routes(routes)
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live OAuth HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live OAuth HTTP address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let denied = "GET /oauth/authorize?response_type=code&client_id=live-pkce-client&redirect_uri=https%3A%2F%2Fclient.example.test%2Fcallback&scope=denied&state=opaque-state&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256 HTTP/1.1\r\nHost: loopback\r\nConnection: close\r\n\r\n".to_owned();
// RH-5: this differs from the successful request only in
// its requested scope. A valid redirect must carry the
// OAuth error and the original opaque state.
let denied = live_http_exchange(address, denied.into_bytes()).await?;
let authorize = "GET /oauth/authorize?response_type=code&client_id=live-pkce-client&redirect_uri=https%3A%2F%2Fclient.example.test%2Fcallback&scope=mcp&state=opaque-state&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256 HTTP/1.1\r\nHost: loopback\r\nConnection: close\r\n\r\n".to_owned();
let authorize = live_http_exchange(address, authorize.into_bytes()).await?;
let location = live_http_response_header(&authorize, "location")?;
let redirect = url::Url::parse(&location)
.map_err(|error| format!("OAuth authorization Location was invalid: {error}"))?;
let code = redirect
.query_pairs()
.find_map(|(name, value)| (name == "code").then(|| value.into_owned()))
.ok_or_else(|| "OAuth authorization Location omitted code".to_owned())?;
if redirect
.query_pairs()
.find_map(|(name, value)| (name == "state").then(|| value.into_owned()))
.as_deref()
!= Some("opaque-state")
{
return Err("OAuth authorization Location changed state".to_owned());
}
let form = format!(
"grant_type=authorization_code&code={}&redirect_uri=https%3A%2F%2Fclient.example.test%2Fcallback&client_id=live-pkce-client&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
url::form_urlencoded::byte_serialize(code.as_bytes()).collect::<String>(),
);
let token = format!(
"POST /oauth/token HTTP/1.1\r\nHost: loopback\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
form.len(), form,
);
let token = live_http_exchange(address, token.into_bytes()).await?;
caller_cx.cancel_with(CancelKind::User, Some("live OAuth PKCE complete"));
Ok::<_, String>((denied, authorize, token))
})
.map_err(|error| format!("live OAuth client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let (denied, authorize, token) = client
.join(&cx)
.await
.map_err(|error| format!("live OAuth client failed: {error:?}"))??;
let shutdown = serve.map_err(|error| format!("live OAuth server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live OAuth PKCE").await?;
if !authorize.starts_with(b"HTTP/1.1 303") {
return Err(format!(
"OAuth authorization did not redirect: {}",
String::from_utf8_lossy(&authorize)
));
}
if !denied.starts_with(b"HTTP/1.1 303") {
return Err(format!(
"valid OAuth error did not redirect: {}",
String::from_utf8_lossy(&denied)
));
}
let denied_location = live_http_response_header(&denied, "location")?;
let denied_redirect = url::Url::parse(&denied_location)
.map_err(|error| format!("OAuth error Location was invalid: {error}"))?;
if denied_redirect
.query_pairs()
.find_map(|(name, value)| (name == "error").then(|| value.into_owned()))
.as_deref()
!= Some("invalid_scope")
|| denied_redirect
.query_pairs()
.find_map(|(name, value)| (name == "state").then(|| value.into_owned()))
.as_deref()
!= Some("opaque-state")
|| denied_redirect
.query_pairs()
.find_map(|(name, value)| (name == "iss").then(|| value.into_owned()))
.as_deref()
!= Some("https://fastmcp.invalid/")
|| denied_redirect
.query_pairs()
.any(|(name, _)| name == "code")
{
return Err(format!(
"OAuth error redirect did not preserve the safe client response: {denied_location}"
));
}
if !token.starts_with(b"HTTP/1.1 200")
|| live_http_response_header(&token, "cache-control")? != "no-store"
{
return Err(format!(
"OAuth token response was not a no-store success: {}",
String::from_utf8_lossy(&token)
));
}
let token: serde_json::Value = serde_json::from_slice(live_http_response_body(&token)?)
.map_err(|error| format!("OAuth token response was not JSON: {error}"))?;
if token["token_type"] != "bearer"
|| token["access_token"].as_str().is_none_or(str::is_empty)
{
return Err(format!("OAuth token response was incomplete: {token}"));
}
Ok(())
});
}
#[test]
fn live_http_oauth_redirect_uri_mismatch_preserves_authorization_code() {
run_live_http_test(|cx| async move {
let oauth = Arc::new(oauth::OAuthServer::with_defaults());
oauth
.register_client(
oauth::OAuthClient::builder("live-redirect-client")
.secret("correct-secret")
.token_endpoint_auth_method(
oauth::TokenEndpointAuthMethod::ClientSecretPost,
)
.redirect_uri("https://client.example.test/callback")
.scope("mcp")
.build()
.map_err(|error| {
format!("live redirect client registration failed: {error}")
})?,
)
.map_err(|error| format!("live redirect client retention failed: {error}"))?;
let routes = OAuthHttpRoutes::new(Arc::clone(&oauth), "https://fastmcp.invalid/oauth")
.map_err(|error| format!("live redirect OAuth routes were invalid: {error}"))?;
let bound = Server::new("live-oauth-redirect-retry", "1.0.0")
.oauth_http_routes(routes)
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live redirect OAuth HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live redirect OAuth HTTP address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let authorize = "GET /oauth/authorize?response_type=code&client_id=live-redirect-client&redirect_uri=https%3A%2F%2Fclient.example.test%2Fcallback&scope=mcp&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256 HTTP/1.1\r\nHost: loopback\r\nConnection: close\r\n\r\n".to_owned();
let authorize = live_http_exchange(address, authorize.into_bytes()).await?;
let location = live_http_response_header(&authorize, "location")?;
let redirect = url::Url::parse(&location)
.map_err(|error| format!("OAuth redirect Location was invalid: {error}"))?;
let code = redirect
.query_pairs()
.find_map(|(name, value)| (name == "code").then(|| value.into_owned()))
.ok_or_else(|| "OAuth redirect Location omitted code".to_owned())?;
let encoded_code = url::form_urlencoded::byte_serialize(code.as_bytes())
.collect::<String>();
let original = format!(
"grant_type=authorization_code&code={encoded_code}&redirect_uri=https%3A%2F%2Fclient.example.test%2Fcallback&client_id=live-redirect-client&client_secret=correct-secret&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
);
// RH-5: this form differs from `original` only at redirect_uri.
let mismatch = original.replace(
"redirect_uri=https%3A%2F%2Fclient.example.test%2Fcallback",
"redirect_uri=https%3A%2F%2Fclient.example.test%2Fother",
);
// RH-5: this form differs from `original` only at the
// confidential client credential. Its rejection must not
// consume the authorization code.
let wrong_secret = original.replace(
"client_secret=correct-secret",
"client_secret=wrong-secret",
);
let request = |path: &str, form: &str, authorization: Option<&str>| {
format!(
"POST {path} HTTP/1.1\r\nHost: loopback\r\nContent-Type: application/x-www-form-urlencoded\r\n{}Content-Length: {}\r\nConnection: close\r\n\r\n{}",
authorization.map_or_else(String::new, |value| format!("Authorization: {value}\r\n")),
form.len(),
form,
)
.into_bytes()
};
let rejected =
live_http_exchange(address, request("/oauth/token", &mismatch, None)).await?;
let wrong_secret_rejected = live_http_exchange(
address,
request("/oauth/token", &wrong_secret, None),
)
.await?;
// RH-5: compared with the eventual successful redemption,
// this adds a second client-credential location.
let header_rejected = live_http_exchange(
address,
request("/oauth/token", &original, Some("Basic ZmFzdG1jcDppbnZhbGlk")),
)
.await?;
// RH-5: this differs from `original` only in its
// unsupported Bearer Authorization header. It must not
// be converted into a Basic challenge or consume `code`.
let bearer_rejected = live_http_exchange(
address,
request("/oauth/token", &original, Some("Bearer unsupported-token")),
)
.await?;
let revocation =
"token=not-a-token&client_id=live-redirect-client&client_secret=wrong-secret";
let revoke_wrong_secret = live_http_exchange(
address,
request("/oauth/revoke", revocation, None),
)
.await?;
// A declared over-limit OAuth form must be rejected from
// its head alone; no body bytes follow this request.
let over_limit = format!(
"POST /oauth/token HTTP/1.1\r\nHost: loopback\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
oauth::MAX_OAUTH_FORM_BODY_BYTES + 1,
)
.into_bytes();
let over_limit = live_http_exchange(address, over_limit).await?;
let retried =
live_http_exchange(address, request("/oauth/token", &original, None)).await?;
let access_token = serde_json::from_slice::<serde_json::Value>(
live_http_response_body(&retried)?,
)
.map_err(|error| {
format!("successful retry token response was not JSON: {error}")
})?["access_token"]
.as_str()
.filter(|token| !token.is_empty())
.ok_or_else(|| {
"successful retry token response omitted access_token".to_owned()
})?
.to_owned();
let bearer_revocation_form = format!(
"token={}&client_id=live-redirect-client&client_secret=correct-secret",
url::form_urlencoded::byte_serialize(access_token.as_bytes())
.collect::<String>(),
);
// RH-5: this differs only by an unsupported Bearer
// Authorization header from the form that would revoke
// the freshly issued access token.
let bearer_revoke_rejected = live_http_exchange(
address,
request(
"/oauth/revoke",
&bearer_revocation_form,
Some("Bearer unsupported-token"),
),
)
.await?;
caller_cx.cancel_with(CancelKind::User, Some("live OAuth redirect retry complete"));
Ok::<_, String>((
rejected,
wrong_secret_rejected,
header_rejected,
bearer_rejected,
revoke_wrong_secret,
over_limit,
retried,
bearer_revoke_rejected,
access_token,
))
})
.map_err(|error| format!("live redirect OAuth client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let (
rejected,
wrong_secret_rejected,
header_rejected,
bearer_rejected,
revoke_wrong_secret,
over_limit,
retried,
bearer_revoke_rejected,
access_token,
) = client
.join(&cx)
.await
.map_err(|error| format!("live redirect OAuth client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("live redirect OAuth server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live OAuth redirect retry").await?;
if !rejected.starts_with(b"HTTP/1.1 400")
|| serde_json::from_slice::<serde_json::Value>(live_http_response_body(&rejected)?)
.ok()
.and_then(|body| body["error"].as_str().map(str::to_owned))
.as_deref()
!= Some("invalid_grant")
{
return Err(format!(
"redirect-only mismatch was not an invalid_grant rejection: {}",
String::from_utf8_lossy(&rejected)
));
}
if !retried.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"a rejected token request consumed the authorization code: {}",
String::from_utf8_lossy(&retried)
));
}
for (endpoint, response) in [
("token form", &wrong_secret_rejected),
("revocation form", &revoke_wrong_secret),
] {
if !response.starts_with(b"HTTP/1.1 400")
|| live_http_response_has_header(response, "www-authenticate")?
|| serde_json::from_slice::<serde_json::Value>(live_http_response_body(
response,
)?)
.ok()
.and_then(|body| body["error"].as_str().map(str::to_owned))
.as_deref()
!= Some("invalid_client")
{
return Err(format!(
"{endpoint} wrong-secret rejection was not an unchallenged invalid_client 400: {}",
String::from_utf8_lossy(response)
));
}
}
if !header_rejected.starts_with(b"HTTP/1.1 400")
|| live_http_response_has_header(&header_rejected, "www-authenticate")?
|| serde_json::from_slice::<serde_json::Value>(live_http_response_body(
&header_rejected,
)?)
.ok()
.and_then(|body| body["error"].as_str().map(str::to_owned))
.as_deref()
!= Some("invalid_request")
{
return Err(format!(
"mixed client credentials were not an unchallenged invalid_request 400: {}",
String::from_utf8_lossy(&header_rejected)
));
}
for (endpoint, response) in [
("token Bearer", &bearer_rejected),
("revocation Bearer", &bearer_revoke_rejected),
] {
if !response.starts_with(b"HTTP/1.1 400")
|| live_http_response_has_header(response, "www-authenticate")?
|| serde_json::from_slice::<serde_json::Value>(live_http_response_body(
response,
)?)
.ok()
.and_then(|body| body["error"].as_str().map(str::to_owned))
.as_deref()
!= Some("invalid_request")
{
return Err(format!(
"{endpoint} rejection was not an unchallenged invalid_request: {}",
String::from_utf8_lossy(response)
));
}
}
if oauth.validate_access_token(&access_token).is_none() {
return Err(
"Bearer rejection on /oauth/revoke changed the issued access-token state"
.to_owned(),
);
}
if !over_limit.starts_with(b"HTTP/1.1 400") {
return Err(format!(
"over-limit OAuth form was not rejected from its head: {}",
String::from_utf8_lossy(&over_limit)
));
}
Ok(())
});
}
#[test]
fn live_http_strict_admission_dispatches_a_canonical_modern_post() {
run_live_http_test(|cx| async move {
let response = live_http_strict_admission_exchange(&cx, SERVER_DISCOVER_METHOD).await?;
if !response.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"canonical strict-admission POST used an unexpected response: {}",
String::from_utf8_lossy(&response)
));
}
let response: JsonRpcResponse =
serde_json::from_slice(live_http_response_body(&response)?).map_err(|error| {
format!("canonical strict-admission response was invalid: {error}")
})?;
if response.id != Some(803_i64.into()) || response.error.is_some() {
return Err(format!(
"canonical strict-admission POST was not dispatched: {response:?}"
));
}
Ok(())
});
}
#[test]
fn live_http_strict_admission_rejects_only_a_mismatched_method_mirror() {
run_live_http_test(|cx| async move {
// This is the canonical live POST above with only its Mcp-Method
// header changed; the JSON body still names server/discover.
let response =
live_http_strict_admission_exchange(&cx, "server/discover-mismatch").await?;
if !response.starts_with(b"HTTP/1.1 400") {
return Err(format!(
"mismatched strict-admission mirror used an unexpected response: {}",
String::from_utf8_lossy(&response)
));
}
let response: JsonRpcResponse =
serde_json::from_slice(live_http_response_body(&response)?).map_err(|error| {
format!("strict-admission rejection was not valid JSON-RPC: {error}")
})?;
if response.id != Some(803_i64.into())
|| response.result.is_some()
|| response
.error
.as_ref()
.and_then(|error| error.code.as_i32())
!= Some(fastmcp_protocol::HEADER_MISMATCH_ERROR_CODE)
|| response
.error
.as_ref()
.and_then(|error| error.data.as_ref())
.is_some()
{
return Err(format!(
"strict-admission mirror did not retain its canonical pre-dispatch error: {response:?}"
));
}
Ok(())
});
}
#[test]
fn live_http_listener_merges_repeated_accept_but_rejects_a_duplicate_singleton() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-repeated-admission", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("repeated admission HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("repeated admission HTTP address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
struct CancelServerOnDrop(Cx);
impl Drop for CancelServerOnDrop {
fn drop(&mut self) {
self.0.cancel_with(
CancelKind::User,
Some("repeated admission client exited"),
);
}
}
let _server_cancellation = CancelServerOnDrop(caller_cx);
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
803_i64,
);
let body = serde_json::to_vec(&request).map_err(|error| {
format!("repeated admission request did not serialize: {error}")
})?;
let later_member_required = live_http_exchange(
address,
live_http_post(
"/mcp",
&body,
&[
("Accept", "text/plain"),
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SERVER_DISCOVER_METHOD),
],
),
)
.await?;
let accepted = live_http_exchange(
address,
live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json"),
("Accept", "text/event-stream; q=0"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SERVER_DISCOVER_METHOD),
],
),
)
.await?;
let rejected = live_http_exchange(
address,
live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json"),
("Accept", "text/event-stream; q=0"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SERVER_DISCOVER_METHOD),
("Mcp-Method", SERVER_DISCOVER_METHOD),
],
),
)
.await?;
Ok::<_, String>((later_member_required, accepted, rejected))
})
.map_err(|error| format!("repeated admission client was not admitted: {error}"))?;
let serve = bound.serve(&cx).await;
let (later_member_required, accepted, rejected) = client
.join(&cx)
.await
.map_err(|error| format!("repeated admission client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("repeated admission server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "repeated admission").await?;
if !later_member_required.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"a later valid repeated Accept member was not retained: {}",
String::from_utf8_lossy(&later_member_required)
));
}
if !accepted.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"wire-ordered repeated Accept fields used an unexpected response: {}",
String::from_utf8_lossy(&accepted)
));
}
if !rejected.starts_with(b"HTTP/1.1 400") {
return Err(format!(
"changing only the repeated header name to a singleton used an unexpected response: {}",
String::from_utf8_lossy(&rejected)
));
}
Ok(())
});
}
#[test]
fn live_http_modern_sse_post_dispatches_without_discovery_or_session_id() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-http-stateless-sse", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(auth_00_mrtr_provider(true))
.tool(LiveHttpMrtrTool {
name: "live_http_mrtr",
calls: Arc::clone(&calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live HTTP MRTR bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live HTTP MRTR address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let initial = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_http_mrtr",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "live-http-mrtr-sse",
},
})),
922_i64,
);
let initial_body = serde_json::to_vec(&initial).map_err(|error| {
format!("MRTR initial request did not serialize: {error}")
})?;
let initial = live_http_exchange(
address,
live_http_post(
"/mcp",
&initial_body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_http_mrtr"),
("Authorization", "Bearer alpha"),
],
),
)
.await?;
caller_cx.cancel_with(
CancelKind::User,
Some("live HTTP stateless SSE request complete"),
);
Ok::<_, String>(initial)
})
.map_err(|error| format!("live HTTP MRTR client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let response = client
.join(&cx)
.await
.map_err(|error| format!("live HTTP MRTR client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("live HTTP MRTR server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live HTTP MRTR").await?;
let response = live_http_sse_jsonrpc_response(&response)?;
if response.id != Some(922_i64.into())
|| response.error.is_some()
|| response
.result
.as_ref()
.and_then(|result| result.get("resultType"))
!= Some(&serde_json::json!("input_required"))
{
return Err(format!(
"sessionless modern SSE POST did not dispatch: {response:?}"
));
}
if calls.load(Ordering::Acquire) != 1 {
return Err(
"sessionless modern SSE POST did not invoke the handler once".to_owned(),
);
}
Ok(())
});
}
#[test]
fn live_http_modern_authorization_allows_native_bearer_once() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-http-modern-authorization", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(ModernHttpAuthProvider)
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live HTTP authorization bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live HTTP authorization address failed: {error}"))?;
let request = modern_http_json_tool_request("modern_http_auth_counter", 935);
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let response = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Accept", "application/json"),
("Authorization", "Bearer alpha"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("live HTTP native bearer proof complete"),
);
response
})
.map_err(|error| {
format!("live HTTP authorization client admission failed: {error}")
})?;
let serve = bound.serve(&cx).await;
let response = client
.join(&cx)
.await
.map_err(|error| format!("live HTTP authorization client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("live HTTP authorization server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live HTTP native bearer").await?;
let response: JsonRpcResponse =
serde_json::from_slice(live_http_response_body(&response)?).map_err(|error| {
format!("live HTTP authorization response was not JSON-RPC: {error}")
})?;
if response.id != Some(935_i64.into()) || response.error.is_some() {
return Err(format!(
"allowed native bearer did not dispatch: {response:?}"
));
}
if calls.load(Ordering::Acquire) != 1 {
return Err(
"allowed native bearer did not invoke the handler exactly once".to_owned(),
);
}
Ok(())
});
}
#[test]
fn live_http_native_authorization_rejects_before_owned_state_and_strips_valid_bearer() {
run_live_http_test(|cx| async move {
let handler_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let saw_credential = Arc::new(AtomicBool::new(false));
let bound = Server::new("live-http-native-auth-admission", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(ModernHttpAuthProvider)
.middleware(ModernHttpAuthMiddleware {
calls: Arc::clone(&middleware_calls),
saw_credential: Arc::clone(&saw_credential),
})
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&handler_calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("native auth admission bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("native auth admission address failed: {error}"))?;
let request = modern_http_json_tool_request("modern_http_auth_counter", 936);
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let common = [
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
];
let result = async {
let missing = live_http_exchange(
address,
live_http_post("/mcp", &request.body, &common),
)
.await?;
let invalid = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Authorization", "Bearer invalid-never-echo"),
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
let malformed = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Authorization", "Bearer"),
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
let duplicate = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Authorization", "Bearer alpha"),
("aUtHoRiZaTiOn", "Bearer beta"),
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
let accepted = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Authorization", "Bearer alpha"),
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
Ok::<_, String>((missing, invalid, malformed, duplicate, accepted))
}
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("native HTTP authorization admission proof complete"),
);
result
})
.map_err(|error| {
format!("native auth admission client admission failed: {error}")
})?;
let serve = bound.serve(&cx).await;
let (missing, invalid, malformed, duplicate, accepted) = client
.join(&cx)
.await
.map_err(|error| format!("native auth admission client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("native auth admission server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "native HTTP authorization admission")
.await?;
for (label, response) in [
("missing", missing),
("invalid", invalid),
("malformed", malformed),
("duplicate", duplicate),
] {
if !response.starts_with(b"HTTP/1.1 401")
|| live_http_response_header(&response, "www-authenticate")? != "Bearer"
|| std::str::from_utf8(&response)
.map_err(|error| format!("{label} auth response was not UTF-8: {error}"))?
.contains("invalid-never-echo")
{
return Err(format!(
"{label} native Authorization was not a bounded pre-dispatch 401"
));
}
}
let accepted: JsonRpcResponse =
serde_json::from_slice(live_http_response_body(&accepted)?).map_err(|error| {
format!("valid native Authorization did not return JSON-RPC: {error}")
})?;
if accepted.error.is_some()
|| handler_calls.load(Ordering::Acquire) != 1
|| middleware_calls.load(Ordering::Acquire) != 1
|| saw_credential.load(Ordering::Acquire)
{
return Err(
"native Authorization admission mutated owned state or leaked bearer"
.to_owned(),
);
}
Ok(())
});
}
#[derive(Clone)]
struct HttpHeaderAuthProbe {
provider: Arc<auth::TokenAuthProvider>,
provider_calls: Arc<AtomicUsize>,
middleware_calls: Arc<AtomicUsize>,
handler_calls: Arc<AtomicUsize>,
token: String,
subject: String,
}
impl HttpHeaderAuthProbe {
fn new() -> Self {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("test clock must follow the Unix epoch")
.as_nanos();
let token = format!("auth01-{}-{nonce}", std::process::id());
let subject = format!("http-principal-{nonce}");
let verifier = auth::StaticTokenVerifier::new([(
token.clone(),
AuthContext::with_subject(subject.clone()),
)])
.expect("runtime-selected static credential must be admissible");
Self {
provider: Arc::new(auth::TokenAuthProvider::new(verifier)),
provider_calls: Arc::new(AtomicUsize::new(0)),
middleware_calls: Arc::new(AtomicUsize::new(0)),
handler_calls: Arc::new(AtomicUsize::new(0)),
token,
subject,
}
}
fn effects(&self) -> (usize, usize, usize) {
(
self.provider_calls.load(Ordering::Acquire),
self.middleware_calls.load(Ordering::Acquire),
self.handler_calls.load(Ordering::Acquire),
)
}
}
impl AuthProvider for HttpHeaderAuthProbe {
fn authenticate(
&self,
ctx: &McpContext,
request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
self.provider_calls.fetch_add(1, Ordering::AcqRel);
self.provider.authenticate(ctx, request)
}
}
impl Middleware for HttpHeaderAuthProbe {
fn on_request(
&self,
ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
self.middleware_calls.fetch_add(1, Ordering::AcqRel);
let serialized = serde_json::to_string(request)
.map_err(|error| McpError::internal_error(error.to_string()))?;
if serialized.contains(&self.token)
|| ctx.auth().and_then(|auth| auth.subject).as_deref() != Some(&self.subject)
{
return Err(McpError::internal_error(
"HTTP credential custody or principal changed",
));
}
Ok(MiddlewareDecision::Continue)
}
}
impl ToolHandler for HttpHeaderAuthProbe {
fn definition(&self) -> Tool {
Tool {
name: "http_header_auth_probe".to_owned(),
description: None,
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, arguments: serde_json::Value) -> McpResult<Vec<Content>> {
self.handler_calls.fetch_add(1, Ordering::AcqRel);
if arguments
!= serde_json::json!({"token": "application-data", "access_token": "application-data"})
{
return Err(McpError::internal_error(
"application arguments changed during authentication",
));
}
Ok(vec![Content::text(
ctx.auth().and_then(|auth| auth.subject).unwrap_or_default(),
)])
}
}
fn require_http_credential_location_rejection(
response: &[u8],
token: &str,
) -> Result<(), String> {
if !response.starts_with(b"HTTP/1.1 401")
|| live_http_response_header(response, "www-authenticate")? != "Bearer"
|| response
.windows(token.len())
.any(|window| window == token.as_bytes())
{
return Err("credential location did not produce a secret-free HTTP 401".to_owned());
}
let body: serde_json::Value = serde_json::from_slice(live_http_response_body(response)?)
.map_err(|error| error.to_string())?;
if body
!= serde_json::json!({
"error": "invalid_request",
"message": "HTTP credentials must use the Authorization header",
})
{
return Err(
"credential location rejection lost its fixed migration message".to_owned(),
);
}
Ok(())
}
#[cfg(feature = "tasks")]
struct AuthTaskSupervisor;
#[cfg(feature = "tasks")]
impl ApplicationTaskSupervisor for AuthTaskSupervisor {
fn resume<'a>(
&'a self,
_cx: &'a Cx,
handoff: FinalTaskSupervisorHandoff,
) -> FinalTaskSupervisorFuture<'a> {
Box::pin(async move {
match handoff {
FinalTaskSupervisorHandoff::Initial(initial) => {
initial.require_input(
serde_json::from_value(
serde_json::json!({"roots": {"method": "roots/list"}}),
)
.unwrap(),
None,
)?;
}
FinalTaskSupervisorHandoff::Resumed(accepted) => {
accepted.complete_task(serde_json::from_value(serde_json::json!({"content": [{"type": "text", "text": "owner input consumed"}]})).unwrap(), None)?;
}
}
Ok(())
})
}
}
#[cfg(feature = "tasks")]
fn auth_task_http_frame(request: &JsonRpcRequest, authorization: &str) -> Vec<u8> {
let mut headers = vec![
(
"Accept",
if request.method == SUBSCRIPTIONS_LISTEN {
"text/event-stream"
} else {
"application/json"
},
),
("Authorization", authorization),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", request.method.as_str()),
];
if request.method == "tools/call" {
headers.push(("Mcp-Name", "durable_final_task"));
} else if let Some(task_id) = request
.params
.as_ref()
.and_then(|params| params.get("taskId"))
.and_then(serde_json::Value::as_str)
{
headers.push(("Mcp-Name", task_id));
}
live_http_post("/mcp", &serde_json::to_vec(request).unwrap(), &headers)
}
#[cfg(feature = "tasks")]
async fn auth_task_http_rpc(
address: SocketAddr,
request: &JsonRpcRequest,
authorization: &str,
) -> Result<JsonRpcResponse, String> {
let frame =
live_http_exchange(address, auth_task_http_frame(request, authorization)).await?;
let response = if live_http_response_header(&frame, "content-type")?
.starts_with("text/event-stream")
{
live_http_sse_jsonrpc_response(&frame)?
} else {
serde_json::from_slice(live_http_response_body(&frame)?)
.map_err(|error| error.to_string())?
};
eprintln!(
"{}",
serde_json::json!({"proof": "auth_00_local_task_owner", "method": request.method, "response": response, "frame_sha256": sha256_bounded(&frame, 65_536).unwrap().as_bytes()})
);
Ok(response)
}
#[cfg(feature = "tasks")]
fn auth_00_local_task_owner_probe(plant_foreign_caller: bool) {
run_live_http_test(move |cx| async move {
let subject = format!(
"task-owner-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
);
// TokenAuthProvider derives a fallback owner per credential, so a
// rotated credential changes it (756719c3). Continuity across the
// refresh therefore needs the verifier's own owner, as auth.rs
// documents; the foreign caller has a distinct one.
let verified_owner = |subject: &str| {
sha256_bounded(subject.as_bytes(), 256).expect("bounded test owner subject")
};
let owner = verified_owner(&subject);
let mut refreshed =
AuthContext::with_subject(subject.clone()).with_session_owner(owner);
refreshed.scopes = vec!["tasks".to_owned()];
let foreign_subject = format!("foreign-{subject}");
let verifier = StaticTokenVerifier::new([
(
"owner-first",
AuthContext::with_subject(subject.clone()).with_session_owner(owner),
),
("owner-refreshed", refreshed),
(
"foreign-valid",
AuthContext::with_subject(foreign_subject.clone())
.with_session_owner(verified_owner(&foreign_subject)),
),
])
.map_err(|error| error.to_string())?;
let store = Arc::new(tasks::InMemoryFinalTaskStore::default());
let task_runtime = FinalTaskRuntime::new(
store.clone(),
FinalTaskRuntimeConfig::new(60_000, Some(5_000)).unwrap(),
Arc::new(|_| {}),
);
let runner = task_runtime
.install_task_service(1, Arc::new(AuthTaskSupervisor))
.map_err(|error| error.to_string())?;
let scope = cx.scope();
let mut service = cx
.spawn_in(&scope, move |service_cx| async move {
runner.run(&service_cx).await
})
.map_err(|error| error.to_string())?;
let ready_deadline = cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while !task_runtime.is_task_service_ready() {
if cx.now() >= ready_deadline {
return Err("task service did not become ready".to_owned());
}
asupersync::runtime::yield_now().await;
}
let bound = Server::new("auth-task-owner", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.unwrap()
.auth_provider(TokenAuthProvider::new(verifier))
.tool(FinalTaskCreatingTool)
.final_tasks(task_runtime.clone())
.unwrap()
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| error.to_string())?;
let address = bound.local_addr().map_err(|error| error.to_string())?;
let task_server = Arc::clone(&bound.endpoint.server);
let caller_cx = cx.clone();
let mut client = cx.spawn(move |client_cx| async move {
struct CancelServerOnDrop(Cx);
impl Drop for CancelServerOnDrop {
fn drop(&mut self) {
self.0.cancel_with(CancelKind::User, Some("task owner probe exited"));
}
}
let _shutdown = CancelServerOnDrop(caller_cx.clone());
let result = async {
let mut streams = Vec::new();
for method in [fastmcp_protocol::tasks_extension::TASK_UPDATE, fastmcp_protocol::TASK_CANCEL] {
let created = auth_task_http_rpc(address, &final_task_creating_tool_request(true), "Bearer owner-first").await?;
let created = created.result.ok_or_else(|| format!("authenticated task creation failed: {:?}", created.error))?;
let task_id = fastmcp_protocol::FinalTaskId::parse(created["taskId"].as_str().ok_or("task handle missing")?).map_err(|error| error.to_string())?;
let deadline = client_cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
loop {
if matches!(task_runtime.get_task(&task_id).unwrap().task, fastmcp_protocol::Task::InputRequired { .. }) { break; }
if client_cx.now() >= deadline { return Err("application did not request task input".to_owned()); }
asupersync::time::sleep(client_cx.now(), Duration::from_millis(5)).await;
}
let params = final_tasks_params(&task_id, serde_json::json!({}));
let get = JsonRpcRequest::new(fastmcp_protocol::TASK_GET, Some(params.clone()), 951_i64);
let mut update_params = params.clone();
update_params["inputResponses"] = serde_json::json!({"roots": {"roots": []}});
let update = JsonRpcRequest::new(fastmcp_protocol::tasks_extension::TASK_UPDATE, Some(update_params), 952_i64);
let cancel = JsonRpcRequest::new(fastmcp_protocol::TASK_CANCEL, Some(params.clone()), 953_i64);
let mut notifications = SubscriptionFilter::default();
set_task_subscription_ids(&mut notifications, vec![task_id.clone()]).unwrap();
let listen = JsonRpcRequest::new(SUBSCRIPTIONS_LISTEN, Some(serde_json::json!({"_meta": params["_meta"], "notifications": notifications})), 954_i64);
if plant_foreign_caller {
let before = store.get_task_snapshot(&task_id).unwrap().unwrap();
let notification_before = serde_json::to_value(store.latest_notification(&task_id)).unwrap();
for request in [&get, &update, &cancel, &listen] {
let foreign = auth_task_http_rpc(address, request, "Bearer foreign-valid").await?;
let mut missing = request.clone();
if request.method == SUBSCRIPTIONS_LISTEN {
let mut absent_filter = SubscriptionFilter::default();
set_task_subscription_ids(&mut absent_filter, vec![fastmcp_protocol::FinalTaskId::parse("absent-task").unwrap()]).unwrap();
missing.params.as_mut().unwrap()["notifications"] = serde_json::to_value(absent_filter).unwrap();
} else {
missing.params.as_mut().unwrap()["taskId"] = serde_json::json!("absent-task");
}
let absent = auth_task_http_rpc(address, &missing, "Bearer foreign-valid").await?;
assert!(foreign.result.is_none());
assert_eq!(serde_json::to_value(&foreign.error).unwrap(), serde_json::to_value(&absent.error).unwrap());
assert_eq!(foreign.error.unwrap().message, "Task not found");
let after = store.get_task_snapshot(&task_id).unwrap().unwrap();
assert_eq!(after.generation(), before.generation());
assert_eq!(serde_json::to_value(after.task()).unwrap(), serde_json::to_value(before.task()).unwrap());
assert_eq!(after.authenticated_principal(), before.authenticated_principal());
assert_eq!(serde_json::to_value(store.latest_notification(&task_id)).unwrap(), notification_before);
assert!(!store.is_cancellation_requested(&task_id).unwrap());
assert!(store.next_accepted_input_snapshot().unwrap().is_none());
}
}
let read = auth_task_http_rpc(address, &get, "Bearer owner-refreshed").await?;
assert!(read.error.is_none());
assert_eq!(read.result.unwrap()["taskId"], serde_json::json!(task_id));
let mut stream = AsyncTcpStream::connect(address).await.map_err(|error| error.to_string())?;
stream.write_all(&auth_task_http_frame(&listen, "Bearer owner-refreshed")).await.map_err(|error| error.to_string())?;
stream.flush().await.map_err(|error| error.to_string())?;
let mut received = Vec::new();
read_live_http_until(&mut stream, &mut received, b"notifications/subscriptions/acknowledged").await?;
let control = if method == fastmcp_protocol::tasks_extension::TASK_UPDATE { &update } else { &cancel };
let controlled = auth_task_http_rpc(address, control, "Bearer owner-refreshed").await?;
assert!(controlled.error.is_none());
assert_eq!(controlled.result.unwrap()["resultType"], "complete");
let terminal = if method == fastmcp_protocol::tasks_extension::TASK_UPDATE { "completed" } else { "cancelled" };
read_live_http_until(&mut stream, &mut received, format!("\"status\":\"{terminal}\"").as_bytes()).await?;
streams.push((stream, received, task_id, terminal));
}
assert_eq!(task_server.terminate_subscription_streams(), 2);
for (mut stream, mut received, task_id, terminal) in streams {
read_live_http_to_end(&mut stream, &mut received, "owner subscription terminal EOF").await?;
let messages = live_http_chunked_sse_messages(&received)?;
let Some(JsonRpcMessage::Request(acknowledgement)) = messages.first() else { panic!("missing subscription acknowledgement"); };
let ServerNotification::SubscriptionsAcknowledged(acknowledgement) = ServerNotification::decode(acknowledgement).unwrap() else { panic!("first event must acknowledge subscription"); };
assert_eq!(task_subscription_ids(&acknowledgement.notifications).unwrap().unwrap(), vec![task_id.clone()]);
let events = messages.iter().filter_map(|message| {
let JsonRpcMessage::Request(request) = message else { return None; };
(request.method == fastmcp_protocol::TASK_STATUS_NOTIFICATION).then(|| serde_json::from_value::<FinalTaskStatusNotification>(serde_json::to_value(request).unwrap()).unwrap())
}).collect::<Vec<_>>();
assert!(!events.is_empty());
assert!(events.iter().all(|event| event.params.task.base().task_id == task_id));
let terminal_events = events.iter().filter(|event| serde_json::to_value(&event.params.task).unwrap()["status"] == terminal).collect::<Vec<_>>();
assert_eq!(terminal_events.len(), 1);
if terminal == "completed" {
let task = serde_json::to_value(&terminal_events[0].params.task).unwrap();
assert_eq!(task["result"]["content"][0]["text"], "owner input consumed");
}
eprintln!("{}", serde_json::json!({"proof": "auth_00_local_task_owner", "subject": subject, "task_id": task_id, "terminal": terminal, "subscription_events": events.len(), "subscription_frame_sha256": sha256_bounded(&received, 65_536).unwrap().as_bytes(), "negative": plant_foreign_caller}));
}
Ok::<_, String>(())
}.await;
caller_cx.cancel_with(CancelKind::User, Some("task owner probe complete"));
result
}).map_err(|error| error.to_string())?;
let serve = bound.serve(&cx).await;
// The application service has its own context. Cancelling the
// HTTP caller does not stop that context or its idle receiver.
service.abort();
let client_result = client
.join(&cx)
.await
.map_err(|error| format!("task owner client failed: {error:?}"));
let mut service_shutdown = Box::pin(asupersync::time::timeout_at(
cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
service.join(&cx),
));
// Keep the real cleanup deadline while shielding its timer from
// the caller's cancellation. The service's own abort stays active.
let service_result = std::future::poll_fn(|task_context| {
cx.masked(|| service_shutdown.as_mut().poll(task_context))
})
.await
.map_err(|_| live_http_test_timeout("task owner service shutdown"))?;
match service_result {
Ok(Ok(())) | Err(asupersync::runtime::JoinError::Cancelled(_)) => {}
other => return Err(format!("task service failed during owner probe: {other:?}")),
}
client_result??;
require_quiescent_http_shutdown(serve.map_err(|error| error.to_string())?, "task owner")
.await
});
}
#[cfg(feature = "tasks")]
#[test]
fn auth_00_local_task_owner_positive() {
auth_00_local_task_owner_probe(false);
}
#[cfg(feature = "tasks")]
#[test]
fn auth_00_local_task_owner_planted_negative() {
auth_00_local_task_owner_probe(true);
}
#[cfg(feature = "tasks")]
fn auth_00_task_notification_origin_probe(foreign_origin: bool) {
let runtime = FinalTaskRuntime::in_memory(
FinalTaskRuntimeConfig::new(60_000, None).unwrap(),
Arc::new(|_| {}),
);
let _service = start_final_tasks_test_service(&runtime);
let captured = Arc::new(Mutex::new(Vec::new()));
let captured_by_emitter = Arc::clone(&captured);
runtime.add_owned_notification_emitter(Arc::new(move |notification, principal| {
captured_by_emitter
.lock()
.unwrap()
.push((notification, principal));
}));
let owner = McpContext::new(Cx::for_testing(), 1)
.with_auth(AuthContext::with_subject("notification-owner"));
let created = runtime
.create_task_for_request(&owner, final_tasks_test_work_descriptor(), None)
.unwrap();
let task_id = created.task.base().task_id.clone();
let (notification, principal) = captured.lock().unwrap()[0].clone();
assert_eq!(
principal,
Some(auth::principal_fingerprint(owner.auth().as_ref()).unwrap())
);
let registry = Arc::new(FinalSubscriptionRegistry::default());
let sent = Arc::new(Mutex::new(Vec::new()));
let sent_by_sender = Arc::clone(&sent);
let mut requested = SubscriptionFilter::default();
set_task_subscription_ids(&mut requested, vec![task_id]).unwrap();
let _lease = registry
.open_with_task_principal(
1_i64.into(),
requested,
true,
None,
McpRequestCancellation::new(),
None,
Arc::new(move |event| sent_by_sender.lock().unwrap().push(event)),
principal,
)
.unwrap();
assert_eq!(sent.lock().unwrap().len(), 1);
let origin = if foreign_origin {
Some(
auth::principal_fingerprint(Some(&AuthContext::with_subject("old-foreign-owner")))
.unwrap(),
)
} else {
principal
};
let published = registry
.publish_owned_task(notification.clone(), origin)
.unwrap();
assert_eq!(published, usize::from(!foreign_origin));
assert_eq!(sent.lock().unwrap().len(), 1 + published);
if foreign_origin {
// A task ID, timestamp, and payload may all be identical after
// store reuse. Origin custody alone prevents the stale owner event.
assert_eq!(
registry
.publish_owned_task(notification, principal)
.unwrap(),
1
);
assert_eq!(sent.lock().unwrap().len(), 2);
}
}
#[cfg(feature = "tasks")]
#[test]
fn auth_00_task_notification_origin_positive() {
auth_00_task_notification_origin_probe(false);
}
#[cfg(feature = "tasks")]
#[test]
fn auth_00_task_notification_origin_planted_negative() {
auth_00_task_notification_origin_probe(true);
}
fn auth_01_http_header_probe(reject_forbidden_locations: bool) {
run_live_http_test(move |cx| async move {
let probe = HttpHeaderAuthProbe::new();
let bound = Server::new("auth-01-http-header", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern policy must be available")
.auth_provider(probe.clone())
.middleware(probe.clone())
.tool(probe.clone())
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| error.to_string())?;
let address = bound.local_addr().map_err(|error| error.to_string())?;
let caller_cx = cx.clone();
let mut client = cx.spawn(move |_client_cx| async move {
let result = async {
let authorization = format!("Bearer {}", probe.token);
let mut request = modern_http_json_tool_request("http_header_auth_probe", 945);
let mut body: serde_json::Value = serde_json::from_slice(&request.body)
.map_err(|error| error.to_string())?;
body["params"]["arguments"] = serde_json::json!({
"token": "application-data", "access_token": "application-data",
});
request.body = serde_json::to_vec(&body).map_err(|error| error.to_string())?;
for (index, accept) in ["application/json", "text/event-stream"].into_iter().enumerate() {
let common = [
("Accept", accept),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "http_header_auth_probe"),
];
let mut headers = common.to_vec();
headers.push(("Authorization", authorization.as_str()));
let mut rejected_locations = 0;
if reject_forbidden_locations {
let before = probe.effects();
for container in [None, Some("_meta"), Some("headers")] {
for field in ["authorization", "Authorization", "auth", "token", "access_token", "accessToken"] {
let mut forbidden = body.clone();
let destination = match container {
Some(container) => &mut forbidden["params"][container],
None => &mut forbidden["params"],
};
destination[field] = serde_json::json!(authorization);
let forbidden = serde_json::to_vec(&forbidden).map_err(|error| error.to_string())?;
for credential_headers in [&common[..], &headers[..]] {
let rejected = live_http_exchange(address, live_http_post("/mcp?application_context=permitted", &forbidden, credential_headers)).await?;
require_http_credential_location_rejection(&rejected, &probe.token)?;
rejected_locations += 1;
if probe.effects() != before {
return Err(format!("body credential invoked provider/middleware/handler: container={container:?}, field={field}"));
}
}
}
}
for field in ["access_token", "%61ccess_token", "ACCESS_TOKEN", "authorization", "auth", "token", "accessToken"] {
for credential_headers in [&common[..], &headers[..]] {
let rejected = live_http_exchange(address, live_http_post(
&format!("/mcp?application_context=permitted&{field}={}", probe.token), &request.body, credential_headers,
)).await?;
require_http_credential_location_rejection(&rejected, &probe.token)?;
rejected_locations += 1;
if probe.effects() != before {
return Err(format!("query credential invoked provider/middleware/handler: field={field}"));
}
}
}
}
let accepted = live_http_exchange(address, live_http_post(
"/mcp?application_context=permitted", &request.body, &headers,
)).await?;
if !accepted.starts_with(b"HTTP/1.1 200") {
return Err(format!("native header did not reach the {accept} handler"));
}
let responses = if accept == "application/json" {
vec![serde_json::from_slice::<JsonRpcResponse>(live_http_response_body(&accepted)?)
.map_err(|error| error.to_string())?]
} else {
live_http_chunked_sse_messages(&accepted)?.into_iter().filter_map(|message| {
if let JsonRpcMessage::Response(response) = message { Some(response) } else { None }
}).collect::<Vec<_>>()
};
if responses.len() != 1 || responses[0].id != Some(945_i64.into())
|| responses[0].error.is_some()
|| responses[0].result.as_ref().and_then(|result| result.pointer("/content/0/text")).and_then(serde_json::Value::as_str) != Some(probe.subject.as_str())
|| probe.effects() != (index + 1, index + 1, index + 1)
{
return Err(format!("native header lost its principal, response, or exactly-once effects: {accept}"));
}
eprintln!("{}", serde_json::json!({
"proof": "auth_01_http_header", "bead": "bd-f6apv",
"transport": accept, "listener": address.to_string(),
"runtime_subject": probe.subject, "request_id": 945,
"response_frame": String::from_utf8_lossy(&accepted),
"response_sha256": sha256_bounded(&accepted, 65_536).map_err(|error| error.to_string())?.as_bytes(),
"effects": probe.effects(), "responses": responses.len(),
"rejected_locations": rejected_locations,
"expected_rejections": if reject_forbidden_locations { 50 } else { 0 },
"finished_unix_ns": std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_err(|error| error.to_string())?.as_nanos(),
}));
}
Ok::<_, String>(())
}.await;
caller_cx.cancel_with(CancelKind::User, Some("AUTH-01 HTTP probe complete"));
result
}).map_err(|error| error.to_string())?;
let serve = bound.serve(&cx).await;
client
.join(&cx)
.await
.map_err(|error| format!("HTTP auth probe client failed: {error:?}"))??;
require_quiescent_http_shutdown(
serve.map_err(|error| error.to_string())?,
"AUTH-01 HTTP",
)
.await
});
}
#[test]
fn auth_01_http_header_only_positive() {
auth_01_http_header_probe(false);
}
#[test]
fn auth_01_http_body_query_planted_negative() {
auth_01_http_header_probe(true);
// Direct embedding retains raw query names too: the same encoded
// credential must be rejected before a session era or provider changes.
let cx = Cx::for_testing();
let probe = HttpHeaderAuthProbe::new();
let endpoint = Server::new("auth-01-direct-http", "1.0.0")
.auth_provider(probe.clone())
.middleware(probe.clone())
.tool(probe.clone())
.test_http_endpoint("http://auth.test")
.expect("endpoint must build");
let mut session = endpoint.open_session(&cx).expect("session must open");
for request in [
modern_http_json_tool_request("http_header_auth_probe", 945),
HttpRequest::new(HttpMethod::Get, "/sse"),
] {
let response = block_on(
session.handle_async(
&cx,
request
.with_header("Authorization", format!("Bearer {}", probe.token))
.with_query("%61ccess_token", &probe.token),
),
)
.expect("credential refusal must be an HTTP response");
assert!(
matches!(response, ServerHttpEndpointResponse::Immediate(response) if response.status == HttpStatus::UNAUTHORIZED)
);
assert_eq!(session.selected_era, None);
assert_eq!(probe.effects(), (0, 0, 0));
}
cx.cancel_with(CancelKind::User, Some("cancel before HTTP authentication"));
let response = block_on(
session.handle_async(
&cx,
modern_http_json_tool_request("http_header_auth_probe", 945)
.with_header("Authorization", format!("Bearer {}", probe.token)),
),
)
.expect("cancelled authentication must return an HTTP refusal");
assert!(
matches!(response, ServerHttpEndpointResponse::Immediate(response) if response.status == HttpStatus::UNAUTHORIZED)
);
assert_eq!(session.selected_era, None);
assert_eq!(probe.effects(), (0, 0, 0));
let expired =
Cx::for_testing_with_budget(Budget::new().with_deadline(asupersync::Time::ZERO));
let mut session = endpoint
.open_session(&Cx::for_testing())
.expect("deadline probe must start with a fresh live session");
let response = block_on(
session.handle_async(
&expired,
modern_http_json_tool_request("http_header_auth_probe", 945)
.with_header("Authorization", format!("Bearer {}", probe.token)),
),
)
.expect("expired authentication must return an HTTP refusal");
assert!(
matches!(response, ServerHttpEndpointResponse::Immediate(response) if response.status == HttpStatus::UNAUTHORIZED)
);
assert_eq!(session.selected_era, None);
assert_eq!(probe.effects(), (0, 0, 0));
#[cfg(feature = "legacy-2024-11-05")]
auth_01_legacy_receipt_keeps_request_zero_and_notification_distinct(&endpoint, &probe);
}
/// The provider's numeric context ID is not a wire identity: a
/// notification and request ID zero both map to zero. Receipt reuse must
/// preserve that distinction without another provider call.
// Exact-2024 era: the receipt is committed through the legacy path.
#[cfg(feature = "legacy-2024-11-05")]
fn auth_01_legacy_receipt_keeps_request_zero_and_notification_distinct(
endpoint: &ServerHttpEndpoint,
probe: &HttpHeaderAuthProbe,
) {
let cx = Cx::for_testing();
let authorization = transport_authorization_from_http_request(
&HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("Authorization", format!("Bearer {}", probe.token)),
)
.expect("native header must be captured");
let original = JsonRpcRequest::new("tools/list", None, 0_i64);
let receipt = endpoint
.server
.preauthenticate_http_request(&cx, &original, &authorization)
.expect("the original request must authenticate");
assert_eq!(probe.effects(), (1, 0, 0));
let changed = JsonRpcRequest::notification("tools/list", None);
let rejected_ctx = McpContext::new(cx.clone(), 0);
assert!(receipt.commit_legacy(&rejected_ctx, &changed).is_err());
assert!(rejected_ctx.auth().is_none());
assert_eq!(probe.effects(), (1, 0, 0));
let accepted_ctx = McpContext::new(cx, 0);
receipt
.commit_legacy(&accepted_ctx, &original)
.expect("the unchanged request must consume its authenticated receipt");
assert_eq!(
accepted_ctx.auth().and_then(|auth| auth.subject),
Some(probe.subject.clone()),
);
assert_eq!(probe.effects(), (1, 0, 0));
}
// Exact-2024 era: legacy HTTP SSE session registry.
#[cfg(feature = "legacy-2024-11-05")]
fn auth_01_http_legacy_cancellation_probe(reject_forbidden_locations: bool) {
run_live_http_test(move |cx| async move {
let probe = HttpHeaderAuthProbe::new();
let started = Arc::new(AtomicBool::new(false));
let observed_cancellation = Arc::new(AtomicBool::new(false));
let bound = Server::new("auth-01-legacy-cancellation", "1.0.0")
.auth_provider(probe.clone())
.middleware(probe.clone())
.tool(LiveLegacyCancellationTool {
started: Arc::clone(&started),
observed_cancellation: Arc::clone(&observed_cancellation),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| error.to_string())?;
let address = bound.local_addr().map_err(|error| error.to_string())?;
let legacy_sessions = Arc::clone(&bound.legacy_sessions);
let caller_cx = cx.clone();
let mut client = cx.spawn(move |client_cx| async move {
let result = async {
let authorization = format!("Bearer {}", probe.token);
let headers = [("Authorization", authorization.as_str())];
let (_sse, session_id, _received) = open_live_legacy_http_session(address, &headers).await?;
if probe.provider_calls.load(Ordering::Acquire) != 1 {
return Err("legacy SSE opener must authenticate exactly once".to_owned());
}
let path = format!("/messages?session_id={session_id}");
for (index, request) in [
JsonRpcRequest::new("initialize", Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "auth-01-legacy", "version": "1.0.0"},
})), 946_i64),
JsonRpcRequest::notification("notifications/initialized", None),
].into_iter().enumerate() {
let body = serde_json::to_vec(&request).map_err(|error| error.to_string())?;
let response = live_http_exchange(address, live_http_post(&path, &body, &headers)).await?;
if !response.starts_with(b"HTTP/1.1 202") {
return Err("header-authenticated legacy setup failed".to_owned());
}
if probe.provider_calls.load(Ordering::Acquire) != index + 2 {
return Err(format!("legacy {} must authenticate exactly once", request.method));
}
}
let target_id = RequestId::Number(947);
let target = JsonRpcRequest::new("tools/call", Some(serde_json::json!({
"name": "live_legacy_cancellation_tool", "arguments": {},
})), target_id.clone());
let target_body = serde_json::to_vec(&target).map_err(|error| error.to_string())?;
let target_request = live_http_post(&path, &target_body, &headers);
let mut target = client_cx.spawn(move |_| async move {
live_http_exchange(address, target_request).await
}).map_err(|error| error.to_string())?;
let deadline = client_cx.now().saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while !started.load(Ordering::Acquire) {
asupersync::time::timeout_at(deadline,
asupersync::time::sleep(client_cx.now(), Duration::from_millis(1)))
.await.map_err(|_| live_http_test_timeout("AUTH-01 active legacy handler"))?;
}
let session = legacy_sessions.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&session_id).cloned().ok_or_else(|| "legacy session missing".to_owned())?;
let cancellation = session.cancellation.admissions.admitted_request_cancellation(&target_id)
.ok_or_else(|| "active legacy cancellation authority missing".to_owned())?;
let notification = serde_json::json!({
"jsonrpc": "2.0", "method": "notifications/cancelled",
"params": {"requestId": target_id, "reason": "AUTH-01 header cancellation"},
});
let before = probe.effects();
if before.0 != 4 {
return Err("legacy tool must authenticate exactly once".to_owned());
}
let mut rejected_locations = 0;
if reject_forbidden_locations {
for container in [None, Some("_meta"), Some("headers")] {
let mut forbidden = notification.clone();
match container {
Some(container) => forbidden["params"][container]["token"] = serde_json::json!(probe.token),
None => forbidden["params"]["token"] = serde_json::json!(probe.token),
}
let body = serde_json::to_vec(&forbidden).map_err(|error| error.to_string())?;
for credential_headers in [&[][..], &headers[..]] {
let rejected = live_http_exchange(address, live_http_post(&path, &body, credential_headers)).await?;
require_http_credential_location_rejection(&rejected, &probe.token)?;
rejected_locations += 1;
if probe.effects() != before || cancellation.is_cancel_requested()
|| observed_cancellation.load(Ordering::Acquire) || !matches!(target.try_join(), Ok(None))
{
return Err("body credential changed provider or active cancellation state".to_owned());
}
}
}
let body = serde_json::to_vec(¬ification).map_err(|error| error.to_string())?;
let rejected = live_http_exchange(address, live_http_post(
&format!("{path}&%61ccess_token={}", probe.token), &body, &headers,
)).await?;
require_http_credential_location_rejection(&rejected, &probe.token)?;
rejected_locations += 1;
if probe.effects() != before || cancellation.is_cancel_requested()
|| observed_cancellation.load(Ordering::Acquire) || !matches!(target.try_join(), Ok(None))
{
return Err("query credential changed provider or active cancellation state".to_owned());
}
}
let body = serde_json::to_vec(¬ification).map_err(|error| error.to_string())?;
let accepted = live_http_exchange(address, live_http_post(&path, &body, &headers)).await?;
if !accepted.starts_with(b"HTTP/1.1 202") {
return Err("header-only cancellation was rejected".to_owned());
}
let response = target.join(&client_cx).await.map_err(|error| format!("legacy target failed: {error:?}"))??;
if !response.starts_with(b"HTTP/1.1 202") || !observed_cancellation.load(Ordering::Acquire)
|| !cancellation.is_cancel_requested() || probe.effects() != (before.0 + 1, before.1, before.2)
{
return Err("header-only cancellation lost its exactly-once authenticated effect".to_owned());
}
eprintln!("{}", serde_json::json!({
"proof": "auth_01_http_legacy_cancellation", "bead": "bd-f6apv",
"listener": address.to_string(), "runtime_subject": probe.subject,
"request_id": target_id, "session_id": session_id,
"response_frame": String::from_utf8_lossy(&accepted),
"response_sha256": sha256_bounded(&accepted, 65_536).map_err(|error| error.to_string())?.as_bytes(),
"effects_before": before, "effects_after": probe.effects(),
"handler_observed_cancellation": observed_cancellation.load(Ordering::Acquire),
"rejected_locations": rejected_locations,
"expected_rejections": if reject_forbidden_locations { 7 } else { 0 },
"finished_unix_ns": std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_err(|error| error.to_string())?.as_nanos(),
}));
Ok::<_, String>(())
}.await;
caller_cx.cancel_with(CancelKind::User, Some("AUTH-01 legacy cancellation probe complete"));
result
}).map_err(|error| error.to_string())?;
let serve = bound.serve(&cx).await;
client
.join(&cx)
.await
.map_err(|error| format!("legacy auth client failed: {error:?}"))??;
require_quiescent_http_shutdown(
serve.map_err(|error| error.to_string())?,
"AUTH-01 legacy cancellation",
)
.await
});
}
// Exact-2024 era: legacy HTTP SSE session registry.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn auth_01_http_legacy_cancellation_header_positive() {
auth_01_http_legacy_cancellation_probe(false);
}
// Exact-2024 era: legacy HTTP SSE session registry.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn auth_01_http_legacy_cancellation_body_planted_negative() {
auth_01_http_legacy_cancellation_probe(true);
}
#[test]
fn admitted_raw_params_are_withheld_after_sanitation_changes_the_request() {
// RH-5 neighbor of the retained-source positive: a byte-exact raw
// sidecar is legal only while the request still carries the value it
// was admitted with. Credential stripping changes that request, so
// continuation routing must receive no stale source.
let raw = r#"{"authorization":"Bearer never-forward","requestState":"state"}"#;
let mut request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::from_str(raw).expect("raw sidecar fixture must decode")),
937_i64,
);
let admitted = request.params.clone();
assert_eq!(
retained_raw_params(Some(raw), admitted.as_ref(), request.params.as_ref()),
Some(raw)
);
auth::strip_recognized_access_credentials(&mut request.params);
assert_eq!(
retained_raw_params(Some(raw), admitted.as_ref(), request.params.as_ref()),
None
);
assert_eq!(
request.params,
Some(serde_json::json!({"requestState": "state"})),
"stale raw parameters must be withheld without mutating the sanitized request",
);
}
#[cfg(feature = "websocket")]
#[test]
fn websocket_auth_custody_rejects_a_stale_connection_generation_before_dispatch() {
let cx = Cx::for_testing();
let custody = AuthDispatchCustody::WebSocket(Arc::new(WebSocketAuthCustody {
connection_generation: 44,
fingerprint: auth::principal_fingerprint(None).expect("anonymous digest must exist"),
authenticated: None,
rejected_inband_request_ids: Mutex::new(HashSet::new()),
}));
let inbound = InboundRequestContext::new(cx.clone(), 939, InboundRequestTransport::Memory);
let mut request = JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({
"_meta": {MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION},
})),
939_i64,
);
let error = custody
.commit(&McpContext::new(cx, 939), &inbound, &mut request, Some(45))
.expect_err("a stale WebSocket custody must not cross a connection generation");
assert_eq!(error.code, McpErrorCode::ResourceForbidden);
assert_eq!(request.method, "tools/list");
assert!(request.params.is_some());
}
#[test]
fn live_http_json_auth_receipt_commits_one_accepted_provider_result() {
run_live_http_test(|cx| async move {
let provider_calls = Arc::new(AtomicUsize::new(0));
let handler_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let saw_credential = Arc::new(AtomicBool::new(false));
let bound = Server::new("live-http-json-auth-receipt", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(OneShotNativeAuthProvider {
calls: Arc::clone(&provider_calls),
})
.middleware(ModernHttpAuthMiddleware {
calls: Arc::clone(&middleware_calls),
saw_credential: Arc::clone(&saw_credential),
})
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&handler_calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("JSON auth receipt bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("JSON auth receipt address failed: {error}"))?;
let request = modern_http_json_tool_request("modern_http_auth_counter", 940);
let mixed_body = serde_json::to_vec(&JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"authorization": "Bearer in-band-never-echo",
"name": "modern_http_auth_counter",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
941_i64,
))
.map_err(|error| format!("mixed JSON auth receipt did not serialize: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let headers = [
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
];
let result = async {
// This is the first provider invocation and the only
// invocation allowed to create an accepted receipt.
let accepted = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Authorization", "Bearer alpha"),
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
let invalid = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Authorization", "Bearer beta-never-echo"),
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
let missing = live_http_exchange(
address,
live_http_post("/mcp", &request.body, &headers),
)
.await?;
let duplicate = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Authorization", "Bearer alpha"),
("aUtHoRiZaTiOn", "Bearer beta"),
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
let mixed = live_http_exchange(
address,
live_http_post(
"/mcp",
&mixed_body,
&[
("Authorization", "Bearer alpha"),
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
Ok::<_, String>((accepted, invalid, missing, duplicate, mixed))
}
.await;
caller_cx
.cancel_with(CancelKind::User, Some("JSON auth receipt proof complete"));
result
})
.map_err(|error| format!("JSON auth receipt client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let (accepted, invalid, missing, duplicate, mixed) = client
.join(&cx)
.await
.map_err(|error| format!("JSON auth receipt client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("JSON auth receipt server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "JSON auth receipt").await?;
let accepted: JsonRpcResponse =
serde_json::from_slice(live_http_response_body(&accepted)?).map_err(|error| {
format!("accepted JSON auth receipt was not JSON-RPC: {error}")
})?;
if accepted.error.is_some()
|| provider_calls.load(Ordering::Acquire) != 2
|| handler_calls.load(Ordering::Acquire) != 1
|| middleware_calls.load(Ordering::Acquire) != 1
|| saw_credential.load(Ordering::Acquire)
{
return Err(
"JSON auth receipt expected two provider attempts and one sanitized dispatch"
.to_owned(),
);
}
for response in [invalid, missing, duplicate, mixed] {
if !response.starts_with(b"HTTP/1.1 401")
|| live_http_response_header(&response, "www-authenticate")? != "Bearer"
|| String::from_utf8_lossy(&response).contains("beta-never-echo")
|| String::from_utf8_lossy(&response).contains("in-band-never-echo")
{
return Err(
"denied JSON auth receipt was not bounded before dispatch".to_owned()
);
}
}
Ok(())
});
}
#[test]
fn live_http_sse_auth_receipt_commits_one_accepted_provider_result_and_fences_denials() {
run_live_http_test(|cx| async move {
let provider_calls = Arc::new(AtomicUsize::new(0));
let handler_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let saw_credential = Arc::new(AtomicBool::new(false));
let bound = Server::new("live-http-auth-receipt", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(OneShotNativeAuthProvider {
calls: Arc::clone(&provider_calls),
})
.middleware(ModernHttpAuthMiddleware {
calls: Arc::clone(&middleware_calls),
saw_credential: Arc::clone(&saw_credential),
})
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&handler_calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("auth receipt bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("auth receipt address failed: {error}"))?;
let request = modern_http_json_tool_request("modern_http_auth_counter", 938);
let mixed_body = serde_json::to_vec(&JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"authorization": "Bearer in-band-never-echo",
"name": "modern_http_auth_counter",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
942_i64,
))
.map_err(|error| format!("mixed SSE auth receipt did not serialize: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let headers = [
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
];
let result = async {
let accepted = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Authorization", "Bearer alpha"),
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
let invalid = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Authorization", "Bearer beta-never-echo"),
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
let missing = live_http_exchange(
address,
live_http_post("/mcp", &request.body, &headers),
)
.await?;
let duplicate = live_http_exchange(
address,
live_http_post(
"/mcp",
&request.body,
&[
("Authorization", "Bearer alpha"),
("AUTHORIZATION", "Bearer beta"),
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
let mixed = live_http_exchange(
address,
live_http_post(
"/mcp",
&mixed_body,
&[
("Authorization", "Bearer alpha"),
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "modern_http_auth_counter"),
],
),
)
.await?;
Ok::<_, String>((accepted, invalid, missing, duplicate, mixed))
}
.await;
caller_cx.cancel_with(CancelKind::User, Some("auth receipt proof complete"));
result
})
.map_err(|error| format!("auth receipt client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let (accepted, invalid, missing, duplicate, mixed) = client
.join(&cx)
.await
.map_err(|error| format!("auth receipt client failed: {error:?}"))??;
let shutdown = serve.map_err(|error| format!("auth receipt server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "auth receipt").await?;
let accepted_event = live_http_sse_jsonrpc_response(&accepted)?;
if !accepted.starts_with(b"HTTP/1.1 200")
|| live_http_response_header(&accepted, "content-type")? != "text/event-stream"
|| accepted_event.error.is_some()
|| provider_calls.load(Ordering::Acquire) != 2
|| handler_calls.load(Ordering::Acquire) != 1
|| middleware_calls.load(Ordering::Acquire) != 1
|| saw_credential.load(Ordering::Acquire)
{
return Err(
"SSE auth receipt did not record exactly two provider attempts and one dispatch"
.to_owned(),
);
}
for response in [invalid, missing, duplicate, mixed] {
if !response.starts_with(b"HTTP/1.1 401")
|| live_http_response_header(&response, "www-authenticate")? != "Bearer"
|| String::from_utf8_lossy(&response).contains("beta-never-echo")
|| String::from_utf8_lossy(&response).contains("in-band-never-echo")
{
return Err(
"denied native auth mutated dispatch or exposed a bearer".to_owned()
);
}
}
Ok(())
});
}
#[test]
fn live_http_anonymous_stateless_posts_isolate_disabled_components() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-http-anonymous-disablement", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.tool(HttpSessionDisablingTool {
calls: Arc::clone(&calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live HTTP anonymous disablement bind failed: {error}"))?;
let address = bound.local_addr().map_err(|error| {
format!("live HTTP anonymous disablement address failed: {error}")
})?;
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_session_disabling_tool",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
924_i64,
);
let body = serde_json::to_vec(&request).map_err(|error| {
format!("anonymous disablement request did not serialize: {error}")
})?;
let request = live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "http_session_disabling_tool"),
],
);
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let result = async {
let first = live_http_exchange(address, request.clone()).await?;
let second = live_http_exchange(address, request).await?;
Ok::<_, String>((first, second))
}
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("live HTTP anonymous disablement proof complete"),
);
result
})
.map_err(|error| {
format!("live HTTP anonymous disablement client admission failed: {error}")
})?;
let serve = bound.serve(&cx).await;
let (first, second) = client.join(&cx).await.map_err(|error| {
format!("live HTTP anonymous disablement client failed: {error:?}")
})??;
let shutdown = serve.map_err(|error| {
format!("live HTTP anonymous disablement server failed: {error}")
})?;
require_quiescent_http_shutdown(shutdown, "live HTTP anonymous disablement").await?;
for response in [first, second] {
let response: JsonRpcResponse = serde_json::from_slice(live_http_response_body(
&response,
)?)
.map_err(|error| {
format!("anonymous disablement response was not valid JSON-RPC: {error}")
})?;
if response.id != Some(924_i64.into()) || response.error.is_some() {
return Err(format!(
"an anonymous stateless POST inherited disabled-component state: {response:?}"
));
}
}
if calls.load(Ordering::Acquire) != 2 {
return Err(
"identical anonymous stateless POSTs did not each enter the disabling handler"
.to_owned(),
);
}
Ok(())
});
}
#[test]
fn live_http_public_final_sampling_json_returns_input_required() {
run_live_http_test(|cx| async move {
let (response, calls) = run_live_http_public_final_sampling_json(&cx).await?;
if response.id != Some(940_i64.into()) {
return Err(format!(
"public final sampling JSON response id was unexpected: {response:?}"
));
}
if let Some(error) = response.error.as_ref() {
return Err(format!(
"public final sampling JSON POST was cancelled or rejected: {error:?}"
));
}
if response
.result
.as_ref()
.and_then(|result| result.get("resultType"))
!= Some(&serde_json::json!("input_required"))
{
return Err(format!(
"public final sampling JSON POST did not yield input_required: {response:?}"
));
}
if response
.result
.as_ref()
.and_then(|result| result.pointer("/inputRequests/sample/method"))
!= Some(&serde_json::json!("sampling/createMessage"))
{
return Err(format!(
"public final sampling JSON POST omitted sampling/createMessage: {response:?}"
));
}
if calls != 1 {
return Err(format!(
"public final sampling handler ran {calls} times instead of once"
));
}
Ok(())
});
}
#[test]
fn live_http_mrtr_resumes_server_issued_state_across_separate_stateless_posts() {
run_live_http_test(|cx| async move {
let (initial, retry, calls) = run_live_http_mrtr_retry(&cx, "").await?;
if initial.id != Some(922_i64.into())
|| initial.error.is_some()
|| initial
.result
.as_ref()
.and_then(|result| result.get("resultType"))
!= Some(&serde_json::json!("input_required"))
{
return Err(format!(
"initial stateless MRTR POST did not yield input_required: {initial:?}"
));
}
if retry.id != Some(923_i64.into())
|| retry.error.is_some()
|| retry
.result
.as_ref()
.and_then(|result| result.get("resultType"))
!= Some(&serde_json::json!("complete"))
|| calls != 2
{
return Err(format!(
"an owner-bound stateless retry did not resume exactly once (retry={retry:?}, calls={calls})"
));
}
Ok(())
});
}
#[test]
fn live_http_mrtr_rejects_foreign_state_across_separate_stateless_posts() {
run_live_http_test(|cx| async move {
// This is the identical cross-POST retry flow above, with only
// the server-issued requestState changed before the retry.
let (initial, retry, calls) = run_live_http_mrtr_retry(&cx, "-foreign").await?;
if initial.id != Some(922_i64.into())
|| initial.error.is_some()
|| initial
.result
.as_ref()
.and_then(|result| result.get("resultType"))
!= Some(&serde_json::json!("input_required"))
{
return Err(format!(
"foreign-state test initial POST did not yield input_required: {initial:?}"
));
}
if retry.id != Some(923_i64.into()) || retry.error.is_none() || calls != 1 {
return Err(format!(
"changing only requestState resumed or reentered the handler (retry={retry:?}, calls={calls})"
));
}
Ok(())
});
}
#[test]
fn live_http_mrtr_wrong_kind_rejects_without_consuming_stateless_state() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-http-mrtr-wrong-kind", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(auth_00_mrtr_provider(true))
.tool(LiveHttpMrtrTool {
name: "live_http_mrtr",
calls: Arc::clone(&calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live HTTP MRTR wrong-kind bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live HTTP MRTR wrong-kind address failed: {error}"))?;
let caller_cx = cx.clone();
let client_calls = Arc::clone(&calls);
let mut client = cx
.spawn(move |_client_cx| async move {
let result = async {
let initial = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_http_mrtr",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "live-http-mrtr-wrong-kind",
},
})),
926_i64,
);
let initial_body = serde_json::to_vec(&initial).map_err(|error| {
format!("MRTR wrong-kind initial request did not serialize: {error}")
})?;
let initial = live_http_exchange(
address,
live_http_post(
"/mcp",
&initial_body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_http_mrtr"),
("Authorization", "Bearer alpha"),
],
),
)
.await?;
let initial = live_http_sse_jsonrpc_response(&initial)?;
let request_state = initial
.result
.as_ref()
.and_then(|result| result.get("requestState"))
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
"MRTR wrong-kind initial response omitted requestState".to_owned()
})?
.to_owned();
let wrong_kind = serde_json::to_value(
bidirectional::MrtrInputResponse::sampling(
serde_json::from_value(serde_json::json!({
"content": {
"type": "text",
"text": "not a roots result",
},
"role": "assistant",
"model": "test-model",
}))
.map_err(|error| {
format!(
"MRTR wrong-kind final sampling response failed to decode: {error}"
)
})?,
)
.map_err(|error| {
format!("MRTR wrong-kind sampling response failed: {error}")
})?,
)
.map_err(|error| {
format!("MRTR wrong-kind sampling response did not serialize: {error}")
})?;
let wrong_kind = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_http_mrtr",
"arguments": {},
"inputResponses": {"roots": wrong_kind},
"requestState": request_state.clone(),
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
927_i64,
);
let wrong_kind_body = serde_json::to_vec(&wrong_kind).map_err(|error| {
format!("MRTR wrong-kind retry did not serialize: {error}")
})?;
let wrong_kind = live_http_exchange(
address,
live_http_post(
"/mcp",
&wrong_kind_body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_http_mrtr"),
("Authorization", "Bearer alpha"),
],
),
)
.await?;
let wrong_kind: JsonRpcResponse = serde_json::from_slice(
live_http_response_body(&wrong_kind)?,
)
.map_err(|error| {
format!(
"MRTR wrong-kind response was not valid JSON-RPC: {error}; raw response={}",
String::from_utf8_lossy(&wrong_kind),
)
})?;
let calls_after_wrong_kind = client_calls.load(Ordering::Acquire);
let roots = serde_json::to_value(
bidirectional::MrtrInputResponse::roots(
fastmcp_protocol::ListRootsResult::empty(),
)
.map_err(|error| {
format!("MRTR valid roots response failed: {error}")
})?,
)
.map_err(|error| {
format!("MRTR valid roots response did not serialize: {error}")
})?;
let valid = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_http_mrtr",
"arguments": {},
"inputResponses": {"roots": roots},
"requestState": request_state,
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
928_i64,
);
let valid_body = serde_json::to_vec(&valid).map_err(|error| {
format!("MRTR valid retry did not serialize: {error}")
})?;
let valid = live_http_exchange(
address,
live_http_post(
"/mcp",
&valid_body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_http_mrtr"),
("Authorization", "Bearer alpha"),
],
),
)
.await?;
let valid: JsonRpcResponse = serde_json::from_slice(
live_http_response_body(&valid)?,
)
.map_err(|error| {
format!("MRTR valid response was not valid JSON-RPC: {error}")
})?;
Ok::<_, String>((initial, wrong_kind, calls_after_wrong_kind, valid))
}
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("live HTTP MRTR wrong-kind retry complete"),
);
result
})
.map_err(|error| {
format!("live HTTP MRTR wrong-kind client admission failed: {error}")
})?;
let serve = bound.serve(&cx).await;
let (initial, wrong_kind, calls_after_wrong_kind, valid) = client
.join(&cx)
.await
.map_err(|error| format!("live HTTP MRTR wrong-kind client failed: {error:?}"))??;
let shutdown = serve
.map_err(|error| format!("live HTTP MRTR wrong-kind server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live HTTP MRTR wrong-kind").await?;
if initial.id != Some(926_i64.into())
|| initial.error.is_some()
|| initial
.result
.as_ref()
.and_then(|result| result.get("resultType"))
!= Some(&serde_json::json!("input_required"))
{
return Err(format!(
"MRTR wrong-kind initial POST did not yield input_required: {initial:?}"
));
}
if wrong_kind.id != Some(927_i64.into()) || wrong_kind.error.is_none() {
return Err(format!(
"wrong-kind inputResponses did not yield a JSON-RPC error: {wrong_kind:?}"
));
}
if calls_after_wrong_kind != 1 {
return Err(format!(
"wrong-kind inputResponses reentered the handler before the valid retry (calls={calls_after_wrong_kind})"
));
}
if valid.id != Some(928_i64.into())
|| valid.error.is_some()
|| valid
.result
.as_ref()
.and_then(|result| result.get("resultType"))
!= Some(&serde_json::json!("complete"))
|| calls.load(Ordering::Acquire) != 2
{
return Err(format!(
"wrong-kind rejection consumed state or valid retry did not resume exactly once (valid={valid:?}, calls={})",
calls.load(Ordering::Acquire),
));
}
Ok(())
});
}
#[test]
fn live_http_listener_shutdown_invalidates_mrtr_before_a_later_stateless_post() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-http-mrtr-shutdown", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(auth_00_mrtr_provider(true))
.tool(LiveHttpMrtrTool {
name: "live_http_mrtr",
calls: Arc::clone(&calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live HTTP MRTR shutdown bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live HTTP MRTR shutdown address failed: {error}"))?;
let endpoint = Arc::clone(&bound.endpoint);
let modern_sessions = Arc::clone(&bound.modern_sessions);
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let result = async {
let initial = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_http_mrtr",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "live-http-mrtr-shutdown",
},
})),
932_i64,
);
let body = serde_json::to_vec(&initial).map_err(|error| {
format!("MRTR shutdown initial request did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(
"/mcp",
&body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_http_mrtr"),
("Authorization", "Bearer alpha"),
],
),
)
.await?;
live_http_sse_jsonrpc_response(&response)
}
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("live HTTP MRTR shutdown after input_required"),
);
result
})
.map_err(|error| {
format!("live HTTP MRTR shutdown client admission failed: {error}")
})?;
let serve = bound.serve(&cx).await;
let initial = client
.join(&cx)
.await
.map_err(|error| format!("live HTTP MRTR shutdown client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("live HTTP MRTR shutdown server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live HTTP MRTR shutdown").await?;
if endpoint.server.router.test_active_mrtr_exchange_count() != 0 {
return Err("listener shutdown retained a stateless MRTR continuation".to_owned());
}
let request_state = initial
.result
.as_ref()
.and_then(|result| result.get("requestState"))
.and_then(serde_json::Value::as_str)
.ok_or_else(|| "MRTR shutdown initial response omitted requestState".to_owned())?;
let roots = serde_json::to_value(
bidirectional::MrtrInputResponse::roots(fastmcp_protocol::ListRootsResult::empty())
.map_err(|error| format!("MRTR shutdown roots response failed: {error}"))?,
)
.map_err(|error| format!("MRTR shutdown roots response did not serialize: {error}"))?;
let retry = HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/call")
.with_header("authorization", "Bearer alpha")
.with_body(
serde_json::to_vec(&JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_http_mrtr",
"arguments": {},
"inputResponses": {"roots": roots},
"requestState": request_state,
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
933_i64,
))
.map_err(|error| format!("MRTR shutdown retry did not serialize: {error}"))?,
);
let retry = dispatch_modern_http_request(
&Cx::for_testing(),
&endpoint,
&modern_sessions,
retry,
)
.await;
let retry: JsonRpcResponse = serde_json::from_slice(&retry.body)
.map_err(|error| format!("MRTR shutdown retry was not valid JSON-RPC: {error}"))?;
if retry.id != Some(933_i64.into())
|| retry.error.is_none()
|| calls.load(Ordering::Acquire) != 1
{
return Err(format!(
"listener shutdown did not invalidate the later stateless MRTR retry (retry={retry:?}, calls={})",
calls.load(Ordering::Acquire),
));
}
Ok(())
});
}
#[test]
fn live_http_modern_sse_rejects_only_session_id_before_handler() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-http-stateless-sse-reject", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(auth_00_mrtr_provider(true))
.tool(LiveHttpMrtrTool {
name: "live_http_mrtr",
calls: Arc::clone(&calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live HTTP MRTR SSE bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live HTTP MRTR SSE address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let initial = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_http_mrtr",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "live-http-mrtr-sse",
},
})),
925_i64,
);
let initial_body = serde_json::to_vec(&initial).map_err(|error| {
format!("MRTR SSE initial request did not serialize: {error}")
})?;
let rejected = live_http_exchange(
address,
live_http_post(
"/mcp",
&initial_body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_http_mrtr"),
("Authorization", "Bearer alpha"),
("MCP-Session-Id", "obsolete-modern-session"),
],
),
)
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("live HTTP stateless SSE session-header rejection complete"),
);
rejected
})
.map_err(|error| format!("live HTTP MRTR SSE client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let rejected = client
.join(&cx)
.await
.map_err(|error| format!("live HTTP MRTR SSE client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("live HTTP MRTR SSE server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live HTTP MRTR SSE").await?;
if !rejected.starts_with(b"HTTP/1.1 400") || calls.load(Ordering::Acquire) != 0 {
return Err("adding only MCP-Session-Id admitted a modern SSE handler".to_owned());
}
Ok(())
});
}
#[test]
fn live_http_modern_discovery_does_not_retain_a_session_after_shutdown() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-stateless-discovery", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live HTTP stateless discovery bind failed: {error}"))?;
let address = bound.local_addr().map_err(|error| {
format!("live HTTP stateless discovery address failed: {error}")
})?;
let modern_sessions = Arc::clone(&bound.modern_sessions);
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let discovery = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
931_i64,
);
let discovery_body = serde_json::to_vec(&discovery).map_err(|error| {
format!("stateless discovery did not serialize: {error}")
})?;
let discovery = live_http_exchange(
address,
live_http_post(
"/mcp",
&discovery_body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SERVER_DISCOVER_METHOD),
],
),
)
.await?;
caller_cx.cancel_with(
CancelKind::User,
Some("live HTTP stateless discovery complete"),
);
Ok::<_, String>(discovery)
})
.map_err(|error| {
format!("live HTTP stateless discovery client admission failed: {error}")
})?;
let serve = bound.serve(&cx).await;
let response = client.join(&cx).await.map_err(|error| {
format!("live HTTP stateless discovery client failed: {error:?}")
})??;
let shutdown = serve
.map_err(|error| format!("live HTTP stateless discovery server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live HTTP stateless discovery").await?;
if !response.starts_with(b"HTTP/1.1 200")
|| live_http_response_header(&response, "mcp-session-id").is_ok()
{
return Err("modern discovery issued a session identifier".to_owned());
}
if !modern_sessions
.sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
{
return Err("listener shutdown retained a modern HTTP session".to_owned());
}
Ok(())
});
}
#[test]
fn live_http_protocol_admission_preserves_jsonrpc_when_only_version_header_differs() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-protocol-admission", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("protocol-admission HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("protocol-admission HTTP address failed: {error}"))?;
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
814_i64,
);
let body = serde_json::to_vec(&request).map_err(|error| {
format!("protocol-admission request did not serialize: {error}")
})?;
let accepted = live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SERVER_DISCOVER_METHOD),
],
);
// This has the same JSON-RPC body and all the same HTTP headers
// except the final protocol-version mirror.
let rejected = live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", "2025-11-25"),
("Mcp-Method", SERVER_DISCOVER_METHOD),
],
);
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let result = async {
let accepted = live_http_exchange(address, accepted).await?;
let rejected = live_http_exchange(address, rejected).await?;
Ok::<_, String>((accepted, rejected))
}
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("protocol-admission loopback complete"),
);
result
})
.map_err(|error| format!("protocol-admission client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let (accepted, rejected) = client
.join(&cx)
.await
.map_err(|error| format!("protocol-admission client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("protocol-admission server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "protocol admission").await?;
if !accepted.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"matching final protocol-version request was not accepted: {accepted:?}"
));
}
let accepted: JsonRpcResponse =
serde_json::from_slice(live_http_response_body(&accepted)?).map_err(|error| {
format!("accepted protocol-admission response was invalid: {error}")
})?;
if accepted.id != Some(814_i64.into()) || accepted.error.is_some() {
return Err(format!(
"matching final protocol-version request did not dispatch: {accepted:?}"
));
}
if !rejected.starts_with(b"HTTP/1.1 400") {
return Err(format!(
"mismatched final protocol-version request did not return HTTP 400: {rejected:?}"
));
}
let rejected: JsonRpcResponse =
serde_json::from_slice(live_http_response_body(&rejected)?).map_err(|error| {
format!("rejected protocol-admission response was invalid: {error}")
})?;
if rejected.id != Some(814_i64.into())
|| rejected.result.is_some()
|| rejected
.error
.as_ref()
.and_then(|error| error.code.as_i32())
!= Some(fastmcp_protocol::HEADER_MISMATCH_ERROR_CODE)
|| rejected
.error
.as_ref()
.and_then(|error| error.data.as_ref())
.is_some()
{
return Err(format!(
"mismatched final protocol-version request did not retain its canonical JSON-RPC error: {rejected:?}"
));
}
Ok(())
});
}
#[test]
fn live_http_dual_accept_sse_flushes_progress_log_and_terminal_once() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-modern-sse-frames", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.log_level(Level::Debug)
.tool(HttpRequestScopedProgressTool {
calls: Arc::clone(&calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live modern SSE bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live modern SSE address failed: {error}"))?;
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_request_scoped_progress",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "live-http-progress",
"io.modelcontextprotocol/logLevel": "info",
},
})),
813_i64,
);
let body = serde_json::to_vec(&request)
.map_err(|error| format!("live SSE request did not serialize: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let request = live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json, text/event-stream; Q=1."),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "http_request_scoped_progress"),
],
);
let deadline = caller_cx
.now()
.saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut stream =
asupersync::time::timeout_at(deadline, AsyncTcpStream::connect(address))
.await
.map_err(|_| {
live_http_test_timeout("live modern SSE client connection")
})?
.map_err(|error| format!("live modern SSE connect failed: {error}"))?;
asupersync::time::timeout_at(deadline, stream.write_all(&request))
.await
.map_err(|_| live_http_test_timeout("live modern SSE request write"))?
.map_err(|error| format!("live modern SSE write failed: {error}"))?;
asupersync::time::timeout_at(deadline, stream.flush())
.await
.map_err(|_| live_http_test_timeout("live modern SSE request flush"))?
.map_err(|error| format!("live modern SSE flush failed: {error}"))?;
let mut received = Vec::new();
read_live_http_until(&mut stream, &mut received, b"notifications/progress")
.await?;
read_live_http_until(&mut stream, &mut received, b"notifications/message")
.await?;
read_live_http_until(&mut stream, &mut received, b"\"id\":813").await?;
read_live_http_to_end(
&mut stream,
&mut received,
"live modern SSE terminal EOF",
)
.await?;
caller_cx.cancel_with(CancelKind::User, Some("modern SSE stream complete"));
Ok::<_, String>(received)
})
.map_err(|error| format!("live modern SSE client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let response = client
.join(&cx)
.await
.map_err(|error| format!("live modern SSE client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("live modern SSE server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live modern SSE").await?;
let response_text = std::str::from_utf8(&response)
.map_err(|error| format!("live modern SSE response was not UTF-8: {error}"))?;
if !response_text.starts_with("HTTP/1.1 200")
|| !response_text
.to_ascii_lowercase()
.contains("content-type: text/event-stream")
{
return Err(format!(
"live modern SSE response head was unexpected: {response_text}"
));
}
let messages = live_http_chunked_sse_messages(&response)?;
let [progress, log, terminal] = messages.as_slice() else {
return Err(format!(
"live modern SSE emitted {} frames instead of progress, log, and response",
messages.len()
));
};
let JsonRpcMessage::Request(progress) = progress else {
return Err("first live SSE frame was not a notification".to_owned());
};
let ServerNotification::Progress(progress) = ServerNotification::decode(progress)
.map_err(|error| format!("first live SSE frame was invalid: {error}"))?
else {
return Err("first live SSE frame was not notifications/progress".to_owned());
};
if progress.progress_token != ProgressMarker::String("live-http-progress".to_owned())
|| progress.progress.as_str() != "12000"
|| progress.total.as_ref().map(|total| total.as_str()) != Some("11999")
|| progress.message.as_deref() != Some("latest")
{
return Err(format!(
"live SSE progress frame was not the coalesced update: {progress:?}"
));
}
let JsonRpcMessage::Request(log) = log else {
return Err("second live SSE frame was not a notification".to_owned());
};
if !matches!(
ServerNotification::decode(log),
Ok(ServerNotification::Message(_))
) {
return Err("second live SSE frame was not notifications/message".to_owned());
}
if !matches!(
terminal,
JsonRpcMessage::Response(response)
if response.id == Some(813_i64.into()) && response.error.is_none()
) {
return Err("third live SSE frame was not the one terminal response".to_owned());
}
if calls.load(Ordering::Acquire) != 1 {
return Err("live modern SSE handler did not execute exactly once".to_owned());
}
Ok(())
});
}
#[test]
fn live_http_sse_handler_admission_preserves_parameter_header_rejection() {
#[derive(Clone)]
struct HeaderAdmissionProbe {
middleware_calls: Arc<AtomicUsize>,
handler_calls: Arc<AtomicUsize>,
}
impl Middleware for HeaderAdmissionProbe {
fn on_request(
&self,
ctx: &McpContext,
_request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
self.middleware_calls.fetch_add(1, Ordering::AcqRel);
ctx.report_progress(0.25, Some("before parameter-header admission"));
Ok(MiddlewareDecision::Continue)
}
}
impl ToolHandler for HeaderAdmissionProbe {
fn definition(&self) -> Tool {
Tool {
name: "http_header_admission_probe".to_owned(),
description: None,
input_schema: serde_json::json!({
"type": "object",
"properties": {
"region": {"type": "string", "x-mcp-header": "Region"}
},
"required": ["region"]
}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
// The annotated `region` is what this test exists to exercise, so
// the projection must be admitted rather than dropped. Derive the
// review from this handler's own schema: a revision hard-coded
// here would go stale the next time the schema is touched.
fn header_exposure_reviews(
&self,
) -> Vec<fastmcp_protocol::http_headers::NonSensitiveHeaderExposure> {
let definition = self.definition();
let revision = fastmcp_protocol::http_headers::ToolSchemaRevision::of(
&definition.input_schema,
)
.expect("the probe's bounded input schema has a revision");
vec![
fastmcp_protocol::http_headers::NonSensitiveHeaderExposure::new(
definition.name,
revision,
["region"],
),
]
}
fn call(
&self,
_ctx: &McpContext,
_arguments: serde_json::Value,
) -> McpResult<Vec<Content>> {
self.handler_calls.fetch_add(1, Ordering::AcqRel);
Ok(vec![Content::text("admitted header")])
}
}
run_live_http_test(|cx| async move {
let probe = HeaderAdmissionProbe {
middleware_calls: Arc::new(AtomicUsize::new(0)),
handler_calls: Arc::new(AtomicUsize::new(0)),
};
let bound = Server::new("live-http-handler-admission", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.middleware(probe.clone())
.tool(probe.clone())
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("handler admission bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("handler admission address failed: {error}"))?;
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_header_admission_probe",
"arguments": {"region": "west"},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "header-admission-progress",
},
})),
839_i64,
);
let body = serde_json::to_vec(&request)
.map_err(|error| format!("handler admission request failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let result = async {
let mut responses = Vec::new();
for region in ["west", "east"] {
responses.push(
live_http_exchange(
address,
live_http_post(
"/mcp",
&body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "http_header_admission_probe"),
("Mcp-Param-Region", region),
],
),
)
.await?,
);
}
Ok::<_, String>(responses)
}
.await;
caller_cx
.cancel_with(CancelKind::User, Some("handler admission proof complete"));
result
})
.map_err(|error| format!("handler admission client spawn failed: {error}"))?;
let serve = bound.serve(&cx).await;
let responses = client
.join(&cx)
.await
.map_err(|error| format!("handler admission client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("handler admission server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "handler admission").await?;
let [accepted, rejected] = responses.as_slice() else {
return Err("the paired handler admission responses are missing".to_owned());
};
if !accepted.starts_with(b"HTTP/1.1 200")
|| live_http_response_header(accepted, "content-type")? != "text/event-stream"
{
return Err("matching parameter header did not admit SSE".to_owned());
}
let messages = live_http_chunked_sse_messages(accepted)?;
if !messages.iter().any(|message| matches!(message,
JsonRpcMessage::Request(notification) if notification.method == "notifications/progress"))
|| !messages.iter().any(|message| matches!(message,
JsonRpcMessage::Response(response) if response.id == Some(839_i64.into()) && response.error.is_none()))
{
return Err("admitted tool lost middleware progress or its terminal result".to_owned());
}
if !rejected.starts_with(b"HTTP/1.1 400")
|| live_http_response_header(rejected, "content-type")? != "application/json"
|| rejected
.windows(b"event: message".len())
.any(|window| window == b"event: message")
{
return Err(
"mismatched parameter header committed SSE before canonical JSON400".to_owned(),
);
}
let rejected: JsonRpcResponse =
serde_json::from_slice(live_http_response_body(rejected)?)
.map_err(|error| format!("header rejection was not JSON-RPC: {error}"))?;
if rejected.id != Some(839_i64.into())
|| rejected
.error
.as_ref()
.and_then(|error| error.code.as_i32())
!= Some(fastmcp_protocol::HEADER_MISMATCH_ERROR_CODE)
|| rejected
.error
.as_ref()
.and_then(|error| error.data.as_ref())
.is_some()
|| probe.middleware_calls.load(Ordering::Acquire) != 2
|| probe.handler_calls.load(Ordering::Acquire) != 1
{
return Err(
"header mismatch changed its canonical error or reached the handler".to_owned(),
);
}
Ok(())
});
}
#[test]
fn live_http_sse_middleware_progress_then_canonical_capability_error_is_json_400() {
run_live_http_test(|cx| async move {
let canonical_data = serde_json::json!({
"requiredCapabilities": {
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
});
let bound = Server::new("live-http-middleware-capability-sse", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.mask_error_details(false)
.middleware(HttpCapabilityErrorMiddleware {
data: canonical_data.clone(),
emit_progress: true,
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live middleware SSE bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live middleware SSE address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let request = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "live-middleware-capability-progress",
},
})),
835_i64,
);
let body = serde_json::to_vec(&request).map_err(|error| {
format!("live middleware capability request did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(
"/mcp",
&body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SERVER_DISCOVER_METHOD),
],
),
)
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("live middleware capability SSE rejection complete"),
);
response
})
.map_err(|error| format!("live middleware SSE client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let response = client
.join(&cx)
.await
.map_err(|error| format!("live middleware SSE client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("live middleware SSE server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live middleware SSE").await?;
if !response.starts_with(b"HTTP/1.1 400")
|| live_http_response_header(&response, "content-type")
.ok()
.as_deref()
!= Some("application/json")
{
return Err(format!(
"canonical middleware capability error did not elect JSON HTTP 400: {response:?}"
));
}
if response
.windows(b"event: message".len())
.any(|window| window == b"event: message")
{
return Err(
"progress-before-error middleware committed an SSE event before JSON 400"
.to_owned(),
);
}
let response: JsonRpcResponse =
serde_json::from_slice(live_http_response_body(&response)?)
.map_err(|error| format!("middleware JSON 400 was not JSON-RPC: {error}"))?;
let error = response
.error
.ok_or_else(|| "middleware JSON 400 omitted its JSON-RPC error".to_owned())?;
if response.id != Some(835_i64.into())
|| error.code.as_i32() != Some(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE)
|| error.data.as_ref() != Some(&canonical_data)
{
return Err(format!(
"middleware JSON 400 did not retain canonical capability error: {error:?}"
));
}
Ok(())
});
}
#[test]
fn live_http_subscription_middleware_capability_error_is_json_400_before_sse_head() {
run_live_http_test(|cx| async move {
let canonical_data = serde_json::json!({
"requiredCapabilities": {
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
});
let bound = Server::new("live-http-subscription-middleware-capability", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.mask_error_details(false)
.middleware(HttpCapabilityErrorMiddleware {
data: canonical_data.clone(),
emit_progress: true,
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live subscription middleware bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live subscription middleware address failed: {error}"))?;
let server = Arc::clone(&bound.endpoint.server);
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let request = JsonRpcRequest::new(
SUBSCRIPTIONS_LISTEN,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "live-subscription-middleware-progress",
},
"notifications": { "toolsListChanged": true },
})),
836_i64,
);
let body = serde_json::to_vec(&request).map_err(|error| {
format!("live subscription middleware request did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(
"/mcp",
&body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SUBSCRIPTIONS_LISTEN),
],
),
)
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("live subscription middleware capability rejection complete"),
);
response
})
.map_err(|error| {
format!("live subscription middleware client admission failed: {error}")
})?;
let serve = bound.serve(&cx).await;
let response = client.join(&cx).await.map_err(|error| {
format!("live subscription middleware client failed: {error:?}")
})??;
let shutdown = serve
.map_err(|error| format!("live subscription middleware server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live subscription middleware").await?;
if !response.starts_with(b"HTTP/1.1 400")
|| live_http_response_header(&response, "content-type")
.ok()
.as_deref()
!= Some("application/json")
{
return Err(format!(
"subscription canonical middleware error did not elect JSON HTTP 400: {response:?}"
));
}
if response
.windows(b"event: message".len())
.any(|window| window == b"event: message")
|| response
.windows(b"notifications/subscriptions/acknowledged".len())
.any(|window| window == b"notifications/subscriptions/acknowledged")
{
return Err(
"subscription middleware capability rejection committed an SSE head or event"
.to_owned(),
);
}
let response: JsonRpcResponse =
serde_json::from_slice(live_http_response_body(&response)?).map_err(|error| {
format!("subscription middleware JSON 400 was not JSON-RPC: {error}")
})?;
let error = response.error.ok_or_else(|| {
"subscription middleware JSON 400 omitted its JSON-RPC error".to_owned()
})?;
if response.id != Some(836_i64.into())
|| error.code.as_i32() != Some(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE)
|| error.data.as_ref() != Some(&canonical_data)
{
return Err(format!(
"subscription middleware JSON 400 did not retain canonical capability error: {error:?}"
));
}
if !server
.final_subscriptions
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.is_empty()
{
return Err(
"subscription middleware rejection mutated the final subscription registry"
.to_owned(),
);
}
Ok(())
});
}
#[test]
fn live_http_subscription_acknowledgement_precedes_deferred_progress() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-subscription-acknowledgement-order", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.middleware(HttpSubscriptionProgressMiddleware)
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("subscription ordering bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("subscription ordering address failed: {error}"))?;
let caller_cx = cx.clone();
let controller = thread::spawn(move || -> Result<Vec<u8>, String> {
struct CancelServerOnDrop(Cx);
impl Drop for CancelServerOnDrop {
fn drop(&mut self) {
self.0.cancel_with(
CancelKind::User,
Some("subscription acknowledgement ordering controller finished"),
);
}
}
let shutdown_cx = caller_cx.clone();
let _server_cancellation = CancelServerOnDrop(caller_cx);
let listen = JsonRpcRequest::new(
SUBSCRIPTIONS_LISTEN,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "subscription-before-ack",
},
"notifications": {"toolsListChanged": true},
})),
RequestId::Number(937),
);
let body = serde_json::to_vec(&listen).map_err(|error| {
format!("subscription ordering request did not serialize: {error}")
})?;
let request = live_http_post(
"/mcp",
&body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SUBSCRIPTIONS_LISTEN),
],
);
let mut stream = std::net::TcpStream::connect(address)
.map_err(|error| format!("subscription ordering connect failed: {error}"))?;
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.map_err(|error| {
format!("subscription ordering timeout setup failed: {error}")
})?;
std::io::Write::write_all(&mut stream, &request)
.map_err(|error| format!("subscription ordering write failed: {error}"))?;
std::io::Write::flush(&mut stream)
.map_err(|error| format!("subscription ordering flush failed: {error}"))?;
stream
.shutdown(std::net::Shutdown::Write)
.map_err(|error| {
format!("subscription ordering write-half close failed: {error}")
})?;
let mut received = Vec::new();
while !received
.windows(b"notifications/subscriptions/acknowledged".len())
.any(|window| window == b"notifications/subscriptions/acknowledged")
|| !received
.windows(b"notifications/progress".len())
.any(|window| window == b"notifications/progress")
{
let mut chunk = [0_u8; 2048];
let count = std::io::Read::read(&mut stream, &mut chunk)
.map_err(|error| format!("subscription ordering read failed: {error}"))?;
if count == 0 {
return Err(
"subscription stream closed before acknowledgement and progress"
.to_owned(),
);
}
received.extend_from_slice(&chunk[..count]);
}
shutdown_cx.cancel_with(
CancelKind::User,
Some("subscription acknowledgement ordering graceful shutdown requested"),
);
std::io::Read::read_to_end(&mut stream, &mut received).map_err(|error| {
format!("subscription ordering terminal read failed: {error}")
})?;
Ok(received)
});
let shutdown = bound
.serve(&cx)
.await
.map_err(|error| format!("subscription ordering server failed: {error}"))?;
let response = controller
.join()
.map_err(|_| "subscription ordering controller panicked".to_owned())??;
require_quiescent_http_shutdown(shutdown, "subscription acknowledgement ordering")
.await?;
let messages = live_http_chunked_sse_messages(&response)?;
let Some((first, remaining)) = messages.split_first() else {
return Err("subscription ordering emitted no SSE frames".to_owned());
};
let JsonRpcMessage::Request(acknowledgement) = first else {
return Err("subscription acknowledgement was not the first SSE frame".to_owned());
};
if !matches!(
ServerNotification::decode(acknowledgement),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
) {
return Err(
"first subscription SSE frame was not subscriptions/acknowledged".to_owned(),
);
}
let progress_count = remaining
.iter()
.filter(|message| {
matches!(
message,
JsonRpcMessage::Request(notification)
if matches!(
ServerNotification::decode(notification),
Ok(ServerNotification::Progress(progress))
if progress.progress_token
== ProgressMarker::String("subscription-before-ack".to_owned())
)
)
})
.count();
if progress_count != 1 {
return Err(format!(
"subscription deferred progress was not flushed exactly once after acknowledgement: {messages:?}"
));
}
if !remaining.iter().any(|message| {
matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(RequestId::Number(937))
&& final_subscription_completion_response(response)
)
}) {
return Err(
"subscription ordering stream omitted its graceful terminal response"
.to_owned(),
);
}
Ok(())
});
}
#[test]
fn live_http_post_admission_canonical_error_is_one_terminal_sse_response() {
run_live_http_test(|cx| async move {
let canonical_data = serde_json::json!({
"requiredCapabilities": {
"extensions": { "io.modelcontextprotocol/tasks": {} }
}
});
let bound = Server::new("live-http-post-admission-capability-error", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.mask_error_details(false)
.middleware(HttpSubscriptionResponseCapabilityErrorMiddleware {
data: canonical_data.clone(),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("post-admission capability bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("post-admission capability address failed: {error}"))?;
let caller_cx = cx.clone();
let controller = thread::spawn(move || -> Result<Vec<u8>, String> {
struct CancelServerOnDrop(Cx);
impl Drop for CancelServerOnDrop {
fn drop(&mut self) {
self.0.cancel_with(
CancelKind::User,
Some("post-admission capability controller finished"),
);
}
}
let shutdown_cx = caller_cx.clone();
let _server_cancellation = CancelServerOnDrop(caller_cx);
let listen = JsonRpcRequest::new(
SUBSCRIPTIONS_LISTEN,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
"notifications": {"toolsListChanged": true},
})),
RequestId::Number(938),
);
let body = serde_json::to_vec(&listen).map_err(|error| {
format!("post-admission capability request did not serialize: {error}")
})?;
let request = live_http_post(
"/mcp",
&body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SUBSCRIPTIONS_LISTEN),
],
);
let mut stream = std::net::TcpStream::connect(address).map_err(|error| {
format!("post-admission capability connect failed: {error}")
})?;
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.map_err(|error| {
format!("post-admission capability timeout setup failed: {error}")
})?;
std::io::Write::write_all(&mut stream, &request)
.map_err(|error| format!("post-admission capability write failed: {error}"))?;
std::io::Write::flush(&mut stream)
.map_err(|error| format!("post-admission capability flush failed: {error}"))?;
stream
.shutdown(std::net::Shutdown::Write)
.map_err(|error| {
format!("post-admission capability write-half close failed: {error}")
})?;
let mut received = Vec::new();
while !received
.windows(b"notifications/subscriptions/acknowledged".len())
.any(|window| window == b"notifications/subscriptions/acknowledged")
{
let mut chunk = [0_u8; 2048];
let count = std::io::Read::read(&mut stream, &mut chunk).map_err(|error| {
format!("post-admission capability acknowledgement read failed: {error}")
})?;
if count == 0 {
return Err(
"post-admission capability stream closed before acknowledgement"
.to_owned(),
);
}
received.extend_from_slice(&chunk[..count]);
}
shutdown_cx.cancel_with(
CancelKind::User,
Some("post-admission capability graceful shutdown requested"),
);
std::io::Read::read_to_end(&mut stream, &mut received).map_err(|error| {
format!("post-admission capability terminal read failed: {error}")
})?;
Ok(received)
});
let shutdown = bound
.serve(&cx)
.await
.map_err(|error| format!("post-admission capability server failed: {error}"))?;
let response = controller
.join()
.map_err(|_| "post-admission capability controller panicked".to_owned())??;
require_quiescent_http_shutdown(shutdown, "post-admission canonical error").await?;
if !response.starts_with(b"HTTP/1.1 200")
|| live_http_response_header(&response, "content-type")
.ok()
.as_deref()
!= Some("text/event-stream")
{
return Err(format!(
"post-admission canonical error did not retain its elected SSE representation: {response:?}"
));
}
let messages = live_http_chunked_sse_messages(&response)?;
let [acknowledgement, terminal] = messages.as_slice() else {
return Err(format!(
"post-admission canonical error emitted {} frames instead of acknowledgement plus one terminal response",
messages.len()
));
};
let JsonRpcMessage::Request(acknowledgement) = acknowledgement else {
return Err(
"post-admission canonical stream did not start with acknowledgement".to_owned(),
);
};
if !matches!(
ServerNotification::decode(acknowledgement),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
) {
return Err(
"post-admission canonical stream first frame was not acknowledgement"
.to_owned(),
);
}
let JsonRpcMessage::Response(terminal) = terminal else {
return Err(
"post-admission canonical stream terminal was not a response".to_owned(),
);
};
let Some(error) = terminal.error.as_ref() else {
return Err("post-admission canonical terminal omitted its error".to_owned());
};
if terminal.id != Some(RequestId::Number(938))
|| error.code.as_i32() != Some(MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE)
|| error.message != "Required client capability is missing"
|| error.data.as_ref() != Some(&canonical_data)
{
return Err(format!(
"post-admission canonical terminal did not retain the exact error: {terminal:?}"
));
}
Ok(())
});
}
#[cfg(feature = "tasks")]
#[test]
fn live_http_sse_create_task_false_flushes_progress_once_and_completes() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let delivered = Arc::new(Mutex::new(Vec::new()));
let bound = Server::new("live-http-task-outcome-sse", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.tool(HttpConditionalTaskOutcomeTool {
calls: Arc::clone(&calls),
})
.final_tasks(final_tasks_test_runtime(Arc::clone(&delivered)))
.expect("final Tasks runtime must install for live complete outcome")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live createTask:false SSE bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live createTask:false SSE address failed: {error}"))?;
let request = final_http_task_outcome_request(false, true);
let body = serde_json::to_vec(&request).map_err(|error| {
format!("live createTask:false request did not serialize: {error}")
})?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let response = live_http_exchange(
address,
live_http_post(
"/mcp",
&body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "durable_final_task"),
],
),
)
.await;
caller_cx.cancel_with(
CancelKind::User,
Some("live createTask:false SSE response complete"),
);
response
})
.map_err(|error| {
format!("live createTask:false SSE client admission failed: {error}")
})?;
let serve = bound.serve(&cx).await;
let response = client
.join(&cx)
.await
.map_err(|error| format!("live createTask:false SSE client failed: {error:?}"))??;
let shutdown = serve
.map_err(|error| format!("live createTask:false SSE server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live createTask:false SSE").await?;
if !response.starts_with(b"HTTP/1.1 200")
|| live_http_response_header(&response, "content-type")
.ok()
.as_deref()
!= Some("text/event-stream")
{
return Err(format!(
"createTask:false did not elect its normal SSE representation: {response:?}"
));
}
let messages = live_http_chunked_sse_messages(&response)?;
let [progress, terminal] = messages.as_slice() else {
return Err(format!(
"createTask:false emitted {} frames instead of one progress and terminal response",
messages.len()
));
};
let JsonRpcMessage::Request(progress) = progress else {
return Err("createTask:false first frame was not progress".to_owned());
};
let ServerNotification::Progress(progress) = ServerNotification::decode(progress)
.map_err(|error| format!("createTask:false progress frame was invalid: {error}"))?
else {
return Err(
"createTask:false first frame was not notifications/progress".to_owned(),
);
};
if progress.progress_token
!= ProgressMarker::String("missing-task-capability".to_owned())
{
return Err(format!(
"createTask:false progress frame was not retained exactly once: {progress:?}"
));
}
if !matches!(
terminal,
JsonRpcMessage::Response(response)
if response.id == Some(81_i64.into())
&& response.error.is_none()
&& response
.result
.as_ref()
.and_then(|result| result.get("resultType"))
== Some(&serde_json::json!("complete"))
) {
return Err("createTask:false terminal frame was not complete".to_owned());
}
if calls.load(Ordering::Acquire) != 1
|| !delivered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
{
return Err("createTask:false mutated a Task or skipped its handler".to_owned());
}
Ok(())
});
}
#[test]
fn live_http_sse_write_half_close_before_election_preserves_response() {
run_live_http_test(|cx| async move {
const REQUEST_ID: u64 = 815;
let control = Arc::new(LiveModernControl::default());
let bound = Server::new("live-http-sse-write-half-close", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("write-half SSE bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("write-half SSE address failed: {error}"))?;
let caller_cx = cx.clone();
let controller_control = Arc::clone(&control);
let controller = thread::spawn(move || -> Result<Vec<u8>, String> {
struct CancelServerOnDrop(Cx);
impl Drop for CancelServerOnDrop {
fn drop(&mut self) {
self.0.cancel_with(
CancelKind::User,
Some("write-half SSE controller finished"),
);
}
}
let _server_cancellation = CancelServerOnDrop(caller_cx);
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_modern_controlled_tool",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
REQUEST_ID as i64,
);
let body = serde_json::to_vec(&request).map_err(|error| {
format!("write-half SSE request did not serialize: {error}")
})?;
let request = live_http_post(
"/mcp",
&body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_modern_controlled_tool"),
],
);
let mut stream = std::net::TcpStream::connect(address)
.map_err(|error| format!("write-half SSE connect failed: {error}"))?;
std::io::Write::write_all(&mut stream, &request)
.map_err(|error| format!("write-half SSE request write failed: {error}"))?;
std::io::Write::flush(&mut stream)
.map_err(|error| format!("write-half SSE request flush failed: {error}"))?;
if !controller_control.wait_for_started(1, Duration::from_secs(2)) {
return Err(
"write-half SSE handler did not reach its pre-election gate".to_owned()
);
}
stream
.shutdown(std::net::Shutdown::Write)
.map_err(|error| format!("write-half SSE shutdown failed: {error}"))?;
stream
.set_read_timeout(Some(Duration::from_millis(100)))
.map_err(|error| {
format!("write-half SSE probe timeout setup failed: {error}")
})?;
let mut probe = [0_u8; 1];
match std::io::Read::read(&mut stream, &mut probe) {
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock
) => {}
Ok(0) => {
return Err("write-half SSE body closed before outcome election".to_owned());
}
Ok(_) => {
return Err(
"write-half SSE committed a response head before outcome election"
.to_owned(),
);
}
Err(error) => {
return Err(format!("write-half SSE no-head probe failed: {error}"));
}
}
if controller_control.was_cancelled(REQUEST_ID) {
return Err("write-half EOF cancelled the pending SSE request".to_owned());
}
controller_control.release(REQUEST_ID);
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.map_err(|error| {
format!("write-half SSE response timeout setup failed: {error}")
})?;
let mut response = Vec::new();
std::io::Read::read_to_end(&mut stream, &mut response)
.map_err(|error| format!("write-half SSE response read failed: {error}"))?;
Ok(response)
});
let shutdown = bound
.serve(&cx)
.await
.map_err(|error| format!("write-half SSE server failed: {error}"))?;
let response = controller
.join()
.map_err(|_| "write-half SSE controller panicked".to_owned())??;
require_quiescent_http_shutdown(shutdown, "write-half SSE response").await?;
if !response.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"write-half SSE response was unexpected: {response:?}"
));
}
if !live_http_chunked_sse_messages(&response)?.iter().any(|message| {
matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some((REQUEST_ID as i64).into()) && response.error.is_none()
)
}) {
return Err("write-half SSE omitted its terminal response".to_owned());
}
if control.was_cancelled(REQUEST_ID) {
return Err("write-half EOF cancelled the completed SSE request".to_owned());
}
Ok(())
});
}
#[test]
fn live_http_sse_full_disconnect_cancels_after_output_failure() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-sse-full-disconnect", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("full-disconnect SSE bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("full-disconnect SSE address failed: {error}"))?;
let server = Arc::clone(&bound.endpoint.server);
let caller_cx = cx.clone();
let controller = thread::spawn(move || -> Result<(), String> {
struct CancelServerOnDrop(Cx);
impl Drop for CancelServerOnDrop {
fn drop(&mut self) {
self.0.cancel_with(
CancelKind::User,
Some("full-disconnect SSE controller finished"),
);
}
}
let _server_cancellation = CancelServerOnDrop(caller_cx);
let listen = JsonRpcRequest::new(
SUBSCRIPTIONS_LISTEN,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
"notifications": {"toolsListChanged": true},
})),
816_i64,
);
let body = serde_json::to_vec(&listen).map_err(|error| {
format!("full-disconnect SSE request did not serialize: {error}")
})?;
let request = live_http_post(
"/mcp",
&body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SUBSCRIPTIONS_LISTEN),
],
);
let mut stream = std::net::TcpStream::connect(address)
.map_err(|error| format!("full-disconnect SSE connect failed: {error}"))?;
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.map_err(|error| {
format!("full-disconnect SSE acknowledgement timeout setup failed: {error}")
})?;
std::io::Write::write_all(&mut stream, &request).map_err(|error| {
format!("full-disconnect SSE request write failed: {error}")
})?;
std::io::Write::flush(&mut stream).map_err(|error| {
format!("full-disconnect SSE request flush failed: {error}")
})?;
let mut acknowledgement = Vec::new();
while !acknowledgement
.windows(b"notifications/subscriptions/acknowledged".len())
.any(|window| window == b"notifications/subscriptions/acknowledged")
{
let mut chunk = [0_u8; 2048];
let count = std::io::Read::read(&mut stream, &mut chunk).map_err(|error| {
format!("full-disconnect SSE acknowledgement read failed: {error}")
})?;
if count == 0 {
return Err(
"full-disconnect SSE closed before subscriptions/acknowledged"
.to_owned(),
);
}
acknowledgement.extend_from_slice(&chunk[..count]);
}
if server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
.map_err(|error| {
format!("full-disconnect initial output publish failed: {error}")
})?
!= 1
{
return Err(
"full-disconnect listener did not accept its first output event".to_owned(),
);
}
// Leave a real server output unread, then close both halves.
// The next output write, not input EOF, is the peer-loss
// signal under test.
std::thread::sleep(Duration::from_millis(20));
stream
.shutdown(std::net::Shutdown::Both)
.map_err(|error| format!("full-disconnect SSE shutdown failed: {error}"))?;
drop(stream);
let deadline = Instant::now() + Duration::from_secs(2);
loop {
if server
.final_subscriptions
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.is_empty()
{
return Ok(());
}
if Instant::now() >= deadline {
return Err(
"full-disconnect SSE did not cancel after output-side failure"
.to_owned(),
);
}
let _ = server
.publish_subscription_notification(ServerNotification::ToolsListChanged(
None,
))
.map_err(|error| {
format!("full-disconnect follow-up output publish failed: {error}")
})?;
std::thread::sleep(Duration::from_millis(1));
}
});
let shutdown = bound
.serve(&cx)
.await
.map_err(|error| format!("full-disconnect SSE server failed: {error}"))?;
controller
.join()
.map_err(|_| "full-disconnect SSE controller panicked".to_owned())??;
require_quiescent_http_shutdown(shutdown, "full-disconnect SSE output failure").await
});
}
#[test]
fn live_http_dual_accept_rejects_only_four_digit_qvalue_before_handler() {
run_live_http_test(|cx| async move {
let calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-modern-sse-qvalue", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.log_level(Level::Debug)
.tool(HttpRequestScopedProgressTool {
calls: Arc::clone(&calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live malformed SSE bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live malformed SSE address failed: {error}"))?;
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_request_scoped_progress",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "live-http-progress",
"io.modelcontextprotocol/logLevel": "info",
},
})),
813_i64,
);
let body = serde_json::to_vec(&request)
.map_err(|error| format!("live malformed request did not serialize: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let request = live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json, text/event-stream; Q=1.0000"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "http_request_scoped_progress"),
],
);
let response = live_http_exchange(address, request).await;
caller_cx.cancel_with(CancelKind::User, Some("malformed SSE request complete"));
response
})
.map_err(|error| format!("live malformed client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let response = client
.join(&cx)
.await
.map_err(|error| format!("live malformed client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("live malformed server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "live malformed").await?;
if !response.starts_with(b"HTTP/1.1 406") || calls.load(Ordering::Acquire) != 0 {
return Err(
"changing only the SSE qvalue precision must reject before handler entry"
.to_owned(),
);
}
Ok(())
});
}
#[test]
fn live_http_sse_discovery_rejects_preissued_session_without_registry_mutation() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-sse-discovery-reject", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("SSE discovery rejection bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("SSE discovery rejection address failed: {error}"))?;
let modern_sessions = Arc::clone(&bound.modern_sessions);
let discovery = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
RequestId::Number(869),
);
let discovery_body = serde_json::to_vec(&discovery)
.map_err(|error| format!("SSE discovery rejection did not serialize: {error}"))?;
let request = live_http_post(
"/mcp",
&discovery_body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SERVER_DISCOVER_METHOD),
("MCP-Session-Id", "preissued-session"),
],
);
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let response = live_http_exchange(address, request).await;
caller_cx.cancel_with(
CancelKind::User,
Some("SSE discovery preissued-session rejection complete"),
);
response
})
.map_err(|error| {
format!("SSE discovery rejection client admission failed: {error}")
})?;
let serve = bound.serve(&cx).await;
let response = client
.join(&cx)
.await
.map_err(|error| format!("SSE discovery rejection client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("SSE discovery rejection server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "SSE discovery rejection").await?;
if !response.starts_with(b"HTTP/1.1 400")
|| !modern_sessions
.sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
{
return Err(
"preissued SSE discovery session header minted or retained a session"
.to_owned(),
);
}
Ok(())
});
}
#[test]
fn live_http_shutdown_owns_registered_sse_body_before_flushing_terminal() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-listen-half-close", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("subscription half-close bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("subscription half-close address failed: {error}"))?;
let server = Arc::clone(&bound.endpoint.server);
let remaining_modern_sessions = Arc::clone(&bound.modern_sessions);
let owned_modern_sessions = Arc::clone(&remaining_modern_sessions);
let caller_cx = cx.clone();
let controller = thread::spawn(move || -> Result<(), String> {
struct CancelServerOnDrop(Cx);
impl Drop for CancelServerOnDrop {
fn drop(&mut self) {
self.0.cancel_with(
CancelKind::User,
Some("subscription half-close controller finished"),
);
}
}
let shutdown_cx = caller_cx.clone();
let _server_cancellation = CancelServerOnDrop(caller_cx);
let listen = JsonRpcRequest::new(
SUBSCRIPTIONS_LISTEN,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
"notifications": {"toolsListChanged": true},
})),
RequestId::Number(871),
);
let listen_body = serde_json::to_vec(&listen)
.map_err(|error| format!("listen request did not serialize: {error}"))?;
let listen_request = live_http_post(
"/mcp",
&listen_body,
&[
("Accept", "text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SUBSCRIPTIONS_LISTEN),
],
);
let mut stream = std::net::TcpStream::connect(address)
.map_err(|error| format!("listen SSE connect failed: {error}"))?;
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.map_err(|error| format!("listen SSE timeout setup failed: {error}"))?;
std::io::Write::write_all(&mut stream, &listen_request)
.map_err(|error| format!("listen SSE write failed: {error}"))?;
std::io::Write::flush(&mut stream)
.map_err(|error| format!("listen SSE flush failed: {error}"))?;
stream
.shutdown(std::net::Shutdown::Write)
.map_err(|error| format!("listen write-half close failed: {error}"))?;
let mut received = Vec::new();
while !received
.windows(b"notifications/subscriptions/acknowledged".len())
.any(|window| window == b"notifications/subscriptions/acknowledged")
{
let mut chunk = [0_u8; 2048];
let count = std::io::Read::read(&mut stream, &mut chunk)
.map_err(|error| format!("listen acknowledgement read failed: {error}"))?;
if count == 0 {
return Err("listen stream closed before acknowledgement".to_owned());
}
received.extend_from_slice(&chunk[..count]);
}
if server
.final_subscriptions
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.len()
!= 1
{
return Err(
"request write-half EOF incorrectly cancelled the live response body"
.to_owned(),
);
}
if owned_modern_sessions
.sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len()
!= 1
{
return Err(
"live SSE response body was not registered for listener shutdown"
.to_owned(),
);
}
shutdown_cx.cancel_with(
CancelKind::User,
Some("subscription graceful shutdown requested"),
);
let mut trailing = Vec::new();
std::io::Read::read_to_end(&mut stream, &mut trailing)
.map_err(|error| format!("terminated listen stream did not close: {error}"))?;
received.extend_from_slice(&trailing);
if received
.windows(b"notifications/cancelled".len())
.any(|window| window == b"notifications/cancelled")
{
return Err(
"modern HTTP teardown emitted a forbidden cancellation notification"
.to_owned(),
);
}
if !received
.windows(b"\"resultType\":\"complete\"".len())
.any(|window| window == b"\"resultType\":\"complete\"")
{
return Err("server teardown omitted its terminal complete result".to_owned());
}
Ok(())
});
let serve = bound.serve(&cx).await;
controller
.join()
.map_err(|_| "subscription half-close controller panicked".to_owned())??;
let shutdown =
serve.map_err(|error| format!("subscription half-close server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "subscription half-close").await?;
if !remaining_modern_sessions
.sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
{
return Err("listener shutdown retained a live SSE session".to_owned());
}
if !remaining_modern_sessions
.retired_dispatches
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
{
return Err("listener shutdown detached a live SSE dispatch".to_owned());
}
Ok(())
});
}
#[test]
fn live_http_ordinary_requests_overlap_in_request_owned_blocking_children() {
let _overlap_lock = http_overlap_lock()
.lock()
.expect("live HTTP overlap test lock poisoned");
reset_http_overlap_metrics();
let control = http_overlap_control();
let _control_guard = control.begin();
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-request-owned-dispatch", "1.0.0")
.tool(HttpOverlapTool)
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("request-owned HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("request-owned HTTP address failed: {error}"))?;
let request = |id| {
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_overlap_tool",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
id,
);
let body = serde_json::to_vec(&request)
.expect("request-owned HTTP request must serialize");
live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "http_overlap_tool"),
],
)
};
let first_request = request(221_i64);
let second_request = request(222_i64);
let cancellation = cx.clone();
let controller = thread::spawn(move || -> Result<(), String> {
fn exchange(address: SocketAddr, request: Vec<u8>) -> Result<Vec<u8>, String> {
let mut stream = std::net::TcpStream::connect(address)
.map_err(|error| format!("request-owned HTTP connect failed: {error}"))?;
std::io::Write::write_all(&mut stream, &request)
.map_err(|error| format!("request-owned HTTP write failed: {error}"))?;
std::io::Write::flush(&mut stream)
.map_err(|error| format!("request-owned HTTP flush failed: {error}"))?;
let mut response = Vec::new();
std::io::Read::read_to_end(&mut stream, &mut response)
.map_err(|error| format!("request-owned HTTP read failed: {error}"))?;
Ok(response)
}
let first = thread::spawn(move || exchange(address, first_request));
let second = thread::spawn(move || exchange(address, second_request));
let both_entered =
http_overlap_control().wait_for_entries(2, Duration::from_secs(2));
http_overlap_control().release_one();
http_overlap_control().release_one();
let first = first
.join()
.map_err(|_| "first request-owned HTTP client panicked".to_string())??;
let second = second
.join()
.map_err(|_| "second request-owned HTTP client panicked".to_string())??;
cancellation.cancel_with(CancelKind::User, Some("request-owned HTTP complete"));
if !both_entered {
return Err("two ordinary HTTP requests did not enter concurrently".to_string());
}
for response in [first, second] {
if !response.starts_with(b"HTTP/1.1 200") {
return Err(format!("request-owned HTTP response failed: {response:?}"));
}
}
Ok(())
});
let serve = bound.serve(&cx).await;
let controller = controller
.join()
.map_err(|_| "request-owned HTTP controller panicked".to_string())?;
controller?;
let shutdown =
serve.map_err(|error| format!("request-owned HTTP server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "request-owned HTTP").await?;
Ok(())
});
assert!(
http_overlap_metrics().max.load(Ordering::SeqCst) >= 2,
"ordinary HTTP requests must overlap in independent request-owned children"
);
}
async fn live_http_modern_json_peer_close_probe(
cx: &Cx,
close_originating_peer: bool,
) -> Result<(), String> {
let started = Arc::new(AtomicBool::new(false));
let release = Arc::new(AtomicBool::new(false));
let observed_cancellation = Arc::new(AtomicBool::new(false));
let bound = Server::new("live-http-modern-json-peer-close", "1.0.0")
.tool(LiveModernHttpPeerCancellationTool {
started: Arc::clone(&started),
release: Arc::clone(&release),
observed_cancellation: Arc::clone(&observed_cancellation),
})
.build()
.bind_http(cx, "127.0.0.1:0")
.await
.map_err(|error| format!("modern JSON peer-close HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("modern JSON peer-close HTTP address failed: {error}"))?;
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_modern_http_peer_cancellation_tool",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
2_401_i64,
);
let body = serde_json::to_vec(&request).map_err(|error| {
format!("modern JSON peer-close request did not serialize: {error}")
})?;
let request = live_http_post(
"/mcp",
&body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_modern_http_peer_cancellation_tool"),
],
);
let caller_cx = cx.clone();
let started_by_client = Arc::clone(&started);
let release_by_client = Arc::clone(&release);
let observed_by_client = Arc::clone(&observed_cancellation);
let mut client = cx
.spawn(move |client_cx| async move {
let deadline = client_cx
.now()
.saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut stream =
asupersync::time::timeout_at(deadline, AsyncTcpStream::connect(address))
.await
.map_err(|_| live_http_test_timeout("modern JSON peer-close connection"))?
.map_err(|error| {
format!("modern JSON peer-close connect failed: {error}")
})?;
asupersync::time::timeout_at(deadline, stream.write_all(&request))
.await
.map_err(|_| live_http_test_timeout("modern JSON peer-close request write"))?
.map_err(|error| format!("modern JSON peer-close write failed: {error}"))?;
asupersync::time::timeout_at(deadline, stream.flush())
.await
.map_err(|_| live_http_test_timeout("modern JSON peer-close request flush"))?
.map_err(|error| format!("modern JSON peer-close flush failed: {error}"))?;
wait_for_live_http_flag(
&client_cx,
&started_by_client,
"modern JSON peer-close handler start",
)
.await?;
if close_originating_peer {
stream
.shutdown(std::net::Shutdown::Write)
.map_err(|error| format!("modern JSON write-half close failed: {error}"))?;
release_by_client.store(true, Ordering::Release);
let mut response = Vec::new();
read_live_http_to_end(
&mut stream,
&mut response,
"modern JSON write-half-close positive response EOF",
)
.await?;
if !response.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"modern JSON write-half-close response was unexpected: {response:?}"
));
}
if observed_by_client.load(Ordering::Acquire) {
return Err(
"a write-half-closed modern JSON peer cancelled its handler".to_owned()
);
}
} else {
release_by_client.store(true, Ordering::Release);
let mut response = Vec::new();
read_live_http_to_end(
&mut stream,
&mut response,
"modern JSON peer-close positive response EOF",
)
.await?;
if !response.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"modern JSON peer-close positive response was unexpected: {response:?}"
));
}
if observed_by_client.load(Ordering::Acquire) {
return Err("a connected modern JSON peer cancelled its handler".to_owned());
}
}
caller_cx.cancel_with(
CancelKind::User,
Some("modern JSON peer-close probe complete"),
);
Ok::<_, String>(())
})
.map_err(|error| format!("modern JSON peer-close client was not admitted: {error}"))?;
let serve = bound.serve(cx).await;
client
.join(cx)
.await
.map_err(|error| format!("modern JSON peer-close client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("modern JSON peer-close server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "modern JSON peer-close").await?;
if observed_cancellation.load(Ordering::Acquire) {
return Err(if close_originating_peer {
"a write-half-closed modern JSON peer cancelled its handler".to_owned()
} else {
"connected modern JSON peer unexpectedly cancelled its handler".to_owned()
});
}
Ok(())
}
#[test]
fn live_http_modern_json_connected_peer_keeps_handler_live() {
run_live_http_test(
|cx| async move { live_http_modern_json_peer_close_probe(&cx, false).await },
);
}
#[test]
fn live_http_modern_json_write_half_close_preserves_handler() {
// A clean write-half EOF is ordinary H1 request completion. It must
// not cancel the still-owned JSON response body.
run_live_http_test(
|cx| async move { live_http_modern_json_peer_close_probe(&cx, true).await },
);
}
#[test]
fn live_http_connection_limit_hard_closes_idle_overflow_peers() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-connection-limit", "1.0.0")
.http_config(HttpServerConfig::new().max_connections(0))
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("connection-limit bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("connection-limit address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
for peer in 0..8 {
let mut stream = AsyncTcpStream::connect(address).await.map_err(|error| {
format!("overflow peer {peer} failed to connect: {error}")
})?;
let mut response = Vec::new();
read_live_http_to_end(
&mut stream,
&mut response,
&format!("overflow peer {peer} close"),
)
.await?;
if !response.is_empty() {
return Err(format!(
"overflow peer {peer} received a response instead of an immediate close: {response:?}"
));
}
}
caller_cx.cancel_with(CancelKind::User, Some("connection-limit complete"));
Ok::<(), String>(())
})
.map_err(|error| format!("connection-limit client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
client
.join(&cx)
.await
.map_err(|error| format!("connection-limit client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("connection-limit server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "connection limit").await?;
Ok(())
});
}
#[test]
fn live_http_runs_lifespan_hooks_once() {
run_live_http_test(|cx| async move {
let startup = Arc::new(AtomicUsize::new(0));
let shutdown = Arc::new(AtomicUsize::new(0));
let startup_hook = Arc::clone(&startup);
let shutdown_hook = Arc::clone(&shutdown);
let bound = Server::new("live-http-lifespan", "1.0.0")
.on_startup(move || {
startup_hook.fetch_add(1, Ordering::AcqRel);
Ok::<(), std::io::Error>(())
})
.on_shutdown(move || {
shutdown_hook.fetch_add(1, Ordering::AcqRel);
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("lifespan HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("lifespan HTTP address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let response = live_http_exchange(
address,
b"GET /health HTTP/1.1\r\nHost: loopback\r\nConnection: close\r\n\r\n"
.to_vec(),
)
.await;
caller_cx.cancel_with(CancelKind::User, Some("lifespan HTTP complete"));
response
})
.map_err(|error| format!("lifespan HTTP client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let response = client
.join(&cx)
.await
.map_err(|error| format!("lifespan HTTP client failed: {error:?}"))??;
let server_shutdown =
serve.map_err(|error| format!("lifespan HTTP server failed: {error}"))?;
require_quiescent_http_shutdown(server_shutdown, "lifespan HTTP").await?;
if !response.starts_with(b"HTTP/1.1 200") {
return Err(format!("lifespan health request failed: {response:?}"));
}
if startup.load(Ordering::Acquire) != 1 || shutdown.load(Ordering::Acquire) != 1 {
return Err(format!(
"HTTP lifespan hooks were not run exactly once (startup={}, shutdown={})",
startup.load(Ordering::Acquire),
shutdown.load(Ordering::Acquire),
));
}
Ok(())
});
}
/// An idle peer and a byte-trickling peer each lose their connection at
/// the request-read deadline, releasing the listener's only slot; a peer
/// that sends a complete request on that same slot is served.
#[test]
fn live_http_request_read_timeout_releases_idle_and_trickling_connections() {
const READ_TIMEOUT: Duration = Duration::from_millis(300);
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-request-read-timeout", "1.0.0")
.http_config(
HttpServerConfig::new()
.max_connections(1)
.request_read_timeout(READ_TIMEOUT),
)
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("read-timeout bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("read-timeout address failed: {error}"))?;
let caller_cx = cx.clone();
let controller = thread::spawn(move || -> Result<Vec<u8>, String> {
struct CancelServerOnDrop(Cx);
impl Drop for CancelServerOnDrop {
fn drop(&mut self) {
self.0.cancel_with(
CancelKind::User,
Some("request read timeout controller finished"),
);
}
}
let _server_cancellation = CancelServerOnDrop(caller_cx);
let connect = || -> Result<std::net::TcpStream, String> {
let stream = std::net::TcpStream::connect(address)
.map_err(|error| format!("read-timeout connect failed: {error}"))?;
stream
.set_read_timeout(Some(Duration::from_secs(10)))
.map_err(|error| format!("read-timeout setup failed: {error}"))?;
Ok(stream)
};
// Each stalled peer must be closed by the server (EOF, no
// response bytes), and not before the deadline.
let closed_at_deadline = |stream: &mut std::net::TcpStream,
started: std::time::Instant,
peer: &str|
-> Result<(), String> {
let mut received = Vec::new();
match std::io::Read::read_to_end(stream, &mut received) {
Ok(_) => {}
// Bytes written after the server's close draw a reset.
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::BrokenPipe
) => {}
Err(error) => return Err(format!("{peer} peer was not closed: {error}")),
}
let elapsed = started.elapsed();
if !received.is_empty() || elapsed < READ_TIMEOUT {
return Err(format!(
"{peer} peer closed after {elapsed:?} with {received:?}"
));
}
Ok(())
};
let started = std::time::Instant::now();
let mut idle = connect()?;
closed_at_deadline(&mut idle, started, "idle")?;
let started = std::time::Instant::now();
let mut trickling = connect()?;
for byte in b"GET /health HTTP/1.1\r\nHost: loop" {
if std::io::Write::write_all(&mut trickling, &[*byte]).is_err() {
break;
}
thread::sleep(Duration::from_millis(25));
}
closed_at_deadline(&mut trickling, started, "trickling")?;
// Near-identical positive on the same single slot: the
// complete request is answered.
let mut complete = connect()?;
std::io::Write::write_all(
&mut complete,
b"GET /health HTTP/1.1\r\nHost: loopback\r\nConnection: close\r\n\r\n",
)
.map_err(|error| format!("complete request write failed: {error}"))?;
let mut response = Vec::new();
std::io::Read::read_to_end(&mut complete, &mut response)
.map_err(|error| format!("complete request read failed: {error}"))?;
Ok(response)
});
let shutdown = bound
.serve(&cx)
.await
.map_err(|error| format!("read-timeout server failed: {error}"))?;
let response = controller
.join()
.map_err(|_| "read-timeout controller panicked".to_owned())??;
require_quiescent_http_shutdown(shutdown, "request read timeout").await?;
if !response.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"a complete request on the released slot was not served: {:?}",
String::from_utf8_lossy(&response)
));
}
Ok(())
});
}
// Exact-2024 era: legacy HTTP SSE endpoint event.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_sse_advertises_the_request_host_on_wildcard_bind() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-http-host-origin", "1.0.0")
.build()
.bind_http(&cx, "0.0.0.0:0")
.await
.map_err(|error| format!("wildcard HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("wildcard HTTP address failed: {error}"))?;
let loopback = SocketAddr::from(([127, 0, 0, 1], address.port()));
let advertised_host = format!("public.example.test:{}", address.port());
let expected_endpoint = format!("data: http://{advertised_host}/messages?session_id=");
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let mut stream = AsyncTcpStream::connect(loopback)
.await
.map_err(|error| format!("wildcard SSE connect failed: {error}"))?;
stream
.write_all(
format!(
"GET /sse HTTP/1.1\r\nHost: {advertised_host}\r\nAccept: text/event-stream\r\nConnection: keep-alive\r\n\r\n"
)
.as_bytes(),
)
.await
.map_err(|error| format!("wildcard SSE write failed: {error}"))?;
stream
.flush()
.await
.map_err(|error| format!("wildcard SSE flush failed: {error}"))?;
let mut received = Vec::new();
read_live_http_until(&mut stream, &mut received, expected_endpoint.as_bytes())
.await?;
caller_cx.cancel_with(CancelKind::User, Some("wildcard SSE complete"));
Ok::<(), String>(())
})
.map_err(|error| format!("wildcard SSE client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
client
.join(&cx)
.await
.map_err(|error| format!("wildcard SSE client failed: {error:?}"))??;
let shutdown = serve.map_err(|error| format!("wildcard SSE server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "wildcard SSE").await?;
Ok(())
});
}
// Exact-2024 era: legacy HTTP SSE lifecycle.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_loopback_preserves_exact_legacy_sse_and_post_lifecycle() {
run_live_http_test(|cx| async move {
let bound = Server::new("live-legacy-http", "1.0.0")
.tool(LiveRuntimeListedTool)
.http_config(
HttpServerConfig::new()
.mcp_path("/bridge")
.legacy_sse_path("/bridge"),
)
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("live legacy HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("live legacy HTTP address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |_client_cx| async move {
let modern = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
810_i64,
);
let modern_body = serde_json::to_vec(&modern)
.map_err(|error| format!("shared-target modern request failed: {error}"))?;
let modern = live_http_exchange(
address,
live_http_post(
"/bridge",
&modern_body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("MCP-Method", SERVER_DISCOVER_METHOD),
],
),
)
.await?;
if !modern.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"shared-target modern POST did not select final HTTP: {modern:?}"
));
}
let mut stream = AsyncTcpStream::connect(address)
.await
.map_err(|error| format!("legacy SSE connect failed: {error}"))?;
stream
.write_all(
format!(
"GET /bridge HTTP/1.1\r\nHost: {address}\r\nAccept: text/event-stream\r\nConnection: keep-alive\r\n\r\n"
)
.as_bytes(),
)
.await
.map_err(|error| format!("legacy SSE request write failed: {error}"))?;
stream
.flush()
.await
.map_err(|error| format!("legacy SSE request flush failed: {error}"))?;
let mut received = Vec::new();
let endpoint_prefix = format!(
"data: http://{address}/messages?session_id="
);
read_live_http_until(&mut stream, &mut received, endpoint_prefix.as_bytes())
.await?;
let received_text = std::str::from_utf8(&received)
.map_err(|error| format!("legacy SSE endpoint was not UTF-8: {error}"))?;
let session_id = received_text
.split_once(&endpoint_prefix)
.and_then(|(_, remainder)| remainder.split_whitespace().next())
.ok_or_else(|| "legacy SSE endpoint omitted its session identifier".to_string())?
.to_owned();
let initialize = JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "loopback-legacy-client", "version": "1.0.0"},
})),
811_i64,
);
let initialized = JsonRpcRequest::notification("notifications/initialized", None);
let call = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_runtime_listed_tool",
"arguments": {},
})),
812_i64,
);
for message in [initialize, initialized] {
let body = serde_json::to_vec(&message).map_err(|error| {
format!("legacy loopback request did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&body,
&[],
),
)
.await?;
let response = std::str::from_utf8(&response).map_err(|error| {
format!("legacy POST response was not UTF-8: {error}")
})?;
if !response.starts_with("HTTP/1.1 202") {
return Err(format!("legacy POST was not accepted: {response}"));
}
}
let call_body = serde_json::to_vec(&call).map_err(|error| {
format!("legacy loopback tool request did not serialize: {error}")
})?;
let wrong_session = live_http_exchange(
address,
live_http_post(
"/messages?session_id=wrong-session",
&call_body,
&[],
),
)
.await?;
let wrong_session = std::str::from_utf8(&wrong_session).map_err(|error| {
format!("wrong-session legacy response was not UTF-8: {error}")
})?;
if !wrong_session.starts_with("HTTP/1.1 404") {
return Err(format!(
"changing only the legacy session ID was not rejected: {wrong_session}"
));
}
let accepted = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&call_body,
&[],
),
)
.await?;
let accepted = std::str::from_utf8(&accepted).map_err(|error| {
format!("correct-session legacy response was not UTF-8: {error}")
})?;
if !accepted.starts_with("HTTP/1.1 202") {
return Err(format!(
"the unchanged exact legacy session did not accept the valid request: {accepted}"
));
}
read_live_http_until(&mut stream, &mut received, br#""id":812"#).await?;
let received = std::str::from_utf8(&received)
.map_err(|error| format!("legacy SSE response was not UTF-8: {error}"))?;
if !received.contains(LEGACY_PROTOCOL_VERSION)
|| !received.contains("live runtime legacy adapter request 812")
|| received.contains("\"resultType\"")
{
return Err(format!("legacy SSE lifecycle was not exact: {received}"));
}
caller_cx.cancel_with(CancelKind::User, Some("legacy loopback complete"));
Ok(())
})
.map_err(|error| format!("legacy loopback client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
let client = client
.join(&cx)
.await
.map_err(|error| format!("legacy loopback client failed: {error:?}"))?;
let shutdown =
serve.map_err(|error| format!("legacy loopback server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "legacy loopback").await?;
client
});
}
// Exact-2024 era: legacy HTTP SSE reverse responses.
#[cfg(feature = "legacy-2024-11-05")]
async fn live_http_legacy_reverse_response_post_probe(
cx: &Cx,
wrong_reverse_response_id: bool,
plant_exact_admission_negatives: bool,
reverse_response_token: Option<&'static str>,
async_handler: bool,
) -> Result<(), String> {
const INITIALIZE_ID: i64 = 842;
const TOOL_CALL_ID: i64 = 843;
// The synchronous tool bridges `ctx.sample` with `block_on` from its
// `call` method, which is the obvious user code (bd-6rfrg). The async
// tool awaits it on the caller-owned path instead.
let server = Server::new("live-http-legacy-reverse-response", "1.0.0");
let server = if async_handler {
server.tool(LiveLegacyReverseResponseAsyncTool)
} else {
server.tool(LiveLegacyRuntimeConnectionTool)
};
let server = if reverse_response_token.is_some() {
let verifier = StaticTokenVerifier::new([
("alpha", AuthContext::with_subject("alice")),
("beta", AuthContext::with_subject("bob")),
])
.map_err(|error| format!("legacy reverse-response verifier failed: {error}"))?
.with_allowed_schemes(["Bearer"])
.map_err(|error| format!("legacy reverse-response scheme failed: {error}"))?;
server.auth_provider(TokenAuthProvider::new(verifier))
} else {
server
};
let bound = server
.build()
.bind_http(cx, "127.0.0.1:0")
.await
.map_err(|error| format!("legacy reverse-response HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("legacy reverse-response address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |client_cx| async move {
let opener_headers = reverse_response_token
.map(|_| vec![("Authorization", "Bearer alpha")])
.unwrap_or_default();
let (mut sse, session_id, mut received) =
open_live_legacy_http_session(address, &opener_headers).await?;
for request in [
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {"sampling": {}},
"clientInfo": {
"name": "legacy-reverse-response-client",
"version": "1.0.0"
},
})),
INITIALIZE_ID,
),
JsonRpcRequest::notification("notifications/initialized", None),
] {
let body = serde_json::to_vec(&request).map_err(|error| {
format!("legacy reverse-response setup did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&body,
&opener_headers,
),
)
.await?;
if !response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy reverse-response setup was not accepted: {response:?}"
));
}
}
let tool_call = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_legacy_runtime_connection_tool",
"arguments": {"sample": true},
})),
TOOL_CALL_ID,
);
let tool_call_body = serde_json::to_vec(&tool_call).map_err(|error| {
format!("legacy sampling tool call did not serialize: {error}")
})?;
let tool_call_request = live_http_post(
&format!("/messages?session_id={session_id}"),
&tool_call_body,
&opener_headers,
);
let mut tool_call = client_cx
.spawn(move |_tool_call_cx| async move {
live_http_exchange(address, tool_call_request).await
})
.map_err(|error| {
format!("legacy sampling tool-call POST was not admitted: {error}")
})?;
read_live_http_until(&mut sse, &mut received, b"\"method\":\"sampling/createMessage\"")
.await?;
let received_text = std::str::from_utf8(&received).map_err(|error| {
format!("legacy sampling reverse request was not UTF-8: {error}")
})?;
let sampling_offset = received_text
.find("\"method\":\"sampling/createMessage\"")
.ok_or_else(|| "legacy sampling reverse request was not retained".to_string())?;
let event_start = received_text[..sampling_offset]
.rfind("data: ")
.ok_or_else(|| "legacy sampling reverse request omitted SSE data".to_string())?;
let event = &received_text[event_start + "data: ".len()..];
let (event, _) = event
.split_once("\n\n")
.ok_or_else(|| "legacy sampling reverse request was not one SSE event".to_string())?;
let JsonRpcMessage::Request(reverse_request) = Codec::new()
.decode_complete_message(event.as_bytes())
.map_err(|error| {
format!("legacy sampling reverse request was not JSON-RPC: {error}")
})?
else {
return Err("legacy sampling reverse event was not a request".to_string());
};
let reverse_request_id = reverse_request
.id
.ok_or_else(|| "legacy sampling reverse request omitted its ID".to_string())?;
let response_id = if wrong_reverse_response_id {
// The only behavioral change in the near-identical
// negative is this correlation ID.
RequestId::Number(-2)
} else {
reverse_request_id
};
let mut sampling_result = serde_json::to_value(
fastmcp_protocol::CreateMessageResult::text(
"http-sampled-value",
"legacy-http-test-model",
),
)
.map_err(|error| format!("legacy sampling response did not serialize: {error}"))?;
sampling_result["_meta"] =
serde_json::json!({"com.example/application": true});
if plant_exact_admission_negatives {
let accepted = serde_json::json!({
"jsonrpc": "2.0",
"id": response_id.clone(),
"result": sampling_result.clone(),
});
let mut result_type = accepted.clone();
result_type["result"]["resultType"] = serde_json::json!("complete");
let mut final_metadata = accepted.clone();
final_metadata["result"]["_meta"]
["io.modelcontextprotocol/protocolVersion"] =
serde_json::json!(LEGACY_PROTOCOL_VERSION);
for (dimension, body, headers) in [
(
"final resultType",
serde_json::to_vec(&result_type).map_err(|error| error.to_string())?,
Vec::new(),
),
(
"final reserved metadata",
serde_json::to_vec(&final_metadata)
.map_err(|error| error.to_string())?,
Vec::new(),
),
(
"modern binding header",
serde_json::to_vec(&accepted).map_err(|error| error.to_string())?,
vec![("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION)],
),
] {
let rejected = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&body,
if opener_headers.is_empty() {
&headers
} else {
&opener_headers
},
),
)
.await?;
if !rejected.starts_with(b"HTTP/1.1 400") {
return Err(format!(
"{dimension} reached the pending reverse waiter: {rejected:?}"
));
}
}
}
let reverse_response = JsonRpcResponse::success(response_id, sampling_result);
let reverse_body = serde_json::to_vec(&reverse_response).map_err(|error| {
format!("legacy sampling reverse response did not serialize: {error}")
})?;
let reverse_authorization = reverse_response_token
.map(|token| format!("Bearer {token}"));
let reverse_headers = reverse_authorization
.as_deref()
.map(|authorization| vec![("Authorization", authorization)])
.unwrap_or_default();
let reverse_post = live_http_post(
&format!("/messages?session_id={session_id}"),
&reverse_body,
&reverse_headers,
);
let mut reverse_post = client_cx
.spawn(move |_reverse_cx| async move { live_http_exchange(address, reverse_post).await })
.map_err(|error| {
format!("legacy sampling reverse-response POST was not admitted: {error}")
})?;
if wrong_reverse_response_id {
// Observe the task without entering and then abandoning a
// join future. Cancellation of a timed join can itself
// alter the child task's authority, which would make this
// planted negative interfere with the settlement path it
// is meant to verify.
asupersync::time::sleep(client_cx.now(), Duration::from_millis(100)).await;
if !matches!(tool_call.try_join(), Ok(None)) {
caller_cx.cancel_with(
CancelKind::User,
Some("wrong-ID reverse response probe failed"),
);
return Err(
"wrong-ID reverse response settled the pending handler"
.to_string(),
);
}
let cancellation = JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": TOOL_CALL_ID,
"reason": "settle wrong-ID reverse-response probe",
})),
);
let cancellation_body = serde_json::to_vec(&cancellation).map_err(|error| {
caller_cx.cancel_with(
CancelKind::User,
Some("cancellation serialization failed"),
);
format!("legacy sampling cancellation did not serialize: {error}")
})?;
let cancellation_response = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&cancellation_body,
&opener_headers,
),
)
.await;
let cancellation_response = match cancellation_response {
Ok(response) => response,
Err(error) => {
caller_cx.cancel_with(
CancelKind::User,
Some("cancellation exchange failed"),
);
return Err(error);
}
};
if !cancellation_response.starts_with(b"HTTP/1.1 202") {
caller_cx.cancel_with(
CancelKind::User,
Some("cancellation not accepted"),
);
return Err(format!(
"legacy sampling cancellation was not accepted: {cancellation_response:?}"
));
}
let _ = tool_call.join(&client_cx).await.map_err(|error| {
caller_cx.cancel_with(
CancelKind::User,
Some("tool-call join failed"),
);
format!("legacy sampling cancelled tool-call POST failed: {error:?}")
})??;
let _ = reverse_post.join(&client_cx).await.map_err(|error| {
caller_cx.cancel_with(
CancelKind::User,
Some("reverse-post join failed"),
);
format!("wrong-ID reverse-response POST failed after settlement: {error:?}")
})??;
} else {
let reverse_response = reverse_post.join(&client_cx).await.map_err(|error| {
format!("legacy sampling reverse-response POST failed: {error:?}")
})??;
if reverse_response_token == Some("beta") {
if !reverse_response.starts_with(b"HTTP/1.1 401") {
return Err(format!(
"foreign-principal reverse response was not rejected before mutation: {reverse_response:?}"
));
}
asupersync::time::sleep(
client_cx.now(),
Duration::from_millis(100),
)
.await;
if !matches!(tool_call.try_join(), Ok(None)) {
return Err(
"foreign-principal reverse response settled the pending handler"
.to_string(),
);
}
let owned_retry = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&reverse_body,
&opener_headers,
),
)
.await?;
if !owned_retry.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"opener-principal reverse-response retry was not accepted: {owned_retry:?}"
));
}
} else if !reverse_response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"matching reverse-response POST was not accepted: {reverse_response:?}"
));
}
let tool_call_response = tool_call.join(&client_cx).await.map_err(|error| {
format!("legacy sampling tool-call POST failed: {error:?}")
})??;
if !tool_call_response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy sampling tool-call POST was not accepted: {tool_call_response:?}"
));
}
read_live_http_until(
&mut sse,
&mut received,
b"legacy-runtime-1-http-sampled-value",
)
.await?;
}
caller_cx.cancel_with(
CancelKind::User,
Some("legacy reverse-response POST probe complete"),
);
Ok::<_, String>(())
})
.map_err(|error| format!("legacy reverse-response client task was not admitted: {error}"))?;
// Bound only the two outermost awaits, so a stall fails the test by
// name instead of freezing the binary. Inner joins stay unbounded: a
// timed join cancels, which would change the authority under test.
// `serve` spans the whole client exchange, so its bound counts only
// runnable time: a loaded full suite stretches it, a stall does not.
let bound_limit = Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let host = RunnableClock::start();
let serve_started = host.mark();
let Some(serve) =
within_runnable(cx, &host, serve_started, bound_limit, bound.serve(cx)).await
else {
// `serve` returns only after the client cancels `caller_cx`, and
// a client that fails early returns without cancelling. Report
// its outcome so that failure is not misread as a server stall.
let client_state = match client.try_join() {
Ok(None) => "the client is still pending".to_owned(),
Ok(Some(Ok(()))) => "the client completed and cancelled".to_owned(),
Ok(Some(Err(error))) => format!("the client failed first: {error}"),
Err(error) => format!("the client task could not be joined: {error:?}"),
};
return Err(format!(
"legacy reverse-response serve did not finish within {}; {client_state}",
host.describe(serve_started)
));
};
let join_started = host.mark();
within_runnable(cx, &host, join_started, bound_limit, client.join(cx))
.await
.ok_or_else(|| {
format!(
"legacy reverse-response client did not finish within {}",
host.describe(join_started)
)
})?
.map_err(|error| format!("legacy reverse-response client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("legacy reverse-response server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "legacy reverse-response").await
}
// Exact-2024 era: legacy HTTP SSE reverse responses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_reverse_response_bypasses_the_originating_session_mutex() {
run_live_http_test(|cx| async move {
live_http_legacy_reverse_response_post_probe(&cx, false, false, None, false).await
});
}
// Exact-2024 era: legacy HTTP SSE reverse responses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_reverse_response_reaches_an_async_handler() {
run_live_http_test(|cx| async move {
// Differs from the synchronous positive only in the handler's
// execution mode, which moves dispatch to the caller-owned path.
live_http_legacy_reverse_response_post_probe(&cx, false, false, None, true).await
});
}
// Exact-2024 era: legacy HTTP SSE reverse responses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_reverse_response_exact_admission_precedes_pending_mutation() {
run_live_http_test(|cx| async move {
live_http_legacy_reverse_response_post_probe(&cx, false, true, None, false).await
});
}
// Exact-2024 era: legacy HTTP SSE reverse responses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_reverse_response_wrong_id_remains_blocked_until_cancellation() {
run_live_http_test(|cx| async move {
// This differs from the positive only in the reverse-response
// correlation ID, so it must not complete the waiting handler.
live_http_legacy_reverse_response_post_probe(&cx, true, false, None, false).await
});
}
// Exact-2024 era: legacy HTTP SSE reverse responses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_reverse_response_accepts_opener_principal() {
run_live_http_test(|cx| async move {
live_http_legacy_reverse_response_post_probe(&cx, false, false, Some("alpha"), false)
.await
});
}
// Exact-2024 era: legacy HTTP SSE reverse responses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_reverse_response_rejects_foreign_principal_before_pending_mutation() {
run_live_http_test(|cx| async move {
// The only behavioral change from the authenticated positive is
// the bearer principal on the first reverse-response POST.
live_http_legacy_reverse_response_post_probe(&cx, false, false, Some("beta"), false)
.await
});
}
// Exact-2024 era: legacy HTTP SSE reverse responses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_unmatched_response_uses_one_auth_provider_evaluation() {
run_live_http_test(|cx| async move {
let provider_calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("legacy-response-auth-receipt", "1.0.0")
.auth_provider(CountingLegacyHttpAuthProvider {
calls: Arc::clone(&provider_calls),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("legacy auth-receipt bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("legacy auth-receipt address failed: {error}"))?;
let caller_cx = cx.clone();
let client_provider_calls = Arc::clone(&provider_calls);
let mut client = cx
.spawn(move |_client_cx| async move {
let (stream, session_id, _) = open_live_legacy_http_session(
address,
&[("Authorization", "Bearer alpha")],
)
.await?;
if client_provider_calls.load(Ordering::Acquire) != 1 {
return Err(
"legacy SSE opener did not evaluate its provider exactly once"
.to_owned(),
);
}
let unmatched = JsonRpcResponse::success(
RequestId::Number(9_999),
serde_json::json!({"ignored": true}),
);
let body = serde_json::to_vec(&unmatched).map_err(|error| {
format!("unmatched legacy response did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&body,
&[("Authorization", "Bearer alpha")],
),
)
.await?;
if !response.starts_with(b"HTTP/1.1 400") {
return Err(format!(
"unmatched legacy response did not reach its serialized rejection: {response:?}"
));
}
if client_provider_calls.load(Ordering::Acquire) != 2 {
return Err(
"serialized unmatched-response fallback re-evaluated authentication"
.to_owned(),
);
}
drop(stream);
caller_cx.cancel_with(
CancelKind::User,
Some("legacy unmatched auth-receipt probe complete"),
);
Ok::<(), String>(())
})
.map_err(|error| format!("legacy auth-receipt client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
client
.join(&cx)
.await
.map_err(|error| format!("legacy auth-receipt client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("legacy auth-receipt server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "legacy unmatched auth receipt").await
});
}
// Exact-2024 era: legacy HTTP blocking dispatch bridge.
#[cfg(feature = "legacy-2024-11-05")]
async fn live_http_legacy_blocking_bridge_probe(
cx: &Cx,
plant_dispatch_panic: bool,
) -> Result<(), String> {
let endpoint = Server::new("legacy-panic-quarantine", "1.0.0")
.build_http_endpoint("http://legacy.test")
.map_err(|error| format!("dual-era endpoint did not build: {error}"))?;
let mut owned = endpoint
.open_session(cx)
.map_err(|error| format!("legacy HTTP session did not open: {error}"))?;
let ServerHttpEndpointResponse::LegacySse(mut stream) = owned
.handle_async(cx, HttpRequest::new(HttpMethod::Get, "/sse"))
.await
.map_err(|error| format!("legacy SSE admission failed: {error}"))?
else {
return Err("legacy GET did not return an SSE response body".to_owned());
};
let _ = stream
.recv_event(cx)
.map_err(|error| format!("legacy SSE endpoint advertisement failed: {error}"))?;
let session_id = owned.legacy_session_id().to_owned();
let shell = Arc::new(LiveHttpSession {
cancellation: owned.cancellation_control(),
legacy_pending_requests: Arc::clone(&owned.legacy_pending_requests),
session: Mutex::new(Some(owned)),
session_available: Condvar::new(),
closing: AtomicBool::new(false),
});
let sessions = Arc::new(Mutex::new(HashMap::from([(
session_id.clone(),
Arc::clone(&shell),
)])));
let permit = try_reserve_blocking_dispatch()
.ok_or_else(|| "legacy blocking bridge test could not reserve capacity".to_owned())?;
let response = run_live_http_legacy_blocking_dispatch(
cx,
&sessions,
Some(session_id.clone()),
permit,
move |_blocking_cx| {
assert!(
!plant_dispatch_panic,
"planted legacy HTTP blocking-dispatch panic"
);
HttpResponse::ok()
},
)
.await
.map_err(|()| "legacy blocking bridge region did not settle".to_owned())?;
let remains_routable = sessions
.lock()
.map_err(|_| "quarantine registry lock was poisoned".to_owned())?
.contains_key(&session_id);
if plant_dispatch_panic {
if response.status != HttpStatus::INTERNAL_SERVER_ERROR
|| remains_routable
|| !shell.closing.load(Ordering::Acquire)
|| shell.cancellation.legacy_lifecycle.is_live()
|| stream.try_recv_event(cx).is_ok()
|| !matches!(
take_live_http_session(cx, &shell, None),
LiveHttpSessionTake::Unavailable
)
{
return Err(
"panicked bridge did not map to 500 and quarantine only its session shell"
.to_owned(),
);
}
} else if response.status != HttpStatus::OK
|| !remains_routable
|| shell.closing.load(Ordering::Acquire)
{
return Err("successful bridge dispatch mutated session admission state".to_owned());
}
drop(stream);
Ok(())
}
// Exact-2024 era: legacy HTTP blocking dispatch bridge.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_blocking_bridge_returns_dispatch_response() {
run_live_http_test(
|cx| async move { live_http_legacy_blocking_bridge_probe(&cx, false).await },
);
}
// Exact-2024 era: legacy HTTP blocking dispatch bridge.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_blocking_bridge_maps_panic_and_quarantines_session_shell() {
run_live_http_test(|cx| async move {
// This differs from the positive only in the closure's planted
// panic, exercising the production pool/catch/settlement path.
live_http_legacy_blocking_bridge_probe(&cx, true).await
});
}
// Exact-2024 era: drives both eras over one HTTP listener.
#[cfg(feature = "legacy-2024-11-05")]
async fn live_bound_http_cross_era_transcript(
cx: &Cx,
policy: ProtocolPolicy,
) -> Result<(Vec<u8>, Vec<u8>), String> {
let bound = Server::new("live-http-cross-era", "1.0.0")
.protocol_policy(policy)
.map_err(|error| format!("cross-era HTTP policy rejected: {error}"))?
.build()
.bind_http(cx, "127.0.0.1:0")
.await
.map_err(|error| format!("cross-era HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("cross-era HTTP address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |client_cx| async move {
let legacy_request = format!(
"GET /sse HTTP/1.1\r\nHost: {address}\r\nAccept: text/event-stream\r\nConnection: close\r\n\r\n"
)
.into_bytes();
let deadline = client_cx
.now()
.saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
let mut legacy = asupersync::time::timeout_at(
deadline,
AsyncTcpStream::connect(address),
)
.await
.map_err(|_| live_http_test_timeout("cross-era legacy connection"))?
.map_err(|error| format!("cross-era legacy connect failed: {error}"))?;
asupersync::time::timeout_at(deadline, legacy.write_all(&legacy_request))
.await
.map_err(|_| live_http_test_timeout("cross-era legacy request write"))?
.map_err(|error| format!("cross-era legacy write failed: {error}"))?;
asupersync::time::timeout_at(deadline, legacy.flush())
.await
.map_err(|_| live_http_test_timeout("cross-era legacy request flush"))?
.map_err(|error| format!("cross-era legacy flush failed: {error}"))?;
let modern = JsonRpcRequest::new(
SERVER_DISCOVER_METHOD,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
891_i64,
);
let modern_body = serde_json::to_vec(&modern)
.map_err(|error| format!("cross-era modern request did not serialize: {error}"))?;
let modern_request = live_http_post(
"/mcp",
&modern_body,
&[
("Accept", "application/json"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", SERVER_DISCOVER_METHOD),
],
);
let mut legacy_bytes = Vec::new();
if matches!(policy, ProtocolPolicy::Auto) {
let endpoint_prefix = format!("data: http://{address}/messages?session_id=");
read_live_http_until(&mut legacy, &mut legacy_bytes, endpoint_prefix.as_bytes())
.await?;
} else {
read_live_http_to_end(
&mut legacy,
&mut legacy_bytes,
"cross-era legacy rejection EOF",
)
.await?;
}
let modern_response = live_http_exchange(address, modern_request).await?;
caller_cx.cancel_with(CancelKind::User, Some("cross-era HTTP transcript complete"));
Ok::<_, String>((legacy_bytes, modern_response))
})
.map_err(|error| format!("cross-era HTTP client admission failed: {error}"))?;
let serve = bound.serve(cx).await;
let transcript = client
.join(cx)
.await
.map_err(|error| format!("cross-era HTTP client failed: {error:?}"))??;
let shutdown = serve.map_err(|error| format!("cross-era HTTP server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "cross-era HTTP").await?;
Ok(transcript)
}
// Exact-2024 era: used only by the cross-era HTTP transcript.
#[cfg(feature = "legacy-2024-11-05")]
fn assert_cross_era_modern_discovery(response: &[u8]) -> Result<(), String> {
if !response.starts_with(b"HTTP/1.1 200") {
return Err(format!(
"cross-era modern request did not receive HTTP 200: {response:?}"
));
}
let response: JsonRpcResponse = serde_json::from_slice(live_http_response_body(response)?)
.map_err(|error| format!("cross-era modern response was invalid: {error}"))?;
if response.id != Some(891_i64.into()) || response.error.is_some() {
return Err(format!(
"cross-era modern request was rejected: {response:?}"
));
}
if response.result.as_ref().and_then(|result| {
result["_meta"][fastmcp_protocol::SERVER_DISCOVER_SERVER_INFO_META_KEY]["name"].as_str()
}) != Some("live-http-cross-era")
{
return Err(format!(
"cross-era modern discovery result changed its server identity: {response:?}"
));
}
Ok(())
}
async fn live_non_sse_shutdown_fence_transcript(
cx: &Cx,
cancel_before_response: bool,
) -> Result<Vec<u8>, String> {
const REQUEST_ID: i64 = 892;
let control = Arc::new(LiveModernControl::default());
control.install_shutdown_cx(cx.clone());
let bound = Server::new("live-http-shutdown-fence", "1.0.0")
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build()
.bind_http(cx, "127.0.0.1:0")
.await
.map_err(|error| format!("shutdown-fence HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("shutdown-fence HTTP address failed: {error}"))?;
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_modern_controlled_tool",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
REQUEST_ID,
);
let request = live_http_post(
"/mcp",
&serde_json::to_vec(&request)
.map_err(|error| format!("shutdown-fence request did not serialize: {error}"))?,
&[
// Streamable HTTP POSTs must accept both JSON and SSE; the
// server still commits this exchange as an ordinary non-SSE
// JSON response.
("Accept", "application/json, text/event-stream"),
("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION),
("Mcp-Method", "tools/call"),
("Mcp-Name", "live_modern_controlled_tool"),
],
);
let shutdown_applied = Arc::new(AtomicBool::new(false));
let relay_control = Arc::clone(&control);
let relay_applied = Arc::clone(&shutdown_applied);
let caller_cx = cx.clone();
let mut cancellation_relay = cx
.spawn(move |_relay_cx| async move {
while !relay_control.shutdown_requested.load(Ordering::Acquire) {
asupersync::runtime::yield_now().await;
}
let reason = if cancel_before_response {
"shutdown-fence cancellation before H1 response"
} else {
"shutdown-fence response committed"
};
caller_cx.cancel_with(CancelKind::Shutdown, Some(reason));
// The parked accept only wakes on I/O; poke the listener so
// the serve loop observes the shutdown cancellation.
let _ = std::net::TcpStream::connect(address);
relay_applied.store(true, Ordering::Release);
})
.map_err(|error| format!("shutdown-fence relay admission failed: {error}"))?;
let controller = Arc::clone(&control);
let client_shutdown_applied = Arc::clone(&shutdown_applied);
let client = thread::spawn(move || -> Result<Vec<u8>, String> {
struct SignalShutdownOnDrop(Arc<LiveModernControl>);
impl Drop for SignalShutdownOnDrop {
fn drop(&mut self) {
self.0.request_shutdown();
}
}
let _shutdown_on_drop = SignalShutdownOnDrop(Arc::clone(&controller));
let mut stream = std::net::TcpStream::connect(address)
.map_err(|error| format!("shutdown-fence client connect failed: {error}"))?;
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.map_err(|error| format!("shutdown-fence client timeout setup failed: {error}"))?;
std::io::Write::write_all(&mut stream, &request)
.map_err(|error| format!("shutdown-fence client write failed: {error}"))?;
std::io::Write::flush(&mut stream)
.map_err(|error| format!("shutdown-fence client flush failed: {error}"))?;
if !controller.wait_for_started(1, Duration::from_secs(2)) {
return Err("shutdown-fence tool did not enter before its deadline".to_owned());
}
if cancel_before_response {
controller.request_shutdown();
let deadline = Instant::now() + Duration::from_secs(2);
while !client_shutdown_applied.load(Ordering::Acquire) {
if Instant::now() >= deadline {
return Err(
"shutdown-fence runtime relay did not apply cancellation".to_owned()
);
}
thread::yield_now();
}
} else {
controller.release(REQUEST_ID as u64);
}
let mut response = Vec::new();
match std::io::Read::read_to_end(&mut stream, &mut response) {
Ok(_) => {}
Err(error)
if cancel_before_response
&& response.is_empty()
&& matches!(
error.kind(),
std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::UnexpectedEof
| std::io::ErrorKind::TimedOut
| std::io::ErrorKind::WouldBlock
) => {}
Err(error) => {
return Err(format!("shutdown-fence client read failed: {error}"));
}
}
if !cancel_before_response {
controller.request_shutdown();
}
Ok(response)
});
let serve = bound.serve(cx).await;
cancellation_relay
.join(cx)
.await
.map_err(|error| format!("shutdown-fence cancellation relay failed: {error:?}"))?;
let response = client
.join()
.map_err(|_| "shutdown-fence client panicked".to_owned())??;
let shutdown = serve.map_err(|error| format!("shutdown-fence server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "shutdown fence").await?;
Ok(response)
}
// Exact-2024 era: tests Auto classification over HTTP.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_bound_http_auto_coexists_with_exact_legacy_and_modern_requests() {
run_live_http_test(|cx| async move {
let (legacy, modern) =
live_bound_http_cross_era_transcript(&cx, ProtocolPolicy::Auto).await?;
if !legacy.starts_with(b"HTTP/1.1 200")
|| !legacy
.windows(b"/messages?session_id=".len())
.any(|window| window == b"/messages?session_id=")
{
return Err(format!(
"Auto did not retain the exact legacy SSE route: {legacy:?}"
));
}
assert_cross_era_modern_discovery(&modern)?;
Ok(())
});
}
// Exact-2024 era: a dual-era build's ModernOnly refusal of its legacy route.
// A no-legacy build has no such route; see
// no_legacy_http_routes_are_not_found_without_pinning_or_adapter_admission.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_bound_http_modern_only_rejects_the_identical_legacy_half() {
run_live_http_test(|cx| async move {
// This transcript differs from the Auto positive only in the
// policy selected at server construction.
let (legacy, modern) =
live_bound_http_cross_era_transcript(&cx, ProtocolPolicy::ModernOnly).await?;
if !legacy.starts_with(b"HTTP/1.1 400") {
return Err(format!(
"ModernOnly admitted the legacy SSE route: {legacy:?}"
));
}
assert_cross_era_modern_discovery(&modern)?;
Ok(())
});
}
#[test]
fn live_http_non_sse_response_commits_before_shutdown() {
run_live_http_test(|cx| async move {
let response = live_non_sse_shutdown_fence_transcript(&cx, false).await?;
if !response.starts_with(b"HTTP/1.1 200")
|| !response
.windows(b"modern request 892".len())
.any(|window| window == b"modern request 892")
{
return Err(format!(
"a non-SSE response did not commit before shutdown: {response:?}"
));
}
Ok(())
});
}
#[test]
fn live_http_non_sse_shutdown_suppresses_the_pending_response() {
run_live_http_test(|cx| async move {
// This differs from the positive only by cancelling after the
// handler enters and before the ordinary H1 response can commit.
let response = live_non_sse_shutdown_fence_transcript(&cx, true).await?;
if !response.is_empty() {
return Err(format!(
"shutdown wrote a non-SSE response after cancellation: {response:?}"
));
}
Ok(())
});
}
// Exact-2024 era: legacy HTTP SSE session registry.
#[cfg(feature = "legacy-2024-11-05")]
async fn live_http_legacy_admission_cancellation_probe(
cx: &Cx,
cancellation_request_id: i64,
) -> Result<usize, String> {
const INITIALIZE_ID: i64 = 830;
const BLOCKER_ID: i64 = 831;
const TARGET_ID: i64 = 832;
let gate = Arc::new(LiveLegacyRequestGate::default());
let target_calls = Arc::new(AtomicUsize::new(0));
let bound = Server::new("live-http-legacy-admission-cancellation", "1.0.0")
.tool(LiveLegacyAdmissionBlocker {
gate: Arc::clone(&gate),
})
.tool(LiveLegacyAdmissionTarget {
calls: Arc::clone(&target_calls),
})
.build()
.bind_http(cx, "127.0.0.1:0")
.await
.map_err(|error| format!("legacy admission HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("legacy admission HTTP address failed: {error}"))?;
let legacy_sessions = Arc::clone(&bound.legacy_sessions);
let caller_cx = cx.clone();
let gate_for_client = Arc::clone(&gate);
let target_calls_for_client = Arc::clone(&target_calls);
let mut client = cx
.spawn(move |client_cx| async move {
struct CancelServerOnDrop(Cx);
impl Drop for CancelServerOnDrop {
fn drop(&mut self) {
self.0.cancel_with(
CancelKind::User,
Some("legacy admission client exited"),
);
}
}
let _server_cancellation = CancelServerOnDrop(caller_cx.clone());
let (mut sse, session_id, mut received) =
open_live_legacy_http_session(address, &[]).await?;
let session = legacy_sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&session_id)
.cloned()
.ok_or_else(|| "legacy admission session was not registered".to_string())?;
for request in [
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-admission-client", "version": "1.0.0"},
})),
INITIALIZE_ID,
),
JsonRpcRequest::notification("notifications/initialized", None),
] {
let body = serde_json::to_vec(&request).map_err(|error| {
format!("legacy admission setup request did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(&format!("/messages?session_id={session_id}"), &body, &[]),
)
.await?;
if !response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy admission setup request was not accepted: {response:?}"
));
}
}
let blocker = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_legacy_admission_blocker",
"arguments": {},
})),
BLOCKER_ID,
);
let blocker_body = serde_json::to_vec(&blocker).map_err(|error| {
format!("legacy admission blocker request did not serialize: {error}")
})?;
let blocker_request = live_http_post(
&format!("/messages?session_id={session_id}"),
&blocker_body,
&[],
);
let mut blocker = client_cx
.spawn(move |_blocker_cx| async move {
live_http_exchange(address, blocker_request).await
})
.map_err(|error| {
format!("legacy admission blocker task was not admitted: {error}")
})?;
let _release_blocker = LiveLegacyRequestGateRelease {
gate: Arc::clone(&gate_for_client),
};
let blocker_deadline = client_cx
.now()
.saturating_add_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while !gate_for_client.entered.load(Ordering::Acquire) {
client_cx
.checkpoint()
.map_err(|error| format!("legacy admission client stopped: {error}"))?;
asupersync::time::timeout_at(
blocker_deadline,
asupersync::time::sleep(client_cx.now(), Duration::from_millis(1)),
)
.await
.map_err(|_| live_http_test_timeout("legacy admission blocker entry"))?;
}
let target_id = RequestId::Number(TARGET_ID);
let target = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_legacy_admission_target",
"arguments": {},
})),
TARGET_ID,
);
let target_body = serde_json::to_vec(&target).map_err(|error| {
format!("legacy admission target request did not serialize: {error}")
})?;
let target_request = live_http_post(
&format!("/messages?session_id={session_id}"),
&target_body,
&[],
);
let mut target = client_cx
.spawn(move |_target_cx| async move {
live_http_exchange(address, target_request).await
})
.map_err(|error| {
format!("legacy admission target task was not admitted: {error}")
})?;
wait_for_live_http_legacy_admission(&client_cx, &session, &target_id).await?;
let cancellation = JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": cancellation_request_id,
"reason": "legacy HTTP admission cancellation",
})),
);
let cancellation_body = serde_json::to_vec(&cancellation).map_err(|error| {
format!("legacy admission cancellation did not serialize: {error}")
})?;
let cancellation_response = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&cancellation_body,
&[],
),
)
.await?;
if !cancellation_response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy admission cancellation control POST was not accepted: {cancellation_response:?}"
));
}
gate_for_client.release();
let blocker_response = blocker
.join(&client_cx)
.await
.map_err(|error| format!("legacy admission blocker task failed: {error:?}"))??;
if !blocker_response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy admission blocker acknowledgement was unexpected: {blocker_response:?}"
));
}
let target_response = target
.join(&client_cx)
.await
.map_err(|error| format!("legacy admission target task failed: {error:?}"))??;
if !target_response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy admission target acknowledgement was unexpected: {target_response:?}"
));
}
let blocker_marker = format!("\"id\":{BLOCKER_ID}");
read_live_http_until(&mut sse, &mut received, blocker_marker.as_bytes()).await?;
let target_marker = format!("\"id\":{TARGET_ID}");
if cancellation_request_id == TARGET_ID {
if target_calls_for_client.load(Ordering::Acquire) != 0
|| received
.windows(target_marker.len())
.any(|window| window == target_marker.as_bytes())
{
return Err(
"a cancellation admitted before the session mutex still entered its target"
.to_string(),
);
}
let probe_deadline = client_cx
.now()
.saturating_add_nanos(100_000_000);
let mut chunk = [0_u8; 1024];
match asupersync::time::timeout_at(probe_deadline, sse.read(&mut chunk)).await {
Err(_) => {}
Ok(Ok(0)) => {
return Err(
"legacy admission SSE stream closed before the bounded silence probe"
.to_string(),
);
}
Ok(Ok(read)) => {
received.extend_from_slice(&chunk[..read]);
return Err(format!(
"cancelled queued legacy request emitted an SSE frame: {received:?}"
));
}
Ok(Err(error)) => {
return Err(format!(
"legacy admission SSE silence probe failed: {error}"
));
}
}
} else {
read_live_http_until(&mut sse, &mut received, target_marker.as_bytes()).await?;
if target_calls_for_client.load(Ordering::Acquire) != 1 {
return Err(
"one-ID-negative cancellation altered the exact target request"
.to_string(),
);
}
}
caller_cx.cancel_with(CancelKind::User, Some("legacy admission probe complete"));
Ok::<_, String>(())
})
.map_err(|error| format!("legacy admission client task was not admitted: {error}"))?;
let serve = bound.serve(cx).await;
client
.join(cx)
.await
.map_err(|error| format!("legacy admission client task failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("legacy admission HTTP server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "legacy admission HTTP").await?;
Ok(target_calls.load(Ordering::Acquire))
}
// Exact-2024 era: legacy HTTP SSE session registry.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_cancellation_survives_admission_to_active_transition() {
run_live_http_test(|cx| async move {
let calls = live_http_legacy_admission_cancellation_probe(&cx, 832).await?;
if calls != 0 {
return Err(format!(
"cancelled queued legacy HTTP request entered its handler {calls} time(s)"
));
}
Ok(())
});
}
// Exact-2024 era: legacy HTTP SSE session registry.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_cancellation_rejects_the_one_id_negative() {
run_live_http_test(|cx| async move {
// This differs from the positive only in the cancellation request
// ID. It must not affect the queued request with ID 832.
let calls = live_http_legacy_admission_cancellation_probe(&cx, 833).await?;
if calls != 1 {
return Err(format!(
"one-ID-negative cancellation changed target handler calls to {calls}"
));
}
Ok(())
});
}
// Exact-2024 era: legacy HTTP SSE session registry.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_initialize_is_peer_cancellation_protected() {
run_live_http_test(|cx| async move {
const INITIALIZE_ID: i64 = 834;
let bound = Server::new("live-http-legacy-initialize-protection", "1.0.0")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("legacy initialize HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("legacy initialize HTTP address failed: {error}"))?;
let legacy_sessions = Arc::clone(&bound.legacy_sessions);
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |client_cx| async move {
let (mut sse, session_id, mut received) =
open_live_legacy_http_session(address, &[]).await?;
let session = legacy_sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&session_id)
.cloned()
.ok_or_else(|| "legacy initialize session was not registered".to_string())?;
let mut session_hold = LiveHttpSessionMutexHold::new(Arc::clone(&session))?;
let initialize = JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-initialize-client", "version": "1.0.0"},
})),
INITIALIZE_ID,
);
let initialize_body = serde_json::to_vec(&initialize).map_err(|error| {
format!("legacy initialize request did not serialize: {error}")
})?;
let initialize_request = live_http_post(
&format!("/messages?session_id={session_id}"),
&initialize_body,
&[],
);
let mut initialize = client_cx
.spawn(move |_initialize_cx| async move {
live_http_exchange(address, initialize_request).await
})
.map_err(|error| {
format!("legacy initialize task was not admitted: {error}")
})?;
let initialize_id = RequestId::Number(INITIALIZE_ID);
wait_for_live_http_legacy_admission(&client_cx, &session, &initialize_id).await?;
let cancellation = JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": INITIALIZE_ID,
"reason": "initialize must be peer-cancellation protected",
})),
);
let cancellation_body = serde_json::to_vec(&cancellation).map_err(|error| {
format!("legacy initialize cancellation did not serialize: {error}")
})?;
let cancellation_response = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&cancellation_body,
&[],
),
)
.await?;
if !cancellation_response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy initialize cancellation control POST was not accepted: {cancellation_response:?}"
));
}
session_hold.release()?;
let initialize_response = initialize
.join(&client_cx)
.await
.map_err(|error| format!("legacy initialize task failed: {error:?}"))??;
if !initialize_response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy initialize acknowledgement was unexpected: {initialize_response:?}"
));
}
let initialize_marker = format!("\"id\":{INITIALIZE_ID}");
read_live_http_until(&mut sse, &mut received, initialize_marker.as_bytes())
.await?;
let received = std::str::from_utf8(&received).map_err(|error| {
format!("legacy initialize SSE response was not UTF-8: {error}")
})?;
if !received.contains(LEGACY_PROTOCOL_VERSION) {
return Err(
"peer cancellation suppressed the exact legacy initialize response"
.to_string(),
);
}
caller_cx.cancel_with(
CancelKind::User,
Some("legacy initialize protection probe complete"),
);
Ok::<_, String>(())
})
.map_err(|error| {
format!("legacy initialize client task was not admitted: {error}")
})?;
let serve = bound.serve(&cx).await;
client
.join(&cx)
.await
.map_err(|error| format!("legacy initialize client task failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("legacy initialize HTTP server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "legacy initialize HTTP").await?;
Ok(())
});
}
// Exact-2024 era: legacy HTTP SSE session registry.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_cancellation_bypasses_the_busy_session_lock() {
run_live_http_test(|cx| async move {
let started = Arc::new(AtomicBool::new(false));
let observed_cancellation = Arc::new(AtomicBool::new(false));
let observed_by_server = Arc::clone(&observed_cancellation);
let bound = Server::new("live-http-legacy-cancellation", "1.0.0")
.tool(LiveLegacyCancellationTool {
started: Arc::clone(&started),
observed_cancellation: Arc::clone(&observed_cancellation),
})
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("legacy cancellation HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("legacy cancellation HTTP address failed: {error}"))?;
let legacy_sessions = Arc::clone(&bound.legacy_sessions);
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |client_cx| async move {
struct CancelServerOnDrop(Cx);
impl Drop for CancelServerOnDrop {
fn drop(&mut self) {
self.0.cancel_with(
CancelKind::User,
Some("legacy cancellation client exited"),
);
}
}
let _server_cancellation = CancelServerOnDrop(caller_cx.clone());
let mut stream = AsyncTcpStream::connect(address)
.await
.map_err(|error| format!("legacy cancellation SSE connect failed: {error}"))?;
stream
.write_all(
format!(
"GET /sse HTTP/1.1\r\nHost: {address}\r\nAccept: text/event-stream\r\nConnection: keep-alive\r\n\r\n"
)
.as_bytes(),
)
.await
.map_err(|error| format!("legacy cancellation SSE write failed: {error}"))?;
stream
.flush()
.await
.map_err(|error| format!("legacy cancellation SSE flush failed: {error}"))?;
let mut received = Vec::new();
let endpoint_prefix = format!("data: http://{address}/messages?session_id=");
read_live_http_until(&mut stream, &mut received, endpoint_prefix.as_bytes())
.await?;
let received_text = std::str::from_utf8(&received).map_err(|error| {
format!("legacy cancellation SSE endpoint was not UTF-8: {error}")
})?;
let session_id = received_text
.split_once(&endpoint_prefix)
.and_then(|(_, remainder)| remainder.split_whitespace().next())
.ok_or_else(|| {
"legacy cancellation SSE endpoint omitted its session identifier"
.to_string()
})?
.to_owned();
let live_session = legacy_sessions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&session_id)
.cloned()
.ok_or_else(|| {
"legacy cancellation session was not registered".to_string()
})?;
for request in [
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-cancellation-client", "version": "1.0.0"},
})),
821_i64,
),
JsonRpcRequest::notification("notifications/initialized", None),
] {
let body = serde_json::to_vec(&request).map_err(|error| {
format!("legacy cancellation setup request did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&body,
&[],
),
)
.await?;
if !response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy cancellation setup request was not accepted: {response:?}"
));
}
}
let call = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_legacy_cancellation_tool",
"arguments": {},
})),
822_i64,
);
let call_body = serde_json::to_vec(&call).map_err(|error| {
format!("legacy cancellation call did not serialize: {error}")
})?;
let call_request = live_http_post(
&format!("/messages?session_id={session_id}"),
&call_body,
&[],
);
let mut call = client_cx
.spawn(move |_call_cx| async move {
live_http_exchange(address, call_request).await
})
.map_err(|error| {
format!("legacy cancellation call admission failed: {error}")
})?;
let deadline =
Instant::now() + Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS);
while !started.load(Ordering::Acquire) && Instant::now() < deadline {
client_cx
.checkpoint()
.map_err(|error| format!("legacy cancellation client stopped: {error}"))?;
asupersync::time::sleep(client_cx.now(), Duration::from_millis(1)).await;
}
if !started.load(Ordering::Acquire) {
return Err("legacy cancellation handler did not start".to_string());
}
if !live_session
.cancellation
.admissions
.contains(&RequestId::Number(822))
{
return Err(
"running legacy cancellation target lost its admission authority"
.to_string(),
);
}
let cancelled = JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": 822,
"reason": "live HTTP cancellation",
"_meta": {"com.example/application": true},
})),
);
let mut final_metadata = serde_json::to_value(&cancelled).map_err(|error| {
format!("legacy cancellation negative did not serialize: {error}")
})?;
final_metadata["params"]["_meta"]
["io.modelcontextprotocol/subscriptionId"] = serde_json::json!("final");
for (dimension, body, headers) in [
(
"final reserved metadata",
serde_json::to_vec(&final_metadata)
.map_err(|error| error.to_string())?,
Vec::new(),
),
(
"modern binding header",
serde_json::to_vec(&cancelled)
.map_err(|error| error.to_string())?,
vec![("MCP-Protocol-Version", MODERN_PROTOCOL_VERSION)],
),
] {
let rejected = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&body,
&headers,
),
)
.await?;
if !rejected.starts_with(b"HTTP/1.1 400") {
return Err(format!(
"{dimension} cancellation reached mutable state: {rejected:?}"
));
}
if observed_by_server.load(Ordering::Acquire) {
return Err(format!(
"{dimension} cancellation reached the running handler"
));
}
if !matches!(call.try_join(), Ok(None)) {
return Err(format!(
"{dimension} cancellation settled the target request"
));
}
}
let cancelled_body = serde_json::to_vec(&cancelled).map_err(|error| {
format!("legacy cancellation notification did not serialize: {error}")
})?;
let cancelled = live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&cancelled_body,
&[],
),
)
.await?;
if !cancelled.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy cancellation control POST was not accepted while request ran: {cancelled:?}"
));
}
let call_response = call
.join(&client_cx)
.await
.map_err(|error| {
format!("legacy cancellation call task failed: {error:?}")
})??;
if !call_response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy cancellation call acknowledgement was unexpected: {call_response:?}"
));
}
if !observed_by_server.load(Ordering::Acquire) {
return Err(
"legacy cancellation notification did not reach the running handler"
.to_string(),
);
}
caller_cx.cancel_with(CancelKind::User, Some("legacy cancellation complete"));
Ok(())
})
.map_err(|error| format!("legacy cancellation client admission failed: {error}"))?;
let serve = bound.serve(&cx).await;
client
.join(&cx)
.await
.map_err(|error| format!("legacy cancellation client failed: {error:?}"))??;
let shutdown =
serve.map_err(|error| format!("legacy cancellation server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "legacy cancellation").await?;
if !observed_cancellation.load(Ordering::Acquire) {
return Err("live HTTP legacy handler did not observe cancellation".to_string());
}
Ok(())
});
}
// Exact-2024 era: legacy HTTP SSE peer close.
#[cfg(feature = "legacy-2024-11-05")]
async fn live_http_legacy_sse_peer_close_probe(
cx: &Cx,
close_originating_peer: bool,
) -> Result<(), String> {
const INITIALIZE_ID: i64 = 835;
const TOOL_CALL_ID: i64 = 836;
let started = Arc::new(AtomicBool::new(false));
let observed_cancellation = Arc::new(AtomicBool::new(false));
let observed_by_handler = Arc::clone(&observed_cancellation);
let bound = Server::new("live-http-legacy-peer-close", "1.0.0")
.tool(LiveLegacyCancellationTool {
started: Arc::clone(&started),
observed_cancellation: Arc::clone(&observed_cancellation),
})
.build()
.bind_http(cx, "127.0.0.1:0")
.await
.map_err(|error| format!("legacy peer-close HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("legacy peer-close HTTP address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |client_cx| async move {
let (originating_sse, session_id, _) =
open_live_legacy_http_session(address, &[]).await?;
let (other_sse, _, _) = open_live_legacy_http_session(address, &[]).await?;
for request in [
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {
"name": "legacy-peer-close-client",
"version": "1.0.0"
},
})),
INITIALIZE_ID,
),
JsonRpcRequest::notification("notifications/initialized", None),
] {
let body = serde_json::to_vec(&request).map_err(|error| {
format!("legacy peer-close setup request did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(&format!("/messages?session_id={session_id}"), &body, &[]),
)
.await?;
if !response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy peer-close setup request was not accepted: {response:?}"
));
}
}
let call = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_legacy_cancellation_tool",
"arguments": {},
})),
TOOL_CALL_ID,
);
let call_body = serde_json::to_vec(&call).map_err(|error| {
format!("legacy peer-close tool call did not serialize: {error}")
})?;
let mut call = client_cx
.spawn(move |_call_cx| async move {
live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&call_body,
&[],
),
)
.await
})
.map_err(|error| {
format!("legacy peer-close tool call was not admitted: {error}")
})?;
wait_for_live_http_flag(&client_cx, &started, "legacy peer-close handler start")
.await?;
let mut originating_sse = Some(originating_sse);
if close_originating_peer {
drop(originating_sse.take());
wait_for_live_http_flag(
&client_cx,
&observed_by_handler,
"originating legacy SSE peer close cancellation",
)
.await?;
drop(other_sse);
} else {
// The near-identical negative closes only the unrelated
// SSE peer. Its deadline proves cancellation remains
// isolated to the originating exact-2024 session.
drop(other_sse);
let deadline = client_cx.now().saturating_add_nanos(100_000_000);
if asupersync::time::timeout_at(
deadline,
wait_for_live_http_flag(
&client_cx,
&observed_by_handler,
"unrelated legacy SSE peer close",
),
)
.await
.is_ok()
{
return Err(
"unrelated legacy SSE peer close cancelled the active request"
.to_string(),
);
}
drop(originating_sse.take());
wait_for_live_http_flag(
&client_cx,
&observed_by_handler,
"originating legacy SSE peer close after isolation check",
)
.await?;
}
let call_response = call
.join(&client_cx)
.await
.map_err(|error| format!("legacy peer-close tool call failed: {error:?}"))??;
if !call_response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"legacy peer-close tool acknowledgement was unexpected: {call_response:?}"
));
}
caller_cx.cancel_with(CancelKind::User, Some("legacy peer-close probe complete"));
Ok::<_, String>(())
})
.map_err(|error| format!("legacy peer-close client was not admitted: {error}"))?;
let serve = bound.serve(cx).await;
let client_result = client
.join(cx)
.await
.map_err(|error| format!("legacy peer-close client failed: {error:?}"));
client_result??;
let shutdown =
serve.map_err(|error| format!("legacy peer-close server failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "legacy peer-close").await?;
if !observed_cancellation.load(Ordering::Acquire) {
return Err("originating legacy peer close did not cancel its request".to_string());
}
Ok(())
}
// Exact-2024 era: legacy HTTP SSE peer close.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_sse_peer_close_cancels_its_busy_request() {
run_live_http_test(
|cx| async move { live_http_legacy_sse_peer_close_probe(&cx, true).await },
);
}
// Exact-2024 era: legacy HTTP SSE peer close.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_legacy_sse_peer_close_does_not_cancel_another_session() {
run_live_http_test(|cx| async move {
// This differs from the positive only in the SSE peer that closes.
live_http_legacy_sse_peer_close_probe(&cx, false).await
});
}
// Exact-2024 era: hosts its child in a legacy HTTP SSE session.
#[cfg(feature = "legacy-2024-11-05")]
async fn live_http_shutdown_ownership_probe(
cx: &Cx,
cooperate_with_cancellation: bool,
) -> Result<(), String> {
struct ReleaseOnExit(Arc<AtomicBool>);
impl Drop for ReleaseOnExit {
fn drop(&mut self) {
self.0.store(true, Ordering::Release);
}
}
let started = Arc::new(AtomicBool::new(false));
let cancellation_observed = Arc::new(AtomicBool::new(false));
let release = Arc::new(AtomicBool::new(false));
// A failed observation must release the deliberately parked handler
// before runtime teardown joins its blocking thread.
let _release_on_exit = ReleaseOnExit(Arc::clone(&release));
let finished = Arc::new(AtomicBool::new(false));
let tool_name = if cooperate_with_cancellation {
"live_http_cooperative_shutdown_tool"
} else {
"live_http_noncooperative_shutdown_tool"
};
let bound = Server::new("live-http-shutdown-ownership", "1.0.0")
.tool(LiveHttpShutdownTool {
name: tool_name,
cooperate_with_cancellation,
started: Arc::clone(&started),
cancellation_observed: Arc::clone(&cancellation_observed),
release: Arc::clone(&release),
finished: Arc::clone(&finished),
})
.build()
.bind_http(cx, "127.0.0.1:0")
.await
.map_err(|error| format!("shutdown-ownership HTTP bind failed: {error}"))?;
let address = bound
.local_addr()
.map_err(|error| format!("shutdown-ownership HTTP address failed: {error}"))?;
let caller_cx = cx.clone();
let started_by_client = Arc::clone(&started);
let mut client = cx
.spawn(move |client_cx| async move {
let (sse, session_id, _) = open_live_legacy_http_session(address, &[]).await?;
for request in [
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {
"name": "shutdown-ownership-client",
"version": "1.0.0"
},
})),
841_i64,
),
JsonRpcRequest::notification("notifications/initialized", None),
] {
let body = serde_json::to_vec(&request).map_err(|error| {
format!("shutdown-ownership setup request did not serialize: {error}")
})?;
let response = live_http_exchange(
address,
live_http_post(&format!("/messages?session_id={session_id}"), &body, &[]),
)
.await?;
if !response.starts_with(b"HTTP/1.1 202") {
return Err(format!(
"shutdown-ownership setup request was not accepted: {response:?}"
));
}
}
let call = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({"name": tool_name, "arguments": {}})),
842_i64,
);
let call_body = serde_json::to_vec(&call).map_err(|error| {
format!("shutdown-ownership tool call did not serialize: {error}")
})?;
let call = client_cx
.spawn(move |_call_cx| async move {
live_http_exchange(
address,
live_http_post(
&format!("/messages?session_id={session_id}"),
&call_body,
&[],
),
)
.await
})
.map_err(|error| {
format!("shutdown-ownership tool call was not admitted: {error}")
})?;
wait_for_live_http_flag(
&client_cx,
&started_by_client,
"shutdown-ownership handler start",
)
.await?;
caller_cx.cancel_with(
CancelKind::User,
Some("live HTTP shutdown ownership probe complete"),
);
Ok::<_, String>((sse, call))
})
.map_err(|error| format!("shutdown-ownership client was not admitted: {error}"))?;
// This probe deliberately cancels the caller. A timeout_at wrapper
// would mistake its cancellation-aware timer's early completion for
// deadline expiry while the server is still settling its children.
let shutdown = bound
.serve(cx)
.await
.map_err(|error| format!("shutdown-ownership server failed: {error}"))?;
let (_sse, _call) = client
.join(cx)
.await
.map_err(|error| format!("shutdown-ownership client failed: {error:?}"))??;
if !cancellation_observed.load(Ordering::Acquire) {
return Err("live HTTP shutdown did not reach the busy handler".to_owned());
}
match (cooperate_with_cancellation, shutdown) {
(true, HttpServerShutdown::Quiescent) if finished.load(Ordering::Acquire) => Ok(()),
(true, HttpServerShutdown::Quiescent) => {
Err("cooperative HTTP handler was joined before recording completion".to_owned())
}
(true, HttpServerShutdown::Nonquiescent(mut shutdown)) => {
let initial = shutdown.poll_settlement();
let settled = shutdown.settle_for(Duration::from_secs(1)).await;
Err(format!(
"cooperative HTTP shutdown unexpectedly retained children: initial={initial:?}, after bounded settlement={settled:?}"
))
}
(false, HttpServerShutdown::Quiescent) => {
Err("non-cooperative HTTP handler was falsely reported as quiescent".to_owned())
}
(false, HttpServerShutdown::Nonquiescent(mut shutdown)) => {
if finished.load(Ordering::Acquire) {
return Err(
"non-cooperative HTTP handler completed before its retained outcome"
.to_owned(),
);
}
if !matches!(
shutdown.poll_settlement(),
HttpShutdownSettlement::Pending { remaining } if remaining > 0
) {
return Err(
"non-cooperative HTTP shutdown did not return its live child handle"
.to_owned(),
);
}
release.store(true, Ordering::Release);
let deadline = Instant::now() + Duration::from_secs(1);
while !finished.load(Ordering::Acquire) {
if Instant::now() >= deadline {
return Err(
"released non-cooperative HTTP handler did not complete".to_owned()
);
}
asupersync::runtime::yield_now().await;
}
shutdown
.settle(cx)
.await
.map_err(|error| format!("caller-owned HTTP child join failed: {error}"))
}
}
}
// Exact-2024 era: hosts its child in a legacy HTTP SSE session.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_shutdown_joins_a_cooperative_child() {
run_live_http_test(|cx| async move { live_http_shutdown_ownership_probe(&cx, true).await });
}
// Exact-2024 era: hosts its child in a legacy HTTP SSE session.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_http_shutdown_returns_caller_owned_noncooperative_child_for_later_join() {
run_live_http_test(|cx| async move {
// This differs from the positive only in the handler's
// cancellation cooperation, preserving the same live HTTP path.
live_http_shutdown_ownership_probe(&cx, false).await
});
}
#[test]
fn live_http_cancellation_wakes_idle_listener_and_connection_read() {
run_live_http_test(|cx| async move {
let listener_wait_before = live_http_listener_wait_count();
let listener = Server::new("idle-listener-http", "1.0.0")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("idle listener bind failed: {error}"))?;
let caller_cx = cx.clone();
let mut canceller = cx
.spawn(move |canceller_cx| async move {
if let Err(error) = wait_for_live_http_probe(
&canceller_cx,
listener_wait_before,
live_http_listener_wait_count,
)
.await
{
caller_cx
.cancel_with(CancelKind::User, Some("idle listener probe timed out"));
return Err(error);
}
caller_cx.cancel_with(CancelKind::User, Some("idle listener cancellation"));
Ok(())
})
.map_err(|error| format!("idle listener canceller admission failed: {error}"))?;
let shutdown = listener
.serve(&cx)
.await
.map_err(|error| format!("idle listener serve failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "idle listener").await?;
canceller
.join(&cx)
.await
.map_err(|error| format!("idle listener canceller failed: {error:?}"))??;
Ok(())
});
run_live_http_test(|cx| async move {
let read_wait_before = live_http_connection_read_wait_count();
let listener = Server::new("idle-connection-http", "1.0.0")
.build()
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("idle connection bind failed: {error}"))?;
let address = listener
.local_addr()
.map_err(|error| format!("idle connection address failed: {error}"))?;
let caller_cx = cx.clone();
let mut client = cx
.spawn(move |client_cx| async move {
let mut stream = AsyncTcpStream::connect(address)
.await
.map_err(|error| format!("idle client connect failed: {error}"))?;
stream
.write_all(b"POST /mcp HTTP/1.1\r\nHost: loopback\r\n")
.await
.map_err(|error| format!("idle client partial write failed: {error}"))?;
stream
.flush()
.await
.map_err(|error| format!("idle client partial flush failed: {error}"))?;
if let Err(error) = wait_for_live_http_probe(
&client_cx,
read_wait_before,
live_http_connection_read_wait_count,
)
.await
{
caller_cx.cancel_with(
CancelKind::User,
Some("idle connection probe timed out"),
);
return Err(error);
}
caller_cx.cancel_with(CancelKind::User, Some("idle connection cancellation"));
let mut byte = [0_u8; 1];
match stream.read(&mut byte).await {
Ok(0) | Err(_) => Ok(()),
Ok(read) => Err(format!(
"idle connection remained live after server cancellation ({read} byte response)"
)),
}
})
.map_err(|error| format!("idle connection client admission failed: {error}"))?;
let shutdown = listener
.serve(&cx)
.await
.map_err(|error| format!("idle connection serve failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "idle connection").await?;
client
.join(&cx)
.await
.map_err(|error| format!("idle connection client failed: {error:?}"))??;
Ok(())
});
}
// Exact-2024 era: legacy HTTP SSE lifecycle.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn builder_http_endpoint_dispatches_exact_legacy_lifecycle_end_to_end() {
let cx = Cx::for_testing();
let endpoint = Server::new("legacy-http-endpoint", "1.0.0")
.tool(LiveRuntimeListedTool)
.build_http_endpoint("http://legacy.test")
.expect("builder must construct the configured dual-era endpoint");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded live session");
let stream = block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("legacy SSE route must open");
let ServerHttpEndpointResponse::LegacySse(mut stream) = stream else {
panic!("legacy GET must open an exact SSE stream");
};
assert_eq!(
stream
.recv_event(&cx)
.expect("legacy stream must advertise its exact POST endpoint")
.data,
format!(
"http://legacy.test/messages?session_id={}",
session.legacy_session_id()
)
);
let legacy_session_id = session.legacy_session_id().to_owned();
let post = |message: JsonRpcRequest| {
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", legacy_session_id.clone())
.with_body(serde_json::to_vec(&message).expect("legacy request must serialize"))
};
let initialize = JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-http-client", "version": "1.0.0"},
})),
211_i64,
);
let initialized = JsonRpcRequest::notification("notifications/initialized", None);
let call = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_runtime_listed_tool",
"arguments": {},
})),
212_i64,
);
for request in [initialize, initialized, call] {
let response = block_on(session.handle_async(&cx, post(request)))
.expect("advertised legacy POST must be dispatched");
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("legacy POST acknowledgement must be immediate");
};
assert_eq!(response.status, HttpStatus::ACCEPTED);
}
let codec = Codec::new();
let initialize = stream
.recv_event(&cx)
.expect("legacy initialize response must stream over the open SSE session");
assert!(matches!(
codec
.decode_complete_message(initialize.data.as_bytes())
.expect("initialize event must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(211_i64.into())
&& response
.result
.as_ref()
.and_then(|result| result["protocolVersion"].as_str())
== Some(LEGACY_PROTOCOL_VERSION)
));
let call = stream
.recv_event(&cx)
.expect("legacy tool response must stream over the exact SSE session");
assert!(matches!(
codec
.decode_complete_message(call.data.as_bytes())
.expect("tool event must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(212_i64.into())
&& response
.result
.as_ref()
.is_some_and(|result| result.get("resultType").is_none())
));
}
// Exact-2024 era: legacy HTTP SSE opener binding.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn public_http_legacy_sse_binds_opener_principal_before_post() {
let cx = Cx::for_testing();
let verifier = StaticTokenVerifier::new([
("alpha", AuthContext::with_subject("alice")),
("beta", AuthContext::with_subject("bob")),
])
.expect("valid verifier configuration")
.with_allowed_schemes(["Bearer"])
.expect("valid scheme configuration");
// The session binds the context the installed provider admits. For
// these subject-only contexts that includes the owner the provider
// derives (756719c3), in a namespace its clones share. So derive each
// expected fingerprint by admitting the credential through it.
let provider = TokenAuthProvider::new(verifier);
let admitted_fingerprint = |token: &str| {
let authorization = format!("Bearer {token}");
let admitted = provider
.authenticate(
&McpContext::new(cx.clone(), 0),
AuthRequest {
method: "initialize",
params: None,
transport_authorization: Some(&authorization),
request_id: 0,
},
)
.expect("the installed provider admits its own credential");
crate::auth::principal_fingerprint(Some(&admitted)).expect("admitted fingerprint")
};
let alice = admitted_fingerprint("alpha");
let bob = admitted_fingerprint("beta");
let endpoint = Server::new("legacy-sse-owner", "1.0.0")
.tool(LiveRuntimeListedTool)
.auth_provider(provider.clone())
.build_http_endpoint("http://legacy.test")
.expect("dual-era endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded live session");
let denied = block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("unauthenticated GET must be answered");
assert!(
matches!(
denied,
ServerHttpEndpointResponse::Immediate(response)
if response.status == HttpStatus::UNAUTHORIZED
),
"GET /sse without a token must fail closed when a verifier is installed"
);
let stream = block_on(session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Get, "/sse").with_header("Authorization", "Bearer alpha"),
))
.expect("authenticated GET must open");
let ServerHttpEndpointResponse::LegacySse(mut stream) = stream else {
panic!("authenticated GET must open an exact SSE stream");
};
assert!(
session
.legacy_session
.principal_binding()
.verify_existing(alice),
"GET /sse must bind the opener before any POST"
);
assert!(
!session
.legacy_session
.principal_binding()
.verify_existing(bob),
"GET /sse must not bind a different principal"
);
let _ = stream
.recv_event(&cx)
.expect("legacy stream must advertise its exact POST endpoint");
let legacy_session_id = session.legacy_session_id().to_owned();
let post = |message: JsonRpcRequest, token: &str| {
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_header("Authorization", format!("Bearer {token}"))
.with_query("session_id", legacy_session_id.clone())
.with_body(serde_json::to_vec(&message).expect("legacy request must serialize"))
};
let initialize = JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-http-client", "version": "1.0.0"},
})),
311_i64,
);
let initialized = JsonRpcRequest::notification("notifications/initialized", None);
let foreign_call = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_runtime_listed_tool",
"arguments": {},
})),
312_i64,
);
let owned_call = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_runtime_listed_tool",
"arguments": {},
})),
313_i64,
);
for request in [initialize, initialized] {
let response = block_on(session.handle_async(&cx, post(request, "alpha")))
.expect("opener POST must be dispatched");
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("legacy POST acknowledgement must be immediate");
};
assert_eq!(response.status, HttpStatus::ACCEPTED);
}
let codec = Codec::new();
let initialize_event = stream
.recv_event(&cx)
.expect("opener initialize response must stream");
assert!(matches!(
codec
.decode_complete_message(initialize_event.data.as_bytes())
.expect("initialize event must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(311_i64.into()) && response.error.is_none()
));
let foreign = block_on(session.handle_async(&cx, post(foreign_call, "beta")))
.expect("foreign POST must be answered");
let ServerHttpEndpointResponse::Immediate(foreign) = foreign else {
panic!("foreign legacy POST acknowledgement must be immediate");
};
assert_eq!(foreign.status, HttpStatus::ACCEPTED);
let foreign_event = stream
.recv_event(&cx)
.expect("foreign tool response must stream");
assert!(matches!(
codec
.decode_complete_message(foreign_event.data.as_bytes())
.expect("foreign tool event must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(312_i64.into()) && response.error.is_some()
));
let owned = block_on(session.handle_async(&cx, post(owned_call, "alpha")))
.expect("opener tool call must be dispatched");
let ServerHttpEndpointResponse::Immediate(owned) = owned else {
panic!("opener tool call acknowledgement must be immediate");
};
assert_eq!(owned.status, HttpStatus::ACCEPTED);
let owned_event = stream
.recv_event(&cx)
.expect("opener tool response must stream");
assert!(matches!(
codec
.decode_complete_message(owned_event.data.as_bytes())
.expect("opener tool event must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(313_i64.into())
&& response.error.is_none()
&& response
.result
.as_ref()
.is_some_and(|result| result.get("resultType").is_none())
));
}
// Exact-2024 era: legacy HTTP SSE session runtime.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_http_runtime_retains_session_state_progress_and_log_level() {
let cx = Cx::for_testing();
let endpoint = Server::new("legacy-http-runtime", "1.0.0")
.log_level(Level::Debug)
.tool(LiveLegacyRuntimeConnectionTool)
.build_http_endpoint("http://legacy.test")
.expect("builder must construct the configured dual-era endpoint");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded live session");
let ServerHttpEndpointResponse::LegacySse(mut stream) =
block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("legacy SSE route must open")
else {
panic!("legacy GET must open an exact SSE stream");
};
let _endpoint = stream
.recv_event(&cx)
.expect("legacy stream must advertise its POST endpoint");
let session_id = session.legacy_session_id().to_owned();
let post = |message: JsonRpcRequest| {
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session_id.clone())
.with_body(serde_json::to_vec(&message).expect("message must serialize"))
};
for request in [
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-http-client", "version": "1.0.0"},
})),
231_i64,
),
JsonRpcRequest::notification("notifications/initialized", None),
JsonRpcRequest::new(
"logging/setLevel",
Some(serde_json::json!({"level": "debug"})),
232_i64,
),
] {
assert!(matches!(
block_on(session.handle_async(&cx, post(request))),
Ok(ServerHttpEndpointResponse::Immediate(response)) if response.status == HttpStatus::ACCEPTED
));
}
let codec = Codec::new();
let _initialize = stream
.recv_event(&cx)
.expect("initialize response must be delivered");
let _set_level = stream
.recv_event(&cx)
.expect("logging response must be delivered");
for id in [233_i64, 234_i64] {
let call = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_legacy_runtime_connection_tool",
"arguments": {"sample": false},
"_meta": {"progressToken": "legacy-http-runtime-progress"},
})),
id,
);
assert!(matches!(
block_on(session.handle_async(&cx, post(call))),
Ok(ServerHttpEndpointResponse::Immediate(response)) if response.status == HttpStatus::ACCEPTED
));
}
let mut progress_count = 0;
let mut log_count = 0;
let mut results = Vec::new();
for _ in 0..6 {
let event = stream
.recv_event(&cx)
.expect("each legacy handler output must reach the same SSE stream");
match codec
.decode_complete_message(event.data.as_bytes())
.expect("legacy runtime output must remain JSON-RPC")
{
JsonRpcMessage::Request(notification)
if notification.method == "notifications/progress" =>
{
progress_count += 1;
}
JsonRpcMessage::Request(notification)
if notification.method == "notifications/message" =>
{
log_count += 1;
}
JsonRpcMessage::Response(response) => {
results.push(
response
.result
.and_then(|result| {
result["content"][0]["text"].as_str().map(str::to_owned)
})
.expect("tool response must preserve the runtime counter text"),
);
}
message => panic!("unexpected legacy HTTP runtime message: {message:?}"),
}
}
assert_eq!(progress_count, 2);
assert_eq!(log_count, 2);
assert_eq!(
results,
[
"legacy-runtime-1-without-sampling".to_owned(),
"legacy-runtime-2-without-sampling".to_owned(),
]
);
assert_eq!(session.legacy_session.log_level(), Some(LogLevel::Debug));
}
// Exact-2024 era: legacy resource subscriptions over HTTP SSE.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_http_delivers_resource_updates_only_to_the_subscribed_uri() {
// resources/subscribe admits only URIs the router can resolve, so the
// subscribed target must exist as a registered resource.
struct SubscribedFileResource;
impl crate::ResourceHandler for SubscribedFileResource {
fn definition(&self) -> Resource {
Resource {
uri: "file:///subscribed.txt".to_string(),
name: "subscribed".to_string(),
description: None,
mime_type: Some("text/plain".to_string()),
icon: None,
version: None,
tags: vec![],
}
}
fn read(&self, _ctx: &McpContext) -> McpResult<Vec<fastmcp_protocol::ResourceContent>> {
Ok(vec![fastmcp_protocol::ResourceContent {
uri: "file:///subscribed.txt".to_string(),
mime_type: Some("text/plain".to_string()),
text: Some("subscribed".to_string()),
blob: None,
}])
}
}
let cx = Cx::for_testing();
let endpoint = Server::new("legacy-http-resource-update", "1.0.0")
.resource(SubscribedFileResource)
.resource_subscriptions()
.build_http_endpoint("http://legacy.test")
.expect("builder must construct the configured dual-era endpoint");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded live session");
let ServerHttpEndpointResponse::LegacySse(mut stream) =
block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("legacy SSE route must open")
else {
panic!("legacy GET must open an exact SSE stream");
};
let _endpoint = stream.recv_event(&cx).expect("legacy endpoint event");
let session_id = session.legacy_session_id().to_owned();
let post = |message: JsonRpcRequest| {
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session_id.clone())
.with_body(serde_json::to_vec(&message).expect("message must serialize"))
};
for request in [
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-http-client", "version": "1.0.0"},
})),
241_i64,
),
JsonRpcRequest::notification("notifications/initialized", None),
JsonRpcRequest::new(
"resources/subscribe",
Some(serde_json::json!({"uri": "file:///subscribed.txt"})),
242_i64,
),
] {
assert!(matches!(
block_on(session.handle_async(&cx, post(request))),
Ok(ServerHttpEndpointResponse::Immediate(response)) if response.status == HttpStatus::ACCEPTED
));
}
let _initialize = stream.recv_event(&cx).expect("initialize response");
let _subscribe = stream.recv_event(&cx).expect("subscribe response");
assert!(session.notify_legacy_resource_updated("file:///subscribed.txt"));
let update = stream
.recv_event(&cx)
.expect("subscribed update must stream");
assert!(matches!(
Codec::new()
.decode_complete_message(update.data.as_bytes())
.expect("resource update must remain JSON-RPC"),
JsonRpcMessage::Request(notification)
if notification.method == "notifications/resources/updated"
&& notification.params == Some(serde_json::json!({"uri": "file:///subscribed.txt"}))
));
let before = session
.legacy_adapter
.as_ref()
.expect("subscription retains the exact adapter")
.snapshot();
assert!(
!session.notify_legacy_resource_updated("file:///other.txt"),
"changing only the URI must not emit a resource update"
);
assert_eq!(
session
.legacy_adapter
.as_ref()
.expect("negative must retain the exact adapter")
.snapshot(),
before,
"the rejected URI must leave subscription state unchanged"
);
assert!(
stream
.try_recv_event(&cx)
.expect("unsubscribed update check must not fail")
.is_none()
);
}
/// The live exact-2024 HTTP handler serves requests only through its async
/// hook; its synchronous hook refuses instead of blocking on the request.
// Exact-2024 era: legacy HTTP SSE session runtime.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_http_runtime_handler_serves_requests_only_through_the_async_hook() {
struct ListedResource;
impl crate::ResourceHandler for ListedResource {
fn definition(&self) -> Resource {
Resource {
uri: "file:///listed.txt".to_string(),
name: "listed".to_string(),
description: None,
mime_type: Some("text/plain".to_string()),
icon: None,
version: None,
tags: vec![],
}
}
fn read(&self, _ctx: &McpContext) -> McpResult<Vec<fastmcp_protocol::ResourceContent>> {
Ok(vec![])
}
}
let cx = Cx::for_testing();
let endpoint = Server::new("legacy-http-async-hook", "1.0.0")
.resource(ListedResource)
.build_http_endpoint("http://legacy.test")
.expect("builder must construct the configured dual-era endpoint");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded live session");
let ServerHttpEndpointResponse::LegacySse(mut stream) =
block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("legacy SSE route must open")
else {
panic!("legacy GET must open an exact SSE stream");
};
let _endpoint = stream.recv_event(&cx).expect("legacy endpoint event");
let session_id = session.legacy_session_id().to_owned();
let post = |message: JsonRpcRequest| {
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session_id.clone())
.with_body(serde_json::to_vec(&message).expect("message must serialize"))
};
let list = |id: i64| JsonRpcRequest::new("resources/list", Some(serde_json::json!({})), id);
for request in [
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-http-client", "version": "1.0.0"},
})),
251_i64,
),
JsonRpcRequest::notification("notifications/initialized", None),
list(252),
] {
assert!(matches!(
block_on(session.handle_async(&cx, post(request))),
Ok(ServerHttpEndpointResponse::Immediate(response)) if response.status == HttpStatus::ACCEPTED
));
}
let _initialize = stream.recv_event(&cx).expect("initialize response");
let listed = stream.recv_event(&cx).expect("resources/list response");
let JsonRpcMessage::Response(listed) = Codec::new()
.decode_complete_message(listed.data.as_bytes())
.expect("the listing must remain JSON-RPC")
else {
panic!("resources/list must answer with a response");
};
assert!(listed.error.is_none(), "{listed:?}");
assert_eq!(
listed.result.as_ref().expect("listing result")["resources"][0]["uri"],
"file:///listed.txt"
);
// Near-identical negative: the same request on the same adapter's
// synchronous entry point is refused instead of being dispatched.
let binding = session.legacy_binding;
let wire = serde_json::to_value(list(253)).expect("request must serialize");
let refused = session
.legacy_adapter
.as_mut()
.expect("the session installed its exact adapter")
.receive(binding, wire)
.expect("a handler refusal is answered on the wire");
let Legacy2024Outbound::Response(refused) = refused else {
panic!("a request must be answered: {refused:?}");
};
assert_eq!(refused["error"]["code"], -32603, "{refused}");
assert_eq!(
refused["error"]["message"], LIVE_LEGACY_SYNC_DISPATCH_REFUSED,
"{refused}"
);
}
// Exact-2024 era: legacy roots provider over HTTP SSE.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_http_roots_provider_requires_only_the_roots_capability() {
let cx = Cx::for_testing();
let endpoint = Server::new("legacy-http-roots", "1.0.0")
.build_http_endpoint("http://legacy.test")
.expect("builder must construct the configured dual-era endpoint");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded live session");
let ServerHttpEndpointResponse::LegacySse(mut stream) =
block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("legacy SSE route must open")
else {
panic!("legacy GET must open an exact SSE stream");
};
let _endpoint = stream.recv_event(&cx).expect("legacy endpoint event");
let session_id = session.legacy_session_id().to_owned();
let post = |message: JsonRpcRequest| {
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session_id.clone())
.with_body(serde_json::to_vec(&message).expect("message must serialize"))
};
for request in [
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {"roots": {}},
"clientInfo": {"name": "legacy-http-client", "version": "1.0.0"},
})),
251_i64,
),
JsonRpcRequest::notification("notifications/initialized", None),
] {
assert!(matches!(
block_on(session.handle_async(&cx, post(request))),
Ok(ServerHttpEndpointResponse::Immediate(response)) if response.status == HttpStatus::ACCEPTED
));
}
let _initialize = stream.recv_event(&cx).expect("initialize response");
let provider = session
.legacy_roots_provider()
.expect("roots capability must expose the shipped provider");
let roots_task = std::thread::spawn(move || block_on(provider.list_roots()));
let request = stream
.recv_event(&cx)
.expect("roots provider must emit a reverse request");
let JsonRpcMessage::Request(request) = Codec::new()
.decode_complete_message(request.data.as_bytes())
.expect("roots reverse request must remain JSON-RPC")
else {
panic!("roots provider must emit a request");
};
assert_eq!(request.method, "roots/list");
let response = JsonRpcResponse::success(
request.id.expect("roots request must have an ID"),
serde_json::json!({"roots": [{"uri": "file:///work", "name": "work"}]}),
);
let response_post = HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session.legacy_session_id())
.with_body(serde_json::to_vec(&response).expect("response must serialize"));
assert!(matches!(
block_on(session.handle_async(&cx, response_post)),
Ok(ServerHttpEndpointResponse::Immediate(response)) if response.status == HttpStatus::ACCEPTED
));
let roots = roots_task
.join()
.expect("roots task must not panic")
.expect("routed roots response must complete the provider");
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].uri, "file:///work");
}
// Exact-2024 era: legacy roots provider over HTTP SSE.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_http_roots_provider_is_absent_without_roots_capability() {
let cx = Cx::for_testing();
let endpoint = Server::new("legacy-http-roots", "1.0.0")
.build_http_endpoint("http://legacy.test")
.expect("builder must construct the configured dual-era endpoint");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded live session");
let ServerHttpEndpointResponse::LegacySse(mut stream) =
block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("legacy SSE route must open")
else {
panic!("legacy GET must open an exact SSE stream");
};
let _endpoint = stream.recv_event(&cx).expect("legacy endpoint event");
let session_id = session.legacy_session_id().to_owned();
let post = |message: JsonRpcRequest| {
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session_id.clone())
.with_body(serde_json::to_vec(&message).expect("message must serialize"))
};
for request in [
JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-http-client", "version": "1.0.0"},
})),
251_i64,
),
JsonRpcRequest::notification("notifications/initialized", None),
] {
assert!(matches!(
block_on(session.handle_async(&cx, post(request))),
Ok(ServerHttpEndpointResponse::Immediate(response)) if response.status == HttpStatus::ACCEPTED
));
}
let _initialize = stream.recv_event(&cx).expect("initialize response");
assert!(
session.legacy_roots_provider().is_none(),
"changing only the negotiated roots capability must withhold the provider"
);
assert!(
stream
.try_recv_event(&cx)
.expect("capability negative check must not fail")
.is_none()
);
}
// Exact-2024 era: legacy HTTP SSE session lifecycle.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_http_wrong_session_rejection_leaves_the_live_lifecycle_unchanged() {
let cx = Cx::for_testing();
let endpoint = Server::new("legacy-http-wrong-session", "1.0.0")
.tool(LiveRuntimeListedTool)
.build_http_endpoint("http://legacy.test")
.expect("builder must construct the configured dual-era endpoint");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded live session");
let stream = block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("legacy SSE route must open");
let ServerHttpEndpointResponse::LegacySse(mut stream) = stream else {
panic!("legacy GET must open an exact SSE stream");
};
let _endpoint = stream
.recv_event(&cx)
.expect("legacy stream must advertise its exact POST endpoint");
let initialize = JsonRpcRequest::new(
"initialize",
Some(serde_json::json!({
"protocolVersion": LEGACY_PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "legacy-http-client", "version": "1.0.0"},
})),
221_i64,
);
let initialize_response = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session.legacy_session_id())
.with_body(
serde_json::to_vec(&initialize)
.expect("legacy initialize request must serialize"),
),
),
)
.expect("correct-session initialize must dispatch");
assert!(matches!(
initialize_response,
ServerHttpEndpointResponse::Immediate(response) if response.status == HttpStatus::ACCEPTED
));
let _initialize = stream
.recv_event(&cx)
.expect("initialize response must be streamed before lifecycle advance");
let initialized = JsonRpcRequest::notification("notifications/initialized", None);
let initialized_response = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session.legacy_session_id())
.with_body(
serde_json::to_vec(&initialized)
.expect("legacy initialized notification must serialize"),
),
),
)
.expect("correct-session initialized notification must dispatch");
assert!(matches!(
initialized_response,
ServerHttpEndpointResponse::Immediate(response) if response.status == HttpStatus::ACCEPTED
));
let valid = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_runtime_listed_tool",
"arguments": {},
})),
222_i64,
);
let mut wrong_session = HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session.legacy_session_id())
.with_body(serde_json::to_vec(&valid).expect("legacy tool request must serialize"));
wrong_session
.query
.insert("session_id".to_string(), "wrong-session".to_string());
let before = session
.legacy_adapter
.as_ref()
.expect("initialize installs the exact legacy adapter")
.snapshot();
let rejected = block_on(session.handle_async(&cx, wrong_session))
.expect("wrong session must become an HTTP rejection");
assert!(matches!(
rejected,
ServerHttpEndpointResponse::Immediate(response) if response.status == HttpStatus::NOT_FOUND
));
assert_eq!(
session
.legacy_adapter
.as_ref()
.expect("wrong session must not remove the adapter")
.snapshot(),
before,
"changing only the session ID must not mutate the live exact lifecycle"
);
let accepted = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session.legacy_session_id())
.with_body(
serde_json::to_vec(&valid).expect("legacy tool request must serialize"),
),
),
)
.expect("the otherwise identical correct-session request must dispatch");
assert!(matches!(
accepted,
ServerHttpEndpointResponse::Immediate(response) if response.status == HttpStatus::ACCEPTED
));
let call = stream
.recv_event(&cx)
.expect("the unchanged lifecycle must still dispatch the valid call");
assert!(matches!(
Codec::new()
.decode_complete_message(call.data.as_bytes())
.expect("valid call event must remain JSON-RPC"),
JsonRpcMessage::Response(response) if response.id == Some(222_i64.into())
));
}
// Exact-2024 era: legacy HTTP SSE cancellation admissions.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_http_closed_stream_rejects_cancellation_before_authority_mutation() {
let cx = Cx::for_testing();
let endpoint = Server::new("legacy-http-closed-cancellation", "1.0.0")
.build_http_endpoint("http://legacy.test")
.expect("dual-era endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("endpoint must open a bounded session");
let ServerHttpEndpointResponse::LegacySse(stream) =
block_on(session.handle_async(&cx, HttpRequest::new(HttpMethod::Get, "/sse")))
.expect("legacy SSE route must open")
else {
panic!("legacy GET must return a live body");
};
let request_id = RequestId::Number(2_223);
let target = JsonRpcRequest::new("tools/list", None, request_id.clone());
let _admission = session
.legacy_admissions
.admit(
&target,
&session.legacy_session.principal_binding(),
auth::principal_fingerprint(None).expect("anonymous principal is admissible"),
)
.expect("target request admission must remain structurally valid")
.expect("correlated target must allocate cancellation authority");
let target_cancellation = session
.legacy_admissions
.admitted_request_cancellation(&request_id)
.expect("target authority must be observable before peer close");
let control = session.cancellation_control();
drop(stream);
let cancellation = JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": request_id,
"_meta": {"com.example/application": true},
})),
);
let rejected = control
.handle(
&cx,
&HttpRequest::new(HttpMethod::Post, "/messages")
.with_header("content-type", "application/json")
.with_query("session_id", session.legacy_session_id())
.with_body(
serde_json::to_vec(&cancellation)
.expect("legacy cancellation must serialize"),
),
)
.expect("the exact cancellation route owns the HTTP response");
assert_eq!(rejected.status, HttpStatus::SERVICE_UNAVAILABLE);
assert!(!target_cancellation.is_cancel_requested());
assert!(
session
.legacy_admissions
.contains(&RequestId::Number(2_223))
);
assert!(
session
.endpoint_session
.lock()
.expect("transport session lock must remain healthy")
.take_legacy_request()
.is_none(),
"closed-stream cancellation rejection cannot enqueue work"
);
}
// Exact-2024 era: scripts legacy cancellation by original wire ID.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyActiveCancellationRecv {
phase: usize,
started: Arc<AtomicBool>,
observed_cancellation: Arc<AtomicBool>,
responses: Arc<LiveModernResponses>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl TransportRecvHalf for LiveLegacyActiveCancellationRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(exact_legacy_initialize_request(
71,
serde_json::json!("1.0.0"),
)),
1 => Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
))),
2 => Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "live_legacy_cancellation_tool",
"arguments": {},
})),
72_i64,
))),
3 => {
let deadline = Instant::now() + Duration::from_secs(1);
while !self.started.load(Ordering::Acquire) && Instant::now() < deadline {
std::thread::yield_now();
}
if !self.started.load(Ordering::Acquire) {
return Err(TransportError::Timeout);
}
Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": 72,
"reason": "test cancellation",
})),
)))
}
4 => {
let deadline = Instant::now() + Duration::from_secs(1);
while !self.observed_cancellation.load(Ordering::Acquire)
&& Instant::now() < deadline
{
std::thread::yield_now();
}
if self.observed_cancellation.load(Ordering::Acquire) {
Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"ping", None, 73_i64,
)))
} else {
Err(TransportError::Timeout)
}
}
_ => {
if self
.responses
.wait_for_responses(&[73], Duration::from_secs(1))
{
Err(TransportError::Closed)
} else {
Err(TransportError::Timeout)
}
}
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
// Exact-2024 era: legacy cancellation by original wire ID.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_runtime_cancels_exact_legacy_tool_by_its_original_wire_id() {
run_live_legacy_active_cancellation(false);
}
// Exact-2024 era: legacy application tool content option.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_application_content_preserves_active_wire_cancellation() {
run_live_legacy_active_cancellation(true);
}
// Exact-2024 era: legacy cancellation by original wire ID.
#[cfg(feature = "legacy-2024-11-05")]
fn run_live_legacy_active_cancellation(application_content: bool) {
let started = Arc::new(AtomicBool::new(false));
let observed_cancellation = Arc::new(AtomicBool::new(false));
let responses = Arc::new(LiveModernResponses::default());
run_live_split_transport(
Duration::from_secs(20),
Server::new("live-legacy-cancellation", "1.0.0")
.legacy_application_tool_content(application_content)
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(LiveLegacyCancellationTool {
started: Arc::clone(&started),
observed_cancellation: Arc::clone(&observed_cancellation),
})
.build(),
LiveLegacyActiveCancellationRecv {
phase: 0,
started: Arc::clone(&started),
observed_cancellation: Arc::clone(&observed_cancellation),
responses: Arc::clone(&responses),
},
LiveModernSplitSend {
responses: Arc::clone(&responses),
},
)
.expect("accepted active cancellation must close the public transport cleanly");
assert!(observed_cancellation.load(Ordering::Acquire));
assert_eq!(responses.response_count(71), 1);
assert_eq!(responses.response_count(72), 0);
assert_eq!(responses.response_count(73), 1);
assert!(
responses
.response(73)
.is_some_and(|response| response.error.is_none())
);
}
// Exact-2024 era: scripts legacy queued cancellation.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyQueuedCancellationRecv {
phase: usize,
cancelled_request_id: i64,
initialize_send_started: Arc<BoundedTestSignal>,
initialize_send_release: Arc<BoundedTestSignal>,
control: Arc<LiveModernControl>,
responses: Arc<LiveModernResponses>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl TransportRecvHalf for LiveLegacyQueuedCancellationRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(exact_legacy_initialize_request(
75,
serde_json::json!("1.0.0"),
)),
1 => Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
))),
2 => {
// Hold the initialization response attempt so request 80
// is certainly the first queued application request when
// its cancellation reaches the receive pump.
if self.initialize_send_started.wait(Duration::from_secs(2)) {
Ok(legacy_controlled_tool_request(80))
} else {
Err(TransportError::Timeout)
}
}
3 => Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": self.cancelled_request_id,
"reason": "queued cancellation",
})),
))),
4 => {
// `recv` is called again only after the cancellation frame
// from phase 3 has been authenticated and applied.
self.initialize_send_release.raise();
Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"ping", None, 82_i64,
)))
}
5 => {
if self.cancelled_request_id == 80 {
if !self
.responses
.wait_for_responses(&[82], Duration::from_secs(2))
{
return Err(TransportError::Timeout);
}
} else {
if !self.control.wait_for_started(1, Duration::from_secs(2)) {
return Err(TransportError::Timeout);
}
self.control.release(80);
if !self
.responses
.wait_for_responses(&[80, 82], Duration::from_secs(2))
{
return Err(TransportError::Timeout);
}
}
Err(TransportError::Closed)
}
_ => Err(TransportError::Closed),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
// Exact-2024 era: records legacy queued-cancellation output.
#[cfg(feature = "legacy-2024-11-05")]
struct LiveLegacyFirstQueuedSend {
initialize_send_started: Arc<BoundedTestSignal>,
initialize_send_release: Arc<BoundedTestSignal>,
responses: Arc<LiveModernResponses>,
}
#[cfg(feature = "legacy-2024-11-05")]
impl TransportSendHalf for LiveLegacyFirstQueuedSend {
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
self.responses.record(message.clone());
if matches!(
message,
JsonRpcMessage::Response(response) if response.id == Some(75_i64.into())
) {
self.initialize_send_started.raise();
if !self.initialize_send_release.wait(Duration::from_secs(2)) {
return Err(TransportError::Timeout);
}
}
Ok(())
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
// Exact-2024 era: scripts legacy queued cancellation.
#[cfg(feature = "legacy-2024-11-05")]
fn live_legacy_queued_cancellation_transcript(
cancelled_request_id: i64,
) -> (
McpResult<()>,
Arc<LiveModernControl>,
Arc<LiveModernResponses>,
) {
let control = Arc::new(LiveModernControl::default());
let responses = Arc::new(LiveModernResponses::default());
let initialize_send_started = Arc::new(BoundedTestSignal::default());
let initialize_send_release = Arc::new(BoundedTestSignal::default());
let result = run_live_split_transport(
Duration::from_secs(20),
Server::new("live-legacy-queued-cancellation", "1.0.0")
.protocol_policy(ProtocolPolicy::Auto)
.expect("Auto must be available to this test build")
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build(),
LiveLegacyQueuedCancellationRecv {
phase: 0,
cancelled_request_id,
initialize_send_started: Arc::clone(&initialize_send_started),
initialize_send_release: Arc::clone(&initialize_send_release),
control: Arc::clone(&control),
responses: Arc::clone(&responses),
},
LiveLegacyFirstQueuedSend {
initialize_send_started,
initialize_send_release,
responses: Arc::clone(&responses),
},
)
.map_err(McpError::internal_error);
(result, control, responses)
}
// Exact-2024 era: legacy queued cancellation after a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_exact_legacy_first_queued_cancellation_suppresses_only_the_target_response() {
let (result, control, responses) = live_legacy_queued_cancellation_transcript(80);
result.expect("accepted queued cancellation must close the public transport cleanly");
assert_eq!(responses.response_count(75), 1);
assert_eq!(responses.response_count(80), 0);
assert_eq!(responses.response_count(82), 1);
assert_eq!(control.max_active(), 0);
assert!(!control.was_cancelled(80));
assert!(
responses
.response(82)
.is_some_and(|response| response.error.is_none())
);
}
// Exact-2024 era: legacy queued cancellation after a legacy opening.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_exact_legacy_first_queued_unknown_cancellation_preserves_the_request() {
// This differs from the positive only in the cancellation request ID.
let (result, control, responses) = live_legacy_queued_cancellation_transcript(83);
result.expect("unknown queued cancellation must leave the public transport usable");
assert_eq!(responses.response_count(75), 1);
assert_eq!(responses.response_count(80), 1);
assert_eq!(responses.response_count(82), 1);
assert_eq!(control.max_active(), 1);
assert!(!control.was_cancelled(80));
assert!(
responses
.response(80)
.is_some_and(|response| response.error.is_none())
);
assert!(
responses
.response(82)
.is_some_and(|response| response.error.is_none())
);
}
#[test]
fn live_modern_stdio_requests_overlap_and_emit_one_response_per_id() {
let control = Arc::new(LiveModernControl::default());
let responses = Arc::new(LiveModernResponses::default());
let control_for_receive = Arc::clone(&control);
let responses_for_receive = Arc::clone(&responses);
let phase = Arc::new(AtomicUsize::new(0));
let phase_for_receive = Arc::clone(&phase);
let exit_code = run_live_modern_pump(
Server::new("live-modern-overlap", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build(),
move |_cx, _worker_failed| match phase_for_receive.fetch_add(1, Ordering::AcqRel) {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_controlled_tool_request(100)),
2 => {
if control_for_receive.wait_for_started(1, Duration::from_secs(2)) {
Ok(modern_controlled_tool_request(101))
} else {
Err(TransportError::Timeout)
}
}
3 => {
if !control_for_receive.wait_for_started(2, Duration::from_secs(2)) {
return Err(TransportError::Timeout);
}
control_for_receive.release(100);
control_for_receive.release(101);
if responses_for_receive.wait_for_responses(&[100, 101], Duration::from_secs(2))
{
Err(TransportError::Closed)
} else {
Err(TransportError::Timeout)
}
}
_ => Err(TransportError::Closed),
},
Arc::clone(&responses),
);
assert_eq!(exit_code, 0);
assert!(
control.max_active() >= 2,
"two independent modern requests must be live before either is released"
);
assert_eq!(responses.response_count(100), 1);
assert_eq!(responses.response_count(101), 1);
}
#[test]
fn live_modern_split_transport_requests_overlap_and_emit_one_response_per_id() {
let control = Arc::new(LiveModernControl::default());
let responses = Arc::new(LiveModernResponses::default());
let run_result = run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
Server::new("live-modern-split-overlap", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build(),
LiveModernSplitRecv {
phase: 0,
control: Arc::clone(&control),
responses: Arc::clone(&responses),
},
LiveModernSplitSend {
responses: Arc::clone(&responses),
},
);
assert!(
control.max_active() >= 2,
"two split-transport modern requests must overlap before release; \
run_result={run_result:?}, response_1200={}, response_1201={}",
responses.response_count(1200),
responses.response_count(1201),
);
assert_eq!(
responses.response_count(1200),
1,
"first split-transport response count; run_result={run_result:?}"
);
assert_eq!(
responses.response_count(1201),
1,
"second split-transport response count; run_result={run_result:?}"
);
run_result.expect("split transport must drain both independently owned modern requests");
}
// Exact-2024 era: selects LegacyOnly, which a no-legacy build refuses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn live_modern_split_transport_legacy_only_policy_rejects_before_children_start() {
let control = Arc::new(LiveModernControl::default());
let responses = Arc::new(LiveModernResponses::default());
// This differs from the overlap-positive setup only in protocol policy.
run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
Server::new("live-modern-split-overlap", "1.0.0")
.protocol_policy(ProtocolPolicy::LegacyOnly)
.expect("LegacyOnly must be available to this test build")
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build(),
LiveModernSplitRecv {
phase: 0,
control: Arc::clone(&control),
responses: Arc::clone(&responses),
},
LiveModernSplitSend {
responses: Arc::clone(&responses),
},
)
.expect_err("LegacyOnly must reject the identical modern split stream");
assert_eq!(control.max_active(), 0);
assert_eq!(responses.response_count(905), 1);
assert_eq!(responses.response_count(1200), 0);
assert_eq!(responses.response_count(1201), 0);
}
#[test]
fn split_returning_modern_progress_write_success_quiesces() {
split_returning_modern_progress_write_case(false);
}
#[test]
fn split_returning_modern_progress_write_failure_survives_successful_terminal_write() {
split_returning_modern_progress_write_case(true);
}
fn split_returning_modern_progress_write_case(fail_progress_write: bool) {
const REQUEST_ID: i64 = 1210;
#[derive(Default)]
struct Probe {
control: Arc<LiveModernControl>,
responses: Arc<LiveModernResponses>,
receive_waiting: BoundedTestSignal,
progress_attempted: BoundedTestSignal,
progress_attempts: AtomicUsize,
receive_calls: AtomicUsize,
closed_after_terminal: AtomicUsize,
timeout_returns: AtomicUsize,
receive_closes: AtomicUsize,
send_closes: AtomicUsize,
}
struct ProgressWriteRecv {
probe: Arc<Probe>,
}
impl TransportRecvHalf for ProgressWriteRecv {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
match self.probe.receive_calls.fetch_add(1, Ordering::AcqRel) {
0 => Ok(modern_discovery_opening_request()),
1 => {
if !self
.probe
.responses
.wait_for_responses(&[905], Duration::from_secs(2))
{
self.probe.timeout_returns.fetch_add(1, Ordering::AcqRel);
return Err(TransportError::Timeout);
}
Ok(modern_controlled_tool_progress_request(REQUEST_ID))
}
2 => {
// Keep the receive operation open until both the progress
// attempt and the terminal write finish. This exercises
// the failure latch even when EOF wins the next observation.
self.probe.receive_waiting.raise();
if !self.probe.progress_attempted.wait(Duration::from_secs(2)) {
self.probe.control.release_all();
self.probe.timeout_returns.fetch_add(1, Ordering::AcqRel);
return Err(TransportError::Timeout);
}
self.probe.control.release(REQUEST_ID as u64);
if self
.probe
.responses
.wait_for_responses(&[REQUEST_ID], Duration::from_secs(2))
{
self.probe
.closed_after_terminal
.fetch_add(1, Ordering::AcqRel);
Err(TransportError::Closed)
} else {
self.probe.timeout_returns.fetch_add(1, Ordering::AcqRel);
Err(TransportError::Timeout)
}
}
_ => Err(TransportError::Closed),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
self.probe.receive_closes.fetch_add(1, Ordering::AcqRel);
Ok(())
}
}
struct ProgressWriteSend {
probe: Arc<Probe>,
fail_progress_write: bool,
}
impl TransportSendHalf for ProgressWriteSend {
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
if matches!(message, JsonRpcMessage::Request(notification)
if notification.method == "notifications/progress")
{
if !self.probe.receive_waiting.wait(Duration::from_secs(2)) {
self.probe.timeout_returns.fetch_add(1, Ordering::AcqRel);
return Err(TransportError::Timeout);
}
self.probe.progress_attempts.fetch_add(1, Ordering::AcqRel);
self.probe.progress_attempted.raise();
// The sole changed input between the paired cases is the
// result of this progress write. All responses still succeed.
if self.fail_progress_write {
return Err(TransportError::Io(std::io::Error::other(
"progress write failed",
)));
}
}
self.probe.responses.record(message.clone());
Ok(())
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
self.probe.send_closes.fetch_add(1, Ordering::AcqRel);
Ok(())
}
}
let probe = Arc::new(Probe::default());
let server = Server::new("split-progress-write", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.tool(LiveModernControlledTool {
control: Arc::clone(&probe.control),
})
.build();
let active_requests = Arc::clone(&server.active_requests);
let result = run_live_split_transport(
Duration::from_nanos(LIVE_HTTP_TEST_TIMEOUT_NANOS),
server,
ProgressWriteRecv {
probe: Arc::clone(&probe),
},
ProgressWriteSend {
probe: Arc::clone(&probe),
fail_progress_write,
},
);
assert_eq!(probe.receive_calls.load(Ordering::Acquire), 3);
assert_eq!(
probe.closed_after_terminal.load(Ordering::Acquire),
1,
"receive must return clean EOF only after observing the terminal write"
);
assert_eq!(
probe.timeout_returns.load(Ordering::Acquire),
0,
"a fixture timeout must never substitute for the notification failure"
);
assert_eq!(probe.progress_attempts.load(Ordering::Acquire), 1);
assert_eq!(probe.responses.response_count(905), 1);
assert_eq!(probe.responses.response_count(REQUEST_ID), 1);
assert_eq!(probe.receive_closes.load(Ordering::Acquire), 1);
assert_eq!(probe.send_closes.load(Ordering::Acquire), 1);
assert!(probe.control.has_started(REQUEST_ID as u64));
assert_eq!(
probe
.control
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.active,
0,
"handler cleanup must finish before the returning runner settles"
);
assert!(
active_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty(),
"the runner must retire every request owner"
);
let progress_writes = probe
.responses
.messages()
.iter()
.filter(|message| {
matches!(message, JsonRpcMessage::Request(notification)
if notification.method == "notifications/progress")
})
.count();
let terminal = probe
.responses
.response(REQUEST_ID)
.expect("the terminal response write succeeds in both cases");
if fail_progress_write {
assert_eq!(progress_writes, 0);
assert!(terminal.result.is_none());
assert_eq!(
terminal.error.and_then(|error| error.code.as_i32()),
Some(i32::from(McpErrorCode::RequestCancelled))
);
let error = result.expect_err(
"a successful cancellation response cannot erase the progress-write failure",
);
assert!(error.contains("Server transport loop failed"), "{error}");
} else {
assert_eq!(progress_writes, 1);
assert!(terminal.error.is_none());
let result_body = terminal.result.expect("successful tool result");
assert_eq!(result_body["resultType"], "complete");
assert_eq!(result_body["content"][0]["text"], "modern request 1210");
result.expect("successful progress and response writes must quiesce cleanly");
}
}
#[test]
fn live_modern_stdio_metadata_free_cancellation_isolated_to_the_matching_request() {
let control = Arc::new(LiveModernControl::default());
let responses = Arc::new(LiveModernResponses::default());
let control_for_receive = Arc::clone(&control);
let responses_for_receive = Arc::clone(&responses);
let phase = Arc::new(AtomicUsize::new(0));
let phase_for_receive = Arc::clone(&phase);
let exit_code = run_live_modern_pump(
Server::new("live-modern-cancellation", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build(),
move |_cx, _worker_failed| match phase_for_receive.fetch_add(1, Ordering::AcqRel) {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_controlled_tool_request(200)),
2 => {
if control_for_receive.wait_for_started(1, Duration::from_secs(2)) {
Ok(modern_controlled_tool_request(201))
} else {
Err(TransportError::Timeout)
}
}
3 => {
if control_for_receive.wait_for_started(2, Duration::from_secs(2)) {
Ok(modern_cancelled_notification(200))
} else {
Err(TransportError::Timeout)
}
}
4 => {
if !control_for_receive.wait_for_cancellation(200, Duration::from_secs(2)) {
return Err(TransportError::Timeout);
}
control_for_receive.release(201);
if responses_for_receive.wait_for_responses(&[200, 201], Duration::from_secs(2))
{
Err(TransportError::Closed)
} else {
Err(TransportError::Timeout)
}
}
_ => Err(TransportError::Closed),
},
Arc::clone(&responses),
);
assert_eq!(
exit_code,
0,
"modern cancellation pump failed: phase={}, started200={}, started201={}, \
cancelled200={}, cancelled201={}, max_active={}, response200={:?}, response201={:?}",
phase.load(Ordering::Acquire),
control.has_started(200),
control.has_started(201),
control.was_cancelled(200),
control.was_cancelled(201),
control.max_active(),
responses.response(200),
responses.response(201),
);
assert!(control.was_cancelled(200));
assert!(!control.was_cancelled(201));
assert_eq!(
control
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.active,
0,
"all controlled calls must finish cleanup before the pump returns"
);
assert_eq!(responses.response_count(200), 1);
assert_eq!(responses.response_count(201), 1);
assert_eq!(
responses
.response(200)
.and_then(|response| response.error)
.and_then(|error| error.code.as_i32()),
Some(i32::from(McpErrorCode::RequestCancelled))
);
assert!(
responses
.response(201)
.is_some_and(|response| response.error.is_none())
);
}
#[test]
fn public_stdio_final_progress_flushes_during_work_before_log_and_terminal_response() {
const REQUEST_ID: i64 = 206;
let control = Arc::new(LiveModernControl::default());
let responses = Arc::new(LiveModernResponses::default());
let control_for_receive = Arc::clone(&control);
let responses_for_receive = Arc::clone(&responses);
let phase = Arc::new(AtomicUsize::new(0));
let phase_for_receive = Arc::clone(&phase);
let exit_code = run_live_modern_pump(
Server::new("public-stdio-final-progress", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.log_level(Level::Debug)
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build(),
move |_cx, _worker_failed| match phase_for_receive.fetch_add(1, Ordering::AcqRel) {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_controlled_tool_progress_request(REQUEST_ID)),
2 => {
if !control_for_receive.wait_for_started(1, Duration::from_secs(2)) {
return Err(TransportError::Timeout);
}
if !responses_for_receive
.wait_for_notification("notifications/progress", Duration::from_secs(2))
|| responses_for_receive.response_count(REQUEST_ID) != 0
{
return Err(TransportError::Timeout);
}
control_for_receive.release(REQUEST_ID as u64);
if responses_for_receive
.wait_for_responses(&[REQUEST_ID], Duration::from_secs(2))
{
Err(TransportError::Closed)
} else {
Err(TransportError::Timeout)
}
}
_ => Err(TransportError::Closed),
},
Arc::clone(&responses),
);
assert_eq!(exit_code, 0);
let messages = responses.messages();
let progress = messages
.iter()
.enumerate()
.find_map(|(index, message)| match message {
JsonRpcMessage::Request(notification)
if notification.method == "notifications/progress" =>
{
Some((index, notification))
}
_ => None,
})
.expect("the public stdio request must emit its coalesced progress");
assert_eq!(
messages
.iter()
.filter(|message| matches!(
message,
JsonRpcMessage::Request(notification)
if notification.method == "notifications/progress"
))
.count(),
1,
"the outer final-progress runtime must retain only its latest update"
);
let progress_wire = serde_json::to_string(
progress
.1
.params
.as_ref()
.expect("coalesced progress must have parameters"),
)
.expect("coalesced progress parameters must serialize");
assert!(progress_wire.contains("\"progressToken\":\"public-stdio-final-progress\""));
assert!(progress_wire.contains("\"progress\":12000"));
assert!(progress_wire.contains("\"total\":11999"));
assert!(progress_wire.contains("\"message\":\"latest\""));
let log_index = messages
.iter()
.position(|message| {
matches!(message, JsonRpcMessage::Request(notification) if notification.method == "notifications/message")
})
.expect("the final log must be written after progress");
let response_index = messages
.iter()
.position(|message| {
matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(REQUEST_ID.into()) && response.error.is_none()
)
})
.expect("the terminal response must be written");
assert!(
progress.0 < log_index && log_index < response_index,
"the rate-flushed progress must precede the final log and terminal response"
);
}
#[test]
fn public_stdio_final_progress_cancellation_suppresses_post_cancellation_frames() {
const REQUEST_ID: i64 = 207;
let control = Arc::new(LiveModernControl::default());
let responses = Arc::new(LiveModernResponses::default());
let control_for_receive = Arc::clone(&control);
let responses_for_receive = Arc::clone(&responses);
let phase = Arc::new(AtomicUsize::new(0));
let phase_for_receive = Arc::clone(&phase);
let exit_code = run_live_modern_pump(
Server::new("public-stdio-final-progress-cancel", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.log_level(Level::Debug)
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build(),
move |_cx, _worker_failed| match phase_for_receive.fetch_add(1, Ordering::AcqRel) {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_controlled_tool_progress_request(REQUEST_ID)),
2 => {
if control_for_receive.wait_for_started(1, Duration::from_secs(2)) {
if responses_for_receive
.wait_for_notification("notifications/progress", Duration::from_secs(2))
{
Ok(modern_cancelled_notification(REQUEST_ID as u64))
} else {
Err(TransportError::Timeout)
}
} else {
Err(TransportError::Timeout)
}
}
3 => {
if !control_for_receive
.wait_for_cancellation(REQUEST_ID as u64, Duration::from_secs(2))
{
return Err(TransportError::Timeout);
}
if responses_for_receive
.wait_for_responses(&[REQUEST_ID], Duration::from_secs(2))
{
Err(TransportError::Closed)
} else {
Err(TransportError::Timeout)
}
}
_ => Err(TransportError::Closed),
},
Arc::clone(&responses),
);
assert_eq!(exit_code, 0);
assert!(control.was_cancelled(REQUEST_ID as u64));
let messages = responses.messages();
let progress_index = messages
.iter()
.position(|message| {
matches!(message, JsonRpcMessage::Request(notification) if notification.method == "notifications/progress")
})
.expect("the rate timer must flush progress before the one-variable cancellation");
assert_eq!(
responses
.response(REQUEST_ID)
.and_then(|response| response.error)
.and_then(|error| error.code.as_i32()),
Some(i32::from(McpErrorCode::RequestCancelled))
);
assert!(
!messages[progress_index.saturating_add(1)..]
.iter()
.any(|message| matches!(
message,
JsonRpcMessage::Request(notification)
if notification.method == "notifications/progress"
|| notification.method == "notifications/message"
)),
"once cancellation wins, no final progress or log frame may follow it"
);
}
#[test]
fn public_stdio_cancellation_fence_drops_interleaved_progress_before_commit() {
const REQUEST_ID: i64 = 208;
let control = Arc::new(LiveModernControl::default());
let responses = Arc::new(LiveModernResponses::default());
let interlock = install_stdio_progress_commit_interlock(REQUEST_ID as u64);
let control_for_receive = Arc::clone(&control);
let responses_for_receive = Arc::clone(&responses);
let interlock_for_receive = Arc::clone(&interlock);
let phase = Arc::new(AtomicUsize::new(0));
let phase_for_receive = Arc::clone(&phase);
let exit_code = run_live_modern_pump(
Server::new("public-stdio-progress-cancellation-fence", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.log_level(Level::Debug)
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build(),
move |_cx, _worker_failed| match phase_for_receive.fetch_add(1, Ordering::AcqRel) {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_controlled_tool_progress_request(REQUEST_ID)),
2 => {
if interlock_for_receive.wait_until_entered(Duration::from_secs(2)) {
// The progress callback is now stopped immediately
// before acquiring the writer. The receiver commits
// this sole changed input under the same writer fence.
Ok(modern_cancelled_notification(REQUEST_ID as u64))
} else {
Err(TransportError::Timeout)
}
}
3 => {
release_stdio_progress_commit_interlock();
if !control_for_receive
.wait_for_cancellation(REQUEST_ID as u64, Duration::from_secs(2))
{
return Err(TransportError::Timeout);
}
if responses_for_receive
.wait_for_responses(&[REQUEST_ID], Duration::from_secs(2))
{
Err(TransportError::Closed)
} else {
Err(TransportError::Timeout)
}
}
_ => Err(TransportError::Closed),
},
Arc::clone(&responses),
);
release_stdio_progress_commit_interlock();
assert_eq!(exit_code, 0);
assert!(control.was_cancelled(REQUEST_ID as u64));
assert_eq!(
responses
.response(REQUEST_ID)
.and_then(|response| response.error)
.and_then(|error| error.code.as_i32()),
Some(i32::from(McpErrorCode::RequestCancelled))
);
assert!(
!responses.messages().iter().any(|message| matches!(
message,
JsonRpcMessage::Request(notification)
if notification.method == "notifications/progress"
|| notification.method == "notifications/message"
)),
"a cancellation that owns the writer fence must suppress interleaved progress and final logs"
);
}
#[test]
fn live_modern_stdio_shutdown_cancels_an_in_flight_notification() {
let control = Arc::new(LiveModernNotificationControl::default());
let control_for_receive = Arc::clone(&control);
let phase = Arc::new(AtomicUsize::new(0));
let phase_for_receive = Arc::clone(&phase);
let exit_code = run_live_modern_pump(
Server::new("live-modern-notification-shutdown", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.middleware(LiveModernBlockingNotificationMiddleware {
control: Arc::clone(&control),
})
.build(),
move |_cx, _worker_failed| match phase_for_receive.fetch_add(1, Ordering::AcqRel) {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_initialized_notification()),
2 => {
if control_for_receive.wait_for_started(Duration::from_secs(2)) {
Err(TransportError::Closed)
} else {
Err(TransportError::Timeout)
}
}
_ => Err(TransportError::Closed),
},
Arc::new(LiveModernResponses::default()),
);
assert_eq!(exit_code, 0);
assert!(
control.was_cancelled(),
"stdio shutdown must cancel a modern notification without a request id"
);
}
#[test]
fn live_modern_stdio_rejects_the_one_request_beyond_bounded_capacity() {
let control = Arc::new(LiveModernControl::default());
let responses = Arc::new(LiveModernResponses::default());
let control_for_receive = Arc::clone(&control);
let responses_for_receive = Arc::clone(&responses);
let phase = Arc::new(AtomicUsize::new(0));
let phase_for_receive = Arc::clone(&phase);
let exit_code = run_live_modern_pump(
Server::new("live-modern-backpressure", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.tool(LiveModernControlledTool {
control: Arc::clone(&control),
})
.build(),
move |_cx, _worker_failed| {
let phase = phase_for_receive.fetch_add(1, Ordering::AcqRel);
match phase {
0 => Ok(modern_discovery_opening_request()),
1 => {
if responses_for_receive.wait_for_responses(&[905], Duration::from_secs(2))
{
Ok(modern_controlled_tool_request(1001))
} else {
Err(TransportError::Timeout)
}
}
2..=65 => Ok(modern_controlled_tool_request(1000 + phase as i64)),
66 => {
if !responses_for_receive
.wait_for_responses(&[1065], Duration::from_secs(2))
{
return Err(TransportError::Timeout);
}
control_for_receive.release_all();
let admitted = (1001..=1064).collect::<Vec<_>>();
if responses_for_receive
.wait_for_responses(&admitted, Duration::from_secs(5))
{
Err(TransportError::Closed)
} else {
Err(TransportError::Timeout)
}
}
_ => Err(TransportError::Closed),
}
},
Arc::clone(&responses),
);
assert_eq!(exit_code, 0);
assert_eq!(responses.response_count(1065), 1);
assert_eq!(
responses
.response(1065)
.and_then(|response| response.error)
.and_then(|error| error.code.as_i32()),
Some(RESOURCE_EXHAUSTED_ERROR_CODE)
);
for id in 1001..=1064 {
assert_eq!(
responses.response_count(id),
1,
"accepted id {id} must finalize once"
);
}
}
#[test]
fn reality_check_regression_returning_transport_treats_clean_stop_as_success() {
for receive in [
ReturningProbeReceive::Closed,
ReturningProbeReceive::Cancelled,
] {
Server::new("returning-clean-stop-test", "1.0.0")
.build()
.run_transport_returning_with_cx(
&Cx::for_testing(),
ReturningProbeTransport {
receive,
fail_send: false,
fail_close: false,
close_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect("clean EOF and transport cancellation must return success");
}
}
#[test]
fn reality_check_regression_returning_transport_propagates_fatal_receive_error() {
let error = Server::new("returning-receive-failure-test", "1.0.0")
.build()
.run_transport_returning_with_cx(
&Cx::for_testing(),
ReturningProbeTransport {
receive: ReturningProbeReceive::Timeout,
fail_send: false,
fail_close: false,
close_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect_err("fatal transport receive failure must propagate");
assert_eq!(error.code, McpErrorCode::InternalError);
assert_eq!(
error.data.as_ref().and_then(|data| data["stage"].as_str()),
Some("receive")
);
assert_eq!(
error.data.as_ref().and_then(|data| data["kind"].as_str()),
Some("timeout")
);
}
// Exact-2024 era: answers an un-negotiated ping, which only the dual-era loop admits.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn reality_check_regression_returning_transport_propagates_response_send_error() {
let error = Server::new("returning-send-failure-test", "1.0.0")
.build()
.run_transport_returning_with_cx(
&Cx::for_testing(),
ReturningProbeTransport {
receive: ReturningProbeReceive::PingThenClosed,
fail_send: true,
fail_close: false,
close_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect_err("response send failure must propagate");
assert_eq!(error.code, McpErrorCode::InternalError);
assert_eq!(
error.data.as_ref().and_then(|data| data["stage"].as_str()),
Some("send")
);
assert_eq!(
error.data.as_ref().and_then(|data| data["kind"].as_str()),
Some("io")
);
}
#[test]
fn reality_check_regression_returning_transport_closes_and_propagates_close_failure() {
let close_calls = Arc::new(AtomicUsize::new(0));
let error = Server::new("returning-close-failure-test", "1.0.0")
.build()
.run_transport_returning_with_cx(
&Cx::for_testing(),
ReturningProbeTransport {
receive: ReturningProbeReceive::Closed,
fail_send: false,
fail_close: true,
close_calls: Arc::clone(&close_calls),
},
)
.expect_err("transport close failure must propagate");
assert_eq!(close_calls.load(Ordering::Acquire), 1);
assert_eq!(
error.data.as_ref().and_then(|data| data["stage"].as_str()),
Some("close")
);
assert_eq!(
error.data.as_ref().and_then(|data| data["kind"].as_str()),
Some("io")
);
}
#[test]
fn shared_transport_close_retires_io_once_positive() {
let recv_calls = Arc::new(AtomicUsize::new(0));
let send_calls = Arc::new(AtomicUsize::new(0));
let close_calls = Arc::new(AtomicUsize::new(0));
let transport = SharedTransport::new(SharedTransportCountingTransport {
recv_calls: Arc::clone(&recv_calls),
send_calls: Arc::clone(&send_calls),
close_calls: Arc::clone(&close_calls),
fail_close: false,
});
let message = JsonRpcMessage::Request(JsonRpcRequest::new("ping", None, 1_i64));
assert!(transport.send(&Cx::for_testing(), &message).is_ok());
assert_eq!(send_calls.load(Ordering::Acquire), 1);
let cx = Cx::for_testing();
assert!(transport.close(&cx).is_ok());
assert!(matches!(
transport.send(&Cx::for_testing(), &message),
Err(TransportError::Closed)
));
assert!(matches!(
transport.recv(&Cx::for_testing()),
Err(TransportError::Closed)
));
assert!(transport.close(&cx).is_ok());
assert_eq!(send_calls.load(Ordering::Acquire), 1);
assert_eq!(recv_calls.load(Ordering::Acquire), 0);
assert_eq!(close_calls.load(Ordering::Acquire), 1);
let sibling_send_calls = Arc::new(AtomicUsize::new(0));
let sibling = SharedTransport::new(SharedTransportCountingTransport {
recv_calls: Arc::new(AtomicUsize::new(0)),
send_calls: Arc::clone(&sibling_send_calls),
close_calls: Arc::new(AtomicUsize::new(0)),
fail_close: false,
});
assert!(sibling.send(&Cx::for_testing(), &message).is_ok());
assert_eq!(sibling_send_calls.load(Ordering::Acquire), 1);
}
#[test]
fn shared_transport_close_failure_keeps_io_retired_negative() {
let recv_calls = Arc::new(AtomicUsize::new(0));
let send_calls = Arc::new(AtomicUsize::new(0));
let close_calls = Arc::new(AtomicUsize::new(0));
let transport = SharedTransport::new(SharedTransportCountingTransport {
recv_calls: Arc::clone(&recv_calls),
send_calls: Arc::clone(&send_calls),
close_calls: Arc::clone(&close_calls),
fail_close: true,
});
let message = JsonRpcMessage::Request(JsonRpcRequest::new("ping", None, 1_i64));
assert!(transport.send(&Cx::for_testing(), &message).is_ok());
assert_eq!(send_calls.load(Ordering::Acquire), 1);
let cx = Cx::for_testing();
assert!(transport.close(&cx).is_err());
assert!(matches!(
transport.send(&Cx::for_testing(), &message),
Err(TransportError::Closed)
));
assert!(matches!(
transport.recv(&Cx::for_testing()),
Err(TransportError::Closed)
));
assert!(transport.close(&cx).is_ok());
assert_eq!(send_calls.load(Ordering::Acquire), 1);
assert_eq!(recv_calls.load(Ordering::Acquire), 0);
assert_eq!(close_calls.load(Ordering::Acquire), 1);
let sibling_send_calls = Arc::new(AtomicUsize::new(0));
let sibling = SharedTransport::new(SharedTransportCountingTransport {
recv_calls: Arc::new(AtomicUsize::new(0)),
send_calls: Arc::clone(&sibling_send_calls),
close_calls: Arc::new(AtomicUsize::new(0)),
fail_close: false,
});
assert!(sibling.send(&Cx::for_testing(), &message).is_ok());
assert_eq!(sibling_send_calls.load(Ordering::Acquire), 1);
}
/// Unsplit transport whose `recv` blocks until the test releases it, and
/// which records each written method with whether `recv` had returned.
struct BlockingRecvTransport {
entered: std::sync::mpsc::Sender<()>,
release: std::sync::mpsc::Receiver<()>,
recv_returned: bool,
fail_send: bool,
written: Arc<Mutex<Vec<(String, bool)>>>,
}
impl Transport for BlockingRecvTransport {
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
let JsonRpcMessage::Request(request) = message else {
panic!("this fixture only writes requests");
};
self.written
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push((request.method.clone(), self.recv_returned));
if self.fail_send {
return Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"peer closed",
)));
}
Ok(())
}
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
self.entered.send(()).expect("test observes recv entry");
self.release.recv().expect("test releases recv");
self.recv_returned = true;
Ok(JsonRpcMessage::Request(JsonRpcRequest::new(
"ping", None, 1_i64,
)))
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
/// bd-8bcfq: sends issued while an unsplit transport's `recv` blocks must
/// neither fail nor wait for it; they are written in order once it returns.
/// Returns what was written, the latched output failure, and the result of
/// one more send issued after `recv` returned.
fn shared_transport_send_during_blocked_recv_case(
fail_send: bool,
) -> (Vec<(String, bool)>, bool, Result<(), TransportError>) {
let (entered_tx, entered_rx) = std::sync::mpsc::channel();
let (release_tx, release_rx) = std::sync::mpsc::channel();
let written = Arc::new(Mutex::new(Vec::new()));
let failure = Arc::new(AtomicBool::new(false));
let shared = SharedTransport::with_output_failure(
BlockingRecvTransport {
entered: entered_tx,
release: release_rx,
recv_returned: false,
fail_send,
written: Arc::clone(&written),
},
Arc::clone(&failure),
);
let receiver = shared.clone();
let pump = std::thread::spawn(move || receiver.recv(&Cx::for_testing()));
entered_rx
.recv_timeout(Duration::from_secs(5))
.expect("recv must be entered");
let sender = shared.clone();
let (sent_tx, sent_rx) = std::sync::mpsc::channel();
let writer = std::thread::spawn(move || {
for method in ["notifications/first", "notifications/second"] {
let notification =
JsonRpcMessage::Request(JsonRpcRequest::notification(method, None));
sent_tx
.send(sender.send(&Cx::for_testing(), ¬ification))
.expect("test collects send results");
}
});
for _ in 0..2 {
let sent = sent_rx
.recv_timeout(Duration::from_secs(5))
.expect("a send must not wait for a blocked recv");
assert!(sent.is_ok(), "{sent:?}");
}
writer.join().expect("writer thread");
assert!(
written
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty(),
"nothing can be written while recv owns the handle"
);
assert!(!failure.load(Ordering::Acquire));
release_tx.send(()).expect("recv is still blocked");
let received = pump.join().expect("pump thread");
assert!(matches!(received, Ok(JsonRpcMessage::Request(_))));
let after = shared.send(
&Cx::for_testing(),
&JsonRpcMessage::Request(JsonRpcRequest::notification("notifications/third", None)),
);
let written = written
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
(written, failure.load(Ordering::Acquire), after)
}
#[test]
fn shared_transport_writes_sends_queued_during_recv_in_order_positive() {
let (written, failed, after) = shared_transport_send_during_blocked_recv_case(false);
assert_eq!(
written,
[
("notifications/first".to_owned(), true),
("notifications/second".to_owned(), true),
("notifications/third".to_owned(), true),
]
);
assert!(!failed);
assert!(after.is_ok(), "{after:?}");
}
#[test]
fn shared_transport_latches_failure_of_queued_send_negative() {
let (written, failed, after) = shared_transport_send_during_blocked_recv_case(true);
// The first queued write fails, so the rest of the queue is dropped,
// the pump's failure flag is latched, and later sends are refused
// rather than written out of order.
assert_eq!(written, [("notifications/first".to_owned(), true)]);
assert!(failed);
assert!(after.is_err(), "{after:?}");
}
#[test]
fn reality_check_regression_returning_transport_preserves_run_and_close_failures() {
let error = Server::new("returning-combined-failure-test", "1.0.0")
.build()
.run_transport_returning_with_cx(
&Cx::for_testing(),
ReturningProbeTransport {
receive: ReturningProbeReceive::Timeout,
fail_send: false,
fail_close: true,
close_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect_err("run and close failures must both propagate");
let data = error.data.expect("combined failure data");
assert_eq!(data["stage"], "run_and_close");
assert_eq!(data["kind"], "multiple_failures");
assert_eq!(data["run"]["data"]["stage"], "receive");
assert_eq!(data["run"]["data"]["kind"], "timeout");
assert_eq!(data["close"]["data"]["stage"], "close");
assert_eq!(data["close"]["data"]["kind"], "io");
}
// Exact-2024 era: legacy dispatch and region-close error combination.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_request_dispatch_and_region_close_retain_simultaneous_failures() {
let combined = combine_legacy_dispatch_and_close::<()>(
Err(Legacy2024HandlerError::new("dispatch failed")),
Err(Legacy2024HandlerError::new("region close failed")),
)
.expect_err("both request failures must remain observable");
assert_eq!(combined.code().as_i32(), Some(-32603));
assert!(combined.message().contains("dispatch failed"));
assert!(combined.message().contains("region close failed"));
}
// Exact-2024 era: used only by LegacyOnly returning-loop tests.
#[cfg(feature = "legacy-2024-11-05")]
struct LegacyReturningOwnershipTool {
regions: Arc<Mutex<Vec<RegionId>>>,
cancel_first: bool,
}
#[cfg(feature = "legacy-2024-11-05")]
impl ToolHandler for LegacyReturningOwnershipTool {
fn definition(&self) -> Tool {
Tool {
name: "legacy_returning_ownership_tool".to_owned(),
description: Some("Records exact legacy request ownership".to_owned()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _arguments: serde_json::Value) -> McpResult<Vec<Content>> {
self.regions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(ctx.cx().region_id());
if self.cancel_first && ctx.request_id() == 201 {
// Ambient request-region cancellation remains response-bearing;
// explicit request-token cancellation is reserved for accepted
// peer cancellation and intentionally suppresses its response.
ctx.cx().cancel_with(
CancelKind::User,
Some("legacy request ambient cancellation fixture"),
);
return Err(McpError::request_cancelled());
}
Ok(vec![Content::text(format!(
"legacy-request-{}",
ctx.request_id()
))])
}
}
// Exact-2024 era: used only by tests that open with a legacy handshake.
#[cfg(feature = "legacy-2024-11-05")]
fn run_returning_transport_with_test_runtime<T>(server: Server, transport: T) -> McpResult<()>
where
T: Transport + Send + 'static,
{
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("legacy returning test reactor must initialize"))
.blocking_threads(2, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("legacy returning test runtime must initialize");
let (result_sender, result_receiver) = sync_channel(1);
runtime.block_on(async move {
let cx = Cx::current().expect("legacy returning test runtime must install Cx");
let pump = cx
.spawn_blocking(move |pump_cx| {
let result = server.run_transport_returning_with_cx(&pump_cx, transport);
let _ = result_sender.send(result);
})
.map_err(|error| McpError::internal_error(error.to_string()))?;
let deadline = cx.now().saturating_add_nanos(RETURNING_PUMP_WATCHDOG_NANOS);
loop {
match result_receiver.try_recv() {
Ok(result) => {
drop(pump);
break result;
}
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
return Err(McpError::internal_error(
"legacy returning test pump exited without a result",
));
}
Err(std::sync::mpsc::TryRecvError::Empty) => {
asupersync::time::timeout_at(
deadline,
asupersync::time::sleep(cx.now(), Duration::from_millis(1)),
)
.await
.map_err(|_| McpError::internal_error("legacy returning test timed out"))?;
}
}
}
})
}
/// Exact acceptance ID: successful legacy request completion followed by
/// sibling reuse on the same caller-owned custom connection.
// Exact-2024 era: selects LegacyOnly, which a no-legacy build refuses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn fnd_04_b_positive() {
let regions = Arc::new(Mutex::new(Vec::new()));
let sent = Arc::new(Mutex::new(Vec::new()));
run_returning_transport_with_test_runtime(
Server::new("fnd-04-b-positive", "1.0.0")
.protocol_policy(ProtocolPolicy::LegacyOnly)
.expect("LegacyOnly must be available to this test build")
.tool(LegacyReturningOwnershipTool {
regions: Arc::clone(®ions),
cancel_first: false,
})
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
exact_legacy_initialize_request(100, serde_json::json!("1.0.0")),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "legacy_returning_ownership_tool",
"arguments": {},
})),
101_i64,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "legacy_returning_ownership_tool",
"arguments": {},
})),
102_i64,
)),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect("legacy custom returning script must complete");
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for id in [100_i64, 101, 102] {
assert_eq!(
sent.iter()
.filter(|message| matches!(message, JsonRpcMessage::Response(response) if response.id == Some(id.into())))
.count(),
1,
"legacy response {id} must be committed exactly once"
);
}
let regions = regions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(regions.len(), 2);
assert_ne!(
regions[0], regions[1],
"sibling requests need distinct child regions"
);
}
/// Exact acceptance ID: near-identical cancellation/EOF case retaining
/// sibling completion and both dispatch/close diagnostics.
// Exact-2024 era: selects LegacyOnly, which a no-legacy build refuses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn fnd_04_b_planted_negative() {
let regions = Arc::new(Mutex::new(Vec::new()));
let sent = Arc::new(Mutex::new(Vec::new()));
run_returning_transport_with_test_runtime(
Server::new("fnd-04-b-planted-negative", "1.0.0")
.protocol_policy(ProtocolPolicy::LegacyOnly)
.expect("LegacyOnly must be available to this test build")
.tool(LegacyReturningOwnershipTool {
regions: Arc::clone(®ions),
cancel_first: true,
})
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
exact_legacy_initialize_request(200, serde_json::json!("1.0.0")),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "legacy_returning_ownership_tool",
"arguments": {},
})),
201_i64,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "legacy_returning_ownership_tool",
"arguments": {},
})),
202_i64,
)),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect("cancellation must not poison the returning custom connection");
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let first = sent
.iter()
.find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(201_i64.into()) => {
Some(response)
}
_ => None,
})
.expect("cancelled request must still commit its exact legacy error response");
assert_eq!(
first.error.as_ref().and_then(|error| error.code.as_i32()),
Some(i32::from(McpErrorCode::RequestCancelled))
);
let sibling = sent
.iter()
.find_map(|message| match message {
JsonRpcMessage::Response(response) if response.id == Some(202_i64.into()) => {
Some(response)
}
_ => None,
})
.expect("sibling response must survive target cancellation and clean EOF");
assert!(sibling.error.is_none());
let regions = regions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(regions.len(), 2);
assert_ne!(
regions[0], regions[1],
"cancellation must not reuse the target region"
);
}
// Exact-2024 era: selects LegacyOnly, which a no-legacy build refuses.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn legacy_returning_completed_request_allows_sibling_reuse_after_eof() {
let regions = Arc::new(Mutex::new(Vec::new()));
let sent = Arc::new(Mutex::new(Vec::new()));
run_returning_transport_with_test_runtime(
Server::new("legacy-returning-sibling-reuse", "1.0.0")
.protocol_policy(ProtocolPolicy::LegacyOnly)
.expect("LegacyOnly must be available to this test build")
.tool(LegacyReturningOwnershipTool {
regions: Arc::clone(®ions),
cancel_first: false,
})
.build(),
ProtocolPolicyScriptTransport {
inbound: std::collections::VecDeque::from([
exact_legacy_initialize_request(300, serde_json::json!("1.0.0")),
JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "legacy_returning_ownership_tool",
"arguments": {},
})),
301_i64,
)),
JsonRpcMessage::Request(JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "legacy_returning_ownership_tool",
"arguments": {},
})),
301_i64,
)),
]),
sent: Arc::clone(&sent),
receive_calls: Arc::new(AtomicUsize::new(0)),
},
)
.expect("completed request must permit same-id sibling reuse before EOF");
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(
sent.iter()
.filter(|message| matches!(message, JsonRpcMessage::Response(response) if response.id == Some(301_i64.into())))
.count(),
2,
"both completed requests must commit their responses"
);
let regions = regions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(regions.len(), 2);
assert_ne!(
regions[0], regions[1],
"reused request IDs must receive distinct child regions"
);
}
#[test]
fn returning_subscription_nonquiescence_preserves_operation_and_close_errors() {
returning_subscription_cleanup_case(false, false);
}
#[test]
fn returning_subscription_quiescence_runs_hook_after_owner_retirement() {
returning_subscription_cleanup_case(true, false);
}
#[test]
fn returning_subscription_protocol_failure_preserves_cleanup_and_close_errors() {
returning_subscription_cleanup_case(false, true);
}
#[test]
fn returning_subscription_protocol_failure_after_quiescence_runs_hook() {
returning_subscription_cleanup_case(true, true);
}
#[test]
fn returning_subscription_background_response_write_succeeds() {
returning_subscription_background_write_case(false);
}
#[test]
fn returning_subscription_background_response_write_failure_survives_eof() {
returning_subscription_background_write_case(true);
}
#[test]
fn returning_subscription_matching_peer_cancel_retires_only_its_owner() {
returning_subscription_peer_cancel_case(true);
}
#[test]
fn returning_subscription_unrelated_peer_cancel_preserves_owner_until_eof() {
returning_subscription_peer_cancel_case(false);
}
fn returning_subscription_peer_cancel_case(matching: bool) {
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("peer cancellation reactor"))
.blocking_threads(2, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("peer cancellation runtime");
let server = Server::new("returning-peer-cancellation", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.build();
let active = Arc::clone(&server.active_requests);
let active_for_recv = Arc::clone(&active);
let sent = Arc::new(Mutex::new(Vec::new()));
let sent_for_send = Arc::clone(&sent);
let (acknowledged, acknowledgement) = sync_channel(1);
runtime
.block_on(async move {
let cx = Cx::current().expect("caller runtime Cx");
let mut pump = cx
.spawn_blocking(move |pump_cx| {
let mut phase = 0;
server.run_loop_returning(
&pump_cx,
move |_| {
let current = phase;
phase += 1;
match current {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_subscriptions_listen_request(908)),
2 => {
acknowledgement
.recv_timeout(Duration::from_secs(2))
.expect("real subscription must acknowledge");
assert_eq!(active_for_recv.lock().unwrap().len(), 1);
Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": if matching { 908 } else { 909 },
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
)))
}
3 => {
if matching {
let deadline = Instant::now() + Duration::from_secs(2);
while !active_for_recv.lock().unwrap().is_empty() {
assert!(Instant::now() < deadline, "peer cancellation must retire its listener before EOF");
std::thread::sleep(Duration::from_millis(1));
}
} else {
let owners = active_for_recv.lock().unwrap();
assert_eq!(owners.len(), 1);
assert!(!owners.values().next().unwrap().cancellation.is_cancel_requested());
}
Err(TransportError::Closed)
}
_ => panic!("unexpected receive after EOF"),
}
},
move |_, message| {
sent_for_send.lock().unwrap().push(message.clone());
Ok(())
},
Arc::new(move |notification| {
if notification.method == "notifications/subscriptions/acknowledged" {
acknowledged.send(()).expect("receive pump owns acknowledgement channel");
}
}),
None,
"test",
)
})
.expect("caller runtime admits receive pump");
let result = asupersync::time::timeout(
cx.now(),
Duration::from_secs(3),
pump.join(&cx),
)
.await
.expect("peer cancellation pump must settle")
.expect("peer cancellation pump must not panic");
assert!(!cx.is_cancel_requested(), "request cancellation must preserve the caller");
result
})
.expect("peer cancellation followed by EOF is clean");
assert!(active.lock().unwrap().is_empty());
let sent = sent.lock().unwrap();
let completions: Vec<_> = sent
.iter()
.filter_map(|message| match message {
JsonRpcMessage::Response(response)
if response.id == Some(RequestId::Number(908)) =>
{
Some(response)
}
_ => None,
})
.collect();
if matching {
assert!(
completions.is_empty(),
"peer cancellation suppresses the terminal response"
);
} else {
assert_eq!(completions.len(), 1);
assert!(final_subscription_completion_response(completions[0]));
}
}
fn returning_subscription_background_write_case(fail_write: bool) {
struct RejectListen;
impl Middleware for RejectListen {
fn on_request(
&self,
_ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
if request.method == SUBSCRIPTIONS_LISTEN {
return Err(McpError::invalid_params("listener rejected by application"));
}
Ok(MiddlewareDecision::Continue)
}
}
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("background write test reactor"))
.blocking_threads(2, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("background write test runtime");
let attempts = Arc::new(AtomicUsize::new(0));
let attempts_for_send = Arc::clone(&attempts);
let shutdown_calls = Arc::new(AtomicUsize::new(0));
let shutdown_calls_for_hook = Arc::clone(&shutdown_calls);
let server = Server::new("background-response-write", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.middleware(RejectListen)
.on_shutdown(move || {
shutdown_calls_for_hook.fetch_add(1, Ordering::AcqRel);
})
.build();
let active = Arc::clone(&server.active_requests);
let (attempted, attempt_received) = sync_channel(1);
let result = runtime.block_on(async move {
let cx = Cx::current().expect("caller runtime Cx");
let mut pump = cx
.spawn_blocking(move |pump_cx| {
let mut incoming = std::collections::VecDeque::from([
modern_discovery_opening_request(),
modern_subscriptions_listen_request(906),
]);
server.run_loop_returning(
&pump_cx,
move |_| {
if let Some(request) = incoming.pop_front() {
return Ok(request);
}
attempt_received
.recv_timeout(Duration::from_secs(2))
.expect("worker must attempt its correlated response");
Err(TransportError::Closed)
},
move |_, message| {
if let JsonRpcMessage::Response(response) = message
&& response.id == Some(RequestId::Number(906))
{
assert_eq!(
response.error.as_ref().map(|error| error.message.as_str()),
Some("listener rejected by application")
);
attempts_for_send.fetch_add(1, Ordering::AcqRel);
attempted
.send(())
.expect("receive pump still owns its channel");
if fail_write {
return Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"background response writer failed",
)));
}
}
Ok(())
},
Arc::new(|_| {}),
None,
"test",
)
})
.expect("caller runtime admits the receive pump");
asupersync::time::timeout(cx.now(), Duration::from_secs(3), pump.join(&cx))
.await
.expect("background response pump must settle")
.expect("background response pump must not panic")
});
assert_eq!(attempts.load(Ordering::Acquire), 1);
if fail_write {
let error = result.expect_err("clean EOF cannot hide the failed response write");
let data = error.data.expect("typed write failure");
assert_eq!(data["stage"], "send");
assert_eq!(data["kind"], "io");
} else {
result.expect("delivered response followed by EOF succeeds");
}
assert_eq!(shutdown_calls.load(Ordering::Acquire), 1);
assert!(active.lock().unwrap().is_empty());
}
fn returning_subscription_cleanup_case(release_before_shutdown: bool, invalid_response: bool) {
struct CloseAfterDrainTransport {
control: Arc<NonQuiescentLegacyControl>,
phase: usize,
close_calls: Arc<AtomicUsize>,
closed_before_drain: Arc<AtomicBool>,
release_before_shutdown: bool,
invalid_response: bool,
}
struct NonQuiescentSubscriptionMiddleware {
control: Arc<NonQuiescentLegacyControl>,
}
impl Middleware for NonQuiescentSubscriptionMiddleware {
fn on_request(
&self,
_ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
if request.method == SUBSCRIPTIONS_LISTEN {
self.control.wait_until_released();
}
Ok(MiddlewareDecision::Continue)
}
}
impl Transport for CloseAfterDrainTransport {
fn send(&mut self, _cx: &Cx, _message: &JsonRpcMessage) -> Result<(), TransportError> {
Ok(())
}
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_subscriptions_listen_request(903)),
2 if self.control.wait_for_started(Duration::from_secs(2)) => {
if self.release_before_shutdown {
self.control.release();
assert!(self.control.wait_for_finished(Duration::from_secs(2)));
}
if self.invalid_response {
let mut response = JsonRpcResponse::success(
RequestId::Number(904),
serde_json::json!({}),
);
response.jsonrpc = "1.0".into();
return Ok(JsonRpcMessage::Response(response));
}
Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"receive while request remains active",
)))
}
_ => Err(TransportError::Timeout),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
self.close_calls.fetch_add(1, Ordering::AcqRel);
if !self.control.has_finished() {
self.closed_before_drain.store(true, Ordering::Release);
}
Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"close after request drain",
)))
}
}
let control = Arc::new(NonQuiescentLegacyControl::default());
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("bounded cleanup test reactor must initialize"))
.blocking_threads(2, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("bounded cleanup test runtime must initialize");
let closed_before_drain = Arc::new(AtomicBool::new(false));
let shutdown_before_drain = Arc::new(AtomicBool::new(false));
let shutdown_calls = Arc::new(AtomicUsize::new(0));
let close_calls = Arc::new(AtomicUsize::new(0));
let control_for_shutdown = Arc::clone(&control);
let shutdown_before_drain_for_hook = Arc::clone(&shutdown_before_drain);
let shutdown_calls_for_hook = Arc::clone(&shutdown_calls);
let server = Server::new("legacy-returning-bounded-cleanup", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.middleware(NonQuiescentSubscriptionMiddleware {
control: Arc::clone(&control),
})
.on_shutdown(move || {
shutdown_calls_for_hook.fetch_add(1, Ordering::AcqRel);
if !control_for_shutdown.has_finished() {
shutdown_before_drain_for_hook.store(true, Ordering::Release);
}
})
.build();
let active_requests_for_assert = Arc::clone(&server.active_requests);
struct ReleaseNonCooperativeOnDrop(Arc<NonQuiescentLegacyControl>);
impl Drop for ReleaseNonCooperativeOnDrop {
fn drop(&mut self) {
self.0.release();
let _ = self.0.wait_for_finished(Duration::from_secs(2));
}
}
let _release_on_failure = ReleaseNonCooperativeOnDrop(Arc::clone(&control));
runtime
.block_on(async move {
let cx = Cx::current().expect("bounded cleanup test runtime must install Cx");
let (result_sender, result_receiver) = sync_channel(1);
let control_for_transport = Arc::clone(&control);
let close_calls_for_transport = Arc::clone(&close_calls);
let closed_before_drain_for_transport = Arc::clone(&closed_before_drain);
let mut pump = cx
.spawn_blocking(move |pump_cx| {
let result = server.run_transport_returning_with_cx(
&pump_cx,
CloseAfterDrainTransport {
control: control_for_transport,
phase: 0,
close_calls: close_calls_for_transport,
closed_before_drain: closed_before_drain_for_transport,
release_before_shutdown,
invalid_response,
},
);
let _ = result_sender.send(result);
})
.expect("returning bounded cleanup pump must be admitted");
let started = Instant::now();
// Same anti-hang watchdog as the sibling returning-pump helpers, and for
// the same reason. 8 s stood here, but the branch below deliberately waits
// `>= SHUTDOWN_CLEANUP_TIMEOUT` (5 s), which left only ~3 s of headroom for
// pump scheduling on a host running the other 2307 tests concurrently. That
// is the same upper-bound-on-wall-time mistake the comment further down
// records for the removed `started.elapsed() < SHUTDOWN_CLEANUP_TIMEOUT`
// assertion, which flaked 1926/0 then 1925/1 on identical source.
let deadline = cx.now().saturating_add_nanos(RETURNING_PUMP_WATCHDOG_NANOS);
let result = loop {
match result_receiver.try_recv() {
Ok(result) => break result,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
panic!(
"returning bounded cleanup pump exited without reporting its result"
);
}
Err(std::sync::mpsc::TryRecvError::Empty) => {
asupersync::time::timeout_at(
deadline,
asupersync::time::sleep(cx.now(), Duration::from_millis(1)),
)
.await
.expect("returning bounded cleanup pump must settle within its bound");
}
}
};
pump.join(&cx)
.await
.expect("returning bounded-failure pump task must finish");
let error = result.expect_err("transport close failure must remain visible");
assert_eq!(
error.data.as_ref().and_then(|data| data["stage"].as_str()),
Some("run_and_close")
);
let data = error
.data
.as_ref()
.expect("combined error must include data");
assert_eq!(data["kind"], "multiple_failures");
let receive_kind = if invalid_response {
"invalid_response"
} else {
"io"
};
if release_before_shutdown {
// `stage == "receive"` already entails that the non-quiescent
// cleanup wait was not entered: entering it produces a
// `run_and_cleanup` combined error carrying a `cleanup` key,
// which is what the other branch asserts. The two are mutually
// exclusive by construction, so this branch needs no separate
// check that the cleanup timeout was skipped.
//
// An `assert!(started.elapsed() < SHUTDOWN_CLEANUP_TIMEOUT)`
// stood here and was removed. It added no information about the
// property above, and it was an UPPER bound on real wall time
// (`Instant::now()`) covering the whole pump lifecycle, so a
// loaded host failed it with no code change: this test passed
// 1926/0 and failed 1925/1 seventeen minutes apart on identical
// source. Note the sibling branch's `>= SHUTDOWN_CLEANUP_TIMEOUT`
// is a LOWER bound and is load-monotone, so it is sound and stays.
// Do not reinstate an upper bound on wall time here.
assert_eq!(data["run"]["data"]["stage"], "receive");
assert_eq!(data["run"]["data"]["kind"], receive_kind);
} else {
assert_eq!(data["run"]["data"]["stage"], "run_and_cleanup");
assert_eq!(data["run"]["data"]["kind"], "multiple_failures");
assert_eq!(data["run"]["data"]["run"]["data"]["stage"], "receive");
assert_eq!(data["run"]["data"]["run"]["data"]["kind"], receive_kind);
assert_eq!(data["run"]["data"]["cleanup"]["data"]["stage"], "shutdown");
assert_eq!(
data["run"]["data"]["cleanup"]["data"]["kind"],
"non_quiescent"
);
assert!(
data["run"]["data"]["cleanup"]["data"]["remaining"]
.as_u64()
.unwrap_or(0)
>= 1
);
assert!(started.elapsed() >= SHUTDOWN_CLEANUP_TIMEOUT);
}
assert_eq!(data["close"]["data"]["stage"], "close");
assert_eq!(data["close"]["data"]["kind"], "io");
assert_eq!(close_calls.load(Ordering::Acquire), 1);
assert_eq!(
closed_before_drain.load(Ordering::Acquire),
!release_before_shutdown
);
assert!(!shutdown_before_drain.load(Ordering::Acquire));
assert_eq!(
shutdown_calls.load(Ordering::Acquire),
usize::from(release_before_shutdown)
);
assert_eq!(
active_requests_for_assert
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty(),
release_before_shutdown
);
control.release();
assert!(control.wait_for_finished(Duration::from_secs(2)));
let cleanup_deadline = cx.now().saturating_add_nanos(2_000_000_000);
while !active_requests_for_assert
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
{
asupersync::time::timeout_at(
cleanup_deadline,
asupersync::time::sleep(cx.now(), Duration::from_millis(1)),
)
.await
.expect("released request region must retire before fixture teardown");
}
assert!(control.has_finished());
assert!(
active_requests_for_assert
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
);
Ok::<(), McpError>(())
})
.expect("returning bounded cleanup failure must preserve ownership and errors");
}
// bd-gl2we: the `returning_subscription_*` family fails intermittently
// through two races around a stdio listen's acknowledgement (comment 5403).
// These pairs FORCE each interleaving instead of waiting for load to find
// it. Each pair differs only in the ordering it forces, and each records
// that the ordering was actually reached, so a pass on an unforced run
// cannot pass silently. They are deliberately outside the family's
// `returning_subscription_*` prefix and its denominator.
//
// Both pairs now assert the contract. The pump-first test was bd-gl2we
// finding F1: stdio shutdown lost a pending graceful election. bd-81cct
// fixed it by having shutdown wait for the owner of a won election before
// generic cancellation. The under-recv-lock test was F2: an
// acknowledgement sent during an unsplit transport's `recv` failed the
// connection. bd-8bcfq fixed it by queueing such output until `recv`
// returns.
#[test]
fn forced_subscription_ack_order_pump_first_keeps_graceful_completion() {
forced_subscription_shutdown_order_case(true);
}
#[test]
fn forced_subscription_ack_order_active_first_keeps_graceful_completion() {
forced_subscription_shutdown_order_case(false);
}
#[test]
fn forced_subscription_ack_order_under_recv_lock_is_written_when_recv_returns() {
forced_subscription_recv_lock_order_case(true);
}
#[test]
fn forced_subscription_ack_order_outside_recv_lock_reports_receive_stage() {
forced_subscription_recv_lock_order_case(false);
}
/// Election phases of every live final-subscription lease. The entry list
/// is copied out before any phase lock is taken, so this never holds the
/// registry mutex and an election mutex at once.
fn final_subscription_phases(
registry: &FinalSubscriptionRegistry,
) -> Vec<FinalSubscriptionPhase> {
let elections: Vec<_> = registry
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.values()
.map(|entry| Arc::clone(&entry.election))
.collect();
elections
.iter()
.map(|election| {
*election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
})
.collect()
}
/// `Active` is published by the opener only after its acknowledgement
/// callback has returned, so reaching it proves the acknowledgement was
/// already attempted.
fn wait_for_single_active_final_subscription(
registry: &FinalSubscriptionRegistry,
timeout: Duration,
) -> bool {
let deadline = Instant::now() + timeout;
loop {
if final_subscription_phases(registry) == [FinalSubscriptionPhase::Active] {
return true;
}
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(1));
}
}
/// EOF shutdown against a listen whose acknowledgement callback has run.
/// `pump_first` holds the opener inside that callback, before it can
/// publish `Active`, until shutdown's `terminate` has recorded
/// `ServerTerminationPending(0)`. It then keeps holding for a grace window
/// in which a generic shutdown cancellation, had it been issued, would
/// defeat the opener's deferred election. Otherwise the pump holds EOF
/// until the opener has published `Active`, and shutdown elects the
/// graceful completion directly. Either order must deliver exactly one
/// graceful completion.
fn forced_subscription_shutdown_order_case(pump_first: bool) {
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("forced shutdown-order reactor"))
.blocking_threads(2, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("forced shutdown-order runtime");
let server = Server::new("forced-subscription-shutdown-order", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.build();
let active = Arc::clone(&server.active_requests);
let active_for_callback = Arc::clone(&active);
let registry = Arc::clone(&server.final_subscriptions);
let registry_for_callback = Arc::clone(®istry);
let sent = Arc::new(Mutex::new(Vec::new()));
let sent_for_send = Arc::clone(&sent);
let forced = Arc::new(AtomicBool::new(false));
let forced_by_callback = Arc::clone(&forced);
let forced_by_recv = Arc::clone(&forced);
let cancelled_during_callback = Arc::new(AtomicBool::new(false));
let cancelled_by_callback = Arc::clone(&cancelled_during_callback);
let (acknowledged, acknowledgement) = sync_channel(1);
runtime
.block_on(async move {
let cx = Cx::current().expect("caller runtime Cx");
let mut pump = cx
.spawn_blocking(move |pump_cx| {
let mut phase = 0;
server.run_loop_returning(
&pump_cx,
move |_| {
let current = phase;
phase += 1;
match current {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_subscriptions_listen_request(910)),
2 => {
acknowledgement
.recv_timeout(Duration::from_secs(2))
.expect("real subscription must acknowledge");
if !pump_first
&& wait_for_single_active_final_subscription(
®istry,
Duration::from_secs(2),
)
{
forced_by_recv.store(true, Ordering::Release);
}
Err(TransportError::Closed)
}
_ => panic!("unexpected receive after EOF"),
}
},
move |_, message| {
sent_for_send.lock().unwrap().push(message.clone());
Ok(())
},
Arc::new(move |notification| {
if notification.method != "notifications/subscriptions/acknowledged"
{
return;
}
// The lease is registered before its
// acknowledgement is sent; keep its election
// so the phase stays observable after
// `terminate` removes the registry entry.
let elections: Vec<_> = registry_for_callback
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.values()
.map(|entry| Arc::clone(&entry.election))
.collect();
acknowledged
.send(())
.expect("receive pump owns acknowledgement channel");
if !pump_first {
return;
}
let deadline = Instant::now() + Duration::from_secs(2);
let server_won_during_callback = loop {
if elections.iter().any(|election| {
*election
.phase
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
== FinalSubscriptionPhase::ServerTerminationPending(0)
}) {
break true;
}
if Instant::now() >= deadline {
break false;
}
std::thread::sleep(Duration::from_millis(1));
};
if !server_won_during_callback {
return;
}
forced_by_callback.store(true, Ordering::Release);
// Shutdown must not cancel the request while
// this callback still owns the won election.
let grace = Instant::now() + Duration::from_millis(500);
while Instant::now() < grace {
if active_for_callback
.lock()
.unwrap()
.values()
.any(|owner| owner.cancellation.is_cancel_requested())
{
cancelled_by_callback.store(true, Ordering::Release);
return;
}
std::thread::sleep(Duration::from_millis(1));
}
}),
None,
"test",
)
})
.expect("caller runtime admits receive pump");
asupersync::time::timeout(cx.now(), Duration::from_secs(3), pump.join(&cx))
.await
.expect("forced shutdown-order pump must settle")
.expect("forced shutdown-order pump must not panic")
})
.expect("EOF after an acknowledged listen is a clean close");
assert!(
forced.load(Ordering::Acquire),
"the {} interleaving was not forced",
if pump_first {
"pump-first"
} else {
"active-first"
}
);
assert!(active.lock().unwrap().is_empty());
let sent = sent.lock().unwrap();
let responses: Vec<_> = sent
.iter()
.filter_map(|message| match message {
JsonRpcMessage::Response(response)
if response.id == Some(RequestId::Number(910)) =>
{
Some(response)
}
_ => None,
})
.collect();
assert!(
!cancelled_during_callback.load(Ordering::Acquire),
"shutdown cancelled the request while its won election was still pending"
);
assert_eq!(responses.len(), 1, "{responses:?}");
assert!(final_subscription_completion_response(responses[0]));
}
/// An invalid response frame after an acknowledged listen, on an unsplit
/// transport. Both variants send the same frames and run the same
/// release-then-wait-for-`Active` step; only its location differs.
/// `ack_under_recv_lock` runs it inside `recv`, while `SharedTransport`'s
/// pump owns the I/O handle, so the acknowledgement is queued and written
/// when that `recv` returns (receive phase 4). Otherwise it runs in
/// middleware for an inline request, after `recv` has released the handle,
/// and is written directly (receive phase 3).
fn forced_subscription_recv_lock_order_case(ack_under_recv_lock: bool) {
struct ForcedAckOrderTransport {
control: Arc<NonQuiescentLegacyControl>,
registry: Arc<FinalSubscriptionRegistry>,
forced: Arc<AtomicBool>,
acknowledgement_phases: Arc<Mutex<Vec<usize>>>,
phase: usize,
ack_under_recv_lock: bool,
}
struct ForcedAckOrderMiddleware {
control: Arc<NonQuiescentLegacyControl>,
registry: Arc<std::sync::OnceLock<Arc<FinalSubscriptionRegistry>>>,
forced: Arc<AtomicBool>,
ack_under_recv_lock: bool,
}
impl Middleware for ForcedAckOrderMiddleware {
fn on_request(
&self,
_ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
if request.method == SUBSCRIPTIONS_LISTEN {
self.control.wait_until_released();
} else if !self.ack_under_recv_lock && request.id == Some(RequestId::Number(907)) {
self.control.release();
let registry = self
.registry
.get()
.expect("registry is published before the pump starts");
if wait_for_single_active_final_subscription(registry, Duration::from_secs(2)) {
self.forced.store(true, Ordering::Release);
}
}
Ok(MiddlewareDecision::Continue)
}
}
impl Transport for ForcedAckOrderTransport {
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
if let JsonRpcMessage::Request(request) = message
&& request.method == "notifications/subscriptions/acknowledged"
{
self.acknowledgement_phases
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(self.phase);
}
Ok(())
}
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
let phase = self.phase;
self.phase = self.phase.saturating_add(1);
match phase {
0 => Ok(modern_discovery_opening_request()),
1 => Ok(modern_subscriptions_listen_request(911)),
2 => Ok(modern_discovery_request(907)),
3 => {
if self.ack_under_recv_lock {
self.control.release();
if wait_for_single_active_final_subscription(
&self.registry,
Duration::from_secs(2),
) {
self.forced.store(true, Ordering::Release);
}
}
let mut response =
JsonRpcResponse::success(RequestId::Number(912), serde_json::json!({}));
response.jsonrpc = "1.0".into();
Ok(JsonRpcMessage::Response(response))
}
_ => Err(TransportError::Timeout),
}
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
let control = Arc::new(NonQuiescentLegacyControl::default());
let forced = Arc::new(AtomicBool::new(false));
let acknowledgement_phases = Arc::new(Mutex::new(Vec::new()));
let registry_cell = Arc::new(std::sync::OnceLock::new());
let runtime = RuntimeBuilder::current_thread()
.with_reactor(create_reactor().expect("forced recv-lock reactor"))
.blocking_threads(2, MAX_DISPATCH_QUEUE_DEPTH)
.build()
.expect("forced recv-lock runtime");
let server = Server::new("forced-subscription-recv-lock-order", "1.0.0")
.protocol_policy(build_default_protocol_policy())
.expect("the build's default policy must be available")
.middleware(ForcedAckOrderMiddleware {
control: Arc::clone(&control),
registry: Arc::clone(®istry_cell),
forced: Arc::clone(&forced),
ack_under_recv_lock,
})
.build();
let registry = Arc::clone(&server.final_subscriptions);
assert!(registry_cell.set(Arc::clone(®istry)).is_ok());
let active = Arc::clone(&server.active_requests);
let transport = ForcedAckOrderTransport {
control: Arc::clone(&control),
registry,
forced: Arc::clone(&forced),
acknowledgement_phases: Arc::clone(&acknowledgement_phases),
phase: 0,
ack_under_recv_lock,
};
let result = runtime.block_on(async move {
let cx = Cx::current().expect("caller runtime Cx");
let mut pump = cx
.spawn_blocking(move |pump_cx| {
server.run_transport_returning_with_cx(&pump_cx, transport)
})
.expect("caller runtime admits receive pump");
asupersync::time::timeout(cx.now(), Duration::from_secs(5), pump.join(&cx))
.await
.expect("forced recv-lock pump must settle")
.expect("forced recv-lock pump must not panic")
});
control.release();
assert!(
forced.load(Ordering::Acquire),
"the {} interleaving was not forced",
if ack_under_recv_lock {
"under-recv-lock"
} else {
"outside-recv-lock"
}
);
let error = result.expect_err("an invalid response frame terminates the connection");
let data = error.data.as_ref().expect("typed run failure");
// Exactly one acknowledgement reaches the wire either way. Under the
// receive lock it is queued and written right after the fourth `recv`
// returns, before the pump classifies that frame; otherwise it is
// written directly while the pump dispatches the third frame.
let expected_phase = if ack_under_recv_lock { 4 } else { 3 };
assert_eq!(
*acknowledgement_phases
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
[expected_phase],
"{error:?}"
);
assert_eq!(data["stage"], "receive", "{error:?}");
assert_eq!(data["kind"], "invalid_response", "{error:?}");
assert!(active.lock().unwrap().is_empty());
}
// Exact-2024 era: negotiates the connection with a legacy initialize.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn returning_loop_replies_to_bounded_codec_error_then_reads_next_frame() {
use std::collections::VecDeque;
use std::sync::atomic::AtomicUsize;
// A malformed OPENING frame is a fatal era-security rejection under
// Auto, so the recoverable-parse-error contract is probed on an
// already negotiated connection.
let steps = Arc::new(Mutex::new(VecDeque::from([
Ok(exact_legacy_initialize_request(
32,
serde_json::json!("1.0.0"),
)),
Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
))),
Err(TransportError::Codec(fastmcp_transport::CodecError::Json(
serde_json::from_str::<serde_json::Value>("{")
.expect_err("fixture must be invalid JSON"),
))),
Err(TransportError::Closed),
])));
let receive_calls = Arc::new(AtomicUsize::new(0));
let sent = Arc::new(Mutex::new(Vec::<JsonRpcMessage>::new()));
let receive_steps = Arc::clone(&steps);
let receive_count = Arc::clone(&receive_calls);
let sent_messages = Arc::clone(&sent);
let cx = Cx::for_testing();
Server::new("recoverable-codec-error-test", "1.0.0")
.build()
.run_loop_returning_with_dispatch_cx(
&cx,
&cx,
move |_, _worker_failed| {
receive_count.fetch_add(1, Ordering::Relaxed);
receive_steps
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.pop_front()
.unwrap_or(Err(TransportError::Closed))
},
move |_, message| {
sent_messages
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(message.clone());
Ok(())
},
Arc::new(|_| {}),
None,
"test",
)
.expect("clean transport closure after a recoverable codec error must succeed");
assert_eq!(receive_calls.load(Ordering::Relaxed), 4);
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// The initialize response races worker shutdown and is incidental;
// the contract is exactly one uncorrelated parse-error reply.
let parse_errors: Vec<&JsonRpcResponse> = sent
.iter()
.filter_map(|message| match message {
JsonRpcMessage::Response(response) if response.id.is_none() => Some(response),
_ => None,
})
.collect();
let [response] = parse_errors.as_slice() else {
panic!("expected exactly one uncorrelated parse-error response");
};
assert_eq!(
response
.error
.as_ref()
.and_then(|error| error.code.as_i32()),
Some(-32700)
);
let wire = serde_json::to_value(response).expect("parse error response must serialize");
assert!(wire.get("id").is_none());
}
struct ModernReturningScriptTransport {
steps: std::collections::VecDeque<Result<JsonRpcMessage, TransportError>>,
received: Arc<AtomicUsize>,
sent: Arc<Mutex<Vec<JsonRpcMessage>>>,
closed: Arc<AtomicUsize>,
}
impl Transport for ModernReturningScriptTransport {
fn recv(&mut self, _cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
self.received.fetch_add(1, Ordering::AcqRel);
self.steps
.pop_front()
.unwrap_or(Err(TransportError::Closed))
}
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
self.sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(message.clone());
Ok(())
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
self.closed.fetch_add(1, Ordering::AcqRel);
Ok(())
}
}
struct ModernReturningCallbackTransport<R> {
receive: R,
sent: Arc<Mutex<Vec<JsonRpcMessage>>>,
}
impl<R> Transport for ModernReturningCallbackTransport<R>
where
R: FnMut(&Cx) -> Result<JsonRpcMessage, TransportError>,
{
fn recv(&mut self, cx: &Cx) -> Result<JsonRpcMessage, TransportError> {
(self.receive)(cx)
}
fn send(&mut self, _cx: &Cx, message: &JsonRpcMessage) -> Result<(), TransportError> {
self.sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(message.clone());
Ok(())
}
fn close(&mut self, _cx: &Cx) -> Result<(), TransportError> {
Ok(())
}
}
struct ReleaseModernReturningBlocker(Arc<NonQuiescentLegacyControl>);
impl Drop for ReleaseModernReturningBlocker {
fn drop(&mut self) {
self.0.release();
}
}
#[test]
fn modern_returning_loop_queued_subscription_below_capacity_allows_request() {
modern_returning_loop_queued_capacity_case(false);
}
#[test]
fn modern_returning_loop_queued_subscription_at_capacity_rejects_request() {
modern_returning_loop_queued_capacity_case(true);
}
fn modern_returning_loop_queued_capacity_case(at_capacity: bool) {
// The receive loop is synchronous; a second async worker must keep
// polling the caller-owned spawn wrappers while this one receives.
let runtime = RuntimeBuilder::multi_thread()
.worker_threads(2)
.blocking_threads(1, 1)
.build()
.expect("queued admission runtime");
runtime.block_on(async move {
let cx = Cx::current().expect("caller runtime context");
let blocker_control = Arc::new(NonQuiescentLegacyControl::default());
let _release = ReleaseModernReturningBlocker(Arc::clone(&blocker_control));
let blocker_wait = Arc::clone(&blocker_control);
let mut blocker = cx
.spawn_blocking(move |_| blocker_wait.wait_until_released())
.expect("the only blocking worker is admitted");
assert!(blocker_control.wait_for_started(Duration::from_secs(2)));
let server = Server::new("modern-returning-queued-capacity", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern policy")
.build();
let active = Arc::clone(&server.active_requests);
let sent = Arc::new(Mutex::new(Vec::new()));
let sent_for_receive = Arc::clone(&sent);
let queued = MAX_DISPATCH_QUEUE_DEPTH - usize::from(!at_capacity);
let mut phase = 0;
server
.run_transport_returning_with_cx(
&cx,
ModernReturningCallbackTransport {
receive: move |_: &Cx| {
let current = phase;
phase += 1;
if current < queued {
return Ok(modern_subscriptions_listen_request(
10_000 + current as i64,
));
}
if current == queued {
return Ok(modern_discovery_request(11_000));
}
let owners = active
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(owners.len(), queued);
let messages = sent_for_receive
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let [JsonRpcMessage::Response(response)] = messages.as_slice() else {
panic!("queued listens must not execute before the worker is free");
};
assert_eq!(response.id, Some(11_000_i64.into()));
if at_capacity {
assert_eq!(
response.error.as_ref().unwrap().code.as_i32(),
Some(RESOURCE_EXHAUSTED_ERROR_CODE)
);
} else {
assert!(response.error.is_none());
}
for owner in owners.values() {
owner.cancellation.cancel();
}
drop(owners);
blocker_control.release();
Err(TransportError::Closed)
},
sent,
},
)
.expect("queued owners retire after their blocker is released");
blocker.join(&cx).await.expect("blocker retires");
});
}
#[test]
fn modern_returning_loop_opening_listen_accepts_queued_peer_cancellation() {
modern_returning_loop_queued_cancellation_case(true);
}
#[test]
fn modern_returning_loop_opening_listen_ignores_unrelated_queued_cancellation() {
modern_returning_loop_queued_cancellation_case(false);
}
fn modern_returning_loop_queued_cancellation_case(matching: bool) {
struct ObserveSubscriptionStart(Arc<BoundedTestSignal>);
impl Middleware for ObserveSubscriptionStart {
fn on_request(
&self,
_ctx: &McpContext,
request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
if request.method == SUBSCRIPTIONS_LISTEN {
self.0.raise();
}
Ok(MiddlewareDecision::Continue)
}
}
let runtime = RuntimeBuilder::multi_thread()
.worker_threads(2)
.blocking_threads(1, 1)
.build()
.expect("queued cancellation runtime");
runtime.block_on(async move {
let cx = Cx::current().expect("caller runtime context");
let blocker_control = Arc::new(NonQuiescentLegacyControl::default());
let _release = ReleaseModernReturningBlocker(Arc::clone(&blocker_control));
let blocker_wait = Arc::clone(&blocker_control);
let mut blocker = cx
.spawn_blocking(move |_| blocker_wait.wait_until_released())
.expect("the only blocking worker is admitted");
assert!(blocker_control.wait_for_started(Duration::from_secs(2)));
let started = Arc::new(BoundedTestSignal::default());
let started_for_receive = Arc::clone(&started);
let server = Server::new("modern-returning-opening-listen-cancellation", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern policy")
.middleware(ObserveSubscriptionStart(Arc::clone(&started)))
.build();
let active = Arc::clone(&server.active_requests);
let active_for_receive = Arc::clone(&active);
let mut phase = 0;
server
.run_transport_returning_with_cx(
&cx,
ModernReturningCallbackTransport {
receive: move |_: &Cx| {
let current = phase;
phase += 1;
match current {
0 => Ok(modern_subscriptions_listen_request(12_000)),
1 => Ok(modern_cancelled_notification(if matching {
12_000
} else {
12_001
})),
_ => {
let owners = active_for_receive
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(owners.len(), 1);
let owner = owners.values().next().unwrap();
assert_eq!(
owner.cancellation.is_cancel_requested(),
matching,
"the opening listen must bind its principal before spawn"
);
let completion = Arc::clone(&owner.completion);
drop(owners);
blocker_control.release();
if matching {
assert!(completion.wait_timeout(Duration::from_secs(2)));
} else {
assert!(started_for_receive.wait(Duration::from_secs(2)));
}
Err(TransportError::Closed)
}
}
},
sent: Arc::new(Mutex::new(Vec::new())),
},
)
.expect("queued cancellation must preserve a clean shutdown");
blocker.join(&cx).await.expect("blocker retires");
assert_eq!(started.wait(Duration::ZERO), !matching);
assert!(active.lock().unwrap().is_empty());
});
}
#[test]
fn modern_returning_loop_valid_peer_response_preserves_next_request() {
modern_returning_loop_peer_response_case(false);
}
#[test]
fn modern_returning_loop_invalid_peer_response_stops_before_next_request() {
modern_returning_loop_peer_response_case(true);
}
fn modern_returning_loop_peer_response_case(invalid_version: bool) {
let mut response = JsonRpcResponse::success(9900_i64.into(), serde_json::json!({}));
if invalid_version {
response.jsonrpc = "1.0".into();
}
let received = Arc::new(AtomicUsize::new(0));
let sent = Arc::new(Mutex::new(Vec::new()));
let closed = Arc::new(AtomicUsize::new(0));
let result = Server::new("modern-returning-response-validation", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("the modern profile must be available")
.build()
.run_transport_returning_with_cx(
&Cx::for_testing(),
ModernReturningScriptTransport {
steps: std::collections::VecDeque::from([
Ok(modern_discovery_request(9901)),
Ok(JsonRpcMessage::Response(response)),
Ok(modern_discovery_request(9902)),
Err(TransportError::Closed),
]),
received: Arc::clone(&received),
sent: Arc::clone(&sent),
closed: Arc::clone(&closed),
},
);
let messages = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(closed.load(Ordering::Acquire), 1);
assert!(matches!(
messages.first(),
Some(JsonRpcMessage::Response(response))
if response.id == Some(9901_i64.into()) && response.error.is_none()
));
if invalid_version {
let error = result.expect_err("an invalid peer response must terminate ingress");
assert_eq!(error.data.as_ref().unwrap()["stage"], "receive");
assert_eq!(error.data.as_ref().unwrap()["kind"], "invalid_response");
assert_eq!(received.load(Ordering::Acquire), 2);
assert_eq!(messages.len(), 1, "no later request may be dispatched");
} else {
result.expect("a valid unmatched response must not poison the connection");
assert_eq!(received.load(Ordering::Acquire), 4);
assert_eq!(messages.len(), 2);
assert!(matches!(
messages.last(),
Some(JsonRpcMessage::Response(response))
if response.id == Some(9902_i64.into()) && response.error.is_none()
));
}
}
#[test]
fn modern_returning_loop_complete_json_error_replies_and_continues() {
modern_returning_loop_codec_boundary_case(false);
}
#[test]
fn modern_returning_loop_unbounded_frame_error_stops_without_reply() {
modern_returning_loop_codec_boundary_case(true);
}
fn modern_returning_loop_codec_boundary_case(lost_frame_boundary: bool) {
let codec_error = if lost_frame_boundary {
fastmcp_transport::CodecError::MessageTooLarge(1)
} else {
fastmcp_transport::CodecError::Json(
serde_json::from_str::<serde_json::Value>("{")
.expect_err("the complete frame contains malformed JSON"),
)
};
let received = Arc::new(AtomicUsize::new(0));
let sent = Arc::new(Mutex::new(Vec::new()));
let closed = Arc::new(AtomicUsize::new(0));
let result = Server::new("modern-returning-codec-boundary", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("the modern profile must be available")
.build()
.run_transport_returning_with_cx(
&Cx::for_testing(),
ModernReturningScriptTransport {
steps: std::collections::VecDeque::from([
Err(TransportError::Codec(codec_error)),
Ok(modern_discovery_request(9911)),
Err(TransportError::Closed),
]),
received: Arc::clone(&received),
sent: Arc::clone(&sent),
closed: Arc::clone(&closed),
},
);
let messages = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(closed.load(Ordering::Acquire), 1);
if lost_frame_boundary {
let error = result.expect_err("a lost framing boundary must terminate ingress");
assert_eq!(error.data.as_ref().unwrap()["stage"], "receive");
assert_eq!(error.data.as_ref().unwrap()["kind"], "codec");
assert_eq!(received.load(Ordering::Acquire), 1);
assert!(
messages.is_empty(),
"an unbounded frame must receive no reply"
);
} else {
result.expect("a complete malformed JSON frame must permit the next request");
assert_eq!(received.load(Ordering::Acquire), 3);
let [
JsonRpcMessage::Response(parse_error),
JsonRpcMessage::Response(discovery),
] = messages.as_slice()
else {
panic!("expected one parse error followed by one discovery response");
};
assert!(parse_error.id.is_none());
assert_eq!(
parse_error.error.as_ref().unwrap().code.as_i32(),
Some(-32700)
);
assert!(parse_error.error.as_ref().unwrap().data.is_none());
assert_eq!(discovery.id, Some(9911_i64.into()));
assert!(discovery.error.is_none());
}
}
// Exact-2024 era: negotiates the connection with a legacy initialize.
#[cfg(feature = "legacy-2024-11-05")]
#[test]
fn returning_loop_correlates_invalid_request_only_when_codec_proves_unique_id() {
use std::collections::VecDeque;
let invalid = fastmcp_transport::Codec::new()
.decode_complete_message(br#"{"jsonrpc":"2.1","method":"tools/list","id":"safe-id"}"#)
.expect_err("wrong JSON-RPC version must be invalid");
// A malformed OPENING frame is a fatal era-security rejection under
// Auto, so the correlation contract is probed on an already
// negotiated connection.
let steps = Arc::new(Mutex::new(VecDeque::from([
Ok(exact_legacy_initialize_request(
31,
serde_json::json!("1.0.0"),
)),
Ok(JsonRpcMessage::Request(JsonRpcRequest::notification(
"notifications/initialized",
None,
))),
Err(TransportError::Codec(invalid)),
Err(TransportError::Closed),
])));
let sent = Arc::new(Mutex::new(Vec::<JsonRpcMessage>::new()));
let receive_steps = Arc::clone(&steps);
let sent_messages = Arc::clone(&sent);
let cx = Cx::for_testing();
Server::new("correlated-invalid-request-test", "1.0.0")
.build()
.run_loop_returning_with_dispatch_cx(
&cx,
&cx,
move |_, _worker_failed| {
receive_steps
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.pop_front()
.unwrap_or(Err(TransportError::Closed))
},
move |_, message| {
sent_messages
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(message.clone());
Ok(())
},
Arc::new(|_| {}),
None,
"test",
)
.expect("clean transport closure after an invalid-request reply must succeed");
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// The initialize response races worker shutdown and is incidental;
// the contract is exactly one reply correlated to the proven id.
let correlated: Vec<&JsonRpcResponse> = sent
.iter()
.filter_map(|message| match message {
JsonRpcMessage::Response(response)
if response.id == Some(RequestId::String("safe-id".to_string())) =>
{
Some(response)
}
_ => None,
})
.collect();
let [response] = correlated.as_slice() else {
panic!("expected exactly one response correlated to the proven unique id");
};
assert_eq!(
response
.error
.as_ref()
.and_then(|error| error.code.as_i32()),
Some(i32::from(McpErrorCode::InvalidRequest))
);
}
#[test]
fn returning_loop_terminates_after_fatal_framing_error_without_logging_payload() {
use std::sync::atomic::AtomicUsize;
let receive_calls = Arc::new(AtomicUsize::new(0));
let receive_count = Arc::clone(&receive_calls);
let sent = Arc::new(AtomicUsize::new(0));
let sent_count = Arc::clone(&sent);
let cx = Cx::for_testing();
let error = Server::new("fatal-framing-error-test", "1.0.0")
.build()
.run_loop_returning_with_dispatch_cx(
&cx,
&cx,
move |_, _worker_failed| {
receive_count.fetch_add(1, Ordering::Relaxed);
Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"SECRET_FRAMING_CANARY\r\nforged-log-line",
)))
},
move |_, _| {
sent_count.fetch_add(1, Ordering::Relaxed);
Ok(())
},
Arc::new(|_| {}),
None,
"test",
)
.expect_err("fatal framing failure must propagate from the returning loop");
assert_eq!(receive_calls.load(Ordering::Relaxed), 1);
assert_eq!(sent.load(Ordering::Relaxed), 0);
assert_eq!(error.code, McpErrorCode::InternalError);
assert_eq!(
error.data.as_ref().and_then(|data| data["kind"].as_str()),
Some("pump_failure")
);
assert_eq!(
classify_receive_error(&TransportError::Timeout),
ReceiveErrorDisposition::Terminate
);
assert_eq!(
classify_receive_error(&TransportError::ReceiveDeadlineExceeded),
ReceiveErrorDisposition::Terminate
);
assert_eq!(
classify_receive_error(&TransportError::ControlFrameTooLarge {
size: 513,
max: 512,
}),
ReceiveErrorDisposition::Terminate
);
assert_eq!(
classify_receive_error(&TransportError::Codec(
fastmcp_transport::CodecError::MessageTooLarge(42)
)),
ReceiveErrorDisposition::Terminate
);
}
#[test]
fn receive_pump_terminates_each_fatal_transport_failure_without_retrying() {
use std::sync::atomic::AtomicUsize;
for error in [
TransportError::Timeout,
TransportError::ReceiveDeadlineExceeded,
TransportError::ControlFrameTooLarge {
size: 513,
max: 512,
},
TransportError::Codec(fastmcp_transport::CodecError::MessageTooLarge(42)),
] {
let error_label = format!("{error:?}");
let receive_calls = Arc::new(AtomicUsize::new(0));
let receive_count = Arc::clone(&receive_calls);
let sent = Arc::new(AtomicUsize::new(0));
let sent_count = Arc::clone(&sent);
let mut next_error = Some(error);
let cx = Cx::for_testing();
let exit_code = Arc::new(Server::new("fatal-receive-error-test", "1.0.0").build())
.run_loop_pump_with_policy(
&cx,
&cx,
move |_, _worker_failed| {
receive_count.fetch_add(1, Ordering::Relaxed);
Err(next_error.take().unwrap_or(TransportError::Closed))
},
move |_, _| {
sent_count.fetch_add(1, Ordering::Relaxed);
Ok(())
},
Arc::new(|_| {}),
"test",
true,
None,
true,
true,
None,
None,
None,
PumpIoMode::Split,
);
assert_eq!(exit_code, 1, "fatal error must fail: {error_label}");
assert_eq!(
receive_calls.load(Ordering::Relaxed),
1,
"fatal error must not be retried: {error_label}"
);
assert_eq!(
sent.load(Ordering::Relaxed),
0,
"fatal error must not emit a response: {error_label}"
);
}
}
#[test]
fn lifespan_hooks_default_matches_new() {
let default_hooks = LifespanHooks::default();
let new_hooks = LifespanHooks::new();
assert!(default_hooks.on_startup.is_none());
assert!(default_hooks.on_shutdown.is_none());
assert!(new_hooks.on_startup.is_none());
assert!(new_hooks.on_shutdown.is_none());
}
#[test]
fn request_completion_wait_resolves_on_concurrent_done() {
use std::sync::Arc;
use std::thread;
let rc = Arc::new(RequestCompletion::new());
let rc_clone = rc.clone();
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(20));
rc_clone.mark_done();
});
// Should resolve within the timeout because the other thread marks done
assert!(rc.wait_timeout(Duration::from_secs(2)));
handle.join().unwrap();
}
#[test]
fn active_request_stores_region_id() {
let cx = Cx::for_testing();
let expected_region = cx.region_id();
let completion = Arc::new(RequestCompletion::new());
let ar = ActiveRequest::new(cx, completion);
assert_eq!(ar.region_id, expected_region);
}
#[test]
fn transport_auth_binds_session_and_rejects_cross_principal_reuse() {
#[derive(Debug)]
struct EchoAuthProvider;
impl AuthProvider for EchoAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
let access = request
.access_token()
.ok_or_else(|| McpError::invalid_request("missing auth token"))?;
let subject = fixed_test_subject_for_credential(&access.token)?;
Ok(AuthContext::with_subject(subject))
}
}
let server = Server::new("http-auth-test-server", "1.0.0")
.auth_provider(EchoAuthProvider)
.tool(HttpAuthEchoToolRuntime)
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "http-auth-test-client".to_string(),
version: "1.0.0".to_string(),
},
fastmcp_protocol::ClientCapabilities::default(),
"2024-11-05".to_string(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let mut run_request = |id: i64, token: &str| {
let authorization = format!("Bearer {token}");
block_on(server.handle_request_internal(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_auth_echo_tool_runtime",
"arguments": {}
})),
id,
),
¬ification_sender,
&request_sender,
None,
Some(&authorization),
))
.map(|handled| handled.finalize_for_return(&mut session))
.expect("tools/call must receive a JSON-RPC response")
};
for id in [1, 2] {
let response = run_request(id, "alpha");
let result = response.result.expect("bound principal should succeed");
let tool_result: CallToolResult =
serde_json::from_value(result).expect("parse tool result payload");
assert!(matches!(
tool_result.content.as_slice(),
[LegacyContent::Text { text, .. }] if text == "principal-alpha"
));
}
let rejected = run_request(3, "beta");
let error = rejected
.error
.expect("a different principal must not reuse session state");
assert_eq!(
error.code,
i32::from(McpErrorCode::ResourceForbidden).into()
);
assert!(rejected.result.is_none());
}
#[test]
fn dispatch_stateful_tool_calls_preserve_session_state_updates() {
let server = Server::new("http-state-test-server", "1.0.0")
.tool(HttpStatefulIncrementTool)
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "http-state-test-client".to_string(),
version: "1.0.0".to_string(),
},
fastmcp_protocol::ClientCapabilities::default(),
"2024-11-05".to_string(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let mut run_request = |id: i64| {
let json = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_stateful_increment_tool",
"arguments": {}
})),
id,
),
¬ification_sender,
&request_sender,
))
.expect("tools/call must receive a JSON-RPC response");
let result = json.result.expect("stateful request should succeed");
let tool_result: CallToolResult =
serde_json::from_value(result).expect("parse tool result payload");
assert!(!tool_result.is_error, "stateful tool unexpectedly errored");
match tool_result.content.as_slice() {
[LegacyContent::Text { text, .. }] => text.clone(),
other => panic!("expected single text tool result, got {other:?}"),
}
};
assert_eq!(run_request(1), "Counter: 1");
assert_eq!(run_request(2), "Counter: 2");
}
#[test]
fn dispatch_exclusive_requests_expose_request_auth_to_middleware() {
#[derive(Debug)]
struct EchoAuthProvider;
impl AuthProvider for EchoAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
let access = request
.access_token()
.ok_or_else(|| McpError::invalid_request("missing auth token"))?;
let subject = fixed_test_subject_for_credential(&access.token)?;
Ok(AuthContext::with_subject(subject))
}
}
let seen = Arc::new(Mutex::new(Vec::new()));
let middleware = CapturingAuthMiddleware {
seen: Arc::clone(&seen),
};
let server = Server::new("http-middleware-auth-test-server", "1.0.0")
.auth_provider(EchoAuthProvider)
.middleware(middleware)
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "http-middleware-client".to_string(),
version: "1.0.0".to_string(),
},
fastmcp_protocol::ClientCapabilities::default(),
"2024-11-05".to_string(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({
"auth": "Bearer alpha"
})),
1_i64,
),
¬ification_sender,
&request_sender,
))
.expect("tools/list must receive a JSON-RPC response");
let observed = seen
.lock()
.expect("captured auth middleware mutex should not be poisoned")
.clone();
assert_eq!(
observed,
vec![(
"tools/list".to_string(),
Some("principal-alpha".to_string())
)]
);
}
#[test]
fn dispatch_read_only_requests_expose_request_auth_to_middleware() {
#[derive(Debug)]
struct EchoAuthProvider;
impl AuthProvider for EchoAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
let access = request
.access_token()
.ok_or_else(|| McpError::invalid_request("missing auth token"))?;
let subject = fixed_test_subject_for_credential(&access.token)?;
Ok(AuthContext::with_subject(subject))
}
}
let seen = Arc::new(Mutex::new(Vec::new()));
let middleware = CapturingAuthMiddleware {
seen: Arc::clone(&seen),
};
let server = Server::new("http-read-only-middleware-auth-test-server", "1.0.0")
.auth_provider(EchoAuthProvider)
.middleware(middleware)
.tool(HttpCurrentAuthSubjectTool)
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "http-read-only-middleware-client".to_string(),
version: "1.0.0".to_string(),
},
fastmcp_protocol::ClientCapabilities::default(),
"2024-11-05".to_string(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let json = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_current_auth_subject_tool",
"arguments": {},
"auth": "Bearer beta"
})),
1_i64,
),
¬ification_sender,
&request_sender,
))
.expect("tools/call must receive a JSON-RPC response");
let result = json.result.expect("read-only auth request should succeed");
let tool_result: CallToolResult =
serde_json::from_value(result).expect("parse tool result payload");
match tool_result.content.as_slice() {
[LegacyContent::Text { text, .. }] => assert_eq!(text, "principal-beta"),
other => panic!("expected single text tool result, got {other:?}"),
}
let observed = seen
.lock()
.expect("captured auth middleware mutex should not be poisoned")
.clone();
assert_eq!(
observed,
vec![("tools/call".to_string(), Some("principal-beta".to_string()))]
);
}
#[test]
fn dispatch_exclusive_middleware_cannot_replace_committed_request_auth() {
let server = Server::new("http-exclusive-auth-override-test-server", "1.0.0")
.middleware(OverridingAuthMiddleware {
subject: "exclusive-override",
})
.tool(HttpCurrentAuthSubjectExclusiveTool)
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "http-exclusive-auth-override-client".to_string(),
version: "1.0.0".to_string(),
},
fastmcp_protocol::ClientCapabilities::default(),
"2024-11-05".to_string(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let json = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_current_auth_subject_exclusive_tool",
"arguments": {}
})),
1_i64,
),
¬ification_sender,
&request_sender,
))
.expect("tools/call must receive a JSON-RPC response");
let result = json
.result
.expect("exclusive request should succeed with committed anonymous auth");
let tool_result: CallToolResult =
serde_json::from_value(result).expect("parse tool result payload");
match tool_result.content.as_slice() {
[LegacyContent::Text { text, .. }] => assert_eq!(text, "anonymous"),
other => panic!("expected single text tool result, got {other:?}"),
}
}
#[test]
fn dispatch_read_only_middleware_cannot_replace_committed_request_auth() {
let server = Server::new("http-read-only-auth-override-test-server", "1.0.0")
.middleware(OverridingAuthMiddleware {
subject: "read-only-override",
})
.tool(HttpCurrentAuthSubjectTool)
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "http-read-only-auth-override-client".to_string(),
version: "1.0.0".to_string(),
},
fastmcp_protocol::ClientCapabilities::default(),
"2024-11-05".to_string(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let json = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_current_auth_subject_tool",
"arguments": {}
})),
1_i64,
),
¬ification_sender,
&request_sender,
))
.expect("tools/call must receive a JSON-RPC response");
let result = json
.result
.expect("read-only request should succeed with committed anonymous auth");
let tool_result: CallToolResult =
serde_json::from_value(result).expect("parse tool result payload");
match tool_result.content.as_slice() {
[LegacyContent::Text { text, .. }] => assert_eq!(text, "anonymous"),
other => panic!("expected single text tool result, got {other:?}"),
}
}
#[test]
fn dispatch_exclusive_auth_failures_flow_through_middleware_error_rewriting() {
let server = Server::new("http-exclusive-auth-error-test-server", "1.0.0")
.auth_provider(AlwaysFailAuthProvider)
.middleware(RewritingErrorMiddleware)
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "http-exclusive-auth-error-client".to_string(),
version: "1.0.0".to_string(),
},
fastmcp_protocol::ClientCapabilities::default(),
"2024-11-05".to_string(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let json = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({
"auth": "Bearer nope"
})),
1_i64,
),
¬ification_sender,
&request_sender,
))
.expect("tools/list must receive a JSON-RPC response");
let error = json
.error
.expect("auth failure should return JSON-RPC error");
assert_eq!(error.message, "rewritten: Authentication failed");
}
#[test]
fn dispatch_read_only_auth_failures_flow_through_middleware_error_rewriting() {
let server = Server::new("http-read-only-auth-error-test-server", "1.0.0")
.auth_provider(AlwaysFailAuthProvider)
.middleware(RewritingErrorMiddleware)
.tool(HttpCurrentAuthSubjectTool)
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "http-read-only-auth-error-client".to_string(),
version: "1.0.0".to_string(),
},
fastmcp_protocol::ClientCapabilities::default(),
"2024-11-05".to_string(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let json = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_current_auth_subject_tool",
"arguments": {},
"auth": "Bearer nope"
})),
1_i64,
),
¬ification_sender,
&request_sender,
))
.expect("tools/call must receive a JSON-RPC response");
let error = json
.error
.expect("auth failure should return JSON-RPC error");
assert_eq!(error.message, "rewritten: Authentication failed");
}
#[test]
fn auth_provider_error_payload_is_sanitized_before_middleware_and_wire() {
const CANARY: &str = "AUTH-PROVIDER-RAW-CREDENTIAL-CANARY";
#[derive(Debug)]
struct LeakingAuthProvider;
impl AuthProvider for LeakingAuthProvider {
fn authenticate(
&self,
_ctx: &McpContext,
_request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
Err(McpError::with_data(
McpErrorCode::ResourceForbidden,
format!("denied bearer {CANARY}"),
serde_json::json!({"raw_token": CANARY}),
))
}
}
#[derive(Debug, Clone)]
struct CapturingErrorMiddleware {
seen: Arc<Mutex<Vec<McpError>>>,
}
impl Middleware for CapturingErrorMiddleware {
fn on_error(
&self,
_ctx: &McpContext,
_request: &JsonRpcRequest,
error: McpError,
) -> McpError {
self.seen
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(error.clone());
error
}
}
let seen = Arc::new(Mutex::new(Vec::new()));
let server = Server::new("auth-error-sanitization-test", "1.0.0")
.auth_provider(LeakingAuthProvider)
.middleware(CapturingErrorMiddleware {
seen: Arc::clone(&seen),
})
.build();
let mut session = Session::new(server.info.clone(), server.capabilities.clone());
session.initialize(
fastmcp_protocol::ClientInfo {
name: "auth-error-sanitization-client".to_string(),
version: "1.0.0".to_string(),
},
fastmcp_protocol::ClientCapabilities::default(),
"2024-11-05".to_string(),
);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({"authorization": "Bearer peer-token"})),
1_i64,
),
¬ification_sender,
&request_sender,
))
.expect("tools/list must receive a JSON-RPC response");
let wire = serde_json::to_string(&response).expect("JSON-RPC response serializes");
assert!(!wire.contains(CANARY));
let response: JsonRpcResponse = serde_json::from_str(&wire).expect("JSON-RPC response");
let error = response.error.expect("authentication must fail");
assert_eq!(error.message, "Authentication failed");
assert!(error.data.is_none());
let seen = seen
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(seen.len(), 1);
assert_eq!(seen[0].message, "Authentication failed");
assert!(seen[0].data.is_none());
assert!(!format!("{:?}", seen[0]).contains(CANARY));
}
#[test]
fn router_tools_call_injects_explicit_request_auth() {
let server = Server::new("router-auth-test-server", "1.0.0")
.tool(HttpCurrentAuthSubjectTool)
.build();
let state = SessionState::new();
let request_ctx = McpContext::with_state(Cx::for_testing(), 41, state.clone())
.with_auth(AuthContext::with_subject("alpha"));
let result = block_on(server.router.handle_tools_call(
&request_ctx,
CallToolParams {
name: "http_current_auth_subject_tool".to_string(),
arguments: Some(serde_json::json!({})),
meta: None,
},
state,
None,
None,
))
.expect("tool call should succeed");
match result.content.as_slice() {
[LegacyContent::Text { text, .. }] => assert_eq!(text, "alpha"),
other => panic!("expected single text tool result, got {other:?}"),
}
}
#[test]
fn request_cost_accounting_flows_through_auth_middleware_and_handler() {
type Observation = (&'static str, Option<u64>);
#[derive(Debug, Clone)]
struct ChargingAuth {
seen: Arc<Mutex<Vec<Observation>>>,
}
impl AuthProvider for ChargingAuth {
fn authenticate(
&self,
ctx: &McpContext,
_request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
ctx.consume_cost(1)?;
self.seen
.lock()
.expect("cost observation mutex poisoned")
.push(("auth", ctx.budget().cost_quota));
Ok(AuthContext::anonymous())
}
}
#[derive(Debug, Clone)]
struct ChargingMiddleware {
seen: Arc<Mutex<Vec<Observation>>>,
}
impl Middleware for ChargingMiddleware {
fn on_request(
&self,
ctx: &McpContext,
_request: &JsonRpcRequest,
) -> McpResult<MiddlewareDecision> {
ctx.consume_cost(1)?;
self.seen
.lock()
.expect("cost observation mutex poisoned")
.push(("middleware", ctx.budget().cost_quota));
Ok(MiddlewareDecision::Continue)
}
}
#[derive(Debug, Clone)]
struct ChargingTool {
seen: Arc<Mutex<Vec<Observation>>>,
}
impl ToolHandler for ChargingTool {
fn definition(&self) -> Tool {
Tool {
name: "charging_tool".to_string(),
description: Some("Charges the shared request cost budget".to_string()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
ctx.consume_cost(1)?;
self.seen
.lock()
.expect("cost observation mutex poisoned")
.push(("handler", ctx.budget().cost_quota));
Ok(vec![Content::text("charged")])
}
}
let seen = Arc::new(Mutex::new(Vec::new()));
let server = Server::new("request-cost-accounting-test", "1.0.0")
.auth_provider(ChargingAuth {
seen: Arc::clone(&seen),
})
.middleware(ChargingMiddleware {
seen: Arc::clone(&seen),
})
.tool(ChargingTool {
seen: Arc::clone(&seen),
})
.build();
let mut session = initialized_test_session(&server);
let cx = Cx::for_testing_with_budget(Budget::new().with_cost_quota(3));
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "charging_tool",
"arguments": {}
})),
1,
);
let response = block_on(server.dispatch_request(
&cx,
&mut session,
request,
¬ification_sender,
&request_sender,
))
.expect("request should produce a response");
assert!(
response.error.is_none(),
"unexpected response: {response:?}"
);
assert_eq!(
*seen.lock().expect("cost observation mutex poisoned"),
vec![
("auth", Some(2)),
("middleware", Some(1)),
("handler", Some(0)),
]
);
assert_eq!(
cx.budget().cost_quota,
Some(3),
"request accounting must not mutate the caller-owned ambient Cx"
);
let rejected_seen = Arc::new(Mutex::new(Vec::new()));
let rejected_server = Server::new("request-cost-overrun-test", "1.0.0")
.auth_provider(ChargingAuth {
seen: Arc::clone(&rejected_seen),
})
.middleware(ChargingMiddleware {
seen: Arc::clone(&rejected_seen),
})
.tool(ChargingTool {
seen: Arc::clone(&rejected_seen),
})
.build();
let mut rejected_session = initialized_test_session(&rejected_server);
let rejected_cx = Cx::for_testing_with_budget(Budget::new().with_cost_quota(2));
let rejected_response = block_on(rejected_server.dispatch_request(
&rejected_cx,
&mut rejected_session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "charging_tool",
"arguments": {}
})),
2,
),
¬ification_sender,
&request_sender,
))
.expect("over-budget tool request should produce a response");
assert!(rejected_response.result.is_none());
assert_eq!(
rejected_response
.error
.as_ref()
.expect("budget refusal must be a JSON-RPC cancellation error")
.code,
i32::from(McpErrorCode::RequestCancelled).into()
);
assert_eq!(
*rejected_seen
.lock()
.expect("cost observation mutex poisoned"),
vec![("auth", Some(1)), ("middleware", Some(0))],
"the N+1 handler debit must fail before handler effects"
);
}
#[test]
fn failed_authentication_cannot_publish_a_tentative_identity() {
#[derive(Debug, Clone, Copy)]
struct MutatingFailAuth;
impl AuthProvider for MutatingFailAuth {
fn authenticate(
&self,
ctx: &McpContext,
_request: AuthRequest<'_>,
) -> McpResult<AuthContext> {
ctx.set_auth(AuthContext::with_subject("tentative-secret-identity"));
Err(McpError::invalid_request("authentication rejected"))
}
}
#[derive(Debug, Clone)]
struct CaptureAuthOnError {
seen: Arc<Mutex<Vec<Option<String>>>>,
}
impl Middleware for CaptureAuthOnError {
fn on_error(
&self,
ctx: &McpContext,
_request: &JsonRpcRequest,
error: McpError,
) -> McpError {
self.seen
.lock()
.expect("auth error observation mutex poisoned")
.push(ctx.auth().and_then(|auth| auth.subject));
error
}
}
let seen = Arc::new(Mutex::new(Vec::new()));
let server = Server::new("transactional-auth-test", "1.0.0")
.auth_provider(MutatingFailAuth)
.middleware(CaptureAuthOnError {
seen: Arc::clone(&seen),
})
.build();
let mut session = initialized_test_session(&server);
let response = dispatch_test_request(&server, &mut session, "tools/list");
assert!(response.error.is_some());
assert_eq!(
*seen.lock().expect("auth error observation mutex poisoned"),
vec![None],
"failed provider identity must remain isolated from error middleware"
);
}
#[test]
fn zero_cost_quota_allows_a_zero_cost_request() {
let server = Server::new("zero-cost-request-test", "1.0.0")
.tool(HttpCurrentAuthSubjectTool)
.build();
let mut session = initialized_test_session(&server);
let cx = Cx::for_testing_with_budget(Budget::new().with_cost_quota(0));
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let response = block_on(server.dispatch_request(
&cx,
&mut session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_current_auth_subject_tool",
"arguments": {}
})),
1,
),
¬ification_sender,
&request_sender,
))
.expect("zero-cost request should produce a response");
assert!(
response.error.is_none(),
"unexpected response: {response:?}"
);
assert!(response.result.is_some());
}
#[test]
fn production_dispatch_wires_router_backed_nested_tool_calls() {
#[derive(Debug, Clone, Copy)]
struct InnerTool;
impl ToolHandler for InnerTool {
fn definition(&self) -> Tool {
Tool {
name: "inner_tool".to_string(),
description: Some("Nested dispatch target".to_string()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
Ok(vec![Content::text("inner-result")])
}
}
#[derive(Debug, Clone, Copy)]
struct OuterTool;
impl ToolHandler for OuterTool {
fn definition(&self) -> Tool {
Tool {
name: "outer_tool".to_string(),
description: Some("Calls another registered tool".to_string()),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
Err(McpError::internal_error(
"outer_tool requires async dispatch",
))
}
fn call_async<'a>(
&'a self,
ctx: &'a McpContext,
_args: serde_json::Value,
) -> BoxFuture<'a, fastmcp_core::McpOutcome<Vec<Content>>> {
Box::pin(async move {
match ctx.call_tool("inner_tool", serde_json::json!({})).await {
Ok(result) => {
let text = result.first_text().unwrap_or("missing nested text");
asupersync::Outcome::Ok(vec![Content::text(text)])
}
Err(error) => asupersync::Outcome::Err(error),
}
})
}
}
let server = Server::new("nested-production-dispatch-test", "1.0.0")
.tool(InnerTool)
.tool(OuterTool)
.build();
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "outer_tool",
"arguments": {}
})),
1_i64,
),
¬ification_sender,
&request_sender,
))
.expect("nested tool request should produce a response");
let result: CallToolResult = serde_json::from_value(
response
.result
.expect("top-level tool should complete through nested router dispatch"),
)
.expect("decode nested tool result");
match result.content.as_slice() {
[LegacyContent::Text { text, .. }] => assert_eq!(text, "inner-result"),
other => panic!("expected nested tool text result, got {other:?}"),
}
}
#[test]
fn escaped_request_context_is_revoked_and_does_not_retain_server_router() {
#[derive(Clone)]
struct CaptureContextTool {
captured: Arc<Mutex<Option<McpContext>>>,
}
impl ToolHandler for CaptureContextTool {
fn definition(&self) -> Tool {
Tool {
name: "capture_context".to_string(),
description: Some(
"Captures a request context for revocation testing".to_string(),
),
input_schema: serde_json::json!({"type": "object"}),
output_schema: None,
icon: None,
version: None,
tags: Vec::new(),
annotations: None,
}
}
fn call(&self, ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
*self
.captured
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(ctx.clone());
Ok(vec![Content::text("captured")])
}
}
let captured = Arc::new(Mutex::new(None));
let server = Server::new("request-lease-test", "1.0.0")
.tool(CaptureContextTool {
captured: Arc::clone(&captured),
})
.build();
let mut session = initialized_test_session(&server);
let notification_sender: NotificationSender = Arc::new(|_| {});
let request_sender = test_request_sender();
let response = block_on(server.dispatch_request(
&Cx::for_testing(),
&mut session,
JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "capture_context",
"arguments": {}
})),
1_i64,
),
¬ification_sender,
&request_sender,
))
.expect("capture request should produce a response");
assert!(
response.error.is_none(),
"unexpected response: {response:?}"
);
let escaped = captured
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take()
.expect("handler should capture its context");
let error = block_on(escaped.call_tool("capture_context", serde_json::json!({})))
.expect_err("nested authority must close when dispatch returns");
assert_eq!(error.code, McpErrorCode::RequestCancelled);
let router = server.into_router();
assert!(
router
.tools()
.iter()
.any(|tool| tool.name == "capture_context")
);
}
#[test]
fn http_bind_rejects_an_invalid_address_without_running_lifecycle_hooks() {
let startup_called = Arc::new(AtomicBool::new(false));
let shutdown_called = Arc::new(AtomicBool::new(false));
let startup_observer = Arc::clone(&startup_called);
let shutdown_observer = Arc::clone(&shutdown_called);
let cx = Cx::for_testing();
let result = block_on(
Server::new("http-bind-test", "1.0.0")
.on_startup(move || {
startup_observer.store(true, Ordering::SeqCst);
Ok::<(), std::io::Error>(())
})
.on_shutdown(move || shutdown_observer.store(true, Ordering::SeqCst))
.build()
.bind_http(&cx, "not-a-bindable-socket-address"),
);
let Err(error) = result else {
panic!("an invalid bind address must be rejected by the live listener");
};
assert!(!startup_called.load(Ordering::SeqCst));
assert!(!shutdown_called.load(Ordering::SeqCst));
assert!(
error.message.contains("HTTP listener bind failed"),
"the bind failure must report the live listener boundary: {error:?}"
);
}
#[test]
fn final_subscription_acknowledges_filters_tags_and_releases_request_owned_state() {
let server = Server::new("final-subscription-stream-test", "1.0.0").build();
let registry = Arc::clone(&server.final_subscriptions);
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
});
let subscription_id = RequestId::String("subscription-71".to_owned());
let lease = registry
.open(
subscription_id.clone(),
SubscriptionFilter {
tools_list_changed: Some(true),
resources_list_changed: Some(false),
..SubscriptionFilter::default()
},
false,
None,
McpRequestCancellation::new(),
None,
sender,
)
.expect("final subscription should admit a bounded stream");
let acknowledgement = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.first()
.cloned()
.expect("acknowledgement must be the first stream frame");
let ServerNotification::SubscriptionsAcknowledged(acknowledgement) =
ServerNotification::decode(&acknowledgement)
.expect("acknowledgement must use the exact final server union")
else {
panic!("first subscription frame must be an acknowledgement");
};
assert_eq!(acknowledgement.notifications.tools_list_changed, Some(true));
assert_eq!(acknowledgement.notifications.resources_list_changed, None);
let acknowledged_id: RequestId = serde_json::from_value(
acknowledgement
.meta
.as_ref()
.and_then(|metadata| metadata.get(FINAL_SUBSCRIPTION_ID_META_KEY))
.cloned()
.expect("acknowledgement must carry its subscription id"),
)
.expect("subscription id metadata must retain the JSON-RPC id");
assert_eq!(acknowledged_id, subscription_id);
assert_eq!(
server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
.expect("selected catalog event should publish"),
1
);
assert_eq!(
server
.publish_subscription_notification(ServerNotification::ResourcesListChanged(None))
.expect("unselected catalog event should be safely filtered"),
0
);
let delivered = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(1)
.cloned()
.expect("selected event must follow acknowledgement");
let ServerNotification::ToolsListChanged(Some(delivered)) =
ServerNotification::decode(&delivered)
.expect("delivered event must use the exact final server union")
else {
panic!("selected event must remain a tools/list_changed notification");
};
let delivered_id: RequestId = serde_json::from_value(
delivered
.meta
.as_ref()
.and_then(|metadata| metadata.get(FINAL_SUBSCRIPTION_ID_META_KEY))
.cloned()
.expect("selected event must carry its subscription id"),
)
.expect("delivery metadata must retain the JSON-RPC id");
assert_eq!(delivered_id, subscription_id);
drop(lease);
assert!(
registry
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.is_empty(),
"request completion or cancellation must release the subscription entry"
);
}
#[cfg(all(feature = "proxy", feature = "tasks"))]
#[test]
fn subscription_filter_admission_matches_reordered_active_sets() {
let task_a =
fastmcp_protocol::FinalTaskId::parse("task-a").expect("bounded final task identifier");
let task_b =
fastmcp_protocol::FinalTaskId::parse("task-b").expect("bounded final task identifier");
let mut expected = SubscriptionFilter {
prompts_list_changed: Some(false),
resource_subscriptions: Some(vec![
"file:///subscriptions/a".to_owned(),
"file:///subscriptions/b".to_owned(),
]),
resources_list_changed: Some(false),
tools_list_changed: Some(true),
additional: BTreeMap::from([(
"io.example/future-filter".to_owned(),
serde_json::json!({"enabled": true}),
)]),
};
set_task_subscription_ids(
&mut expected,
vec![task_a.clone(), task_b.clone(), task_a.clone()],
)
.expect("compose the requested Tasks selection");
let mut acknowledged = SubscriptionFilter {
resource_subscriptions: Some(vec![
"file:///subscriptions/b".to_owned(),
"file:///subscriptions/a".to_owned(),
]),
tools_list_changed: Some(true),
..SubscriptionFilter::default()
};
set_task_subscription_ids(&mut acknowledged, vec![task_b, task_a])
.expect("compose the reordered Tasks acknowledgement");
assert!(
subscription_filter_admission_matches(&expected, &acknowledged)
.expect("equivalent active selections must be valid"),
"resource and Tasks order, inactive booleans, and unknown filters must not narrow admission"
);
}
#[cfg(all(feature = "proxy", feature = "tasks"))]
#[test]
fn subscription_filter_admission_rejects_one_missing_task_id() {
let task_a =
fastmcp_protocol::FinalTaskId::parse("task-a").expect("bounded final task identifier");
let task_b =
fastmcp_protocol::FinalTaskId::parse("task-b").expect("bounded final task identifier");
let mut expected = SubscriptionFilter {
resource_subscriptions: Some(vec![
"file:///subscriptions/a".to_owned(),
"file:///subscriptions/b".to_owned(),
]),
tools_list_changed: Some(true),
..SubscriptionFilter::default()
};
set_task_subscription_ids(&mut expected, vec![task_a.clone(), task_b])
.expect("compose the requested Tasks selection");
let mut acknowledged = expected.clone();
acknowledged
.additional
.remove(fastmcp_protocol::TASK_SUBSCRIPTION_IDS_KEY)
.expect("the acknowledgement starts from the requested Tasks selection");
set_task_subscription_ids(&mut acknowledged, vec![task_a])
.expect("remove exactly one task from the acknowledgement");
assert!(
!subscription_filter_admission_matches(&expected, &acknowledged)
.expect("well-formed narrowed Tasks selection must compare"),
"changing only one requested task ID must remain a rejected narrowing"
);
}
#[test]
fn final_subscription_registry_isolates_duplicate_ids_by_http_body_owner() {
let server = Server::new("modern-http-listen-key-test", "1.0.0").build();
let registry = Arc::clone(&server.final_subscriptions);
let sender: NotificationSender = Arc::new(|_| {});
let first_cancellation = McpRequestCancellation::new();
let second_cancellation = McpRequestCancellation::new();
let first = registry
.open(
RequestId::Integer("873".to_owned()),
SubscriptionFilter::default(),
false,
Some(873),
first_cancellation.clone(),
None,
Arc::clone(&sender),
)
.expect("the first modern HTTP response body must admit");
let second = registry
.open(
RequestId::Number(873),
SubscriptionFilter::default(),
false,
Some(874),
second_cancellation.clone(),
None,
sender,
)
.expect("a distinct response body may reuse the same JSON-RPC id");
assert_eq!(
registry
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.len(),
2,
"request-body ownership keeps equal JSON-RPC ids independent",
);
assert!(
!registry.cancel_modern_http_owner(875),
"a one-variable wrong response-body owner must not cancel either listen",
);
assert!(registry.cancel_modern_http_owner(873));
assert!(
first_cancellation.is_cancel_requested(),
"the selected response body retains its own cancellation authority",
);
assert!(!second_cancellation.is_cancel_requested());
drop(first);
drop(second);
assert!(
registry
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.modern_http_owners
.is_empty(),
"request completion must remove every response-body owner",
);
}
#[cfg(feature = "tasks")]
#[test]
fn final_tasks_runtime_publishes_only_to_acknowledged_exact_task_ids() {
let application_notifications = Arc::new(Mutex::new(Vec::new()));
let (server, _service) = final_tasks_test_server(Arc::clone(&application_notifications));
let runtime = server
.final_task_runtime()
.expect("final Tasks runtime must remain public")
.clone();
let created = runtime
.create_task_with_work(
final_tasks_test_work_descriptor(),
Some("queued".to_owned()),
)
.expect("task must be durable before subscription");
let task_id = created.task.base().task_id.clone();
let foreign_id =
fastmcp_protocol::FinalTaskId::parse("foreign-task").expect("bounded foreign task id");
let mut requested = SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
};
set_task_subscription_ids(
&mut requested,
vec![task_id.clone(), foreign_id.clone(), task_id.clone()],
)
.expect("compose task IDs beside the core filter");
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
});
let subscription_id = RequestId::String("task-subscription-73".to_owned());
let subscription_cancellation = McpRequestCancellation::new();
let _lease = server
.final_subscriptions
.open(
subscription_id.clone(),
requested,
true,
None,
subscription_cancellation.clone(),
None,
sender,
)
.expect("negotiated Tasks filter must open");
let acknowledgement = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.first()
.cloned()
.expect("acknowledgement must precede task events");
let ServerNotification::SubscriptionsAcknowledged(acknowledgement) =
ServerNotification::decode(&acknowledgement).expect("typed acknowledgement")
else {
panic!("first Tasks stream frame must be the core acknowledgement");
};
let accepted = task_subscription_ids(&acknowledgement.notifications)
.expect("acknowledged Tasks fragment must validate")
.expect("acknowledgement must retain taskIds");
assert_eq!(accepted, [task_id.clone(), foreign_id]);
assert_eq!(acknowledgement.notifications.tools_list_changed, Some(true));
runtime
.cancel_task(&task_id)
.expect("unelected task cancellation commits terminal state");
assert!(matches!(
runtime
.get_task(&task_id)
.expect("terminally cancelled task remains readable")
.task,
fastmcp_protocol::Task::Cancelled(_)
));
let delivered = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(1)
.cloned()
.expect("matching task transition must follow acknowledgement");
let delivered: FinalTaskStatusNotification = serde_json::from_value(
serde_json::to_value(delivered).expect("notification request must serialize"),
)
.expect("notification must retain the exact Tasks wire type");
assert_eq!(delivered.params.task.base().task_id, task_id);
assert_eq!(
delivered
.params
.meta
.as_ref()
.and_then(|metadata| metadata.get(FINAL_SUBSCRIPTION_ID_META_KEY)),
Some(&serde_json::to_value(subscription_id).expect("subscription id"))
);
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
2,
"one acknowledged stream receives exactly one matching task event"
);
subscription_cancellation.cancel();
assert_eq!(
server
.final_subscriptions
.publish_task(delivered)
.expect("peer-cancelled task publication remains well-formed"),
0,
"a peer-cancelled token must prevent a Tasks callback before lease cleanup",
);
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
2,
"peer-cancelled Tasks publication must leave delivery state unchanged",
);
}
#[test]
fn final_subscription_rejects_request_scoped_message_without_delivery() {
use fastmcp_protocol::{FinalLogMessageParams, common_types::LoggingLevel};
let server = Server::new("final-subscription-rejection-test", "1.0.0").build();
let registry = Arc::clone(&server.final_subscriptions);
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
});
let _lease = registry
.open(
RequestId::Number(72),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
None,
McpRequestCancellation::new(),
None,
sender,
)
.expect("selected catalog subscription should admit");
assert_eq!(
server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
.expect("the selected event establishes the positive neighbor"),
1
);
let sent_before_rejection = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len();
let error = server
.publish_subscription_notification(ServerNotification::Message(FinalLogMessageParams {
level: LoggingLevel::Info,
logger: None,
data: serde_json::json!({"message": "request scoped"}),
meta: None,
additional: BTreeMap::new(),
}))
.expect_err("changing only the notification category must reject request-scoped logs");
assert_eq!(error.code, McpErrorCode::InvalidParams);
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
sent_before_rejection,
"rejected request-scoped notifications must not mutate the stream"
);
}
#[test]
fn final_subscription_reentrant_ack_publication_flushes_after_acknowledgement() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let published = Arc::new(AtomicUsize::new(usize::MAX));
let callback_count = Arc::new(AtomicUsize::new(0));
let sender_registry = Arc::clone(®istry);
let sender_sent = Arc::clone(&sent);
let sender_published = Arc::clone(&published);
let sender_callback_count = Arc::clone(&callback_count);
let sender: NotificationSender = Arc::new(move |notification| {
sender_sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
if sender_callback_count.fetch_add(1, Ordering::AcqRel) == 0 {
sender_published.store(
sender_registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("reentrant catalogue publication must remain well-formed"),
Ordering::Release,
);
}
});
let lease = registry
.open(
RequestId::Number(721),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
None,
McpRequestCancellation::new(),
None,
sender,
)
.expect("a reentrant acknowledgement publication must open");
assert_eq!(published.load(Ordering::Acquire), 1);
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(sent.len(), 2);
assert!(matches!(
ServerNotification::decode(&sent[0]),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
));
assert!(matches!(
ServerNotification::decode(&sent[1]),
Ok(ServerNotification::ToolsListChanged(_))
));
drop(sent);
drop(lease);
let negative_registry = Arc::new(FinalSubscriptionRegistry::default());
let negative_sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let negative_published = Arc::new(AtomicUsize::new(usize::MAX));
let negative_callback_count = Arc::new(AtomicUsize::new(0));
let negative_sender_registry = Arc::clone(&negative_registry);
let negative_sender_sent = Arc::clone(&negative_sent);
let negative_sender_published = Arc::clone(&negative_published);
let negative_sender_callback_count = Arc::clone(&negative_callback_count);
let negative_sender: NotificationSender = Arc::new(move |notification| {
negative_sender_sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
if negative_sender_callback_count.fetch_add(1, Ordering::AcqRel) == 0 {
negative_sender_published.store(
negative_sender_registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("wrong-filter publication must remain well-formed"),
Ordering::Release,
);
}
});
let _negative_lease = negative_registry
.open(
RequestId::Number(722),
SubscriptionFilter {
prompts_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
None,
McpRequestCancellation::new(),
None,
negative_sender,
)
.expect("the one-filter negative must still acknowledge");
assert_eq!(negative_published.load(Ordering::Acquire), 0);
assert_eq!(
negative_sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"changing only the accepted filter must leave the stream at its acknowledgement",
);
}
#[test]
fn final_subscription_publishers_do_not_wait_for_held_ack_and_flush_fifo() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let gate = Arc::new((Mutex::new((false, false)), Condvar::new()));
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sender_gate = Arc::clone(&gate);
let sender_sent = Arc::clone(&sent);
let callback_count = Arc::new(AtomicUsize::new(0));
let sender_callback_count = Arc::clone(&callback_count);
let sender: NotificationSender = Arc::new(move |notification| {
sender_sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
if sender_callback_count.fetch_add(1, Ordering::AcqRel) == 0 {
let (lock, ready) = &*sender_gate;
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.0 = true;
ready.notify_all();
while !state.1 {
state = ready
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
});
let opener_registry = Arc::clone(®istry);
let opener = thread::spawn(move || {
opener_registry.open(
RequestId::Number(723),
SubscriptionFilter {
resources_list_changed: Some(true),
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
None,
McpRequestCancellation::new(),
None,
sender,
)
});
let (lock, ready) = &*gate;
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while !state.0 {
state = ready
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
drop(state);
let publisher_registry = Arc::clone(®istry);
let (published_sender, published_receiver) = std::sync::mpsc::channel();
let publisher = thread::spawn(move || {
let tools = publisher_registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("held-ack tools publication must remain well-formed");
let resources = publisher_registry
.publish(ServerNotification::ResourcesListChanged(None))
.expect("held-ack resources publication must remain well-formed");
published_sender
.send((tools, resources))
.expect("publication result receiver must remain live");
});
assert_eq!(
published_receiver
.recv_timeout(Duration::from_secs(1))
.expect("publishers must not wait for the acknowledgement callback"),
(1, 1),
);
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.1 = true;
ready.notify_all();
drop(state);
publisher.join().expect("publisher must not panic");
let lease = opener
.join()
.expect("opener must not panic")
.expect("queued publications must preserve stream admission");
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(sent.len(), 3);
assert!(matches!(
ServerNotification::decode(&sent[0]),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
));
assert!(matches!(
ServerNotification::decode(&sent[1]),
Ok(ServerNotification::ToolsListChanged(_))
));
assert!(matches!(
ServerNotification::decode(&sent[2]),
Ok(ServerNotification::ResourcesListChanged(_))
));
drop(sent);
drop(lease);
}
#[test]
fn final_subscription_opening_queue_capacity_fails_closed_without_delivery() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let gate = Arc::new((Mutex::new((false, false)), Condvar::new()));
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sender_gate = Arc::clone(&gate);
let sender_sent = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
sender_sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
let (lock, ready) = &*sender_gate;
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !state.0 {
state.0 = true;
ready.notify_all();
while !state.1 {
state = ready
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
});
let cancellation = McpRequestCancellation::new();
let opener_registry = Arc::clone(®istry);
let opener_cancellation = cancellation.clone();
let opener = thread::spawn(move || {
opener_registry.open(
RequestId::Number(725),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
None,
opener_cancellation,
None,
sender,
)
});
let (lock, ready) = &*gate;
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while !state.0 {
state = ready
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
drop(state);
for admitted in 0..MAX_FINAL_SUBSCRIPTION_QUEUED_EVENTS {
assert_eq!(
registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("bounded opening publication must remain well-formed"),
1,
"opening event {admitted} must fit within the frozen bound",
);
}
assert_eq!(
registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("the first over-capacity publication must fail closed"),
0,
);
assert!(
cancellation.is_cancel_requested(),
"capacity exhaustion must cancel the opening stream"
);
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.1 = true;
ready.notify_all();
drop(state);
assert!(
opener.join().expect("opener must not panic").is_err(),
"a capacity-failed opening must not return a live lease"
);
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"capacity failure must discard every queued event",
);
assert!(
registry
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.is_empty(),
"capacity failure must release its registry entry when acknowledgement unwinds",
);
}
#[test]
fn final_subscription_acknowledgement_panic_discards_reentrant_publication() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let published = Arc::new(AtomicUsize::new(usize::MAX));
let sender_registry = Arc::clone(®istry);
let sender_sent = Arc::clone(&sent);
let sender_published = Arc::clone(&published);
let sender: NotificationSender = Arc::new(move |notification| {
sender_sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
sender_published.store(
sender_registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("reentrant publication before acknowledgement panic must be valid"),
Ordering::Release,
);
panic!("planted acknowledgement sender panic");
});
let cancellation = McpRequestCancellation::new();
assert!(
registry
.open(
RequestId::Number(726),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
None,
cancellation.clone(),
None,
sender,
)
.is_err(),
"an acknowledgement panic must fail stream admission"
);
assert_eq!(published.load(Ordering::Acquire), 1);
assert!(cancellation.is_cancel_requested());
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"the queued event must never be delivered after acknowledgement panic",
);
assert!(
registry
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.is_empty(),
"acknowledgement panic must remove the failed opening entry",
);
}
#[test]
fn final_subscription_peer_cancellation_discards_held_ack_publication() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let gate = Arc::new((Mutex::new((false, false)), Condvar::new()));
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sender_gate = Arc::clone(&gate);
let sender_sent = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
sender_sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
let (lock, ready) = &*sender_gate;
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !state.0 {
state.0 = true;
ready.notify_all();
while !state.1 {
state = ready
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
});
let cancellation = McpRequestCancellation::new();
let opener_registry = Arc::clone(®istry);
let opener_cancellation = cancellation.clone();
let opener = thread::spawn(move || {
opener_registry.open(
RequestId::Number(727),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
Some(727),
opener_cancellation,
None,
sender,
)
});
let (lock, ready) = &*gate;
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while !state.0 {
state = ready
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
drop(state);
assert_eq!(
registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("publication during a held acknowledgement must be admitted"),
1,
);
assert!(registry.cancel_modern_http_owner(727));
assert!(cancellation.is_cancel_requested());
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.1 = true;
ready.notify_all();
drop(state);
assert!(
opener.join().expect("opener must not panic").is_err(),
"peer cancellation must prevent a live lease"
);
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"peer cancellation must discard the queued event",
);
}
#[cfg(feature = "tasks")]
#[test]
fn final_task_subscription_reentrant_ack_publication_flushes_after_acknowledgement() {
let application_notifications = Arc::new(Mutex::new(Vec::new()));
let (server, _service) = final_tasks_test_server(application_notifications);
let runtime = server
.final_task_runtime()
.expect("final Tasks runtime must remain public");
let created = runtime
.create_task_with_work(
final_tasks_test_work_descriptor(),
Some("queued".to_owned()),
)
.expect("task must be durable before subscription");
let task_id = created.task.base().task_id.clone();
let task_notification = crate::tasks::final_task_notification(&created.task);
let registry = Arc::clone(&server.final_subscriptions);
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let published = Arc::new(AtomicUsize::new(usize::MAX));
let callback_count = Arc::new(AtomicUsize::new(0));
let sender_registry = Arc::clone(®istry);
let sender_sent = Arc::clone(&sent);
let sender_published = Arc::clone(&published);
let sender_callback_count = Arc::clone(&callback_count);
let sender: NotificationSender = Arc::new(move |notification| {
sender_sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
if sender_callback_count.fetch_add(1, Ordering::AcqRel) == 0 {
sender_published.store(
sender_registry
.publish_task(task_notification.clone())
.expect("reentrant Tasks publication must remain well-formed"),
Ordering::Release,
);
}
});
let mut requested = SubscriptionFilter::default();
set_task_subscription_ids(&mut requested, vec![task_id.clone()])
.expect("compose the exact Tasks selection");
let _lease = registry
.open(
RequestId::Number(724),
requested,
true,
None,
McpRequestCancellation::new(),
None,
sender,
)
.expect("a reentrant Tasks publication must open");
assert_eq!(published.load(Ordering::Acquire), 1);
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(sent.len(), 2);
assert!(matches!(
ServerNotification::decode(&sent[0]),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
));
let delivered: FinalTaskStatusNotification = serde_json::from_value(
serde_json::to_value(&sent[1]).expect("queued Tasks event must serialize"),
)
.expect("queued Tasks event must retain its wire type");
assert_eq!(delivered.params.task.base().task_id, task_id);
}
#[test]
fn final_subscription_acknowledgement_sender_can_reenter_termination() {
let (done_sender, done_receiver) = std::sync::mpsc::channel();
let worker = std::thread::spawn(move || {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let registry_for_sender = Arc::clone(®istry);
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let callback_count = Arc::new(AtomicUsize::new(0));
let callback_count_for_sender = Arc::clone(&callback_count);
let terminated = Arc::new(AtomicUsize::new(0));
let terminated_for_sender = Arc::clone(&terminated);
let cancellation = McpRequestCancellation::new();
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
if callback_count_for_sender.fetch_add(1, Ordering::AcqRel) == 0 {
terminated_for_sender.store(registry_for_sender.terminate(), Ordering::Release);
}
});
let lease = registry
.open(
RequestId::Number(73),
SubscriptionFilter::default(),
false,
None,
cancellation.clone(),
None,
sender,
)
.expect("reentrant server termination should complete stream admission");
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
done_sender
.send((
lease.has_graceful_completion(),
cancellation.is_cancel_requested(),
callback_count.load(Ordering::Acquire),
terminated.load(Ordering::Acquire),
sent,
))
.expect("reentrant acknowledgement result receiver should remain live");
});
let (graceful, cancelled, callback_count, terminated, sent) = done_receiver
.recv_timeout(Duration::from_secs(2))
.expect("acknowledgement callback reentry must not deadlock");
worker
.join()
.expect("reentrant acknowledgement worker should not panic");
assert!(graceful);
assert!(cancelled);
assert_eq!(callback_count, 2);
assert_eq!(terminated, 1);
assert!(matches!(
ServerNotification::decode(&sent[0]),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
));
assert!(matches!(
ServerNotification::decode(&sent[1]),
Ok(ServerNotification::Cancelled(params))
if params.request_id == RequestId::Number(73)
));
}
#[test]
fn final_subscription_event_sender_can_reenter_termination() {
let (done_sender, done_receiver) = std::sync::mpsc::channel();
let worker = std::thread::spawn(move || {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let registry_for_sender = Arc::clone(®istry);
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let callback_count = Arc::new(AtomicUsize::new(0));
let callback_count_for_sender = Arc::clone(&callback_count);
let terminated = Arc::new(AtomicUsize::new(0));
let terminated_for_sender = Arc::clone(&terminated);
let cancellation = McpRequestCancellation::new();
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
if callback_count_for_sender.fetch_add(1, Ordering::AcqRel) == 1 {
terminated_for_sender.store(registry_for_sender.terminate(), Ordering::Release);
}
});
let lease = registry
.open(
RequestId::Number(74),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
None,
cancellation.clone(),
None,
sender,
)
.expect("event reentry stream should admit");
let published = registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("event delivery with reentrant termination should remain valid");
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
done_sender
.send((
lease.has_graceful_completion(),
cancellation.is_cancel_requested(),
callback_count.load(Ordering::Acquire),
terminated.load(Ordering::Acquire),
published,
sent,
))
.expect("reentrant event result receiver should remain live");
});
let (graceful, cancelled, callback_count, terminated, published, sent) = done_receiver
.recv_timeout(Duration::from_secs(2))
.expect("event callback reentry must not deadlock");
worker
.join()
.expect("reentrant event worker should not panic");
assert!(graceful);
assert!(cancelled);
assert_eq!(callback_count, 3);
assert_eq!(terminated, 1);
assert_eq!(published, 1);
assert!(matches!(
ServerNotification::decode(&sent[0]),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
));
assert!(matches!(
ServerNotification::decode(&sent[1]),
Ok(ServerNotification::ToolsListChanged(_))
));
assert!(matches!(
ServerNotification::decode(&sent[2]),
Ok(ServerNotification::Cancelled(params))
if params.request_id == RequestId::Number(74)
));
}
struct HeldFinalSubscription {
handle: SubscriptionListenHandle,
frames: Arc<Mutex<Vec<JsonRpcRequest>>>,
release: Arc<(Mutex<bool>, Condvar)>,
entered: std::sync::mpsc::Receiver<()>,
max_active: Arc<AtomicUsize>,
}
impl HeldFinalSubscription {
fn open(server: &Arc<Server>, id: i64) -> Self {
let frames = Arc::new(Mutex::new(Vec::new()));
let release = Arc::new((Mutex::new(false), Condvar::new()));
let active = Arc::new(AtomicUsize::new(0));
let max_active = Arc::new(AtomicUsize::new(0));
let (entered_sender, entered) = std::sync::mpsc::channel();
let sender_frames = Arc::clone(&frames);
let sender_release = Arc::clone(&release);
let sender_active = Arc::clone(&active);
let sender_max_active = Arc::clone(&max_active);
let held = AtomicBool::new(false);
let sender: NotificationSender = Arc::new(move |notification| {
let acknowledgement =
final_subscription_acknowledgement_notification(¬ification);
sender_frames.lock().unwrap().push(notification);
if acknowledgement {
return;
}
let active = sender_active.fetch_add(1, Ordering::AcqRel) + 1;
sender_max_active.fetch_max(active, Ordering::AcqRel);
if !held.swap(true, Ordering::AcqRel) {
entered_sender
.send(())
.expect("first callback observer remains live");
let (lock, ready) = &*sender_release;
let mut released = lock.lock().unwrap();
while !*released {
released = ready.wait(released).unwrap();
}
}
sender_active.fetch_sub(1, Ordering::AcqRel);
});
let handle = server
.open_subscription_listen(
RequestId::Number(id),
SubscriptionFilter {
tools_list_changed: Some(true),
resources_list_changed: Some(true),
..SubscriptionFilter::default()
},
sender,
)
.expect("public listener must acknowledge before publication");
Self {
handle,
frames,
release,
entered,
max_active,
}
}
fn release(&self) {
let (lock, ready) = &*self.release;
*lock.lock().unwrap() = true;
ready.notify_all();
}
}
impl Drop for HeldFinalSubscription {
fn drop(&mut self) {
self.release();
}
}
#[test]
fn final_subscription_active_publications_are_serial_and_fifo() {
let server = Arc::new(Server::new("active-subscription-fifo", "1.0.0").build());
let held = HeldFinalSubscription::open(&server, 730);
let first_server = Arc::clone(&server);
let first = thread::spawn(move || {
first_server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
});
held.entered
.recv_timeout(Duration::from_secs(2))
.expect("first event must enter");
let second_server = Arc::clone(&server);
let (done_sender, done) = std::sync::mpsc::channel();
let second = thread::spawn(move || {
let result = second_server
.publish_subscription_notification(ServerNotification::ResourcesListChanged(None));
done_sender
.send(result)
.expect("second publisher observer remains live");
});
let admitted = done.recv_timeout(Duration::from_secs(2));
let frames_before_release = held.frames.lock().unwrap().len();
held.release();
assert_eq!(
first
.join()
.expect("first publisher must not panic")
.expect("first event publishes"),
1
);
second.join().expect("second publisher must not panic");
assert_eq!(
admitted
.expect("concurrent publisher must not wait for callback")
.unwrap(),
1
);
assert_eq!(
frames_before_release, 2,
"only acknowledgement and first callback may enter"
);
assert_eq!(held.max_active.load(Ordering::Acquire), 1);
let frames = held.frames.lock().unwrap();
assert_eq!(
frames
.iter()
.map(|frame| frame.method.as_str())
.collect::<Vec<_>>(),
vec![
"notifications/subscriptions/acknowledged",
"notifications/tools/list_changed",
"notifications/resources/list_changed",
]
);
for frame in frames.iter() {
assert_eq!(
frame.params.as_ref().unwrap()["_meta"][FINAL_SUBSCRIPTION_ID_META_KEY],
serde_json::json!(730)
);
}
let queue = held.handle.lease().election.opening_events.lock().unwrap();
assert_eq!((queue.retained_events, queue.retained_bytes), (0, 0));
}
#[test]
fn final_subscription_active_overflow_retires_only_slow_listener() {
let server = Arc::new(Server::new("active-subscription-overflow", "1.0.0").build());
let held = HeldFinalSubscription::open(&server, 731);
let fast_frames = Arc::new(AtomicUsize::new(0));
let sender_frames = Arc::clone(&fast_frames);
let _fast = server
.open_subscription_listen(
RequestId::Number(732),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
Arc::new(move |_| {
sender_frames.fetch_add(1, Ordering::AcqRel);
}),
)
.expect("unrelated listener must acknowledge");
let first_server = Arc::clone(&server);
let first = thread::spawn(move || {
first_server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
});
held.entered
.recv_timeout(Duration::from_secs(2))
.expect("slow event must enter");
for _ in 1..MAX_FINAL_SUBSCRIPTION_QUEUED_EVENTS {
assert_eq!(
server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
.unwrap(),
2
);
}
{
let queue = held.handle.lease().election.opening_events.lock().unwrap();
assert_eq!(queue.retained_events, MAX_FINAL_SUBSCRIPTION_QUEUED_EVENTS);
assert!(queue.retained_bytes > 0);
}
assert_eq!(
server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
.unwrap(),
1,
"one extra event must retire only the listener at its bound"
);
{
let queue = held.handle.lease().election.opening_events.lock().unwrap();
assert_eq!(
(
queue.retained_events,
queue.retained_bytes,
queue.events.len()
),
(0, 0, 0)
);
}
assert!(
!server
.final_subscriptions
.inner
.lock()
.unwrap()
.entries
.contains_key(&held.handle.lease().key),
"capacity must be released while the failed callback and public handle remain live"
);
assert_eq!(
server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
.unwrap(),
1
);
held.release();
assert_eq!(
first
.join()
.expect("first publisher must not panic")
.unwrap(),
1
);
assert_eq!(
held.frames.lock().unwrap().len(),
2,
"queued events must never run after overflow"
);
assert_eq!(
fast_frames.load(Ordering::Acquire),
MAX_FINAL_SUBSCRIPTION_QUEUED_EVENTS + 3,
"the sibling receives acknowledgement and every publication exactly once"
);
assert_eq!(held.max_active.load(Ordering::Acquire), 1);
}
#[test]
fn final_subscription_active_byte_limit_rejects_before_retaining() {
for excess in [0, 1] {
let server = Arc::new(Server::new("active-subscription-bytes", "1.0.0").build());
let held = HeldFinalSubscription::open(&server, 733);
let first_server = Arc::clone(&server);
let first = thread::spawn(move || {
first_server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
});
held.entered
.recv_timeout(Duration::from_secs(2))
.expect("first event must enter");
let first_bytes = held
.handle
.lease()
.election
.opening_events
.lock()
.unwrap()
.retained_bytes;
let event = |size| {
ServerNotification::ToolsListChanged(Some(
fastmcp_protocol::FinalEmptyNotificationParams {
meta: None,
additional: BTreeMap::from([(
"payload".to_owned(),
serde_json::json!("x".repeat(size)),
)]),
},
))
};
let empty = tag_subscription_notification(&event(0), &RequestId::Number(733))
.unwrap()
.encode()
.unwrap();
let overhead = final_subscription_event_bytes(&empty).unwrap();
let payload = MAX_FINAL_SUBSCRIPTION_QUEUED_BYTES - first_bytes - overhead + excess;
let admitted = server
.publish_subscription_notification(event(payload))
.unwrap();
{
let queue = held.handle.lease().election.opening_events.lock().unwrap();
if excess == 0 {
assert_eq!(
(admitted, queue.retained_events, queue.retained_bytes),
(1, 2, MAX_FINAL_SUBSCRIPTION_QUEUED_BYTES)
);
} else {
assert_eq!(
(admitted, queue.retained_events, queue.retained_bytes),
(0, 0, 0)
);
assert!(
!server
.final_subscriptions
.inner
.lock()
.unwrap()
.entries
.contains_key(&held.handle.lease().key)
);
}
}
held.release();
assert_eq!(
first
.join()
.expect("first publisher must not panic")
.unwrap(),
usize::from(excess == 0)
);
assert_eq!(
held.frames.lock().unwrap().len(),
if excess == 0 { 3 } else { 2 }
);
let queue = held.handle.lease().election.opening_events.lock().unwrap();
assert_eq!((queue.retained_events, queue.retained_bytes), (0, 0));
}
// An individually oversized payload is rejected before recipient
// copies or queue mutation, leaving even a matching listener usable.
let server = Server::new("subscription-prefanout-bound", "1.0.0").build();
let frames = Arc::new(AtomicUsize::new(0));
let sender_frames = Arc::clone(&frames);
let handle = server
.open_subscription_listen(
RequestId::Number(735),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
Arc::new(move |_| {
sender_frames.fetch_add(1, Ordering::AcqRel);
}),
)
.unwrap();
let oversized = ServerNotification::ToolsListChanged(Some(
fastmcp_protocol::FinalEmptyNotificationParams {
meta: None,
additional: BTreeMap::from([(
"payload".to_owned(),
serde_json::json!("x".repeat(MAX_FINAL_SUBSCRIPTION_QUEUED_BYTES + 1)),
)]),
},
));
let error = server
.publish_subscription_notification(oversized)
.expect_err("oversized source data must be rejected before fanout");
assert_eq!(
error.code,
McpErrorCode::Custom(RESOURCE_EXHAUSTED_ERROR_CODE)
);
assert_eq!(frames.load(Ordering::Acquire), 1);
assert!(
server
.final_subscriptions
.inner
.lock()
.unwrap()
.entries
.contains_key(&handle.lease().key)
);
let queue = handle.lease().election.opening_events.lock().unwrap();
assert_eq!((queue.retained_events, queue.retained_bytes), (0, 0));
drop(queue);
assert_eq!(
server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
.unwrap(),
1
);
assert_eq!(frames.load(Ordering::Acquire), 2);
}
#[test]
fn final_subscription_active_reentrant_publish_and_teardown_preserve_order() {
for (terminate, panic_after_enqueue) in [(false, false), (true, false), (false, true)] {
let server = Arc::new(Server::new("active-subscription-reentrant", "1.0.0").build());
let sender_server = Arc::downgrade(&server);
let frames = Arc::new(Mutex::new(Vec::new()));
let sender_frames = Arc::clone(&frames);
let active = AtomicUsize::new(0);
let max_active = Arc::new(AtomicUsize::new(0));
let sender_max = Arc::clone(&max_active);
let queued = Arc::new(AtomicUsize::new(usize::MAX));
let sender_queued = Arc::clone(&queued);
let sender: NotificationSender = Arc::new(move |notification| {
let active_count = active.fetch_add(1, Ordering::AcqRel) + 1;
sender_max.fetch_max(active_count, Ordering::AcqRel);
let publish = notification.method == "notifications/tools/list_changed";
sender_frames.lock().unwrap().push(notification);
if publish {
let server = sender_server
.upgrade()
.expect("public publisher remains live");
sender_queued.store(
server
.publish_subscription_notification(
ServerNotification::ResourcesListChanged(None),
)
.unwrap(),
Ordering::Release,
);
if panic_after_enqueue {
panic!("planted sender panic after reentrant queue admission");
}
if terminate {
assert_eq!(server.terminate_subscription_streams(), 1);
}
}
active.fetch_sub(1, Ordering::AcqRel);
});
let handle = server
.open_subscription_listen(
RequestId::Number(734),
SubscriptionFilter {
tools_list_changed: Some(true),
resources_list_changed: Some(true),
..SubscriptionFilter::default()
},
sender,
)
.unwrap();
assert_eq!(
server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
.unwrap(),
usize::from(!panic_after_enqueue)
);
assert_eq!(queued.load(Ordering::Acquire), 1);
assert_eq!(
max_active.load(Ordering::Acquire),
1,
"reentrant publication must not reenter the callback"
);
let frames = frames.lock().unwrap();
let mut expected = vec![
"notifications/subscriptions/acknowledged",
"notifications/tools/list_changed",
];
if !panic_after_enqueue {
expected.push(if terminate {
"notifications/cancelled"
} else {
"notifications/resources/list_changed"
});
}
assert_eq!(
frames
.iter()
.map(|frame| frame.method.as_str())
.collect::<Vec<_>>(),
expected
);
assert_eq!(handle.lease().has_graceful_completion(), terminate);
if panic_after_enqueue {
assert!(
server
.final_subscriptions
.inner
.lock()
.unwrap()
.entries
.is_empty()
);
}
let queue = handle.lease().election.opening_events.lock().unwrap();
assert_eq!((queue.retained_events, queue.retained_bytes), (0, 0));
}
}
#[test]
fn final_subscription_concurrent_publications_are_not_silently_dropped() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let sent = Arc::new(AtomicUsize::new(0));
let sent_for_sender = Arc::clone(&sent);
let (entered_sender, entered_receiver) = std::sync::mpsc::channel();
let release = Arc::new((Mutex::new(false), Condvar::new()));
let release_for_sender = Arc::clone(&release);
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender.fetch_add(1, Ordering::AcqRel);
if matches!(
ServerNotification::decode(¬ification),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
) {
return;
}
entered_sender
.send(())
.expect("concurrent publication observer should remain live");
let (lock, ready) = &*release_for_sender;
let mut released = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while !*released {
released = ready
.wait(released)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
});
let _lease = registry
.open(
RequestId::Number(75),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
None,
McpRequestCancellation::new(),
None,
sender,
)
.expect("concurrent publication stream should admit");
let first_registry = Arc::clone(®istry);
let first = thread::spawn(move || {
first_registry.publish(ServerNotification::ToolsListChanged(None))
});
entered_receiver
.recv_timeout(Duration::from_secs(2))
.expect("first publication callback should start");
let second_registry = Arc::clone(®istry);
let (admitted_sender, admitted_receiver) = std::sync::mpsc::channel();
let second = thread::spawn(move || {
let result = second_registry.publish(ServerNotification::ToolsListChanged(None));
admitted_sender
.send(())
.expect("second admission observer remains live");
result
});
let second_admitted = admitted_receiver
.recv_timeout(Duration::from_secs(2))
.is_ok();
let second_entered = entered_receiver.try_recv().is_ok();
let (lock, ready) = &*release;
*lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = true;
ready.notify_all();
let first_result = first
.join()
.expect("first concurrent publisher should not panic")
.expect("first concurrent publication should remain valid");
let second_result = second
.join()
.expect("second concurrent publisher should not panic")
.expect("second concurrent publication should remain valid");
assert!(
second_admitted && !second_entered,
"the second event must be admitted without a concurrent callback or a dropped event"
);
assert_eq!(first_result, 1);
assert_eq!(second_result, 1);
assert_eq!(
sent.load(Ordering::Acquire),
3,
"one acknowledgement and both accepted events must reach the sender"
);
}
#[test]
fn final_subscription_terminal_result_is_complete_and_correlated() {
use fastmcp_protocol::CoreRequest;
let server = Server::new("final-subscriptions-test", "1.0.0").build();
let subscription_id = RequestId::String("subscription-terminal".to_owned());
let terminal = server
.final_subscription_complete_result(&subscription_id)
.expect("terminal result should encode through the final result algebra");
assert_eq!(
terminal.get("resultType"),
Some(&serde_json::json!("complete"))
);
assert_eq!(
terminal
.get("_meta")
.and_then(|metadata| metadata.get(FINAL_SUBSCRIPTION_ID_META_KEY)),
Some(&serde_json::to_value(&subscription_id).expect("id must serialize"))
);
let request_metadata = OpenMetadata::try_from_entries([
(
"io.modelcontextprotocol/protocolVersion".to_owned(),
serde_json::json!(MODERN_PROTOCOL_VERSION),
),
(
FINAL_CLIENT_CAPABILITIES_META_KEY.to_owned(),
serde_json::json!({}),
),
])
.expect("final request metadata must be valid");
let params = serde_json::to_value(FinalSubscriptionsListenParams {
meta: request_metadata,
notifications: SubscriptionFilter::default(),
})
.expect("final listen parameters must serialize");
let request =
CoreRequest::decode(ProtocolEra::Modern2026, SUBSCRIPTIONS_LISTEN, Some(¶ms))
.expect("final listen request must select the final result decoder");
let response = JsonRpcResponse::success(subscription_id.clone(), terminal);
let CoreResult::Final(FinalCoreResult::SubscriptionsListen {
subscription_id: decoded_id,
..
}) = request
.decode_response(&response)
.expect("terminal complete result must round-trip with its response id")
else {
panic!("subscriptions/listen must decode only its terminal final result");
};
assert_eq!(decoded_id, subscription_id);
}
#[test]
fn public_stdio_listen_teardown_emits_cancelled_before_correlated_completion() {
let server = Arc::new(Server::new("final-listen-dispatch-test", "1.0.0").build());
let wire = Arc::new(Mutex::new(Vec::<JsonRpcMessage>::new()));
let wire_for_sender = Arc::clone(&wire);
let notification_sender: NotificationSender = Arc::new(move |notification| {
wire_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(JsonRpcMessage::Request(notification));
});
let subscription_id = RequestId::Number(73);
let metadata = OpenMetadata::try_from_entries([
(
"io.modelcontextprotocol/protocolVersion".to_owned(),
serde_json::json!(MODERN_PROTOCOL_VERSION),
),
(
FINAL_CLIENT_CAPABILITIES_META_KEY.to_owned(),
serde_json::json!({}),
),
])
.expect("final request metadata must be valid");
let params = serde_json::to_value(FinalSubscriptionsListenParams {
meta: metadata,
notifications: SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
})
.expect("final listen parameters must serialize");
let request =
JsonRpcRequest::new(SUBSCRIPTIONS_LISTEN, Some(params), subscription_id.clone());
let inbound = InboundRequestContext::new(
Cx::for_testing(),
request_id_to_u64(Some(&subscription_id)),
InboundRequestTransport::Stdio,
);
let terminator = {
let server = Arc::clone(&server);
let wire = Arc::clone(&wire);
std::thread::spawn(move || {
for _ in 0..100 {
if !wire
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty()
{
return server.terminate_subscription_streams() == 1;
}
std::thread::sleep(Duration::from_millis(1));
}
false
})
};
let response = block_on(Arc::clone(&server).dispatch_with_protocol_policy_owned(
ProtocolPolicy::ModernOnly,
&inbound,
request,
None,
None,
None,
None,
McpRequestCancellation::new(),
None,
notification_sender,
));
assert!(
terminator
.join()
.expect("subscription terminator must not panic"),
"the listener must acknowledge before graceful termination"
);
let acknowledgement = wire
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.first()
.cloned()
.expect("listener must emit its acknowledgement before terminating");
let JsonRpcMessage::Request(acknowledgement) = acknowledgement else {
panic!("listener acknowledgement must be a JSON-RPC notification");
};
assert!(matches!(
ServerNotification::decode(&acknowledgement),
Ok(ServerNotification::SubscriptionsAcknowledged(_))
));
let response = response.expect("server-owned stdio teardown must complete the listen");
assert_eq!(response.id, Some(subscription_id.clone()));
assert!(
final_subscription_completion_response(&response),
"server-owned teardown must complete the exact listen request; response={response:?}"
);
wire.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(JsonRpcMessage::Response(response.clone()));
let wire = wire
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let terminal_position = wire
.iter()
.position(|message| {
let JsonRpcMessage::Request(notification) = message else {
return false;
};
matches!(
ServerNotification::decode(notification),
Ok(ServerNotification::Cancelled(_))
)
})
.expect("server teardown must emit one cancellation control after acknowledgement");
let JsonRpcMessage::Request(terminal) = &wire[terminal_position] else {
unreachable!("the terminal position above only selects requests");
};
let ServerNotification::Cancelled(terminal) = ServerNotification::decode(terminal)
.expect("terminal control must use the final server notification union")
else {
panic!("server teardown must emit notifications/cancelled");
};
assert_eq!(terminal.request_id, subscription_id);
assert_eq!(
terminal
.meta
.as_ref()
.and_then(|meta| meta.get(FINAL_SUBSCRIPTION_ID_META_KEY)),
Some(&serde_json::json!(73)),
"terminal metadata must correlate to exactly this listen request",
);
let completion_position = wire
.iter()
.position(|message| {
matches!(
message,
JsonRpcMessage::Response(response)
if response.id == Some(subscription_id.clone())
&& final_subscription_completion_response(response)
)
})
.expect("server teardown must emit the correlated completion result");
assert_eq!(
wire.len(),
3,
"the acknowledgement, correlated cancellation control, and completion are exact",
);
assert!(
terminal_position < completion_position,
"stdio must emit notifications/cancelled before the correlated complete result"
);
}
#[test]
fn http_sse_accept_qvalue_admits_rfc_forms_and_rejects_four_fraction_digits() {
let accepts = |value| {
http_request_accepts_sse(
&HttpRequest::new(HttpMethod::Post, "/mcp").with_header("accept", value),
)
};
assert!(accepts(" */* ; Q=1. "));
assert!(!accepts(" */* ; Q=0. "));
assert!(!accepts("*/*; Q = 1."));
assert!(!accepts("*/*; unrelated=value; Q=1."));
assert!(!accepts("text/event-stream; q=1.0000"));
assert!(!accepts("text/event-stream; q=0.0000"));
}
#[test]
fn http_sse_accept_exclusions_override_wildcards_before_stream_allocation() {
for value in [
"text/event-stream;q=0, */*;q=1",
"*/*;q=1, text/event-stream;q=0",
"text/*;q=0, */*;q=1",
"text/event-stream, text/event-stream;q=0, application/json",
"application/json;profile=\"x, text/event-stream\"",
] {
assert!(
!http_request_accepts_sse(
&HttpRequest::new(HttpMethod::Post, "/mcp").with_header("accept", value),
),
"a required SSE body must honor the full admitted preference: {value}",
);
}
assert!(http_request_accepts_sse(
&HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("accept", "text/event-stream;q=0.1, */*;q=1"),
));
assert!(!http_request_accepts_sse(&HttpRequest::new(
HttpMethod::Post,
"/mcp",
)));
}
#[test]
fn public_http_modern_disabled_components_are_isolated_between_sessions() {
let cx = Cx::for_testing();
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = Server::new("modern-http-session-isolation", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.tool(HttpSessionDisablingTool {
calls: Arc::clone(&calls),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut first_client = endpoint
.open_session(&cx)
.expect("first modern HTTP session must open");
let mut second_client = endpoint
.open_session(&cx)
.expect("second modern HTTP session must open");
let request = modern_http_json_tool_request("http_session_disabling_tool", 901);
for session in [&mut first_client, &mut second_client] {
let response = block_on(session.handle_async(&cx, request.clone()))
.expect("each independently admitted modern session must dispatch its first call");
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("ordinary modern tool calls must return JSON responses");
};
let response: JsonRpcResponse = serde_json::from_slice(&response.body)
.expect("modern tool response must remain JSON-RPC");
assert_eq!(response.id, Some(901_i64.into()));
assert!(response.error.is_none());
}
assert_eq!(
calls.load(Ordering::Acquire),
2,
"disabling the tool for the first client must not prevent the second client entering it",
);
}
#[test]
fn public_http_modern_disabled_components_reject_only_the_same_session() {
let cx = Cx::for_testing();
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = Server::new("modern-http-session-disablement", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.tool(HttpSessionDisablingTool {
calls: Arc::clone(&calls),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut first_client = endpoint
.open_session(&cx)
.expect("first modern HTTP session must open");
let mut second_client = endpoint
.open_session(&cx)
.expect("second modern HTTP session must open");
let first = block_on(first_client.handle_async(
&cx,
modern_http_json_tool_request("http_session_disabling_tool", 903),
))
.expect("first session must dispatch the disabling call");
assert!(matches!(
first,
ServerHttpEndpointResponse::Immediate(response)
if serde_json::from_slice::<JsonRpcResponse>(&response.body)
.is_ok_and(|response| response.error.is_none())
));
let comparison = modern_http_json_tool_request("http_session_disabling_tool", 904);
let rejected = block_on(first_client.handle_async(&cx, comparison.clone()))
.expect("same-session disablement must be represented as JSON-RPC");
assert!(matches!(
rejected,
ServerHttpEndpointResponse::Immediate(response)
if serde_json::from_slice::<JsonRpcResponse>(&response.body).is_ok_and(|response| {
response.id == Some(904_i64.into())
&& response.error.is_some_and(|error| error.code.as_i32() == Some(-32601))
})
));
let independent = block_on(second_client.handle_async(&cx, comparison))
.expect("changing only the client session must restore its independent state");
assert!(matches!(
independent,
ServerHttpEndpointResponse::Immediate(response)
if serde_json::from_slice::<JsonRpcResponse>(&response.body)
.is_ok_and(|response| response.id == Some(904_i64.into()) && response.error.is_none())
));
assert_eq!(calls.load(Ordering::Acquire), 2);
}
#[test]
fn rh5_native_http_rejects_server_notification_before_auth_and_session_admission() {
let cx = Cx::for_testing();
let provider_calls = Arc::new(AtomicUsize::new(0));
let handler_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let saw_credential = Arc::new(AtomicBool::new(false));
let endpoint = Server::new("native-http-wrong-direction-notification", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(OneShotNativeAuthProvider {
calls: Arc::clone(&provider_calls),
})
.middleware(ModernHttpAuthMiddleware {
calls: Arc::clone(&middleware_calls),
saw_credential: Arc::clone(&saw_credential),
})
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&handler_calls),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let server_notification = JsonRpcRequest::notification(
"notifications/progress",
Some(serde_json::json!({"progressToken": "server-only", "progress": 1})),
);
let rejected = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "notifications/progress")
.with_body(
serde_json::to_vec(&server_notification)
.expect("wrong-direction notification must encode"),
),
),
)
.expect("native HTTP must reject the wrong-direction notification");
assert!(matches!(
rejected,
ServerHttpEndpointResponse::Immediate(HttpResponse {
status: HttpStatus::BAD_REQUEST,
..
})
));
assert_eq!(provider_calls.load(Ordering::Acquire), 0);
assert_eq!(middleware_calls.load(Ordering::Acquire), 0);
assert_eq!(handler_calls.load(Ordering::Acquire), 0);
assert!(!saw_credential.load(Ordering::Acquire));
assert_eq!(session.selected_era, None);
assert!(
endpoint
.server
.active_requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.is_empty(),
"rejected native HTTP notification must not allocate a request session"
);
let accepted = block_on(
session.handle_async(
&cx,
modern_http_json_tool_request("modern_http_auth_counter", 947)
.with_header("authorization", "Bearer alpha"),
),
)
.expect("the same HTTP session must remain usable after rejection");
assert!(matches!(
accepted,
ServerHttpEndpointResponse::Immediate(response)
if serde_json::from_slice::<JsonRpcResponse>(&response.body).is_ok_and(|response| {
response.id == Some(947_i64.into()) && response.error.is_none()
})
));
assert_eq!(provider_calls.load(Ordering::Acquire), 1);
assert_eq!(middleware_calls.load(Ordering::Acquire), 1);
assert_eq!(handler_calls.load(Ordering::Acquire), 1);
}
#[test]
fn public_http_modern_authorization_authenticates_before_middleware_and_fences_principals() {
let cx = Cx::for_testing();
let handler_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let saw_credential = Arc::new(AtomicBool::new(false));
let endpoint = Server::new("modern-http-authorization", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(ModernHttpAuthProvider)
.middleware(ModernHttpAuthMiddleware {
calls: Arc::clone(&middleware_calls),
saw_credential: Arc::clone(&saw_credential),
})
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&handler_calls),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let allowed = block_on(
session.handle_async(
&cx,
modern_http_json_tool_request("modern_http_auth_counter", 931)
.with_header("authorization", "Bearer alpha"),
),
)
.expect("allowed bearer must dispatch");
let ServerHttpEndpointResponse::Immediate(allowed) = allowed else {
panic!("ordinary authenticated modern HTTP must return JSON");
};
let allowed: JsonRpcResponse =
serde_json::from_slice(&allowed.body).expect("allowed response must be JSON-RPC");
assert_eq!(allowed.id, Some(931_i64.into()));
assert!(allowed.error.is_none());
assert_eq!(handler_calls.load(Ordering::Acquire), 1);
assert_eq!(middleware_calls.load(Ordering::Acquire), 1);
assert!(!saw_credential.load(Ordering::Acquire));
let rejected = block_on(
session.handle_async(
&cx,
modern_http_json_tool_request("modern_http_auth_counter", 932)
.with_header("authorization", "Bearer beta"),
),
)
.expect("a principal mismatch must be represented as JSON-RPC");
let ServerHttpEndpointResponse::Immediate(rejected) = rejected else {
panic!("principal mismatch must not admit an SSE body");
};
let rejected_wire = String::from_utf8_lossy(&rejected.body);
let rejected: JsonRpcResponse =
serde_json::from_slice(&rejected.body).expect("rejection must remain JSON-RPC");
assert_eq!(rejected.id, Some(932_i64.into()));
assert_eq!(
rejected.error.map(|error| error.code),
Some(i32::from(McpErrorCode::ResourceForbidden).into())
);
assert!(rejected.result.is_none());
assert_eq!(
handler_calls.load(Ordering::Acquire),
1,
"changing only alpha to beta must not invoke the handler",
);
assert_eq!(
middleware_calls.load(Ordering::Acquire),
1,
"changing only alpha to beta must reject before middleware",
);
assert!(!saw_credential.load(Ordering::Acquire));
assert!(!rejected_wire.contains("alpha") && !rejected_wire.contains("beta"));
}
#[test]
fn public_http_modern_sse_authorization_fences_owned_dispatch_before_mutation() {
let cx = Cx::for_testing();
let handler_calls = Arc::new(AtomicUsize::new(0));
let middleware_calls = Arc::new(AtomicUsize::new(0));
let saw_credential = Arc::new(AtomicBool::new(false));
let endpoint = Server::new("modern-http-sse-authorization", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(ModernHttpAuthProvider)
.middleware(ModernHttpAuthMiddleware {
calls: Arc::clone(&middleware_calls),
saw_credential: Arc::clone(&saw_credential),
})
.tool(ModernHttpAuthCounterTool {
calls: Arc::clone(&handler_calls),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let request = |id, bearer| {
modern_http_json_tool_request("modern_http_auth_counter", id)
.with_header("accept", "text/event-stream")
.with_header("authorization", format!("Bearer {bearer}"))
};
let ServerHttpEndpointResponse::ModernSse(allowed) =
block_on(session.handle_async(&cx, request(933, "alpha")))
.expect("allowed bearer must admit an owned SSE dispatch")
else {
panic!("SSE request must retain its owned response body");
};
assert!(matches!(
Codec::new()
.decode_complete_message(
allowed
.recv_event(&cx)
.expect("allowed SSE body must emit its terminal response")
.data
.as_bytes(),
)
.expect("allowed SSE response must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(933_i64.into()) && response.error.is_none()
));
assert_eq!(handler_calls.load(Ordering::Acquire), 1);
assert_eq!(middleware_calls.load(Ordering::Acquire), 1);
let ServerHttpEndpointResponse::ModernSse(rejected) =
block_on(session.handle_async(&cx, request(934, "beta")))
.expect("principal mismatch must retain the protocol-selected SSE body")
else {
panic!("SSE admission must preserve its owned response path");
};
let rejected = rejected
.recv_event(&cx)
.expect("rejected owned dispatch must emit one terminal response");
assert!(!rejected.data.contains("alpha") && !rejected.data.contains("beta"));
assert!(matches!(
Codec::new()
.decode_complete_message(rejected.data.as_bytes())
.expect("rejected SSE response must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(934_i64.into())
&& response.error.as_ref().is_some_and(|error| {
error.code == i32::from(McpErrorCode::ResourceForbidden).into()
})
));
assert_eq!(handler_calls.load(Ordering::Acquire), 1);
assert_eq!(middleware_calls.load(Ordering::Acquire), 1);
assert!(!saw_credential.load(Ordering::Acquire));
}
#[test]
fn public_final_elicitation_projects_admitted_modes_without_mutating_on_rejection() {
let calls = Arc::new(AtomicUsize::new(0));
let server = Server::new("public-final-elicitation", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern-only policy is available")
.tool(PublicFinalElicitationTool {
name: "public-final-form-elicitation",
mode: PublicFinalElicitationMode::Form,
calls: Arc::clone(&calls),
})
.tool(PublicFinalElicitationTool {
name: "public-final-url-elicitation",
mode: PublicFinalElicitationMode::Url,
calls: Arc::clone(&calls),
})
.build();
let cx = Cx::for_testing();
let connection = ModernConnection::new();
let admitted_params = serde_json::json!({
"name": "public-final-form-elicitation",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {"elicitation": {"form": {}}},
},
});
let admitted = block_on(server.dispatch_stateless(
&InboundRequestContext::with_modern_connection(
cx.clone(),
950,
InboundRequestTransport::Memory,
&connection,
),
&JsonRpcRequest::new("tools/call", Some(admitted_params.clone()), 950_i64),
))
.expect("public final form elicitation must respond");
assert!(admitted.error.is_none());
assert_eq!(
admitted
.result
.as_ref()
.and_then(|result| result.get("resultType")),
Some(&serde_json::json!("input_required"))
);
assert_eq!(
admitted
.result
.as_ref()
.and_then(|result| result.pointer("/inputRequests/approval/params/mode")),
Some(&serde_json::json!("form"))
);
assert_eq!(calls.load(Ordering::Acquire), 1);
let active_before_rejection = server.router.test_active_mrtr_exchange_count();
assert_eq!(active_before_rejection, 1);
let mut capability_removed = admitted_params.clone();
let capabilities = capability_removed
.pointer_mut("/_meta/io.modelcontextprotocol~1clientCapabilities")
.and_then(serde_json::Value::as_object_mut)
.expect("admitted fixture has a client capability object");
let admitted_capability_count = capabilities.len();
assert!(
capabilities.remove("elicitation").is_some(),
"the planted negative removes only the elicitation capability"
);
assert_eq!(capabilities.len() + 1, admitted_capability_count);
let capability_rejected = block_on(server.dispatch_stateless(
&InboundRequestContext::with_modern_connection(
cx.clone(),
951,
InboundRequestTransport::Memory,
&connection,
),
&JsonRpcRequest::new("tools/call", Some(capability_removed), 951_i64),
))
.expect("the capability-negative final request must respond");
assert!(capability_rejected.error.is_none());
let capability_rejected = capability_rejected
.result
.expect("a final tool refusal is a complete error result");
assert_eq!(capability_rejected["resultType"], "complete");
assert_eq!(capability_rejected["isError"], true);
assert_eq!(
server.router.test_active_mrtr_exchange_count(),
active_before_rejection,
"removing only client elicitation capability cannot mint or consume MRTR state"
);
assert_eq!(
calls.load(Ordering::Acquire),
2,
"capability rejection occurs inside the handler's elicitation constructor without minting MRTR state"
);
let mut url_mismatch = admitted_params;
url_mismatch["name"] = serde_json::json!("public-final-url-elicitation");
let url_rejected = block_on(server.dispatch_stateless(
&InboundRequestContext::with_modern_connection(
cx,
952,
InboundRequestTransport::Memory,
&connection,
),
&JsonRpcRequest::new("tools/call", Some(url_mismatch), 952_i64),
))
.expect("the form-only URL negative must respond");
assert!(url_rejected.error.is_none());
let url_rejected = url_rejected
.result
.expect("a final tool refusal is a complete error result");
assert_eq!(url_rejected["resultType"], "complete");
assert_eq!(url_rejected["isError"], true);
assert_eq!(
server.router.test_active_mrtr_exchange_count(),
active_before_rejection,
"a form-only client cannot mint URL elicitation state"
);
assert_eq!(
calls.load(Ordering::Acquire),
3,
"mode rejection occurs inside the handler's elicitation constructor without minting MRTR state"
);
}
#[test]
fn public_final_sampling_projects_capability_and_preserves_tool_use_retry() {
let handler_calls = Arc::new(AtomicUsize::new(0));
let server = Server::new("public-final-sampling", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("modern-only policy is available")
.tool(PublicFinalSamplingTool {
calls: Arc::clone(&handler_calls),
})
.build();
let cx = Cx::for_testing();
let connection = ModernConnection::new();
let admitted_params = serde_json::json!({
"name": "public-final-sampling",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {"sampling": {}},
},
});
let initial = block_on(server.dispatch_stateless(
&InboundRequestContext::with_modern_connection(
cx.clone(),
960,
InboundRequestTransport::Memory,
&connection,
),
&JsonRpcRequest::new("tools/call", Some(admitted_params.clone()), 960_i64),
))
.expect("public final sampling request must respond");
assert!(initial.error.is_none());
assert_eq!(
initial
.result
.as_ref()
.and_then(|result| result.get("resultType")),
Some(&serde_json::json!("input_required"))
);
assert_eq!(
initial
.result
.as_ref()
.and_then(|result| result.pointer("/inputRequests/sample/method")),
Some(&serde_json::json!("sampling/createMessage"))
);
assert_eq!(
initial
.result
.as_ref()
.and_then(|result| result.pointer("/inputRequests/sample/params/toolChoice/mode")),
Some(&serde_json::json!("required"))
);
assert_eq!(
initial
.result
.as_ref()
.and_then(|result| result.pointer("/inputRequests/sample/params/tools/0/name")),
Some(&serde_json::json!("weather"))
);
assert_eq!(
initial.result.as_ref().and_then(|result| {
result.pointer("/inputRequests/sample/params/messages/0/content/type")
}),
Some(&serde_json::json!("tool_use"))
);
let request_state = initial
.result
.as_ref()
.and_then(|result| result.get("requestState"))
.and_then(serde_json::Value::as_str)
.expect("framework-issued final sampling state must be returned")
.to_owned();
let active_before_rejection = server.router.test_active_mrtr_exchange_count();
assert_eq!(active_before_rejection, 1);
assert_eq!(handler_calls.load(Ordering::Acquire), 1);
let mut capability_removed = admitted_params.clone();
let capabilities = capability_removed
.pointer_mut("/_meta/io.modelcontextprotocol~1clientCapabilities")
.and_then(serde_json::Value::as_object_mut)
.expect("admitted fixture has a client capability object");
let admitted_capability_count = capabilities.len();
assert!(
capabilities.remove("sampling").is_some(),
"the planted RH-5 negative removes only sampling capability"
);
assert_eq!(capabilities.len() + 1, admitted_capability_count);
let rejected = block_on(server.dispatch_stateless(
&InboundRequestContext::with_modern_connection(
cx.clone(),
961,
InboundRequestTransport::Memory,
&connection,
),
&JsonRpcRequest::new("tools/call", Some(capability_removed), 961_i64),
))
.expect("the capability-negative final sampling request must respond");
assert!(rejected.error.is_none());
let rejected = rejected
.result
.expect("a final tool refusal is a complete error result");
assert_eq!(rejected["resultType"], "complete");
assert_eq!(rejected["isError"], true);
assert_eq!(
server.router.test_active_mrtr_exchange_count(),
active_before_rejection,
"removing only sampling capability cannot mint or consume MRTR state"
);
assert_eq!(
handler_calls.load(Ordering::Acquire),
1,
"the removed capability must reject before this handler's post-admission mutation"
);
let mut retry_params = admitted_params;
retry_params["inputResponses"] = serde_json::json!({
"sample": {
"content": {
"type": "tool_use",
"id": "weather-response",
"name": "weather",
"input": {"city": "Cambridge"},
},
"role": "assistant",
"model": "test-model",
},
});
retry_params["requestState"] = serde_json::json!(request_state);
let resumed = block_on(server.dispatch_stateless(
&InboundRequestContext::with_modern_connection(
cx,
962,
InboundRequestTransport::Memory,
&connection,
),
&JsonRpcRequest::new("tools/call", Some(retry_params), 962_i64),
))
.expect("an admitted final sampling retry must respond");
assert!(resumed.error.is_none());
assert_eq!(
resumed
.result
.as_ref()
.and_then(|result| result.get("resultType")),
Some(&serde_json::json!("complete"))
);
assert_eq!(
handler_calls.load(Ordering::Acquire),
2,
"the valid final tool-use response reaches exactly one resumed handler invocation"
);
assert_eq!(
server.router.test_active_mrtr_exchange_count(),
0,
"only the admitted retry consumes the framework-owned request state"
);
}
#[test]
fn public_http_mrtr_cross_session_resume_is_bound_and_single_use() {
let cx = Cx::for_testing();
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = Server::new("modern-http-mrtr-stateless-resume", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(auth_00_mrtr_provider(true))
.tool(LiveHttpMrtrTool {
name: "live_http_mrtr",
calls: Arc::clone(&calls),
})
.tool(LiveHttpMrtrTool {
name: "other_live_http_mrtr",
calls: Arc::clone(&calls),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut issuing_client = endpoint
.open_session(&cx)
.expect("issuing modern HTTP session must open");
let mut other_client = endpoint
.open_session(&cx)
.expect("independent modern HTTP session must open");
let mut argument_client = endpoint
.open_session(&cx)
.expect("argument-negative modern HTTP session must open");
let mut principal_client = endpoint
.open_session(&cx)
.expect("principal-negative modern HTTP session must open");
let mut valid_client = endpoint
.open_session(&cx)
.expect("valid retry modern HTTP session must open");
let initial = block_on(
issuing_client.handle_async(
&cx,
modern_http_json_tool_request("live_http_mrtr", 906)
.with_header("authorization", "Bearer alpha"),
),
)
.expect("the issuing session must receive an input-required response");
let ServerHttpEndpointResponse::Immediate(initial) = initial else {
panic!("ordinary MRTR requests must return JSON responses");
};
let initial: JsonRpcResponse =
serde_json::from_slice(&initial.body).expect("initial MRTR response must be JSON-RPC");
let request_state = initial
.result
.as_ref()
.and_then(|result| result.get("requestState"))
.and_then(serde_json::Value::as_str)
.expect("initial MRTR response must contain framework-issued state")
.to_owned();
assert_eq!(calls.load(Ordering::Acquire), 1);
let roots = serde_json::to_value(
bidirectional::MrtrInputResponse::roots(fastmcp_protocol::ListRootsResult::empty())
.expect("MRTR roots response must construct"),
)
.expect("MRTR roots response must serialize");
let retry =
|state: &str, name: &str, arguments: serde_json::Value, id: i64, bearer: &str| {
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": name,
"arguments": arguments,
"inputResponses": {"roots": roots.clone()},
"requestState": state,
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
id,
);
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/call")
.with_header("mcp-name", name)
.with_header("authorization", format!("Bearer {bearer}"))
.with_body(serde_json::to_vec(&request).expect("MRTR retry must encode"))
};
let forged_state = format!("{request_state}-foreign");
let forged = block_on(issuing_client.handle_async(
&cx,
retry(
&forged_state,
"live_http_mrtr",
serde_json::json!({}),
907,
"alpha",
),
))
.expect("forged MRTR state must receive a JSON-RPC rejection");
assert!(matches!(
forged,
ServerHttpEndpointResponse::Immediate(response)
if serde_json::from_slice::<JsonRpcResponse>(&response.body).is_ok_and(|response| {
response.id == Some(907_i64.into()) && response.error.is_some()
})
));
assert_eq!(
calls.load(Ordering::Acquire),
1,
"changing only requestState must not consume or resume the issued continuation",
);
assert_eq!(endpoint.server.router.test_active_mrtr_exchange_count(), 1);
let target_mismatch = block_on(other_client.handle_async(
&cx,
retry(
&request_state,
"other_live_http_mrtr",
serde_json::json!({}),
908,
"alpha",
),
))
.expect("a target-mismatched MRTR retry must receive a JSON-RPC rejection");
assert!(matches!(
target_mismatch,
ServerHttpEndpointResponse::Immediate(response)
if serde_json::from_slice::<JsonRpcResponse>(&response.body).is_ok_and(|response| {
response.id == Some(908_i64.into()) && response.error.is_some()
})
));
assert_eq!(
calls.load(Ordering::Acquire),
1,
"changing only the registered tool target must not reenter either handler",
);
assert_eq!(
endpoint.server.router.test_active_mrtr_exchange_count(),
1,
"a target-binding rejection must leave the issued state available",
);
let argument_mismatch = block_on(argument_client.handle_async(
&cx,
retry(
&request_state,
"live_http_mrtr",
serde_json::json!({"city": "Cambridge"}),
909,
"alpha",
),
))
.expect("an argument-mismatched MRTR retry must receive a JSON-RPC rejection");
assert!(matches!(
argument_mismatch,
ServerHttpEndpointResponse::Immediate(response)
if serde_json::from_slice::<JsonRpcResponse>(&response.body).is_ok_and(|response| {
response.id == Some(909_i64.into()) && response.error.is_some()
})
));
assert_eq!(
calls.load(Ordering::Acquire),
1,
"changing only valid arguments must not reenter the handler",
);
assert_eq!(
endpoint.server.router.test_active_mrtr_exchange_count(),
1,
"an argument-binding rejection must leave the issued state available",
);
let principal_mismatch = block_on(principal_client.handle_async(
&cx,
retry(
&request_state,
"live_http_mrtr",
serde_json::json!({}),
910,
"beta",
),
))
.expect("a principal-mismatched MRTR retry must receive a JSON-RPC rejection");
assert!(matches!(
principal_mismatch,
ServerHttpEndpointResponse::Immediate(response)
if serde_json::from_slice::<JsonRpcResponse>(&response.body).is_ok_and(|response| {
response.id == Some(910_i64.into()) && response.error.is_some()
})
));
assert_eq!(
calls.load(Ordering::Acquire),
1,
"changing only the authenticated principal must not reenter the handler",
);
assert_eq!(
endpoint.server.router.test_active_mrtr_exchange_count(),
1,
"a principal-binding rejection must leave the issued state available",
);
let valid_retry = retry(
&request_state,
"live_http_mrtr",
serde_json::json!({}),
911,
"alpha",
);
let resumed = block_on(valid_client.handle_async(&cx, valid_retry.clone()))
.expect("a later stateless session with the bound request must resume");
assert!(matches!(
resumed,
ServerHttpEndpointResponse::Immediate(response)
if serde_json::from_slice::<JsonRpcResponse>(&response.body).is_ok_and(|response| {
response.id == Some(911_i64.into())
&& response.error.is_none()
&& response.result.as_ref().and_then(|result| result.get("resultType"))
== Some(&serde_json::json!("complete"))
})
));
assert_eq!(calls.load(Ordering::Acquire), 2);
let replay = block_on(issuing_client.handle_async(&cx, valid_retry))
.expect("a consumed MRTR state must receive a JSON-RPC rejection");
assert!(matches!(
replay,
ServerHttpEndpointResponse::Immediate(response)
if serde_json::from_slice::<JsonRpcResponse>(&response.body).is_ok_and(|response| {
response.id == Some(911_i64.into()) && response.error.is_some()
})
));
assert_eq!(
calls.load(Ordering::Acquire),
2,
"replaying consumed requestState must not reenter the handler",
);
assert_eq!(endpoint.server.router.test_active_mrtr_exchange_count(), 0);
}
#[test]
fn public_http_stateless_elicitation_retry_resumes_once_across_sessions() {
let cx = Cx::for_testing();
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = Server::new("modern-http-stateless-elicitation", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.auth_provider(auth_00_mrtr_provider(true))
.tool(PublicFinalElicitationTool {
name: "stateless-form-elicitation",
mode: PublicFinalElicitationMode::Form,
calls: Arc::clone(&calls),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut issuing_client = endpoint
.open_session(&cx)
.expect("issuing modern HTTP session must open");
let mut retrying_client = endpoint
.open_session(&cx)
.expect("independent modern HTTP session must open");
let request = |request_state: Option<&str>, id: i64| {
let mut params = serde_json::json!({
"name": "stateless-form-elicitation",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {"elicitation": {"form": {}}},
},
});
if let Some(request_state) = request_state {
params["inputResponses"] = serde_json::json!({
"approval": {"action": "accept", "content": {"approved": true}},
});
params["requestState"] = serde_json::json!(request_state);
}
let request = JsonRpcRequest::new("tools/call", Some(params), id);
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/call")
.with_header("mcp-name", "stateless-form-elicitation")
.with_header("authorization", "Bearer alpha")
.with_body(serde_json::to_vec(&request).expect("elicitation request must encode"))
};
let initial = block_on(issuing_client.handle_async(&cx, request(None, 912)))
.expect("stateless elicitation must return input_required");
let ServerHttpEndpointResponse::Immediate(initial) = initial else {
panic!("ordinary stateless elicitation must use the JSON response lane");
};
let initial: JsonRpcResponse =
serde_json::from_slice(&initial.body).expect("initial response must be JSON-RPC");
assert_eq!(initial.id, Some(912_i64.into()));
assert!(initial.error.is_none());
assert_eq!(
initial
.result
.as_ref()
.and_then(|result| result.get("resultType")),
Some(&serde_json::json!("input_required"))
);
let request_state = initial
.result
.as_ref()
.and_then(|result| result.get("requestState"))
.and_then(serde_json::Value::as_str)
.expect("initial elicitation result must carry requestState");
assert_eq!(calls.load(Ordering::Acquire), 1);
assert_eq!(endpoint.server.router.test_active_mrtr_exchange_count(), 1);
// MCP 2026-07-28 MRTR: an elicitation answer is ordinary input data, so
// a stateless retry from an independent HTTP session resumes the
// operation exactly like sampling or roots input does.
let resumed =
block_on(retrying_client.handle_async(&cx, request(Some(request_state), 913)))
.expect("stateless elicitation retry must receive a JSON-RPC response");
let ServerHttpEndpointResponse::Immediate(resumed) = resumed else {
panic!("stateless elicitation retry must use the JSON response lane");
};
let resumed: JsonRpcResponse =
serde_json::from_slice(&resumed.body).expect("retry response must be JSON-RPC");
assert_eq!(resumed.id, Some(913_i64.into()));
assert!(
resumed.error.is_none(),
"stateless elicitation retry must resume, got {:?}",
resumed.error
);
assert_eq!(
calls.load(Ordering::Acquire),
2,
"an admitted retry re-enters the handler exactly once"
);
// This fixture asks again on every call, so the resumed round mints a
// fresh state; the consumed one must not be reusable.
let reissued_state = resumed
.result
.as_ref()
.and_then(|result| result.get("requestState"))
.and_then(serde_json::Value::as_str)
.expect("the resumed round's input_required result carries a fresh requestState");
assert_ne!(reissued_state, request_state);
// Planted negative: replaying the consumed state is rejected before the
// handler runs.
let replayed =
block_on(retrying_client.handle_async(&cx, request(Some(request_state), 914)))
.expect("replayed elicitation retry must receive a JSON-RPC response");
let ServerHttpEndpointResponse::Immediate(replayed) = replayed else {
panic!("replayed elicitation retry must use the JSON response lane");
};
let replayed: JsonRpcResponse =
serde_json::from_slice(&replayed.body).expect("replay response must be JSON-RPC");
assert_eq!(replayed.id, Some(914_i64.into()));
let error = replayed
.error
.expect("a consumed elicitation requestState must not resume twice");
assert_eq!(error.code, McpErrorCode::InvalidParams.into());
assert_eq!(error.message, "Invalid or expired MRTR request state");
assert_eq!(
calls.load(Ordering::Acquire),
2,
"a replay rejection must happen before handler reentry"
);
}
#[test]
fn public_http_request_scoped_progress_and_logging_use_owned_sse() {
let cx = Cx::for_testing();
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = Server::new("final-http-request-scoped", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.log_level(Level::Debug)
.tool(HttpRequestScopedProgressTool {
calls: Arc::clone(&calls),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_request_scoped_progress",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "final-http-progress",
"io.modelcontextprotocol/logLevel": "info",
},
})),
811_i64,
);
let ServerHttpEndpointResponse::ModernSse(sse) = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json, text/event-stream; Q=1.")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/call")
.with_header("mcp-name", "http_request_scoped_progress")
.with_body(serde_json::to_vec(&request).expect("final request must encode")),
),
)
.expect("notification-capable modern request must dispatch over SSE") else {
panic!("notification-capable modern request must select SSE");
};
let progress = sse
.recv_event(&cx)
.expect("progress must precede the terminal response");
let JsonRpcMessage::Request(progress) = Codec::new()
.decode_complete_message(progress.data.as_bytes())
.expect("progress event must remain JSON-RPC")
else {
panic!("first request-scoped frame must be a progress notification");
};
let ServerNotification::Progress(progress) = ServerNotification::decode(&progress)
.expect("progress must use the final server notification union")
else {
panic!("first request-scoped frame must be notifications/progress");
};
assert_eq!(
progress.progress_token,
ProgressMarker::String("final-http-progress".to_owned())
);
let progress_wire =
serde_json::to_string(&progress).expect("coalesced final HTTP progress must serialize");
assert!(progress_wire.contains("\"progress\":12000"));
assert!(progress_wire.contains("\"total\":11999"));
assert!(progress_wire.contains("\"message\":\"latest\""));
let log = sse
.recv_event(&cx)
.expect("typed log must follow handler progress");
let JsonRpcMessage::Request(log) = Codec::new()
.decode_complete_message(log.data.as_bytes())
.expect("log event must remain JSON-RPC")
else {
panic!("second request-scoped frame must be a log notification");
};
assert!(matches!(
ServerNotification::decode(&log)
.expect("log must use the final server notification union"),
ServerNotification::Message(_)
));
assert!(matches!(
Codec::new()
.decode_complete_message(
sse.recv_event(&cx)
.expect("terminal response must follow notifications")
.data
.as_bytes(),
)
.expect("terminal response must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(811_i64.into()) && response.error.is_none()
));
assert_eq!(calls.load(Ordering::Acquire), 1);
}
#[test]
fn http_sse_would_block_retries_preserve_progress_log_and_terminal_order() {
let cx = Cx::for_testing();
let request_cancellation = McpRequestCancellation::new();
let commit_gate = Mutex::new(());
let frames = Mutex::new(Vec::new());
let attempts = AtomicUsize::new(0);
for frame in ["progress", "log", "response"] {
let _commit = commit_gate
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
retry_http_sse_commit(
&cx,
&request_cancellation,
|| {
if attempts.fetch_add(1, Ordering::AcqRel).is_multiple_of(2) {
return Err(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"forced final HTTP commit contention",
)));
}
frames
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(frame);
Ok(())
},
retryable_http_sse_transport_error,
)
.expect("bounded transient contention must retry to its ordered commit");
}
assert_eq!(attempts.load(Ordering::Acquire), 6);
assert_eq!(
*frames
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
vec!["progress", "log", "response"],
"a forced WouldBlock must not reorder the serialized final frames",
);
}
#[test]
fn http_sse_would_block_retry_stops_at_the_bound() {
let cx = Cx::for_testing();
let request_cancellation = McpRequestCancellation::new();
let attempts = AtomicUsize::new(0);
let error = retry_http_sse_commit(
&cx,
&request_cancellation,
|| {
attempts.fetch_add(1, Ordering::AcqRel);
Err::<(), _>(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"permanently contended final HTTP commit",
)))
},
retryable_http_sse_transport_error,
)
.expect_err("permanent contention must return instead of spinning forever");
assert!(retryable_http_sse_transport_error(&error));
assert_eq!(
attempts.load(Ordering::Acquire),
MAX_HTTP_SSE_COMMIT_RETRIES + 1,
"the initial attempt plus the configured retry bound are exact",
);
}
#[test]
fn public_http_exhausted_progress_commit_retires_before_log_or_terminal_response() {
let cx = Cx::for_testing();
let endpoint = Server::new("final-http-progress-exhaustion", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.log_level(Level::Debug)
.tool(HttpRequestScopedProgressTool {
calls: Arc::new(AtomicUsize::new(0)),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_request_scoped_progress",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "final-http-exhaustion-progress",
"io.modelcontextprotocol/logLevel": "info",
},
})),
813_i64,
);
set_forced_http_sse_would_block_commits(MAX_HTTP_SSE_COMMIT_RETRIES + 1);
let result = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "text/event-stream")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/call")
.with_header("mcp-name", "http_request_scoped_progress")
.with_body(serde_json::to_vec(&request).expect("final request must encode")),
),
);
let remaining = forced_http_sse_would_block_commits_remaining();
set_forced_http_sse_would_block_commits(0);
assert!(
result.is_err(),
"an exhausted public HTTP progress commit must retire instead of dropping progress and returning a log/response out of order"
);
assert_eq!(
remaining, 0,
"the public request must consume the initial attempt plus every bounded retry"
);
}
#[test]
fn public_http_final_progress_cancellation_discards_pending_update_and_log() {
let cx = Cx::for_testing();
let endpoint = Server::new("final-http-progress-cancellation", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.log_level(Level::Debug)
.tool(HttpFinalProgressCancellationTool)
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_final_progress_cancellation",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "final-http-cancelled-progress",
"io.modelcontextprotocol/logLevel": "info",
},
})),
812_i64,
);
let ServerHttpEndpointResponse::ModernSse(sse) = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "text/event-stream")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/call")
.with_header("mcp-name", "http_final_progress_cancellation")
.with_body(serde_json::to_vec(&request).expect("final request must encode")),
),
)
.expect("cancelled modern request must retain its owned SSE body") else {
panic!("cancelled modern request must select SSE");
};
assert!(matches!(
Codec::new()
.decode_complete_message(
sse.recv_event(&cx)
.expect("terminal cancellation response must be queued")
.data
.as_bytes(),
)
.expect("terminal cancellation response must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(812_i64.into())
&& response
.error
.as_ref()
.is_some_and(|error| error.code.as_i32() == Some(i32::from(McpErrorCode::RequestCancelled)))
));
}
#[test]
fn public_http_request_scoped_notifications_reject_only_zero_quality_sse() {
let cx = Cx::for_testing();
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = Server::new("final-http-request-scoped", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.log_level(Level::Debug)
.tool(HttpRequestScopedProgressTool {
calls: Arc::clone(&calls),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_request_scoped_progress",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"progressToken": "final-http-progress",
"io.modelcontextprotocol/logLevel": "info",
},
})),
811_i64,
);
let request = HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/call")
.with_header("mcp-name", "http_request_scoped_progress")
.with_body(serde_json::to_vec(&request).expect("final request must encode"));
for accept in [
"application/json, text/event-stream; Q=0.",
"text/event-stream;q=0, */*;q=1",
"*/*;q=1, text/event-stream;q=0",
"text/*;q=0, */*;q=1",
"text/event-stream, text/event-stream;q=0, application/json",
] {
let response =
block_on(session.handle_async(&cx, request.clone().with_header("accept", accept)))
.expect("zero-quality SSE request must be rejected before dispatch");
assert!(matches!(
response,
ServerHttpEndpointResponse::Immediate(response)
if response.status == HttpStatus::NOT_ACCEPTABLE && response.body.is_empty()
));
assert_eq!(
calls.load(Ordering::Acquire),
0,
"an SSE exclusion must prevent handler entry even with a wildcard: {accept}",
);
}
// Change only the exact SSE quality. JSON remains preferred by the
// wildcard, but a positive SSE weight permits this method's stream.
let response = block_on(session.handle_async(
&cx,
request.with_header("accept", "text/event-stream;q=0.1, */*;q=1"),
))
.expect("an accepted required SSE representation must dispatch after refusals");
let ServerHttpEndpointResponse::ModernSse(sse) = response else {
panic!("the notification-capable method requires its accepted SSE body");
};
let progress = sse.recv_event(&cx).expect("progress frame must be queued");
let log = sse.recv_event(&cx).expect("log frame must be queued");
let terminal = sse
.recv_event(&cx)
.expect("terminal response must be queued");
assert!(matches!(
Codec::new().decode_complete_message(progress.data.as_bytes()),
Ok(JsonRpcMessage::Request(notification))
if notification.method == "notifications/progress"
));
assert!(matches!(
Codec::new().decode_complete_message(log.data.as_bytes()),
Ok(JsonRpcMessage::Request(notification))
if notification.method == "notifications/message"
));
assert!(matches!(
Codec::new().decode_complete_message(terminal.data.as_bytes()),
Ok(JsonRpcMessage::Response(response))
if response.id == Some(811_i64.into()) && response.error.is_none()
));
assert_eq!(calls.load(Ordering::Acquire), 1);
}
#[test]
fn public_http_ordinary_json_request_remains_one_response() {
let cx = Cx::for_testing();
let calls = Arc::new(AtomicUsize::new(0));
let endpoint = Server::new("final-http-json-only", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.tool(HttpRequestScopedProgressTool {
calls: Arc::clone(&calls),
})
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let request = JsonRpcRequest::new(
"tools/call",
Some(serde_json::json!({
"name": "http_request_scoped_progress",
"arguments": {},
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
})),
812_i64,
);
let response = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/call")
.with_header("mcp-name", "http_request_scoped_progress")
.with_body(serde_json::to_vec(&request).expect("final request must encode")),
),
)
.expect("ordinary JSON-only request must dispatch");
let ServerHttpEndpointResponse::Immediate(response) = response else {
panic!("ordinary JSON-only request must retain its one-response representation");
};
assert!(matches!(
Codec::new()
.decode_complete_message(&response.body)
.expect("JSON-only response must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(812_i64.into()) && response.error.is_none()
));
assert_eq!(calls.load(Ordering::Acquire), 1);
}
#[test]
fn final_runtime_emits_typed_log_message_with_server_metadata() {
let cx = Cx::for_testing();
let endpoint = Server::new("final-log-runtime", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.log_level(Level::Debug)
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let request = JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"io.modelcontextprotocol/logLevel": "info",
},
})),
91_i64,
);
let ServerHttpEndpointResponse::ModernSse(sse) = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "text/event-stream")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/list")
.with_body(serde_json::to_vec(&request).expect("final request must encode")),
),
)
.expect("public modern request must dispatch") else {
panic!("public modern request must return an SSE body");
};
let log = sse
.recv_event(&cx)
.expect("typed final log must arrive before the response");
let JsonRpcMessage::Request(log) = Codec::new()
.decode_complete_message(log.data.as_bytes())
.expect("typed final log must remain JSON-RPC")
else {
panic!("first SSE frame must be a typed final log notification");
};
let ServerNotification::Message(message) = ServerNotification::decode(&log)
.expect("final runtime logging must use the typed final notification union")
else {
panic!("final runtime must emit notifications/message");
};
assert_eq!(message.level, LoggingLevel::Info);
assert_eq!(message.logger.as_deref(), Some("fastmcp_rust::server"));
assert_eq!(
message
.meta
.as_ref()
.and_then(|meta| meta.server_info().ok().flatten())
.map(|info| info.name),
Some("final-log-runtime".to_owned()),
"typed final logging must retain final server metadata",
);
assert!(matches!(
Codec::new()
.decode_complete_message(
sse.recv_event(&cx)
.expect("request response must follow the log")
.data
.as_bytes(),
)
.expect("response must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(91_i64.into()) && response.error.is_none()
));
}
#[test]
fn final_runtime_suppresses_info_log_when_only_log_level_changes_to_warning() {
let cx = Cx::for_testing();
let endpoint = Server::new("final-log-runtime", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.log_level(Level::Debug)
.test_http_endpoint("http://final.test")
.expect("modern endpoint must build");
let mut session = endpoint
.open_session(&cx)
.expect("modern endpoint session must open");
let request = JsonRpcRequest::new(
"tools/list",
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
"io.modelcontextprotocol/logLevel": "warning",
},
})),
91_i64,
);
let ServerHttpEndpointResponse::ModernSse(sse) = block_on(
session.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "text/event-stream")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "tools/list")
.with_body(serde_json::to_vec(&request).expect("final request must encode")),
),
)
.expect("public modern request must dispatch") else {
panic!("public modern request must return an SSE body");
};
assert!(
matches!(
Codec::new()
.decode_complete_message(
sse.recv_event(&cx)
.expect("request response must remain readable")
.data
.as_bytes(),
)
.expect("response must remain JSON-RPC"),
JsonRpcMessage::Response(response)
if response.id == Some(91_i64.into()) && response.error.is_none()
),
"changing only final logLevel from info to warning must suppress the info notification"
);
// The dispatch task owns the response sender, so a completed request
// may legitimately close the body afterwards; the negative's
// invariant is only that no suppressed notification was queued.
assert!(
!matches!(sse.pop_event(), Ok(Some(_))),
"the log-level-only negative must leave no notification queued"
);
}
#[test]
fn public_stdio_peer_close_removes_without_terminal_completion() {
// This mirrors the server-teardown positive above; only the terminal
// authority changes from server termination to peer closure.
let server = Arc::new(Server::new("final-listen-peer-cancel-test", "1.0.0").build());
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let (acknowledged, acknowledgement) = std::sync::mpsc::channel::<()>();
let notification_sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
let _ = acknowledged.send(());
});
let subscription_id = RequestId::Number(74);
let metadata = OpenMetadata::try_from_entries([
(
"io.modelcontextprotocol/protocolVersion".to_owned(),
serde_json::json!(MODERN_PROTOCOL_VERSION),
),
(
FINAL_CLIENT_CAPABILITIES_META_KEY.to_owned(),
serde_json::json!({}),
),
])
.expect("final request metadata must be valid");
let params = serde_json::to_value(FinalSubscriptionsListenParams {
meta: metadata,
notifications: SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
})
.expect("final listen parameters must serialize");
let request =
JsonRpcRequest::new(SUBSCRIPTIONS_LISTEN, Some(params), subscription_id.clone());
let inbound = InboundRequestContext::new(
Cx::for_testing(),
request_id_to_u64(Some(&subscription_id)),
InboundRequestTransport::Stdio,
);
let request_cancellation = McpRequestCancellation::new();
// The peer closes the stream once the listener acknowledges. Waiting
// is bounded in runnable time, so a loaded suite cannot expire it
// while a missing acknowledgement still does. The peer closes either
// way, so the dispatch always ends and the assertion reports the wait.
let canceller = {
let request_cancellation = request_cancellation.clone();
let host = RunnableClock::start();
std::thread::spawn(move || {
let started = host.mark();
let outcome = loop {
match acknowledgement.recv_timeout(Duration::from_millis(20)) {
Ok(()) => break Ok(()),
Err(std::sync::mpsc::RecvTimeoutError::Timeout)
if !host.expired(started, Duration::from_secs(10)) => {}
Err(error) => {
break Err(format!("{error} after {}", host.describe(started)));
}
}
};
request_cancellation.cancel();
outcome
})
};
let response = block_on(Arc::clone(&server).dispatch_with_protocol_policy_owned(
ProtocolPolicy::ModernOnly,
&inbound,
request,
None,
None,
None,
None,
request_cancellation,
None,
notification_sender,
));
if let Err(waited) = canceller
.join()
.expect("peer cancellation helper must not panic")
{
panic!("the listener must acknowledge before the peer can close it: {waited}");
}
assert!(
response.is_none(),
"a peer-closed stream must not receive a result"
);
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"changing only the terminal trigger to peer cancellation must not send server cancellation control",
);
assert!(
server
.final_subscriptions
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.is_empty(),
"peer cancellation must release exactly its request-owned registry entry",
);
}
#[test]
fn final_subscription_termination_is_single_winner_and_idempotent() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
});
let cancellation = McpRequestCancellation::new();
let _lease = registry
.open(
RequestId::Number(881),
SubscriptionFilter::default(),
false,
None,
cancellation.clone(),
None,
sender,
)
.expect("a final listen must register before termination");
assert_eq!(registry.terminate(), 1);
assert_eq!(registry.terminate(), 0);
assert!(cancellation.is_cancel_requested());
let sent = sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(sent.len(), 2, "one acknowledgement and one control only");
assert!(matches!(
ServerNotification::decode(&sent[1]),
Ok(ServerNotification::Cancelled(_))
));
}
#[test]
fn final_subscription_stdio_shutdown_receipt_waits_for_terminal_consumption() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let terminal_delivery = Arc::new(FinalSubscriptionTerminalDelivery::default());
let sender: NotificationSender = Arc::new(|_| {});
let _lease = registry
.open(
RequestId::Number(887),
SubscriptionFilter::default(),
false,
None,
McpRequestCancellation::new(),
Some(Arc::clone(&terminal_delivery)),
sender,
)
.expect("a final listen must register before termination");
let receipt = registry.terminate_with_receipt();
assert_eq!(receipt.terminated, 1);
assert!(
!receipt.is_settled(),
"queue admission alone must not masquerade as body consumption"
);
terminal_delivery.mark_drained();
assert!(
!receipt.is_settled(),
"terminal control consumption must not stand in for final completion delivery"
);
terminal_delivery.mark_completion_enqueued();
terminal_delivery.mark_completion_drained();
assert!(receipt.is_settled());
}
#[test]
fn cancelled_http_writer_fails_an_open_completion_so_dual_listen_drain_can_settle() {
let delivery = FinalSubscriptionTerminalDelivery::default();
delivery.mark_control_not_required();
assert!(
!delivery.is_settled(),
"an unelected complete must keep the drain receipt open"
);
delivery.mark_failed();
assert!(
delivery.is_settled(),
"a cancelled connection region must settle instead of burning the drain bound"
);
}
#[test]
fn cancelled_http_writer_fails_after_stream_election_without_a_control_frame() {
let delivery = FinalSubscriptionTerminalDelivery::default();
delivery.mark_control_not_required();
assert!(
delivery.is_committed(),
"control-not-required is the committed control half so a cancelled request stays live until complete"
);
assert!(
!delivery.is_settled(),
"an unelected complete must keep the drain receipt open"
);
delivery.mark_failed();
assert!(
delivery.is_settled(),
"as_proxy dual listen drain must settle when the peer drops after Stream"
);
}
#[test]
fn final_subscription_stdio_shutdown_receipt_fails_closed_after_backpressure_bound() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let terminal_delivery = Arc::new(FinalSubscriptionTerminalDelivery::default());
let sender: NotificationSender = Arc::new(|_| {});
let _lease = registry
.open(
RequestId::Number(888),
SubscriptionFilter::default(),
false,
None,
McpRequestCancellation::new(),
Some(Arc::clone(&terminal_delivery)),
sender,
)
.expect("a final listen must register before termination");
let receipt = registry.terminate_with_receipt();
assert!(!receipt.is_settled());
receipt.fail_pending();
assert!(
receipt.is_settled(),
"bounded shutdown must turn an unconsumed terminal into an explicit failure"
);
assert!(!terminal_delivery.is_drained());
}
#[test]
fn modern_http_writer_exit_without_complete_fails_a_graceful_election_receipt() {
let delivery = FinalSubscriptionTerminalDelivery::default();
delivery.mark_control_not_required();
assert!(
delivery.is_committed(),
"control-not-required is the committed control half; the H1 writer must keep waiting for complete"
);
assert!(
!delivery.is_settled(),
"an elected complete that was never flushed must not settle the receipt"
);
delivery.mark_failed();
assert!(
delivery.is_settled(),
"an H1 writer that closes without flushing complete must fail the receipt immediately"
);
}
#[test]
fn final_subscription_peer_cancellation_wins_before_shutdown_without_control() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
});
let cancellation = McpRequestCancellation::new();
let lease = registry
.open(
RequestId::String("peer-wins-882".to_owned()),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
Some(882),
cancellation.clone(),
None,
sender,
)
.expect("a modern HTTP listen must register");
assert!(registry.cancel_modern_http_owner(882));
assert!(!registry.cancel_modern_http_owner(882));
assert_eq!(
registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("post-terminal publication remains well-formed"),
0,
"a snapshotted entry cannot publish after its peer terminal election",
);
assert_eq!(registry.terminate(), 0);
assert!(cancellation.is_cancel_requested());
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"peer winner leaves only the mandatory acknowledgement"
);
drop(lease);
}
#[test]
fn final_subscription_peer_token_prevents_immediate_publication_and_shutdown_callbacks() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sent_for_sender = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
});
let cancellation = McpRequestCancellation::new();
let _lease = registry
.open(
RequestId::Number(885),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
Some(885),
cancellation.clone(),
None,
sender,
)
.expect("a final listen must acknowledge before peer cancellation");
cancellation.cancel();
assert_eq!(
registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("peer-cancelled publication remains well-formed"),
0,
"a peer-cancelled token must stop the callback before lease cleanup",
);
assert_eq!(registry.terminate(), 0);
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"peer cancellation leaves only the acknowledgement",
);
}
#[test]
fn final_subscription_callback_cancellation_is_not_counted_as_delivery() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let cancellation = McpRequestCancellation::new();
let sent_for_sender = Arc::clone(&sent);
let cancellation_for_sender = cancellation.clone();
let sender: NotificationSender = Arc::new(move |notification| {
let mut sent = sent_for_sender
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !sent.is_empty() {
cancellation_for_sender.cancel();
}
sent.push(notification);
});
let _lease = registry
.open(
RequestId::Number(886),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
Some(886),
cancellation,
None,
sender,
)
.expect("a final listen must acknowledge before callback cancellation");
assert_eq!(
registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("callback cancellation remains a well-formed publication"),
0,
"a callback that observes peer closure must not report a delivery",
);
assert_eq!(registry.terminate(), 0);
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
2,
"the attempted event is retained by this planted sender, but no terminal control follows",
);
}
#[test]
fn final_subscription_open_acknowledgement_is_atomic_with_termination() {
let registry = Arc::new(FinalSubscriptionRegistry::default());
let gate = Arc::new((Mutex::new((false, false)), Condvar::new()));
let sent = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
let sender_gate = Arc::clone(&gate);
let sender_sent = Arc::clone(&sent);
let sender: NotificationSender = Arc::new(move |notification| {
let (lock, ready) = &*sender_gate;
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if !state.0 {
state.0 = true;
ready.notify_all();
while !state.1 {
state = ready
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
}
drop(state);
sender_sent
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(notification);
});
let cancellation = McpRequestCancellation::new();
let opener_registry = Arc::clone(®istry);
let opener_cancellation = cancellation.clone();
let opener = thread::spawn(move || {
opener_registry.open(
RequestId::Number(883),
SubscriptionFilter {
tools_list_changed: Some(true),
..SubscriptionFilter::default()
},
false,
Some(883),
opener_cancellation,
None,
sender,
)
});
let (lock, ready) = &*gate;
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
while !state.0 {
state = ready
.wait(state)
.unwrap_or_else(std::sync::PoisonError::into_inner);
}
drop(state);
assert_eq!(
registry
.publish(ServerNotification::ToolsListChanged(None))
.expect("publication during a held acknowledgement must be admitted"),
1,
);
let (started_sender, started_receiver) = sync_channel(1);
let (terminated_sender, terminated_receiver) = sync_channel(1);
let terminator_registry = Arc::clone(®istry);
let terminator = thread::spawn(move || {
let _ = started_sender.send(());
let _ = terminated_sender.send(terminator_registry.terminate());
});
started_receiver
.recv()
.expect("terminator must reach the teardown attempt");
let deadline = Instant::now() + Duration::from_secs(1);
while !registry.is_terminating() {
assert!(
Instant::now() < deadline,
"terminator did not begin while acknowledgement was held"
);
thread::yield_now();
}
assert_eq!(
terminated_receiver
.recv()
.expect("terminator must report its winner count"),
1,
"shutdown must claim the opening entry without waiting on its acknowledgement callback"
);
terminator.join().expect("terminator must not panic");
assert!(
!cancellation.is_cancel_requested(),
"shutdown must not wake the opener before its acknowledgement callback returns"
);
assert!(
registry
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.is_empty(),
"teardown must remove an opening entry without waiting on its callback"
);
let mut state = lock
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.1 = true;
ready.notify_all();
drop(state);
let lease = opener
.join()
.expect("subscription opener must not panic")
.expect("the shutdown winner must finish the acknowledged stream gracefully");
assert!(lease.has_graceful_completion());
assert!(
cancellation.is_cancel_requested(),
"the acknowledged opener wakes only after graceful completion wins"
);
assert_eq!(
sent.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len(),
1,
"server termination must discard the queued event and modern HTTP emits no stdio cancellation control"
);
}
#[test]
fn sse_body_observation_treats_only_mailbox_contention_as_pending() {
let busy = DualEraHttpEndpointError::Transport(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"streamable response mailbox is busy",
)));
assert!(matches!(sse_body_observation(Err(busy)), Ok(None)));
// Near-identical negative: the same transport I/O failure of another
// kind is a real body failure and must still end the stream.
let broken = DualEraHttpEndpointError::Transport(TransportError::Io(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"streamable response mailbox is busy",
)));
assert!(matches!(
sse_body_observation(Err(broken)),
Err(DualEraHttpEndpointError::Transport(TransportError::Io(error)))
if error.kind() == std::io::ErrorKind::BrokenPipe
));
}
/// bd-a8tbq: a live writer read its body while a handler committed a
/// notification, saw the mailbox busy, and dropped the listen connection.
#[test]
fn live_listen_body_reader_survives_concurrent_notification_commits() {
const COMMITS: usize = 2_000;
run_live_http_test(|cx| async move {
let endpoint = Server::new("public-http-listen-contention", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.test_http_endpoint("http://legacy.test")
.map_err(|error| format!("public endpoint setup failed: {error}"))?;
let mut session = endpoint
.open_session(&cx)
.map_err(|error| format!("public listen session failed: {error}"))?;
let listen = JsonRpcRequest::new(
SUBSCRIPTIONS_LISTEN,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
"notifications": {"toolsListChanged": true},
})),
RequestId::Number(885),
);
let request = HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "text/event-stream")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", SUBSCRIPTIONS_LISTEN)
.with_body(
serde_json::to_vec(&listen).expect("typed listen request must serialize"),
);
let ServerHttpEndpointResponse::ModernSse(sse) = session
.handle_async(&cx, request)
.await
.map_err(|error| format!("public listen failed: {error}"))?
else {
return Err("public listen did not return SSE".to_owned());
};
let deadline = Instant::now() + Duration::from_secs(2);
loop {
match sse.pop_event() {
Ok(Some(_acknowledgement)) => break,
Ok(None) if Instant::now() < deadline => {
asupersync::time::sleep(cx.now(), Duration::from_millis(1)).await;
}
Ok(None) => return Err("public listen acknowledgement timed out".to_owned()),
Err(error) => return Err(format!("public listen closed early: {error}")),
}
}
// Commit from another thread while this one keeps reading, so the
// two sides contend for the response mailbox as a live handler
// and the listen writer do.
let sender = sse.inner.sender();
let producer = thread::spawn(move || {
let cx = Cx::for_testing();
for index in 0..COMMITS {
let notification = JsonRpcRequest::notification(
"notifications/resources/updated",
Some(serde_json::json!({"uri": format!("file:///contention/{index}")})),
);
loop {
match sender.send_notification(&cx, notification.clone()) {
Ok(()) => break,
Err(error) if retryable_http_sse_transport_error(&error) => {
thread::yield_now();
}
Err(error) => return Err(format!("commit {index} failed: {error}")),
}
}
}
Ok(())
});
let mut received = 0;
let deadline = Instant::now() + Duration::from_secs(20);
while received < COMMITS {
match pop_sse_body_event(&sse.inner) {
Ok(Some(_)) => received += 1,
Ok(None) if Instant::now() < deadline => thread::yield_now(),
Ok(None) => {
return Err(format!(
"only {received} of {COMMITS} commits were observed"
));
}
Err(error) => {
return Err(format!(
"contention ended the live body after {received} events: {error}"
));
}
}
}
producer
.join()
.map_err(|_| "notification producer panicked".to_owned())??;
Ok(())
});
}
#[test]
fn public_http_response_body_drop_cancels_only_its_owned_listen() {
run_live_http_test(|cx| async move {
let endpoint = Server::new("public-http-listen", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.test_http_endpoint("http://legacy.test")
.map_err(|error| format!("public endpoint setup failed: {error}"))?;
let mut first_session = endpoint
.open_session(&cx)
.map_err(|error| format!("first public listen session failed: {error}"))?;
let mut second_session = endpoint
.open_session(&cx)
.map_err(|error| format!("second public listen session failed: {error}"))?;
let listen_request = || {
let listen = JsonRpcRequest::new(
SUBSCRIPTIONS_LISTEN,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
"notifications": {"toolsListChanged": true},
})),
RequestId::Number(884),
);
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "text/event-stream")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", SUBSCRIPTIONS_LISTEN)
.with_body(
serde_json::to_vec(&listen).expect("typed listen request must serialize"),
)
};
let first = first_session
.handle_async(&cx, listen_request())
.await
.map_err(|error| format!("first public listen failed: {error}"))?;
let second = second_session
.handle_async(&cx, listen_request())
.await
.map_err(|error| format!("second public listen failed: {error}"))?;
let ServerHttpEndpointResponse::ModernSse(first_sse) = first else {
return Err("first public listen did not return SSE".to_owned());
};
let ServerHttpEndpointResponse::ModernSse(second_sse) = second else {
return Err("second public listen did not return SSE".to_owned());
};
let first_guard = first_sse.cancellation();
let second_guard = second_sse.cancellation();
let deadline = Instant::now() + Duration::from_secs(2);
for sse in [&first_sse, &second_sse] {
loop {
match sse.pop_event() {
Ok(Some(_acknowledgement)) => break,
Ok(None) if Instant::now() < deadline => {
asupersync::time::sleep(cx.now(), Duration::from_millis(1)).await;
}
Ok(None) => {
return Err("public listen acknowledgement timed out".to_owned());
}
Err(error) => return Err(format!("public listen closed early: {error}")),
}
}
}
if first_guard.checkpoint(&cx).is_err() || second_guard.checkpoint(&cx).is_err() {
return Err("an acknowledged response body was not pending".to_owned());
}
let mut rejected_session = endpoint
.open_session(&cx)
.map_err(|error| format!("rejected cancellation session failed: {error}"))?;
let cancellation = JsonRpcRequest::notification(
"notifications/cancelled",
Some(serde_json::json!({
"requestId": 884,
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
},
})),
);
let rejected = rejected_session
.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "application/json")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "notifications/cancelled")
.with_body(
serde_json::to_vec(&cancellation)
.expect("typed cancellation notification must serialize"),
),
)
.await
.map_err(|error| format!("cancellation rejection failed: {error}"))?;
let ServerHttpEndpointResponse::Immediate(rejected) = rejected else {
return Err(
"HTTP cancellation notification was not rejected immediately".to_owned(),
);
};
// Accepted with 202 (a notification), yet it carries no authority
// over either in-flight response body.
if rejected.status != HttpStatus::ACCEPTED
|| first_guard.checkpoint(&cx).is_err()
|| second_guard.checkpoint(&cx).is_err()
{
return Err("HTTP cancellation POST affected a response body".to_owned());
}
let mut streaming_rejected_session = endpoint
.open_session(&cx)
.map_err(|error| format!("streaming cancellation session failed: {error}"))?;
let streaming_rejected = streaming_rejected_session
.begin_modern_sse(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "text/event-stream")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", "notifications/cancelled")
.with_body(
serde_json::to_vec(&cancellation)
.expect("typed cancellation notification must serialize"),
),
TransportAuthorization::default(),
None,
)
.await
.map_err(|error| format!("streaming cancellation rejection failed: {error}"))?
.map_err(|response| *response);
let Err(ServerHttpEndpointResponse::Immediate(streaming_rejected)) = streaming_rejected
else {
return Err("live SSE cancellation POST was not rejected immediately".to_owned());
};
if streaming_rejected.status != HttpStatus::ACCEPTED {
return Err("live SSE cancellation POST used the wrong status".to_owned());
}
streaming_rejected_session.close(&cx).await;
drop(first_sse);
if first_guard.checkpoint(&cx).is_ok() || second_guard.checkpoint(&cx).is_err() {
return Err("response-body drop did not isolate cancellation".to_owned());
}
drop(first_session);
let removal_deadline = Instant::now() + Duration::from_secs(2);
while endpoint
.server
.final_subscriptions
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.len()
!= 1
{
if Instant::now() >= removal_deadline {
return Err("dropped response body was not removed".to_owned());
}
asupersync::time::sleep(cx.now(), Duration::from_millis(1)).await;
}
if endpoint
.server
.publish_subscription_notification(ServerNotification::ToolsListChanged(None))
.map_err(|error| format!("surviving listen publish failed: {error}"))?
!= 1
|| !matches!(second_sse.pop_event(), Ok(Some(_)))
{
return Err("dropped body affected the surviving listen".to_owned());
}
if endpoint.server.terminate_subscription_streams() != 1 {
return Err("server teardown did not elect the surviving listen".to_owned());
}
if second_guard.checkpoint(&cx).is_err() || second_sse.is_finished() {
return Err("HTTP completion was confused with a closed public body".to_owned());
}
let completion_deadline = Instant::now() + Duration::from_secs(2);
let completion = loop {
match second_sse.pop_event() {
Ok(Some(event)) => break event,
Ok(None) if Instant::now() < completion_deadline => {
asupersync::time::sleep(cx.now(), Duration::from_millis(1)).await;
}
Ok(None) => return Err("terminal completion remained pending".to_owned()),
Err(error) => {
return Err(format!("terminal completion was not readable: {error}"));
}
}
};
if let Ok(notification) = serde_json::from_str::<JsonRpcRequest>(&completion.data) {
if final_subscription_terminal_notification(¬ification) {
return Err(
"HTTP graceful teardown emitted notifications/cancelled instead of its complete result"
.to_owned(),
);
}
return Err(
"HTTP graceful teardown emitted a notification instead of its complete result"
.to_owned(),
);
}
let completion: JsonRpcResponse = serde_json::from_str(&completion.data)
.map_err(|error| format!("terminal completion was invalid JSON-RPC: {error}"))?;
if completion.id != Some(RequestId::Number(884))
|| !final_subscription_completion_response(&completion)
{
return Err(
"server teardown did not complete the exact public listen request".to_owned(),
);
}
if !second_sse.is_finished() || second_guard.checkpoint(&cx).is_ok() {
return Err("terminal completion did not finish the public SSE body".to_owned());
}
if !matches!(second_sse.pop_event(), Err(ServerHttpEndpointError::Closed)) {
return Err(
"terminal completion did not close the public SSE response body".to_owned(),
);
}
second_session.close(&cx).await;
if second_guard.checkpoint(&cx).is_ok()
|| !endpoint
.server
.final_subscriptions
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.is_empty()
|| second_sse.pop_event().is_ok()
|| second_session.has_modern_dispatches()
{
return Err("session close remained indistinguishable from pending".to_owned());
}
second_session.close(&cx).await;
Ok(())
});
}
#[test]
fn public_http_peer_close_revokes_listen_without_terminal_completion() {
run_live_http_test(|cx| async move {
let endpoint = Server::new("public-http-last-session-drop", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.test_http_endpoint("http://legacy.test")
.map_err(|error| format!("public endpoint setup failed: {error}"))?;
let mut session = endpoint
.open_session(&cx)
.map_err(|error| format!("public listen session failed: {error}"))?;
let listen = JsonRpcRequest::new(
SUBSCRIPTIONS_LISTEN,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
"notifications": {"toolsListChanged": true},
})),
RequestId::Number(889),
);
let response = session
.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "text/event-stream")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", SUBSCRIPTIONS_LISTEN)
.with_body(
serde_json::to_vec(&listen)
.expect("typed listen request must serialize"),
),
)
.await
.map_err(|error| format!("public listen failed: {error}"))?;
let ServerHttpEndpointResponse::ModernSse(sse) = response else {
return Err("public listen did not return SSE".to_owned());
};
let guard = sse.cancellation();
let deadline = Instant::now() + Duration::from_secs(2);
loop {
match sse.pop_event() {
Ok(Some(_acknowledgement)) => break,
Ok(None) if Instant::now() < deadline => {
asupersync::time::sleep(cx.now(), Duration::from_millis(1)).await;
}
Ok(None) => return Err("public listen acknowledgement timed out".to_owned()),
Err(error) => return Err(format!("public listen closed early: {error}")),
}
}
if guard.checkpoint(&cx).is_err() {
return Err("acknowledged listen was not pending before session drop".to_owned());
}
drop(session);
if guard.checkpoint(&cx).is_ok()
|| !endpoint
.server
.final_subscriptions
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.is_empty()
{
return Err("peer close did not revoke its exact request".to_owned());
}
if !matches!(sse.pop_event(), Err(ServerHttpEndpointError::Closed)) {
return Err(
"peer-close path emitted an event instead of closing without completion"
.to_owned(),
);
}
Ok(())
});
}
#[test]
fn public_http_live_session_keeps_acknowledged_listen_open_without_peer_close() {
// Differs from the peer-close test above only in that the session
// stays open: the listen must stay pending, registered and silent.
run_live_http_test(|cx| async move {
let endpoint = Server::new("public-http-last-session-drop", "1.0.0")
.protocol_policy(ProtocolPolicy::ModernOnly)
.expect("ModernOnly must be available to this test build")
.test_http_endpoint("http://legacy.test")
.map_err(|error| format!("public endpoint setup failed: {error}"))?;
let mut session = endpoint
.open_session(&cx)
.map_err(|error| format!("public listen session failed: {error}"))?;
let listen = JsonRpcRequest::new(
SUBSCRIPTIONS_LISTEN,
Some(serde_json::json!({
"_meta": {
MODERN_PROTOCOL_VERSION_METADATA_KEY: MODERN_PROTOCOL_VERSION,
FINAL_CLIENT_CAPABILITIES_META_KEY: {},
},
"notifications": {"toolsListChanged": true},
})),
RequestId::Number(889),
);
let response = session
.handle_async(
&cx,
HttpRequest::new(HttpMethod::Post, "/mcp")
.with_header("content-type", "application/json")
.with_header("accept", "text/event-stream")
.with_header("mcp-protocol-version", MODERN_PROTOCOL_VERSION)
.with_header("mcp-method", SUBSCRIPTIONS_LISTEN)
.with_body(
serde_json::to_vec(&listen)
.expect("typed listen request must serialize"),
),
)
.await
.map_err(|error| format!("public listen failed: {error}"))?;
let ServerHttpEndpointResponse::ModernSse(sse) = response else {
return Err("public listen did not return SSE".to_owned());
};
let guard = sse.cancellation();
let deadline = Instant::now() + Duration::from_secs(2);
loop {
match sse.pop_event() {
Ok(Some(_acknowledgement)) => break,
Ok(None) if Instant::now() < deadline => {
asupersync::time::sleep(cx.now(), Duration::from_millis(1)).await;
}
Ok(None) => return Err("public listen acknowledgement timed out".to_owned()),
Err(error) => return Err(format!("public listen closed early: {error}")),
}
}
if guard.checkpoint(&cx).is_err() {
return Err("acknowledged listen was not pending before session drop".to_owned());
}
if guard.checkpoint(&cx).is_err()
|| endpoint
.server
.final_subscriptions
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entries
.is_empty()
{
return Err("an open session revoked its acknowledged listen".to_owned());
}
if !matches!(sse.pop_event(), Ok(None)) {
return Err(
"an open session's listen emitted an event or closed without a peer close"
.to_owned(),
);
}
drop(session);
Ok(())
});
}
#[cfg(feature = "tasks")]
struct IdleHostedTaskSupervisor;
#[cfg(feature = "tasks")]
impl ApplicationTaskSupervisor for IdleHostedTaskSupervisor {
fn resume<'a>(
&'a self,
_cx: &'a Cx,
_handoff: FinalTaskSupervisorHandoff,
) -> FinalTaskSupervisorFuture<'a> {
Box::pin(async { Ok(()) })
}
}
/// Serves `bound` until `runtime` reports a ready Task service or a bound
/// passes, then cancels the serve. Returns whether the service was ready.
#[cfg(feature = "tasks")]
async fn serve_until_task_service_ready(
cx: &Cx,
bound: BoundHttpServer,
runtime: FinalTaskRuntime,
) -> Result<bool, String> {
let caller_cx = cx.clone();
let mut observer = cx
.spawn(move |observer_cx| async move {
let mut ready = false;
for _ in 0..2_000 {
if runtime.is_task_service_ready() {
ready = true;
break;
}
asupersync::time::sleep(observer_cx.now(), Duration::from_millis(1)).await;
}
caller_cx.cancel_with(CancelKind::User, Some("hosted Task service observed"));
ready
})
.map_err(|error| format!("observer admission failed: {error}"))?;
let serve = bound.serve(cx).await;
let ready = observer
.join(cx)
.await
.map_err(|error| format!("observer failed: {error:?}"))?;
let shutdown = serve.map_err(|error| format!("hosted Tasks serve failed: {error}"))?;
require_quiescent_http_shutdown(shutdown, "hosted Tasks serve").await?;
Ok(ready)
}
#[cfg(feature = "tasks")]
#[test]
fn live_http_hosts_a_ready_task_service_and_settles_it_when_serve_is_cancelled() {
run_live_http_test(|cx| async move {
let server = Server::new("live-hosted-tasks", "1.0.0")
.task_supervisor(Arc::new(IdleHostedTaskSupervisor))
.build();
let runtime = server
.final_task_runtime()
.cloned()
.ok_or_else(|| "the default Tasks runtime is installed".to_owned())?;
if runtime.is_task_service_ready() {
return Err("building must not start the Task service".to_owned());
}
let bound = server
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("hosted Tasks bind failed: {error}"))?;
if !serve_until_task_service_ready(&cx, bound, runtime.clone()).await? {
return Err("the serve must host a ready Task service".to_owned());
}
if runtime.is_task_service_ready() {
return Err("no Task service runner may still run after serve returns".to_owned());
}
Ok(())
});
}
#[cfg(feature = "tasks")]
#[test]
fn live_http_without_a_task_supervisor_hosts_no_task_service() {
run_live_http_test(|cx| async move {
let server = Server::new("live-unhosted-tasks", "1.0.0").build();
let runtime = server
.final_task_runtime()
.cloned()
.ok_or_else(|| "the default Tasks runtime is installed".to_owned())?;
let bound = server
.bind_http(&cx, "127.0.0.1:0")
.await
.map_err(|error| format!("unhosted Tasks bind failed: {error}"))?;
if serve_until_task_service_ready(&cx, bound, runtime).await? {
return Err("a server without task_supervisor must host no Task service".to_owned());
}
Ok(())
});
}
}