Dotnet MCP Builder
GitHubOfficialFirst committed by AClerboisDistributed by GitHub
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides guidance for developing production-ready MCP servers and clients in C#/.NET. It focuses on the official ModelContextProtocol NuGet packages, specifically targeting the stable 2.x line and the 2026-07-28 specification. It addresses common pitfalls such as stale preview versions, v2 stateless HTTP defaults, deprecated capabilities like sampling and roots, and proper transport configuration for STDIO and Streamable HTTP.
What it does
- Provides setup patterns for MCP servers using Microsoft.Extensions.Hosting.
- Explains how to implement tools, prompts, and resources using C# attributes.
- Guides the configuration of STDIO and Streamable HTTP transports.
- Offers solutions for debugging common errors like HTTP 404s, parameter binding issues, and STDIO stdout conflicts.
- Details the multi-round-trip pattern for eliciting user input mid-tool execution.
- Assists in building .NET-based MCP clients to consume and test servers.
When to use it
- When starting a new MCP project in C# or upgrading an existing one to the 2.x SDK.
- When you need to implement specific MCP primitives like tools, prompts, or resources.
- When troubleshooting transport-level issues in STDIO or HTTP implementations.
- When migrating away from deprecated features like sampling or roots to the current multi-round-trip pattern.
- When you need to integrate an MCP client into a .NET application.
Advantages
- Prevents the use of stale or incompatible preview packages.
- Ensures adherence to current protocol specifications and deprecation warnings.
- Provides clear patterns for handling complex interactions like user elicitation.
- Helps avoid common runtime errors related to transport and DI configuration.
- Promotes consistent project structure using standard .NET hosting patterns.
Usage examples
- “How do I set up a basic MCP server in .NET?”
- The agent provides a code snippet using AddMcpServer, WithStdioServerTransport, and tool registration via DI.
- “Why is my MCP tool not appearing in the client?”
- The agent checks for missing [McpServerToolType] attributes and verifies that the tool class is registered via .WithToolsFromAssembly().
- “How do I ask the user for input during a tool call in v2?”
- The agent explains the multi-round-trip InputRequiredException pattern for modern stateless HTTP.
- “How do I connect to an MCP server from a .NET client?”
- The agent provides a configuration example for StdioClientTransport or HttpClientTransport.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/github/dotnet-mcp-builder/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip dotnet-mcp-builder.zip -d .agents/skills
Documentation by AClerbois
Building MCP servers in .NET
This skill helps you write production-quality MCP servers and basic clients in C#/.NET against the official ModelContextProtocol NuGet packages, maintained by Microsoft and the MCP project. It targets the stable 2.x line and the current spec (2026-07-28).
When this skill earns its keep
The .NET MCP SDK had years of preview packages (0.x-preview) before reaching 1.0, and v2 flipped several defaults. Without help, the model tends to:
Pin a stale preview version that won't compile against current samples.
Apply 1.x-era defaults that v2 reversed (HTTP was stateful by default; in 2.x
Statelessdefaults totrue).Recommend capabilities the 2026-07-28 spec deprecates (roots, sampling, MCP-channel logging — now
[Obsolete], warningMCP9005).Miss recent spec features (multi-round-trip
input_required, discovery-first negotiation, MCP Apps/Tasks extension packages, elicitation URL mode, structured content blocks).Get HTTP transport details wrong (stateful/stateless, proxy buffering, OAuth wiring).
Forget the STDIO stdout/stderr trap.
If the task is one of those, load the matching reference and follow it. If it's truly trivial (e.g. "rename this tool method"), you don't need to read everything — the cardinal rules below are the minimum.
Mental model in 30 seconds
A .NET MCP server is an ordinary Microsoft.Extensions.Hosting (or WebApplication) app that wires an MCP server through DI:
builder.Services
.AddMcpServer()
.WithStdioServerTransport() // OR .WithHttpTransport(...)
.WithToolsFromAssembly() // discover [McpServerToolType] classes
.WithPrompts<MyPrompts>() // optional
.WithResources<MyResources>(); // optionalPrimitives are plain C# methods on classes marked with attributes ([McpServerToolType] + [McpServerTool], [McpServerPromptType] + [McpServerPrompt], [McpServerResourceType] + [McpServerResource]). Parameters bind from JSON-RPC; the SDK builds the JSON Schema from the signature plus [Description] attributes.
Server-to-client features (elicitation, progress notifications, and the now-deprecated sampling/roots/log notifications) are methods on the injected IMcpServer.
Decision tree → which references to load
Always load references/packages.md if you're creating a new project or unsure of the current package version.
| Task | Load |
|---|---|
| New STDIO server | references/transport-stdio.md |
| New HTTP (Streamable) server | references/transport-http.md |
| Add/modify a tool | references/tool-primitive.md |
| Add/modify a prompt | references/prompt-primitive.md |
| Add/modify a resource | references/resource-primitive.md |
| Ask the user a question mid-tool | references/elicitation.md |
| Call the client's LLM from a tool (deprecated in 2026-07-28) | references/sampling.md |
| Read the user's project roots (deprecated in 2026-07-28) | references/roots.md |
| Return an interactive UI | references/mcp-apps.md |
| Argument completions, log/progress notifications, filters, server instructions | references/server-features.md |
| Write a .NET program that consumes an MCP server | references/client.md |
| MCP Inspector, in-memory tests, mocks, CI | references/testing.md |
For multi-primitive tasks, load several at once. For trivial edits in an existing file, you usually don't need any.
Cardinal rules (apply always; these prevent the highest-frequency breakages)
Pin the current stable package, not a preview. Use
ModelContextProtocol/ModelContextProtocol.AspNetCore/ModelContextProtocol.Coreat the latest 2.x. If you find yourself writing0.3-previewor0.4-preview, stop and check NuGet — preview APIs have breaking differences. 1.x still works but predates the 2026-07-28 spec.STDIO servers must not write to stdout. Stdout is the JSON-RPC channel. Configure
LogToStandardErrorThreshold = LogLevel.Tracebefore anything else and neverConsole.WriteLinefrom a tool.HTTP defaults to stateless in 2.x (v1.x defaulted to stateful — the single most impactful v2 breaking change). The 2026-07-28 revision has no HTTP sessions at all: setting
Stateless = falsemakes the server refuse that revision and serve clients via the legacyinitializefallback. For "ask the user something mid-tool" on current-protocol HTTP, use the multi-round-tripInputRequiredExceptionpattern; reserve stateful HTTP (or STDIO) for the legacyElicitAsync/sampling/roots paths and pushed notifications.SSE-only is deprecated. Use Streamable HTTP. Only enable legacy SSE (
EnableLegacySse = true) for an old client you must support, and call it out.Don't design new servers around deprecated capabilities. The 2026-07-28 spec deprecates roots, sampling, and MCP-channel logging; the SDK marks them
[Obsolete](warningMCP9005). They still work against down-level clients, but for new designs prefer the multi-round-tripinput_requiredpattern andILoggerlogging. SuppressMCP9005only as a documented transition measure.Always
[Description]tools and parameters. This is what the LLM sees when picking and shaping calls. Vague descriptions are the #1 reason tools don't get used.Show the registration line every time you add a primitive. A new
[McpServerPromptType]class without.WithPrompts<...>()(or.WithPromptsFromAssembly()) is invisible.Don't invent APIs. If you're unsure a method exists, say so and check the API reference — wrong method names cause silent failures. This applies doubly to the new v2 extension packages (
ModelContextProtocol.Extensions.Tasks,ModelContextProtocol.Extensions.Apps) — check their docs before writing code against them.
Working style
Make minimal, additive changes. Add a method to the existing tool class rather than restructuring the project.
For non-trivial setups, run
dotnet build. Catches missing usings, attribute typos, and TFM mismatches before the user sees them.Confirm transport + .NET version + primitives before scaffolding if context doesn't already make them obvious. Default to .NET 10 for new projects.
When the user is stuck
Walk this checklist before guessing:
STDIO: something is writing to stdout (logger sink,
Console.WriteLine, library banner).HTTP 404: path mismatch —
app.MapMcp()is root,app.MapMcp("/mcp")puts it under/mcp.Tool not appearing: missing
[McpServerToolType]on the class, or no.WithToolsFromAssembly()/.WithTools<T>()registered.Args not bound: parameter names must match the JSON-RPC
argumentskeys; complex types bind viaSystem.Text.Json.Sampling/elicitation/roots failing: these legacy server-to-client calls can't run on current-protocol HTTP — migrate to the multi-round-trip
InputRequiredExceptionpattern, or (legacy paths only) setStateless = false, knowing that pins HTTP clients to a down-levelinitializerevision. Also check the client actually advertises the capability.MCP9005build warnings after upgrading to 2.x: the code uses deprecated roots/sampling/logging APIs. Plan the migration; suppress only temporarily.
Still stuck? Point the user at the EverythingServer sample — it exercises every feature.
Files
- ATTRIBUTION.mdAdded by AutoGineer871 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md8.4 KiB
- references/client.md6.2 KiB
- references/elicitation.md7.8 KiB
- references/mcp-apps.md9.8 KiB
- references/packages.md5.2 KiB
- references/prompt-primitive.md5.0 KiB
- references/resource-primitive.md6.1 KiB
- references/roots.md4.0 KiB
- references/sampling.md5.4 KiB
- references/server-features.md6.6 KiB
- references/testing.md6.1 KiB
- references/tool-primitive.md8.4 KiB
- references/transport-http.md8.4 KiB
- references/transport-stdio.md5.1 KiB
references/tool-primitive.md
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 classThe 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.
v2 behavior change: non-object results are emitted as raw
structuredContentvalues — returning72produces"structuredContent": 72, where 1.x wrapped it as{ "result": 72 }. Clients reading structured output should follow the advertised output schema. If you hand-writeTooldefinitions (rather than using attributes), note thatinputSchemais required on deserialization in 2.x — an empty{}is sufficient.
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.
Requirements and compatibility
Written for: GitHub Copilot
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an MCP server
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- First committed by
AClerbois
- Publisher
- GitHub (Official)
- Distributed by
- GitHub
- Source
- github/awesome-copilot · skills/dotnet-mcp-builder · commit 797337f
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
- Contributors
- AClerbois, jeanpaulhassane-225
AutoGineer is not affiliated with or endorsed by AClerbois or GitHub.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download dotnet-mcp-builder.zip