Every AI coding agent, chat assistant, and IDE integration needs the same thing: a way to call your services. Not your REST API — your actual domain logic, exposed as discrete tools that an LLM can discover, understand, and invoke. The Model Context Protocol (MCP) standardises exactly this, and the official C# SDK has just reached its v1.0 milestone with full support for the 2025-11-25 specification. If you are building .NET services that AI agents should be able to use, this is the SDK you want.
The v1.0 release, announced on 5 March 2026, is not a minor version bump. It introduces authorization flows, icon metadata, elicitation for sensitive data collection, tool calling within sampling requests, long-running task support, and a three-package architecture that fits everything from a minimal console app to a full ASP.NET Core deployment.
What MCP actually solves
Before diving into code, it helps to understand why MCP exists. Without it, every AI tool integration is bespoke. GitHub Copilot talks to extensions one way, Claude Code talks to tools another way, and your custom agent has its own protocol. MCP provides a single standard: servers expose tools, resources, and prompts; clients discover and invoke them. The transport layer — stdio for local tools, HTTP for remote services — is abstracted away.
The C# SDK makes this concrete for .NET. You decorate methods with attributes, wire up hosting, and your service becomes discoverable by any MCP-compliant client — Visual Studio, VS Code, Claude Code, GitHub Copilot, or a custom agent you build yourself.
The three-package architecture
The SDK ships as three NuGet packages, each building on the last:
- ModelContextProtocol.Core — the minimal surface. Client APIs, low-level server APIs, and the protocol types. Use this when you need the smallest dependency footprint or are building a client only.
- ModelContextProtocol — the recommended starting point. Adds
Microsoft.Extensions.Hostingintegration, dependency injection, and attribute-based discovery via[McpServerToolType]and[McpServerTool]. This is what you want for stdio-based servers. - ModelContextProtocol.AspNetCore — HTTP transport support. Adds
MapMcp()endpoint routing for ASP.NET Core applications, with both stateful and stateless modes.
All three target netstandard2.0, so they work on .NET 8, .NET 9, .NET 10, and .NET 11 without compatibility issues.
Building a stdio server in five minutes
The fastest path to a working MCP server is a console application with stdio transport. This is how most local tool integrations work — the AI client launches your process and communicates over standard input/output.
var builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddConsole(options =>
{
options.LogToStandardErrorThreshold = LogLevel.Trace;
});
builder.Services
.AddMcpServer()
.WithStdioServerTransport()
.WithToolsFromAssembly();
await builder.Build().RunAsync();
Logging goes to stderr because stdout is reserved for the MCP protocol. The WithToolsFromAssembly() call scans for every class marked with [McpServerToolType] and registers its [McpServerTool] methods automatically.
Now define a tool:
[McpServerToolType]
public static class HealthCheckTool
{
[McpServerTool, Description("Checks the health status of a service endpoint.")]
public static async Task<string> CheckHealth(
HttpClient httpClient,
[Description("The URL to check")] string url)
{
try
{
var response = await httpClient.GetAsync(url);
return $"Status: {response.StatusCode} ({(int)response.StatusCode})";
}
catch (HttpRequestException ex)
{
return $"Unreachable: {ex.Message}";
}
}
}
The Description attributes are not decorative — they are what the LLM reads to decide when and how to call your tool. Write them as if you are explaining the tool to a colleague, not documenting an API.
// TIP
The SDK resolves method parameters via dependency injection. Register HttpClient, database contexts, or any other service in the DI container and accept them as parameters alongside the tool's input arguments.
Moving to HTTP with ASP.NET Core
For remote servers — tools running as web services rather than local processes — add the ASP.NET Core package and switch transports:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddMcpServer()
.WithHttpTransport(options =>
{
options.Stateless = true;
})
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp();
app.Run();
The Stateless = true option is worth calling out. In stateless mode, each request is self-contained — no server-side session state. This is the right default for most deployments because it lets you scale horizontally behind a load balancer without worrying about sticky sessions.
When you need server-initiated notifications or long-lived connections, use the default stateful mode instead.
What v1.0 brings to the table
The pre-release versions of the SDK handled the basics — tools, resources, prompts, and transports. The v1.0 release adds the features you need for production.
Authorization
The SDK now supports OAuth 2.0 authorization with Protected Resource Metadata (PRM) discovery. There are three ways to expose your PRM to clients:
- Via a
WWW-Authenticateheader parameter on the MCP endpoint - At a well-known path derived from the MCP endpoint URL
- At the root well-known URL
The AddMcp extension on AuthenticationBuilder handles configuration and automatic discovery hosting. Combined with incremental scope consent — where clients start with minimal permissions and request more as needed — this follows the principle of least privilege throughout.
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
McpAuthenticationDefaults.AuthenticationScheme;
})
.AddMcp()
.AddJwtBearer(options =>
{
options.Authority = "https://login.example.com";
options.Audience = "mcp-api";
});
When a client attempts an operation requiring a scope it does not have, the server responds with 401 Unauthorized or 403 Forbidden and a WWW-Authenticate header listing the required scopes. The client can then request those scopes through the standard OAuth flow.
Icon metadata
Tools, resources, and prompts can now include icons. This is a small thing that makes a large difference in client UIs — a tool with an icon is instantly recognisable in a tool picker.
[McpServerToolType]
public static class DatabaseTool
{
[McpServerTool(IconSource = "https://cdn.example.com/icons/database.svg"),
Description("Executes a read-only SQL query against the reporting database.")]
public static async Task<string> QueryReporting(
ReportingDbContext db,
[Description("The SQL query to execute")] string sql)
{
// Implementation
}
}
For more complex scenarios — multiple icons with MIME types and theme preferences — use McpServerToolCreateOptions.Icons when creating tools programmatically.
Elicitation
Some tools need information they should not receive through the LLM — API keys, confirmation codes, or sensitive input. URL-mode elicitation lets the server redirect the user to a secure form (such as a Razor Page) to collect that data out-of-band, keeping it out of the chat context entirely.
Clients declare support via Capabilities.Elicitation.Url, and the server can then present a URL for the user to complete an interaction. This is the right pattern for anything where you do not want the LLM handling the data.
Tool calling in sampling
This is where things get interesting. Servers can now include tool definitions in sampling requests, allowing the LLM to call tools as part of its reasoning. The flow looks like this:
- Your server sends a sampling request to the client, including available tools
- The LLM processes the request and decides to call a tool
- The client executes the tool call and returns the result
- The LLM incorporates the result and produces a final answer
On the client side, declare support via SamplingCapability.Tools and provide a SamplingHandler. The Microsoft.Extensions.AI package provides a CreateSamplingHandler() method that simplifies LLM integration if you are using IChatClient.
Long-running requests and tasks
Not every tool call completes in milliseconds. The SDK now supports two patterns for operations that take time:
Polling mode lets the server drop the SSE connection and have the client poll for results. Implement ISseEventStreamStore (a reference implementation using IDistributedCache is provided) and call EnablePollingAsync() on the request context from within your tool.
Tasks (experimental) provide durable state tracking. Clients include a Task field with a TimeToLive, and servers return a CreateTaskResult with metadata. The client can then poll, retrieve results, list active tasks, or cancel them.
[McpServerToolType]
public class ReportTool
{
[McpServerTool, Description("Generates a sales report for the specified period.")]
public static async Task<string> GenerateReport(
IMcpTaskStore taskStore,
[Description("Start date (yyyy-MM-dd)")] string from,
[Description("End date (yyyy-MM-dd)")] string to)
{
// Long-running report generation
// The SDK handles task lifecycle automatically for async methods
await Task.Delay(TimeSpan.FromSeconds(30)); // Simulated work
return $"Report generated for {from} to {to}: 1,247 orders, GBP 892,340 revenue.";
}
}
Tools returning Task, ValueTask, Task<T>, or ValueTask<T> automatically advertise task support. You can control this with the ToolTaskSupport enum: Forbidden, Optional, or Required.
Building a client
The SDK is not server-only. Building an MCP client to consume tools from external servers is equally straightforward:
var transport = new StdioClientTransport(new StdioClientTransportOptions
{
Name = "MyToolServer",
Command = "dotnet",
Arguments = ["run", "--project", "../MyToolServer"],
});
var client = await McpClient.CreateAsync(transport);
var tools = await client.ListToolsAsync();
foreach (var tool in tools)
{
Console.WriteLine($"{tool.Name}: {tool.Description}");
}
var result = await client.CallToolAsync(
"CheckHealth",
new Dictionary<string, object?> { ["url"] = "https://api.example.com/health" });
The tools returned from ListToolsAsync() are McpClientTool objects that inherit from AIFunction. Pass them directly to any IChatClient implementation:
IList<McpClientTool> tools = await client.ListToolsAsync();
var response = await chatClient.GetResponseAsync(
"Check whether the payments API is healthy",
new() { Tools = [.. tools] });
This is the integration point between MCP and the broader Microsoft.Extensions.AI ecosystem. Your tools become available to any LLM that speaks through IChatClient.
Common pitfalls
Forgetting that stdout is reserved. In stdio mode, all console output goes through the MCP protocol. If your tool writes to Console.WriteLine, it corrupts the protocol stream. Use the injected ILogger or write to stderr.
Vague tool descriptions. The LLM decides whether to call your tool based on its Description attribute. A description like "Does database stuff" guarantees the LLM will either never call it or call it at the wrong time. Be specific: "Executes a read-only SQL query against the reporting database and returns results as a markdown table."
Parameter descriptions matter too. Each parameter's Description tells the LLM what to pass. Without it, the LLM guesses based on the parameter name alone — and string sql is ambiguous enough to produce wildly incorrect inputs.
Using stateful mode without sticky sessions. If you deploy an HTTP MCP server in stateful mode behind a load balancer, requests from the same client must reach the same server instance. Either configure sticky sessions or switch to stateless mode.
Blocking the tool method. MCP tool methods should be async. A synchronous tool that blocks for 30 seconds ties up a thread and degrades throughput. Use async Task<T> and proper async I/O throughout.
Not registering DI services. The SDK resolves method parameters from the DI container. If your tool accepts an HttpClient but you have not registered IHttpClientFactory, you get a runtime error — not a compile-time one. Register your dependencies explicitly.
Summary
- The MCP C# SDK v1.0 implements the full 2025-11-25 MCP specification, shipping as three NuGet packages:
ModelContextProtocol.Core,ModelContextProtocol, andModelContextProtocol.AspNetCore. - Attribute-based discovery with
[McpServerToolType]and[McpServerTool]makes exposing tools trivial — register in DI, decorate methods, and the SDK handles protocol negotiation. - v1.0 adds OAuth authorization with incremental scope consent, icon metadata, URL-mode elicitation for sensitive data, tool calling within sampling, and long-running task support.
- Stdio transport is the default for local tools; HTTP transport with stateless mode is the right choice for remote services behind a load balancer.
- Client-side,
McpClientToolimplementsAIFunction, bridging MCP tools directly into theMicrosoft.Extensions.AIecosystem and anyIChatClientimplementation. - Write detailed, specific descriptions for tools and parameters — they are the interface between the LLM and your code.