Skip to content

Define version-aware MCP wire profiles for Streamable HTTP and JSON-RPC #2174

Description

@shiju-nv

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

  • I've reviewed existing issues and the architecture docs
  • This is a design proposal, not a "please build this" request

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions