All skills
github avatar

/dotnet-mcp-builder

@aa01464 official
by githubgithub/awesome-copilot40k stars
5,040

Build Model Context Protocol (MCP) servers in C#/.NET against the current ModelContextProtocol 2.x NuGet packages. Helps with cases the model gets wrong without guidance — stale versions (0.x preview or 1.x-era defaults), the v2 stateless-by-default HTTP flip, the 2026-07-28 spec deprecations (roots/sampling/logging), MCP Apps and Tasks extension packages, elicitation URL mode, per-session HTTP wiring, OAuth and reverse-proxy deploy specifics, and debugging MapMcp / STDIO / Streamable-HTTP errors. Also covers STDIO and Streamable HTTP transports (SSE is deprecated), tools, prompts, resources, completions, and a basic .NET MCP client. Trigger when the user says or implies any .NET MCP server work: ModelContextProtocol, McpServerTool, MapMcp, WithStdioServerTransport, "MCP server in C#", "MCP tool in dotnet", "expose this as MCP", or names a primitive (prompt/resource/elicitation/MCP App) in a .NET context. Skip for MCP work in other languages.

Use this Skill: https://skilld.dev/gh/github/awesome-copilot/dotnet-mcp-builder

This session only. Nothing lands on disk.

referencestool-primitive.md

≈2.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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 for ElicitAsync, 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 structuredContent values — returning 72 produces "structuredContent": 72, where 1.x wrapped it as { "result": 72 }. Clients reading structured output should follow the advertised output schema. If you hand-write Tool definitions (rather than using attributes), note that inputSchema is 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 by WithToolsFromAssembly.
  • 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 EmbeddedResourceBlock so 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 set IsError = true.

Source: SKILL.md on GitHub

No alerts15d3 checks · Risk SAFE
  • Gen Agent Trust Hub15d

    The skill provides technical guidance and code references for building Model Context Protocol (MCP) servers and clients using the official C#/.NET SDK. It includes security best practices for resource access and follows standard development patterns.

  • Socket15d

    No alerts

  • Snyk15d

    Risk: LOW · No issues

Signed by skilld at aa01464. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 20 hours ago.

Activeupdated 2 months ago
  • MCP
  • dotnet
  • csharp
  • model-context-protocol
  • stdio
  • http
  • tools
  • prompts
  • resources
  • streaming

README badge

README badge for github/awesome-copilot/dotnet-mcp-builder

Builds Model Context Protocol servers in C# / .NET against the stable 1.x SDK, covering STDIO and HTTP transports, tools, prompts, resources, sampling, elicitation, MCP Apps, and debugging. Targets the specific pitfalls that cause breakage: stale preview package versions, stdout pollution in STDIO mode, stateless HTTP misconfiguration, and missing primitive registration in DI.

Generated from the current SKILL.md.

Does this skill work with preview versions of the ModelContextProtocol NuGet packages?
No. The skill targets stable 1.x packages only. Preview versions (0.3, 0.4) have breaking differences and won't compile against current samples. Always pin the latest 1.x release.
Can I use stateless HTTP transport with sampling, elicitation, or server-initiated notifications?
No. Stateless HTTP breaks those features at runtime because it cannot maintain bidirectional communication. Use stateful HTTP or STDIO if you need server-to-client capabilities.
What should I do if my STDIO server isn't working?
First check that nothing is writing to stdout — configure `LogToStandardErrorThreshold = LogLevel.Trace` and remove any `Console.WriteLine` calls, since stdout is the JSON-RPC channel. Also verify tools and prompts are registered with `.WithToolsFromAssembly()` or equivalent.
Does this skill cover building MCP servers in other languages like Python or TypeScript?
No. This skill is C#/.NET only. It skips MCP work in other languages.
Can I write a .NET program that consumes an MCP server instead of building one?
Yes. Load `references/client.md` for guidance on writing a basic .NET MCP client that calls an existing server.

Generated from the current SKILL.md. These answers refresh after source changes.