Your First MCP Server: File Operations
Your First MCP Server: File Operations

Introduction
Now we get hands-on. In this lesson you'll build your first custom MCP server from scratch — one that performs file operations. We'll use TypeScript and the official MCP SDK, following the same patterns used by production MCP servers.
Prerequisites
- Node.js 18 or newer
- npm or a package manager
- A TypeScript build toolchain (tsx, ts-node, or compiled output)
Setting Up the Project
Create a new directory and initialize it:
mkdir my-mcp-server
cd my-mcp-server
npm init -y
Install the MCP SDK:
npm install @modelcontextprotocol/sdk
npm install --save-dev typescript @types/node tsx
The Core SDK
The package @modelcontextprotocol/sdk is the official TypeScript SDK for building MCP servers. It provides the base classes, types, and transport handling you need.
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
}
}
Building a Minimal Server
A basic MCP server must implement at least these methods:
list_tools— advertise what tools the server providescall_tool— execute a requested tool
list_resources and list_prompts for capabilities that expose data or canned prompts.Here's a minimal file-operations server:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";// Only allow operations inside this directory
const ALLOWED_ROOT = path.resolve(process.argv[2] || ".");
const server = new McpServer({
name: "file-ops",
version: "1.0.0",
});
// list_tools is handled by the SDK from your registered tools
server.tool(
"list_files",
"List files in the allowed root directory",
{},
async () => {
const entries = await fs.readdir(ALLOWED_ROOT);
return {
content: [{ type: "text", text: entries.join("\n") }],
};
}
);
server.tool(
"write_file",
"Write text content to a file within the allowed root",
{ filename: z.string(), content: z.string() },
async ({ filename, content }) => {
const target = path.resolve(ALLOWED_ROOT, filename);
// Safety: ensure the target stays inside ALLOWED_ROOT
if (!target.startsWith(ALLOWED_ROOT)) {
throw new Error("Path outside of allowed root");
}
await fs.writeFile(target, content, "utf8");
return { content: [{ type: "text", text: Wrote ${filename} }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Why the Root Restriction Matters
Notice how every file operation is checked against ALLOWED_ROOT. This is the single most important safety pattern in MCP development. By restricting operations to a specific project directory, you prevent the AI from reading or modifying files outside its intended scope.
ALLOWED_ROOT (/project)
|----- read/write allowed here
|
x----- /etc, /home, other paths are OFF LIMITS
Running Your Server
# Run with the project directory as the allowed root
npx tsx src/server.ts /path/to/project
Once running over stdio, an MCP client like Claude Code or Cursor can connect to it and start calling list_files and write_file.
Testing Your Server
The simplest test is to connect it to any MCP-compatible client and ask it to list files. You can also write a tiny script that starts the server and sends a JSON-RPC request, but using a real client is far more practical.
Summary
- Use
@modelcontextprotocol/sdkto build MCP servers in TypeScript - Implement
list_toolsandcall_toolat minimum - Register tools with the SDK and define their input schemas
- Restrict file operations to a scoped root directory for safety
Next Lesson
Let's take things further and build advanced servers combining Git integration and browser automation.
Quiz - Quiz - Your First MCP Server
1. When building an MCP server in TypeScript, which package provides the core SDK?
2. A basic MCP server must implement which of these methods?
3. What is the safest approach when your MCP server needs to write files?