Skip to content

Deprecated: Sampling, Roots, and Logging

The 2026-07-28 revision deprecated three client-side features in one stroke (SEP-2577): sampling, roots, and logging. They are not gone — deprecated features keep working for at least twelve months under the feature lifecycle policy, and they are listed in the deprecated-features registry so you can track them. But new implementations should not adopt them, and existing ones should plan the migration.

This lesson covers what each did, what it looks like today, and what to do instead.

Sampling let a server ask the host’s LLM for a completion — “summarize this diff for me” — without bundling an LLM SDK or API key. The request was sampling/createMessage:

{
"method": "sampling/createMessage",
"params": {
"messages": [
{ "role": "user", "content": { "type": "text", "text": "Summarize this changelog in one line: …" } }
],
"systemPrompt": "You are terse.",
"maxTokens": 100,
"modelPreferences": {
"hints": [{ "name": "claude" }],
"intelligencePriority": 0.8,
"speedPriority": 0.5,
"costPriority": 0.2
}
}
}

modelPreferences was advisory: hints were substring matches on model names, and the three priorities (0–1) let the host pick a model. The host kept the human in the loop — it could show, edit, or refuse the prompt and the completion.

In 2026-07-28 a server cannot send requests, so the only place a sampling/createMessage may appear is inside an InputRequiredResult.inputRequests map under MRTR. The client runs the completion and returns a CreateMessageResult in inputResponses:

{
"capital": {
"role": "assistant",
"content": { "type": "text", "text": "The capital of France is Paris." },
"model": "claude-opus-5",
"stopReason": "endTurn"
}
}

The includeContext values "thisServer" and "allServers" were soft-deprecated in 2025-11-25 and are now formally deprecated; omit the field or send "none".

Two reasons. First, statelessness: a completion is expensive, and forcing it through an MRTR retry means the host pays for it and then re-sends the whole original request. Second, the host’s model is not the server’s model — quality, cost, and safety policies belonged to whoever owns the API key. Servers that need an LLM are better served calling one.

import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic(); // reads ANTHROPIC_API_KEY from the environment
async function summarize(text: string): Promise<string> {
const msg = await anthropic.messages.create({
model: 'claude-opus-5',
max_tokens: 256,
system: 'You are terse.',
messages: [{ role: 'user', content: `Summarize in one line:\n\n${text}` }],
});
const block = msg.content[0];
return block.type === 'text' ? block.text : '';
}
server.registerTool(
'summarize_changelog',
{ description: 'One-line changelog summary', inputSchema: z.object({ text: z.string() }) },
async ({ text }) => ({ content: [{ type: 'text', text: await summarize(text) }] })
);

On stdio, the key comes from the environment the host launches you with; on HTTP, from your own secret store. Either way the server now owns model choice, retries, and cost.

Roots let a server ask the client which filesystem locations it was allowed to operate on — roots/list returned URIs such as file:///home/me/project — and notifications/roots/list_changed told the server when that set changed.

roots/list survives only as an MRTR input request; notifications/roots/list_changed was removed outright (there is no client-to-server notification for it in 2026-07-28). A ListRootsResult still looks like this when it comes back in inputResponses:

{
"roots": [{ "uri": "file:///home/me/project", "name": "project" }]
}

The recommended replacement is embarrassingly simple: pass the directory as a tool argument, a resource URI, or server configuration.

  • A tool that searches files takes a path parameter, validated against an allowlist you configure at launch.
  • A server that always works on one project takes that directory as a CLI argument or environment variable when the host spawns it — which is exactly how npx @modelcontextprotocol/server-filesystem ~/Desktop has always worked.
  • Files the model should read are better exposed as resources than discovered through roots.

Explicit paths are also easier to secure: you can sanitize a parameter; you cannot easily audit an implicit permission the client granted on some earlier request.

Logging gave the server a channel to the client’s log: the client set a threshold with logging/setLevel, and the server pushed notifications/message entries with a level (debug through emergency), an optional logger name, and arbitrary data.

logging/setLevel was removed. The threshold is now per request, carried in _meta:

{
"_meta": {
"io.modelcontextprotocol/logLevel": "info",
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}

A server must not emit notifications/message for a request that did not include this field, and any it does emit ride on that request’s response stream, not on a subscription.

  • On stdio, log to stderr. The transport reserves stdout for protocol messages, but stderr is explicitly for logs; the host may capture, forward, or ignore it. Never write logs to stdout.
  • On HTTP, use your normal logging stack and, for correlation across services, OpenTelemetry. The _meta keys traceparent, tracestate, and baggage are reserved for W3C trace-context propagation, so a client can hand you a trace id and your spans line up with theirs:
{
"params": {
"name": "get_weather",
"arguments": { "location": "Bangkok" },
"_meta": {
"traceparent": "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01",
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
// stdio server: logs go to stderr, never stdout
console.error(JSON.stringify({ level: 'info', msg: 'import started', files: urls.length }));
flowchart LR
  A["Active"] --> D["Deprecated (12+ months, listed in registry)"]
  D --> R["Removed in a later revision"]
Feature lifecycle: Active, Deprecated, Removed

A deprecated feature documents its migration path and stays for at least twelve months (ninety days under the expedited exception) before it becomes eligible for removal. Sampling, roots, and logging entered Deprecated with 2026-07-28; the same revision reclassified the old HTTP+SSE transport and dynamic client registration as Deprecated too. Check the registry page for the current state before you build on anything marked Deprecated.

Where can sampling/createMessage still appear in a 2026-07-28 exchange?
What replaced logging/setLevel?
What is the recommended migration away from roots?
Where should a stdio server write its logs?