Skip to main content
Glama

MCP Smart Filesystem Server

by lofcz
index.ts17 kB
#!/usr/bin/env node /** * LLM-Optimized Smart Filesystem MCP Server * * Provides intelligent file access with: * - Automatic pagination for large files * - Ripgrep integration for fast searching * - Security sandboxing to allowed directories */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ListRootsRequestSchema, Tool } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { setAllowedDirectories, getAllowedDirectories, validatePath, getFileStats, readFileContent, isBinaryFile, detectLanguage, countLines, listDirectory } from './lib.js'; import { paginateFileContent, shouldPaginate, generateReadingSuggestions, LINES_PER_CHUNK, DEFAULT_MAX_RESULTS } from './pagination.js'; import { ripgrepSearch, ripgrepSearchInFile, ripgrepFindFiles } from './ripgrep.js'; import { getValidRootDirectories } from './roots-utils.js'; // Server instance const server = new Server( { name: 'mcp-server-filesystem-smart', version: '1.0.0', }, { capabilities: { tools: {}, roots: { listChanged: false } }, } ); // Tool definitions const tools: Tool[] = [ { name: 'list_directory', description: 'List contents of a directory with metadata including file sizes and line counts', inputSchema: { type: 'object', properties: { path: { type: 'string', description: "Directory path to list. Use '.' for workspace root" } }, required: ['path'] } }, { name: 'read_file', description: 'Read file contents. For large files (>500 lines), use start_line to read in chunks (e.g., 0, 500, 1000). Each call returns up to 500 lines. Binary files return metadata only.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'File path to read' }, start_line: { type: 'number', description: 'Line number to start reading from (0-indexed). For large files, read in chunks: start_line=0 (first 500), start_line=500 (next 500), etc.', default: 0 } }, required: ['path'] } }, { name: 'search_code', description: `Search for code patterns using ripgrep (very fast). Supports regex patterns and advanced filtering. PATTERN EXAMPLES: - Exact text: "functionName" - Multiple options: "\\\\b(class|struct|record|interface|enum)\\\\s+TypeName\\\\b" (finds: class TypeName, record TypeName, interface TypeName, etc.) - Regex: "async.*Promise<.*>" (finds async functions returning Promise) - Any declaration: "\\\\b(public|private|protected)\\\\s+\\\\w+\\\\s+methodName" COMMON USE CASES: - Find type declaration: "\\\\b(class|struct|interface|record|enum)\\\\s+TypeName\\\\b" - Find method: "\\\\b(public|private|protected|internal).*\\\\s+methodName\\\\s*\\\\(" - Find property: "\\\\bpublic\\\\s+\\\\w+\\\\s+propertyName\\\\s*\\\\{" - Find async methods: "async.*Task<" - Find implementations: ":\\\\s*IInterfaceName\\\\b" TIPS: - Use \\\\b for word boundaries - Use \\\\s+ for whitespace - Combine alternatives with (opt1|opt2|opt3) - Escape special chars: \\\\( \\\\) \\\\{ \\\\}`, inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Regex pattern to search. For multiple alternatives use: (class|struct|interface) to match any' }, path: { type: 'string', description: "Limit search to specific directory (e.g., 'src/components'). Omit to search entire workspace." }, filePattern: { type: 'string', description: "File glob pattern (ripgrep -g flag). Examples: '*.js', '*.{ts,tsx}', '!*test*' (exclude). Can specify multiple patterns separated by comma." }, caseInsensitive: { type: 'boolean', description: 'Ignore case in search (ripgrep -i). Default: true for LLM-friendly searching', default: true }, contextLines: { type: 'number', description: 'Lines of context before/after match (ripgrep -C)', default: 2 }, maxResults: { type: 'number', description: `Maximum number of results to return (per page). Default: ${DEFAULT_MAX_RESULTS}. Configure via MCP_MAX_SEARCH_RESULTS env var.`, default: DEFAULT_MAX_RESULTS }, page: { type: 'number', description: 'Page number for paginated results (1-based). Use to get more results beyond maxResults.', default: 1 }, literalString: { type: 'boolean', description: 'Treat pattern as literal string, not regex (ripgrep -F)', default: false }, wordBoundary: { type: 'boolean', description: 'Match whole words only (ripgrep -w)', default: false } }, required: ['pattern'] } }, { name: 'search_in_file', description: 'Search for patterns within a specific file using ripgrep. Like Ctrl+F but with regex support. Useful for finding specific sections in a known file.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'File path to search within' }, pattern: { type: 'string', description: 'Regex pattern to search for' }, caseInsensitive: { type: 'boolean', description: 'Ignore case in search. Default: true', default: true }, contextLines: { type: 'number', description: 'Lines of context before/after match', default: 3 }, literalString: { type: 'boolean', description: 'Treat pattern as literal string, not regex', default: false }, wordBoundary: { type: 'boolean', description: 'Match whole words only', default: false } }, required: ['path', 'pattern'] } }, { name: 'find_files', description: `Find files by name using fast pattern matching. PATTERN EXAMPLES: - Exact name: "config.json" - Wildcard: "*.config" or "*Handler*" - Multiple extensions: "*.{ts,tsx,js}" TIPS: - Use * for any characters - Use ? for single character - Use {a,b,c} for alternatives`, inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: "Filename pattern. Examples: 'Component.tsx', '*.json', '*Handler*', '*.{ts,tsx}'" }, path: { type: 'string', description: 'Limit search to specific directory' } }, required: ['pattern'] } }, { name: 'get_file_info', description: 'Get file metadata without reading contents. Useful to check size/line count before reading. For large files, provides reading strategy recommendations.', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'File path to get info about' } }, required: ['path'] } }, { name: 'list_allowed_directories', description: 'Show which directories this server can access (security boundaries). No parameters required.', inputSchema: { type: 'object', properties: {} } } ]; // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'list_directory': { const schema = z.object({ path: z.string() }); const { path } = schema.parse(args); const validatedPath = await validatePath(path); const result = await listDirectory(validatedPath); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } case 'read_file': { const schema = z.object({ path: z.string(), start_line: z.number().optional().default(0) }); const { path, start_line } = schema.parse(args); const validatedPath = await validatePath(path); // Check if binary const isBinary = await isBinaryFile(validatedPath); if (isBinary) { const stats = await getFileStats(validatedPath); return { content: [{ type: 'text', text: JSON.stringify({ path: validatedPath, error: 'Binary file', message: 'This appears to be a binary file. Use get_file_info for metadata.', size: stats.size, type: 'binary' }, null, 2) }] }; } // Read file content const content = await readFileContent(validatedPath); const totalLines = countLines(content); // Check if pagination is needed if (shouldPaginate(totalLines) || start_line > 0) { const result = paginateFileContent(validatedPath, content, start_line, LINES_PER_CHUNK); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } // Return full file return { content: [{ type: 'text', text: JSON.stringify({ path: validatedPath, content, startLine: 0, endLine: totalLines - 1, totalLines, hasMore: false }, null, 2) }] }; } case 'search_code': { const schema = z.object({ pattern: z.string(), path: z.string().optional(), filePattern: z.string().optional(), caseInsensitive: z.boolean().optional().default(true), contextLines: z.number().optional().default(2), maxResults: z.number().optional().default(DEFAULT_MAX_RESULTS), page: z.number().optional().default(1), literalString: z.boolean().optional().default(false), wordBoundary: z.boolean().optional().default(false) }); const options = schema.parse(args); const result = await ripgrepSearch(options, getAllowedDirectories()); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } case 'search_in_file': { const schema = z.object({ path: z.string(), pattern: z.string(), caseInsensitive: z.boolean().optional().default(true), contextLines: z.number().optional().default(3), literalString: z.boolean().optional().default(false), wordBoundary: z.boolean().optional().default(false) }); const { path, pattern, caseInsensitive, contextLines, literalString, wordBoundary } = schema.parse(args); const validatedPath = await validatePath(path); const result = await ripgrepSearchInFile( validatedPath, pattern, { caseInsensitive, contextLines, literalString, wordBoundary } ); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }; } case 'find_files': { const schema = z.object({ pattern: z.string(), path: z.string().optional() }); const { pattern, path } = schema.parse(args); const files = await ripgrepFindFiles(pattern, path, getAllowedDirectories()); // Get metadata for each file const filesWithMetadata = await Promise.all( files.map(async (file) => { try { const stats = await getFileStats(file); let lines: number | undefined; if (stats.isFile && stats.size < 10 * 1024 * 1024) { const isBinary = await isBinaryFile(file); if (!isBinary) { try { const content = await readFileContent(file); lines = countLines(content); } catch { // Ignore } } } return { path: file, size: `${stats.size} bytes`, lines, matchType: 'match', hint: `Matches pattern '${pattern}'` }; } catch { return { path: file, matchType: 'match' }; } }) ); return { content: [{ type: 'text', text: JSON.stringify({ query: { pattern, searchedIn: path || getAllowedDirectories().join(', ') }, files: filesWithMetadata, summary: { totalFiles: filesWithMetadata.length, searchTimeMs: 0 } }, null, 2) }] }; } case 'get_file_info': { const schema = z.object({ path: z.string() }); const { path } = schema.parse(args); const validatedPath = await validatePath(path); const stats = await getFileStats(validatedPath); const isBinary = await isBinaryFile(validatedPath); const language = detectLanguage(validatedPath); let lines: number | undefined; let readingStrategy: any = undefined; if (!isBinary && stats.isFile) { try { const content = await readFileContent(validatedPath); lines = countLines(content); if (shouldPaginate(lines)) { readingStrategy = { recommendation: `File is large (${lines} lines). Suggested approach:`, options: generateReadingSuggestions(lines) }; } } catch { // Ignore read errors } } return { content: [{ type: 'text', text: JSON.stringify({ path: validatedPath, size: { bytes: stats.size, readable: `${stats.size} bytes` }, lines, type: stats.isDirectory ? 'directory' : 'file', language, isBinary, lastModified: stats.modified.toISOString(), permissions: stats.permissions, readingStrategy }, null, 2) }] }; } case 'list_allowed_directories': { return { content: [{ type: 'text', text: JSON.stringify({ allowedDirectories: getAllowedDirectories(), count: getAllowedDirectories().length, note: 'This server can only access files within these directories for security' }, null, 2) }] }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: 'text', text: JSON.stringify({ error: errorMessage, tool: name, arguments: args }, null, 2) }], isError: true }; } }); // Roots handler server.setRequestHandler(ListRootsRequestSchema, async () => ({ roots: getAllowedDirectories().map(dir => ({ uri: `file://${dir}`, name: dir })) })); // Main startup async function main() { // Parse command line arguments for allowed directories const args = process.argv.slice(2); if (args.length > 0) { // Use provided directories setAllowedDirectories(args); console.error(`MCP Smart Filesystem Server starting with allowed directories: ${args.join(', ')}`); } else { // Default to current working directory setAllowedDirectories([process.cwd()]); console.error(`MCP Smart Filesystem Server starting with default directory: ${process.cwd()}`); } const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP Smart Filesystem Server ready'); } main().catch((error) => { console.error('Fatal error:', 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/lofcz/mcp-filesystem-smart'

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