use crate::error::McpError;
use base64::Engine;
use serde::{Deserialize, Serialize};
const CURSOR_VERSION: u8 = 1;
#[derive(Serialize, Deserialize)]
struct CursorToken {
v: u8,
offset: usize,
}
#[must_use]
pub fn encode_cursor(offset: usize) -> String {
let json = serde_json::to_vec(&CursorToken {
v: CURSOR_VERSION,
offset,
})
.unwrap_or_default();
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json)
}
pub fn decode_cursor(method: &str, cursor: &str) -> Result<usize, McpError> {
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(cursor)
.map_err(|_| McpError::invalid_params(method, "invalid cursor"))?;
let token: CursorToken = serde_json::from_slice(&bytes)
.map_err(|_| McpError::invalid_params(method, "invalid cursor"))?;
if token.v != CURSOR_VERSION {
return Err(McpError::invalid_params(
method,
"unsupported cursor version",
));
}
Ok(token.offset)
}
pub fn paginate<T>(
items: Vec<T>,
cursor: Option<&str>,
page_size: Option<usize>,
method: &str,
) -> Result<(Vec<T>, Option<String>), McpError> {
let offset = match cursor {
Some(c) => decode_cursor(method, c)?,
None => 0,
};
let Some(page_size) = page_size.filter(|&n| n > 0) else {
return Ok((items.into_iter().skip(offset).collect(), None));
};
if offset >= items.len() {
return Ok((Vec::new(), None));
}
let end = (offset + page_size).min(items.len());
let has_more = end < items.len();
let page = items.into_iter().skip(offset).take(page_size).collect();
let next = has_more.then(|| encode_cursor(end));
Ok((page, next))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cursor_roundtrips_and_is_opaque() {
let c = encode_cursor(100);
assert!(
!c.contains("100"),
"cursor should be opaque, not a raw offset"
);
assert_eq!(decode_cursor("tools/list", &c).unwrap(), 100);
}
#[test]
fn invalid_cursor_is_invalid_params() {
assert!(decode_cursor("tools/list", "not-base64!!!").is_err());
let junk = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"{}");
assert!(decode_cursor("tools/list", &junk).is_err());
}
#[test]
fn disabled_pagination_returns_all() {
let (page, next) = paginate(vec![1, 2, 3], None, None, "m").unwrap();
assert_eq!(page, vec![1, 2, 3]);
assert!(next.is_none());
}
#[test]
fn pages_and_emits_next_cursor() {
let items: Vec<u32> = (0..5).collect();
let (page, next) = paginate(items.clone(), None, Some(2), "m").unwrap();
assert_eq!(page, vec![0, 1]);
let next = next.expect("more remain");
let (page, next) = paginate(items.clone(), Some(&next), Some(2), "m").unwrap();
assert_eq!(page, vec![2, 3]);
let next = next.expect("more remain");
let (page, next) = paginate(items, Some(&next), Some(2), "m").unwrap();
assert_eq!(page, vec![4]);
assert!(next.is_none());
}
#[test]
fn offset_at_or_past_end_is_empty() {
let (page, next) = paginate(vec![1, 2], Some(&encode_cursor(2)), Some(10), "m").unwrap();
assert!(page.is_empty());
assert!(next.is_none());
}
}