mcp-api-bridge
by BryceEWatson
README.md
# REST API → MCP Server Bridge
Turn any REST API into an MCP server so Claude, Cursor, and other AI assistants can use it directly.
This is a production-quality starter kit that wraps [JSONPlaceholder](https://jsonplaceholder.typicode.com) (a free REST API) as 4 MCP tools. The real value is the **pattern** — swap the API client to point at your own API and you have a working MCP server.
---
<p align="center">
<img src="docs/demo.svg" alt="Demo: Claude Desktop using MCP API Bridge tools" width="800">
</p>
## What This Does
You give Claude (or any MCP-compatible AI assistant) access to a REST API through typed, validated tools:
```
You: "List the 5 most recent posts by user 3"
Claude calls: api_list_posts(user_id=3, limit=5)
→ Fetches GET /posts?userId=3
→ Returns formatted, paginated results
You: "Create a post about MCP servers"
Claude calls: api_create_post(title="Why MCP Servers Matter", body="...", user_id=1)
→ Sends POST /posts with validated payload
→ Returns the created resource
```
No prompt engineering needed. The AI assistant discovers the tools, validates inputs via Pydantic schemas, and gets structured responses.
## Quick Start
**Prerequisites:** Python 3.10+, [uv](https://docs.astral.sh/uv/) (recommended) or pip
```bash
# Clone and install
git clone https://github.com/BryceEWatson/mcp-api-bridge.git
cd mcp-api-bridge
uv pip install -e ".[dev]"
# Run the server (stdio transport)
python -m api_bridge_mcp.server
# Or run tests
pytest tests/ -v
```
**Add to Claude Desktop** — copy this into your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"api-bridge": {
"command": "uv",
"args": ["run", "--directory", "/path/to/mcp-api-bridge", "api-bridge"]
}
}
}
```
Restart Claude Desktop. The 4 tools appear automatically.
## The Tools
| Tool | Method | What It Does | Key Patterns |
|------|--------|-------------|--------------|
| `api_list_posts` | GET /posts | List posts with filtering + pagination | Query params, in-memory pagination, dual format output |
| `api_get_post` | GET /posts/{id} | Fetch a post with optional comments | Resource lookup, related data joining |
| `api_create_post` | POST /posts | Create a new post | Write operations, input validation |
| `api_update_post` | PATCH /posts/{id} | Update post fields | Partial updates, existence checks |
Every tool supports `response_format: "markdown"` (human-readable) or `"json"` (machine-readable). All inputs are validated with Pydantic v2 models with field constraints. Every tool has MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) set correctly.
## Adapting This For Your API
The whole point of this project is to show a repeatable pattern. Here's how to make it work with any REST API:
### Step 1: Replace the API client
Edit `src/api_bridge_mcp/api_client.py`. Change the base URL and add your auth:
```python
class APIClient:
def __init__(self, base_url: str = "https://api.your-service.com/v1", timeout: int = 30):
self.base_url = base_url
self.timeout = httpx.Timeout(timeout)
self.headers = {"Authorization": f"Bearer {os.environ['YOUR_API_KEY']}"}
```
The rest of the client (get/post/put/patch, error handling) works unchanged.
### Step 2: Define your Pydantic input models
Replace the post-related models in `server.py` with your domain:
```python
class SearchOrdersInput(BaseModel):
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
customer_id: Optional[str] = Field(None, description="Filter by customer")
status: Optional[str] = Field(None, description="Filter by status: pending, shipped, delivered")
limit: int = Field(20, ge=1, le=100, description="Results per page")
```
### Step 3: Register your tools
Same `@mcp.tool` decorator pattern — pass a Pydantic model as the single parameter:
```python
@mcp.tool(
name="orders_search",
description="Search orders by customer and status.",
annotations={"readOnlyHint": True, "idempotentHint": True}
)
async def orders_search(params: SearchOrdersInput) -> str:
async with APIClient() as client:
query = {}
if params.customer_id:
query["customer_id"] = params.customer_id
if params.status:
query["status"] = params.status
orders = await client.get("/orders", params=query)
return format_orders(orders[:params.limit])
```
### Step 4: Update the Claude Desktop config
Point the config at your new server. That's it.
## Project Structure
```
mcp-api-bridge/
├── README.md ← You are here
├── PLAN.md ← Design decisions and research
├── pyproject.toml ← PEP 621 packaging
├── claude_desktop_config.json ← Example Claude Desktop config
├── src/
│ └── api_bridge_mcp/
│ ├── __init__.py
│ ├── api_client.py ← HTTP client (swap this for your API)
│ └── server.py ← MCP tools (4 tools, ~530 lines)
└── tests/
├── conftest.py ← Shared fixtures and mock data
├── test_client.py ← API client tests (14 tests)
└── test_tools.py ← Tool tests (32 tests)
```
The architecture separates the **API layer** (api_client.py) from the **MCP layer** (server.py). When adapting for a new API, you primarily modify api_client.py and the Pydantic models — the MCP wiring stays the same.
## Design Decisions
These are documented in detail in [PLAN.md](./PLAN.md). The short version:
**JSONPlaceholder as the demo API** — zero friction (no auth, no signup, no rate limits), full CRUD, and obviously a stand-in so the pattern is the focus, not the domain.
**Python + FastMCP** — the MCP Python SDK's high-level framework. Handles tool registration, input schema generation, and transport automatically. Fewer lines, fewer bugs.
**stdio transport** — the right default for local-first MCP servers. Add `mcp.run(transport="streamable_http", port=8000)` for remote deployment.
**Pydantic v2 for validation** — every tool input is a typed model with constraints. The AI assistant sees the schema and knows exactly what to send.
**Dual response formats** — markdown for when a human is reading Claude's output, JSON for when another system is consuming it.
## Running Tests
```bash
# All tests
pytest tests/ -v
# Just the API client tests
pytest tests/test_client.py -v
# Just the tool tests
pytest tests/test_tools.py -v
```
The unit and tool tests use `pytest-httpx` to mock HTTP responses — fast and deterministic. The end-to-end tests hit the live JSONPlaceholder API over the real MCP protocol. 74 tests covering input validation, output formatting, pagination, error handling, and full MCP protocol flows.
## Built With
- [FastMCP](https://github.com/modelcontextprotocol/python-sdk) — MCP Python SDK
- [httpx](https://www.python-httpx.org/) — async HTTP client
- [Pydantic v2](https://docs.pydantic.dev/) — input validation
- [JSONPlaceholder](https://jsonplaceholder.typicode.com) — demo REST API
- [pytest](https://docs.pytest.org/) + [pytest-httpx](https://github.com/Colin-b/pytest_httpx) — testing
## About
Built by **Bryce Watson** — senior engineer (10+ years, ex-eBay) specializing in AI engineering, MCP servers, and production AI systems. Contributor to Anthropic's Python SDK.
- **Upwork:** [upwork.com/freelancers/brycewatson](https://www.upwork.com/freelancers/brycewatson)
- **Site:** [brycewatson.com](https://brycewatson.com)
- **GitHub:** [github.com/BryceEWatson](https://github.com/BryceEWatson)
Need an MCP server built for your API? [Get in touch.](https://www.upwork.com/freelancers/brycewatson)
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues