* 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.
4.0 KiB
Roots
Deprecated in the 2026-07-28 spec. SDK 2.x marks the roots APIs
[Obsolete](build warningMCP9005). 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-tripinput_requiredpattern. 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.