MCP, A2A, and the Agent Protocol Stack: What Actually Works in Production in 2026

MCP and A2A have sorted into a two-layer agent protocol stack — MCP for model-to-tool access, A2A for agent-to-agent coordination. What actually works in production: stateless migration, OAuth 2.1 auth, sampling security, observability gaps, and the composition patterns that hold up at scale.

MCP, A2A, and the Agent Protocol Stack: What Actually Works in Production in 2026

By mid-2026, two protocols have sorted themselves out as the dominant primitives in agent infrastructure: MCP for model-to-tool access, and A2A for agent-to-agent coordination. They solve different problems at different layers, and the interesting engineering work is in composing them — not picking one over the other.

This post covers what we’ve learned running both protocols in production, the architectural patterns that hold up, and the pain points that no one warns you about until you’re debugging a 3 AM incident with a misbehaving tool server.

The Two-Layer Model

The mental model that holds up best in practice is simple: MCP operates at layer 1 (model ↔ tools), and A2A operates at layer 2 (agent ↔ agent). They compose, not compete.

MCP gives a model access to external tools, resources, and prompt templates through a structured interface. It’s the protocol you use when your agent needs to call an API, query a database, or read a file. A2A gives one agent the ability to discover, delegate to, and coordinate with another agent. It’s the protocol you use when your orchestrator needs to hand off a complex task to a specialist.

This distinction matters because a common mistake in 2025 was building agent-to-agent communication on top of MCP tool calls. It worked in demos. It fell apart in production the moment you needed streaming results, task lifecycle management, or any kind of multi-turn coordination between agents.

MCP in Production: The Stateless Migration

Anthropic launched MCP in November 2024. The current stable spec is 2025-11-25, but the bigger news arrived with the 2026-07-28 release: a stateless core that removes the session handshake and Mcp-Session-Id header entirely. HTTP+SSE is deprecated via SEP-2596, replaced by Streamable HTTP with Mcp-Method and Mcp-Name headers.

For teams running MCP at scale, this is a significant change. The session-based model meant your infrastructure had to track state across requests — load balancers needed session affinity, and horizontal scaling was a constant headache. The stateless model means any server instance can handle any request. You can deploy behind standard load balancers without sticky sessions. Your Kubernetes HPA can actually work as intended.

The adoption numbers reflect this maturity: 97 million monthly SDK downloads across Python and TypeScript, over 10,000 public servers, and 78% of enterprise teams running MCP in production with 28% of Fortune 500 companies now on board.

Building an MCP Server in Practice

Defining an MCP server is straightforward. The FastMCP library (23.5K stars and growing) makes it even simpler. Here’s a real-world pattern for a tool server that wraps an internal API:

from fastmcp import FastMCP
from pydantic import BaseModel, Field

mcp = FastMCP("inventory-service")

class InventoryQuery(BaseModel):
    sku: str = Field(description="Product SKU to look up")
    warehouse: str | None = Field(default=None, description="Filter by warehouse ID")

@mcp.tool()
async def query_inventory(params: InventoryQuery) -> dict:
    """Query current inventory levels for a product SKU."""
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            f"https://internal-api.example.com/inventory/{params.sku}",
            params={"warehouse": params.warehouse} if params.warehouse else None,
            headers={"Authorization": f"Bearer {get_service_token()}"}
        )
        return resp.json()

@mcp.resource("inventory://warehouses")
async def list_warehouses() -> str:
    """List all active warehouse locations."""
    async with httpx.AsyncClient() as client:
        resp = await client.get("https://internal-api.example.com/warehouses")
        return json.dumps(resp.json())

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=8000)

Note the use of Streamable HTTP transport. For local development, stdio is fine. For anything deployed to a cluster, you want Streamable HTTP, which brings us to the auth question.

OAuth 2.1 + PKCE: Non-Negotiable for Remote Servers

The MCP spec mandates OAuth 2.1 with PKCE for remote Streamable HTTP connections. This isn’t optional guidance — it’s part of the protocol handshake. Your MCP server needs to implement the full OAuth 2.1 flow, including the /.well-known/oauth-authorization-server discovery endpoint.

If you’ve built OAuth before, the implementation is familiar. If you haven’t, the MCP auth spec is a good place to start — it’s one of the cleaner parts of the ecosystem. The main production consideration is token management: your MCP client needs to cache tokens, handle refresh, and gracefully degrade when a token expires mid-request.

A2A: Agent Coordination Without the Pain

Google announced A2A in April 2025 and donated it to the Linux Foundation two months later. By April 2026, over 150 organizations had adopted it, and it’s shipping in major cloud platforms.

A2A uses JSON-RPC over HTTPS with SSE streaming. The core concepts are:

  • Agent Cards: JSON documents that describe an agent’s capabilities, endpoint, and authentication requirements. These are your discovery mechanism.
  • Tasks: The unit of work. A task moves through statuses: submittedworkinginput-required / completed / failed / canceled.
  • Streaming: Server-Sent Events for real-time task updates.

Here’s a practical A2A task submission pattern using the Python SDK:

from a2a.client import A2AClient
from a2a.types import Task, TaskState, Message, TextPart

# Discover the agent's capabilities
client = A2AClient(base_url="https://agent.example.com")

# Check the Agent Card for supported capabilities
agent_card = client.get_agent_card()
print(f"Agent: {agent_card.name}, skills: {[s.name for s in agent_card.skills]}")

# Submit a task
task: Task = client.send_task(
    message=Message(
        role="user",
        parts=[TextPart(text="Analyze Q2 sales data for the APAC region")]
    ),
    metadata={"priority": "high", "callback_url": "https://our-app.example.com/a2a-callback"}
)

# Stream task updates
for event in client.stream_task(task.id):
    if event.status.state == TaskState.WORKING:
        print(f"Progress: {event.status.message}")
    elif event.status.state == TaskState.COMPLETED:
        print(f"Result: {event.artifacts}")
    elif event.status.state == TaskState.INPUT_REQUIRED:
        # Handle human-in-the-loop or inter-agent clarification
        client.send_task(
            task_id=task.id,
            message=Message(role="user", parts=[TextPart(text="Yes, include markdown charts")])
        )

The input-required state is where A2A gets interesting in production. It enables multi-turn task refinement without polling or custom callback mechanisms. Your orchestrator agent can ask the specialist agent for clarification, and the specialist can ask the orchestrator for additional context — all within the protocol’s built-in state machine.

Production Pain Points Nobody Warns You About

The Sampling Attack Vector

MCP allows servers to request LLM completions from the client via “sampling.” Unit42’s research demonstrated this as a prompt injection and data exfiltration vector. A malicious tool server can craft a sampling request that asks the client LLM to output sensitive data from the system prompt or context window.

In production, our guidance is straightforward: disable sampling on untrusted servers. If you need server-initiated LLM calls, implement allowlists for which servers can request sampling, and log every sampling request for audit. This is a real security boundary, not a theoretical concern.

The Observability Gap

Neither MCP nor A2A has built-in distributed tracing. This is a significant gap for production systems where a single user request might flow through an orchestrator agent, trigger three A2A tasks to specialist agents, each of which makes multiple MCP tool calls. Without end-to-end tracing, debugging latency or failures is guesswork.

The practical solution is OpenTelemetry instrumentation across both protocol boundaries. Wrap your MCP client and A2A client calls with OTel spans, propagate trace context through A2A task metadata, and correlate MCP tool call spans with A2A task spans. It’s not glamorous work, but it’s the difference between “the system is slow” and “the inventory MCP server in us-east-2 is timing out on warehouse queries.”

Migration From Stateful to Stateless MCP

If you’re running MCP servers on the pre-2026-07-28 spec, the migration to stateless is not trivial. Your session management code, your health checks that relied on Mcp-Session-Id, your load balancer configuration — all of it needs to change. The good news is the stateless model is simpler. The bad news is you have to update every MCP server and client in your fleet simultaneously, or handle both protocols during a transition period.

We recommend running a protocol version check during the handshake (yes, even in stateless mode, you still identify the server version) and maintaining compatibility shims for clients that haven’t migrated yet. Budget 2-3 weeks for a fleet of 10+ MCP servers.

Composing the Stack

The architecture that works in practice combines both layers with clear boundaries:

  1. Orchestrator Agent receives user requests and uses A2A to delegate to specialist agents.
  2. Specialist Agents each expose A2A Agent Cards describing their capabilities.
  3. Each specialist uses MCP internally to access its tools — databases, APIs, file systems.
  4. OpenTelemetry spans trace the full request path from orchestrator → A2A task → MCP tool call → response.

Google ADK supports this pattern natively with built-in MCP and A2A integration. For teams building custom stacks, the composition is explicit: your A2A task handlers call MCP clients, and your trace context flows through both.

The key architectural insight is that MCP and A2A serve different failure modes. MCP failures are tool-level: a database is down, an API rate-limits you, a file doesn’t exist. A2A failures are coordination-level: an agent is overloaded, a task times out, clarification loops don’t converge. Your retry, fallback, and circuit-breaking logic needs to operate at both layers independently.

Where We Go From Here

The AAIF governance structure under the Linux Foundation is maturing both protocols through open processes. The ecosystem is past the “which protocol wins” phase. The engineering question now is: how do we instrument, secure, and compose these protocols reliably at scale?

The answers are mundane but important: stateless transport, OAuth 2.1 everywhere, OpenTelemetry across boundaries, and clear layer separation in your architecture. No single protocol solves the whole problem. The stack does.

  • Hermes Tutorials — Hermes Agent setup, configuration, and advanced workflows
  • ToolBrain — tool reviews, LLM comparisons, and AI workflow guides
  • NiteAgent — AI agent development, frameworks, and production patterns

Cross-links automatically generated from CodeIntel Log.