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

The complete MCP protocol lifecycle: initialize capability negotiation, tools/list discovery, tools/call invocation, resources/read data access, prompts/get template rendering. Every step explained with real JSON-RPC messages captured from an actual running server.

·6 min read·AI Engineering

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

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

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

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"}
  }
}

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"}
  }
}

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"}

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": {}}

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"]
        }
      }
    ]
  }
}

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}}

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}}

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"
    }]
  }
}

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\",...}"
    }]
  }
}

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}
      ]
    }]
  }
}

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."
      }
    }]
  }
}

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"}

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

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