Build Your Own MCP Server in 30 Minutes: Connecting Claude Code to Any API

The Repo

Everything described here is in the open-source template at github.com/ivandir/mcp-api-builder. It's a Python MCP server you can clone and adapt — not a library, not a framework. The repo ships with a fully working example (a weather API integration), a code generator for teams that already have an OpenAPI spec, and copy-paste auth patterns. The goal is to get from zero to a registered MCP server in under 30 minutes.

The repo structure is intentionally flat:

src/server.py              MCP server entry — wire tool groups here
src/tools/weather.py       Working example wrapping Open-Meteo API
scripts/openapi_to_mcp.py  Generate stubs from OpenAPI 3.x spec
docs/auth-patterns.md      Copy-paste auth patterns

Everything domain-specific lives in src/tools/. The server file itself rarely needs to change once it's set up.

The TOOL_GROUPS Pattern

The most important design decision in the repo is the TOOL_GROUPS list in src/server.py. Instead of a monolithic dispatcher that knows about every tool, the server assembles its tool catalog and routing table from a list of groups — one group per API integration. Here's the full entry point:

TOOL_GROUPS = [
    {"tools": WEATHER_TOOLS, "handler": handle_weather_tool},
    # {"tools": MY_API_TOOLS, "handler": handle_my_api_tool},  ← add more here
]

ALL_TOOLS = [tool for group in TOOL_GROUPS for tool in group["tools"]]

app = Server(os.environ.get("MCP_SERVER_NAME", "mcp-api-builder"))

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [Tool(**t) for t in ALL_TOOLS]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> CallToolResult:
    for group in TOOL_GROUPS:
        if any(t["name"] == name for t in group["tools"]):
            result = await group["handler"](name, arguments)
            return [TextContent(**item) for item in result["content"]]
    raise ValueError(f"Unknown tool: {name}")

Adding a new API integration is a two-line change: import its module, add it to TOOL_GROUPS. The dispatch loop, tool catalog assembly, and stdio wiring don't need to be touched. The comment on the commented-out line is intentional — it's the actual pattern, not pseudocode. The file was written to be read and modified, not just executed.

The server name comes from the MCP_SERVER_NAME environment variable, defaulting to mcp-api-builder. This means the same codebase can be deployed as multiple named servers with different tool sets, controlled entirely through environment configuration.

How a Tool Is Defined

The WEATHER_TOOLS list in src/tools/weather.py shows the tool definition structure the server expects. Each tool is a dict with three keys:

{
    "name": "weather_get_current",
    "description": "Get the current weather conditions for a geographic location. Returns temperature, wind speed, and weather code.",
    "inputSchema": {
        "type": "object",
        "properties": {
            "latitude":  {"type": "number", "description": "Latitude of the location"},
            "longitude": {"type": "number", "description": "Longitude of the location"},
            "units":     {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature unit"}
        },
        "required": ["latitude", "longitude"]
    }
}

A few things worth noting about these choices. The description is written for the model, not for a human reading the source. "Get the current weather conditions for a geographic location" tells the model exactly when to use this tool and what it returns. A description like "calls Open-Meteo API" would be accurate but useless for tool selection.

The units parameter uses an enum because there are exactly two valid values. The model will never produce an invalid string for this field — the schema enforces it. The required array contains only the genuinely required fields; units is absent because the handler defaults to Celsius. Don't put optional parameters in required.

The weather module ships two tools: weather_get_current and weather_get_forecast. They share a single handle_weather_tool handler that dispatches on the tool name internally. This is the pattern the repo follows throughout: one async handler function per module, which keeps the TOOL_GROUPS list clean.

Response Shaping

The Open-Meteo API returns a dense JSON object with raw numeric codes, arrays indexed by hour, and fields the model has no use for. The handler strips it down before returning. For weather_get_current, the handler maps the raw response to:

{"time": "2026-03-13T14:00", "temperature": 12.4, "windSpeed": 18.2, "conditions": "Partly cloudy"}

For weather_get_forecast, it returns a list of daily summaries:

[{"date": "2026-03-14", "high": 15.1, "low": 7.3, "precipitation": 0.0, "conditions": "Clear sky"}, ...]

The WMO weather code (wmo_code: 2) is translated to a human-readable string ("Partly cloudy") inside the handler. The model doesn't need to know what WMO code 71 means; the handler tells it "Slight snowfall." This is the general principle: do the interpretive work in the handler so the model receives a clean, already-reasoned-about result.

Errors are returned as content text, not raised as exceptions. If the API call fails, the handler returns a {"type": "text", "text": "Error: ..."} dict. The model gets something it can reason about — "the API returned a 429, try again in 60 seconds" — rather than a traceback that terminates the tool call branch.

Handlers use httpx.AsyncClient for all HTTP calls. Never use blocking requests calls inside an async handler — that stalls the entire event loop and will cause timeout issues under load.

The Fast Path: OpenAPI to MCP in One Command

If your team already has a REST API with an OpenAPI 3.x spec, you don't need to write tool definitions by hand. The scripts/openapi_to_mcp.py generator reads any OpenAPI spec — local file or URL — and outputs a GENERATED_TOOLS list and a handle_generated_tool stub ready to drop into src/tools/:

python scripts/openapi_to_mcp.py ./your-api.json > src/tools/your_api.py

The generated file is a starting point, not a finished product. The tool names and schemas come directly from the spec's operationId and parameter definitions. You'll want to review the generated descriptions — OpenAPI descriptions are written for human developers, not for models — and trim any tools that expose endpoints the model doesn't need. But having the skeleton auto-generated means the mechanical work of translating a 40-endpoint spec into 40 tool definitions takes minutes, not hours.

Auth Patterns

Most REST APIs require authentication. The repo's docs/auth-patterns.md covers the four patterns that cover the large majority of real-world APIs: API key (header or query param), Bearer token, OAuth 2.0 client credentials, and HTTP Basic Auth. Each pattern is a copy-paste block with the environment variable naming convention and the httpx headers dict already filled in.

The consistent rule across all patterns: credentials come from environment variables read at handler initialization, never from tool arguments. Accepting a token as a tool argument would expose it in the model's context and in any logging that captures tool calls. The server process inherits its environment from Claude Code's MCP server configuration, so the credential flow is: set the variable in the MCP config, read it with os.environ in the handler module, fail fast with a clear error if it's missing.

The 3-Step Workflow

The README distills the full build cycle to three steps. For a new API integration:

# 1. Generate tool stubs from your OpenAPI spec (or write them by hand)
python scripts/openapi_to_mcp.py ./your-api.json > src/tools/your_api.py

# 2. Copy the right auth pattern from docs/auth-patterns.md into your tool module

# 3. Register the server with Claude Code
claude mcp add weather python -- -m src.server

Step 3 is the one that surprises people the first time. The claude mcp add command registers the server in Claude Code's configuration — name, command, and any environment variables it needs. After that, Claude Code spawns the server process automatically at session start and the tools appear in the model's context. There's no daemon to manage, no port to configure. The stdio transport handles all of that.

The weather integration in the repo uses the Open-Meteo API, which requires no API key. That means you can clone the repo and run step 3 immediately — the full round-trip from clone to working MCP server is under five minutes. The no-auth example is deliberate: it removes the credential setup from the critical path when you're trying to understand how the server and transport layer work.

What the Template Gets Right

The design choices in mcp-api-builder reflect the mistakes I made in earlier MCP servers. The TOOL_GROUPS pattern emerged from building servers where adding a third API meant touching the dispatcher in three places and inevitably introducing a routing bug. The OpenAPI generator came from watching teams spend a day transcribing endpoint parameters into JSON Schema by hand when they already had a spec. The auth patterns doc came from copying the same Bearer token header boilerplate into the fifth consecutive project.

The repo is intentionally not a framework. There's no magic, no decorators, no registry. The server file is short enough to read in two minutes and understand in five. When something goes wrong in production, you want to be able to trace the full path from tool call to API response in a single file, not across three layers of abstraction.

The best MCP servers I've built are intentionally narrow. They expose five to ten tools that cover the 80% of tasks the model will actually encounter, with well-curated response shapes that give the model exactly what it needs and nothing more. The curation decisions are the hard part; the protocol mechanics are straightforward once you've done it once.

The full repo, including the working weather example, the OpenAPI generator, and the auth patterns reference, is at github.com/ivandir/mcp-api-builder.