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:
@@ -28,7 +28,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
DirectoryListing with files, total_count, directories_count, files_count, and total_size
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
items = await client.webdav.list_directory(path)
|
||||
|
||||
# Convert to FileInfo models
|
||||
@@ -76,7 +76,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
result = await nc_webdav_read_file("Images/photo.jpg")
|
||||
logger.info(result['encoding']) # 'base64'
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
content, content_type = await client.webdav.read_file(path)
|
||||
|
||||
# Check if this is a parseable document (PDF, DOCX, etc.)
|
||||
@@ -143,7 +143,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Dict with status_code indicating success
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
# Handle base64 encoded content
|
||||
if content_type and "base64" in content_type.lower():
|
||||
@@ -167,7 +167,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Dict with status_code (201 for created, 405 if already exists)
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
return await client.webdav.create_directory(path)
|
||||
|
||||
@mcp.tool()
|
||||
@@ -181,7 +181,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Dict with status_code indicating result (404 if not found)
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
return await client.webdav.delete_resource(path)
|
||||
|
||||
@mcp.tool()
|
||||
@@ -199,7 +199,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Dict with status_code indicating result (404 if source not found, 412 if destination exists and overwrite is False)
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
return await client.webdav.move_resource(
|
||||
source_path, destination_path, overwrite
|
||||
)
|
||||
@@ -219,7 +219,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
Dict with status_code indicating result (404 if source not found, 412 if destination exists and overwrite is False)
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
return await client.webdav.copy_resource(
|
||||
source_path, destination_path, overwrite
|
||||
)
|
||||
@@ -249,7 +249,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
SearchFilesResponse with list of matching files
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
|
||||
# Build where conditions based on filters
|
||||
conditions = []
|
||||
@@ -355,7 +355,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
SearchFilesResponse with list of matching files
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
results = await client.webdav.find_by_name(
|
||||
pattern=pattern, scope=scope, limit=limit
|
||||
)
|
||||
@@ -382,7 +382,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
SearchFilesResponse with list of matching files
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
results = await client.webdav.find_by_type(
|
||||
mime_type=mime_type, scope=scope, limit=limit
|
||||
)
|
||||
@@ -408,7 +408,7 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
Returns:
|
||||
SearchFilesResponse with list of favorite files
|
||||
"""
|
||||
client = get_client(ctx)
|
||||
client = await get_client(ctx)
|
||||
results = await client.webdav.list_favorites(scope=scope, limit=limit)
|
||||
file_infos = [FileInfo(**result) for result in results]
|
||||
return SearchFilesResponse(
|
||||
|
||||
Reference in New Issue
Block a user