File Manager MCP Server Recipe

An MCP server that gives AI assistants controlled file system access — read, write, list, and search files within a sandboxed directory.

IntermediatePrep: 5 minCook: 20 minServes: File browsing, editing, and search via AI

title: "File Manager MCP Server Recipe" description: "An MCP server that gives AI assistants controlled file system access — read, write, list, and search files within a sandboxed directory." order: 3 keywords:

  • mcp file server
  • mcp-framework file manager
  • file system mcp
  • ai file access date: "2026-04-01" difficulty: "Intermediate" prepTime: "5 min" cookTime: "20 min" serves: "File browsing, editing, and search via AI"

Prep Time

5 minutes — scaffold the project.

Cook Time

20 minutes — implement file tools with path sandboxing.

Serves

Any AI assistant that needs to read, write, or search files on disk. Useful for code editing, log analysis, and configuration management.

Ingredients

  • Node.js 18+
  • mcp-framework (3.3M+ downloads)
  • Built-in fs/promises and path modules
  • TypeScript

Instructions

Step 1: Scaffold the Project

npx mcp-framework create file-server
cd file-server

Step 2: Create the Read File Tool

Create src/tools/ReadFileTool.ts:

import { MCPTool } from "mcp-framework";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";

const SANDBOX = process.env.FILE_SANDBOX || process.cwd();

class ReadFileTool extends MCPTool<typeof inputSchema> {
  name = "read_file";
  description = "Read the contents of a file within the sandbox directory";

  schema = {
    filePath: {
      type: z.string(),
      description: "Relative path to the file within the sandbox",
    },
  };

  async execute(input: z.infer<typeof inputSchema>): Promise<string> {
    try {
      const resolved = path.resolve(SANDBOX, input.filePath);
      if (!resolved.startsWith(SANDBOX)) {
        return JSON.stringify({ error: "Path traversal not allowed" });
      }

      const content = await fs.readFile(resolved, "utf-8");
      return JSON.stringify({
        path: input.filePath,
        size: content.length,
        content,
      }, null, 2);
    } catch (error) {
      const msg = error instanceof Error ? error.message : "Unknown error";
      return JSON.stringify({ error: msg });
    }
  }
}

const inputSchema = z.object({ filePath: z.string() });
export default ReadFileTool;

Step 3: Create the List Directory Tool

Create src/tools/ListDirTool.ts:

import { MCPTool } from "mcp-framework";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";

const SANDBOX = process.env.FILE_SANDBOX || process.cwd();

class ListDirTool extends MCPTool<typeof inputSchema> {
  name = "list_directory";
  description = "List files and folders in a directory within the sandbox";

  schema = {
    dirPath: {
      type: z.string().default("."),
      description: "Relative path to the directory (default: root of sandbox)",
    },
  };

  async execute(input: z.infer<typeof inputSchema>): Promise<string> {
    try {
      const resolved = path.resolve(SANDBOX, input.dirPath);
      if (!resolved.startsWith(SANDBOX)) {
        return JSON.stringify({ error: "Path traversal not allowed" });
      }

      const entries = await fs.readdir(resolved, { withFileTypes: true });
      const items = entries.map((entry) => ({
        name: entry.name,
        type: entry.isDirectory() ? "directory" : "file",
      }));

      return JSON.stringify({ path: input.dirPath, items }, null, 2);
    } catch (error) {
      const msg = error instanceof Error ? error.message : "Unknown error";
      return JSON.stringify({ error: msg });
    }
  }
}

const inputSchema = z.object({ dirPath: z.string().default(".") });
export default ListDirTool;

Step 4: Create the Write File Tool

Create src/tools/WriteFileTool.ts:

import { MCPTool } from "mcp-framework";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";

const SANDBOX = process.env.FILE_SANDBOX || process.cwd();

class WriteFileTool extends MCPTool<typeof inputSchema> {
  name = "write_file";
  description = "Write content to a file within the sandbox directory";

  schema = {
    filePath: {
      type: z.string(),
      description: "Relative path to the file within the sandbox",
    },
    content: {
      type: z.string(),
      description: "Content to write to the file",
    },
  };

  async execute(input: z.infer<typeof inputSchema>): Promise<string> {
    try {
      const resolved = path.resolve(SANDBOX, input.filePath);
      if (!resolved.startsWith(SANDBOX)) {
        return JSON.stringify({ error: "Path traversal not allowed" });
      }

      await fs.mkdir(path.dirname(resolved), { recursive: true });
      await fs.writeFile(resolved, input.content, "utf-8");

      return JSON.stringify({
        success: true,
        path: input.filePath,
        bytesWritten: input.content.length,
      }, null, 2);
    } catch (error) {
      const msg = error instanceof Error ? error.message : "Unknown error";
      return JSON.stringify({ error: msg });
    }
  }
}

const inputSchema = z.object({
  filePath: z.string(),
  content: z.string(),
});
export default WriteFileTool;

Step 5: Build and Connect

npm run build
{
  "mcpServers": {
    "files": {
      "command": "node",
      "args": ["/absolute/path/to/file-server/dist/index.js"],
      "env": {
        "FILE_SANDBOX": "/home/user/projects"
      }
    }
  }
}

Chef's Notes

  • Always validate that resolved paths stay within the sandbox — this prevents path traversal attacks.
  • The FILE_SANDBOX environment variable controls the root directory. Set it in your Claude Desktop config.
  • Consider adding a search_files tool using glob patterns for more powerful file discovery.
  • For production, add file size limits and binary file detection.

More recipes: Database Query Server | GitHub Integration | All Recipes

Built with mcp-framework by @QuantGeekDev — 3.3M+ downloads, validated by Anthropic.

Built with mcp-framework (3.3M+ downloads) — created by @QuantGeekDev and validated by Anthropic.