use core::num::{NonZeroU32, NonZeroUsize};
use core::time::Duration;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use axum::Router;
use axum::body::Body;
use axum::http::{HeaderMap, Request, StatusCode, header};
use serde_json::{Value, json};
use tower::ServiceExt;
use super::{
Inbound, McpRouter, RequestHandler, SessionHandle, Unsessioned, authenticate, resolve_session,
};
use crate::adapters::primary::transport::http::limits::RateLimiter;
use crate::adapters::primary::transport::http::{
AuthPolicy, Authenticator, Credentials, EventTail, Refusal, SecurityHeaders, ServerConfig,
SessionStore, Streaming,
};
use crate::adapters::primary::transport::origin::OriginPolicy;
use crate::application::session::IdSource;
use crate::domain::mcp::{ResourceMetadata, ResourceUri};
use crate::domain::registry::auth::{AuthContext, Scope, ScopeSet};
use crate::domain::session::SessionId;
use crate::domain::{EventId, McpSessionId, ProtocolVersion, RateLimit};
type TestError = Box<dyn std::error::Error>;
fn fail(message: impl Into<String>) -> TestError {
message.into().into()
}
fn require<T: PartialEq + core::fmt::Debug>(
got: &T,
want: &T,
because: &str,
) -> Result<(), TestError> {
if got == want {
Ok(())
} else {
Err(fail(format!("{because}: got {got:?}, wanted {want:?}")))
}
}
struct Counting(AtomicU64);
impl IdSource for Counting {
fn next_session_id(&self) -> SessionId {
SessionId::new(self.0.fetch_add(1, Ordering::SeqCst) + 1)
}
}
fn tail() -> Result<Arc<EventTail<Value>>, TestError> {
let capacity = NonZeroUsize::new(8).ok_or("a capacity of zero is not a capacity")?;
Ok(Arc::new(EventTail::new(capacity)))
}
#[derive(Debug, Default)]
struct Recorder {
dispatches: AtomicU64,
seen: std::sync::Mutex<Vec<String>>,
protocols: std::sync::Mutex<Vec<Option<String>>>,
}
impl Recorder {
fn seen(&self) -> Vec<String> {
self.seen
.lock()
.map(|seen| seen.clone())
.unwrap_or_default()
}
fn protocols(&self) -> Vec<Option<String>> {
self.protocols
.lock()
.map(|versions| versions.clone())
.unwrap_or_default()
}
fn dispatches(&self) -> u64 {
self.dispatches.load(Ordering::SeqCst)
}
}
struct Echo {
recorder: Arc<Recorder>,
establish: bool,
stateless: bool,
}
impl RequestHandler<EventTail<Value>> for Echo {
fn unsessioned(&self, _body: &[u8], _headers: &axum::http::HeaderMap) -> Unsessioned {
if self.establish {
Unsessioned::Establishing
} else if self.stateless {
Unsessioned::Stateless
} else {
Unsessioned::NeedsSession
}
}
fn dispatch(
&self,
call: Inbound<'_, EventTail<Value>>,
) -> impl core::future::Future<Output = Result<axum::response::Response, Refusal>> + Send {
let recorder = Arc::clone(&self.recorder);
let name = match &call.session {
SessionHandle::Establishing => "establishing".to_owned(),
SessionHandle::Stateless => "stateless".to_owned(),
SessionHandle::Live { id, .. } => id.to_string(),
};
let protocol = call.protocol.map(|version| version.to_string());
async move {
recorder.dispatches.fetch_add(1, Ordering::SeqCst);
if let Ok(mut seen) = recorder.seen.lock() {
seen.push(name);
}
if let Ok(mut versions) = recorder.protocols.lock() {
versions.push(protocol);
}
Ok(json_response(StatusCode::OK, &json!({ "ok": true })))
}
}
}
struct OneToken;
impl Authenticator for OneToken {
fn authenticate<'a>(
&'a self,
presented: &'a Credentials<'_>,
) -> Pin<Box<dyn Future<Output = AuthContext> + Send + 'a>> {
let context = if presented.authorization() == Some("Bearer good") {
AuthContext::new("holder", ScopeSet::all())
} else {
AuthContext::anonymous()
};
Box::pin(std::future::ready(context))
}
}
fn json_response(status: StatusCode, value: &Value) -> axum::response::Response {
let body = serde_json::to_vec(value).unwrap_or_else(|_| b"{}".to_vec());
let mut response = axum::response::Response::new(Body::from(body));
*response.status_mut() = status;
response.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
);
response
}
struct Harness {
router: Router,
sessions: Arc<SessionStore<EventTail<Value>>>,
recorder: Arc<Recorder>,
}
impl Harness {
fn build(establish: bool) -> Result<Self, TestError> {
Self::with(establish, AuthPolicy::Public, None)
}
fn with(
establish: bool,
policy: AuthPolicy,
authenticator: Option<Arc<dyn Authenticator + Send + Sync>>,
) -> Result<Self, TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
Self::from_parts(
ServerConfig::new()?,
sessions,
establish,
policy,
authenticator,
)
}
fn from_parts(
config: ServerConfig,
sessions: Arc<SessionStore<EventTail<Value>>>,
establish: bool,
policy: AuthPolicy,
authenticator: Option<Arc<dyn Authenticator + Send + Sync>>,
) -> Result<Self, TestError> {
Self::from_shape(config, sessions, establish, false, policy, authenticator)
}
fn stateless(
config: ServerConfig,
sessions: Arc<SessionStore<EventTail<Value>>>,
) -> Result<Self, TestError> {
Self::from_shape(config, sessions, false, true, AuthPolicy::default(), None)
}
fn from_shape(
config: ServerConfig,
sessions: Arc<SessionStore<EventTail<Value>>>,
establish: bool,
stateless: bool,
policy: AuthPolicy,
authenticator: Option<Arc<dyn Authenticator + Send + Sync>>,
) -> Result<Self, TestError> {
let recorder = Arc::new(Recorder::default());
let mut router = McpRouter::new(
config,
Arc::clone(&sessions),
Echo {
recorder: Arc::clone(&recorder),
establish,
stateless,
},
)
.with_policy(policy);
if let Some(authenticator) = authenticator {
router = router.with_authenticator(authenticator);
}
let router = router.into_router()?;
Ok(Self {
router,
sessions,
recorder,
})
}
fn open(&self) -> Result<McpSessionId, TestError> {
Ok(self.sessions.open(tail()?)?)
}
async fn send(&self, request: Request<Body>) -> Result<axum::response::Response, TestError> {
Ok(self.router.clone().oneshot(request).await?)
}
}
const HOST: &str = "localhost:3000";
fn request(method: &str, uri: &str) -> Result<Request<Body>, TestError> {
Ok(Request::builder()
.method(method)
.uri(uri)
.header(header::HOST, HOST)
.body(Body::empty())?)
}
fn post(body: &str) -> Result<Request<Body>, TestError> {
Ok(Request::builder()
.method("POST")
.uri("/mcp")
.header(header::CONTENT_TYPE, "application/json")
.header(header::HOST, HOST)
.body(Body::from(body.to_owned()))?)
}
fn get() -> Result<Request<Body>, TestError> {
request("GET", "/mcp")
}
fn delete() -> Result<Request<Body>, TestError> {
request("DELETE", "/mcp")
}
fn with_session(mut request: Request<Body>, id: McpSessionId) -> Result<Request<Body>, TestError> {
let value = header::HeaderValue::from_str(&id.to_string())?;
request.headers_mut().insert("mcp-session-id", value);
Ok(request)
}
fn headers_with(id: McpSessionId) -> Result<HeaderMap, TestError> {
let mut headers = HeaderMap::new();
headers.insert(
"mcp-session-id",
header::HeaderValue::from_str(&id.to_string())?,
);
Ok(headers)
}
fn wrong_token() -> Result<Request<Body>, TestError> {
Ok(Request::builder()
.method("POST")
.uri("/mcp")
.header(header::CONTENT_TYPE, "application/json")
.header(header::AUTHORIZATION, "Bearer wrong")
.header(header::HOST, HOST)
.body(Body::from("{}"))?)
}
fn right_token() -> Result<Request<Body>, TestError> {
Ok(Request::builder()
.method("POST")
.uri("/mcp")
.header(header::CONTENT_TYPE, "application/json")
.header(header::AUTHORIZATION, "Bearer good")
.header(header::HOST, HOST)
.body(Body::from("{}"))?)
}
#[test]
fn no_refusal_is_ever_a_success_status() {
let every = [
Refusal::MalformedSessionId,
Refusal::MissingSession,
Refusal::UnknownSession,
Refusal::MethodNotAllowed {
method: "PUT",
reason: "not part of the streamable surface",
},
Refusal::ProtocolHeader {
reason: "the header disagreed with the session".to_owned(),
},
Refusal::BodyTooLarge,
Refusal::Unauthenticated,
Refusal::Forbidden,
Refusal::MalformedBody {
reason: "not json".to_owned(),
},
Refusal::StreamGap {
requested: EventId::new(0),
evicted_through: EventId::new(1),
last: EventId::new(4),
},
Refusal::UnknownCursor {
requested: EventId::new(9),
last: EventId::new(4),
},
Refusal::MalformedCursor,
Refusal::Handler("boom".to_owned()),
];
for refusal in &every {
assert!(
!refusal.status().is_success(),
"{refusal:?} is answered with a success status"
);
}
}
#[tokio::test]
async fn auth_is_refused_before_the_session_store_is_touched() -> Result<(), TestError> {
let harness = Harness::with(
true,
AuthPolicy::Require(ScopeSet::all()),
Some(Arc::new(OneToken)),
)?;
let id = harness.open()?;
let response = harness.send(with_session(post("{}")?, id)?).await?;
if response.status() != StatusCode::UNAUTHORIZED {
return Err(fail("an unauthenticated caller was not refused"));
}
if harness.recorder.dispatches() != 0 {
return Err(fail("the handler ran for an unauthenticated caller"));
}
Ok(())
}
#[tokio::test]
async fn a_credential_the_policy_accepts_reaches_the_handler() -> Result<(), TestError> {
let harness = Harness::with(
true,
AuthPolicy::Require(ScopeSet::all()),
Some(Arc::new(OneToken)),
)?;
let id = harness.open()?;
let request = with_session(right_token()?, id)?;
let response = harness.send(request).await?;
if response.status() != StatusCode::OK {
return Err(fail("a valid credential was refused"));
}
if harness.recorder.dispatches() != 1 {
return Err(fail("the handler did not run for an accepted credential"));
}
Ok(())
}
#[tokio::test]
async fn a_wrong_credential_is_refused_exactly_as_a_missing_one() -> Result<(), TestError> {
let harness = Harness::with(
true,
AuthPolicy::Require(ScopeSet::all()),
Some(Arc::new(OneToken)),
)?;
let id = harness.open()?;
let missing = harness.send(with_session(post("{}")?, id)?).await?;
let wrong = harness.send(with_session(wrong_token()?, id)?).await?;
if missing.status() != wrong.status() {
return Err(fail(
"a wrong credential is answered differently from a missing one",
));
}
if harness.recorder.dispatches() != 0 {
return Err(fail("the handler ran for a refused credential"));
}
Ok(())
}
#[test]
fn an_explicit_authenticator_with_the_default_public_policy_is_refused() -> Result<(), TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let error = McpRouter::new(ServerConfig::new()?, sessions, dummy_echo())
.with_authenticator(Arc::new(OneToken))
.into_router();
match error {
Err(super::RouterError::UnprotectedAuthenticator) => Ok(()),
Err(other) => Err(fail(format!(
"expected UnprotectedAuthenticator, got {other:?}"
))),
Ok(_) => Err(fail("the unsafe combination was accepted")),
}
}
#[test]
fn an_explicit_authenticator_with_a_require_policy_mounts() -> Result<(), TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let _router = McpRouter::new(ServerConfig::new()?, sessions, dummy_echo())
.with_authenticator(Arc::new(OneToken))
.with_policy(AuthPolicy::Require(ScopeSet::all()))
.into_router()?;
Ok(())
}
#[test]
fn the_default_router_mounts_under_the_default_public_policy() -> Result<(), TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let _router = McpRouter::new(ServerConfig::new()?, sessions, dummy_echo()).into_router()?;
Ok(())
}
fn dummy_echo() -> Echo {
Echo {
recorder: Arc::new(Recorder::default()),
establish: false,
stateless: true,
}
}
#[tokio::test]
async fn a_refusal_still_carries_the_security_headers() -> Result<(), TestError> {
let harness = Harness::build(true)?;
let response = harness.send(get()?).await?;
if response.status().is_success() {
return Err(fail("a request that should have been refused succeeded"));
}
for name in [
"content-security-policy",
"strict-transport-security",
"x-frame-options",
] {
if !response.headers().contains_key(name) {
return Err(fail(format!("a refusal was answered without {name}")));
}
}
Ok(())
}
#[tokio::test]
async fn the_security_headers_reach_responses_no_handler_wrote() -> Result<(), TestError> {
let harness = Harness::build(true)?;
let unmapped_method = harness.send(request("PUT", "/mcp")?).await?;
if !unmapped_method
.headers()
.contains_key("content-security-policy")
{
return Err(fail("a 405 was answered without the security headers"));
}
let unmapped_path = harness.send(request("POST", "/not-mcp")?).await?;
if !unmapped_path
.headers()
.contains_key("content-security-policy")
{
return Err(fail("a 404 was answered without the security headers"));
}
Ok(())
}
#[tokio::test]
async fn the_security_header_values_are_the_configured_ones() -> Result<(), TestError> {
let config =
ServerConfig::new()?.with_security(SecurityHeaders::default().with_x_frame_options("DENY"));
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let harness = Harness::from_parts(config, sessions, true, AuthPolicy::default(), None)?;
let response = harness.send(post("{}")?).await?;
let frame = response
.headers()
.get("x-frame-options")
.and_then(|value| value.to_str().ok())
.ok_or("x-frame-options was absent")?;
if frame != "DENY" {
return Err(fail(format!("x-frame-options was {frame:?}, not DENY")));
}
Ok(())
}
#[tokio::test]
async fn a_session_id_the_store_never_issued_is_a_404_and_opens_nothing() -> Result<(), TestError> {
let harness = Harness::build(false)?;
let forged = McpSessionId::from(SessionId::new(9_999));
let response = harness.send(with_session(post("{}")?, forged)?).await?;
if response.status() != StatusCode::NOT_FOUND {
return Err(fail("an unissued session id was not refused with 404"));
}
if harness.recorder.dispatches() != 0 {
return Err(fail("a forged session id reached the handler"));
}
Ok(())
}
#[tokio::test]
async fn a_malformed_session_id_is_a_400() -> Result<(), TestError> {
let harness = Harness::build(false)?;
let request = Request::builder()
.method("POST")
.uri("/mcp")
.header("mcp-session-id", "not a session id")
.header(header::CONTENT_TYPE, "application/json")
.header(header::HOST, HOST)
.body(Body::from("{}"))?;
if harness.send(request).await?.status() != StatusCode::BAD_REQUEST {
return Err(fail("a malformed session id was not refused with 400"));
}
Ok(())
}
#[tokio::test]
async fn a_missing_session_id_on_a_stateful_request_is_a_400() -> Result<(), TestError> {
let harness = Harness::build(false)?;
let response = harness.send(post("{}")?).await?;
if response.status() != StatusCode::BAD_REQUEST {
return Err(fail(
"a stateful request with no session id was not refused",
));
}
if harness.recorder.dispatches() != 0 {
return Err(fail("a request with no session reached the handler"));
}
Ok(())
}
#[tokio::test]
async fn a_body_that_opens_a_session_needs_no_session_header() -> Result<(), TestError> {
let harness = Harness::build(true)?;
let response = harness.send(post("{}")?).await?;
if response.status() != StatusCode::OK {
return Err(fail("a session-establishing body was refused"));
}
if harness.recorder.seen() != vec!["establishing".to_owned()] {
return Err(fail(format!(
"the handler saw {:?}",
harness.recorder.seen()
)));
}
Ok(())
}
#[tokio::test]
async fn the_handler_is_given_the_id_the_store_issued() -> Result<(), TestError> {
let harness = Harness::build(false)?;
let id = harness.open()?;
let expected = id.to_string();
let response = harness.send(with_session(post("{}")?, id)?).await?;
if response.status() != StatusCode::OK {
return Err(fail("a request naming a real session was refused"));
}
if harness.recorder.seen() != vec![expected] {
return Err(fail(format!(
"the handler saw {:?}",
harness.recorder.seen()
)));
}
Ok(())
}
#[tokio::test]
async fn a_post_on_a_live_session_reaches_the_handler() -> Result<(), TestError> {
let harness = Harness::build(false)?;
let id = harness.open()?;
let response = harness.send(with_session(post("{\"a\":1}")?, id)?).await?;
if response.status() != StatusCode::OK {
return Err(fail("a POST on a live session did not succeed"));
}
Ok(())
}
#[tokio::test]
async fn a_get_opens_an_event_stream() -> Result<(), TestError> {
let harness = Harness::build(false)?;
let id = harness.open()?;
let response = harness.send(with_session(get()?, id)?).await?;
if response.status() != StatusCode::OK {
return Err(fail("GET did not open a stream"));
}
let headers = response.headers();
let content_type = headers
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.ok_or("no content type")?;
if !content_type.starts_with("text/event-stream") {
return Err(fail(format!("content type was {content_type:?}")));
}
for name in ["cache-control", "x-accel-buffering"] {
if !headers.contains_key(name) {
return Err(fail(format!("a stream was opened without {name}")));
}
}
Ok(())
}
#[tokio::test]
async fn a_get_on_an_unknown_session_is_a_404() -> Result<(), TestError> {
let harness = Harness::build(false)?;
let forged = McpSessionId::from(SessionId::new(9_999));
let response = harness.send(with_session(get()?, forged)?).await?;
if response.status() != StatusCode::NOT_FOUND {
return Err(fail("GET on an unknown session was not refused"));
}
Ok(())
}
#[tokio::test]
async fn a_get_with_no_session_header_is_a_400() -> Result<(), TestError> {
let harness = Harness::build(false)?;
if harness.send(get()?).await?.status() != StatusCode::BAD_REQUEST {
return Err(fail("GET with no session id was not refused"));
}
Ok(())
}
#[tokio::test]
async fn a_get_is_refused_without_a_credential() -> Result<(), TestError> {
let harness = Harness::with(
false,
AuthPolicy::Require(ScopeSet::all()),
Some(Arc::new(OneToken)),
)?;
let id = harness.open()?;
let response = harness.send(with_session(get()?, id)?).await?;
if response.status() != StatusCode::UNAUTHORIZED {
return Err(fail("GET opened a stream for an unauthenticated caller"));
}
Ok(())
}
#[tokio::test]
async fn a_delete_terminates_the_session() -> Result<(), TestError> {
let harness = Harness::build(false)?;
let id = harness.open()?;
let first = harness.send(with_session(delete()?, id)?).await?;
if first.status().is_server_error() {
return Err(fail("DELETE failed"));
}
if harness.sessions.lookup(&id)?.is_some() {
return Err(fail("the session survived a DELETE"));
}
let second = harness.send(with_session(delete()?, id)?).await?;
if second.status() != StatusCode::NOT_FOUND {
return Err(fail("a repeated DELETE did not become a 404"));
}
Ok(())
}
#[tokio::test]
async fn a_delete_on_an_unknown_session_is_a_404() -> Result<(), TestError> {
let harness = Harness::build(false)?;
let forged = McpSessionId::from(SessionId::new(9_999));
if harness
.send(with_session(delete()?, forged)?)
.await?
.status()
!= StatusCode::NOT_FOUND
{
return Err(fail("DELETE on an unknown session was not refused"));
}
Ok(())
}
#[tokio::test]
async fn a_get_forwards_last_event_id_into_the_replay() -> Result<(), TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let tail = tail()?;
for n in 1..=3 {
tail.push(json!({ "n": n }));
}
let id = sessions.open(Arc::clone(&tail))?;
let harness = Harness::from_parts(
ServerConfig::new()?,
sessions,
false,
AuthPolicy::default(),
None,
)?;
let mut quoted = with_session(get()?, id)?;
quoted
.headers_mut()
.insert("last-event-id", header::HeaderValue::from_static("1"));
let body = replayed_body(harness.send(quoted).await?).await?;
let text = String::from_utf8_lossy(&body);
if !text.contains("\"n\":2") || !text.contains("\"n\":3") {
return Err(fail(format!(
"a resume from 1 did not replay the events after it: {text}"
)));
}
if text.contains("\"n\":1") {
return Err(fail(format!(
"a resume from 1 replayed event 1, which the client already has: {text}"
)));
}
Ok(())
}
#[tokio::test]
async fn a_get_with_no_cursor_replays_everything() -> Result<(), TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let tail = tail()?;
for n in 1..=3 {
tail.push(json!({ "n": n }));
}
let id = sessions.open(Arc::clone(&tail))?;
let harness = Harness::from_parts(
ServerConfig::new()?,
sessions,
false,
AuthPolicy::default(),
None,
)?;
let body = replayed_body(harness.send(with_session(get()?, id)?).await?).await?;
let text = String::from_utf8_lossy(&body);
for n in 1..=3 {
if !text.contains(&format!("\"n\":{n}")) {
return Err(fail(format!("event {n} was not replayed: {text}")));
}
}
Ok(())
}
#[tokio::test]
async fn an_unreadable_last_event_id_is_refused_rather_than_treated_as_absent()
-> Result<(), TestError> {
let harness = Harness::build(false)?;
let id = harness.open()?;
let mut request = with_session(get()?, id)?;
request
.headers_mut()
.insert("last-event-id", header::HeaderValue::from_bytes(&[0xff])?);
if harness.send(request).await?.status() != StatusCode::BAD_REQUEST {
return Err(fail("an unreadable Last-Event-ID was not refused"));
}
Ok(())
}
#[tokio::test]
async fn an_unknown_session_is_a_401_and_not_a_404_without_a_credential() -> Result<(), TestError> {
let harness = Harness::with(
true,
AuthPolicy::Require(ScopeSet::all()),
Some(Arc::new(OneToken)),
)?;
let forged = McpSessionId::from(SessionId::new(9_999));
let response = harness.send(with_session(post("{}")?, forged)?).await?;
if response.status() != StatusCode::UNAUTHORIZED {
return Err(fail(
"the store was consulted before the gate: an unauthenticated caller \
was told the difference between a live session and a dead one",
));
}
if harness.recorder.dispatches() != 0 {
return Err(fail("the handler ran for an unauthenticated caller"));
}
Ok(())
}
async fn replayed_body(response: axum::response::Response) -> Result<Vec<u8>, TestError> {
use core::time::Duration;
use futures_util::StreamExt as _;
let mut stream = response.into_body().into_data_stream();
let mut collected = Vec::new();
while let Ok(Some(Ok(chunk))) =
tokio::time::timeout(Duration::from_millis(250), stream.next()).await
{
collected.extend_from_slice(&chunk);
}
Ok(collected)
}
#[tokio::test]
async fn the_streaming_mode_never_changes_the_get_route() -> Result<(), TestError> {
for mode in [Streaming::Json, Streaming::EventStream] {
let config = ServerConfig::new()?.with_streaming(mode);
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let harness = Harness::from_parts(config, sessions, false, AuthPolicy::default(), None)?;
let id = harness.open()?;
let response = harness.send(with_session(get()?, id)?).await?;
let content_type = response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.ok_or("no content type")?;
if !content_type.starts_with("text/event-stream") {
return Err(fail(format!(
"GET in {mode:?} mode answered {content_type:?} rather than a stream"
)));
}
}
Ok(())
}
#[tokio::test]
async fn an_over_limit_body_is_a_refusal_rather_than_a_bare_413() -> Result<(), TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let harness = Harness::from_parts(
ServerConfig::new()?,
sessions,
true,
AuthPolicy::default(),
None,
)?;
let huge = "x".repeat(8 * 1024 * 1024);
let response = harness.send(post(&huge)?).await?;
if response.status() != StatusCode::PAYLOAD_TOO_LARGE {
return Err(fail("an over-limit body was not refused with 413"));
}
let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024).await?;
let text = String::from_utf8_lossy(&bytes);
if !text.contains("error") {
return Err(fail(format!("a 413 carried no JSON-RPC error: {text}")));
}
Ok(())
}
#[tokio::test]
async fn the_gate_and_the_lookup_are_independently_usable() -> Result<(), TestError> {
let authenticator = OneToken;
let policy = AuthPolicy::Require(ScopeSet::all());
let mut headers = HeaderMap::new();
if authenticate(&authenticator, &policy, &headers)
.await
.is_ok()
{
return Err(fail("an anonymous caller passed a required policy"));
}
headers.insert(
header::AUTHORIZATION,
header::HeaderValue::from_static("Bearer good"),
);
if authenticate(&authenticator, &policy, &headers)
.await
.is_err()
{
return Err(fail("a valid credential was refused by the gate"));
}
let sessions = SessionStore::new(Arc::new(Counting(AtomicU64::new(0))));
let issued = sessions.open(tail()?)?;
let found = resolve_session(&sessions, &headers_with(issued)?)?;
let SessionHandle::Live { id, .. } = found else {
return Err(fail("a live session id did not resolve to a live session"));
};
if id != issued {
return Err(fail(
"the lookup returned a different id than the one asked for",
));
}
let forged = McpSessionId::from(SessionId::new(9_999));
if resolve_session(&sessions, &headers_with(forged)?).is_ok() {
return Err(fail("a forged session id resolved"));
}
if resolve_session(&sessions, &HeaderMap::new()).is_ok() {
return Err(fail("a request with no session header resolved"));
}
Ok(())
}
fn post_at_version(version: ProtocolVersion) -> Result<Request<Body>, TestError> {
let mut request = post("{}")?;
let value = header::HeaderValue::from_str(&version.to_string())?;
request.headers_mut().insert("mcp-protocol-version", value);
Ok(request)
}
#[tokio::test]
async fn the_version_header_reaches_the_handler_parsed() -> Result<(), TestError> {
let harness = Harness::build(true)?;
for version in [
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2026_07_28,
] {
let _ = harness.send(post_at_version(version)?).await?;
}
let seen = harness.recorder.protocols();
let expected: Vec<Option<String>> = [
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2026_07_28,
]
.iter()
.map(|version| Some(version.to_string()))
.collect();
require(
&seen,
&expected,
"the handler was not handed the parsed header",
)?;
Ok(())
}
#[tokio::test]
async fn a_version_header_this_build_cannot_read_is_a_four_hundred() -> Result<(), TestError> {
let harness = Harness::build(true)?;
let mut request = post("{}")?;
request.headers_mut().insert(
"mcp-protocol-version",
header::HeaderValue::from_static("not-a-date"),
);
let response = harness.send(request).await?;
require(
&response.status(),
&StatusCode::BAD_REQUEST,
"a malformed version header was not refused",
)?;
require(
&harness.recorder.dispatches(),
&0,
"a request with a malformed version header was still dispatched",
)?;
Ok(())
}
#[tokio::test]
async fn a_version_header_this_build_does_not_know_yet_is_carried_through() -> Result<(), TestError>
{
let future = "2099-01-01".parse::<ProtocolVersion>()?;
let harness = Harness::build(true)?;
let _ = harness.send(post_at_version(future)?).await?;
let seen = harness.recorder.protocols();
let expected = vec![Some(future.to_string())];
require(
&seen,
&expected,
"a future revision was refused rather than carried",
)?;
Ok(())
}
#[tokio::test]
async fn a_request_with_no_version_header_is_not_refused() -> Result<(), TestError> {
let harness = Harness::build(true)?;
let _ = harness.send(post("{}")?).await?;
let expected = vec![None];
require(
&harness.recorder.protocols(),
&expected,
"an absent version header was not carried as absent",
)?;
Ok(())
}
#[tokio::test]
async fn a_stateless_request_is_served_without_a_session() -> Result<(), TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let harness = Harness::stateless(ServerConfig::new()?, Arc::clone(&sessions))?;
let _ = harness
.send(post_at_version(ProtocolVersion::V2026_07_28)?)
.await?;
let expected = vec![String::from("stateless")];
require(
&harness.recorder.seen(),
&expected,
"a stateless request was not served as one",
)?;
let stored = sessions.len()?;
require(
&stored,
&0,
"a stateless request minted a session it does not have",
)?;
Ok(())
}
#[tokio::test]
async fn two_stateless_requests_are_gated_independently() -> Result<(), TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let harness = Harness::stateless(ServerConfig::new()?, Arc::clone(&sessions))?;
for version in [
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2026_07_28,
ProtocolVersion::V2025_03_26,
] {
let _ = harness.send(post_at_version(version)?).await?;
}
let seen = harness.recorder.protocols();
let expected: Vec<Option<String>> = [
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2026_07_28,
ProtocolVersion::V2025_03_26,
]
.iter()
.map(|version| Some(version.to_string()))
.collect();
require(
&seen,
&expected,
"a stateless request was gated by another's header",
)?;
let expected = vec![String::from("stateless"); 3];
require(
&harness.recorder.seen(),
&expected,
"a stateless request was not served as one",
)?;
Ok(())
}
#[tokio::test]
async fn a_request_needing_a_session_and_naming_none_is_a_four_hundred() -> Result<(), TestError> {
let harness = Harness::build(false)?;
let response = harness.send(post("{}")?).await?;
require(
&response.status(),
&StatusCode::BAD_REQUEST,
"a request needing a session was not refused",
)?;
require(
&harness.recorder.dispatches(),
&0,
"a refused request was still dispatched",
)?;
Ok(())
}
const RESOURCE: &str = "https://mcp.example/mcp";
fn publishing() -> Result<ServerConfig, TestError> {
Ok(ServerConfig::new()?.with_resource_metadata(document()?))
}
fn document() -> Result<ResourceMetadata, TestError> {
Ok(ResourceMetadata::new(ResourceUri::new(RESOURCE)?))
}
fn challenge_of(response: &axum::response::Response) -> Option<String> {
response
.headers()
.get(header::WWW_AUTHENTICATE)
.and_then(|value| value.to_str().ok())
.map(str::to_owned)
}
#[tokio::test]
async fn a_401_carries_the_resource_metadata_challenge() -> Result<(), TestError> {
let harness = Harness::from_parts(
publishing()?,
Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0))))),
true,
AuthPolicy::Require(ScopeSet::all()),
Some(Arc::new(OneToken)),
)?;
let id = harness.open()?;
let response = harness.send(with_session(post("{}")?, id)?).await?;
require(&response.status(), &StatusCode::UNAUTHORIZED, "status")?;
let challenge = challenge_of(&response)
.ok_or_else(|| fail("a 401 carried no WWW-Authenticate challenge"))?;
require(
&challenge,
&"Bearer resource_metadata=\"https://mcp.example/.well-known/oauth-protected-resource/mcp\""
.to_owned(),
"challenge",
)
}
#[tokio::test]
async fn a_403_carries_no_challenge() -> Result<(), TestError> {
let harness = Harness::from_parts(
publishing()?,
Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0))))),
true,
AuthPolicy::Require(ScopeSet::empty().with(Scope::Admin)),
Some(Arc::new(ReadOnly)),
)?;
let id = harness.open()?;
let mut request = with_session(post("{}")?, id)?;
request.headers_mut().insert(
header::AUTHORIZATION,
header::HeaderValue::from_static("Bearer good"),
);
let response = harness.send(request).await?;
require(&response.status(), &StatusCode::FORBIDDEN, "status")?;
require(
&challenge_of(&response).is_none(),
&true,
"a 403 carried a WWW-Authenticate challenge",
)
}
#[tokio::test]
async fn a_401_without_published_metadata_is_unadorned() -> Result<(), TestError> {
let harness = Harness::with(
true,
AuthPolicy::Require(ScopeSet::all()),
Some(Arc::new(OneToken)),
)?;
let id = harness.open()?;
let response = harness.send(with_session(post("{}")?, id)?).await?;
require(&response.status(), &StatusCode::UNAUTHORIZED, "status")?;
require(
&challenge_of(&response).is_none(),
&true,
"a 401 from a server publishing no metadata carried a challenge",
)
}
struct ReadOnly;
impl Authenticator for ReadOnly {
fn authenticate<'a>(
&'a self,
presented: &'a Credentials<'_>,
) -> Pin<Box<dyn Future<Output = AuthContext> + Send + 'a>> {
let context = if presented.authorization() == Some("Bearer good") {
AuthContext::new("holder", ScopeSet::empty().with(Scope::Read))
} else {
AuthContext::anonymous()
};
Box::pin(std::future::ready(context))
}
}
fn hardened(origin: OriginPolicy, limiter: RateLimiter) -> Result<Router, TestError> {
let sessions = Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0)))));
let recorder = Arc::new(Recorder::default());
let config = ServerConfig::new()?;
Ok(McpRouter::new(
config,
sessions,
Echo {
recorder,
establish: true,
stateless: false,
},
)
.with_origin_policy(origin)
.with_rate_limiter(limiter)
.into_router()?)
}
fn limiter(limit: u32) -> Result<RateLimiter, TestError> {
Ok(RateLimiter::new(RateLimit::new(
NonZeroU32::new(limit).ok_or("a fixture asked for a zero limit")?,
Duration::from_secs(3600),
)))
}
fn origin(raw: &str) -> Result<OriginPolicy, TestError> {
Ok(OriginPolicy::strict(Some(raw.parse()?)))
}
const INITIALIZE: &str = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
fn post_with(body: &str, headers: &[(&str, &str)]) -> Result<Request<Body>, TestError> {
let mut builder = Request::builder()
.method("POST")
.uri("/mcp")
.header(header::CONTENT_TYPE, "application/json")
.header(header::HOST, HOST);
for (name, value) in headers {
builder = builder.header(*name, *value);
}
Ok(builder.body(Body::from((*body).to_owned()))?)
}
#[tokio::test]
async fn a_foreign_origin_is_refused_before_the_handler() -> Result<(), TestError> {
let router = hardened(origin("http://localhost:3000")?, limiter(10)?)?;
let response = router
.clone()
.oneshot(post_with(INITIALIZE, &[("origin", "http://evil.example")])?)
.await?;
assert_eq!(response.status(), StatusCode::FORBIDDEN);
Ok(())
}
#[tokio::test]
async fn a_request_with_no_origin_is_answered() -> Result<(), TestError> {
let router = hardened(origin("http://localhost:3000")?, limiter(10)?)?;
let response = router.clone().oneshot(post(INITIALIZE)?).await?;
assert_ne!(
response.status(),
StatusCode::FORBIDDEN,
"an absent Origin is a non-browser client, not a rebinding page"
);
Ok(())
}
#[tokio::test]
async fn a_foreign_host_is_refused_before_the_handler() -> Result<(), TestError> {
let router = hardened(origin("http://localhost:3000")?, limiter(10)?)?;
let mut request = post(INITIALIZE)?;
request
.headers_mut()
.insert(header::HOST, "evil.example.com".parse()?);
let response = router.oneshot(request).await?;
assert_eq!(
response.status(),
StatusCode::FORBIDDEN,
"a request for another name was answered"
);
Ok(())
}
#[tokio::test]
async fn a_loopback_origin_is_answered_on_the_wire() -> Result<(), TestError> {
let router = hardened(origin("http://localhost:3000")?, limiter(10)?)?;
for origin in [
"http://localhost:3000",
"http://127.0.0.1:3000",
"http://[::1]:3000",
] {
let response = router
.clone()
.oneshot(post_with(INITIALIZE, &[("origin", origin)])?)
.await?;
assert_ne!(
response.status(),
StatusCode::FORBIDDEN,
"{origin} was refused"
);
}
Ok(())
}
#[tokio::test]
async fn the_permitted_origin_is_answered() -> Result<(), TestError> {
let router = hardened(origin("http://localhost:3000")?, limiter(10)?)?;
let response = router
.clone()
.oneshot(post_with(
INITIALIZE,
&[("origin", "http://localhost:3000")],
)?)
.await?;
assert_ne!(response.status(), StatusCode::FORBIDDEN);
Ok(())
}
#[tokio::test]
async fn exceeding_the_rate_limit_is_a_429_with_retry_after() -> Result<(), TestError> {
let router = hardened(OriginPolicy::strict(None), limiter(1)?)?;
let first = router.clone().oneshot(post(INITIALIZE)?).await?;
assert_ne!(first.status(), StatusCode::TOO_MANY_REQUESTS);
let second = router.oneshot(post(INITIALIZE)?).await?;
assert_eq!(second.status(), StatusCode::TOO_MANY_REQUESTS);
let retry_after = second
.headers()
.get("retry-after")
.and_then(|value| value.to_str().ok())
.ok_or("a 429 without Retry-After tells the client to guess")?;
assert!(
retry_after.parse::<u64>().is_ok_and(|seconds| seconds > 0),
"Retry-After was {retry_after:?}; zero means retry now"
);
Ok(())
}
#[tokio::test]
async fn one_session_does_not_spend_anothers_allowance() -> Result<(), TestError> {
let router = hardened(OriginPolicy::strict(None), limiter(1)?)?;
let _ = router
.clone()
.oneshot(post_with(INITIALIZE, &[("mcp-session-id", "session-a")])?)
.await?;
let blocked = router
.clone()
.oneshot(post_with(INITIALIZE, &[("mcp-session-id", "session-a")])?)
.await?;
assert_eq!(blocked.status(), StatusCode::TOO_MANY_REQUESTS);
let other = router
.oneshot(post_with(INITIALIZE, &[("mcp-session-id", "session-b")])?)
.await?;
assert_ne!(
other.status(),
StatusCode::TOO_MANY_REQUESTS,
"session-b was charged for session-a's requests"
);
Ok(())
}
#[tokio::test]
async fn an_oversized_body_is_still_a_413() -> Result<(), TestError> {
let router = hardened(OriginPolicy::strict(None), limiter(100)?)?;
let huge = "x".repeat(8 * 1024 * 1024);
let response = router
.oneshot(post(&format!(
"{{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"{huge}\"}}"
))?)
.await?;
assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
Ok(())
}
fn challenged_path(challenge: &str) -> Result<String, TestError> {
let url = challenge
.rsplit_once("resource_metadata=\"")
.and_then(|(_, rest)| rest.split_once('"'))
.map(|(url, _)| url)
.ok_or_else(|| {
fail(format!(
"challenge carried no resource_metadata: {challenge}"
))
})?;
let path = url
.split_once("://")
.and_then(|(_, rest)| rest.find('/').map(|at| &rest[at..]))
.ok_or_else(|| fail(format!("challenge URL {url} has no path")))?;
Ok(path.to_owned())
}
#[tokio::test]
async fn the_challenge_resolves_to_a_document_this_server_serves() -> Result<(), TestError> {
let harness = Harness::from_parts(
publishing()?,
Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0))))),
true,
AuthPolicy::Require(ScopeSet::all()),
Some(Arc::new(OneToken)),
)?;
let id = harness.open()?;
let refused = harness.send(with_session(post("{}")?, id)?).await?;
require(&refused.status(), &StatusCode::UNAUTHORIZED, "status")?;
let challenge = challenge_of(&refused)
.ok_or_else(|| fail("a 401 carried no WWW-Authenticate challenge"))?;
let path = challenged_path(&challenge)?;
let served = Request::builder()
.uri(&path)
.header(header::HOST, HOST)
.body(Body::empty())
.map_err(|e| fail(format!("building GET {path}: {e}")))?;
let response = harness.router.clone().oneshot(served).await?;
require(&response.status(), &StatusCode::OK, "metadata status")?;
let content_type = response
.headers()
.get(header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok())
.map(str::to_owned);
require(
&content_type,
&Some("application/json".to_owned()),
"metadata content type",
)?;
let body = axum::body::to_bytes(response.into_body(), 64 * 1024).await?;
let document: Value = serde_json::from_slice(&body)?;
require(
&document.get("resource").cloned(),
&Some(json!(RESOURCE)),
"document resource",
)?;
Ok(())
}
#[tokio::test]
async fn the_discovery_document_needs_no_credential_and_is_read_only() -> Result<(), TestError> {
let harness = Harness::from_parts(
publishing()?,
Arc::new(SessionStore::new(Arc::new(Counting(AtomicU64::new(0))))),
true,
AuthPolicy::Require(ScopeSet::all()),
Some(Arc::new(OneToken)),
)?;
let path = document()?.resource().metadata_path();
let get = || -> Result<Request<Body>, TestError> {
Request::builder()
.uri(&path)
.header(header::HOST, HOST)
.body(Body::empty())
.map_err(|e| fail(format!("building GET {path}: {e}")))
};
let served = harness.router.clone().oneshot(get()?).await?;
require(&served.status(), &StatusCode::OK, "anonymous GET status")?;
let mut post = get()?;
*post.method_mut() = axum::http::Method::POST;
let refused = harness.router.clone().oneshot(post).await?;
require(
&refused.status(),
&StatusCode::METHOD_NOT_ALLOWED,
"POST status",
)?;
Ok(())
}
#[tokio::test]
async fn no_configured_document_means_no_well_known_route() -> Result<(), TestError> {
let harness = Harness::build(true)?;
let request = Request::builder()
.uri("/.well-known/oauth-protected-resource/mcp")
.header(header::HOST, HOST)
.body(Body::empty())
.map_err(|e| fail(format!("building GET: {e}")))?;
let response = harness.router.clone().oneshot(request).await?;
require(&response.status(), &StatusCode::NOT_FOUND, "status")?;
Ok(())
}