I’ve built several MCP servers at this point: for Etsy, for Shopify, for internal tools. Every time, I spend the first few hours on the same scaffolding: an async HTTP client, Pydantic input models, error handling that returns messages an AI assistant can actually act on, and enough tests to ship with confidence.
After the third time writing that scaffolding, I extracted it into a starter kit. The result is MCP API Bridge, a production-quality template for turning any REST API into an MCP server, with 74 tests and a clean separation between HTTP concerns and MCP tool wiring.
This post walks through the architecture decisions and the patterns I keep reusing.
The problem MCP servers actually need to solve
The MCP protocol itself is straightforward. You register tools with a name, description, and input schema. An AI assistant calls them. You return results. The protocol documentation covers this well.
What the docs don’t cover is the engineering between the protocol and your API. In production, you need input validation that generates useful JSON Schema for AI assistants. You need error handling that translates HTTP status codes into messages Claude can reason about. You need response formatting that works for both human readers and downstream tool chains. And you need tests, not just “does it start” tests, but tests that verify your validation boundaries, your error messages, and your full MCP protocol round-trips.
That’s the gap MCP API Bridge fills. Not the protocol wiring (that’s what the MCP SDK is for), but the engineering patterns that make the difference between a demo and something you’d deploy for a client.
Architecture: three layers, one swap point
The codebase separates into three concerns:
HTTP layer (api_client.py, 159 lines): A single async client using httpx that centralizes all HTTP concerns: base URL, headers, timeouts, connection pooling. When adapting the kit for a new API, this is the only file you change. Swap the base URL, add your auth headers, and the rest of the architecture stays the same.
MCP tool layer (server.py, 567 lines): Four decorated tools demonstrating the full CRUD pattern: list with filtering and pagination, get with optional related data, create with field validation, and partial update with existence checks. Each tool uses Pydantic v2 models for input validation and returns dual-format responses (markdown for the Claude UI, JSON for machine consumption).
Test layer (1,171 lines, 74 tests): Three levels of testing. Unit tests mock the HTTP layer with pytest-httpx. Tool tests verify validation boundaries and output formatting. End-to-end tests spin up the full MCP protocol stack in-memory and verify JSON-RPC round-trips.
The key architectural decision is the single swap point. Most MCP server tutorials interleave HTTP calls directly in tool functions. That works for demos, but when a client hands you their API and says “make this work with Claude,” you want to change one file, not grep through every tool function replacing URLs and headers.
Pydantic as the source of truth
Every tool parameter flows through a Pydantic v2 model. This isn’t just for runtime validation; it’s how AI assistants know what to send.
When you define a field like title: str = Field(..., min_length=1, max_length=200), the MCP SDK reads Pydantic’s generated JSON Schema and includes it in the tool’s schema declaration. Claude sees "maxLength": 200 and constrains its output accordingly. The validation is happening before your code even runs.
The more interesting pattern is cross-field validation. For the update tool, at least one field must be provided, since you can’t send an empty PATCH. A Pydantic model validator handles this:
@model_validator(mode='after')
def check_at_least_one_field(self):
if self.title is None and self.body is None and self.user_id is None:
raise ValueError("At least one field must be provided")
return self
This generates a clear error message that Claude can relay to the user, rather than silently sending an empty request to the API.
Error handling for AI assistants, not humans
Standard HTTP error handling returns status codes and technical messages. That’s fine when a developer is reading logs. It’s useless when Claude is the caller.
MCP API Bridge maps every error class to an actionable message:
- 404: “Resource not found. Please check the ID and try again.”
- 429: “Rate limited. Please wait before retrying.”
- Timeout: “Request timed out. The API is taking too long to respond.”
- Connection error: “Connection failed. Check your network or the API endpoint.”
The pattern is centralized in a single handle_api_error() function. Claude reads these messages and can decide what to do: retry, ask the user for a different ID, or report the issue. Raw HTTP 429 responses don’t give it that affordance.
Dual response formats
Every tool returns both markdown and JSON. The caller chooses via a response_format parameter.
This isn’t a cosmetic feature. When Claude calls a tool in a chat context, markdown renders nicely in the UI: bold post titles, formatted comment counts, readable summaries. When Claude is chaining tools in an agentic workflow, it needs structured JSON it can parse and pass to the next tool.
The pattern is simple: build the response data once, then format it based on the parameter. But I’ve seen enough MCP servers that only return raw JSON (unreadable in chat) or only return markdown (unparsable in chains) that it’s worth calling out as a deliberate choice.
MCP annotations: telling the assistant what it’s working with
Each tool declares behavioral annotations: readOnlyHint, destructiveHint, idempotentHint, openWorldHint. These are part of the MCP spec but easy to skip. They tell the assistant whether a tool is safe to call speculatively, whether it modifies state, and whether it might affect systems beyond the immediate API.
For a list endpoint: read-only, not destructive, idempotent, open world (results depend on external state). For a create endpoint: not read-only, not destructive (it adds, doesn’t delete), not idempotent (calling twice creates two resources), open world.
Getting these right matters when an assistant is deciding whether to call your tool without explicit user confirmation.
The test strategy
74 tests in three layers:
HTTP client tests (14) verify that the async client correctly handles responses, timeouts, and connection errors using mocked HTTP responses. No real API calls.
Tool tests (32) verify the MCP tools’ input validation, output formatting, and pagination logic. These test at the function level with a mocked HTTP layer. They confirm that boundary conditions (empty strings, out-of-range values, missing required fields) produce the right errors, and that both markdown and JSON output formats are correct.
End-to-end MCP tests (28) spin up the full protocol stack using the SDK’s create_connected_server_and_client_session helper. These tests send JSON-RPC messages through the MCP protocol layer and verify the complete round-trip: tool discovery, schema validation, invocation, and response formatting. A subset also tests stdio transport by spawning the server as a child process, which is exactly how Claude Desktop connects.
The e2e tests catch a category of bugs that unit tests miss: schema mismatches between your Pydantic models and what the MCP protocol advertises, transport serialization issues, and tool registration errors.
Adapting the kit
The adaptation flow for a new API:
- Replace
api_client.py: change the base URL, add authentication headers, adjust timeouts - Define your Pydantic models: one per tool, with field constraints that match your API’s requirements
- Register your tools: copy the
@mcp.tool()decorator pattern, wire up the HTTP calls - Update your Claude Desktop config: point it at your new server
The reference implementation wraps JSONPlaceholder (a public REST API) so you can run the full test suite and see every pattern in action before touching your own API.
What this is for
I built this because I kept getting the same request: “We have a REST API, we want Claude to use it.” The technical decisions (validation strategy, error handling, test infrastructure, response formatting) are the same every time. The API-specific parts (endpoints, auth, domain models) are different.
MCP API Bridge is the reusable half. It’s the scaffolding I start from on every MCP server engagement, extracted into something others can use too.
The repository includes the full source, test suite, and a detailed README covering configuration for Claude Desktop and Cursor. If you need an MCP server built for your API, get in touch.