{"slug": "mcp-deep-dive-part-12-building-mcp-servers-in-c-and-net-9-the-sdk-di-and-native", "title": "MCP Deep Dive, Part 12: Building MCP Servers in C# and .NET 9 — The SDK, DI, and Native AOT", "summary": "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.", "body_md": "We've built servers, clients, tools, auth, and governance across this series — all in C#, without ever slowing down to look at\n\nhowthe .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.\n\nThis 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.\n\n| Concern | Hand-rolled (before) | MCP C# SDK (after) |\n|---|---|---|\n| Protocol | parse JSON-RPC by hand |\n`AddMcpServer()` owns it |\n| Tool schema | hand-written JSON Schema | auto from the `[McpServerTool]` method |\n| Dependencies | new-up / statics | constructor DI, scoped per call |\n| Transport | locked | stdio host or ASP.NET Core, same tools |\n| Testing | run the whole agent | unit-test methods + in-memory transport |\n| Cold start | fat JIT | trimming / Native AOT |\n\nQuick 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.\n\n```\nYOU WRITE                              THE SDK DOES\n---------                              -----------\nProgram.cs (AddMcpServer,        ->    JSON-RPC framing + initialize + negotiation\n  transport, WithTools...)             tools/list, resources/list (with pagination)\n[McpServerToolType] classes      ->    reflect method signatures -> JSON Schema\n[McpServerTool] methods          ->    dispatch tools/call -> resolve tool from DI -> invoke\nDI registrations (your domain)   ->    scoped instance per call, CancellationToken threaded\n```\n\nAdd two NuGet packages and let the SDK own JSON-RPC, `initialize`\n\n, capability negotiation, and schema generation.\n\n```\n<PackageReference Include=\"ModelContextProtocol\" Version=\"*\" />\n<PackageReference Include=\"ModelContextProtocol.AspNetCore\" Version=\"*\" />\njs\nbuilder.Services\n    .AddMcpServer(o => o.ServerInfo = new() { Name = \"mattrx-analytics\", Version = \"2.4.0\" })\n    .WithHttpTransport()\n    .WithToolsFromAssembly();   // discover every [McpServerToolType] in this assembly\n```\n\n`WithToolsFromAssembly()`\n\nreflects your attributed classes into tools and wires the JSON-RPC dispatch; you register capabilities, not plumbing.\n\nAttribute a typed method; the SDK generates the schema from the signature.\n\n```\n[McpServerToolType]\npublic sealed class AnalyticsTools(ICampaignQueries campaigns, AiPrincipal principal)\n{\n    [McpServerTool(Name = \"get_campaign_kpis\")]\n    [Description(\"Return a campaign's KPI snapshot for a date range.\")]\n    public async Task<CampaignKpis> GetCampaignKpis(\n        [Description(\"Campaign id (GUID) in the caller's tenant.\")] string campaignId,\n        [Description(\"ISO-8601 range, e.g. 2026-06-01/2026-06-30.\")] string range,\n        CancellationToken ct)\n        => await campaigns.GetKpisAsync(principal.TenantId, campaignId, DateRange.Parse(range), ct);\n}\n```\n\nThis is the .NET killer feature. Your typed signature *is* the schema — parameters become properties, non-nullable parameters become `required`\n\n, and `[Description]`\n\nbecomes the descriptions the model reads. There is no hand-written, drift-prone JSON Schema to maintain.\n\nConstructor injection. The SDK resolves a **scoped** tool instance per call from the same DI container as the rest of your app.\n\n```\nbuilder.Services.AddScoped<ICampaignQueries, CampaignQueries>();\nbuilder.Services.AddScoped(sp => sp.GetRequiredService<IPrincipalAccessor>().Current); // AiPrincipal\n// AnalyticsTools' constructor params (ICampaignQueries, AiPrincipal) are injected per call.\n```\n\nA 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`\n\n, and a scoped `DbContext`\n\nyou already have. The MCP server becomes a thin governed surface, not a re-implementation of your backend. Because tools inject the existing `ICampaignQueries`\n\n, exposing the analytics domain over MCP added almost **no new query code**.\n\nThe **same** `AnalyticsTools`\n\nhosts two ways: a console app over stdio, or an ASP.NET Core app over Streamable HTTP.\n\n```\n// stdio host — a console app for local dev / desktop tools. Same AnalyticsTools.\nvar builder = Host.CreateApplicationBuilder(args);\nbuilder.Services.AddMcpServer().WithStdioServerTransport().WithToolsFromAssembly();\nawait builder.Build().RunAsync();\n// HTTP host — ASP.NET Core for production / multi-tenant. Same AnalyticsTools.\nvar builder = WebApplication.CreateBuilder(args);\nbuilder.Services.AddMcpServer().WithHttpTransport().WithToolsFromAssembly();\nvar app = builder.Build();\napp.MapMcp(\"/mcp\");\napp.Run();\n```\n\nWrite 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.\n\nTools are plain methods — unit-test them directly — and an in-memory transport integration-tests the whole server without a model or a network.\n\n```\n// Unit test: a tool is an ordinary method with injected fakes.\n[Fact]\npublic async Task GetCampaignKpis_is_tenant_scoped()\n{\n    var tools = new AnalyticsTools(new FakeCampaignQueries(), TestPrincipal);\n    var kpis  = await tools.GetCampaignKpis(\"4821\", \"2026-06-01/2026-06-30\", default);\n    Assert.Equal(0.021, kpis.Ctr);\n}\n\n// Integration test: connect a client to the server over an in-memory transport.\n[Fact]\npublic async Task Server_lists_and_calls_tools_over_the_protocol()\n{\n    await using var client = await McpTestHost.ConnectInMemoryAsync<AnalyticsTools>();\n    var tools = await client.ListToolsAsync();\n    Assert.Contains(tools, t => t.Name == \"get_campaign_kpis\");\n}\n```\n\nBecause tools are DI methods, the logic unit-tests like any service — no protocol, no model. And an in-memory transport exercises `tools/list`\n\nand `tools/call`\n\nend to end, fast and deterministic.\n\nTrim and (where trim-safe) Native-AOT-publish into a small, fast-starting container.\n\n```\n<PropertyGroup>\n  <PublishTrimmed>true</PublishTrimmed>\n  <InvariantGlobalization>true</InvariantGlobalization>\n  <!-- Native AOT for the fastest cold start on scale-out. VALIDATE trim/AOT support first. -->\n  <PublishAot>true</PublishAot>\n</PropertyGroup>\n```\n\nAn 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`\n\n. Don't cargo-cult it.\n\n**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.\n\nThree habits that make .NET MCP servers clean:\n\n`[Description]`\n\nis the contract — never hand-write JSON Schema.`CancellationToken`\n\n, 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.*", "url": "https://wpnews.pro/news/mcp-deep-dive-part-12-building-mcp-servers-in-c-and-net-9-the-sdk-di-and-native", "canonical_source": "https://dev.to/kirandeepjassalcrypto/mcp-deep-dive-part-12-building-mcp-servers-in-c-and-net-9-the-sdk-di-and-native-aot-4660", "published_at": "2026-07-26 18:02:01+00:00", "updated_at": "2026-07-26 18:29:23.845346+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "ai-agents"], "entities": ["MCP C# SDK", ".NET 9", "ModelContextProtocol", "ModelContextProtocol.AspNetCore"], "alternates": {"html": "https://wpnews.pro/news/mcp-deep-dive-part-12-building-mcp-servers-in-c-and-net-9-the-sdk-di-and-native", "markdown": "https://wpnews.pro/news/mcp-deep-dive-part-12-building-mcp-servers-in-c-and-net-9-the-sdk-di-and-native.md", "text": "https://wpnews.pro/news/mcp-deep-dive-part-12-building-mcp-servers-in-c-and-net-9-the-sdk-di-and-native.txt", "jsonld": "https://wpnews.pro/news/mcp-deep-dive-part-12-building-mcp-servers-in-c-and-net-9-the-sdk-di-and-native.jsonld"}}