Compare commits

...

17 Commits

Author SHA1 Message Date
github-actions[bot] b147814cc4 bump: version 0.61.3 → 0.61.4 2026-01-16 09:54:02 +00:00
Chris Coutinho 5a58c81626 Merge pull request #483 from cbcoutinho/fix/astrolabe-oauth-hybrid-mode
fix(astrolabe): fix OAuth flow for hybrid mode
2026-01-16 10:53:45 +01:00
Chris Coutinho 1cc460b0d8 fix(astrolabe): Address reviewer feedback for hybrid mode
Addresses code review feedback:

Personal.php:
- Consolidate template variables to use camelCase consistently
- Remove duplicate snake_case variables (auth_mode, supports_app_passwords)
- Add oauthUrl to standard OAuth mode parameters (fixes fallback issue)
- Add requesttoken for CSRF protection

personal.php (template):
- Use null coalescing for safe variable access
- Reuse computed $isHybridMode variable instead of duplicate check
- Remove complex fallback URL logic (oauthUrl now always provided)

IdpTokenRefresher.php:
- Use Nextcloud's overwrite.cli.url config when available
- Fall back to http://localhost for container deployments
- Better supports non-containerized environments

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:44:52 +01:00
Chris Coutinho 104a2ec9e3 test: Add unit tests for status endpoint OIDC config
Add unit tests for /api/v1/status endpoint focusing on OIDC config:
- Test hybrid mode (multi_user_basic + enable_offline_access) returns OIDC
- Test pure multi_user_basic mode without offline_access omits OIDC
- Test OAuth mode returns OIDC config
- Test single-user BasicAuth mode omits OIDC config
- Test partial OIDC config (only discovery_url or only issuer)

Also updates docs/authentication.md with Astrolabe hybrid mode setup:
- Two-step credential setup (OAuth + app password)
- Technical details for each credential type
- Request direction table explaining why two credentials needed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 10:43:59 +01:00
Chris Coutinho e87ae56041 fix(astrolabe): Fix NcSelect options and CSS loading
- Use :input-label prop for NcSelect field labels instead of :label
  (the :label prop sets the option label property key, not the visible label)
- Fix CSS loading in admin.php and personal.php templates to use
  astrolabe-main (the bundled CSS file)
- Update minimum Nextcloud version to 31 (required for Vue 3)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 17:21:22 +01:00
Chris Coutinho c95459234b fix(astrolabe): fix OAuth flow and settings UI for hybrid mode
In hybrid mode (multi_user_basic + offline_access), users need BOTH:
- OAuth token for Astrolabe→MCP API calls
- App password for MCP→Nextcloud background sync

Changes:
- Personal.php: Pass correct oauthUrl pointing to Astrolabe's OAuth
  controller instead of MCP server's browser OAuth. Check both OAuth
  token AND app password status in hybrid mode.
- personal.php template: Show two-step workflow UI requiring both
  credentials before showing "Active" status. Each step shows
  completion badges.
- IdpTokenRefresher.php: Use http://localhost for internal token
  refresh requests (consistent with OAuthController). External URLs
  like localhost:8080 don't work from inside the container.

Fixes 401 errors when searching in Astrolabe with hybrid deployment.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 16:14:00 +01:00
Chris Coutinho f16f852b23 fix(api): return OIDC config in hybrid mode for Astrolabe OAuth flow
The /api/v1/status endpoint now returns OIDC configuration (discovery_url,
issuer) when running in hybrid mode (multi_user_basic + offline_access),
not just in pure OAuth mode.

This allows Astrolabe to discover the IdP and complete the OAuth flow
for obtaining tokens to call MCP server management APIs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 16:13:50 +01:00
github-actions[bot] b93d7bd19b bump: version 0.57.2 → 0.57.3 2026-01-15 13:34:11 +00:00
Chris Coutinho 9a69cef815 Merge pull request #474 from cbcoutinho/renovate/docker.io-library-nextcloud-32.0.3
chore(deps): update docker.io/library/nextcloud:32.0.3 docker digest to b865818
2026-01-15 14:33:56 +01:00
github-actions[bot] 2424afbdda bump: version 0.8.0 → 0.8.1 2026-01-15 11:23:42 +00:00
github-actions[bot] 0a987467b5 bump: version 0.57.1 → 0.57.2 2026-01-15 11:23:42 +00:00
github-actions[bot] ab6f7ca0b2 bump: version 0.61.2 → 0.61.3 2026-01-15 11:23:41 +00:00
Chris Coutinho 42fa33d0bf Merge pull request #480 from cbcoutinho/fix/astrolabe-vue3-bindings
fix(astrolabe): update Vue component bindings for Vue 3 compatibility
2026-01-15 12:23:21 +01:00
Chris Coutinho 006a3d95d6 fix(astrolabe): address review feedback for Vue 3 bindings
- Change limit initialization from string '20' to number 20 in App.vue
- Update AdminSettings.vue NcTextField to use v-model instead of legacy
  :value/@update:value bindings
- Update AdminSettings.vue NcSelect components to use :model-value with
  computed getters and @update:model-value for proper object-to-id
  conversion (same pattern as App.vue)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 12:16:08 +01:00
Chris Coutinho cb4e8acd9f fix(astrolabe): update Vue component bindings for Vue 3 compatibility
The astrolabe app was using Vue 2 style bindings that don't work with
@nextcloud/vue 9.x and Vue 3:

- NcTextField: Changed from :value/@update:value to v-model
- NcSelect: Changed from v-model (with computed prop) to
  :model-value/@update:model-value

The legacy :value and @update:value props were being ignored because
@nextcloud/vue 9.x components use modelValue/update:modelValue internally.
This caused the search button to remain disabled and the algorithm
dropdown to be unresponsive.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 12:06:20 +01:00
github-actions[bot] 02418a9531 bump: version 0.57.0 → 0.57.1 2026-01-15 09:00:41 +00:00
renovate-bot-cbcoutinho[bot] fdbf88831a chore(deps): update docker.io/library/nextcloud:32.0.3 docker digest to b865818 2026-01-14 11:11:39 +00:00
20 changed files with 656 additions and 140 deletions
+16
View File
@@ -5,6 +5,22 @@ All notable changes to the Nextcloud MCP Server will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [PEP 440](https://peps.python.org/pep-0440/).
## v0.61.4 (2026-01-16)
### Fix
- **astrolabe**: Address reviewer feedback for hybrid mode
- **astrolabe**: Fix NcSelect options and CSS loading
- **astrolabe**: fix OAuth flow and settings UI for hybrid mode
- **api**: return OIDC config in hybrid mode for Astrolabe OAuth flow
## v0.61.3 (2026-01-15)
### Fix
- **astrolabe**: address review feedback for Vue 3 bindings
- **astrolabe**: update Vue component bindings for Vue 3 compatibility
## v0.61.2 (2026-01-15)
### Fix
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.commitizen]
name = "cz_conventional_commits"
version = "0.57.0"
version = "0.57.3"
tag_format = "nextcloud-mcp-server-$version"
version_scheme = "semver"
update_changelog_on_bump = true
+16
View File
@@ -14,6 +14,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Configurable resource limits
- Grafana dashboard annotations
## nextcloud-mcp-server-0.57.3 (2026-01-15)
## nextcloud-mcp-server-0.57.2 (2026-01-15)
### Fix
- **astrolabe**: address review feedback for Vue 3 bindings
- **astrolabe**: update Vue component bindings for Vue 3 compatibility
## nextcloud-mcp-server-0.57.1 (2026-01-15)
### Fix
- **ci**: bump helm chart version when MCP appVersion changes
- **astrolabe**: define appName and appVersion for @nextcloud/vue
## nextcloud-mcp-server-0.57.0 (2026-01-15)
### Feat
+2 -2
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: nextcloud-mcp-server
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
type: application
version: 0.57.0
appVersion: "0.61.2"
version: 0.57.3
appVersion: "0.61.4"
keywords:
- nextcloud
- mcp
+1 -1
View File
@@ -23,7 +23,7 @@ services:
restart: always
app:
image: docker.io/library/nextcloud:32.0.3@sha256:1a75afcd53b38aa72205ab38a66121ed9f9e8c99f4e70b0dccc858e60ad57b7d
image: docker.io/library/nextcloud:32.0.3@sha256:b8658180f826242849b3f65c42a90529b582f9824bde0b7cc93fd08077bc4e14
restart: always
ports:
- 127.0.0.1:8080:80
+49
View File
@@ -223,6 +223,55 @@ NEXTCLOUD_OIDC_CLIENT_SECRET=<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
+7 -2
View File
@@ -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 = {}
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "nextcloud-mcp-server"
version = "0.61.2"
version = "0.61.4"
description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data"
authors = [
{name = "Chris Coutinho", email = "chris@coutinho.io"}
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.commitizen]
name = "cz_conventional_commits"
version = "0.8.0"
version = "0.8.1"
tag_format = "astrolabe-v$version"
version_scheme = "semver"
update_changelog_on_bump = true
+8
View File
@@ -25,6 +25,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Requires external MCP server deployment
- See documentation for setup: https://github.com/cbcoutinho/nextcloud-mcp-server
## astrolabe-v0.8.1 (2026-01-15)
### Fix
- **astrolabe**: address review feedback for Vue 3 bindings
- **astrolabe**: update Vue component bindings for Vue 3 compatibility
- **ci**: bump helm chart version when MCP appVersion changes
## astrolabe-v0.8.0 (2026-01-15)
### Feat
+2 -2
View File
@@ -29,7 +29,7 @@ Astrolabe connects to a semantic search service that understands the meaning of
See [documentation](https://github.com/cbcoutinho/nextcloud-mcp-server) for configuration details.
]]></description>
<version>0.8.0</version>
<version>0.8.1</version>
<licence>agpl</licence>
<author homepage="https://github.com/cbcoutinho">Chris Coutinho</author>
<namespace>Astrolabe</namespace>
@@ -40,7 +40,7 @@ See [documentation](https://github.com/cbcoutinho/nextcloud-mcp-server) for conf
<screenshot>https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/third_party/astrolabe/screenshots/01-unified-search-astrolabe.png?raw=1</screenshot>
<screenshot>https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/third_party/astrolabe/screenshots/03-chunk-viewer-open.png?raw=1</screenshot>
<dependencies>
<nextcloud min-version="30" max-version="32"/>
<nextcloud min-version="31" max-version="32"/>
</dependencies>
<settings>
<personal>OCA\Astrolabe\Settings\Personal</personal>
+13 -15
View File
@@ -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';
}
+47 -53
View File
@@ -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(
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "astrolabe",
"version": "0.8.0",
"version": "0.8.1",
"license": "AGPL-3.0-or-later",
"engines": {
"node": "^22.0.0",
+6 -8
View File
@@ -48,19 +48,18 @@
<div class="mcp-search-card">
<div class="mcp-search-row">
<NcTextField
:value="query"
v-model="query"
:label="t('astrolabe', 'Search query')"
:placeholder="t('astrolabe', 'Enter your search query...')"
class="mcp-search-input"
@update:value="query = $event"
@keyup.enter="performSearch" />
<NcSelect
v-model="selectedAlgorithmOption"
:model-value="selectedAlgorithmOption"
:options="algorithmOptions"
:placeholder="t('astrolabe', 'Algorithm')"
class="mcp-algorithm-select"
@input="algorithm = $event ? $event.id : 'hybrid'" />
@update:model-value="algorithm = $event ? $event.id : 'hybrid'" />
<NcButton
type="primary"
@@ -105,11 +104,10 @@
<div class="mcp-option-group">
<label>{{ t('astrolabe', 'Result Limit') }}</label>
<NcTextField
:value="limit"
v-model="limit"
type="number"
:min="1"
:max="100"
@update:value="limit = Number($event)" />
:max="100" />
</div>
<div class="mcp-option-group">
@@ -445,7 +443,7 @@ export default {
algorithm: 'hybrid',
showAdvanced: false,
selectedDocTypes: [],
limit: '20',
limit: 20,
scoreThreshold: 0,
loading: false,
error: null,
@@ -152,19 +152,21 @@
<div class="settings-form">
<NcSelect
v-model="settings.algorithm"
:model-value="selectedAlgorithmOption"
:options="algorithmOptions"
:label="t('astrolabe', 'Search Algorithm')"
class="form-field" />
:input-label="t('astrolabe', 'Search Algorithm')"
class="form-field"
@update:model-value="settings.algorithm = $event ? $event.id : 'hybrid'" />
<p class="help-text">
{{ t('astrolabe', 'Hybrid combines semantic understanding with keyword matching. Semantic finds conceptually similar content. BM25 matches exact keywords.') }}
</p>
<NcSelect
v-model="settings.fusion"
:model-value="selectedFusionOption"
:options="fusionOptions"
:label="t('astrolabe', 'Fusion Method')"
class="form-field" />
:input-label="t('astrolabe', 'Fusion Method')"
class="form-field"
@update:model-value="settings.fusion = $event ? $event.id : 'rrf'" />
<p class="help-text">
{{ t('astrolabe', 'Only applies to hybrid search. RRF balances results well for most queries. DBSF may work better when keyword matches are over/under-weighted.') }}
</p>
@@ -184,14 +186,13 @@
</div>
<NcTextField
:value="settings.limit"
v-model="settings.limit"
:label="t('astrolabe', 'Maximum Results')"
type="number"
:min="5"
:max="100"
:step="5"
class="form-field"
@update:value="settings.limit = Number($event)" />
class="form-field" />
<p class="help-text">
{{ t('astrolabe', 'Maximum number of results to return per search query (5-100).') }}
</p>
@@ -276,6 +277,15 @@ const fusionOptions = computed(() => [
{ id: 'dbsf', label: t('astrolabe', 'DBSF - Distribution-Based Score Fusion') },
])
// Computed properties for NcSelect (converts between stored ID and option object)
const selectedAlgorithmOption = computed(() =>
algorithmOptions.value.find(opt => opt.id === settings.value.algorithm) || algorithmOptions.value[0],
)
const selectedFusionOption = computed(() =>
fusionOptions.value.find(opt => opt.id === settings.value.fusion) || fusionOptions.value[0],
)
// Methods
async function loadServerStatus() {
loading.value = true
+1 -1
View File
@@ -7,7 +7,7 @@
*/
script('astrolabe', 'astrolabe-adminSettings');
style('astrolabe', 'astrolabe-adminSettings');
style('astrolabe', 'astrolabe-main'); // All CSS bundled into main
?>
<div id="astrolabe-admin-settings" class="section">
+127 -42
View File
@@ -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
?>
<div class="section">
@@ -43,7 +43,17 @@ style('astrolabe', 'astrolabe-personalSettings');
<div class="section">
<h2><?php p($l->t('Background Sync Access')); ?></h2>
<?php if ($_['hasBackgroundAccess'] || $_['backgroundAccessGranted']): ?>
<?php
// Determine if hybrid mode (multi_user_basic + app passwords)
// In hybrid mode, user needs BOTH OAuth AND app password to be "fully configured"
$isHybridMode = ($_['authMode'] ?? '') === 'multi_user_basic' && !empty($_['supportsAppPasswords']);
$hasOAuthToken = !empty($_['hasOAuthToken']);
$hasBackgroundAccess = !empty($_['hasBackgroundAccess']) || !empty($_['backgroundAccessGranted']);
// In hybrid mode: both credentials required; otherwise just background access
$isFullyConfigured = $isHybridMode ? ($hasOAuthToken && $hasBackgroundAccess) : $hasBackgroundAccess;
?>
<?php if ($isFullyConfigured): ?>
<!-- Already configured -->
<div class="mcp-background-status">
<p>
@@ -110,54 +120,129 @@ style('astrolabe', 'astrolabe-personalSettings');
</div>
<?php else: ?>
<!-- Not configured - show provisioning options -->
<p class="mcp-help-text">
<?php p($l->t('Enable background sync to allow the MCP server to access your Nextcloud data for background operations like content indexing.')); ?>
</p>
<div class="mcp-grant-section">
<h4><?php p($l->t('Option 1: OAuth Refresh Token (Recommended for Future)')); ?></h4>
<?php if ($isHybridMode): ?>
<!-- Hybrid mode: User needs BOTH OAuth AND app password -->
<p class="mcp-help-text">
<?php p($l->t('When Nextcloud fully supports OAuth for app APIs. Currently waiting for upstream PR to merge.')); ?>
</p>
<a href="<?php p($_['serverUrl']); ?>/oauth/login?next=<?php p(urlencode($urlGenerator->getAbsoluteURL($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'astrolabe'])))); ?>" class="button">
<span class="icon icon-confirm"></span>
<?php p($l->t('Authorize via OAuth')); ?>
</a>
</div>
<div class="mcp-grant-section">
<h4><?php p($l->t('Option 2: App Password (Works Today - Recommended)')); ?></h4>
<p class="mcp-help-text">
<?php p($l->t('Generate an app password in Security settings and provide it below. This is the recommended interim solution.')); ?>
<?php p($l->t('To use semantic search, you need to complete two setup steps:')); ?>
</p>
<div class="mcp-app-password-steps">
<p><strong><?php p($l->t('Step 1:')); ?></strong>
<a href="<?php p($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'security'])); ?>" target="_blank">
<?php p($l->t('Generate app password in Security settings')); ?>
<!-- Step 1: OAuth Authorization (for Astrolabe→MCP API calls) -->
<div class="mcp-grant-section">
<h4>
<?php if (!empty($_['hasOAuthToken'])): ?>
<span class="badge badge-success"><span class="icon icon-checkmark-white"></span> <?php p($l->t('Complete')); ?></span>
<?php else: ?>
<span class="badge badge-warning"><?php p($l->t('Required')); ?></span>
<?php endif; ?>
<?php p($l->t('Step 1: Authorize Search Access')); ?>
</h4>
<p class="mcp-help-text">
<?php p($l->t('Authorize Astrolabe to perform searches on your behalf.')); ?>
</p>
<?php if (empty($_['hasOAuthToken'])): ?>
<a href="<?php p($_['oauthUrl']); ?>" class="button primary">
<span class="icon icon-confirm"></span>
<?php p($l->t('Authorize')); ?>
</a>
<?php else: ?>
<p><span class="icon icon-checkmark"></span> <?php p($l->t('Search access authorized.')); ?></p>
<?php endif; ?>
</div>
<!-- Step 2: App Password (for MCP→Nextcloud background sync) -->
<div class="mcp-grant-section">
<h4>
<?php if (!empty($_['hasBackgroundAccess'])): ?>
<span class="badge badge-success"><span class="icon icon-checkmark-white"></span> <?php p($l->t('Complete')); ?></span>
<?php else: ?>
<span class="badge badge-warning"><?php p($l->t('Required')); ?></span>
<?php endif; ?>
<?php p($l->t('Step 2: Enable Background Indexing')); ?>
</h4>
<p class="mcp-help-text">
<?php p($l->t('Provide an app password to allow background indexing of your content.')); ?>
</p>
<?php if (empty($_['hasBackgroundAccess'])): ?>
<div class="mcp-app-password-steps">
<p>
<a href="<?php p($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'security'])); ?>" target="_blank">
<?php p($l->t('Generate app password in Security settings')); ?>
</a>
</p>
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.credentials.storeAppPassword')); ?>" id="mcp-app-password-form">
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
<div class="mcp-input-group">
<input type="password" name="appPassword" id="mcp-app-password-input"
placeholder="xxxxx-xxxxx-xxxxx-xxxxx-xxxxx"
pattern="[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}"
required>
<button type="submit" class="button primary" id="mcp-save-app-password-button">
<span class="icon icon-checkmark"></span>
<?php p($l->t('Save')); ?>
</button>
</div>
<p class="mcp-help-text">
<?php p($l->t('The app password will be validated and securely encrypted before storage.')); ?>
</p>
</form>
</div>
<?php else: ?>
<p><span class="icon icon-checkmark"></span> <?php p($l->t('Background indexing enabled.')); ?></p>
<?php endif; ?>
</div>
<?php else: ?>
<!-- Standard OAuth or BasicAuth mode -->
<p class="mcp-help-text">
<?php p($l->t('Enable background sync to allow the MCP server to access your Nextcloud data for background operations like content indexing.')); ?>
</p>
<div class="mcp-grant-section">
<h4><?php p($l->t('Option 1: OAuth Refresh Token (Recommended for Future)')); ?></h4>
<p class="mcp-help-text">
<?php p($l->t('When Nextcloud fully supports OAuth for app APIs. Currently waiting for upstream PR to merge.')); ?>
</p>
<a href="<?php p($_['oauthUrl']); ?>" class="button">
<span class="icon icon-confirm"></span>
<?php p($l->t('Authorize via OAuth')); ?>
</a>
</div>
<div class="mcp-grant-section">
<h4><?php p($l->t('Option 2: App Password (Works Today - Recommended)')); ?></h4>
<p class="mcp-help-text">
<?php p($l->t('Generate an app password in Security settings and provide it below. This is the recommended interim solution.')); ?>
</p>
<p><strong><?php p($l->t('Step 2:')); ?></strong> <?php p($l->t('Enter the app password below:')); ?></p>
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.credentials.storeAppPassword')); ?>" id="mcp-app-password-form">
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
<div class="mcp-input-group">
<input type="password" name="appPassword" id="mcp-app-password-input"
placeholder="xxxxx-xxxxx-xxxxx-xxxxx-xxxxx"
pattern="[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}"
required>
<button type="submit" class="button primary" id="mcp-save-app-password-button">
<span class="icon icon-checkmark"></span>
<?php p($l->t('Save')); ?>
</button>
</div>
<p class="mcp-help-text">
<?php p($l->t('The app password will be validated and securely encrypted before storage.')); ?>
<div class="mcp-app-password-steps">
<p><strong><?php p($l->t('Step 1:')); ?></strong>
<a href="<?php p($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'security'])); ?>" target="_blank">
<?php p($l->t('Generate app password in Security settings')); ?>
</a>
</p>
</form>
<p><strong><?php p($l->t('Step 2:')); ?></strong> <?php p($l->t('Enter the app password below:')); ?></p>
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.credentials.storeAppPassword')); ?>" id="mcp-app-password-form">
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
<div class="mcp-input-group">
<input type="password" name="appPassword" id="mcp-app-password-input"
placeholder="xxxxx-xxxxx-xxxxx-xxxxx-xxxxx"
pattern="[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}-[a-zA-Z0-9]{5}"
required>
<button type="submit" class="button primary" id="mcp-save-app-password-button">
<span class="icon icon-checkmark"></span>
<?php p($l->t('Save')); ?>
</button>
</div>
<p class="mcp-help-text">
<?php p($l->t('The app password will be validated and securely encrypted before storage.')); ?>
</p>
</form>
</div>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
Generated
+1 -1
View File
@@ -1988,7 +1988,7 @@ wheels = [
[[package]]
name = "nextcloud-mcp-server"
version = "0.61.2"
version = "0.61.4"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },