diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index f0b4f64..da3dd7a 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -1,7 +1,7 @@ //! Minimal MCP server: JSON-RPC over stdio, dispatching to the same ops as the CLI. use std::io::{BufRead, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; use anyhow::Result; use serde_json::{Value, json}; @@ -10,6 +10,7 @@ const PROTOCOL_VERSION: &str = "2024-11-05"; pub fn serve(project: &Path) -> Result<()> { eprintln!("rustytokens MCP server listening on stdio"); + let mut project = project.to_path_buf(); let stdin = std::io::stdin(); let stdout = std::io::stdout(); for line in stdin.lock().lines() { @@ -39,7 +40,7 @@ pub fn serve(project: &Path) -> Result<()> { continue; } }; - if let Some(resp) = handle(&msg, project) { + if let Some(resp) = handle(&msg, &mut project) { let mut out = stdout.lock(); serde_json::to_writer(&mut out, &resp)?; out.write_all(b"\n")?; @@ -49,7 +50,7 @@ pub fn serve(project: &Path) -> Result<()> { Ok(()) } -fn handle(msg: &Value, project: &Path) -> Option { +fn handle(msg: &Value, project: &mut PathBuf) -> Option { let id = msg.get("id").cloned(); // Notifications have no id -> no response. @@ -101,7 +102,7 @@ fn handle(msg: &Value, project: &Path) -> Option { } } -fn call_tool(name: &str, args: &Value, project: &Path) -> Value { +fn call_tool(name: &str, args: &Value, project: &mut PathBuf) -> Value { let result: Result = dispatch(name, args, project); match result { Ok(v) => json!({ @@ -114,7 +115,7 @@ fn call_tool(name: &str, args: &Value, project: &Path) -> Value { } } -fn dispatch(name: &str, args: &Value, project: &Path) -> Result { +fn dispatch(name: &str, args: &Value, project: &mut PathBuf) -> Result { use crate::ops; fn req_str<'a>(args: &'a Value, key: &str, tool: &str) -> Result<&'a str> { @@ -128,8 +129,16 @@ fn dispatch(name: &str, args: &Value, project: &Path) -> Result { let excludes = crate::config::ExcludeSet::new(&exclude_list(args)); match name { - "index_create" => ops::index_create(Path::new(req_str(args, "path", name)?)), - "index_update" => ops::index_update(Path::new(req_str(args, "path", name)?)), + "index_create" => { + let path = PathBuf::from(req_str(args, "path", name)?); + project.clone_from(&path); + ops::index_create(&path) + } + "index_update" => { + let path = PathBuf::from(req_str(args, "path", name)?); + project.clone_from(&path); + ops::index_update(&path) + } "index_file" => ops::index_file(project, req_str(args, "path", name)?), "search_symbols" => ops::search_symbols( project, @@ -143,15 +152,20 @@ fn dispatch(name: &str, args: &Value, project: &Path) -> Result { .unwrap_or(200), &excludes, ), - "search_text" => ops::search_text( - project, - req_str(args, "query", name)?, - args.get("limit") - .and_then(|v| v.as_u64()) - .map(|n| n as usize) - .unwrap_or(100), - &excludes, - ), + "search_text" => { + let search_project = opt_str("project") + .map(PathBuf::from) + .unwrap_or_else(|| project.clone()); + ops::search_text( + &search_project, + req_str(args, "query", name)?, + args.get("limit") + .and_then(|v| v.as_u64()) + .map(|n| n as usize) + .unwrap_or(100), + &excludes, + ) + } "inspect_symbol" => { if let Some(id) = args.get("id").and_then(|v| v.as_i64()) { ops::inspect_symbol(project, &id.to_string()) @@ -272,6 +286,7 @@ fn tools() -> Value { "type": "object", "properties": { "query": { "type": "string", "description": "Text or phrase to search for" }, + "project": { "type": "string", "description": "Project directory to search (defaults to the last indexed project)" }, "limit": { "type": "integer", "description": "Maximum matching files (default 100)" }, "exclude": { "type": "array",