@@ -190,7 +190,7 @@ async def _call_list(kind: str) -> Any:
190190 return asyncio .run (_verify_async ())
191191
192192
193- def server_needs_oauth (server : MCPServerConfig ) -> bool : # noqa
193+ def server_needs_oauth (server : MCPServerConfig ) -> bool :
194194 """Return True if the remote server currently needs OAuth; False otherwise."""
195195
196196 async def _needs_oauth_async () -> bool :
@@ -202,3 +202,112 @@ async def _needs_oauth_async() -> bool:
202202 return info .status == OAuthStatus .NEEDS_AUTH
203203
204204 return asyncio .run (_needs_oauth_async ())
205+
206+
207+ def authorize_server_oauth (server : MCPServerConfig ) -> bool :
208+ """Run an interactive OAuth flow for a remote MCP server and cache tokens.
209+
210+ Returns True if authorization succeeded (tokens cached and a ping succeeded),
211+ False otherwise. Local servers return True immediately.
212+ """
213+
214+ async def _authorize_async () -> bool :
215+ if not server .is_remote_server ():
216+ return True
217+
218+ remote_url : str | None = server .get_remote_url ()
219+ if not remote_url :
220+ log .error ("OAuth requested for remote server '{}' but no URL found" , server .name )
221+ return False
222+
223+ oauth_manager = get_oauth_manager ()
224+
225+ try :
226+ # Import lazily to avoid import-time side effects
227+ from fastmcp import Client as FastMCPClient # type: ignore
228+ from fastmcp .client .auth import OAuth # type: ignore
229+
230+ # Debug info prior to starting OAuth
231+ print (
232+ "[OAuth] Starting authorization" ,
233+ f"server={ server .name } " ,
234+ f"remote_url={ remote_url } " ,
235+ f"cache_dir={ oauth_manager .cache_dir } " ,
236+ f"scopes={ server .oauth_scopes } " ,
237+ f"client_name={ server .oauth_client_name or 'Open Edison Setup' } " ,
238+ )
239+
240+ oauth = OAuth (
241+ mcp_url = remote_url ,
242+ scopes = server .oauth_scopes ,
243+ client_name = server .oauth_client_name or "Open Edison Setup" ,
244+ token_storage_cache_dir = oauth_manager .cache_dir ,
245+ callback_port = 50001 ,
246+ )
247+
248+ # Establish a connection to trigger OAuth if needed
249+ async with FastMCPClient (remote_url , auth = oauth ) as client : # type: ignore
250+ log .info (
251+ "Starting OAuth flow for '{}' (a browser window may open; if not, follow the printed URL)" ,
252+ server .name ,
253+ )
254+ await client .ping ()
255+
256+ # Refresh cached status
257+ info = await oauth_manager .check_oauth_requirement (server .name , remote_url )
258+
259+ # Post-authorization token inspection (no secrets printed)
260+ try :
261+ from fastmcp .client .auth .oauth import FileTokenStorage # type: ignore
262+
263+ storage = FileTokenStorage (server_url = remote_url , cache_dir = oauth_manager .cache_dir )
264+ tokens = await storage .get_tokens ()
265+ access_present = bool (getattr (tokens , "access_token" , None )) if tokens else False
266+ refresh_present = bool (getattr (tokens , "refresh_token" , None )) if tokens else False
267+ expires_at = getattr (tokens , "expires_at" , None ) if tokens else None
268+ print (
269+ "[OAuth] Authorization result:" ,
270+ f"status={ info .status .value } " ,
271+ f"has_refresh_token={ info .has_refresh_token } " ,
272+ f"token_expires_at={ info .token_expires_at or expires_at } " ,
273+ f"tokens_cached=access:{ access_present } /refresh:{ refresh_present } " ,
274+ )
275+ except Exception as _e : # noqa: BLE001
276+ print ("[OAuth] Authorization completed, but token inspection failed:" , _e )
277+
278+ log .info ("OAuth completed and tokens cached for '{}'" , server .name )
279+ return True
280+ except Exception as e : # noqa: BLE001
281+ log .error ("OAuth authorization failed for '{}': {}" , server .name , e )
282+ print ("[OAuth] Authorization failed:" , e )
283+ return False
284+
285+ return asyncio .run (_authorize_async ())
286+
287+
288+ def has_oauth_tokens (server : MCPServerConfig ) -> bool :
289+ """Return True if cached OAuth tokens exist for the remote server.
290+
291+ Local servers return True (no OAuth needed).
292+ """
293+
294+ async def _check_async () -> bool :
295+ if not server .is_remote_server ():
296+ return True
297+
298+ remote_url : str | None = server .get_remote_url ()
299+ if not remote_url :
300+ return False
301+
302+ try :
303+ from fastmcp .client .auth .oauth import FileTokenStorage # type: ignore
304+
305+ storage = FileTokenStorage (
306+ server_url = remote_url , cache_dir = get_oauth_manager ().cache_dir
307+ )
308+ tokens = await storage .get_tokens ()
309+ return bool (tokens and (tokens .access_token or tokens .refresh_token ))
310+ except Exception :
311+ return False
312+
313+ return asyncio .run (_check_async ())
0 commit comments