Files
awesome-copilot/skills/dotnet-mcp-builder/references/roots.md
T
Adrien Clerbois aa01464ccb Update dotnet-mcp-builder skill to ModelContextProtocol 2.x (#2487)
* Update dotnet-mcp-builder skill to ModelContextProtocol 2.x

Align the skill with the C# SDK 2.0.0 release and the MCP 2026-07-28
spec: stable line is now 2.x, HttpServerTransportOptions.Stateless
defaults to true, roots/sampling/MCP-channel logging are [Obsolete]
(MCP9005) with the multi-round-trip input_required pattern as the
replacement, discovery-first negotiation (server/discover) supersedes
the initialize handshake, Mcp-Method/Mcp-Name routable headers, raw
structuredContent for non-object results, required Tool.inputSchema,
and the new ModelContextProtocol.Extensions.Tasks and
ModelContextProtocol.Extensions.Apps packages (typed MCP Apps support
replacing the hand-rolled _meta/ui:// pattern on 1.x).

* Address Copilot review: Apps extension accuracy, header scope, capability ownership

- packages.md: the Apps package replaces the manual _meta wiring, not the
  ui:// resource; note the experimental MCPEXP003 diagnostic; label the
  1.x -> 2.0 list as highlights and add the OAuth/SSE runtime changes
  with a pointer to the full release notes.
- transport-http.md: Mcp-Method is on every POST, Mcp-Name only on named
  invocations (tools/call, prompts/get, resources/read) - do not require
  it globally at gateways.
- mcp-apps.md: current MIME type is text/html;profile=mcp-app (skybridge
  is a legacy draft value); document [McpAppUi] + WithMcpApps().
- server-features.md: roots/sampling are client capabilities, only
  logging sits on ServerCapabilities.

* Correct stateful HTTP guidance: 2026-07-28 has no HTTP sessions

Per the official SDK v2 elicitation docs, a server with Stateless=false
refuses the 2026-07-28 revision so dual-path clients fall back to an
initialize-capable revision; ElicitAsync cannot be used on 2026-07-28
Streamable HTTP at all. Reframe stateful HTTP as down-level
compatibility mode and document the multi-round-trip pattern
(InputRequiredException / InputRequest.ForElicitation, retry with
InputResponses -> ElicitResult) as the current-protocol way to ask
mid-tool, across SKILL.md, transport-http.md, and elicitation.md.
2026-07-31 10:31:14 +10:00

4.0 KiB

Roots

Deprecated in the 2026-07-28 spec. SDK 2.x marks the roots APIs [Obsolete] (build warning MCP9005). They stay wire-compatible with down-level clients during the transition, but for new designs prefer having the client pass in-scope paths as tool arguments, or ask via elicitation / the multi-round-trip input_required pattern. Keep this page for maintaining existing 1.x-era servers.

Roots are filesystem (or URI) locations the client advertises to the server, scoping what the server is allowed to look at. Think "open workspace folders" in an IDE — the user has implicitly approved the server reading from these places. The server pulls the list when it needs it.

When you'd use roots

  • Building a tool that scans/edits the user's project. Use roots to know which directories are in scope.
  • Resolving relative paths in a way that respects the user's open workspace.
  • Restricting file access to the advertised roots (defence in depth).

Prerequisite

Same as sampling/elicitation: server-to-client request → needs STDIO or stateful HTTP. Plus the client must advertise the roots capability.

Reading roots from a tool

using System.ComponentModel;
using System.Text;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

[McpServerToolType]
public class WorkspaceTools
{
    [McpServerTool, Description("Lists the user's project roots.")]
    public static async Task<string> ListProjectRoots(
        IMcpServer server,
        CancellationToken cancellationToken)
    {
        if (server.ClientCapabilities?.Roots is null)
            return "Client does not support roots.";

        var result = await server.RequestRootsAsync(
            new ListRootsRequestParams(),
            cancellationToken);

        var sb = new StringBuilder();
        foreach (var root in result.Roots)
            sb.AppendLine($"- {root.Name ?? root.Uri}: {root.Uri}");

        return sb.ToString();
    }
}

Root has Uri (string, often a file://...) and optional Name (display label).

Reacting to root changes

Clients send notifications/roots/list_changed when the user opens or closes a workspace folder. Subscribe:

builder.Services.Configure<McpServerOptions>(options =>
{
    options.Capabilities ??= new();

    // The client tells us its roots changed; refresh whatever cache we have.
    options.Capabilities.NotificationHandlers ??= [];
    options.Capabilities.NotificationHandlers[NotificationMethods.RootsListChangedNotification] =
        async (notification, ct) =>
        {
            // Trigger your refresh — typically pull RequestRootsAsync again.
        };
});

A useful pattern: cache + refresh

Roots don't change often, but refetching on every tool call is wasteful. Cache them per session and refresh on roots/list_changed:

public class RootsCache
{
    private IReadOnlyList<Root> _roots = Array.Empty<Root>();

    public IReadOnlyList<Root> Current => _roots;

    public async Task RefreshAsync(IMcpServer server, CancellationToken ct)
    {
        if (server.ClientCapabilities?.Roots is null) return;
        var result = await server.RequestRootsAsync(new ListRootsRequestParams(), ct);
        _roots = result.Roots;
    }
}

Register as singleton (per-session in stateful HTTP, naturally singleton in STDIO).

Validating paths against roots

Defence in depth: even if a tool argument looks like a path under a root, validate.

public static bool IsUnderAnyRoot(string absolutePath, IReadOnlyList<Root> roots)
{
    foreach (var root in roots)
    {
        if (!Uri.TryCreate(root.Uri, UriKind.Absolute, out var uri)) continue;
        if (!uri.IsFile) continue;
        var rootPath = Path.GetFullPath(uri.LocalPath);
        if (absolutePath.StartsWith(rootPath, StringComparison.OrdinalIgnoreCase))
            return true;
    }
    return false;
}

If a tool receives a path outside the advertised roots, refuse with a clear message — don't silently expand scope.