@@ -385,49 +385,6 @@ async def app_lifespan_basic(server: FastMCP) -> AsyncIterator[AppContext]:
385385 await client .close ()
386386
387387
388- @asynccontextmanager
389- async def app_lifespan_oauth (server : FastMCP ) -> AsyncIterator [OAuthAppContext ]:
390- """
391- Manage application lifecycle for OAuth mode.
392-
393- Uses pre-initialized OAuth configuration from setup_oauth_config().
394- Does NOT create a Nextcloud client - clients are created per-request.
395- """
396- logger .info ("Starting MCP server in OAuth mode" )
397-
398- # Get pre-initialized OAuth context from server dependencies
399- oauth_ctx = server .dependencies
400-
401- nextcloud_host = oauth_ctx ["nextcloud_host" ]
402- token_verifier = oauth_ctx ["token_verifier" ]
403- refresh_token_storage = oauth_ctx ["refresh_token_storage" ]
404- oauth_client = oauth_ctx ["oauth_client" ]
405- oauth_provider = oauth_ctx ["oauth_provider" ]
406-
407- logger .info (f"Using OAuth provider: { oauth_provider } " )
408- if refresh_token_storage :
409- logger .info ("Refresh token storage is available" )
410- if oauth_client :
411- logger .info ("OAuth client is available for token refresh" )
412-
413- # Initialize document processors
414- initialize_document_processors ()
415-
416- try :
417- yield OAuthAppContext (
418- nextcloud_host = nextcloud_host ,
419- token_verifier = token_verifier ,
420- refresh_token_storage = refresh_token_storage ,
421- oauth_client = oauth_client ,
422- oauth_provider = oauth_provider ,
423- )
424- finally :
425- logger .info ("Shutting down OAuth mode" )
426- # Close OAuth client if it exists
427- if oauth_client and hasattr (oauth_client , "close" ):
428- await oauth_client .close ()
429-
430-
431388async def setup_oauth_config ():
432389 """
433390 Setup OAuth configuration by performing OIDC discovery and client registration.
@@ -498,7 +455,25 @@ async def setup_oauth_config():
498455
499456 # Auto-detect provider mode based on issuer
500457 # External IdP mode: issuer doesn't match Nextcloud host
501- is_external_idp = not issuer .startswith (nextcloud_host )
458+ # Normalize URLs for comparison (handle port differences like :80 for HTTP)
459+ from urllib .parse import urlparse
460+
461+ def normalize_url (url : str ) -> str :
462+ """Normalize URL by removing default ports (80 for HTTP, 443 for HTTPS)."""
463+ parsed = urlparse (url )
464+ # Remove default ports
465+ if (parsed .scheme == "http" and parsed .port == 80 ) or (
466+ parsed .scheme == "https" and parsed .port == 443
467+ ):
468+ # Remove explicit default port
469+ hostname = parsed .hostname or parsed .netloc .split (":" )[0 ]
470+ return f"{ parsed .scheme } ://{ hostname } "
471+ return f"{ parsed .scheme } ://{ parsed .netloc } "
472+
473+ issuer_normalized = normalize_url (issuer )
474+ nextcloud_normalized = normalize_url (nextcloud_host )
475+
476+ is_external_idp = not issuer_normalized .startswith (nextcloud_normalized )
502477
503478 if is_external_idp :
504479 oauth_provider = "external" # Could be Keycloak, Auth0, Okta, etc.
@@ -700,22 +675,46 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
700675 oauth_provider ,
701676 ) = anyio .run (setup_oauth_config )
702677
703- # Store OAuth context for lifespan to access
704- # We'll pass this to the lifespan via server.deps
705- oauth_context = {
706- "nextcloud_host" : nextcloud_host ,
707- "token_verifier" : token_verifier ,
708- "refresh_token_storage" : refresh_token_storage ,
709- "oauth_client" : oauth_client ,
710- "oauth_provider" : oauth_provider ,
711- }
678+ # Create lifespan function with captured OAuth context (closure)
679+ @asynccontextmanager
680+ async def oauth_lifespan (server : FastMCP ) -> AsyncIterator [OAuthAppContext ]:
681+ """
682+ Lifespan context for OAuth mode - captures OAuth configuration from outer scope.
683+ """
684+ logger .info ("Starting MCP server in OAuth mode" )
685+ logger .info (f"Using OAuth provider: { oauth_provider } " )
686+ if refresh_token_storage :
687+ logger .info ("Refresh token storage is available" )
688+ if oauth_client :
689+ logger .info ("OAuth client is available for token refresh" )
690+
691+ # Initialize document processors
692+ initialize_document_processors ()
693+
694+ try :
695+ yield OAuthAppContext (
696+ nextcloud_host = nextcloud_host ,
697+ token_verifier = token_verifier ,
698+ refresh_token_storage = refresh_token_storage ,
699+ oauth_client = oauth_client ,
700+ oauth_provider = oauth_provider ,
701+ )
702+ finally :
703+ logger .info ("Shutting down MCP server" )
704+ # RefreshTokenStorage uses context managers, no close() needed
705+ # OAuth client cleanup (if it has a close method)
706+ if oauth_client and hasattr (oauth_client , "close" ):
707+ try :
708+ await oauth_client .close ()
709+ except Exception as e :
710+ logger .warning (f"Error closing OAuth client: { e } " )
711+ logger .info ("MCP server shutdown complete" )
712712
713713 mcp = FastMCP (
714714 "Nextcloud MCP" ,
715- lifespan = app_lifespan_oauth ,
715+ lifespan = oauth_lifespan ,
716716 token_verifier = token_verifier ,
717717 auth = auth_settings ,
718- dependencies = oauth_context ,
719718 )
720719 else :
721720 logger .info ("Configuring MCP server for BasicAuth mode" )
0 commit comments