* 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>
6.0 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.
To send a log specifically over the MCP channel (so the host UI sees it, not just your container logs):
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
Logging = new()
};
});
By default, the SDK advertises everything you've registered — usually the right behaviour.