use super::graphrag::GraphRagProvider;
use super::logging::{
init_mcp_logging, log_critical_anyhow_error, log_indexing_operation, log_watcher_event,
};
use super::semantic_code::SemanticCodeProvider;
use super::watcher::run_watcher;
use crate::config::Config;
use crate::indexer;
use crate::lock::IndexLock;
use crate::state;
use crate::store::Store;
use crate::watcher_config::{DEFAULT_ADDITIONAL_DELAY_MS, MCP_DEFAULT_DEBOUNCE_MS};
use anyhow::Result;
use rmcp::{
handler::server::{router::tool::ToolRouter, tool::ToolCallContext, wrapper::Parameters},
model::{
CallToolRequestParams, CallToolResult, Implementation, ServerCapabilities, ServerInfo, Tool,
},
schemars,
service::RequestContext,
tool, tool_handler, tool_router, ErrorData, RoleServer, ServerHandler, ServiceExt,
};
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::sync::{mpsc, Mutex};
use tokio::time::{sleep, Duration, Instant};
use tracing::{debug, info, warn};
const MCP_DEBOUNCE_MS: u64 = MCP_DEFAULT_DEBOUNCE_MS; const MCP_MAX_PENDING_EVENTS: usize = 100;
const MCP_INDEX_TIMEOUT_MS: u64 = 300_000;
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct SemanticSearchParams {
pub query: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1, max = 20), extend("default" = 3))]
pub max_results: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("enum" = ["signatures", "partial", "full"]), extend("default" = "partial"))]
pub detail_level: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("enum" = ["code", "text", "docs", "commits", "all"]), extend("default" = "all"))]
pub mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 1))]
pub threshold: Option<f32>,
}
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ViewSignaturesParams {
#[schemars(length(min = 1, max = 100))]
pub files: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct GraphRagParams {
#[schemars(extend("enum" = ["search", "get-node", "get-relationships", "find-path", "overview"]))]
pub operation: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub query: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1, max = 10), extend("default" = 3))]
pub max_depth: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(extend("enum" = ["text", "json", "markdown"]), extend("default" = "text"))]
pub format: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct StructuralSearchParams {
pub pattern: String,
pub language: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub paths: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 0, max = 10))]
pub context: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[schemars(range(min = 1, max = 200), extend("default" = 50))]
pub max_results: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rewrite: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub update_all: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct LspPositionParams {
pub file_path: String,
pub line: u32,
pub symbol: String,
}
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct LspFindReferencesParams {
pub file_path: String,
pub line: u32,
pub symbol: String,
#[serde(default = "default_true")]
pub include_declaration: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct LspDocumentSymbolsParams {
pub file_path: String,
}
#[derive(Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct LspWorkspaceSymbolsParams {
pub query: String,
}
pub struct BackgroundServices {
watcher_handle: Option<tokio::task::JoinHandle<()>>,
index_handle: Option<tokio::task::JoinHandle<()>>,
indexing_handle: Option<tokio::task::JoinHandle<()>>,
}
impl Drop for BackgroundServices {
fn drop(&mut self) {
if let Some(h) = self.watcher_handle.take() {
h.abort();
}
if let Some(h) = self.index_handle.take() {
h.abort();
}
if let Some(h) = self.indexing_handle.take() {
h.abort();
}
}
}
#[derive(Clone)]
pub struct McpServer {
working_directory: std::path::PathBuf,
semantic_code: SemanticCodeProvider,
graphrag: Option<GraphRagProvider>,
lsp: Option<Arc<Mutex<crate::mcp::lsp::LspProvider>>>,
indexer_enabled: bool,
tool_router: ToolRouter<Self>,
}
#[tool_router]
impl McpServer {
#[tool(
description = "Search codebase by meaning. Finds code by what it does, not exact symbol names. Prefer an array of related terms over a single query for broader coverage."
)]
async fn semantic_search(
&self,
Parameters(params): Parameters<SemanticSearchParams>,
) -> Result<String, String> {
debug!("Executing semantic_search with query: {:?}", params.query);
let mut arguments = match serde_json::to_value(¶ms) {
Ok(v) => v,
Err(e) => return Err(format!("Failed to serialize params: {}", e)),
};
if params.max_results.is_none() {
arguments["max_results"] = serde_json::json!(3);
}
match self.semantic_code.execute_search(&arguments).await {
Ok(result) => Ok(result),
Err(e) => Err(e.to_string()),
}
}
#[tool(
description = "Extract function signatures, class definitions, and declarations from files without implementation bodies. Supports Rust, JS/TS, Python, Go, C++, PHP, Ruby, Bash, JSON, CSS, Svelte, Swift, Markdown."
)]
async fn view_signatures(
&self,
Parameters(params): Parameters<ViewSignaturesParams>,
) -> Result<String, String> {
debug!("Executing view_signatures for {} files", params.files.len());
let arguments = match serde_json::to_value(¶ms) {
Ok(v) => v,
Err(e) => return Err(format!("Failed to serialize params: {}", e)),
};
match self.semantic_code.execute_view_signatures(&arguments).await {
Ok(result) => Ok(result),
Err(e) => Err(e.to_string()),
}
}
#[tool(
description = "Knowledge graph operations over the indexed codebase. Use for architectural queries: component relationships, dependency chains, data flows. For simple code lookup use semantic_search instead."
)]
async fn graphrag(
&self,
Parameters(params): Parameters<GraphRagParams>,
) -> Result<String, String> {
debug!("Executing graphrag with operation: {}", params.operation);
match &self.graphrag {
Some(provider) => {
let arguments = match serde_json::to_value(¶ms) {
Ok(v) => v,
Err(e) => return Err(format!("Failed to serialize params: {}", e)),
};
match provider.execute(&arguments).await {
Ok(result) => Ok(result),
Err(e) => Err(e.to_string()),
}
}
None => Err(
"GraphRAG is not enabled in the current configuration. Please enable GraphRAG in octocode.toml to use relationship-aware search.".to_string(),
),
}
}
#[tool(description = "Jump to the definition of a symbol via LSP.")]
async fn lsp_goto_definition(
&self,
Parameters(params): Parameters<LspPositionParams>,
) -> Result<String, String> {
let Some(ref provider) = self.lsp else {
return Err("LSP server is not available. Start MCP server with --with-lsp=\"<command>\" to enable LSP features.".to_string());
};
let args = serde_json::to_value(¶ms).unwrap_or_default();
provider
.lock()
.await
.execute_goto_definition(&args)
.await
.map_err(|e| e.to_string())
}
#[tool(description = "Get type info and documentation for a symbol via LSP.")]
async fn lsp_hover(
&self,
Parameters(params): Parameters<LspPositionParams>,
) -> Result<String, String> {
let Some(ref provider) = self.lsp else {
return Err("LSP server is not available. Start MCP server with --with-lsp=\"<command>\" to enable LSP features.".to_string());
};
let args = serde_json::to_value(¶ms).unwrap_or_default();
provider
.lock()
.await
.execute_hover(&args)
.await
.map_err(|e| e.to_string())
}
#[tool(description = "Find all usages of a symbol across the workspace via LSP.")]
async fn lsp_find_references(
&self,
Parameters(params): Parameters<LspFindReferencesParams>,
) -> Result<String, String> {
let Some(ref provider) = self.lsp else {
return Err("LSP server is not available. Start MCP server with --with-lsp=\"<command>\" to enable LSP features.".to_string());
};
let args = serde_json::to_value(¶ms).unwrap_or_default();
provider
.lock()
.await
.execute_find_references(&args)
.await
.map_err(|e| e.to_string())
}
#[tool(
description = "List all symbols (functions, types, variables) defined in a file via LSP."
)]
async fn lsp_document_symbols(
&self,
Parameters(params): Parameters<LspDocumentSymbolsParams>,
) -> Result<String, String> {
let Some(ref provider) = self.lsp else {
return Err("LSP server is not available. Start MCP server with --with-lsp=\"<command>\" to enable LSP features.".to_string());
};
let args = serde_json::to_value(¶ms).unwrap_or_default();
provider
.lock()
.await
.execute_document_symbols(&args)
.await
.map_err(|e| e.to_string())
}
#[tool(description = "Search for symbols by name across the entire workspace via LSP.")]
async fn lsp_workspace_symbols(
&self,
Parameters(params): Parameters<LspWorkspaceSymbolsParams>,
) -> Result<String, String> {
let Some(ref provider) = self.lsp else {
return Err("LSP server is not available. Start MCP server with --with-lsp=\"<command>\" to enable LSP features.".to_string());
};
let args = serde_json::to_value(¶ms).unwrap_or_default();
provider
.lock()
.await
.execute_workspace_symbols(&args)
.await
.map_err(|e| e.to_string())
}
#[tool(description = "Get code completion suggestions at a symbol position via LSP.")]
async fn lsp_completion(
&self,
Parameters(params): Parameters<LspPositionParams>,
) -> Result<String, String> {
let Some(ref provider) = self.lsp else {
return Err("LSP server is not available. Start MCP server with --with-lsp=\"<command>\" to enable LSP features.".to_string());
};
let args = serde_json::to_value(¶ms).unwrap_or_default();
provider
.lock()
.await
.execute_completion(&args)
.await
.map_err(|e| e.to_string())
}
#[tool(
description = "Search or rewrite code by AST structure. Pattern accepts three shapes (tried in order): \
(1) ast-grep pattern with metavariables — `$X` matches one node, `$$$` matches a sequence; \
(2) a bare tree-sitter kind name like `function_item`, `call_expression`, `import_statement` to match all nodes of that kind; \
(3) a literal code snippet. \
\n\nCommon LLM traps to avoid: \
(a) Decorators/attributes are children of the item — `pub struct $N { $$$ }` will NOT match `#[derive(...)] pub struct ...`. Either include the decorator (`#[derive($$$)] pub struct $N { $$$ }`) or use the bare kind (`struct_item`). \
(b) Method patterns need a receiver — write `$X.map_err($$$)`, not `.map_err($$$)`. \
(c) Generic arity is exact — `Result<$T>` matches one-arg Results only; use `Result<$_, $_>` for two args. \
(d) Pattern must be valid standalone code in the language — wrap type-expressions or fragments in code (e.g. for Rust types: `Arc<Mutex<$T>>` is invalid alone — use kind `generic_type` instead). \
\n\nLanguage tips: \
Rust kinds: function_item, struct_item, impl_item, enum_item, trait_item, use_declaration, call_expression, macro_invocation, match_expression, generic_type. \
JS/TS kinds: function_declaration, class_declaration, method_definition, import_statement, call_expression. \
Python kinds: function_definition, class_definition, import_statement, import_from_statement, decorated_definition. \
Go kinds: function_declaration, method_declaration, type_declaration, call_expression, if_statement. \
\n\nWhen a pattern returns zero matches the tool auto-retries with relaxed strictness and language-aware context wrapping, then emits a short diagnostic. Use `rewrite` to apply a template with metavariable substitution. Use `semantic_search` for meaning-based lookups instead of this tool."
)]
async fn structural_search(
&self,
Parameters(params): Parameters<StructuralSearchParams>,
) -> Result<String, String> {
debug!(
"Executing structural_search with pattern: {} lang: {}",
params.pattern, params.language
);
let current_dir = self.working_directory.clone();
let max_results = params.max_results.unwrap_or(50);
let context = params.context.unwrap_or(0);
let walker = ignore::WalkBuilder::new(¤t_dir)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.hidden(true)
.build();
let mut files = Vec::new();
for entry in walker.flatten() {
if !entry.file_type().is_some_and(|ft| ft.is_file()) {
continue;
}
let path = entry.path();
if crate::grep::language_from_extension(path) != Some(params.language.as_str()) {
continue;
}
if let Some(ref filter_paths) = params.paths {
let rel = path
.strip_prefix(¤t_dir)
.unwrap_or(path)
.to_string_lossy();
if !filter_paths.iter().any(|p| rel.contains(p)) {
continue;
}
}
files.push(path.to_path_buf());
}
if let Some(ref rewrite_template) = params.rewrite {
return self.structural_rewrite(
&files,
¤t_dir,
¶ms.pattern,
rewrite_template,
¶ms.language,
params.update_all.unwrap_or(false),
);
}
let outcome = smart_search_all_files(
&files,
¤t_dir,
¶ms.pattern,
¶ms.language,
max_results,
context > 0,
);
if outcome.matches.is_empty() {
return Ok(outcome
.diagnostic
.unwrap_or_else(|| "No matches found.".to_string()));
}
let output = if context > 0 {
crate::grep::format_matches_with_context(&outcome.matches, &outcome.source_map, context)
} else {
crate::grep::format_matches_grouped(&outcome.matches)
};
let header = outcome
.note
.as_deref()
.map(|n| format!("{}\n", n))
.unwrap_or_default();
Ok(format!(
"{}{}\n\n{} matches found.",
header,
output,
outcome.matches.len()
))
}
}
#[tool_handler(router = self.tool_router)]
impl ServerHandler for McpServer {
fn get_info(&self) -> ServerInfo {
let capabilities = ServerCapabilities::builder().enable_tools().build();
let instructions = if self.indexer_enabled {
"This server provides modular AI tools: semantic code search, view signatures, and GraphRAG (if available). Use 'semantic_search' for code/documentation searches and 'graphrag' (if enabled) for relationship queries."
} else {
"WARNING: Octocode indexer is disabled: not in a git repository root. Run with --no-git to enable indexing outside git repos. Tools available: semantic_search, view_signatures, graphrag (if enabled)."
};
ServerInfo::new(capabilities)
.with_server_info(
Implementation::new("octocode-mcp", env!("CARGO_PKG_VERSION"))
.with_description("Semantic code search server with vector embeddings and optional GraphRAG support"),
)
.with_instructions(instructions)
}
}
impl McpServer {
fn structural_rewrite(
&self,
files: &[std::path::PathBuf],
current_dir: &std::path::Path,
pattern: &str,
rewrite_template: &str,
language: &str,
update_all: bool,
) -> Result<String, String> {
let mut results = Vec::new();
let mut total_replacements = 0;
for path in files {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => continue,
};
let display_path = path
.strip_prefix(current_dir)
.unwrap_or(path)
.to_string_lossy()
.to_string();
match crate::grep::rewrite_file(
&display_path,
&source,
pattern,
rewrite_template,
language,
) {
Ok(Some(result)) => {
total_replacements += result.replacements;
results.push((path.clone(), result));
}
Ok(None) => {}
Err(e) => {
debug!("Error rewriting {}: {}", display_path, e);
}
}
}
if results.is_empty() {
return Ok("No matches found.".to_string());
}
if update_all {
for (path, result) in &results {
if let Err(e) = std::fs::write(path, &result.rewritten_source) {
return Err(format!("Failed to write {}: {}", result.file, e));
}
}
Ok(format!(
"Applied {} replacements across {} files.",
total_replacements,
results.len()
))
} else {
let mut output = String::new();
for (_, result) in &results {
output.push_str(&crate::grep::format_rewrite_diff(result));
output.push_str("\n\n");
}
output.push_str(&format!(
"{} replacements across {} files (preview, set update_all=true to apply).",
total_replacements,
results.len()
));
Ok(output)
}
}
pub(crate) fn new_repo_core(config: Config, working_directory: std::path::PathBuf) -> Self {
let semantic_code = SemanticCodeProvider::new(config.clone(), working_directory.clone());
let graphrag = GraphRagProvider::new(config, working_directory.clone());
let mut tool_router = Self::tool_router();
for name in [
"lsp_goto_definition",
"lsp_hover",
"lsp_find_references",
"lsp_document_symbols",
"lsp_workspace_symbols",
"lsp_completion",
] {
tool_router.remove_route(name);
}
if graphrag.is_none() {
tool_router.remove_route("graphrag");
}
Self {
working_directory,
semantic_code,
graphrag,
lsp: None,
indexer_enabled: true,
tool_router,
}
}
pub(crate) fn list_tool_defs(&self) -> Vec<Tool> {
self.tool_router.list_all()
}
pub(crate) async fn dispatch_tool(
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> Result<CallToolResult, ErrorData> {
let tcc = ToolCallContext::new(self, request, context);
self.tool_router.call(tcc).await
}
pub(crate) async fn start_repo_services(
config: Config,
working_directory: std::path::PathBuf,
no_git: bool,
debug: bool,
) -> Result<BackgroundServices> {
let db_path = crate::storage::get_project_database_path(&working_directory)?;
crate::storage::ensure_project_storage_exists(&working_directory)?;
let store = Arc::new(Store::new_with_path(db_path).await?);
store.initialize_collections().await?;
{
let store = store.clone();
let config = config.clone();
let working_directory = working_directory.clone();
tokio::spawn(async move {
if let Err(e) = perform_indexing(&store, &config, &working_directory, no_git).await
{
warn!(
"Initial index failed for {}: {}",
working_directory.display(),
e
);
}
});
}
start_background_services(config, store, working_directory, no_git, debug, None).await
}
pub async fn new(
config: Config,
debug_mode: bool,
working_directory: std::path::PathBuf,
no_git: bool,
lsp_command: Option<String>,
) -> Result<(Self, BackgroundServices)> {
std::env::set_current_dir(&working_directory).map_err(|e| {
anyhow::anyhow!(
"Failed to change to working directory '{}': {}",
working_directory.display(),
e
)
})?;
let store = Store::new().await?;
store.initialize_collections().await?;
let store = Arc::new(store);
init_mcp_logging(working_directory.clone(), debug_mode)?;
let semantic_code = SemanticCodeProvider::new(config.clone(), working_directory.clone());
let graphrag = GraphRagProvider::new(config.clone(), working_directory.clone());
let lsp = if let Some(command) = lsp_command {
info!(
"LSP provider will be initialized lazily with command: {}",
command
);
let provider = Arc::new(Mutex::new(crate::mcp::lsp::LspProvider::new(
working_directory.clone(),
command,
)));
let provider_clone = provider.clone();
tokio::spawn(async move {
let mut provider_guard = provider_clone.lock().await;
if let Err(e) = provider_guard.start_initialization().await {
warn!("LSP initialization failed: {}", e);
}
});
Some(provider)
} else {
None
};
let should_start_indexer = if !no_git && config.index.require_git {
indexer::git::is_git_repo_root(&working_directory)
} else {
true
};
if !should_start_indexer {
warn!(
"Indexer not started: Not in a git repository and --no-git flag not set. \
Use --no-git to enable indexing outside git repos."
);
}
let mut tool_router = Self::tool_router();
if lsp.is_none() {
for name in [
"lsp_goto_definition",
"lsp_hover",
"lsp_find_references",
"lsp_document_symbols",
"lsp_workspace_symbols",
"lsp_completion",
] {
tool_router.remove_route(name);
}
}
if graphrag.is_none() {
tool_router.remove_route("graphrag");
}
let server = Self {
working_directory: working_directory.clone(),
semantic_code,
graphrag,
lsp,
indexer_enabled: should_start_indexer,
tool_router,
};
let bg = if should_start_indexer {
start_background_services(
config,
store,
working_directory,
no_git,
debug_mode,
server.lsp.clone(),
)
.await?
} else {
BackgroundServices {
watcher_handle: None,
index_handle: None,
indexing_handle: None,
}
};
info!(
"MCP Server initialized (debug_mode={}, indexer={}, debounce={}ms, timeout={}ms, max_events={})",
debug_mode, should_start_indexer, MCP_DEBOUNCE_MS, MCP_INDEX_TIMEOUT_MS, MCP_MAX_PENDING_EVENTS
);
Ok((server, bg))
}
pub async fn run_stdio(self, _bg: BackgroundServices) -> Result<()> {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
super::logging::log_critical_anyhow_error(
"Panic in MCP server",
&anyhow::anyhow!("{}", info),
);
original_hook(info);
}));
info!("Starting MCP server in stdio mode");
let transport = rmcp::transport::io::stdio();
let service = self.serve(transport).await?;
service.waiting().await?;
Ok(())
}
pub async fn run_http(self, bind_addr: &str, _bg: BackgroundServices) -> Result<()> {
use hyper_util::rt::TokioIo;
use hyper_util::service::TowerToHyperService;
use rmcp::transport::streamable_http_server::{
session::local::LocalSessionManager, StreamableHttpService,
};
info!("Starting MCP server in HTTP mode on {}", bind_addr);
let server = self.clone();
let service = StreamableHttpService::new(
move || Ok(server.clone()),
Arc::new(LocalSessionManager::default()),
Default::default(),
);
let addr: std::net::SocketAddr = bind_addr
.parse()
.map_err(|e| anyhow::anyhow!("Invalid bind address '{}': {}", bind_addr, e))?;
let listener = tokio::net::TcpListener::bind(addr).await?;
info!("MCP HTTP server listening on {}", addr);
loop {
let (stream, remote_addr) = listener.accept().await?;
debug!("Accepted connection from {}", remote_addr);
let service = service.clone();
tokio::spawn(async move {
let io = TokioIo::new(stream);
let hyper_service = TowerToHyperService::new(service);
if let Err(e) = hyper::server::conn::http1::Builder::new()
.serve_connection(io, hyper_service)
.await
{
debug!("Connection error from {}: {}", remote_addr, e);
}
});
}
}
}
struct SmartSearchOutcome {
matches: Vec<crate::grep::GrepMatch>,
source_map: std::collections::HashMap<String, String>,
note: Option<String>,
diagnostic: Option<String>,
}
fn smart_search_all_files(
files: &[std::path::PathBuf],
current_dir: &std::path::Path,
pattern: &str,
language: &str,
max_results: usize,
want_source: bool,
) -> SmartSearchOutcome {
let info = crate::grep::pattern_info(pattern, language).ok();
let metavars: Vec<String> = info
.as_ref()
.map(|i| i.defined_vars.clone())
.unwrap_or_default();
let finalize = |mut out: StrategyOutput, note: Option<String>| -> SmartSearchOutcome {
rerank_matches(&mut out.0, &metavars);
out.0.truncate(max_results);
SmartSearchOutcome {
matches: out.0,
source_map: out.1,
note,
diagnostic: None,
}
};
let r1 = run_strategy_pattern(
files,
current_dir,
pattern,
language,
crate::grep::MatchStrictness::Smart,
max_results,
want_source,
);
if !r1.0.is_empty() {
return finalize(r1, None);
}
let relaxed_safe = info
.as_ref()
.map(|i| i.root_kind.is_none() && !i.has_error)
.unwrap_or(false);
if relaxed_safe {
let r2 = run_strategy_pattern(
files,
current_dir,
pattern,
language,
crate::grep::MatchStrictness::Relaxed,
max_results,
want_source,
);
if !r2.0.is_empty() {
return finalize(r2, Some("[strictness: relaxed]".to_string()));
}
}
if let Some((kind, desc)) = item_keyword_kind(pattern, language) {
let r3a = run_strategy_kind(files, current_dir, kind, language, max_results, want_source);
if !r3a.0.is_empty() {
return finalize(
r3a,
Some(format!(
"[kind: {}] broadened from `{}` (returns ALL {} — your pattern likely missed due to decorators or signature shape)",
kind,
pattern.trim(),
desc
)),
);
}
}
let r3 = run_strategy_kind(
files,
current_dir,
pattern,
language,
max_results,
want_source,
);
if !r3.0.is_empty() {
return finalize(r3, Some(format!("[kind: {}]", pattern)));
}
if let Some(canonical) = crate::grep::canonical_kind(pattern, language) {
if canonical != pattern {
let r3b = run_strategy_kind(
files,
current_dir,
canonical,
language,
max_results,
want_source,
);
if !r3b.0.is_empty() {
return finalize(
r3b,
Some(format!(
"[kind: {}] (canonical {} kind for `{}`)",
canonical, language, pattern
)),
);
}
}
}
for (desc, selector, context_src) in auto_contexts(language, pattern) {
let r4 = run_strategy_contextual(
files,
current_dir,
&context_src,
selector,
language,
max_results,
want_source,
);
if !r4.0.is_empty() {
return finalize(r4, Some(format!("[context wrap: {}]", desc)));
}
}
SmartSearchOutcome {
matches: Vec::new(),
source_map: std::collections::HashMap::new(),
note: None,
diagnostic: Some(build_diagnostic(pattern, language)),
}
}
fn rerank_matches(matches: &mut [crate::grep::GrepMatch], metavars: &[String]) {
if metavars.is_empty() {
return;
}
let needles: Vec<String> = metavars.iter().map(|n| n.to_ascii_lowercase()).collect();
matches.sort_by_key(|m| {
let path_lower = m.file.to_ascii_lowercase();
let hit = needles
.iter()
.any(|n| !n.is_empty() && path_lower.contains(n));
(if hit { 0u8 } else { 1u8 }, m.file.clone(), m.line)
});
}
type StrategyOutput = (
Vec<crate::grep::GrepMatch>,
std::collections::HashMap<String, String>,
);
fn run_strategy_pattern(
files: &[std::path::PathBuf],
current_dir: &std::path::Path,
pattern: &str,
language: &str,
strictness: crate::grep::MatchStrictness,
max_results: usize,
want_source: bool,
) -> StrategyOutput {
let mut matches = Vec::new();
let mut source_map = std::collections::HashMap::new();
for path in files {
if matches.len() >= max_results {
break;
}
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => continue,
};
let display_path = path
.strip_prefix(current_dir)
.unwrap_or(path)
.to_string_lossy()
.to_string();
if let Ok(found) = crate::grep::search_file_strict(
&display_path,
&source,
pattern,
language,
strictness.clone(),
) {
if !found.is_empty() {
if want_source {
source_map.insert(display_path.clone(), source);
}
matches.extend(found);
}
}
}
matches.truncate(max_results);
(matches, source_map)
}
fn run_strategy_kind(
files: &[std::path::PathBuf],
current_dir: &std::path::Path,
kind: &str,
language: &str,
max_results: usize,
want_source: bool,
) -> StrategyOutput {
let mut matches = Vec::new();
let mut source_map = std::collections::HashMap::new();
for path in files {
if matches.len() >= max_results {
break;
}
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => continue,
};
let display_path = path
.strip_prefix(current_dir)
.unwrap_or(path)
.to_string_lossy()
.to_string();
if let Ok(found) = crate::grep::search_file_by_kind(&display_path, &source, kind, language)
{
if !found.is_empty() {
if want_source {
source_map.insert(display_path.clone(), source);
}
matches.extend(found);
}
}
}
matches.truncate(max_results);
(matches, source_map)
}
fn run_strategy_contextual(
files: &[std::path::PathBuf],
current_dir: &std::path::Path,
context_src: &str,
selector: &str,
language: &str,
max_results: usize,
want_source: bool,
) -> StrategyOutput {
let mut matches = Vec::new();
let mut source_map = std::collections::HashMap::new();
for path in files {
if matches.len() >= max_results {
break;
}
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => continue,
};
let display_path = path
.strip_prefix(current_dir)
.unwrap_or(path)
.to_string_lossy()
.to_string();
if let Ok(found) = crate::grep::search_file_contextual(
&display_path,
&source,
context_src,
selector,
language,
) {
if !found.is_empty() {
if want_source {
source_map.insert(display_path.clone(), source);
}
matches.extend(found);
}
}
}
matches.truncate(max_results);
(matches, source_map)
}
fn auto_contexts(language: &str, pattern: &str) -> Vec<(&'static str, &'static str, String)> {
match language {
"rust" => {
let looks_like_type = pattern.contains('<')
&& pattern.contains('>')
&& !pattern.contains('{')
&& !pattern.contains(';')
&& !pattern.contains(" fn ")
&& !pattern.contains("fn ");
if looks_like_type {
vec![(
"Rust type alias context, generic_type selector",
"generic_type",
format!("type _ = {};", pattern),
)]
} else {
Vec::new()
}
}
"go" => vec![(
"Go func body, call_expression selector",
"call_expression",
format!("package _\nfunc _() {{ {} }}", pattern),
)],
"typescript" | "javascript" => vec![
(
"class body, field_definition selector",
"field_definition",
format!("class _ {{ {} }}", pattern),
),
(
"class body, method_definition selector",
"method_definition",
format!("class _ {{ {} }}", pattern),
),
],
"json" => vec![(
"object body, pair selector",
"pair",
format!("{{ {} }}", pattern),
)],
_ => Vec::new(),
}
}
fn item_keyword_kind(pattern: &str, language: &str) -> Option<(&'static str, &'static str)> {
let trimmed = pattern.trim_start();
let starts = |kw: &str| {
trimmed.starts_with(kw)
&& trimmed[kw.len()..]
.chars()
.next()
.map(|c| c.is_whitespace())
.unwrap_or(false)
};
match language {
"rust" => {
if starts("pub struct") || starts("struct") {
Some(("struct_item", "struct definitions"))
} else if starts("pub enum") || starts("enum") {
Some(("enum_item", "enum definitions"))
} else if starts("pub trait") || starts("trait") {
Some(("trait_item", "trait definitions"))
} else if starts("impl") {
Some(("impl_item", "impl blocks"))
} else if starts("pub fn")
|| starts("pub async fn")
|| starts("async fn")
|| starts("fn")
|| starts("pub const fn")
|| starts("const fn")
|| starts("pub unsafe fn")
|| starts("unsafe fn")
{
Some(("function_item", "function definitions"))
} else if starts("pub use") || starts("use") {
Some(("use_declaration", "use imports"))
} else if starts("pub mod") || starts("mod") {
Some(("mod_item", "module declarations"))
} else if starts("pub type") || starts("type") {
Some(("type_item", "type aliases"))
} else {
None
}
}
"javascript" | "typescript" => {
if starts("class") || starts("export class") {
Some(("class_declaration", "class declarations"))
} else if starts("function") || starts("async function") || starts("export function") {
Some(("function_declaration", "function declarations"))
} else if starts("interface") || starts("export interface") {
Some(("interface_declaration", "interface declarations"))
} else if starts("import") {
Some(("import_statement", "import statements"))
} else {
None
}
}
"python" => {
if starts("def") || starts("async def") {
Some(("function_definition", "function definitions"))
} else if starts("class") {
Some(("class_definition", "class definitions"))
} else if starts("from") {
Some(("import_from_statement", "from-imports"))
} else if starts("import") {
Some(("import_statement", "imports"))
} else if starts("@") {
Some(("decorated_definition", "decorated definitions"))
} else {
None
}
}
"go" => {
if starts("func") {
Some(("function_declaration", "function declarations"))
} else if starts("type") {
Some(("type_declaration", "type declarations"))
} else if starts("import") {
Some(("import_declaration", "import declarations"))
} else {
None
}
}
_ => None,
}
}
fn build_diagnostic(pattern: &str, language: &str) -> String {
let mut out = String::new();
out.push_str(&format!("No matches: {}\n", pattern));
match crate::grep::pattern_info(pattern, language) {
Ok(info) => {
let kind = info.root_kind.as_deref().unwrap_or("(bare $metavar)");
let vars = if info.defined_vars.is_empty() {
"none".to_string()
} else {
info.defined_vars
.iter()
.map(|v| format!("${}", v))
.collect::<Vec<_>>()
.join(",")
};
out.push_str(&format!(
"Parsed: kind={}, parse_error={}, metavars={}\n",
kind, info.has_error, vars
));
if info.has_error {
out.push_str(&format!(
"Hint: pattern doesn't parse as standalone {}. \
For type expressions try kind 'generic_type'; for code fragments \
wrap in a valid context. See tool description for shapes.\n",
language
));
} else if let Some((kind_name, label)) = item_keyword_kind(pattern, language) {
out.push_str(&format!(
"Hint: for finding all {} regardless of decorators/signature, \
pass `{}` as the pattern (a tree-sitter kind name).\n",
label, kind_name
));
} else if let Some(canonical) = crate::grep::canonical_kind(pattern, language) {
if canonical != pattern {
out.push_str(&format!(
"Hint: canonical {} kind for this intent is '{}'.\n",
language, canonical
));
}
} else {
out.push_str(
"Hint: see tool description for valid pattern shapes per language. \
For meaning-based lookups, use semantic_search.\n",
);
}
}
Err(e) => {
out.push_str(&format!("Parse error: {}\n", e));
out.push_str(
"Hint: pattern must be syntactically valid in the target language. \
See tool description for valid shapes.\n",
);
}
}
out
}
pub(crate) async fn start_background_services(
config: Config,
store: Arc<Store>,
working_directory: std::path::PathBuf,
no_git: bool,
debug: bool,
lsp: Option<Arc<Mutex<crate::mcp::lsp::LspProvider>>>,
) -> Result<BackgroundServices> {
let (file_tx, file_rx) = mpsc::channel(MCP_MAX_PENDING_EVENTS);
let (index_tx, index_rx) = mpsc::channel(10);
let working_dir = working_directory.clone();
let watcher_handle = tokio::spawn(async move {
if let Err(e) = run_watcher(file_tx, working_dir, debug, MCP_MAX_PENDING_EVENTS).await {
log_critical_anyhow_error("Watcher error", &e);
}
});
let indexing_in_progress = Arc::new(AtomicBool::new(false));
let indexing_flag = indexing_in_progress.clone();
let debug_mode = debug;
let index_handle = tokio::spawn(async move {
let mut file_rx = file_rx;
let mut last_event_time = None::<Instant>;
let mut pending_events = 0u32;
loop {
let timeout_duration = Duration::from_millis(MCP_DEBOUNCE_MS);
tokio::select! {
event_result = file_rx.recv() => {
match event_result {
Some(_) => {
pending_events += 1;
last_event_time = Some(Instant::now());
log_watcher_event("file_change", None, pending_events as usize);
}
None => {
debug!("File watcher channel closed, stopping debouncer");
break;
}
}
}
_ = sleep(timeout_duration), if last_event_time.is_some() => {
if let Some(last_time) = last_event_time {
if last_time.elapsed() >= timeout_duration && pending_events > 0 {
if indexing_flag
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
if debug_mode {
debug!(
"Debounce period completed ({} pending events), requesting reindex",
pending_events
);
}
log_watcher_event("debounce_trigger", None, pending_events as usize);
if (index_tx.send(()).await).is_err() {
if debug_mode {
debug!("Failed to send index request - server may be shutting down");
}
indexing_flag.store(false, Ordering::SeqCst);
break;
}
pending_events = 0;
last_event_time = None;
} else if debug_mode {
debug!("Indexing already in progress, will retry after current indexing completes");
}
}
}
}
}
}
});
let indexing_flag2 = indexing_in_progress;
let indexing_handle = tokio::spawn(async move {
let mut index_rx = index_rx;
loop {
match index_rx.recv().await {
Some(_) => {
debug!("Processing index request in background");
sleep(Duration::from_millis(DEFAULT_ADDITIONAL_DELAY_MS)).await;
let indexing_result = tokio::time::timeout(
Duration::from_millis(MCP_INDEX_TIMEOUT_MS),
perform_indexing(&store, &config, &working_directory, no_git),
)
.await;
match indexing_result {
Ok(Ok(())) => {
info!("Background reindex completed successfully");
if let Some(ref lsp_provider) = lsp {
let mut lsp_guard = lsp_provider.lock().await;
if let Err(e) =
update_lsp_after_indexing(&mut lsp_guard, &working_directory)
.await
{
debug!("LSP update after indexing failed: {}", e);
}
}
}
Ok(Err(e)) => {
log_critical_anyhow_error("Background reindex error", &e);
}
Err(_) => {
log_critical_anyhow_error(
"Background reindex timeout",
&anyhow::anyhow!(
"Background reindex timed out after {}ms",
MCP_INDEX_TIMEOUT_MS
),
);
}
}
indexing_flag2.store(false, Ordering::SeqCst);
}
None => {
debug!("Background indexing channel closed, stopping indexing task");
break;
}
}
}
});
Ok(BackgroundServices {
watcher_handle: Some(watcher_handle),
index_handle: Some(index_handle),
indexing_handle: Some(indexing_handle),
})
}
pub(crate) async fn perform_indexing(
store: &Store,
config: &Config,
working_directory: &std::path::Path,
no_git: bool,
) -> Result<()> {
let start_time = std::time::Instant::now();
log_indexing_operation("direct_reindex_start", None, None, true);
let mut lock = IndexLock::new(working_directory)?;
lock.acquire_async()
.await
.map_err(|e| anyhow::anyhow!("Failed to acquire index lock: {}", e))?;
debug!("MCP server: acquired indexing lock");
let state = state::create_shared_state();
state.write().current_directory = working_directory.to_path_buf();
let git_repo_root = if !no_git {
indexer::git::find_git_root(working_directory)
} else {
None
};
let indexing_result = indexer::index_files_with_quiet(
store,
state.clone(),
config,
git_repo_root.as_deref(),
true,
)
.await;
lock.release()?;
debug!("MCP server: released indexing lock");
let duration_ms = start_time.elapsed().as_millis() as u64;
match indexing_result {
Ok(()) => {
log_indexing_operation("direct_reindex_complete", None, Some(duration_ms), true);
Ok(())
}
Err(e) => {
log_indexing_operation("direct_reindex_complete", None, Some(duration_ms), false);
log_critical_anyhow_error("Direct indexing", &e);
Err(e)
}
}
}
async fn update_lsp_after_indexing(
lsp_provider: &mut crate::mcp::lsp::LspProvider,
working_directory: &std::path::Path,
) -> Result<()> {
use crate::indexer::{detect_language, NoindexWalker, PathUtils};
debug!("Updating LSP server with changed files");
let walker = NoindexWalker::create_walker(working_directory).build();
let mut files_updated = 0;
for result in walker {
let entry = match result {
Ok(entry) => entry,
Err(_) => continue,
};
if !entry.file_type().is_some_and(|ft| ft.is_file()) {
continue;
}
if detect_language(entry.path()).is_some() {
let relative_path = PathUtils::to_relative_string(entry.path(), working_directory);
if let Err(e) = lsp_provider.update_file(&relative_path).await {
debug!("Failed to update file {} in LSP: {}", relative_path, e);
} else {
files_updated += 1;
}
}
}
debug!("LSP update completed: {} files updated", files_updated);
Ok(())
}