Problem Statement
OpenShell currently distinguishes generic JSON-RPC from MCP, but an MCP endpoint does not declare which MCP protocol revisions it permits. The parser and policy evaluator therefore cannot derive wire behavior from the revision negotiated for a request or session.
That gap matters because the MCP wire contract changed between stable revisions. MCP 2025-03-26 requires implementations to receive JSON-RPC batches, while MCP 2025-06-18 removed batching. Later revisions also add or change policy-relevant HTTP headers, transport operations, message shapes, and lifecycle rules. Applying one version-independent rule either rejects valid older traffic or accepts forms that are invalid for a newer negotiated revision.
Issue #2082 and PR #2083 expose the immediate ambiguity by proposing unconditional batch rejection for every MCP endpoint.
OpenShell needs an explicit, fail-closed answer to two questions before it evaluates an MCP message:
- Which MCP revisions may traverse this endpoint?
- Which one OpenShell is enforcing for this request or session?
It preserves generic protocol: json-rpc behavior as a separate policy surface.
Proposed Design
Add an explicit MCP revision allowlist
Require each protocol: mcp endpoint to declare a nonempty list of exact stable MCP revisions:
protocol: mcp
mcp:
versions:
- "2025-03-26"
- "2025-11-25"
mcp.versions is an allowlist, not a request to union the syntax of every listed revision. OpenShell still resolves exactly one revision for each initialization exchange and each established session.
Keep an OpenShell-owned profile registry
Each profile records only the protocol facts that OpenShell must understand to make a safe forwarding or policy decision, including:
- Allowed JSON-RPC top-level shapes and batch member classes.
- Required message role and direction constraints.
- Streamable HTTP operations, request and response media types, and policy-relevant headers.
- Initialization and subsequent-request protocol-version rules.
- Stable schema validators for policy-visible MCP methods and parameters.
- Fixed resource limits for profile parsing, including a bounded member count when a revision permits batches.
Derive JSON-RPC batch behavior from the selected profile
The initial built-in profiles should encode these stable contracts:
2025-03-26 accepts a single JSON-RPC message, a nonempty batch of requests and notifications, or a nonempty batch of responses. It rejects a batch that mixes call-side and response-side roles. Every member must pass strict envelope validation, direction checks, and policy or correlation checks before any batch bytes are forwarded. One invalid or denied member denies the complete batch.
2025-06-18 accepts exactly one JSON-RPC request, notification, or response per Streamable HTTP POST and rejects every top-level array.
2025-11-25 accepts exactly one JSON-RPC request, notification, or response per Streamable HTTP POST and rejects every top-level array.
Generic protocol: json-rpc endpoints keep their own bounded JSON-RPC 2.0 batch contract and do not use mcp.versions.
Preserve policy data across every configuration path
Carry mcp.versions without loss through policy YAML, generated protobuf types, provider profiles, policy updates, stored policy round trips, endpoint compilation, proposal comparison, and supervisor configuration. Endpoint equality and policy-generation logic must treat the ordered representation canonically so equivalent allowlists do not create false changes.
Alternatives Considered
- Reject MCP batches for every endpoint. This matches MCP
2025-06-18 and 2025-11-25, but it breaks the required receive behavior of MCP 2025-03-26 and hides the missing version model.
- Accept MCP batches for every endpoint after checking each member. Atomic member validation is correct for a batch-capable profile, but unconditional acceptance violates later revisions and leaves OpenShell unable to enforce other revision-specific wire rules.
- Add
mcp.disable_batch. A standalone boolean can contradict the negotiated revision, creates two sources of truth, and does not scale to protocol-version headers, media types, transport operations, or future profile differences.
- Infer behavior from the installed MCP dependency. That makes policy semantics change when a library is upgraded and prevents operators from auditing the wire contract from policy alone.
- Configure exactly one revision per endpoint. This is simpler, but MCP initialization permits a server to select another supported revision and a shared endpoint may intentionally serve clients using different allowed stable revisions. The allowlist permits that compatibility while each session still binds to one exact profile.
Agent Investigation
- Reviewed upstream
main at f7aa3aa3c76b6483c6572cd02a4121cd7f9ded36. McpOptions already reserves the MCP stanza for OpenShell-owned version profiles, but it currently exposes only strict_tool_names and allow_all_known_mcp_methods.
- Confirmed that the current policy definition has no MCP revision selector and the JSON-RPC inspection options carry no negotiated revision.
- Confirmed that current parser failures are stored as
Option<String> and converted into another string at the proxy boundary, which supports the review request for structural rejection types.
- Confirmed that current upstream parses arrays in MCP and generic JSON-RPC modes through the same batch path and evaluates every call before success in the relay.
- Verified the protocol change in the official MCP sources: the
2025-03-26 base protocol requires batch reception, while the 2025-06-18 changelog removes JSON-RPC batching. The later lifecycle and Streamable HTTP contracts make the negotiated revision policy-relevant.
- Searched existing public issues for
mcp.versions, MCP version profiles, and MCP-Protocol-Version. No separate issue covers this design. #1793, #1058, and #2144 are related but not duplicates.
Checklist
Problem Statement
OpenShell currently distinguishes generic JSON-RPC from MCP, but an MCP endpoint does not declare which MCP protocol revisions it permits. The parser and policy evaluator therefore cannot derive wire behavior from the revision negotiated for a request or session.
That gap matters because the MCP wire contract changed between stable revisions. MCP
2025-03-26requires implementations to receive JSON-RPC batches, while MCP2025-06-18removed batching. Later revisions also add or change policy-relevant HTTP headers, transport operations, message shapes, and lifecycle rules. Applying one version-independent rule either rejects valid older traffic or accepts forms that are invalid for a newer negotiated revision.Issue #2082 and PR #2083 expose the immediate ambiguity by proposing unconditional batch rejection for every MCP endpoint.
OpenShell needs an explicit, fail-closed answer to two questions before it evaluates an MCP message:
It preserves generic
protocol: json-rpcbehavior as a separate policy surface.Proposed Design
Add an explicit MCP revision allowlist
Require each
protocol: mcpendpoint to declare a nonempty list of exact stable MCP revisions:mcp.versionsis an allowlist, not a request to union the syntax of every listed revision. OpenShell still resolves exactly one revision for each initialization exchange and each established session.Keep an OpenShell-owned profile registry
Each profile records only the protocol facts that OpenShell must understand to make a safe forwarding or policy decision, including:
Derive JSON-RPC batch behavior from the selected profile
The initial built-in profiles should encode these stable contracts:
2025-03-26accepts a single JSON-RPC message, a nonempty batch of requests and notifications, or a nonempty batch of responses. It rejects a batch that mixes call-side and response-side roles. Every member must pass strict envelope validation, direction checks, and policy or correlation checks before any batch bytes are forwarded. One invalid or denied member denies the complete batch.2025-06-18accepts exactly one JSON-RPC request, notification, or response per Streamable HTTP POST and rejects every top-level array.2025-11-25accepts exactly one JSON-RPC request, notification, or response per Streamable HTTP POST and rejects every top-level array.Generic
protocol: json-rpcendpoints keep their own bounded JSON-RPC 2.0 batch contract and do not usemcp.versions.Preserve policy data across every configuration path
Carry
mcp.versionswithout loss through policy YAML, generated protobuf types, provider profiles, policy updates, stored policy round trips, endpoint compilation, proposal comparison, and supervisor configuration. Endpoint equality and policy-generation logic must treat the ordered representation canonically so equivalent allowlists do not create false changes.Alternatives Considered
2025-06-18and2025-11-25, but it breaks the required receive behavior of MCP2025-03-26and hides the missing version model.mcp.disable_batch. A standalone boolean can contradict the negotiated revision, creates two sources of truth, and does not scale to protocol-version headers, media types, transport operations, or future profile differences.Agent Investigation
mainatf7aa3aa3c76b6483c6572cd02a4121cd7f9ded36.McpOptionsalready reserves the MCP stanza for OpenShell-owned version profiles, but it currently exposes onlystrict_tool_namesandallow_all_known_mcp_methods.Option<String>and converted into another string at the proxy boundary, which supports the review request for structural rejection types.2025-03-26base protocol requires batch reception, while the2025-06-18changelog removes JSON-RPC batching. The later lifecycle and Streamable HTTP contracts make the negotiated revision policy-relevant.mcp.versions, MCP version profiles, andMCP-Protocol-Version. No separate issue covers this design. #1793, #1058, and #2144 are related but not duplicates.Checklist