Skip to content

Instantly share code, notes, and snippets.

@kwhandy
Created March 27, 2026 10:25
Show Gist options
  • Select an option

  • Save kwhandy/b63e2f731b7fc7c86ca6d271f9814bdb to your computer and use it in GitHub Desktop.

Select an option

Save kwhandy/b63e2f731b7fc7c86ca6d271f9814bdb to your computer and use it in GitHub Desktop.

MCP Auth Adjustment Spec (2025-11-25 Compliant)

Goal: Make repaera/mcp and repaera/chat fully standard, registry-compatible, and work seamlessly with ALL common MCP clients (Claude Desktop, Cursor, Windsurf, VS Code, ChatGPT, etc.) + your own chat client

No custom headers (delete MCP_TOKEN / X-User-Token / X-User-Id logic)

Use official OAuth 2.1 + Protected Resource Metadata only

1. Shared Requirements (both repos)

  • Use latest official SDK: @modelcontextprotocol/sdk@latest and @modelcontextprotocol/hono@latest (for server)
  • Auth must be Bearer token only (Authorization: Bearer )
  • Server must expose /.well-known/oauth-protected-resource (RFC 9728)
  • On missing/invalid token → return 401 + WWW-Authenticate: Bearer ... header
  • On insufficient scope → 403 + WWW-Authenticate with error=insufficient_scope
  • Token verification via introspection endpoint (from AUTH_ISSUER)
  • user_id comes from token.sub (no more JWT fallback)

2. Changes for repaera/mcp (Server)

2.1 Update package.json

  • Add / update: "@modelcontextprotocol/sdk": "latest"
  • Add / update: "@modelcontextprotocol/hono": "latest"

2.2 New/Replace file: src/auth.ts

Create this exact file (or replace existing auth logic):

import "dotenv/config";
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
import type { OAuthProtectedResourceMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";

export const protectedResourceMetadata: OAuthProtectedResourceMetadata = {
  resource: process.env.RESOURCE_URI || "https://your-mcp-server.com",
  authorization_servers: [process.env.AUTH_ISSUER!],
  scopes_supported: ["tools:read", "tools:execute"],
  client_id_metadata_document_supported: true,
};

const tokenVerifier = {
  verifyAccessToken: async (token: string) => {
    const res = await fetch(process.env.INTROSPECTION_ENDPOINT!, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        token,
        client_id: process.env.OAUTH_CLIENT_ID!,
        client_secret: process.env.OAUTH_CLIENT_SECRET || "",
      }),
    });

    if (!res.ok) throw new Error("Token verification failed");
    const data = await res.json();
    if (!data.active) throw new Error("Token inactive");

    return {
      userId: data.sub,
      scopes: (data.scope || "").split(" ").filter(Boolean),
    };
  },
};

export const authMiddleware = requireBearerAuth({
  verifier: tokenVerifier,
  resourceMetadata: protectedResourceMetadata,
  requiredScopes: [], // per-tool scopes can be added later
});

2.3 Update src/index.ts (and src/worker.ts)

  • Add public metadata route:
app.get("/.well-known/oauth-protected-resource", (c) => c.json(protectedResourceMetadata));
  • Update StreamableHTTPServerTransport:
const transport = new StreamableHTTPServerTransport({
  endpoint: "/mcp",
  onRequest: [authMiddleware],   // ← important
});
  • Delete ALL old custom header logic (MCP_TOKEN, X-User-Token, X-User-Id)

2.4 Update .env

Add these variables (remove old MCP_TOKEN ones):

RESOURCE_URI=https://your-mcp-server.com
AUTH_ISSUER=https://your-auth-provider.com/realms/mcp
INTROSPECTION_ENDPOINT=https://your-auth-provider.com/realms/mcp/protocol/openid-connect/token/introspect
OAUTH_CLIENT_ID=mcp-server-introspector
OAUTH_CLIENT_SECRET=your-client-secret

2.5 (Optional but recommended)

  • Add icon to every registerTool: _meta: { icon: "..." }
  • Keep existing todos table and user_id filtering (already correct)

3. Changes for repaera/chat (Client)

3.1 Update MCP client creation (src/app/api/chat/route.ts or wherever createMCPClient is)

Replace old header logic with official auth-aware transport:

import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
// or use @ai-sdk/mcp with authProvider if you prefer

const transport = new StreamableHTTPClientTransport({
  url: process.env.MCP_APPS_URL || process.env.MCP_URL!,
  // No manual headers needed anymore
  // The SDK will automatically handle 401 → metadata discovery
});

const mcpClient = createMCPClient({ transport });

3.2 Update .env.local

MCP_APPS_URL=https://your-mcp-server.com/mcp
MCP_APPS_TOKEN=eyJ...   # ← real OAuth access token from your AUTH_ISSUER
# Delete or ignore: MCP_JWT_SECRET, MCP_TOKEN

3.3 (Optional polish)

  • Remove any X-User-Token / JWT signing code
  • Add a note in UI: “Use a valid OAuth token from your auth provider”

4. Final Validation Steps (do this after changes)

  1. Deploy updated MCP server
  2. Test with official inspector: npx @modelcontextprotocol/inspector https://your-mcp-server.com
  3. Test with your chat client using real Bearer token
  4. Test in Claude Desktop / Cursor by adding the server URL directly
  5. Confirm: no custom headers, proper 401/403 with WWW-Authenticate, and user isolation still works via token.sub

This spec makes both repos 100% compliant, registry-ready, and zero-setup for any standard MCP client.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment