Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
workflow_dispatch:

permissions:
contents: read

env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -D warnings
Expand All @@ -23,6 +26,40 @@ jobs:
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo clippy -p codegraph-graph --features postgres,mysql,redis --tests -- -D warnings

clippy-windows:
name: clippy (windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
targets: x86_64-pc-windows-msvc
- uses: Swatinem/rust-cache@v2
- run: cargo clippy -p codegraph --target x86_64-pc-windows-msvc -- -D warnings

test-windows:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
name: test (windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build -p codegraph
- name: Build (fastembed feature)
run: cargo build -p codegraph --features fastembed
- name: Test (binary crate)
run: cargo test -p codegraph
- name: Test (graph crate, sqlite/lmdb backends)
run: cargo test -p codegraph-graph --features sqlite,lmdb,bloom-search
- name: Smoke (doctor + help)
run: |
cargo run -p codegraph -- --help
cargo run -p codegraph -- doctor

test:
name: test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ report, plus the session tools `codegraph_init` / `codegraph_deinit` /
| `codegraph_index` | Full re-index of the bound workspace |
| `codegraph_sandbox` | Compile a function group to machine code and run it against Rhai mocks |
| `codegraph_diff` | Draft report of what an MR/patch would change in the graph |
| `codegraph_mermaid` | Render a Mermaid diagram (flow / callers / callees / impact) — the visual variant of the diagram queries; requires the server to start with `--mermaid` |

Read the [server instructions](crates/codegraph-mcp/src/server-instructions.md) that ship with the binary — they tell your agent when to reach for which tool.

Expand Down
10 changes: 8 additions & 2 deletions crates/codegraph-api/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,15 +322,21 @@ fn normalize_root(path: Utf8PathBuf) -> Result<Utf8PathBuf> {
.map_err(|e| anyhow!("cannot resolve {}: {e}", path))?;
let canon =
Utf8PathBuf::from_path_buf(canon).map_err(|p| anyhow!("path is not valid UTF-8: {p:?}"))?;
if canon.as_str() == "/" {
if is_fs_root(&canon) {
return Err(anyhow!(
"refusing to use `/` as the workspace root \
"refusing to use the filesystem root as the workspace root \
(MCP hosts may launch servers from `/`). Pass an absolute project path."
));
}
Ok(canon)
}

/// Đường dẫn có phải là gốc filesystem không (`/` trên Unix, `C:\` trên
/// Windows). Dùng `parent().is_none()` cho cross-platform (không so chuỗi `/`).
fn is_fs_root(p: &Utf8Path) -> bool {
p.parent().is_none()
}

/// Full re-index: mở index theo backend config → `Orchestrator::index_all`
/// (ingest = full re-index, bump version → snapshot cũ bị `ensure_fresh` thấy
/// stale và rebuild ở lần query kế).
Expand Down
1 change: 1 addition & 0 deletions crates/codegraph-graph/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ warnings = "deny"

[dependencies]
codegraph-core = { path = "../codegraph-core" }
dirs = { workspace = true }

# SQLite-backed Db (moved here from the removed codegraph-db crate).
rusqlite = { workspace = true }
Expand Down
10 changes: 5 additions & 5 deletions crates/codegraph-graph/src/embeddings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,16 @@ fn default_cache_dir() -> Option<PathBuf> {
expand_tilde("~/.cache/codegraph/embeddings")
}

/// Expand `~` thành home dir (best-effort). Trả `Some` nếu không bắt đầu bằng `~`.
/// Expand `~` thành home dir (best-effort, cross-platform). Trả `Some` nếu
/// không bắt đầu bằng `~`. Dùng `dirs::home_dir()` để lấy home đúng trên mọi OS
/// (Windows: `USERPROFILE`/`HOMEDRIVE`, macOS/Linux: `$HOME`).
fn expand_tilde(path: &str) -> Option<PathBuf> {
if !path.starts_with('~') {
return Some(PathBuf::from(path));
}
let home = std::env::var("HOME")
.ok()
.or_else(|| std::env::var("USERPROFILE").ok())?;
let home = dirs::home_dir()?;
let rest = path.strip_prefix('~').unwrap_or("");
Some(PathBuf::from(home).join(rest.trim_start_matches('/')))
Some(home.join(rest.trim_start_matches('/')))
}

/// Suffix file extension của sqlite-vss theo OS (`.dylib` / `.so` / `.dll`).
Expand Down
19 changes: 19 additions & 0 deletions crates/codegraph-graphql/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,25 @@ mod tests {
assert_eq!(json["data"]["__typename"], "Query");
}

#[tokio::test]
async fn mermaid_gate_enforced() {
// Không bật --mermaid: mermaid phải báo lỗi gate (không gọi index).
let app = build_app(&cfg(false), make_state(false));
let (status, json) = post_graphql(&app, r#"{ mermaid(id: "1", kind: FLOW) }"#).await;
assert_eq!(status, StatusCode::OK);
let msg = json["errors"][0]["message"].as_str().unwrap();
assert!(msg.contains("Mermaid"), "expected gate error, got: {msg}");

// Bật --mermaid: vượt gate, sau đó lỗi do chưa có index (khác gate).
let app2 = build_app(&cfg(true), make_state(true));
let (_status, json2) = post_graphql(&app2, r#"{ mermaid(id: "1", kind: FLOW) }"#).await;
let msg2 = json2["errors"][0]["message"].as_str().unwrap();
assert!(
!msg2.contains("Mermaid"),
"gate should be off when --mermaid set, got: {msg2}"
);
}

#[tokio::test]
async fn api_key_required_when_set() {
let mut c = cfg(false);
Expand Down
41 changes: 41 additions & 0 deletions crates/codegraph-graphql/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,47 @@ impl Query {
.map_err(|e| async_graphql::Error::new(e.to_string()))
}

/// Diagram Mermaid cho một symbol — biến thể hình ảnh của `flow` /
/// `callers` / `callees` / `impact`. `kind` chọn loại diagram; `depth` (mặc
/// định 1) giới hạn BFS hop cho callers/callees/impact (bị bỏ qua với flow).
/// Chỉ hoạt động khi server bật `--mermaid`; tắt → lỗi rõ ràng.
async fn mermaid(
&self,
ctx: &Context<'_>,
id: ID,
kind: MermaidKind,
depth: Option<i32>,
) -> GqlResult<String> {
let state = ctx.data::<Arc<AppState>>()?;
if !state.mermaid {
return Err(async_graphql::Error::new(
"Mermaid output is disabled. Start the GraphQL server with --mermaid to enable diagram rendering.",
));
}
let id = parse_id(&id)?;
let depth = depth.unwrap_or(1).max(1) as u32;
let api = api_for(ctx).await?;
let diagram = match kind {
MermaidKind::Flow => {
let flow = api
.flow(id)
.await
.map_err(|e| async_graphql::Error::new(e.to_string()))?;
codegraph_api::mermaid::control_flow(&flow)
}
MermaidKind::Callers => codegraph_api::mermaid::callers_mermaid(&api, id, depth)
.await
.map_err(|e| async_graphql::Error::new(e.to_string()))?,
MermaidKind::Callees => codegraph_api::mermaid::callees_mermaid(&api, id, depth)
.await
.map_err(|e| async_graphql::Error::new(e.to_string()))?,
MermaidKind::Impact => codegraph_api::mermaid::impact_mermaid(&api, id, depth)
.await
.map_err(|e| async_graphql::Error::new(e.to_string()))?,
};
Ok(diagram)
}

/// Functions có chain chứa pattern (id/marker/tên symbol, cách nhau bởi `,`).
async fn search_flow(
&self,
Expand Down
14 changes: 14 additions & 0 deletions crates/codegraph-graphql/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,17 @@ pub enum TypeKind {
Interface,
Enum,
}

// ==================== Mermaid kind ====================

/// Loại diagram Mermaid cho resolver `mermaid(id, kind, depth)` — render biến
/// thể hình ảnh của các query diagram (`flow` / `callers` / `callees` / `impact`).
/// Chỉ hoạt động khi server bật `--mermaid`.
#[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)]
#[graphql(rename_items = "SCREAMING_SNAKE_CASE")]
pub enum MermaidKind {
Flow,
Callers,
Callees,
Impact,
}
3 changes: 2 additions & 1 deletion crates/codegraph-mcp/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ use crate::{CodegraphServer, OutputStyle};
/// Không có — bind thất bại / lỗi serve trả `Err` qua `anyhow`.
pub async fn serve_http(
format: OutputStyle,
mermaid: bool,
addr: SocketAddr,
allowed_hosts: Vec<String>,
_enable_observability: bool,
Expand All @@ -54,7 +55,7 @@ pub async fn serve_http(
// Per SEP-2567 request 2026-07-28 vẫn luôn chạy stateless.
.with_legacy_session_mode(true);
let service = StreamableHttpService::new(
move || Ok(CodegraphServer::new_with_format(format)),
move || Ok(CodegraphServer::new_with_format(format, mermaid)),
session_manager,
config,
);
Expand Down
35 changes: 29 additions & 6 deletions crates/codegraph-mcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,42 +47,54 @@ pub struct CodegraphServer {
/// Session store cho search resumable — sống qua nhiều tool call để resume
/// id (trả về khi timeout) có thể retry được.
search_sessions: Arc<SearchSessionStore>,
/// Bật output Mermaid cho `codegraph_mermaid` (diagram visualization). Tắt →
/// tool trả lỗi rõ ràng. Tương ứng flag `--mermaid` ở CLI.
mermaid: bool,
}

impl CodegraphServer {
/// Server với session trống — `codegraph_init` sẽ bind root trong phiên.
pub fn new() -> Self {
Self::new_with_format(OutputStyle::default())
Self::new_with_format(OutputStyle::default(), false)
}

/// `new()` nhưng seed output format từ CLI lúc khởi động
/// (`codegraph serve --mcp --format=...`).
pub fn new_with_format(format: OutputStyle) -> Self {
/// (`codegraph serve --mcp --format=...`), và flag `--mermaid`.
pub fn new_with_format(format: OutputStyle, mermaid: bool) -> Self {
Self {
session: Session::new_with_format(format),
usage: Arc::new(Mutex::new(usage::UsageStats::default())),
search_sessions: Arc::new(SearchSessionStore::new()),
mermaid,
}
}

/// Pre-seed root từ `--path` lúc khởi động (tương đương đã `codegraph_init`
/// với root đó, không index thêm). Giữ CLI/watcher flow không vỡ.
pub async fn with_root(root: camino::Utf8PathBuf) -> anyhow::Result<Self> {
Self::with_root_and_format(root, OutputStyle::default()).await
Self::with_root_and_format(root, OutputStyle::default(), false).await
}

/// `with_root()` nhưng seed output format từ CLI lúc khởi động.
/// `with_root()` nhưng seed output format và flag `--mermaid` từ CLI lúc
/// khởi động.
pub async fn with_root_and_format(
root: camino::Utf8PathBuf,
format: OutputStyle,
mermaid: bool,
) -> anyhow::Result<Self> {
Ok(Self {
session: Session::with_root_and_format(root, format).await?,
usage: Arc::new(Mutex::new(usage::UsageStats::default())),
search_sessions: Arc::new(SearchSessionStore::new()),
mermaid,
})
}

/// Flag `--mermaid` của server (gate cho `codegraph_mermaid`).
pub fn mermaid_enabled(&self) -> bool {
self.mermaid
}

/// Dispatch một tool call đã verify tên. Trả [`ToolOutput::Text`] cho thành
/// công, [`ToolOutput::Error`] cho lỗi tool (client thấy `is_error`),
/// [`Err`] cho lỗi protocol (unknown tool đã bị chặn trước ở `call_tool`).
Expand Down Expand Up @@ -213,7 +225,18 @@ impl CodegraphServer {
codegraph_api::tools::dispatch_origin_simulate(&root, sgi.clone(), args.clone())
.await
}
_ => tools::dispatch_with_api(&api, &root, detail, format, name, args).await,
_ => {
tools::dispatch_with_api(
&api,
&root,
detail,
format,
self.mermaid_enabled(),
name,
args,
)
.await
}
};

match dispatch {
Expand Down
1 change: 1 addition & 0 deletions crates/codegraph-mcp/src/server-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ file-reading subtask — codegraph IS the index.
| what does this call directly? | `codegraph_callees` |
| change-impact radius | `codegraph_impact` |
| call chain (markers + callees + sites) | `codegraph_flow` |
| diagram (Mermaid) of flow/callers/callees/impact | `codegraph_mermaid` (needs `--mermaid`) |
| functions whose chain matches a pattern | `codegraph_search_flow` |
| composed context for a symbol/topic | `codegraph_context` |
| who calls library call `foo`? | `codegraph_references` |
Expand Down
43 changes: 43 additions & 0 deletions crates/codegraph-mcp/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ fn tool_defs() -> Vec<ToolDef> {
"format": { "type": "string", "enum": ["minimize", "medium"], "description": "Output format for this call (overrides session default): minimize = symbol items as fixed-order positional arrays (default), medium = objects with default-valued fields omitted." }
}, "required": ["node"] }),
),
tool(
"codegraph_mermaid",
"Render a Mermaid diagram (flowchart / graph) for a symbol — the visual variant of the diagram queries. `kind`: 'flow' (control-flow chain), 'callers' (upstream, transitive), 'callees' (downstream), or 'impact' (transitive callers). `depth` limits BFS hops for callers/callees/impact (default 1; ignored for flow). Requires the server to start with --mermaid; otherwise the tool returns an error.",
json!({ "type": "object", "properties": {
"node": { "type": "integer", "description": "Symbol id to render." },
"kind": { "type": "string", "enum": ["flow", "callers", "callees", "impact"], "default": "flow" },
"depth": { "type": "integer", "default": 1, "description": "BFS hops for callers/callees/impact (ignored for flow)." }
}, "required": ["node"] }),
),
tool(
"codegraph_search_flow",
"Find functions whose call chain contains a pattern. Pattern = comma-separated tokens: numeric ids, marker names (LOOP, IF_TRUE, IF_FALSE, BRANCH_END, RETURN, LOOP_BACK, SWITCH_CASE, SWITCH_END, BREAK, CONTINUE, THROW) or symbol names. On large indexes pass timeout_ms (default 20000); if the call returns a timeout error containing \"resume\": \"<id>\", retry the SAME call with that resume id to continue.",
Expand Down Expand Up @@ -275,10 +284,44 @@ pub async fn dispatch_with_api(
root: &Utf8Path,
session_detail: DetailLevel,
session_format: OutputStyle,
mermaid: bool,
name: &str,
args: Value,
) -> Result<String> {
match name {
"codegraph_mermaid" => {
if !mermaid {
return Err(Error::Invalid(
"Mermaid output is disabled. Start the MCP server with --mermaid to enable diagram rendering.".into(),
));
}
let node = args.get("node").and_then(|v| v.as_u64()).ok_or_else(|| {
Error::Invalid("codegraph_mermaid requires `node` (symbol id)".into())
})?;
let kind_str = args.get("kind").and_then(|v| v.as_str()).unwrap_or("flow");
let depth = args
.get("depth")
.and_then(|v| v.as_u64())
.unwrap_or(1)
.max(1) as u32;
let diagram = match kind_str {
"callers" => codegraph_api::mermaid::callers_mermaid(api, node, depth).await,
"callees" => codegraph_api::mermaid::callees_mermaid(api, node, depth).await,
"impact" => codegraph_api::mermaid::impact_mermaid(api, node, depth).await,
_ => {
let flow = api
.flow(node)
.await
.map_err(|e| Error::Invalid(e.to_string()))?;
Ok(codegraph_api::mermaid::control_flow(&flow))
}
}
.map_err(|e| Error::Invalid(e.to_string()))?;
emit_value(
root.as_str(),
json!({ "node": node, "kind": kind_str, "mermaid": diagram }),
)
}
"codegraph_symbol" => {
let detail = detail_from_args(&args, session_detail);
let format = format_from_args(&args, session_format);
Expand Down
Loading
Loading