mcp-api-bridge
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-api-bridgeList the 5 most recent posts by user 3"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
REST API to MCP Server Bridge
A small Python example of wrapping a REST API as MCP tools, so Claude, Cursor and other MCP clients can call it with validated input. MCP (Model Context Protocol) is the standard those assistants use to discover and call external tools.
I built it around JSONPlaceholder, a free fake REST API, and exposed four tools: list, get, create and update posts. The point is the pattern. Swap the API client for your own and the tool wiring, validation and error handling carry over.
What it does
An MCP client gets typed tools with JSON schemas it can read before calling them:
You: "List the first 5 posts by user 3"
Claude calls: api_list_posts(user_id=3, limit=5)
→ Fetches GET /posts and filters to userId 3
→ Returns a page of results with pagination info
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 the validated payload
→ Returns the created resourceBad input (a limit of 0, an empty title, an update with no fields) is rejected before any HTTP request goes out, and the client gets an MCP error result (isError: true) it can act on.
Related MCP server: OpenAPI MCP Server
Quick start
Prerequisites: Python 3.10 or newer and uv.
git clone https://github.com/BryceEWatson/mcp-api-bridge.git
cd mcp-api-bridge
uv sync --extra dev
# Run the offline tests (HTTP is mocked)
uv run pytest -m "not live"
# Run the server over stdio
uv run api-bridgeThe server speaks MCP over stdin and stdout, so running it by hand just waits for a client. To try it in Claude Desktop, add this to your claude_desktop_config.json and restart Claude Desktop:
{
"mcpServers": {
"api-bridge": {
"command": "uv",
"args": ["run", "--directory", "/path/to/mcp-api-bridge", "api-bridge"]
}
}
}If Claude Desktop can't find uv, use its full path (which uv on macOS and Linux, where uv on Windows), since desktop apps don't always see your shell's PATH. On Windows, write the --directory path with forward slashes.
If you'd rather use pip, python -m venv .venv, activate it, then pip install -e ".[dev]". uv.lock pins the exact versions CI tests against.
The tools
Tool | Method | What it does | Annotations |
| GET /posts | List posts, filter by user, paginate | read-only, idempotent |
| GET /posts/{id} | Fetch one post, optionally with its comments | read-only, idempotent |
| POST /posts | Create a post | writes, not idempotent, not destructive (only adds) |
| PATCH /posts/{id} | Change some fields of a post | writes, idempotent, destructive (overwrites fields) |
All four set openWorldHint because they call an outside service. The list and get tools take response_format: "markdown" for something a person reads, "json" for something a program parses.
Annotations are hints a client can use to decide when to ask the user before a call. The MCP spec tells clients to treat them as untrusted unless the server is trusted, so they don't replace a client's own confirmation rules. JSONPlaceholder fakes its writes, so nothing actually changes on the demo API.
Security notes
I wrote these down because they're the questions I'd ask of any MCP server before connecting it to an assistant.
What the server does:
No server-side request forgery from tool input. The base URL is set in code by whoever runs the server. Tools only build paths from integer IDs that the schema has already checked (
post_id >= 1), so a model can't make the server call another host or path. httpx doesn't follow redirects by default and the client keeps it that way.Input is validated at the protocol boundary. Every parameter has a type and bounds in the JSON schema, and FastMCP enforces it before the tool runs. Titles and bodies are stripped, so whitespace-only strings are rejected. Tests check that rejected calls send no HTTP request at all.
Errors are real MCP errors. API failures raise
ToolError, so the client seesisError: trueinstead of an error string dressed up as a success. HTTP and network errors get a short actionable message. Anything unexpected is logged to stderr and reported as a generic error, so stack traces and response bodies don't end up in the model's context.Timeouts. Connect, read, write and pool each time out after 30 seconds by default. There's no overall deadline per request, so a server that keeps trickling bytes can hold a call open.
No credentials. JSONPlaceholder needs none, so there are no secrets in the repo. If you add a key, read it from the environment as in Step 1 below.
What it doesn't handle (out of scope for this example):
Prompt injection through tool output. Post titles, bodies and comments go back to the model as-is. With a real API, that text may be written by someone other than your user, and the model can read instructions in it. The server doesn't sanitize or label it as untrusted, and the client has to treat tool output as data.
Response size.
api_list_postsloads the whole/postsresponse into memory before paging it, because JSONPlaceholder has no server-side paging. There's no cap on response size. Against a real API, page on the server and cap what you read.Personal data passthrough.
api_get_postwith comments returns commenter emails. A real integration should decide which fields the model needs to see.Rate limiting and retries. A 429 is reported to the client and not retried.
Authentication and authorization. stdio runs as a local process for one user. If you switch to the HTTP transport, you need authentication in front of it.
Adapting it for your API
Step 1: Replace the API client
Edit src/api_bridge_mcp/api_client.py. Change the base URL and add your auth:
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']}"}Pass headers=self.headers when the client creates its httpx.AsyncClient. The get, post, put and patch helpers stay the same. Keep the base URL under the operator's control rather than taking it from tool input, or you've built a request-forgery tool.
Step 2: Register your tools
Each tool is an async function. Its parameters use Annotated[type, Field(...)], and FastMCP turns those into the JSON schema the client sees and enforces it before your code runs:
@mcp.tool(
name="orders_search",
description="Search orders by customer and status.",
annotations={"readOnlyHint": True, "destructiveHint": False, "idempotentHint": True, "openWorldHint": True},
)
async def orders_search(
customer_id: Annotated[Optional[str], Field(description="Filter by customer", max_length=64)] = None,
status: Annotated[Optional[Literal["pending", "shipped", "delivered"]], Field(description="Filter by status")] = None,
limit: Annotated[int, Field(description="Results per page", ge=1, le=100)] = 20,
) -> str:
query = {k: v for k, v in {"customer_id": customer_id, "status": status}.items() if v is not None}
try:
async with APIClient() as client:
orders = await client.get("/orders", params=query)
return format_orders(orders[:limit])
except Exception as e:
_raise_tool_error("searching orders", e)_raise_tool_error passes a ToolError you raise yourself through unchanged, turns HTTP errors into short messages, and reports anything else generically.
Set the annotations to match what the tool really does. A tool that overwrites or deletes data should say destructiveHint: True.
Step 3: Update the Claude Desktop config
Point the config at your server and restart the client.
Project structure
mcp-api-bridge/
├── .github/workflows/ci.yml ← lint and tests on every push and pull request
├── pyproject.toml ← packaging, dependency ranges, pytest and ruff config
├── uv.lock ← exact versions CI installs
├── claude_desktop_config.json ← example Claude Desktop config
├── src/api_bridge_mcp/
│ ├── api_client.py ← HTTP client (swap this for your API)
│ └── server.py ← the four MCP tools
└── tests/
├── conftest.py ← shared fixtures and sample data
├── test_client.py ← API client tests (HTTP mocked)
├── test_tools.py ← tool and validation tests (HTTP mocked)
└── test_e2e_mcp.py ← MCP protocol tests, some liveThe API layer (api_client.py) is separate from the MCP layer (server.py). To adapt it, you mostly change the client and the tool functions; the FastMCP wiring stays the same.
Design decisions
JSONPlaceholder as the demo API. No auth, no signup, full CRUD, and it's obviously a stand-in, so the pattern is the focus.
Python and FastMCP. FastMCP is the high-level server in the official MCP Python SDK. It handles tool registration, schema generation and transport. I've pinned the SDK below 2.0, because 2.x renamed FastMCP to MCPServer; moving to it is a separate change.
stdio transport. It's the default for local MCP servers. The SDK also supports streamable HTTP for remote use, which needs the authentication noted above.
Pydantic constraints on every parameter. The client sees the bounds in the schema before it calls, and the server enforces the same bounds.
Two response formats. Markdown for a person reading the assistant's answer, JSON for a program.
Running tests
# Offline: client, tool, validation, schema and annotation tests. HTTP is mocked.
uv run pytest -m "not live"
# Live: real calls to JSONPlaceholder, including a full session over stdio
uv run pytest -m live
# Lint
uv run ruff check src testsCI runs lint and the offline tests on Python 3.10 through 3.13 on Linux, the offline tests on Windows and macOS, the offline tests again with the oldest dependency versions pyproject.toml allows, and the live tests once against JSONPlaceholder. The live job depends on that public service being up.
Built with
MCP Python SDK (FastMCP)
httpx for async HTTP
Pydantic v2 for validation
JSONPlaceholder as the demo API
pytest, pytest-httpx and ruff
About
I'm Bryce Watson. I build AI tooling and MCP servers. More at brycewatson.com and github.com/BryceEWatson.
MIT licensed.
Available Tools
4 toolsapi_create_postC
Create a new post.
| Name | Required | Description | Default |
|---|---|---|---|
| body | Yes | Post body content (1-5000 characters). | |
| title | Yes | Post title (1-200 characters). | |
| user_id | Yes | ID of the user creating the post. Must be >= 1. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=false, idempotentHint=false, openWorldHint=true and destructiveHint=false, covering the safety profile. The description adds nothing beyond this: it does not mention that calls are non-idempotent (creating duplicates on retry), required authorship, or any side effects. With annotations present, the bar is lower, but the description contributes zero behavioral context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with no wasted words and no filler. Its brevity is not a virtue here, though — there is nothing to front-load, and it reads as under-specification rather than disciplined concision.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool is a simple three-parameter create operation, an output schema exists (so return values need no explanation), and the schema fully documents inputs. Given that the structured fields carry most of the burden, the description is minimally adequate, but it still leaves when-to-use and retry/idempotency behavior unaddressed for what is an open-world, non-idempotent write.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% — title, body, and user_id are each documented with lengths and minimums in the schema itself. The description adds no parameter meaning beyond what the structured schema provides, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource ('Create a new post'), so the action is unambiguous. However, it essentially restates the tool name api_create_post with zero added detail — no scope, no mention of what a post requires, and no explicit differentiation from siblings like api_get_post or api_update_post. Clear but uninformative.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
There is no guidance on when to use this tool versus the sibling api_update_post, api_get_post, or api_list_posts, and no prerequisites or preconditions are mentioned. The create-vs-read distinction is only inferable from the tool name, not from the description.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
api_get_postARead-onlyIdempotent
Fetch a single post by ID with optional comments.
| Name | Required | Description | Default |
|---|---|---|---|
| post_id | Yes | ID of the post to fetch. Must be >= 1. | |
| response_format | No | Output format: 'markdown' for formatted display, 'json' for raw objects. | markdown |
| include_comments | No | If true, also fetch comments on this post. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, destructiveHint=false and openWorldHint=true, so the safety and idempotency profile is fully covered. The description adds only the optional-comments behavior, which is really parameter information, and says nothing about error behavior for missing IDs or remote fetch characteristics.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single, front-loaded sentence with no filler; the core action and the optional-comments behavior come first. It is arguably too terse to be maximally useful, but nothing in it is wasted.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With rich annotations, a fully documented three-parameter schema, and an output schema covering return values, the description needs to do little. It covers the essential action and the optional-comment flag; only routing against sibling tools is left unaddressed.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100% and all three parameters (post_id, response_format, include_comments) carry their own descriptions, so the schema does the heavy lifting. The description's mention of 'by ID' and 'optional comments' restates rather than extends what the schema documents.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description gives a specific verb and resource ('Fetch a single post by ID'), which clearly separates it from collection-level siblings like api_list_posts. It does not explicitly name any sibling or contrast itself with api_create_post/api_update_post, but the singular-resource framing makes the distinction inferable.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied rather than stated: fetch this when you have a post ID and want one post. There is no explicit when-to-use/when-not-to-use guidance and no mention of the alternative (api_list_posts) for retrieving multiple posts, so the agent must infer the routing.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
api_list_postsARead-onlyIdempotent
List posts from JSONPlaceholder with pagination and optional filtering.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Number of posts to return. Default 20, max 100. | |
| offset | No | Number of posts to skip. Default 0. Used for pagination. | |
| user_id | No | Filter posts by user ID (author). If omitted, returns posts from all users. | |
| response_format | No | Output format: 'markdown' for human-readable summaries, 'json' for full objects. | markdown |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, openWorldHint and destructiveHint=false, covering the safety profile. The description adds the pagination/filtering scope but nothing about ordering, total counts, or rate limits, so it adds modest value over the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with zero waste. It is compact and readable, though arguably terse given no usage or behavioral detail.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Output schema exists and annotations cover safety, so the description need not explain return values. For a simple list tool with fully-described parameters, it is nearly complete, missing only ordering/alternative guidance.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so limit, offset, user_id, and response_format are all documented in the schema itself; the description adds no syntax or format detail beyond restating pagination and filtering. Baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('List posts from JSONPlaceholder') plus scope ('pagination and optional filtering'). The verb distinguishes it from api_get_post/api_create_post/api_update_post, though it never names siblings explicitly to route the agent.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Usage is implied by 'pagination and optional filtering' but there is no explicit when-to-use, when-not, or alternative routing (e.g. 'use api_get_post for a single post by id'). The context is inferable but not stated.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
api_update_postBDestructiveIdempotent
Update a post (partial update with PATCH). Overwrites the fields you pass.
| Name | Required | Description | Default |
|---|---|---|---|
| body | No | New body (1-5000 characters). Omit to keep current. | |
| title | No | New title (1-200 characters). Omit to keep current. | |
| post_id | Yes | ID of the post to update. Must be >= 1. | |
| user_id | No | New user ID. Omit to keep current. |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=false, destructiveHint=true, idempotentHint=true, and openWorldHint=true, so the safety profile is covered. The description adds genuinely useful partial-update context ('Overwrites the fields you pass'), though it leaves ambiguous whether omitted fields are preserved or cleared and says nothing about authorization.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two short, front-loaded sentences with no filler; the operation and its partial-update nature come first. The second sentence is slightly redundant with the schema's 'Omit to keep current' notes, keeping it from a 5.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be explained, and the parameter surface is fully documented in the schema. However, for a destructive mutation the description omits prerequisites, failure behavior, and whether omitted fields are preserved versus nulled, leaving clear gaps.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so post_id, title, body, and user_id are all documented with types, ranges, and 'Omit to keep current' semantics. The description contributes no parameter-level detail beyond what the schema already supplies, which is the baseline 3 case.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a clear verb (update) and resource (post) and pins down the semantics as a PATCH-style partial update, which distinguishes it from create_post and get_post. It stops short of referencing siblings by name, but the operation is unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
No when-to-use guidance, no prerequisites, and no routing to alternatives such as api_create_post for new records. The PATCH mention describes how the update works rather than when to reach for this tool over a sibling.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
4 tool updates
v1.0.0- First observed
api_create_post - First observed
api_get_post - First observed
api_list_posts - First observed
api_update_post
TDQS
Scored across 4 tools
Each tool maps to a distinct action on a single resource (list, get, create, update posts), with no overlap in purpose. An agent can trivially select the right tool for each intent.
All four tools follow the same api_verb_noun pattern (api_list_posts, api_get_post, api_create_post, api_update_post). The convention is uniform and predictable.
Four tools is slightly thin but well-scoped to a single posts resource with JSONPlaceholder. Each tool earns its place without redundancy.
The surface covers list/get/create/update but has no delete_post, leaving the CRUD lifecycle incomplete. Comment retrieval is folded into get_post rather than exposed as its own operation, which is workable but a gap for a full posts API.
Maintenance
Related MCP Connectors
AI-native mock API server with MCP. Create REST/SOAP mocks from Claude, Cursor, or Windsurf.
MCP server that lets AI assistants use all OneSchema features exposed via the public API.
Any REST/SOAP/GraphQL/OData/SQL API as MCP tools for Claude & ChatGPT. 547 connectors: SAP, ERP.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceDynamically generates MCP tools from OpenAPI specifications, enabling AI assistants to interact with any REST API through natural language. Supports multiple APIs with authentication, parameter validation, and integration with Claude Desktop and LangChain.1MIT
- AlicenseNot gradedqualityDmaintenanceA generic MCP server that dynamically converts OpenAPI-defined REST APIs into tools for LLMs like Claude. It supports multiple authentication methods and transport protocols, enabling seamless interaction with any OpenAPI-compliant API.12 npmMIT
- AlicenseNot gradedqualityBmaintenanceTurns any OpenAPI/Swagger API into MCP tools, enabling AI assistants to call REST API endpoints directly.2MIT
- FlicenseNot gradedqualityDmaintenanceConverts any REST API endpoints into MCP tools, enabling AI clients like Cursor and Claude Desktop to call internal services directly.-