@@ -49,7 +49,7 @@ const DCR_UNSUPPORTED_MESSAGE =
4949/**
5050 * The MCP SDK throws a plain `Error` (no typed class or code) when an auth server lacks a
5151 * `registration_endpoint`, so this string-match is the only available signal. Pinned to
52- * `@modelcontextprotocol/sdk` v1.29 .0 `registerClient` (client/auth.js): "Incompatible auth
52+ * `@modelcontextprotocol/sdk` v1.31 .0 `registerClient` (client/auth.js): "Incompatible auth
5353 * server: does not support dynamic client registration". If a version bump rephrases this, the
5454 * 422 branch silently stops firing (users get a generic 500) — update the substring here.
5555 */
@@ -170,43 +170,53 @@ export const GET = withRouteHandler(
170170 )
171171 const provider = new SimMcpOauthProvider ( { row, preregistered } )
172172
173- try {
174- // OAuth discovery + DCR through the guarded fetch, bounded so a transient stall fails
175- // fast with a labeled log instead of hanging the popup. `McpOauthRedirectRequired` is
176- // the SUCCESS signal (a throw carrying the authorize URL), so we catch it INSIDE the
177- // bounded step and return it as a normal value — otherwise timedStep would error-log
178- // every successful authorize. Only a real error or a timeout escapes as a throw.
179- const authOutcome = await timedStep ( 'mcpAuthGuarded' , MCP_AUTH_STEP_MS , async ( ) => {
180- try {
181- return { kind : 'result' as const , value : await mcpAuthGuarded ( provider , { serverUrl } ) }
182- } catch ( e ) {
183- if ( e instanceof McpOauthRedirectRequired ) {
184- return { kind : 'redirect' as const , authorizationUrl : e . authorizationUrl }
173+ // OAuth discovery + DCR through the guarded fetch, bounded so a transient stall fails
174+ // fast with a labeled log instead of hanging the popup. `McpOauthRedirectRequired` is
175+ // the SUCCESS signal (a throw carrying the authorize URL), and an authorization-server
176+ // refusal is an expected outcome rather than a Sim fault, so both are caught INSIDE the
177+ // bounded step and returned as values — otherwise timedStep would error-log them. Only
178+ // a real error or a timeout escapes as a throw.
179+ const authOutcome = await timedStep ( 'mcpAuthGuarded' , MCP_AUTH_STEP_MS , async ( ) => {
180+ try {
181+ return { kind : 'result' as const , value : await mcpAuthGuarded ( provider , { serverUrl } ) }
182+ } catch ( e ) {
183+ if ( e instanceof McpOauthRedirectRequired ) {
184+ return { kind : 'redirect' as const , authorizationUrl : e . authorizationUrl }
185+ }
186+ if ( isDynamicClientRegistrationUnsupported ( e ) ) {
187+ return {
188+ kind : 'rejected' as const ,
189+ error : toError ( e ) ,
190+ message : DCR_UNSUPPORTED_MESSAGE ,
185191 }
186- throw e
187192 }
188- } )
189- if ( authOutcome . kind === 'redirect' ) {
190- logger . info ( `OAuth redirect for server ${ serverId } ` )
191- return NextResponse . json ( {
192- status : 'redirect' ,
193- authorizationUrl : authOutcome . authorizationUrl ,
194- } )
195- }
196- if ( authOutcome . value === 'AUTHORIZED' ) {
197- return NextResponse . json ( { status : 'already_authorized' } )
198- }
199- return createMcpErrorResponse (
200- new Error ( 'Provider did not capture redirect URL' ) ,
201- 'Failed to start OAuth flow' ,
202- 500
203- )
204- } catch ( e ) {
205- if ( isDynamicClientRegistrationUnsupported ( e ) ) {
206- return createMcpErrorResponse ( toError ( e ) , DCR_UNSUPPORTED_MESSAGE , 422 )
193+ if ( e instanceof OAuthError ) {
194+ return { kind : 'rejected' as const , error : e , message : surfaceOauthError ( e ) }
195+ }
196+ throw e
207197 }
208- throw e
198+ } )
199+ if ( authOutcome . kind === 'redirect' ) {
200+ logger . info ( `OAuth redirect for server ${ serverId } ` )
201+ return NextResponse . json ( {
202+ status : 'redirect' ,
203+ authorizationUrl : authOutcome . authorizationUrl ,
204+ } )
209205 }
206+ if ( authOutcome . kind === 'rejected' ) {
207+ logger . warn ( `Authorization server refused the MCP OAuth flow for server ${ serverId } ` , {
208+ error : authOutcome . error . message ,
209+ } )
210+ return createMcpErrorResponse ( authOutcome . error , authOutcome . message , 422 )
211+ }
212+ if ( authOutcome . value === 'AUTHORIZED' ) {
213+ return NextResponse . json ( { status : 'already_authorized' } )
214+ }
215+ return createMcpErrorResponse (
216+ new Error ( 'Provider did not capture redirect URL' ) ,
217+ 'Failed to start OAuth flow' ,
218+ 500
219+ )
210220 } catch ( error ) {
211221 // Any bounded step timing out (DB reads or the auth step) is a stall, not a bug —
212222 // surface it as a fast 504 so the popup closes with a clear "try again" rather than a
@@ -223,11 +233,7 @@ export const GET = withRouteHandler(
223233 )
224234 }
225235 logger . error ( 'Error starting MCP OAuth flow:' , error )
226- // Only surface OAuth-flow errors verbatim; everything else (DB, decryption,
227- // network) gets a generic message to avoid leaking internal details.
228- const userMessage =
229- error instanceof OAuthError ? surfaceOauthError ( error ) : 'Failed to start OAuth flow'
230- return createMcpErrorResponse ( toError ( error ) , userMessage , 500 )
236+ return createMcpErrorResponse ( toError ( error ) , 'Failed to start OAuth flow' , 500 )
231237 }
232238 } )
233239)
0 commit comments