Skip to main content
Glama

filesystem-mcp

by sylphxltd
read-content.ts7.52 kB
// src/handlers/readContent.ts import { promises as fs, type Stats } from 'node:fs'; // Import Stats import { z } from 'zod'; import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { resolvePath } from '../utils/path-utils.js'; // --- Types --- interface McpToolResponse { content: { type: 'text'; text: string }[]; } export const ReadContentArgsSchema = z .object({ paths: z .array(z.string()) .min(1, { message: 'Paths array cannot be empty' }) .describe('Array of relative file paths to read.'), start_line: z .number() .int() .min(1) .optional() .describe('Optional 1-based starting line number'), end_line: z.number().int().min(1).optional().describe('Optional 1-based ending line number'), format: z .enum(['raw', 'lines']) .default('lines') .describe('Output format - "raw" for plain text, "lines" for line objects'), }) .strict(); type ReadContentArgs = z.infer<typeof ReadContentArgsSchema>; interface ReadResult { path: string; content?: string | { lineNumber: number; content: string }[]; error?: string; } // --- Helper Functions --- /** Parses and validates the input arguments. */ function parseAndValidateArgs(args: unknown): ReadContentArgs { try { return ReadContentArgsSchema.parse(args); } catch (error) { if (error instanceof z.ZodError) { throw new McpError( ErrorCode.InvalidParams, `Invalid arguments: ${error.errors.map((e) => `${e.path.join('.')} (${e.message})`).join(', ')}`, ); } throw new McpError(ErrorCode.InvalidParams, 'Argument validation failed'); } } /** Handles filesystem errors during file read or stat. */ interface FileReadErrorOptions { pathOutput: string; relativePath?: string; targetPath?: string; } function getBasicFsErrorMessage(fsError: unknown): string { return `Filesystem error: ${fsError instanceof Error ? fsError.message : String(fsError)}`; } function getSpecificFsErrorMessage( code: string, relativePath?: string, targetPath?: string, ): string | undefined { switch (code) { case 'ENOENT': { return targetPath ? `File not found at resolved path '${targetPath}'${relativePath ? ` (from relative path '${relativePath}')` : ''}` : 'File not found'; } case 'EISDIR': { return relativePath ? `Path is a directory, not a file: ${relativePath}` : 'Path is a directory, not a file'; } case 'EACCES': case 'EPERM': { return relativePath ? `Permission denied reading file: ${relativePath}` : 'Permission denied reading file'; } default: { return undefined; } } } function getFsErrorMessage(fsError: unknown, relativePath?: string, targetPath?: string): string { if (!fsError || typeof fsError !== 'object' || !('code' in fsError)) { return getBasicFsErrorMessage(fsError); } const specificMessage = getSpecificFsErrorMessage(String(fsError.code), relativePath, targetPath); return specificMessage || getBasicFsErrorMessage(fsError); } function handleFileReadFsError(fsError: unknown, options: FileReadErrorOptions): ReadResult { const { pathOutput, relativePath, targetPath } = options; const errorMessage = getFsErrorMessage(fsError, relativePath, targetPath); return { path: pathOutput, error: errorMessage }; } /** Handles errors during path resolution. */ function handlePathResolveError( resolveError: unknown, _relativePath: string, pathOutput: string, ): ReadResult { const errorMessage = resolveError instanceof Error ? resolveError.message : String(resolveError); // Error logged via McpError return { path: pathOutput, error: `Error resolving path: ${errorMessage}` }; } /** Processes the reading of a single file. */ interface ReadOperationOptions { startLine?: number | undefined; endLine?: number | undefined; format?: 'raw' | 'lines'; } async function processSingleReadOperation( _relativePath: string, options: ReadOperationOptions = {}, ): Promise<ReadResult> { const { startLine, endLine, format } = options; const pathOutput = _relativePath.replaceAll('\\', '/'); let targetPath = ''; try { targetPath = resolvePath(_relativePath); try { const stats: Stats = await fs.stat(targetPath); // Explicitly type Stats if (!stats.isFile()) { return { path: pathOutput, error: `Path is not a regular file: ${_relativePath}`, }; } if (startLine !== undefined || endLine !== undefined) { // Read file line by line when line range is specified const fileContent = await fs.readFile(targetPath, 'utf8'); const lines = fileContent.split('\n'); const start = startLine ? Math.min(startLine - 1, lines.length) : 0; const end = endLine ? Math.min(endLine, lines.length) : lines.length; const filteredLines = lines.slice(start, end); const content = format === 'raw' ? filteredLines.join('\n') : filteredLines.map((line, i) => ({ lineNumber: start + i + 1, content: line, })); return { path: pathOutput, content }; } else { // Read entire file when no line range specified const content = await fs.readFile(targetPath, 'utf8'); return { path: pathOutput, content: content }; } } catch (fsError: unknown) { return handleFileReadFsError(fsError, { pathOutput, relativePath: _relativePath, targetPath, }); } } catch (resolveError: unknown) { return handlePathResolveError(resolveError, _relativePath, pathOutput); } } /** Processes results from Promise.allSettled. */ function processSettledResults( results: PromiseSettledResult<ReadResult>[], originalPaths: string[], ): ReadResult[] { return results.map((result, index) => { const originalPath = originalPaths[index] ?? 'unknown_path'; const pathOutput = originalPath.replaceAll('\\', '/'); return result.status === 'fulfilled' ? result.value : { path: pathOutput, error: `Unexpected error during processing: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`, }; }); } /** Main handler function */ const handleReadContentFunc = async (args: unknown): Promise<McpToolResponse> => { const { paths: relativePaths, start_line, end_line, format } = parseAndValidateArgs(args); const readPromises = relativePaths.map((path) => processSingleReadOperation(path, { startLine: start_line, endLine: end_line, format }), ); const settledResults = await Promise.allSettled(readPromises); const outputContents = processSettledResults(settledResults, relativePaths); // Sort results by original path order for predictability const originalIndexMap = new Map(relativePaths.map((p, i) => [p.replaceAll('\\', '/'), i])); outputContents.sort((a, b) => { const indexA = originalIndexMap.get(a.path) ?? Infinity; const indexB = originalIndexMap.get(b.path) ?? Infinity; return indexA - indexB; }); return { content: [{ type: 'text', text: JSON.stringify(outputContents, undefined, 2) }], }; }; // Export the complete tool definition export const readContentToolDefinition = { name: 'read_content', description: 'Read content from multiple specified files.', inputSchema: ReadContentArgsSchema, handler: handleReadContentFunc, };

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

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