use std::future::Future;
use std::io;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::task::{ready, Context, Poll};
use rmcp::model::{
ClientJsonRpcMessage, ClientRequest, DiscoverResult, ErrorData as McpError, GetMeta as _,
ProtocolVersion, RequestId, ServerJsonRpcMessage, ServerResult,
};
use rmcp::transport::async_rw::AsyncRwTransport;
use rmcp::transport::Transport;
use rmcp::{RoleServer, ServerHandler as _};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use crate::server::{BugWarden, SUPPORTED_PROTOCOL_VERSIONS};
#[derive(Debug)]
pub struct BoundedLines<R> {
inner: R,
cap: usize,
since_newline: usize,
over_cap: Arc<AtomicBool>,
}
impl<R> BoundedLines<R> {
pub fn new(inner: R, cap: usize) -> Self {
Self {
inner,
cap,
since_newline: 0,
over_cap: Arc::new(AtomicBool::new(false)),
}
}
pub fn cap(&self) -> usize {
self.cap
}
pub fn over_cap(&self) -> Arc<AtomicBool> {
self.over_cap.clone()
}
fn refusal(&self) -> io::Error {
io::Error::new(
io::ErrorKind::InvalidData,
format!("stdio frame exceeds the {}-byte request cap", self.cap),
)
}
fn trip(&mut self) -> io::Error {
self.over_cap.store(true, Ordering::Release);
tracing::warn!(
"stdio frame exceeds the {}-byte request cap; closing the transport",
self.cap
);
self.refusal()
}
}
impl<R: AsyncRead + Unpin> AsyncRead for BoundedLines<R> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let this = self.get_mut();
if this.over_cap.load(Ordering::Acquire) {
return Poll::Ready(Err(this.refusal()));
}
let before = buf.filled().len();
ready!(Pin::new(&mut this.inner).poll_read(cx, buf))?;
let mut rest = &buf.filled()[before..];
while let Some(newline) = rest.iter().position(|&byte| byte == b'\n') {
if this.since_newline.saturating_add(newline) > this.cap {
return Poll::Ready(Err(this.trip()));
}
this.since_newline = 0;
rest = &rest[newline + 1..];
}
this.since_newline = this.since_newline.saturating_add(rest.len());
if this.since_newline > this.cap {
return Poll::Ready(Err(this.trip()));
}
Poll::Ready(Ok(()))
}
}
type QueuedSend<E> = Pin<Box<dyn Future<Output = Result<(), E>> + Send>>;
pub struct DiscoverAnswering<T: Transport<RoleServer>> {
inner: T,
server: BugWarden,
pending: Option<QueuedSend<T::Error>>,
probed: Arc<AtomicBool>,
}
impl<T: Transport<RoleServer>> DiscoverAnswering<T> {
pub fn new(inner: T, server: BugWarden) -> Self {
Self {
inner,
server,
pending: None,
probed: Arc::new(AtomicBool::new(false)),
}
}
pub fn answered(&self) -> Arc<AtomicBool> {
self.probed.clone()
}
fn discover_reply(&self, request: &ClientRequest, id: RequestId) -> ServerJsonRpcMessage {
let meta = request.get_meta();
let missing = meta.missing_required_keys(&ProtocolVersion::V_2026_07_28);
if !missing.is_empty() {
return ServerJsonRpcMessage::error(
McpError::invalid_params(
format!(
"request _meta is missing or has malformed required fields: {}",
missing.join(", ")
),
None,
),
Some(id),
);
}
if let Some(requested) = meta
.protocol_version()
.filter(|version| !SUPPORTED_PROTOCOL_VERSIONS.contains(version))
{
return ServerJsonRpcMessage::error(
McpError::unsupported_protocol_version(requested, SUPPORTED_PROTOCOL_VERSIONS),
Some(id),
);
}
ServerJsonRpcMessage::response(
ServerResult::DiscoverResult(DiscoverResult::from_server_info(
SUPPORTED_PROTOCOL_VERSIONS.to_vec(),
self.server.get_info(),
)),
id,
)
}
}
impl<R, W> DiscoverAnswering<AsyncRwTransport<RoleServer, R, W>>
where
R: AsyncRead + Send + Unpin,
W: AsyncWrite + Send + Unpin + 'static,
{
pub fn framed(read: R, write: W, server: BugWarden) -> Self {
Self::new(AsyncRwTransport::new_server(read, write), server)
}
}
impl<T: Transport<RoleServer>> Transport<RoleServer> for DiscoverAnswering<T> {
type Error = T::Error;
fn send(
&mut self,
item: ServerJsonRpcMessage,
) -> impl Future<Output = Result<(), Self::Error>> + Send + 'static {
self.inner.send(item)
}
async fn receive(&mut self) -> Option<ClientJsonRpcMessage> {
loop {
if let Some(pending) = self.pending.as_mut() {
let sent = pending.await;
self.pending = None;
sent.ok()?;
self.probed.store(true, Ordering::Release);
}
match self.inner.receive().await? {
ClientJsonRpcMessage::Request(request)
if matches!(request.request, ClientRequest::DiscoverRequest(_)) =>
{
let reply = self.discover_reply(&request.request, request.id);
self.pending = Some(Box::pin(self.inner.send(reply)));
}
other => return Some(other),
}
}
}
async fn close(&mut self) -> Result<(), Self::Error> {
if let Some(pending) = self.pending.take() {
let _ = pending.await;
}
self.inner.close().await
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use rmcp::transport::async_rw::AsyncRwTransport;
use rmcp::transport::Transport as _;
use rmcp::RoleServer;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _, DuplexStream};
use super::BoundedLines;
async fn drain(bytes: &[u8], cap: usize) -> std::io::Result<()> {
let mut reader = BoundedLines::new(bytes, cap);
let mut sink = [0u8; 512];
while reader.read(&mut sink).await? != 0 {}
Ok(())
}
fn duplex(cap: usize) -> (DuplexStream, BoundedLines<DuplexStream>) {
let (peer, ours) = tokio::io::duplex(64 * 1024);
(peer, BoundedLines::new(ours, cap))
}
#[tokio::test]
async fn a_frame_of_exactly_the_cap_is_accepted() {
drain(b"0123456789abcdef\n", 16)
.await
.expect("16 bytes is the cap, not past it");
}
#[tokio::test]
async fn a_frame_of_exactly_the_cap_waits_for_its_delimiter() {
let (mut peer, mut reader) = duplex(16);
let mut buf = [0u8; 512];
peer.write_all(b"0123456789abcdef")
.await
.expect("duplex write");
assert_eq!(reader.read(&mut buf).await.expect("exactly the cap"), 16);
peer.write_all(b"\n").await.expect("duplex write");
assert_eq!(reader.read(&mut buf).await.expect("the delimiter"), 1);
}
#[tokio::test]
async fn one_byte_past_the_cap_is_refused() {
let err = drain(b"0123456789abcdefg\n", 16)
.await
.expect_err("17 bytes is past a 16-byte cap");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
assert!(
err.to_string().contains("16-byte request cap"),
"the refusal must name the cap: {err}"
);
}
#[tokio::test]
async fn the_count_is_per_frame_and_not_a_running_total() {
let (mut peer, mut reader) = duplex(16);
let mut buf = [0u8; 512];
peer.write_all(b"0123456789").await.expect("duplex write");
assert_eq!(reader.read(&mut buf).await.expect("10 bytes"), 10);
peer.write_all(b"ab\ncdef").await.expect("duplex write");
assert_eq!(reader.read(&mut buf).await.expect("12 then 4"), 7);
peer.write_all(b"ghijkl\n").await.expect("duplex write");
assert_eq!(reader.read(&mut buf).await.expect("10 bytes, not 20"), 7);
}
#[tokio::test]
async fn a_frame_that_ends_inside_the_tripping_chunk_is_still_refused() {
let err = drain(b"0123456789abcdefg\nshort\n", 16)
.await
.expect_err("the delimiter does not excuse the frame before it");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[tokio::test]
async fn an_over_cap_second_frame_inside_one_chunk_is_refused() {
let err = drain(b"short\n0123456789abcdefg\n", 16)
.await
.expect_err("the second frame of the chunk is over the cap");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[tokio::test]
async fn several_under_cap_frames_in_one_chunk_are_accepted() {
drain(b"aaaaaaa\nbbbbbbb\nccccccc\nddddddd\n", 16)
.await
.expect("every frame is half the cap");
}
#[tokio::test]
async fn the_count_survives_a_cancelled_read() {
let (mut peer, mut reader) = duplex(16);
let mut buf = [0u8; 512];
peer.write_all(b"0123456789").await.expect("duplex write");
assert_eq!(reader.read(&mut buf).await.expect("first chunk"), 10);
assert!(
tokio::time::timeout(Duration::from_millis(50), reader.read(&mut buf))
.await
.is_err(),
"an empty duplex must leave the read pending"
);
peer.write_all(b"0123456789").await.expect("duplex write");
let err = reader
.read(&mut buf)
.await
.expect_err("20 bytes of one frame is past a 16-byte cap");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[tokio::test]
async fn the_refusal_is_sticky() {
let (mut peer, mut reader) = duplex(16);
let mut buf = [0u8; 512];
peer.write_all(b"0123456789abcdefg\n")
.await
.expect("duplex write");
reader.read(&mut buf).await.expect_err("over the cap");
peer.write_all(b"{}\n").await.expect("duplex write");
let err = reader
.read(&mut buf)
.await
.expect_err("a tripped reader stays failed");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[tokio::test]
async fn an_under_cap_stream_reaches_the_reader_unchanged() {
let payload = b"{\"a\":1}\r\n\n{\"b\":2}\ntrailing";
let mut reader = BoundedLines::new(&payload[..], 16);
let mut read = Vec::new();
reader.read_to_end(&mut read).await.expect("under the cap");
assert_eq!(read, payload);
}
#[tokio::test]
async fn rmcp_turns_the_refusal_into_a_closed_transport() {
let (mut peer, reader) = duplex(16);
let mut transport =
AsyncRwTransport::<RoleServer, _, _>::new_server(reader, tokio::io::sink());
peer.write_all(b"0123456789abcdefghij\n")
.await
.expect("duplex write");
let received = tokio::time::timeout(Duration::from_secs(5), transport.receive())
.await
.expect("the transport must not block on a frame it will never accept");
assert!(
received.is_none(),
"an over-cap frame must close the transport, not yield a message"
);
}
}
#[cfg(test)]
mod discover {
use std::sync::Arc;
use std::time::Duration;
use bugwarden_core::guard::Guard;
use bugwarden_core::policy::Policy;
use rmcp::model::{
ClientJsonRpcMessage, ClientRequest, DiscoverResult, ProtocolVersion, ServerJsonRpcMessage,
};
use rmcp::transport::Transport as _;
use rmcp::{RoleServer, ServerHandler as _, ServiceExt as _};
use serde_json::{json, Value};
use tokio::io::{
AsyncBufReadExt as _, AsyncWriteExt as _, BufReader, DuplexStream, ReadHalf, WriteHalf,
};
use super::DiscoverAnswering;
use crate::pinned_cli::pinned;
use crate::server::{bugzilla_client, BugWarden, SUPPORTED_PROTOCOL_VERSIONS};
const REPLY_BUDGET: Duration = Duration::from_secs(10);
const SERVED: &str = "2026-07-28";
const UNSERVED: &str = "2027-01-01";
const PIPE: usize = 8 * 1024;
fn test_server() -> BugWarden {
let cfg = Arc::new(pinned(&[
"bugwarden",
"--bugzilla-server",
"https://bugzilla.example.invalid",
"--transport",
"stdio",
"--api-key",
"test-key",
]));
let guard = Arc::new(Guard {
policy: Policy::from_toml_str("").expect("the empty policy must parse"),
});
let bz = Arc::new(bugzilla_client(&cfg).expect("client must build"));
BugWarden::new(cfg, guard, bz).expect("server must build")
}
fn meta(version: &str) -> Value {
json!({
"io.modelcontextprotocol/protocolVersion": version,
"io.modelcontextprotocol/clientCapabilities": {},
})
}
fn discover(id: u32, meta: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "method": "server/discover",
"params": { "_meta": meta } })
}
fn ping(id: u32) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "method": "ping" })
}
struct Session {
write: WriteHalf<DuplexStream>,
lines: tokio::io::Lines<BufReader<ReadHalf<DuplexStream>>>,
}
impl Session {
fn open() -> Self {
Self::serving(true, 256 * 1024)
}
fn open_bare() -> Self {
Self::serving(false, 256 * 1024)
}
fn open_narrow() -> Self {
Self::serving(true, PIPE)
}
fn serving(wrapped: bool, capacity: usize) -> Self {
let (theirs, ours) = tokio::io::duplex(capacity);
let (read, write) = tokio::io::split(ours);
let server = test_server();
tokio::spawn(async move {
let service = if wrapped {
let transport = DiscoverAnswering::framed(read, write, server.clone());
server.serve(transport).await?
} else {
server.serve((read, write)).await?
};
service.waiting().await?;
Ok::<(), anyhow::Error>(())
});
let (read, write) = tokio::io::split(theirs);
Session {
write,
lines: BufReader::new(read).lines(),
}
}
async fn send(&mut self, message: Value) {
self.write
.write_all(format!("{message}\n").as_bytes())
.await
.expect("the session must accept input");
}
async fn burst(&mut self, frames: &[Value]) {
let mut buffer = String::new();
for frame in frames {
buffer.push_str(&format!("{frame}\n"));
}
self.write
.write_all(buffer.as_bytes())
.await
.expect("the session must accept input");
}
async fn recv(&mut self) -> Value {
let line = tokio::time::timeout(REPLY_BUDGET, self.lines.next_line())
.await
.expect("the server must answer within the budget")
.expect("the session must be readable")
.expect("the server must not close before answering");
serde_json::from_str(&line).unwrap_or_else(|e| panic!("{line:?}: {e}"))
}
async fn call(&mut self, message: Value) -> Value {
self.send(message).await;
self.recv().await
}
async fn initialize(&mut self, id: u32) -> Value {
let reply = self
.call(json!({
"jsonrpc": "2.0", "id": id, "method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "discover-test", "version": "0" },
}
}))
.await;
self.send(json!({ "jsonrpc": "2.0", "method": "notifications/initialized" }))
.await;
reply
}
}
fn tools_of(reply: &Value) -> &Vec<Value> {
reply["result"]["tools"]
.as_array()
.unwrap_or_else(|| panic!("a served listing carries a tools array: {reply}"))
}
fn error_code(reply: &Value) -> i64 {
reply["error"]["code"]
.as_i64()
.unwrap_or_else(|| panic!("a refusal carries an error code: {reply}"))
}
const MISSING_META: &str = "request _meta is missing or has malformed required fields: ";
#[tokio::test]
async fn the_reporter_chain_reaches_the_tool_list() {
let mut session = Session::open();
let probe = session.call(discover(1, meta(UNSERVED))).await;
assert_eq!(error_code(&probe), -32022, "{probe}");
let init = session.initialize(2).await;
assert_eq!(init["result"]["protocolVersion"], "2025-11-25", "{init}");
let listed = session
.call(json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {} }))
.await;
assert!(!tools_of(&listed).is_empty(), "{listed}");
}
#[tokio::test]
async fn a_served_probe_then_a_session_reaches_the_tool_list() {
let mut session = Session::open();
let probe = session.call(discover(1, meta(SERVED))).await;
assert!(probe["result"]["supportedVersions"].is_array(), "{probe}");
session.initialize(2).await;
let listed = session
.call(json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {} }))
.await;
assert!(!tools_of(&listed).is_empty(), "{listed}");
}
#[tokio::test]
async fn a_probe_with_no_meta_is_refused_and_the_session_survives() {
let mut session = Session::open();
let probe = session
.call(
json!({ "jsonrpc": "2.0", "id": 1, "method": "server/discover",
"params": {} }),
)
.await;
assert_eq!(error_code(&probe), -32602, "{probe}");
assert_eq!(
probe["error"]["message"],
json!(format!(
"{MISSING_META}io.modelcontextprotocol/protocolVersion, \
io.modelcontextprotocol/clientCapabilities"
)),
"{probe}"
);
let init = session.initialize(2).await;
assert_eq!(init["result"]["protocolVersion"], "2025-11-25", "{init}");
let listed = session
.call(json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {} }))
.await;
assert!(!tools_of(&listed).is_empty(), "{listed}");
}
#[tokio::test]
async fn a_probe_mid_session_is_answered_and_changes_nothing() {
let mut session = Session::open();
session.initialize(1).await;
let probe = session.call(discover(2, meta(SERVED))).await;
assert_eq!(probe["result"]["resultType"], "complete", "{probe}");
let listed = session
.call(json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {} }))
.await;
assert!(!tools_of(&listed).is_empty(), "{listed}");
}
#[tokio::test]
async fn a_pipelined_probe_is_answered_under_write_contention() {
const FRAMES: u32 = 40;
let mut session = Session::open_narrow();
session.initialize(1).await;
let frames: Vec<Value> = (2..=FRAMES + 1)
.map(|id| {
if id % 2 == 0 {
discover(id, meta(SERVED))
} else {
ping(id)
}
})
.collect();
session.burst(&frames).await;
let mut answered = std::collections::HashSet::new();
for _ in 0..FRAMES {
let reply = session.recv().await;
answered.insert(
reply["id"]
.as_u64()
.unwrap_or_else(|| panic!("every reply names its request: {reply}")),
);
}
let unanswered: Vec<u64> = (2..=u64::from(FRAMES) + 1)
.filter(|id| !answered.contains(id))
.collect();
assert!(unanswered.is_empty(), "unanswered ids: {unanswered:?}");
}
#[tokio::test]
async fn the_per_request_lifecycle_is_still_rmcps_to_choose() {
let mut session = Session::open();
let probe = session.call(discover(1, meta(SERVED))).await;
assert!(probe["result"]["supportedVersions"].is_array(), "{probe}");
let declared = session
.call(json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list",
"params": { "_meta": meta(SERVED) } }))
.await;
assert!(!tools_of(&declared).is_empty(), "{declared}");
let bare = session
.call(json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {} }))
.await;
assert_eq!(error_code(&bare), -32602, "{bare}");
assert!(
bare["error"]["message"]
.as_str()
.is_some_and(|message| message.starts_with(MISSING_META)),
"{bare}"
);
}
#[tokio::test]
async fn a_ping_before_initialize_is_still_answered_by_rmcp() {
let mut session = Session::open();
let pong = session.call(ping(1)).await;
assert_eq!(pong["result"], json!({}), "{pong}");
let probe = session.call(discover(2, meta(SERVED))).await;
assert_eq!(probe["result"]["resultType"], "complete", "{probe}");
let init = session.initialize(3).await;
assert_eq!(init["result"]["protocolVersion"], "2025-11-25", "{init}");
}
#[tokio::test]
async fn a_malformed_meta_names_only_the_key_that_is_wrong() {
let mut session = Session::open();
let probe = session
.call(discover(
1,
json!({
"io.modelcontextprotocol/protocolVersion": 7,
"io.modelcontextprotocol/clientCapabilities": {},
}),
))
.await;
assert_eq!(error_code(&probe), -32602, "{probe}");
assert_eq!(
probe["error"]["message"],
json!(format!(
"{MISSING_META}io.modelcontextprotocol/protocolVersion"
)),
"{probe}"
);
}
#[tokio::test]
async fn an_in_session_probe_wrong_two_ways_is_refused_on_the_metadata() {
let mut session = Session::open();
session.initialize(1).await;
let probe = session
.call(discover(
2,
json!({ "io.modelcontextprotocol/protocolVersion": UNSERVED }),
))
.await;
assert_eq!(error_code(&probe), -32602, "{probe}");
assert_eq!(
probe["error"]["message"],
json!(format!(
"{MISSING_META}io.modelcontextprotocol/clientCapabilities"
)),
"{probe}"
);
}
#[tokio::test]
async fn the_answer_is_the_sdk_default_discover_result() {
let mut session = Session::open();
let probe = session.call(discover(1, meta(SERVED))).await;
let expected = serde_json::to_value(DiscoverResult::from_server_info(
SUPPORTED_PROTOCOL_VERSIONS.to_vec(),
test_server().get_info(),
))
.expect("the SDK result must serialize");
assert_eq!(probe["result"], expected, "{probe}");
assert_eq!(
probe["result"]["_meta"]["io.modelcontextprotocol/serverInfo"],
json!({ "name": "bugwarden", "version": env!("CARGO_PKG_VERSION") }),
"the probe must name this build, and nothing else: {probe}"
);
assert_eq!(probe["result"]["resultType"], "complete", "{probe}");
}
const UNSERVED_WITHOUT_CAPABILITIES: &str = "an unserved revision, no capabilities";
fn probe_shapes() -> Vec<(&'static str, Value)> {
vec![
("both keys, a served revision", meta(SERVED)),
("both keys, a legacy revision", meta("2025-11-25")),
("both keys, the oldest served revision", meta("2024-11-05")),
("both keys, an unserved revision", meta(UNSERVED)),
(
"the Go SDK shape, clientInfo included",
json!({
"io.modelcontextprotocol/protocolVersion": SERVED,
"io.modelcontextprotocol/clientCapabilities": {},
"io.modelcontextprotocol/clientInfo": { "name": "agy", "version": "1.7.0" },
}),
),
("an empty _meta", json!({})),
(
"a served revision, no capabilities",
json!({ "io.modelcontextprotocol/protocolVersion": SERVED }),
),
(
"capabilities, no revision",
json!({ "io.modelcontextprotocol/clientCapabilities": {} }),
),
(
UNSERVED_WITHOUT_CAPABILITIES,
json!({ "io.modelcontextprotocol/protocolVersion": UNSERVED }),
),
(
"a revision that is not a string",
json!({
"io.modelcontextprotocol/protocolVersion": 7,
"io.modelcontextprotocol/clientCapabilities": {},
}),
),
(
"capabilities that are not an object",
json!({
"io.modelcontextprotocol/protocolVersion": SERVED,
"io.modelcontextprotocol/clientCapabilities": "yes",
}),
),
]
}
async fn probe_reply(mut session: Session, in_session: bool, meta: Value) -> Value {
let id = if in_session {
session.initialize(1).await;
2
} else {
1
};
session.call(discover(id, meta)).await
}
#[tokio::test]
async fn the_wrapper_answers_what_rmcp_answered() {
for (name, meta) in probe_shapes() {
for in_session in [false, true] {
let bare = probe_reply(Session::open_bare(), in_session, meta.clone()).await;
let wrapped = probe_reply(Session::open(), in_session, meta.clone()).await;
if in_session && name == UNSERVED_WITHOUT_CAPABILITIES {
assert_eq!(error_code(&bare), -32022, "{name}: {bare}");
assert_eq!(error_code(&wrapped), -32602, "{name}: {wrapped}");
continue;
}
assert_eq!(bare, wrapped, "{name}, in a session: {in_session}");
}
}
}
#[tokio::test]
async fn a_legacy_declaration_is_answered_unstripped() {
let mut session = Session::open();
let legacy = session.call(discover(1, meta("2025-11-25"))).await;
let modern = session.call(discover(2, meta(SERVED))).await;
assert_eq!(legacy["result"], modern["result"], "{legacy}");
assert_eq!(legacy["result"]["resultType"], "complete", "{legacy}");
}
#[tokio::test]
async fn the_refusal_lists_exactly_the_revisions_this_build_serves() {
let mut session = Session::open();
let probe = session.call(discover(1, meta(UNSERVED))).await;
assert_eq!(error_code(&probe), -32022, "{probe}");
assert_eq!(probe["error"]["data"]["requested"], UNSERVED, "{probe}");
assert_eq!(
probe["error"]["data"]["supported"],
serde_json::to_value(SUPPORTED_PROTOCOL_VERSIONS).expect("versions serialize"),
"{probe}"
);
}
struct Queued {
inbound: std::collections::VecDeque<ClientJsonRpcMessage>,
sent: Arc<std::sync::Mutex<Vec<ServerJsonRpcMessage>>>,
broken: bool,
}
#[derive(Debug, thiserror::Error)]
#[error("send refused")]
struct SendRefused;
impl rmcp::transport::Transport<RoleServer> for Queued {
type Error = SendRefused;
fn send(
&mut self,
item: ServerJsonRpcMessage,
) -> impl std::future::Future<Output = Result<(), Self::Error>> + Send + 'static {
let broken = self.broken;
let sent = self.sent.clone();
async move {
sent.lock().expect("sent lock poisoned").push(item);
if broken {
Err(SendRefused)
} else {
Ok(())
}
}
}
async fn receive(&mut self) -> Option<ClientJsonRpcMessage> {
self.inbound.pop_front()
}
async fn close(&mut self) -> Result<(), Self::Error> {
self.inbound.clear();
Ok(())
}
}
fn queued(frames: &[Value], broken: bool) -> DiscoverAnswering<Queued> {
let sent = Arc::new(std::sync::Mutex::new(Vec::new()));
DiscoverAnswering::new(
Queued {
inbound: frames
.iter()
.map(|frame| {
serde_json::from_value(frame.clone())
.unwrap_or_else(|e| panic!("{frame}: {e}"))
})
.collect(),
sent,
broken,
},
test_server(),
)
}
fn written(wrapper: &DiscoverAnswering<Queued>) -> Vec<Value> {
wrapper
.inner
.sent
.lock()
.expect("sent lock poisoned")
.iter()
.map(|message| serde_json::to_value(message).expect("a reply serializes"))
.collect()
}
#[tokio::test]
async fn every_frame_but_a_discover_request_reaches_rmcp() {
let passed = [
json!({ "jsonrpc": "2.0", "method": "notifications/initialized" }),
json!({ "jsonrpc": "2.0", "id": 1, "result": {} }),
json!({ "jsonrpc": "2.0", "id": 2, "error": { "code": -1, "message": "no" } }),
json!({ "jsonrpc": "2.0", "id": 3, "method": "ping" }),
json!({ "jsonrpc": "2.0", "id": 4, "method": "tools/list", "params": {} }),
];
let mut wrapper = queued(&passed, false);
for frame in &passed {
let received = wrapper
.receive()
.await
.unwrap_or_else(|| panic!("{frame} must reach rmcp"));
assert_eq!(
serde_json::to_value(&received).expect("a frame serializes"),
*frame
);
}
assert!(wrapper.receive().await.is_none(), "the queue is drained");
assert!(written(&wrapper).is_empty(), "{:?}", written(&wrapper));
}
#[tokio::test]
async fn a_discover_is_answered_and_the_next_frame_is_returned() {
let ping = json!({ "jsonrpc": "2.0", "id": 2, "method": "ping" });
let mut wrapper = queued(&[discover(1, meta(SERVED)), ping.clone()], false);
let received = wrapper.receive().await.expect("the ping must come through");
assert_eq!(
serde_json::to_value(&received).expect("a frame serializes"),
ping
);
let written = written(&wrapper);
assert_eq!(written.len(), 1, "{written:?}");
assert_eq!(written[0]["id"], json!(1), "{written:?}");
assert_eq!(
written[0]["result"]["resultType"], "complete",
"{written:?}"
);
}
#[tokio::test]
async fn a_reply_that_cannot_be_written_closes_the_transport() {
let mut wrapper = queued(
&[
discover(1, meta(SERVED)),
json!({ "jsonrpc": "2.0", "id": 2, "method": "ping" }),
],
true,
);
assert!(
wrapper.receive().await.is_none(),
"a failed reply must close the transport, not skip to the next frame"
);
}
#[tokio::test]
async fn send_and_close_are_the_inner_transport_s() {
let mut wrapper = queued(
&[json!({ "jsonrpc": "2.0", "id": 1, "method": "ping" })],
false,
);
let pong = ServerJsonRpcMessage::response(
rmcp::model::ServerResult::EmptyResult(rmcp::model::EmptyObject {}),
rmcp::model::RequestId::Number(1),
);
wrapper.send(pong).await.expect("the inner send must run");
assert_eq!(written(&wrapper).len(), 1);
wrapper.close().await.expect("the inner close must run");
assert!(
wrapper.receive().await.is_none(),
"close must reach the inner transport"
);
}
#[test]
fn a_discover_frame_is_recognised_by_its_method() {
let frame: ClientJsonRpcMessage =
serde_json::from_value(discover(1, meta(SERVED))).expect("the probe must parse");
let ClientJsonRpcMessage::Request(request) = frame else {
panic!("a probe is a request");
};
assert!(matches!(request.request, ClientRequest::DiscoverRequest(_)));
assert_eq!(request.request.method(), "server/discover");
}
#[test]
fn the_wrapper_reads_the_list_the_handler_advertises() {
assert_eq!(
test_server().supported_protocol_versions().as_ref(),
SUPPORTED_PROTOCOL_VERSIONS
);
assert!(SUPPORTED_PROTOCOL_VERSIONS.contains(&ProtocolVersion::V_2026_07_28));
assert!(!SUPPORTED_PROTOCOL_VERSIONS
.iter()
.any(|version| version.as_str() == UNSERVED));
}
}