UNPKG

ultimate-mcp-server

Version:

The definitive all-in-one Model Context Protocol server for AI-assisted coding across 30+ platforms

451 lines 17.8 kB
import { z } from 'zod'; import { Logger } from '../utils/logger.js'; import * as fs from 'fs/promises'; import * as path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); const logger = new Logger('UniversalSearchTools'); // Search providers class SearchProvider { } // File system search provider class FileSystemSearchProvider extends SearchProvider { name = 'filesystem'; async search(query, options) { const { searchPath = process.cwd(), includeHidden = false, maxResults = 100, fileTypes = [], excludePatterns = ['node_modules', '.git', 'dist', 'build'] } = options; const results = []; try { // Search for files matching the query using fs.readdir recursively const files = await this.findFiles(searchPath, includeHidden, excludePatterns); // Filter and score results for (const file of files) { const basename = path.basename(file); const relativePath = path.relative(searchPath, file); // Check if file matches query const score = this.calculateScore(basename, query); if (score > 0) { // Check file type filter if (fileTypes.length > 0) { const ext = path.extname(file).toLowerCase(); if (!fileTypes.includes(ext)) continue; } const stats = await fs.stat(file); results.push({ type: stats.isDirectory() ? 'directory' : 'file', path: file, name: basename, metadata: { relativePath, size: stats.size, modified: stats.mtime, extension: path.extname(file) }, score }); if (results.length >= maxResults) break; } } // Sort by score results.sort((a, b) => (b.score || 0) - (a.score || 0)); } catch (error) { logger.error('File system search error:', error); } return results; } calculateScore(filename, query) { const lowerFile = filename.toLowerCase(); const lowerQuery = query.toLowerCase(); // Exact match if (lowerFile === lowerQuery) return 100; // Starts with query if (lowerFile.startsWith(lowerQuery)) return 80; // Contains query if (lowerFile.includes(lowerQuery)) return 60; // Fuzzy match const fuzzyScore = this.fuzzyMatch(lowerFile, lowerQuery); if (fuzzyScore > 0.5) return fuzzyScore * 50; return 0; } fuzzyMatch(str, pattern) { let patternIdx = 0; let score = 0; let consecutive = 0; for (let i = 0; i < str.length && patternIdx < pattern.length; i++) { if (str[i] === pattern[patternIdx]) { score += 1 + consecutive; consecutive++; patternIdx++; } else { consecutive = 0; } } return patternIdx === pattern.length ? score / pattern.length : 0; } async findFiles(dir, includeHidden, excludePatterns) { const files = []; async function walk(currentDir) { try { const entries = await fs.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); const relativePath = path.relative(dir, fullPath); // Skip excluded patterns if (excludePatterns.some(pattern => relativePath.includes(pattern))) { continue; } // Skip hidden files if not included if (!includeHidden && entry.name.startsWith('.')) { continue; } files.push(fullPath); if (entry.isDirectory()) { await walk(fullPath); } } } catch (error) { // Skip directories we can't read } } await walk(dir); return files; } } // Content search provider (grep-like) class ContentSearchProvider extends SearchProvider { name = 'content'; async search(query, options) { const { searchPath = process.cwd(), fileTypes = ['.js', '.ts', '.py', '.java', '.cpp', '.c', '.h', '.md', '.txt'], caseSensitive = false, regex = false, maxResults = 100, contextLines = 2, excludePatterns = ['node_modules', '.git', 'dist', 'build'] } = options; const results = []; try { // Build grep command const grepFlags = [ caseSensitive ? '' : '-i', regex ? '-E' : '-F', '-n', // line numbers '-r', // recursive `--include=*{${fileTypes.join(',')}}`, ...excludePatterns.map((p) => `--exclude-dir=${p}`) ].filter(Boolean).join(' '); const command = `grep ${grepFlags} "${query.replace(/"/g, '\\"')}" "${searchPath}"`; try { const { stdout } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024 }); const lines = stdout.trim().split('\n').filter(line => line); for (const line of lines.slice(0, maxResults)) { const match = line.match(/^(.+?):(\d+):(.*)$/); if (match) { const [, filePath, lineNum, content] = match; results.push({ type: 'content', path: filePath, name: path.basename(filePath), match: content.trim(), lineNumber: parseInt(lineNum), context: await this.getContext(filePath, parseInt(lineNum), contextLines), metadata: { query, relativePath: path.relative(searchPath, filePath) } }); } } } catch (error) { // Grep returns non-zero exit code when no matches found if (error.code !== 1) { logger.error('Grep error:', error); } } } catch (error) { logger.error('Content search error:', error); } return results; } async getContext(filePath, lineNumber, contextLines) { try { const content = await fs.readFile(filePath, 'utf-8'); const lines = content.split('\n'); const startLine = Math.max(0, lineNumber - contextLines - 1); const endLine = Math.min(lines.length, lineNumber + contextLines); return lines.slice(startLine, endLine) .map((line, idx) => { const currentLine = startLine + idx + 1; const prefix = currentLine === lineNumber ? '>>> ' : ' '; return `${currentLine}: ${prefix}${line}`; }) .join('\n'); } catch (error) { return ''; } } } // Process search provider class ProcessSearchProvider extends SearchProvider { name = 'process'; async search(query, options) { const results = []; try { // Use ps command to search processes const { stdout } = await execAsync('ps aux', { maxBuffer: 10 * 1024 * 1024 }); const lines = stdout.trim().split('\n'); const header = lines[0]; const processes = lines.slice(1); for (const process of processes) { if (process.toLowerCase().includes(query.toLowerCase())) { const parts = process.split(/\s+/); const [user, pid, cpu, mem, vsz, rss, tty, stat, start, time, ...cmdParts] = parts; const command = cmdParts.join(' '); results.push({ type: 'process', name: command.split(' ')[0], match: command, metadata: { pid, user, cpu: `${cpu}%`, memory: `${mem}%`, status: stat, startTime: start, cpuTime: time } }); } } } catch (error) { logger.error('Process search error:', error); } return results; } } // Universal search manager class UniversalSearchManager { providers = new Map(); constructor() { this.registerProvider(new FileSystemSearchProvider()); this.registerProvider(new ContentSearchProvider()); this.registerProvider(new ProcessSearchProvider()); } registerProvider(provider) { this.providers.set(provider.name, provider); } async search(query, providers = ['filesystem', 'content'], options = {}) { const allResults = []; for (const providerName of providers) { const provider = this.providers.get(providerName); if (provider) { const results = await provider.search(query, options); allResults.push(...results); } } return allResults; } } const searchManager = new UniversalSearchManager(); // Tool definitions export const universalSearch = { name: 'universal_search', description: 'Search across files, content, and processes using multiple search providers', inputSchema: z.object({ query: z.string().describe('Search query'), providers: z.array(z.enum(['filesystem', 'content', 'process'])) .optional() .default(['filesystem', 'content']) .describe('Search providers to use'), searchPath: z.string().optional().describe('Base path for search'), options: z.object({ includeHidden: z.boolean().optional().default(false), maxResults: z.number().optional().default(50), fileTypes: z.array(z.string()).optional(), caseSensitive: z.boolean().optional().default(false), regex: z.boolean().optional().default(false), contextLines: z.number().optional().default(2), excludePatterns: z.array(z.string()).optional() }).optional() }).strict(), handler: async (args) => { const { query, providers, searchPath, options = {} } = args; const searchOptions = { searchPath: searchPath || process.cwd(), ...options }; const results = await searchManager.search(query, providers, searchOptions); // Group results by type const grouped = results.reduce((acc, result) => { if (!acc[result.type]) acc[result.type] = []; acc[result.type].push(result); return acc; }, {}); return { query, totalResults: results.length, results: grouped, summary: Object.entries(grouped).map(([type, items]) => ({ type, count: items.length })) }; } }; export const searchFiles = { name: 'search_files', description: 'Search for files and directories by name', inputSchema: z.object({ pattern: z.string().describe('Search pattern (supports wildcards)'), searchPath: z.string().optional().describe('Base path for search'), includeHidden: z.boolean().optional().default(false), fileTypes: z.array(z.string()).optional() .describe('File extensions to include (e.g., [".js", ".ts"])'), excludePatterns: z.array(z.string()).optional() .describe('Patterns to exclude (e.g., ["node_modules", ".git"])'), maxResults: z.number().optional().default(100) }).strict(), handler: async (args) => { const results = await searchManager.search(args.pattern, ['filesystem'], { searchPath: args.searchPath, includeHidden: args.includeHidden, fileTypes: args.fileTypes, excludePatterns: args.excludePatterns, maxResults: args.maxResults }); return { pattern: args.pattern, found: results.length, files: results.filter(r => r.type === 'file').map(r => ({ path: r.path, name: r.name, size: r.metadata?.size, modified: r.metadata?.modified, relativePath: r.metadata?.relativePath })), directories: results.filter(r => r.type === 'directory').map(r => ({ path: r.path, name: r.name, relativePath: r.metadata?.relativePath })) }; } }; export const searchContent = { name: 'search_content', description: 'Search for content within files (grep-like functionality)', inputSchema: z.object({ query: z.string().describe('Text to search for'), searchPath: z.string().optional().describe('Base path for search'), fileTypes: z.array(z.string()).optional() .describe('File extensions to search in'), caseSensitive: z.boolean().optional().default(false), regex: z.boolean().optional().default(false) .describe('Treat query as regular expression'), contextLines: z.number().optional().default(2) .describe('Number of context lines to show'), maxResults: z.number().optional().default(50) }).strict(), handler: async (args) => { const results = await searchManager.search(args.query, ['content'], { searchPath: args.searchPath, fileTypes: args.fileTypes, caseSensitive: args.caseSensitive, regex: args.regex, contextLines: args.contextLines, maxResults: args.maxResults }); return { query: args.query, totalMatches: results.length, matches: results.map(r => ({ file: r.path, line: r.lineNumber, match: r.match, context: r.context })) }; } }; export const searchProcesses = { name: 'search_processes', description: 'Search for running processes', inputSchema: z.object({ query: z.string().describe('Process name or command to search for') }).strict(), handler: async (args) => { const results = await searchManager.search(args.query, ['process'], {}); return { query: args.query, found: results.length, processes: results.map(r => ({ name: r.name, command: r.match, pid: r.metadata?.pid, user: r.metadata?.user, cpu: r.metadata?.cpu, memory: r.metadata?.memory, status: r.metadata?.status })) }; } }; export const searchEverything = { name: 'search_everything', description: 'Search across all available providers with a single query', inputSchema: z.object({ query: z.string().describe('Universal search query'), options: z.object({ includeFiles: z.boolean().optional().default(true), includeContent: z.boolean().optional().default(true), includeProcesses: z.boolean().optional().default(false), maxResultsPerType: z.number().optional().default(20) }).optional() }).strict(), handler: async (args) => { const { query, options = {} } = args; const providers = []; if (options.includeFiles) providers.push('filesystem'); if (options.includeContent) providers.push('content'); if (options.includeProcesses) providers.push('process'); const results = await searchManager.search(query, providers, { maxResults: options.maxResultsPerType }); return { query, totalResults: results.length, summary: { files: results.filter(r => r.type === 'file').length, directories: results.filter(r => r.type === 'directory').length, contentMatches: results.filter(r => r.type === 'content').length, processes: results.filter(r => r.type === 'process').length }, topResults: results.slice(0, 10).map(r => ({ type: r.type, name: r.name, path: r.path, match: r.match, score: r.score })) }; } }; // Export all universal search tools export const universalSearchTools = [ universalSearch, searchFiles, searchContent, searchProcesses, searchEverything ]; //# sourceMappingURL=universal-search-tools.js.map