use std::collections::BTreeMap;
use mcp_conformance_core::trace::{Direction, TraceEvent};
use serde_json::Value;
use super::super::super::FindingSink;
use crate::context::TraceContext;
#[cfg(test)]
mod tests;
const CANCELLED: &str = "notifications/cancelled";
const PROGRESS: &str = "notifications/progress";
fn client_notification(event: &TraceEvent) -> Option<(&str, Option<&Value>)> {
if event.direction != Direction::ClientToServer {
return None;
}
let payload = event.message_payload()?;
if payload.get("id").is_some_and(|id| !id.is_null()) {
return None;
}
let method = payload.get("method")?.as_str()?;
Some((method, payload.get("params")))
}
pub(in crate::checks) fn cancel_notification_references_request(
context: &TraceContext<'_>,
sink: &mut FindingSink,
) {
for event in context.events() {
let Some((method, params)) = client_notification(event) else {
continue;
};
if method != CANCELLED {
continue;
}
sink.examined();
let names_a_request = params
.and_then(|params| params.get("requestId"))
.is_some_and(|id| !id.is_null());
if !names_a_request {
sink.push(
Some(event.seq),
format!(
"`{CANCELLED}` carries no `params.requestId`, so it names no request \
to cancel"
),
);
}
}
}
pub(in crate::checks) fn no_messages_after_cancel_notification(
context: &TraceContext<'_>,
sink: &mut FindingSink,
) {
let mut tokens: BTreeMap<String, String> = BTreeMap::new();
let mut cancelled: BTreeMap<String, u64> = BTreeMap::new();
for event in context.events() {
if let Some((method, params)) = client_notification(event) {
if method == CANCELLED
&& let Some(id) = params.and_then(|params| params.get("requestId"))
&& !id.is_null()
{
cancelled.entry(id.to_string()).or_insert(event.seq);
}
continue;
}
if event.direction == Direction::ClientToServer {
record_progress_token(event, &mut tokens);
continue;
}
if cancelled.is_empty() {
continue; }
sink.examined();
report_if_cancelled(event, &tokens, &cancelled, sink);
}
}
fn record_progress_token(event: &TraceEvent, tokens: &mut BTreeMap<String, String>) {
let Some(payload) = event.message_payload() else {
return;
};
let (Some(id), Some(token)) = (
payload.get("id").filter(|id| !id.is_null()),
payload
.get("params")
.and_then(|params| params.get("_meta"))
.and_then(|meta| meta.get("progressToken")),
) else {
return;
};
tokens.insert(id.to_string(), token.to_string());
}
fn report_if_cancelled(
event: &TraceEvent,
tokens: &BTreeMap<String, String>,
cancelled: &BTreeMap<String, u64>,
sink: &mut FindingSink,
) {
let Some(payload) = event.message_payload() else {
return;
};
let answered = payload
.get("method")
.is_none()
.then(|| payload.get("id").filter(|id| !id.is_null()))
.flatten()
.map(ToString::to_string);
let progressed = (payload.get("method").and_then(Value::as_str) == Some(PROGRESS))
.then(|| payload.get("params")?.get("progressToken"))
.flatten()
.map(ToString::to_string)
.and_then(|token| {
tokens
.iter()
.find_map(|(id, opted)| (*opted == token).then(|| id.clone()))
});
for (id, what) in [(answered, "a response"), (progressed, "progress")] {
let Some(id) = id else { continue };
let Some(&cancelled_at) = cancelled.get(&id) else {
continue;
};
sink.push(
Some(event.seq),
format!(
"server sent {what} for request {id}, which the client cancelled at \
seq {cancelled_at}"
),
);
}
}