Add real-time processing status display to the browser UI at /user/page
showing indexed document count, pending queue size, and sync status.
Implements the status display described in ADR-007 lines 280-298.
Changes:
- Store document_queue and related state in app.state for route access
- Add _get_processing_status() helper to query Qdrant and check queue
- Display status section in user_info_html() with indexed/pending counts
- Show color-coded status badge (green "Idle" or orange "Syncing")
- Only displays when VECTOR_SYNC_ENABLED=true
Status appears in both BasicAuth and OAuth modes, positioned after
session info but before logout buttons. Numbers are formatted with
commas for readability.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes background task startup for streamable-http transport by integrating
vector sync initialization into the Starlette lifespan context manager.
Starlette Lifespan Integration:
- Moved background task startup from FastMCP lifespan to Starlette lifespan
- FastMCP lifespan only triggers on MCP session establishment
- Starlette lifespan runs on server startup (correct timing)
- Fixed module scoping issues with local imports (anyio_module, asyncio_module)
- Added conditional startup based on oauth_enabled flag
Scanner Fixes:
- Fixed NotesClient method: list_notes() → get_all_notes()
- Properly handle AsyncIterator with list comprehension
- Collects all notes before processing
Verified Working:
- Background tasks start successfully on server startup
- Scanner fetches notes from Nextcloud API
- Processor pool (3 workers) ready for document processing
- Health endpoint reports Qdrant status
- No startup errors
Phase 3 Complete:
- BasicAuth mode with vector sync fully functional
- Background tasks integrate cleanly with streamable-http transport
- Graceful shutdown with coordinated task cancellation
Related: ADR-007 Background Vector Database Synchronization
🤖 Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements background vector database synchronization using anyio
TaskGroups for BasicAuth mode with single-user credentials.
Scanner Implementation:
- Periodic document discovery (hourly, configurable)
- Timestamp-based change detection (Nextcloud vs Qdrant)
- Wake event for immediate scanning on-demand
- Supports both initial sync (all docs) and incremental sync (changes only)
- Detects deleted documents and queues for removal
Processor Implementation:
- Concurrent document processing pool (3 workers default)
- I/O-bound embedding generation via Ollama API
- Retry logic with exponential backoff (3 retries)
- Document chunking (512 words, 50-word overlap)
- Handles both index and delete operations
- Upserts vectors to Qdrant with rich metadata
App Lifespan Integration:
- Extended AppContext with background task state
- Modified app_lifespan_basic() to start tasks via anyio TaskGroups
- Graceful shutdown with coordinated task cancellation
- Only activates when VECTOR_SYNC_ENABLED=true
Embedding Service:
- OllamaEmbeddingProvider with TLS support
- Singleton pattern for shared client instances
- Batch embedding support for efficiency
- Auto-detects embedding dimension (768 for nomic-embed-text)
Qdrant Client:
- Async client wrapper with singleton pattern
- Auto-creates collection on first use
- COSINE distance metric for semantic similarity
- Integrates with embedding service for dimension detection
Health Check Enhancement:
- Added Qdrant status check to /health/ready endpoint
- Only checks when VECTOR_SYNC_ENABLED=true
- 2-second timeout for health probe
- Reports connection errors with details
Configuration:
- VECTOR_SYNC_ENABLED: Enable background sync
- VECTOR_SYNC_SCAN_INTERVAL: Scanner frequency (3600s default)
- VECTOR_SYNC_PROCESSOR_WORKERS: Concurrent processors (3 default)
- QDRANT_URL, QDRANT_API_KEY, QDRANT_COLLECTION: Vector DB config
- OLLAMA_BASE_URL, OLLAMA_EMBEDDING_MODEL: Embedding service config
Dependencies Added:
- qdrant-client>=1.7.0: Vector database client
Docker Compose:
- Added Qdrant service with health check
- Exposed ports 6333 (REST) and 6334 (gRPC)
- Configured MCP service with vector sync environment
- Added qdrant-data volume for persistence
Known Issue:
- FastMCP lifespan not triggering for streamable-http transport
- Background tasks will start once lifespan integration is complete
- Lifespan triggers on MCP session establishment, not server startup
Related: ADR-007 Background Vector Database Synchronization
🤖 Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit addresses the "Login not detected" issue after completing
OAuth login via elicitation by unifying the session architecture and
adding comprehensive visibility into background session status.
## Changes
### 1. Enhanced check_logged_in with comprehensive logging (oauth_tools.py)
- Added detailed logging at each step of token lookup
- Implemented fallback strategy: first search by provisioning_client_id,
then fall back to user_id lookup
- This allows detection of refresh tokens created via any flow
(elicitation or browser login)
- Log messages include flow_type, provisioned_at, and provisioning_client_id
for debugging
### 2. Unified session architecture (browser_oauth_routes.py)
- Browser login now stores provisioning_client_id=state when saving
refresh token
- This makes browser and elicitation flows consistent - both can be
found by the same state parameter
- Treats Flow 2 (elicitation) and browser login as the same "background
session"
### 3. Enhanced /user/page with session status (userinfo_routes.py)
- Added comprehensive background access section showing:
- Background Access: Granted/Not Granted (with visual indicators)
- Flow Type: browser/flow2/hybrid
- Provisioned At: timestamp
- Token Audience: nextcloud/mcp
- Scopes: detailed scope list
- Status displayed regardless of which flow created the session
(browser login or elicitation)
### 4. Added revoke functionality (userinfo_routes.py, app.py)
- New POST endpoint: /user/revoke
- Allows users to revoke background access (delete refresh token)
- Browser session cookie remains valid for UI access
- Confirmation dialog before revocation
- Success page with auto-redirect back to /user/page
- Registered route in app.py browser_routes
## Testing
All tests pass:
- 6/6 login elicitation tests pass
- 21/21 core OAuth tests pass
- Comprehensive logging helps debug future issues
## Fixes
Resolves: "Login not detected. Please ensure you completed the login
at the provided URL before clicking OK."
The issue occurred because elicitation and browser login created
separate sessions. Now they are unified under the same architecture.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This PR fixes multiple OAuth-related issues:
## Unified OAuth Callback
- Consolidated `/oauth/callback-nextcloud` and `/oauth/login-callback` into single `/oauth/callback` endpoint
- Flow type determined by session lookup via state parameter (no query params in redirect_uri)
- Fixes redirect_uri validation issues with IdPs requiring exact match
- Legacy endpoints kept as aliases for backwards compatibility
## PKCE Implementation
- Implemented PKCE (RFC 7636) for Flow 2 (resource provisioning)
- Generate code_verifier and code_challenge
- Store code_verifier in session storage
- Retrieve and use in token exchange
- Fixed PKCE for browser login (integrated mode)
- Previously only worked for external IdP (Keycloak)
- Now works for both Nextcloud OIDC and external IdP
## Login Elicitation Fixes (ADR-006)
- Fixed elicitation URL to route through MCP server endpoint
- Changed from direct Nextcloud URL to `/oauth/authorize-nextcloud`
- Ensures PKCE is properly handled by server
- Fixed login detection after OAuth flow completes
- Look up refresh token by state parameter instead of user_id
- Works even when Flow 1 token not present
- Added `get_refresh_token_by_provisioning_client_id()` method
## Session Authentication
- Fixed `/user/page` redirect loop
- Shared oauth_context with mounted browser_app
- SessionAuthBackend can now validate sessions correctly
## Tests
- Added comprehensive login elicitation test suite
- Updated scope authorization test expectations
- All 43 OAuth tests passing
## Files Changed
- `app.py`: Shared oauth_context, unified callback route
- `oauth_routes.py`: Unified callback, PKCE for Flow 2
- `browser_oauth_routes.py`: PKCE for integrated mode
- `oauth_tools.py`: Fixed elicitation URL generation
- `refresh_token_storage.py`: Added lookup by provisioning_client_id
- `test_login_elicitation.py`: New test suite
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit completes the OAuth audience validation implementation per RFC 7519,
RFC 8707 (Resource Indicators), and RFC 9728 (Protected Resource Metadata).
## Key Changes
### OAuth Resource Parameters (RFC 8707)
- Add `resource` parameter to Flow 1 (MCP client auth) with MCP server audience
- Add `resource` parameter to Flow 2 (Nextcloud access) with Nextcloud audience
- Add `nextcloud_resource_uri` to oauth_context configuration
- Fix undefined variable error in starlette_lifespan
### PRM-Based Resource Discovery (RFC 9728)
- Update tests to fetch resource identifier from PRM endpoint
- Add fallback to hardcoded value if PRM fetch fails
- Demonstrate correct OAuth client implementation pattern
### ADR-005 Documentation Updates
- Update to reflect simplified RFC 7519 compliant implementation
- Document that MCP validates only its own audience (not Nextcloud's)
- Add section on OAuth resource parameters and PRM discovery
- Update implementation checklist to show completed items
- Mark status as "Implemented" with update date
## Implementation Details
The solution follows RFC 7519 Section 4.1.3: resource servers validate only
their own presence in the audience claim. This simplifies the logic while
maintaining security:
- MCP server validates MCP audience only
- Nextcloud independently validates its own audience
- No dual validation required at MCP layer
- Token reuse is allowed per RFC 8707 for multi-audience tokens
## Test Results
✅ test_mcp_oauth_server_connection - PASSED
✅ test_deck_board_view_permissions - PASSED
✅ test_prm_endpoint - PASSED
All OAuth flows now properly specify target resources, resulting in correct
audience validation throughout the system.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fix two issues preventing OAuth tests from passing:
1. Set oidc_client_id and oidc_client_secret on Settings object
- These were being read from environment but not propagated to the
UnifiedTokenVerifier settings instance
2. Use client_issuer instead of issuer for JWT validation
- client_issuer accounts for NEXTCLOUD_PUBLIC_ISSUER_URL override
- Fixes "Invalid issuer" errors when public URL differs from internal
3. Accept resource URL with /mcp path in audience validation
- During DCR, resource_url is registered as "{mcp_server_url}/mcp"
- Tokens correctly include this full path as audience
- Verifier now accepts both "http://localhost:8001" and
"http://localhost:8001/mcp" as valid MCP audiences
These changes restore OAuth functionality while maintaining ADR-005
security requirements for proper audience validation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Replace two non-compliant token verifiers (NextcloudTokenVerifier and
ProgressiveConsentTokenVerifier) with a single UnifiedTokenVerifier that properly
validates token audiences per MCP Security Best Practices specification.
The previous implementation had a critical security vulnerability where tokens
intended for the MCP server were passed directly to Nextcloud APIs without
proper audience validation (token passthrough anti-pattern). This violates
OAuth 2.0 security principles and the MCP specification.
Changes:
- Add UnifiedTokenVerifier supporting two compliant modes:
* Multi-audience mode (default): Validates tokens contain BOTH MCP and
Nextcloud audiences, enabling direct use without exchange
* Token exchange mode (opt-in): Validates MCP audience only, exchanges
for Nextcloud tokens via RFC 8693 with caching to minimize latency
- Remove token passthrough vulnerability from context.py and context_helper.py
- Implement token exchange caching (5-minute TTL default) to reduce network calls
- Add required environment variables for audience validation:
* NEXTCLOUD_MCP_SERVER_URL - MCP server URL (used as audience)
* NEXTCLOUD_RESOURCE_URI - Nextcloud resource identifier
* TOKEN_EXCHANGE_CACHE_TTL - Cache TTL for exchanged tokens
- Update docker-compose.yml with resource URI configuration for both OAuth modes
- Add comprehensive test suite (29 tests) covering both authentication modes
- Remove legacy NextcloudTokenVerifier and ProgressiveConsentTokenVerifier
Security improvements:
- Eliminates token passthrough anti-pattern
- Enforces proper audience separation between MCP and Nextcloud
- Complies with MCP Security Best Practices and RFC 8707/8693
- Maintains performance with token exchange caching
Test results: 65/65 unit tests passed, 5/5 smoke tests passed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fix nc_get_capabilities resource handler that was missing await when
calling get_nextcloud_client(ctx), causing error:
'coroutine' object has no attribute 'capabilities'
Root cause:
- get_nextcloud_client() is an async function (context.py:9)
- Returns a coroutine that must be awaited
- app.py:737 called it without await
Solution:
- Add await: client = await get_nextcloud_client(ctx)
- The handler is already async, so can await the call
Test fixed:
- test_mcp_resources_access now passes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
SessionAuthBackend middleware was wrapping the entire app including FastMCP,
which prevented FastMCP's OAuth token verification from running properly.
When SessionAuthBackend returned None for /mcp paths, Starlette marked requests
as "anonymous" and allowed them through, bypassing FastMCP's authentication.
Changes:
1. Route restructuring (app.py):
- Create separate Starlette app for browser routes (/user, /user/page)
- Apply SessionAuthBackend only to browser app
- Mount browser app at /user/* before FastMCP
- Mount FastMCP at / (catch-all with its own OAuth)
- Remove global SessionAuthBackend middleware
2. SessionAuthBackend cleanup (session_backend.py):
- Remove path exclusion logic (no longer needed)
- Simplify to only handle browser routes
- Update docstring to reflect mount-based isolation
Benefits:
- FastMCP's OAuth token verification now runs properly
- No middleware interference between authentication mechanisms
- Clear separation: SessionAuth for browser UI, OAuth Bearer for MCP clients
- Tests confirm OAuth authentication works correctly
Testing:
- All OAuth tests pass (test_mcp_oauth_*, test_jwt_*)
- Browser routes still require session auth
- FastMCP routes use OAuth Bearer tokens exclusively
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
The test_mcp_oauth_server_connection test was failing because OAuth tokens
had the wrong audience claim. The MCP server's progressive_token_verifier
expects tokens with audience matching its OAuth client ID, but tokens were
being issued with Nextcloud's default resource server audience.
Changes:
1. Test fixtures (tests/conftest.py):
- Add get_mcp_server_resource_metadata() helper to fetch PRM metadata
- Update playwright_oauth_token to include resource parameter in auth requests
- Update _get_oauth_token_with_scopes to support optional resource parameter
- Automatically fetch resource ID from MCP server's PRM endpoint
2. MCP Server (nextcloud_mcp_server/app.py):
- Fix Protected Resource Metadata endpoint to return OAuth client ID
- Change "resource" field from URL to client ID for proper audience validation
- Ensures tokens obtained with resource parameter have correct audience claim
How it works:
1. Test fetches /.well-known/oauth-protected-resource from MCP server
2. Extracts resource field (MCP server's client ID)
3. Includes &resource=<client-id> in OAuth authorization request (RFC 8707)
4. Nextcloud OIDC issues tokens with aud: [<client-id>]
5. MCP server's progressive_token_verifier accepts tokens (audience matches)
Fixes OAuth test failures:
- test_mcp_oauth_server_connection
- test_mcp_oauth_tool_execution
- test_mcp_oauth_client_with_playwright
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Wire up RFC 8693 token exchange throughout the MCP server to support
stateless per-request token conversion for external IdP scenarios.
Changes:
Authentication Flow:
- Add exchange_token_for_audience() for pure RFC 8693 exchange
- Update context_helper to use stateless token exchange
- Remove fallback to standard OAuth on exchange failure
- Make storage initialization lazy (only for delegation, not MCP tools)
Application Configuration:
- Add ENABLE_TOKEN_EXCHANGE environment variable support
- Skip provisioning tools when token exchange enabled
- Pass mcp_client_id to token broker for proper validation
- Update docker-compose.yml with token exchange config
Token Exchange Service:
- Add TOKEN_EXCHANGE_GRANT constant
- Implement exchange_token_for_audience() method
- Support both "mcp-server" and client_id audiences
- Lazy storage initialization for delegation scenarios
- Enhanced error handling and logging
Progressive Token Verifier:
- Add mcp_client_id parameter for external IdP validation
- Accept both "mcp-server" and configured client_id
- Support external IdP token verification
Key Behavior Changes:
- When ENABLE_TOKEN_EXCHANGE=true: Each MCP tool call triggers
stateless token exchange (client token → Nextcloud token)
- When ENABLE_TOKEN_EXCHANGE=false: Uses pass-through mode
(validates Flow 1 token and passes to Nextcloud)
- No provisioning tools registered in exchange mode
- No refresh tokens needed for request-time operations
This completes the token exchange implementation. The MCP server now
supports both pass-through (default) and exchange (opt-in) modes for
federated authentication architectures.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes import errors in MCP servers by removing references to the deleted
Hybrid Flow functions (oauth_callback and oauth_token).
Changes:
- Remove oauth_callback and oauth_token from imports in app.py
- Remove route registrations for /oauth/callback and /oauth/token
- Update comments to reference Progressive Consent Flow 1
This fixes the container restart loop caused by ImportError.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements /user and /user/page endpoints for displaying authenticated
user information in both BasicAuth and OAuth modes.
Key Features:
- Separate browser OAuth flow (/oauth/login, /oauth/login-callback, /oauth/logout)
- Session-based authentication using signed cookies
- Token refresh for persistent sessions
- HTML and JSON user info endpoints
- IdP profile information retrieval
Architecture:
- BasicAuth mode: Always authenticated as configured user
- OAuth mode: Browser-based authorization code flow with refresh tokens
- Session stored in SQLite with encrypted refresh tokens
- Server-side token refresh using internal Docker hostnames
OAuth Flow:
- /oauth/login: Initiates browser OAuth flow
- /oauth/login-callback: Handles IdP callback and stores refresh token
- /oauth/logout: Clears session cookie
- /user: JSON API endpoint (requires authentication)
- /user/page: HTML page endpoint (requires authentication)
DCR Scopes Fix:
- MCP server DCR now only requests basic OIDC scopes (openid profile email offline_access)
- Nextcloud app scopes (notes:read, etc.) are for MCP clients, not the server itself
- PRM endpoint dynamically advertises supported scopes from tool decorators
Files:
- nextcloud_mcp_server/auth/browser_oauth_routes.py: Browser OAuth flow handlers
- nextcloud_mcp_server/auth/session_backend.py: Starlette session authentication
- nextcloud_mcp_server/auth/userinfo_routes.py: User info endpoints with token refresh
- tests/server/auth/test_userinfo_routes.py: Unit tests
- tests/server/oauth/test_userinfo_integration.py: OAuth integration tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements Progressive Consent architecture with dual OAuth flows:
- Flow 1: Direct client authentication (aud: "mcp-server")
- Flow 2: Resource provisioning with refresh tokens
Components added:
- Client registry with validation (client_registry.py)
- Progressive token verifier (progressive_token_verifier.py)
- Token broker service integration
- Provisioning decorator for MCP tools
- OAuth provisioning tools (provision_nextcloud_access, etc.)
Configuration:
- Progressive Consent enabled by default (ENABLE_PROGRESSIVE_CONSENT=true)
- Client validation with pre-registered clients
- Audience separation framework
KNOWN ISSUE - Token Exchange Pattern Incorrect:
The current implementation does NOT properly implement token exchange.
MCP session tokens should be EXCHANGED for delegated Nextcloud tokens
during tool calls, not stored/reused. Critical corrections needed:
1. Session tokens: Flow 1 token → exchange → ephemeral Nextcloud token
- Generated on-demand per tool call
- Short-lived, not stored
- Scopes limited to tool requirements
2. Background tokens: Flow 2 refresh token → background Nextcloud token
- Only for offline/background jobs
- Potentially different scopes than session tokens
- Must NOT be used for MCP session tool calls
The token exchange mechanism needs to be implemented to properly
separate session-time delegation from background job authorization.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Import recipes from URLs using schema.org metadata
- Full CRUD operations for recipes
- Search, categorize, and organize recipes
- Manage keywords/tags and categories
- Configure app settings and trigger reindexing