Metadata-Version: 2.4
Name: mcp-openai-bridge
Version: 0.1.0
Summary: Expose any MCP server's tools as OpenAI function-calling tools, and execute OpenAI tool calls back against it.
Project-URL: Homepage, https://github.com/Victorchatter/mcp-openai-bridge
Author: Victor
License: MIT
License-File: LICENSE
Keywords: agent,bridge,function-calling,mcp,openai
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Requires-Dist: aiohttp>=3.9
Requires-Dist: mcp>=1.0.0
Description-Content-Type: text/markdown

<div align="center">

# 🌉 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+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Local-first, no telemetry](https://img.shields.io/badge/local--first-no%20telemetry-green.svg)](#privacy--security)
[![v0.1.0](https://img.shields.io/badge/version-0.1.0-orange.svg)](#versioning)

</div>

<div align="center">

[Problem](#the-problem) ·
[What it does](#what-it-does) ·
[Use cases](#use-cases) ·
[Example](#end-to-end-example) ·
[Architecture](#architecture) ·
[Performance](#performance) ·
[Install](#installation) ·
[Security](#privacy--security)

</div>

---

## 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.

```mermaid
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](https://github.com/SecretiveShell/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:

```bash
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

```python
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:

```mermaid
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

```mermaid
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`:

```json
{
  "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):

```json
{
  "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:

```json
{ "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

```bash
pipx install mcp-openai-bridge
```

Or from source:

```bash
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

```bash
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](LICENSE).
