use crate::ir::loader;
use crate::mcp::json::scrap::SearchHitJson;
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;
use tracing_opentelemetry::OpenTelemetrySpanExt;
#[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<SearchHitJson>,
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 = loader::load_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 = tracing::info_span!("search")
.in_scope(|| search_usecase.execute(&scraps, &request.query, num, logic))
.map_err(|e| ErrorData::new(ErrorCode(-32004), format!("Search failed: {e}"), None))?;
let scrap_jsons: Vec<SearchHitJson> = results
.into_iter()
.map(|result| SearchHitJson {
scrap: (&result.scrap).into(),
description: result.description,
})
.collect();
let count = scrap_jsons.len();
tracing::Span::current().set_attribute("scraps.search.result_count", count as i64);
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 by passing its key back: get_scrap {key}; 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,
)
})?,
)]))
}