<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Fri, 02 Oct 2026 18:18:51 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=prince-mantle" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/enabling-mcp-authentication-in-abp-framework-with-openiddict-t6l3cqbt</guid>
      <link>https://abp.io/community/posts/enabling-mcp-authentication-in-abp-framework-with-openiddict-t6l3cqbt</link>
      <a10:author>
        <a10:name>prince-mantle</a10:name>
        <a10:uri>https://abp.io/community/members/prince-mantle</a10:uri>
      </a10:author>
      <category>openiddict</category>
      <category>authentication</category>
      <category>abp-framework</category>
      <category>openid-connect</category>
      <category>openiddict-module</category>
      <title>Enabling MCP Authentication in ABP Framework with OpenIddict</title>
      <description>Every ABP Framework application already ships with a fully configured OAuth 2.1 / OpenID Connect Authorization Server — the bundled OpenIddict module. We usually think of it as the thing that logs users into the web UI and issues tokens to the Swagger client. But it is a real, spec-compliant authorization server, and that turns out to be exactly what the Model Context Protocol (MCP) needs.</description>
      <pubDate>Sun, 07 Jun 2026 09:58:01 Z</pubDate>
      <a10:updated>2026-10-02T16:17:56Z</a10:updated>
      <content:encoded><![CDATA[<hr />
<p>title: Enabling MCP Authentication in ABP Framework with OpenIddict
tags:</p>
<ul>
<li>mcp</li>
<li>oauth</li>
</ul>
<hr />
<h1>Enabling MCP Authentication in ABP Framework with OpenIddict</h1>
<p>Every ABP Framework application already ships with a fully configured OAuth 2.1 / OpenID Connect <strong>Authorization Server</strong> — the bundled <a href="https://abp.io/docs/latest/modules/openiddict">OpenIddict module</a>. We usually think of it as the thing that logs users into the web UI and issues tokens to the Swagger client. But it is a real, spec-compliant authorization server, and that turns out to be exactly what the <strong>Model Context Protocol (MCP)</strong> needs.</p>
<p>The newest wave of AI tooling — Claude Desktop, the VS Code agent, the MCP Inspector — talks to your backend through MCP servers, and the current MCP authorization spec requires those servers to be protected by OAuth 2.1 with PKCE and Resource Indicators. Rather than standing up a separate identity provider, you can reuse the one your ABP app already runs. In this article I'll show you how, end to end, using nothing but the OpenIddict data-seed contributor and a single <code>PreConfigure</code> block in the host module. No custom middleware, no hand-rolled token endpoints.</p>
<p>We'll use the classic <code>Acme.BookStore</code> sample as our ABP application. By the end you'll have:</p>
<ul>
<li>an OAuth <strong>scope</strong> the MCP server exposes (<code>BookStoreMcp</code>);</li>
<li>a <strong>public client</strong> for interactive tools (authorization code + PKCE);</li>
<li>a <strong>confidential client</strong> for machine-to-machine testing;</li>
<li><strong>RFC 8707 Resource Indicators</strong> wired up so the issued tokens carry the right <code>aud</code> for your MCP server.</li>
</ul>
<p>Every line of code you need is in this article — including a minimal ASP.NET Core MCP server in the appendix, so you have a resource server to point the client at.</p>
<blockquote>
<p>Tested with <strong>ABP Framework 10.x</strong> on <strong>.NET 10</strong>.</p>
</blockquote>
<h2>A 2-minute primer: why MCP needs OAuth</h2>
<p>The <strong>Model Context Protocol (MCP)</strong> is the open protocol that lets AI clients (Claude, VS Code, IDE agents) call your &quot;tools&quot; and read your &quot;resources&quot; over a standard interface. An <strong>MCP server</strong> exposes those tools; an <strong>MCP client</strong> consumes them.</p>
<p>As of the current MCP authorization specification, an MCP server is treated as an OAuth 2.0 <strong>protected resource</strong>, and clients must obtain a token before calling it. The spec leans on a stack of well-established RFCs — and the good news is that ABP's OpenIddict module already implements all of them:</p>
<p>| Concept | What it does in MCP | Who provides it |
| --- | --- | --- |
| <strong>OAuth 2.1 + PKCE</strong> | Interactive clients get a token via authorization code + PKCE (no client secret) | ABP / OpenIddict |
| <strong>Authorization Server Metadata</strong> (RFC 8414) | Client discovers <code>/connect/token</code>, <code>/connect/authorize</code>, JWKS, etc. | OpenIddict exposes <code>/.well-known/openid-configuration</code> automatically |
| <strong>Protected Resource Metadata</strong> (RFC 9728) | The MCP server tells the client <em>which</em> authorization server to use | The MCP server (see appendix) |
| <strong>Resource Indicators</strong> (RFC 8707) | The client says &quot;I want a token <em>for this MCP server</em>&quot;; the server stamps it as the token <code>aud</code> | ABP / OpenIddict |</p>
<p>The three roles involved:</p>
<pre><code>┌────────────────┐   1. OAuth 2.1 + PKCE   ┌─────────────────────────┐
│  MCP Client    │ ──────────────────────► │  ABP App                │
│  (Claude /     │                         │  = Authorization Server │
│   VS Code /    │ ◄────────────────────── │  (OpenIddict)           │
│   Inspector)   │   2. access_token (JWT) │  /connect/authorize     │
└──────┬─────────┘     aud = MCP server    │  /connect/token         │
       │                                   └─────────────────────────┘
       │ 3. call tools (Authorization: Bearer &lt;jwt&gt;)
       ▼
┌──────────────────────┐
│  Your MCP Server     │  = Resource Server
│  validates aud == me │  (appendix)
└──────────────────────┘
</code></pre>
<p>Your ABP app is <strong>only</strong> the Authorization Server here. It mints the token; the MCP server (a separate process) validates it. Keep that separation in mind — it's the reason we configure <em>two</em> &quot;resources&quot; lists below.</p>
<h2>Prerequisites</h2>
<ul>
<li><strong>.NET 10 SDK</strong></li>
<li>An <strong>ABP Framework 10.x</strong> application created from the standard startup template (the layered solution with <code>*.HttpApi.Host</code> and <code>*.DbMigrator</code>). The template already includes and configures the OpenIddict module — we only customize it. If you don't have one yet:
<pre><code class="language-bash">abp new Acme.BookStore -t app
</code></pre>
</li>
<li><strong>Node.js</strong>, only to run the MCP Inspector (<code>npx @modelcontextprotocol/inspector</code>).</li>
</ul>
<h2>Step 1 — Define the scope the MCP server exposes</h2>
<p>Open the seed contributor the template generated for you — for <code>Acme.BookStore</code> it's <code>Acme.BookStore.OpenIddict.BookStoreOpenIddictDataSeedContributor</code> in the <strong><code>*.Domain</code></strong> project. We'll add a method that seeds an MCP scope and the MCP clients, and call it from <code>SeedAsync</code>.</p>
<p>Add these <code>using</code> directives at the top of the file:</p>
<pre><code class="language-csharp">using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using OpenIddict.Abstractions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.OpenIddict.Applications;
using Volo.Abp.OpenIddict.Scopes;
using Volo.Abp.Uow;
using static OpenIddict.Abstractions.OpenIddictConstants;
</code></pre>
<p>The scope's <strong><code>Resources</code></strong> are the canonical URL(s) of your MCP server. OpenIddict stamps the granted resource as the token's <code>aud</code>, so this is how the MCP server later recognizes &quot;this token is for me.&quot; Capture the base-class managers in the constructor and seed the scope:</p>
<pre><code class="language-csharp">public class BookStoreOpenIddictDataSeedContributor
    : OpenIddictDataSeedContributorBase, IDataSeedContributor, ITransientDependency
{
    private readonly IOpenIddictScopeManager _scopeManager;
    private readonly IAbpApplicationManager _applicationManager;

    public BookStoreOpenIddictDataSeedContributor(
        IConfiguration configuration,
        IOpenIddictApplicationRepository applicationRepository,
        IAbpApplicationManager applicationManager,
        IOpenIddictScopeRepository scopeRepository,
        IOpenIddictScopeManager scopeManager)
        : base(configuration, applicationRepository, applicationManager, scopeRepository, scopeManager)
    {
        _scopeManager = scopeManager;
        _applicationManager = applicationManager;
    }

    [UnitOfWork]
    public override async Task SeedAsync(DataSeedContext context)
    {
        await CreateScopesAsync();          // generated by the template
        await CreateApplicationsAsync();    // generated by the template
        await CreateMcpResourcesAsync();    // our new method
    }

    private async Task CreateMcpResourcesAsync()
    {
        // The MCP server's own canonical URL(s). Register BOTH the trailing-slash
        // and no-slash forms — the client echoes the value verbatim and different
        // SDKs normalize it differently, so an ordinal match must succeed either way.
        var mcpScope = new OpenIddictScopeDescriptor
        {
            Name        = &quot;BookStoreMcp&quot;,
            DisplayName = &quot;BookStore MCP API access&quot;,
            Resources   =
            {
                &quot;https://localhost:7071&quot;,
                &quot;https://localhost:7071/&quot;,        // dev MCP server
                &quot;https://mcp.example.com&quot;,
                &quot;https://mcp.example.com/&quot;        // production MCP server
            }
        };

        // Create OR update so re-running the migrator re-applies Resources changes.
        var existingScope = await _scopeManager.FindByNameAsync(&quot;BookStoreMcp&quot;);
        if (existingScope is null)
        {
            await _scopeManager.CreateAsync(mcpScope);
        }
        else
        {
            await _scopeManager.UpdateAsync(existingScope, mcpScope);
        }

        await CreateMcpClientsAsync();
    }

    // CreateMcpClientsAsync() is shown in Step 2.
}
</code></pre>
<blockquote>
<p><strong>Tip — create <em>or</em> update.</strong> The DbMigrator runs against an existing database. If you only <code>CreateAsync</code> when the scope is missing, later edits to <code>Resources</code> are silently skipped and you end up editing the DB by hand. The <code>FindByNameAsync</code> → <code>UpdateAsync</code> pattern makes the seed the single source of truth.</p>
</blockquote>
<h2>Step 2 — Register the MCP clients</h2>
<p>We seed two clients: one for <strong>interactive</strong> tools and one for <strong>machine-to-machine</strong> testing. Add this method to the same contributor class:</p>
<pre><code class="language-csharp">private async Task CreateMcpClientsAsync()
{
    // 1) Interactive client (Claude Desktop / VS Code / MCP Inspector).
    //    Public client (no secret); PKCE is mandatory.
    var interactiveClient = new OpenIddictApplicationDescriptor
    {
        ClientId    = &quot;BookStore_McpClient&quot;,
        ClientType  = ClientTypes.Public,        // PKCE, no secret
        ConsentType = ConsentTypes.Explicit,
        DisplayName = &quot;BookStore MCP Client&quot;,

        // Loopback callbacks the client listens on. The MCP Inspector runs at
        // :6274 and builds its callback as `window.location.origin + &quot;/oauth/callback&quot;`,
        // with a &quot;/debug&quot; variant for its Guided OAuth Flow. Register both the
        // localhost and 127.0.0.1 hosts so it works either way.
        RedirectUris =
        {
            new Uri(&quot;http://localhost:6274/oauth/callback&quot;),
            new Uri(&quot;http://localhost:6274/oauth/callback/debug&quot;),
            new Uri(&quot;http://127.0.0.1:6274/oauth/callback&quot;),
            new Uri(&quot;http://127.0.0.1:6274/oauth/callback/debug&quot;)
        },

        Permissions =
        {
            Permissions.Endpoints.Authorization,
            Permissions.Endpoints.Token,

            Permissions.GrantTypes.AuthorizationCode,
            Permissions.GrantTypes.RefreshToken,        // enables offline_access

            Permissions.ResponseTypes.Code,

            Permissions.Scopes.Profile,
            Permissions.Scopes.Email,
            Permissions.Prefixes.Scope + &quot;BookStoreMcp&quot; // our scope
        },

        Requirements =
        {
            Requirements.Features.ProofKeyForCodeExchange   // force PKCE
        }
    };

    var existingInteractive = await _applicationManager.FindByClientIdAsync(&quot;BookStore_McpClient&quot;);
    if (existingInteractive is null)
        await _applicationManager.CreateAsync(interactiveClient);
    else
        await _applicationManager.UpdateAsync(existingInteractive, interactiveClient);

    // 2) Confidential client for quick machine-to-machine testing (see Step 5).
    if (await _applicationManager.FindByClientIdAsync(&quot;BookStore_McpTest&quot;) is null)
    {
        await _applicationManager.CreateAsync(new OpenIddictApplicationDescriptor
        {
            ClientId     = &quot;BookStore_McpTest&quot;,
            ClientSecret = &quot;&lt;your-dev-secret&gt;&quot;,   // DEV ONLY — never ship this
            ClientType   = ClientTypes.Confidential,
            DisplayName  = &quot;BookStore MCP Test (m2m)&quot;,
            Permissions  =
            {
                Permissions.Endpoints.Token,
                Permissions.GrantTypes.ClientCredentials,
                Permissions.Prefixes.Scope + &quot;BookStoreMcp&quot;
            }
        });
    }
}
</code></pre>
<p>A few notes:</p>
<ul>
<li><strong><code>ProofKeyForCodeExchange</code></strong> makes PKCE mandatory — required by OAuth 2.1 and by the MCP spec for public clients.</li>
<li>The <strong><code>RefreshToken</code></strong> grant enables <code>offline_access</code> so the tool can keep a session without re-prompting consent every time.</li>
<li>The <strong>confidential</strong> <code>BookStore_McpTest</code> client is only for the <code>curl</code> smoke test in Step 5. Keep its secret out of source control in a real project.</li>
</ul>
<h2>Step 3 — Register the MCP server URLs as accepted resources (RFC 8707)</h2>
<p>This is the step most people miss, so it deserves a clear explanation. There are <strong>two different lists</strong> called &quot;Resources,&quot; and they do different jobs:</p>
<p>| List | Where you set it | What it validates |
| --- | --- | --- |
| <strong>Server-options resources</strong> | <code>OpenIddictServerBuilder.RegisterResources(...)</code> in the <strong>host module</strong> | The <code>resource</code> <strong>request parameter</strong> the client sends on authorize/token |
| <strong>Scope resources</strong> | The <code>OpenIddictScopeDescriptor.Resources</code> we seeded in Step 1 | Nothing on the request — they only populate the token <strong><code>aud</code></strong> |</p>
<p>When an MCP client requests a token, it sends <code>resource=https://mcp.example.com/</code> (the MCP server URL it discovered from Protected Resource Metadata). OpenIddict <strong>validates that parameter against the server-options list</strong>. If the URL isn't registered there, you get <code>ID2190 invalid_target</code> and the whole flow fails.</p>
<p>Add this to your host module's <code>PreConfigureServices</code> (e.g. <code>BookStoreHttpApiHostModule</code> in the <code>*.HttpApi.Host</code> project):</p>
<pre><code class="language-csharp">using Microsoft.Extensions.Configuration;
using OpenIddict.Server;   // OpenIddictServerBuilder
// ...

public override void PreConfigureServices(ServiceConfigurationContext context)
{
    var configuration = context.Services.GetConfiguration();

    PreConfigure&lt;OpenIddictServerBuilder&gt;(serverBuilder =&gt;
    {
        // Accepted resource URLs come from configuration, so they can differ per
        // environment (dev -&gt; appsettings.json, prod -&gt; appsettings.Production.json).
        var resources = configuration.GetSection(&quot;OpenIddict:Resources&quot;).Get&lt;string[]&gt;();
        if (resources is { Length: &gt; 0 })
        {
            serverBuilder.RegisterResources(resources);
        }

        // RegisterResources fixes ID2190 (invalid_target). ID2192 (&quot;client not
        // allowed to use the resource&quot;) is a SEPARATE per-client resource-permission
        // check. Access is already gated by the BookStoreMcp scope permission on
        // each client, so we don't also maintain per-client resource permissions.
        serverBuilder.IgnoreResourcePermissions();
    });
}
</code></pre>
<blockquote>
<p><strong>Hardening option.</strong> If you'd rather enforce per-client resource permissions, drop <code>IgnoreResourcePermissions()</code> and instead grant each client the explicit permission <code>Permissions.Prefixes.Resource + &quot;https://mcp.example.com/&quot;</code> in its descriptor.</p>
</blockquote>
<h2>Step 4 — Configure the resource URLs per environment</h2>
<p><code>OpenIddict:Resources</code> is read from configuration so you don't hard-code hostnames.</p>
<pre><code class="language-jsonc">// appsettings.json (Development)
&quot;OpenIddict&quot;: {
  &quot;Resources&quot;: [ &quot;https://localhost:7071/&quot; ]
}
</code></pre>
<pre><code class="language-jsonc">// appsettings.Production.json
&quot;OpenIddict&quot;: {
  &quot;Resources&quot;: [ &quot;https://mcp.example.com/&quot; ]
}
</code></pre>
<blockquote>
<p><strong>Keep these in sync</strong> with the <strong>scope</strong> <code>Resources</code> from Step 1. The server-options list (this config) decides which <code>resource</code> values are <em>accepted</em>; the scope list decides what ends up in the token <code>aud</code>. When you add a new MCP server host, update <strong>both</strong>.</p>
</blockquote>
<h2>Step 5 — Run the migrator and verify</h2>
<p>Apply the seed by running the DbMigrator:</p>
<pre><code class="language-bash">cd src/Acme.BookStore.DbMigrator
dotnet run
</code></pre>
<blockquote>
<p><strong>If your migrator's console stays silent:</strong> the ABP template logs through Serilog's <code>WriteTo.Async(...)</code>, which buffers on a background thread. Add a flush at the very end of <code>Program.Main</code> so your seed logs aren't dropped when the process exits:</p>
<pre><code class="language-csharp">await CreateHostBuilder(args).RunConsoleAsync();
Log.CloseAndFlush();   // flush buffered async-sink lines before exit
</code></pre>
</blockquote>
<h3>5a — Machine-to-machine smoke test (curl)</h3>
<p>The fastest way to confirm the scope, client, and resource registration all line up is a client-credentials call with the confidential test client:</p>
<pre><code class="language-bash">curl -k -X POST https://localhost:44300/connect/token \
  -d grant_type=client_credentials \
  -d client_id=BookStore_McpTest \
  -d client_secret='&lt;your-dev-secret&gt;' \
  -d scope=BookStoreMcp \
  -d resource=https://mcp.example.com/
</code></pre>
<p>A <code>200</code> with an <code>access_token</code> means everything is wired correctly. Decode the JWT (for example at <a href="https://jwt.io">jwt.io</a>) and confirm the <code>aud</code> claim equals your MCP server URL and <code>scope</code> contains <code>BookStoreMcp</code>.</p>
<h3>5b — Interactive flow (MCP Inspector)</h3>
<p>Run the Inspector and point it at your MCP server:</p>
<pre><code class="language-bash">npx @modelcontextprotocol/inspector
</code></pre>
<p>When you connect to a protected MCP server, the Inspector discovers your ABP app from the server's Protected Resource Metadata, opens the ABP login page, you sign in and consent, and it completes the <code>authorization_code</code> + PKCE exchange against <code>https://localhost:44300/connect/token</code>. The token it receives is scoped to <code>BookStoreMcp</code> with <code>aud</code> = your MCP server URL.</p>
<h2>Conclusion</h2>
<p>Your ABP application was already a capable OAuth 2.1 authorization server, and MCP authentication is just a matter of telling it about <strong>one scope</strong>, <strong>two clients</strong>, and the <strong>resource URLs</strong> your MCP server answers to. No custom auth code — the OpenIddict module does the heavy lifting, and the seed contributor keeps the configuration reproducible across environments.</p>
<p>From here you can grant the MCP scope to whichever interactive tools your team uses, add more resource URLs as you deploy MCP servers, and rely on standard ABP permission checks inside the tools your MCP server exposes.</p>
<p>If you try this in your own solution, I'd love to hear how it goes — drop a comment with your setup or any rough edges you hit.</p>
<h2>Appendix — A minimal ASP.NET Core MCP server (the resource server)</h2>
<p>The article above configured the <strong>authorization server</strong> (your ABP app). To have something to actually sign in to, here's a minimal <strong>resource server</strong>: an ASP.NET Core MCP server that validates the ABP-issued JWT and advertises its authorization server via Protected Resource Metadata. It's a single <code>Program.cs</code>.</p>
<blockquote>
<p>The official C# MCP SDK evolves quickly — treat the SDK calls (<code>AddMcpServer</code>, <code>WithHttpTransport</code>, <code>MapMcp</code>, the <code>[McpServerTool]</code> attributes) as a guide and check the latest <a href="https://github.com/modelcontextprotocol/csharp-sdk"><code>modelcontextprotocol/csharp-sdk</code></a> docs for the current API surface. The auth/PRM parts are plain ASP.NET Core and are stable.</p>
</blockquote>
<h3>A.1 — Create the project and add packages</h3>
<pre><code class="language-bash">dotnet new web -n Acme.BookStore.McpServer
cd Acme.BookStore.McpServer
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package ModelContextProtocol.AspNetCore   # official MCP server SDK
</code></pre>
<h3>A.2 — The complete Program.cs</h3>
<pre><code class="language-csharp">using System.ComponentModel;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using ModelContextProtocol.Server;   // [McpServerTool], [McpServerToolType]

var builder = WebApplication.CreateBuilder(args);

const string McpServerUrl = &quot;https://localhost:7071/&quot;;   // THIS server's canonical URL
const string AuthServer   = &quot;https://localhost:44300&quot;;   // your ABP app (authorization server)

// --- Validate the ABP-issued token (this is a pure resource server) ---
builder.Services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =&gt;
    {
        options.Authority = AuthServer;     // discovers signing keys via /.well-known/openid-configuration
        options.Audience  = McpServerUrl;   // must equal the token `aud`
        options.TokenValidationParameters.ValidateAudience = true;

        // Per the MCP spec, point unauthenticated callers at this resource server's
        // metadata (RFC 9728) so they can discover the authorization server.
        options.Events = new JwtBearerEvents
        {
            OnChallenge = context =&gt;
            {
                context.HandleResponse();
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                context.Response.Headers.WWWAuthenticate =
                    $&quot;Bearer resource_metadata=\&quot;{McpServerUrl}.well-known/oauth-protected-resource\&quot;&quot;;
                return Task.CompletedTask;
            }
        };
    });

builder.Services.AddAuthorization();

// --- Register the MCP server, HTTP transport, and your tools ---
builder.Services
    .AddMcpServer()
    .WithHttpTransport()
    .WithTools&lt;BookStoreTools&gt;();

var app = builder.Build();

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

// --- RFC 9728: Protected Resource Metadata — which authorization server to use ---
app.MapGet(&quot;/.well-known/oauth-protected-resource&quot;, () =&gt; Results.Json(new
{
    resource = McpServerUrl,
    authorization_servers = new[] { AuthServer }
}));

// Require a valid ABP-issued token on the MCP endpoints.
app.MapMcp().RequireAuthorization();

app.Run();

[McpServerToolType]
public class BookStoreTools
{
    [McpServerTool, Description(&quot;Returns a friendly greeting.&quot;)]
    public string Hello(string name) =&gt; $&quot;Hello, {name}, from the BookStore MCP server!&quot;;
}
</code></pre>
<p>What the three pieces do:</p>
<ul>
<li><strong>JWT bearer</strong> — <code>Authority</code> is your ABP app; the handler fetches its signing keys automatically and rejects any token whose <code>aud</code> isn't this server's URL.</li>
<li><strong><code>OnChallenge</code></strong> — emits <code>WWW-Authenticate: Bearer resource_metadata=&quot;…&quot;</code> on a 401 so an MCP client can find the metadata document below.</li>
<li><strong>Protected Resource Metadata</strong> — the <code>/.well-known/oauth-protected-resource</code> document tells the client which authorization server (your ABP app) to use.</li>
</ul>
<h3>A.3 — Run it end to end</h3>
<ol>
<li>Run your ABP app (<code>Acme.BookStore.HttpApi.Host</code>) on <code>https://localhost:44300</code>.</li>
<li>Run the MCP server on <code>https://localhost:7071</code>.</li>
<li>Run the MCP Inspector (<code>npx @modelcontextprotocol/inspector</code>) and connect to <code>https://localhost:7071</code>.</li>
<li>The Inspector reads the PRM, redirects you to the ABP login, you consent, and it calls your <code>Hello</code> tool with a valid token.</li>
</ol>
<p>Because the token's <code>aud</code> is <code>https://localhost:7071/</code> (set by the scope's <code>Resources</code> in Step 1) and the MCP server validates that exact audience, only tokens minted <em>for this server</em> are accepted — which is the whole point of RFC 8707 Resource Indicators.</p>
<h2>References</h2>
<ul>
<li><a href="https://abp.io/docs/latest/modules/openiddict">ABP OpenIddict Module</a></li>
<li><a href="https://modelcontextprotocol.io/specification">Model Context Protocol — Authorization specification</a></li>
<li><a href="https://github.com/modelcontextprotocol/csharp-sdk">MCP C# SDK (<code>modelcontextprotocol/csharp-sdk</code>)</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8707">RFC 8707 — Resource Indicators for OAuth 2.0</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc9728">RFC 9728 — OAuth 2.0 Protected Resource Metadata</a></li>
<li><a href="https://datatracker.ietf.org/doc/html/rfc8414">RFC 8414 — OAuth 2.0 Authorization Server Metadata</a></li>
<li><a href="https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication">OpenID Connect Core — Client Authentication</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a21b3b1-6dc0-4f41-186b-ec4e6f91a708" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a21b3b1-6dc0-4f41-186b-ec4e6f91a708" medium="image" />
    </item>
  </channel>
</rss>