Metadata-Version: 2.5
Name: mcp-api-gateway
Version: 0.2.0
Summary: Turn any number of OpenAPI/Swagger services into a single MCP server.
Project-URL: Homepage, https://github.com/Vidmich/mcp-api-gateway
Project-URL: Repository, https://github.com/Vidmich/mcp-api-gateway
Author: Dmytro Vidmych
License: MIT License
        
        Copyright (c) 2026 Dmytro Vidmych
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: gateway,mcp,openapi,proxy,swagger
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Requires-Python: >=3.11
Requires-Dist: aiosqlite>=0.20
Requires-Dist: alembic>=1.13
Requires-Dist: cryptography>=42.0
Requires-Dist: fastapi>=0.115
Requires-Dist: httpx2>=2.5
Requires-Dist: httpx>=0.27
Requires-Dist: itsdangerous>=2.2
Requires-Dist: jinja2>=3.1
Requires-Dist: jsonschema>=4.22
Requires-Dist: mcp>=2.2
Requires-Dist: pydantic>=2.7
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: pyyaml>=6.0
Requires-Dist: sqlalchemy[asyncio]>=2.0
Requires-Dist: uvicorn[standard]>=0.30
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: openapi-spec-validator>=0.7; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.2; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: types-jsonschema>=4.22; extra == 'dev'
Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
Description-Content-Type: text/markdown

# mcp-api-gateway

[![CI](https://github.com/Vidmich/mcp-api-gateway/actions/workflows/ci.yml/badge.svg)](https://github.com/Vidmich/mcp-api-gateway/actions/workflows/ci.yml)

Turn any number of OpenAPI or Swagger services into a single MCP server — and
put the MCP servers you already have behind the same `/mcp`.

Register a service by pointing the gateway at its spec URL, tick the operations
worth exposing, and they become tools on one `/mcp` endpoint. When a model calls
one, the gateway makes the corresponding HTTP request — with the credentials you
stored for that service — and hands back the response. Register an MCP server by
its endpoint and its tools join the same list, forwarded as `tools/call` with the
credential you stored for it. It is a proxy: nothing is generated ahead of time,
nothing is cached, and a change you make in the UI is live on the next
`tools/list`.

- **`/mcp`** — one MCP endpoint (streamable HTTP) for every registered service.
- **Two kinds of upstream** — an API described by an OpenAPI or Swagger document,
  or a server that already speaks MCP over streamable HTTP. One tool list, one
  token, one Monitoring page, whichever kind is behind each name.
- **Configuration pages** — register and edit upstreams, choose operations, name
  tools, cap how fast each upstream may be called, refresh specs and tool lists
  and review what changed.
- **Monitoring page** — calls, bytes in and out, failures and throttled calls
  over time, total and per server.
- **Configuration page** — the gateway's own settings: how often specs are
  re-read, who has to sign in, whether `/mcp` requires a bearer token, and
  everything else in force with the layer it came from.
- **Both doors, closed from the browser** — the admin login and the `/mcp`
  bearer token can each be set without editing a file or restarting, and each
  takes effect on the next request. The token is kept as a digest, so the
  gateway can check it and can never show it back.
- **Optional metrics export** — push the same counters to New Relic, so a
  gateway going quiet is noticed by whatever notices everything else going
  quiet. Off unless you turn it on; counts only, never request content.
- **A built-in server, off by default** — switch it on and an MCP client can
  preview a spec or an endpoint, register an upstream of either kind and choose
  its operations without a human opening the UI. It cannot delete a server or
  read a stored credential.
- **`/api/v1`** — the same configuration actions as JSON, for scripts, with
  `kind` on every server and in every create.

Self-hosted, single process, SQLite. No Node build step, no external services.

> ### ⚠️ Read this before it is reachable by anything but you
>
> Out of the box the gateway has **no admin login and no `/mcp` token**, and it
> has **no SSRF protection** — an admin can point an upstream at `127.0.0.1` or
> any private address, and anyone who can reach `/mcp` can call it. Together
> that is an open proxy into whatever network the gateway sits in.
>
> The defaults are safe only because it binds to `127.0.0.1`. Before you change
> that, set both doors and read **[docs/security.md](docs/security.md)**.

## Install

Requires Python 3.11 or newer, on Linux, macOS or Windows.

Nothing is on PyPI yet — build a wheel and install that:

```bash
python -m pip install build && python -m build --wheel
pipx install ./dist/mcp_api_gateway-0.1.0-py3-none-any.whl
mcp-api-gateway --version
```

The distribution will be `mcp-api-gateway`, and so is the command it installs.
The only name spelled differently is the Python package you would import,
`mcp_gateway`.

[docs/install.md](docs/install.md) covers pip, pipx, editable checkouts, what the
first run creates, and upgrades.

## Quickstart

Five minutes, ending with a real MCP client listing real tools.

### 1. Start it

```bash
mkdir gateway && cd gateway
mcp-api-gateway
```

It writes `config.toml` and `data/` in that directory, migrates a fresh
database, and starts listening. Two warnings in the log say the admin pages and
`/mcp` are open — step 5 deals with that.

```
INFO:     mcp-api-gateway 0.1.0
config file:  /home/you/gateway/config.toml
listening on: http://127.0.0.1:8080
data dir:     /home/you/gateway/data
key file:     /home/you/gateway/data/keys.json
mcp endpoint: /mcp (open)
admin login:  disabled
usage export: off
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8080 (Press CTRL+C to quit)
```

### 2. Register a service

Open <http://127.0.0.1:8080/ui/servers> and press **Add a server**.

Paste a spec URL — the Swagger Petstore is a good first one, because it needs no
credentials:

```
https://petstore3.swagger.io/api/v3/openapi.json
```

Press **Fetch the spec**. Nothing has been saved yet: the gateway downloads the
document, reads every operation out of it, and shows you what it found.

### 3. Choose the operations

The picker lists all 19 operations with the tool name each would get. Two things
worth doing before saving:

- **Set the tool prefix** to something short — `petstore`. It leads every tool
  name from this service, and the default derived from the document's title
  (`swagger_petstore_-_openapi_3_0`) makes for long tool names.
- **Untick anything you would not want called.** Everything is selected by
  default; a model that can see `deletePet` can call `deletePet`.

Press **Save the server**. The list page comes back with the service registered
and its operations exposed.

### 4. Point an MCP client at it

The endpoint is `http://127.0.0.1:8080/mcp`, streamable HTTP, no token yet.

For Claude Code:

```bash
claude mcp add --transport http gateway http://127.0.0.1:8080/mcp
```

For a client configured by file, the shape is the usual one:

```json
{
  "mcpServers": {
    "gateway": {
      "type": "http",
      "url": "http://127.0.0.1:8080/mcp"
    }
  }
}
```

Once you have set a token (step 5), add it as a header:

```bash
claude mcp add --transport http gateway http://127.0.0.1:8080/mcp \
  --header "Authorization: Bearer a-long-random-string"
```

```json
{
  "mcpServers": {
    "gateway": {
      "type": "http",
      "url": "http://127.0.0.1:8080/mcp",
      "headers": { "Authorization": "Bearer a-long-random-string" }
    }
  }
}
```

A client that only speaks stdio needs a bridge —
`npx -y mcp-remote http://127.0.0.1:8080/mcp` is the usual one — but prefer a
native HTTP client where you have the choice.

The client should now list 19 tools named `petstore__addPet`,
`petstore__getPetById`, and so on. To check without a client at all:

```bash
curl -sS -X POST http://127.0.0.1:8080/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
```

```
event: message
data: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"experimental":{},"tools":{"listChanged":true}},"protocolVersion":"2025-06-18","serverInfo":{"name":"mcp-api-gateway","version":"0.1.0"}}}
```

Calling one of those Petstore tools reaches the public demo API, which is
frequently down; when it is, the call comes back as `isError: true` carrying the
upstream's own status line and body. That is the gateway working — it reports
what the upstream said rather than hiding it.

### 5. Before you leave it running

Add both doors to `config.toml` and restart:

```toml
[admin]
username = "admin"
password = "something-better-than-this"

[mcp]
auth_token = "a-long-random-string"
```

Both halves can also be set from `/ui/configuration` without a restart and
without either secret ever reaching a file — the page stores a password hash and
a token digest, never the values. `mcp-api-gateway --reset-admin` is the way back
if the login is forgotten; a forgotten token locks out your MCP clients rather
than you, and the same page opens the endpoint again.

Now `/ui` asks for a login and `/mcp` requires `Authorization: Bearer …`. See
[docs/security.md](docs/security.md) for what is still not protected — the SSRF
gap in particular — and [docs/configuration.md](docs/configuration.md) for
storing the password as a hash and the token in the environment instead.

## Putting an MCP server behind it

An upstream that already speaks MCP is registered by its endpoint rather than by
a document. The one MCP server every reader has is this gateway's own: switch
on the built-in **Gateway** server on the API Servers list — its tools configure
the gateway, so do step 5 first — and its `/mcp` is an MCP server like any
other.

Open <http://127.0.0.1:8080/ui/mcp-servers> and press **Add an MCP server**.
Paste the endpoint:

```
http://127.0.0.1:8080/mcp
```

Choose the authentication the endpoint needs — for this one, the bearer token
you set, or none if you have not yet — and press **Connect and list tools**.
Nothing is saved: the gateway connects, asks the server what it is and what it
offers, and shows the tools it listed. The picker is the same one as for an API,
with the tool's own name where the method and path would be. Set the prefix,
untick what you would not want called, and press **Save the server**.

The same thing by script is `POST /api/v1/servers` — behind the same login as
the pages — with the body it takes for a document, except that `kind` says
which and `endpoint` stands where `spec_url` would:

```json
{"kind": "mcp", "endpoint": "http://127.0.0.1:8080/mcp", "tool_prefix": "mirror"}
```

From then on the server is refreshed, monitored, throttled and disabled by the
same rules as an API — **Refresh tools** re-lists them, and a tool that appears
after the first listing waits for somebody to tick it. The gateway holds one
session open to each MCP server and reconnects when the endpoint or the
credential is changed.

## How it works

**Registering.** The spec is fetched (OpenAPI 3.0, 3.1, or Swagger 2.0 — the
last is converted), `$ref`s are resolved, every operation becomes a tool with a
JSON Schema built from its parameters and request body, and the document is
stored alongside them. For an MCP server the gateway connects to the endpoint
instead, runs `initialize` and `tools/list`, and stores each tool with the input
schema the server published.

**Naming.** A tool is `<prefix>__<operationId>` by default — or
`<prefix>__<tool name>` for an MCP server's tool. The prefix is what keeps two
services that both publish `getUser` apart, and both halves are editable per
server and per operation. A collision is reported, never silently resolved.

**Calling.** Arguments are validated against the stored schema before anything
leaves the process, path and query parameters are substituted, the server's
stored credential is applied, and the request goes out through a shared client
with a timeout and a response cap. Errors come back as `isError: true` with the
upstream's status and body, because that is usually what a model needs in order
to correct itself. A call to an MCP server's tool is forwarded as `tools/call`
on a session the gateway keeps open to it, and its result — `isError` included —
comes back as the upstream sent it.

**Refreshing.** Manually per server, or automatically on a global interval; for
an MCP server a refresh is a fresh `tools/list`. Operations that changed are
flagged, and **new operations are never enabled by themselves** — the server is
marked *Needs Attention* and waits for somebody to decide.

**Credentials.** Stored encrypted with a key in `data/keys.json`, never rendered
back into a page or an API response — only `set` / `not set` and the auth type.

## Documentation

| | |
|---|---|
| [docs/install.md](docs/install.md) | pip, pipx, checkouts, what the first run creates, upgrading |
| [docs/configuration.md](docs/configuration.md) | every setting, its default, its environment variable, its flag |
| [docs/service-setup.md](docs/service-setup.md) | systemd, launchd, NSSM, Task Scheduler, Docker, reverse proxy |
| [docs/security.md](docs/security.md) | the deliberate v1 gaps, stated plainly, and how to run it anyway |
| [docs/releasing.md](docs/releasing.md) | for whoever publishes it: tags, trusted publishing, what CI checks |
| [SPEC.md](SPEC.md) | what the thing is meant to be, in full |

## Development

```bash
git clone <repository-url> mcp-api-gateway && cd mcp-api-gateway
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
```

```bash
.venv/bin/ruff check . && .venv/bin/ruff format --check . && .venv/bin/mypy src
```

Three layers. Unit tests; end-to-end scenarios in `tests/e2e` that drive the
whole path — register a document, tick operations, list tools over `/mcp`,
call one — against a stubbed upstream; and integration tests that run a real
server on a real port and point the official MCP client at it. Nothing in the
suite needs the network.

All of it runs on every commit against CPython 3.11 through 3.14 on Linux,
macOS and Windows, and every commit also builds the wheel, installs it into an
empty virtualenv, and starts it. [docs/releasing.md](docs/releasing.md) covers
the rest of the pipeline.

## License

MIT. See [LICENSE](LICENSE).
