#!/usr/bin/env node
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
type Tool,
} from "@modelcontextprotocol/sdk/types.js";
interface SearchResult {
lineNumber: number;
lineContent: string;
matchCount: number;
}
interface SearchResponse {
filePath: string;
keyword: string;
totalMatches: number;
results: SearchResult[];
}
const searchKeywordInFile = async (
filePath: string,
keyword: string,
caseSensitive: boolean = false,
): Promise<SearchResponse> => {
try {
// Read the file
const content = await fs.readFile(filePath, "utf-8");
const lines = content.split("\n");
const results: SearchResult[] = [];
let totalMatches = 0;
// Search for keyword in each line
const searchKeyword = caseSensitive ? keyword : keyword.toLowerCase();
lines.forEach((line, index) => {
const searchLine = caseSensitive ? line : line.toLowerCase();
const matches = searchLine.split(searchKeyword).length - 1;
if (matches > 0) {
results.push({
lineNumber: index + 1,
lineContent: line,
matchCount: matches,
});
totalMatches += matches;
}
});
return {
filePath,
keyword,
totalMatches,
results,
};
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to search file: ${error.message}`);
}
throw error;
}
};
const server = new Server(
{
name: "keyword-search-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
},
);
// Define the search tool
const SEARCH_TOOL: Tool = {
name: "search_keyword",
description:
"Searches for a keyword in a specified file and returns all matching lines with line numbers",
inputSchema: {
type: "object",
properties: {
filePath: {
type: "string",
description: "Path to the file to search in",
},
keyword: {
type: "string",
description: "The keyword to search for",
},
caseSensitive: {
type: "boolean",
description:
"Whether the search should be case-sensitive (default: false)",
default: false,
},
},
required: ["filePath", "keyword"],
},
};
// Handle tool listing
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [SEARCH_TOOL],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "search_keyword") {
const { filePath, keyword, caseSensitive } = request.params.arguments as {
filePath: string;
keyword: string;
caseSensitive?: boolean;
};
try {
// Validate inputs
if (!filePath || typeof filePath !== "string") {
throw new Error("filePath must be a non-empty string");
}
if (!keyword || typeof keyword !== "string") {
throw new Error("keyword must be a non-empty string");
}
// Resolve the file path
const resolvedPath = path.resolve(filePath);
// Check if file exists
await fs.access(resolvedPath);
// Perform the search
const result = await searchKeywordInFile(
resolvedPath,
keyword,
caseSensitive ?? false,
);
// Format the response
let responseText = `Search Results for "${keyword}" in ${result.filePath}\n`;
responseText += `Total matches: ${result.totalMatches}\n`;
responseText += `Lines with matches: ${result.results.length}\n\n`;
if (result.results.length === 0) {
responseText += "No matches found.";
} else {
result.results.forEach((match) => {
responseText += `Line ${match.lineNumber} (${match.matchCount} match${match.matchCount > 1 ? "es" : ""}):\n`;
responseText += ` ${match.lineContent}\n\n`;
});
}
return {
content: [
{
type: "text",
text: responseText,
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error occurred";
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
// Start the server
const runServer = async () => {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Keyword Search MCP Server running on stdio");
};
runServer().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});