Expose any MCP server's tools as OpenAI function-calling tools, and execute OpenAI tool calls back against it.
Project description
🌉 mcp-openai-bridge
Give any OpenAI-function-calling agent access to the entire MCP tool ecosystem — no SDK rewrite, no per-tool adapter.
Problem · What it does · Use cases · Example · Architecture · Performance · Install · Security
The problem
Two tool-calling formats now dominate agent development:
- MCP (Model Context Protocol) — the format most Claude-first tools, IDEs, and local dev tooling speak.
- OpenAI function-calling — the
tools=[{"type": "function", ...}]JSON-Schema format used by the OpenAI API and every SDK/framework that mirrors it (LangChain, most self-rolled agent loops, etc.).
If your agent only speaks OpenAI function-calling, the fast-growing MCP server ecosystem (filesystem tools, database connectors, SaaS integrations, dev tools) is invisible to it — unless someone hand-writes a per-server adapter, or adopts a full proxy that speaks MCP on one side and OpenAI-compatible chat completions on the other.
mcp-openai-bridge is a narrower shim. Point it at any MCP server, get back a local HTTP endpoint that speaks pure OpenAI function-calling — just tool listing and tool execution, nothing else.
flowchart LR
A["OpenAI-format agent\n(tools=[...] / tool_calls)"] -- "GET /v1/tools" --> B["mcp-openai-bridge"]
B -- "OpenAI tools array" --> A
A -- "POST /v1/execute\n{name, arguments}" --> B
B -- "tools/call" --> C["any MCP server\n(stdio or streamable-HTTP)"]
C -- "CallToolResult" --> B
B -- "content + is_error" --> A
How this compares
MCP-Bridge already solves this space, and more broadly: it's a full OpenAI-compatible chat-completions proxy that forwards to inference engines like vLLM and Ollama and drives the entire tool-use loop for you. It owns the mcp-bridge PyPI name and is listed in awesome-mcp-servers — the more mature, more capable option if you want a drop-in chat-completions endpoint.
mcp-openai-bridge is a deliberately smaller tool for a narrower ask: you don't want a chat-completions proxy or an inference-engine dependency, you just want an MCP server's tools reachable as two stateless HTTP endpoints — GET /v1/tools and POST /v1/execute — and nothing else in front of your existing agent loop. It's small, sharp, and exactly the kind of shim that pays for itself the first time you want to reuse one MCP server across two differently-stacked agents without adopting a bigger piece of infrastructure to do it.
What it does
Two commands, two HTTP endpoints, one process:
pipx install mcp-openai-bridge
# Bridge a local, stdio-launched MCP server
mcp-openai-bridge serve --stdio "npx -y @modelcontextprotocol/server-filesystem /path/to/dir" --port 8800
# Or bridge one already running over streamable-HTTP
mcp-openai-bridge serve --http http://localhost:1234/mcp --port 8800
Alias: the package also installs a
mcp2openaicommand — same program, shorter name:mcp2openai serve --stdio "..." --port 8800.
GET http://localhost:8800/v1/tools -> OpenAI-format tools array
POST http://localhost:8800/v1/execute -> {"name": "...", "arguments": {...}}
<- {"content": "...", "is_error": false}
That's the entire surface area. No config file, no auth layer, no streaming — just tool listing and tool execution, converted.
Use cases
- Reuse one MCP server across two agent stacks. You already have an MCP filesystem/database/SaaS server running for Claude — point an OpenAI-function-calling agent at the same server without writing a second integration.
- Evaluate or migrate agent frameworks. Testing whether an OpenAI-native framework can replace a Claude-native one? Keep your MCP tools as-is and swap only the agent loop.
- Give a non-Claude LLM your MCP tools. Any model behind an OpenAI-compatible chat API (self-hosted, Azure, local llama.cpp servers with function-calling support) can call MCP tools through this bridge.
- Prototype quickly against community MCP servers. Hundreds of MCP servers exist for APIs, databases, and dev tools; this bridge makes all of them reachable from a plain
requests/httpx+ OpenAI SDK agent loop with no MCP client code at all.
End-to-end example
import httpx
from openai import OpenAI
BRIDGE = "http://localhost:8800"
# 1. Fetch the MCP server's tools in OpenAI function-calling shape
tools = httpx.get(f"{BRIDGE}/v1/tools").json()
client = OpenAI()
messages = [{"role": "user", "content": "What files are in the project root?"}]
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
)
tool_call = response.choices[0].message.tool_calls[0]
# 2. Execute the tool call the model asked for, against the real MCP server
result = httpx.post(
f"{BRIDGE}/v1/execute",
json={"name": tool_call.function.name, "arguments": tool_call.function.arguments},
).json()
# 3. Feed the result back to the model as a normal tool message
messages.append(response.choices[0].message)
messages.append(
{"role": "tool", "tool_call_id": tool_call.id, "content": result["content"]}
)
No MCP client code in the agent at all — httpx and the OpenAI SDK are the only dependencies on the agent side.
Full request flow for the example above:
sequenceDiagram
participant Agent as OpenAI agent (httpx + openai SDK)
participant Bridge as mcp-openai-bridge
participant MCP as target MCP server
Agent->>Bridge: GET /v1/tools
Bridge-->>Agent: OpenAI tools array (cached at startup)
Agent->>Agent: chat.completions.create(tools=...)
Note over Agent: model returns a tool_call
Agent->>Bridge: POST /v1/execute {name, arguments}
Bridge->>MCP: tools/call
MCP-->>Bridge: CallToolResult
Bridge-->>Agent: {"content": "...", "is_error": false}
Agent->>Agent: append as a "tool" message, continue the chat
Architecture
flowchart TB
subgraph Bridge["mcp-openai-bridge serve --stdio/--http --port N"]
direction LR
Client["MCP client\n(mcp SDK ClientSession)"]
Cache[("cached tools array")]
Web["aiohttp.web\nGET /v1/tools\nPOST /v1/execute"]
Client -- "tools/list, once at startup" --> Cache
Web -- "reads" --> Cache
Web -- "tools/call, per /v1/execute" --> Client
end
MCP["target MCP server"]
Client <--> MCP
One MCP connection, opened once and held for the process lifetime. Tools are listed once at startup and cached — no live refresh, no reconnect logic. If the target MCP server restarts, restart the bridge.
Components
| File | Responsibility |
|---|---|
cli.py |
argparse entry point; wires --stdio/--http/--port, starts the server |
mcp_client.py |
thin async wrapper around the official mcp SDK's ClientSession (stdio or streamable-HTTP transport) |
convert.py |
MCP inputSchema → OpenAI function.parameters; inlines $defs/$ref |
server.py |
the two aiohttp.web routes |
Schema conversion
MCP tool schemas (especially ones generated from Pydantic models, as most
Python MCP servers do) commonly use $defs/$ref for nested objects.
OpenAI function-calling schemas are expected to be self-contained, so the
bridge inlines every $ref before serving a tool.
Given an MCP tool with this inputSchema:
{
"type": "object",
"properties": {
"name": { "type": "string", "title": "Name" },
"address": { "$ref": "#/$defs/Address" }
},
"required": ["name", "address"],
"$defs": {
"Address": {
"type": "object",
"properties": {
"city": { "type": "string", "title": "City" },
"zip_code": { "type": "string", "title": "Zip Code" }
},
"required": ["city", "zip_code"],
"title": "Address"
}
}
}
/v1/tools serves it fully inlined, with $defs gone (real output, captured
from selfcheck.py's nested-args tool):
{
"type": "function",
"function": {
"name": "describe_person",
"description": "Describe a person and their address.",
"parameters": {
"type": "object",
"properties": {
"name": { "type": "string", "title": "Name" },
"address": {
"type": "object",
"properties": {
"city": { "type": "string", "title": "City" },
"zip_code": { "type": "string", "title": "Zip Code" }
},
"required": ["city", "zip_code"],
"title": "Address"
}
},
"required": ["name", "address"],
"title": "describe_personArguments"
}
}
}
Edge cases:
| MCP schema feature | Bridge behavior |
|---|---|
$ref to a sibling $defs entry |
Inlined recursively |
Same $defs entry referenced from two places (a "diamond") |
Inlined at each site independently — no error, some size duplication |
Circular $ref (A → B → A) |
Rejected with UnsupportedSchemaError at startup — not flattened, not silently truncated |
$ref outside #/$defs/... (e.g. a remote URI) |
Rejected with UnsupportedSchemaError — v1 only supports the local-defs form every MCP SDK actually emits |
| Non-text tool result content (images, embedded resources) | JSON-stringified into /v1/execute's content field — documented lossy conversion, not an error |
/v1/execute error handling
Every MCP error — an unknown tool name, a tool that raises, a transport
failure — is folded into a normal 200 response:
{ "content": "<error message>", "is_error": true }
This is deliberate: a failed tool call is a routine event in an agent's
tool-use loop (the model sees it and can retry or adjust), not an HTTP-layer
failure. The bridge never returns a non-2xx status from /v1/execute.
Performance
mcp-openai-bridge adds one JSON-over-HTTP hop plus one JSON-Schema
conversion in front of whatever the underlying MCP server already costs. To
put a number on that fixed overhead (isolated from real transport/network
variance), selfcheck.py's harness was extended into a micro-benchmark:
an in-process fake MCP server, 200 calls per scenario, measured after a
20-call warmup.
| Path | mean | p50 | p95 |
|---|---|---|---|
POST /v1/execute (HTTP → bridge → in-memory MCP call) |
14.5 ms | 14.2 ms | 17.9 ms |
Direct session.call_tool() (in-memory MCP, no HTTP) |
3.6 ms | 3.1 ms | 5.6 ms |
GET /v1/tools (served from cache, no MCP call) |
6.5 ms | 6.3 ms | 8.5 ms |
Methodology: measured on the developer's machine using aiohttp's
in-process TestClient/TestServer (loopback, no real socket) against an
in-memory MCP session (mcp.shared.memory) — this isolates the bridge's
own overhead from MCP-transport cost, at the expense of not reflecting a
real stdio subprocess or networked streamable-HTTP server. In production,
absolute latency is dominated by whatever transport the target MCP server
uses (subprocess spawn + pipe I/O for --stdio, real network round-trips
for --http) — the bridge's own added cost is the ~10 ms delta between the
first two rows above, i.e. one HTTP round trip plus one schema lookup from
an in-memory cache. GET /v1/tools never touches the MCP server after
startup, so its cost is pure aiohttp routing + JSON serialization of the
cached array.
Re-run it yourself: python benchmarks/overhead.py (a ~50-line variant of
selfcheck.py that swaps the assertions for time.perf_counter() samples
around the same in-process fake FastMCP server + aiohttp TestClient
harness). Numbers above are indicative; absolute latency on your machine
will vary, but the delta between the first two rows is the bridge's own
added cost.
Installation
pipx install mcp-openai-bridge
Or from source:
git clone https://github.com/Victorchatter/mcp-openai-bridge
cd mcp-openai-bridge
pip install -e .
Requires Python >= 3.10. Dependencies: mcp (the official MCP SDK) and
aiohttp. No telemetry, no network calls beyond the target MCP server and
the bridge's own local HTTP endpoint.
CLI reference
mcp-openai-bridge serve --stdio "<command>" # launch and bridge a stdio MCP server
mcp-openai-bridge serve --http <url> # bridge an MCP server over streamable-HTTP
mcp-openai-bridge serve ... --port 8800 # HTTP port (default 8800)
mcp-openai-bridge serve ... --host 127.0.0.1 # interface to bind (default 127.0.0.1)
--stdio and --http are mutually exclusive; exactly one is required.
There is no config file in v1 — everything is a flag, nothing to keep in
sync between a config and the command line.
Verifying your install
python selfcheck.py
Spins up a fake in-process MCP server (a simple tool, a nested-object tool
to exercise $defs/$ref inlining, and a tool that always raises),
exercises /v1/tools and /v1/execute against it through the real
aiohttp app, and asserts the exact expected shapes. No pytest, no
fixtures — plain assert statements, exit code 0 on success.
Scope (v1) and roadmap
In v1:
mcp2openaidirection: list an MCP server's tools, serve them as OpenAI function-calling tools, execute tool calls against it.--stdioand streamable---httpMCP transports.$defs/$refinlining for nested-object schemas.
Explicitly out of v1 (not partially built, not stubbed — simply not present):
openai2mcp(the reverse direction: expose an OpenAI-function-calling toolset as an MCP server). Natural follow-up if there's demand.- Streaming tool results.
- Auth/secrets for either the target MCP server or the bridge's own HTTP endpoint — run it on localhost / behind your own network boundary.
- MCP resources and prompts — only
tools/*is bridged. - Live tool-list refresh — restart the bridge if the target server's tool set changes.
Privacy & security
Fully local-first: the bridge only talks to the MCP server you point it at
and serves its own HTTP endpoint on the host/port you choose. No telemetry,
no external calls, no data leaves the machine unless the MCP server or the
agent you wire up to /v1/tools//v1/execute sends it somewhere.
Two things v1 does for you, and one it deliberately doesn't:
- Binds to
127.0.0.1by default. Pass--host 0.0.0.0only if you specifically need another host to reach it, and understand that anything on your network can then call/v1/executeunauthenticated. - Rejects requests whose
Hostheader isn't a loopback name (localhost/127.0.0.1/::1), independent of which interface it's bound to. Without this, a malicious webpage open in your browser could DNS-rebind an attacker-controlled hostname to127.0.0.1and drive/v1/execute— the tool-call endpoint — from ordinary page JavaScript while the bridge happens to be running. This check closes that off at effectively no cost. - No auth layer (API key, token, etc.) — out of scope for v1, per the design spec. Anything that can reach the loopback interface on the chosen port can call any bridged tool. Don't run it on a shared/multi-tenant host, and don't bind it beyond loopback unless you've added your own auth in front of it (e.g. a reverse proxy).
Versioning
0.1.0 — first release. Semantic versioning from here.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_openai_bridge-0.1.0.tar.gz.
File metadata
- Download URL: mcp_openai_bridge-0.1.0.tar.gz
- Upload date:
- Size: 23.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e1ae8762fec7bd1009f369635bead3ac4260711d3e15b298bcb88174ac9890a6
|
|
| MD5 |
8b333305d9510118cf06d0037a4b0e01
|
|
| BLAKE2b-256 |
0d945b537aa36308d673408bc4ace4d2a53f2362f69ee834360d06e554fa58a5
|
File details
Details for the file mcp_openai_bridge-0.1.0-py3-none-any.whl.
File metadata
- Download URL: mcp_openai_bridge-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
14def27eb6494b4a94490d1a908bdf477fdfe894744398a7d9c6266938bb51fe
|
|
| MD5 |
15a9809948624b0b3e5cd1f00fad031e
|
|
| BLAKE2b-256 |
082644aa23a153e0b7ca01ae566df48134459798a6459c095e27ffa5b3c9821e
|