use super::*;
use crate::mcp::era::{classify_message, conclude_request, RequestAssessment, UnknownReason};
use crate::mcp::era::{
conclude, EnvelopeObservation, EraResolution, IncompleteReason, InvalidReason, ParsedMcpEvent,
RequestMetadata, RequestMetadata::*, ResultConclusion, ResultObservation,
};
use serde_json::{json, Value};
const V2026: &str = "2026-07-28";
const V2025: &str = "2025-06-18";
fn detailed(input: &str, format: McpInputFormat) -> Vec<ParsedMcpEvent> {
parse_mcp_transcript_detailed(input, format).expect("transcript parses")
}
fn req(meta: Option<Value>) -> Value {
let mut params = json!({"name": "Calculator", "arguments": {}});
if let Some(m) = meta {
params["_meta"] = m;
}
json!({"jsonrpc": "2.0", "id": "call-1", "method": "tools/call", "params": params})
}
fn meta(version: Value) -> Value {
json!({"io.modelcontextprotocol/protocolVersion": version})
}
fn framed(ctx: Option<Value>, entry_ctx: Option<Value>, message: Value) -> String {
let key = if message.get("method").is_some() {
"request"
} else {
"response"
};
let mut entry = json!({"timestamp_ms": 1000, key: message});
if let Some(c) = entry_ctx {
entry["transport_context"] = c;
}
let mut doc = json!({"transport": "streamable-http", "entries": [entry]});
if let Some(c) = ctx {
doc["transport_context"] = c;
}
doc.to_string()
}
fn headers(version: Value) -> Value {
json!({"headers": {"MCP-Protocol-Version": version}})
}
#[test]
fn a_transcript_level_header_is_observed() {
let input = framed(Some(headers(json!(V2026))), None, req(None));
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Present(V2026.into())
);
}
#[test]
fn an_entry_level_header_is_observed() {
let input = framed(None, Some(headers(json!(V2026))), req(None));
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Present(V2026.into())
);
}
#[test]
fn two_levels_agreeing_are_present() {
let input = framed(
Some(headers(json!(V2026))),
Some(headers(json!(V2026))),
req(None),
);
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Present(V2026.into())
);
}
#[test]
fn two_levels_disagreeing_are_malformed() {
let input = framed(
Some(headers(json!(V2025))),
Some(headers(json!(V2026))),
req(None),
);
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Malformed
);
}
#[test]
fn a_non_string_header_is_malformed_not_absent() {
for bad in [
json!(2026),
json!({"v": V2026}),
json!([V2026]),
json!(null),
] {
let input = framed(Some(headers(bad.clone())), None, req(None));
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Malformed,
"{bad}"
);
}
}
#[test]
fn an_empty_header_is_malformed() {
let input = framed(Some(headers(json!(""))), None, req(None));
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Malformed
);
}
#[test]
fn a_framed_transcript_with_no_header_is_absent() {
let input = framed(None, None, req(None));
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Absent
);
}
#[test]
fn unframed_formats_are_not_applicable() {
for (input, format) in [
(req(None).to_string(), McpInputFormat::JsonRpc),
(
json!({"events": [req(None)]}).to_string(),
McpInputFormat::Inspector,
),
] {
assert_eq!(
detailed(&input, format)[0].context.envelope,
EnvelopeObservation::NotApplicable
);
}
}
#[test]
fn a_request_metadata_version_is_read_from_params_meta() {
let input = req(Some(meta(json!(V2026)))).to_string();
assert_eq!(
detailed(&input, McpInputFormat::JsonRpc)[0]
.context
.request_metadata,
Some(Present(V2026.into()))
);
}
#[test]
fn a_request_without_meta_reports_absent_metadata() {
let input = req(None).to_string();
assert_eq!(
detailed(&input, McpInputFormat::JsonRpc)[0]
.context
.request_metadata,
Some(Absent)
);
}
#[test]
fn a_non_string_metadata_version_is_malformed() {
for bad in [json!(7), json!({}), json!([]), json!("")] {
let input = req(Some(meta(bad.clone()))).to_string();
assert_eq!(
detailed(&input, McpInputFormat::JsonRpc)[0]
.context
.request_metadata,
Some(RequestMetadata::Malformed),
"{bad}"
);
}
}
fn response(result_type: Option<Value>) -> Value {
let mut r = json!({"content": []});
if let Some(t) = result_type {
r["resultType"] = t;
}
json!({"jsonrpc": "2.0", "id": "call-1", "result": r})
}
fn observed(result_type: Option<Value>) -> ResultObservation {
let input = framed(Some(headers(json!(V2026))), None, response(result_type));
detailed(&input, McpInputFormat::StreamableHttp)
.into_iter()
.find_map(|e| e.context.result_observation)
.expect("a response event")
}
fn observed_with_continuation(result_type: Value) -> ResultObservation {
let mut msg = response(Some(result_type));
msg["result"]["requestState"] = json!("s1");
let input = framed(Some(headers(json!(V2026))), None, msg);
detailed(&input, McpInputFormat::StreamableHttp)
.into_iter()
.find_map(|e| e.context.result_observation)
.expect("a response event")
}
#[test]
fn every_result_type_shape_reaches_the_axis() {
assert_eq!(observed(None), ResultObservation::Missing);
assert_eq!(
observed(Some(json!("complete"))),
ResultObservation::Complete
);
assert_eq!(
observed(Some(json!("input_required"))),
ResultObservation::InputRequiredWithoutContinuation
);
assert_eq!(
observed_with_continuation(json!("input_required")),
ResultObservation::InputRequired
);
assert_eq!(
observed(Some(json!("banana"))),
ResultObservation::Unrecognized
);
}
#[test]
fn a_non_string_result_type_is_malformed() {
for bad in [json!(1), json!({}), json!([]), json!(null)] {
assert_eq!(
observed(Some(bad.clone())),
ResultObservation::Malformed,
"{bad}"
);
}
}
#[test]
fn a_request_has_no_result_observation() {
let input = framed(Some(headers(json!(V2026))), None, req(None));
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.result_observation,
None
);
}
#[test]
fn malformed_observations_never_fail_the_parse() {
let cases = [
framed(Some(headers(json!(2026))), None, req(None)),
framed(
Some(headers(json!(V2025))),
Some(headers(json!(V2026))),
req(None),
),
req(Some(meta(json!(7)))).to_string(),
framed(Some(headers(json!(V2026))), None, response(Some(json!(1)))),
];
for (i, input) in cases.iter().enumerate() {
let format = if input.contains("\"transport\"") {
McpInputFormat::StreamableHttp
} else {
McpInputFormat::JsonRpc
};
assert!(
parse_mcp_transcript_detailed(input, format).is_ok(),
"case {i}"
);
assert!(parse_mcp_transcript(input, format).is_ok(), "case {i}");
}
}
#[test]
fn a_deviant_entry_does_not_contaminate_the_others() {
let doc = json!({
"transport": "streamable-http",
"entries": [
{"timestamp_ms": 1000, "request": req(None)},
{"timestamp_ms": 1001, "transport_context": headers(json!(2026)),
"request": {"jsonrpc": "2.0", "id": "call-2", "method": "tools/call",
"params": {"name": "Calculator", "arguments": {}}}}
]
});
let mut ctx = json!({"headers": {"MCP-Protocol-Version": V2026}});
let mut doc = doc;
doc["transport_context"] = std::mem::take(&mut ctx);
let parsed = detailed(&doc.to_string(), McpInputFormat::StreamableHttp);
assert_eq!(
parsed[0].context.envelope,
EnvelopeObservation::Present(V2026.into()),
"the clean entry keeps the transcript default"
);
assert_eq!(
parsed[1].context.envelope,
EnvelopeObservation::Malformed,
"only the deviant entry is malformed"
);
}
#[test]
fn a_direct_headers_field_is_read_at_both_levels() {
for doc in [
json!({"transport": "streamable-http",
"headers": {"MCP-Protocol-Version": V2026},
"entries": [{"timestamp_ms": 1000, "request": req(None)}]}),
json!({"transport": "streamable-http",
"entries": [{"timestamp_ms": 1000,
"headers": {"MCP-Protocol-Version": V2026},
"request": req(None)}]}),
] {
assert_eq!(
detailed(&doc.to_string(), McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Present(V2026.into())
);
}
}
#[test]
fn duplicate_case_variants_of_the_header_are_malformed() {
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {
"MCP-Protocol-Version": V2026,
"mcp-protocol-version": V2025
}},
"entries": [{"timestamp_ms": 1000, "request": req(None)}]
});
assert_eq!(
detailed(&doc.to_string(), McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Malformed
);
}
#[test]
fn a_response_reports_no_request_metadata() {
let input = framed(Some(headers(json!(V2026))), None, response(None));
let parsed = detailed(&input, McpInputFormat::StreamableHttp);
let response_event = parsed
.iter()
.find(|e| e.context.result_observation.is_some())
.expect("a response event");
assert_eq!(response_event.context.request_metadata, None);
}
#[test]
fn a_non_object_meta_is_malformed() {
for bad in [json!("2026-07-28"), json!(7), json!([]), json!(null)] {
let input = req(Some(bad.clone())).to_string();
assert_eq!(
detailed(&input, McpInputFormat::JsonRpc)[0]
.context
.request_metadata,
Some(RequestMetadata::Malformed),
"{bad}"
);
}
}
#[test]
fn no_event_ever_carries_both_axes() {
let hybrid_result = json!({
"jsonrpc": "2.0", "id": "call-1", "method": "tools/call",
"params": {"name": "Calculator", "arguments": {}, "_meta": meta(json!(V2026))},
"result": {"content": [], "resultType": "complete"}
});
let hybrid_error = json!({
"jsonrpc": "2.0", "id": "call-2", "method": "tools/list",
"error": {"code": -32000, "message": "boom"}
});
let hybrid_unreadable_result = json!({
"jsonrpc": "2.0", "id": "call-3", "method": "notifications/initialized",
"result": null
});
let notification = json!({
"jsonrpc": "2.0", "method": "notifications/progress",
"params": {"progressToken": "t", "progress": 1}
});
for message in [
req(Some(meta(json!(V2026)))),
response(Some(json!("complete"))),
hybrid_result,
hybrid_error,
hybrid_unreadable_result,
notification,
] {
let parsed = detailed(&message.to_string(), McpInputFormat::JsonRpc);
let context = &parsed[0].context;
assert!(
context.request_metadata.is_none() || context.result_observation.is_none(),
"both axes populated for {message}: {context:?}"
);
let expects_request_axis =
matches!(classify_message(&message), Ok(MessageKind::Request { .. }));
assert_eq!(
context.request_metadata.is_some(),
expects_request_axis,
"the request axis must follow the payload discriminant for {message}"
);
}
}
#[test]
fn a_non_object_transport_context_is_malformed_not_absent() {
for bad in [json!(7), json!("2026-07-28"), json!([]), json!(null)] {
let input = framed(Some(bad.clone()), None, req(None));
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Malformed,
"{bad}"
);
}
}
#[test]
fn a_non_object_entry_transport_context_does_not_inherit_the_transcript_default() {
for bad in [json!(7), json!("2026-07-28"), json!([]), json!(null)] {
let input = framed(Some(headers(json!(V2026))), Some(bad.clone()), req(None));
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Malformed,
"{bad}"
);
}
}
#[test]
fn a_readable_transport_context_without_headers_stays_absent() {
let input = framed(Some(json!({"status": 200})), None, req(None));
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Absent
);
}
#[test]
fn an_explicit_null_headers_slot_is_malformed_not_absent() {
let doc = json!({"transport": "streamable-http", "headers": null,
"entries": [{"timestamp_ms": 1000, "request": req(None)}]});
assert_eq!(
detailed(&doc.to_string(), McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Malformed
);
}
#[test]
fn duplicate_case_variants_agreeing_are_present() {
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {
"MCP-Protocol-Version": V2026,
"mcp-protocol-version": V2026
}},
"entries": [{"timestamp_ms": 1000, "request": req(None)}]
});
assert_eq!(
detailed(&doc.to_string(), McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Present(V2026.into())
);
}
#[test]
fn an_oversized_header_is_not_retained_per_entry() {
let huge = "x".repeat(1024 * 1024);
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {"MCP-Protocol-Version": huge}},
"entries": [
{"timestamp_ms": 1000, "request": req(None)},
{"timestamp_ms": 1001,
"request": {"jsonrpc": "2.0", "id": "call-2", "method": "tools/call",
"params": {"name": "Calculator", "arguments": {}}}}
]
});
let parsed = detailed(&doc.to_string(), McpInputFormat::StreamableHttp);
assert_eq!(parsed.len(), 2);
for (i, event) in parsed.iter().enumerate() {
assert_eq!(
event.context.envelope,
EnvelopeObservation::Malformed,
"entry {i} must not carry the value"
);
}
}
#[test]
fn an_oversized_metadata_version_is_not_retained() {
let huge = "x".repeat(1024 * 1024);
let input = req(Some(meta(json!(huge)))).to_string();
assert_eq!(
detailed(&input, McpInputFormat::JsonRpc)[0]
.context
.request_metadata,
Some(RequestMetadata::Malformed)
);
}
#[test]
fn a_non_object_headers_node_is_malformed_not_absent() {
for doc in [
json!({"transport": "streamable-http", "headers": 7,
"entries": [{"timestamp_ms": 1000, "request": req(None)}]}),
json!({"transport": "streamable-http", "transport_context": {"headers": 7},
"entries": [{"timestamp_ms": 1000, "request": req(None)}]}),
] {
assert_eq!(
detailed(&doc.to_string(), McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Malformed,
"{doc}"
);
}
}
#[test]
fn a_non_object_result_is_malformed_not_missing() {
for bad in [json!(null), json!(7), json!([]), json!("done")] {
let message = json!({"jsonrpc": "2.0", "id": "call-1", "result": bad});
let input = framed(Some(headers(json!(V2025))), None, message);
let observed = detailed(&input, McpInputFormat::StreamableHttp)
.into_iter()
.find_map(|e| e.context.result_observation)
.expect("a response event");
assert_eq!(observed, ResultObservation::Malformed, "{bad}");
}
}
#[test]
fn an_empty_result_type_is_unrecognized_not_malformed() {
let input = framed(Some(headers(json!(V2026))), None, response(Some(json!(""))));
let observed = detailed(&input, McpInputFormat::StreamableHttp)
.into_iter()
.find_map(|e| e.context.result_observation)
.expect("a response event");
assert_eq!(observed, ResultObservation::Unrecognized);
}
#[test]
fn composite_a_null_result_under_a_legacy_era_is_invalid() {
let message = json!({"jsonrpc": "2.0", "id": "call-1", "result": null});
let input = framed(Some(headers(json!(V2025))), None, message);
let parsed = detailed(&input, McpInputFormat::StreamableHttp);
let event = parsed
.iter()
.find(|e| e.context.result_observation.is_some())
.expect("a response event");
assert_eq!(event.context.era, EraResolution::Known(V2025.into()));
assert_eq!(
event.context.result_observation,
Some(ResultObservation::Malformed)
);
assert_eq!(
conclude(
&event.context.era,
event.context.result_observation.as_ref().unwrap(),
None
),
ResultConclusion::Invalid(InvalidReason::MalformedResultType)
);
}
#[test]
fn composite_an_unknown_token_is_incomplete_and_value_free() {
let input = framed(
Some(headers(json!(V2026))),
None,
response(Some(json!("banana"))),
);
let parsed = detailed(&input, McpInputFormat::StreamableHttp);
let event = parsed
.iter()
.find(|e| e.context.result_observation.is_some())
.expect("a response event");
let conclusion = conclude(
&event.context.era,
event.context.result_observation.as_ref().unwrap(),
None,
);
assert_eq!(event.context.capability_observation, None);
assert_eq!(
conclusion,
ResultConclusion::Incomplete(IncompleteReason::RecognitionUndeterminable)
);
assert!(
!format!("{conclusion:?}").contains("banana"),
"the token must not travel: {conclusion:?}"
);
}
#[test]
fn a_hybrid_reports_request_metadata_and_no_result_observation() {
let hybrid = json!({
"jsonrpc": "2.0", "id": "call-1", "method": "tools/call",
"params": {"name": "Calculator", "arguments": {}, "_meta": meta(json!(V2026))},
"result": {"content": []}
});
let event = &detailed(&hybrid.to_string(), McpInputFormat::JsonRpc)[0];
assert_eq!(event.context.request_metadata, Some(Present(V2026.into())));
assert_eq!(event.context.result_observation, None);
}
#[test]
fn a_non_object_params_is_malformed_not_absent() {
for bad in [
json!(7),
json!("x"),
json!([]),
json!(null),
json!([{"_meta": {}}]),
] {
let message = json!({"jsonrpc": "2.0", "id": "call-1", "method": "tools/call",
"params": bad});
assert_eq!(
detailed(&message.to_string(), McpInputFormat::JsonRpc)[0]
.context
.request_metadata,
Some(RequestMetadata::Malformed),
"{bad}"
);
}
}
#[test]
fn composite_a_deviant_params_under_a_legacy_era_is_invalid() {
let message = json!({"jsonrpc": "2.0", "id": "call-1", "method": "tools/call", "params": 7});
let input = framed(Some(headers(json!(V2025))), None, message);
let event = &detailed(&input, McpInputFormat::StreamableHttp)[0];
assert_eq!(
event.context.era,
EraResolution::Unknown(UnknownReason::MalformedSignal)
);
let observed = event
.context
.request_metadata
.as_ref()
.expect("a request reports metadata");
assert_eq!(observed, &RequestMetadata::Malformed);
assert_eq!(
conclude_request(
&event.context.era,
observed,
Some(&CapabilityObservation::CoreOnly)
),
RequestAssessment::Invalid(InvalidReason::MalformedRequestMetadata)
);
}
#[test]
fn composite_a_deviant_params_at_2026_is_invalid_for_the_container_not_the_era() {
let message = json!({"jsonrpc": "2.0", "id": "call-1", "method": "tools/call", "params": []});
let input = framed(Some(headers(json!(V2026))), None, message);
let event = &detailed(&input, McpInputFormat::StreamableHttp)[0];
let observed = event
.context
.request_metadata
.as_ref()
.expect("a request reports metadata");
assert_eq!(observed, &RequestMetadata::Malformed);
assert_eq!(
conclude_request(
&event.context.era,
observed,
Some(&CapabilityObservation::CoreOnly)
),
RequestAssessment::Invalid(InvalidReason::MalformedRequestMetadata)
);
}
#[test]
fn an_explicit_null_entry_headers_slot_does_not_inherit_the_transcript_default() {
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {"MCP-Protocol-Version": V2026}},
"entries": [{"timestamp_ms": 1000, "headers": null, "request": req(None)}]
});
assert_eq!(
detailed(&doc.to_string(), McpInputFormat::StreamableHttp)[0]
.context
.envelope,
EnvelopeObservation::Malformed
);
}
#[test]
fn a_notification_is_not_held_to_the_request_metadata_requirement() {
let notification = json!({"jsonrpc": "2.0", "method": "notifications/progress",
"params": {"progressToken": "t", "progress": 1}});
let input = framed(Some(headers(json!(V2026))), None, notification);
let event = &detailed(&input, McpInputFormat::StreamableHttp)[0];
assert_eq!(event.context.era, EraResolution::Known(V2026.into()));
assert_eq!(
event.context.request_metadata, None,
"a notification has no request-metadata axis"
);
assert_eq!(event.context.result_observation, None);
}
#[test]
fn an_explicit_null_id_is_a_refused_request_not_a_notification() {
let message = json!({"jsonrpc": "2.0", "id": null, "method": "tools/call",
"params": {"name": "Calculator", "arguments": {}}});
let input = framed(Some(headers(json!(V2026))), None, message);
let err = parse_mcp_transcript(&input, McpInputFormat::StreamableHttp)
.expect_err("a request with a null id is refused");
assert!(
format!("{err:?}").contains("must be a string or a number"),
"{err:?}"
);
let notification = json!({"jsonrpc": "2.0", "method": "notifications/progress", "params": {}});
assert!(parse_mcp_transcript(
&framed(Some(headers(json!(V2026))), None, notification),
McpInputFormat::StreamableHttp
)
.is_ok());
}
#[test]
fn a_request_still_reports_the_metadata_axis() {
let input = framed(
Some(headers(json!(V2026))),
None,
req(Some(meta(json!(V2026)))),
);
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.request_metadata,
Some(Present(V2026.into()))
);
}
#[test]
fn a_non_string_method_is_refused_at_parse_time() {
for bad in [json!(7), json!({}), json!([]), json!(null), json!(true)] {
let message = json!({"jsonrpc": "2.0", "id": "call-1", "method": bad,
"params": {"name": "Calculator", "arguments": {}}});
let input = framed(Some(headers(json!(V2026))), None, message);
let err = parse_mcp_transcript(&input, McpInputFormat::StreamableHttp)
.expect_err(&format!("must refuse method {bad}"));
let rendered = err.to_string();
let (prefix, diagnostic) = rendered
.split_once(": ")
.unwrap_or_else(|| panic!("expected a located diagnostic for {bad}: {rendered}"));
let line = prefix
.strip_prefix("MCP event at source line ")
.unwrap_or_else(|| panic!("unexpected prefix for {bad}: {prefix}"));
assert!(
!line.is_empty() && line.bytes().all(|c| c.is_ascii_digit()),
"source line is not a number for {bad}: {line}"
);
assert_eq!(
diagnostic, "JSON-RPC method must be a string",
"the diagnostic must be exactly this constant, for {bad}"
);
}
}
#[test]
fn each_valid_shape_lands_on_its_own_axes() {
let request = json!({"jsonrpc": "2.0", "id": "call-1", "method": "tools/call",
"params": {"name": "Calculator", "arguments": {}}});
let notification = json!({"jsonrpc": "2.0", "method": "notifications/progress",
"params": {"progressToken": "t", "progress": 1}});
let response = json!({"jsonrpc": "2.0", "id": "call-1",
"result": {"content": [], "resultType": "complete"}});
for (label, message, meta_axis, result_axis) in [
("request", request, true, false),
("notification", notification, false, false),
("response", response, false, true),
] {
let input = framed(Some(headers(json!(V2026))), None, message);
let event = &detailed(&input, McpInputFormat::StreamableHttp)[0];
assert_eq!(
event.context.request_metadata.is_some(),
meta_axis,
"{label} metadata axis"
);
assert_eq!(
event.context.result_observation.is_some(),
result_axis,
"{label} result axis"
);
}
}
#[test]
fn the_sidecar_axes_follow_the_shared_classifier() {
let shapes = [
json!({"jsonrpc": "2.0", "id": "call-1", "method": "tools/call",
"params": {"name": "Calculator", "arguments": {}}}),
json!({"jsonrpc": "2.0", "method": "notifications/progress", "params": {}}),
json!({"jsonrpc": "2.0", "id": "call-1", "method": "", "params": {}}),
json!({"jsonrpc": "2.0", "id": "call-1", "result": {"content": [], "resultType": "complete"}}),
json!({"jsonrpc": "2.0", "id": "call-1", "error": {"code": -1, "message": "x"}}),
];
for shape in shapes {
let expected = classify_message(&shape).expect("a classifiable shape");
let input = framed(Some(headers(json!(V2026))), None, shape.clone());
let event = &detailed(&input, McpInputFormat::StreamableHttp)[0];
let (meta, result) = (
event.context.request_metadata.is_some(),
event.context.result_observation.is_some(),
);
match expected {
MessageKind::Request { .. } => assert_eq!((meta, result), (true, false), "{shape}"),
MessageKind::Notification { .. } => {
assert_eq!((meta, result), (false, false), "{shape}")
}
MessageKind::Response => assert!(!meta, "{shape}"),
}
}
}
#[test]
fn a_response_inherits_the_conflicting_era_of_its_call() {
let request = json!({"jsonrpc": "2.0", "id": "call-1", "method": "tools/call",
"params": {"name": "Calculator", "arguments": {},
"_meta": meta(json!(V2026))}});
let response = json!({"jsonrpc": "2.0", "id": "call-1", "result": {"content": []}});
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {"MCP-Protocol-Version": V2025}},
"entries": [
{"timestamp_ms": 1000, "request": request},
{"timestamp_ms": 1001, "response": response}
]
});
let events = detailed(&doc.to_string(), McpInputFormat::StreamableHttp);
let req_event = &events[0];
let resp_event = &events[1];
assert_eq!(
req_event.context.era,
EraResolution::Conflicting {
header: V2025.into(),
body: V2026.into()
},
"the request itself is contradicted"
);
assert_eq!(
resp_event.context.era,
EraResolution::Conflicting {
header: V2025.into(),
body: V2026.into()
},
"the response must inherit its call's era"
);
let observed = resp_event
.context
.result_observation
.as_ref()
.expect("a response reports a result");
assert_eq!(observed, &ResultObservation::Missing);
assert_eq!(
conclude(&resp_event.context.era, observed, None),
ResultConclusion::Invalid(InvalidReason::EraConflicting {
header: V2025.into(),
body: V2026.into()
}),
"a contradicted call cannot conclude that the action completed"
);
}
#[test]
fn an_uncorrelated_response_keeps_its_own_era() {
let response = json!({"jsonrpc": "2.0", "id": "orphan", "result": {"content": []}});
let input = framed(Some(headers(json!(V2025))), None, response);
assert_eq!(
detailed(&input, McpInputFormat::StreamableHttp)[0]
.context
.era,
EraResolution::Known(V2025.into())
);
}
#[test]
fn duplicate_members_are_refused_at_the_boundary() {
let cases = [
(
"resultType",
r#"{"transport":"streamable-http","entries":[{"response":{"jsonrpc":"2.0","id":"c","result":{"content":[],"resultType":"complete","resultType":"input_required"}}}]}"#,
),
(
"params._meta protocolVersion",
r#"{"transport":"streamable-http","entries":[{"request":{"jsonrpc":"2.0","id":"c","method":"tools/call","params":{"name":"C","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2025-06-18","io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}}]}"#,
),
(
"MCP-Protocol-Version header",
r#"{"transport":"streamable-http","transport_context":{"headers":{"MCP-Protocol-Version":"2025-06-18","MCP-Protocol-Version":"2026-07-28"}},"entries":[{"request":{"jsonrpc":"2.0","id":"c","method":"tools/call","params":{"name":"C","arguments":{}}}}]}"#,
),
];
for (label, raw) in cases {
let err = parse_mcp_transcript(raw, McpInputFormat::StreamableHttp)
.expect_err(&format!("must refuse a duplicate {label}"));
let rendered = format!("{err:?}");
assert!(
rendered.contains("duplicate member"),
"unexpected message for {label}: {rendered}"
);
assert!(
!rendered.contains("2026-07-28") && !rendered.contains("input_required"),
"refusal echoed an input value for {label}: {rendered}"
);
}
}
#[test]
fn a_duplicate_member_on_a_jsonrpc_line_is_refused() {
let line =
r#"{"jsonrpc":"2.0","id":"c","method":"tools/call","method":"tools/list","params":{}}"#;
let err = parse_mcp_transcript(line, McpInputFormat::JsonRpc).expect_err("must refuse");
assert!(format!("{err:?}").contains("duplicate member"), "{err:?}");
}
#[test]
fn distinct_members_are_not_duplicates() {
let raw = r#"{"transport":"streamable-http","transport_context":{"headers":{"MCP-Protocol-Version":"2026-07-28","X-Other":"2025-06-18"}},"entries":[{"request":{"jsonrpc":"2.0","id":"c","method":"tools/call","params":{"name":"C","arguments":{}}}}]}"#;
assert!(parse_mcp_transcript(raw, McpInputFormat::StreamableHttp).is_ok());
}
#[test]
fn duplicate_members_in_sse_data_are_refused() {
let structured = r#"{"transport":"http-sse","entries":[{"sse":{"event":"message","data":{"jsonrpc":"2.0","id":"c","result":{"content":[],"resultType":"complete","resultType":"input_required"}}}}]}"#;
let as_string = r#"{"transport":"http-sse","entries":[{"sse":{"event":"message","data":"{\"jsonrpc\":\"2.0\",\"id\":\"c\",\"result\":{\"content\":[],\"resultType\":\"complete\",\"resultType\":\"input_required\"}}"}}]}"#;
for (label, raw) in [("structured", structured), ("json string", as_string)] {
let err = parse_mcp_transcript(raw, McpInputFormat::HttpSse)
.expect_err(&format!("must refuse a duplicate in {label} SSE data"));
assert!(
format!("{err:?}").contains("duplicate member"),
"unexpected message for {label}: {err:?}"
);
}
}
#[test]
fn a_response_takes_the_era_of_the_request_that_preceded_it() {
let conflicted = json!({"jsonrpc": "2.0", "id": "reused", "method": "notifications/x",
"params": {"_meta": meta(json!(V2026))}});
let response = json!({"jsonrpc": "2.0", "id": "reused", "result": {"content": []}});
let clean = json!({"jsonrpc": "2.0", "id": "reused", "method": "notifications/x",
"params": {"_meta": meta(json!(V2025))}});
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {"MCP-Protocol-Version": V2025}},
"entries": [
{"timestamp_ms": 1000, "request": conflicted},
{"timestamp_ms": 1001, "response": response},
{"timestamp_ms": 1002, "request": clean}
]
});
let events = detailed(&doc.to_string(), McpInputFormat::StreamableHttp);
assert_eq!(
events[1].context.era,
EraResolution::Conflicting {
header: V2025.into(),
body: V2026.into()
},
"the response must not inherit a later request's era"
);
}
#[test]
fn a_response_before_any_request_stays_self_resolved() {
let response = json!({"jsonrpc": "2.0", "id": "early", "result": {"content": []}});
let request = json!({"jsonrpc": "2.0", "id": "early", "method": "notifications/x",
"params": {"_meta": meta(json!(V2026))}});
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {"MCP-Protocol-Version": V2025}},
"entries": [
{"timestamp_ms": 1000, "response": response},
{"timestamp_ms": 1001, "request": request}
]
});
let events = detailed(&doc.to_string(), McpInputFormat::StreamableHttp);
assert_eq!(events[0].context.era, EraResolution::Known(V2025.into()));
}
#[test]
fn two_outstanding_requests_on_one_id_are_refused() {
let first = json!({"jsonrpc": "2.0", "id": "dup", "method": "notifications/x", "params": {}});
let second = json!({"jsonrpc": "2.0", "id": "dup", "method": "notifications/x", "params": {}});
let doc = json!({
"transport": "streamable-http",
"entries": [
{"timestamp_ms": 1000, "request": first},
{"timestamp_ms": 1001, "request": second}
]
});
let err = parse_mcp_transcript(&doc.to_string(), McpInputFormat::StreamableHttp)
.expect_err("must refuse two outstanding requests on one id");
assert!(
format!("{err:?}").contains("outstanding"),
"unexpected message: {err:?}"
);
}
#[test]
fn sequential_id_reuse_after_a_response_is_allowed() {
let mk_req = || {
json!({"jsonrpc": "2.0", "id": "seq", "method": "notifications/x",
"params": {}})
};
let mk_resp = || json!({"jsonrpc": "2.0", "id": "seq", "result": {"content": []}});
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {"MCP-Protocol-Version": V2025}},
"entries": [
{"timestamp_ms": 1000, "request": mk_req()},
{"timestamp_ms": 1001, "response": mk_resp()},
{"timestamp_ms": 1002, "request": mk_req()},
{"timestamp_ms": 1003, "response": mk_resp()}
]
});
assert!(parse_mcp_transcript(&doc.to_string(), McpInputFormat::StreamableHttp).is_ok());
}
#[test]
fn an_error_response_inherits_the_era_of_its_call() {
let request = json!({"jsonrpc": "2.0", "id": "err", "method": "notifications/x",
"params": {"_meta": meta(json!(V2026))}});
let error = json!({"jsonrpc": "2.0", "id": "err",
"error": {"code": -32000, "message": "boom"}});
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {"MCP-Protocol-Version": V2025}},
"entries": [
{"timestamp_ms": 1000, "request": request},
{"timestamp_ms": 1001, "response": error}
]
});
let events = detailed(&doc.to_string(), McpInputFormat::StreamableHttp);
assert_eq!(
events[1].context.era,
EraResolution::Conflicting {
header: V2025.into(),
body: V2026.into()
},
"an error response must inherit its call's era"
);
assert_eq!(
events[1].context.result_observation, None,
"and still reports no result observation, since it carries no result"
);
}
#[test]
fn an_error_response_frees_the_id_for_sequential_reuse() {
let mk_req = || {
json!({"jsonrpc": "2.0", "id": "err", "method": "notifications/x",
"params": {}})
};
let error = json!({"jsonrpc": "2.0", "id": "err",
"error": {"code": -32000, "message": "boom"}});
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {"MCP-Protocol-Version": V2025}},
"entries": [
{"timestamp_ms": 1000, "request": mk_req()},
{"timestamp_ms": 1001, "response": error},
{"timestamp_ms": 1002, "request": mk_req()}
]
});
assert!(
parse_mcp_transcript(&doc.to_string(), McpInputFormat::StreamableHttp).is_ok(),
"an error response must consume the outstanding id"
);
}
#[test]
fn a_response_carrying_both_result_and_error_is_refused() {
let both = json!({"jsonrpc": "2.0", "id": "c",
"result": {"content": [], "resultType": "complete"},
"error": {"code": -32000, "message": "boom"}});
let input = framed(Some(headers(json!(V2026))), None, both);
let err = parse_mcp_transcript(&input, McpInputFormat::StreamableHttp)
.expect_err("must refuse both result and error");
let rendered = format!("{err:?}");
assert!(
rendered.contains("exactly one of result or error"),
"unexpected message: {rendered}"
);
assert!(
!rendered.contains("complete") && !rendered.contains("boom"),
"refusal echoed an input value: {rendered}"
);
}
#[test]
fn a_no_method_object_with_neither_result_nor_error_is_refused() {
let request = json!({"jsonrpc": "2.0", "id": "n", "method": "notifications/x", "params": {}});
let neither = json!({"jsonrpc": "2.0", "id": "n"});
let doc = json!({
"transport": "streamable-http",
"entries": [
{"timestamp_ms": 1000, "request": request},
{"timestamp_ms": 1001, "response": neither}
]
});
let err = parse_mcp_transcript(&doc.to_string(), McpInputFormat::StreamableHttp)
.expect_err("must refuse a response that is neither");
assert!(
format!("{err:?}").contains("exactly one of result or error"),
"unexpected message: {err:?}"
);
}
#[test]
fn valid_success_and_error_responses_still_correlate() {
for response in [
json!({"jsonrpc": "2.0", "id": "ok", "result": {"content": [], "resultType": "complete"}}),
json!({"jsonrpc": "2.0", "id": "ok", "error": {"code": -1, "message": "x"}}),
] {
let request = json!({"jsonrpc": "2.0", "id": "ok", "method": "notifications/x",
"params": {"_meta": meta(json!(V2026))}});
let doc = json!({
"transport": "streamable-http",
"transport_context": {"headers": {"MCP-Protocol-Version": V2025}},
"entries": [
{"timestamp_ms": 1000, "request": request},
{"timestamp_ms": 1001, "response": response.clone()}
]
});
let events = detailed(&doc.to_string(), McpInputFormat::StreamableHttp);
assert_eq!(
events[1].context.era,
EraResolution::Conflicting {
header: V2025.into(),
body: V2026.into()
},
"must still inherit for {response}"
);
}
}
fn two_entry_doc(first: (Value, &str), second: (Value, &str)) -> String {
let entry = |m: Value, v: &str, ts: u64| {
let key = if m.get("method").is_some() {
"request"
} else {
"response"
};
json!({"timestamp_ms": ts, "transport_context": headers(json!(v)), key: m})
};
json!({
"transport": "streamable-http",
"entries": [entry(first.0, first.1, 1000), entry(second.0, second.1, 1001)]
})
.to_string()
}
#[test]
fn a_numeric_id_does_not_correlate_with_a_string_id() {
let request = json!({"jsonrpc": "2.0", "id": 1, "method": "notifications/x", "params": {}});
let response = json!({"jsonrpc": "2.0", "id": "1", "result": {"content": []}});
let events = detailed(
&two_entry_doc((request, V2025), (response, V2026)),
McpInputFormat::StreamableHttp,
);
assert_eq!(
events[1].context.era,
EraResolution::Known(V2026.into()),
"the response must stay orphaned and keep its own era"
);
let observed = events[1]
.context
.result_observation
.as_ref()
.expect("a response reports a result");
assert_eq!(
conclude(&events[1].context.era, observed, None),
ResultConclusion::Invalid(InvalidReason::MissingResultType)
);
}
#[test]
fn a_string_id_does_not_correlate_with_a_numeric_id() {
let request = json!({"jsonrpc": "2.0", "id": "1", "method": "notifications/x", "params": {}});
let response = json!({"jsonrpc": "2.0", "id": 1, "result": {"content": []}});
let events = detailed(
&two_entry_doc((request, V2025), (response, V2026)),
McpInputFormat::StreamableHttp,
);
assert_eq!(events[1].context.era, EraResolution::Known(V2026.into()));
}
#[test]
fn same_typed_ids_still_correlate() {
for id in [json!(1), json!("1")] {
let request = json!({"jsonrpc": "2.0", "id": id, "method": "notifications/x",
"params": {}});
let response = json!({"jsonrpc": "2.0", "id": id, "result": {"content": []}});
let events = detailed(
&two_entry_doc((request, V2025), (response, V2026)),
McpInputFormat::StreamableHttp,
);
assert_eq!(
events[1].context.era,
EraResolution::Known(V2025.into()),
"id {id} must correlate to its own call"
);
}
}
#[test]
fn outstanding_ids_are_tracked_per_typed_key() {
let numeric = json!({"jsonrpc": "2.0", "id": 1, "method": "notifications/x", "params": {}});
let stringy = json!({"jsonrpc": "2.0", "id": "1", "method": "notifications/x", "params": {}});
assert!(
parse_mcp_transcript(
&two_entry_doc((numeric.clone(), V2025), (stringy, V2025)),
McpInputFormat::StreamableHttp
)
.is_ok(),
"a numeric and a string id are different calls"
);
let err = parse_mcp_transcript(
&two_entry_doc((numeric.clone(), V2025), (numeric, V2025)),
McpInputFormat::StreamableHttp,
)
.expect_err("two numeric 1s are one id twice");
let rendered = format!("{err:?}");
assert!(rendered.contains("outstanding"), "unexpected: {rendered}");
assert!(
!rendered.contains("notifications/x"),
"refusal echoed input: {rendered}"
);
}
#[test]
fn a_null_request_id_is_refused() {
let message = json!({"jsonrpc": "2.0", "id": null, "method": "tools/call",
"params": {"name": "Calculator", "arguments": {}}});
let input = framed(Some(headers(json!(V2026))), None, message);
let err = parse_mcp_transcript(&input, McpInputFormat::StreamableHttp)
.expect_err("must refuse a null request id");
let rendered = format!("{err:?}");
assert!(
rendered.contains("must be a string or a number"),
"unexpected: {rendered}"
);
assert!(
!rendered.contains("Calculator"),
"refusal echoed input: {rendered}"
);
}
#[test]
fn a_notification_may_have_no_id() {
let message = json!({"jsonrpc": "2.0", "method": "notifications/progress", "params": {}});
let input = framed(Some(headers(json!(V2026))), None, message);
assert!(parse_mcp_transcript(&input, McpInputFormat::StreamableHttp).is_ok());
}
#[test]
fn a_success_response_must_carry_an_acceptable_id() {
for bad in [Some(json!(null)), None] {
let mut message = json!({"jsonrpc": "2.0", "result": {"content": [],
"resultType": "complete"}});
if let Some(id) = &bad {
message["id"] = id.clone();
}
let input = framed(Some(headers(json!(V2026))), None, message);
assert!(
parse_mcp_transcript(&input, McpInputFormat::StreamableHttp).is_err(),
"must refuse a success response with id {bad:?}"
);
}
}
#[test]
fn an_invalid_request_error_response_may_have_no_usable_id() {
for id in [Some(json!(null)), None] {
let mut message = json!({"jsonrpc": "2.0",
"error": {"code": -32600, "message": "Invalid Request"}});
if let Some(id) = &id {
message["id"] = id.clone();
}
let input = framed(Some(headers(json!(V2025))), None, message);
let events = detailed(&input, McpInputFormat::StreamableHttp);
assert_eq!(events.len(), 1, "must remain ingestible for id {id:?}");
assert_eq!(
events[0].context.era,
EraResolution::Known(V2025.into()),
"and stay uncorrelated, keeping its own era"
);
}
}
#[test]
fn negative_and_large_integers_are_accepted_ids() {
for id in [json!(-1), json!(i64::MIN), json!(u64::MAX)] {
let request = json!({"jsonrpc": "2.0", "id": id, "method": "notifications/x",
"params": {}});
let response = json!({"jsonrpc": "2.0", "id": id, "result": {"content": []}});
let events = detailed(
&two_entry_doc((request, V2025), (response, V2026)),
McpInputFormat::StreamableHttp,
);
assert_eq!(
events[1].context.era,
EraResolution::Known(V2025.into()),
"id {id} must be accepted and correlate"
);
}
}
#[test]
fn an_unacceptable_response_id_can_never_license_terminal() {
let request = json!({"jsonrpc": "2.0", "id": 1, "method": "notifications/x", "params": {}});
let response = json!({"jsonrpc": "2.0", "id": null, "result": {"content": []}});
let doc = two_entry_doc((request, V2025), (response, V2025));
assert!(
parse_mcp_transcript(&doc, McpInputFormat::StreamableHttp).is_err(),
"a null response id is refused rather than silently correlated"
);
}
#[test]
fn a_request_only_method_without_an_id_is_refused() {
for method in ["tools/call", "tools/list"] {
let message = json!({"jsonrpc": "2.0", "method": method,
"params": {"name": "Calculator", "arguments": {}}});
let input = framed(Some(headers(json!(V2026))), None, message);
let err = parse_mcp_transcript(&input, McpInputFormat::StreamableHttp)
.expect_err(&format!("{method} without an id must be refused"));
let rendered = format!("{err:?}");
assert!(
rendered.contains("must be a string or a number"),
"unexpected for {method}: {rendered}"
);
assert!(
!rendered.contains("Calculator"),
"refusal echoed input: {rendered}"
);
}
}
#[test]
fn real_and_extension_notifications_still_need_no_id() {
for method in ["notifications/progress", "x-vendor/telemetry"] {
let message = json!({"jsonrpc": "2.0", "method": method, "params": {}});
let input = framed(Some(headers(json!(V2026))), None, message);
assert!(
parse_mcp_transcript(&input, McpInputFormat::StreamableHttp).is_ok(),
"{method} must remain ingestible without an id"
);
}
}
#[test]
fn an_acceptable_integer_id_does_license_terminal_under_a_legacy_era() {
let request = json!({"jsonrpc": "2.0", "id": 1, "method": "notifications/x", "params": {}});
let response = json!({"jsonrpc": "2.0", "id": 1, "result": {"content": []}});
let events = detailed(
&two_entry_doc((request, V2025), (response, V2025)),
McpInputFormat::StreamableHttp,
);
let observed = events[1]
.context
.result_observation
.as_ref()
.expect("a response reports a result");
assert_eq!(observed, &ResultObservation::Missing);
assert_eq!(
conclude(&events[1].context.era, observed, None),
ResultConclusion::Terminal,
"a legacy era with an absent resultType is Terminal, which is the state the refusal protects"
);
}
#[test]
fn duplicate_members_on_the_transcript_wrapper_are_refused() {
let raw = r#"{"transport":"streamable-http","x":1,"x":2,"entries":[{"request":{"jsonrpc":"2.0","id":"c","method":"tools/call","params":{"name":"C","arguments":{}}}}]}"#;
let err = parse_mcp_transcript(raw, McpInputFormat::StreamableHttp)
.expect_err("a duplicate on the transcript wrapper is refused");
assert!(format!("{err:?}").contains("duplicate member"), "{err:?}");
}
#[test]
fn duplicate_members_on_an_entry_are_refused() {
let raw = r#"{"transport":"streamable-http","entries":[{"y":1,"y":2,"request":{"jsonrpc":"2.0","id":"c","method":"tools/call","params":{"name":"C","arguments":{}}}}]}"#;
let err = parse_mcp_transcript(raw, McpInputFormat::StreamableHttp)
.expect_err("a duplicate on an entry is refused");
assert!(format!("{err:?}").contains("duplicate member"), "{err:?}");
}
fn raw_pair(req_id: &str, req_ver: &str, resp_id: &str, resp_ver: &str) -> String {
format!(
r#"{{"transport":"streamable-http","entries":[
{{"transport_context":{{"headers":{{"MCP-Protocol-Version":"{req_ver}"}}}},
"request":{{"jsonrpc":"2.0","id":{req_id},"method":"notifications/x","params":{{}}}}}},
{{"transport_context":{{"headers":{{"MCP-Protocol-Version":"{resp_ver}"}}}},
"response":{{"jsonrpc":"2.0","id":{resp_id},"result":{{"content":[]}}}}}}]}}"#
)
}
#[test]
fn exactly_representable_integer_ids_correlate() {
for id in ["1", "0", "-1", "9007199254740993"] {
let events = detailed(
&raw_pair(id, V2025, id, V2026),
McpInputFormat::StreamableHttp,
);
assert_eq!(
events[1].context.era,
EraResolution::Known(V2025.into()),
"id {id} must correlate to its own call"
);
}
}
#[test]
fn a_non_representable_number_id_does_not_correlate() {
for id in ["1.0", "1e0", "1.5", "-0.0"] {
let events = detailed(
&raw_pair(id, V2025, id, V2026),
McpInputFormat::StreamableHttp,
);
assert_eq!(
events[1].context.era,
EraResolution::Known(V2026.into()),
"id {id} must not correlate, and keeps its own envelope era"
);
}
}
#[test]
fn decimal_ids_beyond_two_to_the_53_never_land_on_one_call() {
let events = detailed(
&raw_pair("9007199254740992.0", V2025, "9007199254740993.0", V2026),
McpInputFormat::StreamableHttp,
);
assert_eq!(events[1].context.era, EraResolution::Known(V2026.into()));
}
#[test]
fn a_string_id_never_correlates_with_a_numeric_one() {
let events = detailed(
&raw_pair("1", V2025, "\"1\"", V2026),
McpInputFormat::StreamableHttp,
);
assert_eq!(events[1].context.era, EraResolution::Known(V2026.into()));
}
#[test]
fn integers_beyond_two_to_the_53_never_collide() {
let events = detailed(
&raw_pair("9007199254740992", V2025, "9007199254740993", V2026),
McpInputFormat::StreamableHttp,
);
assert_eq!(
events[1].context.era,
EraResolution::Known(V2026.into()),
"9007199254740992 and ...93 are different ids"
);
}
fn tools_call(id: &str) -> String {
format!(
r#"{{"jsonrpc":"2.0","id":{id},"method":"tools/call","params":{{"name":"C","arguments":{{}}}}}}"#
)
}
fn jsonrpc_lines(lines: &[String]) -> String {
lines.join("\n")
}
#[test]
fn a_numeric_and_a_string_tools_call_id_may_both_be_outstanding() {
let input = jsonrpc_lines(&[tools_call("1"), tools_call("\"1\"")]);
assert!(
parse_mcp_transcript(&input, McpInputFormat::JsonRpc).is_ok(),
"number 1 and string \"1\" are different calls"
);
}
#[test]
fn a_tools_call_id_may_be_reused_after_its_response() {
let response = r#"{"jsonrpc":"2.0","id":1,"result":{"content":[]}}"#.to_string();
let input = jsonrpc_lines(&[tools_call("1"), response, tools_call("1")]);
assert!(
parse_mcp_transcript(&input, McpInputFormat::JsonRpc).is_ok(),
"an id is free once its call has been answered"
);
}
#[test]
fn two_outstanding_tools_calls_on_one_id_are_refused() {
let input = jsonrpc_lines(&[tools_call("1"), tools_call("1")]);
let err = parse_mcp_transcript(&input, McpInputFormat::JsonRpc)
.expect_err("one id, two calls in flight");
let rendered = format!("{err:?}");
assert!(rendered.contains("outstanding"), "unexpected: {rendered}");
assert!(
!rendered.contains("\"C\""),
"refusal echoed input: {rendered}"
);
}
#[test]
fn a_duplicate_inside_an_unknown_transcript_subtree_is_refused() {
let raw = r#"{"transport":"streamable-http","junk":{"a":1,"a":2},"entries":[{"request":{"jsonrpc":"2.0","id":"c","method":"tools/call","params":{"name":"C","arguments":{}}}}]}"#;
assert!(
format!(
"{:?}",
parse_mcp_transcript(raw, McpInputFormat::StreamableHttp).unwrap_err()
)
.contains("duplicate member"),
"a duplicate inside an ignored subtree must still be refused"
);
}
#[test]
fn a_duplicate_inside_an_unknown_entry_subtree_is_refused() {
let raw = r#"{"transport":"streamable-http","entries":[{"junk":{"b":1,"b":2},"request":{"jsonrpc":"2.0","id":"c","method":"tools/call","params":{"name":"C","arguments":{}}}}]}"#;
assert!(
format!(
"{:?}",
parse_mcp_transcript(raw, McpInputFormat::StreamableHttp).unwrap_err()
)
.contains("duplicate member"),
"an ignored entry subtree must still be duplicate-aware"
);
}
#[test]
fn duplicate_members_on_the_sse_envelope_are_refused() {
for raw in [
r#"{"transport":"http-sse","entries":[{"sse":{"event":"message","event":"other","data":{}}}]}"#,
r#"{"transport":"http-sse","entries":[{"sse":{"event":"message","junk":{"c":1,"c":2},"data":{}}}]}"#,
] {
assert!(
format!(
"{:?}",
parse_mcp_transcript(raw, McpInputFormat::HttpSse).unwrap_err()
)
.contains("duplicate member"),
"SSE envelope must be duplicate-aware: {raw}"
);
}
}
#[test]
fn an_explicit_null_sse_slot_is_malformed_not_absent() {
for raw in [
r#"{"transport":"http-sse","entries":[{"sse":null}]}"#,
r#"{"transport":"http-sse","entries":[{"request":{"jsonrpc":"2.0","id":"c","method":"tools/call","params":{"name":"C","arguments":{}}},"sse":null}]}"#,
] {
let err = parse_mcp_transcript(raw, McpInputFormat::HttpSse).expect_err(&format!(
"an explicit null sse slot must not fold to absent: {raw}"
));
assert!(
format!("{err:?}").contains("sse slot is present and null"),
"refused for the wrong reason: {err:?}"
);
}
}
#[test]
fn every_known_request_method_requires_an_id() {
let expected = [
"completion/complete",
"elicitation/create",
"initialize",
"logging/setLevel",
"ping",
"prompts/get",
"prompts/list",
"resources/list",
"resources/read",
"resources/subscribe",
"resources/templates/list",
"resources/unsubscribe",
"roots/list",
"sampling/createMessage",
"server/discover",
"subscriptions/listen",
"tasks/cancel",
"tasks/get",
"tasks/list",
"tasks/result",
"tools/call",
"tools/list",
];
let mut declared: Vec<&str> = crate::mcp::era::REQUEST_ONLY_METHODS.to_vec();
let mut listed: Vec<&str> = expected.to_vec();
declared.sort_unstable();
listed.sort_unstable();
assert_eq!(declared, listed, "the table and the constant have drifted");
for method in expected {
let message = json!({"jsonrpc": "2.0", "method": method, "params": {}});
let input = framed(Some(headers(json!(V2026))), None, message);
assert!(
parse_mcp_transcript(&input, McpInputFormat::StreamableHttp).is_err(),
"{method} without an id must be refused"
);
}
}
#[test]
fn an_unknown_extension_method_stays_a_notification() {
for method in ["x-vendor/telemetry", "experimental/whatever", "vendor.ping"] {
let message = json!({"jsonrpc": "2.0", "method": method, "params": {}});
let input = framed(Some(headers(json!(V2026))), None, message);
assert!(
parse_mcp_transcript(&input, McpInputFormat::StreamableHttp).is_ok(),
"{method} must remain ingestible without an id"
);
}
}
#[test]
fn an_sse_frame_without_data_is_refused() {
let raw = r#"{"transport":"http-sse","entries":[{"sse":{"event":"message"}}]}"#;
let err = parse_mcp_transcript(raw, McpInputFormat::HttpSse)
.expect_err("an SSE frame with no data must be refused");
assert!(
format!("{err:?}").contains("data"),
"refused for the wrong reason: {err:?}"
);
}
#[test]
fn the_framed_helper_places_messages_by_their_kind() {
let cases = [
(
"error-only response",
json!({"jsonrpc": "2.0", "id": "c", "error": {"code": -1, "message": "x"}}),
"response",
),
(
"success response",
json!({"jsonrpc": "2.0", "id": "c", "result": {"content": []}}),
"response",
),
(
"request",
json!({"jsonrpc": "2.0", "id": "c", "method": "tools/call",
"params": {"name": "C", "arguments": {}}}),
"request",
),
(
"notification",
json!({"jsonrpc": "2.0", "method": "notifications/progress", "params": {}}),
"request",
),
(
"non-string method",
json!({"jsonrpc": "2.0", "id": "c", "method": 7,
"params": {"name": "C", "arguments": {}}}),
"request",
),
];
for (label, message, slot) in cases {
let doc: Value =
serde_json::from_str(&framed(None, None, message)).expect("the helper emits JSON");
let entry = &doc["entries"][0];
assert!(
entry.get(slot).is_some(),
"{label} belongs in the {slot} slot: {entry}"
);
}
}