MCP Architecture

MCP uses a client-server architecture with two distinct layers (data and transport) and three participant roles (host, client, server). Understanding this structure is prerequisite to building or debugging MCP integrations.

Participants

RoleDescriptionExample
MCP HostThe AI application — coordinates and manages one or multiple MCP clientsClaude Desktop, VS Code, Claude Code
MCP ClientA component inside the host — maintains a dedicated connection to one MCP serverInstantiated per server by the host runtime
MCP ServerA program that provides context to MCP clientsFilesystem server, Sentry, database

Key point: the host creates one MCP client per server. VS Code connecting to both the Sentry server and the filesystem server instantiates two separate client objects.

MCP servers can be local (stdio transport, same machine) or remote (Streamable HTTP, network). The term “MCP server” refers to the program regardless of where it runs.

Two Layers

Data Layer (inner)

Defines the JSON-RPC 2.0 based exchange protocol — message structure, semantics, and all the things developers care about:

  • Lifecycle management — connection initialization, capability negotiation, termination
  • Server primitives — tools, resources, prompts
  • Client primitives — sampling, elicitation, logging
  • Notifications — real-time updates from server to client (and vice versa)
  • Tasks (experimental) — durable execution wrappers for long-running operations

Transport Layer (outer)

Manages communication channels and authentication. Abstracts away from the data layer so the same JSON-RPC 2.0 messages work over either transport:

TransportMechanismUse case
StdioStandard input/output streamsLocal servers on the same machine; no network overhead
Streamable HTTPHTTP POST + optional SSERemote servers; supports bearer tokens, API keys, OAuth

Lifecycle Management

MCP is a stateful protocol — every connection begins with an initialization handshake that negotiates capabilities.

Initialization sequence

  1. Client sends initialize with its protocolVersion and capabilities
  2. Server responds with its own protocolVersion and capabilities
  3. Client sends notifications/initialized to signal readiness
// Client initialize request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": { "elicitation": {} },
    "clientInfo": { "name": "example-client", "version": "1.0.0" }
  }
}
 
// Server initialize response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": { "listChanged": true },
      "resources": {}
    },
    "serverInfo": { "name": "example-server", "version": "1.0.0" }
  }
}

The capabilities object determines which primitives are active and which notifications will be sent. If the server declares tools.listChanged: true, it will push notifications/tools/list_changed when its tool list changes — clients react rather than poll.

If a mutually compatible protocol version cannot be negotiated, the connection should be terminated.

Notifications

Notifications are JSON-RPC 2.0 messages sent without an id — no response is expected. Servers use them to push real-time updates (e.g. tool list changed); clients respond by re-fetching the relevant data.

{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }

Notifications are only sent if the capability was declared during initialization.

See Also

  • mcp — what MCP is and why it exists
  • mcp-primitives — tools, resources, prompts, and client-side primitives