scraps 2.4.1

Scraps is a portable CLI knowledge hub for managing interconnected Markdown documentation with Wiki-link notation.
use crate::ir::loader;
use crate::mcp::json::scrap::ScrapKeyJson;
use crate::usecase::scrap::lookup_backlinks::usecase::LookupScrapBacklinksUsecase;
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, Deserialize, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct LookupScrapBacklinksRequest {
    /// Title of the scrap to get backlinks for
    pub title: String,
    /// Optional context if the scrap has one
    pub ctx: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct LookupScrapBacklinksResponse {
    pub results: Vec<ScrapKeyJson>,
    pub count: usize,
    pub next: String,
}

pub async fn lookup_scrap_backlinks(
    scraps_dir: &Path,
    exclude_dirs: &[std::path::PathBuf],
    _context: RequestContext<RoleServer>,
    Parameters(request): Parameters<LookupScrapBacklinksRequest>,
) -> Result<CallToolResult, ErrorData> {
    // Load scraps from directory
    let scraps = loader::load_scraps(scraps_dir, exclude_dirs).map_err(|e| {
        ErrorData::new(
            ErrorCode(-32003),
            format!("Failed to load scraps: {e}"),
            None,
        )
    })?;

    // Create get scrap backlinks usecase
    let get_backlinks_usecase = LookupScrapBacklinksUsecase::new();

    // Execute get backlinks
    let title = scraps_libs::model::title::Title::from(request.title.as_str());
    let ctx = request
        .ctx
        .as_ref()
        .map(|c| scraps_libs::model::context::Ctx::from(c.as_str()));

    let results = get_backlinks_usecase
        .execute(&scraps, &title, &ctx)
        .map_err(|e| {
            ErrorData::new(
                ErrorCode(-32006),
                format!("Get scrap backlinks failed: {e}"),
                None,
            )
        })?;

    // Convert results to structured response
    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 backlinks. Try lookup_scrap_links {title, ctx} for outbound references."
    } else {
        "Read a linking scrap with get_scrap {title, ctx}."
    };
    let response = LookupScrapBacklinksResponse {
        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(-32007),
                format!("JSON serialization failed: {e}"),
                None,
            )
        })?,
    )]))
}