Authio.Mcp.AspNetCore 0.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Authio.Mcp.AspNetCore --version 0.1.0
                    
NuGet\Install-Package Authio.Mcp.AspNetCore -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Authio.Mcp.AspNetCore" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Authio.Mcp.AspNetCore" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Authio.Mcp.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Authio.Mcp.AspNetCore --version 0.1.0
                    
#r "nuget: Authio.Mcp.AspNetCore, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Authio.Mcp.AspNetCore@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Authio.Mcp.AspNetCore&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Authio.Mcp.AspNetCore&version=0.1.0
                    
Install as a Cake Tool

Authio.Mcp.AspNetCore

Protect an ASP.NET Core MCP server with Authio.

The MCP server is an OAuth 2.1 resource server; Authio is its authorization server. This package wires the server's half of that contract:

  • Token validation — JWT bearer against the realm's published keys: signature (asymmetric algorithms only: RS/PS/ES 256/384/512), issuer, audience (your resource identifier) and expiry.
  • Introspection (RFC 7662) — every token that passes local validation is checked with Authio, because only Authio knows whether it was revoked or its session ended. An active answer is cached per jti for 30 seconds.
  • Protected resource metadata (RFC 9728) — the document an MCP client reads to discover Authio and the scopes to request.
  • MCP-ready challenges — 401 with WWW-Authenticate: Bearer … resource_metadata="…" and 403 with error="insufficient_scope", scope="…".
  • Connected accounts — trade the caller's Authio token for the provider token of an account they connected (GitHub, Google…) through Authio's token exchange (RFC 8693).
  • Start-up checks — the realm's metadata is read when the application starts; a wrong base URL, realm or issuer stops it with a sentence instead of every token being refused later.

Targets .NET 8 and .NET 10. It does not depend on the MCP SDK; it works with any endpoint you protect.

Install

dotnet add package Authio.Mcp.AspNetCore
dotnet add package ModelContextProtocol.AspNetCore

Register the server in Authio

In the Authio console, in your realm:

  1. Create a resource server whose identifier is the public URL of your MCP endpoint, e.g. https://mcp.example.com/mcp, with a scope such as mcp:tools.
  2. Create a confidential client for the MCP server itself and set it as the resource server's introspection client. Its id and secret go in ClientId / ClientSecret.
  3. For connected accounts, add a connection provider (e.g. alias github) and allow the MCP clients that call you to use it.

Configure

appsettings.json (keep the secret in user-secrets or an environment variable: Authio__Mcp__ClientSecret):

{
  "Authio": {
    "Mcp": {
      "BaseUrl": "https://auth.example.com",
      "Realm": "demo",
      "ResourceUrl": "https://mcp.example.com/mcp",
      "ClientId": "mcp-server",
      "ClientSecret": "<secret>",
      "RequiredScopes": [ "mcp:tools" ],
      "ResourceName": "My MCP server"
    }
  }
}
Setting Meaning
BaseUrl Where Authio answers, without the realm. Must match the base URL Authio publishes (Auth:PublicBaseUrl): the issuer {BaseUrl}/realms/{Realm} is built from it.
Realm The realm the resource server and its clients live in.
ResourceUrl The public URL of the MCP endpoint. It is the resource identifier and the token's aud (scheme and host lower-cased, default port dropped, trailing slash trimmed — as Authio stores it).
ClientId / ClientSecret The resource server's introspection client. Also used for the token exchange.
RequiredScopes Scopes every token must carry. Default: mcp:tools.
ResourceName Optional resource_name in the protected resource metadata.
IntrospectionCacheDuration How long an active introspection answer is trusted. Default: 00:00:30; 00:00:00 disables the cache.

Every setting can also be set in code: AddAuthioMcp(options => { … }) runs after the configuration binding.

Use with the official C# MCP SDK

using Authio.Mcp.AspNetCore.Defaults;
using Authio.Mcp.AspNetCore.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication().AddAuthioMcp();
builder.Services.AddCors(cors => cors.AddAuthioMcpPolicy());
builder.Services.AddHttpContextAccessor();
builder.Services
    .AddMcpServer()
    .WithHttpTransport(transport => transport.Stateless = true)
    .WithToolsFromAssembly();

var app = builder.Build();

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

app.MapAuthioProtectedResourceMetadata();
app.MapMcp("/mcp")
    .RequireAuthorization(AuthioMcpDefaults.Policy)
    .RequireCors(AuthioMcpDefaults.CorsPolicy);

app.Run();

MapAuthioProtectedResourceMetadata() serves the document at /.well-known/oauth-protected-resource{resource path} — for https://mcp.example.com/mcp that is /.well-known/oauth-protected-resource/mcp:

{
  "resource": "https://mcp.example.com/mcp",
  "authorization_servers": [ "https://auth.example.com/realms/demo" ],
  "scopes_supported": [ "mcp:tools" ],
  "bearer_methods_supported": [ "header" ],
  "resource_name": "My MCP server"
}

A tool that reads the caller and uses a connected account

using System.ComponentModel;
using Authio.Mcp.AspNetCore.Exchanges;
using Authio.Mcp.AspNetCore.Extensions;
using Authio.Mcp.AspNetCore.Models;
using ModelContextProtocol.Server;

[McpServerToolType]
public sealed class GitHubTools
{
    [McpServerTool(Name = "github_account", ReadOnly = true)]
    [Description("Shows which GitHub account the caller connected through Authio.")]
    public static async Task<string> GitHubAccount(
        IHttpContextAccessor accessor,
        IAuthioConnectionExchange exchange,
        [Description("Which connected account to use, when there are several.")] string? connectionId = null,
        CancellationToken cancellationToken = default)
    {
        var context = accessor.HttpContext!;
        var caller = context.GetAuthioCaller();          // sub, azp, scopes, org_id, org_alias, jti, exp
        var subjectToken = context.GetAuthioAccessToken()!; // the verified Authio access token

        var result = await exchange.ExchangeAsync(subjectToken, "github", connectionId, cancellationToken);

        return result switch
        {
            AuthioConnectionGranted granted =>
                $"{caller!.Subject} is {granted.ExternalAccount?.Login} on GitHub.", // use granted.AccessToken
            AuthioConnectionRequired required =>
                $"Connect your GitHub account first: {required.ConnectUri}",
            AuthioConnectionSelectionRequired selection =>
                "Pick one: " + string.Join(", ", selection.Connections.Select(c => $"{c.Id} ({c.ExternalLogin})")),
            AuthioConnectionRefused refused =>
                $"Authio refused ({refused.Error}, {refused.Code}): {refused.Description}",
            _ => "Unexpected result."
        };
    }
}

Return each refusal to the model as text the person can act on: the address where they connect the account, the accounts to choose from, or the reason Authio gave.

A runnable version lives in samples/Authio.Mcp.Sample.

Challenges

Situation Status WWW-Authenticate
No token 401 Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"
Invalid, expired, foreign-audience or revoked token 401 Bearer error="invalid_token", error_description="…", resource_metadata="…"
Token without the required scopes 403 Bearer error="insufficient_scope", scope="mcp:tools", resource_metadata="…"

If Authio refuses the introspection request itself (wrong client credentials), the request fails with a 500 and the error is logged naming Authio:Mcp:ClientId and Authio:Mcp:ClientSecret.

Browser-based MCP clients (MCP Inspector)

A browser client reads the protected resource metadata cross-origin and must read WWW-Authenticate from the 401 to find it. The metadata endpoint already answers any origin. For the MCP endpoint, register the CORS policy and add it to the endpoint, with app.UseCors() before app.UseAuthentication():

builder.Services.AddCors(cors => cors.AddAuthioMcpPolicy());
// …
app.UseCors();
app.MapMcp("/mcp").RequireAuthorization(AuthioMcpDefaults.Policy).RequireCors(AuthioMcpDefaults.CorsPolicy);

The policy allows any origin, the MCP headers (Authorization, Content-Type, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID) and exposes WWW-Authenticate and Mcp-Session-Id. If you use your own policy, expose WWW-Authenticate in it.

API

Member Purpose
AuthenticationBuilder.AddAuthioMcp(Action<AuthioMcpOptions>?) Registers the scheme, the policy, introspection, the exchange and the start-up check.
IEndpointRouteBuilder.MapAuthioProtectedResourceMetadata() Serves the RFC 9728 document.
CorsOptions.AddAuthioMcpPolicy(string name = AuthioMcpDefaults.CorsPolicy) CORS policy for browser MCP clients.
AuthioMcpDefaults.Policy / .AuthenticationScheme / .CorsPolicy / .HttpClientName Names to refer to.
IAuthioConnectionExchange.ExchangeAsync(subjectToken, provider, connectionId?, ct) Connected-accounts token exchange.
HttpContext.GetAuthioAccessToken() The verified raw token (subject token for the exchange).
HttpContext.GetAuthioCaller() / ClaimsPrincipal.GetAuthioCaller() AuthioCaller (subject, client, scopes, organization, token id, expiry).
ClaimsPrincipal.GetAuthioScopes() / HasAuthioScopes(scopes) Scope checks, e.g. per tool.
IAuthioTokenIntrospector Replaceable introspection service.

All calls to Authio (metadata, keys, introspection, exchange) go through the named HttpClient AuthioMcpDefaults.HttpClientName, so you can add a proxy, a handler or resilience with services.AddHttpClient(AuthioMcpDefaults.HttpClientName)….

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.0 109 10/14/2026
0.4.1 93 10/14/2026
0.4.0 101 10/14/2026
0.3.0 93 10/13/2026
0.2.0 96 10/8/2026
0.1.0 100 10/4/2026