/**
* Call graph analysis tool
*/
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { CallGraphOptions } from "../types.js";
import { formatCallGraphMarkdown } from "../parsers/callgraph-parser.js";
import { SEARCHFOX_BASE_URL } from "../constants.js";
/**
* Execute call graph query
*/
export async function getCallGraph(
options: CallGraphOptions
): Promise<CallToolResult> {
try {
const repo = options.repo || "mozilla-central";
const depth = options.depth || 1;
// Validate depth
if (depth < 1 || depth > 3) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Depth must be between 1 and 3",
},
null,
2
),
},
],
isError: true,
};
}
// Build query string based on mode
let queryString: string;
let queryText: string;
switch (options.mode) {
case "calls-from":
queryString = `calls-from:'${options.source_symbol}' depth:${depth} graph-format:json`;
queryText = `Calls from: ${options.source_symbol} (depth: ${depth})`;
break;
case "calls-to":
queryString = `calls-to:'${options.source_symbol}' depth:${depth} graph-format:json`;
queryText = `Calls to: ${options.source_symbol} (depth: ${depth})`;
break;
case "calls-between":
if (!options.target_symbol) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "target_symbol is required for calls-between mode",
},
null,
2
),
},
],
isError: true,
};
}
queryString = `calls-between-source:'${options.source_symbol.trim()}' calls-between-target:'${options.target_symbol.trim()}' depth:${depth} graph-format:json`;
queryText = `Calls between: ${options.source_symbol} -> ${options.target_symbol} (depth: ${depth})`;
break;
default:
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: `Invalid mode: ${options.mode}`,
},
null,
2
),
},
],
isError: true,
};
}
// Build URL
const searchParams = new URLSearchParams();
searchParams.set("q", queryString);
const url = `${SEARCHFOX_BASE_URL}/${repo}/query/default?${searchParams.toString()}`;
// Execute query
const response = await fetch(url, {
headers: {
Accept: "application/json",
"User-Agent": `searchfox-mcp/1.0.0 (+https://github.com/canova/searchfox-mcp)`,
},
});
if (!response.ok) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: `HTTP ${response.status}: ${response.statusText}`,
},
null,
2
),
},
],
isError: true,
};
}
const responseText = await response.text();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let jsonData: any;
try {
jsonData = JSON.parse(responseText);
} catch {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Failed to parse response as JSON",
raw_response: responseText.substring(0, 500),
},
null,
2
),
},
],
isError: true,
};
}
// Extract SymbolGraphCollection if present
const graphData = jsonData.SymbolGraphCollection || jsonData;
// Format as markdown
const markdown = formatCallGraphMarkdown(queryText, graphData);
// Also return raw data for programmatic access
return {
content: [
{
type: "text",
text: JSON.stringify(
{
mode: options.mode,
source_symbol: options.source_symbol,
target_symbol: options.target_symbol,
repo,
depth,
markdown_output: markdown,
query_url: url,
raw_data: graphData,
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: `Call graph query failed: ${error instanceof Error ? error.message : String(error)}`,
},
null,
2
),
},
],
isError: true,
};
}
}