use mcp_conformance_core::message::MessageKind;
use mcp_conformance_core::trace::Direction;
use serde_json::Value;
use super::super::FindingSink;
use crate::context::TraceContext;
#[cfg(test)]
mod tests;
const LOG_LEVEL: &str = "io.modelcontextprotocol/logLevel";
const MESSAGE: &str = "notifications/message";
fn is_log(event: &mcp_conformance_core::trace::TraceEvent, kind: &MessageKind<'_>) -> bool {
event.direction == Direction::ServerToClient
&& matches!(kind, MessageKind::Notification { method } if *method == MESSAGE)
}
pub(in crate::checks) fn level_requested(context: &TraceContext<'_>, sink: &mut FindingSink) {
let any_requested = context.messages().any(|(event, _, _)| {
event.direction == Direction::ClientToServer
&& event.message_payload().is_some_and(|payload| {
payload
.get("params")
.and_then(|params| params.get("_meta"))
.and_then(|meta| meta.get(LOG_LEVEL))
.is_some()
})
});
for (event, kind, _) in context.messages() {
if !is_log(event, kind) {
continue;
}
sink.examined();
if !any_requested {
sink.push(
Some(event.seq),
"server emitted `notifications/message` though no request in this session \
carried `io.modelcontextprotocol/logLevel`"
.to_owned(),
);
}
}
}
pub(in crate::checks) fn not_on_subscription(context: &TraceContext<'_>, sink: &mut FindingSink) {
for (event, kind, _) in context.messages() {
if event.direction != Direction::ServerToClient {
continue;
}
if !is_log(event, kind) {
continue;
}
sink.examined();
let tagged = event
.message_payload()
.and_then(|payload| payload.get("params"))
.and_then(|params| params.get("_meta"))
.and_then(|meta| meta.get("io.modelcontextprotocol/subscriptionId"))
.is_some_and(|id| !id.is_null());
if tagged {
sink.push(
Some(event.seq),
"`notifications/message` carries a subscription id, so it is travelling on a \
`subscriptions/listen` stream; logging is request-scoped"
.to_owned(),
);
}
}
}
pub(in crate::checks) fn invalid_level_rejected(
context: &TraceContext<'_>,
sink: &mut FindingSink,
) {
const LEVELS: &[&str] = &[
"debug",
"info",
"notice",
"warning",
"error",
"critical",
"alert",
"emergency",
];
for exchange in context.exchanges() {
let Some(level) = exchange
.params
.and_then(|params| params.get("_meta"))
.and_then(|meta| meta.get(LOG_LEVEL))
else {
continue;
};
if level.as_str().is_some_and(|name| LEVELS.contains(&name)) {
continue;
}
sink.examined();
let code = exchange
.response
.message_payload()
.and_then(|payload| payload.get("error"))
.and_then(|error| error.get("code"))
.and_then(Value::as_i64);
if code != Some(-32602) {
sink.push(
Some(exchange.response.seq),
format!(
"the request declared log level {level}, which is not one of the eight \
RFC 5424 levels, and drew {} rather than -32602",
code.map_or_else(|| "a result".to_owned(), |code| format!("error {code}"))
),
);
}
}
}