Skip to main content

remembrall_server/tools/
ingest.rs

1//! Ingest tool parameter structs and implementation helpers.
2//!
3//! Covers: remembrall_ingest_github, remembrall_ingest_docs.
4//! The `#[tool]` wrapper methods live in `lib.rs` (required by `#[tool_router]`).
5//!
6//! All ingestion logic lives in `remembrall_core::ingest`. The functions here
7//! are thin wrappers that extract params, delegate to core, and convert the
8//! returned `IngestResult` into an MCP `CallToolResult` JSON response.
9
10use std::sync::Arc;
11
12use rmcp::{ErrorData as McpError, model::*, schemars};
13use serde_json::json;
14
15use remembrall_core::{embed::Embedder, ingest, memory::store::MemoryStore};
16
17// ---------------------------------------------------------------------------
18// Parameter structs
19// ---------------------------------------------------------------------------
20
21#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
22pub struct IngestGithubParams {
23    #[schemars(description = "GitHub repo in owner/repo format (e.g. 'owner/repo')")]
24    pub repo: String,
25    #[schemars(description = "Maximum number of recent merged PRs to ingest (default 50, max 200)")]
26    pub limit: Option<u32>,
27    #[schemars(description = "Project name to tag memories with (defaults to the repo name)")]
28    pub project: Option<String>,
29}
30
31#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
32pub struct IngestDocsParams {
33    #[schemars(description = "Path to project directory to scan for markdown files")]
34    pub path: String,
35    #[schemars(description = "Project name to tag memories with")]
36    pub project: Option<String>,
37}
38
39// ---------------------------------------------------------------------------
40// Thin wrappers - delegate all logic to remembrall_core::ingest
41// ---------------------------------------------------------------------------
42
43pub async fn ingest_github_impl(
44    memory: &Arc<MemoryStore>,
45    embedder: &Arc<dyn Embedder>,
46    params: IngestGithubParams,
47) -> Result<CallToolResult, McpError> {
48    let IngestGithubParams { repo, limit, project } = params;
49
50    // Derive the effective project name here so we can include it in the
51    // response even though core also derives it internally.
52    let effective_project = project.clone().unwrap_or_else(|| {
53        repo.split('/').last().unwrap_or("unknown").to_string()
54    });
55
56    let result = ingest::ingest_github_prs(
57        &repo,
58        limit.map(|l| l as i32),
59        project.as_deref(),
60        memory.as_ref(),
61        Arc::clone(embedder),
62    )
63    .await
64    .map_err(|e| McpError::internal_error(e.to_string(), None))?;
65
66    let text = json!({
67        "repo": repo,
68        "project": effective_project,
69        "memories_stored": result.memories_stored,
70        "memories_skipped": result.memories_skipped,
71        "errors": result.errors,
72    })
73    .to_string();
74
75    Ok(CallToolResult::success(vec![Content::text(text)]))
76}
77
78pub async fn ingest_docs_impl(
79    memory: &Arc<MemoryStore>,
80    embedder: &Arc<dyn Embedder>,
81    params: IngestDocsParams,
82) -> Result<CallToolResult, McpError> {
83    let IngestDocsParams { path, project } = params;
84
85    // Derive the effective project name here so we can include it in the
86    // response even though core also derives it internally.
87    let effective_project = project.clone().unwrap_or_else(|| {
88        std::path::Path::new(&path)
89            .file_name()
90            .and_then(|n| n.to_str())
91            .unwrap_or("unknown")
92            .to_string()
93    });
94
95    let result = ingest::ingest_docs(
96        &path,
97        project.as_deref(),
98        memory.as_ref(),
99        Arc::clone(embedder),
100    )
101    .await
102    .map_err(|e| McpError::internal_error(e.to_string(), None))?;
103
104    let text = json!({
105        "path": path,
106        "project": effective_project,
107        "memories_stored": result.memories_stored,
108        "memories_skipped": result.memories_skipped,
109        "errors": result.errors,
110    })
111    .to_string();
112
113    Ok(CallToolResult::success(vec![Content::text(text)]))
114}