use std::collections::BTreeSet;
use mcp_conformance_core::trace::Direction;
use serde_json::Value;
use super::super::FindingSink;
use super::transport::designations_by_tool;
use crate::context::TraceContext;
#[cfg(test)]
mod tests;
const SAFE_INTEGER: i64 = 9_007_199_254_740_991;
pub(in crate::checks) fn deterministic_order(context: &TraceContext<'_>, sink: &mut FindingSink) {
let mut seen: Option<(u64, Vec<String>)> = None;
for exchange in context.exchanges_for("tools/list") {
let Some(names) = exchange
.result
.and_then(|result| result.get("tools"))
.and_then(Value::as_array)
.map(|tools| {
tools
.iter()
.filter_map(|tool| tool.get("name").and_then(Value::as_str))
.map(str::to_owned)
.collect::<Vec<_>>()
})
else {
continue;
};
if let Some((first_seq, first)) = &seen {
let same_set: BTreeSet<&String> = first.iter().collect();
let this_set: BTreeSet<&String> = names.iter().collect();
if same_set != this_set {
continue; }
sink.examined();
if *first != names {
sink.push(
Some(exchange.response.seq),
format!(
"`tools/list` returned the same tools in a different order than the \
result at seq {first_seq}, though the set did not change"
),
);
}
} else {
seen = Some((exchange.response.seq, names));
}
}
}
pub(in crate::checks) fn x_mcp_header_integer_range(
context: &TraceContext<'_>,
sink: &mut FindingSink,
) {
let designations = designations_by_tool(context);
for (event, _, _) in context.messages() {
if event.direction != Direction::ClientToServer {
continue;
}
let Some(payload) = event.message_payload() else {
continue;
};
if payload.get("method").and_then(Value::as_str) != Some("tools/call") {
continue;
}
let Some(params) = payload.get("params") else {
continue;
};
let Some(name) = params.get("name").and_then(Value::as_str) else {
continue;
};
let Some(paths) = designations.get(name) else {
continue;
};
for designation in paths {
let mut value = params.get("arguments");
for segment in &designation.path {
value = value.and_then(|current| current.get(segment));
}
let Some(integer) = value.and_then(Value::as_i64) else {
continue;
};
sink.examined();
if !(-SAFE_INTEGER..=SAFE_INTEGER).contains(&integer) {
sink.push(
Some(event.seq),
format!(
"the argument mirrored into `{}` is {integer}, outside the \
IEEE 754 safe integer range",
designation.name
),
);
}
}
}
}
pub(in crate::checks) fn read_contents_non_empty(
context: &TraceContext<'_>,
sink: &mut FindingSink,
) {
for exchange in context.exchanges_for("resources/read") {
let Some(contents) = exchange
.result
.and_then(|result| result.get("contents"))
.and_then(Value::as_array)
else {
continue;
};
sink.examined();
if contents.is_empty() {
sink.push(
Some(exchange.response.seq),
"`resources/read` answered with an empty `contents` array, which is ambiguous \
between an empty resource and a missing one"
.to_owned(),
);
}
}
}