Building Custom MCP Integrations
Learn how to create custom MCP servers to extend ClaudeCode with your own tools, APIs, and services. Complete guide from setup to deployment.
Model Context Protocol (MCP) is ClaudeCode's superpower - it lets you connect Claude to virtually any external tool, service, or data source. In this guide, you'll learn how to build your own MCP servers to extend ClaudeCode's capabilities.
What You'll Build
By the end of this guide, you'll have created a custom MCP server that:
- Exposes custom tools to ClaudeCode
- Provides access to external data sources
- Implements proper security and authentication
- Can be deployed and shared with your team
Prerequisites
Before starting, ensure you have:
- Node.js 18+ or Python 3.10+
- ClaudeCode installed and configured
- Basic understanding of TypeScript/Python
- API keys for any services you want to integrate
Understanding MCP Architecture
Core Concepts
MCP uses a client-server architecture:
- MCP Client: ClaudeCode acts as the client
- MCP Server: Your custom integration that exposes capabilities
- Transport: Communication layer (stdio, HTTP+SSE, or custom)
Server Capabilities
An MCP server can provide:
- Tools: Functions Claude can call to perform actions
- Resources: Data sources Claude can read from
- Prompts: Reusable prompt templates
- Sampling: Custom AI model integrations
Setting Up Your Development Environment
Option 1: TypeScript/Node.js
Install the MCP SDK:
npm install @anthropic/mcp-sdk
# or
yarn add @anthropic/mcp-sdk
Create a new project:
mkdir my-mcp-server
cd my-mcp-server
npm init -y
npm install @anthropic/mcp-sdk zod
Option 2: Python
Install the Python SDK:
pip install anthropic-mcp
Create your project:
mkdir my-mcp-server
cd my-mcp-server
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install anthropic-mcp pydantic
Building Your First MCP Server
Let's create a custom MCP server that integrates with a REST API.
Example: Weather API Integration
Here's a complete MCP server that provides weather data:
TypeScript Implementation:
import { Server } from '@anthropic/mcp-sdk/server/index.js';
import { StdioServerTransport } from '@anthropic/mcp-sdk/server/stdio.js';
import { z } from 'zod';
// Initialize MCP server
const server = new Server({
name: 'weather-server',
version: '1.0.0',
});
// Define tool schema
const GetWeatherSchema = z.object({
city: z.string().describe('The city to get weather for'),
units: z.enum(['metric', 'imperial']).optional(),
});
// Register weather tool
server.tool(
'get_weather',
'Get current weather for a city',
GetWeatherSchema,
async ({ city, units = 'metric' }) => {
try {
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${city}&units=${units}`
);
if (!response.ok) {
throw new Error(`Weather API error: ${response.statusText}`);
}
const data = await response.json();
return {
content: [
{
type: 'text',
text: JSON.stringify({
location: data.location.name,
temperature: data.current.temp_c,
condition: data.current.condition.text,
humidity: data.current.humidity,
wind_speed: data.current.wind_kph,
}, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Error fetching weather: ${error.message}`,
},
],
isError: true,
};
}
}
);
// Register forecast tool
server.tool(
'get_forecast',
'Get weather forecast for the next 3 days',
GetWeatherSchema,
async ({ city }) => {
// Implementation similar to get_weather
// ...
}
);
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Weather MCP server running on stdio');
}
main().catch(console.error);
Python Implementation:
from anthropic_mcp import MCPServer, Tool
from anthropic_mcp.transport import StdioTransport
from pydantic import BaseModel, Field
import os
import requests
import json
# Initialize server
server = MCPServer(
name="weather-server",
version="1.0.0"
)
# Define input schema
class WeatherInput(BaseModel):
city: str = Field(description="The city to get weather for")
units: str = Field(default="metric", description="Temperature units")
# Define weather tool
@server.tool("get_weather", "Get current weather for a city")
def get_weather(input: WeatherInput) -> dict:
try:
api_key = os.getenv("WEATHER_API_KEY")
url = f"https://api.weatherapi.com/v1/current.json"
params = {
"key": api_key,
"q": input.city,
"units": input.units
}
response = requests.get(url, params=params)
response.raise_for_status()
data = response.json()
return {
"content": [{
"type": "text",
"text": json.dumps({
"location": data["location"]["name"],
"temperature": data["current"]["temp_c"],
"condition": data["current"]["condition"]["text"],
"humidity": data["current"]["humidity"],
"wind_speed": data["current"]["wind_kph"]
}, indent=2)
}]
}
except Exception as e:
return {
"content": [{
"type": "text",
"text": f"Error fetching weather: {str(e)}"
}],
"isError": True
}
# Start server
if __name__ == "__main__":
transport = StdioTransport()
server.run(transport)
Configuring ClaudeCode to Use Your Server
Add your MCP server to ClaudeCode's configuration:
~/.claudecode/config.json:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/path/to/your/weather-server/index.js"],
"env": {
"WEATHER_API_KEY": "your-api-key-here"
}
}
}
}
For Python servers:
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/path/to/your/weather-server/main.py"],
"env": {
"WEATHER_API_KEY": "your-api-key-here"
}
}
}
}
Testing Your MCP Server
1. Verify Server Startup
Run your server directly to check for errors:
# TypeScript
node index.js
# Python
python main.py
2. Test with ClaudeCode
Start ClaudeCode and try using your tool:
claudecode
Then in the ClaudeCode session:
You: What's the weather like in London?
Claude should automatically use your get_weather tool!
3. Debug Issues
Enable MCP debugging:
{
"mcp": {
"debug": true,
"logLevel": "debug"
}
}
Advanced Features
1. Adding Resources
Resources let Claude read data from your server:
server.resource(
'config://settings',
'Application configuration settings',
async () => {
return {
content: [
{
type: 'text',
text: JSON.stringify(await loadConfig()),
},
],
};
}
);
2. Implementing Authentication
Secure your MCP server:
server.use(async (context, next) => {
const apiKey = context.headers['x-api-key'];
if (!apiKey || apiKey !== process.env.EXPECTED_API_KEY) {
throw new Error('Unauthorized');
}
await next();
});
3. Rate Limiting
Protect your server from excessive requests:
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
});
server.use(limiter);
4. Caching Responses
Improve performance with caching:
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 600 }); // 10-minute cache
server.tool('get_weather', ..., async ({ city }) => {
const cacheKey = `weather:${city}`;
const cached = cache.get(cacheKey);
if (cached) {
return cached;
}
const result = await fetchWeather(city);
cache.set(cacheKey, result);
return result;
});
Real-World Integration Examples
1. Database Integration
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_KEY
);
server.tool(
'query_database',
'Execute SQL query on database',
QuerySchema,
async ({ query }) => {
const { data, error } = await supabase.rpc('execute_query', { query });
if (error) throw error;
return {
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
};
}
);
2. Slack Integration
import { WebClient } from '@slack/web-api';
const slack = new WebClient(process.env.SLACK_TOKEN);
server.tool(
'send_slack_message',
'Send a message to a Slack channel',
SlackMessageSchema,
async ({ channel, text }) => {
await slack.chat.postMessage({
channel,
text,
});
return {
content: [{ type: 'text', text: 'Message sent successfully!' }],
};
}
);
3. AWS Integration
import { S3Client, ListBucketsCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: 'us-east-1' });
server.tool(
'list_s3_buckets',
'List all S3 buckets',
z.object({}),
async () => {
const command = new ListBucketsCommand({});
const response = await s3.send(command);
return {
content: [{
type: 'text',
text: JSON.stringify(response.Buckets, null, 2)
}],
};
}
);
Best Practices
1. Error Handling
Always handle errors gracefully:
try {
// Your logic
} catch (error) {
return {
content: [{
type: 'text',
text: `Error: ${error.message}`
}],
isError: true
};
}
2. Input Validation
Use Zod or Pydantic for schema validation:
const schema = z.object({
email: z.string().email(),
age: z.number().min(0).max(150),
});
3. Logging
Implement comprehensive logging:
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'mcp-server.log' }),
],
});
server.tool('my_tool', ..., async (input) => {
logger.info('Tool called', { tool: 'my_tool', input });
// ...
});
4. Documentation
Document your tools with clear descriptions:
server.tool(
'complex_operation',
`Performs a complex operation on the data.
Parameters:
- data: The input data (JSON object)
- operation: One of 'transform', 'validate', or 'analyze'
Returns:
- Processed data with operation results`,
schema,
handler
);
Deployment
Packaging Your Server
Create a package.json with proper configuration:
{
"name": "my-mcp-server",
"version": "1.0.0",
"main": "dist/index.js",
"bin": {
"my-mcp-server": "dist/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Publishing to npm
npm login
npm publish
Users can then install with:
npm install -g my-mcp-server
Docker Deployment
Create a Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
CMD ["node", "index.js"]
Troubleshooting
Common Issues
Server not starting:
- Check your transport configuration
- Verify all dependencies are installed
- Review error logs
Tools not appearing in ClaudeCode:
- Ensure server is properly registered in config
- Check for syntax errors in tool definitions
- Verify server is running
Authentication failures:
- Confirm environment variables are set
- Check API keys are valid
- Review authentication middleware
Next Steps
Now that you've built your first MCP server, explore:
- MCP Protocol Specification - Deep dive into the protocol
- Pre-built Servers - Learn from existing implementations
- Enterprise Deployment - Scale your MCP infrastructure
- Security Best Practices - Secure your integrations
Community Examples
Check out these community-built MCP servers:
- GitHub Server: Manage issues, PRs, and repositories
- PostgreSQL Server: Query and manage databases
- Jira Server: Create and update tickets
- Kubernetes Server: Manage clusters and deployments
Conclusion
MCP opens up limitless possibilities for extending ClaudeCode. Start small with simple integrations, then gradually build more sophisticated servers as you learn. The key is providing Claude with the context and tools it needs to help you work more effectively.
Happy building!
Have questions or built something cool? Share your MCP servers with the community on GitHub Discussions.