Chapter 8
20 min read
Section 47 of 175

MCP Architecture Deep Dive

Model Context Protocol (MCP)

Introduction

Understanding MCP's architecture is essential for building robust servers and clients. This section dissects the protocol layer by layerβ€”from transport mechanisms to message formats to the lifecycle of a session.

Architecture Philosophy: MCP is built on JSON-RPC 2.0, uses capability-based negotiation, and supports multiple transport mechanismsβ€”balancing simplicity with flexibility.

Transport Layers

MCP supports multiple transport mechanisms, each suited for different deployment scenarios:

1. Standard I/O (stdio)

The most common transport for local integrations:

πŸ“stdio_transport.txt
1STDIO TRANSPORT
2
3β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     stdin      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
4β”‚                 β”‚ ─────────────▢ β”‚                 β”‚
5β”‚   MCP Client    β”‚                β”‚   MCP Server    β”‚
6β”‚   (AI App)      β”‚ ◀───────────── β”‚   (Tool)        β”‚
7β”‚                 β”‚     stdout     β”‚                 β”‚
8β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
9         β”‚                                  β”‚
10         β”‚                                  β”‚
11         └── Spawns server β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
12             as subprocess
13
14Characteristics:
15β€’ Server runs as subprocess of client
16β€’ Simple: just stdin/stdout pipes
17β€’ Secure: no network exposure
18β€’ Perfect for: local tools, IDE extensions
🐍stdio_example.py
1import sys
2import json
3
4def stdio_server():
5    """Simple stdio-based MCP server."""
6
7    # Read from stdin
8    for line in sys.stdin:
9        request = json.loads(line)
10
11        # Process request
12        response = handle_request(request)
13
14        # Write to stdout
15        sys.stdout.write(json.dumps(response) + "\n")
16        sys.stdout.flush()
17
18def handle_request(request: dict) -> dict:
19    """Route JSON-RPC requests to handlers."""
20    method = request.get("method")
21    params = request.get("params", {})
22    request_id = request.get("id")
23
24    if method == "tools/list":
25        result = list_tools()
26    elif method == "tools/call":
27        result = call_tool(params["name"], params["arguments"])
28    else:
29        return {
30            "jsonrpc": "2.0",
31            "id": request_id,
32            "error": {"code": -32601, "message": "Method not found"}
33        }
34
35    return {
36        "jsonrpc": "2.0",
37        "id": request_id,
38        "result": result
39    }

2. HTTP with Server-Sent Events (SSE)

For remote servers and web-based deployments:

πŸ“http_sse_transport.txt
1HTTP/SSE TRANSPORT
2
3β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
4β”‚                 β”‚   HTTP POST    β”‚                 β”‚
5β”‚   MCP Client    β”‚ ─────────────▢ β”‚   MCP Server    β”‚
6β”‚                 β”‚                β”‚                 β”‚
7β”‚                 β”‚ ◀───────────── β”‚                 β”‚
8β”‚                 β”‚      SSE       β”‚                 β”‚
9β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
10
11Request flow:
121. Client POSTs JSON-RPC request to server endpoint
132. Server processes request
143. Server streams response via SSE (Server-Sent Events)
15
16Characteristics:
17β€’ Works over network (remote servers)
18β€’ Streaming support for long operations
19β€’ Firewall-friendly (standard HTTP)
20β€’ Perfect for: cloud services, shared tools
πŸ“˜http_sse_server.ts
1import express from "express";
2
3const app = express();
4app.use(express.json());
5
6// SSE endpoint for streaming responses
7app.get("/sse", (req, res) => {
8  res.setHeader("Content-Type", "text/event-stream");
9  res.setHeader("Cache-Control", "no-cache");
10  res.setHeader("Connection", "keep-alive");
11
12  // Store connection for sending events
13  const clientId = Date.now();
14  clients.set(clientId, res);
15
16  req.on("close", () => clients.delete(clientId));
17});
18
19// HTTP endpoint for requests
20app.post("/message", async (req, res) => {
21  const request = req.body;
22
23  // Process the JSON-RPC request
24  const response = await handleRequest(request);
25
26  // Send response via SSE to the connected client
27  const client = clients.get(req.headers["x-client-id"]);
28  if (client) {
29    client.write(`data: ${JSON.stringify(response)}\n\n`);
30  }
31
32  res.status(202).json({ status: "accepted" });
33});
34
35app.listen(3000, () => {
36  console.log("MCP server listening on port 3000");
37});

3. WebSocket (Custom)

For bidirectional real-time communication:

πŸ“websocket_transport.txt
1WEBSOCKET TRANSPORT
2
3β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
4β”‚                 β”‚ ◀────────────▢ β”‚                 β”‚
5β”‚   MCP Client    β”‚   WebSocket    β”‚   MCP Server    β”‚
6β”‚                 β”‚   (full duplex)β”‚                 β”‚
7β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
8
9Characteristics:
10β€’ Full bidirectional communication
11β€’ Low latency
12β€’ Server can push updates
13β€’ Perfect for: real-time features, notifications
TransportUse CaseProsCons
stdioLocal toolsSimple, secure, no networkSingle machine only
HTTP/SSERemote servicesWorks over internet, standard HTTPHigher latency
WebSocketReal-timeFull duplex, low latencyMore complex setup

Message Format

MCP uses JSON-RPC 2.0 as its message format:

Request Message

{}request_format.json
1{
2  "jsonrpc": "2.0",
3  "id": 1,
4  "method": "tools/call",
5  "params": {
6    "name": "create_file",
7    "arguments": {
8      "path": "/tmp/hello.txt",
9      "content": "Hello, World!"
10    }
11  }
12}

Response Message (Success)

{}response_success.json
1{
2  "jsonrpc": "2.0",
3  "id": 1,
4  "result": {
5    "content": [
6      {
7        "type": "text",
8        "text": "File created successfully at /tmp/hello.txt"
9      }
10    ],
11    "isError": false
12  }
13}

Response Message (Error)

{}response_error.json
1{
2  "jsonrpc": "2.0",
3  "id": 1,
4  "error": {
5    "code": -32602,
6    "message": "Invalid params",
7    "data": {
8      "details": "Path must be absolute"
9    }
10  }
11}

Notification Message (No Response Expected)

{}notification.json
1{
2  "jsonrpc": "2.0",
3  "method": "notifications/progress",
4  "params": {
5    "progressToken": "task-123",
6    "progress": 0.5,
7    "total": 1.0
8  }
9}
10// Note: No "id" field means no response expected

Standard Error Codes

CodeNameMeaning
-32700Parse errorInvalid JSON
-32600Invalid RequestNot a valid Request object
-32601Method not foundMethod doesn't exist
-32602Invalid paramsInvalid method parameters
-32603Internal errorServer error

Protocol Flow

A typical MCP session follows this flow:

πŸ“protocol_flow.txt
1MCP SESSION FLOW
2
3CLIENT                                    SERVER
4   β”‚                                         β”‚
5   │─────── initialize ─────────────────────▢│
6   β”‚        (protocol version, capabilities) β”‚
7   β”‚                                         β”‚
8   │◀────── initialize response ─────────────│
9   β”‚        (server info, capabilities)      β”‚
10   β”‚                                         β”‚
11   │─────── initialized ────────────────────▢│
12   β”‚        (handshake complete)             β”‚
13   β”‚                                         β”‚
14   β”œβ”€β”€β”€β”€β”€β”€β”€ CAPABILITY DISCOVERY ─────────────
15   β”‚                                         β”‚
16   │─────── tools/list ─────────────────────▢│
17   β”‚                                         β”‚
18   │◀────── tools list ──────────────────────│
19   β”‚                                         β”‚
20   │─────── resources/list ─────────────────▢│
21   β”‚                                         β”‚
22   │◀────── resources list ──────────────────│
23   β”‚                                         β”‚
24   β”œβ”€β”€β”€β”€β”€β”€β”€ NORMAL OPERATION ─────────────────
25   β”‚                                         β”‚
26   │─────── tools/call ─────────────────────▢│
27   β”‚        (execute tool)                   β”‚
28   β”‚                                         β”‚
29   │◀────── tool result ─────────────────────│
30   β”‚                                         β”‚
31   │─────── resources/read ─────────────────▢│
32   β”‚        (read resource)                  β”‚
33   β”‚                                         β”‚
34   │◀────── resource content ────────────────│
35   β”‚                                         β”‚
36   β”œβ”€β”€β”€β”€β”€β”€β”€ SESSION END ──────────────────────
37   β”‚                                         β”‚
38   │─────── shutdown ───────────────────────▢│
39   β”‚                                         β”‚
40   │◀────── shutdown ack ────────────────────│
41   β”‚                                         β”‚

1. Initialization

πŸ“˜initialization.ts
1// Client sends initialize request
2const initRequest = {
3  jsonrpc: "2.0",
4  id: 1,
5  method: "initialize",
6  params: {
7    protocolVersion: "2024-11-05",
8    capabilities: {
9      roots: {
10        listChanged: true
11      },
12      sampling: {}
13    },
14    clientInfo: {
15      name: "MyAIApp",
16      version: "1.0.0"
17    }
18  }
19};
20
21// Server responds with its capabilities
22const initResponse = {
23  jsonrpc: "2.0",
24  id: 1,
25  result: {
26    protocolVersion: "2024-11-05",
27    capabilities: {
28      tools: {},
29      resources: {
30        subscribe: true,
31        listChanged: true
32      },
33      prompts: {
34        listChanged: true
35      }
36    },
37    serverInfo: {
38      name: "GitHub MCP Server",
39      version: "1.2.0"
40    }
41  }
42};
43
44// Client confirms initialization
45const initializedNotification = {
46  jsonrpc: "2.0",
47  method: "initialized"
48  // No id = notification, no response expected
49};

2. Capability Discovery

πŸ“˜discovery.ts
1// List available tools
2const toolsRequest = {
3  jsonrpc: "2.0",
4  id: 2,
5  method: "tools/list"
6};
7
8const toolsResponse = {
9  jsonrpc: "2.0",
10  id: 2,
11  result: {
12    tools: [
13      {
14        name: "create_issue",
15        description: "Create a GitHub issue",
16        inputSchema: {
17          type: "object",
18          properties: {
19            repo: { type: "string", description: "owner/repo" },
20            title: { type: "string", description: "Issue title" },
21            body: { type: "string", description: "Issue body" }
22          },
23          required: ["repo", "title"]
24        }
25      },
26      {
27        name: "search_code",
28        description: "Search code across repositories",
29        inputSchema: {
30          type: "object",
31          properties: {
32            query: { type: "string", description: "Search query" },
33            language: { type: "string", description: "Filter by language" }
34          },
35          required: ["query"]
36        }
37      }
38    ]
39  }
40};
41
42// List available resources
43const resourcesRequest = {
44  jsonrpc: "2.0",
45  id: 3,
46  method: "resources/list"
47};
48
49const resourcesResponse = {
50  jsonrpc: "2.0",
51  id: 3,
52  result: {
53    resources: [
54      {
55        uri: "github://repos/anthropics/claude-code",
56        name: "Claude Code Repository",
57        description: "Main repo files",
58        mimeType: "text/plain"
59      }
60    ]
61  }
62};

3. Tool Execution

πŸ“˜tool_execution.ts
1// Client calls a tool
2const toolCallRequest = {
3  jsonrpc: "2.0",
4  id: 4,
5  method: "tools/call",
6  params: {
7    name: "create_issue",
8    arguments: {
9      repo: "myorg/myrepo",
10      title: "Bug: Login not working",
11      body: "Users report login button is unresponsive"
12    }
13  }
14};
15
16// Server executes and responds
17const toolCallResponse = {
18  jsonrpc: "2.0",
19  id: 4,
20  result: {
21    content: [
22      {
23        type: "text",
24        text: "Created issue #42: Bug: Login not working"
25      },
26      {
27        type: "resource",
28        resource: {
29          uri: "github://issues/myorg/myrepo/42",
30          mimeType: "text/markdown",
31          text: "Issue created successfully with ID 42"
32        }
33      }
34    ],
35    isError: false
36  }
37};

Capability Negotiation

MCP uses capability negotiation to ensure clients and servers can work together effectively:

Client Capabilities

πŸ“˜client_capabilities.ts
1interface ClientCapabilities {
2  // Experimental features
3  experimental?: Record<string, object>;
4
5  // Root URI support
6  roots?: {
7    // Client can notify of root changes
8    listChanged?: boolean;
9  };
10
11  // Sampling support (LLM calls from server)
12  sampling?: {};
13}

Server Capabilities

πŸ“˜server_capabilities.ts
1interface ServerCapabilities {
2  // Experimental features
3  experimental?: Record<string, object>;
4
5  // Logging support
6  logging?: {};
7
8  // Prompt capabilities
9  prompts?: {
10    listChanged?: boolean;
11  };
12
13  // Resource capabilities
14  resources?: {
15    subscribe?: boolean;      // Resources can be subscribed to
16    listChanged?: boolean;    // Resource list can change
17  };
18
19  // Tool capabilities
20  tools?: {
21    listChanged?: boolean;    // Tool list can change
22  };
23}

Capability Matching

πŸ“˜capability_matching.ts
1function negotiateCapabilities(
2  clientCaps: ClientCapabilities,
3  serverCaps: ServerCapabilities
4): SessionCapabilities {
5  return {
6    // Both sides must support for feature to be active
7    canSubscribeToResources:
8      serverCaps.resources?.subscribe === true,
9
10    canReceiveListChanges:
11      (clientCaps.roots?.listChanged === true) &&
12      (serverCaps.resources?.listChanged === true),
13
14    canRequestSampling:
15      clientCaps.sampling !== undefined,
16
17    // Server-only features
18    hasLogging: serverCaps.logging !== undefined,
19    hasPrompts: serverCaps.prompts !== undefined,
20    hasTools: serverCaps.tools !== undefined,
21    hasResources: serverCaps.resources !== undefined
22  };
23}

Graceful Degradation

Clients should handle servers that don't support all capabilities. Check capabilities before using advanced features and fall back gracefully when features aren't available.

Session Lifecycle

Understanding the session lifecycle helps build robust implementations:

Session States

πŸ“session_states.txt
1SESSION STATE MACHINE
2
3β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
4β”‚  DISCONNECTED  β”‚
5β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
6        β”‚ connect()
7        β–Ό
8β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
9β”‚  CONNECTING    β”‚
10β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
11        β”‚ transport ready
12        β–Ό
13β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
14β”‚  INITIALIZING  │◀──────────────────┐
15β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
16        β”‚ initialize handshake       β”‚
17        β–Ό                            β”‚
18β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
19β”‚    READY       │────────────────────
20β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜   reconnect       β”‚
21        β”‚                            β”‚
22        β”‚ shutdown or error          β”‚
23        β–Ό                            β”‚
24β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
25β”‚  DISCONNECTING β”‚                   β”‚
26β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   β”‚
27        β”‚                            β”‚
28        β–Ό                            β”‚
29β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                   β”‚
30β”‚  DISCONNECTED  β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
31β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementing Session Management

πŸ“˜session_management.ts
1import { EventEmitter } from "events";
2
3enum SessionState {
4  DISCONNECTED = "disconnected",
5  CONNECTING = "connecting",
6  INITIALIZING = "initializing",
7  READY = "ready",
8  DISCONNECTING = "disconnecting"
9}
10
11class MCPSession extends EventEmitter {
12  private state: SessionState = SessionState.DISCONNECTED;
13  private transport: Transport | null = null;
14  private capabilities: ServerCapabilities | null = null;
15
16  async connect(transport: Transport): Promise<void> {
17    this.setState(SessionState.CONNECTING);
18    this.transport = transport;
19
20    try {
21      await transport.connect();
22      this.setState(SessionState.INITIALIZING);
23
24      // Perform initialization handshake
25      const initResult = await this.sendRequest("initialize", {
26        protocolVersion: "2024-11-05",
27        capabilities: this.getClientCapabilities(),
28        clientInfo: { name: "MyClient", version: "1.0" }
29      });
30
31      this.capabilities = initResult.capabilities;
32
33      // Send initialized notification
34      await this.sendNotification("initialized");
35
36      this.setState(SessionState.READY);
37      this.emit("ready", this.capabilities);
38
39    } catch (error) {
40      this.setState(SessionState.DISCONNECTED);
41      throw error;
42    }
43  }
44
45  async disconnect(): Promise<void> {
46    if (this.state !== SessionState.READY) return;
47
48    this.setState(SessionState.DISCONNECTING);
49
50    try {
51      await this.sendRequest("shutdown");
52    } finally {
53      await this.transport?.close();
54      this.transport = null;
55      this.setState(SessionState.DISCONNECTED);
56      this.emit("disconnected");
57    }
58  }
59
60  private setState(state: SessionState): void {
61    const oldState = this.state;
62    this.state = state;
63    this.emit("stateChange", { from: oldState, to: state });
64  }
65}

Handling Reconnection

πŸ“˜reconnection.ts
1class RobustMCPSession extends MCPSession {
2  private reconnectAttempts = 0;
3  private maxReconnectAttempts = 5;
4  private reconnectDelay = 1000;
5
6  constructor() {
7    super();
8
9    this.on("disconnected", () => {
10      if (this.shouldReconnect()) {
11        this.scheduleReconnect();
12      }
13    });
14
15    this.on("error", (error) => {
16      console.error("Session error:", error);
17      // Error might trigger disconnection and reconnect
18    });
19  }
20
21  private shouldReconnect(): boolean {
22    return this.reconnectAttempts < this.maxReconnectAttempts;
23  }
24
25  private scheduleReconnect(): void {
26    const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
27
28    setTimeout(async () => {
29      this.reconnectAttempts++;
30      console.log(`Reconnect attempt ${this.reconnectAttempts}...`);
31
32      try {
33        await this.connect(this.transport!);
34        this.reconnectAttempts = 0; // Reset on success
35      } catch (error) {
36        console.error("Reconnect failed:", error);
37        // Will trigger another reconnect via disconnected event
38      }
39    }, delay);
40  }
41}
Always implement proper cleanup in disconnect handlers. Leaked connections can cause resource exhaustion, especially with stdio transports where orphaned processes may remain running.

Summary

Key architectural concepts of MCP:

  1. Transport layers: stdio for local, HTTP/SSE for remote, WebSocket for real-time
  2. JSON-RPC 2.0: Standard message format with request/response/notification types
  3. Protocol flow: Initialize β†’ Discover capabilities β†’ Operate β†’ Shutdown
  4. Capability negotiation: Client and server agree on supported features
  5. Session lifecycle: State machine managing connection states and reconnection
Next: Now that we understand the architecture, let's build an MCP server from scratch.