use std::collections::HashMap;
use mcp_conformance_core::canonical::to_canonical_string;
use mcp_conformance_core::message::MessageKind;
use mcp_conformance_core::trace::Direction;
use serde_json::Value;
use super::super::FindingSink;
use crate::context::TraceContext;
fn responses_match_requests(
context: &TraceContext<'_>,
sink: &mut FindingSink,
want_results: bool,
) {
let mut outstanding: HashMap<(Direction, String), u64> = HashMap::new();
for (event, kind, _) in context.messages() {
match kind {
MessageKind::Request { id, .. } => {
if !id.is_null() {
outstanding.insert((event.direction, to_canonical_string(id)), event.seq);
}
}
MessageKind::Result { id } => {
if want_results {
sink.examined();
check_response_id(
event.seq,
event.direction,
*id,
&mut outstanding,
sink,
"result",
);
} else {
consume_outstanding(event.direction, *id, &mut outstanding);
}
}
MessageKind::Error { id, .. } => {
if want_results {
consume_outstanding(event.direction, *id, &mut outstanding);
} else if id.is_some_and(|id| !id.is_null()) {
sink.examined();
check_response_id(
event.seq,
event.direction,
*id,
&mut outstanding,
sink,
"error",
);
}
}
_ => {}
}
}
}
fn consume_outstanding(
response_direction: Direction,
id: Option<&Value>,
outstanding: &mut HashMap<(Direction, String), u64>,
) {
if let Some(id) = id.filter(|id| !id.is_null()) {
let requester = match response_direction {
Direction::ClientToServer => Direction::ServerToClient,
Direction::ServerToClient => Direction::ClientToServer,
};
outstanding.remove(&(requester, to_canonical_string(id)));
}
}
fn check_response_id(
seq: u64,
response_direction: Direction,
id: Option<&Value>,
outstanding: &mut HashMap<(Direction, String), u64>,
sink: &mut FindingSink,
flavor: &str,
) {
let requester = match response_direction {
Direction::ClientToServer => Direction::ServerToClient,
Direction::ServerToClient => Direction::ClientToServer,
};
match id {
None => sink.push(
Some(seq),
format!("{flavor} response is missing its id; responses must echo the request id"),
),
Some(id) if id.is_null() => sink.push(
Some(seq),
format!("{flavor} response carries a null id; responses must echo the request id"),
),
Some(id) => {
let key = (requester, to_canonical_string(id));
if outstanding.remove(&key).is_none() {
sink.push(
Some(seq),
format!(
"{flavor} response answers id {}, but that party has no outstanding request with that id (never sent, or already answered)",
key.1
),
);
}
}
}
}
pub(in crate::checks) fn result_id_matches(context: &TraceContext<'_>, sink: &mut FindingSink) {
responses_match_requests(context, sink, true);
}
pub(in crate::checks) fn error_id_matches(context: &TraceContext<'_>, sink: &mut FindingSink) {
responses_match_requests(context, sink, false);
}