Skip to main content
Glama
ahmetshbz1

Filesystem MCP Server

by ahmetshbz1
list.ts6.37 kB
import fs from 'fs/promises'; import path from 'path'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { validatePath, formatSize } from '../lib.js'; import { minimatch } from 'minimatch'; import type { ToolInput, MCPResponse, HandlerFunction, FileEntryWithStats } from './types.js'; import type { Dirent } from 'fs'; const ListArgsSchema = z.object({ path: z.string(), mode: z.enum(['simple', 'detailed', 'tree', 'recursive']).default('simple'), page: z.number().min(1).optional().default(1), pageSize: z.number().min(1).max(1000).optional().default(100), includeHidden: z.boolean().optional().default(false), pattern: z.string().optional(), sortBy: z.enum(['name', 'size', 'mtime', 'atime']).optional().default('name'), includePermissions: z.boolean().optional().default(false), excludePatterns: z.array(z.string()).optional().default([]), maxDepth: z.number().optional(), includeSize: z.boolean().optional().default(false), respectGitignore: z.boolean().optional().default(false), includeStats: z.boolean().optional().default(false) }); type ListArgs = z.infer<typeof ListArgsSchema>; function formatPermissions(mode: number): string { const perms = [ (mode & 0o400) ? 'r' : '-', (mode & 0o200) ? 'w' : '-', (mode & 0o100) ? 'x' : '-', (mode & 0o040) ? 'r' : '-', (mode & 0o020) ? 'w' : '-', (mode & 0o010) ? 'x' : '-', (mode & 0o004) ? 'r' : '-', (mode & 0o002) ? 'w' : '-', (mode & 0o001) ? 'x' : '-', ]; return perms.join(''); } export const tools = [{ name: 'list', description: 'Unified directory listing. Use mode: simple|detailed|tree|recursive', inputSchema: zodToJsonSchema(ListArgsSchema) as ToolInput }]; export const handlers: Record<string, HandlerFunction> = { async list(args: Record<string, unknown>): Promise<MCPResponse> { const parsed = ListArgsSchema.safeParse(args); if (!parsed.success) throw new Error(`Invalid arguments: ${parsed.error}`); const validPath = await validatePath(parsed.data.path); switch(parsed.data.mode) { case 'simple': return handleSimpleList(validPath, parsed.data); case 'detailed': return handleDetailedList(validPath, parsed.data); case 'tree': return handleTreeList(validPath, parsed.data); case 'recursive': return handleRecursiveList(validPath, parsed.data); default: throw new Error('Invalid mode'); } } }; async function handleSimpleList(validPath: string, data: ListArgs): Promise<MCPResponse> { let entries = await fs.readdir(validPath, { withFileTypes: true }); if (!data.includeHidden) entries = entries.filter((e: Dirent) => !e.name.startsWith('.')); if (data.pattern) entries = entries.filter((e: Dirent) => minimatch(e.name, data.pattern || '')); const startIdx = (data.page - 1) * data.pageSize; const paginatedEntries = entries.slice(startIdx, startIdx + data.pageSize); const formatted = paginatedEntries.map((e: Dirent) => `${e.isDirectory() ? '[DIR] ' : '[FILE]'} ${e.name}` ).join('\n'); return { content: [{ type: 'text', text: `${formatted}\n\nPage ${data.page}/${Math.ceil(entries.length / data.pageSize)} | Total entries: ${entries.length}` }] }; } async function handleDetailedList(validPath: string, data: ListArgs): Promise<MCPResponse> { let entries = await fs.readdir(validPath, { withFileTypes: true }); if (!data.includeHidden) entries = entries.filter((e: Dirent) => !e.name.startsWith('.')); if (data.pattern) entries = entries.filter((e: Dirent) => minimatch(e.name, data.pattern || '')); const entriesWithStats = await Promise.all(entries.map(async (e: Dirent) => { const stats = await fs.stat(path.join(validPath, e.name)); return { entry: e, stats }; })); entriesWithStats.sort((a, b) => { const key = data.sortBy || 'name'; if (key === 'name') return a.entry.name.localeCompare(b.entry.name); if (key === 'size') return b.stats.size - a.stats.size; if (key === 'mtime') return b.stats.mtime.getTime() - a.stats.mtime.getTime(); return 0; }); const formatted = entriesWithStats.map(({ entry, stats }) => { let line = data.includePermissions ? `${formatPermissions(stats.mode)} ` : ''; line += `${entry.isDirectory() ? '[DIR] ' : '[FILE]'} `; line += `${formatSize(stats.size).padStart(10)} ${entry.name}`; return line; }).join('\n'); return { content: [{ type: 'text', text: formatted }] }; } async function handleTreeList(validPath: string, data: ListArgs, depth = 0): Promise<MCPResponse> { const indent = ' '.repeat(depth); let result = ''; if (data.maxDepth !== undefined && depth >= data.maxDepth) { return { content: [{ type: 'text', text: result }] }; } const entries = await fs.readdir(validPath, { withFileTypes: true }); for (const entry of entries) { if (!data.includeHidden && entry.name.startsWith('.')) continue; const fullPath = path.join(validPath, entry.name); const stats = data.includeSize ? await fs.stat(fullPath) : null; const sizeStr = stats ? ` (${formatSize(stats.size)})` : ''; result += `${indent}${entry.isDirectory() ? '[DIR]' : '[FILE]'} ${entry.name}${sizeStr}\n`; if (entry.isDirectory() && (!data.maxDepth || depth < data.maxDepth - 1)) { const subResult = await handleTreeList(fullPath, data, depth + 1); result += subResult.content[0].text; } } return { content: [{ type: 'text', text: result }] }; } async function handleRecursiveList(validPath: string, data: ListArgs, results: string[] = [], depth = 0): Promise<MCPResponse> { if (data.maxDepth && depth >= data.maxDepth) { return { content: [{ type: 'text', text: results.join('\n') }] }; } const entries = await fs.readdir(validPath, { withFileTypes: true }); for (const entry of entries) { if (!data.includeHidden && entry.name.startsWith('.')) continue; const fullPath = path.join(validPath, entry.name); const relativePath = path.relative(data.path, fullPath); if (data.pattern && !minimatch(entry.name, data.pattern) && entry.isFile()) continue; if (entry.isFile()) results.push(relativePath); else if (entry.isDirectory()) { await handleRecursiveList(fullPath, data, results, depth + 1); } } if (depth === 0) return { content: [{ type: 'text', text: results.join('\n') }] }; return { content: [{ type: 'text', text: '' }] }; }

Latest Blog Posts

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

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