diff --git a/docs/authentication.md b/docs/authentication.md index 271b795..ef48fe8 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -223,6 +223,55 @@ NEXTCLOUD_OIDC_CLIENT_SECRET= | Token Storage | None | Refresh tokens only | All tokens | | Deployment Complexity | Low | Medium | High | +### Astrolabe User Setup (Hybrid Mode) + +When Astrolabe connects to an MCP server running in hybrid mode, users must complete a **two-step credential setup**: + +#### Step 1: OAuth Authorization (Search Access) + +**Purpose**: Allows Astrolabe to call MCP server APIs on the user's behalf. + +**Flow**: +1. User opens Astrolabe Personal Settings in Nextcloud +2. Clicks "Authorize" button +3. Redirected to Astrolabe's OAuth controller (`/apps/astrolabe/oauth/initiate`) +4. OAuth controller discovers IdP from MCP server's `/api/v1/status` endpoint +5. User authenticates with Identity Provider (Nextcloud OIDC or external IdP) +6. Tokens stored in Nextcloud user config (`McpTokenStorage`) +7. Astrolabe can now perform semantic searches via MCP API + +**Technical Details**: +- Token audience: MCP server +- Token storage: Nextcloud app config (`oc_preferences`) +- Used for: `/api/v1/search`, `/api/v1/status` (authenticated endpoints) + +#### Step 2: App Password (Background Indexing) + +**Purpose**: Allows MCP server to access Nextcloud content for background sync. + +**Flow**: +1. User generates app password in Nextcloud Security settings +2. Enters app password in Astrolabe Personal Settings +3. App password validated against Nextcloud and stored (encrypted) +4. MCP server can now index user's content in the background + +**Technical Details**: +- Credential type: Nextcloud app password +- Token storage: MCP server's refresh token database +- Used for: Background indexing, content sync to vector database + +#### Why Two Credentials? + +| Direction | Auth Method | Purpose | +|-----------|-------------|---------| +| Astrolabe → MCP Server | OAuth Bearer Token | User searches, settings management | +| MCP Server → Nextcloud | BasicAuth (App Password) | Background content indexing | + +The separation ensures: +- **Security**: Each credential has limited scope +- **Audit Trail**: OAuth tokens identify users; app passwords enable background ops +- **User Control**: Users explicitly grant each type of access + ### See Also - [OAuth Architecture](oauth-architecture.md) - Progressive Consent (Flow 2) details - [Configuration](configuration.md#enable_offline_access) - Hybrid mode configuration diff --git a/nextcloud_mcp_server/api/management.py b/nextcloud_mcp_server/api/management.py index 4922939..bfded9e 100644 --- a/nextcloud_mcp_server/api/management.py +++ b/nextcloud_mcp_server/api/management.py @@ -387,8 +387,13 @@ async def get_server_status(request: Request) -> JSONResponse: if mode == AuthMode.MULTI_USER_BASIC: response_data["supports_app_passwords"] = settings.enable_offline_access - # Include OIDC configuration if in OAuth mode - if auth_mode == "oauth": + # Include OIDC configuration if OAuth is available + # This includes OAuth mode AND hybrid mode (multi_user_basic + offline_access) + # Astrolabe needs OIDC config to discover IdP for OAuth flow in hybrid mode + oauth_provisioning_available = auth_mode == "oauth" or ( + mode == AuthMode.MULTI_USER_BASIC and settings.enable_offline_access + ) + if oauth_provisioning_available: # Provide IdP discovery information for NC PHP app oidc_config = {} diff --git a/tests/unit/test_management_status_endpoint.py b/tests/unit/test_management_status_endpoint.py new file mode 100644 index 0000000..11fff7e --- /dev/null +++ b/tests/unit/test_management_status_endpoint.py @@ -0,0 +1,337 @@ +""" +Unit tests for Management API status endpoint. + +Tests the /api/v1/status endpoint focusing on: +- OIDC config availability in different auth modes +- Hybrid mode (multi_user_basic + enable_offline_access) returning OIDC config +- OAuth mode returning OIDC config +- Non-OAuth modes NOT returning OIDC config +""" + +from unittest.mock import MagicMock, patch + +import pytest +from starlette.applications import Starlette +from starlette.routing import Route +from starlette.testclient import TestClient + +from nextcloud_mcp_server.api.management import get_server_status +from nextcloud_mcp_server.config_validators import AuthMode + +pytestmark = pytest.mark.unit + + +def create_test_app(): + """Create a test Starlette app with the status endpoint.""" + return Starlette( + routes=[ + Route("/api/v1/status", get_server_status, methods=["GET"]), + ] + ) + + +def create_mock_settings( + enable_multi_user_basic: bool = False, + enable_offline_access: bool = False, + oidc_discovery_url: str | None = None, + oidc_issuer: str | None = None, + vector_sync_enabled: bool = False, + nextcloud_url: str = "http://localhost", + enable_token_exchange: bool = False, + mcp_client_id: str | None = None, + mcp_client_secret: str | None = None, +): + """Create mock settings with specified auth configuration.""" + settings = MagicMock() + settings.enable_multi_user_basic_auth = enable_multi_user_basic + settings.enable_offline_access = enable_offline_access + settings.oidc_discovery_url = oidc_discovery_url + settings.oidc_issuer = oidc_issuer + settings.vector_sync_enabled = vector_sync_enabled + settings.nextcloud_url = nextcloud_url + settings.enable_token_exchange = enable_token_exchange + settings.mcp_client_id = mcp_client_id + settings.mcp_client_secret = mcp_client_secret + return settings + + +class TestStatusEndpointOidcConfig: + """Tests for OIDC configuration in status endpoint.""" + + def test_hybrid_mode_returns_oidc_config(self): + """Test that hybrid mode (multi_user_basic + offline_access) returns OIDC config.""" + mock_settings = create_mock_settings( + enable_multi_user_basic=True, + enable_offline_access=True, + oidc_discovery_url="http://keycloak/.well-known/openid-configuration", + oidc_issuer="http://keycloak/realms/test", + ) + + # get_settings and detect_auth_mode are imported inside the function + with ( + patch( + "nextcloud_mcp_server.config.get_settings", return_value=mock_settings + ), + patch( + "nextcloud_mcp_server.config_validators.detect_auth_mode", + return_value=AuthMode.MULTI_USER_BASIC, + ), + ): + app = create_test_app() + client = TestClient(app) + response = client.get("/api/v1/status") + + assert response.status_code == 200 + data = response.json() + + # Verify auth mode + assert data["auth_mode"] == "multi_user_basic" + assert data["supports_app_passwords"] is True + + # Verify OIDC config is present (key feature for hybrid mode) + assert "oidc" in data + assert ( + data["oidc"]["discovery_url"] + == "http://keycloak/.well-known/openid-configuration" + ) + assert data["oidc"]["issuer"] == "http://keycloak/realms/test" + + def test_hybrid_mode_without_oidc_settings_no_oidc_key(self): + """Test that hybrid mode without OIDC settings doesn't include empty oidc key.""" + mock_settings = create_mock_settings( + enable_multi_user_basic=True, + enable_offline_access=True, + oidc_discovery_url=None, + oidc_issuer=None, + ) + + with ( + patch( + "nextcloud_mcp_server.config.get_settings", return_value=mock_settings + ), + patch( + "nextcloud_mcp_server.config_validators.detect_auth_mode", + return_value=AuthMode.MULTI_USER_BASIC, + ), + ): + app = create_test_app() + client = TestClient(app) + response = client.get("/api/v1/status") + + assert response.status_code == 200 + data = response.json() + + # OIDC key should NOT be present if no OIDC settings configured + assert "oidc" not in data + + def test_multi_user_basic_without_offline_access_no_oidc(self): + """Test that multi_user_basic WITHOUT offline_access doesn't return OIDC config.""" + mock_settings = create_mock_settings( + enable_multi_user_basic=True, + enable_offline_access=False, # Key difference: no offline access + oidc_discovery_url="http://keycloak/.well-known/openid-configuration", + oidc_issuer="http://keycloak/realms/test", + ) + + with ( + patch( + "nextcloud_mcp_server.config.get_settings", return_value=mock_settings + ), + patch( + "nextcloud_mcp_server.config_validators.detect_auth_mode", + return_value=AuthMode.MULTI_USER_BASIC, + ), + ): + app = create_test_app() + client = TestClient(app) + response = client.get("/api/v1/status") + + assert response.status_code == 200 + data = response.json() + + # Verify auth mode + assert data["auth_mode"] == "multi_user_basic" + assert data["supports_app_passwords"] is False + + # OIDC config should NOT be present (not hybrid mode) + assert "oidc" not in data + + def test_oauth_mode_returns_oidc_config(self): + """Test that OAuth mode returns OIDC config.""" + mock_settings = create_mock_settings( + enable_multi_user_basic=False, + enable_offline_access=True, + oidc_discovery_url="http://nextcloud/.well-known/openid-configuration", + oidc_issuer="http://nextcloud", + ) + + with ( + patch( + "nextcloud_mcp_server.config.get_settings", return_value=mock_settings + ), + patch( + "nextcloud_mcp_server.config_validators.detect_auth_mode", + return_value=AuthMode.OAUTH_SINGLE_AUDIENCE, + ), + ): + app = create_test_app() + client = TestClient(app) + response = client.get("/api/v1/status") + + assert response.status_code == 200 + data = response.json() + + # Verify auth mode + assert data["auth_mode"] == "oauth" + + # Verify OIDC config is present + assert "oidc" in data + assert ( + data["oidc"]["discovery_url"] + == "http://nextcloud/.well-known/openid-configuration" + ) + + def test_single_user_basic_no_oidc(self): + """Test that single-user BasicAuth mode doesn't return OIDC config.""" + mock_settings = create_mock_settings( + enable_multi_user_basic=False, + enable_offline_access=False, + oidc_discovery_url="http://keycloak/.well-known/openid-configuration", + oidc_issuer="http://keycloak/realms/test", + ) + + with ( + patch( + "nextcloud_mcp_server.config.get_settings", return_value=mock_settings + ), + patch( + "nextcloud_mcp_server.config_validators.detect_auth_mode", + return_value=AuthMode.SINGLE_USER_BASIC, + ), + ): + app = create_test_app() + client = TestClient(app) + response = client.get("/api/v1/status") + + assert response.status_code == 200 + data = response.json() + + # Verify auth mode + assert data["auth_mode"] == "basic" + + # OIDC config should NOT be present + assert "oidc" not in data + # supports_app_passwords should NOT be present (only for multi_user_basic) + assert "supports_app_passwords" not in data + + def test_oidc_partial_config_only_discovery_url(self): + """Test OIDC config with only discovery URL set.""" + mock_settings = create_mock_settings( + enable_multi_user_basic=True, + enable_offline_access=True, + oidc_discovery_url="http://keycloak/.well-known/openid-configuration", + oidc_issuer=None, # Only discovery URL + ) + + with ( + patch( + "nextcloud_mcp_server.config.get_settings", return_value=mock_settings + ), + patch( + "nextcloud_mcp_server.config_validators.detect_auth_mode", + return_value=AuthMode.MULTI_USER_BASIC, + ), + ): + app = create_test_app() + client = TestClient(app) + response = client.get("/api/v1/status") + + assert response.status_code == 200 + data = response.json() + + assert "oidc" in data + assert ( + data["oidc"]["discovery_url"] + == "http://keycloak/.well-known/openid-configuration" + ) + assert "issuer" not in data["oidc"] + + def test_oidc_partial_config_only_issuer(self): + """Test OIDC config with only issuer set.""" + mock_settings = create_mock_settings( + enable_multi_user_basic=True, + enable_offline_access=True, + oidc_discovery_url=None, # Only issuer + oidc_issuer="http://keycloak/realms/test", + ) + + with ( + patch( + "nextcloud_mcp_server.config.get_settings", return_value=mock_settings + ), + patch( + "nextcloud_mcp_server.config_validators.detect_auth_mode", + return_value=AuthMode.MULTI_USER_BASIC, + ), + ): + app = create_test_app() + client = TestClient(app) + response = client.get("/api/v1/status") + + assert response.status_code == 200 + data = response.json() + + assert "oidc" in data + assert "discovery_url" not in data["oidc"] + assert data["oidc"]["issuer"] == "http://keycloak/realms/test" + + +class TestStatusEndpointBasicResponse: + """Tests for basic status endpoint response fields.""" + + def test_status_includes_version(self): + """Test that status endpoint includes version.""" + mock_settings = create_mock_settings() + + with ( + patch( + "nextcloud_mcp_server.config.get_settings", return_value=mock_settings + ), + patch( + "nextcloud_mcp_server.config_validators.detect_auth_mode", + return_value=AuthMode.SINGLE_USER_BASIC, + ), + ): + app = create_test_app() + client = TestClient(app) + response = client.get("/api/v1/status") + + assert response.status_code == 200 + data = response.json() + + assert "version" in data + assert "uptime_seconds" in data + assert "management_api_version" in data + assert data["management_api_version"] == "1.0" + + def test_status_includes_vector_sync_enabled(self): + """Test that status endpoint includes vector_sync_enabled.""" + mock_settings = create_mock_settings(vector_sync_enabled=True) + + with ( + patch( + "nextcloud_mcp_server.config.get_settings", return_value=mock_settings + ), + patch( + "nextcloud_mcp_server.config_validators.detect_auth_mode", + return_value=AuthMode.SINGLE_USER_BASIC, + ), + ): + app = create_test_app() + client = TestClient(app) + response = client.get("/api/v1/status") + + assert response.status_code == 200 + data = response.json() + + assert data["vector_sync_enabled"] is True diff --git a/third_party/astrolabe/appinfo/info.xml b/third_party/astrolabe/appinfo/info.xml index c408ee3..0d03bac 100644 --- a/third_party/astrolabe/appinfo/info.xml +++ b/third_party/astrolabe/appinfo/info.xml @@ -40,7 +40,7 @@ See [documentation](https://github.com/cbcoutinho/nextcloud-mcp-server) for conf https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/third_party/astrolabe/screenshots/01-unified-search-astrolabe.png?raw=1 https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/third_party/astrolabe/screenshots/03-chunk-viewer-open.png?raw=1 - + OCA\Astrolabe\Settings\Personal diff --git a/third_party/astrolabe/lib/Service/IdpTokenRefresher.php b/third_party/astrolabe/lib/Service/IdpTokenRefresher.php index d57e051..f7faa4c 100644 --- a/third_party/astrolabe/lib/Service/IdpTokenRefresher.php +++ b/third_party/astrolabe/lib/Service/IdpTokenRefresher.php @@ -38,25 +38,23 @@ class IdpTokenRefresher { /** * Get Nextcloud base URL for constructing internal OIDC endpoint URLs. * - * @return string Base URL (e.g., "https://nextcloud.example.com") + * Uses Nextcloud's CLI URL config if set (for non-containerized deployments), + * otherwise defaults to http://localhost for container environments. + * + * Configuration priority: + * 1. overwrite.cli.url - Official Nextcloud system config for CLI operations + * 2. http://localhost - Default for Docker containers (web server on port 80) + * + * @return string Base URL for internal requests (e.g., "http://localhost") */ private function getNextcloudBaseUrl(): string { - // Prefer explicit CLI URL override - $baseUrl = $this->config->getSystemValue('overwrite.cli.url', ''); - - if (!empty($baseUrl)) { - return rtrim($baseUrl, '/'); + // Check for overwrite.cli.url (used in non-containerized deployments) + $cliUrl = $this->config->getSystemValue('overwrite.cli.url', ''); + if (!empty($cliUrl)) { + return rtrim($cliUrl, '/'); } - // Fallback to first trusted domain with protocol - $trustedDomains = $this->config->getSystemValue('trusted_domains', []); - if (!empty($trustedDomains)) { - $protocol = $this->config->getSystemValue('overwriteprotocol', 'https'); - return $protocol . '://' . $trustedDomains[0]; - } - - // Last resort: localhost (log warning) - $this->logger->warning('IdpTokenRefresher: No Nextcloud URL configured, using localhost fallback'); + // Default: container environment with web server on localhost:80 return 'http://localhost'; } diff --git a/third_party/astrolabe/lib/Settings/Personal.php b/third_party/astrolabe/lib/Settings/Personal.php index 577f1e8..a93ca7f 100644 --- a/third_party/astrolabe/lib/Settings/Personal.php +++ b/third_party/astrolabe/lib/Settings/Personal.php @@ -79,60 +79,46 @@ class Personal implements ISettings { // Check if user has MCP OAuth token $token = $this->tokenStorage->getUserToken($userId); - // For multi_user_basic mode with app password support, check if user has app password + // For multi_user_basic mode with app password support (hybrid mode) + // User needs BOTH: + // 1. OAuth token for Astrolabe→MCP API calls (stored in McpTokenStorage) + // 2. App password for MCP→Nextcloud background sync if ($authMode === 'multi_user_basic' && $supportsAppPasswords) { - // Check if user has already provided an app password - $hasBackgroundAccess = $this->tokenStorage->hasBackgroundSyncAccess($userId); + // Check both credentials + $hasOAuthToken = ($token !== null && !$this->tokenStorage->isExpired($token)); + $hasAppPassword = $this->tokenStorage->hasBackgroundSyncAccess($userId); + $backgroundSyncType = $this->tokenStorage->getBackgroundSyncType($userId); + $backgroundSyncProvisionedAt = $this->tokenStorage->getBackgroundSyncProvisionedAt($userId); - if (!$hasBackgroundAccess) { - // No app password yet - show app password entry form - return new TemplateResponse( - Application::APP_ID, - 'settings/personal', - [ - 'serverUrl' => $this->client->getPublicServerUrl(), // Changed from server_url to serverUrl - 'serverStatus' => $serverStatus, - 'auth_mode' => $authMode, - 'authMode' => $authMode, // Add camelCase version for template - 'supports_app_passwords' => $supportsAppPasswords, - 'supportsAppPasswords' => $supportsAppPasswords, // Add camelCase version - 'session' => null, // No session yet - 'hasBackgroundAccess' => false, // FIXED: Add missing parameter - 'backgroundAccessGranted' => false, // FIXED: Add missing parameter - 'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false, - 'hasToken' => false, // No OAuth token in multi_user_basic mode - 'requesttoken' => \OCP\Util::callRegister(), - ], - TemplateResponse::RENDER_AS_BLANK - ); - } else { - // User has app password - show active status - $backgroundSyncType = $this->tokenStorage->getBackgroundSyncType($userId); - $backgroundSyncProvisionedAt = $this->tokenStorage->getBackgroundSyncProvisionedAt($userId); + // OAuth URL for Astrolabe's own OAuth controller (NOT MCP server's browser OAuth) + $oauthUrl = $this->urlGenerator->linkToRoute('astrolabe.oauth.initiateOAuth'); - $parameters = [ - 'userId' => $userId, - 'serverStatus' => $serverStatus, - 'session' => null, // No user session for app passwords - 'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false, - 'backgroundAccessGranted' => true, // App password grants background access - 'serverUrl' => $this->client->getPublicServerUrl(), - 'hasToken' => false, // No OAuth token - 'hasBackgroundAccess' => true, - 'backgroundSyncType' => $backgroundSyncType, - 'backgroundSyncProvisionedAt' => $backgroundSyncProvisionedAt, - 'authMode' => $authMode, - 'supportsAppPasswords' => $supportsAppPasswords, - 'requesttoken' => \OCP\Util::callRegister(), - ]; + // Consolidated template parameters (camelCase convention) + $parameters = [ + 'userId' => $userId, + 'serverUrl' => $this->client->getPublicServerUrl(), + 'serverStatus' => $serverStatus, + 'authMode' => $authMode, + 'supportsAppPasswords' => $supportsAppPasswords, + 'session' => null, // No session in hybrid mode + 'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false, + // OAuth token status (for Astrolabe→MCP API calls) + 'hasOAuthToken' => $hasOAuthToken, + 'oauthUrl' => $oauthUrl, + // App password status (for MCP→Nextcloud background sync) + 'hasBackgroundAccess' => $hasAppPassword, + 'backgroundAccessGranted' => $hasAppPassword, // Legacy alias + 'backgroundSyncType' => $backgroundSyncType, + 'backgroundSyncProvisionedAt' => $backgroundSyncProvisionedAt, + 'requesttoken' => \OCP\Util::callRegister(), + ]; - return new TemplateResponse( - Application::APP_ID, - 'settings/personal', - $parameters, - TemplateResponse::RENDER_AS_BLANK - ); - } + return new TemplateResponse( + Application::APP_ID, + 'settings/personal', + $parameters, + TemplateResponse::RENDER_AS_BLANK + ); } // For OAuth modes, if no token or token is expired, show OAuth authorization UI elseif (!$token || $this->tokenStorage->isExpired($token)) { @@ -198,6 +184,9 @@ class Personal implements ISettings { $backgroundSyncType = $this->tokenStorage->getBackgroundSyncType($userId); $backgroundSyncProvisionedAt = $this->tokenStorage->getBackgroundSyncProvisionedAt($userId); + // OAuth URL for standard OAuth mode (in case user needs to re-authorize) + $oauthUrl = $this->urlGenerator->linkToRoute('astrolabe.oauth.initiateOAuth'); + // Provide initial state for Vue.js frontend (if needed) $this->initialState->provideInitialState('user-data', [ 'userId' => $userId, @@ -205,17 +194,22 @@ class Personal implements ISettings { 'session' => $userSession, ]); + // Consolidated template parameters (camelCase convention) $parameters = [ 'userId' => $userId, + 'serverUrl' => $this->client->getPublicServerUrl(), 'serverStatus' => $serverStatus, 'session' => $userSession, 'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false, - 'backgroundAccessGranted' => $userSession['background_access_granted'] ?? false, - 'serverUrl' => $this->client->getPublicServerUrl(), - 'hasToken' => true, + // OAuth status + 'hasOAuthToken' => true, + 'oauthUrl' => $oauthUrl, + // Background sync status 'hasBackgroundAccess' => $hasBackgroundAccess, + 'backgroundAccessGranted' => $userSession['background_access_granted'] ?? false, // Legacy 'backgroundSyncType' => $backgroundSyncType, 'backgroundSyncProvisionedAt' => $backgroundSyncProvisionedAt, + 'requesttoken' => \OCP\Util::callRegister(), ]; return new TemplateResponse( diff --git a/third_party/astrolabe/src/components/admin/AdminSettings.vue b/third_party/astrolabe/src/components/admin/AdminSettings.vue index 53db507..396bc4b 100644 --- a/third_party/astrolabe/src/components/admin/AdminSettings.vue +++ b/third_party/astrolabe/src/components/admin/AdminSettings.vue @@ -154,7 +154,7 @@

@@ -164,7 +164,7 @@

diff --git a/third_party/astrolabe/templates/settings/admin.php b/third_party/astrolabe/templates/settings/admin.php index da80fd4..cbe1e79 100644 --- a/third_party/astrolabe/templates/settings/admin.php +++ b/third_party/astrolabe/templates/settings/admin.php @@ -7,7 +7,7 @@ */ script('astrolabe', 'astrolabe-adminSettings'); -style('astrolabe', 'astrolabe-adminSettings'); +style('astrolabe', 'astrolabe-main'); // All CSS bundled into main ?>

diff --git a/third_party/astrolabe/templates/settings/personal.php b/third_party/astrolabe/templates/settings/personal.php index f4738be..61db152 100644 --- a/third_party/astrolabe/templates/settings/personal.php +++ b/third_party/astrolabe/templates/settings/personal.php @@ -18,7 +18,7 @@ $urlGenerator = \OC::$server->getURLGenerator(); script('astrolabe', 'astrolabe-personalSettings'); -style('astrolabe', 'astrolabe-personalSettings'); +style('astrolabe', 'astrolabe-main'); // All CSS bundled into main ?>
@@ -43,7 +43,17 @@ style('astrolabe', 'astrolabe-personalSettings');

t('Background Sync Access')); ?>

- + +

@@ -110,54 +120,129 @@ style('astrolabe', 'astrolabe-personalSettings');

-

- t('Enable background sync to allow the MCP server to access your Nextcloud data for background operations like content indexing.')); ?> -

- -
-

t('Option 1: OAuth Refresh Token (Recommended for Future)')); ?>

+ +

- t('When Nextcloud fully supports OAuth for app APIs. Currently waiting for upstream PR to merge.')); ?> -

- - - t('Authorize via OAuth')); ?> - -
- -
-

t('Option 2: App Password (Works Today - Recommended)')); ?>

-

- t('Generate an app password in Security settings and provide it below. This is the recommended interim solution.')); ?> + t('To use semantic search, you need to complete two setup steps:')); ?>

-
-

t('Step 1:')); ?> - - t('Generate app password in Security settings')); ?> + +

+ + +
+

+ + t('Complete')); ?> + + t('Required')); ?> + + t('Step 2: Enable Background Indexing')); ?> +

+

+ t('Provide an app password to allow background indexing of your content.')); ?> +

+ +
+

+ + t('Generate app password in Security settings')); ?> + +

+ +
+ +
+ + +
+

+ t('The app password will be validated and securely encrypted before storage.')); ?> +

+
+
+ +

t('Background indexing enabled.')); ?>

+ +
+ + + +

+ t('Enable background sync to allow the MCP server to access your Nextcloud data for background operations like content indexing.')); ?> +

+ +
+

t('Option 1: OAuth Refresh Token (Recommended for Future)')); ?>

+

+ t('When Nextcloud fully supports OAuth for app APIs. Currently waiting for upstream PR to merge.')); ?> +

+ + + t('Authorize via OAuth')); ?> + +
+ +
+

t('Option 2: App Password (Works Today - Recommended)')); ?>

+

+ t('Generate an app password in Security settings and provide it below. This is the recommended interim solution.')); ?>

-

t('Step 2:')); ?> t('Enter the app password below:')); ?>

- -
- -
- - -
-

- t('The app password will be validated and securely encrypted before storage.')); ?> +

+

t('Step 1:')); ?> + + t('Generate app password in Security settings')); ?> +

- + +

t('Step 2:')); ?> t('Enter the app password below:')); ?>

+ +
+ +
+ + +
+

+ t('The app password will be validated and securely encrypted before storage.')); ?> +

+
+
-
+