Advanced MCP primitives
Tools, resources, and prompts are only half of MCP. Servers can also ask the user for input mid-call, run long jobs, and return UI. The 2026-07-28 revision reshaped this layer: multi round-trip requests replace server-initiated calls, and sampling, roots, and logging are deprecated.
What you'll learn
- What MCP's server-initiated primitives were, and what the 2026-07-28 revision did to them
- Multi round-trip requests — how elicitation works on a stateless protocol
- Sampling, roots, and logging — deprecated, and what to use instead
- The tasks and MCP Apps extensions
Before you start
The Model Context Protocol intro covered the basics: a server exposes tools, resources, and prompts, and the host (the LLM app) invokes them. That makes MCP sound one-directional — host calls server. For the protocol’s first two years it wasn’t: the server could call back to the host’s model, ask the user a question mid-operation, be confined to declared boundaries, and stream progress on long jobs.
The 2026-07-28 revision changed the shape of that layer. With sessions gone, a server has no open channel to push a request down, so the parts worth keeping were folded into the request/response cycle, and the parts nobody used were deprecated. You need both pictures: the new one to build against, the old one because most servers running today were written before it.
Here is the whole change in one table.
| Primitive | Status after 2026-07-28 | What to use instead |
|---|---|---|
Sampling (sampling/createMessage) | Deprecated (SEP-2577) | Call an LLM provider API directly from the server |
Roots (roots/list) | Deprecated (SEP-2577) | State the scope explicitly — a tool parameter, a resource URI, server config, or an env var |
Logging (logging/setLevel, notifications/message) | Deprecated (SEP-2577) | stderr for stdio servers; OpenTelemetry for structured observability |
| Elicitation | Kept, reshaped | Multi round-trip requests |
| Async tasks | Moved out of core | The io.modelcontextprotocol/tasks extension |
| Apps / UI | Formalized | The MCP Apps extension |
Deprecated does not mean broken. Wire behaviour is unchanged, and the features stay in the spec for at least twelve months under the revision’s new deprecation policy — long enough to migrate, short enough that you should not start anything new on them.
Multi round-trip requests: asking by returning
Elicitation — a server pausing to ask the user for a missing value, a confirmation, or
a choice — is the callback that earned its keep. On a stateless protocol it works by
inversion: the server does not push a question at the host, it returns one. A
tools/call, prompts/get, or resources/read can come back with
resultType: "input_required" in place of a result:
{
"resultType": "input_required",
"inputRequests": {
"env": {
"message": "Which environment should I deploy to?",
"requestedSchema": { "type": "object",
"properties": { "env": { "type": "string", "enum": ["staging", "prod"] } } }
}
},
"requestState": "opaque-blob-the-client-hands-back"
}
The client collects the answers, then re-issues the same call with inputResponses
and the requestState string it was given. The server picks up where it left off. All of
the state rides in the payload, which is what lets the second half of the exchange land on
a different server instance than the first.
Sampling: the server borrows the host’s LLM (deprecated)
For two years the most powerful advanced primitive was sampling: the server requested a completion from the client’s LLM. Instead of bringing its own model and API key, a server could delegate a reasoning sub-task back to the host — and the host stayed in control, able to review, modify, or deny the request (human-in-the-loop). A server that, say, processes a document could ask the host model to summarize a section:
{
"method": "sampling/createMessage",
"params": {
"messages": [{ "role": "user", "content": { "type": "text", "text": "Summarize the section above." } }],
"maxTokens": 200,
"modelPreferences": { "hints": [{ "name": "claude-sonnet" }] }
}
}
That inverted the usual flow — the server prompting the host’s model — and it is why MCP servers could be intelligent without each shipping (and paying for) their own LLM. You will still meet it in deployed servers, so it is worth recognising on sight.
SEP-2577 deprecated it, for three reasons worth knowing. Few clients ever implemented it: doing sampling correctly means human-in-the-loop approval, model selection, and a tool loop. Servers that want a model can simply call a provider API, with full control over model, parameters, and streaming. And it was the most security-sensitive primitive in the protocol — a server prompting the host’s model is a ready-made channel for prompt injection and context exfiltration. Writing a server today, call an LLM API directly.
Roots, logging, tasks, and apps
- Roots (host → server, deprecated): the client declared the filesystem paths or
URIs the server was allowed to operate within — part convenience (the server knows the
workspace), part boundary (the server isn’t handed the whole disk). Two problems sank
it: few clients implemented it, and the spec only ever called it informational —
servers were never required to respect a root, which is a weak thing to lean on for
security. State the scope explicitly instead: a
pathparameter on the tool, a resource URI, server config, or an environment variable. Real confinement comes from sandboxing the server, not from telling it where to behave. - Logging (server → host, deprecated): servers could push structured log messages down the protocol. Use what the rest of your stack already uses — stderr for a stdio server, OpenTelemetry when you want structured, queryable traces.
- Async tasks: long-running work moved out of the core into the
io.modelcontextprotocol/tasksextension. The server hands back a task handle and the client pollstasks/get;tasks/updatefeeds input into a running task, andsubscriptions/listencovers streams of updates. The blockingtasks/resultand thetasks/listenumeration were removed — holding a request open until a slow job finishes is exactly what pins a client to one server instance. - Apps / UI: no longer emerging. MCP Apps is a formal extension: the server ships an interactive HTML interface that the host renders in a sandboxed iframe — a form, a chart, a picker instead of a wall of text. Anything the user triggers in that UI goes back through the same JSON-RPC path as any other call, so it inherits the same consent prompts and the same audit trail.
- Enterprise Managed Authorization (EMA) is the third extension in the framework: organisation-level control over which servers and scopes its users may reach.
In one breath
- Beyond host-invoked tools/resources/prompts, MCP servers could once initiate — the 2026-07-28 revision folded the useful half of that back into request/response.
- Elicitation survives as a multi round-trip request: the server returns
resultType: "input_required", the client re-sends the same call withinputResponsesand the echoedrequestState, so all the state rides in the payload. - Sampling, roots, and logging are deprecated (SEP-2577) — call an LLM API directly, pass the scope as an explicit argument, and log to stderr or OpenTelemetry. They keep working for at least twelve months, so you will still meet them.
- Async tasks moved to the
io.modelcontextprotocol/tasksextension (polltasks/get; blockingtasks/resultandtasks/listare gone), and MCP Apps is now a formal extension for servers that return interactive UI in a sandboxed iframe. - These powers widen the attack surface — an
input_requiredresult is still a server questioning your user — so keep humans in the loop, sandbox the server, and distrust anything a server authors (MCP security).
Quick check
Quick check
Next
These primitives sit alongside the cross-protocol view in MCP vs A2A vs ACP vs ANP, and the threats they introduce are covered in MCP security. To build a server that uses them, see FastMCP.