TAI BUI
Back to curriculum
LearnPython

MCP Fundamentals — Primitives, Lifecycle, JSON-RPC Base

Before MCP, every integration was a one-off. The Model Context Protocol, first shipped by Anthropic in November 2024 and now stewarded by the Linux Foundation's Agentic AI Foundation, standardizes discovery and invocation so any client can talk to any server. The 2025-11-25 spec names six primitives (three on the server, three on the client), a three-phase lifecycle, and a JSON-RPC 2.0 wire format. Learn those and the rest of the MCP chapter is mostly reading.

TIME: ~45 minutesPREREQUISITES: Phase 13 · 01 through 05 (the tool interface and function calling)

Learning Objectives

  • Name all six MCP primitives (tools, resources, prompts on the server; roots, sampling, elicitation on the client) and give one use case each.
  • Walk through the three-phase lifecycle (initialize, operation, shutdown) and state who sends which message at each phase.
  • Parse and emit JSON-RPC 2.0 request, response, and notification envelopes.
  • Explain what capability negotiation at `initialize` is and what breaks without it.

The Problem

Before MCP, every tool-using agent had its own protocol. Cursor had an MCP-shaped but incompatible tool system, Claude Desktop shipped a different one, and VS Code's Copilot extension had a third. A team that built a "Postgres query" tool wrote it three times, once per host API, and reusing it meant copying code.

That left a pile of one-off integrations and a ceiling on how fast the ecosystem could grow.

MCP fixes this by standardizing the wire format. A single MCP server works in any MCP client: Claude Desktop, ChatGPT, Cursor, VS Code, Gemini, Goose, Zed, Windsurf, and 300+ others by April 2026. SDK downloads hit 110M a month and there are 10,000+ public servers. The Linux Foundation took stewardship in December 2025 under the new Agentic AI Foundation.

This phase uses spec revision 2025-11-25. It adds async Tasks (SEP-1686), URL-mode elicitation (SEP-1036), sampling with tools (SEP-1577), incremental scope consent (SEP-835), and OAuth 2.1 resource-indicator semantics. Phase 13 · 09 through 16 cover those extensions. This lesson stops at the base.

Warm-up0/3
1. In the four-step tool-call loop from Phase 13 · 01, which component actually runs the tool?Warm-up
The four-step tool-call loop as four boxes in a row: describe, decide, execute, observe. Above them the user asks for the weather in Oslo and the model's call get_weather(Oslo) appears in box two. In box three the runner of the tool is shown as a question mark.
Illustrative: tool name and city are made up; step names are from Phase 13 · 01.
2. Which tool description gives the model the best chance of picking the right tool?Warm-up
A user message 'Find my notes that mention jwt.' goes to a model. The model reads a tool menu with one entry, notes_search, whose description is an empty dashed box with a question mark.
Illustrative scene; notes_search is the tool defined in main.py.
3. A model emits three tool calls in one turn. How does the host pair each result with its call?Warm-up
Three tool calls from one model turn (get_weather for Oslo, get_weather for Rome, get_time for Tokyo) in a column on the left, a host box in the middle, and three loose result chips (21:40, sunny, rainy) on the right. No links between calls and results are drawn.
Illustrative: calls and result values are made up.
Check: The Problem0/3
1. What is a SEP in the MCP ecosystem?Define
A list of four items from the lesson's Problem section, each written as a SEP number and a name: SEP-1686 async Tasks, SEP-1036 URL-mode elicitation, SEP-1577 sampling with tools, SEP-835 incremental scope consent. A dashed box on the right reads 'SEP = ?'.
Items copied from the lesson's Problem section.
2. What is MCP?Define
A client box named learner-client and a server box named notes-server, each at version 1.0.0, joined by a line labelled MCP, spec 2025-11-25.
Names and versions come from main.py's pretend client and server.
3. What problem was MCP built to solve?Explain
One 'Postgres query' tool box on the left and three host boxes on the right (Cursor, Claude Desktop, VS Code Copilot), joined by dashed lines with a question mark between them. A heading says a team wants one Postgres query tool in three hosts.
Before MCP. Hosts are the ones named in the lesson's Problem section.

The Concept

Three server primitives

  1. Tools. Callable actions, run through the same four-step loop from Phase 13 · 01.
  2. Resources. Exposed data. Read-only content addressable by URI: file:///path, db://query/..., custom schemes.
  3. Prompts. Reusable templates. They show up as slash-commands in the host UI: the server supplies the template and the client fills in the arguments.

Three client primitives

  1. Roots. The set of URIs the server is allowed to touch. The client declares them and the server respects them.
  2. Sampling. The server asks the client's model to perform a completion. This lets a server host an agent loop without holding any API keys of its own.
  3. Elicitation. The server asks the client's user for structured input mid-flight, through a form or a URL (SEP-1036).

Each capability in MCP belongs to one of these six. Phase 13 · 10 through 14 cover them in depth.

Wire format: JSON-RPC 2.0

Each message is a JSON object, and its shape tells you which kind it is:

  • Requests: {jsonrpc: "2.0", id, method, params}.
  • Responses: {jsonrpc: "2.0", id, result | error}.
  • Notifications: {jsonrpc: "2.0", method, params}. No id, and no response expected.

The base spec has about 15 methods, grouped by primitive. These are the ones to know:

  • initialize / initialized (handshake)
  • tools/list, tools/call
  • resources/list, resources/read, resources/subscribe
  • prompts/list, prompts/get
  • sampling/createMessage (server-to-client)
  • notifications/tools/list_changed, notifications/resources/updated, notifications/progress

Three-phase lifecycle

Phase 1: initialize.

The client sends initialize with its capabilities and clientInfo. The server responds with its own capabilities, serverInfo, and the spec version it speaks. Once the client has processed that response, it sends notifications/initialized. After that, either side can send requests within the negotiated capabilities.

Phase 2: operation.

Traffic flows both ways. The client calls tools/list to discover tools, then tools/call to invoke one. The server can send sampling/createMessage if the client declared that capability, and notifications/tools/list_changed when its tool set changes. The client can send notifications/roots/list_changed when the user changes root scope.

Phase 3: shutdown.

Either side closes the transport. MCP has no structured shutdown method, so the transport (stdio or Streamable HTTP, Phase 13 · 09) carries the end-of-connection signal.

Capability negotiation

The capabilities field in the initialize handshake is the contract. Here is one from a server:

json
{
  "tools": {"listChanged": true},
  "resources": {"subscribe": true, "listChanged": true},
  "prompts": {"listChanged": true}
}

This server declares that it can emit tools/list_changed notifications and supports resources/subscribe. The client answers with its own declaration:

json
{
  "roots": {"listChanged": true},
  "sampling": {},
  "elicitation": {}
}

If the client does not declare sampling, the server must not call sampling/createMessage. The rule runs the other way too: if the server does not declare resources.subscribe, the client must not try to subscribe.

This is what keeps the ecosystem from drifting apart. A client without sampling is still a valid MCP client, and a server that never calls sampling is still a valid MCP server. They just don't use that feature together.

Structured content and error shapes

tools/call returns a content array of typed blocks: text, image, resource. Phase 13 · 14 adds MCP Apps (ui:// interactive UI) to that list.

Errors use JSON-RPC error codes. The spec-defined additions here are -32002 "Resource not found" and -32603 "Internal error", plus MCP-specific error data in error.data.

Client capabilities vs tool call details

A common confusion: capabilities.tools says whether the client supports tool-list-changed notifications. Whether the client will call a specific tool is a runtime choice made by its model, and no capability flag covers it. The flag is the spec-level contract; the model's choice is a separate matter.

Why JSON-RPC and not REST?

JSON-RPC 2.0 (2010) is a lightweight bidirectional protocol, while REST is client-initiated. MCP needed server-initiated messages (sampling, notifications), and JSON-RPC's symmetric request/response shape fits that. It also runs cleanly over stdio and WebSocket/Streamable HTTP without reinventing HTTP's request shape.

Check: The Concept0/22
1. How many primitives does the MCP 2025-11-25 spec define, and how do they split?Define
A client box and a server box joined by a line. On the line a dashed box reads 'primitives = ?'. Text below says the scene is the MCP specification 2025-11-25 and asks how many primitives there are and how they are divided.
2. Which JSON-RPC 2.0 message type carries no id and expects no response?Define
Six field-name chips that can appear in a JSON-RPC 2.0 message: jsonrpc, id, method, params, result, error. A sentence asks which of the three message types leaves out id and expects nothing back.
3. Which of these is one of the base JSON-RPC methods MCP defines?Define
Part of the lesson's list of base MCP methods: initialize, tools/list, tools/call, prompts/list, prompts/get, sampling/createMessage. A dashed box with question marks stands for the hidden rest.
A partial list from the lesson; the rest is hidden.
4. What does the term server primitive refer to in MCP?Define
A client box learner-client joined by a line to a server box notes-server, which carries the label 'server primitive: ?'.
Names are from main.py's pretend client and server.
5. What does the term client primitive refer to in MCP?Define
A client box learner-client joined by a line to a server box notes-server. The client box carries the label 'client primitive: ?'.
Names are from main.py's pretend client and server.
6. What is the initialize handshake?Define
A client-to-server arrow carrying the initialize request with id 1, shown as a JSON envelope with its params collapsed to '...'. Below it a dashed box asks what the response to id 1 is.
Envelope from main.py with params left out.
7. What does a tools/list request do?Define
A client-to-server arrow carrying the one-line request {jsonrpc 2.0, id 2, method tools/list}. Below it a dashed box asks what the response to id 2 contains.
Request envelope from main.py, laid out on one line.
8. What does a tools/call request do?Define
A client-to-server arrow carrying the tools/call request with id 3: name notes_search, arguments query JSON-RPC and limit 5. Below it a dashed box asks what the response to id 3 is.
Request envelope from main.py, laid out on one line.
9. What does a notifications/tools/list_changed message tell the client?Define
A server-to-client arrow carrying a message named notifications/tools/list_changed. The client box says it holds a tool list.
10. What is a content block in a tools/call result?Define
A made-up tools/call result envelope with id 9 whose content array holds a text entry and an image entry, followed by an isError false field.
Made-up example, typed, not captured.
11. Why can a server run an agent loop through sampling without holding any API keys?Explain
A notes-server box that needs text completions for an agent loop and holds no API keys, a learner-client box, and a dashed line with a question mark between them.
Illustrative scene.
12. What do roots do in MCP, and who sets them?Explain
A client box and a server box joined by a plain line, and below them three locations on the user's machine: file:///work/notes/, file:///work/photos/, file:///home/other/.
Illustrative URIs.
13. A server does not declare resources.subscribe in its capabilities. What must the client do?Explain
A server capabilities JSON with tools and prompts collapsed to '...' and resources holding only listChanged, with no subscribe entry. Below it a client that wants live updates for a resource and a server joined by a plain line.
Typed variant of main.py's server capabilities with resources.subscribe removed; other values collapsed.
14. Why does MCP use JSON-RPC 2.0 instead of REST?Explain
A JSON-RPC 2.0 envelope from main.py (id 2, method tools/list), and below it a client and a server joined by a line labelled stdio or Streamable HTTP.
Transports are the ones the lesson names for MCP.
15. A client declares no sampling capability at initialize. What must the server do?Explain
A client capabilities JSON with roots.listChanged and elicitation but no sampling entry. Below it a server and a client box joined by a plain line.
Typed variant of main.py's client capabilities with sampling removed.
16. What happens in MCP's Phase 3, shutdown?Explain
Three boxes in a row for the lifecycle: Phase 1 initialize (the handshake), Phase 2 operation (the working session), Phase 3 shutdown (how does it end?).
Phase names are from the lesson's Three-phase lifecycle subsection.
17. What does JSON-RPC error code -32002 mean in MCP, and where does MCP-specific detail go?Explain
An error response envelope sent to a client with id 5 and error code -32002 whose message is hidden by question marks.
Envelope shape from the lesson; id 5 is made up.
18. What does the capabilities.tools flag actually tell you?Explain
Server capabilities from main.py with the tools, resources and prompts entries each collapsed to '...'.
Key names from main.py's server capabilities; every value collapsed.
19. A server adds a new tool while connected, but the client keeps showing the old tool list. Which message was missing?Analyze
A server box listing its tools as notes_search plus a new tool, and a client box that still shows only notes_search. A plain line joins them with no message drawn.
Illustrative: the question does not name the new tool.
20. A transcript shows the client's initialize declaring only roots. Later the server sends sampling/createMessage. What is the correct reading?Analyze
A sequence diagram: the client sends initialize declaring only roots with listChanged, the server answers with its capabilities, and later the server sends sampling/createMessage to the client.
Typed transcript matching the question; main.py's own client also declares sampling.
21. Your server must summarize long query results with an LLM, but you will not ship or store an API key. Which design fits?Design
A notes-server box that has long query results, wants a summary and has no API key, and a client box whose declared capabilities are not shown, joined by a plain line.
Illustrative scene built from the question.
22. A tool needs the user to pick one of three deployment targets in a form halfway through a call. What fits best?Design
A client and a server lifeline. The client sends tools/call, then a dashed line labelled mid-call, needs the user's choice. Beside it a form titled Deployment target with three radio choices, target 1, target 2, target 3.
Placeholders: the question names no targets and no tool.

Use It

code/main.py ships a minimal JSON-RPC 2.0 parser and emitter, then walks the initialize → tools/list → tools/call → shutdown sequence by hand and prints every message. There is no real transport, only the message shapes. Compare them to the spec linked in Further Reading to verify each envelope.

What to look at:

  • initialize declares capabilities in both directions, and the response carries serverInfo and protocolVersion: "2025-11-25".
  • tools/list returns a tools array, and each entry has name, description, inputSchema.
  • tools/call takes params.name and params.arguments.
  • The response content is an array of {type, text} blocks.
Check: Use It0/6
1. What does code/main.py demonstrate, and what does it leave out?Define
The top of the terminal output of python code/main.py: a title line 'PHASE 13 LESSON 06 - MCP LIFECYCLE WALK', the heading '--- PHASE 1: initialize ---' and the first lines of the client request.
First lines of the real run, condensed: rule lines shortened, envelope on one line, the $ line typed.
2. main.py builds notification('notifications/initialized') and passes it to Message.kind. What does kind return?Apply
The Message.kind property from main.py with its four checks, and below it the call notification('notifications/initialized') with the dict it builds (jsonrpc 2.0, method notifications/initialized). The result of Message.kind is shown as a question mark.
Code copied from main.py.
3. In the walk in main.py, what does the client send right after receiving the server's response to initialize?Apply
A sequence diagram between client and server: the client sends initialize (request, id 1), the server answers with a response for id 1, then a dashed arrow with a question mark from client to server marks the client's next message.
Sequence of main.py's walk, stopped after the response to initialize.
4. With the notes_search inputSchema in main.py (query required string, limit integer from 1 to 50), which arguments are valid?Apply
The notes_search inputSchema from main.py: query is a required string, limit is an integer with minimum 1 and maximum 50. Below it a dashed box asks which tools/call arguments are valid.
Schema copied from main.py.
5. What does the server's response to the tools/call with id 3 in main.py contain?Apply
A client-to-server arrow carrying the tools/call request with id 3 (notes_search, query JSON-RPC, limit 5). Below it a dashed box asks what the result of id 3 contains.
Request envelope copied from main.py.
6. main.py answers a call to the unknown tool notes_delete with code -32601 'Method not found'. How should a reader treat that example?Analyze
The tools/call request with id 4 for notes_delete from main.py, and the response with id 4 carrying error code -32601, message 'Method not found' and data tool notes_delete.
Condensed layout of main.py's real output; values unchanged.

Ship It

This lesson produces outputs/skill-mcp-handshake-tracer.md. Given a pcap-style transcript of an MCP client-server interaction, the skill annotates each message with which primitive, which lifecycle phase, and which capability it depends on.

Check: Ship It0/1
1. Given a pcap-style transcript, what does the mcp-handshake-tracer skill do?Define
A pcap-style transcript excerpt of three messages (initialize id 1, result id 1, tools/list id 2) feeding a box named mcp-handshake-tracer skill, whose output is a dashed box with a question mark.
Typed excerpt in main.py's message order.
Author's Note

I'd run main.py with the spec open next to it and check each envelope field by field. The script prints every message with an arrow and a kind (request, response, notification), and the kind is decided by one rule in Message.kind: a message with a method and no id is a notification. That rule separates notifications from the other two envelope types from the Concept section. The checks right after it (a method with an id is a request, a result or error is a response) cover the rest, so it's worth seeing in code once.

Be clear about what the script does and doesn't do. It declares client and server capabilities in two dicts and sends them in initialize, but nothing in it checks a call against those dicts. It would happily print a sampling/createMessage request even if the client never declared sampling. The contract from the Concept section is something you'd have to enforce yourself, and exercise 1 is a good place to try.

Two smaller things. The error example answers an unknown tool with -32601 "Method not found", which isn't one of the two MCP codes listed above, so check it against the spec before copying it. And shutdown is just two print calls with no message at all, because there is no JSON-RPC method for it.

Exercises

  1. Run code/main.py. Identify the line where capability negotiation happens and describe what would change if the server did not declare tools.listChanged.

  2. Extend the parser to handle notifications/progress. The message shape: {method: "notifications/progress", params: {progressToken, progress, total}}. Emit it while a long-running tools/call is in progress and confirm the client handler would display a progress bar.

  3. Read the MCP 2025-11-25 spec top to bottom (the whole document is about 80 pages). Identify the one capability flag most servers do NOT need. Hint: it relates to resource subscription.

  4. Sketch on paper the primitive a hypothetical "cron job" feature would belong to. (Hint: the server wants the client to invoke it at a scheduled time. None of the six primitives fit today.) MCP's 2026 roadmap has a draft SEP for this.

  5. Parse one session log from an open MCP server on GitHub. Count request vs response vs notification messages. Compute what fraction of traffic is lifecycle vs operation.

Key Terms

TermWhat people sayWhat it actually means
MCP"Model Context Protocol"Open protocol for model-to-tool discovery and invocation
Server primitive"What a server exposes"tools (actions), resources (data), prompts (templates)
Client primitive"What a client lets servers use"roots (scope), sampling (LLM callbacks), elicitation (user input)
JSON-RPC 2.0"The wire format"Symmetric request/response/notification envelopes
initialize handshake"Capability negotiation"First message pair; servers and clients declare features they support
tools/list"Discovery"Client asks server for its current tool set
tools/call"Invocation"Client asks server to execute a tool with arguments
notifications/*_changed"Mutation events"Server tells client that its primitive list has changed
Content block"Typed result"{type: "text" | "image" | "resource" | "ui_resource"} in tool result
SEP"Spec Evolution Proposal"Named draft proposal (e.g. SEP-1686 for async Tasks)

Further Reading

Code

Ships With