use crate::checks::draft::testkit::{META, client, findings_for, server, status, trace};
fn incomplete_envelope(answer: &str) -> String {
trace(&[
client(
0,
r#"{"jsonrpc":"2.0","id":1,"method":"ping","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}"#,
),
server(1, answer),
])
}
fn capability_error(data: &str) -> String {
trace(&[
client(
0,
&format!(r#"{{"jsonrpc":"2.0","id":1,"method":"ping","params":{{{META}}}}}"#),
),
server(
1,
&format!(r#"{{"jsonrpc":"2.0","id":1,"error":{{"code":-32021,"message":"x"{data}}}}}"#),
),
])
}
fn asks_for(capabilities: &str, method: &str) -> String {
trace(&[
client(
0,
&format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"name":"t","_meta":{{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{capabilities}}}}}}}"#
),
),
server(
1,
&format!(
r#"{{"jsonrpc":"2.0","id":1,"result":{{"resultType":"input_required","inputRequests":{{"a":{{"method":"{method}"}}}}}}}}"#
),
),
])
}
fn listening(notification: &str) -> String {
trace(&[
client(
0,
&format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"subscriptions/listen","params":{{{META}}}}}"#
),
),
server(1, notification),
])
}
#[test]
fn a_malformed_envelope_must_draw_invalid_params() {
let check = "meta.missing-required-field-rejected";
assert_eq!(
findings_for(
check,
&incomplete_envelope(r#"{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}}"#)
)
.len(),
1
);
let findings = findings_for(
check,
&incomplete_envelope(r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32603,"message":"x"}}"#),
);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("-32603"), "{findings:?}");
assert!(
findings_for(
check,
&incomplete_envelope(
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"x"}}"#
)
)
.is_empty()
);
let complete = trace(&[
client(
0,
&format!(r#"{{"jsonrpc":"2.0","id":1,"method":"ping","params":{{{META}}}}}"#),
),
server(
1,
r#"{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}}"#,
),
]);
assert!(findings_for(check, &complete).is_empty());
}
#[test]
fn a_legacy_initialize_is_outside_the_rejection_rule() {
let check = "meta.missing-required-field-rejected";
let handshake = trace(&[
client(
0,
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}"#,
),
server(
1,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"x"}}"#,
),
]);
assert!(findings_for(check, &handshake).is_empty());
let other = trace(&[
client(
0,
r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"protocolVersion":"2025-11-25"}}"#,
),
server(
1,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"x"}}"#,
),
]);
assert_eq!(findings_for(check, &other).len(), 1);
}
#[test]
fn the_missing_capability_error_must_name_what_was_missing() {
let check = "meta.missing-capability-error";
assert_eq!(findings_for(check, &capability_error("")).len(), 1);
assert_eq!(
findings_for(
check,
&capability_error(r#","data":{"requiredCapabilities":{}}"#)
)
.len(),
1
);
assert_eq!(
findings_for(
check,
&capability_error(r#","data":{"requiredCapabilities":"elicitation"}"#)
)
.len(),
1
);
assert_eq!(
findings_for(
check,
&capability_error(r#","data":{"requiredCapabilities":["elicitation"]}"#)
)
.len(),
1,
"an array is not a ClientCapabilities object"
);
assert!(
findings_for(
check,
&capability_error(r#","data":{"requiredCapabilities":{"elicitation":{}}}"#)
)
.is_empty()
);
let other = capability_error("").replace("-32021", "-32602");
assert!(findings_for(check, &other).is_empty());
}
#[test]
fn an_input_request_is_matched_to_the_capability_it_needs() {
let check = "meta.no-undeclared-capability-reliance";
for (method, capability) in [
("elicitation/create", "elicitation"),
("sampling/createMessage", "sampling"),
("roots/list", "roots"),
] {
assert_eq!(
findings_for(check, &asks_for("{}", method)).len(),
1,
"{method} needs {capability}"
);
let declared = format!(r#"{{"{capability}":{{}}}}"#);
assert!(
findings_for(check, &asks_for(&declared, method)).is_empty(),
"{method} is fine once {capability} is declared"
);
assert_eq!(
findings_for(check, &asks_for(r#"{"logging":{}}"#, method)).len(),
1,
"{method} is not covered by an unrelated capability"
);
}
assert!(findings_for(check, &asks_for("{}", "tools/list")).is_empty());
}
#[test]
fn subscription_tagging_binds_only_listen_streams_and_only_untagged_notifications() {
let check = "meta.subscription-id-present";
assert_eq!(
findings_for(
check,
&listening(r#"{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}"#)
)
.len(),
1
);
assert!(
findings_for(
check,
&listening(
r#"{"jsonrpc":"2.0","method":"notifications/tools/list_changed","params":{"_meta":{"io.modelcontextprotocol/subscriptionId":"s1"}}}"#
)
)
.is_empty()
);
for method in ["notifications/progress", "notifications/message"] {
assert!(
findings_for(
check,
&listening(&format!(r#"{{"jsonrpc":"2.0","method":"{method}"}}"#))
)
.is_empty(),
"{method} is request-scoped"
);
}
assert!(
findings_for(
check,
&listening(r#"{"jsonrpc":"2.0","id":7,"method":"elicitation/create","params":{}}"#)
)
.is_empty()
);
assert!(findings_for(check, &listening(r#"{"jsonrpc":"2.0"}"#)).is_empty());
assert!(
findings_for(
check,
&listening(r#"{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}}"#)
)
.is_empty()
);
let unsubscribed = trace(&[
client(
0,
&format!(r#"{{"jsonrpc":"2.0","id":1,"method":"ping","params":{{{META}}}}}"#),
),
server(
1,
r#"{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}"#,
),
]);
assert!(findings_for(check, &unsubscribed).is_empty());
}
#[test]
fn the_http_status_clauses_judge_only_their_own_error_code() {
const INCOMPLETE: &str = r#""_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}"#;
for (check, code, meta) in [
(
"meta.missing-required-field-http-status",
-32602,
INCOMPLETE,
),
("meta.missing-capability-http-status", -32021, META),
] {
let document = trace(&[
client(
0,
&format!(r#"{{"jsonrpc":"2.0","id":1,"method":"ping","params":{{{meta}}}}}"#),
),
status(1, 500),
server(
2,
&format!(r#"{{"jsonrpc":"2.0","id":1,"error":{{"code":{code},"message":"x"}}}}"#),
),
]);
assert_eq!(findings_for(check, &document).len(), 1, "{check}");
assert!(
findings_for(check, &document.replace("500", "400")).is_empty(),
"{check} accepts 400"
);
let other = document.replace(&code.to_string(), "-32700");
assert!(findings_for(check, &other).is_empty(), "{check}");
}
}
#[test]
fn the_status_clause_binds_only_the_malformed_envelope_that_drew_the_error() {
let well_formed = trace(&[
client(
0,
&format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"resources/read","params":{{{META},"uri":"test://gone"}}}}"#
),
),
status(1, 404),
server(
2,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"resource not found"}}"#,
),
]);
assert!(
findings_for("meta.missing-required-field-http-status", &well_formed).is_empty(),
"a -32602 that answers a well-formed request is not this clause's subject"
);
let malformed = well_formed.replace(
r#""io.modelcontextprotocol/clientCapabilities":{}"#,
r#""unrelated":{}"#,
);
assert_eq!(
findings_for("meta.missing-required-field-http-status", &malformed).len(),
1,
"{malformed}"
);
}