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 istrueand 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
- MCP Protocol Specification
- JSON-RPC 2.0 Specification
- Full demo code: mcp-02-protocol
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)