Skip to main content

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.

Python 3.10+ License: MIT Local-first, no telemetry v0.1.0


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 mcp2openai command — 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 (ABA) 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:

  • mcp2openai direction: list an MCP server's tools, serve them as OpenAI function-calling tools, execute tool calls against it.
  • --stdio and streamable---http MCP transports.
  • $defs/$ref inlining 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.1 by default. Pass --host 0.0.0.0 only if you specifically need another host to reach it, and understand that anything on your network can then call /v1/execute unauthenticated.
  • Rejects requests whose Host header 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 to 127.0.0.1 and 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcp_openai_bridge-0.1.0.tar.gz (23.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcp_openai_bridge-0.1.0-py3-none-any.whl (12.9 kB view details)

Uploaded Python 3

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

Hashes for mcp_openai_bridge-0.1.0.tar.gz
Algorithm Hash digest
SHA256 e1ae8762fec7bd1009f369635bead3ac4260711d3e15b298bcb88174ac9890a6
MD5 8b333305d9510118cf06d0037a4b0e01
BLAKE2b-256 0d945b537aa36308d673408bc4ace4d2a53f2362f69ee834360d06e554fa58a5

See more details on using hashes here.

File details

Details for the file mcp_openai_bridge-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for mcp_openai_bridge-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 14def27eb6494b4a94490d1a908bdf477fdfe894744398a7d9c6266938bb51fe
MD5 15a9809948624b0b3e5cd1f00fad031e
BLAKE2b-256 082644aa23a153e0b7ca01ae566df48134459798a6459c095e27ffa5b3c9821e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page