OAuth In The MCP C# SDK: Simple, Secure, Standard
Table of Contents
Authorization: the part of your project that’s always more complicated than you expect, and never quite as interesting as the thing you actually want to build. If you’ve ever tried to wire up OAuth for an app on any platform, I am truly sorry - endless configuration, permissions, registrations, multiple endpoints that are just ever-so-slightly-different between providers for whatever reason, mysterious errors, and a creeping sense that you’re just one copy-paste away from a security hole.
Things did get better in the past few years with quite a few options to help developers worry less about the auth-related plumbing, with all sorts of higher-level libraries and auth proxies that take on the burden of tackling the toil. And now if you’re looking to build either Model Context Protocol (MCP) clients or servers with the MCP C# SDK - you get OAuth 2.1 authorization support built-in. Because it’s the official SDK, it, naturally, follows the official specification (you might’ve heard about it on this very blog).
No more rolling your own spec implementation from scratch. No more cargo-culting code from ancient blog posts. Your MCP servers (and clients, yes) can be secure by default in just a few lines of C# code.
Check out the codeWhat this means for developers #
One of the goals of the new SDK change is to not overcomplicate things: this is standard OAuth 2.1, minus the boilerplate that you’d have to write if you didn’t have the authorization support built-in. The real win is that you don’t have to wrestle with token endpoints, their structure, figuring out what’s a GET
and what’s a POST
, scope configurations, and all this super fun stuff. The SDK handles the gritty details so you can spend your time building features you actually care about, not auth glue.
Because the new MCP authorization specification snapped to support RFC 9728: OAuth 2.0 Protected Resource Metadata, so does the SDK. I don’t expect you to go start reading RFCs, because in 99.99% of cases you don’t need to know everything there, but the gist is that your MCP servers can now hand out a business card to connecting MCP clients that says, “Here’s the authorization server and method I am using.”
The MCP server would host a JSON blob like this to tell the client about its authorization configuration:
{
"resource": "https://api.example.com/v1/",
"authorization_servers": [
"https://auth.example.com"
],
"scopes_supported": [
"read:data",
"write:data"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
]
}
This metadata, colloquially referred to as the “PRM document” (from “Protected Resource Metadata”) lives at a well-known endpoint (.well-known/oauth-protected-resource
), so clients can just look it up and know what to do. Once the client gets the PRM document, they can initiate the authorization server discovery, followed by the token acquisition dance. Still quite a few steps to handle manually.
The MCP C# SDK wraps the discovery logic along with some goodies that make the entire OAuth authorization code flow much, much easier to integrate for someone that wants an idiomatic, familiar way of plugging in authorization into their C# code.
Getting started with authorization in the MCP C# SDK #
To get started, install the pre-release version of the package:
dotnet add package ModelContextProtocol --prerelease
Building a MCP client that speaks OAuth #
For a client, the OAuth-related configuration is attached to the transport configuration, like this:
var transport = new SseClientTransport(new()
{
Endpoint = new Uri(serverUrl),
Name = "Secure Weather Client",
OAuth = new()
{
ClientName = "ProtectedMcpClient",
RedirectUri = new Uri("http://localhost:1179/callback"),
AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync,
}
}, httpClient, consoleLoggerFactory);
In the context of the snippet above, OAuth
is an instance of ClientOAuthOptions
, that is used to set up the client. Under the hood, those are used with a ClientOAuthProvider
. But that’s it - that’s as complicated it is to get the client to use OAuth.

Oh, that’s because the client itself is Authorization Server unaware! It doesn’t really need to do a lot of configuration because a lot of that comes from the PRM document, followed by standard discovery steps?
Exactly! The client responsibilities can be neatly swept under the rug when it comes to the developer experience we’re providing because the steps are standard across authorization servers. The client developer needs to provide just basic OAuth client metadata.
Building protected MCP servers #
Server-side, things get a bit more wordy, but not by much. Here is what a protected MCP server configuration may look like:
var builder = WebApplication.CreateBuilder(args);
var serverUrl = "http://localhost:7071/";
var inMemoryOAuthServerUrl = "https://localhost:7029";
builder.Services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// Configure to validate tokens from our in-memory OAuth server
options.Authority = inMemoryOAuthServerUrl;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = serverUrl, // Validate that the audience matches the resource metadata as suggested in RFC 8707
ValidIssuer = inMemoryOAuthServerUrl,
NameClaimType = "name",
RoleClaimType = "roles"
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var name = context.Principal?.Identity?.Name ?? "unknown";
var email = context.Principal?.FindFirstValue("preferred_username") ?? "unknown";
Console.WriteLine($"Token validated for: {name} ({email})");
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
Console.WriteLine($"Authentication failed: {context.Exception.Message}");
return Task.CompletedTask;
},
OnChallenge = context =>
{
Console.WriteLine($"Challenging client to authenticate with Entra ID");
return Task.CompletedTask;
}
};
})
.AddMcp(options =>
{
options.ResourceMetadata = new()
{
Resource = new Uri(serverUrl),
ResourceDocumentation = new Uri("https://docs.example.com/api/weather"),
AuthorizationServers = { new Uri(inMemoryOAuthServerUrl) },
ScopesSupported = ["mcp:tools"],
};
});
builder.Services.AddAuthorization();
builder.Services.AddHttpContextAccessor();
builder.Services.AddMcpServer()
.WithTools<WeatherTools>()
.WithHttpTransport();
// Configure HttpClientFactory for weather.gov API
builder.Services.AddHttpClient("WeatherApi", client =>
{
client.BaseAddress = new Uri("https://api.weather.gov");
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0"));
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// Use the default MCP policy name that we've configured
app.MapMcp().RequireAuthorization();
In this snippet, AddJwtBearer
is standard ASP.NET Core construct that adds JWT-bearer authentication support, and packs a punch with built-in validation logic (you must always validate inbound tokens).
In AddMcp
, we’re defining the PRM - it’s the JSON you saw above, in C# form. The rest, like AddAuthorization
, UseAuthentication
, UseAuthorization
, and RequireAuthorization
, is also following standard ASP.NET Core conventions - remember when I mentioned idiomatic design?
Next Steps #
The MCP C# SDK is in preview, and the authorization logic is still fresh, so expect some rough edges. The team is looking at making some incremental improvements in the near future, so if you hit a snag or have ideas, open a GitHub issue - we’re very much listening.
If you want to learn more, I highly recommend checking out the following resources:
- GitHub Repository:
modelcontextprotocol/csharp-sdk
- NuGet Package:
ModelContextProtocol
- MCP Specification: Authorization
- Standard: RFC 9728 OAuth 2.0 Protected Resource Metadata
And of course, because you’re going to be dealing with tokens, reading security best practices is an absolute must.
Acknowledgements #
Special thank you to Stephen Halter for doing a significant amount of work preparing the authorization PR for production release.