use rust_mcp_sdk::macros::{JsonSchema, mcp_tool};
use rust_mcp_sdk::schema::{CallToolResult, TextContent, schema_utils::CallToolError};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{info, instrument};
use crate::mcp_server::tools::lsp_helpers::document_symbols::SymbolSearchBuilder;
use crate::mcp_server::tools::lsp_helpers::workspace_symbols::WorkspaceSymbolSearchBuilder;
use crate::mcp_server::tools::utils;
use crate::project::index::IndexStatusView;
use crate::project::{ComponentSession, ProjectComponent, ProjectWorkspace};
use crate::symbol::Symbol;
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchResult {
pub success: bool,
pub query: String,
pub total_matches: usize,
pub symbols: Vec<Symbol>,
pub metadata: SearchMetadata,
#[serde(skip_serializing_if = "Option::is_none")]
pub index_status: Option<IndexStatusView>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SearchMetadata {
pub search_type: String,
pub build_directory: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub files_processed: Option<Vec<FileProcessingResult>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FileProcessingResult {
pub file: String,
pub status: String,
pub symbols_found: usize,
}
#[mcp_tool(
name = "search_symbols",
description = "Advanced C++ symbol search engine with intelligent dual-mode operation for comprehensive \
codebase exploration. Leverages clangd LSP for semantic understanding and provides \
both broad workspace discovery and precise file-specific analysis capabilities.
🚀 RECOMMENDED WORKFLOW FOR AI AGENTS:
1. ALWAYS call get_project_details first to discover available build directories
2. Use the ABSOLUTE build directory paths from get_project_details output
3. Then call search_symbols with the build_directory parameter
Example workflow:
• get_project_details {} → Returns: {\"/home/project/build-debug\": {...}}
• search_symbols {\"query\": \"Math\", \"build_directory\": \"/home/project/build-debug\"}
⚡ WHY USE THESE TOOLS:
• MUCH FASTER than filesystem reads (ls, find, grep commands)
• SEMANTIC AWARENESS: Understands C++ syntax, templates, namespaces
• PROJECT INTELLIGENCE: Filters out system/external symbols automatically
• LSP INTEGRATION: Uses same semantic understanding as IDEs
🔍 DUAL SEARCH MODES:
• Workspace Search (default): Fuzzy matching across entire codebase using clangd workspace symbols
• Document Search (with files parameter): Comprehensive symbol enumeration within specific files
• Smart mode selection based on parameters for optimal results
📋 SYMBOL OVERVIEW CAPABILITY:
• Use empty query (\"\") with files parameter to list ALL symbols in specified files
• Use empty query (\"\") without files for workspace-wide symbol discovery (subject to clangd heuristics)
• Perfect for getting complete symbol inventory of headers or source files
• Ideal for API exploration and codebase familiarization
• No search filtering - shows comprehensive symbol catalog
• ⚠️ Note: Workspace-wide empty queries may not return all symbols due to clangd's internal filtering
🎯 INTELLIGENT FILTERING:
• Symbol kinds: Class, Function, Method, Variable, Enum, Namespace, Constructor, Field, Interface, Struct
• Project boundary detection (exclude external/system symbols by default)
• Fuzzy matching with clangd's relevance ranking preserved
• Configurable result limits with smart client-side application
⚡ PERFORMANCE & RELIABILITY:
• Fixed 2000-symbol queries to clangd with client-side limiting for consistent ranking
• Indexing progress tracking with configurable timeout control
• Automatic build directory detection and validation
• Graceful handling of large codebases with intelligent result capping
🏗️ BUILD SYSTEM INTEGRATION:
• Multi-provider support (CMake, Meson, extensible architecture)
• Automatic compilation database discovery and validation
• Custom build directory specification for multi-component projects
• Project vs external symbol classification using compilation database analysis
🎮 USAGE PATTERNS:
• Discovery: search_symbols {\"query\": \"vector\", \"max_results\": 10}
• Type filtering: search_symbols {\"query\": \"Process\", \"kinds\": [\"Class\", \"Struct\"]}
• File overview: search_symbols {\"query\": \"\", \"files\": [\"include/api.h\"]}
• PROJECT EXPLORATION: search_symbols {\"query\": \"\", \"max_results\": 100, \"build_directory\": \"/abs/path\"}
→ Returns top symbols to understand what the project does (classes, main functions, key APIs)
• Workspace overview: search_symbols {\"query\": \"\", \"max_results\": 500} (limited by clangd)
• External symbols: search_symbols {\"query\": \"std::\", \"include_external\": true}
INPUT PARAMETERS:
• query: C++ symbol name to search (NOT file paths!) - use \"\" when unsure to explore first
• files: Optional file paths for document-specific search
• kinds: Optional symbol type filtering (PascalCase names)
• max_results: Result limit (default: 100, max: 1000)
• include_external: Include system/library symbols (default: false)
• build_directory: Custom build directory path (STRONGLY PREFER ABSOLUTE PATHS from get_project_details)
• wait_timeout: Indexing completion timeout in seconds (default: 20s)"
)]
#[derive(Debug, serde::Serialize, serde::Deserialize, JsonSchema)]
pub struct SearchSymbolsTool {
pub query: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kinds: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub files: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_results: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_external: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_directory: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wait_timeout: Option<u64>,
}
impl SearchSymbolsTool {
#[instrument(name = "search_symbols", skip(self, component_session, workspace))]
pub async fn call_tool(
&self,
component_session: Arc<ComponentSession>,
workspace: &ProjectWorkspace,
) -> Result<CallToolResult, CallToolError> {
let symbol_kinds: Option<Vec<lsp_types::SymbolKind>> =
if let Some(ref kind_names) = self.kinds {
let mut kinds = Vec::new();
for kind_name in kind_names {
match lsp_types::SymbolKind::try_from(kind_name.as_str()) {
Ok(kind) => kinds.push(kind),
Err(_) => {
return Err(CallToolError::new(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("Invalid symbol kind: '{}'", kind_name),
)));
}
}
}
Some(kinds)
} else {
None
};
info!(
"Searching symbols (v2): query='{}', kinds={:?}, max_results={:?}, wait_timeout={:?}",
self.query, symbol_kinds, self.max_results, self.wait_timeout
);
let index_status = utils::handle_selective_indexing_wait(
&component_session,
self.files.is_some(), self.wait_timeout,
if self.files.is_some() {
"Document search"
} else {
"Workspace search"
},
)
.await;
let build_dir = component_session.build_dir();
let component = workspace
.get_component_by_build_dir(build_dir)
.ok_or_else(|| {
CallToolError::new(std::io::Error::other(
"Build directory not found in workspace",
))
})?;
let mut result = if let Some(ref files) = self.files {
self.search_in_files(&component_session, files, component, symbol_kinds.as_ref())
.await?
} else {
self.search_workspace_symbols(&component_session, component, symbol_kinds.as_ref())
.await?
};
result.index_status = index_status;
let output = serde_json::to_string_pretty(&result).map_err(|e| {
CallToolError::new(std::io::Error::other(format!(
"Failed to serialize result: {}",
e
)))
})?;
Ok(CallToolResult::text_content(vec![TextContent::from(
output,
)]))
}
async fn search_workspace_symbols(
&self,
component_session: &ComponentSession,
component: &ProjectComponent,
symbol_kinds: Option<&Vec<lsp_types::SymbolKind>>,
) -> Result<SearchResult, CallToolError> {
let mut search_builder = WorkspaceSymbolSearchBuilder::new(self.query.clone())
.include_external(self.include_external.unwrap_or(false));
if let Some(kinds) = symbol_kinds {
search_builder = search_builder.with_kinds(kinds.clone());
}
if let Some(max) = self.max_results {
search_builder = search_builder.with_max_results(max);
}
let workspace_symbols = search_builder
.search(component_session, component)
.await
.map_err(|e| {
CallToolError::new(std::io::Error::other(format!(
"Failed to search symbols: {}",
e
)))
})?;
let symbols: Vec<Symbol> = workspace_symbols.into_iter().map(Symbol::from).collect();
Ok(SearchResult {
success: true,
query: self.query.clone(),
total_matches: symbols.len(),
symbols,
metadata: SearchMetadata {
search_type: "workspace".to_string(),
build_directory: component.build_dir_path.display().to_string(),
files_processed: None,
},
index_status: None, })
}
async fn search_in_files(
&self,
component_session: &ComponentSession,
files: &[String],
component: &ProjectComponent,
symbol_kinds: Option<&Vec<lsp_types::SymbolKind>>,
) -> Result<SearchResult, CallToolError> {
info!(
"Document search: query='{}', files={:?}, kinds={:?}",
self.query, files, symbol_kinds
);
let project_root = &component.source_root_path;
let mut absolute_files = Vec::new();
for file_path in files {
let absolute_path = if std::path::Path::new(file_path).is_absolute() {
file_path.clone()
} else {
let resolved_path = project_root.join(file_path);
if !resolved_path.exists() {
return Err(CallToolError::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!(
"File not found: {} (resolved to {})",
file_path,
resolved_path.display()
),
)));
}
resolved_path.to_string_lossy().to_string()
};
absolute_files.push(absolute_path);
}
info!("Resolved files: {:?}", absolute_files);
let mut search_builder = SymbolSearchBuilder::new();
if !self.query.is_empty() {
search_builder = search_builder.with_name(&self.query);
}
if let Some(kinds) = symbol_kinds {
search_builder = search_builder.with_kinds(kinds);
}
info!("Created search builder: {:?}", search_builder);
let file_results = search_builder
.search_multiple_files(component_session, &absolute_files, self.max_results)
.await
.map_err(|e| {
CallToolError::new(std::io::Error::other(format!(
"Failed to search files: {}",
e
)))
})?;
info!(
"File search results: {} files processed",
file_results.len()
);
let mut all_symbols = Vec::new();
let mut processed_files = Vec::new();
for (file_path, symbols) in file_results {
processed_files.push(FileProcessingResult {
file: file_path.clone(),
status: "success".to_string(),
symbols_found: symbols.len(),
});
for symbol in symbols {
let path = std::path::PathBuf::from(&file_path);
let converted_symbol = Symbol::from((&symbol, path.as_path()));
all_symbols.push(converted_symbol);
}
}
Ok(SearchResult {
success: true,
query: self.query.clone(),
total_matches: all_symbols.len(),
symbols: all_symbols,
metadata: SearchMetadata {
search_type: "file_specific".to_string(),
build_directory: component.build_dir_path.display().to_string(),
files_processed: Some(processed_files),
},
index_status: None, })
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_search_symbols_deserialize() {
let json_data = json!({
"query": "vector",
"kinds": ["Class", "Function"],
"max_results": 50
});
let tool: SearchSymbolsTool = serde_json::from_value(json_data).unwrap();
assert_eq!(tool.query, "vector");
assert_eq!(
tool.kinds,
Some(vec!["Class".to_string(), "Function".to_string()])
);
assert_eq!(tool.max_results, Some(50));
assert_eq!(tool.wait_timeout, None);
}
#[test]
fn test_search_symbols_minimal() {
let json_data = json!({
"query": "main"
});
let tool: SearchSymbolsTool = serde_json::from_value(json_data).unwrap();
assert_eq!(tool.query, "main");
assert_eq!(tool.kinds, None);
assert_eq!(tool.max_results, None);
assert_eq!(tool.wait_timeout, None);
}
}