Get Helio running in under 5 minutes. By the end, you'll have an MCP governance proxy intercepting tool calls, enforcing policy rules, and recording an audit trail — with a live dashboard showing everything.
- Node.js 22+ — check with
node --version jq(optional) — used in examples below for pretty-printing JSON. If unavailable, remove| jqand read raw JSON output.- An MCP server to govern — any server that speaks Streamable HTTP, SSE, or stdio. Helio works with any spec-compliant MCP server (e.g. FastMCP, the official MCP SDKs) with zero changes to the server. No server to test against? Use the zero-dependency echo server below — no clone or install needed. (If you've cloned the repo as a contributor, you can instead run the bundled examples with
pnpm start; seeexamples/basic/.)
Helio ships a tiny echo server that speaks MCP over plain node:http — no dependencies, no install. Save it and run it on the default upstream port:
curl -O https://raw.githubusercontent.com/gethelio/helio/main/examples/_shared/mcp-echo-server.mjs
node mcp-echo-server.mjsIt prints MCP echo server listening on http://127.0.0.1:8080/mcp and exposes a handful of demo tools (get_weather, send_email, delete_record, create_payment, create_refund, stripe_charge, paypal_payout) with realistic annotations, so the policy examples below have something meaningful to match. Leave it running in its own terminal — the default upstream.url (http://localhost:8080/mcp) already points at it.
Scaffold a new configuration file:
npx @gethelio/proxy initThis creates a helio.yaml in your current directory with commented defaults. Alternatively, install globally:
npm install -g @gethelio/proxy
helio init@gethelio/proxy includes the dashboard assets in the same package. There is no separate dashboard npm install step.
Open helio.yaml and set upstream.url to point at your MCP server.
Helio starts in audit-only mode.
helio initscaffolds thepoliciessection commented out, so out of the box Helio runs withdefault: allowand zero rules — it records every tool call but blocks nothing until you add rules. The example below is an illustrative target (not the fileinitwrites); uncomment and adaptpoliciesto start enforcing. See the Policy Guide for rule syntax.
version: '1'
upstream:
url: 'http://localhost:8080/mcp'
transport: streamable-http
listen:
port: 3000
policies:
default: allow
# These rules match on MCP tool annotations (readOnlyHint / destructiveHint):
rules:
- name: block-destructive
match:
annotations:
destructiveHint: true
action: deny
feedback:
message: 'Destructive operations are blocked by policy.'
suggestion: 'Use a non-destructive alternative.'
- name: allow-reads
match:
annotations:
readOnlyHint: true
action: allow
audit:
storage: sqlite
retention: 90d
include_responses: true
dashboard:
enabled: true
port: 3100
api_secret: '${HELIO_DASHBOARD_SECRET}'Omitted fields like listen.host, dashboard.host, and audit.path fall back to safe defaults (127.0.0.1 for both hosts — loopback only — and ./helio-audit.db). The section order here (upstream → listen → policies → audit → dashboard) matches what helio init scaffolds and the Configuration Reference.
This example configuration blocks any tool marked as destructive, explicitly allows read-only tools, and falls through to default: allow for everything else. Every tool call is recorded to a local SQLite database. (Until you uncomment a policies block like this one, the scaffolded config blocks nothing.)
If you use the ${HELIO_DASHBOARD_SECRET} placeholder above, export it before starting:
export HELIO_DASHBOARD_SECRET="$(openssl rand -hex 32)"If you started from helio init, the generated helio.yaml already includes a secret, so you can keep that value instead.
See the Configuration Reference for all available fields and defaults.
npx @gethelio/proxy start
# or: helio start (if installed globally)You should see output like:
Helio proxy listening on http://127.0.0.1:3000
Policies: 2 rules loaded (default: allow)
Upstream: http://localhost:8080/mcp (streamable-http)
Audit: ./helio-audit.db (retention: 90d)
Dashboard API listening on http://127.0.0.1:3100
Approvals: timeout 300s, default on timeout: deny, 0 channels configured
Rate limits: enabled
Spend limits: enabled
Config: helio.yaml
Watching helio.yaml for policy changes
Note: Use
-cto specify a different config file:npx @gethelio/proxy start -c production.yamlOn startup, Helio also sends a synthetic upstream
tools/listto warm the tool-annotation cache before first traffic. If the prime attempt cannot complete quickly, Helio continues startup and retries in the background. During that window, annotation matching remains fail-closed using MCP defaults.If
dashboard.enabled: true, startup requires bundled dashboard assets to be present. If assets are missing, Helio exits with an actionable error instead of silently serving API-only mode.
Instead of connecting your MCP client directly to the upstream server, point it at the proxy on http://localhost:3000/mcp.
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"my-server": {
"url": "http://localhost:3000/mcp"
}
}
}Any HTTP MCP client — change the server URL from http://localhost:8080/mcp to http://localhost:3000/mcp. The proxy is fully transparent: it forwards all MCP methods unchanged and only intercepts tools/call for policy evaluation.
No client or agent handy? You don't need one to try Helio:
- MCP Inspector — run
npx @modelcontextprotocol/inspector, choose the Streamable HTTP transport, and point it athttp://localhost:3000/mcp. Every tool call you make appears in the dashboard with its policy decision. - curl — send a request straight through the proxy (see Step 6 below). This works against any upstream, including the echo server.
For requests that pass JSON-RPC ingress validation at /mcp, Helio always returns a JSON-RPC response envelope.
- Valid upstream JSON-RPC responses pass through unchanged at the body layer.
- Upstream forwarding failures and non-JSON-RPC upstream payloads are normalized to:
{"jsonrpc":"2.0","id":<request-id>,"error":{"code":-32603,"message":"...","data":{"failure_class":"..."}}} - HTTP ingress validation errors (for example malformed JSON or wrong
Content-Type) still return transport HTTP errors such as400or415.
Navigate to http://localhost:3100 to open the governance dashboard. If dashboard.api_secret is set (recommended/default), enter that secret on the login screen first.
The dashboard has six tabs:
- Feed — Real-time stream of tool calls and policy decisions
- Approvals — Pending approval queue (for
require_approvalrules) - Audit — Searchable log of every recorded action with filters
- Limits — Current rate and spend limit status per tool
- Budgets — Named cross-tool spend pots with live depletion and the spend ledger
- Analytics — Charts showing action volume, decision breakdown, and top tools
With the proxy running, send a request through it:
If
jqis not installed, remove the trailing| jqfrom the commands below.
# List available tools
curl -s -X POST http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq# Call a tool
curl -s -X POST http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_weather","arguments":{"city":"London"}}}' | jqThe tool call passes through the policy engine, gets forwarded to the upstream server, and the response is returned — with an audit record created in the background.
Add a rule to your config and watch hot-reload pick it up. With the proxy still running, edit helio.yaml and add a deny rule before the existing rules:
rules:
- name: block-email
match:
tool: 'send_email'
action: deny
feedback:
message: 'Email sending is disabled.'
suggestion: 'Contact your admin to enable email.'
- name: block-destructive
# ... existing rulesSave the file. The proxy detects the change and reloads:
[helio] Policy reloaded: 3 rules (default: allow)
Now try calling the blocked tool:
curl -s -X POST http://localhost:3000/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"send_email","arguments":{"to":"alice@example.com","body":"Hello"}}}' | jqThe response includes a structured error with the feedback message and suggestion — information an AI agent can use to self-correct.
You can also run Helio with Docker Compose. One-time setup:
cd docker
cp .env.example .env
openssl rand -hex 32 # paste this into .env as HELIO_DASHBOARD_SECRET
docker compose upThis starts the proxy, a demo MCP echo server, and the dashboard. By default both published ports are bound to 127.0.0.1 on the host (not the LAN) so the quickstart is safe on untrusted networks. See docker/README.md for the security model and how to opt in to LAN or remote access.
To run Helio in its own container next to a coding agent or dev container — so the agent is forced through governance and can't reach the MCP server directly — see Running Helio as a Sidecar.
The quickstart above is safe by default on a single-operator workstation. Before running Helio anywhere reachable from other people or machines, work through this checklist:
- Generate and store a strong
dashboard.api_secret.helio initwrites a fresh 256-bit hex value intohelio.yaml; if you authored the file by hand, runopenssl rand -hex 32and paste it intodashboard.api_secret. This value is stable until you rotate it. Treat it like a password-manager secret: if you lose it, you must generate a new one, updatehelio.yaml, and restart/reload the proxy. - Understand browser login behavior. The dashboard UI now uses a manual secret login screen: enter
dashboard.api_secretonce, then the browser uses a short-lived HttpOnly session cookie. The raw secret is not injected into frontend JS anymore. - Keep
dashboard.hoston127.0.0.1. The dashboard sideband assumes a single trust boundary and has no identity layer of its own. If you need to reach it remotely, put it behind a reverse proxy (nginx, Caddy, Cloudflare Access, Tailscale Serve) that terminates TLS and adds per-user authentication before forwarding to127.0.0.1:3100. Do not binddashboard.hostto0.0.0.0directly. - Keep the audit database on local disk. Helio's audit sqlite file is created with mode
0600(owner read/write only) — if you move it to a shared filesystem or back it up into an unencrypted bucket, you lose that isolation. Audit records contain tool inputs and upstream responses, often including PII and credentials. - Rotate secrets deliberately and expect session invalidation. The proxy reads
dashboard.api_secretfrom disk on startup and on config hot-reload. Change the value inhelio.yaml, then restart/reload to rotate. Existing dashboard sessions are revoked and users must log in again with the new secret. - Keep the main MCP port behind the same trust boundary as the dashboard.
listen.hostdefaults to127.0.0.1; if you need remote agents, use a reverse proxy or SSH tunnel rather than binding to0.0.0.0. The main port does not carry the dashboard secret — it accepts MCP traffic based on network reachability alone.
- Configuration Reference — Every
helio.yamlfield with defaults and types - Running Helio as a Sidecar — Deploy next to a coding agent / dev container so it can't bypass governance
- Policy Guide — Rule syntax, matchers, actions, rate limits, spend limits, and common patterns
- Approval Workflows — Route sensitive actions to humans via Slack, webhook, or dashboard
- Audit Trail — What's recorded, how to search, and how to export
- Examples — Four runnable configurations: basic, slack-approvals, spend-limits, budgets

