use rmcp::model::{CallToolResult, ContentBlock, ErrorData};
use serde_json::{Value, json};
use crate::catalog::Catalog;
#[cfg(feature = "picker")]
use crate::retrieval::Shortlist;
#[cfg(feature = "picker")]
use super::reply::text_error;
pub(super) const PAGE_LIMIT: usize = 100;
#[cfg(feature = "picker")]
const RETRIEVAL_UNAVAILABLE: &str = "need_prompt cannot answer: this server's retrieval index is not loaded. Call list_prompts and choose a prompt from the catalog instead.";
pub(super) fn page_start(cursor: Option<&str>) -> Result<usize, ErrorData> {
match cursor {
None => Ok(0),
Some(cursor) => cursor.parse::<usize>().map_err(|_| {
ErrorData::invalid_params(
format!("cursor {cursor:?} is not a valid pagination cursor"),
None,
)
}),
}
}
pub(super) fn list_prompts_result(
catalog: &Catalog,
cursor: &str,
) -> Result<CallToolResult, ErrorData> {
let entries = catalog.entries();
let start = page_start((!cursor.is_empty()).then_some(cursor))?;
let end = start.saturating_add(PAGE_LIMIT).min(entries.len());
let prompts: Vec<Value> = entries
.get(start..end)
.unwrap_or(&[])
.iter()
.map(|entry| {
json!({
"name": entry.name(),
"description": entry.description(),
"problem": entry.problem(),
})
})
.collect();
let mut structured = json!({ "prompts": prompts });
if end < entries.len()
&& let Some(object) = structured.as_object_mut()
{
object.insert("next_cursor".to_owned(), Value::String(end.to_string()));
}
let text = serde_json::to_string(&structured)
.map_err(|e| ErrorData::internal_error(format!("render the prompt listing: {e}"), None))?;
let mut result = CallToolResult::success(vec![ContentBlock::text(text)]);
result.structured_content = Some(structured);
Ok(result)
}
#[cfg(feature = "picker")]
pub(crate) fn need_prompt_result(shortlist: &Shortlist) -> Result<CallToolResult, ErrorData> {
let candidates = match shortlist {
Shortlist::Candidates(candidates) => candidates,
Shortlist::Unavailable => return Ok(text_error(RETRIEVAL_UNAVAILABLE.to_owned())),
Shortlist::Failed(detail) => {
return Err(ErrorData::internal_error(
format!("rank prompts for the capability: {detail}"),
None,
));
}
};
let structured = json!({ "prompts": candidates });
let text = serde_json::to_string_pretty(&structured)
.map_err(|e| ErrorData::internal_error(format!("render the candidates: {e}"), None))?;
let mut result = CallToolResult::success(vec![ContentBlock::text(text)]);
result.structured_content = Some(structured);
Ok(result)
}
#[cfg(test)]
mod tests {
use rmcp::model::ErrorCode;
use super::page_start;
#[test]
fn a_cursor_is_an_offset_absent_is_the_first_page_and_garbage_is_a_client_bug() {
assert_eq!(page_start(None).expect("no cursor is the first page"), 0);
assert_eq!(
page_start(Some("100")).expect("a valid cursor is its offset"),
100
);
let error = page_start(Some("not-a-number"))
.expect_err("a cursor this server never issued is the client's bug");
assert_eq!(error.code, ErrorCode::INVALID_PARAMS);
}
}