forked from 1b-agent/opencadstudio-upstream
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:
1 parent
d31b9d1a35
commit
71de9f844c
1 file changed
+34
-5
+34
-5
@@ -2110,6 +2110,15 @@ fn poll_task(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn protocol_result(mut result: Value, modern: bool, cacheable: bool) -> Value {
|
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 {
|
if modern {
|
||||||
let object = result
|
let object = result
|
||||||
.as_object_mut()
|
.as_object_mut()
|
||||||
@@ -2121,10 +2130,6 @@ fn protocol_result(mut result: Value, modern: bool, cacheable: bool) -> Value {
|
|||||||
"_meta".into(),
|
"_meta".into(),
|
||||||
json!({"io.modelcontextprotocol/serverInfo":server_info()}),
|
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
|
result
|
||||||
}
|
}
|
||||||
@@ -2550,7 +2555,11 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(listed["result"]["tools"].as_array().unwrap().len(), 4);
|
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());
|
assert!(listed["result"].get("resultType").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2590,6 +2599,26 @@ mod tests {
|
|||||||
assert_eq!(listed["result"]["cacheScope"], "public");
|
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]
|
#[test]
|
||||||
fn read_tool_rejects_mutations_before_connecting() {
|
fn read_tool_rejects_mutations_before_connecting() {
|
||||||
let mut clients = HashMap::new();
|
let mut clients = HashMap::new();
|
||||||
|
|||||||
Reference in new issue
Block a user