Field notes
What are MCP roots?
MCP roots let a client tell a server which folders it may use. The 2026-07-28 spec deprecates them, and roots/list now fails before it reaches the wire.
Mark
Head of Marketing, MCPOrbit
- Published
- Updated
- · Updated
- Read time
- · 8 min read
Roots are how a Model Context Protocol (MCP) client tells a server which directories it is allowed to work in: a list of file:// URIs the client hands over on request. As of the 2026-07-28 specification they are deprecated under SEP-2577, and on that protocol version roots/list does not work at all. The revision removed the server-to-client request channel, so the call fails inside the SDK before anything reaches the wire. If you are writing a server today, take the path as a tool argument instead.
That is a stronger statement than most of what is written about roots, so this post shows the experiment rather than describing it. The short version: roots is not a feature you should design around in 2026. It is a legacy-era feature you may still need to recognize in older servers.
What is an MCP root?
A root is a directory or file the client has granted the server access to. The data shape is small. This is what a roots/list response looked like.
{
"roots": [
{ "uri": "file:///Users/you/projects/api", "name": "api" },
{ "uri": "file:///Users/you/projects/web", "name": "web" }
]
}The uri must start with file://. The spec reserved the right to relax that later, and never did. The name is for display, so a user can see which folder a server is asking about.
The important part is the direction. Roots were a client capability, not a server one. The client held the list, and the server had to ask for it. In the Go SDK the client declares its roots before it connects.
// The client owns the roots. It declares them before connecting.
client := mcp.NewClient(&mcp.Implementation{Name: "roots-probe", Version: "1.0.0"}, nil)
client.AddRoots(
&mcp.Root{URI: "file:///Users/you/projects/api", Name: "api"},
&mcp.Root{URI: "file:///Users/you/projects/web", Name: "web"},
)The server then asks for that list from inside a tool handler, which is the pattern nearly every roots tutorial shows.
mcp.AddTool(server, &mcp.Tool{
Name: "show_roots",
Description: "Ask the client which directories it has granted.",
}, func(ctx context.Context, req *mcp.CallToolRequest, _ none) (*mcp.CallToolResult, any, error) {
// The SERVER calls back into the CLIENT here. This is roots/list.
res, err := req.Session.ListRoots(ctx, nil)
if err != nil {
return nil, nil, fmt.Errorf("%w", err)
}
var b strings.Builder
for _, r := range res.Roots {
fmt.Fprintf(&b, "%s (%s)\n", r.URI, r.Name)
}
return &mcp.CallToolResult{
Content: []mcp.Content{&mcp.TextContent{Text: strings.TrimSpace(b.String())}},
}, nil, nil
})That code compiles against the current SDK. It also does not work, and the reason is the interesting part.
What changed in the 2026-07-28 spec?
Two things, and only one of them is the deprecation. SEP-2577 deprecates roots, sampling, and logging as of revision 2026-07-28, with a window of at least twelve months. On its own, a deprecation would mean the feature keeps working while you migrate.
The second change is what actually breaks it. The 2026-07-28 revision removed the server-to-client request channel. In the 2025 model a server could reach back through an open connection and ask the client a question, which is how roots/list, sampling/createMessage, and elicitation all worked. That channel is gone, replaced by input_required results carrying inputRequests, which is Multi Round-Trip Requests under SEP-2322.
What happens if you call roots/list today?
It fails, with an error that names the replacement. This is the real output from the server above, built on Go 1.27.0 with github.com/modelcontextprotocol/go-sdk v1.7.0, driven by a real client over stdio.
"roots/list" cannot be sent while serving a request on protocol version 2026-07-28: return an InputRequests map instead (multi round-trip requests, SEP-2322)The wording suggests a timing problem, as though calling it outside a request would work. It does not. Here is the same call from its own goroutine, on a background context, with no request in flight.
// Not inside any request. Its own goroutine, its own background context.
go func() {
time.Sleep(50 * time.Millisecond)
res, err := ss.ListRoots(context.Background(), nil)
if err != nil {
fmt.Fprintf(os.Stderr, "[outside-request] ERROR: %v\n", err)
return
}
fmt.Fprintf(os.Stderr, "[outside-request] OK, %d root(s)\n", len(res.Roots))
}()[outside-request] calling roots/list out of band
[outside-request] ERROR: "roots/list" cannot be sent while serving a request on protocol version 2026-07-28: return an InputRequests map instead (multi round-trip requests, SEP-2322)Identical error. There is no context in which roots/list succeeds on 2026-07-28, because the failure is not about request state. The SDK is refusing to send a message that the negotiated protocol revision has no channel for.
This is not one SDK being conservative
The TypeScript SDK refuses it too, and its source says why in plain terms. The guard covers createMessage, elicitInput, listRoots, and ping together, and describes the 2026-07-28 revision as having no server-to-client request channel, so the call fails before any wire transmission. Two independent implementations, one reason.
The 2025 push-style server-to-client request model is replaced by input_required results in the 2026-07-28 protocol. If your factory serves both eras, this only works on the legacy path.
That last sentence is the one useful escape hatch. Roots still functions if the session negotiates a legacy protocol revision such as 2025-06-18. If you maintain a server that must talk to older clients, the code path is still there. Nothing you write against the current revision will use it.
Does anything about roots still work?
One piece does, and it is worth knowing because it explains the rule. When the client changed its root list mid-session, the server still received the notification.
[notification] roots/list_changed receivedThat notification travels from client to server, which is a direction the 2026-07-28 revision still has. Only the server-to-client request direction was removed. So a server can still be told that something changed, and then has no supported way to ask what it changed to. That is not a useful pair, which is why the whole feature is deprecated rather than half of it.
What should you use instead of MCP roots?
The SDK deprecation notice names three replacements: tool parameters, resource URIs, and configuration. For most servers the first one is the whole answer, and it is less code than roots ever was.
type IndexInput struct {
Root string `json:"root" jsonschema:"absolute path of the directory to index"`
}
mcp.AddTool(server, &mcp.Tool{
Name: "index_project",
Description: "Index a project directory. The caller supplies the path.",
}, func(ctx context.Context, req *mcp.CallToolRequest, in IndexInput) (*mcp.CallToolResult, any, error) {
// The path arrives as an argument. No callback, no capability, no round trip.
return nil, map[string]any{"indexed": in.Root}, nil
})The path is now an ordinary argument with a description the model reads. No capability negotiation, no callback, no round trip, and it works on every protocol revision. It is also easier to test, because you can call the tool with a path instead of standing up a client that declares roots.
Configuration covers the case where the directory is fixed for the life of the server. Pass it as an environment variable or a command-line flag in the client's server entry, and the server never has to ask. This is what most filesystem-style MCP servers already do.
The remaining case is genuinely interactive: the tool needs to ask the user something mid-call. That is what Multi Round-Trip Requests replaced the push channel with. The tool returns an input_required result carrying inputRequests and an opaque requestState, and the client comes back with the answer.
Frequently asked questions
Frequently asked questions
- What are roots in MCP?
- Roots are a list of
file://URIs that an MCP client grants a server access to, each with an optional display name. The client owned the list and the server retrieved it with aroots/listrequest. The feature is deprecated as of the 2026-07-28 specification. - Are MCP roots deprecated?
- Yes. SEP-2577 deprecates roots, sampling, and logging as of protocol revision 2026-07-28, with a stated window of at least twelve months. Unlike a normal deprecation,
roots/listalready fails on that revision, because the same release removed the server-to-client request channel it depended on. - Why does roots/list fail with an error about multi round-trip requests?
- Because
roots/listis a server-to-client request, and the 2026-07-28 revision has no server-to-client request channel. The SDK refuses the call before it reaches the wire and points you atinputRequests(SEP-2322), which is the mechanism that replaced push-style server-to-client calls. - How does an MCP server get a directory path now?
- Take it as a tool parameter, a resource URI, or configuration. A tool argument is usually the right answer: declare a
rootfield on your input struct, describe it, and the model supplies the path. It works on every protocol revision and needs no capability negotiation. - Do MCP roots still work on older protocol versions?
- Yes. If a session negotiates a legacy revision such as 2025-06-18, the push-style path is still available and
roots/listworks. Only sessions on 2026-07-28 and later refuse it. Servers that must support both eras keep the legacy path for old clients. - Does the roots/list_changed notification still fire?
- Yes, because it travels from client to server and that direction still exists. Your server can still be told the root list changed, but on 2026-07-28 it has no supported way to ask what the new list is.
The short version: roots answered a real question, which is how a server learns what it is allowed to touch. The 2026-07-28 answer to that question is a tool parameter. If you are reading a roots tutorial written before mid-2026, it is describing a protocol revision you are probably not running.
About the author
Mark
Head of Marketing, MCPOrbit
Mark leads marketing at MCPOrbit, the free desktop client for the Model Context Protocol. He writes the build-it and reliability guides, and the code in them is run before it ships.

