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.

ClaudeCode Guide Team
advancedmcpintegrationextension

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:

  1. Tools: Functions Claude can call to perform actions
  2. Resources: Data sources Claude can read from
  3. Prompts: Reusable prompt templates
  4. 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:

  1. MCP Protocol Specification - Deep dive into the protocol
  2. Pre-built Servers - Learn from existing implementations
  3. Enterprise Deployment - Scale your MCP infrastructure
  4. 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.