Advanced · Lesson 3 of 6 · 24 min

MCP and A2A

How the Model Context Protocol connects agents to tools and data, what the stateless 2026-07-28 revision changed, how to build and test a minimal server, and where A2A and the Agentic AI Foundation fit.

What MCP standardises

Before MCP, every AI app needed its own integration for every tool. MCP defines one protocol: a host application such as Claude, Claude Code or an n8n workflow runs MCP clients, and each client talks to an MCP server that exposes capabilities. Servers offer tools the model can call, resources it can read and prompts users can invoke. Build a server once and any MCP-capable host can use it.

The ecosystem is now large enough that you will often consume servers rather than build them. Zapier exposes its app catalogue over MCP, HighLevel runs an MCP server in front of its API, and n8n ships MCP Client Tool and MCP Server Trigger nodes. Build your own when you need a narrow, well-described interface to an internal system, which usually beats handing an agent a sprawling generic API.

Treat every server as code you are trusting. Its tool descriptions go straight into the model's context, and its tools act with whatever credentials you gave it. The spec itself says there should always be a human in the loop able to deny tool invocations.

Transports and the stateless 2026-07-28 revision

MCP messages are JSON-RPC, carried by one of two standard transports. With stdio, the client launches the server as a subprocess and they exchange newline-delimited messages over standard input and output, which suits local tools. With Streamable HTTP, each message is an HTTP POST to a single endpoint and replies come back as JSON or a request-scoped event stream, which suits remote and shared servers. The older HTTP plus SSE transport is formally deprecated.

The 2026-07-28 revision made the protocol stateless. The initialize handshake and the Mcp-Session-Id header are gone; every request carries its protocol version and client capabilities in its metadata, and servers must implement server/discover to advertise what they support. Server-initiated requests are replaced by Multi Round-Trip Requests: a server that needs more input returns an input_required result and the client retries with the answers. Roots, Sampling and Logging are deprecated under a new policy with a minimum 12-month window, and tasks moved into an optional extension.

Operationally, stateless means you can run a remote server behind an ordinary load balancer without sticky sessions. It also means a broken response stream loses the in-flight request and the client must re-issue it, so tools that write data must be idempotent. If a tool needs state across calls, the spec's answer is an explicit handle your server mints and the model passes back as a normal argument.

A minimal server

This server, written against the current official TypeScript SDK, exposes one read-only tool that looks up a CRM contact by email. Note the design choices: a narrow tool with a precise description, input validated by a schema, credentials read from environment variables, and failures returned as a tool error the model can read rather than a crash.

On stdio, standard output carries the protocol itself, so any log line written there corrupts the message stream. Log to standard error only. The SDK's serveStdio helper negotiates the protocol revision with each client, so the same server answers both current clients and older ones that still open with the initialize handshake.

typescript
import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";

const CRM_BASE_URL = process.env.CRM_BASE_URL;
const CRM_TOKEN = process.env.CRM_TOKEN;
if (!CRM_BASE_URL || !CRM_TOKEN) throw new Error("Set CRM_BASE_URL and CRM_TOKEN");

// serveStdio calls this whenever it needs an instance, so return a fresh server each time.
function createServer() {
  const server = new McpServer({ name: "crm-lookup", version: "1.0.0" });

  server.registerTool(
    "find_contact_by_email",
    {
      description: "Find one CRM contact by exact email address. Read-only. Returns name, tags and pipeline stage.",
      inputSchema: z.object({ email: z.string().min(3).max(254).describe("Exact email address") }),
    },
    async ({ email }) => {
      const response = await fetch(CRM_BASE_URL + "/contacts?email=" + encodeURIComponent(email), {
        headers: { Authorization: "Bearer " + CRM_TOKEN },
      });
      if (!response.ok) {
        return { content: [{ type: "text", text: "CRM lookup failed with HTTP " + response.status }], isError: true };
      }
      return { content: [{ type: "text", text: JSON.stringify(await response.json()) }] };
    },
  );
  return server;
}

// stdout carries the protocol on stdio, so errors go to stderr.
serveStdio(createServer, { onerror: (error) => console.error("crm-lookup error:", error) });

Connecting and testing a server

Test a server on its own before you put a model in front of it. The official MCP Inspector runs through npx with no install: point it at the command that starts your server and it opens a browser UI, or use its CLI mode to list tools and call them from a script, which also makes a good CI smoke test.

To use a local stdio server from Claude for Desktop, add it under mcpServers in the app's claude_desktop_config.json with the command and arguments that start it, then restart the app. Remote Streamable HTTP servers are added by URL instead, usually with OAuth so the user grants access without pasting a token.

json
{
  "mcpServers": {
    "crm-lookup": {
      "command": "node",
      "args": ["/ABSOLUTE/PATH/TO/crm-lookup/build/index.js"]
    }
  }
}

A2A, and who governs these standards

MCP connects an agent to tools and data. A2A, the Agent2Agent protocol, connects an agent to other agents, including ones run by another team or company. The A2A project describes the two as complementary: tools are typically stateless functions, while agents talking over A2A can reason, plan, ask for clarification and run long tasks asynchronously, with their inner workings kept opaque to each other. A client discovers what a remote agent can do, and how to authenticate, by fetching its Agent Card. A2A has a stable v1.0 specification.

You rarely need A2A for a single business's automations; a subagent or an MCP tool is simpler. It earns its place when independently built agents, often from different vendors, must hand work to each other, such as a buyer's procurement agent negotiating with a supplier's quoting agent.

Both standards are now vendor-neutral. The Agentic AI Foundation (AAIF), under the Linux Foundation, was co-founded by Anthropic, Block and OpenAI and hosts MCP, A2A, AGENTS.md, goose and Agent Skills; A2A joined in August 2026 and Anthropic donated Agent Skills in October 2026. For you, neutral governance plus MCP's 12-month deprecation window means a slower, more predictable upgrade path: read each spec changelog and plan migrations rather than discovering them when a client breaks.

Key points

  • ◆MCP hosts run clients that talk to servers exposing tools, resources and prompts; build once, use in any MCP host.
  • ◆Use stdio for local servers and Streamable HTTP for remote ones; HTTP plus SSE is deprecated.
  • ◆Since 2026-07-28 MCP is stateless: no initialize handshake or session header, server/discover is required, and write tools must be idempotent.
  • ◆Never write logs to stdout in a stdio server, and test every server with the MCP Inspector before connecting a model.
  • ◆A2A is for agent-to-agent delegation across systems; both MCP and A2A are governed by the AAIF under the Linux Foundation.

Terms in this lesson

Try it · exercise

Build the minimal MCP server from this lesson against fake data and verify it with the MCP Inspector, no model required.

  1. Create a project and install the SDK and zod as in the official build-server guide (npm install @modelcontextprotocol/server zod).
  2. Replace the fetch call with an in-memory map of three contacts, returning isError true with a clear message for unknown emails.
  3. Build the server, then run npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list.
  4. Open the Inspector UI with npx @modelcontextprotocol/inspector node build/index.js and call the tool with one known and one unknown email.

You're done when: tools/list prints find_contact_by_email with your description and schema; a known email returns that contact's JSON; an unknown email returns your error text flagged as an error; and nothing but protocol messages appear on stdout.

Quiz me on this lesson