import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
// Import types
import type {
SearchOptions,
FileOptions,
PathSearchOptions,
SymbolSearchOptions,
DefinitionOptions,
CallGraphOptions,
FieldLayoutOptions,
BlameOptions,
} from "./types.js";
// Import utilities
import {
DEFAULT_SEARCH_LIMIT,
DEFAULT_PATH_SEARCH_LIMIT,
} from "./constants.js";
// Import tools
import { search } from "./tools/search.js";
import { getFile, searchPaths } from "./tools/files.js";
import { searchSymbols, getDefinition } from "./tools/symbols.js";
import { getCallGraph } from "./tools/callgraph.js";
import { getFieldLayout } from "./tools/analysis.js";
import { getBlame } from "./tools/blame.js";
class SearchfoxServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: "searchfox-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupHandlers();
}
private setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "search",
description:
"Unified search for code in Mozilla repositories with advanced query syntax support. Supports symbol:, id:, path:, pathre:, text:, re: prefixes for precise searching. Includes language and category filtering (tests, generated, third-party).",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description:
"Search query with optional advanced syntax. Supports: symbol:Name (symbol search), id:Name (exact identifier), path:dir/file (path filter), pathre:regex (path regex), text:exact (exact text), re:pattern (regex pattern). Plain text without prefix performs standard search.",
},
repo: {
type: "string",
description:
"Repository to search in (e.g., mozilla-central, comm-central, autoland)",
default: "mozilla-central",
},
case_sensitive: {
type: "boolean",
description: "Enable case sensitive search",
default: false,
},
limit: {
type: "number",
description: "Maximum number of results to return",
default: 50,
},
context_lines: {
type: "number",
description:
"Number of lines of context to include before and after each match",
},
filters: {
type: "object",
description: "Additional filters to apply to search results",
properties: {
language: {
type: "string",
enum: ["cpp", "c", "js", "webidl", "rust"],
description: "Filter by programming language",
},
exclude_tests: {
type: "boolean",
description: "Exclude test files from results",
},
exclude_generated: {
type: "boolean",
description: "Exclude generated files from results",
},
exclude_thirdparty: {
type: "boolean",
description: "Exclude third-party files from results",
},
only_tests: {
type: "boolean",
description: "Include only test files in results",
},
},
},
},
required: ["query"],
},
},
{
name: "get_file",
description:
"Get the contents of a specific file from a repository with optional line range support. Can retrieve full files or specific line ranges with context.",
inputSchema: {
type: "object",
properties: {
repo: {
type: "string",
description: "Repository name",
default: "mozilla-central",
},
path: {
type: "string",
description: "File path within the repository",
},
lines: {
type: "object",
description:
"Optional line range to retrieve. If not specified, returns full file.",
properties: {
start: {
type: "number",
description: "Starting line number (1-based)",
},
end: {
type: "number",
description: "Ending line number (inclusive)",
},
context: {
type: "number",
description:
"Number of additional context lines to include before and after the range",
},
},
},
},
required: ["path"],
},
},
{
name: "search_paths",
description:
"Search for files by path pattern in Mozilla repositories. Useful for finding files matching a specific name or directory pattern.",
inputSchema: {
type: "object",
properties: {
pattern: {
type: "string",
description:
"File path pattern to search for (e.g., 'AudioContext', 'dom/media/*.cpp', 'test_*')",
},
repo: {
type: "string",
description: "Repository to search in",
default: "mozilla-central",
},
limit: {
type: "number",
description: "Maximum number of file paths to return",
default: 100,
},
},
required: ["pattern"],
},
},
{
name: "search_symbols",
description:
"Search for symbol definitions using searchfox's symbol indexing. Uses 'symbol:' query syntax internally.",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description:
"Symbol to search for. Example symbol: 'T_ActivePS'",
},
type: {
type: "string",
enum: ["symbol", "identifier"],
description:
"Search type: 'symbol' for symbol search (definitions/declarations), 'identifier' for exact identifier matching",
default: "symbol",
},
repo: {
type: "string",
description: "Repository to search in",
default: "mozilla-central",
},
path_filter: {
type: "string",
description:
"Optional path filter to narrow results (e.g., 'dom/media')",
},
limit: {
type: "number",
description: "Maximum number of results to return",
default: 50,
},
language: {
type: "string",
enum: ["cpp", "c", "js", "webidl", "rust"],
description: "Filter results by programming language",
},
},
required: ["symbol"],
},
},
{
name: "get_definition",
description:
"Extract the complete definition of a symbol (function, class, struct, etc.) from Mozilla repositories. Uses intelligent brace-matching to extract complete code blocks.",
inputSchema: {
type: "object",
properties: {
symbol: {
type: "string",
description: "Symbol name to get definition for",
},
repo: {
type: "string",
description: "Repository to search in",
default: "mozilla-central",
},
path_filter: {
type: "string",
description:
"Optional path filter to find the right definition (e.g., 'dom/media')",
},
language: {
type: "string",
enum: ["cpp", "c", "js", "webidl", "rust", "auto"],
description:
"Language hint for definition extraction. Use 'auto' for automatic detection.",
default: "auto",
},
max_lines: {
type: "number",
description: "Maximum number of lines to extract",
default: 500,
},
},
required: ["symbol"],
},
},
{
name: "get_call_graph",
description:
"Analyze function call relationships in Mozilla repositories. Shows what functions call or are called by a target function, or finds call paths between two functions. Supports C++ and languages with SCIP indexing (Java, Kotlin, Python), but not JavaScript/TypeScript.",
inputSchema: {
type: "object",
properties: {
mode: {
type: "string",
enum: ["calls-from", "calls-to", "calls-between"],
description:
"Query mode: 'calls-from' shows functions called by the source, 'calls-to' shows functions that call the source, 'calls-between' finds call paths between two functions",
},
source_symbol: {
type: "string",
description:
"Source symbol/function name (e.g., 'mozilla::dom::AudioContext::Create')",
},
target_symbol: {
type: "string",
description:
"Target symbol/function name (required for 'calls-between' mode)",
},
repo: {
type: "string",
description: "Repository to search in",
default: "mozilla-central",
},
depth: {
type: "number",
description:
"Maximum traversal depth (1-3). Controls both breadth-first traversal depth and maximum path length.",
default: 1,
minimum: 1,
maximum: 3,
},
},
required: ["mode", "source_symbol"],
},
},
{
name: "get_field_layout",
description:
"Get memory layout information for C++ classes and structs. Shows size, alignment, base class layouts, and field offsets. Only works with C++ code.",
inputSchema: {
type: "object",
properties: {
class_name: {
type: "string",
description:
"C++ class or struct name (e.g., 'mozilla::dom::AudioContext', 'nsISupports')",
},
repo: {
type: "string",
description: "Repository to search in",
default: "mozilla-central",
},
},
required: ["class_name"],
},
},
{
name: "get_blame",
description:
"Get git blame information for a file showing commit hash, author, date, and message for each line. Supports line range filtering.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "File path within the repository",
},
repo: {
type: "string",
description: "Repository to search in",
default: "mozilla-central",
},
start_line: {
type: "number",
description: "Starting line number (1-based, inclusive)",
default: 1,
},
end_line: {
type: "number",
description:
"Ending line number (inclusive). If not specified, shows blame to end of file.",
},
},
required: ["path"],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (!args) {
throw new McpError(ErrorCode.InvalidParams, "Missing arguments");
}
switch (name) {
case "search": {
const searchArgs = args as Record<string, unknown>;
if (!searchArgs.query || typeof searchArgs.query !== "string") {
throw new McpError(
ErrorCode.InvalidParams,
"Query parameter is required and must be a string"
);
}
const options: SearchOptions = {
query: searchArgs.query,
repo:
typeof searchArgs.repo === "string"
? searchArgs.repo
: "mozilla-central",
case_sensitive:
typeof searchArgs.case_sensitive === "boolean"
? searchArgs.case_sensitive
: false,
limit:
typeof searchArgs.limit === "number"
? searchArgs.limit
: DEFAULT_SEARCH_LIMIT,
context_lines:
typeof searchArgs.context_lines === "number"
? searchArgs.context_lines
: undefined,
};
// Parse filters if provided
if (searchArgs.filters && typeof searchArgs.filters === "object") {
const filters = searchArgs.filters as Record<string, unknown>;
options.filters = {
language:
typeof filters.language === "string"
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
(filters.language as any)
: undefined,
exclude_tests:
typeof filters.exclude_tests === "boolean"
? filters.exclude_tests
: undefined,
exclude_generated:
typeof filters.exclude_generated === "boolean"
? filters.exclude_generated
: undefined,
exclude_thirdparty:
typeof filters.exclude_thirdparty === "boolean"
? filters.exclude_thirdparty
: undefined,
only_tests:
typeof filters.only_tests === "boolean"
? filters.only_tests
: undefined,
};
}
return search(options);
}
case "get_file": {
const fileArgs = args as Record<string, unknown>;
if (!fileArgs.path || typeof fileArgs.path !== "string") {
throw new McpError(
ErrorCode.InvalidParams,
"Path parameter is required and must be a string"
);
}
const options: FileOptions = {
path: fileArgs.path,
repo:
typeof fileArgs.repo === "string"
? fileArgs.repo
: "mozilla-central",
};
// Parse line range if provided
if (fileArgs.lines && typeof fileArgs.lines === "object") {
const lines = fileArgs.lines as Record<string, unknown>;
options.lines = {
start: typeof lines.start === "number" ? lines.start : undefined,
end: typeof lines.end === "number" ? lines.end : undefined,
context:
typeof lines.context === "number" ? lines.context : undefined,
};
}
return getFile(options);
}
case "search_paths": {
const pathArgs = args as Record<string, unknown>;
if (!pathArgs.pattern || typeof pathArgs.pattern !== "string") {
throw new McpError(
ErrorCode.InvalidParams,
"Pattern parameter is required and must be a string"
);
}
const options: PathSearchOptions = {
pattern: pathArgs.pattern,
repo:
typeof pathArgs.repo === "string"
? pathArgs.repo
: "mozilla-central",
limit:
typeof pathArgs.limit === "number"
? pathArgs.limit
: DEFAULT_PATH_SEARCH_LIMIT,
};
return searchPaths(options);
}
case "search_symbols": {
const symbolArgs = args as Record<string, unknown>;
if (!symbolArgs.symbol || typeof symbolArgs.symbol !== "string") {
throw new McpError(
ErrorCode.InvalidParams,
"Symbol parameter is required and must be a string"
);
}
const options: SymbolSearchOptions = {
symbol: symbolArgs.symbol,
type: symbolArgs.type === "identifier" ? "identifier" : "symbol",
repo:
typeof symbolArgs.repo === "string"
? symbolArgs.repo
: "mozilla-central",
path_filter:
typeof symbolArgs.path_filter === "string"
? symbolArgs.path_filter
: undefined,
limit:
typeof symbolArgs.limit === "number"
? symbolArgs.limit
: DEFAULT_SEARCH_LIMIT,
language:
typeof symbolArgs.language === "string"
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
(symbolArgs.language as any)
: undefined,
};
return searchSymbols(options);
}
case "get_definition": {
const defArgs = args as Record<string, unknown>;
if (!defArgs.symbol || typeof defArgs.symbol !== "string") {
throw new McpError(
ErrorCode.InvalidParams,
"Symbol parameter is required and must be a string"
);
}
const options: DefinitionOptions = {
symbol: defArgs.symbol,
repo:
typeof defArgs.repo === "string"
? defArgs.repo
: "mozilla-central",
path_filter:
typeof defArgs.path_filter === "string"
? defArgs.path_filter
: undefined,
language:
typeof defArgs.language === "string"
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
(defArgs.language as any)
: "auto",
max_lines:
typeof defArgs.max_lines === "number" ? defArgs.max_lines : 500,
};
return getDefinition(options);
}
case "get_call_graph": {
const cgArgs = args as Record<string, unknown>;
if (!cgArgs.mode || typeof cgArgs.mode !== "string") {
throw new McpError(
ErrorCode.InvalidParams,
"Mode parameter is required and must be a string"
);
}
if (
!cgArgs.source_symbol ||
typeof cgArgs.source_symbol !== "string"
) {
throw new McpError(
ErrorCode.InvalidParams,
"source_symbol parameter is required and must be a string"
);
}
const mode = cgArgs.mode as
| "calls-from"
| "calls-to"
| "calls-between";
if (!["calls-from", "calls-to", "calls-between"].includes(mode)) {
throw new McpError(
ErrorCode.InvalidParams,
"Mode must be one of: calls-from, calls-to, calls-between"
);
}
const options: CallGraphOptions = {
mode,
source_symbol: cgArgs.source_symbol,
target_symbol:
typeof cgArgs.target_symbol === "string"
? cgArgs.target_symbol
: undefined,
repo:
typeof cgArgs.repo === "string" ? cgArgs.repo : "mozilla-central",
depth: typeof cgArgs.depth === "number" ? cgArgs.depth : 1,
};
return getCallGraph(options);
}
case "get_field_layout": {
const flArgs = args as Record<string, unknown>;
if (!flArgs.class_name || typeof flArgs.class_name !== "string") {
throw new McpError(
ErrorCode.InvalidParams,
"class_name parameter is required and must be a string"
);
}
const options: FieldLayoutOptions = {
class_name: flArgs.class_name,
repo:
typeof flArgs.repo === "string" ? flArgs.repo : "mozilla-central",
};
return getFieldLayout(options);
}
case "get_blame": {
const blameArgs = args as Record<string, unknown>;
if (!blameArgs.path || typeof blameArgs.path !== "string") {
throw new McpError(
ErrorCode.InvalidParams,
"path parameter is required and must be a string"
);
}
const options: BlameOptions = {
path: blameArgs.path,
repo:
typeof blameArgs.repo === "string"
? blameArgs.repo
: "mozilla-central",
start_line:
typeof blameArgs.start_line === "number"
? blameArgs.start_line
: 1,
end_line:
typeof blameArgs.end_line === "number"
? blameArgs.end_line
: undefined,
};
return getBlame(options);
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
});
}
async start() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("Searchfox MCP Server started");
}
}
// Start the server
const server = new SearchfoxServer();
server.start().catch(console.error);