This commit fixes two OAuth issues in the Astrolabe app:
1. **Always use PKCE (RFC 9207)**:
- PKCE is now used for all OAuth flows (public and confidential clients)
- Previous code only used PKCE for public clients, causing failures
- Confidential clients now use both PKCE + client_secret (defense in depth)
- Nextcloud OIDC provider requires PKCE, so token exchange was failing
2. **Add token_broker to oauth_context**:
- Token broker is now stored in oauth_context for management API access
- Fixes "Token broker not configured" error when revoking access
- Revoke endpoint needs token_broker to delete refresh tokens and invalidate cache
Changes:
- OAuthController.php: Always generate PKCE verifier/challenge for all clients
- OAuthController.php: Always include code_verifier in token exchange
- app.py: Store token_broker in oauth_context after creation
Fixes:
- Astrolabe OAuth flow now works with Nextcloud OIDC
- Revoke/disconnect functionality now works in Astrolabe settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The files_pdfviewer app route is internal to Nextcloud and not a valid
external URL. Reverted to using the standard Files app viewer URL for
all file types.
- Removed PDF-specific handling that used /apps/files_pdfviewer/
- All files now link to /apps/files/files/{id} (standard Files viewer)
- Fixes broken links in chunk modal titles and search results
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Rewrote Astrolabe README to be user-friendly and release-ready by
incorporating pitch.md content and moving technical details to linked
documentation.
Key changes:
- Incorporated compelling pitch narrative as opening
- Restructured around "What You Can Do" rather than architecture
- Added clear use cases for individuals, teams, and developers
- Simplified installation to 3 steps
- Moved OAuth flow and architecture details to ADR links
- Added emoji sections for visual appeal
- Focused on benefits over implementation
Sections:
- What You Can Do (search, visualization, AI agents)
- Installation (app store + manual)
- Quick Start (3-step setup)
- Features (personal, admin, unified search)
- Use Cases (research, collaboration, RAG workflows)
- Requirements (Nextcloud 30+, MCP server, OAuth)
- Documentation (links to installation, configuration, ADRs)
- Troubleshooting (quick fixes with links to detailed guides)
This README is now suitable for Nextcloud App Store submission.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements Alembic for managing token storage database schema versions.
Migrations run automatically on startup with full backward compatibility.
**Changes:**
- Add Alembic dependency (1.14.0+) and SQLAlchemy (auto-installed)
- Create migration infrastructure in alembic/ directory
- Add initial migration (001) capturing current schema
- Modify RefreshTokenStorage.initialize() to run migrations via anyio
- Add CLI commands: db upgrade, current, history, downgrade, migrate
- Add comprehensive migration documentation
**Backward Compatibility:**
- Pre-Alembic databases automatically stamped with revision 001
- No schema changes for existing databases
- Automatic upgrade on first startup after update
**Migration Strategy:**
Three scenarios handled:
1. New database → Run migrations from scratch
2. Pre-Alembic database → Stamp with 001 (no changes)
3. Alembic-managed → Upgrade to latest
**Architecture:**
- Uses anyio.to_thread.run_sync() for structured concurrency
- Alembic env.py runs with anyio.run() in worker thread
- SQLite-friendly migration patterns documented
- No ThreadPoolExecutor needed (anyio handles it)
**CLI Usage:**
```bash
nextcloud-mcp-server db upgrade # Upgrade to latest
nextcloud-mcp-server db current # Show version
nextcloud-mcp-server db history # View changelog
nextcloud-mcp-server db downgrade # Rollback (with confirmation)
nextcloud-mcp-server db migrate "description" # Create migration
```
**Testing:**
- All 13 webhook storage tests pass
- New/pre-Alembic database scenarios validated
- anyio integration tested
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add clickable link to modal title with OpenInNew icon
- Store currentResult to enable document navigation
- Fix deck_card URLs to use metadata.board_id
- Fix news_item URLs to use external article URL from metadata.url
- Add hover styling for title link and icon
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Update the unified search provider to show only chunk/page metadata
in search results, consistent with the chunk visualization result list.
Also fix news item URLs to link directly to the specific item.
Changes to SemanticSearchProvider:
1. Result display improvements:
- Remove excerpt text from search result subline
- Show only chunk/page metadata (e.g., "Chunk 2/5 · Page 3/10")
- Consistent with chunk visualization UI in App.vue
2. News item URL fix:
- Change from generic news index to specific item URL
- Format: /apps/news/item/{id}
- Allows direct navigation to the news article
3. Code cleanup:
- Remove unused $excerpt variable
- Remove unused truncateExcerpt() method
- Simplify transformResult() logic
Benefits:
- Cleaner, more scannable search results
- Consistent UX between unified search and app UI
- Functional links to news items instead of generic news page
- Reduced code complexity
Replace expensive Plotly.restyle() hover handlers with native hoverlabel
styling to indicate clickable points without performance degradation.
Implementation:
- Add hoverlabel configuration to document trace with distinct styling
- Bright blue background (#0082c9) to make hover state obvious
- Larger font size (15px) for better visibility
- White text for contrast against blue background
- Handled natively by Plotly - no JavaScript event handlers needed
Benefits:
- Zero performance impact - no chart re-renders on hover
- Smooth, responsive hover feedback
- Clear visual indication that points are clickable
- Consistent with existing hover tooltip pattern
Removed:
- Expensive handlePlotHover() and handlePlotUnhover() methods
- Plotly.restyle() calls that caused severe lag and freezing
- hover/unhover event listener registrations
The hover tooltip now uses the styled hoverlabel to stand out visually,
providing clear feedback that points are interactive without any
performance cost.
Enable users to click on points in the vector space visualization to
open the chunk viewer modal, providing a more direct interaction
method alongside the existing "Show Chunk" button.
Implementation details:
- Register plotly_click event handler in renderPlot() after chart creation
- Add handlePlotClick() method to process click events
- Use point index mapping to access full result object from this.results
- Add loading guard in viewChunk() to prevent concurrent chunk loading
- Add cursor styling: pointer for result points, default for query point
- Add beforeDestroy() lifecycle hook to cleanup event handlers
Features:
- Both interaction methods work: click chart points OR "Show Chunk" button
- Only result points (trace 0) are clickable, query point (trace 1) ignored
- Pointer cursor on hover indicates clickable points
- Loading state prevents rapid clicks from causing issues
- Memory leak prevention through proper event handler cleanup
Technical approach:
- Uses index mapping (not data duplication) for efficiency
- Results and coordinates arrays have guaranteed 1:1 mapping from API
- Event handler re-registered on each chart re-render
- CSS-based cursor styling (more performant than JS hover handlers)
Testing:
- ESLint validation passed
- Follows Vue 2.7 component property order conventions
- Compatible with existing chunk viewer modal
This commit implements three UI improvements for the chunk viewer:
1. Fixed modal footer with navigation controls
- Moved PDF navigation buttons to a fixed footer
- Footer remains visible while scrolling content
- Three-section layout: fixed header, scrollable body, fixed footer
2. Removed duplicate navigation controls
- Removed previous/next buttons from PDFViewer component
- Controls now only in App.vue modal footer
- Cleaned up unused imports and CSS
3. Markdown rendering for chunk content
- Created MarkdownViewer component using markdown-it
- Renders markdown content aligned with Nextcloud design system
- Removed problematic markdown-it-task-checkbox dependency
- Combines before/chunk/after context with visual separators
4. Cleaned up search results display
- Removed excerpt snippets from results list
- Kept only chunk/page metadata for cleaner UI
The modal structure now has:
- Fixed header (title + close button)
- Scrollable body (PDF canvas or markdown content)
- Fixed footer (page navigation - always visible)
Fixes markdown rendering "require is not defined" error by using
only markdown-it without CommonJS plugins.
Fixes 401 errors after first token refresh when using IdPs that
implement refresh token rotation (Keycloak, modern OAuth providers).
**Root Cause**:
McpTokenStorage::getAccessToken() was discarding the new refresh token
returned by the IdP after successful refresh, always keeping the old one.
This caused:
- First refresh: works (uses original refresh token)
- Second refresh: fails with 401 (old refresh token invalidated by IdP)
**Solution**:
Use new refresh token from IdP response if provided, fall back to old
token for providers that don't rotate refresh tokens.
**Changed**:
- lib/Service/McpTokenStorage.php:184
From: $token['refresh_token'] // Always old token
To: $newTokenData['refresh_token'] ?? $token['refresh_token']
**Verified**:
ApiController already handles rotation correctly using the same pattern.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit includes two improvements to the Astroglobe semantic search UI:
1. **Multi-select Document Types** (App.vue):
- Changed NcCheckboxRadioSwitch binding from v-model to :checked/:update:checked
- Implemented toggleDocType() method to manually manage selectedDocTypes array
- Fixes issue where only single document type could be selected at a time
- Users can now filter search results by multiple doc types simultaneously
2. **PDF Viewer Reactive Rendering** (PDFViewer.vue):
- Refactored canvas rendering to use Vue reactive watcher pattern
- Added watcher on 'loading' state that triggers rendering when canvas available
- Removed imperative renderPage() call from loadPDF() method
- Inspired by files_pdfviewer's promise/event-based initialization approach
- Improves alignment with Vue's reactive data flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit addresses 4 critical issues identified in code review:
1. **Token Rotation Race Condition** (token_broker.py)
- Added per-user locking mechanism to prevent concurrent refresh token corruption
- Implemented double-check pattern for cache after acquiring lock
- Users can now safely refresh concurrently without token desync
2. **Hardcoded OAuth Client ID** (PHP files)
- Made client ID configurable via `astroglobe_client_id` in system config
- Updated McpServerClient to provide getClientId() method
- Injected McpServerClient into IdpTokenRefresher and OAuthController
- Updated admin settings UI to display client ID configuration status
- App gracefully handles missing client ID with warnings in admin UI
3. **Missing Cache Invalidation** (management.py:revoke_user_access)
- Added cache.invalidate() call when revoking user access
- Ensures both storage AND cache are cleared atomically
- Prevents stale cached tokens from being used after revocation
4. **Error Message Exposure** (management.py)
- Created _sanitize_error_for_client() helper function
- Updated all error handlers to log detailed errors internally
- Returns generic messages to clients to prevent information leakage
- Protects against exposing database paths, API URLs, tokens, etc.
All changes are backward compatible and preserve existing functionality.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Improve unified search results with chunk/page metadata and add
webhook management capabilities to McpServerClient.
Changes:
- SemanticSearchProvider improvements:
- Display chunk position (e.g., "Chunk 2/5")
- Display page numbers for PDFs (e.g., "Page 3/10")
- Fix file links to open in Files app correctly
- Fix deck card links to use proper URL format
- Show metadata in subline before excerpt
- Use proper icons and thumbnails for each doc type
- McpServerClient webhook methods:
- listWebhooks() - Get all registered webhooks
- createWebhook() - Register new webhook
- deleteWebhook() - Remove webhook registration
- enableWebhook() / disableWebhook() - Toggle webhook status
- getWebhookLogs() - Retrieve delivery logs
Benefits:
- Better search result context with chunk and page info
- Clickable links that open correct resources
- Full webhook lifecycle management via API
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add admin interface for configuring real-time webhook sync with
pre-configured presets for common scenarios.
Changes:
- Add webhook presets section to admin settings page
- Shows available presets filtered by installed apps
- Enable/disable presets with one click
- Displays current webhook status
- Add client secret configuration status display
- Shows whether confidential client is configured
- Provides setup instructions for optional client secret
- Add adminSettings.js for webhook management
- Load webhook presets via API
- Enable/disable webhook presets
- Handle search settings form submission
- Update vite.config.js to build adminSettings entry point
- Pass clientSecretConfigured flag to template
UI Features:
- Real-time preset status (enabled/disabled)
- One-click enable/disable for webhook bundles
- App-aware filtering (only shows presets for installed apps)
- Clear instructions for requirements and setup
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Improve search result display to match Nextcloud's native search providers by using mimetype-specific icons and preview thumbnails.
**File Results:**
- Use preview thumbnails for images/PDFs (core.Preview API)
- Use mimetype-specific icon classes (icon-pdf, icon-text, icon-image, etc.)
- Detect folders and use icon-folder appropriately
**Other Document Types:**
- Notes: icon-notes
- Deck Cards: icon-deck
- Calendar: icon-calendar
- News: icon-rss
- Contacts: icon-contacts
**API Changes:**
- Management API now includes mime_type in search results
- SemanticSearchProvider uses IMimeTypeDetector and IPreview services
This makes Astroglobe search results visually consistent with Files, Notes, and other native providers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Integrate semantic search into Nextcloud's unified search UI. File results now use fileId parameter to properly open files instead of just navigating to the Files app.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add Plotly.js 3D scatter plot showing search results in PCA space
- Create shared visualization.py module to avoid code duplication
- Pass include_pca parameter through API chain to enable coordinates
- Fix OAuth redirects to use /settings/user/astroglobe
The visualization shows document embeddings projected to 3D via PCA,
with the query point highlighted in red. Uses Viridis colorscale
for score visualization, matching the existing vector-viz page.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Update all user-facing text to focus on Astroglobe as a semantic
search service for Nextcloud users:
- info.xml: New description focusing on finding content by meaning
- Settings sections: Renamed from "MCP Server" to "Astroglobe"
- Personal settings: Reframed as content indexing controls
- Admin settings: Reframed as semantic search administration
- OAuth flow: Explains semantic search benefits to users
Key messaging changes:
- "MCP Server" → "Astroglobe"
- "Grant Background Access" → "Enable Semantic Search"
- "Vector Sync" → "Content Indexing"
- Focus on user benefits: natural language search, finding by meaning
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds a native Nextcloud app "Astroglobe" that provides:
- Personal settings: OAuth authorization for background MCP access
- Admin settings: Server status and vector sync monitoring
- API endpoints for MCP server communication
The app uses PKCE OAuth flow to obtain tokens for the MCP server,
enabling features like background vector sync per ADR-018.
Includes:
- PHP app structure (controllers, services, settings)
- Vue.js frontend components
- Docker compose mount configuration
- Installation hook for development testing
- ADR-018 documentation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Migrates from custom word-based chunking to LangChain's MarkdownTextSplitter
for better semantic search quality. This implements the chunking portion of
ADR-011.
Changes:
- Replace custom regex word chunker with MarkdownTextSplitter
- Optimized for Markdown content (headers, code blocks, lists)
- Convert from word-based (512 words) to character-based (2048 chars) chunking
- Maintain backward-compatible ChunkWithPosition interface
- Update configuration defaults and validation
- Update all unit tests (12/12 passing)
Benefits:
- Respects markdown structure boundaries
- Never breaks code blocks or headers mid-chunk
- Preserves semantic coherence within chunks
- Expected 20-30% improvement in recall quality
- Industry-standard approach (used by production RAG systems)
Note: Full reindex required to apply new chunking to existing documents.
Current vector database still contains old word-based chunks.
Related: ADR-011 (Improving Semantic Search Quality)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit enhances the vector visualization interface with better score
transparency and improved UX:
**Dual-Score Display:**
- Store original algorithm scores before normalization (viz_routes.py:203)
- Display both raw and normalized scores: "Raw Score: 0.842 (89% relative)"
- Update plot hover text with dual scores (userinfo_routes.py:740)
- Fixes issue where all queries showed at least one 100% match regardless
of actual relevance (normalization artifact)
**UI Improvements:**
1. Fusion Method dropdown: Changed from x-show to :disabled
- Prevents jarring layout shift when switching algorithms
- Dropdown stays visible but grayed out when Semantic is selected
- Better UX with opacity: 0.5 and cursor: not-allowed
2. Score Threshold: Changed step from 0.1 to "any"
- Allows arbitrary float precision (0.7, 0.85, 0.123)
- Users can now fine-tune threshold values
3. Document Types: Converted multi-select to checkbox grid
- Replaced clunky Ctrl/Cmd multi-select listbox
- Checkbox grid with cleaner layout
- Positioned left of Score Threshold and Result Limit inputs
- More intuitive UX
**Technical Details:**
- Raw score ranges vary by algorithm:
- Semantic: 0.0-1.0 (cosine similarity)
- BM25 RRF: ~0.001-0.033 (Reciprocal Rank Fusion)
- BM25 DBSF: Can exceed 1.0 (Distribution-Based Score Fusion)
- Normalized scores (0-1) used for visual encoding (marker size, color)
- Original scores preserved in API response via getattr fallback
Files modified:
- nextcloud_mcp_server/auth/viz_routes.py (store original_score)
- nextcloud_mcp_server/auth/templates/vector_viz.html (UI controls)
- nextcloud_mcp_server/auth/userinfo_routes.py (plot hover text)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Vector Visualization Improvements:
- Add interactive vector viz tab with Alpine.js and Plotly.js to user info page
- Refactor viz route CSS for better scoping and maintainability
- Remove unused nextcloud_host variable
Performance Optimizations:
- Parallelize access verification in fuzzy and keyword search algorithms
- Use asyncio.gather() to verify multiple documents concurrently
- Add exception handling with return_exceptions=True for resilience
Dependencies:
- Update third_party/oidc submodule to include RFC 9728 resource_url support
🤖 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>
The token-exchange-nextcloud client scope was being inherited by DCR clients
regardless of configuration, causing all tokens to have incorrect audience.
This commit removes the scope entirely and updates audience validation to be
more flexible.
## Problem
1. **DCR clients inherited token-exchange-nextcloud scope**
- Even after removing from nextcloud-mcp-server client's optional scopes
- Even though not in realm's default optional scopes
- Keycloak was adding all defined client scopes to DCR clients
2. **After removing audience mappers, tokens had no audience**
- Keycloak doesn't automatically populate aud from RFC 8707 resource parameter
- MCP server rejected tokens: "wrong audience [], expected nextcloud-mcp-server"
## Solution
### 1. Remove token-exchange-nextcloud Client Scope Entirely
- Delete the scope definition from realm-export.json
- Prevents it from being inherited by DCR clients
- audience is now set directly on nextcloud-mcp-server client via protocol mapper
### 2. Update Audience Validation Logic
Make progressive_token_verifier.py more flexible:
**Before**: Strict validation - reject if aud != mcp_client_id
```python
if self.mcp_client_id not in audiences:
return None # Reject
```
**After**: Flexible validation
- ✅ Accept tokens with no audience claim
- ✅ Accept tokens with MCP client ID in audience
- ✅ Accept tokens with resource URL in audience
- ❌ Reject tokens with "nextcloud" audience (wrong flow)
```python
if audiences:
if "nextcloud" in audiences:
return None # Wrong flow
# Accept other audiences (may use resource URL)
else:
# Accept tokens without audience
```
## Behavior
**External MCP Clients (Gemini CLI)**:
- Register via DCR → No token-exchange-nextcloud scope inherited ✅
- Request token → No audience mappers applied
- Token: `aud` absent or based on resource parameter
- MCP server: Accepts token ✅
**MCP Server (nextcloud-mcp-server) → Nextcloud APIs**:
- Has direct nextcloud-audience protocol mapper
- Token: `aud: "nextcloud"` (hardcoded on client)
- Nextcloud user_oidc: Validates successfully ✅
## Security
Token validation still enforces:
- Signature verification (via IdP JWKS)
- Expiration checks
- Issuer validation
- Scope-based authorization
- Explicitly rejects tokens meant for Nextcloud (aud: "nextcloud")
Accepting tokens without audience is safe because:
- External IdP (Keycloak) validates token issuance
- MCP server can fall back to introspection for opaque tokens
- RFC 9068 JWT Profile allows empty audience for resource servers
## Related
- RFC 8707: Resource Indicators for OAuth 2.0
- RFC 9068: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens
- Keycloak DCR client scope inheritance behavior
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>