Skip to main content
Glama

Filesystem MCP Server

utility-handlers.ts9.95 kB
import fs from 'fs/promises'; import path from 'path'; import { XMLParser } from 'fast-xml-parser'; import { Permissions } from '../config/permissions.js'; import { validatePath } from '../utils/path-utils.js'; import { parseArgs } from '../utils/schema-utils.js'; import { searchFiles, findFilesByExtension, regexSearchContent } from '../utils/file-utils.js'; import { GetPermissionsArgsSchema, SearchFilesArgsSchema, FindFilesByExtensionArgsSchema, XmlToJsonArgsSchema, XmlToJsonStringArgsSchema, RegexSearchContentArgsSchema, // Added import type GetPermissionsArgs, type SearchFilesArgs, type FindFilesByExtensionArgs, type XmlToJsonArgs, type XmlToJsonStringArgs, type RegexSearchContentArgs } from '../schemas/utility-operations.js'; export function handleGetPermissions( args: unknown, permissions: Permissions, readonlyFlag: boolean, noFollowSymlinks: boolean, allowedDirectories: string[] ) { parseArgs(GetPermissionsArgsSchema, args, 'get_permissions'); return { content: [{ type: "text", text: `Current permission state: readOnly: ${readonlyFlag} followSymlinks: ${!noFollowSymlinks} fullAccess: ${permissions.fullAccess} Operations allowed: - create: ${permissions.create} - edit: ${permissions.edit} - move: ${permissions.move} - rename: ${permissions.rename} - delete: ${permissions.delete} Server was started with ${allowedDirectories.length} allowed ${allowedDirectories.length === 1 ? 'directory' : 'directories'}. Use 'list_allowed_directories' to see them.` }], }; } export async function handleSearchFiles( args: unknown, allowedDirectories: string[], symlinksMap: Map<string, string>, noFollowSymlinks: boolean ) { const parsed = parseArgs(SearchFilesArgsSchema, args, 'search_files'); const { path: startPath, pattern, excludePatterns, maxDepth, maxResults } = parsed; const validPath = await validatePath(startPath, allowedDirectories, symlinksMap, noFollowSymlinks); const results = await searchFiles(validPath, pattern, excludePatterns, maxDepth, maxResults); return { content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matches found" }], }; } export async function handleFindFilesByExtension( args: unknown, allowedDirectories: string[], symlinksMap: Map<string, string>, noFollowSymlinks: boolean ) { const parsed = parseArgs(FindFilesByExtensionArgsSchema, args, 'find_files_by_extension'); const { path: startPath, extension, excludePatterns, maxDepth, maxResults } = parsed; const validPath = await validatePath(startPath, allowedDirectories, symlinksMap, noFollowSymlinks); const results = await findFilesByExtension( validPath, extension, excludePatterns, maxDepth, maxResults ); return { content: [{ type: "text", text: results.length > 0 ? results.join("\n") : "No matching files found" }], }; } export async function handleXmlToJson( args: unknown, permissions: Permissions, allowedDirectories: string[], symlinksMap: Map<string, string>, noFollowSymlinks: boolean ) { const parsed = parseArgs(XmlToJsonArgsSchema, args, 'xml_to_json'); const { xmlPath, jsonPath, maxBytes, options } = parsed; const validXmlPath = await validatePath(xmlPath, allowedDirectories, symlinksMap, noFollowSymlinks); // Source must exist const validJsonPath = await validatePath( jsonPath, allowedDirectories, symlinksMap, noFollowSymlinks, { checkParentExists: false } // Add option here for output JSON path ); try { // Read the XML file (no input size gating; limit only output) const xmlContent = await fs.readFile(validXmlPath, "utf-8"); // Parse XML to JSON const parserOptions = { ignoreAttributes: options?.ignoreAttributes ?? false, preserveOrder: options?.preserveOrder ?? true, // Add other options as needed }; const parser = new XMLParser(parserOptions); const jsonObj = parser.parse(xmlContent); // Format JSON if requested const format = options?.format ?? true; const indentSize = options?.indentSize ?? 2; let jsonContent = format ? JSON.stringify(jsonObj, null, indentSize) : JSON.stringify(jsonObj); // Enforce response-size cap for file write by truncating content if needed const responseLimit = (parsed as any).maxResponseBytes ?? maxBytes; if (typeof responseLimit === 'number' && responseLimit > 0) { const size = Buffer.byteLength(jsonContent, 'utf8'); if (size > responseLimit) { // Produce a summarized payload to fit limit const summary = { _meta: { truncated: true, originalSize: size, note: `JSON too large to write fully; summarizing to fit ${responseLimit} bytes.` }, sample: Array.isArray(jsonObj) ? jsonObj.slice(0, 3) : (typeof jsonObj === 'object' ? Object.fromEntries(Object.entries(jsonObj).slice(0, 50)) : jsonObj) }; jsonContent = JSON.stringify(summary, null, indentSize); } } // Check if JSON file exists to determine if this is a create operation let fileExists = false; try { await fs.access(validJsonPath); fileExists = true; } catch (error) { // File doesn't exist - this is a create operation } // Enforce permission checks for writing if (fileExists && !permissions.edit && !permissions.fullAccess) { throw new Error('Cannot write to existing JSON file: edit permission not granted (requires --allow-edit)'); } if (!fileExists && !permissions.create && !permissions.fullAccess) { throw new Error('Cannot create new JSON file: create permission not granted (requires --allow-create)'); } // Write JSON to file // Ensure parent dir exists before writing the JSON file const jsonParentDir = path.dirname(validJsonPath); await fs.mkdir(jsonParentDir, { recursive: true }); // Ensure parent exists await fs.writeFile(validJsonPath, jsonContent, "utf-8"); return { content: [{ type: "text", text: `Successfully converted XML from ${xmlPath} to JSON at ${jsonPath}` }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to convert XML to JSON: ${errorMessage}`); } } export async function handleXmlToJsonString( args: unknown, allowedDirectories: string[], symlinksMap: Map<string, string>, noFollowSymlinks: boolean ) { const parsed = parseArgs(XmlToJsonStringArgsSchema, args, 'xml_to_json_string'); const { xmlPath, maxBytes, options } = parsed; const validXmlPath = await validatePath(xmlPath, allowedDirectories, symlinksMap, noFollowSymlinks); try { // Read the XML file (no input size gating; limit only output) const xmlContent = await fs.readFile(validXmlPath, "utf-8"); // Parse XML to JSON const parserOptions = { ignoreAttributes: options?.ignoreAttributes ?? false, preserveOrder: options?.preserveOrder ?? true, // Add other options as needed }; const parser = new XMLParser(parserOptions); const jsonObj = parser.parse(xmlContent); // Return the JSON as a string let jsonContent = JSON.stringify(jsonObj, null, 2); // Apply response-size cap const responseLimit = (parsed as any).maxResponseBytes ?? maxBytes ?? 200 * 1024; // default 200KB if (typeof responseLimit === 'number' && responseLimit > 0) { const size = Buffer.byteLength(jsonContent, 'utf8'); if (size > responseLimit) { const summary = { _meta: { truncated: true, originalSize: size, note: `JSON too large; summarizing to fit ${responseLimit} bytes.` }, sample: Array.isArray(jsonObj) ? jsonObj.slice(0, 5) : (typeof jsonObj === 'object' ? Object.fromEntries(Object.entries(jsonObj).slice(0, 100)) : jsonObj) }; jsonContent = JSON.stringify(summary, null, 2); } } return { content: [{ type: "text", text: jsonContent }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to convert XML to JSON: ${errorMessage}`); } } export function handleListAllowedDirectories( args: unknown, allowedDirectories: string[] ): { content: [{ type: string; text: string }] } { return { content: [{ type: "text", text: `Allowed directories:\n${allowedDirectories.join('\n')}` }], }; } export async function handleRegexSearchContent( args: unknown, allowedDirectories: string[], symlinksMap: Map<string, string>, noFollowSymlinks: boolean ) { const parsed = parseArgs(RegexSearchContentArgsSchema, args, 'regex_search_content'); const { path: startPath, regex, filePattern, maxDepth, maxFileSize, maxResults } = parsed; const validPath = await validatePath(startPath, allowedDirectories, symlinksMap, noFollowSymlinks); try { const results = await regexSearchContent( validPath, regex, filePattern, maxDepth, maxFileSize, maxResults ); if (results.length === 0) { return { content: [{ type: "text", text: "No matches found for the given regex pattern." }] }; } // Format the output const formattedResults = results.map(fileResult => { const matchesText = fileResult.matches .map(match => ` Line ${match.lineNumber}: ${match.lineContent.trim()}`) .join('\n'); return `File: ${fileResult.path}\n${matchesText}`; }).join('\n\n'); return { content: [{ type: "text", text: formattedResults }], }; } catch (error: any) { // Catch errors from regexSearchContent (e.g., invalid regex) throw new Error(`Error during regex content search: ${error.message}`); } }

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

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