refactor: integrate token exchange into unified get_client() pattern
Resolves the token exchange implementation gap where get_session_client() was implemented but never used by tools. Unifies token acquisition into a single async get_client() method that handles both pass-through and token exchange modes transparently. Core Changes: - Make get_client() async and merge token exchange logic into it - Remove scopes parameter from token exchange (Nextcloud doesn't support OAuth scopes) - Update all 8 tool modules to use await get_client(ctx) - Fix provisioning decorator to skip checks in BasicAuth mode Token Acquisition Modes: 1. BasicAuth: Returns shared client (no token operations) 2. OAuth pass-through (default): Verifies and passes Flow 1 token to Nextcloud 3. OAuth token exchange (opt-in): Exchanges Flow 1 token for ephemeral token via RFC 8693 Key Architectural Clarifications: - Progressive Consent (Flow 1/2) = Authorization architecture - Token Exchange = Token acquisition pattern during tool execution - Refresh tokens from Flow 2 are NEVER used for tool calls (only background jobs) - Nextcloud scopes are "soft-scopes" enforced by MCP server, not IdP Documentation Updates: - ADR-004: Added comprehensive token acquisition patterns section - CRITICAL-TOKEN-EXCHANGE-PATTERN.md: Updated to reflect implementation status - CLAUDE.md: Updated architectural patterns with async get_client() Testing: - All 36 unit tests passing - All 4 smoke tests passing (BasicAuth mode) - Linting issues fixed (ruff) Configuration: ENABLE_TOKEN_EXCHANGE=false (default) - pass-through mode ENABLE_TOKEN_EXCHANGE=true (opt-in) - token exchange mode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
@require_scopes("calendar:read")
|
||||
async def nc_calendar_list_calendars(ctx: Context) -> ListCalendarsResponse:
|
||||
"""List all available calendars for the user"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
calendars_data = await client.calendar.list_calendars()
|
||||
|
||||
calendars = [Calendar(**cal_data) for cal_data in calendars_data]
|
||||
@@ -79,7 +79,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Dict with event creation result
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
event_data = {
|
||||
"title": title,
|
||||
@@ -139,7 +139,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
List of events matching the filters
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
# Convert YYYY-MM-DD format dates to datetime objects
|
||||
start_datetime = None
|
||||
@@ -214,7 +214,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
ctx: Context,
|
||||
):
|
||||
"""Get detailed information about a specific event"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
event_data, etag = await client.calendar.get_event(calendar_name, event_uid)
|
||||
return event_data
|
||||
|
||||
@@ -248,7 +248,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
etag: str = "",
|
||||
):
|
||||
"""Update any aspect of an existing event"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
# Build update data with only non-None values
|
||||
event_data = {}
|
||||
@@ -299,7 +299,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
ctx: Context,
|
||||
):
|
||||
"""Delete a calendar event"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
return await client.calendar.delete_event(calendar_name, event_uid)
|
||||
|
||||
@mcp.tool()
|
||||
@@ -342,7 +342,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Dict with meeting creation result
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
# Combine date and time for start_datetime
|
||||
start_datetime = f"{date}T{time}:00"
|
||||
@@ -377,7 +377,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
limit: int = 10,
|
||||
):
|
||||
"""Get upcoming events in next N days"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
now = dt.datetime.now()
|
||||
end_datetime = now + dt.timedelta(days=days_ahead)
|
||||
@@ -447,7 +447,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
List of available time slots with start/end times and duration
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
# Parse attendees
|
||||
attendee_list = []
|
||||
@@ -549,7 +549,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Summary of operation results including counts and details
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
if operation not in ["update", "delete", "move"]:
|
||||
raise ValueError("Operation must be 'update', 'delete', or 'move'")
|
||||
@@ -772,7 +772,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Result of the calendar management operation
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
if action == "list":
|
||||
return await client.calendar.list_calendars()
|
||||
@@ -839,7 +839,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
List of todos matching the filters
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
# Build filters dictionary
|
||||
filters = {}
|
||||
@@ -890,7 +890,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Dict with todo creation result
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
todo_data = {
|
||||
"summary": summary,
|
||||
@@ -939,7 +939,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Dict with todo update result
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
# Build update data with only non-None values
|
||||
todo_data = {}
|
||||
@@ -981,7 +981,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Dict with deletion status
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
return await client.calendar.delete_todo(calendar_name, todo_uid)
|
||||
|
||||
@mcp.tool()
|
||||
@@ -1005,7 +1005,7 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
List of todos matching the filters from all calendars
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
# Build filters dictionary
|
||||
filters = {}
|
||||
|
||||
Reference in New Issue
Block a user