MCP Has Six Primitives, Not Three. Here's the Half You're Ignoring.

Pop quiz. Name the MCP primitives.
If you've read any MCP introduction in the last eighteen months, you said: Tools, Resources, Prompts. You're right. Those are the things you build, the things you expose, the things every tutorial walks you through.
You are also wrong, in the sense that you are halfway right.
MCP has six primitives. The three you know are server-side: things your server exposes to the client. The three nobody seems to want to talk about are client-side: things the client exposes to your server. They are Roots, Sampling, and Elicitation, and the reason they get ignored is exactly the reason you should care about them.
Why You've Never Heard of the Other Half
The first six months of MCP content focused, sensibly, on the obvious story: you build a server, it exposes tools, the AI uses them. That's the part most developers need first. Most people writing tutorials do not get past it.
But MCP is not actually a one-way protocol. The spec is fully bidirectional. Servers can ask clients for things, not just respond to client requests. The client capabilities are how that conversation works.
Both directions, on one picture:
The arrow from server to client is the half this post is about.
The asymmetry of attention is partly an SDK story. Until quite recently, most Java and Python frameworks made the server-side primitives much easier to use than the client-side ones. If you were building with Spring AI, you were probably writing @Tool-annotated beans (today it is @McpTool) long before you knew your client could hand a server a list of roots at all. The protocol always supported it; the ergonomics didn't.
That has changed in the last year. The 2025-06-18 spec made the client-side primitives more useful by adding Elicitation. The 2025-11-25 spec polished the corners. And the SDKs are catching up. If you only know the three server-side primitives, you're working with half the protocol.
The client-side half arrived in stages:
Until that last box, Roots and Sampling work exactly as this post describes them.
When this post was published, deprecating three of these six primitives was only a draft proposal. It is now ratified. The 2026-07-28 specification, published 28 July 2026, deprecates Roots, Sampling, and Logging:
| Primitive | Status in 2026-07-28 | What to use instead | Earliest removal |
|---|---|---|---|
| Roots | deprecated (SEP-2577) | directories and files passed as tool parameters, resource URIs, or server configuration | first revision released on or after 2027-07-28 |
| Sampling | deprecated (SEP-2577) | direct integration with an LLM provider's own API | first revision released on or after 2027-07-28 |
| Logging | deprecated (SEP-2577) | stderr on stdio, or OpenTelemetry | first revision released on or after 2027-07-28 |
| Elicitation | active | it stays as it is | not scheduled |
Deprecated does not mean removed. The same revision adopted a feature-lifecycle policy guaranteeing a minimum twelve-month window. That policy is SEP-2596, a different SEP from the deprecation itself, and the two are easy to conflate. The three stay fully functional today, but new implementations should not adopt them. Elicitation is not deprecated and remains the most actively recommended client-side primitive.
Three mechanics described below also changed, so read them as 2025-11-25 history rather than current fact:
- Servers can no longer open a request to a client. The spec now requires server-to-client requests such as
roots/list,sampling/createMessageandelicitation/createto use the Multi Round-Trip Requests pattern. So "the spec is fully bidirectional" is still true in spirit (a server can still ask), but only as an interim result to a request the client made first. notifications/roots/list_changedwas removed outright, not merely deprecated alongside Roots (SEP-2575 removedpingandlogging/setLeveltoo). The live-updating boundary described below applies to2025-11-25and earlier.- Capability negotiation moved onto every request. Protocol-level sessions, the
Mcp-Session-Idheader and theinitializehandshake are gone. Client capabilities now ride in_meta, the metadata object attached to every MCP request, under the keyio.modelcontextprotocol/clientCapabilities. A server's versions, capabilities and identity come from a new mandatory RPC,server/discover. Wherever this post says capabilities are declared "at session start", read that as "on every request".
The new shape, for a tool call that needs an answer from the user:
The retry is a separate request with a different id, carrying the answers and the opaque requestState the server sent back, which is what lets the server stay stateless.
Primitive 4: Roots (The Server Asks "What Am I Allowed To See?")
Suppose you write a filesystem MCP server. It can read files. Wonderful. Now: which files?
The server doesn't have arbitrary access to the user's machine. It can't. Imagine the security disaster if it did. Some entity has to decide what's on-limits and what isn't. That entity is the client, because the client is the part that knows what the user has authorised.
Roots are the mechanism for that conversation. When a client supports Roots, it advertises that it can answer the question:
Here are the directories this server should operate on.
Directories and files, specifically: a root's uri must be a file:// URI. A filesystem MCP server can call roots/list on the client to find out: maybe the user has pointed it at ~/Documents/work and nothing else. The server then knows the intended boundary of its world.
In 2025-11-25 and earlier, if the user adds a folder to the workspace, the client emits notifications/roots/list_changed. The server re-queries. The boundary updates live, mid-connection, without anyone reconnecting:
The third message is what makes the boundary live. Under 2026-07-28 that notification is gone.
What this enables in practice:
- Workspace-aware servers. Your IDE plugin (Cursor, Continue, Claude Code) can tell the MCP server which project the user is currently working in. The server scopes everything to that project. Switch projects, the scope changes.
- Multi-tenant clients. A single client can serve multiple users; each user has their own roots; the server doesn't have to manage that mapping.
- A declared scope, though not an enforced one. This is the place to be precise, because it is easy to over-read. The
2026-07-28spec is blunt: roots "are informational guidance rather than an access-control mechanism. The protocol does not enforce that servers stay within roots." Servers only SHOULD respect the boundary. What you gain is that the scope becomes explicit and inspectable (the client declares it, the server has to ask for it) instead of being buried in a config file. But the actual fence still has to be a real one: OS permissions, a container, a sandbox.
If you've ever shipped an MCP server that took a base_directory configuration parameter at startup time, Roots are the more honest version of that pattern. The user, through the client, gets to dynamically tell you what counts as the base.
Primitive 5: Sampling (The Server Asks the AI for Help)
This one bends people's brains, so we'll go slowly.
When you think of an MCP server, you think of something the AI uses. The AI calls a tool; the server runs code; the result goes back to the AI. The intelligence is in the AI; the server is just hands.
But what if the server needs the intelligence too?
Suppose your MCP server fetches a web page and needs to summarise it. Or fetches a code file and needs to extract its dependencies. Or processes a customer support ticket and needs to classify its sentiment. These are tasks that ought to use an LLM. But your MCP server doesn't have direct access to one. It doesn't have an API key. It doesn't know which provider the user is using. It doesn't know if the user wants their requests sent to a third party at all.
Sampling involves four parties:
The user sees the prompt and can approve, edit or refuse it before any model runs.
Sampling is the protocol's answer. The server can call sampling/createMessage on the client to say: "I have a prompt. Please run it through whatever model you're using, with whatever budget you allow, and give me the response." The client owns the model relationship. The client can refuse, log, redact, change models, or budget the request. The server doesn't have to think about any of that. It just gets a completion.
This is what makes truly composable agents possible. Without Sampling, every MCP server has to either:
- Be entirely deterministic (no LLM reasoning inside the server), or
- Bring its own model (with all the configuration, secrets, and surprise-vendor-coupling that implies).
With Sampling, the server can borrow the client's brain. A summarise_url tool that uses Sampling internally to produce a high-quality summary, in whatever model the user has chosen, with the user retaining full control. That's a kind of architectural cleanness you can't get any other way.
The caveat, and it's a real one: Sampling has historically been one of the least-implemented capabilities. Many clients don't support it. And it is now formally on the way out: the 2026-07-28 specification deprecates it, as the table at the top of this post records. New servers should not adopt it, and if you maintain one that already does, check whether your target clients support it.
Primitive 6: Elicitation (The Server Asks the User a Question)
This is the newest of the client-side primitives. It landed in 2025-06-18 and is the one most likely to change how you design tools.
Picture this. Your MCP server has a deploy_service tool. The user (through the AI) asks to deploy payment-service. Your tool is ready to run, but you need one more piece of information: which environment? Staging, production, or canary?
Before Elicitation, you had four bad options:
- Make the AI ask the user in chat. ("Which environment?") The user answers, the AI parses, the AI re-calls your tool. Three round-trips, hope nothing gets lost in translation.
- Add
environmentto your tool's required parameters and hope the AI guessed right. - Pop up an OS-level dialog. Out of band. Awful UX.
- Just pick a default and pray.
Elicitation is option 5. Mid-tool-call, the server can call elicitation/create on the client with a JSON Schema describing the structured input it needs. That schema is a restricted subset of JSON Schema: form mode is limited to a flat object of primitive fields, with nested objects and arrays left out. The client surfaces a form to the user, in the host UI, that exactly matches that schema. The user fills it in. The response comes back to the server as a validated object. The tool continues.
The deploy example, message by message:
The user answered a form, and the model saw only the tool's result.
The user always retains the right to cancel. The schema is what the server asked for, but the client owns the rendering. The conversation in chat is uninterrupted.
One hard limit: a server MUST NOT use form mode to ask for a password, an API key, an access token or a payment credential. Those go through URL mode, added in 2025-11-25. The server sends a URL, the client asks the user to consent before opening it, and the secret goes straight to the server's own page without passing through the client.
What this changes:
- Structured questions stop bouncing through the model. Previously, "ask the user" had to happen via the model, which meant the model had to understand the question and the answer. Now the question can be structured (a date picker, a dropdown, a typed text field) and the model just receives the validated answer.
- Trust boundaries become cleaner. The server explicitly admits when it doesn't have all the information. The user explicitly approves before the operation proceeds. No more "the AI just decided to deploy to production because it interpreted my sentence aggressively."
- The "are you sure?" pattern gets a protocol-level home. High-risk operations can elicit explicit confirmation, with the confirmation being a typed response, not a chat string the model might paraphrase.
This is the primitive I'd predict will reshape tool design over the next year. Once developers internalise that "ask the user a structured question" is a protocol-supported operation, a lot of tools that currently overstuff their parameter schemas will quietly transition to "elicit what's missing."
Why the Asymmetry Matters
The three server-side primitives encode what your server provides. The three client-side primitives encode what your server can ask for. Both are about explicit capabilities, declared at the start of a session.
This explicit declaration is the part that makes MCP feel different from other AI integration approaches. In a normal SDK, you have to assume the worst about what the other side can do. You write defensive code. You handle the cases that never arise. You build for the lowest common denominator.
Under 2026-07-28 the declaration travels in _meta on every request:
{
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {
"elicitation": {}
}
}
}
In MCP, the capability declaration tells you exactly what the other side can do. If the client doesn't advertise sampling, you don't call sampling/createMessage, and you also don't have to write code defending against the case where it might not work. If the client advertises elicitation, you can write your tool assuming you can ask for structured input. The contract is upfront.
That bidirectional capability negotiation is what makes MCP, in my view, a serious protocol. It is the client-side half that shows it most clearly, because that is where a server learns what the other end can do before it asks for anything.
Practical Translation
If you're designing an MCP server today, here are concrete questions to ask:
On Roots:
- Does my server have a notion of "scope"? Could it benefit from the client telling me what scope to use?
- Am I currently accepting a
base_directory(orworkspace, orproject) configuration parameter that should really be dynamic?
On Sampling:
- Are there steps inside my tool implementation that would benefit from an LLM call (classification, summarisation, decision-making)?
- If yes, am I currently solving that by either (a) being deterministic, or (b) bringing my own model? Could Sampling let me offload that decision to the client?
On Elicitation:
- Are there optional parameters in my tools that the model frequently gets wrong, that a structured question to the user would always answer correctly?
- Are there high-risk operations where I'd like explicit user confirmation outside the chat stream?
The first time you ask these questions, you'll probably find at least one tool in your server that would be better if it used one of the client-side primitives. That's how I knew, the first time, that the other half of the protocol was doing real work.
The Bigger Picture
The fact that "MCP has three primitives" became conventional wisdom is a textbook case of protocols being learned through tutorials rather than specs. The tutorial writers wanted to ship the minimum viable mental model. The minimum viable mental model is "tools, resources, prompts." That's good enough to get someone building. It's also wrong by half.
If you're building servers, the three server-side primitives are where you'll spend most of your time. Elicitation is where the next layer of capability sits, and it is the one of these three that survived the deprecation. Patterns that look hard in a server-only world (mid-tool user input, structured confirmation before a risky operation) become clean once you remember the protocol gives a server a way to ask.
All six, with the method each uses and where it stands now:
| Primitive | Side | Method | Status in 2026-07-28 |
|---|---|---|---|
| Tools | server | tools/list, tools/call | active |
| Resources | server | resources/list, resources/read | active |
| Prompts | server | prompts/list, prompts/get | active |
| Roots | client | roots/list | deprecated (SEP-2577) |
| Sampling | client | sampling/createMessage | deprecated (SEP-2577) |
| Elicitation | client | elicitation/create | active |
The docs' architecture overview counts the client side differently: it names Elicitation, Sampling and Logging, and leaves Roots out. This post follows the grouping the specification uses in its Client Features section, which is the one with Roots in it.
So: MCP has six primitives. Tools, Resources, Prompts on one side. Roots, Sampling, Elicitation on the other. Three of the six are now deprecated, and the update at the top of this post is the part to act on: build on Elicitation, and read Roots and Sampling as design history. That history is still worth having. The questions those two were answering, who declares the scope a server may work in and whose model a server gets to use, outlived the answers.
Three questions, and where each one goes now:
| What your server needs | The 2025-11-25 answer | What to build on today |
|---|---|---|
| A value the caller left out | Elicitation | Elicitation, unchanged |
| The directories it may work in | Roots | tool parameters, resource URIs, or server configuration |
| An LLM call inside the server | Sampling | your own LLM provider API |
For the protocol-level mechanics, the Capability Negotiation module walks through how clients and servers declare and discover these capabilities at session start.
For a deeper dive on Elicitation specifically (the human-in-the-loop primitive), the Human in the Loop module of the Agentic Workflows course shows how to design tools around it. That course is coming soon.
And if you want to see how all six primitives compose into a real architecture, the MCP Architecture Patterns course threads them through the bigger picture. It is also coming soon.
Three primitives is the bumper sticker. Six is the protocol.
Further Reading
- MCP client concepts: the gentler introduction to this half of the protocol, one primitive at a time, before you read the normative pages.
- MCP specification 2026-07-28: Key Changes: the place to start if you are porting a server written against an older revision.
- MCP specification: Multi Round-Trip Requests: the pattern every server-to-client question now uses, with worked message sequences for each side.
- MCP specification: Elicitation: the page to build against, with example requests for form mode and URL mode and the OAuth pattern URL mode enables.
- MCP specification: Roots: the full request and response shapes, and the security considerations a server author should read before trusting a root.
- MCP specification: Sampling: how model preferences are expressed, if you are maintaining a server that already uses it.
- MCP specification: server/discover: the mandatory request that replaced the handshake as the way to learn what a server supports.
- Spring AI: MCP Client Annotations: the Java side of this half, where an application handles sampling, elicitation and roots requests.
Sources
- MCP specification 2026-07-28: Key Changes: that SEP-2577 deprecates Roots, Sampling and Logging with those three migration paths, and that SEP-2575 removed
ping,logging/setLevelandnotifications/roots/list_changed. It also carries the switch to Multi Round-Trip Requests, the removal of theinitializehandshake andMcp-Session-Id, and the rule thatserver/discoveris mandatory. - MCP specification: Deprecated Features: the earliest removal of "first revision released on or after 2027-07-28" for all three, and the absence of Elicitation from the registry.
- Feature Lifecycle and Deprecation Policy: the minimum twelve-month deprecation window, adopted as SEP-2596.
- MCP specification: Roots (2026-07-28): the quoted sentence about informational guidance, the SHOULD that servers respect root boundaries, and the rule that a root
urimust be afile://URI. - MCP specification: Roots (2025-11-25): that a client supporting
listChangedsendsnotifications/roots/list_changedand the server then re-issuesroots/list. - MCP specification: Sampling: that the client keeps control of model access and selection without a server API key, and may modify or ignore the system prompt, temperature and context settings.
- MCP specification: Elicitation: the restricted subset of JSON Schema limited to flat objects with primitive properties, the rule that form mode must not request credentials, URL mode as the replacement, and the accept, decline and cancel actions.
- MCP specification: Multi Round-Trip Requests:
resultType: "input_required", theinputRequestsandinputResponsesmaps, the opaquerequestState, and the requirement that the retry carry a different id. - MCP specification 2025-06-18: Key Changes: that Elicitation was added in that revision.
- MCP Architecture overview: the docs' own count of the client primitives, which names Logging where this post names Roots, and the
_metaexample showingio.modelcontextprotocol/clientCapabilities. - SEP-2577: Deprecate Roots, Sampling, and Logging: the merged proposal, whose rationale for Sampling is low client adoption against its implementation complexity.
- Spring AI: MCP Server Annotations: that the MCP server annotation in Spring AI today is
@McpTool.