cd /news/developer-tools/mcp-deep-dive-part-12-building-mcp-s… · home topics developer-tools article
[ARTICLE · art-74512] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

MCP Deep Dive, Part 12: Building MCP Servers in C# and .NET 9 — The SDK, DI, and Native AOT

A developer building MCP servers in C# and .NET 9 details how the MCP C# SDK simplifies protocol handling, schema generation, and dependency injection. The SDK automatically generates JSON Schema from attributed methods and supports Native AOT for cold-start optimization, making it a high-leverage tool for .NET backends.

read5 min views1 publishedJul 26, 2026

We've built servers, clients, tools, auth, and governance across this series — all in C#, without ever slowing down to look at

howthe .NET tooling makes it pleasant. This part does. If your backend is already .NET, an MCP server turns out to be one of the highest-leverage things you can build: a thin, attributed, DI-native surface over the domain services you already own.

This is Part 12 of a 15-part deep dive on Model Context Protocol (MCP). Part 3 built a production server treating C# as the vehicle; this part is about the vehicle itself — the MCP C# SDK, the attribute-to-schema model, dependency injection, the two hosting models, testing, and Native AOT.

Concern Hand-rolled (before) MCP C# SDK (after)
Protocol parse JSON-RPC by hand
AddMcpServer() owns it
Tool schema hand-written JSON Schema auto from the [McpServerTool] method
Dependencies new-up / statics constructor DI, scoped per call
Transport locked stdio host or ASP.NET Core, same tools
Testing run the whole agent unit-test methods + in-memory transport
Cold start fat JIT trimming / Native AOT

Quick honesty first: the Python and TypeScript SDKs are the most mature MCP ecosystems, and if your stack is Python/TS, use those. The reason to build MCP servers in .NET is singular but decisive — your domain already lives there. An MCP server should sit next to the data and logic it exposes and reuse them; rewriting a C# domain in Python just to speak MCP is pure waste.

YOU WRITE                              THE SDK DOES
---------                              -----------
Program.cs (AddMcpServer,        ->    JSON-RPC framing + initialize + negotiation
  transport, WithTools...)             tools/list, resources/list (with pagination)
[McpServerToolType] classes      ->    reflect method signatures -> JSON Schema
[McpServerTool] methods          ->    dispatch tools/call -> resolve tool from DI -> invoke
DI registrations (your domain)   ->    scoped instance per call, CancellationToken threaded

Add two NuGet packages and let the SDK own JSON-RPC, initialize

, capability negotiation, and schema generation.

<PackageReference Include="ModelContextProtocol" Version="*" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="*" />
js
builder.Services
    .AddMcpServer(o => o.ServerInfo = new() { Name = "mattrx-analytics", Version = "2.4.0" })
    .WithHttpTransport()
    .WithToolsFromAssembly();   // discover every [McpServerToolType] in this assembly

WithToolsFromAssembly()

reflects your attributed classes into tools and wires the JSON-RPC dispatch; you register capabilities, not plumbing.

Attribute a typed method; the SDK generates the schema from the signature.

[McpServerToolType]
public sealed class AnalyticsTools(ICampaignQueries campaigns, AiPrincipal principal)
{
    [McpServerTool(Name = "get_campaign_kpis")]
    [Description("Return a campaign's KPI snapshot for a date range.")]
    public async Task<CampaignKpis> GetCampaignKpis(
        [Description("Campaign id (GUID) in the caller's tenant.")] string campaignId,
        [Description("ISO-8601 range, e.g. 2026-06-01/2026-06-30.")] string range,
        CancellationToken ct)
        => await campaigns.GetKpisAsync(principal.TenantId, campaignId, DateRange.Parse(range), ct);
}

This is the .NET killer feature. Your typed signature is the schema — parameters become properties, non-nullable parameters become required

, and [Description]

becomes the descriptions the model reads. There is no hand-written, drift-prone JSON Schema to maintain.

Constructor injection. The SDK resolves a scoped tool instance per call from the same DI container as the rest of your app.

builder.Services.AddScoped<ICampaignQueries, CampaignQueries>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IPrincipalAccessor>().Current); // AiPrincipal
// AnalyticsTools' constructor params (ICampaignQueries, AiPrincipal) are injected per call.

A tool is a class with constructor dependencies, resolved scoped to the call exactly like an MVC controller — so it reuses the domain services, the request's AiPrincipal

, and a scoped DbContext

you already have. The MCP server becomes a thin governed surface, not a re-implementation of your backend. Because tools inject the existing ICampaignQueries

, exposing the analytics domain over MCP added almost no new query code.

The same AnalyticsTools

hosts two ways: a console app over stdio, or an ASP.NET Core app over Streamable HTTP.

// stdio host — a console app for local dev / desktop tools. Same AnalyticsTools.
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMcpServer().WithStdioServerTransport().WithToolsFromAssembly();
await builder.Build().RunAsync();
// HTTP host — ASP.NET Core for production / multi-tenant. Same AnalyticsTools.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer().WithHttpTransport().WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp("/mcp");
app.Run();

Write the capability once; choose the host by deployment. The ASP.NET Core app is where the MCP endpoint composes with the auth, OTel, and health-check middleware you already run.

Tools are plain methods — unit-test them directly — and an in-memory transport integration-tests the whole server without a model or a network.

// Unit test: a tool is an ordinary method with injected fakes.
[Fact]
public async Task GetCampaignKpis_is_tenant_scoped()
{
    var tools = new AnalyticsTools(new FakeCampaignQueries(), TestPrincipal);
    var kpis  = await tools.GetCampaignKpis("4821", "2026-06-01/2026-06-30", default);
    Assert.Equal(0.021, kpis.Ctr);
}

// Integration test: connect a client to the server over an in-memory transport.
[Fact]
public async Task Server_lists_and_calls_tools_over_the_protocol()
{
    await using var client = await McpTestHost.ConnectInMemoryAsync<AnalyticsTools>();
    var tools = await client.ListToolsAsync();
    Assert.Contains(tools, t => t.Name == "get_campaign_kpis");
}

Because tools are DI methods, the logic unit-tests like any service — no protocol, no model. And an in-memory transport exercises tools/list

and tools/call

end to end, fast and deterministic.

Trim and (where trim-safe) Native-AOT-publish into a small, fast-starting container.

<PropertyGroup>
  <PublishTrimmed>true</PublishTrimmed>
  <InvariantGlobalization>true</InvariantGlobalization>
  <!-- Native AOT for the fastest cold start on scale-out. VALIDATE trim/AOT support first. -->
  <PublishAot>true</PublishAot>
</PropertyGroup>

An MCP server on Container Apps scales out with demand, so cold start is a real cost. Trimming shrinks the image and Native AOT slashes startup — with one caveat: reflection-based schema generation isn't automatically trim-safe, so validate (or lean on source generators) before flipping PublishAot

. Don't cargo-cult it.

The MCP C# SDK is at its best when the server is a thin, attributed, DI-native surface over domain services you already own. Write the tool as a typed method, let the SDK generate the schema and own the protocol, host it over stdio or ASP.NET Core, and test it as plain methods. The value of .NET here isn't a new framework to learn — it's reusing the one you already have, which is exactly why an MCP server is such high leverage on an existing .NET backend.

Three habits that make .NET MCP servers clean:

[Description]

is the contract — never hand-write JSON Schema.CancellationToken

, and leave the logic in the domain.Originally published at prepstack.co.in. Part 13 takes this server to production infrastructure: hosting MCP on Azure at real scale.

── more in #developer-tools 4 stories · sorted by recency
── more on @mcp c# sdk 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/mcp-deep-dive-part-1…] indexed:0 read:5min 2026-07-26 ·