use std::collections::BTreeSet;
use serde_json::Value;
use super::super::super::FindingSink;
use crate::context::TraceContext;
use mcp_conformance_core::trace::{Direction, EventBody, LifecycleEvent, TransportKind};
#[cfg(test)]
mod tests;
pub(in crate::checks) fn client_no_responses(context: &TraceContext<'_>, sink: &mut FindingSink) {
for (event, _, _) in context.messages() {
if event.direction != Direction::ClientToServer {
continue;
}
let Some(payload) = event.message_payload() else {
continue;
};
sink.examined();
let is_response = payload.get("id").is_some()
&& payload.get("method").is_none()
&& (payload.get("result").is_some() || payload.get("error").is_some());
if is_response {
sink.push(
Some(event.seq),
"client sent a JSON-RPC response; 2026-07-28 removed server-initiated \
requests, so there is nothing for one to answer"
.to_owned(),
);
}
}
}
pub(in crate::checks) fn no_independent_server_requests(
context: &TraceContext<'_>,
sink: &mut FindingSink,
) {
for (event, _, _) in context.messages() {
if event.direction != Direction::ServerToClient {
continue;
}
let Some(payload) = event.message_payload() else {
continue;
};
sink.examined();
if let Some(method) = payload.get("method").and_then(Value::as_str)
&& payload.get("id").is_some_and(|id| !id.is_null())
{
sink.push(
Some(event.seq),
format!(
"server sent an independent request `{method}`; 2026-07-28 replaces \
server-initiated requests with MRTR input requests"
),
);
}
}
}
pub(in crate::checks) fn accel_buffering_header(
context: &TraceContext<'_>,
sink: &mut FindingSink,
) {
for event in context.events() {
if event.direction != Direction::ServerToClient {
continue;
}
let EventBody::Http { headers, .. } = &event.body else {
continue;
};
let is_sse = headers
.get("content-type")
.is_some_and(|value| value.starts_with("text/event-stream"));
if !is_sse {
continue; }
sink.examined();
if headers.get("x-accel-buffering").map(String::as_str) != Some("no") {
sink.push(
Some(event.seq),
"SSE response does not carry `X-Accel-Buffering: no`".to_owned(),
);
}
}
}
pub(in crate::checks) fn no_messages_after_cancellation(
context: &TraceContext<'_>,
sink: &mut FindingSink,
) {
let mut outstanding: BTreeSet<String> = BTreeSet::new();
let mut closed_at: Option<u64> = None;
for event in context.events() {
if let Some(closed_at) = closed_at {
report_after_close(event, &outstanding, closed_at, sink);
} else if is_cancellation(event) {
closed_at = Some(event.seq);
} else {
track_outstanding(event, &mut outstanding);
}
}
}
fn is_cancellation(event: &mcp_conformance_core::trace::TraceEvent) -> bool {
let closed = matches!(
event.body,
EventBody::Lifecycle {
event: LifecycleEvent::TransportClose | LifecycleEvent::TransportAbort
}
);
closed && event.transport == TransportKind::StreamableHttp
}
fn track_outstanding(
event: &mcp_conformance_core::trace::TraceEvent,
outstanding: &mut BTreeSet<String>,
) {
let Some(payload) = event.message_payload() else {
return;
};
let Some(id) = payload.get("id").filter(|id| !id.is_null()) else {
return;
};
if payload.get("method").is_some() {
outstanding.insert(id.to_string());
} else {
outstanding.remove(&id.to_string());
}
}
fn report_after_close(
event: &mcp_conformance_core::trace::TraceEvent,
outstanding: &BTreeSet<String>,
closed_at: u64,
sink: &mut FindingSink,
) {
if event.direction != Direction::ServerToClient {
return;
}
let Some(id) = event
.message_payload()
.and_then(|payload| payload.get("id"))
.filter(|id| !id.is_null())
else {
return;
};
sink.examined();
if outstanding.contains(&id.to_string()) {
sink.push(
Some(event.seq),
format!(
"server sent a further message for request id {id}, whose response \
stream closed at seq {closed_at}; a close is cancellation at this revision"
),
);
}
}