Skip to content

feat(policy): add MCP-aware JSON-RPC L7 governance#1938

Open
ddurst-nvidia wants to merge 7 commits into
NVIDIA:hicks/push-nvuozlywzuwufrom
ddurst-nvidia:dev/ddurst/mcp-tower-types-variant-3
Open

feat(policy): add MCP-aware JSON-RPC L7 governance#1938
ddurst-nvidia wants to merge 7 commits into
NVIDIA:hicks/push-nvuozlywzuwufrom
ddurst-nvidia:dev/ddurst/mcp-tower-types-variant-3

Conversation

@ddurst-nvidia

@ddurst-nvidia ddurst-nvidia commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Builds on #1865 by keeping its shared JSON-RPC-family L7 enforcement foundation and adding an MCP-specific policy surface on top of it.

PR #1865 introduces generic JSON-RPC method/params enforcement. This variant preserves generic JSON-RPC as a first-class supported protocol for non-MCP services, while adding MCP-specific policy spelling, MCP validation via tower-mcp-types, and conformance-oriented e2e behavior.

Related Issue

Addresses #1793.

Builds on #1865.

Changes

Relationship to #1865

PR #1865 adds the shared JSON-RPC-family L7 enforcement layer. This branch builds on that layer with an MCP-specific policy surface and MCP message validation via tower-mcp-types, while preserving generic JSON-RPC as a first-class supported protocol for non-MCP services.

How This Addresses #1793

Issue #1793 asks for method-level governance for MCP tool calls because MCP traffic is carried as JSON-RPC over a single allowed connection, making network-only policy all-or-nothing.

This branch addresses that by allowing policy to distinguish MCP requests inside the connection:

  • mcp_method filters MCP/JSON-RPC method names such as tools/call.
  • tool filters MCP tool calls by params.name.
  • Nested params matchers allow policy to constrain scalar MCP params such as arguments.scope.
  • Allow and deny rules are evaluated before forwarding to the MCP server.
  • MCP traffic remains in OpenShell's existing policy/audit path instead of requiring an external governance gateway.

Key Differences

  • Adds protocol: mcp while preserving protocol: json-rpc.
  • Adds MCP policy fields:
    • mcp.max_body_bytes
    • mcp_method
    • tool
    • nested params maps for MCP readability.
  • Uses tower-mcp-types to validate known MCP request/notification shapes.
  • Keeps generic JSON-RPC parsing intentionally protocol-neutral for non-MCP JSON-RPC servers.
  • Keeps enforcement on the existing OPA path by compiling MCP aliases back to the current JSON-RPC-family rule model.

Rule / OPA Tightening

  • mcp_method normalizes to the existing rpc_method field, so OPA has one method-matching path.
  • tool normalizes to MCP params.name, but only when params.name was not explicitly set.
  • Nested MCP params are flattened before OPA evaluation, preserving the existing scalar matcher behavior.
  • Literal dotted request param keys are rejected to avoid ambiguous collisions with flattened nested paths.
  • JSON-RPC/MCP params matching remains restricted to protocol: json-rpc or protocol: mcp.
  • MCP request parsing validates known MCP method params before policy evaluation.
  • Methodless MCP Streamable HTTP receive streams and client JSON-RPC responses are allowed only as a narrow 2025-11-25 conformance compatibility path after host, path, binary, endpoint protocol, and parse-shape checks. MCP 2026-07-28 (draft) removes the GET stream endpoint and client-sent JSON-RPC responses, so this allowance should be version-gated or removed when OpenShell enforces that transport version.

Type Enforcement Follow-up

This branch does not add typed MCP argument enforcement. The current L7 JSON-RPC-family policy input flattens scalar params into string values before OPA evaluation, so 1 and "1" or true and "true" are not distinguishable by policy today.

Typed argument enforcement would be better handled as a wider L7 policy input change, for example by preserving scalar JSON type metadata alongside the existing flattened matcher keys. That would let a future MCP argument policy express type: integer or type: boolean without weakening the existing JSON-RPC compatibility path.

Conformance Notes

The MCP conformance policy template now uses:

protocol: mcp
mcp:
  max_body_bytes: 131072
rules:
  allow:
    - mcp_method: "*"

Expected-failure carve-outs are limited to one known upstream conformance-client behavior:

client:
  - elicitation-sep1034-client-defaults
server: []

Note

elicitation-sep1034-client-defaults is allowed to fail because the pinned upstream TypeScript conformance client appears to advertise elicitation.applyDefaults, while @modelcontextprotocol/sdk 1.29.0 applies defaults only for elicitation.form.applyDefaults. The failure reproduces with the upstream conformance client directly, without OpenShell in the path. The carve-out should be removed when OPENSHELL_MCP_CONFORMANCE_REF is bumped to an upstream commit where the bundled client advertises the corrected capability shape.

Testing

  • mise run pre-commit passes
  • Unit tests added/updated
  • E2E tests added/updated (if applicable)

Local validation run for this branch includes:

  • mise run e2e:mcp for the MCP conformance suite.
    • The suite exits successfully with one expected upstream-client failure: elicitation-sep1034-client-defaults.
  • cargo test -p openshell-supervisor-network mcp_tool_deny_rule_blocks_tools_call
  • cargo clippy -p openshell-supervisor-network --all-targets -- -D warnings
  • cargo test -p openshell-policy
  • cargo test -p openshell-supervisor-network

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable)

Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
@ddurst-nvidia

Copy link
Copy Markdown
Contributor Author

@krishicks - Mostly for your review.

Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
@ddurst-nvidia ddurst-nvidia force-pushed the dev/ddurst/mcp-tower-types-variant-3 branch from a432508 to 8c9606b Compare June 16, 2026 23:27
@ddurst-nvidia

Copy link
Copy Markdown
Contributor Author

I don't have permissions to add the test:e2e label, but it would be appreciated if someone could add it.

max_body_bytes: 131072
rules:
deny:
- mcp_method: tools/call

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

protocol is already mcp, so why do we need it to be mcp_method here vs just method?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Easy enough to remove, leaving unresolved until I can.

mcp:
max_body_bytes: 131072
rules:
deny:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would be a deviation from a separate deny_rules block that sits at the same level as rules, is there a particular reason for that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Particular reasons are:

But I'll address it more completely in the other comment.

| `rules` | list of rule objects | No | Fine-grained protocol-specific allow rules. Mutually exclusive with `access`. |
| `deny_rules` | list of deny rule objects | No | L7 deny rules that block specific requests even when allowed by `access` or `rules`. Deny rules take precedence over allow rules. |
| `rules` | list or grouped object | No | Fine-grained protocol-specific allow rules. For MCP, prefer grouped `rules.allow` and `rules.deny`. Mutually exclusive with `access`. |
| `deny_rules` | list of deny rule objects | No | L7 deny rules that block specific requests even when allowed by `access` or `rules`. Deny rules take precedence over allow rules. Existing policies can keep this shape; new MCP policies should prefer grouped `rules.deny`. |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to do this I would prefer we do it for all L7 and not just MCP. Which would be out of scope for this PR but having just a single L7 parser introduce this isn't good UX.

@ddurst-nvidia ddurst-nvidia Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My particular reasons, don't outweigh precedent & uniformity across these specific objects.

I agree that it should be addressed separately as a Policy Format breaking change.

I do feel strongly that rules and deny_rules is a pretty awkward semantic sibling pair, but is that something you'd be interested in considering changing? I don't know the history of how it came to be other than conjecture.

  • If there's a track to change it elsewhere, no concerns with changing it to rules_deny.
  • If there's no real want to change it, I'll protest with a small sign, but would still make the change here with the goal of advancing the mission 😄

(Either way, leaving it unresolved until I have an opportunity to change it)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants