MCP uses a three-layer architecture: Hosts (AI apps like Claude Desktop or Cursor) contain Clients (one per server connection) that communicate with Servers (lightweight processes exposing tools, resources, and prompts). Communication flows over JSON-RPC 2.0 using either stdio for local servers or Streamable HTTP for remote ones. Understanding this architecture is essential for debugging connection issues and building reliable integrations.
How MCP's Three-Layer Architecture Connects AI to External Services
MCP separates concerns into three distinct roles. The Host is the user-facing application (Claude Desktop, Cursor, VS Code). The Client lives inside the host and manages a single server connection, handling protocol negotiation, message routing, and capability tracking. The Server is a lightweight process that exposes tools, resources, and prompts from one external service. This clean separation means you can swap hosts without changing servers, run multiple servers in parallel, and debug each layer independently.
Prerequisites
- Read the 'What is Model Context Protocol' overview or have basic MCP familiarity
- Understanding of client-server communication concepts
- Familiarity with JSON as a data format
Step-by-step guide
Understand the Host layer
Understand the Host layer
The Host is the AI application the user directly interacts with. Examples include Claude Desktop, Cursor, VS Code with GitHub Copilot, Windsurf, and Claude Code CLI. The host is responsible for creating and managing MCP client instances, enforcing security policies (which servers are allowed, what permissions they have), mediating between the AI model and the MCP clients, and presenting server capabilities to the user interface. A single host can create multiple clients, each connected to a different server.
Expected result: You understand that the host is the outer shell — the AI app that orchestrates everything and enforces security boundaries.
Understand the Client layer
Understand the Client layer
Each MCP Client maintains a 1:1 connection with exactly one MCP Server. The client handles protocol version negotiation during the initialization handshake, discovers the server's capabilities (which tools, resources, and prompts it offers), routes requests from the host to the server and responses back, and manages the connection lifecycle (connect, reconnect, disconnect). The host creates one client per server configuration entry. If you have three servers in your config, the host runs three clients.
1// Conceptual: how a host creates clients (simplified)2// This is internal to the host — you do not write this code3//4// for each server in config:5// client = new McpClient()6// client.connect(server.transport)7// client.initialize() // handshake + capability discovery8// host.registerClient(client)Expected result: You understand that clients are the internal connection managers — one client per server, handling all protocol details.
Understand the Server layer
Understand the Server layer
The MCP Server is the component you build or configure. It is a lightweight process that connects to one external service and exposes that service's functionality through MCP's three capability types: tools (model-invoked actions), resources (application-loaded data), and prompts (user-selected templates). Servers run as separate processes — either launched by the host (stdio transport) or running independently (Streamable HTTP transport). Servers must never assume anything about which host is connecting; they speak pure MCP protocol.
1import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";2import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";34// A server declares its identity and version5const server = new McpServer({6 name: "weather-service",7 version: "1.0.0",8});910// Then exposes capabilities (tools, resources, prompts)11// ... tool and resource definitions go here ...1213// Finally connects via a transport14const transport = new StdioServerTransport();15await server.connect(transport);Expected result: You understand that servers are standalone processes exposing capabilities from one external service through the MCP protocol.
Trace a complete request flow
Trace a complete request flow
When a user asks Claude Desktop 'What is the weather in Tokyo?', here is what happens: (1) The host sends the user's message to the AI model. (2) The model decides it needs the 'get_weather' tool and returns a tool_use request. (3) The host finds which client is connected to the weather server. (4) The client sends a JSON-RPC 'tools/call' request to the server over stdio or HTTP. (5) The server calls the weather API, gets the result, and returns a JSON-RPC response. (6) The client passes the result back to the host. (7) The host feeds the tool result to the model. (8) The model generates a natural language response for the user.
1// JSON-RPC request from client to server2{3 "jsonrpc": "2.0",4 "id": 1,5 "method": "tools/call",6 "params": {7 "name": "get_weather",8 "arguments": { "city": "Tokyo" }9 }10}1112// JSON-RPC response from server to client13{14 "jsonrpc": "2.0",15 "id": 1,16 "result": {17 "content": [18 { "type": "text", "text": "Tokyo: 18°C, partly cloudy, humidity 65%" }19 ]20 }21}Expected result: You can trace a complete request from user question through host, client, server, external API, and back to the user.
Compare stdio and Streamable HTTP transports
Compare stdio and Streamable HTTP transports
MCP supports two transport mechanisms. Stdio transport is used for local servers: the host launches the server as a child process and communicates over stdin/stdout. This is the most common setup — fast, secure (no network), and simple. Streamable HTTP transport (which replaced the older SSE transport) is used for remote servers: the server runs on a separate machine and communicates over HTTP. This enables shared servers, cloud deployment, and multi-user access. Most developers start with stdio and move to Streamable HTTP when they need remote access.
1// Stdio transport — host launches server as child process2// Config in claude_desktop_config.json:3{4 "mcpServers": {5 "filesystem": {6 "command": "npx",7 "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/docs"]8 }9 }10}1112// Streamable HTTP transport — server runs independently13// Config in claude_desktop_config.json:14{15 "mcpServers": {16 "remote-api": {17 "url": "https://mcp.example.com/sse"18 }19 }20}Expected result: You understand when to use stdio (local, fast, simple) versus Streamable HTTP (remote, shared, multi-user).
Understand the initialization handshake
Understand the initialization handshake
When a client connects to a server, they perform an initialization handshake. The client sends an 'initialize' request with its protocol version and capabilities. The server responds with its own protocol version, capabilities (which of tools, resources, and prompts it supports), and server info. The client then sends an 'initialized' notification to confirm. Only after this handshake can the client call tools or read resources. If the protocol versions are incompatible, the connection fails gracefully.
1// Step 1: Client sends initialize request2{ "jsonrpc": "2.0", "id": 1, "method": "initialize",3 "params": {4 "protocolVersion": "2025-03-26",5 "capabilities": {},6 "clientInfo": { "name": "claude-desktop", "version": "1.5.0" }7 }8}910// Step 2: Server responds with its capabilities11{ "jsonrpc": "2.0", "id": 1,12 "result": {13 "protocolVersion": "2025-03-26",14 "capabilities": { "tools": {}, "resources": {} },15 "serverInfo": { "name": "weather-service", "version": "1.0.0" }16 }17}1819// Step 3: Client confirms20{ "jsonrpc": "2.0", "method": "notifications/initialized" }Expected result: You understand the three-step handshake (initialize, response, initialized notification) that establishes every MCP connection.
Complete working example
1// A demo server that shows all three capability types2// Run: npx -y tsx architecture-demo-server.ts34import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";5import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";6import { z } from "zod";78const server = new McpServer({9 name: "architecture-demo",10 version: "1.0.0",11});1213// TOOL — model-controlled, AI decides when to call this14server.tool(15 "get_server_time",16 "Returns the current server time",17 {},18 async () => ({19 content: [{20 type: "text",21 text: `Current time: ${new Date().toISOString()}`,22 }],23 })24);2526// RESOURCE — app-controlled, host pulls this for context27server.resource(28 "server-info",29 "info://server",30 { description: "Information about this MCP server" },31 async (uri) => ({32 contents: [{33 uri: uri.href,34 mimeType: "text/plain",35 text: "Architecture Demo Server v1.0.0 — demonstrates hosts, clients, and servers.",36 }],37 })38);3940// PROMPT — user-controlled, user selects this template41server.prompt(42 "explain-architecture",43 "Explain MCP architecture to a beginner",44 { detail_level: z.enum(["brief", "detailed"]).describe("How detailed the explanation should be") },45 ({ detail_level }) => ({46 messages: [{47 role: "user",48 content: {49 type: "text",50 text: detail_level === "brief"51 ? "Explain MCP architecture in 3 sentences."52 : "Explain MCP architecture in detail: hosts, clients, servers, transports, and the initialization handshake.",53 },54 }],55 })56);5758// Connect via stdio — host launches this process59const transport = new StdioServerTransport();60await server.connect(transport);6162// IMPORTANT: Never use console.log in stdio servers63// Use console.error for debug output instead64console.error("Architecture demo server started");Common mistakes
Why it's a problem: Confusing the host with the client
How to avoid: The host is the user-facing application (Claude Desktop, Cursor). The client is an internal component inside the host that manages one server connection. You configure hosts and build servers — clients are managed automatically.
Why it's a problem: Building one server that connects to many services
How to avoid: Each MCP server should connect to one external service. If you need GitHub and PostgreSQL access, run two separate servers. This keeps servers focused, debuggable, and reusable across projects.
Why it's a problem: Using console.log for debugging in stdio servers
How to avoid: Stdio servers communicate over stdout. Any console.log output corrupts the JSON-RPC message stream. Use console.error instead — it writes to stderr, which does not interfere with the protocol.
Why it's a problem: Assuming the server knows which host is connecting
How to avoid: MCP servers should be host-agnostic. Do not write code that checks if the client is Claude Desktop vs Cursor. The server speaks the MCP protocol and lets the host handle UI and security.
Best practices
- Keep servers single-purpose: one server per external service for maximum reusability
- Use stdio transport for local development — it is faster and the host manages the server lifecycle
- Always log to stderr (console.error) in stdio servers, never stdout (console.log)
- Test servers with MCP Inspector before connecting them to a host to isolate protocol issues from host issues
- Handle initialization errors gracefully — return clear error messages if the server cannot start
- Version your server using semantic versioning so hosts can track compatibility
- Document which capabilities your server exposes (tools, resources, prompts) in your README
- Consider connection timeouts — if your server calls slow external APIs, implement reasonable timeouts
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
Explain the MCP architecture with hosts, clients, and servers. Trace a complete request from a user asking a question in Claude Desktop through the host, client, server, and back. Include the JSON-RPC messages exchanged during a tool call.
Explain the MCP host-client-server architecture. I have Claude Desktop as my host with three MCP servers configured. Walk me through exactly what happens when the AI model decides to call a tool — from the model's tool_use request through the JSON-RPC exchange to the final response.
Frequently asked questions
Can one MCP client connect to multiple servers?
No. Each MCP client maintains a 1:1 relationship with exactly one server. If you need multiple servers, the host creates multiple clients — one per server. This is handled automatically by the host based on your configuration.
What happens if an MCP server crashes?
The client detects the disconnection and reports it to the host. Most hosts (Claude Desktop, Cursor) will show an error indicator. The server's tools become unavailable until it restarts. Stdio servers can be restarted by the host automatically on the next request.
Is the JSON-RPC communication encrypted?
For stdio transport, communication happens through OS pipes between processes on the same machine — no network encryption needed. For Streamable HTTP transport, you should always use HTTPS to encrypt communication over the network.
Can I run MCP servers on a different machine than the host?
Yes, using Streamable HTTP transport. The server runs on a remote machine and the host connects to it via HTTP URL. This is how teams share MCP servers across multiple developers.
How does the host decide which server to send a tool call to?
During initialization, each server reports its available tools. The host builds a registry mapping tool names to clients. When the model requests a tool, the host looks up which client (and therefore which server) owns that tool and routes the request accordingly.
Can RapidDev help design MCP architecture for complex setups?
Yes. RapidDev helps teams plan multi-server MCP architectures, build custom servers for internal services, and set up remote deployments with proper authentication and monitoring. This is especially useful for enterprise environments with many interconnected systems.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation