Files
awesome-copilot/skills/dotnet-mcp-builder/references/roots.md
T
Adrien Clerbois 2c275f2ef9 feat(skills): add dotnet-mcp-builder, deprecate csharp-mcp-server-gen… (#1645)
* feat(skills): add dotnet-mcp-builder, deprecate csharp-mcp-server-generator

Adds a comprehensive skill for building MCP (Model Context Protocol)
servers in C#/.NET against the official ModelContextProtocol 1.x NuGet
packages. Covers both transports (STDIO, Streamable HTTP — SSE is
deprecated) and every primitive in the current MCP spec (2025-11-25):
tools, prompts, resources, elicitation (form + URL mode), sampling,
roots, completions, logging, and MCP Apps. Includes a thin .NET MCP
client reference and testing guidance (MCP Inspector + in-memory
transport for unit tests).

Steers the model toward the current stable 1.x packages instead of the
0.x previews it tends to pin by default, and enforces the STDIO
stdout/stderr trap.

Also deprecates the existing csharp-mcp-server-generator skill, which
predates ModelContextProtocol 1.0 and only covered a subset of the
current spec. Its SKILL.md now redirects users to dotnet-mcp-builder so
existing install URLs keep working without surprises.

* fix: address PR review from aaronpowell

- Delete csharp-mcp-server-generator skill (rather than deprecating it)
- Update mcp-apps.md pitfalls section to reference .NET Tool.Meta type
  instead of the serialized _meta JSON property names
- Rebuild docs/README.skills.md

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: remove C# MCP development plugin files

* chore: remove csharp-mcp-development plugin entry from marketplace

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 09:35:26 +10:00

3.6 KiB

Roots

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.