JSON-RPC in MCP
MCP uses JSON-RPC 2.0 under the hood. Learn what that means, how to manually send messages to your server, and what the tools/list and tools/call lifecycle looks like.
Before you connect Claude Desktop, it helps to understand what messages are actually flowing between client and server. Everything in MCP speaks JSON-RPC 2.0 -- a protocol from 2009 that predates most of our current careers.
Once you know the message format, MCP stops being a black box.
What Is JSON-RPC?
JSON-RPC is a way to call functions on a remote system using JSON messages. The key difference from REST:
- REST is resource-oriented -- you manipulate objects (users, posts, products) using HTTP verbs
- RPC is function-oriented -- you tell a server to run a specific function with specific parameters
MCP uses RPC because tools are literally function calls. You're not fetching a "weather resource" -- you're calling a get_weather function with latitude and longitude. RPC matches the mental model perfectly.
The Message Format
Every JSON-RPC message follows this shape:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}| Field | Purpose |
|---|---|
jsonrpc | Always "2.0" -- specifies the protocol version |
id | Matches requests to responses -- you never manage this manually |
method | What you want to do (tools/list, tools/call, initialize) |
params | Arguments for the method |
The Startup Handshake
When Claude Desktop connects to your MCP server, it follows this sequence:
The SDK handles all of this for you. But knowing the sequence helps when debugging -- if tools aren't showing up, it's usually a tools/list response problem.
Testing Directly with echo
You can test your server without any client. The echo command sends input to the server's stdin:
# Ask for the tools list:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
| node mcp.jsInstall jq to pretty-print the output (brew install jq on Mac):
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
| node mcp.js | jq .You'll see something like:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "add",
"title": "Addition Tool",
"description": "Add two numbers together...",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number", "description": "First number" },
"b": { "type": "number", "description": "Second number" }
},
"required": ["a", "b"]
}
}
]
}
}This is exactly what Claude Desktop receives at startup. The LLM sees this entire object as context.
Calling a Tool
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}' \
| node mcp.js | jq .Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{ "type": "text", "text": "5" }
]
}
}Through all that machinery, we discovered that 2 + 3 = 5.
echo | node mcp.js and see the raw response is invaluable. You'll know immediately whether the problem is in your server or in the client.What Does the Client Actually Send to the LLM?
The client takes the tools/list response and feeds it as context to the LLM. Think of it like this:
System context given to the LLM:
"You have the following tools available:
Tool: add
Description: Add two numbers together. Use when the user wants to sum two numeric values.
Parameters:
- a (number): First number
- b (number): Second number
If the user asks to add numbers, call this tool."The LLM reads this, understands what it can do, and routes accordingly. The more clearly you write your description and parameter docs, the better the LLM routes.
Lab -- Simulate the Full Message Flow
Lab 2 -- Spot the Invalid Messages
id field. Message 2 uses jsonrpc: "1.0" instead of "2.0". Message 3 is missing params.name for a tools/call request. Message 4 is valid.Key Takeaways
- MCP speaks JSON-RPC 2.0 -- function calls, not REST resources
- Every message has:
jsonrpc: "2.0",id,method,params - The handshake: initialize → tools/list → tools/call
- Test without a client:
echo '...' | node mcp.js | jq . - The tools list response goes directly to the LLM as context -- descriptions matter enormously
- The SDK handles all routing -- you only write tool handlers
What's Next
Connect the server to Claude Desktop and watch an actual LLM call your tool in response to a natural language prompt.
Practice what you just read.