Skip to main content

codingest_mcp/
lib.rs

1//! Reusable Codingest MCP server composition.
2//!
3//! KGLite owns the graph/Cypher server and `mcp-methods` owns the generic MCP
4//! lifecycle. This crate contributes the Codingest workspace-graph producer:
5//! source parsing, revision builds, and watch relevance.
6
7use kglite_mcp_server::{
8    ServerExtensions, WorkspaceGraphHooks, WorkspaceGraphMode, WorkspaceGraphRequest,
9    WorkspaceGraphResult,
10};
11use std::path::Path;
12
13fn is_graph_source(path: &Path) -> bool {
14    codingest::language_for_path(path).is_some()
15        || path
16            .extension()
17            .and_then(|extension| extension.to_str())
18            .is_some_and(|extension| {
19                extension.eq_ignore_ascii_case("md") || extension.eq_ignore_ascii_case("rst")
20            })
21}
22
23fn server_extensions() -> ServerExtensions {
24    let hooks = WorkspaceGraphHooks {
25        // Unified plain/revision-set build. Call shapes mirror the previous
26        // in-tree activation (`build_code_tree(dir, verbose=false,
27        // include_tests=true, save_to=None, max_loc=None, include_docs)`).
28        build: Box::new(|request: WorkspaceGraphRequest| {
29            // The github-workspace (open-source) mode ingests each cloned
30            // repo's markdown as `:Doc` nodes and links them to code
31            // (MENTIONS/DOCUMENTS) — a repo's prose is part of its
32            // intelligence. Local workspace / watch modes keep the lean
33            // code-only graph.
34            let include_docs = matches!(request.mode(), WorkspaceGraphMode::Workspace);
35            match request.revisions() {
36                // Multi-rev build: the producer owns rev canonicalization —
37                // dedup the requested labels, build over the deduped set, and
38                // return the graph together with the canonical labels the
39                // server records on the slot.
40                Some(revisions) => {
41                    let revisions = codingest::dedup_revs(revisions);
42                    let graph = codingest::build_code_tree_revs(
43                        request.root(),
44                        &revisions,
45                        None,
46                        false,
47                        true,
48                        None,
49                        None,
50                        include_docs,
51                    )?;
52                    Ok(WorkspaceGraphResult::with_revisions(graph, revisions))
53                }
54                None => {
55                    let graph = codingest::build_code_tree(
56                        request.root(),
57                        false,
58                        true,
59                        None,
60                        None,
61                        include_docs,
62                    )?;
63                    Ok(WorkspaceGraphResult::new(graph))
64                }
65            }
66        }),
67        // Watch relevance: is a change to this path graph-relevant?
68        is_relevant: Box::new(|change| is_graph_source(change.path())),
69    };
70
71    ServerExtensions::default().with_workspace_graph(hooks)
72}
73
74/// Run the KGLite MCP server with Codingest's workspace builder installed.
75///
76/// `args` includes the program name, matching `std::env::args_os()` and clap's
77/// normal process-level contract. Both the standalone binary and the Python
78/// wheel call this entry point so their behavior cannot drift.
79pub fn run<I, T>(args: I) -> anyhow::Result<()>
80where
81    I: IntoIterator<Item = T>,
82    T: Into<std::ffi::OsString> + Clone,
83{
84    kglite_mcp_server::run_with_extensions(args, server_extensions())
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn graph_source_predicate_includes_code_and_docs_only() {
93        assert!(is_graph_source(Path::new("src/lib.rs")));
94        assert!(is_graph_source(Path::new("README.md")));
95        assert!(is_graph_source(Path::new("GUIDE.RST")));
96        assert!(!is_graph_source(Path::new("notes.txt")));
97        assert!(!is_graph_source(Path::new("artifact.kgl")));
98    }
99}