/**
* File retrieval and path search tools
*/
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { FileOptions, PathSearchOptions } from "../types.js";
import { githubClient } from "../utils/github-client.js";
import { getSearchfoxFileUrl } from "../utils/repo-mapping.js";
import { searchfoxClient } from "../utils/searchfox-client.js";
import { DEFAULT_PATH_SEARCH_LIMIT } from "../constants.js";
/**
* Format content with line numbers
*/
function formatWithLineNumbers(content: string, startLine: number = 1): string {
const lines = content.split("\n");
const maxLineNumWidth = String(startLine + lines.length - 1).length;
return lines
.map((line, idx) => {
const lineNum = startLine + idx;
const paddedNum = String(lineNum).padStart(maxLineNumWidth, " ");
return `${paddedNum} | ${line}`;
})
.join("\n");
}
/**
* Get file content with optional line range support
*/
export async function getFile(options: FileOptions): Promise<CallToolResult> {
try {
const repo = options.repo || "mozilla-central";
const { path, lines } = options;
// Fetch file content with line range if specified
let content: string;
let actualStartLine = 1;
let actualEndLine: number | undefined;
if (lines) {
// Use GitHub client's line range support
content = await githubClient.getFileContentWithRange(
repo,
path,
lines.start,
lines.end,
lines.context
);
// Calculate actual line numbers for display
if (lines.start !== undefined || lines.end !== undefined) {
actualStartLine = Math.max(
1,
(lines.start ?? 1) - (lines.context ?? 0)
);
if (lines.end !== undefined) {
actualEndLine = lines.end + (lines.context ?? 0);
}
}
} else {
// Fetch full file
content = await githubClient.getFileContent(repo, path);
}
// Build response
const response: Record<string, unknown> = {
repo,
path,
source: "github",
searchfox_url: getSearchfoxFileUrl(repo, path, lines?.start ?? undefined),
};
// Add line range info if specified
if (lines?.start !== undefined || lines?.end !== undefined) {
response.line_range = {
requested_start: lines.start,
requested_end: lines.end,
actual_start: actualStartLine,
actual_end: actualEndLine,
context_lines: lines.context ?? 0,
};
// Format with line numbers for range requests
response.content = formatWithLineNumbers(content, actualStartLine);
} else {
response.content = content;
}
return {
content: [
{
type: "text",
text: JSON.stringify(response, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: `Failed to fetch file: ${error instanceof Error ? error.message : String(error)}`,
repo: options.repo || "mozilla-central",
path: options.path,
},
null,
2
),
},
],
isError: true,
};
}
}
/**
* Search for files by path pattern
*/
export async function searchPaths(
options: PathSearchOptions
): Promise<CallToolResult> {
try {
const repo = options.repo || "mozilla-central";
const limit = options.limit || DEFAULT_PATH_SEARCH_LIMIT;
// Use Searchfox search with path: prefix
// This searches for files matching the pattern
const query = `path:${options.pattern}`;
const data = await searchfoxClient.search(repo, query, {
case: false,
regexp: false,
});
// Collect unique file paths
const pathSet = new Set<string>();
// Process all sections to extract file paths
for (const [sectionKey, sectionValue] of Object.entries(data)) {
// Skip metadata fields
if (sectionKey.startsWith("*")) {
continue;
}
if (typeof sectionValue === "object" && sectionValue !== null) {
// Handle direct arrays
if (Array.isArray(sectionValue)) {
for (const file of sectionValue) {
if (file.path) {
pathSet.add(file.path);
if (pathSet.size >= limit) {
break;
}
}
}
} else {
// Handle categorized results
const categoryMap = sectionValue as Record<
string,
Array<{ path: string }>
>;
for (const categoryResults of Object.values(categoryMap)) {
if (Array.isArray(categoryResults)) {
for (const file of categoryResults) {
if (file.path) {
pathSet.add(file.path);
if (pathSet.size >= limit) {
break;
}
}
}
}
}
}
}
if (pathSet.size >= limit) {
break;
}
}
// Convert to sorted array
const paths = Array.from(pathSet).sort();
return {
content: [
{
type: "text",
text: JSON.stringify(
{
pattern: options.pattern,
repo,
count: paths.length,
limit,
timedout: data["*timedout*"],
paths,
searchfox_url: `https://searchfox.org/${repo}/search?q=path:${encodeURIComponent(options.pattern)}`,
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: `Path search failed: ${error instanceof Error ? error.message : String(error)}`,
pattern: options.pattern,
repo: options.repo || "mozilla-central",
},
null,
2
),
},
],
isError: true,
};
}
}