Generated banner for Dotnet MCP Builder
Generated logo for Dotnet MCP Builder

Dotnet MCP Builder

GitHubOfficialFirst committed by AClerboisDistributed by GitHub

  • Skill
  • Tool integration
  • Software development
  • MIT
  • No issues found

From the author

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.
— the description by AClerbois

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/github/dotnet-mcp-builder/download
  2. Unzip 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 Stateless defaults to true).

  • Recommend capabilities the 2026-07-28 spec deprecates (roots, sampling, MCP-channel logging — now [Obsolete], warning MCP9005).

  • 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>();   // optional

Primitives 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.

TaskLoad
New STDIO serverreferences/transport-stdio.md
New HTTP (Streamable) serverreferences/transport-http.md
Add/modify a toolreferences/tool-primitive.md
Add/modify a promptreferences/prompt-primitive.md
Add/modify a resourcereferences/resource-primitive.md
Ask the user a question mid-toolreferences/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 UIreferences/mcp-apps.md
Argument completions, log/progress notifications, filters, server instructionsreferences/server-features.md
Write a .NET program that consumes an MCP serverreferences/client.md
MCP Inspector, in-memory tests, mocks, CIreferences/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)

  1. Pin the current stable package, not a preview. Use ModelContextProtocol / ModelContextProtocol.AspNetCore / ModelContextProtocol.Core at the latest 2.x. If you find yourself writing 0.3-preview or 0.4-preview, stop and check NuGet — preview APIs have breaking differences. 1.x still works but predates the 2026-07-28 spec.

  2. STDIO servers must not write to stdout. Stdout is the JSON-RPC channel. Configure LogToStandardErrorThreshold = LogLevel.Trace before anything else and never Console.WriteLine from a tool.

  3. 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 = false makes the server refuse that revision and serve clients via the legacy initialize fallback. For "ask the user something mid-tool" on current-protocol HTTP, use the multi-round-trip InputRequiredException pattern; reserve stateful HTTP (or STDIO) for the legacy ElicitAsync/sampling/roots paths and pushed notifications.

  4. SSE-only is deprecated. Use Streamable HTTP. Only enable legacy SSE (EnableLegacySse = true) for an old client you must support, and call it out.

  5. 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] (warning MCP9005). They still work against down-level clients, but for new designs prefer the multi-round-trip input_required pattern and ILogger logging. Suppress MCP9005 only as a documented transition measure.

  6. 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.

  7. Show the registration line every time you add a primitive. A new [McpServerPromptType] class without .WithPrompts<...>() (or .WithPromptsFromAssembly()) is invisible.

  8. 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:

  1. STDIO: something is writing to stdout (logger sink, Console.WriteLine, library banner).

  2. HTTP 404: path mismatch — app.MapMcp() is root, app.MapMcp("/mcp") puts it under /mcp.

  3. Tool not appearing: missing [McpServerToolType] on the class, or no .WithToolsFromAssembly() / .WithTools<T>() registered.

  4. Args not bound: parameter names must match the JSON-RPC arguments keys; complex types bind via System.Text.Json.

  5. Sampling/elicitation/roots failing: these legacy server-to-client calls can't run on current-protocol HTTP — migrate to the multi-round-trip InputRequiredException pattern, or (legacy paths only) set Stateless = false, knowing that pins HTTP clients to a down-level initialize revision. Also check the client actually advertises the capability.

  6. MCP9005 build 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

references/elicitation.md

Elicitation

Elicitation lets a tool ask the user for input mid-execution, via the client. The LLM doesn't see the question; the client surfaces it directly to the user. This turns one-shot tool calls into interactive flows — collecting confirmation, missing parameters, credentials (URL mode), etc.

Spec version: current through 2026-07-28 (elicitation is not among the v2 deprecations — sampling/roots/logging are). URL mode is the newer addition (originally 2025-06-18 had only form mode).

2026-07-28 changes the rules on HTTP. The current revision removes HTTP sessions and the server→client elicitation/create request, so ElicitAsync cannot be used on 2026-07-28 Streamable HTTP at all — a server with Stateless = false refuses that revision and serves clients via the legacy initialize fallback. The current-protocol way to ask mid-tool is the multi-round-trip pattern: throw InputRequiredException from the tool (building requests with InputRequest.ForElicitation(...)), and on the client's retried call read context.Params.InputResponses back as an ElicitResult. It works under both protocol revisions and both session modes, including fully stateless HTTP. The ElicitAsync guidance below applies to STDIO and down-level stateful HTTP — see the SDK elicitation docs for the full MRTR pattern.

Two modes

ModeWhat it doesWhen to use
Form (in-band)Server sends a JSON Schema; client renders a form; user submits values back through the same MCP channel.Confirmations, missing parameters, structured choices.
URL (out-of-band)Server sends a URL; client opens it in a browser; user completes the flow there; server checks state separately.OAuth, payments, anything the MCP channel must not see.

Prerequisite for ElicitAsync: stateful transport (down-level revisions)

ElicitAsync requires the server to send a request to the client and wait for a response. That only works on:

  • STDIO (always).

  • Stateful HTTP (options.Stateless = false) — which, on 2.x, means down-level clients on an initialize-capable revision (see banner above).

In stateless HTTP — including all 2026-07-28 Streamable HTTP — ElicitAsync throws (InvalidOperationException): there's no transport channel back. Use the multi-round-trip InputRequiredException pattern instead.

Form mode — full example

using System.ComponentModel;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;

[McpServerToolType]
public class BookingTools
{
    [McpServerTool, Description("Books a meeting room. Asks the user for confirmation.")]
    public static async Task<string> BookRoom(
        IMcpServer server,
        [Description("Room name")] string room,
        [Description("Start time (ISO 8601)")] DateTime start,
        CancellationToken ct)
    {
        var elicit = await server.ElicitAsync(new ElicitRequestParams
        {
            Message = $"Confirm booking '{room}' at {start:HH:mm}?",
            RequestedSchema = new ElicitRequestParams.RequestSchema
            {
                Properties = new Dictionary<string, ElicitRequestParams.PrimitiveSchemaDefinition>
                {
                    ["confirm"] = new ElicitRequestParams.BooleanSchema
                    {
                        Description = "Confirm the booking",
                        Default = true
                    },
                    ["notes"] = new ElicitRequestParams.StringSchema
                    {
                        Description = "Optional notes for the booking"
                    }
                }
            }
        }, ct);

        if (elicit.Action != "accept")
            return "Booking cancelled by user.";

        var confirmed = elicit.Content?["confirm"].GetBoolean() ?? false;
        var notes     = elicit.Content?["notes"].GetString() ?? "";

        if (!confirmed)
            return "User declined to confirm.";

        // …perform the booking…
        return $"Booked '{room}' at {start:O}. Notes: {notes}";
    }
}
Schema primitive types

You can build a RequestedSchema from these:

TypeC# classNotes
StringStringSchemaDefault, Description. Add JSON-Schema validation at server side if you need it.
NumberNumberSchemaUse for ints and floats.
BooleanBooleanSchemaRenders as a checkbox / toggle.
Single-select enum (untitled)UntitledSingleSelectEnumSchemaList of values; client renders as dropdown/radio.
Single-select enum (titled)TitledSingleSelectEnumSchemaEach value has a display title.
Multi-select enumUntitledMultiSelectEnumSchema / TitledMultiSelectEnumSchemaMulti-select dropdown / checkbox group.

Each accepts Description and Default.

Response shape

ElicitResult:

  • Action — "accept", "reject", or "cancel". Always check this first.

  • Content — Dictionary<string, JsonElement>? with the user's submitted values. null if the user rejected/cancelled.

Always handle the non-accept paths:

if (elicit.Action == "cancel")
    return "User cancelled. No changes made.";
if (elicit.Action == "reject")
    return "User declined.";
// Action == "accept" → safe to read elicit.Content

URL mode — full example

URL mode is for flows where the user must complete something outside the MCP channel — typically OAuth.

[McpServerTool, Description("Connects the user's GitHub account.")]
public static async Task<string> ConnectGitHub(
    IMcpServer server,
    IOAuthService oauth,
    CancellationToken ct)
{
    var elicitationId = Guid.NewGuid().ToString();
    var authUrl = oauth.BuildAuthorizationUrl(state: elicitationId);

    var result = await server.ElicitAsync(new ElicitRequestParams
    {
        Mode = "url",
        ElicitationId = elicitationId,
        Url = authUrl,
        Message = "Please authorize access to GitHub in the browser window that just opened."
    }, ct);

    if (result.Action != "accept")
        return "Authorization cancelled.";

    // The user has come back. Look up the persisted token by elicitationId.
    var token = await oauth.GetTokenByStateAsync(elicitationId, ct);
    return token is not null ? "Connected." : "Authorization did not complete.";
}
UrlElicitationRequiredException

When a tool is blocked on auth (rather than walking the user through it), throw UrlElicitationRequiredException. The client surfaces the URL to the user and the call fails cleanly. Useful for retry-after-auth patterns:

if (!oauth.HasValidToken)
{
    var id = Guid.NewGuid().ToString();
    throw new UrlElicitationRequiredException(
        "Authorization required",
        new[]
        {
            new ElicitRequestParams
            {
                Mode = "url",
                ElicitationId = id,
                Url = oauth.BuildAuthorizationUrl(state: id),
                Message = "Sign in to continue."
            }
        });
}

When NOT to use elicitation

  • Trivial confirmations the LLM can ask in natural language. If you can phrase "Should I do X?" in your tool's docstring and let the LLM ask, that's lower friction than a modal form.

  • Branching that the LLM should reason about. Don't replace the LLM's judgment with a form — only elicit for things the LLM literally cannot decide (user secrets, real-time consent, picking from a list only the user knows).

  • Stateless deployments. Doesn't work — see prerequisite above.

Client capability check

Don't blindly call ElicitAsync. Check first:

if (server.ClientCapabilities?.Elicitation is null)
    return "This client doesn't support elicitation; please pass the value as an argument.";

var elicit = await server.ElicitAsync(...);

This degrades gracefully on older clients.

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
GitHub avatar of AClerboisAClerbois
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