Skip to content

Commit 7175cdf

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

4 files changed

Lines changed: 97 additions & 4 deletions

File tree

crates/rmcp-macros/src/prompt_handler.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ pub fn prompt_handler(attr: TokenStream, input: TokenStream) -> syn::Result<Toke
6464
prompts,
6565
meta: #meta,
6666
next_cursor: None,
67+
ttl_ms: None,
68+
cache_scope: None,
6769
})
6870
}
6971
};

crates/rmcp-macros/src/tool_handler.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ pub fn tool_handler(attr: TokenStream, input: TokenStream) -> syn::Result<TokenS
7272
tools: #router.list_all(),
7373
meta: #result_meta,
7474
next_cursor: None,
75+
ttl_ms: None,
76+
cache_scope: None,
7577
})
7678
}
7779
})?;

crates/rmcp/src/model.rs

Lines changed: 55 additions & 4 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
@@ -1155,19 +1167,35 @@ macro_rules! paginated_result {
11551167
pub meta: Option<Meta>,
11561168
#[serde(skip_serializing_if = "Option::is_none")]
11571169
pub next_cursor: Option<Cursor>,
1170+
#[serde(skip_serializing_if = "Option::is_none")]
1171+
pub ttl_ms: Option<u64>,
1172+
#[serde(skip_serializing_if = "Option::is_none")]
1173+
pub cache_scope: Option<CacheScope>,
11581174
pub $i_item: $t_item,
11591175
}
11601176

11611177
impl $t {
1162-
pub fn with_all_items(
1163-
items: $t_item,
1164-
) -> Self {
1178+
pub fn with_all_items(items: $t_item) -> Self {
11651179
Self {
11661180
meta: None,
11671181
next_cursor: None,
1182+
ttl_ms: None,
1183+
cache_scope: None,
11681184
$i_item: items,
11691185
}
11701186
}
1187+
1188+
/// Set the time, in milliseconds, that this result may be treated as fresh.
1189+
pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self {
1190+
self.ttl_ms = Some(ttl_ms);
1191+
self
1192+
}
1193+
1194+
/// Set the cache scope for this result.
1195+
pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self {
1196+
self.cache_scope = Some(cache_scope);
1197+
self
1198+
}
11711199
}
11721200
};
11731201
}
@@ -1239,17 +1267,40 @@ pub type ReadResourceRequestParam = ReadResourceRequestParams;
12391267

12401268
/// Result containing the contents of a read resource
12411269
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1270+
#[serde(rename_all = "camelCase")]
12421271
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
12431272
#[non_exhaustive]
12441273
pub struct ReadResourceResult {
12451274
/// The actual content of the resource
12461275
pub contents: Vec<ResourceContents>,
1276+
/// Time, in milliseconds, that this result may be treated as fresh.
1277+
#[serde(skip_serializing_if = "Option::is_none")]
1278+
pub ttl_ms: Option<u64>,
1279+
/// Scope describing who may cache this result.
1280+
#[serde(skip_serializing_if = "Option::is_none")]
1281+
pub cache_scope: Option<CacheScope>,
12471282
}
12481283

12491284
impl ReadResourceResult {
12501285
/// Create a new ReadResourceResult with the given contents.
12511286
pub fn new(contents: Vec<ResourceContents>) -> Self {
1252-
Self { contents }
1287+
Self {
1288+
contents,
1289+
ttl_ms: None,
1290+
cache_scope: None,
1291+
}
1292+
}
1293+
1294+
/// Set the time, in milliseconds, that this result may be treated as fresh.
1295+
pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self {
1296+
self.ttl_ms = Some(ttl_ms);
1297+
self
1298+
}
1299+
1300+
/// Set the cache scope for this result.
1301+
pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self {
1302+
self.cache_scope = Some(cache_scope);
1303+
self
12531304
}
12541305
}
12551306

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use rmcp::model::{CacheScope, ListToolsResult, ReadResourceResult};
2+
use serde_json::json;
3+
4+
#[test]
5+
fn paginated_results_serialize_ttl_and_cache_scope() {
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+
"tools": [],
16+
"ttlMs": 5000,
17+
"cacheScope": "user"
18+
})
19+
);
20+
}
21+
22+
#[test]
23+
fn read_resource_results_serialize_ttl_and_cache_scope() {
24+
let result = ReadResourceResult::new(Vec::new())
25+
.with_ttl_ms(10_000)
26+
.with_cache_scope(CacheScope::Shared);
27+
28+
let actual = serde_json::to_value(result).expect("serialize read resource result");
29+
30+
assert_eq!(
31+
actual,
32+
json!({
33+
"contents": [],
34+
"ttlMs": 10000,
35+
"cacheScope": "shared"
36+
})
37+
);
38+
}

0 commit comments

Comments
 (0)