Chio/Docs
LOGIN · JOIN

BuildConnect

Wrap an MCP Server

Wrap an MCP server with Chio to evaluate tool calls against policy and emit signed receipts.

Prerequisites

This guide assumes you have the Chio CLI installed. If not, see the Installation guide. You also need the MCP server you want to wrap. This guide uses the official filesystem server as the example, but any MCP server works.

How the MCP Adapter Works

The MCP adapter is a transparent proxy that wraps an existing MCP server. It does three things:

  • Discovers tools: reads the MCP server's tool list via the tools/list method (chio mediates this transparently) and generates a tool manifest
  • Intercepts calls: translates incoming requests into MCP tools/call messages, evaluating each one against your policy before forwarding
  • Records decisions: produces a signed receipt for each allow or deny decision

The MCP server itself runs as a sandboxed subprocess. It communicates with Chio over stdio. Your agent connects to Chio the same way it would connect to any MCP server, with no client-side changes required.

rendering
Agent talks to Chio as if it were the MCP server; Chio enforces policy and spawns the real server as a subprocess.

chio mcp wrap

The walkthrough below uses chio mcp serve, the policy-driven path. Before you author a policy, there is a lower-ceremony command for a first look: chio mcp wrap. It spawns the wrapped stdio server, pulls tools/list once, and infers a capability-scope manifest scaffold from the live tool list instead of requiring a hand-authored policy. By default it is a manifest-gated pass-through: a tool passes once you promote it to allow in the scaffold, and anything else is denied with a urn:chio:error:capability:scope-exceeded JSON-RPC error.

  • --print-scopes prints the inferred scaffold and exits, so you can see what the server exposes before promoting anything.
  • --emit-config <ide> prints a paste-ready client config for cursor, claude-desktop, continue, or zed and exits.
  • --manifest <path> runs the gated proxy loop against a promoted scaffold. With no --manifest, the allow-set is empty and every tools/call is denied.
  • --strict-execution-nonce upgrades the path: allowed tools/call requests run through a kernel preflight that mints a single-use execution nonce and re-presents it before the wrapped server is invoked, closing the TOCTOU window between the check and the call.
bash
# Infer and print the capability scaffold, no config changes:
$ chio mcp wrap --print-scopes \
    -- npx -y @modelcontextprotocol/server-filesystem ./workspace

# Emit a paste-ready Cursor config for the wrapped server:
$ chio mcp wrap --emit-config cursor \
    -- npx -y @modelcontextprotocol/server-filesystem ./workspace

--emit-config cursor writes the client entry to stdout and exits. It re-spells the same argv you just typed, with chio mcp wrap in front of it, so the editor launches the wrapper rather than the bare server:

chio mcp wrap --emit-config cursor stdoutjson
{
  "mcpServers": {
    "mcp": {
      "args": [
        "mcp",
        "wrap",
        "--server-id",
        "mcp",
        "--",
        "npx",
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "./workspace"
      ],
      "command": "chio",
      "metadata": {
        "chioSchema": "cursor.mcp/2024-12",
        "displayName": "mcp"
      }
    }
  }
}

Promote tools in two steps. --print-scopes writes a TOML scaffold to stdout: a server_id line plus one [[capability]] block per discovered tool, every block defaulting to allow = false. Capture it to a file, flip the tools you trust to allow = true, then start the loop against that file with --manifest.

bash
# Capture the inferred scaffold to a file you can edit:
$ chio mcp wrap --print-scopes \
    -- npx -y @modelcontextprotocol/server-filesystem ./workspace \
    > fs-scaffold.toml

Against @modelcontextprotocol/server-filesystem the inference produces one block per discovered tool, each carrying the scope it was classified into, the scope URN, and the tool's own description verbatim from tools/list. Two of the fourteen, abridged:

chio mcp wrap --print-scopes stdout (excerpt)toml
# chio mcp wrap -- inferred capability manifest scaffold
# Review each tool below; flip `allow = true` to promote.
# Default is deny.

server_id = "mcp"
# TODO: review the inferred scopes below before promoting.

[[capability]]
tool = "read_text_file"
scope = "read"
urn = "urn:chio:scope:tool:read"
description = "Read the complete contents of a file from the file system as text. ..."
allow = false
# TODO: review and promote

[[capability]]
tool = "write_file"
scope = "destructive"
urn = "urn:chio:scope:tool:destructive"
description = "Create a new file or completely overwrite an existing file with new content. ..."
allow = false
# TODO: review and promote

The classifier is what makes the scaffold worth reading before you promote anything: write_file lands in destructive, not the filesystem bucket its name suggests, and edit_file lands there too. Flip only what you mean to grant:

fs-scaffold.toml (after editing)toml
server_id = "mcp"

[[capability]]
tool = "read_text_file"
scope = "read"
urn = "urn:chio:scope:tool:read"
allow = true   # promoted

[[capability]]
tool = "write_file"
scope = "destructive"
urn = "urn:chio:scope:tool:destructive"
allow = false  # stays denied
bash
# Start the gated proxy against the promoted allow-set. Drop
# --print-scopes / --emit-config so the loop actually runs:
$ chio mcp wrap --manifest ./fs-scaffold.toml \
    -- npx -y @modelcontextprotocol/server-filesystem ./workspace

A tool denied by the scaffold comes back as a JSON-RPC error with the urn:chio:error:capability:scope-exceeded reason code, the same as any tool you never promoted.

wrap and serve have different guarantees

chio mcp wrap and chio mcp serve are different code paths with different guarantees. The default wrap path is a manifest-gated pass-through: no HushSpec compilation, and no execution-nonce enforcement unless you add --strict-execution-nonce. serve compiles a HushSpec policy and runs its guards on each call. Use serve (or serve-http) when you need HushSpec policy evaluation and signed receipts.

1. Choose Your MCP Server

Chio is server-agnostic. Any MCP server that communicates over stdio works. Common choices:

ServerCommandTools Exposed
Filesystemnpx -y @modelcontextprotocol/server-filesystem ./workspaceread_text_file, write_file, list_directory, and eleven more
PostgreSQLnpx -y @modelcontextprotocol/server-postgres $DATABASE_URLquery, list_tables, describe_table
GitHubnpx -y @modelcontextprotocol/server-githubsearch_repositories, create_issue, get_file_contents
Fetch (HTTP)npx -y @modelcontextprotocol/server-fetchfetch

This guide uses the filesystem server. The same steps apply to any MCP server, but the tool names do not carry over, and a policy that names a tool the server does not expose is silently inert. Run chio mcp wrap --print-scopes against your own server and write the policy from its output. The filesystem server exposes fourteen tools, and its read_file carries the description DEPRECATED: Use read_text_file instead., so a read-only policy should allow read_text_file.


2. Write a Policy Tailored to Your Server

The policy defines which tools the agent can invoke, which paths it can access, and what other constraints apply. Write a HushSpec YAML file tailored to the tools your MCP server exposes.

Here is a policy for a filesystem server that allows reading and listing but blocks writing, restricts access to a workspace directory, and forbids sensitive files:

fs-readonly-policy.yamlyaml
hushspec: "0.1.0"
name: fs-readonly

rules:
  # Only allow read-oriented tools
  tool_access:
    enabled: true
    default: block
    allow:
      - read_text_file
      - read_file
      - list_directory
      - search_files

  # Restrict filesystem access to the workspace.
  # Globs are matched against the normalized path, which drops "." segments,
  # so a "./workspace/**" pattern would never match anything.
  path_allowlist:
    enabled: true
    read:
      - "**/workspace/**"
    write: []
    patch: []

  # Block sensitive file patterns
  forbidden_paths:
    enabled: true
    patterns:
      - "**/.env"
      - "**/.env.*"
      - "**/*.pem"
      - "**/*.key"
      - "**/.ssh/**"
      - "**/credentials*"
    exceptions: []

  # No shell commands through this server
  shell_commands:
    enabled: true
    forbidden_patterns:
      - ".*"

  # No network egress
  egress:
    enabled: true
    allow: []
    block: []

  # Scan for secrets in tool arguments
  secret_patterns:
    enabled: true

  # Validate patches
  patch_integrity:
    enabled: true

  # Rate limit: 200 calls per 2 minutes
  velocity:
    enabled: true
    max_invocations_per_window: 200
    window_secs: 120

Start restrictive, open selectively

Begin with default: block on tool access and an empty write list. Add permissions only when your workflow requires them. This follows the principle of least privilege.

For a database server, the policy shape changes. Here is one for a PostgreSQL MCP server that allows read queries but blocks mutations:

db-readonly-policy.yamlyaml
hushspec: "0.1.0"
name: db-readonly

rules:
  tool_access:
    enabled: true
    default: block
    allow:
      - query
      - list_tables
      - describe_table

  # Block destructive SQL patterns
  shell_commands:
    enabled: true
    forbidden_patterns:
      - "(?i)\b(DROP|DELETE|TRUNCATE|ALTER|INSERT|UPDATE)\b"

  # Allow egress only to the database host
  egress:
    enabled: true
    allow:
      - "db.internal:5432"

  # Scan for leaked credentials in query results
  secret_patterns:
    enabled: true

  velocity:
    enabled: true
    max_invocations_per_window: 50
    window_secs: 60

3. Dry-run the Policy with chio check

chio check evaluates one tool call against a policy without starting a server. It answers some questions about this policy and cannot answer others, so it is worth being precise about which.

The command as written on most pages does not run against this policy. secret_patterns and patch_integrity are output-sensitive, so the default preflight mode has nothing to evaluate them against and refuses:

mcp-wrap · check-preflighttranscript
$ chio --session-db ./admission-0.db --receipt-db ./receipts.db \
  check --policy ./fs-readonly-policy.yaml \
  --tool read_text_file --server srv-files \
  --params '{"path": "./workspace/src/main.ts"}'
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

Add --mode full, an --output-fixture file holding the JSON the tool would have returned, and the two required database flags, and the call is evaluated. It is still a deny:

mcp-wrap · check-fulltranscript
$ chio --session-db ./admission-1.db --receipt-db ./receipts.db \
  check --policy ./fs-readonly-policy.yaml \
  --tool read_text_file --server srv-files \
  --params '{"path": "./workspace/src/main.ts"}' \
  --mode full --output-fixture ./output-fixture.json
verdict:    DENY
tool:       read_text_file
server:     srv-files
reason:     guard denied the request: guard "guard-pipeline" denied the request
receipt_id: 50c9d02dce2d678c3a6b97edcfb8e6dee357a3e837541b4cf7555a21ad8a1478
policy:     08e0e4550624c92d7154575cb2f2b8210019f483bd9f9f49b1c033c7b6db7720
source:     cad65775097103eb33ac614535fcc47b74eddd34af34e2979757b4567c4298a7
mode:       full
fixture:    true
exit 2deny

That is not a policy bug. chio check opens a session that declares no filesystem roots, and PathAllowlistGuard treats an empty root set as matching nothing, so it denies every path-bearing call before it reads its own allowlist (crates/kernel/chio-kernel/src/kernel/dispatch.rs:751-753 with crates/guards/chio-guards/src/path_allowlist.rs:198-203). Any policy enabling path_allowlist therefore has no allow case in the dry run.

What the dry run does prove is the tool boundary. A tool outside tool_access.allow is refused at capability selection, before any guard, and the reason names the tool and the server:

mcp-wrap · check-tool-denytranscript
$ chio --session-db ./admission-2.db --receipt-db ./receipts.db \
  check --policy ./fs-readonly-policy.yaml \
  --tool write_file --server srv-files \
  --params '{"path": "./workspace/out.txt", "content": "hello"}' \
  --mode full --output-fixture ./output-fixture.json
verdict:    DENY
tool:       write_file
server:     srv-files
reason:     requested tool write_file on server srv-files is not in capability scope
receipt_id: a9314084d4f0fed6f25e21ebf92418fc47b648b77b2934cdfbfcc617f9643f7d
policy:     08e0e4550624c92d7154575cb2f2b8210019f483bd9f9f49b1c033c7b6db7720
source:     cad65775097103eb33ac614535fcc47b74eddd34af34e2979757b4567c4298a7
mode:       full
fixture:    true
exit 2deny

The two verdicts read very differently in the receipt store, which is where the guard that fired is recorded. The reason: line only ever names the pipeline (crates/guards/chio-guards/src/pipeline.rs:58-61); evidence[].guard_name names the guard:

mcp-wrap · check-evidencetranscript
$ chio --receipt-db ./receipts.db receipt list --admin-all \
  | jq -c '{tool: .tool_name, verdict: .decision.verdict,
$             reason: .decision.reason,
$             evidence: [.evidence[]?.guard_name]}'
{"tool":"read_text_file","verdict":"deny","reason":"guard denied the request: guard \"guard-pipeline\" denied the request","evidence":["path-allowlist"]}
{"tool":"write_file","verdict":"deny","reason":"requested tool write_file on server srv-files is not in capability scope","evidence":[]}
exit 0deny

What to check before you serve

Use the dry run for the tool boundary, for forbidden_paths, for egress and for shell_commands. Give each check its own --session-db: the command issues every request under the fixed id check-001 (crates/products/chio-cli/src/cli/runtime.rs:703), so a second check against the same database is denied by admission rather than by policy. For path_allowlist, use the live session in step 5.

4. Run chio mcp serve

With the policy tested, start the governed MCP server. --policy and --preset are mutually exclusive: pass one or the other, not both.

bash
# Using a policy file you wrote:
$ chio mcp serve --policy <file> --server-id <id> \
    -- <command>

# Or using a bundled preset (today: code-agent):
$ chio mcp serve --preset code-agent --server-id <id> \
    -- <command>

The --preset code-agent preset bundles the deny-by-default guards appropriate for code-agent workflows (safe file reads, denies .env / .git/** / .ssh/** writes, denies git push --force). Against the filesystem server with the custom policy from Step 2:

bash
$ chio --session-db ./admission.sqlite --receipt-db ./receipts.sqlite \
    mcp serve --policy ./fs-readonly-policy.yaml --server-id srv-files \
    -- npx -y @modelcontextprotocol/server-filesystem ./workspace

Both database flags are required, on this command as on chio check. Without them the edge refuses to start with durable admission mode requires a database, and --preset code-agent does not exempt it. Two edges may share one --receipt-db, which is what puts both servers in one audit trail.

Chio starts, spawns the MCP server as a subprocess, discovers its tools, and begins proxying tool calls. The proxy is transparent to the agent. Even though the MCP server exposes write_file, the policy blocks it, and the call is refused before it reaches the subprocess.

Two kinds of refusal come out of this edge and only one of them writes a receipt. A tool outside the active capability set is refused during tool selection, before the kernel evaluates anything, and returns a JSON-RPC result with isError set and a notifications/message log entry (crates/protocol/chio-mcp-edge/src/runtime/tool_calls.rs:339-361). A call that reaches the guards produces a receipt whichever way it goes. Step 5 shows both.

Server ID must be unique

The --server-id identifies this tool server in capability tokens and receipts. Use a descriptive, stable ID like srv-files or srv-postgres-prod. Changing the ID invalidates existing capability tokens scoped to it.

5. Connect Your Agent to the Chio Proxy

Your agent connects to Chio exactly as it would connect to any MCP server. If you are using an MCP client configuration file, point the command at Chio instead of the raw server:

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

The agent sees the same tools, request and response format, and transport. What changes is what happens between the call and the subprocess.

Filesystem tools take absolute paths, and path_allowlist is enforced against the roots the client declares over roots/list, so a client that declares no roots has every path-bearing call denied. The three calls below come from a client that declares ./workspace as a root: one read the policy permits, one read forbidden_paths blocks, and one write that is not in the capability set at all.

mcp-wrap · sessiontranscript
$ python3 ./mcp_client.py \
  chio --session-db ./stdio-session.db --receipt-db ./stdio-receipts.db \
    mcp serve --policy ./fs-readonly-policy.yaml --server-id srv-files \
    -- npx -y @modelcontextprotocol/server-filesystem ./workspace
{"tool": "read_text_file", "isError": false, "content": [{"text": "export const x = 1;\n", "type": "text"}]}
{"tool": "read_text_file", "isError": true, "content": [{"text": "guard denied the request: guard \"guard-pipeline\" denied the request", "type": "text"}]}
{"tool": "write_file", "isError": true, "content": [{"text": "tool is not authorized by the active capability set", "type": "text"}]}
exit 0

The first is the allow this policy exists to permit, and it is the case chio check could not produce. The second is a guard deny, carrying the pipeline's message. The third is the capability refusal, which never reaches the guards and reads differently for that reason.


6. Monitor Receipts

While the server is running, every tool call, allowed or denied, produces a signed receipt. Inspect the log to verify policy enforcement:

mcp-wrap · session-receiptstranscript
$ chio --receipt-db ./stdio-receipts.db receipt list --admin-all \
  | jq -c '{tool: .tool_name, server: .tool_server,
$             verdict: .decision.verdict,
$             evidence: [.evidence[]?.guard_name]}'
{"tool":"read_text_file","server":"srv-files","verdict":"allow","evidence":[]}
{"tool":"read_text_file","server":"srv-files","verdict":"deny","evidence":["forbidden-path"]}
exit 0allow

Two receipts for three calls. The write_file refusal produced none, because tool selection rejected it before the kernel was asked (crates/protocol/chio-mcp-edge/src/runtime/tool_calls.rs:339-361). Everything the kernel evaluates is recorded; a call it never sees is visible in the edge's notifications/message log instead.

decision.reason on the guard deny names guard-pipeline and decision.guard reads kernel, on this path as on every other. The rule that fired is in evidence[].guard_name.

A local --receipt-db read fails closed unless you pass exactly one of --tenant <id> or --admin-all, so the operator names the read boundary explicitly rather than defaulting to a cross-tenant read. Output is JSON Lines (one receipt per line). Pipe to jq for summaries or a pretty printer, and filter by capability, tool, outcome, or cost using the flags on chio receipt list --help.

Each receipt is cryptographically signed with the kernel's Ed25519 key. Receipts are non-repudiable evidence of what was requested, what decision was made, and which guards were evaluated. See the Receipts guide for the full receipt format and verification.


7. Advanced: HTTP Edge Mode

chio mcp serve-http exposes the same governed server over Streamable HTTP instead of stdio. It requires --policy and --server-id, and takes --listen <addr>, which defaults to 127.0.0.1:8931.

Two more things are required and neither is optional. The edge is network-facing, so it refuses to start without an authentication source:

mcp-wrap · serve-http-no-authtranscript
$ chio mcp serve-http --policy ./fs-readonly-policy.yaml \
  --server-id srv-files --listen 127.0.0.1:8931 \
  -- npx -y @modelcontextprotocol/server-filesystem ./workspace
error [urn:chio:error:cli:other]: remote MCP edge requires either --auth-token, --auth-jwt-public-key, --auth-jwt-discovery-url, --auth-introspection-url, or --auth-server-seed-file
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

Any one of those five satisfies it. --auth-token is the simplest, and reads CHIO_AUTH_TOKEN from the environment so the bearer does not appear in ps. With one supplied, the durable-admission requirement is next:

mcp-wrap · serve-http-no-dbtranscript
$ chio mcp serve-http --policy ./fs-readonly-policy.yaml \
  --server-id srv-files --listen 127.0.0.1:8931 \
  --auth-token demo-token \
  -- 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

With auth and both databases, it starts. Startup writes one line, on stderr, naming the endpoint:

mcp-wrap · serve-httptranscript
$ chio --session-db ./edge-session.db --receipt-db ./edge-receipts.db \
  mcp serve-http --policy ./fs-readonly-policy.yaml \
  --server-id srv-files --listen 127.0.0.1:8931 --auth-token demo-token \
  -- npx -y @modelcontextprotocol/server-filesystem ./workspace
remote MCP edge listening on http://127.0.0.1:8931/mcp
exit 0

Agents connect to that endpoint instead of spawning a local process. The guard pipeline, the receipt writing and the policy enforcement are the same as in stdio mode.

Connect from Python or TypeScript

An agent written in code, rather than configured through a client config file, opens a session against the edge and calls tools through it. Two clients ship for this, one per ecosystem, and they are the same object with the same three moves: construct with a bearer token, initialize to get a session, then list and call tools on that session. Neither has a default endpoint: the base URL is a required first argument in both (sdks/python/chio-py/src/chio/client.py:43-50, sdks/typescript/chio-ts/src/client/client.ts:41-50), so it must match whatever --listen was set to.

bash
$ pip install chio-sdk
$ npm install @chio-protocol/sdk

The Python distribution is chio-sdk and the import package it installs is chio. The same block also queries the receipts those calls produced, so one script drives the server and reads its own audit trail:

sdks/python/chio-py/README.md:17-31sdks/typescript/chio-ts/README.md:18-34
from chio import ChioClient, ReceiptQueryClient

client = ChioClient.with_static_bearer("http://127.0.0.1:8931", "demo-token")
session = client.initialize()

try:
    tools = session.list_tools()
    print(tools)

    receipts = ReceiptQueryClient("http://127.0.0.1:8940", "demo-token").query(
        {"toolServer": "wrapped-http-mock", "limit": 5}
    )
    print(receipts["totalCount"])
finally:
    session.close()

Set --server-id on the edge to whatever you pass as toolServer in the query, and the receipts from these calls come back under that name. The TypeScript package is ESM, ships its own declarations, and requires Node 22 or newer; the Python package requires 3.11 or newer and pulls in httpx and pure25519.

Secure the HTTP edge

In production, use TLS and authenticate incoming requests with the built-in auth flags. The HTTP edge is a network-facing service; apply the access controls required for a public API endpoint.

8. Advanced: Multiple MCP Servers Under One Policy

A single Chio instance can govern multiple MCP servers. Each server gets its own server ID, but they share a policy. This is useful when an agent needs access to both a filesystem and a database, for example.

Write a combined policy that addresses tools from all servers:

multi-server-policy.yamlyaml
hushspec: "0.1.0"
name: multi-server

rules:
  tool_access:
    enabled: true
    default: block
    allow:
      # Filesystem tools
      - read_text_file
      - list_directory
      # Database tools
      - query
      - list_tables
      - describe_table

  path_allowlist:
    enabled: true
    read:
      - "**/workspace/**"
    write: []
    patch: []

  forbidden_paths:
    enabled: true
    patterns:
      - "**/.env"
      - "**/.ssh/**"

  egress:
    enabled: true
    allow:
      - "db.internal:5432"

  shell_commands:
    enabled: true
    forbidden_patterns:
      - "(?i)\b(DROP|DELETE|TRUNCATE|ALTER|INSERT|UPDATE)\b"

  secret_patterns:
    enabled: true

  patch_integrity:
    enabled: true

  velocity:
    enabled: true
    max_invocations_per_window: 300
    window_secs: 120

Then start each server with its own server ID, pointing at the shared policy:

bash
# Terminal 1: Filesystem server
$ chio --session-db ./admission-files.sqlite --receipt-db ./receipts.sqlite \
    mcp serve --policy ./multi-server-policy.yaml --server-id srv-files \
    -- npx -y @modelcontextprotocol/server-filesystem ./workspace

# Terminal 2: Database server. Same receipt database, its own session database.
$ chio --session-db ./admission-db.sqlite --receipt-db ./receipts.sqlite \
    mcp serve --policy ./multi-server-policy.yaml --server-id srv-postgres \
    -- npx -y @modelcontextprotocol/server-postgres $DATABASE_URL

The two edges share a receipt database, which is what makes one audit trail out of two servers, and each carries its own session database. Both flags are required on every edge.

Your agent connects to both chio proxies. The mcp-tool guard applies uniformly: the agent can call read_text_file on srv-files and query on srv-postgres, but write_file is blocked on both. Receipts from all servers are collected in the same log, tagged by server ID.


Summary

Wrapping an MCP server with Chio gives you:

  • Policy enforcement: every tool call evaluated against your HushSpec policy
  • Seven stateless guards + velocity: forbidden-path, path-allowlist, shell-command, egress-allowlist, mcp-tool, secret-leak, patch-integrity, and velocity
  • Signed receipts: cryptographic proof of every decision
  • Zero server modifications: the MCP server runs unmodified as a subprocess
  • Transparent proxy: agents connect to Chio the same way they connect to any MCP server

Next Steps

  • Write a Policy · HushSpec policy authoring and the available guard blocks
  • Native Tool Server · build a chio-native tool server for tighter integration
  • Receipts · deep dive into the receipt format and verification