You know the architecture — hosts, clients, servers. But how do they actually talk to each other? In this module, you will learn the wire format (JSON-RPC 2.0) and the four building blocks that every MCP server is made from: Tools, Resources, Prompts, and Sampling.
JSON-RPC 2.0 Foundation
MCP uses JSON-RPC 2.0 as its message format. If you have never heard of it, here is the short version: it is a lightweight remote procedure call protocol encoded in JSON. You send a JSON object that says “call this method with these parameters,” and you get back a JSON object with the result.
Why JSON-RPC instead of something custom?
- Simplicity. The entire spec fits on one page. There are only three message types.
- Language-agnostic. Every language can parse JSON. There is no code generation step, no schema compilation, no binary serialization.
- Battle-tested. JSON-RPC has been used in Bitcoin, Ethereum, Language Server Protocol (LSP), and dozens of other protocols. It works.
Fun fact: MCP and LSP (the protocol that powers autocomplete in VS Code) both use JSON-RPC 2.0. If you have worked with LSP, the message patterns will feel very familiar.
The Three Message Types
Every message in MCP is one of exactly three types. No exceptions. Learn these and you can read any MCP conversation.
1. Requests (need a response)
A request says: “Do something and tell me the result.” It always has an id field, because the sender needs to match the response to the request.
// A request from the client to call a tool
{
"jsonrpc": "2.0", // Always "2.0" — this is the protocol version
"id": 42, // Unique ID — the response will reference this
"method": "tools/call", // What operation to perform
"params": { // Arguments for the operation
"name": "read_file",
"arguments": {
"path": "/projects/readme.md"
}
}
}Every request has four fields:
jsonrpc— Always the string"2.0". Non-negotiable.id— A number or string that uniquely identifies this request. The response will include the sameid.method— The name of the operation. MCP defines specific method names liketools/call,tools/list,resources/read, etc.params— An object containing the arguments for the method. Optional for some methods, required for others.
2. Responses (answer to a request)
A response is always a direct reply to a specific request. It carries the same id as the request it answers.
// Success response
{
"jsonrpc": "2.0",
"id": 42, // Matches the request's id
"result": { // The operation's return value
"content": [
{
"type": "text",
"text": "# My Project\n\nThis is the readme content..."
}
]
}
}
// Error response
{
"jsonrpc": "2.0",
"id": 42, // Still matches the request
"error": { // "error" instead of "result"
"code": -32602,
"message": "File not found: /projects/readme.md"
}
}A response has either a result field or an error field. Never both. Never neither. This is a strict rule of JSON-RPC 2.0.
3. Notifications (fire and forget)
A notification is a message that does not expect a response. The key difference from a request: it has no id field.
// Client notifies server that initialization is complete
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
// No "id" field — this is a notification, not a request
// No response expected or allowed
}
// Server notifies client that a resource changed
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": {
"uri": "file:///projects/readme.md"
}
}When to use notifications:
- Progress updates — “I am 50% done processing.”
- State changes — “The tool list changed, re-fetch it.”
- Handshake completion — The
notifications/initializedmessage signals that the client is ready to work.
Key Takeaway: The presence or absence of an
idfield is the only thing that distinguishes a request from a notification. If it has anid, the receiver must respond. If it does not, the receiver must not respond.
Error Response Format
When something goes wrong, the server returns an error response. The error object has three fields:
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32602, // Numeric error code
"message": "Invalid params: path is required", // Human-readable
"data": { // Optional extra info
"field": "path",
"reason": "missing_required_field"
}
}
}MCP uses standard JSON-RPC error codes plus its own:
Standard JSON-RPC Error Codes:
-32700 Parse error — Invalid JSON
-32600 Invalid request — JSON is valid but not a proper request
-32601 Method not found — The method does not exist
-32602 Invalid params — Method exists but params are wrong
-32603 Internal error — Something broke inside the server
MCP-Specific Error Codes:
-32001 Request cancelled — The client cancelled the request
-32002 Content too large — Response exceeds size limitsWhen you build servers, you will use these codes to give clients actionable information about what went wrong.
Primitive 1: Tools
Tools are actions — things the AI can do. Every tool has three parts: a name, a description (so the AI knows when to use it), and an input schema (so the AI knows what arguments to pass).
// How a server declares a tool (tools/list response)
{
"name": "search_users",
"description": "Search for users by name or email. Returns matching user profiles.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search term — matches against name and email"
},
"limit": {
"type": "number",
"description": "Maximum results to return (default: 10)",
"default": 10
}
},
"required": ["query"]
}
}The description matters enormously. The AI model reads it to decide whether this tool is appropriate for the user's request. A vague description like “search stuff” will lead to misuse. A precise description leads to accurate tool selection.
When the AI calls a tool, the server executes it and returns content:
// Tool result (tools/call response)
{
"content": [
{
"type": "text",
"text": "Found 3 users matching 'john':\n1. John Smith ([email protected])\n2. ..."
}
],
"isError": false // Set to true if the tool execution itself failed
}Primitive 2: Resources
Resources are data — things the AI can read. Unlike tools, resources have no side effects. Reading a resource does not change anything; it just provides context.
Every resource has a URI (Uniform Resource Identifier) and a MIME type:
// Resource declaration (resources/list response)
{
"uri": "postgres://localhost/mydb/schema",
"name": "Database Schema",
"description": "Current table definitions and relationships",
"mimeType": "application/json"
}
// Resource content (resources/read response)
{
"contents": [
{
"uri": "postgres://localhost/mydb/schema",
"mimeType": "application/json",
"text": "{ \"tables\": [{ \"name\": \"users\", \"columns\": [...] }] }"
}
]
}When to use a resource instead of a tool:
- The data is read-only context that the AI should have, not an action it should take.
- Examples: file contents, configuration, database schemas, API documentation, log output.
- Resources can be subscribed to — the client gets notified when the data changes.
Primitive 3: Prompts
Prompts are reusable templates. They let the server define structured interaction patterns that the host can present to the user.
// Prompt declaration (prompts/list response)
{
"name": "review-code",
"description": "Perform a thorough code review with security and performance analysis",
"arguments": [
{
"name": "code",
"description": "The code to review",
"required": true
},
{
"name": "language",
"description": "Programming language (for context-specific feedback)",
"required": false
}
]
}
// Prompt execution (prompts/get response)
{
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Review this TypeScript code for bugs, security issues, and performance...\n\nfunction processInput(data) { ... }"
}
}
]
}Prompts are different from tools in a key way: tools are model-controlled (the AI decides to call them), while prompts are user-controlled (the user selects them from a menu, like slash commands). The host presents available prompts as UI elements the user can trigger.
Primitive 4: Sampling
Sampling flips the direction. Instead of the AI calling a tool, the server asks the AI to generate text. The server sends a sampling/createMessage request to the client, which forwards it to the AI model.
// Server asks the AI model to generate text
{
"jsonrpc": "2.0",
"id": 99,
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Classify this log entry as INFO, WARNING, or ERROR: 'Connection timeout after 30s'"
}
}
],
"maxTokens": 50
}
}This is a powerful but advanced feature. It lets servers embed AI reasoning into their logic. For example, a code analysis server could use sampling to have the AI classify the severity of each finding. We will explore this in depth later in the course.
Key Takeaway: The four primitives map to four interaction patterns. Tools = AI executes an action. Resources = AI reads data. Prompts = user triggers a template. Sampling = server asks AI to think. Most servers start with just Tools.
A Complete Request/Response Cycle
Let us trace every message in a tool call, from the moment the user types to the moment they see a response:
User types: "What files are in /projects?"
1. Host sends user message to AI model
2. AI model decides to call "list_directory" tool
3. Host sends to Client, Client sends to Server:
─── REQUEST ────────────────────────────────────>
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "list_directory",
"arguments": { "path": "/projects" }
}
}
4. Server executes: reads the directory listing
5. Server sends back to Client, Client forwards to Host:
<── RESPONSE ────────────────────────────────────
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "readme.md\nsrc/\npackage.json\ntsconfig.json"
}
]
}
}
6. Host feeds result to AI model
7. AI model formulates: "The /projects directory contains:
readme.md, a src/ folder, package.json, and tsconfig.json."
8. Host displays this to the user.Eight steps, but only two MCP messages (one request, one response). The rest is host-internal logic. This is why MCP is a thin protocol — it handles the communication contract and stays out of the way.
Try It Yourself: Write JSON-RPC Messages
Get a text editor or a scratchpad open. Write the raw JSON for each scenario. Do not copy from above — write from memory. The act of constructing these messages by hand will cement the format in your mind.
- Write a request to list all available tools. (Hint: the method is
tools/listand it takes no params.) - Write a response to that request that declares one tool called
get_weatherwith a requiredcityparameter. - Write a notification that tells the client the tool list has changed. (Hint: the method is
notifications/tools/list_changed.) - Write an error response for an invalid tool name. Use error code
-32602(invalid params).
Check Your Answers
// 1. Request to list tools
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}
// 2. Response with one tool
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "get_weather",
"description": "Get current weather for a city",
"inputSchema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name"
}
},
"required": ["city"]
}
}
]
}
}
// 3. Notification (no id!)
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}
// 4. Error response
{
"jsonrpc": "2.0",
"id": 5,
"error": {
"code": -32602,
"message": "Unknown tool: nonexistent_tool"
}
}Common Mistakes
- Adding an
idto notifications. If you include anid, it becomes a request and the receiver will try to send a response. Notifications never have anid. - Including both
resultanderrorin a response. A response has one or the other, never both. - Using Tools when Resources would be better. If the AI just needs to read data (no side effects), use a Resource. Tools are for actions. This distinction matters for security: hosts may auto-approve resource reads but require user confirmation for tool calls.
- Writing vague tool descriptions. The AI model relies on descriptions to select the right tool. “Do stuff” is useless. “Search for users by name or email in the company directory. Returns up to 10 matching profiles with name, email, and department.” is actionable.
- Forgetting that Prompts are user-controlled. Do not expect the AI to discover and select prompts autonomously. Prompts are surfaced to the user as menu items or slash commands.
You now understand both the wire format (JSON-RPC 2.0) and the four building blocks (Tools, Resources, Prompts, Sampling) of every MCP server. In the next module, you will set up your development environment and get ready to write actual code.