BuildWithMatija
  1. Home
  2. Blog
  3. AI
  4. MCP Authentication: Why Remote MCP Servers Need OAuth

MCP Authentication: Why Remote MCP Servers Need OAuth

OAuth discovery for MCP: RFC 9728 resource_metadata, Hono CORS exposure, and practical fixes for Claude & ChatGPT…

25th September 2026·Updated on:30th September 2026·MŽMatija Žiberna·
AI
MCP Authentication: Why Remote MCP Servers Need OAuth

📚 Get Practical Development Guides

Join developers getting comprehensive guides, code examples, optimization tips, and time-saving prompts to accelerate their development workflow.

No spam. Unsubscribe anytime.

📄View markdown version
0

Frequently Asked Questions

Comments

About the author

Matija Žiberna

Matija Žiberna

Full-stack developer, co-founder

AboutResume

Self-taught full-stack developer sharing lessons from building software and startups.

I'm Matija Žiberna, a self-taught full-stack developer and co-founder passionate about building products, writing clean code, and figuring out how to turn ideas into businesses. I write about web development with Next.js, lessons from entrepreneurship, and the journey of learning by doing. My goal is to provide value through code—whether it's through tools, content, or real-world software.

Contents

  • What It Is
  • The Mental Model: The Valet and the Hotel Concierge
  • When To Use It
  • Public AI Agent Integrations (Claude & ChatGPT)
  • Multi-Tenant SaaS with Scoped User Data
  • Protocol Handshake Nuances vs Local Stdio
  • When NOT To Use It
  • Gotchas & Common Mistakes
  • 1. The Missing `resource_metadata` Challenge Header
  • 2. Failing to Expose CORS Headers
  • 3. Missing the RFC 9207 `iss` Parameter on Redirects
  • Conclusion
On this page:
  • What It Is
  • The Mental Model: The Valet and the Hotel Concierge
  • When To Use It
  • When NOT To Use It
  • Gotchas & Common Mistakes
Build with Matija Logo

Build with Matija

Complex B2B websites, headless CMS platforms, AI workflows, and internal systems designed and built with Next.js and Payload CMS.

Services

  • B2B Website Development
  • CMS Architecture Review & Platform Blueprint
  • Next.js + Payload Advisory
  • AI Integration & Implementation

Resources

  • CMS Hub
  • B2B Website Strategy
  • E-commerce Hub
  • Blog
  • Case Studies
  • About Matija

Payload CMS

  • Payload CMS Developer
  • Payload CMS Migration
  • Payload CMS Demos
  • All Payload CMS Resources

Discuss your project

Planning a rebuild, migration, application, workflow change, or platform decision? Start with the business problem and the system behind it.

Book a discovery callContact me →
© 2026Build with Matija•Alle Rechte vorbehalten•Datenschutzerklärung•Nutzungsbedingungen
BuildWithMatija
Get In Touch

While I haven't written extensively about the Model Context Protocol (MCP) in standalone pieces, I've spent the past year embedding custom MCP servers into Next.js and Payload CMS across client production environments (such as our Next.js MCP Server Guide). Recently, I tackled a project requiring a lightweight, standalone backend using Hono and needed to expose remote MCP access to it.

The initial intuition for most developers—myself included—is to wrap a few tool definitions in a route, throw a static API key or bearer token check in front, and call it secure.

But the moment you connect a remote MCP server to modern AI hosts like Claude.ai or ChatGPT, that mental model shatters: neither client prompts for a static key. Instead, they probe the endpoint unauthenticated, expecting RFC 9728 OAuth discovery headers. Here is what I learned about how remote MCP authorization actually functions, why static keys fail with AI clients, and how to wire standard OAuth discovery and CORS exposure in Hono.


What It Is

Remote MCP authentication is an automated, standards-based discovery and authorization handshake between an AI agent and your API, governed primarily by RFC 8414 and RFC 9728.

When you connect a modern AI client like Claude Desktop or ChatGPT to a remote MCP endpoint over Streamable HTTP, the client does not expect the human operator to manually paste secret keys into a form. Instead, the agent initiates an unauthenticated probe. Your server is expected to reject this probe with a standard 401 Unauthorized challenge that contains a resource_metadata URI. The client follows this link, discovers your authorization server endpoints, dynamically registers itself as an OAuth application (RFC 7591), and directs the user through an authorization code flow with PKCE before ever exchanging a single MCP protocol frame.

Diagram

The Mental Model: The Valet and the Hotel Concierge

Think of a static API key like giving a delivery driver your master building key: it works, but it gives unconditional access, cannot be easily scoped per user session, and requires manual out-of-band setup.

The MCP OAuth discovery pattern works more like a hotel concierge checking in a valet. When the valet (the AI client) arrives at the garage gate (/api/mcp), the guard does not say "go away"; the guard hands them a card pointing to the concierge desk (/.well-known/oauth-protected-resource). The valet walks over to the desk, registers their company name dynamically, gets temporary visitor credentials, and asks you (the owner) to sign a slip approving access. Only after you sign does the valet return to the gate with a validated badge. The gatekeeper checks the badge, confirms which room it belongs to, and unlocks only that room's parking bay.

Diagram

When To Use It

You should implement the full OAuth 2.0 discovery contract whenever your MCP server runs on a public domain and serves external agents such as Claude.ai, ChatGPT Custom Actions, or multi-tenant web clients.

Public AI Agent Integrations (Claude & ChatGPT)

When integrating with cloud-hosted AI hosts like Claude.ai or ChatGPT, OAuth discovery is the only interoperable authentication path. These platforms do not prompt end-users to enter static database credentials into text fields. Instead, their web clients expect standard RFC discovery headers so they can safely initiate an OAuth popup and retrieve a scoped token directly on behalf of the user.

Multi-Tenant SaaS with Scoped User Data

In multi-tenant SaaS environments where tools manipulate private user data—like an image hosting service where uploads must be tagged to the authenticated user's gallery—OAuth is mandatory. It eliminates the need for users to manually generate and copy database secret keys into third-party chat interfaces. Because the flow terminates in an authorization code grant tied to your primary authentication system (such as Better Auth), every downstream tool invocation automatically inherits the authenticated user's identity.

Protocol Handshake Nuances vs Local Stdio

If you are building on Next.js or looking for a full implementation of dynamic client registration, Upstash Redis token storage, and custom HTML consent screens for Claude Web and CLI, read our companion guide OAuth for MCP Server: Complete Guide to Protecting Claude. While that walkthrough handles Redis session persistence, this guide focuses on the protocol handshake nuances—RFC 9728 resource metadata, Streamable HTTP challenges, and Hono CORS exposure—that remote endpoints require when serving both Claude and ChatGPT.

For more on the architectural risks of static tokens and how multi-user authorization differs across AI clients, see Building Secure Multi-User MCP Servers: Claude vs OpenAI's Authentication Gap.


When NOT To Use It

Do not implement this flow if your MCP server is strictly local and runs over stdio inside a desktop environment like Cursor or Claude Desktop on your personal laptop.

In local process-spawned integrations, the client and the server share the same process boundary or operating system user. Adding OAuth discovery here introduces unnecessary network round-trips, database tables, and browser redirects for zero security gain. Similarly, if your MCP server is intended solely for internal server-to-server cron jobs or automated CI/CD runners, a permanent personal API key or a static master token passed directly in the Authorization header is significantly simpler to maintain and debug.


Gotchas & Common Mistakes

1. The Missing resource_metadata Challenge Header

The most common failure happens on the very first HTTP request. When an unauthorized client hits your MCP route, returning a simple JSON { "error": "Unauthorized" } with a 401 status code causes Claude or ChatGPT to fail immediately with a generic connection error.

AI clients do not assume OAuth is supported unless your 401 Unauthorized response explicitly broadcasts where to find the Protected Resource Metadata per RFC 9728. In Hono, you must supply the WWW-Authenticate header containing the exact metadata URL.

typescript
// apps/api/src/mcp/routes.ts
if (!identity && !config.publicMode) {
  const resourceMetadataUrl = `${config.publicBaseUrl}/.well-known/oauth-protected-resource/api/mcp`;
  const challenge = `Bearer error="invalid_token", error_description="Unauthorized", resource_metadata="${resourceMetadataUrl}"`;

  return c.json({ error: 'Unauthorized' }, 401, {
    'WWW-Authenticate': challenge,
  });
}

If that header or the resource_metadata parameter is missing, the client has no way to locate your authorization server and terminates the handshake.

2. Failing to Expose CORS Headers

Even if your server responds with the proper WWW-Authenticate header, web-based clients running in the browser will drop it if your CORS configuration does not explicitly expose it.

Standard browser security prevents client-side scripts from reading response headers unless they are safelisted in Access-Control-Expose-Headers. When setting up your Hono middleware, you must expose both the authentication challenge and MCP protocol headers.

typescript
// apps/api/src/index.ts
app.use('*', cors({
  origin: (origin) => origin || '*',
  allowHeaders: ['Content-Type', 'Authorization', 'Range', 'Mcp-Session-Id', 'Mcp-Protocol-Version'],
  exposeHeaders: ['Mcp-Session-Id', 'WWW-Authenticate', 'Last-Event-Id', 'Mcp-Protocol-Version'],
  credentials: true,
}));

Omitting WWW-Authenticate from exposeHeaders leaves web clients blind to the discovery URL.

3. Missing the RFC 9207 iss Parameter on Redirects

Once the user approves the authorization prompt in their browser, your server redirects back to the client's redirect_uri with an authorization code. Modern MCP clients enforce RFC 9207 to prevent authorization code injection attacks.

RFC 9207 requires that the authorization server append an iss (issuer) query parameter to the redirect URL matching the issuer URL declared in your metadata. If your authentication library redirects without this parameter, clients like Claude will reject the code exchange during the final step.

typescript
// apps/api/src/index.ts
app.use('/api/auth/mcp/authorize', async (c, next) => {
  await next();
  const location = c.res.headers.get('Location');
  if (c.res.status >= 300 && c.res.status < 400 && location && !location.startsWith('/')) {
    const issuer = config.publicBaseUrl.replace(/\/$/, '');
    const redirectUrl = new URL(location);
    if (!redirectUrl.searchParams.has('iss')) {
      redirectUrl.searchParams.set('iss', issuer);
      c.res.headers.set('Location', redirectUrl.href);
    }
  }
});

Capturing the redirect response and appending the verified issuer parameter ensures strict compliance with client security verifiers.


Conclusion

Building a production-ready remote MCP server requires treating authentication as an automated protocol conversation rather than a static header check. When an AI agent connects to your backend, it relies on RFC 9728 protected resource discovery, RFC 8414 metadata, dynamic registration, and RFC 9207 redirect binding to establish trust autonomously.

Reach for full OAuth discovery when your MCP tools operate on user-scoped data in cloud environments, and stick to simple personal tokens or stdio when running local desktop tools.

If you have questions or ran into a different gotcha, drop a comment below. And if you found this useful, subscribe for more.

Thanks, Matija

Comments