Reusable C# library that implements MCP OAuth2 authentication with Keycloak, including Dynamic Client Registration (RFC 7591). Handles JWT Bearer validation, per-tool authorization via [McpAuthorize], automatic client registration at startup, and Protected Resource Metadata (RFC 9728). One call to AddMcpOAuth2() and the auth is done -- your MCP server tools only need a [McpAuthorize] attribute to require authentication and specific scopes.
- .NET 9 SDK
- Docker (for Keycloak)
curlandjq(for the Keycloak setup script)
docker run -d -p 8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:latest start-devKC_REALM=my-realm KC_AUDIENCE=my-server KC_SCOPE=my:scope \
KC_ADMIN_PASSWORD=admin ./scripts/keycloak-create-realm.shThis script creates the realm, enables anonymous DCR (by removing the Trusted Hosts policy), creates an OAuth scope with an audience mapper, and adds it as a default scope. Re-running the script is safe -- it verifies and corrects state idempotently.
dotnet add package Palpid.McpAuthAlso published to GitHub Packages as an alternative feed -- see docs/usage.md for setup instructions if you prefer that source.
using McpAuth.Authorization;
using McpAuth.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMcpOAuth2(options =>
{
options.AuthServerUrl = "http://localhost:8080/realms/my-realm";
options.Audience = "my-server";
options.ServerUrl = "http://localhost:3000";
options.ScopesSupported = ["my:scope"];
});
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithTools<MyTools>()
.AddAuthorizationFilters()
.WithRequestFilters(filters =>
{
filters.AddCallToolFilter(McpAuthorizationFilter.Create());
});
var app = builder.Build();
app.MapMcpOAuth2Metadata();
app.MapMcp();
app.Run("http://localhost:3000");using McpAuth.Authorization;
using ModelContextProtocol.Server;
public class MyTools
{
[McpServerTool, Description("Public tool -- no auth required")]
public static string PublicTool() => "anyone can call this";
[McpServerTool, McpAuthorize(Scopes = ["my:scope"])]
[Description("Protected tool -- requires my:scope")]
public static string ProtectedTool() => "only authenticated users";
}Tools without [McpAuthorize] are public and accessible without a token. Tools decorated with [McpAuthorize] require a valid JWT with the specified scopes.
dotnet run
# In another terminal:
curl http://localhost:3000/.well-known/oauth-protected-resource | jq .Expected output: a JSON document with resource, authorization_servers, and scopes_supported fields, confirming the server advertises its OAuth2 protection metadata per RFC 9728.
The repository includes a complete demo server in src/DiceServer/ with a public roll_d6 tool and a protected roll_xdy tool that requires the dice:roll scope. See src/DiceServer/Program.cs for the full integration pattern, including CORS configuration and environment-based settings.
For advanced McpAuthOptions configuration (ClockSkew, JwksCacheInterval, RequireHttpsMetadata), architecture internals, end-to-end verification with curl, troubleshooting common errors, and GitHub Packages feed setup, see docs/usage.md.
For a visual, non-technical walkthrough of what McpAuth does and why (suitable for sharing outside the engineering team), see docs/product-overview.html.
Apache License 2.0. See LICENSE file.