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.

April 1, 20268 min read2 / 7

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:

Diagram
  • 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.

ℹ️ JSON-RPC 2.0 is ancient tech doing new things. Released in 2009, it's been used for DevOps tooling, infrastructure automation, and language server protocols (like VS Code's LSP) for years. MCP is just the latest use case.

The Message Format

Every JSON-RPC message follows this shape:

JSON
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }
FieldPurpose
jsonrpcAlways "2.0" -- specifies the protocol version
idMatches requests to responses -- you never manage this manually
methodWhat you want to do (tools/list, tools/call, initialize)
paramsArguments for the method

The Startup Handshake

When Claude Desktop connects to your MCP server, it follows this sequence:

Diagram

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:

Bash
# Ask for the tools list: echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \ | node mcp.js

Install jq to pretty-print the output (brew install jq on Mac):

Bash
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \ | node mcp.js | jq .

You'll see something like:

JSON
{ "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

Bash
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3}}}' \ | node mcp.js | jq .

Response:

JSON
{ "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "5" } ] } }

Through all that machinery, we discovered that 2 + 3 = 5.

✅ Why this exercise matters. When something breaks and Claude isn't calling your tool correctly, being able to 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:

Plain text
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

JavaScript · Live Editor
Loading editor...

Lab 2 -- Spot the Invalid Messages

JavaScript · Live Editor
Loading editor...
ℹ️ Answers: Message 1 is missing the 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.

Explore the JSON-RPC Protocol
1 exercise