Chio/Docs
LOGIN · JOIN

Migrate from MCP to Chio

Move a working MCP connection behind the kernel, then prove one permitted call, one refusal, and an export that verifies offline.

Guide chio@5f63e6e

On this page

Moving an MCP connection behind the kernel changes one entry in your client's configuration. The server binary, its arguments, and your client's MCP library stay as they are, and from the first call through chio mcp serve the kernel admits each tools/call and signs a receipt for it.

Save the connection's current executable, arguments, environment, and working directory before you edit the launcher. Then rerun the same representative work through Chio, including one request your policy must refuse.

Start

The client launches Chio, and Chio launches your server

Before the migration the client spawns the server and every call runs. After it, the client spawns chio, Chio spawns the same command as a child process over stdio, and the kernel decides each call first.

MCP edgeChoose an outcome.

κcapabilityσscopegguardsBbudget

The guards check refuses before dispatch

A guard denies what the grant covers, and the first denial stops evaluation. The kernel signs a deny receipt and the client receives an error result. The tool server is never called. Guards.

One MCP tools/call through the edge to the kernel and the wrapped stdio server, with the kernel’s checks in evaluation order. Output checks after dispatch and the child process’s host confinement are left out.Wrap an MCP Server

Tool names, input and output schemas, descriptions, and annotations are forwarded verbatim from tools/list, so the agent sees what the server exposes. What the migration adds:

  • Signed receipts. Each kernel decision, allow or deny, produces a chio.receipt.v1 signed with the kernel's Ed25519 key, which you verify offline and feed to audit pipelines.
  • Policy refusals. A call your policy refuses, such as a read of .env, returns a structured error the agent can read and re-plan from, and its receipt names the refusing guard.
  • Optional metering. With tool pricing configured, each call carries a price and the agent can surface budget state. Pricing is opt-in and off by default.

Keep any permissions the server enforces itself. The kernel governs the calls routed through this connection, so close other routes to the server and its backend.

1. Read the tool names your server exposes

Grants and presets match server ids and tool names exactly, so read the live names before you write a rule. Put your own server command after --.

Terminalbash
chio mcp wrap --server-id workspace --print-scopes \
  -- npx -y @modelcontextprotocol/server-filesystem ./workspace
Output, one entry
[[capability]]
tool = "read_text_file"
scope = "read"
urn = "urn:chio:scope:tool:read"

You should see one [[capability]] block per tool. The filesystem package's read tool is read_text_file, while the code-agent preset grants read_file; write the policy for the names you see. The policy and captured checks below use read_file as their sample name. Wrap an MCP Server walks through the full scaffold.

Before you begin

Install the chio binary from Installation. Copy the server command your client launches, verbatim: Chio spawns it unchanged, so whatever your client config puts after the executable name goes after --. Choose two database paths. --session-db holds admission state and --receipt-db holds receipts; both are global flags that go before the subcommand, and chio check and chio mcp serve each exit without them. Either path may name a file that does not exist yet, because the store is created on first use.

2. Start from a deny-by-default policy

Allow one tool and refuse sensitive paths. Every other tool is denied, which gives you a baseline to widen one tool at a time.

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

The agent can now call read_file on anything outside the forbidden patterns. Expect the first run to refuse tools that plain MCP allowed; add each one to the allow list with the guards it needs. Write a Policy covers the full rule set.

Start from the code-agent preset instead

The code-agent preset is a deny-by-default baseline for file, shell, and git work, and it needs no policy file. It grants named tools on the server ids workspace, shell, and git. No command writes the preset out for editing; when you outgrow it, write a HushSpec policy and swap --preset code-agent for --policy ./policy.yaml. chio init <path> is a different tool: it scaffolds a standalone demo project with its own tool server and smoke runner.

Terminalbash
# 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 workspace \
    -- npx -y @modelcontextprotocol/server-filesystem ./workspace

3. Check the policy before the agent meets it

Run chio check against the policy in full mode. This policy enables secret_patterns, which reads the tool's output, so --output-fixture takes a path to a JSON file standing in for the result; inline JSON is rejected.

The allowed readtranscript
$ 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

You should see ALLOW. Then check the two refusals the baseline exists to produce. The first is a scope refusal: write_file is outside the allow list, so the request never reaches a guard.

A tool outside the allow listtranscript
$ 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. Its reason: line names the registered top-level guard, the compiled pipeline; the rule block that fired is recorded in the receipt, in step 6.

A read of .envtranscript
$ 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

Without --mode full, the check stops with CHIO-CLI-PREFLIGHT-NEEDS-OUTPUT: preflight never runs the tool, so it cannot evaluate post-output guards.

4. Change the client's launcher

Point the client's server entry at chio. Everything after -- is the literal command Chio runs as the child process, so copy your original command verbatim.

mcp-client-config.jsonjson
{
  "mcpServers": {
    "workspace": {
      "command": "chio",
      "args": [
        "--session-db", "/abs/path/session.db",
        "--receipt-db", "/abs/path/receipts.db",
        "mcp", "serve",
        "--policy", "/abs/path/policy.yaml",
        "--server-id", "workspace",
        "--",
        "npx", "-y",
        "@modelcontextprotocol/server-filesystem", "./workspace"
      ]
    }
  }
}

Use absolute paths for the policy and both databases: the client chooses the working directory, so a path that resolves in your shell may not resolve under the client. Restart the client. If you run several MCP servers, migrate them one at a time; each wrapped server takes its own --server-id, and the others keep working unwrapped.

A launcher missing either database flag exits before it spawns anything, and the preset does not exempt it:

chio mcp serve without the databasestranscript
$ chio mcp serve --preset code-agent --server-id fs \
  -- npx -y @modelcontextprotocol/server-filesystem ./workspace
error[CHIO-CLI-ADMISSION-STORE-REQUIRED]: durable admission needs a session database so operations and tool outcomes survive a restart
  help: pass --session-db <path>
  code: urn:chio:error:cli:admission-store-required
  docs: https://chio.computer/docs/reference/errors#error-cli-admission-store-required
exit 1

5. Confirm one permitted call and one refusal

Each check is a command with an answer.

  1. The edge starts. Run your chio mcp serve line from a shell in the client's working directory. It stays running; an immediate exit prints the missing flag.
  2. The client sees the same tools. Compare the tool list before and after: names, descriptions, and schemas match.
  3. An ordinary call passes. Ask the agent for its most routine task, then run chio --receipt-db <path> receipt list --admin-all and find a receipt whose decision.verdict is allow.
  4. A forbidden call fails. Ask it to read a .env. The agent gets an error, and the store holds a deny whose evidence[].guard_name is forbidden-path. A migration with no captured deny is untested.

6. Export the receipts and verify them offline

Read the receipts back with the path the edge wrote. A local read names its boundary with exactly one of --tenant <id> or --admin-all, and output is JSON Lines whatever format flags you pass. The three checks from step 3, projected down to the decision:

Three receipts, one line eachtranscript
$ 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

You should see guard: "kernel" on both denials and forbidden-path in evidence[].guard_name on the guard denial. Filter and alert on that field; the reason string names only the pipeline. Pipe the same stream to jq for decision counts, denied tools, and cost histograms. Then export a package with the same explicit boundary, and verify it without contacting the kernel.

Export with a read boundarytranscript
$ chio --receipt-db ./receipts.db evidence export --output ./pkg --admin-all
exit 0
Verify the packagetranscript
$ 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

You should see evidence package verified with three tool receipts and their capability lineage. The package bundles receipts, capability lineage, checkpoints, and inclusion proofs, and a gap or a mutation fails verification with a non-zero exit. The audit trail starts at the first call through chio mcp serve; calls made before the migration have no receipts.

One receipt in full

The metadata object is dropped here because it carries per-run nonces and admission bookkeeping; the rest is the whole receipt.

The .env denialtranscript
$ 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

Read more in Verify Receipts Offline.

MCP migration reference

MCP feature coverage

The adapter is a thin translation layer over chio-mcp-adapter; most MCP features pass through unchanged.

MCP featureHandlingGoverned by
tools/list
Forwarded verbatim
Governed manifest built once
tools/call
Admitted, then dispatched
Kernel checks and receipt
stdio transport
Supported
Client spawns chio
Streamable HTTP
Supported
chio mcp serve-http
Server notifications
Forwarded in order
No guard
resources/list, resources/read
Passthrough
Opt-in scoping
resources/templates/list
Passthrough
No guard
prompts/list, prompts/get
Passthrough
Opt-in scoping
completion/complete
Forwarded unchanged
No guard
sampling/createMessage
Passthrough
allow_sampling_tool_use
Elicitation, including URL mode
Supported
Structured operations
OAuth2 and OIDC
HTTP edge only
serve-http auth flags
  • Tools. The adapter queries the upstream once and builds a governed manifest, preserving schemas and annotations. Calls admitted by the edge reach the kernel's checks before dispatch; scope-refusal errors from tool selection come first and carry no kernel receipt.
  • Streamable HTTP. The session contract follows the MCP specification: initialize on POST /mcp, the session id in the response, and GET /mcp for notifications and replay.
  • Resources and prompts. Both are allow-through by default. For per-URI or per-prompt control, grant them under capabilities.default.resources and capabilities.default.prompts in chio.yaml, each entry a uri or prompt pattern with operations and a ttl. These are keys of the deployment config; a HushSpec document has eight top-level keys and capabilities is not one of them. The kernel validates the selected capability and scope before calling the provider. Successful reads and prompt gets return provider data without a tool-call receipt, and the filesystem resource-root refusal returns a signed denial, so retain operational access logs for complete fetch history. Templates are surfaced to the client with no guard.
  • Completion, sampling, elicitation. Completion requests for prompts and resource URIs forward to the upstream, which answers unchanged. Nested sampling is proxied unchanged; enable allow_sampling_tool_use in the policy's kernel block to govern tool use inside it. URL-mode elicitations are parsed into structured operations, so the agent can prompt the user to open an auth page.
  • OAuth2 and OIDC. The auth flags live on chio mcp serve-http, which takes --policy and has no --preset.
  • Platforms. macOS and Linux run the stdio edge natively; on Windows, run it under WSL.

CHIO-CLI-ADMISSION-STORE-REQUIRED

The edge printed durable admission needs a session database so operations and tool outcomes survive a restart: one or both of --session-db and --receipt-db is missing. Supply both, before the subcommand. With --session-db present, --revocation-db and --budget-db are rejected, because the durable admission authority owns that state.

durable receipt persistence unavailable on every verdict

Only --session-db was supplied, and every call denies until a receipt store exists. Add --receipt-db.

CHIO-CLI-PREFLIGHT-NEEDS-OUTPUT

The policy enables an output-sensitive rule block, secret_patterns or patch_integrity. Rerun with --mode full --output-fixture <path>, where the value is a path to a JSON file.

The preflight refusaltranscript
$ chio --session-db ./admission-0.db --receipt-db ./receipts.db \
  check --policy ./policy.yaml --tool read_file \
  --params '{"path": "./workspace/README.md"}'
error[CHIO-CLI-PREFLIGHT-NEEDS-OUTPUT]: preflight cannot evaluate post-output guards, because it never runs the tool
  help: run the check with --mode full --output-fixture <JSON> so output-sensitive guards see explicit fixture output
  code: urn:chio:error:cli:preflight-needs-output
  docs: https://chio.computer/docs/reference/errors#error-cli-preflight-needs-output
exit 1

CHIO-KERNEL-RECEIPT-READ-CONTEXT-MISSING

A local receipt read needs a read boundary; pass exactly one of --tenant <id> and --admin-all. Over --control-url the boundary comes from the token, and passing --tenant there is an error. The export path applies the same rule:

Export without a read boundarytranscript
$ chio --receipt-db ./receipts.db evidence export --output ./pkg
error[CHIO-KERNEL-RECEIPT-READ-CONTEXT-MISSING]: evidence export needs a read boundary
  help: pass --tenant <id> to export one tenant's receipts, or --admin-all to export every tenant as the operator
  code: urn:chio:error:kernel:receipt-read-context-missing
  docs: https://chio.computer/docs/reference/errors#error-kernel-receipt-read-context-missing
exit 1

unsupported protocolVersion from the edge

The client offered a protocol version this edge does not implement. The error body carries supportedProtocolVersions, and the client picks from that list.

The agent stalls with no error and no receipt

The upstream can still be running after your MCP client stops waiting. Inspect the original request, its retained outcome, and the upstream state before you retry. A recorded policy refusal and a missing terminal record establish different things, and an empty receipt query leaves open whether execution began.

Behaviors a first migration meets

  • Large results are scanned on the way back. secret_patterns compiles a guard on the write path, which can deny a file write carrying a secret, and a post-invocation hook on the read path, which redacts matches from the tool's result. On a stdio-wrapped server that scan can dominate latency for multi-megabyte responses; slice them server-side.
  • A preset grant is a tool name and a server id, matched exactly. A server wrapped under another --server-id, or one whose tools are spelled differently, falls outside the preset's scope. Those calls return an MCP result with isError: true reading tool is not authorized by the active capability set, and leave nothing in the receipt store.
  • Unusual tool names need quotes. Names with colons, dots, or whitespace are legal in MCP; quote them in the allow list, as in - "workspace:write". If you control the server, prefer plain snake_case.

Next: widen the policy from what the receipts show

Capability tokens, delegation, tool pricing, HTTP edges, and custom guards build on this baseline. When your application owns the request and execution path, use the Python SDK in place of a launcher change.