Chio/Docs

Bridge OpenAPI to MCP

Every REST API with an OpenAPI 3 spec is one step away from being an agent-callable MCP tool server. The chio-openapi-bridge crate takes a spec in, produces a chio-governed MCP surface out. Operations become tools. Method semantics become side-effect hints. Request and response schemas become tool input and output schemas. Every invocation is evaluated by the kernel and signed as a receipt. No server code to write.

Prerequisites

This guide assumes you have a Rust toolchain and an OpenAPI 3.0 or 3.1 spec (JSON or YAML) for the upstream API. The bridge is a library crate today, not a CLI; you embed it in a small Rust binary that owns the HTTP dispatcher and the kernel registration.

Bridge vs. Protect

Chio exposes two different patterns for governing a REST API, and picking the right one is the first decision.

Bridge OpenAPI to MCPProtect an API
Caller protocolMCP (tool calls from an agent)HTTP (existing REST clients)
TransformationProtocol translation: REST → MCP tool surfaceReverse proxy: policy in front of the same REST surface
Client change requiredNone on the HTTP side; agents speak MCPNone on either side; proxy is transparent
Best forExposing a REST API for agent consumptionGoverning an API without changing its protocol

The two are complementary. A production deployment often runs both: the bridge fronts the API for agent traffic, and chio api protect fronts the same upstream for legacy HTTP clients, with a shared policy.


How the Bridge Builds a Tool Surface

Given a spec, the bridge produces a chio.manifest.v1 with one MCP tool per publishable operation and a route binding that maps each tool back to its HTTP method and path template. Tool invocations land on the kernel for guard evaluation; allowed calls are dispatched to the upstream through a pluggable HTTP dispatcher; every decision is returned as a signed chio.receipt.v1.

rendering…
The bridge is kernel-mediated, not a passthrough. Every invocation produces a signed receipt — including denials — and the HTTP dispatcher is only consulted after guards allow the call.

The bridge never touches the network itself. All HTTP mechanics live in the dispatcher you supply, which keeps the crate transport-agnostic and testable: you can run the whole flow in-process with a closure that returns canned responses.


Quickstart

Start from the canonical Pet Store spec. Six lines of glue turn it into a kernel-registered tool server.

toml
# Cargo.toml
[dependencies]
chio-openapi-bridge = "0.1"
chio-kernel = "0.1"
reqwest = { version = "0.12", features = ["json"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
rust
use chio_openapi_bridge::{OpenApiBridge, BridgeConfig, BridgedResponse};
use chio_kernel::Kernel;
use serde_json::json;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let spec = std::fs::read_to_string("petstore.yaml")?;

    let bridge = OpenApiBridge::from_spec(
        &spec,
        BridgeConfig {
            server_id: "petstore".into(),
            server_name: "Pet Store".into(),
            server_version: "1.0.0".into(),
            public_key: std::env::var("CHIO_SERVER_PUBLIC_KEY")?,
            base_url: "https://petstore.example.com".into(),
        },
    )?;

    // The dispatcher is where the bridge meets the network. You own it,
    // so you also own timeouts, retries, connection pooling, and auth.
    let http = reqwest::Client::builder().build()?;
    bridge.set_dispatcher(Box::new(move |method, url, args| {
        let http = http.clone();
        Box::pin(async move {
            let resp = http.request(method, url).json(&args).send().await?;
            Ok(BridgedResponse {
                status: resp.status().as_u16(),
                body: resp.json().await.unwrap_or(json!({})),
                is_error: false,
            })
        })
    }));

    let kernel = Kernel::from_env().await?;
    kernel.register_tool_server(bridge.as_tool_server()).await?;
    kernel.serve().await
}

That is the whole integration. Agents connected to this kernel can now call listPets, createPet, and getPetById as MCP tools. The kernel decides which calls are allowed; the bridge dispatches the allowed ones; every decision is a receipt in the log.


Operation to Tool Mapping

Each publishable OpenAPI operation becomes one MCP tool. The mapping is deterministic:

  • Name. The tool name is the operation's operationId. If the spec omits one, the bridge falls back to "{METHOD} {path}" (for example, GET /pets). Prefer explicit operation ids; fallback names are stable per spec version but can shift between versions.
  • Input schema. Path, query, header, and request-body parameters are merged into a single JSON Schema object that becomes the tool's inputSchema. Required fields on the spec surface as required fields on the tool.
  • Output schema. If the primary success response declares a body schema, it becomes the tool's outputSchema. Agents that honor output schemas can validate responses; others ignore it.
  • Description. The operation's summary and description are joined and forwarded verbatim. Good spec docs become good tool docs.
  • Route binding. The bridge keeps a BTreeMap<tool_name, RouteBinding> so that at invoke time it can reconstruct the exact method and URL to dispatch — path parameters are substituted back into the template from the tool arguments.

Side-Effect Classification

The bridge classifies every tool by HTTP method. This feeds the has_side_effects flag on each tool, which the kernel uses to decide whether a capability token is required.

HTTP methodClassificationDefault guard
GET, HEAD, OPTIONSSafe readAudit receipt only, no capability required
POST, PUT, PATCH, DELETESide effectValid capability token required, signed receipt on allow or deny

Semantics over syntax

If your API returns a read over POST (common in search-style RPC-over-REST designs), the bridge will default to treating it as a side effect. Mark those operations explicitly with x-chio-read-only: true rather than relying on method inference.

OpenAPI Extensions

The bridge recognises a small set of x-chio-* extensions you can place on any operation or path. They control what becomes a tool, how it is scoped, and how it is described.

ExtensionEffect
x-chio-publish: falseOmit the operation from the tool manifest entirely (useful for admin or internal routes)
x-chio-read-only: trueOverride method-based side-effect classification to force safe-read semantics
x-chio-scope: ["pets:write"]Attach required capability scopes; the kernel enforces subset during guard evaluation
x-chio-tool-name: createPetOverride the tool name (useful when operationId is missing or auto-generated)
x-chio-tags: ["dangerous"]Attach arbitrary tags; policies and the receipt log can filter on these
yaml
paths:
  /pets:
    post:
      operationId: createPet
      x-chio-scope: ["pets:write"]
      x-chio-tags: ["billing-relevant"]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewPet'
      responses:
        '201':
          description: Pet created
  /admin/reindex:
    post:
      operationId: reindex
      x-chio-publish: false   # never exposed as a tool

Receipts

Every bridged call produces a chio.receipt.v1, and the receipt format is identical to native chio tool invocations. Bridge receipts carry enough HTTP context for forensics — status, method, and resolved path — without leaking request or response bodies by default.

json
{
  "version": "chio.receipt.v1",
  "receipt_id": "01HXYZ...7K4",
  "decision": "allow",
  "server_id": "petstore",
  "tool_name": "createPet",
  "meta": {
    "bridge": "openapi",
    "http": {
      "method": "POST",
      "path": "/pets",
      "status": 201
    }
  },
  "signature": "..."
}

Denials produce the same structure with decision: "deny" and the reason populated by the failing guard. The upstream is never contacted on a deny, which is the whole point of placing the bridge in front of the kernel.


Simulation Mode

Skip the dispatcher during development and the bridge runs in simulation: every tool call returns a structured mock that echoes the method, resolved path, and arguments. This is how the crate tests itself, and it is how you should develop policy against a spec before pointing at a real upstream.

rust
let bridge = OpenApiBridge::from_spec(&spec, config)?;
// No set_dispatcher call — simulation is the default.

let result = bridge.invoke_tool("createPet", json!({
    "name": "Rex",
    "tag": "dog"
}))?;

assert_eq!(result["method"], "POST");
assert_eq!(result["path"], "/pets");
assert_eq!(result["simulated"], true);

Policy against a spec, not a service

Simulation mode lets you author and test policy before the upstream even exists. CI pipelines can run the full bridge + kernel flow against a spec, asserting that createPet denies without a token and listPets allows with audit. This catches policy regressions before they reach a staging environment.

Limitations and Gotchas

  • No streaming responses. The current bridge models responses as a single JSON body. Long- poll endpoints, SSE, and chunked transfer are out of scope and should go through Protect an API instead.
  • OpenAPI 3.0 and 3.1 only. Swagger 2.0 specs must be converted first; tooling like swagger2openapi handles this cleanly.
  • operationId collisions. Two operations with the same id across paths will fail at manifest construction, not at runtime. Run OpenApiBridge::from_spec in CI against your spec to catch this before deploy.
  • Authentication to the upstream. The bridge does not manage upstream credentials. Your dispatcher is responsible for attaching Authorization headers or signed requests. This is a feature: the kernel already authenticates the caller, so upstream credentials should be a separate concern held by the operator, not the agent.

Next Steps

  • Write a Policy · author the rules that decide which bridged operations an agent can call
  • Protect an API · the companion pattern for governing HTTP clients rather than MCP agents
  • Receipt format · the exact structure of the receipts emitted on every bridged call
  • Capabilities · how scope tokens gate side-effect operations at the bridge boundary
Bridge OpenAPI to MCP · Chio Docs