Skip to main content
Glama

MCP Filesystem Server

by A-Niranjan
index.ts28.8 kB
#!/usr/bin/env node /** * MCP-Filesystem Server * * A secure filesystem server implementing the Model Context Protocol (MCP). * Provides controlled access to the filesystem with strict path validation * and comprehensive security measures. * * Features: * - Secure path validation * - Structured logging * - Performance metrics * - Configuration management * - Comprehensive error handling */ import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema, } from '@modelcontextprotocol/sdk/types.js' import { z } from 'zod' import { zodToJsonSchema } from 'zod-to-json-schema' import fs from 'node:fs/promises' import path from 'node:path' import { minimatch } from 'minimatch' // Import internal modules import { logger } from './logger/index.js' import { Config, createSampleConfig, loadConfig } from './config/index.js' import { validatePath } from './utils/path.js' import { editFile, EditFileArgsSchema, readFile, ReadFileArgsSchema, readMultipleFiles, ReadMultipleFilesArgsSchema, writeFile, WriteFileArgsSchema, } from './utils/tools.js' import { FileSystemError } from './errors/index.js' import { executeCommand, ExecuteCommandArgsSchema } from './utils/exec/index.js' // Import bash tools import { BashExecuteArgsSchema, BashPipeArgsSchema } from './utils/bash/bash_tools.js' import { handleBashExecute, handleBashPipe } from './bash/tools/index.js' // Import curl tool import { CurlRequestArgsSchema, handleCurlRequest } from './utils/curl/index.js' import { metrics } from './metrics/index.js' // Command-line argument processing const args = process.argv.slice(2) let configPath: string | undefined // Check for special commands if (args.includes('--help') || args.includes('-h')) { console.log(` MCP-Filesystem Server Usage: mcp-server-filesystem [options] <allowed-directory> [additional-directories...] Options: --help, -h Show this help message --version, -v Show version information --config=<path> Use configuration file at <path> --create-config=<path> Create a sample configuration file at <path> Examples: mcp-server-filesystem /path/to/directory # Allow access to one directory mcp-server-filesystem --config=/path/to/config.json # Use a config file mcp-server-filesystem --create-config=config.json # Create a sample config `) process.exit(0) } if (args.includes('--version') || args.includes('-v')) { console.log('MCP-Filesystem Server v0.3.0') process.exit(0) } // Check for config creation request const createConfigArg = args.find((arg) => arg.startsWith('--create-config=')) if (createConfigArg) { const configOutputPath = createConfigArg.split('=')[1] if (!configOutputPath) { console.error('Error: Missing path for --create-config') process.exit(1) } createSampleConfig(configOutputPath) .then(() => { console.log(`Sample configuration created at: ${configOutputPath}`) process.exit(0) }) .catch((error: unknown) => { console.error(`Error creating sample configuration: ${error}`) process.exit(1) }) } else { // Check for config file path const configArg = args.find((arg) => arg.startsWith('--config=')) if (configArg) { configPath = configArg.split('=')[1] if (!configPath) { console.error('Error: Missing path for --config') process.exit(1) } } } // Define tool schemas not imported from tools module const CreateDirectoryArgsSchema = z.object({ path: z.string().describe('Path of the directory to create'), }) const ListDirectoryArgsSchema = z.object({ path: z.string().describe('Path of the directory to list'), }) const DirectoryTreeArgsSchema = z.object({ path: z.string().describe('Path of the directory to create a tree view for'), }) const MoveFileArgsSchema = z.object({ source: z.string().describe('Source path of the file or directory to move'), destination: z.string().describe('Destination path where to move the file or directory'), }) const SearchFilesArgsSchema = z.object({ path: z.string().describe('Root path to start searching from'), pattern: z.string().describe('Pattern to match against filenames and directories'), excludePatterns: z .array(z.string()) .optional() .default([]) .describe('Patterns to exclude from search results'), }) const GetFileInfoArgsSchema = z.object({ path: z.string().describe('Path to the file or directory to get information about'), }) // Types used in tool implementations const ToolInputSchema = ToolSchema.shape.inputSchema type ToolInput = z.infer<typeof ToolInputSchema> interface FileInfo { size: number created: Date modified: Date accessed: Date isDirectory: boolean isFile: boolean permissions: string } interface TreeEntry { name: string type: 'file' | 'directory' children?: TreeEntry[] } /** * Gets detailed file stats */ async function getFileStats(filePath: string): Promise<FileInfo> { const stats = await fs.stat(filePath) return { size: stats.size, created: stats.birthtime, modified: stats.mtime, accessed: stats.atime, isDirectory: stats.isDirectory(), isFile: stats.isFile(), permissions: stats.mode.toString(8).slice(-3), } } /** * Main server initialization and run function */ async function runServer(config: Config) { // Set up logger with configuration logger.setLogLevel(config.logLevel) if (config.logFile) { logger.setLogFile(config.logFile) } await logger.info('Starting MCP-Filesystem server', { version: config.serverVersion, allowedDirectories: config.allowedDirectories, }) // Validate that all specified directories exist await Promise.all( config.allowedDirectories.map(async (dir: string) => { try { const stats = await fs.stat(dir) if (!stats.isDirectory()) { await logger.error(`Error: ${dir} is not a directory`) process.exit(1) } } catch (error) { await logger.error(`Error accessing directory ${dir}:`, { error }) process.exit(1) } }) ) // Initialize the MCP server const server = new Server( { name: config.serverName, version: config.serverVersion, }, { capabilities: { tools: {}, }, } ) // Register tool list handler server.setRequestHandler(ListToolsRequestSchema, async () => { const endMetric = metrics.startOperation('list_tools') try { await logger.debug('Handling ListTools request') const result = { tools: [ { name: 'read_file', description: 'Read the complete contents of a file from the file system. ' + 'Handles various text encodings and provides detailed error messages ' + 'if the file cannot be read. Use this tool when you need to examine ' + 'the contents of a single file. Only works within allowed directories.', inputSchema: zodToJsonSchema(ReadFileArgsSchema) as ToolInput, }, { name: 'read_multiple_files', description: 'Read the contents of multiple files simultaneously. This is more ' + 'efficient than reading files one by one when you need to analyze ' + "or compare multiple files. Each file's content is returned with its " + "path as a reference. Failed reads for individual files won't stop " + 'the entire operation. Only works within allowed directories.', inputSchema: zodToJsonSchema(ReadMultipleFilesArgsSchema) as ToolInput, }, { name: 'write_file', description: 'Create a new file or completely overwrite an existing file with new content. ' + 'Use with caution as it will overwrite existing files without warning. ' + 'Handles text content with proper encoding. Only works within allowed directories.', inputSchema: zodToJsonSchema(WriteFileArgsSchema) as ToolInput, }, { name: 'edit_file', description: 'Make line-based edits to a text file. Each edit replaces exact line sequences ' + 'with new content. Returns a git-style diff showing the changes made. ' + 'Only works within allowed directories.', inputSchema: zodToJsonSchema(EditFileArgsSchema) as ToolInput, }, { name: 'create_directory', description: 'Create a new directory or ensure a directory exists. Can create multiple ' + 'nested directories in one operation. If the directory already exists, ' + 'this operation will succeed silently. Perfect for setting up directory ' + 'structures for projects or ensuring required paths exist. Only works within allowed directories.', inputSchema: zodToJsonSchema(CreateDirectoryArgsSchema) as ToolInput, }, { name: 'list_directory', description: 'Get a detailed listing of all files and directories in a specified path. ' + 'Results clearly distinguish between files and directories with [FILE] and [DIR] ' + 'prefixes. This tool is essential for understanding directory structure and ' + 'finding specific files within a directory. Only works within allowed directories.', inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput, }, { name: 'directory_tree', description: 'Get a recursive tree view of files and directories as a JSON structure. ' + "Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " + 'Files have no children array, while directories always have a children array (which may be empty). ' + 'The output is formatted with 2-space indentation for readability. Only works within allowed directories.', inputSchema: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput, }, { name: 'move_file', description: 'Move or rename files and directories. Can move files between directories ' + 'and rename them in a single operation. If the destination exists, the ' + 'operation will fail. Works across different directories and can be used ' + 'for simple renaming within the same directory. Both source and destination must be within allowed directories.', inputSchema: zodToJsonSchema(MoveFileArgsSchema) as ToolInput, }, { name: 'search_files', description: 'Recursively search for files and directories matching a pattern. ' + 'Searches through all subdirectories from the starting path. The search ' + 'is case-insensitive and matches partial names. Returns full paths to all ' + "matching items. Great for finding files when you don't know their exact location. " + 'Only searches within allowed directories.', inputSchema: zodToJsonSchema(SearchFilesArgsSchema) as ToolInput, }, { name: 'get_file_info', description: 'Retrieve detailed metadata about a file or directory. Returns comprehensive ' + 'information including size, creation time, last modified time, permissions, ' + 'and type. This tool is perfect for understanding file characteristics ' + 'without reading the actual content. Only works within allowed directories.', inputSchema: zodToJsonSchema(GetFileInfoArgsSchema) as ToolInput, }, { name: 'list_allowed_directories', description: 'Returns the list of directories that this server is allowed to access. ' + 'Use this to understand which directories are available before trying to access files.', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'get_metrics', description: 'Returns performance metrics about server operations. ' + 'Useful for monitoring and debugging.', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'execute_command', description: 'Execute a system command with security restrictions. ' + 'Validates commands for safety and provides detailed output. ' + 'Limited to basic system operations with security checks.', inputSchema: zodToJsonSchema(ExecuteCommandArgsSchema) as ToolInput, }, { name: 'bash_execute', description: 'Execute a Bash command directly with output capture. ' + 'More flexible than execute_command but still with security restrictions. ' + 'Allows for direct access to Bash functionality.', inputSchema: zodToJsonSchema(BashExecuteArgsSchema) as ToolInput, }, { name: 'bash_pipe', description: 'Execute a sequence of Bash commands piped together. ' + 'Allows for powerful command combinations with pipes. ' + 'Results include both stdout and stderr.', inputSchema: zodToJsonSchema(BashPipeArgsSchema) as ToolInput, }, { name: 'curl_request', description: 'Execute a curl request to an external HTTP API. ' + 'Allows specifying URL, method, headers, and data. ' + 'Useful for integrating with external services via HTTP.', inputSchema: zodToJsonSchema(CurlRequestArgsSchema) as ToolInput, }, ], } endMetric() return result } catch (error) { metrics.recordError('list_tools') await logger.error('Error in ListTools handler', { error }) throw error } }) // Register tool call handler server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: a } = request.params const endMetric = metrics.startOperation(name) await logger.debug(`Handling tool call: ${name}`, { args: a }) try { switch (name) { case 'read_file': { const parsed = ReadFileArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const content = await readFile(parsed.data, config) endMetric() return { content: [{ type: 'text', text: content }], } } case 'read_multiple_files': { const parsed = ReadMultipleFilesArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const results = await readMultipleFiles(parsed.data, config) const formattedResults = Object.entries(results) .map(([filePath, content]) => { if (content instanceof Error) { return `${filePath}: Error - ${content.message}` } return `${filePath}:\n${content}\n` }) .join('\n---\n') endMetric() return { content: [{ type: 'text', text: formattedResults }], } } case 'write_file': { const parsed = WriteFileArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const result = await writeFile(parsed.data, config) endMetric() return { content: [{ type: 'text', text: result }], } } case 'edit_file': { const parsed = EditFileArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const result = await editFile(parsed.data, config) endMetric() return { content: [{ type: 'text', text: result }], } } case 'create_directory': { const parsed = CreateDirectoryArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const validPath = await validatePath(parsed.data.path, config) await fs.mkdir(validPath, { recursive: true }) await logger.debug(`Created directory: ${validPath}`) endMetric() return { content: [{ type: 'text', text: `Successfully created directory ${parsed.data.path}` }], } } case 'list_directory': { const parsed = ListDirectoryArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const validPath = await validatePath(parsed.data.path, config) const entries = await fs.readdir(validPath, { withFileTypes: true }) // Sort directories first, then files, both alphabetically entries.sort((c, d) => { if (c.isDirectory() && !d.isDirectory()) return -1 if (!c.isDirectory() && d.isDirectory()) return 1 return c.name.localeCompare(d.name) }) const formatted = entries .map((entry) => `${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}`) .join('\n') await logger.debug(`Listed directory: ${validPath}`, { entryCount: entries.length }) endMetric() return { content: [{ type: 'text', text: formatted }], } } case 'directory_tree': { const parsed = DirectoryTreeArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } async function buildTree(currentPath: string): Promise<TreeEntry[]> { const validPath = await validatePath(currentPath, config) const entries = await fs.readdir(validPath, { withFileTypes: true }) // Sort directories first, then files, both alphabetically entries.sort((f, g) => { if (f.isDirectory() && !g.isDirectory()) return -1 if (!f.isDirectory() && g.isDirectory()) return 1 return f.name.localeCompare(g.name) }) const result: TreeEntry[] = [] for (const entry of entries) { const entryData: TreeEntry = { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', } if (entry.isDirectory()) { try { const subPath = path.join(currentPath, entry.name) entryData.children = await buildTree(subPath) } catch (error) { // If we can't access a subdirectory, represent it as empty entryData.children = [] } } result.push(entryData) } return result } const treeData = await buildTree(parsed.data.path) await logger.debug(`Generated directory tree: ${parsed.data.path}`) endMetric() return { content: [ { type: 'text', text: JSON.stringify(treeData, null, 2), }, ], } } case 'move_file': { const parsed = MoveFileArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const validSourcePath = await validatePath(parsed.data.source, config) const validDestPath = await validatePath(parsed.data.destination, config) // Ensure the destination parent directory exists const destDir = path.dirname(validDestPath) await fs.mkdir(destDir, { recursive: true }) await fs.rename(validSourcePath, validDestPath) await logger.debug(`Moved file from ${validSourcePath} to ${validDestPath}`) endMetric() return { content: [ { type: 'text', text: `Successfully moved ${parsed.data.source} to ${parsed.data.destination}`, }, ], } } case 'search_files': { const parsed = SearchFilesArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const validPath = await validatePath(parsed.data.path, config) const patternLower = parsed.data.pattern.toLowerCase() const results: string[] = [] async function search(currentPath: string) { try { const entries = await fs.readdir(currentPath, { withFileTypes: true }) for (const entry of entries) { const fullPath = path.join(currentPath, entry.name) try { await validatePath(fullPath, config) const relativePath = path.relative(validPath, fullPath) // Check if the path should be excluded const shouldExclude = parsed.data && parsed.data.excludePatterns.some((excludePattern) => { const globPattern = excludePattern.includes('*') ? excludePattern : `**/${excludePattern}**` return minimatch(relativePath, globPattern, { nocase: true }) }) if (shouldExclude) { continue } // Check if the name matches the search pattern if (entry.name.toLowerCase().includes(patternLower)) { results.push(fullPath) } // Recursively search subdirectories if (entry.isDirectory()) { await search(fullPath) } } catch (error) { // Skip paths we can't access or validate continue } } } catch (error) { // Skip directories we can't read return } } await search(validPath) await logger.debug(`Search complete: ${parsed.data.pattern}`, { resultCount: results.length, }) endMetric() return { content: [ { type: 'text', text: results.length > 0 ? `Found ${results.length} matches:\n${results.join('\n')}` : 'No matches found', }, ], } } case 'get_file_info': { const parsed = GetFileInfoArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const validPath = await validatePath(parsed.data.path, config) const info = await getFileStats(validPath) await logger.debug(`Retrieved file info: ${validPath}`) endMetric() return { content: [ { type: 'text', text: Object.entries(info) .map(([key, value]) => `${key}: ${value}`) .join('\n'), }, ], } } case 'list_allowed_directories': { await logger.debug('Listed allowed directories') endMetric() return { content: [ { type: 'text', text: `Allowed directories:\n${config.allowedDirectories.join('\n')}`, }, ], } } case 'get_metrics': { const metricsData = metrics.getMetrics() await logger.debug('Retrieved metrics') endMetric() return { content: [ { type: 'text', text: JSON.stringify(metricsData, null, 2), }, ], } } case 'execute_command': { const parsed = ExecuteCommandArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } const result = await executeCommand(parsed.data, config) endMetric() return { content: [ { type: 'text', text: `Command execution completed with exit code: ${result.exitCode}\n\nSTDOUT:\n${result.stdout}\n\nSTDERR:\n${result.stderr}`, }, ], } } case 'bash_execute': { return await handleBashExecute(a, config) } case 'bash_pipe': { return await handleBashPipe(a, config) } case 'curl_request': { const parsed = CurlRequestArgsSchema.safeParse(a) if (!parsed.success) { throw new FileSystemError(`Invalid arguments for ${name}`, 'INVALID_ARGS', undefined, { errors: parsed.error.format(), }) } return await handleCurlRequest(parsed.data, config) } default: throw new FileSystemError(`Unknown tool: ${name}`, 'UNKNOWN_TOOL') } } catch (error) { metrics.recordError(name) if (error instanceof FileSystemError) { await logger.error( `Error in ${name}:`, error instanceof FileSystemError ? error.toJSON() : { message: String(error) } ) return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, } } const errorMessage = error instanceof Error ? error.message : String(error) await logger.error(`Unexpected error in ${name}:`, { error }) return { content: [{ type: 'text', text: `Error: ${errorMessage}` }], isError: true, } } }) // Start the server const transport = new StdioServerTransport() await server.connect(transport) await logger.info('MCP-Filesystem Server running on stdio', { allowedDirectories: config.allowedDirectories, serverVersion: config.serverVersion, }) // Periodic metrics reporting if (config.metrics.enabled && config.metrics.reportIntervalMs > 0) { setInterval(() => { const metricsData = metrics.getMetrics() logger.info('Performance metrics', { metrics: metricsData }) }, config.metrics.reportIntervalMs) } } // Load configuration and start server loadConfig(configPath) .then(runServer) .catch(async (error: unknown) => { console.error('Fatal error loading configuration or running server:', 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/A-Niranjan/mcp-filesystem'

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