DEV Community

Santhosh M
Santhosh M

Posted on

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

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!
Enter fullscreen mode Exit fullscreen mode

With MCP:

Agent A → MCP Protocol → Any MCP Server
Agent B → MCP Protocol → Any MCP Server  // Build once, use everywhere
Enter fullscreen mode Exit fullscreen mode

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

Communication Flow

┌─────────────┐     JSON-RPC      ┌─────────────┐
│  MCP Client │  ←────────────→   │ MCP Server  │
│  (Claude,   │   stdio or SSE    │ (Your Code) │
│   Your App) │                   │             │
└─────────────┘                   └──────┬──────┘
                                         │
                                    ┌────┴────┐
                                    │ External│
                                    │ System  │
                                    │(DB, API)│
                                    └─────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Dependencies

npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure TypeScript

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
Enter fullscreen mode Exit fullscreen mode

Building the MCP Server

Here's the complete server implementation:

// 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'), // 10MB
} 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'),
});

// 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;
}

// File operations with error handling
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
const TOOLS = [
  {
    name: 'read_file',
    description: 'Read the contents of a file. Returns the file content as text.',
    inputSchema: {
      type: 'object',
      properties: {
        path: {
          type: 'string',
          description: 'Absolute path to the file to read',
        },
      },
      required: ['path'],
    },
  },
  {
    name: 'write_file',
    description: 'Write content to a file. Creates parent directories if needed.',
    inputSchema: {
      type: 'object',
      properties: {
        path: {
          type: 'string',
          description: 'Absolute path to write the file',
        },
        content: {
          type: 'string',
          description: 'Content to write to the file',
        },
      },
      required: ['path', 'content'],
    },
  },
];

// Create and configure the server
const server = new Server(
  {
    name: 'filesystem-mcp-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// Handler: List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return { tools: TOOLS };
});

// Handler: Execute a tool
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  try {
    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);
        const validatedPath = validatePath(filePath);
        const parentDir = path.dirname(validatedPath);
        await fs.mkdir(parentDir, { recursive: true });
        await fs.writeFile(validatedPath, content, 'utf-8');
        return {
          content: [{ type: 'text', text: `Successfully wrote to ${filePath}` }],
        };
      }

      default:
        throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
    }
  } catch (error) {
    if (error instanceof z.ZodError) {
      throw new McpError(
        ErrorCode.InvalidParams,
        `Invalid parameters: ${error.errors.map(e => e.message).join(', ')}`
      );
    }
    throw error;
  }
});

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('MCP Filesystem Server running on stdio');
}

main().catch((error) => {
  console.error('Fatal error:', error);
  process.exit(1);
});
Enter fullscreen mode Exit fullscreen mode

Testing Your MCP Server

Step 1: Build the Project

npm run build
Enter fullscreen mode Exit fullscreen mode

Step 2: Test with Environment Variables

export ALLOWED_DIRS="/home/user/projects,/tmp"
export MAX_FILE_SIZE=5242880  # 5MB
npm start
Enter fullscreen mode Exit fullscreen mode

Step 3: Test with the MCP Inspector

npx @anthropics/mcp-inspector node dist/index.js
Enter fullscreen mode Exit fullscreen mode

Integrating with Claude Desktop

Add to your Claude Desktop configuration:

{
  "mcpServers": {
    "filesystem": {
      "command": "node",
      "args": ["/path/to/mcp-filesystem-server/dist/index.js"],
      "env": {
        "ALLOWED_DIRS": "/Users/yourname/Documents",
        "MAX_FILE_SIZE": "10485760"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Restart Claude Desktop after updating the config.


What's Next?

Now that you've built a filesystem MCP server, try:

  1. Database MCP Server: Connect to PostgreSQL or MongoDB
  2. API Integration Server: Wrap REST APIs with MCP
  3. GitHub MCP Server: Enable AI to manage issues and PRs
  4. Slack/Discord Server: Send messages and manage channels

Want to Build AI Agents Faster?

If you're excited about MCP and want to build AI agents without the boilerplate, check out my AI Automation Starter Kit. It includes:

  • ✅ 4 pre-built autonomous agents
  • ✅ Shared utilities and memory systems
  • ✅ Deployment scripts and monitoring
  • ✅ Complete setup guides

Only $9Get the AI Automation Starter Kit


Resources


Built by QuantBitRealm. Need custom AI development? Hire us or check out our GitHub.

Star this tutorial if it helped you!

Top comments (0)