TL;DR: Learn how to build Model Context Protocol (MCP) servers that connect AI agents to any data source or tool. We'll build a production-ready file system MCP server with TypeScript, authentication, and error handling.
What is MCP and Why Should You Care?
The Model Context Protocol (MCP) is an open standard created by Anthropic that acts like "USB-C for AI applications." It provides a standardized way to connect AI agents to external systems—databases, APIs, file systems, or any tool your agent needs.
The Problem MCP Solves
Before MCP, connecting an AI agent to external tools required custom integrations for every combination:
Agent A → Custom Integration → Tool X
Agent A → Custom Integration → Tool Y
Agent B → Custom Integration → Tool X // Duplicate effort!
With MCP:
Agent A → MCP Protocol → Any MCP Server
Agent B → MCP Protocol → Any MCP Server // Build once, use everywhere
Real-World Use Cases
- AI IDEs: Claude Code can generate web apps from Figma designs
- Enterprise Chatbots: Connect to multiple databases across your organization
- Personal Assistants: Access Google Calendar, Notion, and email
- Creative Workflows: Control Blender, 3D printers, or design tools
Architecture Overview
MCP follows a client-server architecture with three core primitives:
| Primitive | Controlled By | Use Case |
|---|---|---|
| Tools | Model | Actions the AI can take (search, calculate, send email) |
| Resources | Application | Data exposed to the AI (files, database records) |
| Prompts | User | Reusable prompt templates with context injection |
Project Setup
Let's build a production-ready MCP server that provides file system operations to AI agents.
Step 1: Initialize the Project
mkdir mcp-filesystem-server
cd mcp-filesystem-server
npm init -y
Step 2: Install Dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
Step 3: Configure TypeScript
Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Building the MCP Server
Create the Server Structure
// src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { promises as fs } from 'fs';
import path from 'path';
// Configuration
const CONFIG = {
allowedDirectories: process.env.ALLOWED_DIRS?.split(',') || [process.cwd()],
maxFileSize: parseInt(process.env.MAX_FILE_SIZE || '10485760'),
logLevel: process.env.LOG_LEVEL || 'info',
} as const;
// Validation schemas
const ReadFileSchema = z.object({
path: z.string().describe('Absolute path to the file to read'),
});
const WriteFileSchema = z.object({
path: z.string().describe('Absolute path to write the file'),
content: z.string().describe('Content to write'),
});
File Operations with Security
// Security: Validate paths are within allowed directories
function validatePath(requestedPath: string): string {
const resolvedPath = path.resolve(requestedPath);
const isAllowed = CONFIG.allowedDirectories.some(dir => {
const resolvedDir = path.resolve(dir);
return resolvedPath.startsWith(resolvedDir);
});
if (!isAllowed) {
throw new McpError(
ErrorCode.InvalidRequest,
`Access denied: Path ${requestedPath} is outside allowed directories`
);
}
return resolvedPath;
}
async function readFile(filePath: string): Promise<string> {
const validatedPath = validatePath(filePath);
try {
const stats = await fs.stat(validatedPath);
if (!stats.isFile()) {
throw new McpError(ErrorCode.InvalidRequest, 'Path is not a file');
}
if (stats.size > CONFIG.maxFileSize) {
throw new McpError(
ErrorCode.InvalidRequest,
`File size exceeds maximum (${CONFIG.maxFileSize} bytes)`
);
}
return await fs.readFile(validatedPath, 'utf-8');
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
throw new McpError(ErrorCode.InvalidRequest, `File not found: ${filePath}`);
}
throw error;
}
}
Tool Definitions and Server Setup
const TOOLS = [
{
name: 'read_file',
description: 'Read the contents of a file',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Absolute path to the file' },
},
required: ['path'],
},
},
{
name: 'write_file',
description: 'Write content to a file',
inputSchema: {
type: 'object',
properties: {
path: { type: 'string', description: 'Absolute path to write' },
content: { type: 'string', description: 'Content to write' },
},
required: ['path', 'content'],
},
},
];
const server = new Server(
{ name: 'filesystem-mcp-server', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools: TOOLS };
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'read_file': {
const { path: filePath } = ReadFileSchema.parse(args);
const content = await readFile(filePath);
return { content: [{ type: 'text', text: content }] };
}
case 'write_file': {
const { path: filePath, content } = WriteFileSchema.parse(args);
await writeFile(filePath, content);
return { content: [{ type: 'text', text: `Wrote to ${filePath}` }] };
}
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
});
Start the Server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP Filesystem Server running on stdio');
}
main().catch(console.error);
Testing Your MCP Server
Build and test your server:
npm run build
export ALLOWED_DIRS="/home/user/projects"
node dist/index.js
Test with the MCP Inspector:
npx @anthropics/mcp-inspector node dist/index.js
Integrating with Claude Desktop
Add to your Claude Desktop config:
{
"mcpServers": {
"filesystem": {
"command": "node",
"args": ["/path/to/mcp-filesystem-server/dist/index.js"],
"env": {
"ALLOWED_DIRS": "/Users/yourname/Documents"
}
}
}
}
Best Practices
- Security First: Always validate paths, set file size limits
- Error Handling: Use McpError for protocol errors, don't expose internals
- Tool Design: Clear names, detailed descriptions, proper required fields
- Logging: Log to stderr (stdout is for MCP protocol)
Conclusion
MCP is transforming how we build AI-powered applications. The filesystem server we built demonstrates production-ready patterns with TypeScript, Zod validation, and security best practices.
Your AI agents are only as powerful as the tools they can access. Start building MCP servers today!
Want to Build AI Agents Faster?
If you're building AI agents and want to skip the boilerplate, check out my AI Automation Starter Kit:
- 4 production-ready agent templates
- Shared utilities for memory, auth, and monitoring
- Deployment scripts for Docker and cloud
- Complete setup guides
Get it on Gumroad for $9 → quantbit1.gumroad.com/l/ai-automation-starter-kit
Built by QuantBitRealm. Need custom AI development? Hire us
Top comments (0)