MCP Scribe
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
pip install mcp-scribe
Or from source, as a global CLI:
git clone https://github.com/kyegomez/mcp-scribe && cd mcp-scribe
uv tool install --editable ".[http]"
Deploy
One command. Spec in, server up.
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:
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:
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:
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:
{
"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
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:
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:
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
{
"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
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:
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:
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:
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: swarms_api.py · swarms_api.yaml · petstore_readonly.py
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.
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:
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:
mcp-scribe deploy $SPEC \
--include-tag Agents --include-tag Swarms \
--exclude-path '^/internal' \
--read-only
Ship it
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
cd my-server && docker build -t my-server . && docker run --rm -i --env-file .env my-server
Or containerize the CLI directly:
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 config.py is reachable from all three.
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
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_byteswith a note telling the model how to narrow the request. Context windows are a resource. outputSchemais 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_intervalset, the toolset is re-fetched and hot-swapped in place. Clients that cache tool listings pick it up on their nexttools/list. - Banners go to stderr and disable colour when the output is not a terminal, when
NO_COLORis set, or whenTERM=dumb. stdout carries JSON-RPC framing on stdio and stays clean.
Development
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
installtime, before writing client config - Response streaming for
text/event-streamendpoints - AsyncAPI and gRPC reflection as additional front ends
- Prompt generation from operation examples
Citations
@misc{mcpscribe2026,
title = {mcp-scribe: production-grade MCP servers from OpenAPI schemas},
author = {Gomez, Kye},
year = {2026},
url = {https://github.com/kyegomez/mcp-scribe}
}
@misc{mcp2024,
title = {Model Context Protocol},
author = {Anthropic},
year = {2024},
url = {https://modelcontextprotocol.io}
}
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mcp_scribe-0.2.0.tar.gz.
File metadata
- Download URL: mcp_scribe-0.2.0.tar.gz
- Upload date:
- Size: 87.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
poetry/2.1.3 CPython/3.12.3 Darwin/24.6.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37d4d34dc2c74a0d29872219e0842afbeec7bcdf05dc6ae5ee63637287b8c688
|
|
| MD5 |
931d0c796114d533fb0c4094dd2cd825
|
|
| BLAKE2b-256 |
835406a6efcde5418ce599d0f4faf893e0c5b731ad662bc77251fd8d60b69087
|
File details
Details for the file mcp_scribe-0.2.0-py3-none-any.whl.
File metadata
- Download URL: mcp_scribe-0.2.0-py3-none-any.whl
- Upload date:
- Size: 96.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
poetry/2.1.3 CPython/3.12.3 Darwin/24.6.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
983ab9b3868f6b4f104fbeaf0903162e072c01b91a42e10664302586c7460bce
|
|
| MD5 |
31a2864bc9bc86bb888126ca2a60e6ed
|
|
| BLAKE2b-256 |
8fe8ed07be07a93cb120962b6286cddc6fdc29200145794cc8336f3d338f75e9
|