Wrap an MCP Server
Chio sits between your agent and any MCP server as a transparent proxy. The server runs unmodified as a subprocess while Chio intercepts every tool call, evaluates it against your policy, and produces a signed receipt. This guide walks through the full lifecycle: writing a policy, testing it offline, running a governed server, and connecting an agent.
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 every allow or deny, creating an immutable audit trail
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.
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 |
For this guide, we will use the filesystem server to keep things concrete. 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: 200
window_seconds: 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: 50
window_seconds: 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-paths" 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, the same request/response format, and the same transport. The only difference is that every call now passes through the guard pipeline 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
{"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-paths\" denied the request","guard":"forbidden-paths"},...}
{"id":"rcpt-019dbbf8-4db2-...","timestamp":1776975107,"capability_id":"cap-...","tool_server":"srv-files","tool_name":"read_file","action":{...},"decision":{"verdict":"allow"},...}Default 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 a full suite of auth flags for incoming request authentication.
$ 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: 300
window_seconds: 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 · comprehensive guide to HushSpec policy authoring and every guard
- Native Tool Server · build a chio-native tool server for tighter integration
- Receipts · deep dive into the receipt format and verification