* 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>
8.0 KiB
Tools
Tools are functions the LLM can call. In the C# SDK they're plain methods on a class marked [McpServerToolType], with each method marked [McpServerTool]. The SDK generates the JSON Schema from the method signature and [Description] attributes.
Anatomy of a tool
using System.ComponentModel;
using ModelContextProtocol.Server;
[McpServerToolType]
public class WeatherTools
{
// Static or instance — both work. Instance methods get DI for the containing class.
[McpServerTool, Description("Returns the current weather for a city.")]
public static string GetWeather(
[Description("City name, e.g. 'Brussels'")] string city,
[Description("Units: 'celsius' or 'fahrenheit'")] string units = "celsius")
{
return $"{city}: 18°{units[0]}";
}
}
Register it (one of):
.WithToolsFromAssembly() // discovers all [McpServerToolType] in the calling assembly
.WithTools<WeatherTools>() // explicit, single class
The tool name shown to the LLM is GetWeather (PascalCase converted to snake_case is not automatic — what you see is what you get unless you set Name explicitly).
Attribute options
[McpServerTool(
Name = "get_weather", // override the tool name
Title = "Get current weather", // human-readable display name
Destructive = false, // hint: tool modifies state irreversibly
Idempotent = true, // hint: same args ⇒ same result
OpenWorld = true, // hint: interacts with external systems
ReadOnly = true // hint: doesn't mutate any state
)]
[Description("Returns the current weather for a city.")]
public static string GetWeather(...) { ... }
The behaviour hints (Destructive, Idempotent, OpenWorld, ReadOnly) are advisory — clients use them to decide things like auto-approval. They don't change runtime behaviour.
Async, cancellation, DI
[McpServerTool, Description("Fetches the latest commits for a repo.")]
public async Task<IEnumerable<Commit>> GetCommits(
string owner,
string repo,
IGitHubClient github, // injected from DI
CancellationToken cancellationToken) // injected by the SDK
{
return await github.GetCommitsAsync(owner, repo, cancellationToken);
}
The SDK recognises and special-cases these parameter types — they don't appear in the tool schema:
IMcpServer/McpServer— the current server (used forElicitAsync,SampleAsync,RequestRootsAsync, sending notifications).CancellationToken— propagated from the JSON-RPC request.RequestContext<CallToolRequestParams>— full request context if you need it.IServiceProvider— request-scoped service provider.- Anything resolvable from DI that the SDK can recognise as not a primitive payload.
Everything else is treated as a JSON-RPC argument and goes into the schema.
Return types
The SDK serialises whatever you return into the appropriate content blocks. Practical guidance:
| Return type | What the LLM sees |
|---|---|
string |
Single text content block. |
int, bool, double, etc. |
Stringified into a text content block. |
| Any DTO (record/class) | Serialized to JSON in a text content block, plus structured content for clients that support it. |
IEnumerable<T> of DTOs |
JSON array. |
ContentBlock / ImageContentBlock / AudioContentBlock / EmbeddedResourceBlock |
That single block, untouched. |
IEnumerable<ContentBlock> |
Multiple blocks in order. |
CallToolResult |
Full control — set Content, StructuredContent, IsError. |
Returning structured data the LLM can act on
public record Forecast(string City, double TempC, string Conditions);
[McpServerTool, Description("Returns a 3-day forecast.")]
public static Forecast[] GetForecast(string city) =>
new[]
{
new Forecast(city, 18.0, "sunny"),
new Forecast(city, 16.5, "cloudy"),
new Forecast(city, 14.2, "rain"),
};
The SDK emits the array as both a JSON text block (for older clients) and structuredContent (for newer ones), and infers an output schema from Forecast.
Returning images / audio
[McpServerTool, Description("Generates a chart and returns it as a PNG.")]
public static ImageContentBlock RenderChart(string title)
{
byte[] png = Renderer.Render(title);
return ImageContentBlock.FromBytes(png, "image/png");
}
[McpServerTool, Description("Synthesises speech.")]
public static AudioContentBlock Speak(string text)
{
byte[] wav = Tts.Synthesize(text);
return AudioContentBlock.FromBytes(wav, "audio/wav");
}
Mixing content blocks
[McpServerTool, Description("Returns the chart and a caption.")]
public static IEnumerable<ContentBlock> RenderAnnotatedChart(string title)
{
byte[] png = Renderer.Render(title);
return new ContentBlock[]
{
new TextContentBlock { Text = $"Chart for: {title}" },
ImageContentBlock.FromBytes(png, "image/png"),
new TextContentBlock { Text = "Generated at " + DateTime.UtcNow.ToString("u") }
};
}
Returning an embedded resource
Useful when the tool result is a document the user might want to reuse:
[McpServerTool, Description("Looks up a contract.")]
public static EmbeddedResourceBlock GetContract(string id)
{
return new EmbeddedResourceBlock
{
Resource = new TextResourceContents
{
Uri = $"contracts://{id}",
MimeType = "text/markdown",
Text = LoadContract(id)
}
};
}
Errors
There are two flavours of error a tool can produce:
Tool-level errors (the LLM can read and recover from these)
Throw any exception — the SDK catches it and returns a CallToolResult with IsError = true and the exception message in a text block:
[McpServerTool, Description("Divides a by b.")]
public static double Divide(double a, double b)
{
if (b == 0)
throw new ArgumentException("Cannot divide by zero.");
return a / b;
}
You can also build the result explicitly:
[McpServerTool, Description("…")]
public static CallToolResult Foo(...)
{
return new CallToolResult
{
IsError = true,
Content = [new TextContentBlock { Text = "Detailed error explanation for the LLM." }]
};
}
Protocol-level errors (the call is rejected before the LLM sees a result)
Use McpException (or McpProtocolException with an explicit error code) for things like bad arguments:
[McpServerTool, Description("…")]
public static string Process(string input)
{
if (string.IsNullOrWhiteSpace(input))
throw new McpProtocolException("Missing required input", McpErrorCode.InvalidParams);
return $"Processed: {input}";
}
Heuristic: if the LLM should try again with different arguments, throw a regular exception so it gets a tool error. If the call is malformed in a way the LLM can't fix, throw McpProtocolException.
Notifying clients of tool list changes
If your tools come and go at runtime (e.g. plugin loaded, user logged in), notify the client:
await server.SendNotificationAsync(
NotificationMethods.ToolListChangedNotification,
new ToolListChangedNotificationParams(),
cancellationToken);
Requires a stateful transport (STDIO or stateful HTTP).
Common pitfalls
- Forgetting
[McpServerToolType]on the class. The method-level[McpServerTool]alone won't be discovered byWithToolsFromAssembly. - Vague descriptions.
[Description("Gets data")]makes the LLM guess. Spend a sentence describing what the tool does, when to call it, and what it returns. - Big payloads. Tools that return megabytes of JSON eat the model's context. Trim or paginate. For binary blobs, return an
EmbeddedResourceBlockso the host can decide how to render it. - Hiding errors. Returning
"failed"as a string looks like success to the SDK. Throw the exception or setIsError = true.