Metadata-Version: 2.3
Name: mcp-scribe
Version: 0.2.0
Summary: Transcribe any OpenAPI schema into a production-grade MCP server
License: Apache-2.0
Keywords: mcp,model context protocol,openapi,swagger,openapi to mcp,mcp server,mcp server generator,api to mcp,rest to mcp,tool calling,llm tools,agents,agent tools,llms,code generation,api client,json schema,openapi 3.1,swagger 2.0,streamable http,stdio,multi-tenant,production-grade
Author: Kye Gomez
Author-email: kye@swarms.world
Requires-Python: >=3.10,<4.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Software Development :: Code Generators
Classifier: Typing :: Typed
Provides-Extra: http
Requires-Dist: PyYAML (>=6.0)
Requires-Dist: httpx[http2] (>=0.27)
Requires-Dist: mcp (>=2.0.0,<3.0.0)
Requires-Dist: pydantic (>=2.7)
Requires-Dist: starlette (>=0.37) ; extra == "http"
Requires-Dist: typer (>=0.12)
Requires-Dist: uvicorn (>=0.30) ; extra == "http"
Project-URL: Documentation, https://github.com/kyegomez/mcp-scribe#readme
Project-URL: Homepage, https://github.com/kyegomez/mcp-scribe
Project-URL: Repository, https://github.com/kyegomez/mcp-scribe
Description-Content-Type: text/markdown

## MCP Scribe

![MCP SCRIBE LOGO](img.png)

[![Swarms GitHub](https://img.shields.io/badge/Swarms-GitHub-181717?style=for-the-badge&logo=github&logoColor=white)](https://github.com/kyegomez/swarms)
[![Swarms website](https://img.shields.io/badge/Website-swarms.ai-0A84FF?style=for-the-badge&logo=safari&logoColor=white)](https://swarms.ai)
[![Discord](https://img.shields.io/badge/Discord-Join%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/SSj7FyRSwy)
[![Twitter](https://img.shields.io/badge/Twitter-@swarms_corp-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white)](https://twitter.com/swarms_corp)


Point it at an OpenAPI schema. Get an MCP server.

Every operation in the spec becomes a tool a model can call, with the JSON Schema, the
credentials, the retries, the rate limiting, and the response shaping already handled.

## Install

```bash
pip install mcp-scribe
```

Or from source, as a global CLI:

```bash
git clone https://github.com/kyegomez/mcp-scribe && cd mcp-scribe
uv tool install --editable ".[http]"
```

## Deploy

One command. Spec in, server up.

```bash
mcp-scribe deploy https://api.swarms.world/openapi.json --port 8000
```

```
╭────────────────────────────────────────────────────────────────────────────────────────────────╮
│    ▄  █  ▄     mcp-scribe                                                                      │
│   ▄███████▄    Swarms API 1.0.0  ·  23 tools                                                   │
│  ██▄█████▄██   https://api.swarms.world                                                        │
│  ▀ █▄   ▄█ ▀                                                                                   │
├────────────────────────────────────────────────────────────────────────────────────────────────┤
│  ▸ mcp       http://localhost:8000/mcp                                                         │
│  ▸ health    http://localhost:8000/health                                                      │
│  ▸ auth      caller-supplied (x-api-key), required                                             │
├────────────────────────────────────────────────────────────────────────────────────────────────┤
│  connect a client                                                                              │
│  claude mcp add --transport http swarms http://localhost:8000/mcp --header "x-api-key: <key>"  │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
```

`deploy` is `serve` with the defaults a shared server wants: HTTP transport, stateless
sessions, a `0.0.0.0` bind, a `/health` probe, and caller credential passthrough
switched on automatically when you supply no key of your own. Everything is
overridable — `--port`, `--host`, `--path`, `--rate-limit`, `--timeout`, `--read-only`,
`--include-tag`, `--stateful`.

Hold the credential server-side instead, and passthrough turns itself off:

```bash
mcp-scribe deploy https://api.swarms.world/openapi.json \
    --port 8000 --env-file .env --api-key-env SWARMS_API_KEY --api-key-name x-api-key
```

## Install into a client

For a local, personal server, skip the ports entirely:

```bash
mcp-scribe install --spec https://api.swarms.world/openapi.json \
    --api-key sk-... --api-key-name x-api-key
```

```
Building the server…
  Swarms API 1.0.0 — 23 tool(s) from 25 operation(s)
✓ registered with Claude Code (scope: user)

Server name: swarms_api
Credential parameters hidden from the model: x-api-key
Restart your MCP client to pick up the new server.
```

It builds the server first and only writes config if that succeeds, so you never
register something that fails on first launch. Client configs are merged and backed up,
never rewritten. `--client` takes `auto` (default), `claude-code`, `claude-desktop`,
`cursor`, `project` (a local `.mcp.json`), `all`, or `print`. `--dry-run` shows the JSON
without touching anything.

### Keep the key out of the config

MCP clients launch servers with a bare environment — they inherit no shell variables and
expand nothing — so credentials normally get pasted into the client's JSON. Point at an
env file instead and the server reads it at startup:

```bash
echo 'SWARMS_API_KEY=sk-...' >> .env    # already gitignored

mcp-scribe install --spec https://api.swarms.world/openapi.json \
    --env-file .env --api-key-env SWARMS_API_KEY --api-key-name x-api-key \
    --name swarms
```

The registered config now holds a path and no secret:

```json
{
  "command": "/abs/path/to/mcp-scribe",
  "args": ["serve", "--spec", "https://api.swarms.world/openapi.json",
           "--env-file", "/abs/path/.env",
           "--api-key-env", "SWARMS_API_KEY", "--api-key-name", "x-api-key"]
}
```

`--env-file` and `--api-key-env` work on `serve`, `deploy`, `inspect`, and `call` too.
Real environment variables win over the file, so an exported value still overrides.

## Look before you wire

```bash
mcp-scribe inspect --spec https://api.swarms.world/openapi.json
```

```
Swarms API 1.0.0
  source:   https://api.swarms.world/openapi.json
  base URL: https://api.swarms.world
  tools:    23 of 25 operations

  check_swarm_types_v1_swarms_available_get   GET  /v1/swarms/available     1 arg(s), 1 required
  run_swarm_v1_swarm_completions_post         POST /v1/swarm/completions   22 arg(s), 1 required
  run_agent_v1_agent_completions_post         POST /v1/agent/completions    7 arg(s), 2 required
```

That required argument is the API key — this spec declares `x-api-key` as a required
header parameter on every operation, so without a credential the model would have to
invent one. Supply it and the parameter disappears:

```bash
mcp-scribe inspect --spec https://api.swarms.world/openapi.json \
    --env-file .env --api-key-env SWARMS_API_KEY --api-key-name x-api-key
```

```
  check_swarm_types_v1_swarms_available_get   GET  /v1/swarms/available     0 arg(s)
  run_swarm_v1_swarm_completions_post         POST /v1/swarm/completions   21 arg(s)
  run_agent_v1_agent_completions_post         POST /v1/agent/completions    6 arg(s), 1 required
```

If a credential still shows up in that listing, it isn't wired up.

## Debug from the terminal

`call` runs the exact code path the server uses:

```bash
mcp-scribe call run_agent_v1_agent_completions_post \
    --spec https://api.swarms.world/openapi.json \
    --env-file .env --api-key-env SWARMS_API_KEY --api-key-name x-api-key \
    --args '{"task": "What is 2+2?",
             "agent_config": {"agent_name": "calc", "model_name": "gpt-4o-mini"}}' \
    --dry-run
```

```json
{
  "method": "POST",
  "url": "https://api.swarms.world/v1/agent/completions",
  "query": [],
  "headers": {
    "Accept": "application/json, */*;q=0.1",
    "x-api-key": "<redacted>"
  },
  "body": {
    "agent_config": {"agent_name": "calc", "model_name": "gpt-4o-mini"},
    "task": "What is 2+2?"
  }
}
```

Drop `--dry-run` to actually send it.

## From Python

```python
import asyncio
from mcp_scribe import Settings, build_server

settings = Settings.model_validate({
    "spec": {"url": "https://api.swarms.world/openapi.json"},
    "auth": [{"type": "api_key", "name": "x-api-key", "api_key": "sk-..."}],
})

async def main():
    app = await build_server(settings)
    try:
        await app.run_stdio()
    finally:
        await app.aclose()

asyncio.run(main())
```

Serving over HTTP instead, with a health probe:

```python
import asyncio, uvicorn
from mcp_scribe import Settings, build_server

settings = Settings.model_validate({
    "spec": {"url": "https://api.swarms.world/openapi.json"},
    "transport": {"kind": "http", "host": "0.0.0.0", "port": 8000, "stateless": True},
    "passthrough": {"enabled": True, "required": True},
})

async def main():
    app = await build_server(settings)
    config = uvicorn.Config(app.http_app(health=True), host="0.0.0.0", port=8000)
    try:
        await uvicorn.Server(config).serve()
    finally:
        await app.aclose()

asyncio.run(main())
```

Inspecting the generated tools without starting anything:

```python
import asyncio
from mcp_scribe import Settings, load_toolset

async def main():
    settings = Settings.model_validate({
        "spec": {"url": "https://api.swarms.world/openapi.json"},
        "filters": {"include_tags": ["Agents"]},
    })
    toolset, spec = await load_toolset(settings)
    for tool in toolset.tools:
        required = tool.input_schema.get("required", [])
        print(f"{tool.name:60} {tool.signature:40} {required}")

asyncio.run(main())
```

Calling one tool directly, no MCP client in the loop:

```python
import asyncio
from mcp_scribe import Settings, load_toolset
from mcp_scribe.runtime.executor import HTTPExecutor

async def main():
    settings = Settings.model_validate({
        "spec": {"url": "https://api.swarms.world/openapi.json"},
        "auth": [{"type": "api_key", "name": "x-api-key", "api_key": "sk-..."}],
    })
    toolset, _ = await load_toolset(settings)
    async with HTTPExecutor(settings, toolset.base_url) as executor:
        output = await executor.call(toolset.get("list_models_v1_models_get"), {})
        print(output.is_error, output.content[0].text[:200])

asyncio.run(main())
```

Examples: <a href="examples/swarms_api.py">swarms_api.py</a> ·
<a href="examples/swarms_api.yaml">swarms_api.yaml</a> ·
<a href="examples/petstore_readonly.py">petstore_readonly.py</a>

## How it works

```
spec url  ->  fetch  ->  normalize  ->  lower  ->  tools  ->  serve
                 |           |            |          |          |
              json/yaml   swagger 2.0   internal   JSON      stdio or
              $refs       -> openapi 3   IR        Schema    streamable http
              caching     dialect fixes            2020-12
```

**fetch** — url, file, or stdin. JSON or YAML. External `$ref` documents are collected in
one pass and loaded concurrently, so resolution afterwards is synchronous.

**normalize** — Swagger 2.0 is converted up to OpenAPI 3. `nullable: true` becomes a type
union, boolean `exclusiveMinimum` becomes a number, `allOf` of plain objects is
flattened. Component schemas land in `$defs` and are referenced, so recursive models stay
finite.

**lower** — every operation becomes an `Operation`: parameters with their
`style`/`explode` rules resolved, one chosen request media type, path-level parameters
inherited. Nothing downstream ever touches a raw OpenAPI dict again.

**tools** — one tool per operation. Two decisions matter here:

- *Credential parameters are hidden.* Specs routinely declare the API key as a required
  header parameter (FastAPI does this by default). Configure the credential and the
  parameter vanishes from the schema — the runtime injects it. The model is never asked
  to produce a secret it does not have.
- *Simple bodies are flattened.* `{"task": "..."}` beats `{"body": {"task": "..."}}` for
  tool-calling accuracy. Recursive, huge, non-object, or colliding bodies stay nested.

**serve** — a tool call becomes one HTTP request on a warm connection. Around it:
full-jitter exponential backoff honoring `Retry-After`, a per-host circuit breaker, a
token bucket, and a response renderer that returns structured JSON when the API returns
JSON and truncates with a hint when it returns 40MB.

## What it handles

| | |
| --- | --- |
| OpenAPI 3.0 / 3.1, Swagger 2.0 | converted up front |
| `$ref`, external and recursive | prefetched, emitted as `$defs` |
| `style` / `explode` | the full matrix — `deepObject`, `pipeDelimited`, `matrix`, `label` |
| bodies | json, form, multipart with binary fields, text, octet-stream |
| auth | api key (header/query/cookie), bearer, basic, oauth2 client credentials, static headers |
| secrets | `.env` files, `MCP_SCRIBE_*` env vars, `${VAR}` in config, never in tool schemas |
| retries | idempotent by default, `Retry-After`, total wall-clock budget |
| failure | per-host circuit breaker, token bucket, concurrency ceiling |
| transports | stdio, streamable http, `/health` probe |
| multi-tenant | per-caller credential passthrough with allowlisting |

## Sharing one server between callers

A stdio server is a personal adapter — one process per user, launched by that user, so
the key it holds *is* the caller's. A shared HTTP server is different: one held key would
bill every caller to the operator's account. Passthrough lets each caller carry their
own credential.

```bash
mcp-scribe deploy $SPEC --port 8000        # passthrough is the default here
```

```
Alice ──POST /mcp  x-api-key: sk-alice──▶ server ──x-api-key: sk-alice──▶ upstream API
Bob   ──POST /mcp  x-api-key: sk-bob────▶ server ──x-api-key: sk-bob────▶ upstream API
```

Clients already speak it:

```bash
claude mcp add --transport http swarms https://your-host/mcp --header "x-api-key: sk-..."
```

Credentials travel as an argument through a single call and are never stored on the
shared executor, so concurrent callers cannot cross. They are redacted from logs and from
`--dry-run` output. Give the server its own key and it falls back to that when a caller
sends none, so the same binary serves both models.

Only allowlisted headers are forwarded, and transport headers (`Cookie`, `Host`,
`Mcp-Session-Id`, …) never are. `authorization` is **not** forwarded by default: when the
MCP server is itself behind OAuth, that header carries the token minted for *this*
server, and relaying it would hand a third-party API a credential meant for you. Opt in
with `--passthrough-header authorization` when the upstream genuinely expects it.

For a public multi-tenant service, the MCP authorization spec (OAuth 2.1) is the real
answer; passthrough is the pragmatic one.

## Trimming the surface

Two hundred tools is worse than twenty. Filters compose, and work on every command:

```bash
mcp-scribe deploy $SPEC \
    --include-tag Agents --include-tag Swarms \
    --exclude-path '^/internal' \
    --read-only
```

## Ship it

```bash
mcp-scribe generate --spec $SPEC -o ./my-server --freeze
```

```
my-server/
├── server.py          thin entrypoint
├── config.yaml        every setting, ${VAR} placeholders for secrets
├── openapi.json       vendored by --freeze, so startup needs no network
├── Dockerfile         non-root, slim base
├── requirements.txt
├── mcp.json           paste into a client
├── .env.example
└── README.md          the tool table for this API
```

```bash
cd my-server && docker build -t my-server . && docker run --rm -i --env-file .env my-server
```

Or containerize the CLI directly:

```bash
docker run -p 8000:8000 mcp-scribe \
    deploy https://api.swarms.world/openapi.json --host 0.0.0.0 --port 8000
```

## Configuration

Defaults < config file < environment < flags. Every value in
<a href="src/mcp_scribe/config.py">config.py</a> is reachable from all three.

```yaml
spec:
  url: https://api.swarms.world/openapi.json
  cache_ttl: 3600          # survive a cold start without the network
  refresh_interval: 0      # >0 re-fetches and hot-swaps the toolset

auth:
  - type: api_key
    name: x-api-key
    api_key: ${SWARMS_API_KEY}   # read from the environment at startup

passthrough:
  enabled: false           # forward each caller's own credentials instead
  headers: [x-api-key]
  required: false

http:
  timeout: { read: 300.0, connect: 10.0 }
  http2: true

retry:
  max_attempts: 3
  retry_non_idempotent: false    # never re-send a billable POST
  total_budget: 90.0

rate_limit:
  requests_per_second: 5
  max_concurrency: 8

filters:
  include_tags: [Agents, Swarms]
  read_only: false

schema:
  body_mode: auto          # flatten simple bodies, nest complex ones
  inline_refs: false       # true for clients that cannot follow $ref

response:
  max_bytes: 120000
```

```bash
mcp-scribe deploy --config swarms.yaml --port 8000
```

Every scalar also has an env var: `MCP_SCRIBE_SPEC`, `MCP_SCRIBE_API_KEY`,
`MCP_SCRIBE_PORT`, `MCP_SCRIBE_RATE_LIMIT_RPS`, `MCP_SCRIBE_INCLUDE_TAGS`,
`MCP_SCRIBE_PASSTHROUGH`, and so on. `MCP_SCRIBE_HEADER_X_TENANT_ID=acme` becomes an
`X-Tenant-Id: acme` header on every upstream request.

## Notes

- Tool output is truncated to `response.max_bytes` with a note telling the model how to
  narrow the request. Context windows are a resource.
- `outputSchema` is opt-in (`--output-schema`). Clients must reject responses that do not
  validate against it, and real APIs drift from their specs. Structured content is
  returned either way.
- Arguments get light coercion — `"5"` for an integer, a JSON string for an object — and
  then the API is the authority. Imperfect specs should not block calls the API would
  have accepted.
- With `spec.refresh_interval` set, the toolset is re-fetched and hot-swapped in place.
  Clients that cache tool listings pick it up on their next `tools/list`.
- Banners go to stderr and disable colour when the output is not a terminal, when
  `NO_COLOR` is set, or when `TERM=dumb`. stdout carries JSON-RPC framing on stdio and
  stays clean.

## Development

```bash
poetry install --all-extras --with lint,test
poetry run pytest -q                 # 201 tests
poetry run ruff check src tests && poetry run ruff format --check src tests
poetry run mypy src/mcp_scribe
poetry build
```

## Todo

- [ ] OAuth2 authorization code flow with local callback
- [ ] Credential validation at `install` time, before writing client config
- [ ] Response streaming for `text/event-stream` endpoints
- [ ] AsyncAPI and gRPC reflection as additional front ends
- [ ] Prompt generation from operation examples

## Citations

```bibtex
@misc{mcpscribe2026,
    title   = {mcp-scribe: production-grade MCP servers from OpenAPI schemas},
    author  = {Gomez, Kye},
    year    = {2026},
    url     = {https://github.com/kyegomez/mcp-scribe}
}
```

```bibtex
@misc{mcp2024,
    title   = {Model Context Protocol},
    author  = {Anthropic},
    year    = {2024},
    url     = {https://modelcontextprotocol.io}
}
```

