Skip to main content
Glama
BryceEWatson

mcp-api-bridge

by BryceEWatson

REST API to MCP Server Bridge

CI

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 resource

Bad 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-bridge

The 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

api_list_posts

GET /posts

List posts, filter by user, paginate

read-only, idempotent

api_get_post

GET /posts/{id}

Fetch one post, optionally with its comments

read-only, idempotent

api_create_post

POST /posts

Create a post

writes, not idempotent, not destructive (only adds)

api_update_post

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 sees isError: true instead 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_posts loads the whole /posts response 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_post with 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 live

The 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 tests

CI 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

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 tools
api_create_postC

Create a new post.

NameRequiredDescriptionDefault
bodyYesPost body content (1-5000 characters).
titleYesPost title (1-200 characters).
user_idYesID of the user creating the post. Must be >= 1.

Output Schema

NameRequiredDescription
resultYes

TDQS

C2.6/5.0
Behavior2/5

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.

Conciseness3/5

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.

Completeness3/5

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.

Parameters3/5

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.

Purpose3/5

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.

Usage Guidelines2/5

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_postA
Read-onlyIdempotent

Fetch a single post by ID with optional comments.

NameRequiredDescriptionDefault
post_idYesID of the post to fetch. Must be >= 1.
response_formatNoOutput format: 'markdown' for formatted display, 'json' for raw objects.markdown
include_commentsNoIf true, also fetch comments on this post.

Output Schema

NameRequiredDescription
resultYes

TDQS

A3.5/5.0
Behavior3/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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_postsA
Read-onlyIdempotent

List posts from JSONPlaceholder with pagination and optional filtering.

NameRequiredDescriptionDefault
limitNoNumber of posts to return. Default 20, max 100.
offsetNoNumber of posts to skip. Default 0. Used for pagination.
user_idNoFilter posts by user ID (author). If omitted, returns posts from all users.
response_formatNoOutput format: 'markdown' for human-readable summaries, 'json' for full objects.markdown

Output Schema

NameRequiredDescription
resultYes

TDQS

A3.5/5.0
Behavior3/5

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.

Conciseness4/5

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.

Completeness4/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines3/5

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_postB
DestructiveIdempotent

Update a post (partial update with PATCH). Overwrites the fields you pass.

NameRequiredDescriptionDefault
bodyNoNew body (1-5000 characters). Omit to keep current.
titleNoNew title (1-200 characters). Omit to keep current.
post_idYesID of the post to update. Must be >= 1.
user_idNoNew user ID. Omit to keep current.

Output Schema

NameRequiredDescription
resultYes

TDQS

B3.2/5.0
Behavior3/5

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.

Conciseness4/5

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.

Completeness3/5

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.

Parameters3/5

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.

Purpose4/5

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.

Usage Guidelines2/5

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.

  1. 4 tool updatesv1.0.0
    • First observedapi_create_post
    • First observedapi_get_post
    • First observedapi_list_posts
    • First observedapi_update_post

TDQS

B3.4/5.0

Scored across 4 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count4/5

Four tools is slightly thin but well-scoped to a single posts resource with JSONPlaceholder. Each tool earns its place without redundancy.

Completeness3/5

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

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Dynamically 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.
    1
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    A 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 npm
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Converts any REST API endpoints into MCP tools, enabling AI clients like Cursor and Claude Desktop to call internal services directly.
    -