If you already have a working HTTP MCP server, MCP server authentication in C# is the next design decision that determines whether your tools are safe to expose beyond a local process. A remote server is no longer protected by "it only runs on my machine." It has callers, tokens, browsers, authorization servers, and tool handlers that may need to know exactly which user made the request.
This is a MOFU implementation guide. We are not covering general ASP.NET Core hosting, Azure deployment, or transport theory here. The assumption is that your Streamable HTTP MCP endpoint already works. Now we are adding OAuth 2.0 protected resource metadata, JWT bearer validation, endpoint authorization, per-tool authorization, claims-aware handlers, and browser-safe CORS.
As of July 5, 2026, the latest stable NuGet version for ModelContextProtocol, ModelContextProtocol.Core, and ModelContextProtocol.AspNetCore is 1.4.0, with 2.0.0-preview.1 also listed as a prerelease. Check NuGet before pinning because the SDK is moving quickly. One naming detail matters: the live SDK exposes the authentication APIs under the ModelContextProtocol.AspNetCore.Authentication namespace inside the ModelContextProtocol.AspNetCore package. There is no separate NuGet package named ModelContextProtocol.AspNetCore.Authentication.
Why MCP Server Authentication in C# Is Different From Normal API Auth
At first glance, this looks like normal ASP.NET Core API security. You add JWT bearer authentication, call UseAuthentication(), call UseAuthorization(), and protect an endpoint. That is part of it, but MCP server authentication in C# has one extra discovery problem: an MCP client may not know which authorization server issues tokens for your resource.
That is where OAuth 2.0 Protected Resource Metadata comes in. The MCP authorization specification requires protected HTTP MCP servers to publish metadata that tells clients which authorization servers can issue tokens for the MCP resource. When a client calls your MCP endpoint without a valid token, your server can return 401 Unauthorized with a WWW-Authenticate header that points at that metadata document. The client then follows the discovery chain, obtains an access token, and retries the MCP request with an Authorization header containing a bearer token.
That flow is different from many internal APIs where the client already has hardcoded authority and audience settings. For MCP, discovery is part of interoperability. It also gives you a clean way to support clients that are connecting to a resource server they have never seen before.
If you are still shaping the remote endpoint itself, the HTTP fundamentals in HttpClient in C#: The Complete Guide for .NET Developers and IHttpClientFactory in .NET are useful background. For this article, we will stay focused on secure MCP server .NET authentication behavior.
The Core Pieces for Secure MCP Server .NET Auth
A practical secure MCP server .NET setup has four cooperating layers. The first layer is standard ASP.NET Core JWT bearer authentication. It validates issuer, audience, signing keys, lifetime, and claims. The second layer is the MCP authentication handler, registered with .AddMcp(...), which adds the protected resource metadata challenge behavior. The third layer is endpoint authorization, usually app.MapMcp().RequireAuthorization(). The fourth layer is handler-level authorization and identity flow inside tools, prompts, and resources.
Here is the smallest useful shape, adapted from the live ProtectedMcpServer sample and the SDK identity docs:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using ModelContextProtocol.AspNetCore.Authentication;
var builder = WebApplication.CreateBuilder(args);
var resourceUrl = builder.Configuration["Mcp:ResourceUrl"]
?? throw new InvalidOperationException("Configure Mcp:ResourceUrl.");
var authorizationServerUrl = builder.Configuration["Mcp:AuthorizationServerUrl"]
?? throw new InvalidOperationException("Configure Mcp:AuthorizationServerUrl.");
builder.Services.AddAuthentication(options =>
{
// Let the MCP handler produce OAuth protected resource metadata challenges.
options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = authorizationServerUrl;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = resourceUrl,
ValidIssuer = authorizationServerUrl,
NameClaimType = "name",
RoleClaimType = "roles"
};
})
.AddMcp(options =>
{
// Keep this aligned with the resource/audience clients request tokens for.
options.ResourceMetadata = new()
{
Resource = resourceUrl,
AuthorizationServers = { authorizationServerUrl },
ScopesSupported = ["mcp:tools"],
ResourceDocumentation = "https://learn.microsoft.com/dotnet/ai/get-started-mcp"
};
});
builder.Services.AddAuthorization();
builder.Services.AddMcpServer()
.WithHttpTransport(options => options.Stateless = true)
.WithToolsFromAssembly();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapMcp("/mcp").RequireAuthorization();
app.Run();
This code shows the decision boundary. JWT bearer validates the token. The MCP authentication scheme handles challenges and resource metadata. RequireAuthorization() protects the MCP endpoint. The endpoint path and Resource value should line up with the canonical resource URI that clients will request tokens for.
There is a tradeoff here. Endpoint-level authorization is simple and safe because every MCP operation requires a valid token. If you need some public tools and some protected tools on the same server, you can still use handler-level [Authorize] and [AllowAnonymous], but you need to be deliberate about what anonymous clients can list and call.
How OAuth Protected Resource Metadata Works for MCP Clients
The discovery flow is the part of MCP server authentication in C# that is easiest to skip in a quick demo and most important for real clients. The MCP authorization spec says an HTTP MCP server acts as an OAuth resource server. The MCP client acts as an OAuth client. The authorization server issues the access token.
A typical protected flow looks like this:
- The MCP client sends a request to the MCP server without a token.
- The MCP server returns
401 Unauthorizedwith aWWW-Authenticateheader. - That header includes a
resource_metadataURL. - The MCP client fetches the OAuth protected resource metadata document.
- The client reads
authorization_serversfrom that document. - The client fetches the authorization server metadata.
- The client completes the OAuth flow, including the
resourceparameter. - The client retries the MCP request with a bearer token.
The exact metadata path has a nuance in the C# SDK. The ProtectedMcpServer sample prints the root metadata URL as /.well-known/oauth-protected-resource because the sample maps MCP at the server root. The live McpAuthenticationOptions source and tests also show a default resource-specific shape: when ResourceMetadataUri is null, the handler can use /.well-known/oauth-protected-resource/<resource-path> to mirror the requested resource path. For an endpoint at /mcp, that means the challenge can point at /.well-known/oauth-protected-resource/mcp unless you configure ResourceMetadataUri explicitly.
If you need a fixed metadata URL, set it directly:
using ModelContextProtocol.AspNetCore.Authentication;
var resourceUrl = builder.Configuration["Mcp:ResourceUrl"]
?? throw new InvalidOperationException("Configure Mcp:ResourceUrl.");
var authorizationServerUrl = builder.Configuration["Mcp:AuthorizationServerUrl"]
?? throw new InvalidOperationException("Configure Mcp:AuthorizationServerUrl.");
builder.Services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = "Bearer";
})
.AddJwtBearer()
.AddMcp(options =>
{
// Use a fixed metadata endpoint when you do not want the default path-suffixed endpoint.
options.ResourceMetadataUri = new Uri(
"/.well-known/oauth-protected-resource",
UriKind.Relative);
options.ResourceMetadata = new()
{
Resource = resourceUrl,
AuthorizationServers = { authorizationServerUrl },
ScopesSupported = ["mcp:tools"]
};
});
The benefit of the default resource-specific endpoint is that multiple protected resources can coexist more naturally. The benefit of a fixed metadata URI is simpler documentation and easier inspection. The right choice depends on whether your service exposes one MCP resource or several resource paths with different audiences.
For the protocol side, the Model Context Protocol authorization specification is the source to keep close. For the C# APIs, the live MCP C# SDK repository and ProtectedMcpServer sample are the reference points I would trust over older blog posts.
RequireAuthorization on MapMcp Protects the Remote Endpoint
Once authentication is registered, the remote endpoint still needs to require it. In ASP.NET Core terms, UseAuthentication() establishes the principal and UseAuthorization() enforces policies. The endpoint must opt into authorization with RequireAuthorization() unless you are applying authorization another way.
The live sample uses:
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapMcp()
.RequireAuthorization()
.RequireCors("McpBrowserClient");
That ordering matters. CORS must run where it can handle preflight and response headers. Authentication must run before authorization. The mapped MCP endpoint then gets both authorization enforcement and, if needed, a CORS policy.
For many teams, endpoint-level protection is the baseline I would evaluate first for MCP server authentication in C#. It is easy to reason about. No authenticated caller, no MCP access. That reduces the chance that tools/list accidentally reveals private tool names or descriptions.
The downside is that public discovery becomes impossible without a token. In most enterprise and internal-tooling scenarios, that is fine. If you intentionally expose public tools, consider separating public and protected MCP endpoints or using [AllowAnonymous] with authorization filters after you have tested list and call behavior.
Inject ClaimsPrincipal Into MCP Tools Without Polluting the Schema
Authentication is not only about blocking requests. Tool handlers often need to filter data by tenant, user, role, department, or entitlement. The SDK identity docs verify that ClaimsPrincipal can be declared as a parameter on tool, prompt, and resource handlers. The SDK injects it from the current request context and excludes it from the generated tool schema.
That last part is important. The model should not be asked to provide the caller identity as a JSON argument. Identity comes from the transport and token, not from the LLM.
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Security.Claims;
[McpServerToolType]
public sealed class AccountTools
{
[McpServerTool]
[Description("Searches support tickets visible to the authenticated user.")]
public async Task<string> SearchTicketsAsync(
ClaimsPrincipal user,
TicketSearchService tickets,
[Description("The search phrase to match against visible tickets.")] string query)
{
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier)
?? throw new UnauthorizedAccessException("Missing user identifier claim.");
var results = await tickets.SearchVisibleTicketsAsync(userId, query);
return string.Join("
", results.Select(ticket => $"- {ticket.Title}"));
}
}
This pattern keeps MCP server JWT validation separate from application authorization decisions. The token proves who the caller is. The handler still decides what that caller can see. That is the same principle you would apply in a normal API, but MCP tools make the boundary more visible because every method becomes an externally invokable capability.
If your handler truly needs HTTP details beyond identity, IHttpContextAccessor is an option for HTTP transports. I would treat that as an HTTP-specific escape hatch. For portable handler code, prefer ClaimsPrincipal injection.
AddAuthorizationFilters Enables Per-Tool Authorize Attributes
Endpoint-level authorization answers: can this caller reach the MCP endpoint? Per-tool authorization answers: can this caller see or invoke this specific capability? The SDK supports standard ASP.NET Core [Authorize] and [AllowAnonymous] attributes on tools, prompts, and resources when you call AddAuthorizationFilters() on the MCP server builder.
using Microsoft.AspNetCore.Authorization;
using ModelContextProtocol.Server;
using System.ComponentModel;
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CanReadFinance", policy =>
policy.RequireClaim("scope", "finance.read"));
});
builder.Services.AddMcpServer()
.WithHttpTransport(options => options.Stateless = true)
.AddAuthorizationFilters()
.WithTools<FinanceTools>();
[McpServerToolType]
[Authorize]
public sealed class FinanceTools
{
[McpServerTool]
[Description("Returns a summary of finance records visible to the caller.")]
[Authorize(Policy = "CanReadFinance")]
public string GetFinanceSummary(string accountId)
{
// Real handlers should filter by both authorization policy and caller-owned data.
return $"Finance summary for account {accountId}.";
}
[McpServerTool]
[Description("Returns public finance documentation links.")]
[AllowAnonymous]
public string GetPublicFinanceDocs()
{
return "Public finance documentation is available from your internal documentation portal.";
}
}
The live filters docs call out two behaviors worth designing around. For list operations, unauthorized tools, prompts, and resources are removed from results so callers only see what they can access. For individual operations, authorization failures produce a JSON-RPC error. That is useful because tool visibility and tool invocation both respect authorization, but it can surprise you in tests if you expect every registered tool to appear for every user.
This is where tool approval and human-in-the-loop workflows are conceptually related. Authorization says whether a caller is allowed. Approval says whether a risky action should proceed right now. For sensitive MCP tools, you may need both.
CORS for Browser MCP Clients
CORS is not an authentication mechanism. It is a browser security boundary. But if your MCP client runs in a browser, your secure MCP server .NET implementation needs a restrictive CORS policy so the browser is allowed to send the request and read the relevant response headers.
The ProtectedMcpServer sample configures a named policy that allows only configured origins, permits POST, allows Content-Type, Authorization, and MCP-Protocol-Version, and exposes WWW-Authenticate so the browser client can inspect the challenge.
using Microsoft.Net.Http.Headers;
var allowedOrigins = builder.Configuration
.GetSection("Mcp:AllowedOrigins")
.Get<string[]>()
?? throw new InvalidOperationException("Configure at least one MCP browser client origin.");
builder.Services.AddCors(options =>
{
options.AddPolicy("McpBrowserClient", policy =>
{
policy.WithOrigins(allowedOrigins)
.WithMethods("POST")
.WithHeaders(
HeaderNames.ContentType,
HeaderNames.Authorization,
"MCP-Protocol-Version")
.WithExposedHeaders(HeaderNames.WWWAuthenticate);
});
});
var app = builder.Build();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapMcp("/mcp")
.RequireAuthorization()
.RequireCors("McpBrowserClient");
The tradeoff is convenience versus containment. AllowAnyOrigin() is convenient for demos, but it is rarely the right default for a protected remote MCP server. A browser-accessible MCP endpoint may expose powerful tools. Restrict origins to clients you actually trust, and keep CORS separate from token validation in your mental model.
If your server calls downstream HTTP APIs after a tool is invoked, HttpClient resilience in .NET becomes relevant too. Authentication gets the request through the front door. Resilience and timeouts keep downstream dependencies from turning tool calls into production incidents.
Enterprise Identity Assertion Grant Scenarios
Most teams can start MCP OAuth with normal authorization code plus PKCE or another supported OAuth flow from their identity provider. Enterprise environments sometimes need a different shape: a user is already authenticated to a company identity provider, and the MCP client needs non-interactive access to an MCP server without asking the user to approve every server separately.
The C# SDK docs describe support for the Identity Assertion Authorization Grant flow through IdentityAssertionGrantProvider in the ModelContextProtocol.Authentication namespace. The transport docs summarize it as a two-step exchange: an OIDC ID token becomes a JWT Authorization Grant at the enterprise identity provider, then that grant becomes an access token at the MCP authorization server.
That is not the first thing I would implement for a simple server. It is a MOFU decision point for organizations with SSO, managed authorization servers, and cross-application access requirements. If you are building for that environment, do not hand-wave it as "just pass the user's JWT." Model the resource, audience, token exchange, cache invalidation, and failure behavior explicitly.
Decision Criteria for MCP Server Authentication in C#
When you compare approaches, start with the resource you are protecting. If every tool requires a signed-in user, endpoint-level RequireAuthorization() gives you a strong baseline. If different tools have different sensitivity levels, add AddAuthorizationFilters() and use policies or roles on individual tools. If browser clients are in scope, add restrictive CORS and expose WWW-Authenticate. If enterprise SSO is required, evaluate Identity Assertion Grant support instead of building a proprietary token relay.
The main pros of this SDK-aligned approach are interoperability and familiarity. OAuth protected resource metadata helps MCP clients discover how to authenticate. JWT bearer auth uses the ASP.NET Core security stack your team likely already knows. ClaimsPrincipal injection avoids custom identity plumbing. [Authorize] attributes let you reuse policy-based authorization.
The cons are configuration complexity and version sensitivity. The metadata URL behavior is easy to misunderstand. The authentication APIs live in a namespace that sounds like a package name, but the live package story points at ModelContextProtocol.AspNetCore. The SDK is changing quickly, so pin versions and re-check docs before publishing.
For observability, the patterns in Logging in .NET and HttpClient logging and observability in .NET matter after auth is working. Log authentication failures carefully, avoid token leakage, and audit high-risk tool calls by user and tool name.
FAQ
What package do I need for MCP server authentication in C#?
Use ModelContextProtocol.AspNetCore for HTTP MCP servers. The live SDK exposes authentication types such as McpAuthenticationDefaults, McpAuthenticationOptions, and .AddMcp(...) under the ModelContextProtocol.AspNetCore.Authentication namespace. There is no separate NuGet package named ModelContextProtocol.AspNetCore.Authentication, so do not add a package reference for that namespace.
Does MCP OAuth replace ASP.NET Core JWT bearer authentication?
No. MCP OAuth discovery and protected resource metadata help the client discover how to obtain a token for the MCP resource. ASP.NET Core JWT bearer authentication still validates the token on incoming requests. In practice, MCP server authentication in C# uses both: .AddJwtBearer(...) for validation and .AddMcp(...) for MCP-aware challenge and metadata behavior.
Should I protect the whole MCP endpoint or individual tools?
Protect the whole endpoint when every MCP operation requires an authenticated caller. That is the safer default for most remote servers. Use individual [Authorize] attributes when different tools, prompts, or resources have different access requirements. If you use those attributes, enable AddAuthorizationFilters() so the SDK applies them during list and call operations.
Where is the OAuth protected resource metadata served?
The sample prints /.well-known/oauth-protected-resource for a root-mapped server. The live SDK options and tests show that the default can also mirror the requested resource path as /.well-known/oauth-protected-resource/<resource-path>, such as /.well-known/oauth-protected-resource/mcp. Configure ResourceMetadataUri if you want one fixed metadata URL.
Can a tool receive the authenticated user directly?
Yes. Add a ClaimsPrincipal parameter to a tool, prompt, or resource handler. The SDK injects the current user from the request context and excludes that parameter from the generated schema. That is the preferred approach when handler logic needs identity but the model should not provide identity as input.
Is CORS enough to secure a browser-accessible MCP server?
No. CORS controls which browser origins can call and read responses from your server. It does not validate users, tokens, scopes, or roles. For browser clients, combine restrictive CORS with JWT bearer authentication, OAuth protected resource metadata, endpoint authorization, and tool-level policies where needed.
Final Takeaway
MCP server authentication in C# works best when you treat the MCP server as an OAuth protected resource and a normal ASP.NET Core application at the same time. Use JWT bearer auth to validate tokens. Use .AddMcp(...) to publish protected resource metadata and produce MCP-aware challenges. Use RequireAuthorization() on MapMcp() as your baseline. Then add ClaimsPrincipal injection and AddAuthorizationFilters() when tools need user-aware behavior or different access rules.
That gives you a remote HTTP MCP server that clients can discover, security teams can reason about, and .NET developers can maintain without inventing a custom auth protocol.

