diff --git a/crates/rmcp/src/model.rs b/crates/rmcp/src/model.rs index 4aabab1d..a263618e 100644 --- a/crates/rmcp/src/model.rs +++ b/crates/rmcp/src/model.rs @@ -1142,6 +1142,18 @@ pub type ProgressNotification = Notification Self { + pub fn with_all_items(items: $t_item) -> Self { Self { meta: None, next_cursor: None, $i_item: items, } } + + /// Set the time, in milliseconds, that this result may be treated as fresh. + pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self { + let meta = self.meta.get_or_insert_with(Meta::new); + meta.insert("ttlMs".to_string(), Value::Number(ttl_ms.into())); + self + } + + /// Set the cache scope for this result. + pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self { + let meta = self.meta.get_or_insert_with(Meta::new); + let cache_scope = match cache_scope { + CacheScope::User => "user", + CacheScope::Shared => "shared", + }; + meta.insert( + "cacheScope".to_string(), + Value::String(cache_scope.to_string()), + ); + self + } } }; } @@ -1239,6 +1270,7 @@ pub type ReadResourceRequestParam = ReadResourceRequestParams; /// Result containing the contents of a read resource #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "camelCase")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[non_exhaustive] pub struct ReadResourceResult { @@ -1251,6 +1283,37 @@ impl ReadResourceResult { pub fn new(contents: Vec) -> Self { Self { contents } } + + /// Set the time, in milliseconds, that this result may be treated as fresh. + pub fn with_ttl_ms(mut self, ttl_ms: u64) -> Self { + self.contents.iter_mut().for_each(|content| match content { + ResourceContents::TextResourceContents { meta, .. } + | ResourceContents::BlobResourceContents { meta, .. } => { + let meta = meta.get_or_insert_with(Meta::new); + meta.insert("ttlMs".to_string(), Value::Number(ttl_ms.into())); + } + }); + self + } + + /// Set the cache scope for this result. + pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self { + let cache_scope = match cache_scope { + CacheScope::User => "user", + CacheScope::Shared => "shared", + }; + self.contents.iter_mut().for_each(|content| match content { + ResourceContents::TextResourceContents { meta, .. } + | ResourceContents::BlobResourceContents { meta, .. } => { + let meta = meta.get_or_insert_with(Meta::new); + meta.insert( + "cacheScope".to_string(), + Value::String(cache_scope.to_string()), + ); + } + }); + self + } } /// Request to read a specific resource diff --git a/crates/rmcp/tests/test_cache_hints.rs b/crates/rmcp/tests/test_cache_hints.rs new file mode 100644 index 00000000..a18fe553 --- /dev/null +++ b/crates/rmcp/tests/test_cache_hints.rs @@ -0,0 +1,35 @@ +use rmcp::model::{CacheScope, ListToolsResult, ReadResourceResult, ResourceContents}; +use serde_json::json; + +#[test] +fn paginated_results_serialize_cache_hints_in_meta() { + let result = ListToolsResult::with_all_items(Vec::new()) + .with_ttl_ms(5_000) + .with_cache_scope(CacheScope::User); + + let actual = serde_json::to_value(result).expect("serialize list tools result"); + + assert_eq!( + actual, + json!({ + "_meta": { + "ttlMs": 5000, + "cacheScope": "user" + }, + "tools": [] + }) + ); +} + +#[test] +fn read_resource_results_serialize_cache_hints_in_content_meta() { + let result = + ReadResourceResult::new(vec![ResourceContents::text("hello", "file:///example.txt")]) + .with_ttl_ms(10_000) + .with_cache_scope(CacheScope::Shared); + + let actual = serde_json::to_value(result).expect("serialize read resource result"); + + assert_eq!(actual["contents"][0]["_meta"]["ttlMs"], 10000); + assert_eq!(actual["contents"][0]["_meta"]["cacheScope"], "shared"); +}