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
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 MCP | Protect an API | |
|---|---|---|
| Caller protocol | MCP (tool calls from an agent) | HTTP (existing REST clients) |
| Transformation | Protocol translation: REST → MCP tool surface | Reverse proxy: policy in front of the same REST surface |
| Client change required | None on the HTTP side; agents speak MCP | None on either side; proxy is transparent |
| Best for | Exposing a REST API for agent consumption | Governing 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.
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.
# 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"] }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
summaryanddescriptionare 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 method | Classification | Default guard |
|---|---|---|
GET, HEAD, OPTIONS | Safe read | Audit receipt only, no capability required |
POST, PUT, PATCH, DELETE | Side effect | Valid capability token required, signed receipt on allow or deny |
Semantics over syntax
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.
| Extension | Effect |
|---|---|
x-chio-publish: false | Omit the operation from the tool manifest entirely (useful for admin or internal routes) |
x-chio-read-only: true | Override 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: createPet | Override 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 |
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 toolReceipts
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.
{
"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.
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
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
swagger2openapihandles this cleanly. - operationId collisions. Two operations with the same id across paths will fail at manifest construction, not at runtime. Run
OpenApiBridge::from_specin 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
Authorizationheaders 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