<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: chaogetalks</title>
    <description>The latest articles on DEV Community by chaogetalks (@chaogetalks).</description>
    <link>https://dev.to/chaogetalks</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4238204%2F7e96541c-df66-41db-9390-dcf6ab32087e.png</url>
      <title>DEV Community: chaogetalks</title>
      <link>https://dev.to/chaogetalks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chaogetalks"/>
    <language>en</language>
    <item>
      <title>MCP Authorization: Protect an MCP Server with OAuth 2.1</title>
      <dc:creator>chaogetalks</dc:creator>
      <pubDate>Wed, 11 Nov 2026 17:07:48 +0000</pubDate>
      <link>https://dev.to/chaogetalks/mcp-authorization-protect-an-mcp-server-with-oauth-21-3o3</link>
      <guid>https://dev.to/chaogetalks/mcp-authorization-protect-an-mcp-server-with-oauth-21-3o3</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://geekcoding101.com/posts/mcp-authorization-oauth-2-1" rel="noopener noreferrer"&gt;GeekCoding101&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You put an MCP server on an HTTP port, and now anyone who can reach that port can call your tools. That is the problem MCP authorization solves, and the spec's answer is OAuth 2.1 plus a small discovery document.&lt;/p&gt;

&lt;p&gt;This post covers what the spec requires, how the 401-to-token flow works, and how to wire it up with the Python SDK. I ran a small demo server and show the real &lt;code&gt;curl&lt;/code&gt; output. If you're new to the protocol itself, start with &lt;a href="https://geekcoding101.com/posts/what-is-an-mcp-server" rel="noopener noreferrer"&gt;what is an MCP server&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One honest caveat up front: the demo uses a &lt;strong&gt;fake hard-coded token&lt;/strong&gt; and a &lt;strong&gt;placeholder issuer&lt;/strong&gt; (&lt;code&gt;auth.example.com&lt;/code&gt;). There is no real authorization server behind it, and I did not run the full authorization code flow. The section on what the SDK does versus what you own says what to plug in for production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Short answer: what is MCP authorization?
&lt;/h2&gt;

&lt;p&gt;MCP authorization is the part of the spec that lets an MCP server accept requests only from clients holding a valid OAuth 2.1 access token. Per the &lt;a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization" rel="noopener noreferrer"&gt;MCP authorization spec (2026-07-28)&lt;/a&gt;, your MCP server acts as an OAuth 2.1 &lt;strong&gt;resource server&lt;/strong&gt; and the MCP client acts as an OAuth 2.1 &lt;strong&gt;client&lt;/strong&gt;. The authorization server that issues tokens is out of the spec's scope.&lt;/p&gt;

&lt;p&gt;Authorization is optional. HTTP-based transports &lt;strong&gt;SHOULD&lt;/strong&gt; follow it, while stdio servers &lt;strong&gt;SHOULD NOT&lt;/strong&gt; and should take credentials from the environment instead.&lt;/p&gt;

&lt;p&gt;In practice your server does three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publishes &lt;strong&gt;Protected Resource Metadata&lt;/strong&gt; (RFC 9728) that names its authorization server(s).&lt;/li&gt;
&lt;li&gt;Answers unauthenticated requests with &lt;code&gt;401&lt;/code&gt; and a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header that points at that metadata.&lt;/li&gt;
&lt;li&gt;Validates the bearer token on every request, including its audience.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the MCP OAuth flow works
&lt;/h2&gt;

&lt;p&gt;Here is the sequence, from the first request to an authorized call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The client sends a request to the MCP endpoint with no token.&lt;/li&gt;
&lt;li&gt;The server replies &lt;code&gt;401&lt;/code&gt; with a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header containing a &lt;code&gt;resource_metadata&lt;/code&gt; URL (and, ideally, the &lt;code&gt;scope&lt;/code&gt; needed).&lt;/li&gt;
&lt;li&gt;The client fetches the Protected Resource Metadata. If the header doesn't help, it falls back to the well-known URL, either at the root or path-suffixed. Clients &lt;strong&gt;MUST&lt;/strong&gt; support both discovery methods.&lt;/li&gt;
&lt;li&gt;The metadata lists &lt;code&gt;authorization_servers&lt;/code&gt;. The client discovers that server's endpoints using RFC 8414 or OpenID Connect discovery. The issuer must match.&lt;/li&gt;
&lt;li&gt;The client gets a client ID. The spec's order of preference is: pre-registered credentials, then Client ID Metadata Documents (&lt;strong&gt;SHOULD&lt;/strong&gt;), then Dynamic Client Registration (&lt;strong&gt;MAY&lt;/strong&gt;, marked deprecated), then manual entry by the user.&lt;/li&gt;
&lt;li&gt;The client runs the authorization code flow with &lt;strong&gt;PKCE using S256&lt;/strong&gt;. It must refuse to proceed if the authorization server doesn't advertise &lt;code&gt;code_challenge_methods_supported&lt;/code&gt;. It also sends the &lt;code&gt;resource&lt;/code&gt; parameter (RFC 8707) in both the authorization and token requests. Authorization servers &lt;strong&gt;SHOULD&lt;/strong&gt; include &lt;code&gt;iss&lt;/code&gt; (RFC 9207) in the authorization response, and the client checks it against the issuer it recorded before sending the code to the token endpoint.&lt;/li&gt;
&lt;li&gt;The client sends the token in the &lt;code&gt;Authorization: Bearer&lt;/code&gt; header on &lt;strong&gt;every&lt;/strong&gt; request. Never in the query string.&lt;/li&gt;
&lt;li&gt;The server validates the token, including that it was issued for this server (the audience). Invalid or expired tokens get &lt;code&gt;401&lt;/code&gt;. A valid token with too few scopes gets &lt;code&gt;403&lt;/code&gt; with &lt;code&gt;error="insufficient_scope"&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the user-facing part of steps 5 and 6 is unfamiliar, read &lt;a href="https://geekcoding101.com/posts/oauth-grant-types" rel="noopener noreferrer"&gt;OAuth grant types&lt;/a&gt; and the &lt;a href="https://geekcoding101.com/posts/oauth-2-0-authorization-code-flow" rel="noopener noreferrer"&gt;authorization code flow&lt;/a&gt; first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who plays which role
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Who it is in MCP&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Resource server&lt;/td&gt;
&lt;td&gt;Your MCP server&lt;/td&gt;
&lt;td&gt;Publishes Protected Resource Metadata, validates tokens, enforces scopes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth client&lt;/td&gt;
&lt;td&gt;The MCP client (the app hosting the model)&lt;/td&gt;
&lt;td&gt;Discovers metadata, uses PKCE, obtains tokens, sends them as Bearer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization server&lt;/td&gt;
&lt;td&gt;Your identity provider or a self-hosted AS&lt;/td&gt;
&lt;td&gt;Authenticates the user, issues access tokens. Out of scope for the spec&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Hands-on: a protected MCP server with the Python SDK
&lt;/h2&gt;

&lt;p&gt;I used &lt;code&gt;mcp&lt;/code&gt; 2.3.0, Python 3.12.12, uvicorn 0.54.0 and curl 8.7.1. The server has one tool, &lt;code&gt;add&lt;/code&gt;, and requires a bearer token with the &lt;code&gt;mcp:tools&lt;/code&gt; scope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;AnyHttpUrl&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;AccessToken&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;AuthSettings&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mcpserver&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;MCPServer&lt;/span&gt;

&lt;span class="no"&gt;RESOURCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:8765/mcp"&lt;/span&gt;
&lt;span class="no"&gt;DEMO_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"demo-token"&lt;/span&gt;  &lt;span class="c1"&gt;# fake value, for demonstration only&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoTokenVerifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;AccessToken&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;DEMO_TOKEN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# -&amp;gt; 401 invalid_token&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"demo-client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp:tools"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;RESOURCE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MCPServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"auth-demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;token_verifier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;DemoTokenVerifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;AuthSettings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;issuer_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;AnyHttpUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://auth.example.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;resource_server_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;AnyHttpUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RESOURCE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;required_scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp:tools"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;validate_token_resource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""Add two numbers."""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"streamable-http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8765&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two pieces do the work. A &lt;code&gt;TokenVerifier&lt;/code&gt; is any object with an async &lt;code&gt;verify_token(token)&lt;/code&gt; that returns an &lt;code&gt;AccessToken&lt;/code&gt; or &lt;code&gt;None&lt;/code&gt;. &lt;code&gt;AuthSettings&lt;/code&gt; tells the SDK which issuer to advertise, what your server's own URL is, and which scopes are required.&lt;/p&gt;

&lt;p&gt;Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python server.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the requests below I saved an MCP &lt;code&gt;initialize&lt;/code&gt; message (protocol version &lt;code&gt;2025-06-18&lt;/code&gt;) as &lt;code&gt;init.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"initialize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"params"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;"protocolVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"2025-06-18"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"capabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;"clientInfo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"curl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run a: no token
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -si -X POST http://127.0.0.1:8765/mcp \
  -H &lt;span class="s1"&gt;'Accept: application/json, text/event-stream'&lt;/span&gt; \
  -H &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; \
  -d @init.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="nc"&gt;Unauthorized&lt;/span&gt;
&lt;span class="na"&gt;www-authenticate: Bearer error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"invalid_token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Authentication required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource_metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://127.0.0.1:8765/.well-known/oauth-protected-resource/mcp"&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"invalid_token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"error_description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Authentication required"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is step 2 of the flow. The &lt;code&gt;resource_metadata&lt;/code&gt; URL in the header is where a client goes next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fmkabl49zabu8a1mwml.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fmkabl49zabu8a1mwml.webp" alt="curl without a token gets 401 and a WWW-Authenticate header with resource_metadata" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Run b: fetch the Protected Resource Metadata
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -si http://127.0.0.1:8765/.well-known/oauth-protected-resource/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is &lt;code&gt;200&lt;/code&gt; with &lt;code&gt;cache-control&lt;/code&gt; set to &lt;code&gt;public&lt;/code&gt; and &lt;code&gt;max-age=3600&lt;/code&gt;, and this body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"http://127.0.0.1:8765/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"authorization_servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"https://auth.example.com/"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"scopes_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp:tools"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"bearer_methods_supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"header"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A real client would now read &lt;code&gt;authorization_servers&lt;/code&gt; and go discover that server's endpoints. Mine points at a placeholder, so this is where the demo stops being a full flow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zq53fodz6apsn535q3p.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zq53fodz6apsn535q3p.webp" alt="Protected resource metadata JSON returned by the demo server" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Run c: a wrong token
&lt;/h3&gt;

&lt;p&gt;Same request as run a, with &lt;code&gt;-H 'Authorization: Bearer wrong-token'&lt;/code&gt; added. The result is a &lt;code&gt;401&lt;/code&gt; with headers and body identical to run a. The server doesn't say "your token is wrong" in a different way. It just says authentication is required and points back at the metadata.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jum9jvmifhuqf8t9sub.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8jum9jvmifhuqf8t9sub.webp" alt="A wrong bearer token still gets 401 invalid_token" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Run d: the demo token
&lt;/h3&gt;

&lt;p&gt;Again the same request, this time with &lt;code&gt;-H 'Authorization: Bearer demo-token'&lt;/code&gt;. The server answers &lt;code&gt;200&lt;/code&gt; with &lt;code&gt;content-type: text/event-stream&lt;/code&gt; and an &lt;code&gt;mcp-session-id&lt;/code&gt; header. The event carries the initialize result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;event: message
data: {"jsonrpc":"2.0","id":1,"result":{"capabilities":{"prompts":{"listChanged":false},"resources":{"listChanged":false,"subscribe":false},"tools":{"listChanged":false}},"protocolVersion":"2025-06-18","serverInfo":{"name":"auth-demo","version":""}}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4in6i88y63lwagjywmzh.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4in6i88y63lwagjywmzh.webp" alt="The demo token gets a 200 event stream with the initialize result" width="800" height="347"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What the SDK does vs what you own
&lt;/h2&gt;

&lt;p&gt;The SDK handles the HTTP plumbing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;BearerAuthBackend&lt;/code&gt; and &lt;code&gt;RequireAuthMiddleware&lt;/code&gt; read the &lt;code&gt;Authorization&lt;/code&gt; header. They return &lt;code&gt;401&lt;/code&gt; for a bad or missing token and &lt;code&gt;403&lt;/code&gt; when a required scope is missing. I did not run the &lt;code&gt;403&lt;/code&gt; case or a &lt;code&gt;tools/call&lt;/code&gt; in this demo.&lt;/li&gt;
&lt;li&gt;It adds the Protected Resource Metadata route automatically, built from your &lt;code&gt;AuthSettings&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You own everything that makes the token mean something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real verification.&lt;/strong&gt; My &lt;code&gt;DemoTokenVerifier&lt;/code&gt; compares a string. In production, &lt;code&gt;verify_token&lt;/code&gt; should validate a signed JWT (signature, issuer, expiry) or call your authorization server's token introspection endpoint. For the JWT route, see &lt;a href="https://geekcoding101.com/posts/unlocking-web-security-master-jwt-authentication" rel="noopener noreferrer"&gt;JWT authentication explained&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audience.&lt;/strong&gt; The spec says the server &lt;strong&gt;MUST&lt;/strong&gt; validate that the token was issued for it. Fill in &lt;code&gt;AccessToken.resource&lt;/code&gt; from the token's audience or resource claim, and keep &lt;code&gt;validate_token_resource=True&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scopes.&lt;/strong&gt; Decide what &lt;code&gt;required_scopes&lt;/code&gt; should be, and what each scope allows. Return the scopes the token actually carries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The authorization server.&lt;/strong&gt; Pick one, point &lt;code&gt;issuer_url&lt;/code&gt; at it, and make sure it supports PKCE with S256. Registering clients is its job and the client's, not your resource server's.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Gotchas I hit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A wrong token gets 401, not 403.&lt;/strong&gt; &lt;code&gt;403&lt;/code&gt; is for a valid token that lacks a required scope (&lt;code&gt;insufficient_scope&lt;/code&gt;). An unknown or invalid token is &lt;code&gt;401&lt;/code&gt;, as run c shows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The metadata lives only at the path-suffixed URL.&lt;/strong&gt; My server's endpoint is &lt;code&gt;/mcp&lt;/code&gt;, so the metadata is at &lt;code&gt;/.well-known/oauth-protected-resource/mcp&lt;/code&gt;. The root &lt;code&gt;/.well-known/oauth-protected-resource&lt;/code&gt; returned &lt;code&gt;404&lt;/code&gt;. Clients must try both, but don't test only the root and conclude it's broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The issuer gained a trailing slash in my run.&lt;/strong&gt; I passed &lt;code&gt;https://auth.example.com&lt;/code&gt; and the metadata came back with &lt;code&gt;https://auth.example.com/&lt;/code&gt;. I only report what I saw and did not dig into why. The spec requires the issuer to match during discovery, so with a real authorization server, set &lt;code&gt;issuer_url&lt;/code&gt; to exactly the issuer string it publishes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;validate_token_resource&lt;/code&gt; warns if you leave it unset.&lt;/strong&gt; The SDK emits a deprecation warning, because the default becomes &lt;code&gt;True&lt;/code&gt; in 3.0. Set it explicitly now.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  MCP server security checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Serve over HTTPS in production. My demo uses plain HTTP on &lt;code&gt;127.0.0.1&lt;/code&gt; only for local testing.&lt;/li&gt;
&lt;li&gt;Validate the signature, expiry, issuer and &lt;strong&gt;audience&lt;/strong&gt; of every token, on every request.&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;401&lt;/code&gt; for invalid or expired tokens and &lt;code&gt;403&lt;/code&gt; with &lt;code&gt;insufficient_scope&lt;/code&gt; for missing scopes. Include &lt;code&gt;scope&lt;/code&gt; on the &lt;code&gt;401&lt;/code&gt; where you can.&lt;/li&gt;
&lt;li&gt;Accept tokens only in the &lt;code&gt;Authorization: Bearer&lt;/code&gt; header, never in the query string.&lt;/li&gt;
&lt;li&gt;Never pass the client's token on to upstream APIs. If your tool calls another service, get a separate token for that service.&lt;/li&gt;
&lt;li&gt;Pick the narrowest scopes that work, and check them per tool if the tools differ in risk.&lt;/li&gt;
&lt;li&gt;Keep secrets out of &lt;code&gt;server.py&lt;/code&gt;. The &lt;code&gt;demo-token&lt;/code&gt; in this post is fake on purpose.&lt;/li&gt;
&lt;li&gt;Read the spec's security considerations page before shipping.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your server exposes powerful local tooling, such as in the &lt;a href="https://geekcoding101.com/posts/ghidra-mcp-ai-reverse-engineering" rel="noopener noreferrer"&gt;Ghidra MCP reverse engineering setup&lt;/a&gt;, this is even more important.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is MCP authorization required?
&lt;/h3&gt;

&lt;p&gt;No. The spec makes authorization optional. HTTP-based transports &lt;strong&gt;SHOULD&lt;/strong&gt; follow it, but if your server only listens on a private machine or you control every client, you may choose otherwise. Anything reachable over a network should have it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does MCP use OAuth 2.1?
&lt;/h3&gt;

&lt;p&gt;Yes. The MCP server is an OAuth 2.1 resource server and the MCP client is an OAuth 2.1 client. Clients must use PKCE with S256 and send the &lt;code&gt;resource&lt;/code&gt; parameter from RFC 8707.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is Protected Resource Metadata?
&lt;/h3&gt;

&lt;p&gt;It's a JSON document (RFC 9728) that your server publishes to say which authorization servers it trusts. The one in run b contains &lt;code&gt;resource&lt;/code&gt;, &lt;code&gt;authorization_servers&lt;/code&gt;, &lt;code&gt;scopes_supported&lt;/code&gt; and &lt;code&gt;bearer_methods_supported&lt;/code&gt;. Clients find it through the &lt;code&gt;resource_metadata&lt;/code&gt; URL in the &lt;code&gt;401&lt;/code&gt; response or at a well-known path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does a stdio MCP server need OAuth?
&lt;/h3&gt;

&lt;p&gt;No. The spec says stdio transports &lt;strong&gt;SHOULD NOT&lt;/strong&gt; follow the HTTP authorization flow and should take credentials from the environment instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Dynamic Client Registration still supported?
&lt;/h3&gt;

&lt;p&gt;Yes, but as a &lt;strong&gt;MAY&lt;/strong&gt; and marked deprecated. The spec's preferred order is pre-registered credentials, then Client ID Metadata Documents, then DCR, then manual entry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can my MCP server forward the user's token to another API?
&lt;/h3&gt;

&lt;p&gt;No. The spec says servers &lt;strong&gt;MUST NOT&lt;/strong&gt; pass tokens through to upstream APIs. Use a separate credential for any downstream service.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>oauth</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
