use std::collections::HashMap;
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CachedResultHandle(pub String);
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CachedResultEnvelope {
pub handle: CachedResultHandle,
pub total_items: usize,
pub page_size: usize,
pub first_page: Vec<Value>,
pub truncated: bool,
pub omitted_items: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
}
impl CachedResultEnvelope {
pub fn page_token(handle: &CachedResultHandle, offset: usize) -> String {
format!("{}:offset:{offset}", handle.0)
}
}
pub trait ResultCache: Send + Sync {
fn store(&self, items: Vec<Value>) -> CachedResultHandle;
fn page(&self, handle: &CachedResultHandle, offset: usize, limit: usize) -> Option<Vec<Value>>;
fn len(&self, handle: &CachedResultHandle) -> Option<usize>;
fn release(&self, handle: &CachedResultHandle) -> bool;
}
#[derive(Debug, Default)]
pub struct MemoryResultCache {
next_id: Mutex<u64>,
inner: Mutex<HashMap<String, Vec<Value>>>,
}
impl MemoryResultCache {
pub fn new() -> Self {
Self::default()
}
pub fn live_handles(&self) -> usize {
self.inner.lock().map(|g| g.len()).unwrap_or(0)
}
fn fresh_handle(&self) -> CachedResultHandle {
let id = {
let mut g = match self.next_id.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
let id = *g;
*g = g.wrapping_add(1);
id
};
CachedResultHandle(format!("mcp-cache-{id}"))
}
}
impl ResultCache for MemoryResultCache {
fn store(&self, items: Vec<Value>) -> CachedResultHandle {
let handle = self.fresh_handle();
if let Ok(mut g) = self.inner.lock() {
g.insert(handle.0.clone(), items);
}
handle
}
fn page(&self, handle: &CachedResultHandle, offset: usize, limit: usize) -> Option<Vec<Value>> {
let g = self.inner.lock().ok()?;
let items = g.get(&handle.0)?;
let end = offset.saturating_add(limit).min(items.len());
let start = offset.min(items.len());
Some(
items
.get(start..end)
.map(<[Value]>::to_vec)
.unwrap_or_default(),
)
}
fn len(&self, handle: &CachedResultHandle) -> Option<usize> {
let g = self.inner.lock().ok()?;
g.get(&handle.0).map(Vec::len)
}
fn release(&self, handle: &CachedResultHandle) -> bool {
match self.inner.lock() {
Ok(mut g) => g.remove(&handle.0).is_some(),
Err(_) => false,
}
}
}
pub fn cache_if_large(
value: Value,
cache: &dyn ResultCache,
threshold_bytes: usize,
page_size: usize,
) -> Value {
let arr = match value {
Value::Array(items) => items,
other => return other,
};
let serialized_size = match serde_json::to_string(&Value::Array(arr.clone())) {
Ok(s) => s.len(),
Err(_) => return Value::Array(arr),
};
if serialized_size <= threshold_bytes {
return Value::Array(arr);
}
let total_items = arr.len();
let first_page_len = page_size.min(total_items);
let first_page: Vec<Value> = arr
.get(..first_page_len)
.map(<[Value]>::to_vec)
.unwrap_or_default();
let handle = cache.store(arr);
let omitted_items = total_items.saturating_sub(first_page_len);
let envelope = CachedResultEnvelope {
page_token: (omitted_items > 0)
.then(|| CachedResultEnvelope::page_token(&handle, first_page_len)),
handle,
total_items,
page_size,
first_page,
truncated: omitted_items > 0,
omitted_items,
};
serde_json::to_value(envelope).unwrap_or(Value::Null)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn store_then_page_returns_slice() {
let cache = MemoryResultCache::new();
let h = cache.store(vec![json!(1), json!(2), json!(3), json!(4)]);
assert_eq!(cache.len(&h), Some(4));
assert_eq!(cache.page(&h, 0, 2), Some(vec![json!(1), json!(2)]));
assert_eq!(cache.page(&h, 2, 2), Some(vec![json!(3), json!(4)]));
}
#[test]
fn page_past_end_returns_empty_not_none() {
let cache = MemoryResultCache::new();
let h = cache.store(vec![json!(1)]);
assert_eq!(cache.page(&h, 5, 10), Some(vec![]));
}
#[test]
fn page_unknown_handle_returns_none() {
let cache = MemoryResultCache::new();
let phantom = CachedResultHandle("nope".to_string());
assert!(cache.page(&phantom, 0, 1).is_none());
assert!(cache.len(&phantom).is_none());
assert!(!cache.release(&phantom));
}
#[test]
fn release_frees_handle_and_subsequent_calls_return_none() {
let cache = MemoryResultCache::new();
let h = cache.store(vec![json!("a"), json!("b")]);
assert_eq!(cache.live_handles(), 1);
assert!(cache.release(&h));
assert_eq!(cache.live_handles(), 0);
assert!(cache.page(&h, 0, 1).is_none());
assert!(cache.len(&h).is_none());
assert!(!cache.release(&h));
}
#[test]
fn handles_are_unique_per_store_call() {
let cache = MemoryResultCache::new();
let h1 = cache.store(vec![json!(1)]);
let h2 = cache.store(vec![json!(2)]);
assert_ne!(h1, h2);
}
#[test]
fn cache_if_large_passes_through_when_under_threshold() {
let cache = MemoryResultCache::new();
let v = json!([1, 2, 3]);
let out = cache_if_large(v.clone(), &cache, 1024, 10);
assert_eq!(out, v);
assert_eq!(cache.live_handles(), 0);
}
#[test]
fn cache_if_large_passes_through_for_non_arrays() {
let cache = MemoryResultCache::new();
let v = json!({"k": "v"});
let out = cache_if_large(v.clone(), &cache, 0, 10);
assert_eq!(out, v);
assert_eq!(cache.live_handles(), 0);
}
#[test]
fn cache_if_large_envelopes_oversized_array() {
let cache = MemoryResultCache::new();
let items: Vec<Value> = (0..50).map(|i| json!({"id": i})).collect();
let out = cache_if_large(Value::Array(items), &cache, 16, 5);
let env: CachedResultEnvelope = serde_json::from_value(out).unwrap();
assert_eq!(env.total_items, 50);
assert_eq!(env.page_size, 5);
assert_eq!(env.first_page.len(), 5);
assert_eq!(env.first_page[0], json!({"id": 0}));
assert_eq!(env.first_page[4], json!({"id": 4}));
assert!(env.truncated);
assert_eq!(env.omitted_items, 45);
assert_eq!(env.page_token.as_deref(), Some("mcp-cache-0:offset:5"));
assert_eq!(cache.len(&env.handle), Some(50));
let page2 = cache.page(&env.handle, 5, 5).unwrap();
assert_eq!(page2.len(), 5);
assert_eq!(page2[0], json!({"id": 5}));
}
#[test]
fn cache_if_large_marks_empty_preview_as_truncated() {
let cache = MemoryResultCache::new();
let items: Vec<Value> = (0..3).map(|i| json!({"id": i})).collect();
let out = cache_if_large(Value::Array(items), &cache, 1, 0);
let env: CachedResultEnvelope = serde_json::from_value(out).unwrap();
assert!(env.first_page.is_empty());
assert!(env.truncated);
assert_eq!(env.omitted_items, 3);
assert_eq!(env.page_token.as_deref(), Some("mcp-cache-0:offset:0"));
}
}