Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eceefdacc | |||
| b147814cc4 | |||
| 5a58c81626 | |||
| 1cc460b0d8 | |||
| 104a2ec9e3 | |||
| e87ae56041 | |||
| c95459234b | |||
| f16f852b23 | |||
| b93d7bd19b | |||
| 9a69cef815 | |||
| 2424afbdda | |||
| fdbf88831a |
@@ -5,6 +5,15 @@ 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/),
|
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/).
|
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)
|
## v0.61.3 (2026-01-15)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.commitizen]
|
[tool.commitizen]
|
||||||
name = "cz_conventional_commits"
|
name = "cz_conventional_commits"
|
||||||
version = "0.57.2"
|
version = "0.57.4"
|
||||||
tag_format = "nextcloud-mcp-server-$version"
|
tag_format = "nextcloud-mcp-server-$version"
|
||||||
version_scheme = "semver"
|
version_scheme = "semver"
|
||||||
update_changelog_on_bump = true
|
update_changelog_on_bump = true
|
||||||
|
|||||||
@@ -14,6 +14,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Configurable resource limits
|
- Configurable resource limits
|
||||||
- Grafana dashboard annotations
|
- Grafana dashboard annotations
|
||||||
|
|
||||||
|
## nextcloud-mcp-server-0.57.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
|
||||||
|
|
||||||
|
## nextcloud-mcp-server-0.57.3 (2026-01-15)
|
||||||
|
|
||||||
## nextcloud-mcp-server-0.57.2 (2026-01-15)
|
## nextcloud-mcp-server-0.57.2 (2026-01-15)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
|||||||
name: nextcloud-mcp-server
|
name: nextcloud-mcp-server
|
||||||
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
|
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
|
||||||
type: application
|
type: application
|
||||||
version: 0.57.2
|
version: 0.57.4
|
||||||
appVersion: "0.61.3"
|
appVersion: "0.61.4"
|
||||||
keywords:
|
keywords:
|
||||||
- nextcloud
|
- nextcloud
|
||||||
- mcp
|
- mcp
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: docker.io/library/nextcloud:32.0.3@sha256:1a75afcd53b38aa72205ab38a66121ed9f9e8c99f4e70b0dccc858e60ad57b7d
|
image: docker.io/library/nextcloud:32.0.3@sha256:b8658180f826242849b3f65c42a90529b582f9824bde0b7cc93fd08077bc4e14
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- 127.0.0.1:8080:80
|
- 127.0.0.1:8080:80
|
||||||
|
|||||||
@@ -223,6 +223,55 @@ NEXTCLOUD_OIDC_CLIENT_SECRET=<client-secret>
|
|||||||
| Token Storage | None | Refresh tokens only | All tokens |
|
| Token Storage | None | Refresh tokens only | All tokens |
|
||||||
| Deployment Complexity | Low | Medium | High |
|
| 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
|
### See Also
|
||||||
- [OAuth Architecture](oauth-architecture.md) - Progressive Consent (Flow 2) details
|
- [OAuth Architecture](oauth-architecture.md) - Progressive Consent (Flow 2) details
|
||||||
- [Configuration](configuration.md#enable_offline_access) - Hybrid mode configuration
|
- [Configuration](configuration.md#enable_offline_access) - Hybrid mode configuration
|
||||||
|
|||||||
@@ -387,8 +387,13 @@ async def get_server_status(request: Request) -> JSONResponse:
|
|||||||
if mode == AuthMode.MULTI_USER_BASIC:
|
if mode == AuthMode.MULTI_USER_BASIC:
|
||||||
response_data["supports_app_passwords"] = settings.enable_offline_access
|
response_data["supports_app_passwords"] = settings.enable_offline_access
|
||||||
|
|
||||||
# Include OIDC configuration if in OAuth mode
|
# Include OIDC configuration if OAuth is available
|
||||||
if auth_mode == "oauth":
|
# 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
|
# Provide IdP discovery information for NC PHP app
|
||||||
oidc_config = {}
|
oidc_config = {}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nextcloud-mcp-server"
|
name = "nextcloud-mcp-server"
|
||||||
version = "0.61.3"
|
version = "0.61.4"
|
||||||
description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data"
|
description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Chris Coutinho", email = "chris@coutinho.io"}
|
{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
|
||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[tool.commitizen]
|
[tool.commitizen]
|
||||||
name = "cz_conventional_commits"
|
name = "cz_conventional_commits"
|
||||||
version = "0.8.0"
|
version = "0.8.1"
|
||||||
tag_format = "astrolabe-v$version"
|
tag_format = "astrolabe-v$version"
|
||||||
version_scheme = "semver"
|
version_scheme = "semver"
|
||||||
update_changelog_on_bump = true
|
update_changelog_on_bump = true
|
||||||
|
|||||||
Vendored
+8
@@ -25,6 +25,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Requires external MCP server deployment
|
- Requires external MCP server deployment
|
||||||
- See documentation for setup: https://github.com/cbcoutinho/nextcloud-mcp-server
|
- 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)
|
## astrolabe-v0.8.0 (2026-01-15)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|||||||
+2
-2
@@ -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.
|
See [documentation](https://github.com/cbcoutinho/nextcloud-mcp-server) for configuration details.
|
||||||
]]></description>
|
]]></description>
|
||||||
<version>0.8.0</version>
|
<version>0.8.1</version>
|
||||||
<licence>agpl</licence>
|
<licence>agpl</licence>
|
||||||
<author homepage="https://github.com/cbcoutinho">Chris Coutinho</author>
|
<author homepage="https://github.com/cbcoutinho">Chris Coutinho</author>
|
||||||
<namespace>Astrolabe</namespace>
|
<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/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>
|
<screenshot>https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/third_party/astrolabe/screenshots/03-chunk-viewer-open.png?raw=1</screenshot>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<nextcloud min-version="30" max-version="32"/>
|
<nextcloud min-version="31" max-version="32"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<settings>
|
<settings>
|
||||||
<personal>OCA\Astrolabe\Settings\Personal</personal>
|
<personal>OCA\Astrolabe\Settings\Personal</personal>
|
||||||
|
|||||||
+13
-15
@@ -38,25 +38,23 @@ class IdpTokenRefresher {
|
|||||||
/**
|
/**
|
||||||
* Get Nextcloud base URL for constructing internal OIDC endpoint URLs.
|
* 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 {
|
private function getNextcloudBaseUrl(): string {
|
||||||
// Prefer explicit CLI URL override
|
// Check for overwrite.cli.url (used in non-containerized deployments)
|
||||||
$baseUrl = $this->config->getSystemValue('overwrite.cli.url', '');
|
$cliUrl = $this->config->getSystemValue('overwrite.cli.url', '');
|
||||||
|
if (!empty($cliUrl)) {
|
||||||
if (!empty($baseUrl)) {
|
return rtrim($cliUrl, '/');
|
||||||
return rtrim($baseUrl, '/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to first trusted domain with protocol
|
// Default: container environment with web server on localhost:80
|
||||||
$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');
|
|
||||||
return 'http://localhost';
|
return 'http://localhost';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+47
-53
@@ -79,60 +79,46 @@ class Personal implements ISettings {
|
|||||||
// Check if user has MCP OAuth token
|
// Check if user has MCP OAuth token
|
||||||
$token = $this->tokenStorage->getUserToken($userId);
|
$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) {
|
if ($authMode === 'multi_user_basic' && $supportsAppPasswords) {
|
||||||
// Check if user has already provided an app password
|
// Check both credentials
|
||||||
$hasBackgroundAccess = $this->tokenStorage->hasBackgroundSyncAccess($userId);
|
$hasOAuthToken = ($token !== null && !$this->tokenStorage->isExpired($token));
|
||||||
|
$hasAppPassword = $this->tokenStorage->hasBackgroundSyncAccess($userId);
|
||||||
|
$backgroundSyncType = $this->tokenStorage->getBackgroundSyncType($userId);
|
||||||
|
$backgroundSyncProvisionedAt = $this->tokenStorage->getBackgroundSyncProvisionedAt($userId);
|
||||||
|
|
||||||
if (!$hasBackgroundAccess) {
|
// OAuth URL for Astrolabe's own OAuth controller (NOT MCP server's browser OAuth)
|
||||||
// No app password yet - show app password entry form
|
$oauthUrl = $this->urlGenerator->linkToRoute('astrolabe.oauth.initiateOAuth');
|
||||||
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);
|
|
||||||
|
|
||||||
$parameters = [
|
// Consolidated template parameters (camelCase convention)
|
||||||
'userId' => $userId,
|
$parameters = [
|
||||||
'serverStatus' => $serverStatus,
|
'userId' => $userId,
|
||||||
'session' => null, // No user session for app passwords
|
'serverUrl' => $this->client->getPublicServerUrl(),
|
||||||
'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false,
|
'serverStatus' => $serverStatus,
|
||||||
'backgroundAccessGranted' => true, // App password grants background access
|
'authMode' => $authMode,
|
||||||
'serverUrl' => $this->client->getPublicServerUrl(),
|
'supportsAppPasswords' => $supportsAppPasswords,
|
||||||
'hasToken' => false, // No OAuth token
|
'session' => null, // No session in hybrid mode
|
||||||
'hasBackgroundAccess' => true,
|
'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false,
|
||||||
'backgroundSyncType' => $backgroundSyncType,
|
// OAuth token status (for Astrolabe→MCP API calls)
|
||||||
'backgroundSyncProvisionedAt' => $backgroundSyncProvisionedAt,
|
'hasOAuthToken' => $hasOAuthToken,
|
||||||
'authMode' => $authMode,
|
'oauthUrl' => $oauthUrl,
|
||||||
'supportsAppPasswords' => $supportsAppPasswords,
|
// App password status (for MCP→Nextcloud background sync)
|
||||||
'requesttoken' => \OCP\Util::callRegister(),
|
'hasBackgroundAccess' => $hasAppPassword,
|
||||||
];
|
'backgroundAccessGranted' => $hasAppPassword, // Legacy alias
|
||||||
|
'backgroundSyncType' => $backgroundSyncType,
|
||||||
|
'backgroundSyncProvisionedAt' => $backgroundSyncProvisionedAt,
|
||||||
|
'requesttoken' => \OCP\Util::callRegister(),
|
||||||
|
];
|
||||||
|
|
||||||
return new TemplateResponse(
|
return new TemplateResponse(
|
||||||
Application::APP_ID,
|
Application::APP_ID,
|
||||||
'settings/personal',
|
'settings/personal',
|
||||||
$parameters,
|
$parameters,
|
||||||
TemplateResponse::RENDER_AS_BLANK
|
TemplateResponse::RENDER_AS_BLANK
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// For OAuth modes, if no token or token is expired, show OAuth authorization UI
|
// For OAuth modes, if no token or token is expired, show OAuth authorization UI
|
||||||
elseif (!$token || $this->tokenStorage->isExpired($token)) {
|
elseif (!$token || $this->tokenStorage->isExpired($token)) {
|
||||||
@@ -198,6 +184,9 @@ class Personal implements ISettings {
|
|||||||
$backgroundSyncType = $this->tokenStorage->getBackgroundSyncType($userId);
|
$backgroundSyncType = $this->tokenStorage->getBackgroundSyncType($userId);
|
||||||
$backgroundSyncProvisionedAt = $this->tokenStorage->getBackgroundSyncProvisionedAt($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)
|
// Provide initial state for Vue.js frontend (if needed)
|
||||||
$this->initialState->provideInitialState('user-data', [
|
$this->initialState->provideInitialState('user-data', [
|
||||||
'userId' => $userId,
|
'userId' => $userId,
|
||||||
@@ -205,17 +194,22 @@ class Personal implements ISettings {
|
|||||||
'session' => $userSession,
|
'session' => $userSession,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Consolidated template parameters (camelCase convention)
|
||||||
$parameters = [
|
$parameters = [
|
||||||
'userId' => $userId,
|
'userId' => $userId,
|
||||||
|
'serverUrl' => $this->client->getPublicServerUrl(),
|
||||||
'serverStatus' => $serverStatus,
|
'serverStatus' => $serverStatus,
|
||||||
'session' => $userSession,
|
'session' => $userSession,
|
||||||
'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false,
|
'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false,
|
||||||
'backgroundAccessGranted' => $userSession['background_access_granted'] ?? false,
|
// OAuth status
|
||||||
'serverUrl' => $this->client->getPublicServerUrl(),
|
'hasOAuthToken' => true,
|
||||||
'hasToken' => true,
|
'oauthUrl' => $oauthUrl,
|
||||||
|
// Background sync status
|
||||||
'hasBackgroundAccess' => $hasBackgroundAccess,
|
'hasBackgroundAccess' => $hasBackgroundAccess,
|
||||||
|
'backgroundAccessGranted' => $userSession['background_access_granted'] ?? false, // Legacy
|
||||||
'backgroundSyncType' => $backgroundSyncType,
|
'backgroundSyncType' => $backgroundSyncType,
|
||||||
'backgroundSyncProvisionedAt' => $backgroundSyncProvisionedAt,
|
'backgroundSyncProvisionedAt' => $backgroundSyncProvisionedAt,
|
||||||
|
'requesttoken' => \OCP\Util::callRegister(),
|
||||||
];
|
];
|
||||||
|
|
||||||
return new TemplateResponse(
|
return new TemplateResponse(
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "astrolabe",
|
"name": "astrolabe",
|
||||||
"version": "0.8.0",
|
"version": "0.8.1",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.0.0",
|
"node": "^22.0.0",
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
<NcSelect
|
<NcSelect
|
||||||
:model-value="selectedAlgorithmOption"
|
:model-value="selectedAlgorithmOption"
|
||||||
:options="algorithmOptions"
|
:options="algorithmOptions"
|
||||||
:label="t('astrolabe', 'Search Algorithm')"
|
:input-label="t('astrolabe', 'Search Algorithm')"
|
||||||
class="form-field"
|
class="form-field"
|
||||||
@update:model-value="settings.algorithm = $event ? $event.id : 'hybrid'" />
|
@update:model-value="settings.algorithm = $event ? $event.id : 'hybrid'" />
|
||||||
<p class="help-text">
|
<p class="help-text">
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
<NcSelect
|
<NcSelect
|
||||||
:model-value="selectedFusionOption"
|
:model-value="selectedFusionOption"
|
||||||
:options="fusionOptions"
|
:options="fusionOptions"
|
||||||
:label="t('astrolabe', 'Fusion Method')"
|
:input-label="t('astrolabe', 'Fusion Method')"
|
||||||
class="form-field"
|
class="form-field"
|
||||||
@update:model-value="settings.fusion = $event ? $event.id : 'rrf'" />
|
@update:model-value="settings.fusion = $event ? $event.id : 'rrf'" />
|
||||||
<p class="help-text">
|
<p class="help-text">
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
script('astrolabe', 'astrolabe-adminSettings');
|
script('astrolabe', 'astrolabe-adminSettings');
|
||||||
style('astrolabe', 'astrolabe-adminSettings');
|
style('astrolabe', 'astrolabe-main'); // All CSS bundled into main
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div id="astrolabe-admin-settings" class="section">
|
<div id="astrolabe-admin-settings" class="section">
|
||||||
|
|||||||
+127
-42
@@ -18,7 +18,7 @@
|
|||||||
$urlGenerator = \OC::$server->getURLGenerator();
|
$urlGenerator = \OC::$server->getURLGenerator();
|
||||||
|
|
||||||
script('astrolabe', 'astrolabe-personalSettings');
|
script('astrolabe', 'astrolabe-personalSettings');
|
||||||
style('astrolabe', 'astrolabe-personalSettings');
|
style('astrolabe', 'astrolabe-main'); // All CSS bundled into main
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@@ -43,7 +43,17 @@ style('astrolabe', 'astrolabe-personalSettings');
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<h2><?php p($l->t('Background Sync Access')); ?></h2>
|
<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 -->
|
<!-- Already configured -->
|
||||||
<div class="mcp-background-status">
|
<div class="mcp-background-status">
|
||||||
<p>
|
<p>
|
||||||
@@ -110,54 +120,129 @@ style('astrolabe', 'astrolabe-personalSettings');
|
|||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<!-- Not configured - show provisioning options -->
|
<!-- Not configured - show provisioning options -->
|
||||||
<p class="mcp-help-text">
|
<?php if ($isHybridMode): ?>
|
||||||
<?php p($l->t('Enable background sync to allow the MCP server to access your Nextcloud data for background operations like content indexing.')); ?>
|
<!-- Hybrid mode: User needs BOTH OAuth AND app password -->
|
||||||
</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">
|
<p class="mcp-help-text">
|
||||||
<?php p($l->t('When Nextcloud fully supports OAuth for app APIs. Currently waiting for upstream PR to merge.')); ?>
|
<?php p($l->t('To use semantic search, you need to complete two setup steps:')); ?>
|
||||||
</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.')); ?>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mcp-app-password-steps">
|
<!-- Step 1: OAuth Authorization (for Astrolabe→MCP API calls) -->
|
||||||
<p><strong><?php p($l->t('Step 1:')); ?></strong>
|
<div class="mcp-grant-section">
|
||||||
<a href="<?php p($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'security'])); ?>" target="_blank">
|
<h4>
|
||||||
<?php p($l->t('Generate app password in Security settings')); ?>
|
<?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>
|
</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>
|
||||||
|
|
||||||
<p><strong><?php p($l->t('Step 2:')); ?></strong> <?php p($l->t('Enter the app password below:')); ?></p>
|
<div class="mcp-app-password-steps">
|
||||||
|
<p><strong><?php p($l->t('Step 1:')); ?></strong>
|
||||||
<form method="post" action="<?php p($urlGenerator->linkToRoute('astrolabe.credentials.storeAppPassword')); ?>" id="mcp-app-password-form">
|
<a href="<?php p($urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'security'])); ?>" target="_blank">
|
||||||
<input type="hidden" name="requesttoken" value="<?php p($_['requesttoken']); ?>">
|
<?php p($l->t('Generate app password in Security settings')); ?>
|
||||||
<div class="mcp-input-group">
|
</a>
|
||||||
<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>
|
</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>
|
||||||
</div>
|
<?php endif; ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1988,7 +1988,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nextcloud-mcp-server"
|
name = "nextcloud-mcp-server"
|
||||||
version = "0.61.3"
|
version = "0.61.4"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
|
|||||||
Reference in New Issue
Block a user