DEV Community

Cover image for MCP Series (02): Protocol Deep Dive — Host/Client/Server Model and JSON-RPC Communication
WonderLab
WonderLab

Posted on

MCP Series (02): Protocol Deep Dive — Host/Client/Server Model and JSON-RPC Communication

Three-Layer Model

MCP separates responsibilities across three distinct roles:

Host
  → The application running LLM inference: Claude Desktop, Claude Code, custom Agent
  → Manages connections to one or more MCP Servers
  → Decides which tools/resources to expose to the LLM

Client (embedded in Host)
  → Maintains a 1:1 connection with a single MCP Server
  → Sends and receives JSON-RPC 2.0 messages
  → Tracks session state (negotiated capabilities, discovered tool list)

Server (independent process)
  → Exposes three capability types: Tools / Resources / Prompts
  → Communicates with Clients via stdio or HTTP
  → One Server can be connected by multiple Hosts simultaneously
Enter fullscreen mode Exit fullscreen mode

Host is what the user sees. Client is the protocol adapter inside the Host. Server is where the tools live.


Three Capability Types

An MCP Server can expose three kinds of capabilities:

Tools      → Actions the LLM can invoke
             Examples: search Jira, run SQL, send email
             The LLM decides when to call them during reasoning

Resources  → Data sources the LLM can read
             Examples: current sprint status, repository file tree
             The Host decides when to inject them into context

Prompts    → Reusable prompt templates
             Examples: bug analysis report, code review template
             The user or Host invokes them directly, filling in arguments
Enter fullscreen mode Exit fullscreen mode

Transport Options

Client and Server communicate through three transport mechanisms:

stdio (standard I/O)
  → Server runs as a subprocess; messages flow through stdin/stdout
  → Simplest; preferred for local development
  → Default transport for Claude Code MCP integrations

HTTP + SSE (Server-Sent Events)
  → Server runs as an HTTP service
  → Client → Server: HTTP POST
  → Server → Client: SSE stream (supports Server-initiated push)
  → Right for remote Servers shared across multiple Clients

Streamable HTTP (newer)
  → HTTP POST with optional SSE stream
  → Supports both synchronous calls and streaming push
  → Recommended for remote deployments since 2025 spec update
Enter fullscreen mode Exit fullscreen mode

This article's demo uses stdio — the simplest way to observe the protocol directly.


Complete Protocol Exchange

Eight rounds of real JSON-RPC messages captured from the demo server, showing a full protocol session from start to finish.

Step 1: initialize (capability negotiation)

The first message of every session. Both sides declare what they support.

Client → Server (request):

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "roots": {"listChanged": true},
      "sampling": {}
    },
    "clientInfo": {"name": "demo-client", "version": "1.0.0"}
  }
}
Enter fullscreen mode Exit fullscreen mode

Server → Client (response):

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "experimental": {},
      "prompts": {"listChanged": false},
      "resources": {"subscribe": false, "listChanged": false},
      "tools": {"listChanged": false}
    },
    "serverInfo": {"name": "mcp-protocol-demo", "version": "1.13.1"}
  }
}
Enter fullscreen mode Exit fullscreen mode

tools.listChanged: false means the Server's tool list is static — the Client doesn't need to subscribe to change notifications.

After initialize completes, the Client sends a notification to confirm readiness:

{"jsonrpc": "2.0", "method": "notifications/initialized"}
Enter fullscreen mode Exit fullscreen mode

No id field — this is a JSON-RPC Notification (fire-and-forget, no response expected).


Steps 2: tools/list (tool discovery)

Request:

{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "echo",
        "description": "Repeats the input message back.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "message": {"type": "string", "description": "The message to echo back"}
          },
          "required": ["message"]
        }
      },
      {
        "name": "add",
        "description": "Adds two numbers and returns the result.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "a": {"type": "number", "description": "First number"},
            "b": {"type": "number", "description": "Second number"}
          },
          "required": ["a", "b"]
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

inputSchema is standard JSON Schema. The Host passes it to the LLM as a tool definition — same structure as Function Calling schemas.


Steps 3-4: tools/call (tool invocation)

echo:

// request
{"jsonrpc":"2.0","id":3,"method":"tools/call",
 "params":{"name":"echo","arguments":{"message":"Hello from MCP client!"}}}

// response
{"jsonrpc":"2.0","id":3,
 "result":{"content":[{"type":"text","text":"Echo: Hello from MCP client!"}],"isError":false}}
Enter fullscreen mode Exit fullscreen mode

add:

// request
{"jsonrpc":"2.0","id":4,"method":"tools/call",
 "params":{"name":"add","arguments":{"a":42,"b":58}}}

// response
{"jsonrpc":"2.0","id":4,
 "result":{"content":[{"type":"text","text":"42 + 58 = 100"}],"isError":false}}
Enter fullscreen mode Exit fullscreen mode

Two fields matter in the response:

  • content: array of content blocks (text / image / resource)
  • isError: boolean. When a tool execution fails, this is true and the content contains the error message. Tool failures are not JSON-RPC errors — they're normal responses so the LLM can read the error and adapt

Steps 5-6: resources/list + resources/read

Discover resources:

{
  "result": {
    "resources": [{
      "name": "Server Information",
      "uri": "info://server-info",
      "description": "Metadata about this MCP server",
      "mimeType": "application/json"
    }]
  }
}
Enter fullscreen mode Exit fullscreen mode

Read a resource:

// request
{"method":"resources/read","params":{"uri":"info://server-info"}}

// response
{
  "result": {
    "contents": [{
      "uri": "info://server-info",
      "mimeType": "text/plain",
      "text": "{\"name\":\"mcp-protocol-demo\",\"version\":\"1.0.0\",...}"
    }]
  }
}
Enter fullscreen mode Exit fullscreen mode

URI schemes are custom — the Server defines its own (info://, file://, jira://, github://). Resources are the data layer: read-only, no side effects.


Steps 7-8: prompts/list + prompts/get

Discover templates:

{
  "result": {
    "prompts": [{
      "name": "summarize",
      "description": "Summarize a piece of text concisely",
      "arguments": [
        {"name": "text", "description": "The text to summarize", "required": true},
        {"name": "max_words", "required": false}
      ]
    }]
  }
}
Enter fullscreen mode Exit fullscreen mode

Render a template (with arguments filled in):

// request
{
  "method": "prompts/get",
  "params": {
    "name": "summarize",
    "arguments": {
      "text": "MCP defines a standard protocol for connecting AI models to tools and data sources.",
      "max_words": "20"
    }
  }
}

// response
{
  "result": {
    "messages": [{
      "role": "user",
      "content": {
        "type": "text",
        "text": "Summarize the following text in at most 20 words:\n\nMCP defines a standard protocol for connecting AI models to tools and data sources."
      }
    }]
  }
}
Enter fullscreen mode Exit fullscreen mode

The Host calls prompts/get, gets back a ready-to-use messages array, and passes it directly to the LLM. For enterprise use: maintain shared analysis templates on the Server; every Agent uses the same canonical version.


JSON-RPC 2.0 Message Types

MCP uses standard JSON-RPC 2.0. Three message types:

Request  has id, expects response
  {"jsonrpc":"2.0", "id": N, "method": "...", "params": {...}}

Response  has id, matches a request
  {"jsonrpc":"2.0", "id": N, "result": {...}}    success
  {"jsonrpc":"2.0", "id": N, "error": {...}}     transport-level error

Notification  no id, fire-and-forget
  {"jsonrpc":"2.0", "method": "notifications/initialized"}
Enter fullscreen mode Exit fullscreen mode

Tool execution failures use result.isError: true, not the error response type. The error field is reserved for JSON-RPC transport errors (method not found, parse error). This distinction lets the LLM see and reason about tool failure messages rather than just getting an exception.


Running the Demo

conda activate llm_base
pip install mcp
cd llm-in-action/mcp-02-protocol

# Option A: terminal — prints all 8 JSON-RPC request/response pairs
python demo_protocol_client.py

# Option B: MCP Inspector visual UI (requires Node.js)
npx @modelcontextprotocol/inspector python demo_mcp_server.py
Enter fullscreen mode Exit fullscreen mode

References


Check out PrimeSkills — a curated marketplace of AI agents and skills that have been validated in real-world, enterprise-grade workflows. No fluff, just what actually works.

Find more useful knowledge and interesting products on my Homepage

Top comments (0)