Advanced

Building a Custom MCP Server

When no existing MCP server covers your integration — an internal API, a proprietary database, a custom workflow — you build your own. The MCP SDK handles the protocol plumbing; you write the tool definitions and handler functions.

SDK Setup

Anthropic provides first-party SDKs for Python and TypeScript. For a Python server:

pip install mcp

For a TypeScript server:

npm install @modelcontextprotocol/sdk

Defining a Tool: Python Example

A complete minimal MCP server in Python that exposes one tool:

from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types

# Create the server instance
server = Server("my-custom-server")

# Register a tool
@server.list_tools()
async def list_tools() -> list[types.Tool]:
    return [
        types.Tool(
            name="get_customer",
            description="Look up a customer record by customer ID. Returns name, email, and account status.",
            inputSchema={
                "type": "object",
                "properties": {
                    "customer_id": {
                        "type": "string",
                        "description": "The customer ID, e.g. 'CUST-12345'"
                    }
                },
                "required": ["customer_id"]
            }
        )
    ]

# Handle tool calls
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
    if name == "get_customer":
        customer_id = arguments["customer_id"]
        # Your actual implementation here
        customer = fetch_customer_from_db(customer_id)
        if not customer:
            return [types.TextContent(
                type="text",
                text=f"Error: Customer '{customer_id}' not found"
            )]
        return [types.TextContent(
            type="text",
            text=f"Name: {customer['name']}\nEmail: {customer['email']}\nStatus: {customer['status']}"
        )]
    raise ValueError(f"Unknown tool: {name}")

# Run the server using stdio transport
if __name__ == "__main__":
    import asyncio
    asyncio.run(stdio_server(server))

Tool Definition Details

The Tool object has three required fields — the same as function calling in the Claude API:

  • name: Unique identifier for the tool. Use snake_case. Claude uses this to call the tool.
  • description: What the tool does, what inputs it accepts, and what it returns. This is what Claude reads to decide when to call the tool — write it clearly.
  • inputSchema: JSON Schema defining the expected inputs. Marks required fields. Claude uses this to construct valid tool calls.

Exposing Resources

Resources are read-only data sources (documents, config, reference data) that Claude can access without calling a tool:

@server.list_resources()
async def list_resources() -> list[types.Resource]:
    return [
        types.Resource(
            uri="config://app-settings",
            name="Application Settings",
            description="Current application configuration",
            mimeType="application/json"
        )
    ]

@server.read_resource()
async def read_resource(uri: str) -> str:
    if uri == "config://app-settings":
        return json.dumps(load_app_settings())
    raise ValueError(f"Unknown resource: {uri}")

Running and Testing

Test your server with Claude Code before deploying:

  1. Run the server manually to check for startup errors: python my_server.py
  2. Add it to .claude/settings.json with the correct command and args
  3. Start a Claude Code session and ask "what tools do you have?"
  4. Test the tool with a natural language request: "Look up customer CUST-12345"
// .claude/settings.json
{
  "mcpServers": {
    "my-custom-server": {
      "type": "stdio",
      "command": "python",
      "args": ["/path/to/my_server.py"]
    }
  }
}

Error Handling

Return descriptive error messages — Claude reads them and adapts its next action:

  • Return a text result with a clear error message rather than raising an unhandled exception
  • Include what went wrong and, if possible, what the caller should try instead
  • Unhandled exceptions cause the tool call to fail silently — the server logs the error but Claude receives no useful information

Checklist: Do You Understand This?

  • SDK: mcp (Python) or @modelcontextprotocol/sdk (TypeScript) handles protocol plumbing
  • Tool definition: name + description (most important for Claude) + inputSchema (JSON Schema)
  • Handler: @server.list_tools() returns tool definitions; @server.call_tool() executes them
  • Resources: optional read-only data sources; use for reference data Claude should be able to access
  • Test by adding to settings.json and asking Claude what tools it has — error messages are returned as text results, not exceptions

Page built: 01 Jun 2026