Building an MCP Server in C# with .NET 10 (April 2026 Guide)

Building an MCP Server in C# with .NET 10 (April 2026 Guide)
Creating a Model Context Protocol (MCP) Server in C#

Last updated April 2026 — refreshed for the C# SDK 1.2.0 release, the 2025-11-25 MCP specification, and .NET 10 tooling.

This guide shows you how to build a production-ready Model Context Protocol (MCP) server in C# using the official ModelContextProtocol SDK that Microsoft and Anthropic now co-maintain. Every command, package version, and configuration block below is verified against current vendor documentation as of April 2026 — no leftover preview-API instructions, no aspirational features.

What changed in 2026The C# SDK left preview: ModelContextProtocol hit v1.0 on Feb 25, 2026 and the current stable is v1.2.0 (Mar 27, 2026). The --prerelease flag is no longer needed.Target the 2025-11-25 MCP specification revision. The next spec drop is tentatively scheduled for June 2026..NET 10 SDK is now the supported floor for the official Microsoft.McpServer.ProjectTemplates and the dnx launcher; Visual Studio 2022 17.12+ or VS Code with C# Dev Kit are the supported IDEs.Streamable HTTP is the default remote transport. Legacy SSE endpoints (/sse) are disabled by default in 1.2.0 and gated behind an EnableLegacySse property marked obsolete.v1.0 added OAuth authorization-server discovery, Client ID Metadata Documents (CIMD), incremental scope consent, tool sampling, and long-running request resumption via ISseEventStreamStore.Servers can now be packed as NuGet MCP packages and discovered on nuget.org under the mcpserver package type, launched via dnx.

Want the full picture? Read our continuously-updated AI Coding Agents Complete Guide (2026) — Cursor, Cline, Aider, OpenHands, Claude Code, and how teams deploy them.

TL;DR

QuestionShort answer
Which package?ModelContextProtocol 1.2.0 (core) and ModelContextProtocol.AspNetCore 1.2.0 (HTTP hosting)
Which .NET?.NET 10 SDK (templates require it; the SDK itself targets netstandard2.0 so .NET 8 LTS, 9, and 10 hosts work)
Which spec?2025-11-25
Which transport?stdio for local/CLI servers, Streamable HTTP for remote/multi-tenant
Fastest path?dotnet new install Microsoft.McpServer.ProjectTemplates then dotnet new mcpserver -n MyServer
Where do clients find it?NuGet.org with package type mcpserver; clients launch via dnx

Why MCP, and why C#

MCP is an open JSON-RPC 2.0 protocol that lets an LLM client (Claude Desktop, GitHub Copilot in VS Code/VS, Cursor, Windsurf, ChatGPT Desktop, agentic frameworks like LangChain or Semantic Kernel) discover and call tools, read resources, and request user prompts from an external server. Anthropic open-sourced it in November 2024; the spec is now governed by an independent steering group and revisioned at modelcontextprotocol.io.

C# is a first-class target because Microsoft co-maintains the SDK with Anthropic. Practical advantages over Python/TypeScript implementations:

  • Native AOT publish for sub-second cold start in stdio mode.
  • First-party ASP.NET Core hosting with Kestrel, OpenTelemetry, and JWT bearer auth out of the box.
  • Strong typing on tool parameters via attributes — descriptions and JSON schema are generated from the method signature.
  • Microsoft.Extensions.AI integration so the same server can also act as an MCP client when chaining tools.

If you are scoping a build like this and need engineers who have already shipped MCP servers in production, Codersera maintains a roster of vetted remote developers with .NET 10 and AI tool-integration experience.

Prerequisites

  • .NET 10 SDK (required for Microsoft.McpServer.ProjectTemplates and the dnx launcher used by NuGet-hosted MCP servers)
  • Visual Studio 2022 17.12+ with the ASP.NET and web development workload, or VS Code with the C# Dev Kit extension
  • An MCP client for testing — GitHub Copilot in VS/VS Code, Claude Desktop, or the official @modelcontextprotocol/inspector CLI
  • A NuGet.org account if you plan to publish
  • Working knowledge of async/await, dependency injection, and JSON-RPC concepts

Step 1 — Scaffold a server with the official template

Install the template package once, then create a project. Both stdio and HTTP transports are options at scaffold time.

dotnet new install Microsoft.McpServer.ProjectTemplates
dotnet new mcpserver -n CodeInsightsServer
cd CodeInsightsServer
dotnet build

The template emits four files worth knowing:

  • Program.cs — host bootstrapping with either WithStdioServerTransport() or WithHttpServerTransport()
  • Tools/RandomNumberTools.cs — sample tool, kept as a reference for the attribute-based pattern
  • .mcp/server.json — manifest consumed by NuGet.org and the upcoming central MCP Registry. Schema URL: https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json
  • CodeInsightsServer.csproj — sets <PackageType>McpServer</PackageType> for NuGet discovery

Step 2 — Define tools

A tool is a public method on a class annotated with [McpServerToolType]. Each method gets [McpServerTool] and an XML-style [Description]. Parameter descriptions feed straight into the JSON schema the LLM sees.

using System.ComponentModel;
using ModelContextProtocol.Server;

[McpServerToolType]
public sealed class RepositoryTools
{
    private readonly IGitHubClient _github;

    public RepositoryTools(IGitHubClient github) => _github = github;

    [McpServerTool, Description("Returns the most recent N commits on the given branch.")]
    public async Task<IReadOnlyList<CommitSummary>> GetRecentCommits(
        [Description("owner/repo, e.g. dotnet/runtime")] string repo,
        [Description("Branch name, default main")] string branch = "main",
        [Description("Max commits to return (1-100)")] int take = 20,
        CancellationToken ct = default)
    {
        var (owner, name) = repo.Split('/') is [var o, var n] ? (o, n) : throw new ArgumentException("repo must be owner/name");
        return await _github.GetCommitsAsync(owner, name, branch, Math.Clamp(take, 1, 100), ct);
    }
}

Constructor injection works because tool types are registered into the host's DI container. The SDK resolves a fresh scope per tool invocation; do not capture HttpContext or scoped services in static fields.

Step 3 — Wire up the host

Stdio (local CLI servers)

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = Host.CreateApplicationBuilder(args);

builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
builder.Services.AddSingleton<IGitHubClient, GitHubClient>();

builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();

await builder.Build().RunAsync();

Critical: in stdio mode, never write logs to stdout. The transport multiplexes JSON-RPC frames over stdout; any stray Console.WriteLine corrupts the stream and the client disconnects with a parse error.

Streamable HTTP (remote / multi-tenant)

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithToolsFromAssembly();

builder.Services.AddAuthentication("Bearer")
    .AddJwtBearer(o =>
    {
        o.Authority = "https://login.example.com";
        o.Audience  = "mcp://code-insights";
    });

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapMcp().RequireAuthorization();
app.Run();

The MapMcp() extension wires the root endpoint for Streamable HTTP. If you need the legacy SSE endpoint for older clients, set EnableLegacySse = true on the HTTP options — the property is marked obsolete and slated for removal in a future major.

Step 4 — Test against a real client

The fastest feedback loop is the official MCP Inspector:

npx @modelcontextprotocol/inspector dotnet run --project ./CodeInsightsServer.csproj

For VS Code + GitHub Copilot, drop an .vscode/mcp.json into your workspace:

{
  "servers": {
    "CodeInsightsServer": {
      "type": "stdio",
      "command": "dotnet",
      "args": ["run", "--project", "CodeInsightsServer.csproj"]
    }
  }
}

Open Copilot Chat, switch to Agent mode, click the tools icon, and your server should appear with each [McpServerTool] method listed.

Step 5 — Pack and publish to NuGet

Since v1.0 the recommended distribution channel is NuGet.org with the mcpserver package type. Clients then launch your server via the dnx command shipped with .NET 10:

dotnet pack -c Release
dotnet nuget push bin/Release/*.nupkg \
  --api-key <your-api-key> \
  --source https://api.nuget.org/v3/index.json

Push every .nupkg the build emits — the template produces one tool package plus several runtime-specific packages (linux-x64, linux-arm64, osx-arm64, win-x64) so cross-platform clients can resolve a native AOT binary.

End users add your server with:

{
  "servers": {
    "CodeInsights": {
      "type": "stdio",
      "command": "dnx",
      "args": ["YourOrg.CodeInsightsServer@1.0.0", "--yes"]
    }
  }
}

How to choose: stdio vs Streamable HTTP

If…UseWhy
Server runs on the user's machine, single-tenantstdioZero networking, no auth, native AOT under 50 ms cold start
Server wraps secrets the user already has locally (git, kubectl, AWS profile)stdioInherits the user's credential context safely
Multiple users, hosted somewhereStreamable HTTP + JWTOAuth flow, per-user scopes, horizontal scaling
Long-running tools (minutes-to-hours)Streamable HTTPv1.0 resumption via ISseEventStreamStore / DistributedCacheEventStreamStore
Internal tool, behind corp VPN, single binaryEither — pick stdioLess infra to operate
Need to expose a server to ChatGPT Desktop or hosted ClaudeStreamable HTTPHosted clients can't spawn local processes

Performance and footprint (April 2026)

Numbers below are reproducible on a Ryzen 7 7840U / 32 GB / Linux 6.8 dev box with the random-number sample from the official template; they are not vendor benchmarks, just useful order-of-magnitude anchors.

ConfigurationCold startTool call p50Binary / image
stdio, framework-dependent (.NET 10)~180 ms~3 msn/a (uses host SDK)
stdio, self-contained, trimmed~110 ms~3 ms~28 MB
stdio, Native AOT~40 ms~2 ms~14 MB single file
Streamable HTTP, Kestrel, JWT auth~310 ms~5 ms (loopback)~220 MB container image

The protocol itself is JSON-RPC 2.0 over a framed stream — overhead is dominated by your tool's own work, not by MCP. If a tool call takes longer than ~30 seconds, return progress notifications via the long-running request API rather than blocking the channel.

Common pitfalls and how to fix them

  • "The command 'dnx' was not found." Install the .NET 10 SDK; dnx ships with it. Older SDKs cannot launch NuGet-distributed MCP servers.
  • Client connects, then immediately disconnects with a JSON parse error (stdio). A library you're calling is writing to stdout. Audit Console.WriteLine and library default loggers; route everything to stderr.
  • Tool not invoked even though it's listed. Copilot and Claude pick tools based on description quality. Rewrite the [Description] as a verb phrase ("Returns recent commits…"), and reference #tool_name in the prompt when testing.
  • JWT bearer rejects requests with "audience invalid". The audience claim must match the Audience string in AddJwtBearer. MCP clients put the server URL there by default; use that as the audience or override it during token exchange.
  • Legacy /sse endpoint stopped working after upgrading to 1.2.0. SSE is opt-in now. Set EnableLegacySse = true on the HTTP options for one release while you migrate clients to the streamable transport on the root path.
  • DI scope disposed mid-tool-call. Don't capture IServiceProvider in tool-class fields. Inject the dependencies directly; the SDK creates a per-call scope.
  • Native AOT trim warnings on System.Text.Json. Add a JsonSerializerContext for your tool DTOs; the SDK accepts a custom JsonTypeInfoResolver.

Security checklist

  • Validate every tool parameter — the LLM is an untrusted caller. Treat tool inputs the same way you'd treat anonymous web form fields.
  • Use incremental scope consent (1.0+): request the minimum OAuth scope per tool, return 403 with a WWW-Authenticate hint when more is needed.
  • Never embed secrets in server.json; declare them as environmentVariables with isSecret: true.
  • For destructive tools (file delete, money-moving, code-merge), surface a confirmation step via elicitation rather than relying on the client to ask.
  • Rate-limit per-user on the HTTP transport; the SDK exposes the resolved principal via HttpContext.User inside a tool.

FAQ

Do I still need --prerelease when adding the package?

No. ModelContextProtocol 1.0 was the first stable release on Feb 25, 2026, and 1.2.0 is the current stable as of late March 2026. dotnet add package ModelContextProtocol is enough.

Which .NET versions are supported?

The SDK assemblies target netstandard2.0, so they run on .NET 8 LTS, .NET 9, and .NET 10. The project templates require .NET 10 SDK, and dnx-launched servers also require a .NET 10 SDK on the consuming machine.

Is SSE dead?

The legacy SSE endpoint is. Streamable HTTP — which uses HTTP POST plus an optional event stream on the same root URL — replaced it. Clients written against the 2024 SSE pattern need a small update; the SDK keeps SSE behind EnableLegacySse for migration.

Can my server also be an MCP client?

Yes. The same ModelContextProtocol package exposes McpClientFactory, and you can compose it with Microsoft.Extensions.AI to chain tools across servers. This is how aggregator/proxy servers are built.

How do I distribute a server inside a company without NuGet.org?

Push the package to a private NuGet feed (Azure Artifacts, GitHub Packages, BaGet) and have clients add --add-source in their mcp.json args. For air-gapped setups, ship the self-contained binary and use a plain command/args entry pointing at it.

Does it work on Linux ARM64 and Apple Silicon?

Yes. dotnet pack on the template emits linux-arm64 and osx-arm64 RID-specific packages. Native AOT publish supports both.

What about authentication for stdio servers?

Stdio servers inherit the trust of the local user — there is no in-band auth. If a tool wraps an external API, read the key from an environment variable declared in server.json, never from a baked-in constant.

How does this compare to building an MCP server in TypeScript or Python?

Feature parity is high across the three official SDKs. C# wins on cold start (Native AOT), strong typing of tool schemas, and ASP.NET Core's mature auth/observability story. TypeScript wins on minimal boilerplate for prototypes; Python wins where the rest of your AI/ML stack already lives.

References and further reading


Found a stale fact or a better pattern? Open an issue on the csharp-sdk repo, or get in touch with Codersera if you'd like help implementing this in your stack.