0d14c75eb1
- Consolidate MCP session + login flow cleanup into _mcp_session_with_login_flow() helper,
replacing 4 duplicated AsyncExitStack sites in app.py
- Fix get_shared_storage() race condition by using module-level anyio.Lock() init
(reverts regression from ba59763)
- Collapse cosmetic if/else branching in scope_authorization.py
- Consolidate dual password storage paths into single store_app_password_with_scopes() call
- Mark unused request param as _ in list_supported_scopes
- Make ALL_SUPPORTED_SCOPES an immutable tuple; use list() instead of .copy()
- Add hasattr(ctx, "elicit") guard in elicitation.py, narrow except to NotImplementedError
- Add YAML comment explaining --oauth flag for mcp-login-flow service
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.7 KiB
Python
85 lines
2.7 KiB
Python
"""MCP elicitation helpers for Login Flow v2.
|
|
|
|
Provides a unified way to present login URLs to users, using MCP elicitation
|
|
when the client supports it, or falling back to returning the URL in a message.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from mcp.server.fastmcp import Context
|
|
from pydantic import BaseModel, Field
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class LoginFlowConfirmation(BaseModel):
|
|
"""Schema for Login Flow v2 confirmation elicitation."""
|
|
|
|
acknowledged: bool = Field(
|
|
default=False,
|
|
description="Check this box after completing login at the provided URL",
|
|
)
|
|
|
|
|
|
async def present_login_url(
|
|
ctx: Context,
|
|
login_url: str,
|
|
message: str | None = None,
|
|
) -> str:
|
|
"""Present a login URL to the user via MCP elicitation or message.
|
|
|
|
Tries MCP elicitation first (ctx.elicit) for interactive clients.
|
|
Falls back to returning the URL as a plain message.
|
|
|
|
Args:
|
|
ctx: MCP context
|
|
login_url: URL the user should open in their browser
|
|
message: Optional custom message (defaults to standard Login Flow prompt)
|
|
|
|
Returns:
|
|
"accepted" if user acknowledged via elicitation,
|
|
"declined" if user declined,
|
|
"message_only" if elicitation not supported (URL returned in message)
|
|
"""
|
|
if message is None:
|
|
message = (
|
|
f"Please log in to Nextcloud to grant access:\n\n"
|
|
f"{login_url}\n\n"
|
|
f"Open this URL in your browser, log in, and grant the requested permissions. "
|
|
f"Then check the box below and click OK."
|
|
)
|
|
|
|
if not hasattr(ctx, "elicit"):
|
|
logger.debug(
|
|
"Elicitation not available (no elicit method), returning URL in message"
|
|
)
|
|
return "message_only"
|
|
|
|
try:
|
|
result = await ctx.elicit(
|
|
message=message,
|
|
schema=LoginFlowConfirmation,
|
|
)
|
|
|
|
if result.action == "accept":
|
|
if hasattr(result, "data") and not result.data.acknowledged: # type: ignore[union-attr]
|
|
logger.warning(
|
|
"User accepted login flow without checking the acknowledged box — "
|
|
"login completion will be verified via polling"
|
|
)
|
|
logger.info("User acknowledged login flow completion")
|
|
return "accepted"
|
|
elif result.action == "decline":
|
|
logger.info("User declined login flow")
|
|
return "declined"
|
|
else:
|
|
logger.info("User cancelled login flow")
|
|
return "cancelled"
|
|
|
|
except NotImplementedError as e:
|
|
# Elicitation not supported by this client/SDK - fall back to message
|
|
logger.debug(
|
|
f"Elicitation not available ({type(e).__name__}: {e}), returning URL in message"
|
|
)
|
|
return "message_only"
|