feat(mcp): emit SEP-2549 TTL on every protocol version

List results (tools/list, resources/list, resources/read) carry ttlMs and cacheScope on the legacy 2025-11-25 path too; legacy clients ignore unknown fields per the SEP's backward-compat rule. The modern envelope (resultType, _meta.serverInfo) stays negotiated. Legacy-shape pin updated deliberately to assert presence.
This commit is contained in:
Karim Jerbi committed 2026-10-04 17:00:38 +01:00
1 parent d31b9d1a35
commit 71de9f844c
1 file changed
+34 -5
+34 -5
View File
@@ -2110,6 +2110,15 @@ fn poll_task(
}
fn protocol_result(mut result: Value, modern: bool, cacheable: bool) -> Value {
// SEP-2549 freshness hints are version-independent: legacy clients
// ignore unknown fields, so list results always carry them. The modern
// envelope (resultType/_meta) stays negotiated.
if cacheable {
if let Some(object) = result.as_object_mut() {
object.insert("ttlMs".into(), Value::from(CACHE_TTL_MS));
object.insert("cacheScope".into(), Value::String("public".into()));
}
}
if modern {
let object = result
.as_object_mut()
@@ -2121,10 +2130,6 @@ fn protocol_result(mut result: Value, modern: bool, cacheable: bool) -> Value {
"_meta".into(),
json!({"io.modelcontextprotocol/serverInfo":server_info()}),
);
if cacheable {
object.insert("ttlMs".into(), Value::from(CACHE_TTL_MS));
object.insert("cacheScope".into(), Value::String("public".into()));
}
}
result
}
@@ -2550,7 +2555,11 @@ mod tests {
)
.unwrap();
assert_eq!(listed["result"]["tools"].as_array().unwrap().len(), 4);
assert!(listed["result"].get("ttlMs").is_none());
// SEP-2549: freshness hints ship on every protocol version; only
// the modern envelope stays negotiated (see
// legacy_list_results_carry_sep2549_ttl).
assert_eq!(listed["result"]["ttlMs"], CACHE_TTL_MS);
assert_eq!(listed["result"]["cacheScope"], "public");
assert!(listed["result"].get("resultType").is_none());
}
@@ -2590,6 +2599,26 @@ mod tests {
assert_eq!(listed["result"]["cacheScope"], "public");
}
#[test]
fn legacy_list_results_carry_sep2549_ttl() {
// SEP-2549 makes ttlMs mandatory on list results; legacy clients
// ignore unknown fields, so the stamp is version-independent while
// the modern envelope (resultType/_meta) stays negotiated.
for method in ["tools/list", "resources/list"] {
let listed = handle_message(
json!({"jsonrpc":"2.0","id":method,"method":method}),
&mut HashMap::new(),
&mut TaskStore::default(),
&mut ResourceStore::default(),
)
.unwrap();
assert_eq!(listed["result"]["ttlMs"], CACHE_TTL_MS, "{method}");
assert_eq!(listed["result"]["cacheScope"], "public", "{method}");
assert!(listed["result"].get("resultType").is_none(), "{method}");
assert!(listed["result"].get("_meta").is_none(), "{method}");
}
}
#[test]
fn read_tool_rejects_mutations_before_connecting() {
let mut clients = HashMap::new();