use std::convert::Infallible;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use axum::Router;
use axum::body::{Body, Bytes};
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode, header};
use axum::response::{IntoResponse, Response};
use axum::routing::post;
use serde_json::{Value, json};
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use crate::application::client::Client;
use crate::application::client::handler::{
ConnectOptions, HandlerRefusal, ServerRequestHandler, apply_elicitation_defaults,
};
use crate::domain::ir::JsonObject;
use crate::domain::{Capability, CapabilitySet, SupportedVersions, TransportKind, TransportSpec};
type TestResult = Result<(), Box<dyn std::error::Error>>;
const SESSION: &str = "session-1";
#[derive(Default)]
struct Script {
call_id: Mutex<Option<Value>>,
resumed_with: Mutex<Vec<String>>,
resumed_at: Mutex<Option<Instant>>,
closed_at: Mutex<Option<Instant>>,
stream: Mutex<Option<mpsc::UnboundedSender<Bytes>>>,
answered: Mutex<Option<Value>>,
}
fn json_response(body: &Value) -> Response {
(
[
(header::CONTENT_TYPE, "application/json"),
(header::HeaderName::from_static("mcp-session-id"), SESSION),
],
body.to_string(),
)
.into_response()
}
fn sse(body: String) -> Response {
(
[
(header::CONTENT_TYPE, "text/event-stream"),
(header::HeaderName::from_static("mcp-session-id"), SESSION),
],
body,
)
.into_response()
}
fn common(script: &Script, request: &Value) -> Option<Response> {
let id = request.get("id").cloned();
match request.get("method").and_then(Value::as_str) {
Some("initialize") => Some(json_response(&json!({
"jsonrpc": "2.0", "id": id,
"result": {
"protocolVersion": "2025-11-25",
"serverInfo": { "name": "script", "version": "1" },
"capabilities": { "tools": {} },
},
}))),
Some(method) if method.starts_with("notifications/") => {
Some(StatusCode::ACCEPTED.into_response())
}
Some("tools/call") => {
*script
.call_id
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = id;
None
}
_ if request.get("result").is_some() || request.get("error").is_some() => {
*script
.answered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(request.clone());
Some(StatusCode::ACCEPTED.into_response())
}
_ => Some(json_response(
&json!({ "jsonrpc": "2.0", "id": id, "result": {} }),
)),
}
}
async fn spawn(router: Router) -> Result<String, Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
tokio::spawn(async move {
let _ = axum::serve(listener, router).await;
});
Ok(format!("http://{addr}/mcp"))
}
fn spec(url: String) -> Result<TransportSpec, Box<dyn std::error::Error>> {
Ok(TransportSpec::new(TransportKind::StreamableHttp, url)?)
}
fn versions() -> Result<SupportedVersions, Box<dyn std::error::Error>> {
Ok(SupportedVersions::from_slice(&[
crate::domain::ProtocolVersion::V2025_11_25,
])?)
}
async fn closes_early(State(script): State<Arc<Script>>, body: String) -> Response {
let request: Value = serde_json::from_str(&body).unwrap_or_default();
if let Some(response) = common(&script, &request) {
return response;
}
*script
.closed_at
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Instant::now());
sse("id: event-1\nretry: 200\ndata: \n\n".to_owned())
}
async fn resumed(State(script): State<Arc<Script>>, headers: HeaderMap) -> Response {
*script
.resumed_at
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Instant::now());
if let Some(id) = headers.get("last-event-id").and_then(|v| v.to_str().ok()) {
script
.resumed_with
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(id.to_owned());
}
let id = script
.call_id
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let result = json!({
"jsonrpc": "2.0", "id": id,
"result": { "content": [{ "type": "text", "text": "resumed" }] },
});
sse(format!("id: event-2\nevent: message\ndata: {result}\n\n"))
}
#[tokio::test]
async fn a_stream_closed_early_is_resumed_after_the_retry_interval() -> TestResult {
let script = Arc::new(Script::default());
let router = Router::new()
.route("/mcp", post(closes_early).get(resumed))
.with_state(Arc::clone(&script));
let url = spawn(router).await?;
let client = Client::connect_with(&spec(url)?, &versions()?, ConnectOptions::new()).await?;
let answered = client.call_tool("t", &JsonObject::default()).await?;
assert!(
answered.as_str().contains("resumed"),
"{}",
answered.as_str()
);
let resumed_with = script
.resumed_with
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
assert_eq!(
resumed_with,
["event-1"],
"the last id is sent back verbatim"
);
let closed = script
.closed_at
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.ok_or("never closed")?;
let came_back = script
.resumed_at
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.ok_or("never resumed")?;
assert!(
came_back.duration_since(closed) + Duration::from_millis(50) >= Duration::from_millis(200),
"the client came back after {:?}, before the 200 ms the server asked for",
came_back.duration_since(closed)
);
Ok(())
}
async fn asks(State(script): State<Arc<Script>>, body: String) -> Response {
let request: Value = serde_json::from_str(&body).unwrap_or_default();
if let Some(response) = common(&script, &request) {
if request.get("result").is_some() {
let id = script
.call_id
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let tx = script
.stream
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if let Some(tx) = tx {
let done = json!({ "jsonrpc": "2.0", "id": id, "result": { "content": [{ "type": "text", "text": "done" }] } });
let _ = tx.send(Bytes::from(format!("event: message\ndata: {done}\n\n")));
}
}
return response;
}
let (tx, rx) = mpsc::unbounded_channel::<Bytes>();
let question = json!({
"jsonrpc": "2.0", "id": 99, "method": "elicitation/create",
"params": {
"message": "Details?",
"requestedSchema": { "type": "object", "properties": {
"name": { "type": "string", "default": "John Doe" },
"age": { "type": "integer", "default": 30 },
}},
},
});
let _ = tx.send(Bytes::from(format!("event: message\ndata: {question}\n\n")));
*script
.stream
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(tx);
let stream = futures_util::stream::unfold(rx, |mut rx| async move {
rx.recv()
.await
.map(|bytes| (Ok::<Bytes, Infallible>(bytes), rx))
});
(
[
(header::CONTENT_TYPE, "text/event-stream"),
(header::HeaderName::from_static("mcp-session-id"), SESSION),
],
Body::from_stream(stream),
)
.into_response()
}
#[derive(Debug)]
struct EmptyAnswers;
impl ServerRequestHandler for EmptyAnswers {
fn handle<'a>(
&'a self,
method: &'a str,
params: &'a JsonObject,
) -> Pin<Box<dyn Future<Output = Result<JsonObject, HandlerRefusal>> + Send + 'a>> {
Box::pin(async move {
if method != "elicitation/create" {
return Err(HandlerRefusal::unsupported(method));
}
let accepted =
JsonObject::new(r#"{"action":"accept","content":{}}"#).unwrap_or_default();
Ok(apply_elicitation_defaults(params, &accepted))
})
}
}
#[tokio::test]
async fn a_request_the_server_sends_mid_call_is_answered_with_defaults_applied() -> TestResult {
let script = Arc::new(Script::default());
let router = Router::new()
.route("/mcp", post(asks))
.with_state(Arc::clone(&script));
let url = spawn(router).await?;
let client = Client::connect_with(
&spec(url)?,
&versions()?,
ConnectOptions::new()
.declaring(CapabilitySet::empty().with(Capability::Elicitation))
.answering(Arc::new(EmptyAnswers)),
)
.await?;
let answered = tokio::time::timeout(
Duration::from_secs(5),
client.call_tool("t", &JsonObject::default()),
)
.await
.map_err(|_| "the call hung waiting for an answer nobody gave")??;
assert!(answered.as_str().contains("done"), "{}", answered.as_str());
let reply = script
.answered
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.ok_or("no reply")?;
assert_eq!(
reply.get("id"),
Some(&json!(99)),
"the reply carries the question's id"
);
assert_eq!(
reply.pointer("/result/content/name"),
Some(&json!("John Doe"))
);
assert_eq!(reply.pointer("/result/content/age"), Some(&json!(30)));
Ok(())
}