rust-web-server 18.18.0

Single-binary web server and reverse proxy: automatic HTTPS (Let's Encrypt), HTTP/1.1, HTTP/2 and HTTP/3, load balancing, auth and rate limiting, configured in one TOML file. Also a Rust library: routing, middleware, MCP server, ORM, background jobs. No hyper, no Tower; HTTP/2 and HTTP/3 framing via h2 and quinn.
Documentation
[Read Me](../README.md) > [Spec](.) > OpenAPI to MCP tools

# OpenAPI → MCP tools (18.19.0)

Planned 2026-10-31. Most services an agent should use already have an
OpenAPI document; writing an MCP tool per operation by hand is busywork.
Kong's AI MCP proxy, FastMCP's `from_openapi`, Speakeasy and Stainless
generate tools from OpenAPI; rws can do it at runtime, both for its own
routes and for upstream APIs it proxies.

## Two directions

**1. Your app's own routes** (`openapi` + MCP): routes described with
`OpenApiConfig::operation(…)` (summary, body, query and path types) become
MCP tools on the app's `McpServer`, calling the handler in-process — no
HTTP round trip, the request built from the tool arguments.

```rust,ignore
let app = App::with_state(state)
    .get("/invoices/:id", get_invoice)
    .post("/invoices", create_invoice)
    .openapi(OpenApiConfig::new("Invoices", "1.0")
        .operation("GET", "/invoices/:id", Operation::new("Get an invoice").path_param::<u64>("id")))
    .mcp_tools_from_routes(McpRoutes::described_only().read_only_get());
```

**2. An upstream API** (config file, proxy): an MCP endpoint whose tools
are an upstream service's OpenAPI operations, each call forwarded as an
HTTP request — an MCP gateway source next to MCP upstreams.

```toml
[[route]]
name = "billing-mcp"
[route.match]
path = "/mcp/billing"
[route.action]
type = "mcp"
[[route.action.openapi]]
name    = "billing"
spec    = "https://billing.internal/openapi.json"   # or a file
base_url = "https://billing.internal"
include = ["getInvoice", "listInvoices"]          # operationIds; default: all GET
auth_header_env = "BILLING_TOKEN"                 # sent upstream, never to the client
```

## Mapping

- Tool name: `operationId` (sanitized to MCP's `[A-Za-z0-9_-]{1,64}`), else
  `<method>_<path>`; prefixed `<source>__` like gateway tools; collisions
  get a numeric suffix and a startup warning.
- Description: `summary`, then `description` (trimmed to 1 KiB).
- `inputSchema`: an object with one property per path, query and header
  parameter (their schemas) plus `body` for `requestBody`
  (`application/json` schema); `required` from the spec. Local `$ref`s
  (`#/components/...`) inlined with a depth cap (cycles → `{}`); remote
  refs refused.
- `outputSchema` from the 2xx `application/json` response when present.
- Annotations: `GET`/`HEAD` → `readOnlyHint`; `DELETE` →
  `destructiveHint`; `PUT` → `idempotentHint`.
- Call: path params percent-encoded into the path, query params encoded,
  JSON body; response → `structuredContent` for JSON, text otherwise;
  non-2xx → `isError: true` with status and body (truncated).
- Default exposure: only `GET` operations unless `include` lists others —
  write operations are an explicit choice.

## Safety

- Upstream calls go only to `base_url` (no URL from the spec's `servers`
  unless chosen), redirects not followed across hosts.
- Per-source timeout, body size cap, rate limit via the route's middleware,
  the MCP gateway's `allow` rules per tool, audit log lines per call.
- Spec fetched at startup and on reload; a spec that fails to parse keeps
  the previous tools.

## Parsing

OpenAPI 3.0 and 3.1, JSON (YAML only if a user asks: no YAML parser today).
A bounded, loop-based JSON value parser (`service_discovery::json_lite`'s,
with a depth and size cap) — a stack-depth case and a fuzz target
(`openapi_to_mcp`) per CLAUDE.md.

## Tranches

| # | Scope | Status |
|---|---|---|
| 1 | Spec parser + `$ref` resolution + operation → tool mapping (pure, tested on real specs: Petstore 3.0/3.1, GitHub's, Stripe's subset) + fuzz target | — |
| 2 | Upstream source in the MCP gateway (`[[route.action.openapi]]`): fetch, tools, forwarding, auth header, include/exclude, reload; e2e with a Python API + the MCP conformance client | — |
| 3 | In-process tools from `AppWithState` routes + `OpenApiConfig` operations | — |
| 4 | Docs (`mcp/openapi-tools.md`), example, CHANGELOG, release 18.19.0 | — |