feat!: implement SEP-2549 cache hints (#889)
implements https://github.com/modelcontextprotocol/rust-sdk/issues/875 Co-authored-by: Jack Amadeo <jackamadeo@squareup.com>
This commit is contained in:
parent
2d4c29fb02
commit
d8331d9442
1 changed files with 403 additions and 7 deletions
|
|
@ -1246,6 +1246,34 @@ pub type ProgressNotification = Notification<ProgressNotificationMethod, Progres
|
|||
|
||||
pub type Cursor = String;
|
||||
|
||||
/// Scope describing who may cache cacheable list/read results (SEP-2549).
|
||||
///
|
||||
/// Defaults to [`CacheScope::Public`] when absent from the wire.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[non_exhaustive]
|
||||
pub enum CacheScope {
|
||||
/// Any client or intermediary may cache and serve the response to any user.
|
||||
#[default]
|
||||
Public,
|
||||
/// Only the requesting user's client may cache the response.
|
||||
Private,
|
||||
}
|
||||
|
||||
/// Normalize a `ttlMs` value during deserialization.
|
||||
///
|
||||
/// Per SEP-2549, `ttlMs` MUST be `>= 0`; if a server returns a negative value,
|
||||
/// clients SHOULD treat it as `0` (immediately stale). This tolerates that case
|
||||
/// rather than erroring, while still accepting an absent field as `None`.
|
||||
fn deserialize_ttl_ms<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<i64>::deserialize(deserializer)?;
|
||||
Ok(value.map(|ttl_ms| ttl_ms.max(0) as u64))
|
||||
}
|
||||
|
||||
macro_rules! paginated_result {
|
||||
($t:ident {
|
||||
$i_item: ident: $t_item: ty
|
||||
|
|
@ -1258,24 +1286,50 @@ macro_rules! paginated_result {
|
|||
/// Result type discriminator. Absent values deserialize as `"complete"`.
|
||||
#[serde(default)]
|
||||
pub result_type: ResultType,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
|
||||
pub meta: Option<Meta>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub next_cursor: Option<Cursor>,
|
||||
/// Time, in milliseconds, that this result may be treated as fresh (SEP-2549).
|
||||
/// Required by spec version 2026-07-28, but optional here to maintain compatibility
|
||||
/// with older spec versions.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_ttl_ms",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub ttl_ms: Option<u64>,
|
||||
/// Scope describing who may cache this result (SEP-2549).
|
||||
/// Required by spec version 2026-07-28, but optional here to maintain compatibility
|
||||
/// with older spec versions.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_scope: Option<CacheScope>,
|
||||
pub $i_item: $t_item,
|
||||
}
|
||||
|
||||
impl $t {
|
||||
pub fn with_all_items(
|
||||
items: $t_item,
|
||||
) -> Self {
|
||||
pub fn with_all_items(items: $t_item) -> Self {
|
||||
Self {
|
||||
result_type: ResultType::default(),
|
||||
meta: None,
|
||||
next_cursor: None,
|
||||
ttl_ms: None,
|
||||
cache_scope: 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 {
|
||||
self.ttl_ms = Some(ttl_ms);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the cache scope for this result.
|
||||
pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self {
|
||||
self.cache_scope = Some(cache_scope);
|
||||
self
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1368,12 +1422,27 @@ 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 {
|
||||
/// Result type discriminator. Absent values deserialize as `"complete"`.
|
||||
#[serde(default)]
|
||||
pub result_type: ResultType,
|
||||
/// Time, in milliseconds, that this result may be treated as fresh (SEP-2549).
|
||||
/// Required by spec version 2026-07-28, but optional here to maintain compatibility
|
||||
/// with older spec versions.
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "deserialize_ttl_ms",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub ttl_ms: Option<u64>,
|
||||
/// Scope describing who may cache this result (SEP-2549).
|
||||
/// Required by spec version 2026-07-28, but optional here to maintain compatibility
|
||||
/// with older spec versions.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cache_scope: Option<CacheScope>,
|
||||
/// The actual content of the resource
|
||||
pub contents: Vec<ResourceContents>,
|
||||
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -1385,10 +1454,24 @@ impl ReadResourceResult {
|
|||
pub fn new(contents: Vec<ResourceContents>) -> Self {
|
||||
Self {
|
||||
result_type: ResultType::default(),
|
||||
ttl_ms: None,
|
||||
cache_scope: None,
|
||||
contents,
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.ttl_ms = Some(ttl_ms);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the cache scope for this result.
|
||||
pub fn with_cache_scope(mut self, cache_scope: CacheScope) -> Self {
|
||||
self.cache_scope = Some(cache_scope);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Request to read a specific resource
|
||||
|
|
|
|||
Loading…
Reference in a new issue