From 40a0a2e7d4f7a910ff39f96e2c1bb0406a12ad38 Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Mon, 9 Mar 2026 11:16:41 -0700 Subject: [PATCH] feat: add OAuth discovery endpoints for ChatGPT MCP compatibility Add /.well-known/oauth-protected-resource (root-level) and /.well-known/oauth-authorization-server to satisfy RFC 9728 and RFC 8414 discovery requirements expected by ChatGPT's MCP connector. Co-Authored-By: Claude Sonnet 4.6 --- .../oauth-authorization-server/route.ts | 51 ++++++++++++++++++ .../oauth-protected-resource/route.ts | 52 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/app/.well-known/oauth-authorization-server/route.ts create mode 100644 src/app/.well-known/oauth-protected-resource/route.ts diff --git a/src/app/.well-known/oauth-authorization-server/route.ts b/src/app/.well-known/oauth-authorization-server/route.ts new file mode 100644 index 0000000..2011735 --- /dev/null +++ b/src/app/.well-known/oauth-authorization-server/route.ts @@ -0,0 +1,51 @@ +import { NextRequest } from "next/server"; + +export async function OPTIONS(): Promise { + return new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }, + }); +} + +export async function GET(request: NextRequest): Promise { + const clerkDomain = process.env.NEXT_PUBLIC_CLERK_DOMAIN; + + if (!clerkDomain) { + return Response.json( + { error: "server_error", error_description: "Clerk domain not found" }, + { status: 500 }, + ); + } + + const baseUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}`; + const clerkBaseUrl = `https://${clerkDomain}`; + + const metadata = { + issuer: baseUrl, + authorization_endpoint: `${baseUrl}/authorize`, + token_endpoint: `${baseUrl}/token`, + registration_endpoint: `${clerkBaseUrl}/oauth/register`, + jwks_uri: `${clerkBaseUrl}/.well-known/jwks.json`, + scopes_supported: ["openid"], + response_types_supported: ["code"], + grant_types_supported: ["authorization_code", "refresh_token"], + code_challenge_methods_supported: ["S256"], + token_endpoint_auth_methods_supported: [ + "none", + "client_secret_post", + "client_secret_basic", + ], + }; + + return Response.json(metadata, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }, + }); +} diff --git a/src/app/.well-known/oauth-protected-resource/route.ts b/src/app/.well-known/oauth-protected-resource/route.ts new file mode 100644 index 0000000..40cdd32 --- /dev/null +++ b/src/app/.well-known/oauth-protected-resource/route.ts @@ -0,0 +1,52 @@ +import { protectedResourceHandlerClerk } from "@clerk/mcp-tools/next"; +import { NextRequest } from "next/server"; + +const handler = async (request: NextRequest) => { + const clerkResponse = await protectedResourceHandlerClerk({ + scopes_supported: ["openid"], + })(request); + + const clerkMetadata = await clerkResponse.json(); + + const baseUrl = `${request.nextUrl.protocol}//${request.nextUrl.host}`; + const clerkDomain = process.env.NEXT_PUBLIC_CLERK_DOMAIN; + + if (!clerkDomain) { + return Response.json( + { error: "server_error", error_description: "Clerk domain not found" }, + { status: 500 }, + ); + } + + const clerkBaseUrl = `https://${clerkDomain}`; + + const modifiedMetadata: Record = { + ...clerkMetadata, + resource: baseUrl, + authorization_servers: [baseUrl], + authorization_endpoint: `${baseUrl}/authorize`, + token_endpoint: `${baseUrl}/token`, + registration_endpoint: `${clerkBaseUrl}/oauth/register`, + scopes_supported: ["openid"], + }; + + return Response.json(modifiedMetadata, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }, + }); +}; + +export const OPTIONS = async (): Promise => + new Response(null, { + status: 204, + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }, + }); + +export { handler as GET };