📝 Description
A protocol: mcp sidecar lane embeds mcpproxy from the nested module sidecar/mcpgateway (ADR-0034): an MCP client reaches one remote MCP server through the sidecar, mcpproxy filters the tool catalog, and each tools/call is a statement the lane rules judge and the audit trail records, one sidecar session per MCP session.
- Scope: a streamable-http or sse upstream with no credential. Masking (RD-373), the analyzer (RD-372), caller auth (RD-462) and upstream credentials (RD-464, RD-465) come with their tickets; validate refuses what does not run yet.
- A rule reload ends the open MCP sessions (ADR-0034, decision 5); a change to the lane itself is restart-required.
- mcpproxy stays at v0.1.1: clients that negotiate 2025-11-25 work; 2025-06-18 clients wait for the RD-469 release and the bump.
📣 User-facing impact
A sidecar can front a remote MCP server from its config file, with tool allow/deny and one audit session per MCP session. No control plane form yet (RD-466).
🔗 Related Issue
RD-375
🚀 Type of Change
- ✨ New feature (non-breaking change which adds functionality)
📋 Changes Made
sidecar/daemon/mcpgateway.go: the lane contract: themcp_gatewayblock (cap:"mcp_gateway"), validation, the engine registry, the gate bridge, the rule swapper that ends sessions.sidecar/mcpgateway: the engine: mcpproxygateway.Optionsbuilt in code, the hoop check in its pipeline,EndSessions.protocol: mcpinProtocols(),protocolLabels,schema.json;protocol:mcpin the capabilities header only with the engine;listenerConnectionKindmaps it.hoop-inspectandhoop start sidecarlink the module;go.worklists it, somake test-sidecarruns it.sidecar/README.mdmcp section;sidecar/CLAUDE.mdmodule row.
🧪 Testing
Result: an MCP client reaches a remote MCP server through a sidecar lane; a denied tool is absent from its catalog and refused on call, a rule-denied call comes back as a tool error, and the control plane shows one session per MCP session with the denied call and its rule.
What it solves: an operator fronts a remote MCP server with a sidecar from one config file, with tool allow and deny, sidecar rules on each call and one audit session per MCP session in the control plane. The control plane form arrives with RD-466; caller auth with RD-462.
Definition of done:
- Nested module links mcpproxy and registers from
init;hoop-inspectandhoop start sidecarlink it;go.worklists it:TestMCPGatewayValidateNeedsAnEngineAndResolvesTheLane -
protocol: mcpinProtocols()andprotocolLabels;protocol:mcpin the header only with the engine; a build without it refuses the lane at-validatenaming the module:TestTheHeaderCarriesMCPOnlyWithAnEngine - Lane config fields, zero values omitted,
cap:"mcp_gateway",schema.jsonregenerated:TestMCPGatewayListenerValidation,TestListenerSchemaIsCurrent,TestEveryConfigFieldOmitsItsZeroValue - A denied tool is missing from
tools/listand refused on call:TestDeniedToolIsAbsentFromTheCatalogAndRefused - A non-loopback
listenis refused at validate:TestMCPGatewayListenerValidation - One sidecar session per MCP session (session_start, one statement per call, session_end), in
/api/sessionsand the UI:TestMCPLaneSessionRecordsOneStatementPerCall,TestOneSidecarSessionPerMCPSession -
listenerConnectionKindmapsmcp:TestProjectListenersMapsEveryProtocol - Rule and config reload per ADR-0034: a rule swap ends the open MCP sessions, a lane change is restart-required:
TestMCPLaneRuleSwapEndsSessions,TestEndSessionsRefusesTheNextMessageUntilTheClientInitializesAgain - Tests pass with
-race -
sidecar/README.mdmcp section;sidecar/CLAUDE.mdmodule row - The ADR-0034 first example validates: inherited mask rules are accepted with a
-validatenote (RD-373); amaskblock on the lane is refused - mcpproxy release with RD-469, RD-470 and RD-471, and the bump: after those merge. The PR stays a draft until then.
QA found and the branch fixed: listen without a port passed validate (now refused), a plain-http upstream was accepted anywhere (loopback only now), the ADR example was refused for its inherited mask: block (accepted and noted now), the idle_timeout_sec help did not name the mcp default.
Test Configuration:
- Browser(s): Chrome (control plane UI)
- OS: macOS (local), Linux (CI)
Tests performed:
- Unit tests pass:
cd sidecar && go test -race ./daemon/ ./gate/ ./inspect/;cd sidecar/mcpgateway && go test -race ./...;go test ./gateway/services/ - Integration tests pass (CI)
- Manual testing completed:
cd sidecar/cmd && go build -o /tmp/hoop-inspect . && /tmp/hoop-inspect -validate -config lane.yaml, then an MCP client (curl, Claude Code) through a plane-managed sidecar lane: initialize,tools/list, an allowed call, a policy-denied call, a rule-denied call, the session in the control plane, and a rule edit ending the open session
✅ Checklist
- My code follows the style guidelines of this project
- I have performed a self-review of my own code
- I have commented my code, particularly in hard-to-understand areas
- My changes generate no new warnings
- New and existing unit tests pass locally with my changes
- I have checked my code and corrected any misspellings
📄 Additional Notes
- The ADR-0034 example validates without its
mask:block: mask rules on an mcp lane are refused until RD-373, with the message naming it. - Sessions live in memory: one replica, or affinity on
Mcp-Session-Id.
Activity
7✅ Build Completed with Success, Version=2094.0.0-gf0d1684
✅ Build Completed with Success, Version=2094.0.0-g11736e4
Four questions, one request.
1. Runnable example. Add a demo next to deploy/docker-compose/envoy-stack/. One config file plus the curl sequence: initialize, tools/list, an allowed call, a denied call. The README example points at api.githubcopilot.com, which nobody can run without a token.
2. Which rule types apply to an mcp statement? The README says table names a tool. Document the rest in the same paragraph. From ToolCall I read:
guardrails:
rules:
- {name: no-delete, type: table, tables: [delete_customer]} # tool name, lower case
- {name: no-prod, type: deny_words, words: ["prod-db"]} # matches inside the args JSON?
- {name: no-calls, type: operation, operations: [call]} # blocks every tools/call?Say which of these fire, and whether the tools/list statement (OpShow, FromServer) is judged too or only recorded.
3. HITL. As I read it there is none in this PR: the analyzer block is refused (RD-372), and approval_tools from mcpproxy is refused as unknown field. State that in the description. A reader who sees "hoop check" and "session in the control plane" assumes the review flow is there.
4. OPA. live.policy comes from buildPolicy(..., opa, ...), so a lane with opa.url consults it on every tools/call. Nothing in the PR tests or documents that. Add one test and one README line with the input document:
{"protocol": "mcp", "direction": "request", "operation": "call",
"tables": ["get_file_contents"], "statement": "get_file_contents\n{\"path\":\"README.md\"}"}5. Two deny layers. denied_tools and a table rule both refuse a tool, with different behavior:
mcp_gateway.denied_tools | guardrails table rule | |
|---|---|---|
tools/list | tool removed from the catalog | tool visible |
| denied call | JSON-RPC error | tool result, isError: true |
| reload | restart-required | hot, ends open sessions |
| control plane | no form yet (RD-466) | existing rules UI |
Same for allowed_tools: empty means all allowed, denied_tools wins over allow (mcpproxy config.go:104). An operator will write one and expect the other. Did ADR-0034 decide to keep tool allow/deny in mcp_gateway instead of under guardrails? If yes, link the decision in the README paragraph. If not, I would move allowed_tools and denied_tools under guardrails and keep mcp_gateway for what has no rule equivalent: block_sampling, block_elicitation, on_rug_pull, the budgets and overrides.
dont forget to open PR on documentation
This pull request is a draft
Merging is unavailable until it is marked ready for review.
📋 API Changelog
API Changelog unknown vs. unknown
No changes detected