71e77e95bc
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>
65 lines
2.2 KiB
Python
65 lines
2.2 KiB
Python
import logging
|
|
|
|
from mcp.server.fastmcp import Context, FastMCP
|
|
|
|
from nextcloud_mcp_server.auth import require_scopes
|
|
from nextcloud_mcp_server.context import get_client
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def configure_tables_tools(mcp: FastMCP):
|
|
# Tables tools
|
|
@mcp.tool()
|
|
@require_scopes("tables:read")
|
|
async def nc_tables_list_tables(ctx: Context):
|
|
"""List all tables available to the user"""
|
|
client = await get_client(ctx)
|
|
return await client.tables.list_tables()
|
|
|
|
@mcp.tool()
|
|
@require_scopes("tables:read")
|
|
async def nc_tables_get_schema(table_id: int, ctx: Context):
|
|
"""Get the schema/structure of a specific table including columns and views"""
|
|
client = await get_client(ctx)
|
|
return await client.tables.get_table_schema(table_id)
|
|
|
|
@mcp.tool()
|
|
@require_scopes("tables:read")
|
|
async def nc_tables_read_table(
|
|
table_id: int,
|
|
ctx: Context,
|
|
limit: int | None = None,
|
|
offset: int | None = None,
|
|
):
|
|
"""Read rows from a table with optional pagination"""
|
|
client = await get_client(ctx)
|
|
return await client.tables.get_table_rows(table_id, limit, offset)
|
|
|
|
@mcp.tool()
|
|
@require_scopes("tables:write")
|
|
async def nc_tables_insert_row(table_id: int, data: dict, ctx: Context):
|
|
"""Insert a new row into a table.
|
|
|
|
Data should be a dictionary mapping column IDs to values, e.g. {1: "text", 2: 42}
|
|
"""
|
|
client = await get_client(ctx)
|
|
return await client.tables.create_row(table_id, data)
|
|
|
|
@mcp.tool()
|
|
@require_scopes("tables:write")
|
|
async def nc_tables_update_row(row_id: int, data: dict, ctx: Context):
|
|
"""Update an existing row in a table.
|
|
|
|
Data should be a dictionary mapping column IDs to new values, e.g. {1: "new text", 2: 99}
|
|
"""
|
|
client = await get_client(ctx)
|
|
return await client.tables.update_row(row_id, data)
|
|
|
|
@mcp.tool()
|
|
@require_scopes("tables:write")
|
|
async def nc_tables_delete_row(row_id: int, ctx: Context):
|
|
"""Delete a row from a table"""
|
|
client = await get_client(ctx)
|
|
return await client.tables.delete_row(row_id)
|