From 0a910ffb5daa4c2f6ca6d4808feec90efb552238 Mon Sep 17 00:00:00 2001 From: sunerpy Date: Fri, 26 Jun 2026 11:39:36 +0800 Subject: [PATCH] fix(mcp): scope Kiro MCP to project path and guard home-root indexing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kiro launches the stdio MCP server from $HOME and its initialize handshake carries no rootUri/workspaceFolders and does not advertise capabilities.roots, so a bare global `serve --mcp` could not discover the project and degraded to home safe mode (and previously risked indexing the whole home tree). - installer(kiro): inject --path like Cursor — local install pins the concrete project root, global install pins ${workspaceFolder} for per-workspace expansion — so every Kiro window serves its own project. - cli(init/index): refuse a too-broad root ($HOME or filesystem root) instead of building a home-wide index that would peg a CPU on a home-launched serve. - mcp(serve): on late client-root adoption, stop the home direct loop and proxy the current stdio session into the adopted project's shared daemon (run_until_adoption); effective for clients that advertise MCP roots. - docs: document Kiro project-level install and the init/index home-root guard. --- Cargo.lock | 20 +-- .../src/installer/targets/kiro.rs | 66 ++++++++- crates/codegraph-cli/src/main.rs | 124 ++++++++++++++-- crates/codegraph-mcp/src/lib.rs | 2 +- crates/codegraph-mcp/src/server.rs | 139 +++++++++++++++--- docs/cli.md | 28 +++- docs/mcp.md | 24 +-- 7 files changed, 340 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b5c203..fbcd020 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,7 +303,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "codegraph-bench" -version = "0.15.5" +version = "0.15.6" dependencies = [ "anyhow", "clap", @@ -318,7 +318,7 @@ dependencies = [ [[package]] name = "codegraph-core" -version = "0.15.5" +version = "0.15.6" dependencies = [ "anyhow", "serde", @@ -334,7 +334,7 @@ dependencies = [ [[package]] name = "codegraph-daemon" -version = "0.15.5" +version = "0.15.6" dependencies = [ "anyhow", "codegraph-core", @@ -352,7 +352,7 @@ dependencies = [ [[package]] name = "codegraph-extract" -version = "0.15.5" +version = "0.15.6" dependencies = [ "anyhow", "codegraph-core", @@ -394,7 +394,7 @@ dependencies = [ [[package]] name = "codegraph-graph" -version = "0.15.5" +version = "0.15.6" dependencies = [ "anyhow", "codegraph-core", @@ -406,7 +406,7 @@ dependencies = [ [[package]] name = "codegraph-mcp" -version = "0.15.5" +version = "0.15.6" dependencies = [ "anyhow", "codegraph-core", @@ -422,7 +422,7 @@ dependencies = [ [[package]] name = "codegraph-resolve" -version = "0.15.5" +version = "0.15.6" dependencies = [ "anyhow", "codegraph-core", @@ -436,7 +436,7 @@ dependencies = [ [[package]] name = "codegraph-rs" -version = "0.15.5" +version = "0.15.6" dependencies = [ "anyhow", "assert_cmd", @@ -470,7 +470,7 @@ dependencies = [ [[package]] name = "codegraph-store" -version = "0.15.5" +version = "0.15.6" dependencies = [ "anyhow", "codegraph-core", @@ -482,7 +482,7 @@ dependencies = [ [[package]] name = "codegraph-watch" -version = "0.15.5" +version = "0.15.6" dependencies = [ "anyhow", "codegraph-core", diff --git a/crates/codegraph-cli/src/installer/targets/kiro.rs b/crates/codegraph-cli/src/installer/targets/kiro.rs index db62b0c..3f66de4 100644 --- a/crates/codegraph-cli/src/installer/targets/kiro.rs +++ b/crates/codegraph-cli/src/installer/targets/kiro.rs @@ -8,7 +8,7 @@ use std::fs; use std::path::PathBuf; -use serde_json::{json, Map}; +use serde_json::{json, Map, Value}; use super::super::shared::{ mcp_server_config, read_config_file, read_json_file, remove_codegraph_from_mcp_servers, @@ -35,6 +35,26 @@ fn steering_path(ctx: &InstallContext, loc: Location) -> PathBuf { config_dir(ctx, loc).join("steering").join("codegraph.md") } +/// Build the Kiro MCP entry with an explicit `--path`, mirroring Cursor. +/// +/// Kiro's `initialize` carries no `rootUri`/`workspaceFolders` and does not +/// advertise `capabilities.roots`, so a bare global `serve --mcp` launched from +/// `$HOME` cannot discover the project and degrades to home safe mode. Pinning +/// `--path` fixes that: Local pins the concrete `ctx.cwd`; Global pins +/// `${workspaceFolder}`, which Kiro expands per workspace. +fn build_kiro_mcp_config(ctx: &InstallContext, loc: Location) -> Value { + let path_arg = match loc { + Location::Local => ctx.cwd.to_string_lossy().to_string(), + Location::Global => "${workspaceFolder}".to_string(), + }; + let mut base = mcp_server_config(); + if let Some(args) = base.get_mut("args").and_then(|a| a.as_array_mut()) { + args.push(json!("--path")); + args.push(json!(path_arg)); + } + base +} + impl AgentTarget for KiroTarget { fn id(&self) -> TargetId { TargetId::Kiro @@ -73,6 +93,8 @@ impl AgentTarget for KiroTarget { "Restart Kiro for MCP changes to take effect.".to_string(), "Kiro IDE: also enable MCP in Settings (search \"MCP\" → \"Enabled\"). Kiro CLI users can skip this step." .to_string(), + "Prefer a project-level install: run `codegraph install --target=kiro --local` from each project root so the MCP entry pins that project's --path. A global install uses ${workspaceFolder}, which Kiro must expand per workspace." + .to_string(), ], } } @@ -104,8 +126,9 @@ impl AgentTarget for KiroTarget { // Ports kiroTarget.printConfig (kiro.ts:120). fn print_config(&self, ctx: &InstallContext, loc: Location) -> String { let target = mcp_json_path(ctx, loc); - let snippet = - to_upstream_json(&json!({ "mcpServers": { "codegraph": mcp_server_config() } })); + let snippet = to_upstream_json( + &json!({ "mcpServers": { "codegraph": build_kiro_mcp_config(ctx, loc) } }), + ); format!("# Add to {}\n\n{snippet}\n", target.display()) } @@ -124,7 +147,7 @@ fn write_mcp_entry(ctx: &InstallContext, loc: Location) -> FileWrite { if let Some(dir) = file.parent() { let _ = fs::create_dir_all(dir); } - let after = mcp_server_config(); + let after = build_kiro_mcp_config(ctx, loc); match read_config_file(&file) { ConfigRead::Unparseable => FileWrite { path: file, @@ -198,4 +221,39 @@ mod tests { assert!(local.ends_with(".kiro/skills")); assert_eq!(local, PathBuf::from("/work/proj/.kiro/skills")); } + + #[test] + fn kiro_local_mcp_entry_pins_concrete_project_path() { + // Given a local Kiro install context + let ctx = ctx(); + + // When the MCP entry is built for the local location + let entry = build_kiro_mcp_config(&ctx, Location::Local); + + // Then args end with --path pinned to the concrete cwd + let args = entry["args"].as_array().expect("args array"); + assert_eq!( + args, + &vec![ + json!("serve"), + json!("--mcp"), + json!("--path"), + json!("/work/proj"), + ] + ); + } + + #[test] + fn kiro_global_mcp_entry_pins_workspace_folder_variable() { + // Given a global Kiro install context + let ctx = ctx(); + + // When the MCP entry is built for the global location + let entry = build_kiro_mcp_config(&ctx, Location::Global); + + // Then args end with --path ${workspaceFolder} for per-workspace expansion + let args = entry["args"].as_array().expect("args array"); + assert_eq!(args.last(), Some(&json!("${workspaceFolder}"))); + assert!(args.contains(&json!("--path"))); + } } diff --git a/crates/codegraph-cli/src/main.rs b/crates/codegraph-cli/src/main.rs index 5a41134..debcb66 100644 --- a/crates/codegraph-cli/src/main.rs +++ b/crates/codegraph-cli/src/main.rs @@ -23,7 +23,7 @@ use codegraph_core::types::{ExtractionResult, FileRecord, Language, Node, NodeKi use codegraph_extract::{detect_language, extract_source, ExtractOptions}; use codegraph_graph::graph::{GodotReach, GraphTraverser}; use codegraph_graph::query::{search_nodes, SearchOptions}; -use codegraph_mcp::McpServer; +use codegraph_mcp::{McpServer, RunUntilAdoption}; use codegraph_resolve::ReferenceResolver; use codegraph_store::queries::SearchResult; use codegraph_store::Store; @@ -797,6 +797,7 @@ fn cmd_init(path: Option) -> Result<()> { println!("Use \"codegraph index\" to re-index or \"codegraph sync\" to update"); return Ok(()); } + guard_indexable_root(&project)?; fs::create_dir_all(codegraph_dir(&project)) .with_context(|| format!("creating {}", codegraph_dir(&project).display()))?; let result = index_project(&project, true, false)?; @@ -818,6 +819,7 @@ fn cmd_uninit(path: Option, force: bool) -> Result<()> { fn cmd_index(path: Option, force: bool, quiet: bool, verbose: bool) -> Result<()> { let project = resolve_required_project(path)?; + guard_indexable_root(&project)?; if force { remove_db_files(&project)?; } @@ -1120,22 +1122,76 @@ fn serve_direct(project: Option, project_root: &Path, no_watch: bool) - fn serve_direct_no_services(project: Option, _project_root: &Path) -> Result<()> { let stdin = io::stdin(); let stdout = io::stdout(); - let mut server = - McpServer::with_adoption_observer(project, Box::new(start_daemon_for_adopted_root)); - server - .run(BufReader::new(stdin.lock()), stdout.lock()) - .context("running MCP stdio server") + let mut server = McpServer::new(project); + match server + .run_until_adoption(BufReader::new(stdin.lock()), stdout.lock()) + .context("running MCP stdio server until workspace adoption")? + { + RunUntilAdoption::Eof => Ok(()), + RunUntilAdoption::Adopted { + project_root, + reader, + } => serve_adopted_project(reader, stdout, project_root), + } } -fn start_daemon_for_adopted_root(project_root: &Path) { +fn serve_adopted_project( + reader: R, + writer: W, + project_root: PathBuf, +) -> Result<()> { + let Some(socket_path) = start_daemon_for_adopted_root(&project_root) else { + let mut server = McpServer::new(Some(project_root)); + return server + .run(reader, writer) + .context("running MCP stdio server for adopted project"); + }; + + match codegraph_daemon::attach_to_daemon(&socket_path) { + Ok(client) if codegraph_daemon::verify_daemon_hello(&client.hello).is_none() => {} + Ok(_) => { + tracing::debug!("adopted project daemon version mismatch; serving direct"); + let mut server = McpServer::new(Some(project_root)); + return server + .run(reader, writer) + .context("running MCP stdio server for adopted project"); + } + Err(err) => { + tracing::debug!(error = %err, "adopted project daemon preflight failed; serving direct"); + let mut server = McpServer::new(Some(project_root)); + return server + .run(reader, writer) + .context("running MCP stdio server for adopted project"); + } + } + + match codegraph_daemon::run_proxy( + &socket_path, + Some(codegraph_daemon::current_ppid()), + reader, + writer, + ) { + Ok(codegraph_daemon::ProxyOutcome::Proxied) => Ok(()), + Ok(codegraph_daemon::ProxyOutcome::VersionMismatch) => Ok(()), + Err(err) => { + tracing::debug!(error = %err, "adopted project proxy attach failed"); + Ok(()) + } + } +} + +fn start_daemon_for_adopted_root(project_root: &Path) -> Option { if daemon_opt_out() || is_daemon_internal() || !should_run_daemon_services(project_root) { - return; + return None; } - if !codegraph_dir(project_root).is_dir() || daemon_already_running(project_root) { - return; + if !codegraph_dir(project_root).is_dir() { + return None; + } + if daemon_already_running(project_root) { + return Some(codegraph_daemon::daemon_socket_path(project_root)); } let Ok(exe) = std::env::current_exe() else { - return; + return None; }; match codegraph_daemon::spawn_detached_daemon(&exe, project_root) { Ok(()) => { @@ -1144,9 +1200,12 @@ fn start_daemon_for_adopted_root(project_root: &Path) { "[CodeGraph MCP] Started shared daemon for adopted project root {}", project_root.display() ); + let socket_path = codegraph_daemon::daemon_socket_path(project_root); + socket_path.exists().then_some(socket_path) } Err(err) => { eprintln!("[CodeGraph MCP] Adopted project daemon start failed: {err}"); + None } } } @@ -1159,6 +1218,16 @@ fn should_run_daemon_services(root: &Path) -> bool { codegraph_watch::too_broad_root_reason(root).is_none() } +fn guard_indexable_root(root: &Path) -> Result<()> { + if let Some(reason) = codegraph_watch::too_broad_root_reason(root) { + bail!( + "refusing to index {}: {reason}. Run `codegraph init`/`index` inside a specific project directory instead.", + root.display() + ); + } + Ok(()) +} + /// Spawn a ONE-SHOT background catch-up sync that absorbs edits made while the /// server was down (upstream colby `catchUpSync`, #905). Returns an /// `Arc` flipped to `true` when the background sync finishes, so a @@ -1333,7 +1402,7 @@ pub fn select_serve_mode( #[cfg(test)] mod serve_mode_tests { - use super::{select_serve_mode, should_run_daemon_services, ServeMode}; + use super::{guard_indexable_root, select_serve_mode, should_run_daemon_services, ServeMode}; use std::path::Path; use std::sync::Mutex; @@ -1380,6 +1449,37 @@ mod serve_mode_tests { } let _ = std::fs::remove_dir_all(&tmp); } + + #[test] + fn guard_indexable_root_rejects_home_and_root_allows_nested_project() { + let _lock = ENV_LOCK.lock().unwrap(); + let home_key = if cfg!(windows) { "USERPROFILE" } else { "HOME" }; + let prev_home = std::env::var_os(home_key); + + let tmp = std::env::temp_dir().join(format!("cg-guard-home-{}", std::process::id())); + let nested = tmp.join("workspace/proj"); + std::fs::create_dir_all(&nested).unwrap(); + std::env::set_var(home_key, &tmp); + + assert!( + guard_indexable_root(&tmp).is_err(), + "$HOME must be refused as an index root" + ); + assert!( + guard_indexable_root(Path::new("/")).is_err(), + "filesystem root must be refused as an index root" + ); + assert!( + guard_indexable_root(&nested).is_ok(), + "a project nested under $HOME must be indexable" + ); + + match prev_home { + Some(v) => std::env::set_var(home_key, v), + None => std::env::remove_var(home_key), + } + let _ = std::fs::remove_dir_all(&tmp); + } } fn cmd_unlock(path: Option) -> Result<()> { diff --git a/crates/codegraph-mcp/src/lib.rs b/crates/codegraph-mcp/src/lib.rs index ac6ebd3..7b4ed87 100644 --- a/crates/codegraph-mcp/src/lib.rs +++ b/crates/codegraph-mcp/src/lib.rs @@ -20,4 +20,4 @@ pub mod schemas; pub mod server; pub use engine::CodeGraphEngine; -pub use server::{initialize_result, McpServer}; +pub use server::{initialize_result, McpServer, RunUntilAdoption}; diff --git a/crates/codegraph-mcp/src/server.rs b/crates/codegraph-mcp/src/server.rs index 5c78a32..c2c7dd4 100644 --- a/crates/codegraph-mcp/src/server.rs +++ b/crates/codegraph-mcp/src/server.rs @@ -11,7 +11,7 @@ use std::collections::HashMap; use std::io::{BufRead, Write}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use serde_json::{json, Value}; @@ -184,6 +184,11 @@ pub fn reopen_count() -> u64 { type AdoptionObserver = Box; +pub enum RunUntilAdoption { + Eof, + Adopted { project_root: PathBuf, reader: R }, +} + /// Holds the default project path and a per-path engine cache (mirrors /// `ToolHandler.projectCache`, `tools.ts:591`). Each cached engine carries the /// db-file identity it was opened against, so [`McpServer::engine_for`] can @@ -232,12 +237,17 @@ impl McpServer { /// response line per request to `writer`. Notifications (no `id`) produce no /// output (`session.ts:118` gates every reply on `isRequest`). pub fn run(&mut self, reader: R, mut writer: W) -> anyhow::Result<()> { - for line in reader.lines() { - let line = line?; + let mut reader = reader; + loop { + let mut line = String::new(); + if reader.read_line(&mut line)? == 0 { + break; + } if line.trim().is_empty() { continue; } - for response in self.handle_line(&line) { + let handled = self.handle_line(&line); + for response in handled.responses { let serialized = serde_json::to_string(&response)?; writeln!(writer, "{serialized}")?; writer.flush()?; @@ -246,41 +256,80 @@ impl McpServer { Ok(()) } + pub fn run_until_adoption( + &mut self, + mut reader: R, + mut writer: W, + ) -> anyhow::Result> { + loop { + let mut line = String::new(); + if reader.read_line(&mut line)? == 0 { + return Ok(RunUntilAdoption::Eof); + } + if line.trim().is_empty() { + continue; + } + let handled = self.handle_line(&line); + for response in handled.responses { + let serialized = serde_json::to_string(&response)?; + writeln!(writer, "{serialized}")?; + writer.flush()?; + } + if let Some(project_root) = handled.adopted { + return Ok(RunUntilAdoption::Adopted { + project_root, + reader, + }); + } + } + } + /// Parse + dispatch one line. Returns `Some(response)` for a request, /// `None` for a notification or unparseable notification. - fn handle_line(&mut self, line: &str) -> Vec { + fn handle_line(&mut self, line: &str) -> HandledLine { let value: Value = match serde_json::from_str(line) { Ok(v) => v, Err(_) => { // `transport.ts:167-171`: parse error with a null id. - return vec![json_response(JsonRpcResponse::error( + return HandledLine::responses(vec![json_response(JsonRpcResponse::error( Value::Null, error_codes::PARSE_ERROR, "Parse error: invalid JSON", - ))]; + ))]); } }; if value.get("method").is_none() { - self.handle_response(&value); - return Vec::new(); + return HandledLine { + responses: Vec::new(), + adopted: self.handle_response(&value), + }; } let req: JsonRpcRequest = match serde_json::from_value(value) { Ok(r) => r, Err(_) => { - return vec![json_response(JsonRpcResponse::error( + return HandledLine::responses(vec![json_response(JsonRpcResponse::error( Value::Null, error_codes::INVALID_REQUEST, "Invalid Request", - ))] + ))]) } }; let id = req.id.clone(); - match self.dispatch(&req) { + let handled = match self.dispatch(&req) { Dispatch::Reply(value) => id .map(|id| vec![json_response(JsonRpcResponse::result(id, value))]) .unwrap_or_default(), + Dispatch::ReplyWithAdoption { reply, adopted } => { + let responses = id + .map(|id| vec![json_response(JsonRpcResponse::result(id, reply))]) + .unwrap_or_default(); + return HandledLine { + responses, + adopted: Some(adopted), + }; + } Dispatch::ReplyAndRequest { reply, request } => id .map(|id| vec![json_response(JsonRpcResponse::result(id, reply)), request]) .unwrap_or_default(), @@ -288,7 +337,8 @@ impl McpServer { .map(|id| vec![json_response(JsonRpcResponse::error(id, code, msg))]) .unwrap_or_default(), Dispatch::Notification => Vec::new(), - } + }; + HandledLine::responses(handled) } /// Method dispatch, mirroring `session.ts:119-156`. @@ -301,6 +351,10 @@ impl McpServer { .adopt_from_initialize(&mut self.default_project, req.params.as_ref()) { self.notify_adopted(&adopted); + return Dispatch::ReplyWithAdoption { + reply: initialize_result(), + adopted, + }; } if self .workspace_roots @@ -339,21 +393,23 @@ impl McpServer { } } - fn handle_response(&mut self, response: &Value) { + fn handle_response(&mut self, response: &Value) -> Option { let is_roots_response = response.get("id").and_then(Value::as_str) == Some(ROOTS_LIST_REQUEST_ID); if !is_roots_response { - return; + return None; } if let Some(adopted) = self .workspace_roots .adopt_from_roots_result(&mut self.default_project, response.get("result")) { self.notify_adopted(&adopted); + return Some(adopted); } + None } - fn notify_adopted(&mut self, path: &std::path::Path) { + fn notify_adopted(&mut self, path: &Path) { if let Some(observer) = &mut self.adoption_observer { observer(path); } @@ -476,11 +532,26 @@ impl McpServer { enum Dispatch { Reply(Value), + ReplyWithAdoption { reply: Value, adopted: PathBuf }, ReplyAndRequest { reply: Value, request: Value }, Err(i64, String), Notification, } +struct HandledLine { + responses: Vec, + adopted: Option, +} + +impl HandledLine { + fn responses(responses: Vec) -> Self { + Self { + responses, + adopted: None, + } + } +} + fn json_response(response: JsonRpcResponse) -> Value { match serde_json::to_value(response) { Ok(value) => value, @@ -508,7 +579,7 @@ pub fn initialize_result() -> Value { #[cfg(test)] mod tests { use super::*; - use std::io::Cursor; + use std::io::{Cursor, Read}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; @@ -638,6 +709,40 @@ mod tests { assert_eq!(observed.lock().unwrap().as_deref(), Some(project.path())); } + #[test] + fn roots_list_adoption_returns_remaining_reader_for_proxy_handoff() { + let project = indexed_project("handoff"); + let response = json!({ + "jsonrpc": "2.0", + "id": ROOTS_LIST_REQUEST_ID, + "result": { + "roots": [{ "uri": format!("file://{}", project.path().display()), "name": "proj" }] + }, + }); + let next = json!({ "jsonrpc": "2.0", "id": 99, "method": "tools/list" }); + let input = format!("{response}\n{next}\n"); + let mut out = Vec::new(); + let mut server = McpServer::new(None); + + let outcome = server + .run_until_adoption(Cursor::new(input.into_bytes()), Cursor::new(&mut out)) + .unwrap(); + + assert!(out.is_empty(), "roots/list responses are client replies"); + let RunUntilAdoption::Adopted { + project_root, + mut reader, + } = outcome + else { + panic!("indexed roots/list response must stop the loop for handoff"); + }; + assert_eq!(project_root, project.path()); + + let mut remaining = String::new(); + reader.read_to_string(&mut remaining).unwrap(); + assert_eq!(remaining, format!("{next}\n")); + } + #[test] fn initialize_with_no_params_returns_standard_result_without_panic() { let request = json!({ "jsonrpc": "2.0", "id": 7, "method": "initialize" }); diff --git a/docs/cli.md b/docs/cli.md index 3147de4..86c3770 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -47,6 +47,12 @@ > [Daemon, watch & environment variables](#daemon-watch--environment-variables) > for the full env-var reference. +> **`init` / `index` refuse a too-broad root.** Running `codegraph init` or +> `codegraph index` against exactly `$HOME` or the filesystem root (`/`) is +> rejected with an error instead of building a home-wide index — that index +> would be enormous and would make a home-launched `serve --mcp` peg a CPU. Run +> these commands inside a specific project directory. + --- ## `codegraph install` / `uninstall` — wire up AI agents @@ -57,7 +63,15 @@ config file; `uninstall` reverses it. No hand-editing of JSON/TOML required. Supported agents (`ALL_TARGETS` order): **Claude Code, Cursor, Codex CLI, opencode, Hermes Agent, Gemini CLI, Antigravity IDE, Kiro.** The written MCP command launches the Rust binary: `command: "codegraph"`, `args: ["serve", -"--mcp"]` (Cursor also injects `--path`). +"--mcp"]` (Cursor and Kiro also inject `--path`). + +> **Kiro is best installed project-level.** Kiro launches its stdio MCP +> subprocess from `$HOME` and its `initialize` carries no workspace root and no +> `roots` capability, so a bare `serve --mcp` would degrade to home safe mode. +> The installer therefore injects `--path`. Run +> `codegraph install --target=kiro --local` from each project root to pin that +> project's absolute path; a global install pins `${workspaceFolder}`, which +> Kiro expands per workspace. ```bash codegraph install --yes # auto-detect installed agents, global @@ -408,12 +422,12 @@ The watcher is also auto-disabled when the resolved project root is the filesystem root (`/`) or the current user's home directory (`$HOME`). This commonly happens when an IDE or agent (e.g. Kiro) launches `codegraph serve --mcp` with no `--path` and its working directory resolves to `$HOME`. In that -case the watcher is disabled and the reason is logged — tool queries still work -off any existing index, and clients that advertise MCP roots support are asked -for `roots/list` so the server can adopt their first indexed workspace root and -start that root's shared daemon. The remedy for clients that do not support -roots: open a specific project folder, let the client send its workspace root via -the MCP `initialize` handshake, or pass `--path ` explicitly. +case the watcher is disabled and the reason is logged. Clients that advertise +MCP roots support are asked for `roots/list`; once the server adopts their first +indexed workspace root, it starts or attaches to that root's shared daemon and +proxies the current stdio session to it. The remedy for clients that do not +support roots: open a specific project folder, let the client send its workspace +root via the MCP `initialize` handshake, or pass `--path ` explicitly. `CODEGRAPH_FORCE_WATCH=1` does **not** override this guard (it only overrides the WSL2 `/mnt/` disable). diff --git a/docs/mcp.md b/docs/mcp.md index 239ef4b..aa19ae2 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -122,18 +122,18 @@ server first disables the daemon, the file watcher, AND catch-up sync — not ju the watcher. This happens when an IDE or agent (e.g. Kiro) launches `codegraph serve --mcp` with no `--path` and its CWD is the home directory; without the guard, the server would spawn a daemon that indexes the entire home -tree and peg a CPU at 99%. In this initial safe mode the server still answers all -tool queries off any existing `.codegraph` index, but it will not start -background services against `$HOME`. If the client advertises MCP roots support, -the server then sends `roots/list` and adopts the first indexed root from the -client's response. When that adopted root is indexed, CodeGraph also starts the -shared project daemon for that root, so a single global config can recover the -real project even when the launch CWD was home. `CODEGRAPH_FORCE_WATCH` does -**not** override this guard (it only overrides the WSL2 `/mnt/` disable). A real -project nested under `$HOME` (e.g. `~/projects/myapp`) is unaffected and gets the -full daemon, watcher, and catch-up. To guarantee per-project services for clients -that do not support roots, pin the root via `--path ` in the client's -MCP config args (e.g. a workspace-level `.kiro/settings/mcp.json`), or open the +tree and peg a CPU at 99%. In this initial safe mode the server still answers the +handshake, but it will not start background services against `$HOME`. If the +client advertises MCP roots support, the server sends `roots/list`, adopts the +first indexed root from the client's response, starts or attaches to that root's +shared project daemon, then proxies the current stdio session to that daemon. +That lets a single global config recover the real project even when the launch +CWD was home, without hardcoding `--path`. `CODEGRAPH_FORCE_WATCH` does **not** +override this guard (it only overrides the WSL2 `/mnt/` disable). A real project +nested under `$HOME` (e.g. `~/projects/myapp`) is unaffected and gets the full +daemon, watcher, and catch-up. To guarantee per-project services for clients that +do not support roots, pin the root via `--path ` in the client's MCP +config args (e.g. a workspace-level `.kiro/settings/mcp.json`), or open the project folder as the working directory. When `serve --mcp` is started without an explicit `--path`, the server reads the