Menu

AI

How to Build an MCP Server in C# and .NET (Official SDK, 2026)

Build an MCP server in C# with the official ModelContextProtocol 2.3.0 packages: attribute tools, stdio, stateless ASP.NET Core HTTP and NuGet publishing.

By: Irina ShvayaNovember 2, 2026

How to Build an MCP Server in C# and .NET (Official SDK, 2026)

This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.

You build an MCP server in C# with the official ModelContextProtocol NuGet package. You add it to a .NET console app, mark a static class with [McpServerToolType] and its methods with [McpServerTool], then wire it up with AddMcpServer().WithStdioServerTransport().WithToolsFromAssembly(). For a remote server, the ModelContextProtocol.AspNetCore package adds MapMcp() for Streamable HTTP.

This guide shows which package to pick, a minimal server, a second tool with proper errors, testing with MCP Inspector, HTTP hosting and NuGet publishing. It assumes you know C# and the .NET generic host.

Code was checked against the C# SDK getting-started and tools docs (SDK 2.3.0) and Microsoft's quickstart on 2 November 2026. eSEOspace did not build or run it, because no .NET SDK was available for this article. Run dotnet build and the Inspector checks yourself.

Key Takeaways

  • The official C# SDK ships as NuGet packages; as of November 2026 ModelContextProtocol and ModelContextProtocol.AspNetCore are at 2.3.0.
  • Start with ModelContextProtocol for stdio servers; add ModelContextProtocol.AspNetCore for HTTP.
  • WithToolsFromAssembly() finds every [McpServerToolType] class and registers each [McpServerTool] method.
  • Exceptions in tools become IsError results; only McpException messages reach the client, so other exceptions show a generic message.
  • In ASP.NET Core, stateless session mode is the default and the setting that serves 2026-07-28 clients natively.
  • Send all console logs to stderr in stdio servers; Console.WriteLine corrupts the protocol stream.

Prerequisites

  • .NET SDK. The official build-a-server tutorial C# tab asks for .NET 8 or higher. Microsoft's project template and the dnx runner need .NET 10, per Microsoft's quickstart.
  • Node.js to run MCP Inspector through npx.
  • An MCP host such as Claude Desktop, VS Code with GitHub Copilot, or Claude Code.

For MCP basics, start with how to build your first MCP server. This page covers only the .NET specifics.

Which package to install

The C# SDK README lists three core packages plus extensions:

PackageUse it when
ModelContextProtocol.CoreYou need only the client or low-level server APIs, with the fewest dependencies
ModelContextProtocolYou are building a stdio server or a client and want hosting, dependency injection and attribute discovery
ModelContextProtocol.AspNetCoreYou are building an HTTP server hosted in ASP.NET Core
ModelContextProtocol.Extensions.Apps, .Skills, .TasksYou need the MCP Apps, Skills or Tasks extensions

The getting-started guide calls ModelContextProtocol "the right starting point for most projects." On 2 November 2026 the NuGet feed showed 2.3.0 as the newest version of both ModelContextProtocol and ModelContextProtocol.AspNetCore; the GitHub release v2.3.0 is dated 20 October 2026.

The official SDK page lists C# as Tier 1. See our official SDKs overview for what that means.

Option A: scaffold with Microsoft's template

If you have .NET 10, Microsoft's template creates a ready project with a sample get_random_number tool and a server.json for publishing:

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

Microsoft notes the template package is "currently in preview." Its options let you choose a local (stdio) or remote (http) transport and native AOT or self-contained publishing.

Option B: a minimal server by hand

The getting-started guide builds the same thing from an empty console app:

dotnet new console
dotnet add package ModelContextProtocol
dotnet add package Microsoft.Extensions.Hosting

Replace Program.cs with:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
using System.ComponentModel;

var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(consoleLogOptions =>
{
    // Configure all logs to go to stderr
    consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
    .AddMcpServer()
    .WithStdioServerTransport()
    .WithToolsFromAssembly();
await builder.Build().RunAsync();

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

What each part does:

  • Host.CreateApplicationBuilder gives you the standard .NET generic host with logging and dependency injection.
  • LogToStandardErrorThreshold = LogLevel.Trace sends every log line to stderr, keeping stdout clean for JSON-RPC.
  • AddMcpServer() registers the server, WithStdioServerTransport() serves stdin and stdout, and WithToolsFromAssembly() discovers your tools.
  • [Description] on the method becomes the tool description the model reads. Parameters become the input schema.

The guide says prompts and resources work the same way with [McpServerPromptType]/[McpServerPrompt] and [McpServerResourceType]/[McpServerResource].

Add a tool with real errors

Add a second class to the same project. [Description] on each parameter documents the argument for the model:

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

[McpServerToolType]
public static class InventoryTools
{
    private static readonly Dictionary<string, int> Stock = new()
    {
        ["TSHIRT-M"] = 42,
        ["MUG"] = 17,
    };

    [McpServerTool, Description("Returns the number of units in stock for one product SKU.")]
    public static string CheckStock(
        [Description("The product SKU, for example TSHIRT-M")] string sku)
    {
        if (!Stock.TryGetValue(sku, out var qty))
        {
            throw new McpException($"Unknown SKU '{sku}'.");
        }
        return $"{qty} units of {sku} in stock.";
    }
}

Why McpException? The C# tools doc says a thrown exception becomes a CallToolResult with IsError = true. If the exception derives from McpException, its message is included; otherwise "a generic message is returned to avoid leaking internal details." An ArgumentException, for example, reaches the model only as "An error occurred invoking" the tool.

For protocol-level problems, throw McpProtocolException with a code such as McpErrorCode.InvalidParams. That becomes a JSON-RPC error rather than a tool result.

Test with MCP Inspector

Point the Inspector CLI at your project. Flags come from the Inspector CLI README:

dotnet build
npx @modelcontextprotocol/inspector --cli dotnet run --project . --no-build --method tools/list
npx @modelcontextprotocol/inspector --cli dotnet run --project . --no-build \
  --method tools/call --tool-name check_stock --tool-arg sku=MUG

In Microsoft's quickstart, a method named GetCityWeather shows up as the tool get_city_weather, so expect CheckStock to appear as check_stock. Confirm the exact name in the tools/list output before you call it. Try --protocol-era legacy and --protocol-era modern to check both the old handshake and the 2026-07-28 model. Drop --cli for the web UI, covered in our Inspector lesson.

Connect it to a host

The official tutorial's C# tab uses dotnet run in the Claude Desktop config:

{
  "mcpServers": {
    "inventory": {
      "command": "dotnet",
      "args": ["run", "--project", "/ABSOLUTE/PATH/TO/PROJECT", "--no-build"]
    }
  }
}

The file is claude_desktop_config.json in ~/Library/Application Support/Claude/ on macOS or $env:AppData\Claude\ on Windows. Microsoft's quickstart shows the VS Code equivalent with a servers block and "type": "stdio". The MCP config generator writes either format.

Serve over Streamable HTTP with ASP.NET Core

For a remote server, start from dotnet new web and add ModelContextProtocol.AspNetCore. The getting-started guide's HTTP example:

using ModelContextProtocol.AspNetCore;
using ModelContextProtocol.Server;
using System.ComponentModel;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer()
    .WithHttpTransport(options =>
    {
        options.SessionMode = HttpServerSessionMode.Stateless;
    })
    .WithToolsFromAssembly();
var app = builder.Build();

app.MapMcp();

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

The SDK's stateless and stateful mode doc says Stateless is now the default and "the forward-compatible choice." In Stateful mode, a 2026-07-28 request is refused with -32022 UnsupportedProtocolVersion and the client must fall back to the old handshake. A third mode, StatefulForInitializeClients, gives sessions to old clients while serving 2026-07-28 clients statelessly. The doc recommends setting SessionMode explicitly so a future default change cannot surprise you.

Two security notes from the getting-started guide: Kestrel does not validate Host headers by default, so set AllowedHosts to known names rather than "*" to resist DNS rebinding attacks. And enable CORS only if you truly need browser access.

Publish to NuGet

Microsoft's quickstart packs and pushes a stdio server as a NuGet tool package:

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

Users can then run it with dnx <PackageId>@<version> --yes, which ships with .NET 10. NuGet.org reads the template's server.json to generate a VS Code config for your package. The file follows the official MCP Registry's server.json schema.

Common pitfalls

  • Writing to stdout. The official tutorial says never use Console.WriteLine() or Console.Write() in a stdio server. Keep the stderr logging setting.
  • Throwing plain exceptions for user-facing errors. Use McpException when you want the model to see the message.
  • Stateful by habit. Older samples set stateful mode. Under 2026-07-28 that forces clients back to the legacy handshake.
  • Nullable ServerInfo in clients. The v2.3.0 release notes say McpClient.ServerInfo is now nullable, because 2026-07-28 servers may omit identity.
  • Legacy SSE. The stateless doc says the legacy SSE endpoint is off by default and needs stateful mode plus EnableLegacySse.
  • Missing dnx. Microsoft's quickstart says that error means you need the .NET 10 SDK.

Next step

Compare this with the Go guide or the Java guide, or start the free MCP course. If your team wants a production .NET server designed, secured and shipped, see eSEOspace's MCP development service.

More in the MCP Guide

Free tools: server.json Validator · MCP Error Code Lookup

Latest research: State of MCP: October 2026

Frequently asked questions

Which NuGet package do I need for an MCP server?

Use ModelContextProtocol for a stdio server and add ModelContextProtocol.AspNetCore for HTTP. ModelContextProtocol.Core is for clients or low-level servers that want minimal dependencies.

Which .NET version does the C# MCP SDK need?

The official build-a-server tutorial lists .NET 8 or higher. Microsoft's MCP server template and the dnx command require the .NET 10 SDK.

Is the C# MCP SDK official?

Yes. It lives in the modelcontextprotocol GitHub organization and is listed as Tier 1 on the official SDK page as of November 2026. It is licensed Apache 2.0.

Why does my tool error show a generic message?

The SDK hides messages from ordinary exceptions to avoid leaking internal details. Throw McpException with a clear message when the model should see why the call failed.

Should my ASP.NET Core MCP server be stateless?

Usually yes. The SDK docs recommend stateless mode unless you need unsolicited notifications, resource subscriptions, per-client state or server-to-client requests with clients that lack 2026-07-28 support.

You Might Also like to Read

Get a FREE Website Audit

Enter your website and get a 20–30 page SEO, GEO, AEO & CRO audit within 24 hours. Completely free.

Don't have a site yet? Get in touch →

Ready to speak with a marketing expert?
Give us a call! (916) 866-7893

Websites Launched
1407
Websites Launched
Marketing Experts
29
Marketing Experts
Hours of Dedicated Work
204533
Hours of Dedicated Work
Irina S.Benjamin G.Jeanette M.David G.