BuildConnect
Migrate from MCP to Chio
Add signed receipts and policy enforcement to an existing MCP client-server connection without changing the agent or server.
Who this guide is for
Prerequisites
- The
chiobinary. See Installation. Every transcript on this page was captured from it. - The MCP server command you run today, verbatim. Chio spawns it unchanged, so whatever your client config puts after the executable name is what goes after
--. - Two database paths.
--session-dbholds admission state and--receipt-dbholds receipts. Both are global flags, they go before the subcommand, and bothchio checkandchio mcp serverefuse to start without them. Either path may name a file that does not exist; the store is created on first use. - A fresh
--session-dbperchio check. The dry-run command uses a fixed request id, so a second check against the same session database conflicts with the operation the first one retained. The transcripts below number theirsadmission-0.dbthroughadmission-3.dbfor that reason. A long-livedchio mcp servekeeps one session database for its whole life.
What Doesn't Change
The design goal of the adapter is that both ends of the conversation remain MCP-native. Existing client and server implementations remain in use.
- Your agent stays unchanged. It still speaks MCP. The same client library, the same config file, the same
tools/listandtools/callrequest shapes, the same response format. - Your server stays unchanged. It runs as a subprocess behind Chio exactly as it would run on its own. Same binary, same arguments, same stdio transport.
- Tool definitions are preserved. Names, input schemas, output schemas, descriptions, and annotations are forwarded verbatim from
tools/list. Whatever your server exposes, the agent sees. - Stdio transport is preserved. If you launch MCP servers via stdio today, you keep doing that. Chio just happens to be the process the client launches.
The migration requires a client configuration change and a policy file, not a client or server rewrite.
What Does Change
What the agent experiences is still MCP, but the semantics get richer:
- Signed receipts. Every call the kernel evaluates produces a
chio.receipt.v1signed with Chio's Ed25519 key, allow or deny. You can verify receipts offline and feed them into auditing pipelines. One refusal happens earlier than that and is the exception: a tool the active capability set does not cover is turned away at the edge, before any kernel evaluation, and writes no receipt. - Policy-enforced denials. Calls that were previously unconditional can now be blocked by your policy. A filesystem write against
.envreturns a structured error instead of executing. - Optional cost metering. If you configure tool pricing, each call exposes a per-call price and the agent can surface budget state. Opt-in; disabled by default.
- Structured deny reasons. A denied call comes back as a structured error the agent can read, and the receipt behind it records which guard fired. The two live in different places, which the receipt section below spends some time on. Agents that handle errors well can recover or re-plan.
Before and After
Before Chio, the agent talks directly to the MCP server. After Chio, the agent talks to Chio, Chio talks to the MCP server, and the agent-facing interface is identical.
Notice that the agent side of both diagrams is the same set of MCP messages. The change is entirely inside the middle hop.
Step 1: Wrap Your Existing MCP Server
The Wrap an MCP Server guide has the complete walkthrough. For migration, you need two things: a policy file and a one-line launcher change.
First, pick a starting policy. The code-agent preset is a safe, opinionated baseline for file / shell / git workflows, and it needs no policy file at all. Pass --preset code-agent and Chio applies the bundled deny-by-default guard set directly:
# No policy file required -- the preset is built in.
$ chio --session-db ./session.db --receipt-db ./receipts.db \
mcp serve --preset code-agent --server-id fs \
-- npx -y @modelcontextprotocol/server-filesystem ./workspaceThe two database flags are not optional and the preset does not exempt you from them. Drop either one and the command exits before it spawns anything:
$ chio mcp serve --preset code-agent --server-id fs \
-- npx -y @modelcontextprotocol/server-filesystem ./workspaceerror [urn:chio:error:cli:other]: durable admission mode requires a database so operations and tool outcomes survive restart
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.There is no command that writes the preset out to a file for editing. When you outgrow it, hand-author a HushSpec policy (Step 2) and swap --preset code-agent for --policy ./policy.yaml. Note that chio init <path> is a different tool: it scaffolds a standalone runnable demo project with its own toy tool server plus a smoke runner. It does not produce a drop-in policy you can point at an existing filesystem, postgres, or github server.
Second, change your MCP client config so it launches chio instead of the raw server. Everything after -- is the literal command Chio runs as the subprocess, so copy your original command verbatim:
{
"mcpServers": {
"filesystem": {
"command": "chio",
"args": [
"--session-db", "/abs/path/session.db",
"--receipt-db", "/abs/path/receipts.db",
"mcp", "serve",
"--policy", "/abs/path/policy.yaml",
"--server-id", "fs",
"--",
"npx", "-y",
"@modelcontextprotocol/server-filesystem", "./workspace"
]
}
}
}Restart the client after this change. The agent receives the same tools, schemas, and responses, with each call mediated by Chio's kernel. Use absolute paths for the policy and both databases: the client chooses the working directory, and a relative path that resolves under your shell will not resolve under the client.
Migrate one server at a time
--server-id, and the unwrapped ones keep working. There is no big-bang migration; you can run a mixed fleet indefinitely.Python-first? Embed the SDK instead
pip install chio-code-agent chio-sdk-python, then from chio_code_agent import CodeAgent and from chio_sdk import ChioClient. The import is chio_sdk (from the PyPI package chio-sdk-python), not a top-level chio. The file-backed HushSpec plus chio mcp serve stays the default operator workflow.Step 2: Write Your First Policy
The Write a Policy guide is the reference. For migration, begin with a deny-by-default policy that allows one tool. This provides a baseline before you add more tools.
hushspec: "0.1.0"
name: migration-baseline
rules:
tool_access:
enabled: true
default: block
allow:
- read_file
forbidden_paths:
enabled: true
patterns:
- "**/.env"
- "**/.env.*"
- "**/.ssh/**"
- "**/*.pem"
- "**/*.key"
- "**/credentials*"
exceptions: []
secret_patterns:
enabled: true
velocity:
enabled: true
max_invocations_per_window: 100
window_secs: 60With this in place, your agent can call read_file on anything that does not match a forbidden pattern; all other tools are denied. The resulting receipts show which tools to add to the allow list.
Exercise the policy with chio check before you point an agent at it. The first attempt does not get a verdict, and the refusal is the useful part: this policy turns on secret_patterns, which reads the tool's output, and preflight has no output to read.
$ chio --session-db ./admission-0.db --receipt-db ./receipts.db \
check --policy ./policy.yaml --tool read_file \
--params '{"path": "./workspace/README.md"}'error [urn:chio:error:cli:other]: chio check preflight cannot evaluate post-output guards; use --mode full --output-fixture <JSON> so output-sensitive policy is evaluated against explicit fixture output
context: {"domain":"cli","severity":"error","stability":"deprecated","string_code":"CHIO-CLI-OTHER"}
suggested fix: Preserve the original message and migrate the call site to a specific registry code when touched.--mode full supplies that missing half. --output-fixture takes a path to a JSON file standing in for what the tool would have returned, not inline JSON. With one in place the same call gets a verdict:
$ chio --session-db ./admission-1.db --receipt-db ./receipts.db \
check --policy ./policy.yaml --tool read_file \
--params '{"path": "./workspace/README.md"}' \
--mode full --output-fixture ./output-fixture.jsonverdict: ALLOW tool: read_file server: * receipt_id: 8cf7f0cb9468defc881ba90137f5978636c7dc7e0d9911f5ed6f15061e2a9b84 policy: 60df7b477be3ee42dfbe33fec4bac9b95628ac5fcb8ae526e439b69cd322acd4 source: b91346824d582bfba3f3fad1eb0627a2ede05f7c0fc778a93d224d055449c21d mode: full fixture: true
Now the two denials the starter policy is there to produce. The first is a capability-scope refusal: write_file is not in the allow list, so the request never reaches a guard.
$ chio --session-db ./admission-2.db --receipt-db ./receipts.db \
check --policy ./policy.yaml --tool write_file \
--params '{"path": "./workspace/output.txt", "content": "hi"}' \
--mode full --output-fixture ./output-fixture.jsonverdict: DENY tool: write_file server: * reason: requested tool write_file on server * is not in capability scope receipt_id: 41cfc48636745c6113a5ad03f80c89a82d3cf0e1884566391ae7ddc31f9ad465 policy: 60df7b477be3ee42dfbe33fec4bac9b95628ac5fcb8ae526e439b69cd322acd4 source: b91346824d582bfba3f3fad1eb0627a2ede05f7c0fc778a93d224d055449c21d mode: full fixture: true
The second is a guard denial. Read the reason: line carefully, because it is the one line on this page readers most often expect to say something it does not say. It names the registered top-level guard, which is the compiled pipeline, and never the individual rule block that fired. The guard that fired is recorded, but in the receipt, not here.
$ chio --session-db ./admission-3.db --receipt-db ./receipts.db \
check --policy ./policy.yaml --tool read_file \
--params '{"path": "./workspace/.env"}' \
--mode full --output-fixture ./output-fixture.jsonverdict: DENY tool: read_file server: * reason: guard denied the request: guard "guard-pipeline" denied the request receipt_id: b1c490817e95b7b8a3340f1fc0a8d79a4a83f9a4cbc3773241f35b387345d58b policy: 60df7b477be3ee42dfbe33fec4bac9b95628ac5fcb8ae526e439b69cd322acd4 source: b91346824d582bfba3f3fad1eb0627a2ede05f7c0fc778a93d224d055449c21d mode: full fixture: true
Fail-closed by default
Step 3: Check Receipts
Run the agent through representative work, then inspect the receipts. They provide a cryptographic record of the tool calls it attempted.
Point chio mcp serve at the receipt database you want it to write, then read it back with the same path. A local read fails closed unless you pass exactly one of --tenant <id> or --admin-all, so the boundary is always explicit. Output is JSON Lines, one receipt per line, whatever format flags you pass.
The three dry-runs above wrote three receipts. Projected down to the decision, they read:
$ chio --receipt-db ./receipts.db receipt list --admin-all \
| jq -c '{tool: .tool_name, verdict: .decision.verdict,
$ reason: .decision.reason, guard: .decision.guard,
$ evidence: [.evidence[]?.guard_name]}'{"tool":"read_file","verdict":"allow","reason":null,"guard":null,"evidence":[]}
{"tool":"write_file","verdict":"deny","reason":"requested tool write_file on server * is not in capability scope","guard":"kernel","evidence":[]}
{"tool":"read_file","verdict":"deny","reason":"guard denied the request: guard \"guard-pipeline\" denied the request","guard":"kernel","evidence":["forbidden-path"]}That is where the guard name lives. The decision object carries guard: "kernel" on every kernel denial, allow or deny, and the rule block that actually fired appears once, as an evidence[].guard_name. Filter or alert on that field, not on the reason string.
Unprojected, one receipt is this. The metadata object is dropped here because it carries per-run nonces and admission bookkeeping; everything else is what a receipt actually contains.
$ chio --receipt-db ./receipts.db receipt list --admin-all \
| jq 'select(.action.parameters.path == "./workspace/.env") | del(.metadata)'{
"id": "b1c490817e95b7b8a3340f1fc0a8d79a4a83f9a4cbc3773241f35b387345d58b",
"timestamp": 1788597783,
"capability_id": "cap-01a070bc-4b26-7422-bf79-e5a64fe86edd",
"tool_server": "*",
"tool_name": "read_file",
"action": {
"parameters": {
"path": "./workspace/.env"
},
"parameter_hash": "01a3da31bac3d250f150339143ffea8d946d0b78c2373e0470878a1ab14b802e"
},
"decision": {
"verdict": "deny",
"reason": "guard denied the request: guard \"guard-pipeline\" denied the request",
"guard": "kernel"
},
"receipt_kind": "mediated_decision",
"boundary_class": "prevent",
"tool_origin": "caller_executed",
"redaction_mode": "none",
"content_hash": "74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b",
"policy_hash": "60df7b477be3ee42dfbe33fec4bac9b95628ac5fcb8ae526e439b69cd322acd4",
"evidence": [
{
"guard_name": "forbidden-path",
"verdict": false,
"details": "action=deny; reason=guard denied request"
}
],
"trust_level": "mediated",
"kernel_key": "50e420c0095f75b0dfa7d5b0d22763207a5afcd04ab08b035f5bd997018bc30d",
"signature": "35ebb666768125063f446bb07065d9f3192f1af733b3aba2975ed6c2fd3c8c59c06b4eff9e08e1095bcd9d398e37042ee8646e8c17a13cbb57228102c18c4306"
}Pipe to jq for summaries (decision counts, denied tools, cost histograms) and wire the same stream into downstream audit pipelines.
For offline verification, export an evidence package and verify it without contacting the kernel. The export needs the same explicit read boundary the list does, and says so:
$ chio --receipt-db ./receipts.db evidence export --output ./pkgerror [urn:chio:error:attest:provenance-missing]: receipt read boundary error: evidence export requires an explicit receipt read boundary
context: {"domain":"attest","severity":"error","stability":"unstable","string_code":"CHIO-ATTEST-PROVENANCE-MISSING"}
suggested fix: Regenerate the evidence bundle and include provenance before submitting the operation.With a boundary the export writes the package directory and says nothing at all:
$ chio --receipt-db ./receipts.db evidence export --output ./pkg --admin-allVerification is the step that talks:
$ chio evidence verify --input ./pkgevidence package verified tool_receipts: 3 child_receipts: 0 checkpoints: 0 checkpoint_publications: 0 checkpoint_witnesses: 0 checkpoint_consistency_proofs: 0 checkpoint_equivocations: 0 capability_lineage: 3 inclusion_proofs: 0 uncheckpointed_receipts: 3 authorized_receipts: 1 trace_observations: 0 advisory_evaluations: 0 verified_files: 12 child_receipt_scope: FullQueryWindow transparency_preview_logs: 0 publication_state: transparency_preview
The package bundles receipts, capability lineage, checkpoints, and inclusion proofs. Any gap or mutation fails the verification with a non-zero exit. See Query and Audit Receipts for the full query surface (filter by server, tool, outcome, capability, time range) and Verify Receipts Offline for the air-gapped verification workflow.
Verify the Result
Four checks, in order. Each one is a command with an answer, not an impression.
- The edge starts. Run your
chio mcp serveline from a shell with the client's working directory. It should not print an error and should not exit. If it exits immediately, the message names the missing flag. - The client sees the same tools. Compare the tool list your agent shows before and after. It should be identical: names, descriptions and schemas are forwarded verbatim.
- One call you expect to pass, passes. Ask the agent to do the most ordinary thing it does. Then run
chio --receipt-db <path> receipt list --admin-alland confirm a receipt exists withdecision.verdictofallow. - One call you expect to fail, fails. Ask it to read a
.env. The agent should get an error, and the receipt store should hold adenywhoseevidence[].guard_nameisforbidden-path. A migration with no captured deny has not been tested.
Compatibility Matrix
The adapter is a thin translation layer; most MCP features pass through unchanged. The table below is grounded in what chio-mcp-adapter implements.
| MCP feature | Status | Notes |
|---|---|---|
tools/list discovery | Supported | Adapter queries the upstream once and builds a governed manifest; schemas and annotations are preserved verbatim. |
tools/call invocation | Supported | Every call runs through the guard pipeline before dispatch; every decision is a signed receipt. |
| stdio transport | Supported | Canonical migration path. Your client spawns chio instead of the raw server. |
| Streamable HTTP transport | Supported via chio mcp serve-http | Session contract follows MCP spec: initialize on POST /mcp, session id in response, GET /mcp for notifications and replay. |
| Server notifications | Forwarded | Drained from the upstream and delivered to the client in order; not subject to guard evaluation. |
resources/list, resources/read | Passthrough with opt-in scoping | Default is allow-through. For per-URI control, grant them explicitly under capabilities.default.resources in chio.yaml, each entry a uri pattern plus operations and a ttl. |
resources/templates/list | Passthrough | Templates are surfaced to the client. No guard evaluates a template. |
prompts/list, prompts/get | Passthrough with opt-in scoping | Similar to resources. Per-prompt control lives under capabilities.default.prompts, each entry a prompt pattern plus operations and a ttl; receipts still cover every fetch. |
Argument completion (completion/complete) | Supported | Completion requests for prompts and resource URIs forward to the upstream, which returns its response unchanged. |
Sampling (sampling/createMessage) | Passthrough | Nested sampling is proxied unchanged. For governance on nested calls, enable allow_sampling_tool_use in the policy kernel block. |
| Elicitation (including URL elicitation) | Supported | URL-mode elicitations are parsed and surfaced as structured operations, so the agent can prompt the user to open an auth page. |
| OAuth2 / OIDC on the HTTP edge | Supported | Available only on the HTTP edge, not on stdio. The auth flags live on chio mcp serve-http, which has no --preset: a remote edge takes --policy. |
| Windows native stdio | Untested | Use WSL. macOS and Linux are the supported platforms. |
Failures and Recovery
Most migrations require only the configuration change. These are the failures that do come up, and what each one wants.
| What you see | What it means, and what to do |
|---|---|
durable admission mode requires a database | Neither --session-db nor --receipt-db was supplied, or only one was. Supply both, before the subcommand. Do not reach for --revocation-db or --budget-db to satisfy it: with --session-db present those two are rejected, because the durable admission authority owns that state. |
durable receipt persistence unavailable on every verdict | Only --session-db was supplied. Every call denies until a receipt store exists. Add --receipt-db. |
preflight cannot evaluate post-output guards | The policy enables an output-sensitive rule block, secret_patterns or patch_integrity. Re-run with --mode full --output-fixture <path>. The value is a path to a JSON file, not inline JSON. |
request id conflicts with retained operation | A second chio check ran against a session database an earlier one already used. Point each check at its own fresh --session-db. |
--tenant <id> or --admin-all is required for local receipt reads | A local receipt read has no boundary. Pass exactly one. Over --control-url the boundary comes from the token instead, and passing --tenant there is an error. |
evidence export requires an explicit receipt read boundary | The same rule, on the export path. Add --tenant <id> or --admin-all. |
unsupported protocolVersion from the edge | The client offered a protocol version this edge does not implement. The error body carries supportedProtocolVersions; the client has to pick from that list. |
| The agent stalls, no error, no receipt | Chio adds no tool timeout of its own and waits as long as the upstream takes, so a hung upstream looks like nothing happening. Your MCP client may time out first, and that error is easy to mistake for a deny. The receipt store settles it: a policy denial wrote a deny receipt, a timeout wrote nothing. |
Four more things that surprise people, none of them errors:
- Large tool results are scanned on the way back. Enabling
secret_patternscompiles two things, not one: a guard on the write path that can deny a file write carrying a secret, and a post-invocation hook on the read path that scans the tool's result. The read path redacts rather than denies, so a tool that returns many megabytes still returns them, minus the matches. On a stdio-wrapped server that scan can dominate latency on large responses. Slice those responses server-side. - A preset grant is a tool name and a server id, matched exactly. The
code-agentpreset grants named tools on the server idsfs,shellandgit. Wrap a server under a different--server-id, or wrap one whose tools are spelled differently, and those calls are not in scope. They come back as an MCP result withisError: truereadingtool is not authorized by the active capability set, and they leave nothing in the receipt store to explain themselves. The filesystem server is a live example: the preset grantsread_file, and the current package's read tool isread_text_file. Runchio mcp wrap --print-scopesagainst your server first and read the tool names back before you trust a preset to cover them. - Unusual tool names. Tool names become identifiers in policy files. Names with colons, dots, or whitespace are legal in MCP but awkward to target in YAML. If your server exposes a tool called
fs:write, quote it in the allow list:- "fs:write". If you control the server, prefer plain snake_case. - Prompts and resources are allow-through by default. The starter preset governs
tools/call. MCP resources and prompts are proxied untouched unless you grant them explicitly undercapabilities.default.resourcesandcapabilities.default.promptsin achio.yaml. Those are keys of the deployment config, not of a HushSpec policy file: a HushSpec document has eight top-level keys andcapabilitiesis not one of them. Receipts still record every fetch, so nothing is hidden, but the guard pipeline is not enforcing here until you ask it to. - Receipts are not retroactive. Chio can only sign what passed through it. Tool calls your agent made before the migration are gone; the audit trail starts the first time the client talks to
chio mcp serve.
Summary
The migration has three steps:
- Wrap. Point your MCP client at
chio mcp servewith the original server command after--. - Policy. Start with a deny-by-default rule and one allowed tool. Expand from there using receipts to see what the agent actually wants.
- Audit. Query and verify receipts to confirm the policy is doing what you think it is.
Capability tokens, delegation, tool pricing, HTTP edges, and custom guards build on that baseline. After step three, Chio evaluates calls and records signed receipts.
Next Steps
- Wrap an MCP Server · the full walkthrough with every flag and option covered
- Write a Policy · author HushSpec policies that match your real workflows
- Query and Audit Receipts · filter, export, and verify the audit trail
- Architecture · how the kernel, guards, and adapters fit together