protocol: mcp lane terminates MCP streamable HTTP on /mcp and forwards to one remote MCP server over streamable-http or sse. It hides the tools a rule denies, judges each tools/call with the lane’s guardrails and OPA, and records one Sidecar session per MCP session.
The lane embeds mcpproxy and ships in Sidecar 1.289.0 and later. A Sidecar build without the MCP engine refuses the lane at -validate.
Configure a lane
config.yaml
http://127.0.0.1:18901/mcp.
Listener keys
Tool allow and deny
A guardrail rule of typemcp_tool decides which tools a lane offers. tools lists the tools it denies; allow lists the only tools it lets through. A rule sets one of the two.
config.yaml
tools/list every tool an mcp_tool rule denies, so the model never sees it. The catalog statement names the hidden tools in mcp.tools_hidden.
A call to a hidden tool is denied like any rule denial: a tool result with isError: true and the text denied by the hoop sidecar (rule <name>): <message>. MCP reports a failed tool call this way, as a result rather than a JSON-RPC error, so the model sees the denial and its message.
A tools/list result the lane cannot read ends the session and records mcp_catalog_unreadable: the lane could neither hide tools from it nor judge it.
With action: defer, an mcp_tool rule hides nothing, and OPA decides each call: without OPA, a deferring rule denies the call.
Two allow rules let through only the tools both list.
A tools/call is judged by its exact name key. Params that name no tool, or carry keys that differ only in case, are refused as invalid params (-32602) and recorded as mcp_invalid_params.
Other rule types and OPA judge calls too, and hide nothing.
A glob has one metacharacter: * matches any run of characters, including none. Every other character matches literally, and case counts: *delete* matches bulk_delete_rows, and get_* does not match Get_file.
The rule reads the real tool name, not the name overrides gives it.
An mcp_tool rule in the top-level guardrails reaches every mcp lane, and other lanes ignore it. A lane’s own rules run first and the top-level ones after them; rules: [] on the lane drops the top-level ones.
A rule edit applies without a restart and ends the open MCP sessions; the next session gets the new catalog. The rules screen of the control plane does not offer mcp_tool yet: write it in the config file.
mcp_gateway keys
tool_rate_limits globs work as in mcp_tool.
Once the session has listed tools, a call to a tool the server did not list is refused too: -32010, tool "x" was not advertised by the server.
A call these keys refuse, a truncated result and a rug pull reach the Sidecar session as activities: mcp.tool_denied, mcp.budget_exceeded, mcp.tools_changed.
A key the block does not map, such as allowed_tools (an mcp_tool rule instead), approval_tools, command or headers, fails as unknown field.
What the rules read
Eachtools/call is one statement: operation: call, tables: [<tool>] in lower case, the text <tool> with its arguments JSON on the next line, and the metadata mcp.method, mcp.tool and mcp.session.
config.yaml
tools/list is one catalog statement: direction: server, operation: show, no tables, one <name>: <description> line per tool.
Rules and OPA judge the catalog too, and a hit ends the session. A deny_words_list word in a tool description ends it.
A table rule never matches the catalog, require_table_match included.
Policy Rules describes each rule type.
OPA
With anopa block, the lane consults OPA on every call and on the catalog. The input for a call, beside the context every lane sends:
responses: false keeps the catalog from OPA:
config.yaml
Sessions and audit
One Sidecar session per MCP session. It opens on the first message and closes on the client’sDELETE or the idle timeout.
The session records initialize as an activity (client name, version, protocol version), one statement per tools/call, allowed or denied with its rule, and one statement per tools/list.
mcpproxy refusals, such as a budget or a rug pull, reach the session as activities.
A rule reload ends the open MCP sessions. The next message on one is refused, and the client initializes again under the new rules.
A change to the lane itself (upstream, listen, the mcp_gateway block) needs a restart, as for every listener.
What the audit records
Each statement carries this metadata:
The session also records these activities:
A call a guardrail rule or OPA denies is a statement with its rule, not an activity.
Auth
The lane can check a caller token. It sends no credential to the upstream except a mutual TLS certificate.Caller to lane
identity_header names a caller and verifies nothing: any process that reaches the port can set it. Use it only behind a proxy on the same host or pod that authenticates the caller and sets the header.
Callers the lane cannot tell apart share the anonymous identity, so any of them that holds a session id can use that session.
Not supported yet: a verified identity per user, and OAuth login of MCP clients.
Caller token
caller_token admits a caller by a shared token, so the lane can listen on an address other than loopback. Sidecar 1.290.0 and later; older builds refuse the key at -validate.
config.yaml
- What is refused. No header, an empty value, a value not in the file, or the header sent twice. The request gets a 403
request refused: <reason>before the lane reads its body, and the connection closes. Claude Code shows the server asneeds-auth. - 403, not 401. A 401 sends MCP clients into OAuth discovery, which the lane does not serve.
- Refusals are logged, not audited. No MCP session exists yet. The Sidecar logs
mcp request refusedwith the peer address and the reason. - What is admitted. The session and every statement name
nameas the principal. - A session belongs to its token. An
Mcp-Session-Idworks only with the token that opened it. A request with another token gets a 404, and the session recordsmcp.session_refused. - The token stays in the Sidecar. The lane removes the header first. The token never reaches the upstream, an audit row, policy input or a log.
- Refused at startup. A
headerthat MCP uses (Mcp-*,Last-Event-ID), or one the upstream, HTTP framing or the Sidecar already uses, such asauthorizationorhost.caller_tokentogether withidentity_header. - No TLS of its own. The token travels as written. Set
downstream_tls, or put TLS in front of the listener.
http lane. See Caller token.
Lane to upstream
The lane sends no
Authorization header to the upstream, the caller’s included. It refuses headers, because the config holds no inline secret.
For a server that needs a bearer token or OAuth: run the MCP server yourself with its credential, or run a proxy on loopback that adds the credential, and point upstream at that proxy (http://127.0.0.1:<port>/mcp is accepted on loopback).
Not supported yet: a token from a file or a secret source and the caller’s own token, OAuth to the upstream, and AWS SigV4.
Limits
listentakes an address other than loopback only withcaller_token. See Caller token.- No masking yet. Mask rules inherited from the top level are accepted, and
-validatenotes that they do not apply. Amaskblock on the lane is refused. - No human review yet: an
analyzerblock is refused, and no call can be held for approval. max_connsandnetwork: unixare refused.- Sessions live in memory: run one replica, or pin
Mcp-Session-Idto one.
Try it
Theenvoy-stack/mcp demo runs one session through a lane in front of a test MCP server: a request without the caller token, refused with 403, then initialize, tools/list without the tools an mcp_tool rule hides, an allowed call, a call to a hidden tool and a call over the mcp_gateway budget, then the audit rows under the token’s name.
Next
Policy Rules
Every rule type, and how a lane inherits rules.
Config File
The whole configuration, including the OPA block.