MCP proxy
Put HESO in front of any MCP server so every tools/call is gated, signed, and receipted. Point your MCP client at heso-mcp instead of the real server command — the server and the agent stay exactly as they are.
heso-mcp is a console script the Python SDK ships (the heso-mcp entry point runs heso._mcp_proxy:main). It speaks the MCP stdio transport on both sides: it spawns the real server, forwards every message verbatim, and intercepts only tools/call requests. Each intercepted call is captured, evaluated against your policy, signed, and audited before it reaches the server. initialize, tools/list, notifications, and server logs all pass through byte-for-byte, so neither side knows the proxy is there.
Run it
Everything before the -- configures the proxy; everything after it is the real server command, run with its own flags untouched.
# everything after -- is the real server command, run with its own flags
heso-mcp --project-root ~/agent -- npx -y @some/mcp-server --its --own --flagsThe operator key is read from <project-root>/heso-local-data/identity.key, so the proxy gates against the same identity and policy as your scaffolded project. Run heso init in that directory first if you have not already.
Options
Each flag has an environment-variable equivalent, and an explicit flag always wins over its env var.
--project-root(HESO_PROJECT_ROOT) — the directory holdingheso.tomlandheso-local-data/.--policy— a path to aheso.tomlfile; its directory becomes the project root. Use it when the client launches the server from an unpredictable working directory.--key-passphrase/--key-passphrase-file(HESO_KEY_PASSPHRASE/HESO_KEY_PASSPHRASE_FILE) — the passphrase for an encrypted operator key. Prefer the file or env form; flags are visible in process listings. The gitignored dev passphrase file inheso-local-data/is the last-resort fallback, and it is refused underHESO_ENV=production.--workflow(HESO_WORKFLOW) /--account(HESO_ACCOUNT) — the workflow id and account/tenant stamped on every captured action.--api-key/--endpoint(HESO_API_KEY/HESO_ENDPOINT) — optional HESO cloud credentials. When set, a suspended call also opens a hosted approval in the console so an approver can co-sign from there.--observe— mirror-only mode: every call is still captured, signed, and receipted, but refusals are not enforced (theHESO_BLOCKING=0equivalent). Use it to shadow-deploy a policy without breaking the agent.
Wire a client
Set the server’s command to heso-mcp and move the real server command into args after a -- separator. That is the whole change. The model still sees the same tools.
Claude Desktop
{
"mcpServers": {
"filesystem": {
"command": "heso-mcp",
"args": [
"--project-root", "/Users/you/agent",
"--",
"npx", "-y", "@modelcontextprotocol/server-filesystem", "/Users/you/work"
]
}
}
}Cursor
Cursor reads the same shape from .cursor/mcp.json. Here we also pass an explicit --policy so the project root is unambiguous regardless of where Cursor launches the server from.
{
"mcpServers": {
"filesystem": {
"command": "heso-mcp",
"args": [
"--project-root", "/Users/you/agent",
"--policy", "/Users/you/agent/heso.toml",
"--",
"npx", "-y", "@modelcontextprotocol/server-filesystem", "/Users/you/work"
]
}
}
}What gets captured
Each tools/call becomes one tool_callaction: the tool name and the parsed arguments, captured under the engine’s mcp_proxy surface. When policy allowsthe call, the proxy writes the signed receipt first, then forwards the original request bytes untouched. When the server’s response comes back, the result is bound to that same action as a follow-up receipt, so the trail records what happened, not only what was allowed. Both land at <project-root>/heso-local-data/receipts/<action_hash>.json — the signed envelope documented field by field in the receipt schema.
What happens on block or suspend
When policy refuses the call, the request is never forwarded. The client receives a normal MCP tool result with isError: true carrying the reason, so the model can relay it instead of hanging:
- block — the result text names the responsible rule and says the call was not forwarded.
- suspend — the result text carries the
action_hasha human approves and tells the model to retry the exact same tool call once approved, which then forwards it with an L1 (human co-signed) receipt. With cloud credentials configured, the approval is also opened in the HESO console.
If the engine faults while gating a call, the proxy fails closed: the call is refused with an isError tool result and never forwarded ungated. A JSON-RPC batch (which MCP forbids) is refused outright rather than forwarded, since forwarding one would be a gate bypass.
Gate in-process instead
If you own the MCP server code, you can gate inside it rather than running a separate proxy. The Python SDK exposes the same gate as heso.mcp: wrap_call_tool wraps the (name, arguments) dispatch your @server.call_tool() handler registers, so every call is gated on the way in and receipted on the way out.
import heso
heso.init(project_root="~/agent")
# wrap the (name, arguments) dispatch your @server.call_tool() handler registers
@server.call_tool()
@heso.mcp.wrap_call_tool
async def handle(name, arguments):
...For gating frameworks that aren’t MCP — LangChain, CrewAI, the Claude Agent SDK, and others — see the framework adapters.
Verify the receipts offline
Every receipt the proxy writes is a standalone artifact you can check with no HESO infrastructure. Point heso verify at the file the proxy wrote, or at its action_hash:
# the proxy wrote one JSON receipt per gated call into the data dir
heso verify ~/agent/heso-local-data/receipts/7f3c91ab….json
# or by action_hash, straight from the local store
heso verify 7f3c91abIt re-runs the same BLAKE3 and Ed25519 math locally and prints the verdict: Valid (L0) for an operator-signed call, Valid (L1) once a human has co-signed an approved one. The browser verifier checks the same receipts the same way.
heso-mcp gates what the agent does — the tools/callrequests it sends and the arguments they carry. It does not gate the model’s reasoning or its text. A receipt proves you authorized this tool call under your policy; it does not prove the action succeeded downstream.