use crate::input::file::read_scraps;
use crate::mcp::json::scrap::ScrapKeyJson;
use crate::usecase::search::usecase::SearchUsecase;
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::ErrorCode;
use rmcp::model::{CallToolResult, ContentBlock};
use rmcp::schemars::JsonSchema;
use rmcp::service::RequestContext;
use rmcp::{ErrorData, RoleServer};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Copy, Default, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum SearchLogic {
And,
#[default]
Or,
}
impl From<SearchLogic> for scraps_libs::search::engine::SearchLogic {
fn from(logic: SearchLogic) -> Self {
match logic {
SearchLogic::And => scraps_libs::search::engine::SearchLogic::And,
SearchLogic::Or => scraps_libs::search::engine::SearchLogic::Or,
}
}
}
#[derive(Debug, Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct SearchRequest {
pub query: String,
pub num: Option<usize>,
pub logic: Option<SearchLogic>,
}
#[derive(Debug, Serialize)]
pub struct SearchResponse {
pub results: Vec<ScrapKeyJson>,
pub count: usize,
pub next: String,
}
pub async fn search_scraps(
scraps_dir: &Path,
exclude_dirs: &[std::path::PathBuf],
_context: RequestContext<RoleServer>,
Parameters(request): Parameters<SearchRequest>,
) -> Result<CallToolResult, ErrorData> {
let scraps = read_scraps::to_all_scraps(scraps_dir, exclude_dirs).map_err(|e| {
ErrorData::new(
ErrorCode(-32003),
format!("Failed to load scraps: {e}"),
None,
)
})?;
let search_usecase = SearchUsecase::new();
let num = request.num.unwrap_or(100);
let logic = request.logic.unwrap_or_default().into();
let results = search_usecase
.execute(&scraps, &request.query, num, logic)
.map_err(|e| ErrorData::new(ErrorCode(-32004), format!("Search failed: {e}"), None))?;
let scrap_jsons: Vec<ScrapKeyJson> = results
.into_iter()
.map(|result| ScrapKeyJson {
title: result.title.to_string(),
ctx: result.ctx.map(|c| c.to_string()),
})
.collect();
let count = scrap_jsons.len();
let next = if count == 0 {
"No hits. Vary the keywords (default OR logic matches any), or start from the topic map with list_tags."
} else {
"Read a hit with get_scrap {title, ctx}; then traverse it with lookup_scrap_links."
};
let response = SearchResponse {
results: scrap_jsons,
count,
next: next.to_string(),
};
Ok(CallToolResult::success(vec![ContentBlock::text(
serde_json::to_string(&response).map_err(|e| {
ErrorData::new(
ErrorCode(-32005),
format!("JSON serialization failed: {e}"),
None,
)
})?,
)]))
}