@@ -22,6 +22,12 @@ export type MCPConnectResult = { ok: true; client: MCPClient } | { ok: false; se
2222export type MCPConnectOptions = {
2323 stderr ?: "inherit" | "ignore" | "pipe" ;
2424 onAuthURL ?: ( serverName : string , authorizationUrl : string ) => void ;
25+ /**
26+ * Interactive OAuth finished and the retried operation succeeded. Callers
27+ * that already registered tools for this server can re-emit a connected
28+ * status so standing "needs auth" chrome clears mid-session.
29+ */
30+ onAuthorized ?: ( serverName : string ) => void ;
2531 signal ?: AbortSignal ;
2632} ;
2733
@@ -39,7 +45,15 @@ export function unwrapToolContent(content: unknown): string {
3945 } ) . join ( "\n" ) ;
4046}
4147
42- type HTTPAuthContext = { url : URL ; authProvider : CorbitsOAuthProvider ; callback : CallbackServer ; signal ?: AbortSignal ; interactive : boolean } ;
48+ type HTTPAuthContext = {
49+ url : URL ;
50+ authProvider : CorbitsOAuthProvider ;
51+ callback : CallbackServer ;
52+ signal ?: AbortSignal ;
53+ interactive : boolean ;
54+ serverName : string ;
55+ onAuthorized ?: ( serverName : string ) => void ;
56+ } ;
4357
4458function isRecoverableAuthError ( err : unknown ) : boolean {
4559 return err instanceof UnauthorizedError || err instanceof OAuthError ;
@@ -51,14 +65,35 @@ async function completeInteractiveAuth(context: HTTPAuthContext): Promise<void>
5165 await new StreamableHTTPClientTransport ( context . url , { authProvider : context . authProvider } ) . finishAuth ( code ) ;
5266}
5367
68+ /**
69+ * Run interactive OAuth, retry the failed operation, and notify only when the
70+ * retry itself succeeded — a failed re-auth must leave standing "needs auth"
71+ * chrome alone.
72+ */
73+ export async function retryAfterInteractiveAuth < T > (
74+ completeAuth : ( ) => Promise < void > ,
75+ operation : ( ) => Promise < T > ,
76+ onAuthorized : ( ( ) => void ) | undefined ,
77+ ) : Promise < T > {
78+ await completeAuth ( ) ;
79+ const value = await operation ( ) ;
80+ onAuthorized ?.( ) ;
81+ return value ;
82+ }
83+
5484async function recoverHTTPAuthorization < T > ( err : unknown , context : HTTPAuthContext | undefined , operation : ( ) => Promise < T > ) : Promise < T > {
5585 if ( context === undefined || ! isRecoverableAuthError ( err ) ) throw err ;
5686 let lastErr : unknown = err ;
5787 for ( let attempt = 0 ; attempt < 2 ; attempt += 1 ) {
5888 if ( lastErr instanceof OAuthError ) await context . authProvider . resetAuthorization ( ) ;
5989 if ( lastErr instanceof UnauthorizedError ) {
60- await completeInteractiveAuth ( context ) ;
61- return operation ( ) ;
90+ return retryAfterInteractiveAuth (
91+ ( ) => completeInteractiveAuth ( context ) ,
92+ operation ,
93+ context . onAuthorized === undefined
94+ ? undefined
95+ : ( ) => context . onAuthorized ?.( context . serverName ) ,
96+ ) ;
6297 }
6398 try {
6499 return await operation ( ) ;
@@ -128,7 +163,15 @@ async function connectHttp(config: MCPServerConfig, options: MCPConnectOptions):
128163 } ) ;
129164 const makeTransport = ( ) : Transport => new StreamableHTTPClientTransport ( url , { authProvider } ) as unknown as Transport ;
130165 const client = new Client ( { name : MCP_CLIENT_NAME , version : "1.0.0" } ) ;
131- const authContext : HTTPAuthContext = { url, authProvider, callback, interactive : options . onAuthURL !== undefined , ...( options . signal !== undefined ? { signal : options . signal } : { } ) } ;
166+ const authContext : HTTPAuthContext = {
167+ url,
168+ authProvider,
169+ callback,
170+ interactive : options . onAuthURL !== undefined ,
171+ serverName : config . name ,
172+ ...( options . onAuthorized !== undefined ? { onAuthorized : options . onAuthorized } : { } ) ,
173+ ...( options . signal !== undefined ? { signal : options . signal } : { } ) ,
174+ } ;
132175 try {
133176 await withHTTPAuthorizationRecovery ( authContext , ( ) => client . connect ( makeTransport ( ) ) ) ;
134177 return { ok : true , client : await finishClient ( client , config . name , authContext ) } ;
0 commit comments