MCP Is the USB-C of AI — Here's What That Actually Means for Your Architecture
A senior engineer's guide to production MCP: JSON-RPC 2.0 transport, OAuth 2.1 auth, stateless horizontal scaling, and where the protocol genuinely breaks.
Join the DZone community and get the full member experience.
Join For FreeThree weeks. That's how long it took my team to wire Claude into our internal ticketing system last year. Not because the API was hard. Because every layer of the stack was speaking a different dialect — custom function schemas on one side, brittle REST wrappers on the other, and a Python shim in the middle that I was too embarrassed to commit without a comment that said: "don't look at this." We shipped it. It worked. For about four days, until the vendor updated their response payload and our parser silently swallowed the change. Tickets started routing to the wrong queue at 2 AM on a Tuesday. I learned about it from Slack, not monitoring.
That experience is why Model Context Protocol (MCP) landed so differently for me than it did for the people writing blog posts about it from a fresh MacBook. This wasn't "interesting new protocol." It was a direct answer to a specific, grinding pain.
Stop Calling It a Framework
The USB-C analogy gets repeated so often it's starting to lose meaning. Let me make it concrete.
USB-C solved a problem the tech industry had been ignoring for a decade: every device spoke a slightly different power/data dialect, and the combinatorial explosion of adapters was genuinely slowing things down. USB-C collapsed that N×M adapter problem into a single connector. One port. Any cable. Any device. You still need to negotiate speeds and capabilities over the wire — but the physical contract is shared, which means you can stop thinking about connectors and start thinking about what you're actually moving.
MCP does exactly that for AI tool integration. Before it, connecting an LLM to a tool meant writing a custom schema for that LLM's function-call format, a custom parsing layer for that tool's response shape, and — if you wanted to switch providers — starting over. Six integrations across three LLM providers meant eighteen combinations to maintain. The N×M problem.
MCP's answer is a shared protocol layer: one JSON-RPC 2.0 contract, negotiated at initialization, that any compliant client can speak to any compliant server. Tools become server capabilities, not one-off function schemas. The LLM doesn't care whether it's talking to a Salesforce connector or a PostgreSQL server — both speak MCP, both expose the same tool-call lifecycle, both fail in predictable ways.
That last part matters more than people give it credit for.
The Protocol Stack, Actually Explained
Most articles stop at "MCP uses JSON-RPC 2.0." That's true, but it's like saying "HTTP uses TCP." Correct. Not sufficient.
Layer 1: JSON-RPC 2.0 Messaging
JSON-RPC 2.0 is a stateless, lightweight remote procedure call protocol. It predates AI by over a decade — Ethereum uses it, Ethereum Classic uses it, VS Code's Language Server Protocol is built on it. Anthropic's team made a smart choice borrowing from LSP specifically, because LSP proved that you could build richly typed, bidirectional tooling protocols on top of a dead-simple message format.
Every MCP message is one of three shapes:
// Request (client → server)
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "search_tickets",
"arguments": { "query": "priority:high assignee:me" }
}
}
// Response (server → client)
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [{ "type": "text", "text": "Found 3 tickets..." }],
"isError": false
}
}
// Notification (no id — fire and forget, no response expected)
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}
The id field is doing important work there. Requests have IDs; notifications don't. The client matches responses to requests by ID — which means you can pipeline multiple concurrent requests without ordering guarantees. That's relevant once you start running parallel tool calls, which is exactly what modern agent orchestrators do.
Layer 2: Transport Options
MCP supports two transports, and picking the wrong one is one of the most common production mistakes I see.

stdio is for developer tooling. Cursor uses it. Claude Desktop uses it for local servers. The server runs as a child process, stdin/stdout are the pipe. Zero network overhead, instant startup, trivially secure. Wrong choice for anything multi-tenant or horizontally scaled.
Streamable HTTP is what you deploy to production. Single HTTPS endpoint, HTTP POST for client-to-server, optional SSE (Server-Sent Events) stream for server-to-client pushes. The March 2025 spec update replaced the earlier dedicated SSE-only transport — importantly, Streamable HTTP added support for stateless operation, which is the feature that makes real horizontal scaling possible. More on that in a minute.
# Client → Server: negotiate capabilities
POST /mcp HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGc...
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": { "tools": {} },
"clientInfo": { "name": "my-agent", "version": "1.4.0" }
}
}
# Server → Client: confirm supported capabilities
HTTP/1.1 200 OK
Content-Type: application/json
Mcp-Session-Id: a3f9-c2d1-8b04 # only in stateful mode
{
"jsonrpc": "2.0", "id": 1,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": { "tools": { "listChanged": true } },
"serverInfo": { "name": "ticketing-mcp", "version": "2.1.0" }
}
}
Notice Mcp-Session-Id. That header only appears in stateful mode. In stateless mode — which you want for any horizontally scaled deployment — there's no session header. Every request is self-contained. Critically, that means load balancers can route requests to any instance without sticky sessions. That's the architectural unlock.
Layer 3: The Three Primitives
MCP servers expose exactly three types of capabilities. This is deliberate. The constraint is the feature.

The distinction between Tools and Resources isn't cosmetic. Tools can have side effects. Resources can't. The November 2025 spec update formalized tool annotations — you now declare whether a tool is read-only, destructive, or idempotent in the schema itself. That annotation is what lets your gateway apply different rate limits and audit policies per tool class without building bespoke middleware.
OAuth 2.1: Why It's Here and What It Costs You
Auth was technically optional in early MCP. The community paid for that decision: trojanized packages, unauthenticated community servers running wide open in local dev environments, and at least one incident report I've seen from an enterprise pilot that I won't name where an MCP server was reachable from a public IP with no credentials required. The November 2025 spec update made OAuth 2.1 the recommended standard for remote servers. In practice, if you're deploying Streamable HTTP in a production environment, treat it as mandatory.

A few things worth knowing before you implement this:
- OAuth 2.1 drops implicit flow entirely. If you have legacy client code that used implicit — and plenty of older enterprise apps do — you're rewriting that before you go live. Plan a sprint.
- PKCE is mandatory for public clients even with authorization code flow. The spec doesn't give you a waiver for this.
- Server discovery at
/.well-known/oauth-authorization-serveris how clients find your token endpoint without hardcoding. Don't skip implementing this. Dynamic client registration makes onboarding new agent clients 10x less painful. - Tokens are per-user context, not per-MCP-server. Your gateway needs to thread the right token to the right downstream server. That routing logic is where I've seen the most production bugs — specifically, token scope mismatches that silently returned empty results instead of 403s.
Production Architecture: The Full Stack
Here's what the actual architecture looks like once you move past single-developer demos.

The Stateless Scaling Model
This is the piece most tutorials gloss over, and it's the piece that will bite you at 3 AM.
The original MCP spec used session IDs. Every client got pinned to a server instance via Mcp-Session-Id. That's great for local development. For Kubernetes? It means sticky sessions, broken pod rollouts, and a load balancer that has to track which client is where. The November 2025 spec update added stateless operation as a first-class option — no session IDs; every request carries all context it needs.
Stateless vs. Stateful: The Decision Tree
Choose stateless(no session ID) if your tools are side-effect-free queries or short-lived mutations. Your load balancer routes freely, Kubernetes rolling deployments work cleanly, horizontal scale is trivial.
Choose stateful(session pinned) only when you genuinely need server-side context across calls — browser automation, long-running file operations, or multi-step transactions where partial state lives on the server. For stateful deployments, you need Redis-backed session storage and sticky session config at the ingress level.
from fastmcp import FastMCP
from fastmcp.server.auth import BearerAuthProvider
import httpx, os
# All state lives in downstream systems. Zero server-side session state.
mcp = FastMCP(
"ticketing-mcp",
auth=BearerAuthProvider(
jwks_uri="https://auth.corp.example/.well-known/jwks.json",
required_scopes=["mcp:ticketing:read"],
),
)
@mcp.tool(
description="Search tickets by JQL query. Read-only.",
annotations={"readOnlyHint": True, "idempotentHint": True},
)
async def search_tickets(query: str, max_results: int = 20) -> list[dict]:
# Auth context injected per-request by the BearerAuthProvider.
# No session object. No global state. Safe for any pod to handle.
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://jira.corp.example/rest/api/3/search",
params={"jql": query, "maxResults": max_results},
headers={"Authorization": f"Bearer {os.environ['JIRA_API_TOKEN']}"},
)
resp.raise_for_status()
return resp.json()["issues"]
Where MCP Actually Breaks
I've been building on this protocol for over a year. Here's the honest list of failure modes nobody talks about until they've hit them.

The SSE timeout one catches almost everyone. You configure Streamable HTTP, everything works in dev, you push to prod, and suddenly long-running tool calls are silently dying. The load balancer's idle connection timeout — usually 60 seconds — kills the SSE stream before your database export finishes. The fix is simple once you know it: push heartbeat notifications every 30 seconds, and bump your ingress idle timeout to at least 5 minutes. The discovery process is not simple.
MCP vs. the Alternatives: An Honest Comparison

The column that matters most in that table is the one everyone argues about least: LLM portability. Right now you might be locked into Claude or GPT-4o. Six months from now, there'll be a model from a lab you've never heard of that outperforms both on your specific task. If your tool integrations are written against a provider's function-call schema, you're rewriting them. If they're MCP servers, you're updating a client config file.
The Migration Playbook: 3 Days to 11 Minutes
Here's exactly how we did our migration. Not the sanitized version. The version that includes the detour through a broken approach we had to back out.
- Audit your existing tool integrations – catalog every function schema, every parsing layer, every auth mechanism. We found 14 distinct integrations in our codebase, 6 of which were duplicates with slightly different error handling. Don't migrate duplicates; kill them first.
- Pick FastMCP, not the raw SDK – We initially tried building directly against the TypeScript SDK for more control. That cost us a week. FastMCP's Python decorator model handles 90% of the scaffolding — schema generation, transport setup, error wrapping. Use it. You can always drop to the raw SDK for edge cases.
- Deploy your gateway first, before any servers – the gateway is where your auth, rate limiting, and audit logging live. Getting it right before servers come online means you're not retrofitting security. We used a simple FastAPI proxy with
httpxfor upstream calls. Took two days. Worth every hour. - Migrate one server per sprint, not all at once — We tried a big-bang migration on our second attempt. It failed. One server per sprint gives you a working fallback and real production data on how each integration behaves under MCP before you cut over.
- Instrument tool calls from day one – every
tools/callshould emit a structured log with: tool name, calling agent, token scope used, response time, and whether it succeeded. That data will save you during the first production incident, which will happen.
What's Coming — And What to Plan For
MCP governance moved to the Linux Foundation's Agentic AI Foundation in December 2025. That matters because it de-risks the protocol against any single vendor's agenda. OpenAI adopted it in April 2025. Google DeepMind's Vertex AI came on board in March 2026. AWS Bedrock in November 2025. This is no longer Anthropic's protocol. It's infrastructure.
The 2026 roadmap has four working-group priorities worth knowing:
- MCP Server Cards – Machine-readable server manifests at a
/.well-known/mcp.jsonendpoint. Think package.json for your MCP server: capabilities, auth requirements, tool list, rate limits. Enables automatic gateway discovery and policy enforcement without configuration drift. - Stateless transport formalization – The current stateless mode is in spec but not yet standardized in behavior across SDKs. The Q2 2026 working group is closing those gaps. Wait for this before going all-in on multi-cloud stateless deployments.
- A2A protocol integration – Google's Agent-to-Agent protocol handles horizontal agent coordination. MCP handles vertical tool connection. The integration point is where agents hand off tasks to subagents that themselves use MCP. Plan for this architecture now, even if you don't need it yet.
- Audit extensions – Structured compliance fields for tool calls: user context, data classification, retention tags. If you're building in a regulated industry, this will make your compliance team significantly less anxious. Targeting Q3 2026.
Opinions expressed by DZone contributors are their own.
Comments