Files
awesome-copilot/skills/dotnet-mcp-builder/references/server-features.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

6.6 KiB

Other server features (completions, logging, progress, filters)

A quick reference for the smaller MCP server features beyond the core primitives. Each section is short — load this file when one of these comes up.

Argument completions

Completions let the host autocomplete prompt arguments and resource template parameters. The user starts typing; the client asks the server "what are valid values?".

Implement via the low-level handler (no high-level attribute exists yet):

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

    options.Capabilities.Completions.CompleteHandler = async (ctx, ct) =>
    {
        // ctx.Params.Ref tells us what they're completing (a prompt or resource).
        // ctx.Params.Argument has the partial value typed so far.
        var partial = ctx.Params.Argument.Value ?? "";
        var matches = MyDataSource
            .Where(x => x.StartsWith(partial, StringComparison.OrdinalIgnoreCase))
            .Take(100)
            .ToArray();

        return new CompleteResult
        {
            Completion = new()
            {
                Values = matches,
                HasMore = false,
                Total = matches.Length
            }
        };
    };
});

Useful for: project IDs, file names, enum values that depend on dynamic data.

Logging

Servers can emit log messages that hosts surface in their UI (and the LLM can sometimes see). Use the standard ILogger<T> injected via DI — the SDK plumbs it through.

public class WeatherTools
{
    private readonly ILogger<WeatherTools> _log;
    public WeatherTools(ILogger<WeatherTools> log) => _log = log;

    [McpServerTool, Description("…")]
    public string GetWeather(string city)
    {
        _log.LogInformation("Looking up weather for {City}", city);
        return "...";
    }
}

For STDIO servers, remember: console logging must go to stderr (LogToStandardErrorThreshold = LogLevel.Trace) — otherwise it corrupts the JSON-RPC stream. See transport-stdio.md.

MCP-channel logging is deprecated in the 2026-07-28 spec — SDK 2.x marks it [Obsolete] (MCP9005), along with the logging capability and the client's setLevel method (replaced by a _meta log level on requests). ILogger-based logging above is unaffected and remains the right default. Only use the MCP-channel notification below when supporting down-level clients that expect it:

await server.SendNotificationAsync(
    NotificationMethods.LoggingMessageNotification,
    new LoggingMessageNotificationParams
    {
        Level = LoggingLevel.Info,
        Logger = "weather",
        Data = JsonSerializer.SerializeToElement(new { city, latency_ms = 123 })
    },
    ct);

The client may have set a setLevel filter — don't spam levels below it.

Progress notifications

For long-running tools, send progress updates so the host can display a spinner with text:

[McpServerTool, Description("Processes a large dataset.")]
public static async Task<string> Process(
    IMcpServer server,
    RequestContext<CallToolRequestParams> ctx,
    string datasetId,
    CancellationToken ct)
{
    var progressToken = ctx.Params.Meta?.ProgressToken;

    for (int i = 0; i < 100; i++)
    {
        await Task.Delay(50, ct);

        if (progressToken is not null)
        {
            await server.SendNotificationAsync(
                NotificationMethods.ProgressNotification,
                new ProgressNotificationParams
                {
                    ProgressToken = progressToken,
                    Progress = i + 1,
                    Total = 100,
                    Message = $"Processing item {i + 1} of 100"
                },
                ct);
        }
    }

    return "Done.";
}

Only send progress if the client passed a progressToken in the request meta — otherwise the host isn't listening.

Notification handlers (server-side)

Servers can react to notifications the client sends:

options.Capabilities ??= new();
options.Capabilities.NotificationHandlers ??= [];

options.Capabilities.NotificationHandlers[NotificationMethods.RootsListChangedNotification] =
    async (notification, ct) =>
    {
        // Refresh root cache, etc.
    };

options.Capabilities.NotificationHandlers[NotificationMethods.CancelledNotification] =
    async (notification, ct) =>
    {
        // The client cancelled a request; if you have side-effects in flight, abort them.
    };

Filters / middleware

The SDK supports filters that wrap tool calls (think ASP.NET Core middleware for MCP). Use them for cross-cutting concerns: auth checks, telemetry, rate limiting, audit logging.

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly()
    .WithCallToolFilter(async (ctx, next) =>
    {
        var sw = Stopwatch.StartNew();
        try
        {
            return await next(ctx);
        }
        finally
        {
            sw.Stop();
            ctx.Server.Services?
                .GetRequiredService<ILogger<Program>>()
                .LogInformation("Tool {Tool} took {Ms}ms",
                    ctx.Params.Name, sw.ElapsedMilliseconds);
        }
    });

Similar With*Filter helpers exist for resources, prompts, and other capabilities — check the SDK API reference for the current set.

Server instructions (system-prompt-ish)

You can supply instructions sent to the client at initialise time. Hosts may include them in the LLM's system prompt.

builder.Services.AddMcpServer(options =>
{
    options.ServerInstructions =
        "Use the booking tools to schedule meetings. " +
        "Always confirm with the user before booking via elicitation.";
});

Keep this short — every token here costs the user.

Capabilities advertising

If you want to not advertise a capability you happen to have code for, you can mute it:

builder.Services.AddMcpServer(options =>
{
    options.Capabilities = new()
    {
        Tools = new(),       // advertise tools
        Prompts = new(),     // advertise prompts
        Resources = null,    // do NOT advertise resources, even if some are registered
    };
});

By default, the SDK advertises everything you've registered — usually the right behaviour. Note that Logging is [Obsolete] in 2.x (MCP9005) — don't advertise it on new servers. Roots and sampling are client capabilities: a server never advertises them, it only checks server.ClientCapabilities?.Roots / .Sampling before using the (equally deprecated) client-side features.