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.
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.
The Concept
Three server primitives
- Tools. Callable actions, run through the same four-step loop from Phase 13 · 01.
- Resources. Exposed data. Read-only content addressable by URI:
file:///path,db://query/..., custom schemes. - 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
- Roots. The set of URIs the server is allowed to touch. The client declares them and the server respects them.
- 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.
- 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}. Noid, 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/callresources/list,resources/read,resources/subscribeprompts/list,prompts/getsampling/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:
{
"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:
{
"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.
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:
initializedeclares capabilities in both directions, and the response carriesserverInfoandprotocolVersion: "2025-11-25".tools/listreturns atoolsarray, and each entry hasname,description,inputSchema.tools/calltakesparams.nameandparams.arguments.- The response
contentis an array of{type, text}blocks.
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.
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
-
Run
code/main.py. Identify the line where capability negotiation happens and describe what would change if the server did not declaretools.listChanged. -
Extend the parser to handle
notifications/progress. The message shape:{method: "notifications/progress", params: {progressToken, progress, total}}. Emit it while a long-runningtools/callis in progress and confirm the client handler would display a progress bar. -
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.
-
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.
-
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
| Term | What people say | What 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
- Model Context Protocol: Specification 2025-11-25: the canonical spec document
- Model Context Protocol: Architecture concepts: the six-primitive mental model
- Anthropic: Introducing the Model Context Protocol: November 2024 launch post
- MCP blog: First MCP anniversary: one-year retrospective and the 2025-11-25 spec changes
- WorkOS: MCP 2025-11-25 spec update: summary of SEP-1686, 1036, 1577, 835, and 1724
Code
- main.py (python, 165 lines)