use crate::checks::draft::testkit::{
META, client, error, findings_for, post, server, status, trace,
};
fn exchange(header_version: &str, body_version: &str, code: u16, answer: &str) -> String {
trace(&[
post(
0,
&format!(r#"{{"mcp-protocol-version":"{header_version}","mcp-method":"tools/list"}}"#),
),
client(
1,
&format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{{"_meta":{{"io.modelcontextprotocol/protocolVersion":"{body_version}","io.modelcontextprotocol/clientCapabilities":{{}}}}}}}}"#
),
),
status(2, code),
server(3, answer),
])
}
const RESULT: &str = r#"{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","tools":[]}}"#;
const MISMATCH: &str = r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32020,"message":"x"}}"#;
#[test]
fn a_version_mismatch_must_draw_header_mismatch() {
let check = "transport.version-mismatch-rejected";
let findings = findings_for(check, &exchange("2025-11-25", "2026-07-28", 200, RESULT));
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("a result"), "{findings:?}");
let findings = findings_for(
check,
&exchange(
"2025-11-25",
"2026-07-28",
400,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"x"}}"#,
),
);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("error -32602"), "{findings:?}");
assert!(findings_for(check, &exchange("2025-11-25", "2026-07-28", 400, MISMATCH)).is_empty());
assert!(findings_for(check, &exchange("2026-07-28", "2026-07-28", 200, RESULT)).is_empty());
}
#[test]
fn header_mismatch_must_ride_a_400() {
let check = "transport.header-mismatch-status";
let findings = findings_for(check, &exchange("2026-07-28", "2026-07-28", 500, MISMATCH));
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("HTTP 500"), "{findings:?}");
assert!(findings_for(check, &exchange("2026-07-28", "2026-07-28", 400, MISMATCH)).is_empty());
assert!(
findings_for(
check,
&exchange(
"2026-07-28",
"2026-07-28",
500,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"x"}}"#
)
)
.is_empty()
);
}
fn after_discovery(versions: &str, requested: &str, answer: &str) -> String {
trace(&[
post(
0,
r#"{"mcp-protocol-version":"2026-07-28","mcp-method":"server/discover"}"#,
),
client(
1,
&format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{{{META}}}}}"#
),
),
status(2, 200),
server(
3,
&format!(
r#"{{"jsonrpc":"2.0","id":1,"result":{{"resultType":"complete","supportedVersions":{versions},"capabilities":{{}}}}}}"#
),
),
post(
4,
&format!(r#"{{"mcp-protocol-version":"{requested}","mcp-method":"tools/list"}}"#),
),
client(
5,
&format!(
r#"{{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{{"_meta":{{"io.modelcontextprotocol/protocolVersion":"{requested}","io.modelcontextprotocol/clientCapabilities":{{}}}}}}}}"#
),
),
status(6, 200),
server(7, answer),
])
}
#[test]
fn an_unsupported_version_is_judged_against_what_the_server_declared() {
let check = "transport.unsupported-version-error";
let result = r#"{"jsonrpc":"2.0","id":2,"result":{"resultType":"complete","tools":[]}}"#;
let findings = findings_for(
check,
&after_discovery(r#"["2026-07-28"]"#, "2025-11-25", result),
);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(
findings_for(
check,
&after_discovery(r#"["2026-07-28","2025-11-25"]"#, "2025-11-25", result)
)
.is_empty()
);
assert!(findings_for(check, &after_discovery("[]", "2025-11-25", result)).is_empty());
assert!(findings_for(check, &exchange("2026-07-28", "2026-07-28", 200, RESULT)).is_empty());
}
#[test]
fn the_unsupported_version_error_must_list_the_versions() {
let check = "transport.unsupported-version-error";
let without_list = trace(&[
post(
0,
r#"{"mcp-protocol-version":"2026-07-28","mcp-method":"tools/list"}"#,
),
client(
1,
&format!(r#"{{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{{{META}}}}}"#),
),
status(2, 400),
error(3, "1", -32022),
]);
let findings = findings_for(check, &without_list);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("data.supported"), "{findings:?}");
let complete = trace(&[
post(
0,
r#"{"mcp-protocol-version":"2026-07-28","mcp-method":"tools/list"}"#,
),
client(
1,
&format!(r#"{{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{{{META}}}}}"#),
),
status(2, 400),
server(
3,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32022,"message":"x","data":{"supported":["2026-07-28"],"requested":"1900-01-01"}}}"#,
),
]);
assert!(findings_for(check, &complete).is_empty());
let empty = complete.replace(r#"["2026-07-28"]"#, "[]");
let findings = findings_for(check, &empty);
assert_eq!(findings.len(), 1, "{findings:?}");
let wrong_status = complete.replace(r#""status":400"#, r#""status":200"#);
let on_status = findings_for(check, &wrong_status);
assert!(on_status.is_empty(), "{on_status:?}");
}
#[test]
fn the_unsupported_version_error_must_carry_a_400() {
let check = "transport.unsupported-version-status";
let answered = |status_code: u16| {
trace(&[
post(
0,
r#"{"mcp-protocol-version":"2026-07-28","mcp-method":"tools/list"}"#,
),
client(
1,
&format!(r#"{{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{{{META}}}}}"#),
),
status(2, status_code),
server(
3,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32022,"message":"x","data":{"supported":["2026-07-28"]}}}"#,
),
])
};
let findings = findings_for(check, &answered(200));
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].contains("HTTP 200"), "{findings:?}");
assert!(findings_for(check, &answered(400)).is_empty());
let other_code = answered(200).replace("-32022", "-32602");
assert!(findings_for(check, &other_code).is_empty());
let no_http = trace(&[
client(
0,
&format!(r#"{{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{{{META}}}}}"#),
),
server(
1,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32022,"message":"x","data":{"supported":["2026-07-28"]}}}"#,
),
]);
assert!(findings_for(check, &no_http).is_empty());
}
#[test]
fn method_not_found_must_ride_a_404() {
let check = "transport.unknown-method-404";
let not_found = trace(&[
post(
0,
r#"{"mcp-protocol-version":"2026-07-28","mcp-method":"tools/nope"}"#,
),
client(
1,
&format!(r#"{{"jsonrpc":"2.0","id":1,"method":"tools/nope","params":{{{META}}}}}"#),
),
status(2, 200),
error(3, "1", -32601),
]);
assert_eq!(findings_for(check, ¬_found).len(), 1);
assert!(findings_for(check, ¬_found.replace("200", "404")).is_empty());
let stdio = [
r#"{"seq":0,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":1,"method":"tools/nope"}}"#,
r#"{"seq":1,"direction":"server-to-client","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"x"}}}"#,
]
.join("\n");
assert!(findings_for(check, &stdio).is_empty());
}
#[test]
fn the_removed_handshake_is_outside_the_status_rule() {
let handshake = trace(&[
post(0, r#"{"mcp-method":"initialize"}"#),
client(
1,
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"legacy","version":"0"}}}"#,
),
status(2, 200),
server(
3,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32022,"message":"x","data":{"supported":["2026-07-28"]}}}"#,
),
]);
assert!(
findings_for("transport.unsupported-version-status", &handshake).is_empty(),
"{handshake}"
);
let requested = trace(&[
post(0, r#"{"mcp-method":"tools/list"}"#),
client(
1,
r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"1999-01-01","io.modelcontextprotocol/clientCapabilities":{}}}}"#,
),
status(2, 200),
server(
3,
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32022,"message":"x","data":{"supported":["2026-07-28"]}}}"#,
),
]);
assert_eq!(
findings_for("transport.unsupported-version-status", &requested).len(),
1,
"{requested}"
);
}