Clawist
📖 Guide5 min read••By Claw Team

Building MCP Servers for Claude Integration: A Developer's Guide

Building MCP Servers for Claude Integration: A Developer's Guide

The Model Context Protocol (MCP) represents a revolutionary approach to extending AI assistant capabilities. By building MCP servers, developers can give Claude access to custom tools, databases, APIs, and services that dramatically expand what the assistant can accomplish.

This comprehensive guide walks you through everything needed to build production-ready MCP servers, from basic concepts to advanced patterns and deployment strategies.

Understanding the Model Context Protocol

MCP is an open standard developed by Anthropic that defines how AI assistants communicate with external tools and services. Unlike simple function calling, MCP provides a structured, secure, and scalable way to extend AI capabilities.

Why MCP Matters

Traditional AI tool integration often involves ad-hoc implementations that are difficult to maintain, secure, and scale. MCP addresses these challenges by providing:

Standardized Communication: All MCP servers follow the same protocol, making it easy to mix and match tools from different sources.

Security by Design: The protocol includes built-in support for authentication, authorization, and sandboxing.

Type Safety: Request and response schemas ensure data integrity between the AI and tools.

Discovery: AI assistants can query MCP servers to discover available tools dynamically.

Protocol Architecture

MCP uses a client-server model where:

  • The MCP Client (Claude/OpenClaw) initiates requests to tools
  • The MCP Server (your code) handles requests and returns results
  • Communication happens over JSON-RPC 2.0 with optional transport layers

The protocol supports three main capabilities:

  1. Tools: Functions the AI can call with specific parameters
  2. Resources: Data sources the AI can read from
  3. Prompts: Templates that guide AI behavior

Setting Up Your Development Environment

Before building MCP servers, you'll need a proper development environment.

Prerequisites

Ensure you have the following installed:

  • Node.js 18+ or Python 3.10+
  • A code editor with TypeScript/Python support
  • Git for version control
  • Docker (optional, for containerized deployment)

Installing the MCP SDK

For TypeScript/JavaScript development:

mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @anthropic-ai/mcp-sdk
npm install -D typescript @types/node ts-node

Create a tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  }
}

For Python development:

mkdir my-mcp-server
cd my-mcp-server
python -m venv venv
source venv/bin/activate
pip install anthropic-mcp

Building Your First MCP Server

Let's create a simple MCP server that provides a calculator tool.

Basic Server Structure

// src/index.ts
import { McpServer, Tool, ToolResult } from '@anthropic-ai/mcp-sdk';

const server = new McpServer({
  name: 'calculator-server',
  version: '1.0.0',
  description: 'A simple calculator MCP server'
});

// Define the calculator tool
const calculatorTool: Tool = {
  name: 'calculate',
  description: 'Performs mathematical calculations',
  inputSchema: {
    type: 'object',
    properties: {
      expression: {
        type: 'string',
        description: 'The mathematical expression to evaluate'
      }
    },
    required: ['expression']
  }
};

// Register the tool
server.registerTool(calculatorTool, async (params): Promise<ToolResult> => {
  try {
    const { expression } = params;
    
    // Safe evaluation using a math parser (not eval!)
    const result = evaluateMathExpression(expression);
    
    return {
      success: true,
      result: {
        expression,
        result,
        formatted: `${expression} = ${result}`
      }
    };
  } catch (error) {
    return {
      success: false,
      error: `Failed to evaluate expression: ${error.message}`
    };
  }
});

// Start the server
server.listen(3000);
console.log('Calculator MCP server running on port 3000');

Safe Math Evaluation

Never use eval() for math expressions. Instead, use a proper math parser:

import { evaluate } from 'mathjs';

function evaluateMathExpression(expression: string): number {
  // Sanitize input
  const sanitized = expression.replace(/[^0-9+\-*/().^%\s]/g, '');
  
  if (sanitized !== expression) {
    throw new Error('Expression contains invalid characters');
  }
  
  return evaluate(sanitized);
}

Running and Testing

Start your server:

npx ts-node src/index.ts

Test with a curl request:

curl -X POST http://localhost:3000/rpc \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "calculate",
      "arguments": {
        "expression": "2 + 2 * 3"
      }
    },
    "id": 1
  }'

Creating Complex Tools

Real-world MCP servers often need to interact with external services, databases, and APIs.

Database Integration Tool

Let's build a tool that queries a PostgreSQL database:

import { McpServer, Tool, ToolResult } from '@anthropic-ai/mcp-sdk';
import { Pool } from 'pg';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL
});

const server = new McpServer({
  name: 'database-server',
  version: '1.0.0'
});

const queryTool: Tool = {
  name: 'query_customers',
  description: 'Searches the customer database',
  inputSchema: {
    type: 'object',
    properties: {
      search_term: {
        type: 'string',
        description: 'Name or email to search for'
      },
      limit: {
        type: 'integer',
        description: 'Maximum results to return',
        default: 10
      }
    },
    required: ['search_term']
  }
};

server.registerTool(queryTool, async (params): Promise<ToolResult> => {
  const { search_term, limit = 10 } = params;
  
  try {
    const result = await pool.query(
      `SELECT id, name, email, created_at 
       FROM customers 
       WHERE name ILIKE $1 OR email ILIKE $1 
       LIMIT $2`,
      [`%${search_term}%`, limit]
    );
    
    return {
      success: true,
      result: {
        count: result.rows.length,
        customers: result.rows
      }
    };
  } catch (error) {
    return {
      success: false,
      error: `Database query failed: ${error.message}`
    };
  }
});

External API Integration

Here's a tool that fetches weather data:

const weatherTool: Tool = {
  name: 'get_weather',
  description: 'Gets current weather for a location',
  inputSchema: {
    type: 'object',
    properties: {
      city: {
        type: 'string',
        description: 'City name'
      },
      country: {
        type: 'string',
        description: 'Country code (e.g., US, UK)',
        default: 'US'
      }
    },
    required: ['city']
  }
};

server.registerTool(weatherTool, async (params): Promise<ToolResult> => {
  const { city, country = 'US' } = params;
  
  const apiKey = process.env.WEATHER_API_KEY;
  const url = `https://api.openweathermap.org/data/2.5/weather?q=${city},${country}&appid=${apiKey}&units=metric`;
  
  try {
    const response = await fetch(url);
    const data = await response.json();
    
    if (data.cod !== 200) {
      return {
        success: false,
        error: `Weather API error: ${data.message}`
      };
    }
    
    return {
      success: true,
      result: {
        city: data.name,
        country: data.sys.country,
        temperature: data.main.temp,
        feels_like: data.main.feels_like,
        humidity: data.main.humidity,
        description: data.weather[0].description,
        wind_speed: data.wind.speed
      }
    };
  } catch (error) {
    return {
      success: false,
      error: `Failed to fetch weather: ${error.message}`
    };
  }
});

Implementing Resources

Resources allow Claude to read data from your MCP server. Unlike tools, resources are read-only and designed for providing context.

Static Resources

import { Resource, ResourceContent } from '@anthropic-ai/mcp-sdk';

const documentationResource: Resource = {
  uri: 'docs://api-reference',
  name: 'API Reference',
  description: 'Documentation for our REST API',
  mimeType: 'text/markdown'
};

server.registerResource(documentationResource, async (): Promise<ResourceContent> => {
  const docs = await fs.readFile('./docs/api-reference.md', 'utf-8');
  
  return {
    uri: 'docs://api-reference',
    mimeType: 'text/markdown',
    text: docs
  };
});

Dynamic Resources

Resources can also be dynamic, generating content based on parameters:

const customerResource: Resource = {
  uri: 'customers://{id}',
  name: 'Customer Details',
  description: 'Detailed information about a specific customer',
  mimeType: 'application/json'
};

server.registerResource(customerResource, async (uri): Promise<ResourceContent> => {
  const customerId = uri.split('://')[1];
  
  const customer = await pool.query(
    'SELECT * FROM customers WHERE id = $1',
    [customerId]
  );
  
  if (customer.rows.length === 0) {
    throw new Error(`Customer ${customerId} not found`);
  }
  
  return {
    uri,
    mimeType: 'application/json',
    text: JSON.stringify(customer.rows[0], null, 2)
  };
});

Creating Prompts

Prompts are templates that guide Claude's behavior for specific tasks.

import { Prompt, PromptMessage } from '@anthropic-ai/mcp-sdk';

const customerServicePrompt: Prompt = {
  name: 'customer_service',
  description: 'Prompt for handling customer service inquiries',
  arguments: [
    {
      name: 'customer_name',
      description: 'The customer\'s name',
      required: true
    },
    {
      name: 'issue_type',
      description: 'Type of issue (billing, technical, general)',
      required: true
    }
  ]
};

server.registerPrompt(customerServicePrompt, async (args): Promise<PromptMessage[]> => {
  return [
    {
      role: 'system',
      content: `You are a helpful customer service representative. 
                You're speaking with ${args.customer_name} about a ${args.issue_type} issue.
                Be empathetic, professional, and solution-focused.
                Always verify customer information before making changes.`
    },
    {
      role: 'user',
      content: `Customer ${args.customer_name} has contacted us regarding ${args.issue_type}. 
                Please help them with their inquiry.`
    }
  ];
});

Authentication and Security

Securing your MCP server is crucial for production deployments.

API Key Authentication

import { McpServer, AuthConfig } from '@anthropic-ai/mcp-sdk';

const authConfig: AuthConfig = {
  type: 'api_key',
  validateKey: async (key: string) => {
    // Check against your database or secret store
    const validKeys = await getValidApiKeys();
    return validKeys.includes(key);
  },
  headerName: 'X-API-Key'
};

const server = new McpServer({
  name: 'secure-server',
  version: '1.0.0',
  auth: authConfig
});

OAuth 2.0 Integration

For more sophisticated authentication:

const oauthConfig: AuthConfig = {
  type: 'oauth2',
  tokenUrl: 'https://auth.example.com/token',
  introspectionUrl: 'https://auth.example.com/introspect',
  clientId: process.env.OAUTH_CLIENT_ID,
  clientSecret: process.env.OAUTH_CLIENT_SECRET,
  scopes: ['mcp:read', 'mcp:write']
};

Input Validation

Always validate and sanitize inputs:

import { z } from 'zod';

const QuerySchema = z.object({
  search_term: z.string().min(1).max(100),
  limit: z.number().int().min(1).max(100).default(10)
});

server.registerTool(queryTool, async (params): Promise<ToolResult> => {
  const validated = QuerySchema.safeParse(params);
  
  if (!validated.success) {
    return {
      success: false,
      error: `Invalid parameters: ${validated.error.message}`
    };
  }
  
  const { search_term, limit } = validated.data;
  // ... rest of the handler
});

Rate Limiting

Protect your server from abuse:

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100, // 100 requests per minute
  message: 'Too many requests, please try again later'
});

server.use(limiter);

Error Handling and Logging

Robust error handling makes debugging easier and improves reliability.

Structured Error Responses

interface McpError {
  code: number;
  message: string;
  data?: Record<string, unknown>;
}

function createError(code: number, message: string, data?: Record<string, unknown>): McpError {
  return { code, message, data };
}

// Standard error codes
const ErrorCodes = {
  INVALID_PARAMS: -32602,
  INTERNAL_ERROR: -32603,
  TOOL_NOT_FOUND: -32601,
  UNAUTHORIZED: -32000,
  RATE_LIMITED: -32001
};

Comprehensive Logging

import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

server.on('request', (request) => {
  logger.info('Incoming request', {
    method: request.method,
    params: request.params,
    requestId: request.id
  });
});

server.on('response', (response, request) => {
  logger.info('Outgoing response', {
    requestId: request.id,
    success: !response.error,
    duration: response.duration
  });
});

Testing MCP Servers

Thorough testing ensures reliability.

Unit Tests

import { describe, it, expect, beforeEach } from 'vitest';
import { McpServer } from '@anthropic-ai/mcp-sdk';

describe('Calculator MCP Server', () => {
  let server: McpServer;
  
  beforeEach(() => {
    server = createCalculatorServer();
  });
  
  it('should evaluate simple expressions', async () => {
    const result = await server.callTool('calculate', {
      expression: '2 + 2'
    });
    
    expect(result.success).toBe(true);
    expect(result.result.result).toBe(4);
  });
  
  it('should handle complex expressions', async () => {
    const result = await server.callTool('calculate', {
      expression: '(10 + 5) * 2 / 3'
    });
    
    expect(result.success).toBe(true);
    expect(result.result.result).toBeCloseTo(10);
  });
  
  it('should reject invalid expressions', async () => {
    const result = await server.callTool('calculate', {
      expression: 'DROP TABLE users;'
    });
    
    expect(result.success).toBe(false);
    expect(result.error).toContain('invalid characters');
  });
});

Integration Tests

describe('Database Tool Integration', () => {
  it('should query customers successfully', async () => {
    // Set up test data
    await pool.query(
      'INSERT INTO customers (name, email) VALUES ($1, $2)',
      ['Test User', 'test@example.com']
    );
    
    const result = await server.callTool('query_customers', {
      search_term: 'Test'
    });
    
    expect(result.success).toBe(true);
    expect(result.result.customers).toHaveLength(1);
    expect(result.result.customers[0].name).toBe('Test User');
  });
});

Deployment Strategies

Deploy your MCP server for production use.

Docker Deployment

FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY dist ./dist

ENV NODE_ENV=production
EXPOSE 3000

CMD ["node", "dist/index.js"]
# docker-compose.yml
version: '3.8'
services:
  mcp-server:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - WEATHER_API_KEY=${WEATHER_API_KEY}
    restart: unless-stopped

Kubernetes Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mcp-server
  template:
    metadata:
      labels:
        app: mcp-server
    spec:
      containers:
      - name: mcp-server
        image: your-registry/mcp-server:latest
        ports:
        - containerPort: 3000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: mcp-secrets
              key: database-url
        resources:
          limits:
            memory: "256Mi"
            cpu: "500m"

Registering with OpenClaw

Once deployed, register your MCP server with OpenClaw:

// In your OpenClaw configuration
{
  "mcp_servers": [
    {
      "name": "my-custom-tools",
      "url": "https://mcp.yourcompany.com",
      "auth": {
        "type": "api_key",
        "key": "${MCP_API_KEY}"
      }
    }
  ]
}

Best Practices

Follow these guidelines for production-quality MCP servers:

Performance Optimization

  1. Connection pooling: Reuse database and HTTP connections
  2. Caching: Cache frequently accessed data
  3. Async processing: Use async/await for all I/O operations
  4. Timeouts: Set appropriate timeouts for external calls

Reliability

  1. Health checks: Implement /health endpoints
  2. Graceful shutdown: Handle SIGTERM properly
  3. Circuit breakers: Prevent cascade failures
  4. Retries: Implement retry logic with exponential backoff

Maintainability

  1. Documentation: Document all tools, resources, and prompts
  2. Versioning: Use semantic versioning for your server
  3. Monitoring: Track metrics, errors, and performance
  4. Logging: Log all requests and responses

Conclusion

Building MCP servers opens up unlimited possibilities for extending Claude's capabilities. From database queries to external API integrations, MCP provides a secure and standardized way to connect AI assistants with your systems.

Key takeaways from this guide:

  • MCP uses JSON-RPC 2.0 for standardized communication
  • Tools, Resources, and Prompts are the three main capabilities
  • Security should be built in from the start
  • Thorough testing and monitoring are essential for production

Start building your own MCP servers today and unlock the full potential of AI-assisted automation. The possibilities are truly endless when you can give Claude direct access to your systems and data.