Skip to main content

Building Custom MCP Servers

Custom MCP servers allow you to extend the Model Context Protocol with integrations to any external service, API, or system. While the MCP catalog provides many pre-built servers, building a custom server gives you complete control over functionality specific to your needs.

When to Build a Custom MCP Server

Build a custom MCP server when:
  • The service you need isn’t available in the catalog
  • You need specialized functionality or custom business logic
  • You want to integrate internal APIs or proprietary systems
  • You require fine-grained control over authentication, rate limiting, or data access

Development with Claude MCP Builder Skill

The fastest and most reliable way to build MCP servers is using Claude with the MCP Builder skill. This skill provides:
  • Step-by-step guidance for both Python and TypeScript implementations
  • Best practice patterns and conventions
  • Input validation templates
  • Error handling patterns
  • Complete working examples
We strongly recommend using this skill when building custom MCP servers to ensure you follow current best practices and avoid common pitfalls.

Best Practices

Naming Conventions

Follow these standardized naming patterns: Python servers: Use format {service}_mcp (lowercase with underscores)
  • Examples: slack_mcp, github_mcp, stripe_mcp
TypeScript servers: Use format {service}-mcp-server (lowercase with hyphens)
  • Examples: slack-mcp-server, github-mcp-server, stripe-mcp-server

Tool Naming

Use snake_case with service prefix:
  • Format: {service}_{action}_{resource}
  • Examples: slack_send_message, github_create_issue, stripe_create_payment

Input Validation

Always validate inputs with type-safe schemas:
  • Python: Use Pydantic models with descriptive Field definitions
  • TypeScript: Use Zod schemas with detailed descriptions
This ensures:
  • Type safety at runtime
  • Clear parameter documentation
  • Automatic schema generation
  • Prevention of invalid inputs

Tool Annotations

Every tool should include these annotations for optimal LLM interaction:
annotations={
    "title": "Human-Readable Tool Title",
    "readOnlyHint": True,      # Tool only reads data
    "destructiveHint": False,  # Tool doesn't delete/modify critical data
    "idempotentHint": True,    # Same call produces same result
    "openWorldHint": False     # Doesn't interact with external entities
}

Error Handling

Provide clear, actionable error messages: Good: "Error: API authentication failed. Please check that SLACK_API_TOKEN environment variable is set correctly." Bad: "Error 401" Include:
  • What went wrong
  • Why it happened
  • How to fix it
  • Relevant context (status codes, failed parameters)

Security

  • Store API keys in environment variables, never in code
  • Validate and sanitize all inputs (especially file paths, URLs, system commands)
  • Implement rate limiting for resource-intensive operations
  • Use appropriate authorization checks
  • Never expose internal errors or stack traces to clients

Documentation

  • Write comprehensive tool descriptions (they become the description field)
  • Include expected behavior, return values, and error conditions
  • Document all parameters with examples where helpful
  • Explain any required environment variables or configuration

Quick Start

1. Use Claude with MCP Builder Skill

The recommended approach is to use Claude Desktop with the MCP Builder skill enabled:
  1. Open Claude Desktop
  2. Start a conversation about building an MCP server
  3. Claude will guide you through the entire process using best practices

2. Choose Your Language

  • Python: Use FastMCP (simpler, faster development)
  • TypeScript: Use MCP SDK (more control, better for Node.js ecosystems)

3. Essential Dependencies

Python:
pip install mcp[cli]
# or with uv:
uv pip install mcp[cli]
TypeScript:
npm install @modelcontextprotocol/sdk
# or with pnpm:
pnpm add @modelcontextprotocol/sdk

Implementation Guides

  • Python
  • TypeScript

Python MCP Server with FastMCP

FastMCP provides a simple, decorator-based API for building MCP servers in Python.

Complete Working Example

This example shows a weather MCP server with proper validation, error handling, and annotations:
#!/usr/bin/env python3
"""
Weather MCP Server
Provides tools to fetch weather data from a weather API.
"""

from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
from typing import Optional
import httpx
import os
import json

# Initialize the MCP server
mcp = FastMCP("weather_mcp")

# Define input validation model
class WeatherQuery(BaseModel):
    """Input parameters for weather lookup."""

    location: str = Field(
        description="City name or location to get weather for (e.g., 'London', 'New York')",
        min_length=1,
        max_length=100
    )
    units: Optional[str] = Field(
        default="metric",
        description="Temperature units: 'metric' (Celsius), 'imperial' (Fahrenheit), or 'kelvin'",
        pattern="^(metric|imperial|kelvin)$"
    )

@mcp.tool(
    name="weather_get_current",
    annotations={
        "title": "Get Current Weather",
        "readOnlyHint": True,
        "destructiveHint": False,
        "idempotentHint": True,
        "openWorldHint": True
    }
)
async def get_current_weather(params: WeatherQuery) -> str:
    """Get current weather conditions for a location.

    Returns current temperature, conditions, humidity, and wind speed.
    Requires WEATHER_API_KEY environment variable to be set.

    Example response:
        Location: London, UK
        Temperature: 15°C
        Conditions: Partly cloudy
        Humidity: 72%
        Wind: 12 km/h NW

    Errors:
        - Returns "Error: Missing API key" if WEATHER_API_KEY not set
        - Returns "Error: Location not found" if location is invalid
        - Returns "Error: API request failed" for network/API errors
    """

    # Validate environment
    api_key = os.getenv("WEATHER_API_KEY")
    if not api_key:
        return "Error: Missing API key. Please set WEATHER_API_KEY environment variable."

    try:
        # Make API request
        async with httpx.AsyncClient() as client:
            response = await client.get(
                "https://api.weatherapi.com/v1/current.json",
                params={
                    "key": api_key,
                    "q": params.location,
                    "aqi": "no"
                },
                timeout=10.0
            )

            # Handle different status codes
            if response.status_code == 401:
                return "Error: Invalid API key. Please check your WEATHER_API_KEY."
            elif response.status_code == 400:
                return f"Error: Location '{params.location}' not found. Please check the spelling."
            elif response.status_code != 200:
                return f"Error: API request failed with status {response.status_code}"

            # Parse and format response
            data = response.json()
            location = data["location"]
            current = data["current"]

            # Convert temperature if needed
            temp = current["temp_c"]
            unit = "°C"
            if params.units == "imperial":
                temp = current["temp_f"]
                unit = "°F"
            elif params.units == "kelvin":
                temp = current["temp_c"] + 273.15
                unit = "K"

            # Format output
            result = f"""Location: {location['name']}, {location['country']}
Temperature: {temp}{unit}
Conditions: {current['condition']['text']}
Humidity: {current['humidity']}%
Wind: {current['wind_kph']} km/h {current['wind_dir']}"""

            return result

    except httpx.TimeoutException:
        return "Error: Request timed out. The weather API is not responding."
    except httpx.RequestError as e:
        return f"Error: Network error occurred: {str(e)}"
    except KeyError as e:
        return f"Error: Unexpected API response format (missing field: {e})"
    except Exception as e:
        return f"Error: An unexpected error occurred: {str(e)}"

# Run the server
if __name__ == "__main__":
    mcp.run()

Key Features

  1. FastMCP initialization: mcp = FastMCP("weather_mcp")
  2. Pydantic validation: Type-safe input with constraints and descriptions
  3. Tool decorator: Register tools with @mcp.tool() and annotations
  4. Async support: Use async def for I/O operations
  5. Error handling: Comprehensive error cases with actionable messages
  6. Documentation: Docstring becomes tool description

Project Structure

weather-mcp/
├── weather_mcp.py          # Main server file
├── pyproject.toml          # Dependencies
└── README.md               # Usage instructions

Running the Server

# Set environment variable
export WEATHER_API_KEY="your_api_key"

# Run directly
python weather_mcp.py

# Or use MCP CLI
mcp dev weather_mcp.py

Testing Your Server

Manual Testing with MCP Inspector

Use the MCP Inspector to test your server interactively:
npx @modelcontextprotocol/inspector node path/to/your/server.js

Testing with Claude Desktop

Add your server to Claude Desktop’s configuration: macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "weather": {
      "command": "python",
      "args": ["/path/to/weather_mcp.py"],
      "env": {
        "WEATHER_API_KEY": "your_api_key"
      }
    }
  }
}

Additional Resources


Next Steps

After building your custom MCP server:
  1. Test thoroughly with various inputs and error cases
  2. Add comprehensive documentation
  3. Consider publishing to the MCP Catalog for community use
  4. Monitor usage and iterate based on feedback
  5. Keep up with MCP specification updates
For deploying custom MCP servers in production on the Runlayer platform, see Platform MCPs.