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
6 changes: 5 additions & 1 deletion crates/codegraph-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,11 @@ fn cmd_files(
}

fn cmd_serve(path: Option<PathBuf>, mcp: bool, no_watch: bool) -> Result<()> {
let project = path.map(|p| resolve_project_path_optional(&absolute_path(p)));
// Default the MCP project to cwd so `serve --mcp` (no --path, as the
// installer injects) finds the index of the agent's project root.
let project = Some(resolve_project_path_optional(&absolute_path(
path.unwrap_or_else(|| PathBuf::from(".")),
)));
if no_watch {
std::env::set_var("CODEGRAPH_NO_WATCH", "1");
}
Expand Down
73 changes: 73 additions & 0 deletions crates/codegraph-mcp/tests/golden_mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,79 @@ fn tools_list_is_empty_when_workspace_not_indexed() {
);
}

#[test]
fn default_project_indexed_serves_full_tools_list_after_initialize() {
// Regression for the `serve --mcp` (no --path) bug: the installer launches
// the server with the agent's project root as cwd and no projectPath, so the
// CLI must default the project to that indexed root. With a Some(indexed)
// default_project, the initialize->tools/list handshake must expose 4 tools.
let project = index_fixture(&[(
"src/app.ts",
"export function greet(name: string): string {\n return `hi ${name}`;\n}\n",
)]);
let frames = format!(
"{}\n{}\n",
serde_json::to_string(&json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}))
.unwrap(),
serde_json::to_string(&json!({"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}))
.unwrap(),
);
let mut output = Vec::new();
let mut server = McpServer::new(Some(project.path().to_path_buf()));
server
.run(Cursor::new(frames.into_bytes()), &mut output)
.expect("server run");
let text = String::from_utf8(output).expect("utf8 output");
let tools_line = text.lines().nth(1).expect("tools/list response line");
let resp: Value = serde_json::from_str(tools_line).expect("response json");
assert_eq!(
resp["result"]["tools"].as_array().map(Vec::len),
Some(4),
"indexed default project must serve the 4-tool default surface"
);
}

#[test]
fn default_project_unindexed_serves_empty_tools_list() {
// The non-bailing cwd default still starts the server for an unindexed root;
// it must serve an EMPTY tools/list (upstream parity, golden-pinned).
let base = std::env::temp_dir().join(format!(
"cg-mcp-default-noidx-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::create_dir_all(&base).unwrap();
let mut output = Vec::new();
let mut server = McpServer::new(Some(base.clone()));
server
.run(
Cursor::new(
format!(
"{}\n",
serde_json::to_string(
&json!({"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}})
)
.unwrap()
)
.into_bytes(),
),
&mut output,
)
.expect("server run");
let _ = fs::remove_dir_all(&base);
let text = String::from_utf8(output).expect("utf8 output");
let line = text.lines().next().expect("one response line");
let resp: Value = serde_json::from_str(line).expect("response json");
assert_eq!(
resp["result"]["tools"].as_array().map(Vec::len),
Some(0),
"unindexed default project must serve empty tools/list"
);
}

#[test]
fn tools_list_honors_codegraph_mcp_tools_allowlist() {
// CODEGRAPH_MCP_TOOLS replaces the default surface with exactly the named
Expand Down