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:ModelContextProtocolhit v1.0 on Feb 25, 2026 and the current stable is v1.2.0 (Mar 27, 2026). The--prereleaseflag 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 officialMicrosoft.McpServer.ProjectTemplatesand thednxlauncher; 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 anEnableLegacySseproperty marked obsolete.v1.0 added OAuth authorization-server discovery, Client ID Metadata Documents (CIMD), incremental scope consent, tool sampling, and long-running request resumption viaISseEventStreamStore.Servers can now be packed as NuGet MCP packages and discovered on nuget.org under themcpserverpackage type, launched viadnx.
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
| Question | Short 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
stdiomode. - 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.AIintegration 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.ProjectTemplatesand thednxlauncher 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/inspectorCLI - 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 eitherWithStdioServerTransport()orWithHttpServerTransport()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.jsonCodeInsightsServer.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… | Use | Why |
|---|---|---|
| Server runs on the user's machine, single-tenant | stdio | Zero networking, no auth, native AOT under 50 ms cold start |
| Server wraps secrets the user already has locally (git, kubectl, AWS profile) | stdio | Inherits the user's credential context safely |
| Multiple users, hosted somewhere | Streamable HTTP + JWT | OAuth flow, per-user scopes, horizontal scaling |
| Long-running tools (minutes-to-hours) | Streamable HTTP | v1.0 resumption via ISseEventStreamStore / DistributedCacheEventStreamStore |
| Internal tool, behind corp VPN, single binary | Either — pick stdio | Less infra to operate |
| Need to expose a server to ChatGPT Desktop or hosted Claude | Streamable HTTP | Hosted 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.
| Configuration | Cold start | Tool call p50 | Binary / image |
|---|---|---|---|
| stdio, framework-dependent (.NET 10) | ~180 ms | ~3 ms | n/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;
dnxships 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.WriteLineand 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_namein the prompt when testing. - JWT bearer rejects requests with "audience invalid". The audience claim must match the
Audiencestring inAddJwtBearer. MCP clients put the server URL there by default; use that as the audience or override it during token exchange. - Legacy
/sseendpoint stopped working after upgrading to 1.2.0. SSE is opt-in now. SetEnableLegacySse = trueon 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
IServiceProviderin tool-class fields. Inject the dependencies directly; the SDK creates a per-call scope. - Native AOT trim warnings on
System.Text.Json. Add aJsonSerializerContextfor your tool DTOs; the SDK accepts a customJsonTypeInfoResolver.
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
403with aWWW-Authenticatehint when more is needed. - Never embed secrets in
server.json; declare them asenvironmentVariableswithisSecret: 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.Userinside a tool.
Related Codersera reads
- Creating an MCP server with .NET — a complete guide — the broader .NET-stack walkthrough that complements this C#-focused post.
- C# project ideas — twenty-plus build prompts if you want a concrete tool to wrap in your first MCP server.
- Hire vetted .NET developers if you need extra hands to ship a production MCP server in weeks, not quarters.
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
- Model Context Protocol — Specification, revision 2025-11-25 (modelcontextprotocol.io)
- modelcontextprotocol/csharp-sdk on GitHub — official SDK source, issues, and discussions
- csharp-sdk releases — v1.0 (Feb 25, 2026), v1.1 (Mar 6, 2026), v1.2 (Mar 27, 2026)
- ModelContextProtocol on NuGet Gallery — package metadata and version history
- .NET Blog — Release v1.0 of the official MCP C# SDK (Microsoft, March 2026)
- Microsoft Learn — Quickstart: Create a minimal MCP server in C# and publish to NuGet
- The 2026 MCP Roadmap — official MCP blog
- MCP vs. REST: Why do we need a new protocol? — practitioner discussion on the protocol's design tradeoffs
- Awesome-DotNET-MCP — curated list of .NET MCP servers and resources
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.