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.

referencestransport-http.md

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

Streamable HTTP transport (ASP.NET Core)

Streamable HTTP is the modern remote transport. A single endpoint accepts JSON-RPC over HTTP POST and (optionally) streams responses back as Server-Sent Events when the server has more than one message to send.

SSE-only is deprecated. The legacy "HTTP+SSE" transport (separate POST endpoint + GET SSE endpoint) is gone from new clients. Use Streamable HTTP. Only enable legacy SSE (EnableLegacySse = true) if you must support a known-old client, and document why.

When to choose HTTP

  • Multi-tenant or remote-hosted server.
  • Auth via OAuth / API gateway in front.
  • Horizontally scaled deployments (with Stateless = true).
  • Containers, Azure Container Apps, Kubernetes, etc.

For local single-user scenarios, STDIO is simpler.

Minimal server

dotnet new web -n MyHttpServer -f net10.0
cd MyHttpServer
dotnet add package ModelContextProtocol.AspNetCore --version 2.2.0
// Program.cs
using ModelContextProtocol.Server;
using System.ComponentModel;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMcpServer()
    // Since 2.x, Stateless defaults to true: each request is independent,
    // no Mcp-Session-Id tracking, no SSE session endpoints — ready for
    // horizontal scaling without sticky sessions.
    .WithHttpTransport()
    .WithToolsFromAssembly();

var app = builder.Build();

app.MapMcp();              // mounts the MCP endpoints at "/"
// app.MapMcp("/mcp");     // or under a path prefix

app.Run("http://localhost:3001");

[McpServerToolType]
public static class EchoTool
{
    [McpServerTool, Description("Echoes the message back to the client.")]
    public static string Echo(string message) => $"hello {message}";
}

Stateless vs. stateful — the most important decision

v2 breaking change: HttpServerTransportOptions.Stateless now defaults to true (it defaulted to false on 1.x). A server upgraded to 2.x without touching options stops creating sessions and stops exposing SSE endpoints. Set Stateless = false explicitly to restore the legacy behavior.

Mode options.Stateless Behaviour Use when
Stateless true (default since 2.x) No Mcp-Session-Id. Each POST is independent. Serves the current (2026-07-28) revision. Horizontal scaling, simple tool servers, current-protocol clients.
Stateful false Server assigns and tracks Mcp-Session-Id. Long-lived session. Down-level compatibility mode: the server refuses the 2026-07-28 revision so dual-path clients fall back to an initialize-capable revision (2025-11-25 or earlier). Legacy ElicitAsync/sampling/roots paths, pushed log notifications, clients that haven't adopted 2026-07-28. Requires session affinity at the load balancer.

Rule: on the current (2026-07-28) protocol there are no HTTP sessions — "ask the user something mid-tool" uses the multi-round-trip pattern (throw InputRequiredException, handle the retried call; see elicitation.md), which works in both session modes and both revisions. Set Stateless = false only for the legacy paths — ElicitAsync, the deprecated SampleAsync/RequestRootsAsync, or pushed log/notification messages — and be aware it pins HTTP clients to a down-level, initialize-capable revision. On the stateless default those legacy calls fail at runtime with no channel to deliver them on.

Endpoint shape

MapMcp(pattern = "") creates a route group at pattern and maps:

  • POST — accepts JSON-RPC requests/responses/notifications. Returns either a JSON response or an SSE stream depending on Accept header and whether multiple messages need to flow back.
  • GET — used by stateful sessions for the server-to-client SSE channel.
  • DELETE — terminates a stateful session.

Default pattern is the root (/). To put MCP under /mcp/v1:

app.MapMcp("/mcp/v1");

Match this on the client side (Endpoint = new Uri("https://host/mcp/v1")).

Version negotiation and routing (2026-07-28)

  • Discovery-first: v2 clients probe the server/discover method to learn capabilities instead of the legacy initialize handshake. The SDK answers both and falls back automatically for down-level peers (2025-11-25 and earlier) — you don't write any code for this, but don't be surprised to see server/discover in traffic captures.
  • Routable headers: every Streamable HTTP POST carries an Mcp-Method header (e.g. tools/call), and named invocations (tools/call, prompts/get, resources/read) additionally carry Mcp-Name (e.g. the tool name), so gateways and rate limiters can route/throttle per tool without parsing JSON bodies. Don't require Mcp-Name globally at the gateway — discovery and list requests legitimately omit it.

Per-session configuration (HttpContext access)

When you need to vary server behaviour per HTTP request (auth, tenant, headers), use the ConfigureSessionOptions callback:

builder.Services
    .AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.ConfigureSessionOptions = async (httpContext, mcpOptions, ct) =>
        {
            var tenantId = httpContext.Request.Headers["X-Tenant"].ToString();
            mcpOptions.ServerInstructions = $"Tenant: {tenantId}";
            // mutate any McpServerOptions fields per-session
        };
    });

Inside a tool, you can also inject IHttpContextAccessor if AddHttpContextAccessor() is registered. See the AspNetCoreMcpPerSessionTools sample.

Authentication

The MCP endpoint is just an ASP.NET Core endpoint — apply standard middleware:

builder.Services
    .AddAuthentication("Bearer")
    .AddJwtBearer(/* configure */);
builder.Services.AddAuthorization();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapMcp().RequireAuthorization();   // protect the endpoint

For OAuth flows where the MCP server is the resource server, follow the MCP authorization spec. The ProtectedMcpServer sample shows a working setup with discovery endpoints.

For machine-to-machine, an API key middleware is fine:

app.Use(async (ctx, next) =>
{
    if (ctx.Request.Headers["X-Api-Key"] != Configuration["ApiKey"])
    {
        ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
        return;
    }
    await next();
});

CORS (when the client is in-browser)

builder.Services.AddCors(o => o.AddDefaultPolicy(p =>
    p.WithOrigins("https://my-host.example.com")
     .AllowAnyHeader()
     .AllowAnyMethod()
     .AllowCredentials()));
// ...
app.UseCors();
app.MapMcp();

Health checks and observability

Add the standard ASP.NET Core probes; the MCP endpoint shouldn't be the liveness check.

builder.Services.AddHealthChecks();
// ...
app.MapHealthChecks("/healthz");

The SDK emits OpenTelemetry traces (Activity per tool call) and metrics. Wire them up if the user has an OTel pipeline:

builder.Services
    .AddOpenTelemetry()
    .WithTracing(t => t.AddSource("ModelContextProtocol").AddOtlpExporter())
    .WithMetrics(m => m.AddMeter("ModelContextProtocol").AddOtlpExporter());

Deployment notes

  • Containerise normally. No special MCP-specific Dockerfile — it's just an ASP.NET Core app.
  • Behind a reverse proxy (nginx, Azure Front Door, AWS ALB), make sure SSE buffering is disabled for the MCP path. nginx: proxy_buffering off;. Without this, streaming responses are batched into one slow blob.
  • Timeouts. The client may keep an SSE connection open for a long time. Set proxy idle timeout high (e.g. 5+ minutes) for stateful deployments; less critical for stateless.
  • Azure Container Apps / App Service work out of the box; both support long-lived HTTP responses.

Enabling legacy SSE (compatibility only)

builder.Services
    .AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.EnableLegacySse = true;
#pragma warning disable MCP9004
        options.Stateless = false; // SSE requires stateful mode
#pragma warning restore MCP9004
    })
    .WithToolsFromAssembly();

Only do this if the user has a documented client that hasn't migrated. New deployments should not enable it.

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.