Chio/Docs
LOGIN · JOIN

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

You have an MCP client (Claude Desktop, Cursor, a custom agent) pointed at an MCP server (filesystem, postgres, github, your own) and you want audit trails, policy, and denials for unsafe calls. You do not want to rewrite either side. You are in the right place.

Prerequisites

  • The chio binary. 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-db holds admission state and --receipt-db holds receipts. Both are global flags, they go before the subcommand, and both chio check and chio mcp serve refuse to start without them. Either path may name a file that does not exist; the store is created on first use.
  • A fresh --session-db per chio 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 theirs admission-0.db through admission-3.db for that reason. A long-lived chio mcp serve keeps 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/list and tools/call request 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.v1 signed 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 .env returns 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.

rendering
Plain MCP: the agent speaks MCP straight to the server. Every call executes and nothing is written down.
rendering
Governed MCP: Chio is the process the client launches and the server runs unchanged as a subprocess. The agent speaks the same MCP it spoke before.

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:

bash
# 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 ./workspace

The 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:

mcp-migration · serve-no-dbtranscript
$ chio mcp serve --preset code-agent --server-id fs \
  -- npx -y @modelcontextprotocol/server-filesystem ./workspace
error [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.
exit 1

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:

mcp-client-config.jsonjson
{
  "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

If you have multiple MCP servers configured, wrap them one by one. Each wrapped server gets its own --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

If your coding agent is a Python process, you can skip the CLI wrap and embed the SDK directly: 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.

policy.yamlyaml
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: 60

With 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.

mcp-migration · preflighttranscript
$ 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.
exit 1

--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:

mcp-migration · allowtranscript
$ 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.json
verdict:    ALLOW
tool:       read_file
server:     *
receipt_id: 8cf7f0cb9468defc881ba90137f5978636c7dc7e0d9911f5ed6f15061e2a9b84
policy:     60df7b477be3ee42dfbe33fec4bac9b95628ac5fcb8ae526e439b69cd322acd4
source:     b91346824d582bfba3f3fad1eb0627a2ede05f7c0fc778a93d224d055449c21d
mode:       full
fixture:    true
exit 0allow

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.

mcp-migration · deny-scopetranscript
$ 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.json
verdict:    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
exit 2deny

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.

mcp-migration · deny-forbiddentranscript
$ 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.json
verdict:    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
exit 2deny

Fail-closed by default

Chio denies anything the policy does not explicitly allow. That is a deliberate inversion of the plain-MCP default. Expect the first run of your agent to hit denials for tools that were silently allowed before. That is the whole point. Add them to the allow list one at a time, with the guards you need, without broadly enabling tools.

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:

mcp-migration · receipt-summarytranscript
$ 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"]}
exit 0allow

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.

mcp-migration · receipt-fulltranscript
$ 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"
}
exit 0deny

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:

mcp-migration · export-no-boundarytranscript
$ chio --receipt-db ./receipts.db evidence export --output ./pkg
error [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.
exit 1

With a boundary the export writes the package directory and says nothing at all:

mcp-migration · exporttranscript
$ chio --receipt-db ./receipts.db evidence export --output ./pkg --admin-all
exit 0

Verification is the step that talks:

mcp-migration · verifytranscript
$ chio evidence verify --input ./pkg
evidence 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
exit 0

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.

  1. The edge starts. Run your chio mcp serve line 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.
  2. 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.
  3. 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-all and confirm a receipt exists with decision.verdict of allow.
  4. 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 a deny whose evidence[].guard_name is forbidden-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 featureStatusNotes
tools/list discoverySupportedAdapter queries the upstream once and builds a governed manifest; schemas and annotations are preserved verbatim.
tools/call invocationSupportedEvery call runs through the guard pipeline before dispatch; every decision is a signed receipt.
stdio transportSupportedCanonical migration path. Your client spawns chio instead of the raw server.
Streamable HTTP transportSupported via chio mcp serve-httpSession contract follows MCP spec: initialize on POST /mcp, session id in response, GET /mcp for notifications and replay.
Server notificationsForwardedDrained from the upstream and delivered to the client in order; not subject to guard evaluation.
resources/list, resources/readPassthrough with opt-in scopingDefault 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/listPassthroughTemplates are surfaced to the client. No guard evaluates a template.
prompts/list, prompts/getPassthrough with opt-in scopingSimilar 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)SupportedCompletion requests for prompts and resource URIs forward to the upstream, which returns its response unchanged.
Sampling (sampling/createMessage)PassthroughNested sampling is proxied unchanged. For governance on nested calls, enable allow_sampling_tool_use in the policy kernel block.
Elicitation (including URL elicitation)SupportedURL-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 edgeSupportedAvailable 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 stdioUntestedUse 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 seeWhat it means, and what to do
durable admission mode requires a databaseNeither --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 verdictOnly --session-db was supplied. Every call denies until a receipt store exists. Add --receipt-db.
preflight cannot evaluate post-output guardsThe 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 operationA 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 readsA 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 boundaryThe same rule, on the export path. Add --tenant <id> or --admin-all.
unsupported protocolVersion from the edgeThe 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 receiptChio 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_patterns compiles 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-agent preset grants named tools on the server ids fs, shell and git. 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 with isError: true reading tool 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 grants read_file, and the current package's read tool is read_text_file. Run chio mcp wrap --print-scopes against 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 under capabilities.default.resources and capabilities.default.prompts in a chio.yaml. Those are keys of the deployment config, not of a HushSpec policy file: a HushSpec document has eight top-level keys and capabilities is 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 serve with 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