Pawan Bhardwaj (gRPC maintainer, Google) made a focused case: as MCP moves from demos into the enterprise, its default JSON-RPC transport becomes a liability for organisations whose entire backend already speaks gRPC. Bridging the two with transcoding gateways adds latency, cost, and operational surface. The fix is a native gRPC transport for the Model Context Protocol — typed protobuf messages with first-class RPCs (ListResources, CallTool, GetPrompt) instead of JSON-RPC wrappers — delivered without forking the protocol, via the dispatcher pattern: MCP semantics stay put, only the wire layer swaps. Implement five dispatcher methods and all nineteen MCP methods work over gRPC for free. It's early (Python first, SEP-2598 under review) but the architecture is settled.
This is the second Day 2 deep dive. It pairs naturally with Day 1's agentic systems (DD10) and agent observability (DD11) — once agents call tools in production, how those calls travel on the wire stops being a detail.
Why gRPC for MCP?
The talk opened on three cards that frame the whole problem:
- Reliable communication — AI agents are moving from test environments into enterprise operations, which demands reliable, structured communication with external tools and services.
- The MCP standard — the Model Context Protocol is the standard that lets agents talk to tools. It's the lingua franca; that's settled.
- The gRPC gap — many enterprises run their services on gRPC. MCP's default JSON-RPC transport forces a transcoding gateway to sit between the agent and those services, adding complexity and overhead.
A quick MCP primer
For readers who haven't built against it: the Model Context Protocol is a standard way for an AI application (the "host," e.g. an IDE or agent) to connect to "servers" that expose tools (functions the model can call), resources (data the model can read), and prompts (reusable templates). It's the "USB-C for AI tools" — write a tool server once and any MCP-speaking host can use it. Under the hood MCP messages are JSON-RPC 2.0 (a request/response/notification envelope), and the spec defines a handful of transports to carry that JSON-RPC: stdio (the host launches the server as a subprocess and pipes JSON over stdin/stdout — great for local tools), and Streamable HTTP/SSE (for remote servers). gRPC is not one of the blessed transports today, which is precisely the gap this talk fills.
The important architectural point: JSON-RPC and stdio were chosen for ease of getting started — a developer can stand up an MCP server in minutes with no infrastructure. That's the right call for adoption and demos. But those same defaults are exactly what an enterprise that already runs a typed, mTLS-secured, service-meshed gRPC backend does not want, because they sit outside all of that existing machinery.
gRPC as the transport backbone
The performance and platform case for gRPC isn't new, but it's worth restating in the MCP context:
| Property | What it buys MCP |
|---|---|
| Binary encoding (protobuf) | ~10× smaller messages than JSON — lower latency, reduced network cost. |
| HTTP/2 | Client and server stream continuously over a single persistent connection — no per-call setup. |
| Built-in flow control | Native backpressure in the transport itself — a fast tool can't drown a slow client. |
Enterprise security and authorization
This is where gRPC's maturity really tells, and it maps directly onto cloud-native identity:
- Identity & encryption — mutual TLS and SPIFFE identity for secure, verified service-to-service communication. (This is the same SPIFFE/workload-identity story as Day 1's Keycloak federated auth (DD09).)
- Strong authentication — native hooks for JWT and OAuth, i.e. industry-standard access control.
- Granular control — method-level authorization across the entire mesh: you can permit
CallToolbut denyGetPromptper caller.
Operational maturity
Six properties that make gRPC "production-grade" rather than "works in a demo": unified observability (OpenTelemetry for full-stack insight); robust resiliency (deadlines, timeouts, flow control); schema validation (protobuf strict typing rejects malformed input at the boundary); polyglot development (C++, Go, Python, Java, Node, PHP, Rust); metadata support (extensible per-request context); and xDS support (proxyless service-mesh integration). Each is something MCP-over-JSON-RPC would have to reinvent.
How gRPC's streaming maps onto MCP
One reason gRPC fits MCP better than it first appears is its four call types, which line up with MCP's interaction shapes. Unary (one request, one response) covers the common case — CallTool, GetPrompt, ListResources. Server-streaming (one request, a stream of responses) is the natural home for progress notifications and incremental/streamed tool output — exactly the things JSON-RPC-over-stdio handles awkwardly with correlated notification messages. Client-streaming and bidirectional streaming open the door to long-lived, two-way agent↔tool sessions where both sides push messages. Critically, gRPC's built-in flow control (from HTTP/2) gives backpressure on these streams natively, so a fast-producing tool can't overwhelm a slow consumer — something MCP otherwise has to manage by hand. The dispatcher pattern is what lets MCP's semantics ride on top of whichever of these shapes a given method needs.
Mapping the spec — mcp-grpc-transport-proto
The implementation centres on a canonical protobuf definition (github.com/GoogleCloudPlatform/mcp-grpc-transport-proto). Four design commitments:
- Canonical source of truth — one proto used for all language implementations.
- Synchronized evolution — updates driven by the MCP transport workgroup to stay in parity with the latest MCP Specification releases.
- Native gRPC service definition — a first-class
Mcpservice with dedicated RPC methods forListResources,CallTool, andGetPrompt, replacing JSON-RPC 2.0 wrappers. - Semantic parity via direct mapping — MCP primitives map directly to typed protobuf messages (e.g.
Resource,Tool), so the contract is strictly type-safe.
Tool is a protobuf Tool message, validated by the schema. No double-encoding, no stringly-typed payloads.Concretely, the direct mapping turns MCP's JSON-RPC method names into real gRPC RPCs on a first-class service, roughly:
// mcp-grpc-transport-proto (illustrative shape)
service Mcp {
rpc ListResources(ListResourcesRequest) returns (ListResourcesResponse);
rpc CallTool(CallToolRequest) returns (CallToolResponse);
rpc GetPrompt(GetPromptRequest) returns (GetPromptResponse);
// streaming variants carry progress / incremental output
rpc CallToolStream(CallToolRequest) returns (stream CallToolEvent);
}
message Tool {
string name = 1;
string description = 2;
// typed input schema — validated by the wire, not hand-checked
ToolInputSchema input_schema = 3;
}
Compare that to the JSON-RPC reality, where every call is {"jsonrpc":"2.0","method":"tools/call","params":{…},"id":7} — a stringly-typed envelope whose params shape is validated (if at all) by application code after parsing. With the proto, the contract is the .proto file: malformed input is rejected at the transport boundary before your handler ever runs, the message is a compact binary blob instead of verbose JSON, and the generated client/server stubs give you typed objects in every language. This is the difference between "the schema is documentation" and "the schema is enforced by the wire."
curl | jq), and you take a build-step dependency on codegen. For a quick local tool those costs outweigh the benefits, which is why stdio/JSON-RPC stays the default. For a high-volume enterprise mesh the costs are already paid (you have the gRPC toolchain) and the benefits compound across millions of tool calls. The point of pluggable transports is you don't have to pick globally — you pick per deployment.The real design problem — pluggable transports
The most interesting section drew on Anthropic's "Path to V2 for MCP SDKs" (Max Isbey). MCP has three layers, and "pluggable transports" usually means swapping the bottom two together:
Fig 1 — MCP's three layers. gRPC isn't just a transport swap; doing it right means changing the message-format layer too (protobuf, not JSON-RPC) — which is why both bottom layers move together.
The key insight: gRPC with protobuf over JSON-RPC makes no sense — you'd smuggle JSON-RPC as a string and lose protobuf's whole value proposition. So you can't treat the transport in isolation; the message format has to move with it. The solution is the dispatcher pattern: cleanly separate "what MCP means" from "how it's framed and sent," then swap both lower layers as a unit.
The dispatcher pattern
This is the architectural payoff, and it's elegant. The problem with the old design was entanglement: the session object mixed MCP semantics with wire-protocol concerns, so anyone wanting gRPC had to reimplement the whole session.
| Before | After (python-sdk PR #2320) | |
|---|---|---|
| BaseSession | MCP semantics (initialize(), call_tool(), list_tools(), progress tokens, cancellation, validation) plus tangled wire protocol (JSON-RPC wrap, ID correlation, receive loop, stream management) — 19 methods. | MCP semantics only — the same 19 methods, unchanged. |
| Dispatcher | — (didn't exist; wire logic lived in the session) | Extracted out: just 5 methods — send_request, send_notification, send_response, set_handlers, run — with JSON-RPC (default), gRPC, or Protobuf behind them. |
GrpcDispatcher with five methods. The session, the tool definitions, the semantics: untouched. That's the difference between "reimplement the session" (before) and "implement 5 methods" (after).Fig 2 — The dispatcher pattern: one unchanged BaseSession (19 MCP methods) plugs into any dispatcher. Swap JSONRPCDispatcher for GrpcDispatcher at init; everything above stays identical.
MCP gRPC in Python
The first concrete implementation is github.com/GoogleCloudPlatform/mcp-grpc-transport-py. Its properties:
- Status: active development — a work-in-progress pluggable transport for the MCP Python SDK.
- Dispatcher-based architecture — built on the official transport dispatchers, which makes it easy to add MCP endpoints to existing gRPC services.
- Standard evolution — built in lockstep with
mcp-grpc-transport-protoand the official Python libs to keep strict parity with evolving MCP specs. - Unified developer experience — tool definitions stay identical; developers simply swap
JSONRPCDispatcherforGrpcDispatcherduring session initialization.
# the entire migration, conceptually # before session = ClientSession(dispatcher=JSONRPCDispatcher(...)) # after — same tools, same 19 methods, gRPC on the wire session = ClientSession(dispatcher=GrpcDispatcher(...))
That one-line swap is the whole point: the tool author writes MCP, the platform team picks the transport.
Who this is for — and who it isn't
It's worth being clear that native gRPC is not meant to replace JSON-RPC/stdio for everyone — it's a second option for a specific audience. The dispatcher pattern exists precisely so both can coexist without forking the protocol.
| gRPC transport fits when… | Stick with JSON-RPC/stdio when… |
|---|---|
| Your backend already speaks gRPC (typed contracts, mTLS, xDS mesh, OTel). | You're building a local tool launched as a subprocess — stdio is simpler and has zero infra. |
| You need method-level authz, native backpressure, and end-to-end identity. | You want the fastest path from idea to a working MCP server. |
| Tool calls are high-volume or latency-sensitive and JSON overhead matters. | Your consumers are browsers or simple HTTP clients without gRPC tooling. |
| You operate at enterprise scale where one transcoding gateway per tool is real cost. | Interop breadth matters more than per-call efficiency. |
What's next — the roadmap
The proposal is formalised as SEP-2598: Pluggable Transports (under review, modelcontextprotocol PR #2598). The near-term plan:
- Language support — Python first, others to follow.
- Expansion beyond Python — strategic growth of the SDK ecosystem to multi-language support (which the canonical proto already enables).
- Conformance testing — comprehensive tests to verify a gRPC transport behaves identically to JSON-RPC.
- Migration playbook — guidelines for migrating existing gRPC services to expose MCP tools and for changing transports.
FAQ
Why not just keep JSON-RPC and add a gateway?
A transcoding gateway is exactly the overhead the talk is removing. It adds latency, cost, and an extra component to operate, and it stops your gRPC stack's guarantees (mTLS, xDS, OTel, method-level authz) at the boundary. A native gRPC transport carries those guarantees end to end.
Doesn't gRPC-over-MCP just wrap JSON in protobuf?
No — that's the anti-pattern it explicitly avoids. The design uses direct mapping: MCP primitives like Resource and Tool become typed protobuf messages, and there are first-class RPCs (ListResources, CallTool, GetPrompt). Wrapping JSON-RPC in a protobuf string would throw away protobuf's typing and size benefits.
What exactly is the dispatcher pattern?
It separates MCP semantics from wire concerns. BaseSession keeps the 19 MCP methods unchanged; a Dispatcher implements just 5 wire methods (send_request, send_notification, send_response, set_handlers, run). Implement those 5 for a new transport and all 19 MCP methods work over it for free.
Can I use this in production today?
Not yet as a stable release. The Python transport is in active development, SEP-2598 is under review, and conformance tests are being built. But you can follow the canonical proto and the Python repo, and the migration is designed to be a one-line dispatcher swap when it lands.
Does adopting gRPC mean abandoning stdio/JSON-RPC tools?
No. The whole design keeps MCP a single protocol with pluggable transports, so JSON-RPC/stdio remains the default and best choice for local subprocess tools and quick starts. gRPC is an additional transport for enterprises whose backends already run on it. A tool author writes MCP once; the deploying org picks the wire — both coexist without forking.
How does MCP's progress/streaming work over gRPC?
It maps onto gRPC's streaming call types: unary for plain request/response (most tool calls), and server-streaming for progress notifications and incremental output. gRPC's HTTP/2 flow control provides native backpressure on those streams, so a fast tool can't overwhelm a slow client — which MCP otherwise has to coordinate manually with correlated notification messages.
Why is keeping the proto canonical so important?
Because it's what keeps every language implementation in agreement and prevents drift. One mcp-grpc-transport-proto is the single source of truth; the per-language libraries (Python first) are generated/built against it and evolve in lockstep with the MCP spec via the transport workgroup. It's the same discipline that let gRPC stay one coherent framework across C++, Go, Python, Java, Node, and Rust.
Takeaways
- The gRPC gap is real for enterprises. MCP's default JSON-RPC forces transcoding gateways in front of gRPC backends — latency, cost, lost guarantees.
- gRPC brings the whole package — protobuf (~10× smaller), HTTP/2, flow control, mTLS/SPIFFE, JWT/OAuth, method-level authz, OTel, xDS, polyglot.
- Do it natively, not wrapped. Direct mapping of MCP primitives to typed protobuf via a canonical
mcp-grpc-transport-proto— first-class RPCs, no JSON-in-string smuggling. - The dispatcher pattern is the unlock. Separate MCP semantics (19 methods, unchanged) from a 5-method dispatcher; implement 5 → every MCP method works over the new transport.
- Pluggable, not forked. Swap
JSONRPCDispatcherforGrpcDispatcherat init; SEP-2598 is standardising it. Early, but the architecture is settled.
Next in Day 2 — Inference in Progress… Please Monitor Responsibly, on what observability actually means for LLM serving.
References
- KubeCon Mumbai 2026 — Day 2 index · the rest of Day 2
- mcp-grpc-transport-proto · canonical protobuf definition
- mcp-grpc-transport-py · the Python implementation
- SEP-2598 — Pluggable Transports · the spec proposal
- grpc.io · the transport itself