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
| Role | Description | Example |
|---|---|---|
| MCP Host | The AI application — coordinates and manages one or multiple MCP clients | Claude Desktop, VS Code, Claude Code |
| MCP Client | A component inside the host — maintains a dedicated connection to one MCP server | Instantiated per server by the host runtime |
| MCP Server | A program that provides context to MCP clients | Filesystem 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:
| Transport | Mechanism | Use case |
|---|---|---|
| Stdio | Standard input/output streams | Local servers on the same machine; no network overhead |
| Streamable HTTP | HTTP POST + optional SSE | Remote 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
- Client sends
initializewith itsprotocolVersionandcapabilities - Server responds with its own
protocolVersionandcapabilities - Client sends
notifications/initializedto 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