Metadata-Version: 2.4
Name: openapi-mcp-hub
Version: 0.4.1
Summary: Turn any OpenAPI spec — or a live PostgreSQL/MySQL database — into production-ready MCP endpoints. One process, one port, zero code.
Author: openapi-mcp-hub contributors
License-Expression: MIT
Project-URL: Homepage, https://github.com/zhangjk1207/openapi-mcp-hub
Project-URL: Repository, https://github.com/zhangjk1207/openapi-mcp-hub
Project-URL: Issues, https://github.com/zhangjk1207/openapi-mcp-hub/issues
Project-URL: Changelog, https://github.com/zhangjk1207/openapi-mcp-hub/blob/main/CHANGELOG.md
Project-URL: Documentation, https://github.com/zhangjk1207/openapi-mcp-hub/blob/main/USAGE.md
Keywords: mcp,model-context-protocol,openapi,swagger,gateway,ai-agents,llm-tools,claude
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Software Development :: Code Generators
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.110
Requires-Dist: uvicorn>=0.29
Requires-Dist: mcp<3,>=2
Requires-Dist: jsonschema>=4
Requires-Dist: httpx>=0.27
Requires-Dist: brotli>=1.1
Requires-Dist: zstandard>=0.18
Requires-Dist: pydantic>=2.6
Requires-Dist: PyYAML>=6.0
Requires-Dist: asyncpg>=0.29
Requires-Dist: aiomysql>=0.2
Requires-Dist: PyJWT[crypto]>=2.8
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Dynamic: license-file

# openapi-mcp-hub

**Turn any OpenAPI spec — or a live PostgreSQL / MySQL database — into production-ready MCP endpoints. One process. One port. Zero code.**

[![CI](https://github.com/zhangjk1207/openapi-mcp-hub/actions/workflows/ci.yml/badge.svg)](https://github.com/zhangjk1207/openapi-mcp-hub/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-6e7bf4.svg)](https://github.com/zhangjk1207/openapi-mcp-hub/blob/main/LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-3776ab.svg)](https://github.com/zhangjk1207/openapi-mcp-hub/blob/main/pyproject.toml)
[![中文文档](https://img.shields.io/badge/README-中文-red.svg)](https://github.com/zhangjk1207/openapi-mcp-hub/blob/main/README.zh-CN.md)

![demo](https://raw.githubusercontent.com/zhangjk1207/openapi-mcp-hub/main/docs/demo.gif)

> Install: `pip install openapi-mcp-hub` or `uvx openapi-mcp-hub` · Python 3.10+ · MIT


POST an `openapi.json` to the gateway and you instantly get an MCP endpoint any agent can call:

```
POST /services        →   http://your-gateway:20033/mcp/{service-name}
```

No wrapper code. No per-spec deployment. No port sprawl. Register as many services as you like — REST APIs or read-only databases — all behind a single port, hot-loaded the moment they register.

> 🇨🇳 [中文文档看这里](README.zh-CN.md) · 📖 Full usage guide: [USAGE.md](USAGE.md) (中文)

---

## Why

- **OpenAPI/Swagger** is the de-facto standard for describing REST APIs — most internal services already have a spec.
- **MCP** (Model Context Protocol) is how agents consume tools — Claude Code and friends speak it natively over Streamable HTTP.
- Hand-writing MCP wrappers for every endpoint is repetitive and drifts out of sync. Running *one MCP server per spec* means port sprawl and linear ops cost.

**openapi-mcp-hub is a gateway**: one resident process, one port, path-level multi-tenancy (`/mcp/{service}`). Registration takes effect immediately — no restarts.

Public 2026 MCP ecosystem reports keep finding the same pain: only ~8.5% of MCP servers use OAuth, and tool-poisoning / rug-pull changes go unnoticed until an agent misbehaves. That is why this gateway treats **data-plane auth and tool-change visibility as first-class features**, not enterprise add-ons.

```text
             ┌──────────── admin plane (admin token) ─────────────┐
             │  POST /services            register (REST or DB)   │
             │  GET  /services            list / inspect          │
             │  POST /services/preview    dry-run registration    │
             └────────────────────┬───────────────────────────────┘
                                  ▼
   Agents ── MCP (streamable HTTP / SSE) ──▶  openapi-mcp-hub gateway  ── HTTP ──▶  your APIs
   Claude Code, Cursor, custom clients        single port :20033   ── SQL ──▶  PostgreSQL / MySQL 8+
                                             per-service api_key
```

## Quickstart — 60 seconds

```bash
git clone https://github.com/zhangjk1207/openapi-mcp-hub && cd openapi-mcp-hub
docker compose up -d       # gateway + PostgreSQL + demo API, auto-registers a petstore service
```

Open **http://localhost:20033** and paste the demo admin token: `demo-admin-token` — the console shows a ready-to-use `petstore` service (fixed demo key, listed below).

Try the agent-callable endpoint right away:

```bash
curl -X POST http://localhost:20033/mcp/petstore \
  -H "X-Api-Key: psk-demo-petstore-key" -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_pets","arguments":{"limit":3}}}'
```

And in Claude Code:

```bash
claude mcp add --transport http petstore http://localhost:20033/mcp/petstore \
  --header "X-Api-Key: psk-demo-petstore-key"
```

> The demo mode (`OPENAPI_MCP_HUB_DEMO=1` in `docker-compose.yml`) uses a fixed, publicly-documented key for a friction-free tour. Turn it off (and change `OPENAPI_MCP_HUB_ADMIN_TOKEN`) before any real use — real registrations always mint a fresh one-time key.

### Register your own API

```bash
ADMIN_TOKEN=demo-admin-token

# spec_text receives the raw file content (JSON or YAML)
jq -n --rawfile s your-openapi.json '{name:"my-api", base_url:"https://api.example.com", spec_text:$s}' | \
  curl -X POST http://localhost:20033/services \
    -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" -d @-
```

Connect it to Claude Code (the response includes a ready-to-run command):

```bash
claude mcp add --transport http my-api http://localhost:20033/mcp/my-api \
  --header 'X-Api-Key: psk-xxxx'
```

That's it — your API's operations are now agent-callable tools.

## Register a database instead of an API

No REST spec needed. Point the gateway at a database (via an **environment variable name** — the DSN itself never touches disk):

```bash
export WORKFLOW_DB_URL='postgresql://ro_user:pass@db.internal.example:5432/workflow'
curl -X POST http://localhost:20033/services \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"workflow-db","db":{"url_env":"WORKFLOW_DB_URL"}}'
```

You get three tools the agent can use blind: `list_tables`, `describe_table` (schema + comments embedded), and `query` (SELECT-only).

### Read-only is enforced in six layers

| Layer | What it does |
|---|---|
| Lexical whitelist | First token must be `SELECT`/`WITH`/`EXPLAIN`; comments blocked; string literals respected |
| `READ ONLY` transactions | Server-enforced — writes fail even if they slip past the lexer (PG / MySQL 8+) |
| Outer `LIMIT` wrap | `SELECT * FROM (your SQL) sub LIMIT max_rows+1` — result size capped regardless |
| Statement timeout | `statement_timeout` (PG) / `max_execution_time` (MySQL) per pool |
| Isolated pool | Separate connection pool per service, `minsize=1, maxsize=5` |
| Table filtering | `include_tables` / `exclude_tables` at introspection time |

Credentials are referenced **by env var name only** — nothing sensitive is persisted or logged. SQL digests land in the in-memory call log for auditing.

## Features

| | |
|---|---|
| **Sources** | OpenAPI 3.0/3.1, Swagger 2.0 (auto-normalized), spec URL with SSRF allowlist, JSON/YAML upload, PostgreSQL, MySQL 8+ |
| **Transport** | Streamable HTTP + legacy SSE (`/mcp/{name}/sse`) on one port |
| **Multi-tenant security** | Per-service `api_key` (shown once, stored hashed, rotatable), admin token for the management plane |
| **MCP data-plane auth** | Static key **or JWT as an OAuth 2.1 resource server**: external IdP (RS256/ES256 + JWKS + issuer/audience checks, RFC 9728 discovery, `WWW-Authenticate` on 401) or built-in issuance (HS256, short-TTL, `aud` bound to the service) |
| **Tool-poisoning visibility** | Every update/`refresh` returns a tool-set diff (`added` / `removed` / `description_changed` / `input_schema_changed`) plus a human-review warning when descriptions change — descriptions are the LLM's instruction surface |
| **Target API auth** | Bearer / API key / Basic / **OAuth2 client_credentials** — token fetched, cached & auto-refreshed by the gateway (one 401 retry); credentials referenced by env var name only, `token_url` allowlist-checked at registration |
| **Hot registration** | Register / update / delete without restarts; failures roll back atomically; `spec_url` sources re-pull via `refresh` (key preserved) |
| **Filtering** | Path include/exclude globs, tool whitelist, `max_tools` cap, response templates |
| **Persistence** | JSON file or PostgreSQL backend, automatic one-way migration |
| **Console** | Single-file admin UI, zero build chain, zero external assets — works in air-gapped intranets |
| **Observability** | Health probes, in-memory ring call log (last 20/service), call stats (ok-rate / p50 / p95 / top tools), structured error codes (`-32000`/-`32602`/-`32002`) |

## Comparison

| | openapi-mcp-hub | one-MCP-server-per-spec | general API gateways + MCP plugin |
|---|---|---|---|
| New service | POST a spec, ~1s | Write & deploy a wrapper | Plugin config + reload |
| Ports | **1** | N | 1 (heavyweight) |
| Database → MCP | **Built-in, read-only enforced** | Separate project | Rarely |
| Air-gapped console | **Yes, single HTML file** | — | Usually needs internet |
| MCP data-plane auth (OAuth 2.1 resource server) | **Built-in, free** | — | Kong AI Gateway: RBAC/OIDC requires the paid Enterprise tier; Zuplo: SaaS |
| Tool-poisoning visibility | **Tool-set diff + review warning on every update/refresh** | — | Rarely |
| Runtime deps | FastAPI + mcp SDK (~10 packages) | varies | Full gateway stack |

## Security posture

Everything below ships in the free, self-hosted, single-binary build:

| Layer | Mechanism |
|---|---|
| Management plane | `OPENAPI_MCP_HUB_ADMIN_TOKEN` (env only); all admin routes require it |
| Data plane (static mode) | Per-service `X-Api-Key`, shown once, stored as SHA-256 hash, rotatable via `rotate-key` |
| Data plane (JWT mode) | `Authorization: Bearer` — **external IdP**: RS256/ES256 via JWKS (cached 300s, kid rotation aware), `iss`/`aud` enforced, algorithms whitelisted (no alg-confusion); **built-in**: HS256 with `OPENAPI_MCP_HUB_JWT_SECRET` (env only, never persisted), TTL ≤ 24h, `aud` bound to the service name. RFC 9728 metadata at `/.well-known/oauth-protected-resource/mcp/{name}` so spec-native clients (e.g. Claude Code) can discover your IdP |
| SSRF | Three layers: registration-time host allowlist, per-hop re-check on redirects, response timeouts |
| Secrets at rest | Target-API credentials referenced by env var *names* only — nothing sensitive is persisted |
| Database tools | Six-layer read-only (see above) |
| Tool poisoning | `tool_changes` diff + human-review warning whenever tool descriptions change on update/refresh |

JWT mode in three commands (built-in issuance):

```bash
export OPENAPI_MCP_HUB_JWT_SECRET="$(openssl rand -hex 32)"
# register with authn={"mode":"jwt"} (see USAGE.md for external-IdP mode)
curl -X POST $GW/services -H "Authorization: Bearer $ADMIN" -d '{"name":"crm","spec_url":"...","base_url":"...","authn":{"mode":"jwt"}}'
curl -X POST $GW/services/crm/token -H "Authorization: Bearer $ADMIN" -d '{"ttl_s":3600}'
# → use the returned token as: Authorization: Bearer <token>
```

## Console screenshots

Real console, not mockups — REST and database sources in one table, generated tool sets,
health probes and key rotation, all from a single static page: no build chain, no external
assets, works air-gapped.

**Services** — REST and database sources side by side, `/mcp/{name}` per row:

![Services list](https://raw.githubusercontent.com/zhangjk1207/openapi-mcp-hub/main/docs/screenshots/console-list.png)

**Service detail** — endpoint, generated tools with descriptions, health probe, key rotation:

![Service detail](https://raw.githubusercontent.com/zhangjk1207/openapi-mcp-hub/main/docs/screenshots/console-detail.png)

**Register a REST API** — spec upload / paste, path filters, target auth by env var name:

![Register a REST API](https://raw.githubusercontent.com/zhangjk1207/openapi-mcp-hub/main/docs/screenshots/console-register-rest.png)

**Register a database** — DSN referenced by env var name only, read-only tool trio generated:

![Register a database](https://raw.githubusercontent.com/zhangjk1207/openapi-mcp-hub/main/docs/screenshots/console-register-db.png)

## Roadmap

- [x] v0.4.0 — **JWT data-plane auth (OAuth 2.1 resource server, RFC 9728)** · tool-poisoning drift diff on update/refresh
- [x] v0.3.0 — OAuth2 client_credentials target auth · spec refresh endpoint · call stats · console auth fix
- [x] M5-1 — Database → MCP (PostgreSQL + MySQL 8, six-layer read-only)
- [ ] M5-2 — per-table tool mode, sensitive-column masking
- [ ] M5-3 — write operations (opt-in, statement allowlist)
- [ ] Metrics endpoint (Prometheus) & key expiry
- [ ] Per-tenant quotas / rate limiting
- [ ] Per-service circuit breaking

**Not implemented today** — do not plan around these: circuit breaking, per-tenant
rate limiting or quotas, HA / multi-replica coordination, Prometheus metrics,
database write operations, per-table database tools, API-key expiry. Safety isolation
today is per-service httpx client + independent timeout + connection-pool cap + the
SSRF allowlist, all inside a single process.

## Development

```bash
pip install -e ".[dev]"
pytest tests/ -q          # 178 pass; 22 opt-in integration tests skip without PG/MySQL DSNs
```

## License

[MIT](LICENSE)
