Skip to main content
Glama

Grep MCP Server

by 247arjun
MIT License
12
3
  • Apple
index.ts•17.1 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { spawn } from "child_process"; import { existsSync, statSync } from "fs"; import { resolve } from "path"; // Create server instance const server = new McpServer({ name: "mcp-grep", version: "1.0.0", capabilities: { resources: {}, tools: {}, }, }); // Helper function to execute grep command safely async function executeGrep(args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> { return new Promise((resolve) => { // Ensure we're only calling grep with safe arguments if (!args.includes('grep')) { args.unshift('grep'); } const child = spawn('grep', args.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], shell: false, }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { resolve({ stdout, stderr, exitCode: code || 0, }); }); child.on('error', (error) => { resolve({ stdout: '', stderr: error.message, exitCode: 1, }); }); }); } // Helper function to validate file/directory paths function validatePath(path: string): { isValid: boolean; isDirectory: boolean; error?: string } { try { const resolvedPath = resolve(path); if (!existsSync(resolvedPath)) { return { isValid: false, isDirectory: false, error: `Path does not exist: ${path}` }; } const stats = statSync(resolvedPath); return { isValid: true, isDirectory: stats.isDirectory() }; } catch (error) { return { isValid: false, isDirectory: false, error: `Invalid path: ${path}` }; } } // Helper function to convert plain English search intent to regex patterns function interpretSearchIntent(intent: string): string { const lowerIntent = intent.toLowerCase().trim(); // Common search patterns const patterns: Record<string, string> = { // Email patterns 'email': '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', 'email address': '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', 'emails': '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}', // URL patterns 'url': 'https?://[^\\s]+', 'urls': 'https?://[^\\s]+', 'website': 'https?://[^\\s]+', 'link': 'https?://[^\\s]+', 'links': 'https?://[^\\s]+', // IP addresses 'ip address': '\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b', 'ip addresses': '\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b', 'ip': '\\b(?:[0-9]{1,3}\\.){3}[0-9]{1,3}\\b', // Phone numbers 'phone number': '\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b', 'phone numbers': '\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b', 'phone': '\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b', // Dates 'date': '\\b\\d{1,2}[/-]\\d{1,2}[/-]\\d{2,4}\\b', 'dates': '\\b\\d{1,2}[/-]\\d{1,2}[/-]\\d{2,4}\\b', // Numbers 'number': '\\b\\d+\\b', 'numbers': '\\b\\d+\\b', 'integer': '\\b\\d+\\b', 'integers': '\\b\\d+\\b', // Common code patterns 'function': '\\bfunction\\s+\\w+\\s*\\(', 'functions': '\\bfunction\\s+\\w+\\s*\\(', 'class': '\\bclass\\s+\\w+', 'classes': '\\bclass\\s+\\w+', 'import': '^\\s*import\\b', 'imports': '^\\s*import\\b', 'export': '^\\s*export\\b', 'exports': '^\\s*export\\b', // Common file types 'todo': '\\b(TODO|FIXME|HACK|XXX)\\b', 'todos': '\\b(TODO|FIXME|HACK|XXX)\\b', 'comment': '^\\s*(/\\*|//|#)', 'comments': '^\\s*(/\\*|//|#)', // Error patterns 'error': '\\b(error|Error|ERROR)\\b', 'errors': '\\b(error|Error|ERROR)\\b', 'warning': '\\b(warning|Warning|WARNING)\\b', 'warnings': '\\b(warning|Warning|WARNING)\\b', }; // Check for exact matches first if (patterns[lowerIntent]) { return patterns[lowerIntent]; } // Check for partial matches for (const [key, pattern] of Object.entries(patterns)) { if (lowerIntent.includes(key)) { return pattern; } } // If no pattern matches, treat as literal string search (escaped for regex) return intent.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Tool: Search with plain English intent server.tool( "grep_search_intent", "Search for patterns using plain English descriptions (e.g., 'email addresses', 'phone numbers', 'TODO comments')", { intent: z.string().describe("Plain English description of what to search for"), target: z.string().describe("File or directory path to search in"), case_sensitive: z.boolean().optional().default(false).describe("Whether the search should be case sensitive"), max_results: z.number().optional().describe("Maximum number of results to return"), show_context: z.boolean().optional().default(false).describe("Show surrounding lines for context"), context_lines: z.number().optional().default(2).describe("Number of context lines to show before/after matches"), }, async ({ intent, target, case_sensitive, max_results, show_context, context_lines }) => { // Validate target path const pathValidation = validatePath(target); if (!pathValidation.isValid) { return { content: [ { type: "text", text: `Error: ${pathValidation.error}`, }, ], }; } const args = ['grep']; // Convert intent to regex pattern const pattern = interpretSearchIntent(intent); args.push('-E'); // Use extended regex args.push(pattern); // Add case sensitivity option if (!case_sensitive) { args.push('-i'); } // Add recursive search if target is directory if (pathValidation.isDirectory) { args.push('-r'); } // Add line numbers args.push('-n'); // Add context if requested if (show_context && context_lines > 0) { args.push(`-C${context_lines}`); } // Add max results limit if (max_results) { args.push('-m', max_results.toString()); } // Add target path args.push(target); try { const result = await executeGrep(args); return { content: [ { type: "text", text: `Search Intent: "${intent}"\nPattern Used: ${pattern}\nExit Code: ${result.exitCode}\n\nResults:\n${result.stdout}${result.stderr ? `\n\nErrors:\n${result.stderr}` : ''}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error executing grep: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool: Direct regex pattern search server.tool( "grep_regex", "Search using a direct regex pattern", { pattern: z.string().describe("Regular expression pattern to search for"), target: z.string().describe("File or directory path to search in"), case_sensitive: z.boolean().optional().default(false).describe("Whether the search should be case sensitive"), whole_words: z.boolean().optional().default(false).describe("Match whole words only"), invert_match: z.boolean().optional().default(false).describe("Show lines that don't match the pattern"), max_results: z.number().optional().describe("Maximum number of results to return"), show_context: z.boolean().optional().default(false).describe("Show surrounding lines for context"), context_lines: z.number().optional().default(2).describe("Number of context lines to show before/after matches"), file_extensions: z.array(z.string()).optional().describe("Only search files with these extensions (e.g., ['js', 'ts'])"), }, async ({ pattern, target, case_sensitive, whole_words, invert_match, max_results, show_context, context_lines, file_extensions }) => { // Validate target path const pathValidation = validatePath(target); if (!pathValidation.isValid) { return { content: [ { type: "text", text: `Error: ${pathValidation.error}`, }, ], }; } const args = ['grep']; // Use extended regex args.push('-E'); args.push(pattern); // Add case sensitivity option if (!case_sensitive) { args.push('-i'); } // Add whole words option if (whole_words) { args.push('-w'); } // Add invert match option if (invert_match) { args.push('-v'); } // Add recursive search if target is directory if (pathValidation.isDirectory) { args.push('-r'); } // Add line numbers args.push('-n'); // Add context if requested if (show_context && context_lines > 0) { args.push(`-C${context_lines}`); } // Add max results limit if (max_results) { args.push('-m', max_results.toString()); } // Add file extension filters if (file_extensions && file_extensions.length > 0 && pathValidation.isDirectory) { file_extensions.forEach(ext => { args.push('--include', `*.${ext}`); }); } // Add target path args.push(target); try { const result = await executeGrep(args); return { content: [ { type: "text", text: `Pattern: ${pattern}\nExit Code: ${result.exitCode}\n\nResults:\n${result.stdout}${result.stderr ? `\n\nErrors:\n${result.stderr}` : ''}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error executing grep: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool: Count matches server.tool( "grep_count", "Count the number of matches for a pattern", { pattern: z.string().describe("Regular expression pattern or plain text to count"), target: z.string().describe("File or directory path to search in"), case_sensitive: z.boolean().optional().default(false).describe("Whether the search should be case sensitive"), whole_words: z.boolean().optional().default(false).describe("Match whole words only"), by_file: z.boolean().optional().default(false).describe("Show count per file when searching directories"), file_extensions: z.array(z.string()).optional().describe("Only search files with these extensions"), }, async ({ pattern, target, case_sensitive, whole_words, by_file, file_extensions }) => { // Validate target path const pathValidation = validatePath(target); if (!pathValidation.isValid) { return { content: [ { type: "text", text: `Error: ${pathValidation.error}`, }, ], }; } const args = ['grep']; // Add count option args.push('-c'); // Use extended regex args.push('-E'); args.push(pattern); // Add case sensitivity option if (!case_sensitive) { args.push('-i'); } // Add whole words option if (whole_words) { args.push('-w'); } // Add recursive search if target is directory if (pathValidation.isDirectory) { args.push('-r'); // Add filename display for directory searches if (by_file) { args.push('-H'); } } // Add file extension filters if (file_extensions && file_extensions.length > 0 && pathValidation.isDirectory) { file_extensions.forEach(ext => { args.push('--include', `*.${ext}`); }); } // Add target path args.push(target); try { const result = await executeGrep(args); return { content: [ { type: "text", text: `Pattern: ${pattern}\nExit Code: ${result.exitCode}\n\nMatch Counts:\n${result.stdout}${result.stderr ? `\n\nErrors:\n${result.stderr}` : ''}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error executing grep: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool: Find files containing pattern server.tool( "grep_files_with_matches", "List only the names of files that contain the pattern", { pattern: z.string().describe("Regular expression pattern or plain text to search for"), target: z.string().describe("Directory path to search in"), case_sensitive: z.boolean().optional().default(false).describe("Whether the search should be case sensitive"), whole_words: z.boolean().optional().default(false).describe("Match whole words only"), file_extensions: z.array(z.string()).optional().describe("Only search files with these extensions"), exclude_patterns: z.array(z.string()).optional().describe("Exclude files matching these patterns"), }, async ({ pattern, target, case_sensitive, whole_words, file_extensions, exclude_patterns }) => { // Validate target path const pathValidation = validatePath(target); if (!pathValidation.isValid) { return { content: [ { type: "text", text: `Error: ${pathValidation.error}`, }, ], }; } if (!pathValidation.isDirectory) { return { content: [ { type: "text", text: `Error: Target must be a directory for this operation`, }, ], }; } const args = ['grep']; // List filenames only args.push('-l'); // Use extended regex args.push('-E'); args.push(pattern); // Add case sensitivity option if (!case_sensitive) { args.push('-i'); } // Add whole words option if (whole_words) { args.push('-w'); } // Add recursive search args.push('-r'); // Add file extension filters if (file_extensions && file_extensions.length > 0) { file_extensions.forEach(ext => { args.push('--include', `*.${ext}`); }); } // Add exclusion patterns if (exclude_patterns && exclude_patterns.length > 0) { exclude_patterns.forEach(excludePattern => { args.push('--exclude', excludePattern); }); } // Add target path args.push(target); try { const result = await executeGrep(args); return { content: [ { type: "text", text: `Pattern: ${pattern}\nExit Code: ${result.exitCode}\n\nFiles containing matches:\n${result.stdout}${result.stderr ? `\n\nErrors:\n${result.stderr}` : ''}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error executing grep: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); // Tool: Advanced grep with custom options server.tool( "grep_advanced", "Execute grep with custom arguments (advanced usage)", { args: z.array(z.string()).describe("Array of grep arguments (excluding 'grep' itself)"), }, async ({ args }) => { // Basic validation to prevent potentially dangerous operations const dangerousFlags = [ '--devices=', // Device operations could be dangerous '--binary-files=', // Binary file operations '-f', '--file', // Reading patterns from files '-D', '--devices', // Device handling ]; const hasUnsafeFlag = args.some(arg => dangerousFlags.some(dangerous => arg.startsWith(dangerous)) ); if (hasUnsafeFlag) { return { content: [ { type: "text", text: `Error: This command contains potentially unsafe flags. Please use the specific grep tools for safety.`, }, ], }; } try { const result = await executeGrep(['grep', ...args]); return { content: [ { type: "text", text: `Exit Code: ${result.exitCode}\n\nResults:\n${result.stdout}${result.stderr ? `\n\nErrors:\n${result.stderr}` : ''}`, }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error executing grep: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } ); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); // Log to stderr since stdout is used for MCP communication console.error("MCP Grep Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/247arjun/mcp-grep'

If you have feedback or need assistance with the MCP directory API, please join our Discord server