Tools: Building Production-Ready MCP Servers with TypeScript: A Complete Guide

Tools: Building Production-Ready MCP Servers with TypeScript: A Complete Guide

Source: Dev.to

What is MCP and Why Should You Care? ## The Problem MCP Solves ## Real-World Use Cases ## Architecture Overview ## Project Setup ## Step 1: Initialize the Project ## Step 2: Install Dependencies ## Step 3: Configure TypeScript ## Building the MCP Server ## Create the Server Structure ## File Operations with Security ## Tool Definitions and Server Setup ## Start the Server ## Testing Your MCP Server ## Integrating with Claude Desktop ## Best Practices ## Conclusion ## Want to Build AI Agents Faster? 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. 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. Before MCP, connecting an AI agent to external tools required custom integrations for every combination: MCP follows a client-server architecture with three core primitives: Let's build a production-ready MCP server that provides file system operations to AI agents. Create tsconfig.json: Build and test your server: Test with the MCP Inspector: Add to your Claude Desktop config: 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! If you're building AI agents and want to skip the boilerplate, check out my AI Automation Starter Kit: Get it on Gumroad for $9 → quantbit1.gumroad.com/l/ai-automation-starter-kit Built by QuantBitRealm. Need custom AI development? Hire us Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: Agent A → Custom Integration → Tool X Agent A → Custom Integration → Tool Y Agent B → Custom Integration → Tool X // Duplicate effort! Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Agent A → Custom Integration → Tool X Agent A → Custom Integration → Tool Y Agent B → Custom Integration → Tool X // Duplicate effort! CODE_BLOCK: Agent A → Custom Integration → Tool X Agent A → Custom Integration → Tool Y Agent B → Custom Integration → Tool X // Duplicate effort! CODE_BLOCK: Agent A → MCP Protocol → Any MCP Server Agent B → MCP Protocol → Any MCP Server // Build once, use everywhere Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: Agent A → MCP Protocol → Any MCP Server Agent B → MCP Protocol → Any MCP Server // Build once, use everywhere CODE_BLOCK: Agent A → MCP Protocol → Any MCP Server Agent B → MCP Protocol → Any MCP Server // Build once, use everywhere CODE_BLOCK: mkdir mcp-filesystem-server cd mcp-filesystem-server npm init -y Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: mkdir mcp-filesystem-server cd mcp-filesystem-server npm init -y CODE_BLOCK: mkdir mcp-filesystem-server cd mcp-filesystem-server npm init -y COMMAND_BLOCK: npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node tsx Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node tsx COMMAND_BLOCK: npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node tsx CODE_BLOCK: { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } CODE_BLOCK: { "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } CODE_BLOCK: // 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'), }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: // 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'), }); CODE_BLOCK: // 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'), }); COMMAND_BLOCK: // 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; } } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // 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; } } COMMAND_BLOCK: // 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; } } COMMAND_BLOCK: 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}`); } }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: 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}`); } }); COMMAND_BLOCK: 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}`); } }); CODE_BLOCK: async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP Filesystem Server running on stdio'); } main().catch(console.error); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP Filesystem Server running on stdio'); } main().catch(console.error); CODE_BLOCK: async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP Filesystem Server running on stdio'); } main().catch(console.error); COMMAND_BLOCK: npm run build export ALLOWED_DIRS="/home/user/projects" node dist/index.js Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: npm run build export ALLOWED_DIRS="/home/user/projects" node dist/index.js COMMAND_BLOCK: npm run build export ALLOWED_DIRS="/home/user/projects" node dist/index.js CODE_BLOCK: npx @anthropics/mcp-inspector node dist/index.js Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: npx @anthropics/mcp-inspector node dist/index.js CODE_BLOCK: npx @anthropics/mcp-inspector node dist/index.js CODE_BLOCK: { "mcpServers": { "filesystem": { "command": "node", "args": ["/path/to/mcp-filesystem-server/dist/index.js"], "env": { "ALLOWED_DIRS": "/Users/yourname/Documents" } } } } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: { "mcpServers": { "filesystem": { "command": "node", "args": ["/path/to/mcp-filesystem-server/dist/index.js"], "env": { "ALLOWED_DIRS": "/Users/yourname/Documents" } } } } CODE_BLOCK: { "mcpServers": { "filesystem": { "command": "node", "args": ["/path/to/mcp-filesystem-server/dist/index.js"], "env": { "ALLOWED_DIRS": "/Users/yourname/Documents" } } } } - 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 - 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) - 4 production-ready agent templates - Shared utilities for memory, auth, and monitoring - Deployment scripts for Docker and cloud - Complete setup guides