Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,18 @@ pub type ProgressNotification = Notification<ProgressNotificationMethod, Progres

pub type Cursor = String;

/// Scope describing who may cache cacheable list/read results.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum CacheScope {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/// The response may be cached for the current user.
User,
/// The response may be shared by clients with equivalent authorization.
Shared,
}

macro_rules! paginated_result {
($t:ident {
$i_item: ident: $t_item: ty
Expand All @@ -1159,15 +1171,34 @@ macro_rules! paginated_result {
}

impl $t {
pub fn with_all_items(
items: $t_item,
) -> 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
}
}
};
}
Expand Down Expand Up @@ -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 {
Expand All @@ -1251,6 +1283,37 @@ impl ReadResourceResult {
pub fn new(contents: Vec<ResourceContents>) -> 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
Expand Down
35 changes: 35 additions & 0 deletions crates/rmcp/tests/test_cache_hints.rs
Original file line number Diff line number Diff line change
@@ -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");
}