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:
- Tools: Functions the AI can call with specific parameters
- Resources: Data sources the AI can read from
- 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
- Connection pooling: Reuse database and HTTP connections
- Caching: Cache frequently accessed data
- Async processing: Use async/await for all I/O operations
- Timeouts: Set appropriate timeouts for external calls
Reliability
- Health checks: Implement /health endpoints
- Graceful shutdown: Handle SIGTERM properly
- Circuit breakers: Prevent cascade failures
- Retries: Implement retry logic with exponential backoff
Maintainability
- Documentation: Document all tools, resources, and prompts
- Versioning: Use semantic versioning for your server
- Monitoring: Track metrics, errors, and performance
- 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.
More Articles
The Ultimate OpenClaw AWS Setup Guide

The definitive guide to setting up OpenClaw on AWS. Includes spot instance configuration, cost optimization, and step-by-step instructions.
Building AI Workflows with Tool Chaining in OpenClaw
Master the art of chaining tools and function calls to build powerful multi-step AI automation workflows—from data extraction to content generation and deployment.
Cost Optimization Guide for Self-Hosted AI Assistants: Run Claude on a Budget
Practical strategies to reduce API costs for self-hosted AI assistants—smart model routing, caching, batching, and OpenClaw-specific optimizations to run Claude affordably.