<?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: Tom Howland</title>
    <description>The latest articles on DEV Community by Tom Howland (@tom_howland_571545aeee419).</description>
    <link>https://dev.to/tom_howland_571545aeee419</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%2F3977625%2F422d8af4-de3b-41fc-a36f-d8101b66b4ca.jpg</url>
      <title>DEV Community: Tom Howland</title>
      <link>https://dev.to/tom_howland_571545aeee419</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tom_howland_571545aeee419"/>
    <language>en</language>
    <item>
      <title>OAuth for Remote MCP Servers</title>
      <dc:creator>Tom Howland</dc:creator>
      <pubDate>Wed, 10 Jun 2026 12:48:14 +0000</pubDate>
      <link>https://dev.to/tom_howland_571545aeee419/oauth-for-remote-mcp-servers-3n8i</link>
      <guid>https://dev.to/tom_howland_571545aeee419/oauth-for-remote-mcp-servers-3n8i</guid>
      <description>&lt;h1&gt;
  
  
  OAuth for Remote MCP Servers
&lt;/h1&gt;

&lt;p&gt;How each AI assistant signs in to a remote MCP (Model Context Protocol) server, and why the flow differs by client and by where it runs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Overview&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The protocol throughout is standard &lt;strong&gt;OAuth 2.1&lt;/strong&gt; — an open, widely implemented authorization standard. The human sign-in runs through &lt;strong&gt;oauth2-proxy&lt;/strong&gt;, one of the most widely deployed open-source auth proxies; the only deployment-specific piece is a thin, spec-conforming authorization server (the &lt;code&gt;/oauth&lt;/code&gt; endpoints) that hands MCP clients their tokens. Every client ends up the same way — a person signs in against Google (restricted to your organization's domain), and the client holds a short-lived &lt;strong&gt;bearer token&lt;/strong&gt; it presents on each &lt;code&gt;/mcp&lt;/code&gt; call. Two things differ between assistants: &lt;strong&gt;where the client runs&lt;/strong&gt; (the user's machine — &lt;em&gt;local&lt;/em&gt; — vs. the vendor's cloud), which decides where the OAuth callback lands; and &lt;strong&gt;what kind of OAuth client it is&lt;/strong&gt; — a public client proving itself with PKCE (Proof Key for Code Exchange, which lets a client with no secret prove the token request comes from the same client that started the flow), or a confidential client proving itself with a secret.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The participants
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;oauth2-proxy&lt;/strong&gt; — the public-facing reverse proxy. It authenticates the human against Google (the sign-in restricted to your organization's domain) and forwards the verified identity to the app behind it. Only oauth2-proxy faces the internet. It is a mature, heavily-deployed open-source project — the standard way to put Google/OIDC (OpenID Connect) single sign-on in front of a service, widely used in Kubernetes deployments — so the most security-sensitive leg of the flow (the OAuth exchange with the identity provider) runs on battle-tested code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The MCP server&lt;/strong&gt; — the app on a loopback port behind the proxy. It plays two roles: the OAuth &lt;em&gt;authorization server&lt;/em&gt; (&lt;code&gt;/oauth/authorize&lt;/code&gt;, &lt;code&gt;/oauth/token&lt;/code&gt;, &lt;code&gt;/oauth/register&lt;/code&gt;, &lt;code&gt;.well-known&lt;/code&gt; discovery) and the &lt;code&gt;/mcp&lt;/code&gt; tool endpoint. It mints codes and tokens, and validates a token on every &lt;code&gt;/mcp&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google&lt;/strong&gt; — the identity provider oauth2-proxy delegates the actual sign-in to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The MCP client&lt;/strong&gt; — whatever holds the token and calls the tools: a &lt;em&gt;local&lt;/em&gt; client (Claude Code, Cursor) on a machine, or a &lt;em&gt;cloud&lt;/em&gt; client (Claude Desktop, Gemini Enterprise) on the vendor's servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The user's browser&lt;/strong&gt; — where the person completes the Google sign-in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The two axes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Local vs. cloud — where the client runs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Local&lt;/strong&gt; clients (Claude Code, Cursor) run on the user's machine and receive the OAuth callback on &lt;strong&gt;loopback&lt;/strong&gt; (&lt;code&gt;127.0.0.1&lt;/code&gt;) — the authorization code never leaves the machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloud&lt;/strong&gt; clients (Claude Desktop, Gemini Enterprise) run in the vendor's cloud, so the callback is a registered vendor URL — which is why the server has to be reachable from the internet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Public vs. confidential — the OAuth client type
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Public clients&lt;/strong&gt; self-register at runtime (dynamic registration) and prove themselves with &lt;strong&gt;PKCE&lt;/strong&gt;, no secret — Claude Code, Cursor, and Claude Desktop.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;confidential client&lt;/strong&gt; is pre-provisioned once with a &lt;code&gt;client_id&lt;/code&gt; + secret and proves itself with that &lt;strong&gt;secret&lt;/strong&gt; — only Gemini Enterprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The configurations at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assistant&lt;/th&gt;
&lt;th&gt;Client runs&lt;/th&gt;
&lt;th&gt;OAuth callback&lt;/th&gt;
&lt;th&gt;OAuth client&lt;/th&gt;
&lt;th&gt;Proves itself with&lt;/th&gt;
&lt;th&gt;Notes / requirements&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code (CLI)&lt;/td&gt;
&lt;td&gt;local machine&lt;/td&gt;
&lt;td&gt;loopback&lt;/td&gt;
&lt;td&gt;dynamic, public&lt;/td&gt;
&lt;td&gt;PKCE&lt;/td&gt;
&lt;td&gt;works once registered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor (IDE)&lt;/td&gt;
&lt;td&gt;local machine&lt;/td&gt;
&lt;td&gt;loopback&lt;/td&gt;
&lt;td&gt;dynamic, public&lt;/td&gt;
&lt;td&gt;PKCE&lt;/td&gt;
&lt;td&gt;works via the &lt;code&gt;mcp-remote&lt;/code&gt; shim *&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Desktop&lt;/td&gt;
&lt;td&gt;Anthropic cloud&lt;/td&gt;
&lt;td&gt;vendor URL (&lt;code&gt;claude.ai&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;dynamic, public&lt;/td&gt;
&lt;td&gt;PKCE&lt;/td&gt;
&lt;td&gt;works once registered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini Enterprise&lt;/td&gt;
&lt;td&gt;Google cloud&lt;/td&gt;
&lt;td&gt;vendor URL (Google)&lt;/td&gt;
&lt;td&gt;pre-provisioned, confidential&lt;/td&gt;
&lt;td&gt;client secret&lt;/td&gt;
&lt;td&gt;requires a Gemini Enterprise license + admin connector registration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor Cloud Agents&lt;/td&gt;
&lt;td&gt;Cursor cloud&lt;/td&gt;
&lt;td&gt;vendor URL&lt;/td&gt;
&lt;td&gt;dynamic / static&lt;/td&gt;
&lt;td&gt;PKCE / secret&lt;/td&gt;
&lt;td&gt;requires a Cursor team admin to add the server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;* Cursor's local IDE connects through the &lt;code&gt;mcp-remote&lt;/code&gt; shim — see its section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Code &amp;amp; Cursor — local
&lt;/h2&gt;

&lt;p&gt;This is the local baseline: the client runs on the user's machine, so it completes the full OAuth flow with a &lt;strong&gt;loopback callback&lt;/strong&gt; (&lt;code&gt;127.0.0.1&lt;/code&gt;). It self-registers (dynamic registration), the loopback redirect is allowlisted, and the authorization code never leaves the machine. It is a &lt;strong&gt;public client&lt;/strong&gt;: no secret — PKCE ties the token request back to the same client that started the flow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3r48unno1elrvue1irte.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3r48unno1elrvue1irte.png" alt="Local PKCE flow — everything but the Google sign-in stays on the user's machine." width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Local PKCE flow — everything but the Google sign-in stays on the user's machine.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Cursor exception
&lt;/h3&gt;

&lt;p&gt;Cursor follows the same local flow, but a &lt;a href="https://forum.cursor.com/t/oauth-browser-redirect-not-triggered-for-http-based-mcp-servers/146988" rel="noopener noreferrer"&gt;known Cursor bug&lt;/a&gt; stops the IDE from opening the browser after it registers — so the sign-in step never starts. The workaround is the &lt;code&gt;mcp-remote&lt;/code&gt; shim (&lt;code&gt;npx -y mcp-remote@latest https://mcp.example.com/mcp&lt;/code&gt;), which runs the OAuth flow itself and hands Cursor a working connection. Nothing on the server changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude Desktop — cloud
&lt;/h2&gt;

&lt;p&gt;Claude Desktop's connector runs in &lt;strong&gt;Anthropic's cloud&lt;/strong&gt;. It is still a &lt;strong&gt;public client&lt;/strong&gt;: it discovers the server's endpoints and registers itself dynamically (PKCE, no secret), exactly like the local clients — the only difference is that the callback is a cloud URL (&lt;code&gt;claude.ai&lt;/code&gt;) instead of loopback, so the authorization code — minted by your server, not by Google — transits Anthropic's servers. The person still signs in with their organization-domain Google account in the browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8hrsod9aezjs929jj3ra.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8hrsod9aezjs929jj3ra.png" alt="Cloud PKCE flow — like the local one, but the callback and token-bearing calls originate from Anthropic's cloud, so the server must be public." width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cloud PKCE flow — like the local one, but the callback and token-bearing calls originate from Anthropic's cloud, so the server must be reachable from the internet.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemini Enterprise — cloud, confidential (requires license + admin registration)
&lt;/h2&gt;

&lt;p&gt;Gemini Enterprise is the one &lt;strong&gt;confidential client&lt;/strong&gt;. Instead of registering itself at runtime, an admin mints a &lt;code&gt;client_id&lt;/code&gt; + &lt;strong&gt;secret&lt;/strong&gt; once (out of band) and enters them into the Gemini Enterprise connector config. The connector runs in &lt;strong&gt;Google's cloud&lt;/strong&gt;. The human still signs in (legs 1–6); then, server-to-server with no browser, Google's cloud exchanges the code for a token using its &lt;strong&gt;secret&lt;/strong&gt; (leg 7) and calls &lt;code&gt;/mcp&lt;/code&gt; (leg 9). This path requires a Gemini Enterprise license and admin registration of the connector on the Google side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88y71jc3gah0h1xvijg7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88y71jc3gah0h1xvijg7.png" alt="Confidential cloud flow — the connector proves itself with a pre-shared secret at token exchange rather than PKCE." width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Confidential cloud flow — the connector proves itself with a pre-shared secret at token exchange (leg 7) rather than PKCE. Note the two "Googles": Google cloud is the connector (the OAuth client, redirecting through &lt;code&gt;vertexaisearch.cloud.google.com&lt;/code&gt;); Google is the identity provider that signs the person in.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cursor Cloud Agents — cloud (requires team admin)
&lt;/h2&gt;

&lt;p&gt;Cursor's Cloud Agents would connect from Cursor's cloud like Gemini Enterprise and Claude Desktop (Streamable HTTP, with OAuth). But adding the server is gated by Cursor's own permissions — only a &lt;strong&gt;Cursor team admin&lt;/strong&gt; can add an MCP server to the team ("Only team admins can manage the default team marketplace"). Until an admin adds the server, no OAuth flow runs, so there is no completed flow to diagram. This is a vendor-side gate, not a property of the OAuth design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting a client
&lt;/h2&gt;

&lt;p&gt;What to type into each assistant once the server is deployed. Every path&lt;br&gt;
ends the same way: a browser opens and the person signs in with their&lt;br&gt;
organization Google account.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Desktop&lt;/strong&gt; — Settings → Connectors → Add custom connector; paste
&lt;code&gt;https://mcp.example.com/mcp&lt;/code&gt; and click Connect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code&lt;/strong&gt; — &lt;code&gt;claude mcp add --transport http my-server https://mcp.example.com/mcp&lt;/code&gt;,
then run &lt;code&gt;/mcp&lt;/code&gt; and choose Authenticate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor&lt;/strong&gt; — configure the server through the &lt;code&gt;mcp-remote&lt;/code&gt; shim in
&lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;, then toggle it off and back on under Settings →
Tools &amp;amp; Integrations → MCP:
&lt;/li&gt;
&lt;/ul&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"my-server"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote@latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.example.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini Enterprise&lt;/strong&gt; — an admin registers the connector in Gemini
Enterprise with the pre-provisioned client id and secret and enables it
for the team; each person authorizes once from the Gemini side panel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;The OAuth 2.1 backbone here is well-trodden: the authorization flow, PKCE, dynamic client registration, and Protected Resource Metadata discovery all follow the published standard and the common explainers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://modelcontextprotocol.io/specification/draft/basic/authorization" rel="noopener noreferrer"&gt;Authorization — Model Context Protocol specification&lt;/a&gt; — the source of truth: OAuth 2.1, PKCE (S256) for every client, and the 401 → metadata discovery sequence.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@yagmur.sahin/remote-mcp-in-the-real-world-oauth-2-1-9d149de6e475" rel="noopener noreferrer"&gt;Remote MCP in the Real World: OAuth 2.1, Dynamic Client Registration, and Protected Resource Metadata&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://mcp.directory/blog/oauth-21-for-remote-mcp-servers-streamable-http-explained-2026" rel="noopener noreferrer"&gt;OAuth 2.1 for Remote MCP Servers — Streamable HTTP explained (2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.scalekit.com/blog/ship-secure-mcp-server" rel="noopener noreferrer"&gt;Scalekit — Secure your MCP servers with OAuth 2.1&lt;/a&gt; · &lt;a href="https://aembit.io/blog/mcp-oauth-2-1-pkce-and-the-future-of-ai-authorization/" rel="noopener noreferrer"&gt;Aembit — MCP, OAuth 2.1, PKCE, and the Future of AI Authorization&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://sunpeak.ai/blogs/claude-connector-oauth-authentication/" rel="noopener noreferrer"&gt;Claude Connector Authentication: How OAuth Works and When You Need It&lt;/a&gt; and &lt;a href="https://platform.claude.com/docs/en/agents-and-tools/mcp-connector" rel="noopener noreferrer"&gt;Anthropic — MCP connector&lt;/a&gt; — the Claude Desktop leg.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The takeaway
&lt;/h3&gt;

&lt;p&gt;The protocol is not the interesting part — the standard is borrowed and identical for every client. What varies, and what this comparison maps, are the two axes that decide everything else: where the client runs (local vs. cloud), which fixes where the OAuth callback lands; and how it proves itself (PKCE vs. a pre-shared secret), which fixes whether it can self-register or must be provisioned by an admin. The deployment shape that makes this work is worth naming: front the server with oauth2-proxy for the Google sign-in, place a thin spec-conforming authorization server behind it, and serve every client from a single internet-reachable host — the OAuth callback must be public for cloud clients anyway, and one host keeps the topology simple. Authentication, not network placement, is the boundary. Within that shape, only a confidential client (Gemini Enterprise) needs a pre-shared secret, and the practical friction is rarely the protocol — it is vendor-side gates such as a client browser-open bug or a team-admin permission on adding the server.&lt;/p&gt;

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