/**
* Symbol search and definition extraction tools
*/
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type {
SymbolSearchOptions,
DefinitionOptions,
SearchResult,
} from "../types.js";
import { searchfoxClient } from "../utils/searchfox-client.js";
import { githubClient } from "../utils/github-client.js";
import { parseQuerySyntax, buildQueryString } from "../utils/query-builder.js";
import { applyFilters } from "../utils/filters.js";
import { getSearchfoxSearchUrl } from "../utils/repo-mapping.js";
import {
extractDefinition,
detectLanguage,
} from "../parsers/definition-extractor.js";
import { DEFAULT_SEARCH_LIMIT } from "../constants.js";
/**
* Search for symbols or identifiers
*/
export async function searchSymbols(
options: SymbolSearchOptions
): Promise<CallToolResult> {
try {
const repo = options.repo || "mozilla-central";
const limit = options.limit || DEFAULT_SEARCH_LIMIT;
const searchType = options.type || "symbol";
// Build query based on search type
const prefix = searchType === "symbol" ? "symbol:" : "id:";
let query = `${prefix}${options.symbol}`;
// Add path filter if provided
if (options.path_filter) {
query += ` path:${options.path_filter}`;
}
// Parse and build query
const components = parseQuerySyntax(query);
const queryString = buildQueryString(components);
// Execute search
const data = await searchfoxClient.search(repo, queryString, {});
const results: SearchResult[] = [];
// Process all sections dynamically
for (const [sectionKey, sectionValue] of Object.entries(data)) {
if (sectionKey.startsWith("*")) {
continue;
}
if (typeof sectionValue === "object" && sectionValue !== null) {
if (Array.isArray(sectionValue)) {
for (const file of sectionValue) {
if (file.lines && Array.isArray(file.lines)) {
for (const line of file.lines) {
// Apply language filter if specified
if (options.language) {
const filters = { language: options.language };
if (!applyFilters(file.path, filters)) {
continue;
}
}
if (results.length >= limit) {
break;
}
results.push({
path: file.path,
line: line.lno,
column: line.bounds?.[0] || 0,
snippet: line.line,
context: sectionKey,
contextsym: line.contextsym,
peekRange: line.peekRange,
upsearch: line.upsearch,
bounds: line.bounds,
});
}
}
}
} else {
// Handle categorized results
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const categoryMap = sectionValue as Record<string, Array<any>>;
for (const [category, categoryResults] of Object.entries(
categoryMap
)) {
if (Array.isArray(categoryResults)) {
for (const file of categoryResults) {
if (file.lines && Array.isArray(file.lines)) {
for (const line of file.lines) {
// Apply language filter if specified
if (options.language) {
const filters = { language: options.language };
if (!applyFilters(file.path, filters)) {
continue;
}
}
if (results.length >= limit) {
break;
}
results.push({
path: file.path,
line: line.lno,
column: line.bounds?.[0] || 0,
snippet: line.line,
context: `${sectionKey}: ${category}`,
contextsym: line.contextsym,
peekRange: line.peekRange,
upsearch: line.upsearch,
bounds: line.bounds,
});
}
}
}
}
}
}
}
if (results.length >= limit) {
break;
}
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{
symbol: options.symbol,
search_type: searchType,
repo,
count: results.length,
results,
searchfox_url: getSearchfoxSearchUrl(repo, queryString, {}),
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: `Symbol search failed: ${error instanceof Error ? error.message : String(error)}`,
},
null,
2
),
},
],
isError: true,
};
}
}
/**
* Get complete definition of a symbol
*/
export async function getDefinition(
options: DefinitionOptions
): Promise<CallToolResult> {
try {
const repo = options.repo || "mozilla-central";
const maxLines = options.max_lines || 500;
// First, search for the symbol to find its location
// Use id: prefix for exact identifier matching
let query = `id:${options.symbol}`;
if (options.path_filter) {
query += ` path:${options.path_filter}`;
}
const components = parseQuerySyntax(query);
const queryString = buildQueryString(components);
const data = await searchfoxClient.search(repo, queryString, {});
// Find the first definition result (prefer "Definitions" category and non-test files)
let definitionFile: string | null = null;
let definitionLine: number | null = null;
// Priority order: look for "Definitions" categories first, then others
const categoriesToCheck = ["Definitions", "Declarations", ""];
for (const priorityCategory of categoriesToCheck) {
if (definitionFile) break;
for (const [sectionKey, sectionValue] of Object.entries(data)) {
if (sectionKey.startsWith("*")) {
continue;
}
if (typeof sectionValue === "object" && sectionValue !== null) {
if (Array.isArray(sectionValue)) {
// Handle direct array (non-categorized results)
if (priorityCategory !== "") continue; // Skip on first passes
for (const file of sectionValue) {
if (file.lines && Array.isArray(file.lines)) {
for (const line of file.lines) {
// Apply language filter if specified
if (options.language && options.language !== "auto") {
const filters = { language: options.language };
if (!applyFilters(file.path, filters)) {
continue;
}
}
// Skip test and third-party files unless they're the only option
const isPreferred =
!file.path.includes("test") &&
!file.path.includes("third_party") &&
!file.path.includes("thirdparty");
if (isPreferred || !definitionFile) {
definitionFile = file.path;
definitionLine = line.lno;
if (isPreferred) {
break;
}
}
}
if (definitionFile && definitionLine) {
break;
}
}
}
} else {
// Handle categorized results (e.g., "normal: Definitions (nsIFrame)")
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const categoryMap = sectionValue as Record<string, Array<any>>;
for (const [categoryName, categoryResults] of Object.entries(
categoryMap
)) {
// Check if this category matches our priority
if (
priorityCategory &&
!categoryName.includes(priorityCategory)
) {
continue;
}
if (Array.isArray(categoryResults)) {
for (const file of categoryResults) {
if (file.lines && Array.isArray(file.lines)) {
for (const line of file.lines) {
// Apply language filter if specified
if (options.language && options.language !== "auto") {
const filters = { language: options.language };
if (!applyFilters(file.path, filters)) {
continue;
}
}
// Skip test and third-party files unless they're the only option
const isPreferred =
!file.path.includes("test") &&
!file.path.includes("third_party") &&
!file.path.includes("thirdparty");
if (isPreferred || !definitionFile) {
definitionFile = file.path;
definitionLine = line.lno;
if (isPreferred) {
break;
}
}
}
if (definitionFile && definitionLine) {
break;
}
}
}
}
if (definitionFile && definitionLine) {
break;
}
}
}
if (definitionFile && definitionLine) {
break;
}
}
}
}
if (!definitionFile || !definitionLine) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: `Symbol '${options.symbol}' not found in repository`,
},
null,
2
),
},
],
isError: true,
};
}
// Fetch the file content from GitHub (not Searchfox, which returns HTML)
const fileContent = await githubClient.getFileContent(repo, definitionFile);
// Detect language from file path
let language =
options.language === "auto" || !options.language
? detectLanguage(definitionFile)
: options.language;
if (!language) {
// Default to cpp if we can't detect
language = "cpp";
}
// Extract the definition
const result = extractDefinition(
fileContent,
definitionLine,
language,
maxLines
);
if (!result) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error:
"Failed to extract definition - could not find complete code block",
},
null,
2
),
},
],
isError: true,
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{
symbol: options.symbol,
path: definitionFile,
repo,
language,
start_line: result.startLine,
end_line: result.endLine,
signature: result.signature,
definition: result.definition,
lines: result.endLine - result.startLine + 1,
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: `Get definition failed: ${error instanceof Error ? error.message : String(error)}`,
},
null,
2
),
},
],
isError: true,
};
}
}