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:
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 extensions1import 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:
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 tools1import 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:
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| Transport | Use Case | Pros | Cons |
|---|---|---|---|
| stdio | Local tools | Simple, secure, no network | Single machine only |
| HTTP/SSE | Remote services | Works over internet, standard HTTP | Higher latency |
| WebSocket | Real-time | Full duplex, low latency | More complex setup |
Message Format
MCP uses JSON-RPC 2.0 as its message format:
Request Message
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)
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)
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)
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 expectedStandard Error Codes
| Code | Name | Meaning |
|---|---|---|
| -32700 | Parse error | Invalid JSON |
| -32600 | Invalid Request | Not a valid Request object |
| -32601 | Method not found | Method doesn't exist |
| -32602 | Invalid params | Invalid method parameters |
| -32603 | Internal error | Server error |
Protocol Flow
A typical MCP session follows this flow:
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
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
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
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
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
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
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
Session Lifecycle
Understanding the session lifecycle helps build robust implementations:
Session States
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
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
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}Summary
Key architectural concepts of MCP:
- Transport layers: stdio for local, HTTP/SSE for remote, WebSocket for real-time
- JSON-RPC 2.0: Standard message format with request/response/notification types
- Protocol flow: Initialize β Discover capabilities β Operate β Shutdown
- Capability negotiation: Client and server agree on supported features
- Session lifecycle: State machine managing connection states and reconnection
Next: Now that we understand the architecture, let's build an MCP server from scratch.