DEV Community

Cover image for How to build your own file-system MCP server (A step-by-step guide)
Jefree Sujit
Jefree Sujit

Posted on

How to build your own file-system MCP server (A step-by-step guide)

MCP servers are powerful: they let clients (IDEs, agents) interact with remote or local features and services in a structured, RPC-style way. But what if you want to build your own MCP server — one tailored to your domain or use case?

Welcome to the last part of the three-part series on MCP (Model Context Protocol). If you haven’t checked the previous parts, feel free to check Part 1 (what MCP is, and its underlying concept) and Part 2 (Connecting existing MCP servers and popular MCPs)

In this final part, we’ll walk through how to build a file-system MCP server — i.e. an MCP server that lets an IDE or any MCP client interact with your file system over MCP. You can use it locally or even host it remotely, depending on your needs.

What you’ll walk away with

By the end of this article, you will be able to:

  • Understand the difference between local vs remote MCP servers
  • Build a simple file-system MCP server (list, read, write, delete files)
  • Connect your custom MCP server with clients like Cursor, Windsurf, etc.
  • Package and publish your MCP server as an npm package

Why build your own MCP server?

Before jumping into code, let’s consider why you might want a custom MCP server:

  • Standard MCP servers give you generic data or tools, but you may want domain-specific ones (e.g. file system, database, cloud storage, diagnostics).
  • With your own MCP server, you control which operations are allowed, access rules, optimization / caching, and extensions.
  • It lets your clients (IDE extensions, agent systems) talk to your data or environment via MCP in a consistent, structured way.
  • You can run it locally (on the user's machine) or remotely (in a server or VM), depending on your architecture.

Types of MCP servers: Local vs Remote

  • Local MCP server: runs on the user’s machine (e.g. shipped with your app/CLI), communicates over stdio or IPC.
  • Remote MCP server: lives on a server or cluster, communicates over HTTP, WebSocket, or another network transport.

From the MCP logic’s point of view, tools / resource handlers are the same. Only the transport (stdio vs HTTP) changes.

Because file systems are inherently local, we will target a local MCP server (running on the same machine). But you can later extend it to remote transports, bridge via HTTP/WS, or host it in a container.

Setting up the codebase

Enough with the boring lectures, let's start with the actual implementation. Here’s roughly how you’d structure your project:

src/
  server.ts
  tools.ts
tsconfig.json
package.json
Enter fullscreen mode Exit fullscreen mode

Tools: The Core File System Operations

Create a new file called tools.ts In tools.ts, you’ll define functions (handlers) for each operation. Each handler receives input arguments (e.g. directory path, file path, file content) and returns a result or error in MCP protocol format (or using Zod-validated schemas).

Here are the basic tool definitions you'd need:

  • listDirectory(dirPath: string): DirectoryListingResponse
  • readFile(filePath: string): FileContentResponse
  • writeFile(filePath: string, content: string): FileWriteResponse
  • removeFile(filePath: string): FileDeleteResponse

Here's a simple implementation of tools.ts to setup necessary tools for the filesystem:

// tools.ts
export async function listDirectory(args: { path: string }) {
  const abs = resolveSafe(args.path);
  const stat = await fs.stat(abs);
  if (!stat.isDirectory()) {
    throw new Error(`${args.path} is not a directory`);
  }
  const names = await fs.readdir(abs);
  const entries = await Promise.all(
    names.map(async (name) => {
      const child = path.join(abs, name);
      const st = await fs.stat(child);
      return { name, isDirectory: st.isDirectory() };
    })
  );
  return { entries };
}

export async function readFile(args: { path: string }) {
  const abs = resolveSafe(args.path);
  const content = await fs.readFile(abs, "utf-8");
  return { content };
}

export async function writeFile(args: { path: string; content: string }) {
  const abs = resolveSafe(args.path);
  await fs.writeFile(abs, args.content, "utf-8");
  return { success: true };
}

export async function removeFile(args: { path: string }) {
  const abs = resolveSafe(args.path);
  await fs.unlink(abs);
  return { success: true };
}

Enter fullscreen mode Exit fullscreen mode

You should wrap in try / catch and handle errors (e.g. file not found, permission denied) gracefully, returning structured error objects.

Using Zod (or similar) you can define input & output schemas to validate and serialize them.

Setting up the MCP Server files

Now that we have the tools setup ready, lets start with the actual MCP server file setup.

In server.ts, we'll:

  • Import MCP server from @modelcontextprotocol/sdk
  • Import the transport (e.g. StdioServerTransport as we are build a local MCP server)
  • Instantiate the server
  • Register the required tools (from tools.ts)
  • Start / connect the transport
// server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";  
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";  
import * as fsTools from "./tools";

async function main() {
  const server = new McpServer({
    name: "fs-mcp-server",
    version: "0.1.0",
  });

  server.registerTool("listDirectory", fsTools.listDirectory);
  server.registerTool("readFile",     fsTools.readFile);
  server.registerTool("writeFile",    fsTools.writeFile);
  server.registerTool("removeFile",   fsTools.removeFile);

  const transport = new StdioServerTransport();
  await server.connect(transport);

  console.log("fs-mcp-server is running");
}

main().catch((err) => {
  console.error("Server failed:", err);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

At runtime, this listens for incoming MCP RPCs over stdio and invokes your file system handlers.

Build & run it, test with a client

Lets setup the package.json with npm init and install necessary dependencies. It should look something like this:


{
  "name": "fs-mcp-server",
  "version": "0.1.0",
  "main": "dist/server.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^…",
    "zod": "^…",
    // other required dependencies
  }
}
Enter fullscreen mode Exit fullscreen mode

Set up tsconfig.json so tsc compiles into dist/.

Then run:

npm run build
npm run start

Enter fullscreen mode Exit fullscreen mode

That should launch your MCP server.

Client Connection Setup

The next setup is to connect and test the local MCP. Once you have the local MCP server running, go to your MCP Client (like Cursor, VS Code, or Claude Desktop) settings and add the MCP Config.

Here’s a valid MCP config you can drop into an IDE or MCP-client config to connect a local MCP server via stdio / command. You can adjust command, args, working_directory, etc., to match your setup.

{
  "mcpServers": {
    "my-fs-mcp": {
      "command": "node",
      "args": ["/absolute/path/to/your/mcp-project/dist/server.js"],
      "env": {
        "FS_ROOT": "/absolute/path/to/exposed/root"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Once you have configured the MCP properly, the MCP client would automatically verify and list all available tools from our MCP. This shows the MCP is properly connected.

Now you can go to the Chat and ask the Agent to use our MCP to perform any file related operations and you'll see the Agents making necessary calls to our MCP tools to perform the actions.

Publishing & Distribution

Once it works locally and you're confident, the next step is to make it available globally:

Publish as an npm package, so users can npx fs-mcp-server. Here's the official guide on how to publish this package into NPM

Once you've published you can connect your MCP directly with your npx command:

{
  "mcpServers": {
    "my-fs-mcp": {
      "type": "stdio",
      "command": "npx",
      "args": ["fs-mcp-server"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Verify the MCP by connecting with your MCP Client.

Some extensions and cool things you can add next

Once the basics are working, here are ideas to expand:

  • Add rename / move and create directory / delete directory
  • Support search / glob matching inside a directory
  • Handle binary files (base64 encoding or streaming buffers)
  • Support remote transport (HTTP, WebSocket) so you can host it
  • Offer mounting multiple roots under alias names (e.g. workspace/, config/)
  • Add validation schemas (using Zod) to enforce input/output correctness

Conclusion & next steps

In this final part of the series, you’ve learned how to build your own MCP server — specifically a file-system server — from the ground up.

To quickly try our the filesystem MCP directly in your IDE, check out this NPM package. To see the full implementation and reference code along with the mini version of Gemini CLI (with filesystem capabilities), check out this GitHub repo.

If you found this helpful, I’d greatly appreciate your support — a like, share, or comment helps a ton. And if you run into any bottlenecks, questions, or ideas while building your MCP server, feel free to drop a comment — I’m happy to help.

Top comments (0)