Roots, Sampling and Logging
By the Syncfusion DevTutorials team · Last updated
The three client features deprecated in 2026-07-28, why they were deprecated, the 12-month window, and migration paths: tool arguments and scopes, the host's model or your own model API, and stderr with OpenTelemetry.
Think about a kitchen that still has an old oven. It still works, but the chef has been told to stop relying on it. The kitchen is not broken; it is just being moved to newer equipment. That is the idea behind deprecating old MCP features.
MCP has a few older features that are still legal to use for a while, but are already on their way out. In the 2026-07-28 spec, three older client/server patterns are marked as deprecated: roots, sampling, and logging. They still keep working for at least twelve months, but new servers should not build on them, and older servers should plan a migration.
The main idea is simple: the old ways still work for now, but they are not the path forward. The new model is to pass context explicitly, let the host do the model work, and send logs to ordinary systems instead of through the protocol.
A Real-World Example
Picture a file server built in 2025. It used to do all three old behaviors:
- ask the client for roots so it knew which folders were allowed,
- ask the client's model to summarize files,
- send log messages back through the protocol itself.
After the 2026-07-28 update, that same server is rewritten to fit the newer patterns:
- the host passes the folder as a tool argument, and the host checks it against the allowed project directories,
- the server returns the raw text and the host's model writes the summary in the next model turn,
- logs go to stderr or the normal server logging system, with trace IDs attached to the request.
The result is simpler, more portable, and easier for hosts to support.
In simple words: the old features still exist for a while, but the protocol is pushing you toward clearer, host-owned, and server-agnostic patterns.
What You Will Learn
In this lesson, you will learn:
- What deprecation means in MCP and why the 12-month window matters.
- What roots were for and how to replace them with tool arguments and scopes.
- What sampling was for and how to replace it with the host model or your own model API.
- What logging was for and how to replace it with stderr and OpenTelemetry.
- How to migrate an existing server without guessing.
This lesson is mainly for older servers and older guides. The MCP Lab client does not offer roots or sampling by default, which matches what a modern host should do.
Deprecation
What It Means
The three deprecated features and what to use instead. They keep working for at least twelve months.
A deprecated feature is not removed instantly. It is still available, but the protocol is telling you to stop depending on it and to move to the newer design.
In the 2026-07-28 spec, these features are deprecated:
- roots and
roots/list, - sampling and
sampling/createMessage, - logging and log notifications.
They remain available for at least twelve months. That means a host or server can keep supporting them while migrating, but they are no longer the preferred long-term design.
The deprecation window
const deprecated = new Date("2026-07-28T00:00:00Z");
const earliestRemoval = new Date(deprecated);
earliestRemoval.setUTCMonth(earliestRemoval.getUTCMonth() + 12);
const today = new Date("2026-10-09T00:00:00Z");
const monthsLeft =
(earliestRemoval.getUTCFullYear() - today.getUTCFullYear()) * 12 +
(earliestRemoval.getUTCMonth() - today.getUTCMonth());
for (const feature of ["roots", "sampling", "logging"]) {
console.log(`${feature.padEnd(8)} deprecated 2026-07-28`);
}
console.log("earliest removal:", earliestRemoval.toISOString().slice(0, 10));
console.log("about", monthsLeft, "months left from", today.toISOString().slice(0, 10));
Expected output:
roots deprecated 2026-07-28
sampling deprecated 2026-07-28
logging deprecated 2026-07-28
earliest removal: 2027-07-28
about 9 months left from 2026-10-09
Why They Were Deprecated
Each of these older patterns pushed work into the client or let the server send messages outside the usual request/response flow. That worked in the older model, but it is not the cleanest fit for stateless servers and multi-round-trip request flows.
The newer design is more explicit:
- the host decides what a tool may access,
- the server validates input and output,
- the model writes the final answer when needed,
- logs live in ordinary systems, not in protocol messages.
Roots
What Roots Did
The old roots feature let a server say, "Which folders or locations are available to me?" The client returned a list of file URIs, such as file:///home/ravi/project.
This was useful in a world where the server needed to know what area it was allowed to inspect, but it also created a hidden dependency on the client.
How to Replace Them
The usual replacement is to stop asking the client for folder choices and instead pass the folder as part of the tool call or resource access.
Use one of these patterns:
- tool arguments: the host passes a folder such as
search_files(folder, query), - resource URIs and scopes: the host exposes only the resources the user is allowed to access,
- server configuration: the server reads a safe allowlist from startup config or environment variables.
The important rule is this: even when you stop using roots, you must still validate the path before reading or writing anything.
From roots to arguments and scope checks
const oldRootsReply = { roots: [{ uri: "file:///home/ravi/project", name: "project" }] };
console.log(
"old: server asked roots/list ->",
oldRootsReply.roots.map((r) => r.uri).join(", "),
);
const toolCall = {
name: "search_files",
arguments: { folder: "file:///home/ravi/project", query: "TODO" },
};
console.log("new: tool argument ->", toolCall.arguments.folder);
const allowed = "file:///home/ravi/project/";
for (const uri of [
"file:///home/ravi/project/README.md",
"file:///home/ravi/.ssh/id_rsa",
]) {
console.log(
"new: scope check ->",
uri,
uri.startsWith(allowed) ? "allowed" : "blocked",
);
}
Expected output:
old: server asked roots/list -> file:///home/ravi/project
new: tool argument -> file:///home/ravi/project
new: scope check -> file:///home/ravi/project/README.md allowed
new: scope check -> file:///home/ravi/.ssh/id_rsa blocked
The takeaway is simple: the server should not rely on the client to tell it what is safe. The server still has to protect itself.
Sampling
What Sampling Did
Sampling let a server ask the client's model to produce something on its behalf, like a summary or a rewrite. That was convenient because the server did not need its own model or API key.
The problem was that the client had to show the request to the user, approve it, and often pay for it. Many hosts never supported it, and the pattern was hard to reason about.
How to Replace It
The most common replacement is to let the host's model do the work in its next turn.
- The server returns the data in the tool result.
- The host passes that data to its own model.
- The model produces the summary or answer in the usual conversation.
This is simpler and keeps the model work in one place.
If the server truly needs AI inside its own system, it can call its own model API directly. That means its own authentication, quotas, and cost control.
A temporary migration path is still possible through MRTR input requests, but that is also deprecated.
Sampling and its replacements
const oldSampling = {
method: "sampling/createMessage",
params: {
messages: [
{
role: "user",
content: { type: "text", text: "Summarize: lamp broken, 2 boxes" },
},
],
maxTokens: 50,
},
};
console.log(
"old:",
oldSampling.method,
"->",
oldSampling.params.messages[0].content.text,
);
const options = [
"Return the data and let the host's model do the writing in its next turn",
"Call your own model API from the server, with your own key and costs",
"If you must use the client's model for now: MRTR inputRequests (deprecated too)",
];
options.forEach((option, index) => console.log(`${index + 1}. ${option}`));
Expected output:
old: sampling/createMessage -> Summarize: lamp broken, 2 boxes
1. Return the data and let the host's model do the writing in its next turn
2. Call your own model API from the server, with your own key and costs
3. If you must use the client's model for now: MRTR inputRequests (deprecated too)
This is not just simpler. It is also easier to explain to users and easier to audit. One model, one context, one cost.
Logging
What Logging Did
The logging feature let a server send protocol-level log messages to the client, such as warnings or info messages. In the older protocol, that could look like notifications with levels and a logger name.
This is convenient for debugging but not ideal for a long-lived, multi-tool system. The protocol is for requests and responses, not for your operational logs.
How to Replace It
For a stdio server, write to stderr. For HTTP servers, use your normal application logging pipeline. For distributed systems, attach OpenTelemetry trace IDs so a request can be followed from the host through the client into the server.
From log messages to stderr and traces
const oldLog = {
jsonrpc: "2.0",
method: "notifications/message",
params: { level: "warning", logger: "orders", data: "Slow database" },
};
console.log("old (deprecated):", oldLog.method, oldLog.params.level, oldLog.params.data);
const line = JSON.stringify({
time: "2026-10-09T10:00:00Z",
level: "warn",
msg: "Slow database",
tool: "lookup_order",
});
console.log("stderr:", line);
const meta = { traceparent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" };
const [version, traceId, spanId, flags] = meta.traceparent.split("-");
console.log("trace id:", traceId, "| parent span:", spanId);
Expected output:
old (deprecated): notifications/message warning Slow database
stderr: {"time":"2026-10-09T10:00:00Z","level":"warn","msg":"Slow database","tool":"lookup_order"}
trace id: 4bf92f3577b34da6a3ce929d0e0e4736 | parent span: 00f067aa0ba902b7
Logs are not private just because they are on stderr. They can still contain user data, tool arguments, and sensitive request details. Redact them the same way you would redact user content anywhere else.
Planning a Migration
A migration is easiest when you do it one feature at a time.
- Make a list of the deprecated features your server depends on.
- Pick a replacement for each one.
- Support both the old and new path while older hosts are still around.
- Remove the old code before the window closes.
A migration checklist
const server = {
usesRoots: true,
usesSampling: false,
usesLogging: true,
usesElicitation: true,
};
const plan = [];
if (server.usesRoots) {
plan.push(
"roots -> take folders as tool arguments or resource URIs; check them against allowed paths",
);
}
if (server.usesSampling) {
plan.push("sampling -> return data for the host's model, or call your own model API");
}
if (server.usesLogging) {
plan.push("logging -> write to stderr / your log system; add OpenTelemetry traces");
}
if (server.usesElicitation) {
plan.push("elicitation -> keep it: it now travels through MRTR (input_required)");
}
plan.forEach((item, index) => console.log(`${index + 1}. ${item}`));
Expected output:
1. roots -> take folders as tool arguments or resource URIs; check them against allowed paths
2. logging -> write to stderr / your log system; add OpenTelemetry traces
3. elicitation -> keep it: it now travels through MRTR (input_required)
Elicitation is not deprecated. It moved into MRTR, so it still works through the newer request flow.
Do This, Not That
| Do | Don't |
|---|---|
| Pass folders as tool arguments and validate them | Build new servers around roots/list |
| Return data for the host's model to summarize | Add more hidden sampling requests |
| Send logs to stderr or your standard logger | Route new logs through MCP protocol messages |
| Migrate within the 12-month window | Wait until the feature is actually removed |
Common mistakes
- Treating deprecation as removal. Old hosts may still use these features.
- Treating deprecation as harmless forever. The window ends, and the feature can disappear.
- Dropping path checks when roots are removed. The server must still protect file access.
- Using your own model API without telling the user. If you replace sampling internally, you still need to account for cost and user visibility.
- Logging secrets because they are on stderr. Redact and minimize.
Exercises
Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.
What to Remember
- Roots, sampling, and logging are deprecated in 2026-07-28.
- They remain available for at least 12 months, but the direction is clear.
- Roots should become explicit tool arguments or resource scope checks.
- Sampling should become host-model work or your own server-side model call.
- Logging should move to stderr, regular logs, and OpenTelemetry traces.
- Elicitation is not deprecated; it moved into the MRTR flow.
The migration story is simple: do not wait for removal. Make the new pattern the default and keep compatibility only long enough to support older hosts.
Next: Chapter 6, Server Architecture, where you learn how to build a server that is secure, maintainable, and easy to reason about.