Skip to content

Support method-level governance for MCP tool calls (JSON-RPC) in sandbox policy #1793

Description

@prakashmirji

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:

  1. Detect JSON-RPC traffic on governed connections (Content-Type or protocol detection)
  2. Parse the JSON-RPC method and params.name fields
  3. Apply allow/deny rules before forwarding to the MCP server
  4. 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

  1. rpelevin commented on Jun 9, 2026

    @rpelevin

    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:

    • sandbox profile id;
    • MCP server identity;
    • JSON-RPC request id;
    • method, usually tools/call;
    • resolved tool name;
    • canonical args digest;
    • read/write/destructive classification;
    • decision: allow, deny, require approval;
    • decision id recorded in the sandbox log;
    • expiry or policy version.

    The proxy can still enforce at the JSON-RPC layer, but the policy key should be server + tool + args digest + policy version, not just host + port + method.

    Acceptance tests I would want:

    • the same MCP connection allows search_web but denies send_email;
    • tools/list is allowed without implying that every listed tool is callable;
    • changed args no longer match a prior approval;
    • renamed or newly discovered tools fail closed until policy is refreshed;
    • denied calls never reach the upstream MCP server;
    • the sandbox log records the resolved tool name and decision id without storing secrets.

    That keeps the network sandbox useful while avoiding an all-or-nothing MCP server grant.

  2. added
    area:policyPolicy engine and policy lifecycle work
    and removed
    state:triage-neededOpened without agent diagnostics and needs triage
    on Jun 9, 2026
  3. johntmyers commented on Jun 9, 2026

    @johntmyers
    Collaborator

    related: #1531

    For a first iteration, would JSON-RPC over HTTP be a good injection point?

  4. rpelevin commented on Jun 9, 2026

    @rpelevin

    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:

    • transport adapter: intercept JSON-RPC over HTTP before forwarding upstream;
    • MCP normalizer: parse method, request id, and params.name / params.arguments for tools/call;
    • policy evaluator: decide against the normalized MCP call envelope;
    • dispatcher: forward only if the decision is allow.

    For v1, I would keep the enforcement narrow:

    1. allow tools/list and other metadata methods by default, but do not treat listing as authority to call every tool.
    2. for tools/call, resolve the server identity, tool name, canonical args digest, policy version, and sandbox profile.
    3. if decision is deny, return a structured JSON-RPC error or MCP tool error without forwarding upstream.
    4. if decision is require_approval, return an approval-required response without forwarding upstream.
    5. if decision is allow, forward the original request unchanged and log the decision id.

    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/list succeeds but does not grant execution authority.
    • tools/call:search_web forwards while tools/call:send_email is denied on the same server connection.
    • denied and approval-required calls never reach the upstream MCP server.
    • changed args produce a different digest and cannot reuse a prior approval.
    • unknown or newly discovered tools fail closed until policy is refreshed.
    • the sandbox log records transport, server id, tool name, decision id, and args digest without storing raw secrets.

    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."

  5. johntmyers commented on Jun 9, 2026

    @johntmyers
    Collaborator

    Something like this:

    version: 1
    
    network_policies:
      github_mcp_jsonrpc:
        name: github_mcp_jsonrpc
        endpoints:
          - host: mcp-github.internal.example.com
            port: 443
            path: /mcp
            protocol: json-rpc
            enforcement: enforce
    
            json_rpc:
              max_body_bytes: 65536
              on_parse_error: deny
              batch_policy: deny_if_any_denied
    
            rules:
              - allow:
                  rpc_method: initialize
    
              - allow:
                  rpc_method: tools/list
    
              - allow:
                  rpc_method: tools/call
                  params:
                    name:
                      any:
                        - get_*
                        - list_*
                        - search_*
    
              - allow:
                  rpc_method: tools/call
                  params:
                    name: create_issue
                    arguments.repository: NVIDIA/OpenShell
    
            deny_rules:
              - rpc_method: tools/call
                params:
                  name:
                    any:
                      - delete_*
                      - push_*
                      - create_pull_request
                      - execute_code
    
        binaries:
          - path: /usr/local/bin/claude
          - path: /usr/bin/node

    For a first iteration, some thoughts:

    • no supervisor *.local listener
    • focus on a json-rpc protocol 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 changes
    • logs should record endpoint, rpc method, args digest, policy version
  6. rpelevin commented on Jun 9, 2026

    @rpelevin

    Yes, this is the shape I would expect for a first iteration. I especially like keeping the first policy surface as protocol: json-rpc rather than making MCP itself the first-class policy object too early.

    A few boundaries I would pin in the YAML / tests:

    • protocol: json-rpc is the parser/enforcement profile; the decision record should still capture endpoint, RPC method, parsed target fields, args digest, policy version, and decision outcome.
    • for tools/call, params.name matching 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_denied is 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: deny should return a structured JSON-RPC error and log parser_failure, not fall back to the network allow rule.
    • rule precedence should be deterministic. I would either make explicit deny win or define most-specific-rule wins, but avoid order-dependent surprises.
    • logs should include endpoint, RPC method, resolved tool name when available, args digest, policy version, decision id/outcome, and avoid raw args by default.

    The useful v1 test set seems small:

    1. initialize and tools/list pass.
    2. allowed tools/call forwards unchanged.
    3. denied tools/call never reaches upstream.
    4. malformed JSON-RPC fails closed with a structured error.
    5. mixed batch with one denied call forwards none, or at least follows the documented batch policy exactly.
    6. changed args produce a different digest in the log.
    7. allow/deny conflict resolves deterministically.

    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.

  7. johntmyers commented on Jun 9, 2026

    @johntmyers
    Collaborator

    rule precedence should be deterministic. I would either make explicit deny win or define most-specific-rule wins, but avoid order-dependent surprises.

    This behavior already exists in our L7 parsers: explicit deny, explicit allow, implicit deny. So we would keep that. We already have rest and graphql parsers, this is just a new parser type, not changing rule evaluation behavior.

  8. rpelevin commented on Jun 9, 2026

    @rpelevin

    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:

    1. JSON-RPC parser returns the same decision enum shape as REST/GraphQL: explicit deny, explicit allow, implicit deny.
    2. explicit deny for tools/call wins over an allow wildcard.
    3. no matching rule becomes implicit deny, with a logged decision id/outcome.
    4. parse failure maps to deny plus parser_failure, without falling through to network allow.
    5. allowed JSON-RPC call forwards the original request unchanged after decision.
    6. denied or implicit-deny calls never reach upstream.
    7. log shape stays consistent across parser types: endpoint, parser type, RPC method, resolved target fields when available, args digest, policy version, decision id/outcome.

    Then JSON-RPC is just another L7 parser contract, and later MCP-specific support can enrich the normalized attributes without forking policy semantics.

  9. ddurst-nvidia commented on Jun 9, 2026

    @ddurst-nvidia
    Contributor

    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 Task progress would be expressed.

    With the MCP case, mcp_server->agent is used by MCP (elicitation, roots and sampling requests - 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 params approach similar to method (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.

  10. rpelevin commented on Jun 9, 2026

    @rpelevin

    Yes, I think direction should 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:

    • direction: agent_to_server, server_to_agent, response_to_agent, response_to_server;
    • frame kind: request, response_result, response_error, notification, progress;
    • transport: http, websocket, stdio later;
    • endpoint / server identity;
    • JSON-RPC id, when present;
    • method, when this is a request or notification;
    • params selector and params digest, when params exist;
    • response correlation id, when this is a result/error;
    • progress correlation id or task id, when this is progress;
    • MCP role hint when known: client_request, server_request, tool_result, sampling_request, roots_request, elicitation_request, progress_update;
    • policy version;
    • decision id and outcome.

    Then the policy can distinguish:

    1. agent_to_server requests such as tools/call;
    2. server_to_agent requests such as sampling, roots, and elicitation;
    3. responses/results that should be correlated to an already allowed in-flight request, not treated as new authority;
    4. progress/notification frames that should usually be correlated and logged, not independently allowed to expand authority.

    That avoids the trap you named: a tools/call policy should not accidentally prevent the corresponding result or progress stream from returning. But a server-initiated sampling or elicitation request 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:

    • if progress belongs to an allowed in-flight request, allow/log it under that request id or task id;
    • if progress has no matching in-flight request, treat it as orphaned and deny or log at high severity;
    • if progress carries payload fields beyond status text / counters / task metadata, run the params selector and digest path before allowing it.

    For params, I agree it should be parallel to method matching:

    • method match chooses the JSON-RPC operation class;
    • params selectors choose the resource/tool/action surface inside that method;
    • params digest gives the audit/correlation boundary when the full payload is too large or sensitive to log.

    Acceptance tests I would add:

    1. allowed agent_to_server tools/call permits the matching response with the same JSON-RPC id.
    2. matching progress for an allowed in-flight request is allowed and logged without creating a fresh authority decision.
    3. a response/result/progress frame with no matching allowed in-flight request is denied or logged as orphaned.
    4. server_to_agent sampling can be denied or require approval without affecting ordinary tool-call results.
    5. roots / elicitation requests are policy-addressable by method and params selector.
    6. params selector mismatch fails closed even when the method is allowed.
    7. logs include direction, frame kind, parser type, method, params digest, policy version, decision id/outcome, and response/progress correlation id where applicable.
    8. MCP conformance fixtures pass through the parser, and OpenShell-specific policy tests assert the direction/params/progress behavior above.

    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.

  11. ddurst-nvidia commented on Jun 9, 2026

    @ddurst-nvidia
    Contributor

    Digging in a little deeper. For 2025-11-25, and ignoring stdio, 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 in 2025-11-25 either.

    I think the outcome of this, is something approaching:

    • "Streamable HTTP" support
      • OpenShell's L7 support covers a lot of the coarse HTTP surface (host/port/path/method/query, TLS).
      • MCP-Specific headers will need to be supported, or more flexible HTTP header rules/tools need to be exposed.
      • The awkward 2025-11-25 pieces 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 the 2026-07-28 draft (as it exists today).
    • JSON-RPC 2.0 Codec
      • This really should stand on its own, even though it's sandwiched between two MCP layers.
      • Generic things like batch_policy would be allowed here.
    • MCP Semantics
      • Things like batch_policy would be bounced here, by MCP contract.

    With the 2026-07-28 draft "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.

  12. rpelevin commented on Jun 9, 2026

    @rpelevin

    This split feels right to me.

    I would keep the layers separate:

    1. Streamable HTTP transport policy: host, port, path, HTTP method, query, TLS, selected headers, MCP protocol version, and stream/session shape.
    2. JSON-RPC 2.0 codec policy: frame decode, id, method, params, result/error, direction, batch behavior, parse errors, payload size, and correlation.
    3. MCP semantic policy: which JSON-RPC methods are legal under the MCP contract, which direction they can appear in, and which params surfaces carry authority risk.

    That gives OpenShell a cleaner answer to the 2025-11-25 versus 2026-07-28 problem:

    • if v1 targets the 2026-07-28 POST-shaped Streamable HTTP path, JSON-RPC can stay a generic codec behind a simpler HTTP envelope;
    • if v1 also supports the 2025-11-25 SSE shape, I would not bury GET-opened SSE stream handling inside the JSON-RPC codec. I would model it as transport/session policy first, then feed decoded server-to-client JSON-RPC frames into the same codec with direction set 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:

    • the JSON-RPC codec can expose generic batch behavior and fail-closed parser rules;
    • MCP semantics can reject or constrain batch use where the MCP contract does not allow it;
    • OpenShell policy can still log both the codec-level reason and the MCP-level reason.

    A first implementation boundary I would like:

    • support Streamable HTTP POST plus JSON-RPC 2.0 request, response, error, and notification frames;
    • normalize mcp_protocol_version, transport, direction, http_method, path, selected headers, rpc_method, id, params selector, params digest, parser version, and policy version;
    • fail closed on parse error, unsupported transport shape, unsupported method/direction pairing, and unsupported batch shape;
    • treat 2025-11-25 SSE as either explicitly unsupported in v1 or as a separate transport/session feature, not as a hidden side effect of the JSON-RPC parser.

    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.

  13. ddurst-nvidia commented on Jun 10, 2026

    @ddurst-nvidia
    Contributor

    batch_policy is 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.

    treat 2025-11-25 SSE as either explicitly unsupported in v1

    I think that's an extremely unlikely scenario.

  14. rpelevin commented on Jun 10, 2026

    @rpelevin

    Agreed on both points.

    I would not make batch_policy carry 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 make batch_policy normative.

    And if 2025-11-25 SSE 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:

    1. Streamable HTTP session policy handles POST request traffic plus GET-opened SSE streams, MCP protocol version, session identity, selected headers, and stream lifecycle.
    2. JSON-RPC codec policy decodes frames in either direction and emits method, id, params selector, params digest, frame kind, and parse result.
    3. MCP semantic policy decides whether a decoded frame is legal for that MCP role/direction and whether it creates a new authority boundary.

    That keeps 2025-11-25 support viable without blurring the layers:

    • client-to-server POST tools/call is a request that can require allow/deny/approval before forwarding.
    • server-to-client JSON-RPC over SSE is decoded by the same codec, but with explicit direction = server_to_client.
    • ordinary results/progress should be correlated to an already allowed in-flight request or session.
    • server-initiated sampling, roots, or elicitation remains policy-addressable because those are new authority boundaries, not mere responses.

    Tests I would pin:

    1. 2025-11-25 POST request traffic and GET-opened SSE stream traffic are classified as different Streamable HTTP session shapes before JSON-RPC decoding.
    2. server-to-client SSE JSON-RPC frames never inherit client-to-server tools/call authority by default.
    3. a tools/call response/progress frame is allowed only when it correlates to an allowed in-flight request/session.
    4. orphaned response/progress frames fail closed or log high-severity according to policy.
    5. server-to-client sampling / roots / elicitation can be denied or approval-gated independently of tool-call results.
    6. selected MCP headers and protocol version are logged and policy-addressable without hardcoding MCP-only header logic into the JSON-RPC codec.
    7. unsupported or malformed transport/session shapes fail closed with a typed parser/session reason.

    That gives OpenShell a practical v1 path: support the likely 2025-11-25 SSE reality, keep JSON-RPC generic, and make MCP semantics the layer that decides which decoded frames are authority-bearing.

  15. johntmyers commented on Jun 13, 2026

    @johntmyers
    Collaborator

    gator-agent

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:policyPolicy engine and policy lifecycle workarea:supervisorProxy and routing-path workgator:validatedGator validated this issue as ready for work

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions