#![cfg(feature = "http")]
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use axum::Router;
use axum::body::Body;
use axum::extract::State;
use axum::response::Response;
use axum::routing::post;
use serde_json::{Value, json};
use tokio::sync::Notify;
use protocol_droid::adapters::primary::transport::http::{
HttpBridge, SecurityHeaders, ServerConfig, SessionStore,
};
use protocol_droid::adapters::secondary::codec::result::WireToolResults;
use protocol_droid::adapters::secondary::transport::http::{HttpClient, HttpClientConfig};
use protocol_droid::application::bridge::{
BridgePeer, Identity, OpenedUpstream, Upstream, UpstreamError,
};
use protocol_droid::application::client::{ClientConfig, handshake};
use protocol_droid::application::session::IdSource;
use protocol_droid::domain::session::SessionId;
use protocol_droid::domain::{DegradePolicy, Leg, SupportedVersions, TransportKind};
struct Counting(std::sync::atomic::AtomicU64);
impl IdSource for Counting {
fn next_session_id(&self) -> SessionId {
SessionId::new(self.0.fetch_add(1, Ordering::SeqCst) + 1)
}
}
const UPSTREAM_SESSION: &str = "0f0e0d0c0b0a0908";
const PATIENCE: Duration = Duration::from_secs(5);
async fn wait_for_open(state: &Server) -> bool {
let deadline = tokio::time::Instant::now() + PATIENCE;
loop {
if state.opened.load(Ordering::SeqCst) > 0 {
return true;
}
if tokio::time::Instant::now() >= deadline {
return false;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
struct Server {
opened: AtomicUsize,
release: Option<Arc<Notify>>,
}
async fn upstream_post(body: axum::body::Bytes) -> Response {
let Ok(Value::Object(request)) = serde_json::from_slice::<Value>(&body) else {
return Response::builder()
.status(400)
.body(Body::empty())
.unwrap_or_default();
};
let id = request.get("id").cloned().unwrap_or(Value::Null);
let result = match request.get("method").and_then(Value::as_str) {
Some("initialize") => json!({
"protocolVersion": "2025-11-25",
"capabilities": {"tools": {}},
"serverInfo": {"name": "far", "version": "1"}
}),
Some("tools/list") => {
json!({"tools": [{"name": "echo", "inputSchema": {"type": "object"}}]})
}
_ => Value::Null,
};
let reply = json!({"jsonrpc": "2.0", "id": id, "result": result});
Response::builder()
.header("content-type", "text/event-stream")
.header("mcp-session-id", UPSTREAM_SESSION)
.body(Body::from(format!("event: message\ndata: {reply}\n\n")))
.unwrap_or_default()
}
async fn upstream_get(State(state): axum::extract::State<Arc<Server>>) -> Response {
state.opened.fetch_add(1, Ordering::SeqCst);
if let Some(release) = &state.release {
release.notified().await;
}
let notice = json!({
"jsonrpc": "2.0",
"method": "notifications/message",
"params": {"level": "info", "data": "pushed while you were quiet"}
});
let frames = format!("event: message\ndata: {notice}\n\n");
Response::builder()
.header("content-type", "text/event-stream")
.body(Body::from(frames))
.unwrap_or_default()
}
async fn spawn_upstream(state: Arc<Server>) -> Result<String, Box<dyn std::error::Error>> {
let app = Router::new().route(
"/mcp",
post(upstream_post)
.get(upstream_get)
.with_state(Arc::clone(&state)),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;
tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
Ok(format!("http://{addr}/mcp"))
}
struct HttpUpstream {
url: String,
}
impl Upstream for HttpUpstream {
async fn open(&self) -> Result<OpenedUpstream, UpstreamError> {
let config = HttpClientConfig::new(&self.url).map_err(wire)?;
let client = HttpClient::new(config).map_err(wire)?;
let versions = SupportedVersions::all().map_err(wire)?;
let speaking =
ClientConfig::speaking(versions.as_slice().to_vec(), TransportKind::StreamableHttp)
.map_err(wire)?;
let session = handshake(&client, &speaking).await.map_err(wire)?;
let leg = Leg::new(session.version, session.profile);
Ok(OpenedUpstream::new(
leg,
Arc::new(client) as Arc<dyn BridgePeer>,
))
}
}
fn wire(error: impl std::fmt::Display) -> UpstreamError {
let text = error.to_string();
UpstreamError::Unreachable(text)
}
async fn running_bridge(state: Arc<Server>) -> Result<String, Box<dyn std::error::Error>> {
let url = spawn_upstream(Arc::clone(&state)).await?;
let ids: Arc<dyn IdSource + Send + Sync> =
Arc::new(Counting(std::sync::atomic::AtomicU64::new(0)));
let sessions: Arc<SessionStore<_>> = Arc::new(SessionStore::new(ids));
let server = ServerConfig::new()?.with_security(SecurityHeaders::default());
let bridge = HttpBridge::new(
server,
sessions,
HttpUpstream { url },
Identity::new("push-test", "0.0.0"),
DegradePolicy::default(),
WireToolResults,
)?;
let router = bridge.into_router()?;
let listener = tokio::net::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"))
}
struct Reading {
response: reqwest::Response,
pending: String,
}
impl Reading {
async fn open(
client: &reqwest::Client,
url: &str,
session: &str,
) -> Result<Self, Box<dyn std::error::Error>> {
let response = client
.get(url)
.header("accept", "text/event-stream")
.header("mcp-session-id", session)
.send()
.await?;
if !response.status().is_success() {
return Err(format!("the stream answered {}", response.status()).into());
}
Ok(Self {
response,
pending: String::new(),
})
}
async fn await_frame(
&mut self,
needle: &str,
budget: Duration,
) -> Result<Option<Vec<String>>, Box<dyn std::error::Error>> {
let deadline = tokio::time::Instant::now() + budget;
let mut seen: Vec<String> = Vec::new();
loop {
let arrived = tokio::time::timeout_at(deadline, self.response.chunk()).await;
let Ok(Ok(Some(bytes))) = arrived else {
return Ok(if seen.iter().any(|f| f.contains(needle)) {
Some(seen)
} else {
None
});
};
self.pending.push_str(&String::from_utf8_lossy(&bytes));
let ready: Vec<String> = self
.pending
.lines()
.filter_map(|line| line.strip_prefix("data:"))
.map(|data| data.trim().to_owned())
.collect();
if !ready.is_empty() {
seen.extend(ready);
let cut = self.pending.rfind('\n').map_or(0, |at| at + 1);
self.pending = self.pending.split_off(cut);
}
}
}
}
fn initialize(id: i64) -> String {
json!({
"jsonrpc": "2.0",
"id": id,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {"name": "t", "version": "1"}
}
})
.to_string()
}
async fn connected(
url: &str,
) -> Result<(reqwest::Client, String, Reading), Box<dyn std::error::Error>> {
let client = reqwest::Client::new();
let opened = client
.post(url)
.header("content-type", "application/json")
.header("accept", "application/json, text/event-stream")
.body(initialize(1))
.send()
.await?;
if !opened.status().is_success() {
return Err(format!("the handshake answered {}", opened.status()).into());
}
let session = opened
.headers()
.get("mcp-session-id")
.and_then(|value| value.to_str().ok())
.ok_or("no session id came back")?
.to_owned();
let reading = Reading::open(&client, url, &session).await?;
Ok((client, session, reading))
}
#[tokio::test]
async fn a_push_reaches_a_client_that_has_said_nothing() -> Result<(), Box<dyn std::error::Error>> {
let release = Arc::new(Notify::new());
let state = Arc::new(Server {
opened: AtomicUsize::new(0),
release: Some(Arc::clone(&release)),
});
let url = running_bridge(Arc::clone(&state)).await?;
let (_client, _session, mut reading) = connected(&url).await?;
release.notify_one();
if !wait_for_open(&state).await {
return Err("the bridge never opened the upstream's standing stream".into());
}
let _ = reading
.await_frame("notifications/message", PATIENCE)
.await?
.ok_or("no frame carried it")?;
Ok(())
}
#[tokio::test]
async fn a_push_that_lands_before_the_client_reads_is_not_lost()
-> Result<(), Box<dyn std::error::Error>> {
let state = Arc::new(Server {
opened: AtomicUsize::new(0),
release: None,
});
let url = running_bridge(Arc::clone(&state)).await?;
let client = reqwest::Client::new();
let opened = client
.post(&url)
.header("content-type", "application/json")
.header("accept", "application/json, text/event-stream")
.body(initialize(1))
.send()
.await?;
let session = opened
.headers()
.get("mcp-session-id")
.and_then(|value| value.to_str().ok())
.ok_or("no session id came back")?
.to_owned();
tokio::time::sleep(Duration::from_millis(150)).await;
if !wait_for_open(&state).await {
return Err("the bridge never opened the upstream's standing stream".into());
}
let mut reading = Reading::open(&client, &url, &session).await?;
reading
.await_frame("notifications/message", PATIENCE)
.await?
.ok_or("no frame carried the notification")?;
Ok(())
}
#[tokio::test]
async fn a_request_still_crosses_to_the_upstream() -> Result<(), Box<dyn std::error::Error>> {
let state = Arc::new(Server {
opened: AtomicUsize::new(0),
release: None,
});
let url = running_bridge(Arc::clone(&state)).await?;
let (client, session, mut reading) = connected(&url).await?;
client
.post(&url)
.header("content-type", "application/json")
.header("accept", "application/json, text/event-stream")
.header("mcp-session-id", &session)
.body(json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}).to_string())
.send()
.await?;
reading
.await_frame("\"echo\"", PATIENCE)
.await?
.ok_or("no frame carried the tool list")?;
Ok(())
}