Repository navigation
Support method-level governance for MCP tool calls (JSON-RPC) in sandbox policy #1793
Description
Problem Statement
Problem
The current sandbox policy supports network rules (allow/deny TCP connections to hosts/ports), filesystem rules, and process rules. However, when an agent communicates with MCP (Model Context Protocol) tool servers, the actual tool invocations are JSON-RPC calls over a single TCP connection. The policy cannot distinguish between different MCP methods on the same connection.
Current Behavior
Policy can only allow/deny at the network level:
network:
- host: "10.200.0.1"
port: 8001
access: full # All-or-nothing: either ALL methods are allowed, or NONE
Once a connection is allowed, the agent can call any method on that MCP server — there's no way to restrict which tools/methods are permitted.
Proposed Design
Desired Behavior
Extend the policy language to support method-level filtering for JSON-RPC/MCP traffic:
network:
- host: "10.200.0.1"
port: 8001
access: full
methods:
allow:
- "tools/call:search_web" # Allow Tavily search
- "tools/call:get_weather" # Allow weather lookup
deny:
- "tools/call:send_email" # Block email sending
- "tools/call:execute_code" # Block code execution
The supervisor would need to:
- Detect JSON-RPC traffic on governed connections (Content-Type or protocol detection)
- Parse the JSON-RPC method and params.name fields
- Apply allow/deny rules before forwarding to the MCP server
- Log/audit which methods were called (for governance compliance)
Alternatives Considered
Use Case
Enterprise governance requires fine-grained control over what an AI agent can do — not just which services it can connect to, but which specific operations it can perform. This is critical for:
- Compliance (agent can read but not write)
- Safety (agent can search but not send emails)
- Least-privilege (agent only gets the specific tools it needs)
- Audit trail (which methods were invoked, by which user, when)
Workaround
We built an external "MCP Governance Gateway" that sits between the agent and the MCP server, intercepts JSON-RPC calls, and enforces method-level policy. This works but adds latency, deployment complexity, and doesn't benefit from the supervisor's built-in audit/policy infrastructure.
Agent Investigation
No response
Checklist
- I've reviewed existing issues and the architecture docs
- This is a design proposal, not a "please build this" request
Activity
rpelevin commentedon Jun 9, 2026
Method-level filtering is the right direction here. The important boundary I would make explicit is that raw TCP access and raw JSON-RPC method names are still too coarse unless the sandbox also understands the resolved MCP tool call.
For MCP traffic, I would evaluate policy against a normalized call envelope before dispatch:
tools/call;The proxy can still enforce at the JSON-RPC layer, but the policy key should be
server + tool + args digest + policy version, not justhost + port + method.Acceptance tests I would want:
search_webbut deniessend_email;tools/listis allowed without implying that every listed tool is callable;That keeps the network sandbox useful while avoiding an all-or-nothing MCP server grant.
johntmyers commentedon Jun 9, 2026
related: #1531
For a first iteration, would JSON-RPC over HTTP be a good injection point?
rpelevin commentedon Jun 9, 2026
Yes, JSON-RPC over HTTP seems like a good first injection point for v1, as long as the policy core is not designed as "HTTP-specific."
The clean split I would use:
params.name/params.argumentsfortools/call;For v1, I would keep the enforcement narrow:
tools/listand other metadata methods by default, but do not treat listing as authority to call every tool.tools/call, resolve the server identity, tool name, canonical args digest, policy version, and sandbox profile.The main caution: HTTP should be the first adapter, not the policy boundary. Later stdio, websocket, or in-process MCP paths should feed the same normalized envelope into the same evaluator.
Tests I would add:
tools/listsucceeds but does not grant execution authority.tools/call:search_webforwards whiletools/call:send_emailis denied on the same server connection.So: yes for JSON-RPC over HTTP as the first interception layer, but I would make the durable abstraction "normalized MCP tool-call envelope before dispatch."
johntmyers commentedon Jun 9, 2026
Something like this:
For a first iteration, some thoughts:
*.locallistenerjson-rpcprotocol vs mcp as first class. still a lot of changes coming with MCP. based on the recent community call we agreed that we will wait for first-class mcp support based on pending changesrpelevin commentedon Jun 9, 2026
Yes, this is the shape I would expect for a first iteration. I especially like keeping the first policy surface as
protocol: json-rpcrather than making MCP itself the first-class policy object too early.A few boundaries I would pin in the YAML / tests:
protocol: json-rpcis the parser/enforcement profile; the decision record should still capture endpoint, RPC method, parsed target fields, args digest, policy version, and decision outcome.tools/call,params.namematching is a good v1 start, but compute the args digest before decision/logging even if v1 only matches selected argument fields.batch_policy: deny_if_any_deniedis a good default; test that no allowed item from a mixed batch is forwarded before every item has been parsed and decided.on_parse_error: denyshould return a structured JSON-RPC error and logparser_failure, not fall back to the network allow rule.The useful v1 test set seems small:
initializeandtools/listpass.tools/callforwards unchanged.tools/callnever reaches upstream.That keeps the first iteration independent of MCP churn while preserving the later path: first parse JSON-RPC reliably, then let MCP-specific adapters feed richer normalized envelopes into the same evaluator.
johntmyers commentedon Jun 9, 2026
This behavior already exists in our L7 parsers: explicit deny, explicit allow, implicit deny. So we would keep that. We already have
restandgraphqlparsers, this is just a new parser type, not changing rule evaluation behavior.rpelevin commentedon Jun 9, 2026
That makes sense. If REST, GraphQL, and JSON-RPC all share the same evaluator semantics, then the v1 JSON-RPC work can stay nicely bounded: parser in, normalized attributes out, existing rule evaluation unchanged.
I would probably make the acceptance tests parser-contract focused:
tools/callwins over an allow wildcard.parser_failure, without falling through to network allow.Then JSON-RPC is just another L7 parser contract, and later MCP-specific support can enrich the normalized attributes without forking policy semantics.
ddurst-nvidia commentedon Jun 9, 2026
I'm reacting to this issue so far as a whole, but I generally like the "JSON-RPC is just another L7 parser contract" approach @rpelevin, with not much in the way of MCP associated with it. Maybe there's syntactic sugar for MCP at some point, and that could be separate.
One aspect of this shape I'm not seeing, is the bi-directional nature of a JSON-RPC 2.0 connection. I don't know if the lack of "direction" in the policy shapes above (agent->mcp_server vs mcp_server->agent), is intentional or not, or meant to apply in both directions. As I see it above, tool calls would never be able to issue a
result. I don't know which direction of the stream, the rules apply, or how a Taskprogresswould be expressed.With the MCP case,
mcp_server->agentis used by MCP (elicitation,rootsandsamplingrequests - all of which are reasonable to apply policy upon, since they allow the MCP server, to gather information from the agent, and potentially forward it to inference).I would like to see a
paramsapproach similar tomethod(unfortunately with more complexity), if "JSON-RPC is just another L7 parser contract." If that spawns to a different issue, I think that's fine as well.I would ultimately use the mcp conformance tests as part of the cycle here, since that's likely the 99% case for JSON-RPC, within OpenShell.
rpelevin commentedon Jun 9, 2026
Yes, I think
directionshould be explicit rather than implied.I would not make the policy apply "both ways" silently, because requests, responses, and progress/notification frames have different safety properties.
A useful normalized JSON-RPC parser record could include:
Then the policy can distinguish:
agent_to_serverrequests such astools/call;server_to_agentrequests such as sampling, roots, and elicitation;That avoids the trap you named: a
tools/callpolicy should not accidentally prevent the correspondingresultor progress stream from returning. But a server-initiatedsamplingorelicitationrequest is a new authority boundary, because the MCP server is asking the agent/client side to disclose or produce something.For progress, I would make the default rule correlation-based:
For params, I agree it should be parallel to method matching:
Acceptance tests I would add:
agent_to_server tools/callpermits the matching response with the same JSON-RPC id.server_to_agent samplingcan be denied or require approval without affecting ordinary tool-call results.roots/elicitationrequests are policy-addressable by method and params selector.That keeps JSON-RPC as the generic L7 parser contract while still covering the MCP reality: MCP is bidirectional, server-to-agent requests can be policy-relevant, and responses/progress need correlation rather than a blanket allow or deny.
ddurst-nvidia commentedon Jun 9, 2026
Digging in a little deeper. For
2025-11-25, and ignoringstdio, what I hadn't realized before is that Streamable HTTP is an MCP defined transport. So we're not looking at WebSockets here at all (unless added as a Custom Transport). I didn't realize how much of the SSE semantics remained in2025-11-25either.I think the outcome of this, is something approaching:
2025-11-25pieces are GET-opened SSE streams and server-to-client JSON-RPC over SSE. Those may need their own layer if we need to enforce them, but can likely be sidestepped if we only support the2026-07-28draft (as it exists today).batch_policywould be allowed here.batch_policywould be bounced here, by MCP contract.With the
2026-07-28draft "Streamable HTTP" shape changes toward alignment with a Connect-RPC-like HTTP POST model (not literally Connect-RPC, but much closer), but given the date on it, probably something that needs to be considered.rpelevin commentedon Jun 9, 2026
This split feels right to me.
I would keep the layers separate:
That gives OpenShell a cleaner answer to the 2025-11-25 versus 2026-07-28 problem:
directionset explicitly.For headers, I would start with generic HTTP header selectors rather than MCP-only names. Then MCP-specific sugar can be added later if the same header set repeats enough to justify it.
For
batch_policy, I agree with your layer split:A first implementation boundary I would like:
mcp_protocol_version,transport,direction,http_method,path, selected headers,rpc_method,id, params selector, params digest, parser version, and policy version;That keeps "JSON-RPC as another L7 parser" intact while acknowledging that MCP Streamable HTTP has transport-level semantics above it and MCP method rules below it.
ddurst-nvidia commentedon Jun 10, 2026
batch_policyis just an example, not a fully fledged out analysis of JSON-RPC 2.0 vs MCP semantics. AFAIK, there is no enumerated subset to filter against.I think that's an extremely unlikely scenario.
rpelevin commentedon Jun 10, 2026
Agreed on both points.
I would not make
batch_policycarry more meaning than it deserves. It is a JSON-RPC codec/parser concern only if a batch frame is actually present; the MCP layer can still reject, constrain, or ignore batch-shaped traffic according to the MCP contract. So I would not try to invent an MCP method subset just to makebatch_policynormative.And if
2025-11-25SSE support is likely required, I would model that explicitly instead of treating it as an edge case inside the JSON-RPC parser.The split I would use for v1 is:
That keeps
2025-11-25support viable without blurring the layers:tools/callis a request that can require allow/deny/approval before forwarding.direction = server_to_client.sampling,roots, orelicitationremains policy-addressable because those are new authority boundaries, not mere responses.Tests I would pin:
2025-11-25POST request traffic and GET-opened SSE stream traffic are classified as different Streamable HTTP session shapes before JSON-RPC decoding.tools/callauthority by default.tools/callresponse/progress frame is allowed only when it correlates to an allowed in-flight request/session.sampling/roots/elicitationcan be denied or approval-gated independently of tool-call results.That gives OpenShell a practical v1 path: support the likely
2025-11-25SSE reality, keep JSON-RPC generic, and make MCP semantics the layer that decides which decoded frames are authority-bearing.johntmyers commentedon Jun 13, 2026
Validated
This issue is valid for OpenShell because it describes a concrete sandbox policy gap for JSON-RPC/MCP method-level governance, has a clear policy/security use case, and has maintainer discussion bounding the first implementation around a generic JSON-RPC L7 parser.
Recommended next step: continue implementation review in linked PR #1865.