Skip to main content
Glama

fast-filesystem-mcp

server.ts29.8 kB
import { promises as fs } from 'fs'; import path from 'path'; import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); // Claude 최적화 설정 const CLAUDE_MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB const CLAUDE_MAX_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB const CLAUDE_MAX_LINES = 2000; // 최대 2000줄 const CLAUDE_MAX_DIR_ITEMS = 1000; // 디렉토리 항목 최대 1000개 // 기본 허용 디렉토리들 const DEFAULT_ALLOWED_DIRECTORIES = [ process.env.HOME || '/home', '/tmp', '/Users', '/home' ]; // 기본 제외 패턴 (보안 및 성능) const DEFAULT_EXCLUDE_PATTERNS = [ '.venv', 'venv', 'node_modules', '.git', '.svn', '.hg', '__pycache__', '.pytest_cache', '.mypy_cache', '.coverage', 'dist', 'build', 'target', 'bin', 'obj', '.vs', '.vscode', '*.pyc', '*.pyo', '*.pyd', '.DS_Store', 'Thumbs.db' ]; // 유틸리티 함수들 function isPathAllowed(targetPath: string): boolean { const absolutePath = path.resolve(targetPath); return DEFAULT_ALLOWED_DIRECTORIES.some(allowedDir => absolutePath.startsWith(path.resolve(allowedDir)) ); } function safePath(inputPath: string): string { if (!isPathAllowed(inputPath)) { throw new Error(`Access denied to path: ${inputPath}`); } return path.resolve(inputPath); } function formatSize(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(2)} ${units[unitIndex]}`; } function shouldExcludePath(targetPath: string, excludePatterns: string[] = []): boolean { const patterns = [...DEFAULT_EXCLUDE_PATTERNS, ...excludePatterns]; const pathName = path.basename(targetPath).toLowerCase(); const pathParts = targetPath.split(path.sep); return patterns.some(pattern => { const patternLower = pattern.toLowerCase(); if (pattern.includes('*') || pattern.includes('?')) { // 간단한 와일드카드 매칭 const regex = new RegExp(pattern.replace(/\*/g, '.*').replace(/\?/g, '.')); return regex.test(pathName); } return pathParts.some(part => part.toLowerCase() === patternLower) || pathName === patternLower; }); } function truncateContent(content: string, maxSize: number = CLAUDE_MAX_RESPONSE_SIZE) { const contentBytes = Buffer.byteLength(content, 'utf8'); if (contentBytes <= maxSize) { return { content, truncated: false }; } let truncated = content; while (Buffer.byteLength(truncated, 'utf8') > maxSize) { truncated = truncated.slice(0, -1); } return { content: truncated, truncated: true, original_size: contentBytes, truncated_size: Buffer.byteLength(truncated, 'utf8') }; } // MCP 툴 목록 정의 const MCP_TOOLS = [ { name: 'fast_list_allowed_directories', description: '허용된 디렉토리 목록을 조회합니다', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'fast_read_file', description: '파일을 읽습니다 (청킹 지원)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '읽을 파일 경로' }, start_offset: { type: 'number', description: '시작 바이트 위치' }, max_size: { type: 'number', description: '읽을 최대 크기' }, line_start: { type: 'number', description: '시작 라인 번호' }, line_count: { type: 'number', description: '읽을 라인 수' }, encoding: { type: 'string', description: '텍스트 인코딩', default: 'utf-8' } }, required: ['path'] } }, { name: 'fast_write_file', description: '파일을 쓰거나 수정합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '파일 경로' }, content: { type: 'string', description: '파일 내용' }, encoding: { type: 'string', description: '텍스트 인코딩', default: 'utf-8' }, create_dirs: { type: 'boolean', description: '디렉토리 자동 생성', default: true }, append: { type: 'boolean', description: '추가 모드', default: false } }, required: ['path', 'content'] } }, { name: 'fast_list_directory', description: '디렉토리 목록을 조회합니다 (페이징 지원)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '디렉토리 경로' }, page: { type: 'number', description: '페이지 번호', default: 1 }, page_size: { type: 'number', description: '페이지당 항목 수' }, pattern: { type: 'string', description: '파일명 필터 패턴' }, show_hidden: { type: 'boolean', description: '숨김 파일 표시', default: false }, sort_by: { type: 'string', description: '정렬 기준', enum: ['name', 'size', 'modified', 'type'], default: 'name' }, reverse: { type: 'boolean', description: '역순 정렬', default: false } }, required: ['path'] } }, { name: 'fast_get_file_info', description: '파일/디렉토리 상세 정보를 조회합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '조회할 경로' } }, required: ['path'] } }, { name: 'fast_create_directory', description: '디렉토리를 생성합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '생성할 디렉토리 경로' }, recursive: { type: 'boolean', description: '재귀적 생성', default: true } }, required: ['path'] } }, { name: 'fast_search_files', description: '파일을 검색합니다 (이름/내용)', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '검색할 디렉토리' }, pattern: { type: 'string', description: '검색 패턴' }, content_search: { type: 'boolean', description: '파일 내용 검색', default: false }, case_sensitive: { type: 'boolean', description: '대소문자 구분', default: false }, max_results: { type: 'number', description: '최대 결과 수', default: 100 } }, required: ['path', 'pattern'] } }, { name: 'fast_get_directory_tree', description: '디렉토리 트리 구조를 가져옵니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '루트 디렉토리 경로' }, max_depth: { type: 'number', description: '최대 깊이', default: 3 }, show_hidden: { type: 'boolean', description: '숨김 파일 표시', default: false }, include_files: { type: 'boolean', description: '파일 포함', default: true } }, required: ['path'] } }, { name: 'fast_get_disk_usage', description: '디스크 사용량을 조회합니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '조회할 경로', default: '/' } } } }, { name: 'fast_find_large_files', description: '큰 파일들을 찾습니다', inputSchema: { type: 'object', properties: { path: { type: 'string', description: '검색할 디렉토리' }, min_size: { type: 'string', description: '최소 크기 (예: 100MB, 1GB)', default: '100MB' }, max_results: { type: 'number', description: '최대 결과 수', default: 50 } }, required: ['path'] } } ]; export default async function handler(req: any, res: any) { // CORS 헤더 설정 res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.status(200).end(); return; } try { if (req.method === 'GET') { res.setHeader('Content-Type', 'application/json'); res.status(200).json({ status: 'online', name: 'fast-filesystem', version: '2.1.0', deployment: new Date().toISOString(), environment: 'production', config: { url: 'https://fast-filesystem-mcp.vercel.app/api/server', claude_desktop_config: { "mcpServers": { "fast-filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-fetch", "https://fast-filesystem-mcp.vercel.app/api/server" ] } } } }, tools: MCP_TOOLS.map(tool => tool.name), total_tools: MCP_TOOLS.length, features: [ 'Advanced file operations', 'Directory tree traversal', 'File search and content search', 'Claude optimized responses' ], github: 'https://github.com/efforthye/fast-filesystem-mcp' }); } else if (req.method === 'POST') { res.setHeader('Content-Type', 'application/json'); const { method, params, id } = req.body || {}; if (method === 'initialize') { // MCP 초기화 프로토콜 res.status(200).json({ jsonrpc: '2.0', id: id, result: { protocolVersion: '2024-11-05', capabilities: { tools: {}, prompts: {}, resources: {}, logging: {} }, serverInfo: { name: 'fast-filesystem', version: '2.1.0' } } }); } else if (method === 'notifications/initialized') { // 초기화 완료 알림 - notification이므로 응답하지 않음 res.status(200).end(); return; } else if (method === 'tools/list') { res.status(200).json({ jsonrpc: '2.0', id: id, result: { tools: MCP_TOOLS } }); } else if (method === 'tools/call') { const { name, arguments: args } = params || {}; try { let result; switch (name) { case 'fast_list_allowed_directories': result = await handleListAllowedDirectories(); break; case 'fast_read_file': result = await handleReadFile(args); break; case 'fast_write_file': result = await handleWriteFile(args); break; case 'fast_list_directory': result = await handleListDirectory(args); break; case 'fast_get_file_info': result = await handleGetFileInfo(args); break; case 'fast_create_directory': result = await handleCreateDirectory(args); break; case 'fast_search_files': result = await handleSearchFiles(args); break; case 'fast_get_directory_tree': result = await handleGetDirectoryTree(args); break; case 'fast_get_disk_usage': result = await handleGetDiskUsage(args); break; case 'fast_find_large_files': result = await handleFindLargeFiles(args); break; default: throw new Error(`Tool not implemented: ${name}`); } res.status(200).json({ jsonrpc: '2.0', id: id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } }); } catch (error) { res.status(200).json({ jsonrpc: '2.0', id: id, error: { code: -32603, message: 'Internal error', data: error instanceof Error ? error.message : 'Unknown error' } }); } } else { // 알 수 없는 메서드에 대한 에러 응답 res.status(200).json({ jsonrpc: '2.0', id: id, error: { code: -32601, message: 'Method not found', data: `Unknown method: ${method}` } }); } } else { res.setHeader('Allow', ['GET', 'POST', 'OPTIONS']); res.status(405).json({ error: 'Method Not Allowed', allowed_methods: ['GET', 'POST', 'OPTIONS'] }); } } catch (error) { console.error('Server error:', error); res.status(500).json({ jsonrpc: '2.0', id: req.body?.id || null, error: { code: -32603, message: 'Internal Server Error', data: error instanceof Error ? error.message : 'Unknown error' } }); } } // 툴 핸들러 함수들 async function handleListAllowedDirectories() { return { allowed_directories: DEFAULT_ALLOWED_DIRECTORIES, current_working_directory: process.cwd(), exclude_patterns: DEFAULT_EXCLUDE_PATTERNS, claude_limits: { max_response_size_mb: CLAUDE_MAX_RESPONSE_SIZE / (1024**2), max_chunk_size_mb: CLAUDE_MAX_CHUNK_SIZE / (1024**2), max_lines_per_read: CLAUDE_MAX_LINES, max_dir_items: CLAUDE_MAX_DIR_ITEMS }, server_info: { name: 'fast-filesystem', version: '2.1.0', total_tools: MCP_TOOLS.length, timestamp: new Date().toISOString() } }; } async function handleReadFile(args: any) { const { path: filePath, start_offset = 0, max_size, line_start, line_count, encoding = 'utf-8' } = args; const safePath_resolved = safePath(filePath); const stats = await fs.stat(safePath_resolved); if (!stats.isFile()) { throw new Error('Path is not a file'); } const maxReadSize = max_size ? Math.min(max_size, CLAUDE_MAX_CHUNK_SIZE) : CLAUDE_MAX_CHUNK_SIZE; if (line_start !== undefined) { const linesToRead = line_count ? Math.min(line_count, CLAUDE_MAX_LINES) : CLAUDE_MAX_LINES; const fileContent = await fs.readFile(safePath_resolved, encoding as BufferEncoding); const lines = fileContent.split('\n'); const selectedLines = lines.slice(line_start, line_start + linesToRead); return { content: selectedLines.join('\n'), mode: 'lines', start_line: line_start, lines_read: selectedLines.length, total_lines: lines.length, file_size: stats.size, file_size_readable: formatSize(stats.size), encoding: encoding, path: safePath_resolved }; } const fileHandle = await fs.open(safePath_resolved, 'r'); const buffer = Buffer.alloc(maxReadSize); const { bytesRead } = await fileHandle.read(buffer, 0, maxReadSize, start_offset); await fileHandle.close(); const content = buffer.subarray(0, bytesRead).toString(encoding as BufferEncoding); const result = truncateContent(content); return { content: result.content, mode: 'bytes', start_offset: start_offset, bytes_read: bytesRead, file_size: stats.size, file_size_readable: formatSize(stats.size), encoding: encoding, truncated: result.truncated, has_more: start_offset + bytesRead < stats.size, path: safePath_resolved }; } async function handleWriteFile(args: any) { const { path: filePath, content, encoding = 'utf-8', create_dirs = true, append = false } = args; let targetPath: string; if (path.isAbsolute(filePath)) { targetPath = filePath; } else { targetPath = path.join(process.cwd(), filePath); } if (!isPathAllowed(targetPath)) { throw new Error(`Access denied to path: ${targetPath}`); } const resolvedPath = path.resolve(targetPath); if (create_dirs) { const dir = path.dirname(resolvedPath); await fs.mkdir(dir, { recursive: true }); } if (append) { await fs.appendFile(resolvedPath, content, encoding as BufferEncoding); } else { await fs.writeFile(resolvedPath, content, encoding as BufferEncoding); } const stats = await fs.stat(resolvedPath); return { message: `File ${append ? 'appended' : 'written'} successfully`, path: resolvedPath, size: stats.size, size_readable: formatSize(stats.size), encoding: encoding, mode: append ? 'append' : 'write', timestamp: new Date().toISOString() }; } async function handleListDirectory(args: any) { const { path: dirPath, page = 1, page_size, pattern, show_hidden = false, sort_by = 'name', reverse = false } = args; const safePath_resolved = safePath(dirPath); const stats = await fs.stat(safePath_resolved); if (!stats.isDirectory()) { throw new Error('Path is not a directory'); } const pageSize = page_size ? Math.min(page_size, CLAUDE_MAX_DIR_ITEMS) : 50; const entries = await fs.readdir(safePath_resolved, { withFileTypes: true }); let filteredEntries = entries.filter(entry => { if (!show_hidden && entry.name.startsWith('.')) return false; if (shouldExcludePath(path.join(safePath_resolved, entry.name))) return false; if (pattern) { return entry.name.toLowerCase().includes(pattern.toLowerCase()); } return true; }); // 정렬 filteredEntries.sort((a, b) => { let comparison = 0; switch (sort_by) { case 'name': comparison = a.name.localeCompare(b.name); break; case 'type': const aType = a.isDirectory() ? 'directory' : 'file'; const bType = b.isDirectory() ? 'directory' : 'file'; comparison = aType.localeCompare(bType); break; default: comparison = a.name.localeCompare(b.name); } return reverse ? -comparison : comparison; }); const startIdx = (page - 1) * pageSize; const endIdx = startIdx + pageSize; const pageEntries = filteredEntries.slice(startIdx, endIdx); const items = await Promise.all(pageEntries.map(async (entry) => { try { const fullPath = path.join(safePath_resolved, entry.name); const itemStats = await fs.stat(fullPath); return { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', size: entry.isFile() ? itemStats.size : null, size_readable: entry.isFile() ? formatSize(itemStats.size) : null, modified: itemStats.mtime.toISOString(), created: itemStats.birthtime.toISOString(), permissions: itemStats.mode, path: fullPath }; } catch { return { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file', size: null, size_readable: null, modified: null, created: null, permissions: null, path: path.join(safePath_resolved, entry.name) }; } })); return { path: safePath_resolved, items: items, page: page, page_size: pageSize, total_count: filteredEntries.length, total_pages: Math.ceil(filteredEntries.length / pageSize), has_more: endIdx < filteredEntries.length, sort_by: sort_by, reverse: reverse, timestamp: new Date().toISOString() }; } async function handleGetFileInfo(args: any) { const { path: targetPath } = args; const safePath_resolved = safePath(targetPath); const stats = await fs.stat(safePath_resolved); const info = { path: safePath_resolved, name: path.basename(safePath_resolved), type: stats.isDirectory() ? 'directory' : 'file', size: stats.size, size_readable: formatSize(stats.size), created: stats.birthtime.toISOString(), modified: stats.mtime.toISOString(), accessed: stats.atime.toISOString(), permissions: stats.mode, is_readable: true, is_writable: true }; if (stats.isFile()) { (info as any).extension = path.extname(safePath_resolved); (info as any).mime_type = getMimeType(safePath_resolved); if (stats.size > CLAUDE_MAX_CHUNK_SIZE) { (info as any).claude_guide = { message: 'File is large, consider using chunked reading', recommended_chunk_size: CLAUDE_MAX_CHUNK_SIZE, total_chunks: Math.ceil(stats.size / CLAUDE_MAX_CHUNK_SIZE) }; } } else if (stats.isDirectory()) { try { const entries = await fs.readdir(safePath_resolved); (info as any).item_count = entries.length; if (entries.length > CLAUDE_MAX_DIR_ITEMS) { (info as any).claude_guide = { message: 'Directory has many items, consider using pagination', recommended_page_size: CLAUDE_MAX_DIR_ITEMS, total_pages: Math.ceil(entries.length / CLAUDE_MAX_DIR_ITEMS) }; } } catch { (info as any).item_count = 'Unable to count'; } } return info; } async function handleCreateDirectory(args: any) { const { path: dirPath, recursive = true } = args; const safePath_resolved = safePath(dirPath); await fs.mkdir(safePath_resolved, { recursive }); return { message: 'Directory created successfully', path: safePath_resolved, recursive: recursive, timestamp: new Date().toISOString() }; } async function handleSearchFiles(args: any) { const { path: searchPath, pattern, content_search = false, case_sensitive = false, max_results = 100 } = args; const safePath_resolved = safePath(searchPath); const maxResults = Math.min(max_results, 200); const results: any[] = []; const searchPattern = case_sensitive ? pattern : pattern.toLowerCase(); async function searchDirectory(dirPath: string) { if (results.length >= maxResults) return; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { if (results.length >= maxResults) break; const fullPath = path.join(dirPath, entry.name); if (shouldExcludePath(fullPath)) continue; if (entry.isFile()) { const searchName = case_sensitive ? entry.name : entry.name.toLowerCase(); let matched = false; let matchType = ''; if (searchName.includes(searchPattern)) { matched = true; matchType = 'filename'; } if (!matched && content_search) { try { const stats = await fs.stat(fullPath); if (stats.size < 10 * 1024 * 1024) { // 10MB 제한 const content = await fs.readFile(fullPath, 'utf-8'); const searchContent = case_sensitive ? content : content.toLowerCase(); if (searchContent.includes(searchPattern)) { matched = true; matchType = 'content'; } } } catch { // 바이너리 파일 등 읽기 실패 무시 } } if (matched) { const stats = await fs.stat(fullPath); results.push({ path: fullPath, name: entry.name, match_type: matchType, size: stats.size, size_readable: formatSize(stats.size), modified: stats.mtime.toISOString(), extension: path.extname(fullPath) }); } } else if (entry.isDirectory()) { await searchDirectory(fullPath); } } } catch { // 권한 없는 디렉토리 등 무시 } } await searchDirectory(safePath_resolved); return { results: results, total_found: results.length, search_pattern: pattern, search_path: safePath_resolved, content_search: content_search, case_sensitive: case_sensitive, max_results_reached: results.length >= maxResults, timestamp: new Date().toISOString() }; } async function handleGetDirectoryTree(args: any) { const { path: rootPath, max_depth = 3, show_hidden = false, include_files = true } = args; const safePath_resolved = safePath(rootPath); async function buildTree(currentPath: string, currentDepth: number): Promise<any> { if (currentDepth > max_depth) return null; try { const stats = await fs.stat(currentPath); const name = path.basename(currentPath); if (!show_hidden && name.startsWith('.')) return null; if (shouldExcludePath(currentPath)) return null; const node: any = { name: name, path: currentPath, type: stats.isDirectory() ? 'directory' : 'file', size: stats.size, size_readable: formatSize(stats.size), modified: stats.mtime.toISOString() }; if (stats.isDirectory()) { node.children = []; try { const entries = await fs.readdir(currentPath, { withFileTypes: true }); for (const entry of entries) { const childPath = path.join(currentPath, entry.name); if (entry.isDirectory()) { const childNode = await buildTree(childPath, currentDepth + 1); if (childNode) node.children.push(childNode); } else if (include_files) { const childNode = await buildTree(childPath, currentDepth + 1); if (childNode) node.children.push(childNode); } } } catch { // 권한 없는 디렉토리 node.error = 'Access denied'; } } return node; } catch { return null; } } const tree = await buildTree(safePath_resolved, 0); return { tree: tree, root_path: safePath_resolved, max_depth: max_depth, show_hidden: show_hidden, include_files: include_files, timestamp: new Date().toISOString() }; } async function handleGetDiskUsage(args: any) { const { path: targetPath = '/' } = args; try { const { stdout } = await execAsync(`df -h "${targetPath}"`); const lines = stdout.split('\n').filter(line => line.trim()); if (lines.length > 1) { const data = lines[1].split(/\s+/); return { filesystem: data[0], total: data[1], used: data[2], available: data[3], use_percentage: data[4], mounted_on: data[5], path: targetPath, timestamp: new Date().toISOString() }; } } catch { // Fallback for systems without df command } return { error: 'Unable to get disk usage information', path: targetPath, timestamp: new Date().toISOString() }; } async function handleFindLargeFiles(args: any) { const { path: searchPath, min_size = '100MB', max_results = 50 } = args; const safePath_resolved = safePath(searchPath); const maxResults = Math.min(max_results, 100); // 크기 파싱 (예: 100MB -> bytes) const parseSize = (sizeStr: string): number => { const match = sizeStr.match(/^(\d+(\.\d+)?)\s*(B|KB|MB|GB|TB)?$/i); if (!match) return 100 * 1024 * 1024; // 기본값 100MB const value = parseFloat(match[1]); const unit = (match[3] || 'B').toUpperCase(); const units: {[key: string]: number} = { 'B': 1, 'KB': 1024, 'MB': 1024 * 1024, 'GB': 1024 * 1024 * 1024, 'TB': 1024 * 1024 * 1024 * 1024 }; return value * (units[unit] || 1); }; const minSizeBytes = parseSize(min_size); const results: any[] = []; async function findLargeFilesRecursive(dirPath: string) { if (results.length >= maxResults) return; try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { if (results.length >= maxResults) break; const fullPath = path.join(dirPath, entry.name); if (shouldExcludePath(fullPath)) continue; if (entry.isFile()) { try { const stats = await fs.stat(fullPath); if (stats.size >= minSizeBytes) { results.push({ path: fullPath, name: entry.name, size: stats.size, size_readable: formatSize(stats.size), modified: stats.mtime.toISOString(), extension: path.extname(fullPath) }); } } catch { // 파일 접근 실패 무시 } } else if (entry.isDirectory()) { await findLargeFilesRecursive(fullPath); } } } catch { // 권한 없는 디렉토리 무시 } } await findLargeFilesRecursive(safePath_resolved); // 크기별로 정렬 (큰 것부터) results.sort((a, b) => b.size - a.size); return { results: results, total_found: results.length, search_path: safePath_resolved, min_size: min_size, min_size_bytes: minSizeBytes, max_results_reached: results.length >= maxResults, timestamp: new Date().toISOString() }; } function getMimeType(filePath: string): string { const ext = path.extname(filePath).toLowerCase(); const mimeTypes: {[key: string]: string} = { '.txt': 'text/plain', '.html': 'text/html', '.css': 'text/css', '.js': 'application/javascript', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.pdf': 'application/pdf', '.zip': 'application/zip', '.md': 'text/markdown' }; return mimeTypes[ext] || 'application/octet-stream'; }

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/efforthye/fast-filesystem-mcp'

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