Skip to content

Commit f52d879

Browse files
committed
Merge branch 'main' into release
2 parents e7f4f73 + 48b30f1 commit f52d879

5 files changed

Lines changed: 185 additions & 20 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "codebase-mcp"
3-
version = "1.3.0"
3+
version = "1.3.1"
44
edition = "2024"
55
license = "Apache-2.0"
66
description = "High-performance MCP server for filesystem and code intelligence workflows"

README.md

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -360,29 +360,13 @@ tests/ integration and behavior tests
360360
## Project Status
361361

362362
- **Package:** `codebase-mcp`
363-
- **Version:** `1.3.0`
363+
- **Version:** `1.3.1`
364364
- **Runtime:** Rust + Tokio
365365
- **Transport:** MCP `stdio`
366366
- **MCP protocol version:** `2024-11-05`
367367
- **Tool count:** 25
368368
- **License:** Apache-2.0
369369

370-
### Release Notes - 1.3.0
371-
372-
- Slims the exposed MCP toolset to core codebase, search, read, edit, and code-intelligence workflows.
373-
- Removes Markdown, JSON utility, SQLite, diff snippet, and undo/redo command tools from the public catalog.
374-
- Simplifies runtime configuration and workspace-root handling for session-aware clients.
375-
- Tunes default indexing limits for large Chromium-sized workspaces.
376-
- Adds server instructions and richer tool parameter descriptions to improve agent tool selection.
377-
378-
### Release Notes - 1.2.1
379-
380-
- Adds Swift and Objective-C language support for AST-backed tools.
381-
- Improves import/export handling for Swift and Objective-C.
382-
- Formats workspace stats imports.
383-
- Fixes Clippy sort warnings.
384-
- Refreshes README project information.
385-
386370
---
387371

388372
## Acknowledgements

src/common.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,72 @@ pub fn resolve_tool_path(raw: &str) -> PathBuf {
191191
return canonicalize_if_exists(path);
192192
}
193193

194-
canonicalize_if_exists(default_tool_root().join(path))
194+
canonicalize_if_exists(resolve_relative_tool_path(&path))
195+
}
196+
197+
fn resolve_relative_tool_path(path: &Path) -> PathBuf {
198+
let mut roots = Vec::new();
199+
200+
if let Some((root, _)) = preferred_workspace_root() {
201+
push_unique_path(&mut roots, root);
202+
}
203+
204+
for runtime in crate::indexer::get_runtime_snapshots() {
205+
let root = canonicalize_if_exists(PathBuf::from(runtime.workspace_root));
206+
push_unique_path(&mut roots, root.clone());
207+
if let Some(parent) = root.parent() {
208+
push_unique_path(&mut roots, parent.to_path_buf());
209+
}
210+
}
211+
212+
if let Ok(current_dir) = std::env::current_dir() {
213+
if let Some(root) = discover_workspace_root(&current_dir) {
214+
push_unique_path(&mut roots, root);
215+
}
216+
push_unique_path(&mut roots, canonicalize_if_exists(current_dir));
217+
}
218+
219+
roots
220+
.into_iter()
221+
.map(|root| root.join(path))
222+
.max_by_key(|candidate| existing_ancestor_depth(candidate))
223+
.unwrap_or_else(|| PathBuf::from(path))
224+
}
225+
226+
fn push_unique_path(paths: &mut Vec<PathBuf>, candidate: PathBuf) {
227+
let candidate_key = normalize_path_key(&candidate);
228+
if paths
229+
.iter()
230+
.any(|existing| normalize_path_key(existing) == candidate_key)
231+
{
232+
return;
233+
}
234+
235+
paths.push(candidate);
236+
}
237+
238+
fn existing_ancestor_depth(path: &Path) -> usize {
239+
let mut current = Some(path);
240+
while let Some(candidate) = current {
241+
if candidate.exists() {
242+
return candidate.components().count();
243+
}
244+
current = candidate.parent();
245+
}
246+
247+
0
248+
}
249+
250+
fn normalize_path_key(path: &Path) -> String {
251+
let normalized = path.to_string_lossy().replace('\\', "/");
252+
#[cfg(windows)]
253+
{
254+
normalized.to_ascii_lowercase()
255+
}
256+
#[cfg(not(windows))]
257+
{
258+
normalized
259+
}
195260
}
196261

197262
pub fn bounded_walk_threads() -> usize {

tests/test_qa_regressions.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,59 @@ fn call_binary_server_tool_then_health(
167167
)
168168
}
169169

170+
fn call_binary_server_tools(
171+
current_dir: &Path,
172+
initialize_params: Value,
173+
tool_calls: Vec<(&str, Value)>,
174+
settle_ms: u64,
175+
) -> Vec<Value> {
176+
let exe = server_binary();
177+
let mut command = Command::new(&exe);
178+
command
179+
.current_dir(current_dir)
180+
.stdin(Stdio::piped())
181+
.stdout(Stdio::piped())
182+
.stderr(Stdio::piped());
183+
let mut child = command.spawn().unwrap();
184+
185+
{
186+
let stdin = child.stdin.as_mut().unwrap();
187+
writeln!(
188+
stdin,
189+
"{}",
190+
json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":initialize_params})
191+
)
192+
.unwrap();
193+
writeln!(
194+
stdin,
195+
"{}",
196+
json!({"jsonrpc":"2.0","method":"notifications/initialized"})
197+
)
198+
.unwrap();
199+
stdin.flush().unwrap();
200+
thread::sleep(Duration::from_millis(settle_ms));
201+
202+
for (index, (tool_name, tool_arguments)) in tool_calls.into_iter().enumerate() {
203+
writeln!(
204+
stdin,
205+
"{}",
206+
json!({"jsonrpc":"2.0","id":index + 2,"method":"tools/call","params":{"name":tool_name,"arguments":tool_arguments}})
207+
)
208+
.unwrap();
209+
}
210+
}
211+
212+
let output = child.wait_with_output().unwrap();
213+
let stdout = String::from_utf8(output.stdout).unwrap();
214+
stdout
215+
.lines()
216+
.filter(|line| line.trim_start().starts_with('{'))
217+
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
218+
.filter(|response| response.get("id").and_then(|v| v.as_i64()).unwrap_or(0) >= 2)
219+
.map(|response| decode_tool_rpc_response(&response))
220+
.collect()
221+
}
222+
170223
fn decode_tool_rpc_response(rpc: &Value) -> Value {
171224
serde_json::from_str(
172225
rpc.get("result")
@@ -524,6 +577,69 @@ fn test_tool_call_auto_indexes_workspace_from_request_path() {
524577
);
525578
}
526579

580+
#[test]
581+
fn test_relative_paths_can_target_sibling_workspace_after_active_child_changes() {
582+
let current_dir = tempdir().unwrap();
583+
let workspace = tempdir().unwrap();
584+
let api_dir = workspace.path().join("workboardapi");
585+
let ui_dir = workspace.path().join("workboardui");
586+
let api_search_dir = api_dir.join("src/redmine-sync");
587+
let ui_search_dir = ui_dir.join("src/app/features/task-current");
588+
fs::create_dir_all(&api_search_dir).unwrap();
589+
fs::create_dir_all(&ui_search_dir).unwrap();
590+
fs::write(
591+
api_dir.join("Cargo.toml"),
592+
"[package]\nname = \"api\"\nversion = \"0.1.0\"\n",
593+
)
594+
.unwrap();
595+
fs::write(ui_dir.join("package.json"), "{\"name\":\"ui\"}\n").unwrap();
596+
fs::write(api_search_dir.join("import.rs"), "fn basicImport() {}\n").unwrap();
597+
fs::write(
598+
ui_search_dir.join("import.ts"),
599+
"export const basicImport = true;\n",
600+
)
601+
.unwrap();
602+
603+
let responses = call_binary_server_tools(
604+
current_dir.path(),
605+
json!({
606+
"workspaceFolders": [
607+
{ "uri": file_uri_for_test(workspace.path()) }
608+
]
609+
}),
610+
vec![
611+
("resolve_path", json!({ "path": api_dir.to_str().unwrap() })),
612+
(
613+
"text_search",
614+
json!({
615+
"paths": [
616+
"workboardapi/src/redmine-sync",
617+
"workboardui/src/app/features/task-current"
618+
],
619+
"query": "basicImport",
620+
"max_results": 50,
621+
"context_lines": 2
622+
}),
623+
),
624+
],
625+
300,
626+
);
627+
628+
let text_search_result = responses.last().unwrap();
629+
assert_eq!(
630+
text_search_result
631+
.get("total_returned")
632+
.and_then(|v| v.as_u64()),
633+
Some(2)
634+
);
635+
assert_eq!(
636+
text_search_result
637+
.get("files_searched")
638+
.and_then(|v| v.as_u64()),
639+
Some(2)
640+
);
641+
}
642+
527643
#[test]
528644
fn test_tool_call_switches_active_workspace_context() {
529645
let current_dir = tempdir().unwrap();

0 commit comments

Comments
 (0)