BuildConnect
Wrap an MCP Server
Wrap an MCP server with Chio to evaluate tool calls against policy and emit signed receipts.
Prerequisites
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/listmethod (chio mediates this transparently) and generates a tool manifest - Intercepts calls: translates incoming requests into MCP
tools/callmessages, 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.
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-scopesprints 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 forcursor,claude-desktop,continue, orzedand exits.--manifest <path>runs the gated proxy loop against a promoted scaffold. With no--manifest, the allow-set is empty and everytools/callis denied.--strict-execution-nonceupgrades the path: allowedtools/callrequests 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.
# 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 ./workspacePromote 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.
# Capture the inferred scaffold to a file you can edit:
$ chio mcp wrap --print-scopes \
-- npx -y @modelcontextprotocol/server-filesystem ./workspace \
> fs-scaffold.toml# chio mcp wrap -- inferred capability manifest scaffold
# Review each tool below; flip `allow = true` to promote.
# Default is deny.
server_id = "mcp"
[[capability]]
tool = "read_file"
scope = "filesystem"
urn = "urn:chio:scope:tool:filesystem"
allow = true # promoted
[[capability]]
tool = "write_file"
scope = "filesystem"
urn = "urn:chio:scope:tool:filesystem"
allow = false # stays denied# 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 ./workspaceA 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:
| Server | Command | Tools Exposed |
|---|---|---|
| Filesystem | npx -y @modelcontextprotocol/server-filesystem ./workspace | read_file, write_file, list_directory |
| PostgreSQL | npx -y @modelcontextprotocol/server-postgres $DATABASE_URL | query, list_tables, describe_table |
| GitHub | npx -y @modelcontextprotocol/server-github | search_repositories, create_issue, get_file_contents |
| Fetch (HTTP) | npx -y @modelcontextprotocol/server-fetch | fetch |
This guide uses the filesystem server. The same steps apply to any MCP server.
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:
hushspec: "0.1.0"
name: fs-readonly
rules:
# Only allow read-oriented tools
tool_access:
enabled: true
default: block
allow:
- read_file
- list_directory
- search_files
# Restrict filesystem access to the workspace
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 and responses
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: 120Start restrictive, open selectively
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:
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: 603. Test with chio check
Before running a live server, dry-run your policy against specific tool calls. The chio check command evaluates a tool call against a policy without starting any server.
# Should ALLOW: reading a file inside the workspace
$ chio check --policy ./fs-readonly-policy.yaml \
--tool read_file --server srv-files \
--params '{"path": "./workspace/src/main.ts"}'
verdict: ALLOW
tool: read_file
server: srv-files
receipt_id: rcpt-019dbbf8-33db-7f21-81c7-aab0427616c8
policy: a40c24d0930d773e060fac86dd77e24e68af4cb0a59b1b836759ed63fbaa23b8
source: d14550004f854d4131839bd2388b3ec9aa3784c898a47f261d434bffbc88d799# Should DENY: write_file is not in the allow list
$ chio check --policy ./fs-readonly-policy.yaml \
--tool write_file --server srv-files \
--params '{"path": "./workspace/output.txt", "content": "hello"}'
verdict: DENY
tool: write_file
server: srv-files
reason: requested tool write_file on server srv-files is not in capability scope
receipt_id: rcpt-019dbbf8-33ef-7dc3-9143-25968ddd18e9
policy: a40c24d0930d773e060fac86dd77e24e68af4cb0a59b1b836759ed63fbaa23b8
source: d14550004f854d4131839bd2388b3ec9aa3784c898a47f261d434bffbc88d799# Should DENY: path matches forbidden pattern
$ chio check --policy ./fs-readonly-policy.yaml \
--tool read_file --server srv-files \
--params '{"path": "./workspace/.env"}'
verdict: DENY
tool: read_file
server: srv-files
reason: guard denied the request: guard "forbidden-path" denied the request: path matches forbidden pattern **/.env
receipt_id: rcpt-019dbbf8-3405-74b2-86b5-07ac94779b39
policy: a40c24d0930d773e060fac86dd77e24e68af4cb0a59b1b836759ed63fbaa23b8
source: d14550004f854d4131839bd2388b3ec9aa3784c898a47f261d434bffbc88d799# Should DENY: path outside the allowlist
$ chio check --policy ./fs-readonly-policy.yaml \
--tool read_file --server srv-files \
--params '{"path": "/etc/passwd"}'
verdict: DENY
tool: read_file
server: srv-files
reason: guard denied the request: guard "path-allowlist" denied the request: path not in read allowlist
receipt_id: rcpt-019dbbf8-3421-7a03-9b14-2f7d0ad0e12f
policy: a40c24d0930d773e060fac86dd77e24e68af4cb0a59b1b836759ed63fbaa23b8
source: d14550004f854d4131839bd2388b3ec9aa3784c898a47f261d434bffbc88d799Check before you serve
chio check for every tool your server exposes. Verify that allowed tools pass and forbidden operations are denied. This catches policy mistakes before they reach production.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.
# 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:
$ chio mcp serve --policy ./fs-readonly-policy.yaml --server-id srv-files \
--receipt-db ./receipts.sqlite \
-- npx -y @modelcontextprotocol/server-filesystem ./workspaceChio starts, spawns the MCP server as a subprocess, discovers its tools, and begins proxying tool calls. Every allow and deny is appended to the receipt database at ./receipts.sqlite. The proxy is transparent. Even though the MCP server exposes write_file, your policy blocks it: every invocation attempt is denied before reaching the subprocess.
Server ID must be unique
--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:
{
"mcpServers": {
"filesystem": {
"command": "chio",
"args": [
"--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/response format, and transport. Each call now passes through the configured guards and produces a signed receipt.
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:
$ chio --receipt-db ./receipts.sqlite receipt list \
--tool-server srv-files --limit 5 --admin-all
{"id":"rcpt-019dbbf8-4cfe-...","timestamp":1776975105,"capability_id":"cap-...","tool_server":"srv-files","tool_name":"read_file","action":{"parameters":{"path":"./workspace/README.md"},"parameter_hash":"a3c8a200..."},"decision":{"verdict":"allow"},"content_hash":"42e9fd40...","policy_hash":"a40c24d0...","kernel_key":"25403c1e...","signature":"7be63cdb..."}
{"id":"rcpt-019dbbf8-4d46-...","timestamp":1776975105,"capability_id":"cap-...","tool_server":"srv-files","tool_name":"list_directory","action":{...},"decision":{"verdict":"allow"},...}
{"id":"rcpt-019dbbf8-4d78-...","timestamp":1776975105,"capability_id":"cap-...","tool_server":"srv-files","tool_name":"write_file","action":{...},"decision":{"verdict":"deny","reason":"requested tool write_file on server srv-files is not in capability scope","guard":"kernel"},...}
{"id":"rcpt-019dbbf8-4d90-...","timestamp":1776975106,"capability_id":"cap-...","tool_server":"srv-files","tool_name":"read_file","action":{"parameters":{"path":"./workspace/.env"},...},"decision":{"verdict":"deny","reason":"guard \"forbidden-path\" denied the request","guard":"forbidden-path"},...}
{"id":"rcpt-019dbbf8-4db2-...","timestamp":1776975107,"capability_id":"cap-...","tool_server":"srv-files","tool_name":"read_file","action":{...},"decision":{"verdict":"allow"},...}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
For production deployments, you can expose the governed MCP server over Streamable HTTP instead of stdio using chio mcp serve-http. It requires --policy and --server-id, takes --listen <addr> (default 127.0.0.1:8931), and exposes authentication flags for incoming requests.
$ chio mcp serve-http --policy ./fs-readonly-policy.yaml --server-id srv-files \
--listen 0.0.0.0:8080 \
-- npx -y @modelcontextprotocol/server-filesystem ./workspaceINFO starting HTTP edge
listen: 0.0.0.0:8080
policy: ./fs-readonly-policy.yaml
server_id: srv-files
INFO chio HTTP edge ready
endpoint: http://0.0.0.0:8080/mcpAgents connect to the HTTP endpoint instead of spawning a local process. The guard pipeline, receipt generation, and policy enforcement are identical to stdio mode. The HTTP edge adds TLS termination, request routing, and connection management for multi-tenant scenarios.
Secure the HTTP edge
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:
hushspec: "0.1.0"
name: multi-server
rules:
tool_access:
enabled: true
default: block
allow:
# Filesystem tools
- read_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: 120Then start each server with its own server ID, pointing at the shared policy:
# Terminal 1: Filesystem server
$ chio mcp serve --policy ./multi-server-policy.yaml --server-id srv-files \
--receipt-db ./receipts.sqlite \
-- npx -y @modelcontextprotocol/server-filesystem ./workspace
# Terminal 2: Database server (shares the same receipt database)
$ chio mcp serve --policy ./multi-server-policy.yaml --server-id srv-postgres \
--receipt-db ./receipts.sqlite \
-- npx -y @modelcontextprotocol/server-postgres $DATABASE_URLYour agent connects to both chio proxies. The mcp-tool guard applies uniformly: the agent can call read_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