How to Build an MCP Server for Your API
Convert any REST API into an MCP server so AI tools can use it. Covers endpoint mapping, authentication, error handling, and deployment with Python and TypeScript examples.
William Wang — Founder of TokRepo & GEOScore AI. Building tools for AI developer productivity and search visibility.
Table of Contents
- Prerequisites
- Architecture: REST to MCP Mapping
- Step 1 — Plan Your Endpoint Mapping
- Step 2 — Set Up the Project
- Step 3 — Build the Server
- Step 4 — Handle Authentication Securely
- Step 5 — Error Handling
- Step 6 — Schema Design for LLMs
- Step 7 — Deploy as Remote HTTP Server
- Step 8 — Handling Large APIs
- Step 9 — Test Everything
- Next Steps
Learn how to wrap any REST API as an MCP server so Claude Code, Cursor, and other AI tools can interact with it through natural language. This guide covers endpoint mapping, secure authentication, error handling, and deployment.
Prerequisites
- A REST API you want to expose to AI tools
- Python 3.10+ with
uv, or Node.js 16+ with npm - API documentation or endpoint list for your target API
- Claude Code or Cursor for testing
Architecture: REST to MCP Mapping
| REST Concept | MCP Equivalent | When to Use |
|---|---|---|
| GET endpoints (no params) | Resources | Read-only data retrieval |
| GET with query params | Tools | LLM decides what to query |
| POST/PUT/DELETE | Tools | Actions with side effects |
| Auth headers | Environment variables | Injected at server startup |
| Pagination | Multiple tool calls | LLM handles iteration |
Key principle: GET endpoints with no parameters become Resources. Everything else becomes a Tool.
Step 1 — Plan Your Endpoint Mapping
Example for a project management API:
GET /projects → Resource: projects://list
GET /projects/{id} → Resource: projects://{id}
POST /projects → Tool: create_project
PUT /projects/{id} → Tool: update_project
DELETE /projects/{id} → Tool: delete_project
GET /projects/{id}/tasks → Tool: list_tasks (has query params)
POST /projects/{id}/tasks → Tool: create_task
Step 2 — Set Up the Project
uv init api-mcp-server
cd api-mcp-server
uv venv && source .venv/bin/activate
uv add "mcp[cli]" httpx
touch server.py
Step 3 — Build the Server
"""MCP Server wrapping a Project Management REST API."""
import os
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP
API_BASE_URL = os.environ.get("API_BASE_URL", "https://api.example.com/v1")
API_KEY = os.environ.get("API_KEY", "")
mcp = FastMCP("project-api")
async def api_request(
method: str, path: str,
params: dict[str, Any] | None = None,
json_body: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Make an authenticated request to the REST API."""
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
async with httpx.AsyncClient() as client:
response = await client.request(
method, f"{API_BASE_URL}{path}",
headers=headers, params=params,
json=json_body, timeout=30.0,
)
response.raise_for_status()
return response.json()
# --- Resources (GET, read-only) ---
@mcp.resource("projects://list")
async def list_projects() -> str:
"""List all projects in the workspace."""
data = await api_request("GET", "/projects")
projects = data.get("projects", [])
lines = [f"- [{p['id']}] {p['name']} ({p['status']})" for p in projects]
return "\n".join(lines) if lines else "No projects found."
# --- Tools (actions with side effects) ---
@mcp.tool()
async def create_project(name: str, description: str = "") -> str:
"""Create a new project.
Args:
name: Project name
description: Optional project description
"""
data = await api_request("POST", "/projects",
json_body={"name": name, "description": description})
return f"Created project '{data['name']}' with ID: {data['id']}"
@mcp.tool()
async def list_tasks(
project_id: str,
status: str | None = None,
assignee: str | None = None,
limit: int = 20,
) -> str:
"""List tasks in a project with optional filters.
Args:
project_id: The project to list tasks for
status: Filter by 'todo', 'in_progress', or 'done' (optional)
assignee: Filter by assignee email (optional)
limit: Max tasks to return (default 20)
"""
params: dict[str, Any] = {"limit": min(limit, 100)}
if status: params["status"] = status
if assignee: params["assignee"] = assignee
data = await api_request("GET", f"/projects/{project_id}/tasks", params=params)
tasks = data.get("tasks", [])
if not tasks:
return "No tasks found."
lines = [
f"- [{t['id']}] {t['title']} | {t['status']} | {t.get('assignee', 'unassigned')}"
for t in tasks
]
return "\n".join(lines)
@mcp.tool()
async def create_task(
project_id: str, title: str,
description: str = "", priority: str = "medium",
) -> str:
"""Create a new task in a project.
Args:
project_id: The project to create the task in
title: Task title
description: Task description (optional)
priority: 'low', 'medium', 'high', or 'urgent'
"""
data = await api_request(
"POST", f"/projects/{project_id}/tasks",
json_body={"title": title, "description": description, "priority": priority},
)
return f"Created task '{data['title']}' (ID: {data['id']})"
if __name__ == "__main__":
mcp.run(transport="stdio")
Step 4 — Handle Authentication Securely
Never hardcode API keys. Use environment variables:
{
"mcpServers": {
"project-api": {
"command": "uv",
"args": ["--directory", "/path/to/api-mcp-server", "run", "server.py"],
"env": {
"API_BASE_URL": "https://api.example.com/v1",
"API_KEY": "sk-your-api-key-here"
}
}
}
}
Step 5 — Error Handling
Wrap API errors gracefully so the LLM gets useful feedback:
import httpx
@mcp.tool()
async def safe_api_call(endpoint: str) -> str:
"""Robust error handling example."""
try:
data = await api_request("GET", endpoint)
return str(data)
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
return f"Not found: {endpoint}"
elif e.response.status_code == 403:
return "Permission denied. Check your API key."
elif e.response.status_code == 429:
return "Rate limited. Try again shortly."
return f"API error {e.response.status_code}: {e.response.text[:200]}"
except httpx.TimeoutException:
return "Request timed out."
Step 6 — Schema Design for LLMs
LLMs work best with simple, clear schemas:
# BAD — nested objects confuse LLMs
@mcp.tool()
async def create_user(data: dict) -> str: ...
# GOOD — flat, typed parameters with descriptions
@mcp.tool()
async def create_user(
name: str, email: str, role: str = "member"
) -> str:
"""Create a new user account.
Args:
name: Full name of the user
email: Email address (must be unique)
role: User role — 'member', 'admin', or 'viewer'
"""
Return human-readable text, not raw JSON:
# BAD
return json.dumps(api_response)
# GOOD
return f"Created user {data['name']} (ID: {data['id']}) with role {data['role']}"
Step 7 — Deploy as Remote HTTP Server
For production, use streamable HTTP transport:
mcp = FastMCP("project-api", stateless_http=True)
# ... define tools and resources ...
if __name__ == "__main__":
mcp.run(transport="streamable-http", host="0.0.0.0", port=8080)
Connect from Claude Code:
claude mcp add --transport http project-api https://your-server.com:8080/mcp
Step 8 — Handling Large APIs
For APIs with 50+ endpoints, split into focused servers:
api-users-mcp/ → User management tools
api-projects-mcp/ → Project tools
api-billing-mcp/ → Billing and payment tools
Each server stays focused. Claude Code handles multiple MCP servers simultaneously.
Step 9 — Test Everything
# Visual debugging
npx @modelcontextprotocol/inspector uv run server.py
# Dev mode
mcp dev server.py
# Test in Claude Code
claude mcp add --transport stdio project-api \
--env API_KEY=sk-test --env API_BASE_URL=https://api.example.com/v1 \
-- uv --directory /path/to/server run server.py
FAQ
Q: How do I convert a REST API to MCP? A: Map GET endpoints (no params) to MCP Resources, and everything else (POST, PUT, DELETE, parameterized GET) to MCP Tools. Use environment variables for authentication.
Q: Should I create one large MCP server or multiple small ones? A: Multiple focused servers (under 30 tools each) perform better. LLMs choose more accurately when tool count is manageable.
Q: Can I use OAuth instead of API keys? A: Yes, but OAuth flows are more complex. Store the access token as an environment variable, and handle token refresh in your server's lifespan manager.
Next Steps
- How to Build an MCP Server in Python — Python SDK basics
- How to Build an MCP Server in TypeScript — TypeScript SDK
- Best MCP Servers for Claude Code — see real examples
- What Are Claude Code Skills? — another extension model