Skip to main content
Glama
MartinSchlott

BetterMCPFileServer

pathUtils.ts8.85 kB
import fs from "fs/promises"; import path from "path"; import os from "os"; // Interface for path aliases export interface PathAlias { alias: string; fullPath: string; normalizedPath: string; // Lowercase, normalized for comparison } // Store mapping between aliases and full paths export const pathAliases: PathAlias[] = []; // Normalize all paths consistently export function normalizePath(p: string): string { return path.normalize(p).toLowerCase(); } export function expandHome(filepath: string): string { if (filepath.startsWith('~/') || filepath === '~') { return path.join(os.homedir(), filepath.slice(1)); } return filepath; } // Process command line arguments to get path aliases export async function parseAliasArgs(args: string[]): Promise<PathAlias[]> { if (args.length === 0) { console.error("Usage: mcp-file-server <alias>:<allowed-directory> [<alias2>:<directory2>...]"); process.exit(1); } const aliases: PathAlias[] = []; // Parse and validate all aliases for (const arg of args) { const parts = arg.split(':', 2); // Check if the argument contains a colon if (parts.length !== 2 || !parts[0] || !parts[1]) { console.error(`Error: Invalid alias:path format - ${arg}`); process.exit(1); } const [alias, fullPath] = parts; // Check alias format (no slashes, special chars) if (!/^[a-zA-Z0-9_-]+$/.test(alias)) { console.error(`Error: Alias must only contain letters, numbers, underscores, or hyphens - ${alias}`); process.exit(1); } // Check for duplicate aliases if (aliases.some(pa => pa.alias === alias)) { console.error(`Error: Duplicate alias - ${alias}`); process.exit(1); } const expandedPath = expandHome(fullPath); const absolutePath = path.resolve(expandedPath); try { const stats = await fs.stat(absolutePath); if (!stats.isDirectory()) { console.error(`Error: ${fullPath} is not a directory`); process.exit(1); } aliases.push({ alias, fullPath: absolutePath, normalizedPath: normalizePath(absolutePath) }); } catch (error) { console.error(`Error accessing directory ${fullPath}:`, error); process.exit(1); } } return aliases; } // Process command line arguments to get allowed directories (legacy method) export function initAllowedDirectories(dirs: string[]): string[] { return dirs.map(dir => normalizePath(path.resolve(expandHome(dir))) ); } // Convert an alias path to an absolute filesystem path export function resolveAliasPath(aliasPath: string): string { // Special case: root if (aliasPath === 'root' || aliasPath === '/') { return 'root'; } // Split path into components const parts = aliasPath.split('/'); const requestedAlias = parts[0]; // Find matching alias const matchingAlias = pathAliases.find(pa => pa.alias === requestedAlias); if (!matchingAlias) { throw new Error(`Unknown alias: ${requestedAlias}`); } // Replace alias with full path parts[0] = matchingAlias.fullPath; return path.join(...parts); } // Convert an absolute filesystem path to an alias path export function toAliasPath(absolutePath: string): string { // Find the longest matching alias path let bestMatch: PathAlias | null = null; let longestPrefix = 0; const normalizedPath = normalizePath(absolutePath); for (const alias of pathAliases) { if (normalizedPath.startsWith(alias.normalizedPath) && alias.normalizedPath.length > longestPrefix) { bestMatch = alias; longestPrefix = alias.normalizedPath.length; } } if (!bestMatch) { throw new Error(`Path outside alias directories: ${absolutePath}`); } // Replace the matching prefix with the alias const relativePath = path.relative(bestMatch.fullPath, absolutePath); return path.join(bestMatch.alias, relativePath); } // Check if a path is a write operation to root export function isWriteOperationToRoot(operation: string, path: string): boolean { return (operation === "write" || operation === "create" || operation === "delete") && (path === "root" || path === "/"); } // Validate directory arguments export async function validateDirectories(dirs: string[]): Promise<void> { await Promise.all(dirs.map(async (dir) => { try { // Extract the path part if it's an alias:path format const pathPart = dir.includes(':') ? dir.split(':', 2)[1] : dir; const stats = await fs.stat(pathPart); if (!stats.isDirectory()) { console.error(`Error: ${pathPart} is not a directory`); process.exit(1); } } catch (error) { console.error(`Error accessing directory ${dir}:`, error); process.exit(1); } })); } // Handle the special case for listing root (all aliases) export async function listRootDirectory(): Promise<any[]> { return pathAliases.map(pa => ({ name: pa.alias, path: pa.alias, type: "directory", // Skip including the actual path to protect privacy })); } // Modified validatePath to work with aliases export async function validatePath(requestedPath: string, allowedDirectories: string[] = []): Promise<string> { // If we're using aliases and the path isn't absolute, try to resolve it as an alias path if (pathAliases.length > 0 && (!path.isAbsolute(requestedPath) || requestedPath.startsWith('~'))) { // Special case for root if (requestedPath === 'root' || requestedPath === '/') { throw new Error("Cannot perform this operation on the filesystem root directly"); } try { // Convert alias path to absolute path const absolutePath = resolveAliasPath(requestedPath); // Handle symlinks by checking their real path try { const realPath = await fs.realpath(absolutePath); // Verify that the real path is still within our allowed paths const normalizedReal = normalizePath(realPath); const isRealPathAllowed = pathAliases.some(pa => normalizedReal.startsWith(pa.normalizedPath) ); if (!isRealPathAllowed) { throw new Error("Access denied - symlink target outside allowed directories"); } return realPath; } catch (error) { // For new files that don't exist yet, verify parent directory const parentDir = path.dirname(absolutePath); try { const realParentPath = await fs.realpath(parentDir); const normalizedParent = normalizePath(realParentPath); const isParentAllowed = pathAliases.some(pa => normalizedParent.startsWith(pa.normalizedPath) ); if (!isParentAllowed) { throw new Error("Access denied - parent directory outside allowed directories"); } return absolutePath; } catch { throw new Error(`Parent directory does not exist: ${parentDir}`); } } } catch (error) { throw new Error(`Invalid alias path: ${error instanceof Error ? error.message : String(error)}`); } } else { // Legacy direct path validation logic (no aliases) const expandedPath = expandHome(requestedPath); const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath); const normalizedRequested = normalizePath(absolute); // Check if path is within allowed directories const isAllowed = allowedDirectories.some(dir => normalizedRequested.startsWith(dir)); if (!isAllowed) { throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${allowedDirectories.join(', ')}`); } // Handle symlinks by checking their real path try { const realPath = await fs.realpath(absolute); const normalizedReal = normalizePath(realPath); const isRealPathAllowed = allowedDirectories.some(dir => normalizedReal.startsWith(dir)); if (!isRealPathAllowed) { throw new Error("Access denied - symlink target outside allowed directories"); } return realPath; } catch (error) { // For new files that don't exist yet, verify parent directory const parentDir = path.dirname(absolute); try { const realParentPath = await fs.realpath(parentDir); const normalizedParent = normalizePath(realParentPath); const isParentAllowed = allowedDirectories.some(dir => normalizedParent.startsWith(dir)); if (!isParentAllowed) { throw new Error("Access denied - parent directory outside allowed directories"); } return absolute; } catch { throw new Error(`Parent directory does not exist: ${parentDir}`); } } } }

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/MartinSchlott/BetterMCPFileServer'

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