Hi, I'm Farhad — a .NET developer who spends too much time reading changelogs so you don't have to. This is my first post here. The plan is code-first write-ups on .NET, C#, and the AI tooling space. Things that are worth your time, not just worth a tweet.
The official Model Context Protocol C# SDK hit v1.0 on March 5, 2026. If you've been building AI-powered .NET apps, this one matters. It's not just a version bump — the release brings full support for the 2025-11-25 MCP specification and closes some real gaps that have been annoying developers for a while.
Quick recap if you're new to this: MCP (Model Context Protocol) is an open standard for how AI models talk to external tools and data sources. Think of it as a USB-C port for LLMs — one standard, many devices. The C# SDK is maintained jointly by Anthropic and Microsoft and handles enough of the plumbing that you can focus on your tools rather than the protocol.
The Packages
Three NuGet packages, each with a different scope:
ModelContextProtocol— the main one. Hosting extensions, dependency injection, everything you need for most scenarios.ModelContextProtocol.AspNetCore— for HTTP-based servers in ASP.NET Core. Adds SSE transport and theMapMcp()extension.ModelContextProtocol.Core— stripped down. Just the client and low-level server APIs, no hosting overhead.
All three target netstandard2.0 and run on .NET 8+.
Getting a Server Running 🚀
Haven't built an MCP server in C# yet? Here's the short version. Create an ASP.NET Core project, add ModelContextProtocol.AspNetCore, and wire it up:
using ModelContextProtocol.Server;
using System.ComponentModel;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp();
app.Run("http://localhost:3001");
[McpServerToolType]
public static class WeatherTools
{
[McpServerTool, Description("Gets the current weather for a given city.")]
public static string GetWeather(string city) =>
$"The weather in {city} is sunny and 22°C."; // swap in your real API call
}
WithToolsFromAssembly() picks up every class tagged [McpServerToolType] and registers its [McpServerTool] methods. MapMcp() adds the /sse and /messages endpoints. That's it.
Connecting a client is just as short:
using ModelContextProtocol.Client;
using Microsoft.Extensions.AI;
var transport = new SseClientTransport(new SseClientTransportOptions
{
Endpoint = new Uri("http://localhost:3001/sse")
});
await using var client = await McpClientFactory.CreateAsync(transport);
var tools = await client.ListToolsAsync();
Now let's get to what's actually new.
Authorization Gets a Serious Upgrade
Auth was always the weak spot in earlier MCP releases. V1.0 fixes that with two additions: discovery of Protected Resource Metadata and Client ID Metadata Documents (CIMDs).
Protected Resource Metadata lets a server tell clients which Authorization Server protects it. Before this, clients had to already know the Auth Server URL — you had to configure it manually. Now the server exposes that info directly, and the SDK's client automatically picks it up. Nothing to configure on your end.
CIMDs are the more interesting change. They replace Dynamic Client Registration (DCR) as the preferred method for establishing client identity with an Authorization Server. Instead of registering dynamically at runtime, the client gives a URL as its client_id. That URL points to a JSON document that the client hosts — it contains the client's metadata:
{
"client_id": "https://myapp.example.com/.well-known/mcp-client",
"client_name": "My .NET MCP Client",
"redirect_uris": ["https://myapp.example.com/callback"]
}
If you've done OAuth work before, this feels similar to OpenID Connect client metadata. No registration round-trips before auth can start — you host the document, and any spec-compliant server finds it.
Icons for Tools, Resources, and Prompts
Small feature, but I actually like this one. Tools, resources, and prompts can now have icons. You set them with IconSource on the attribute:
[McpServerToolType]
public static class ImageTools
{
[McpServerTool(IconSource = "https://cdn.example.com/icons/image.png")]
[Description("Resizes an image to the specified dimensions.")]
public static string ResizeImage(string filePath, int width, int height) =>
$"Resized {filePath} to {width}x{height}";
}
Need more control? You can configure multiple icons with MIME types, size hints, and theme preferences. Host applications that build proper UI around MCP — sidebars, tool pickers, agent dashboards — will use this. A named tool with an icon reads very differently from a plain-text entry in a list.
URL Mode Elicitation
Elicitation has been in MCP for a while: servers could ask clients to collect input from the user. The new URL mode takes that a step further. Instead of popping up a form in the MCP client, the server sends the user to a URL — one hosted externally, outside the client entirely.
This is the right solution for OAuth flows, API key collection, payment steps, and anything sensitive. Here's what it looks like server-side:
[McpServerTool]
[Description("Accesses a protected resource. Requires user authorization.")]
public static async Task<string> GetProtectedData(
IMcpServer server,
CancellationToken ct)
{
var elicitResult = await server.RequestElicitationAsync(
new ElicitRequestParams
{
Message = "This action requires you to authorize access.",
Mode = "url",
Url = "https://auth.example.com/authorize?state=abc123",
ElicitationId = Guid.NewGuid().ToString()
},
ct);
if (elicitResult?.Action != "accept")
return "Authorization cancelled.";
return "Access granted. Here's your data.";
}
On the client side, you set up an ElicitationHandler in McpClientOptions. It receives the message and URL and shows them to the user, in whatever way makes sense for your app. The ElicitationId tracks the async flow — the server knows when the out-of-band step completes and can resume.
If you've ever built a tool that wraps a third-party API and wrestled with where to put the credentials, this solves it cleanly. No tokens pasted into chat windows, no hardcoded config. Just a proper OAuth redirect.
Tool Calling in Sampling
Sampling — where a server asks the MCP client to run an LLM call on its behalf — isn't new. What's new is that servers can now pass tools along with that sampling request. The LLM can call those tools while building its response.
Here's the thing that tripped me up when I first read about this: these tools are not the same as the [McpServerTool] methods you've already registered. They share the same structure (name, description, inputSchema), but they're one-offs. The server defines them inline and handles the invocations itself — they exist only for that single sampling call.
var samplingRequest = new CreateMessageRequestParams
{
Messages = [new SamplingMessage { Role = "user", Content = new TextContentBlock { Text = prompt } }],
Tools =
[
new McpClientTool(new Tool
{
Name = "lookup_order",
Description = "Looks up an order by ID and returns its status.",
InputSchema = JsonSerializer.SerializeToElement(new
{
type = "object",
properties = new { orderId = new { type = "string" } },
required = new[] { "orderId" }
})
})
]
};
var result = await server.RequestSamplingAsync(samplingRequest, ct);
The spec describes this as a key addition. I'd agree — once you see that the server can drive an LLM call with its own ephemeral tools, the patterns you can build get a lot more interesting.
Long-Running Requests with SSE Streams ⏱️
HTTP requests have timeouts. Long-running operations don't finish in 30 seconds. Generating reports, processing files, running analysis pipelines — these need something better than "just make the timeout longer."
The fix is a durable SSE stream pattern. When the request kicks off, the server opens an SSE stream and immediately sends an empty event with an Event ID. Then it can close the connection whenever it wants. The client holds onto that Event ID — if the connection drops or the server closes it intentionally, the client reconnects and picks up exactly where things left off.
The SDK ships DistributedCacheEventStreamStore to back this. Configure it like this:
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithToolsFromAssembly();
builder.Services.AddDistributedMemoryCache();
builder.Services.Configure<DistributedCacheEventStreamStoreOptions>(opts =>
{
opts.EventSlidingExpiration = TimeSpan.FromMinutes(10);
opts.MetadataSlidingExpiration = TimeSpan.FromHours(1);
});
There's also experimental support for Tasks in durable state tracking across reconnects. Still marked experimental, but it's clearly the direction things are heading for production agents.
Seeing It in Action
Mike Kistler's mcp-whats-new repo is worth bookmarking — it's built specifically to show these v1.0 features in working code rather than just describing them.
El Bruno's colour picker MCP app shows something I hadn't thought about: MCP tools don't have to be text-in, text-out. His tool returns metadata that links to a resource, which renders an actual colour picker UI in the MCP host. Instead of asking a user to type #3498DB, they get a real picker. Small demo, but it shows what's possible when you stop thinking about tools as pure functions.
What's Next? 🔮
V1.0 is a meaningful milestone. The auth story is solid enough for production now, URL elicitation handles the awkward cases that didn't have clean answers before, and the SSE streaming approach finally gives long-running work somewhere to run.
Two caveats worth knowing: the Tasks API is still experimental, and CIMD adoption is early enough that you won't see it everywhere yet. That said, the spec is locked, the version number is real, and Microsoft isn't going anywhere.
Start with the official docs and the GitHub repo — the samples folder covers most of what you'd want to try first.
Install: dotnet add package ModelContextProtocol and dotnet add package ModelContextProtocol.AspNetCore