📚Academy
likeone
online

MCP Architecture

Three components, one protocol. Understand how Hosts, Clients, and Servers form the MCP communication layer.

The Three Components

MCP has three components that work together:

🧠 Host Application

The AI application the user interacts with — like Claude Desktop, Claude Code, or a custom app built with the Anthropic SDK. The Host initiates MCP connections and decides which servers to connect to. It contains one or more MCP Clients.

Role: Receives user input, decides when tools are needed, orchestrates the overall interaction.

🌐 MCP Client

A protocol bridge inside the Host that maintains a 1:1 connection with a single MCP Server. Each Client handles negotiation, capability exchange, and message routing for its server.

Role: Translates between the Host's internal format and the MCP protocol. One Client per Server connection.

⚙️ MCP Server

A lightweight program that exposes tools, resources, or prompts to the AI through the MCP protocol. Servers are where the business logic lives — reading files, querying databases, calling APIs.

Role: Receives tool calls from the Client, executes them, and returns results.

Data Flow — Step by Step

Here is the complete request lifecycle when a user asks a question that requires external data:

1 User asks a question — "What files are in my project directory?"
2 Host realizes it needs external data — The LLM sees available tools and decides to use the filesystem tool.
3 Client sends request to Server — MCP Client sends a tools/call request via JSON-RPC to the filesystem server.
4 Server executes and returns data — Filesystem server reads the directory and returns the file listing.
5 Host generates an informed answer — The LLM uses the real file data to respond accurately to the user.

Why JSON-RPC?

MCP uses JSON-RPC instead of REST, GraphQL, or gRPC. Here is why:

JSON-RPC is method-based, not URL-based.

REST maps operations to URLs and HTTP verbs (GET /tools, POST /tools/call). JSON-RPC uses a single channel with method names ({"method": "tools/call"}). This works over any transport — stdio pipes, WebSockets, HTTP — without needing URL routing.

It works over stdio.

Local MCP servers communicate through stdin/stdout — no network stack needed. REST requires an HTTP server, a port, and URL parsing. JSON-RPC is just JSON messages on a stream, making local servers trivially simple to build.

Bidirectional by design.

JSON-RPC supports both request/response AND notifications (one-way messages). This lets servers send progress updates, log events, or signal resource changes back to the client without being asked. REST is strictly request/response.

Transport Layer

MCP supports two transport mechanisms. The transport is separate from the protocol — the same server code works with either:

stdio (Local Servers)

Claude launches the server as a child process. Messages flow through stdin/stdout. Zero configuration — no ports, no TLS, no firewall rules. Ideal for personal tools and local development.

Streamable HTTP (Remote Servers)

The server runs on a remote machine and exposes an HTTP endpoint. Uses Server-Sent Events (SSE) for streaming responses. Required for team-shared servers, cloud deployments, and multi-user setups.

Capability Negotiation

Before any tools are called or resources accessed, the Client and Server perform an initialize handshake. This is the first message in every MCP connection — nothing else can happen until it completes.

Step 1 — Client sends initialize request

The Client tells the Server which protocol version it supports and what capabilities it has (like roots for filesystem access or sampling for LLM inference). This lets the Server know what it can ask the Client to do.

Step 2 — Server responds with its capabilities

The Server declares exactly what it supports: tools (callable functions), resources (data the AI can read), and prompts (reusable prompt templates). It also confirms the protocol version it will use. If the versions are incompatible, the connection fails gracefully.

Step 3 — Client sends initialized notification

Once the Client processes the Server's capabilities, it sends an initialized notification (a one-way message, no response expected). Only after this notification can the Client begin calling tools or reading resources.

Here is what the initialize handshake looks like on the wire:

Client → Server: initialize request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "roots": { "listChanged": true },
      "sampling": {}
    },
    "clientInfo": {
      "name": "Claude Desktop",
      "version": "1.5.0"
    }
  }
}
Server → Client: initialize response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-03-26",
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": { "subscribe": true },
      "prompts": { "listChanged": true }
    },
    "serverInfo": {
      "name": "filesystem-server",
      "version": "0.3.1"
    }
  }
}

Notice the Server declares it supports tools, resources, and prompts. The listChanged flag means the Server can notify the Client if its available tools change at runtime — enabling dynamic tool discovery without reconnecting.

What Happens When Something Goes Wrong

The protocol has built-in error handling for every failure mode:

Server crashes The Client detects the broken pipe and reports to the Host. Claude tells the user the tool is unavailable. No silent failures.
Invalid arguments The MCP SDK validates inputs against your Zod schema before calling your handler. Invalid calls are rejected with a clear error — your code never sees bad data.
Handler throws If your tool handler throws an unhandled exception, the SDK catches it and returns a JSON-RPC error response. The server stays running.
Version mismatch During initialize, client and server exchange protocol versions. If incompatible, the connection is rejected gracefully with a clear message.

Architecture in Practice: A Real Example

Theory makes more sense with a concrete scenario. Here is how Claude Desktop looks when connected to three MCP servers — a common real-world setup for a developer:

Claude Desktop (Host)

Contains 3 MCP Clients — one per server connection

Client A
Filesystem Server

stdio transport
Tools: read_file, write_file, list_directory, search_files

Client B
GitHub Server

stdio transport
Tools: create_issue, search_repos, create_pr, list_commits

Client C
Database Server

Streamable HTTP
Tools: query, list_tables
Resources: schema://tables

Key things to notice in this architecture:

1 One Client per Server. Client A only talks to the Filesystem Server. It knows nothing about GitHub or the database. Each connection is isolated.
2 Mixed transports. The Filesystem and GitHub servers use stdio (local processes). The Database server uses Streamable HTTP (remote, shared with the team). The Host handles both transparently.
3 The Host sees all tools. Even though tools come from different servers, the LLM sees a unified tool list. When it decides to call search_files, the Host routes the call to Client A, which forwards it to the Filesystem Server. The LLM does not need to know which server provides which tool.
4 Each connection negotiated independently. Client C's Database Server declared resources capability (exposing database schemas as readable resources). Clients A and B did not — their servers only expose tools. The Host adapts to each server's capabilities.

This is the power of MCP's architecture — the protocol handles all the complexity of multiple connections, different transports, and varying capabilities. You configure which servers to connect to, and the Host handles the rest.

Academy
Built with soul — likeone.ai