Skip to content

Commit f8edae7

Browse files
committed
feat: implement SEP-2549 cache hints
1 parent f1ef2ec commit f8edae7

2 files changed

Lines changed: 101 additions & 3 deletions

File tree

crates/rmcp/src/model.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,18 @@ pub type ProgressNotification = Notification<ProgressNotificationMethod, Progres
11421142

11431143
pub type Cursor = String;
11441144

1145+
/// Scope describing who may cache cacheable list/read results.
1146+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
1147+
#[serde(rename_all = "camelCase")]
1148+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1149+
#[non_exhaustive]
1150+
pub enum CacheScope {
1151+
/// The response may be cached for the current user.
1152+
User,
1153+
/// The response may be shared by clients with equivalent authorization.
1154+
Shared,
1155+
}
1156+
11451157
macro_rules! paginated_result {
11461158
($t:ident {
11471159
$i_item: ident: $t_item: ty
@@ -1159,15 +1171,34 @@ macro_rules! paginated_result {
11591171
}
11601172

11611173
impl $t {
1162-
pub fn with_all_items(
1163-
items: $t_item,
1164-
) -> Self {
1174+
pub fn with_all_items(items: $t_item) -> Self {
11651175
Self {
11661176
meta: None,
11671177
next_cursor: None,
11681178
$i_item: items,
11691179
}
11701180
}
1181+
1182+
/// Set the time, in milliseconds, that this result may be treated as fresh.
1183+
pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self {
1184+
let meta = self.meta.get_or_insert_with(Meta::new);
1185+
meta.insert("ttlMs".to_string(), Value::Number(ttl_ms.into()));
1186+
self
1187+
}
1188+
1189+
/// Set the cache scope for this result.
1190+
pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self {
1191+
let meta = self.meta.get_or_insert_with(Meta::new);
1192+
let cache_scope = match cache_scope {
1193+
CacheScope::User => "user",
1194+
CacheScope::Shared => "shared",
1195+
};
1196+
meta.insert(
1197+
"cacheScope".to_string(),
1198+
Value::String(cache_scope.to_string()),
1199+
);
1200+
self
1201+
}
11711202
}
11721203
};
11731204
}
@@ -1239,6 +1270,7 @@ pub type ReadResourceRequestParam = ReadResourceRequestParams;
12391270

12401271
/// Result containing the contents of a read resource
12411272
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1273+
#[serde(rename_all = "camelCase")]
12421274
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
12431275
#[non_exhaustive]
12441276
pub struct ReadResourceResult {
@@ -1251,6 +1283,37 @@ impl ReadResourceResult {
12511283
pub fn new(contents: Vec<ResourceContents>) -> Self {
12521284
Self { contents }
12531285
}
1286+
1287+
/// Set the time, in milliseconds, that this result may be treated as fresh.
1288+
pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self {
1289+
self.contents.iter_mut().for_each(|content| match content {
1290+
ResourceContents::TextResourceContents { meta, .. }
1291+
| ResourceContents::BlobResourceContents { meta, .. } => {
1292+
let meta = meta.get_or_insert_with(Meta::new);
1293+
meta.insert("ttlMs".to_string(), Value::Number(ttl_ms.into()));
1294+
}
1295+
});
1296+
self
1297+
}
1298+
1299+
/// Set the cache scope for this result.
1300+
pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self {
1301+
let cache_scope = match cache_scope {
1302+
CacheScope::User => "user",
1303+
CacheScope::Shared => "shared",
1304+
};
1305+
self.contents.iter_mut().for_each(|content| match content {
1306+
ResourceContents::TextResourceContents { meta, .. }
1307+
| ResourceContents::BlobResourceContents { meta, .. } => {
1308+
let meta = meta.get_or_insert_with(Meta::new);
1309+
meta.insert(
1310+
"cacheScope".to_string(),
1311+
Value::String(cache_scope.to_string()),
1312+
);
1313+
}
1314+
});
1315+
self
1316+
}
12541317
}
12551318

12561319
/// Request to read a specific resource
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use rmcp::model::{CacheScope, ListToolsResult, ReadResourceResult, ResourceContents};
2+
use serde_json::json;
3+
4+
#[test]
5+
fn paginated_results_serialize_cache_hints_in_meta() {
6+
let result = ListToolsResult::with_all_items(Vec::new())
7+
.with_ttl_ms(5_000)
8+
.with_cache_scope(CacheScope::User);
9+
10+
let actual = serde_json::to_value(result).expect("serialize list tools result");
11+
12+
assert_eq!(
13+
actual,
14+
json!({
15+
"_meta": {
16+
"ttlMs": 5000,
17+
"cacheScope": "user"
18+
},
19+
"tools": []
20+
})
21+
);
22+
}
23+
24+
#[test]
25+
fn read_resource_results_serialize_cache_hints_in_content_meta() {
26+
let result =
27+
ReadResourceResult::new(vec![ResourceContents::text("hello", "file:///example.txt")])
28+
.with_ttl_ms(10_000)
29+
.with_cache_scope(CacheScope::Shared);
30+
31+
let actual = serde_json::to_value(result).expect("serialize read resource result");
32+
33+
assert_eq!(actual["contents"][0]["_meta"]["ttlMs"], 10000);
34+
assert_eq!(actual["contents"][0]["_meta"]["cacheScope"], "shared");
35+
}

0 commit comments

Comments
 (0)