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}