#!/usr/bin/env tsx
/**
* Test client for searchfox-mcp server
* Allows programmatic testing of all MCP tools without manual app testing
*/
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { spawn } from "child_process";
interface ToolTest {
name: string;
tool: string;
args: Record<string, unknown>;
description: string;
}
// Test cases for all 8 tools
const TOOL_TESTS: ToolTest[] = [
// 1. Search tool
{
name: "Search - Basic text search",
tool: "search",
args: {
query: "nsIFrame",
repo: "mozilla-central",
limit: 5,
},
description: "Search for 'nsIFrame' in mozilla-central",
},
{
name: "Search - Symbol search",
tool: "search",
args: {
query: "symbol:nsIFrame",
repo: "mozilla-central",
limit: 5,
},
description: "Search for nsIFrame symbol definition",
},
{
name: "Search - With path filter",
tool: "search",
args: {
query: "nsIFrame path:layout",
repo: "mozilla-central",
limit: 5,
},
description: "Search for nsIFrame in layout directory",
},
// 2. File retrieval
{
name: "Get File - Full file",
tool: "get_file",
args: {
repo: "mozilla-central",
path: "dom/base/nsGlobalWindowOuter.h",
},
description: "Retrieve full file content",
},
{
name: "Get File - With line range",
tool: "get_file",
args: {
repo: "mozilla-central",
path: "dom/base/nsGlobalWindowOuter.h",
lines: {
start: 1,
end: 50,
},
},
description: "Retrieve first 50 lines of file",
},
// 3. Path search
{
name: "Search Paths - Pattern match",
tool: "search_paths",
args: {
pattern: "nsGlobal*.h",
repo: "mozilla-central",
limit: 10,
},
description: "Find files matching pattern 'nsGlobal*.h'",
},
// 4. Symbol search
{
name: "Symbol Search - Class definition",
tool: "search_symbols",
args: {
symbol: "nsIFrame",
type: "symbol",
repo: "mozilla-central",
limit: 10,
},
description: "Search for nsIFrame symbol occurrences",
},
{
name: "Symbol Search - With language filter",
tool: "search_symbols",
args: {
symbol: "T_ActivePS",
type: "symbol",
repo: "mozilla-central",
language: "cpp",
limit: 10,
},
description: "Search for MessagePort in C++ files only",
},
// 5. Get definition
{
name: "Get Definition - Class",
tool: "get_definition",
args: {
symbol: "nsIFrame",
repo: "mozilla-central",
},
description: "Extract complete nsIFrame class definition",
},
{
name: "Get Definition - Method",
tool: "get_definition",
args: {
symbol: "nsIFrame::GetContent",
repo: "mozilla-central",
},
description: "Extract nsIFrame::GetContent method definition",
},
// 6. Call graph
{
name: "Call Graph - Calls from",
tool: "get_call_graph",
args: {
mode: "calls-from",
source_symbol: "nsIFrame::GetContent",
repo: "mozilla-central",
depth: 1,
},
description: "Find what nsIFrame::GetContent calls",
},
{
name: "Call Graph - Calls to",
tool: "get_call_graph",
args: {
mode: "calls-to",
source_symbol: "nsIFrame::GetContent",
repo: "mozilla-central",
depth: 1,
},
description: "Find what calls nsIFrame::GetContent",
},
// 7. Field layout
{
name: "Field Layout - nsIFrame",
tool: "get_field_layout",
args: {
class_name: "nsIFrame",
repo: "mozilla-central",
},
description: "Get memory layout of nsIFrame class",
},
{
name: "Field Layout - nsPresContext",
tool: "get_field_layout",
args: {
class_name: "nsPresContext",
repo: "mozilla-central",
},
description: "Get memory layout of nsPresContext class",
},
// 8. Blame
{
name: "Blame - File range",
tool: "get_blame",
args: {
path: "dom/base/nsGlobalWindowOuter.cpp",
repo: "mozilla-central",
start_line: 1,
end_line: 20,
},
description: "Get blame info for first 20 lines",
},
];
class TestRunner {
private client: Client;
private transport: StdioClientTransport | null = null;
private results: {
test: ToolTest;
success: boolean;
error?: string;
duration: number;
}[] = [];
constructor() {
this.client = new Client(
{
name: "searchfox-test-client",
version: "1.0.0",
},
{
capabilities: {},
}
);
}
async connect(): Promise<void> {
console.log("π Connecting to searchfox-mcp server...\n");
// Spawn the server process
spawn("tsx", ["src/index.ts"], {
stdio: ["pipe", "pipe", "inherit"],
cwd: process.cwd(),
});
this.transport = new StdioClientTransport({
command: "tsx",
args: ["src/index.ts"],
});
await this.client.connect(this.transport);
console.log("β
Connected to server\n");
}
async disconnect(): Promise<void> {
if (this.transport) {
await this.client.close();
console.log("\nπ Disconnected from server");
}
}
async runTest(test: ToolTest): Promise<void> {
console.log(`\n${"=".repeat(80)}`);
console.log(`π§ͺ Test: ${test.name}`);
console.log(`π Description: ${test.description}`);
console.log(`π§ Tool: ${test.tool}`);
console.log(`π¦ Args: ${JSON.stringify(test.args, null, 2)}`);
console.log(`${"=".repeat(80)}\n`);
const startTime = Date.now();
let success = false;
let error: string | undefined;
try {
const result = await this.client.callTool({
name: test.tool,
arguments: test.args,
});
const duration = Date.now() - startTime;
if (result.isError) {
console.error("β Test FAILED with error:");
console.error(JSON.stringify(result.content, null, 2));
error = JSON.stringify(result.content);
} else {
console.log("β
Test PASSED");
console.log(`β±οΈ Duration: ${duration}ms\n`);
console.log("π€ Result preview:");
// Parse and show preview of result
const content = result.content[0];
if (content.type === "text") {
const parsed = JSON.parse(content.text);
// Show different preview based on tool
if (test.tool === "search" || test.tool === "search_symbols") {
console.log(` Found ${parsed.count} results`);
if (parsed.results && parsed.results.length > 0) {
console.log(
` First result: ${parsed.results[0].path}:${parsed.results[0].line}`
);
}
} else if (test.tool === "get_definition") {
console.log(` Symbol: ${parsed.symbol}`);
console.log(` Path: ${parsed.path}`);
console.log(` Lines: ${parsed.start_line}-${parsed.end_line}`);
if (parsed.signature) {
console.log(
` Signature: ${parsed.signature.split("\n")[0]}...`
);
}
} else if (test.tool === "get_call_graph") {
console.log(` Mode: ${parsed.mode}`);
if (parsed.markdown_output) {
const lines = parsed.markdown_output.split("\n").slice(0, 5);
console.log(` Preview:\n${lines.join("\n")}`);
}
} else if (test.tool === "get_field_layout") {
console.log(` Class: ${parsed.class_name}`);
console.log(` Size: ${parsed.size_bytes} bytes`);
console.log(` Fields: ${parsed.fields_count}`);
} else if (test.tool === "get_blame") {
console.log(` Path: ${parsed.path}`);
console.log(` Lines analyzed: ${parsed.lines_count}`);
} else if (test.tool === "get_file") {
console.log(` Path: ${parsed.path}`);
console.log(
` Lines: ${parsed.content ? parsed.content.split("\n").length : 0}`
);
} else if (test.tool === "search_paths") {
console.log(` Found ${parsed.count} paths`);
}
}
success = true;
}
this.results.push({ test, success, error, duration });
} catch (err) {
const duration = Date.now() - startTime;
console.error("β Test FAILED with exception:");
console.error(err);
error = err instanceof Error ? err.message : String(err);
this.results.push({ test, success: false, error, duration });
}
}
async runAllTests(filter?: string): Promise<void> {
const testsToRun = filter
? TOOL_TESTS.filter(
(t) =>
t.name.toLowerCase().includes(filter.toLowerCase()) ||
t.tool === filter
)
: TOOL_TESTS;
console.log(`π Running ${testsToRun.length} test(s)...\n`);
for (const test of testsToRun) {
await this.runTest(test);
}
this.printSummary();
}
printSummary(): void {
console.log(`\n${"=".repeat(80)}`);
console.log("π TEST SUMMARY");
console.log(`${"=".repeat(80)}\n`);
const passed = this.results.filter((r) => r.success).length;
const failed = this.results.filter((r) => !r.success).length;
const totalDuration = this.results.reduce((sum, r) => sum + r.duration, 0);
console.log(`Total tests: ${this.results.length}`);
console.log(`β
Passed: ${passed}`);
console.log(`β Failed: ${failed}`);
console.log(`β±οΈ Total duration: ${totalDuration}ms\n`);
if (failed > 0) {
console.log("Failed tests:");
this.results
.filter((r) => !r.success)
.forEach((r) => {
console.log(` β ${r.test.name}`);
if (r.error) {
console.log(` Error: ${r.error.substring(0, 100)}...`);
}
});
console.log();
}
// Group by tool
const byTool = new Map<string, { passed: number; failed: number }>();
for (const result of this.results) {
const stats = byTool.get(result.test.tool) || { passed: 0, failed: 0 };
if (result.success) {
stats.passed++;
} else {
stats.failed++;
}
byTool.set(result.test.tool, stats);
}
console.log("Results by tool:");
for (const [tool, stats] of byTool.entries()) {
const total = stats.passed + stats.failed;
const icon = stats.failed === 0 ? "β
" : "β οΈ";
console.log(` ${icon} ${tool}: ${stats.passed}/${total} passed`);
}
process.exit(failed > 0 ? 1 : 0);
}
}
// Main execution
async function main() {
const args = process.argv.slice(2);
const filter = args[0]; // Optional filter for test name or tool
const runner = new TestRunner();
try {
await runner.connect();
await runner.runAllTests(filter);
} catch (error) {
console.error("π₯ Fatal error:", error);
process.exit(1);
} finally {
await runner.disconnect();
}
}
main();