Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6eceefdacc | |||
| b147814cc4 | |||
| 5a58c81626 | |||
| 1cc460b0d8 | |||
| 104a2ec9e3 | |||
| e87ae56041 | |||
| c95459234b | |||
| f16f852b23 | |||
| b93d7bd19b | |||
| 9a69cef815 | |||
| 2424afbdda | |||
| 0a987467b5 | |||
| ab6f7ca0b2 | |||
| 42fa33d0bf | |||
| 006a3d95d6 | |||
| cb4e8acd9f | |||
| 02418a9531 | |||
| f89151d099 | |||
| dc86386bf8 | |||
| 929c40709a | |||
| a60560256d | |||
| aa583ab973 | |||
| 4103924b83 | |||
| c192bd2ec9 | |||
| 2005d2841f | |||
| c6295b48a5 | |||
| 7444c73a5a | |||
| cf0781d2fe | |||
| 6681cd0603 | |||
| c305a549d3 | |||
| 1f1dd94598 | |||
| 585ed46f2d | |||
| dbbbab5320 | |||
| e5844b3da8 | |||
| fdbf88831a | |||
| 7465e962d4 | |||
| 99fe764c5e | |||
| 46f896b526 | |||
| a61572e8ef | |||
| a474996df4 | |||
| 5d6dd5ad38 | |||
| 21e4d3effd | |||
| 817df43af1 | |||
| 906b9d892c | |||
| 534723c9f6 | |||
| 1d5832ed3a | |||
| 844bd589e0 | |||
| 127af15623 | |||
| ff5fc5d5b2 | |||
| 158865d99f | |||
| 94674eca27 | |||
| a8b5d6e701 | |||
| e0675b2127 | |||
| 86582bdb8f | |||
| dc8009a785 | |||
| b5e658e1ff | |||
| 6a19c2d136 | |||
| 99e359ffbf | |||
| f16f4e8cb5 | |||
| 8597f2a272 | |||
| 11f67e2bc4 | |||
| 2e49a16e49 | |||
| 713fddeaa5 | |||
| 0dfefb0516 | |||
| 63d2aeaa43 | |||
| 07f0a7c0dc | |||
| 84bde6d5ed | |||
| 9695f8a6d7 | |||
| a2c410e8d2 | |||
| 271b5f6155 | |||
| ba4f7c1429 | |||
| c763e96596 | |||
| 23e9cbaec5 | |||
| ddd5defa40 | |||
| 723dcc524d | |||
| 46eba0a693 | |||
| b61980a623 | |||
| 65cc894e21 | |||
| 700996e100 | |||
| a987643f8e |
@@ -87,21 +87,32 @@ jobs:
|
||||
mcp_commit_count=$(git log "$commit_range" --oneline --grep="^(feat|fix|docs|refactor|perf|test|build|ci|chore)" -E | \
|
||||
{ grep -v "(helm)" || true; } | { grep -v "(astrolabe)" || true; } | wc -l)
|
||||
|
||||
MCP_BUMPED=false
|
||||
if [ "$mcp_commit_count" -gt 0 ]; then
|
||||
echo "Found $mcp_commit_count commits for MCP server since $last_mcp_tag"
|
||||
echo "Bumping MCP server version..."
|
||||
./scripts/bump-mcp.sh
|
||||
BUMPED_COMPONENTS="$BUMPED_COMPONENTS mcp"
|
||||
MCP_BUMPED=true
|
||||
else
|
||||
echo "No commits found for MCP server since $last_mcp_tag"
|
||||
fi
|
||||
|
||||
# Bump Helm chart (scope: helm)
|
||||
# Bump Helm chart (scope: helm OR when MCP appVersion changes)
|
||||
echo "Checking Helm chart for version bump..."
|
||||
HELM_HAS_COMMITS=false
|
||||
if has_commits_since_tag "nextcloud-mcp-server-" "(feat|fix|docs|refactor|perf|test|build|ci|chore)\(helm\)(!)?:"; then
|
||||
echo "Bumping Helm chart version..."
|
||||
HELM_HAS_COMMITS=true
|
||||
fi
|
||||
|
||||
if [ "$HELM_HAS_COMMITS" = true ]; then
|
||||
echo "Bumping Helm chart version (helm-scoped commits)..."
|
||||
./scripts/bump-helm.sh
|
||||
BUMPED_COMPONENTS="$BUMPED_COMPONENTS helm"
|
||||
elif [ "$MCP_BUMPED" = true ]; then
|
||||
echo "Bumping Helm chart version (appVersion changed)..."
|
||||
./scripts/bump-helm.sh --increment PATCH
|
||||
BUMPED_COMPONENTS="$BUMPED_COMPONENTS helm"
|
||||
fi
|
||||
|
||||
# Bump Astrolabe (scope: astrolabe)
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@7145c3e0510bcdbdd29f67cc4a8c1958f1acfa2f # v1
|
||||
uses: anthropics/claude-code-action@1b8ee3b94104046d71fde52ec3557651ad8c0d71 # v1.0.29
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
prompt: |
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@7145c3e0510bcdbdd29f67cc4a8c1958f1acfa2f # v1
|
||||
uses: anthropics/claude-code-action@1b8ee3b94104046d71fde52ec3557651ad8c0d71 # v1.0.29
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Run docker compose with vector sync
|
||||
uses: hoverkraft-tech/compose-action@248470ecc5ed40d8ed3d4480d8260d77179ef579 # v2.4.2
|
||||
uses: hoverkraft-tech/compose-action@05da55b2bb8a5a759d1c4732095044bd9018c050 # v2.4.3
|
||||
with:
|
||||
compose-file: |
|
||||
./docker-compose.yml
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
VECTOR_SYNC_SCAN_INTERVAL: "5"
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
|
||||
- name: Wait for Nextcloud to be ready
|
||||
run: |
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- name: Install Python 3.11
|
||||
run: uv python install 3.11
|
||||
- name: Build
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
- name: Check format
|
||||
run: |
|
||||
uv run --frozen ruff format --diff
|
||||
@@ -66,14 +66,14 @@ jobs:
|
||||
|
||||
|
||||
- name: Run docker compose
|
||||
uses: hoverkraft-tech/compose-action@248470ecc5ed40d8ed3d4480d8260d77179ef579 # v2.4.2
|
||||
uses: hoverkraft-tech/compose-action@05da55b2bb8a5a759d1c4732095044bd9018c050 # v2.4.3
|
||||
with:
|
||||
compose-file: "./docker-compose.yml"
|
||||
#compose-flags: "--profile qdrant"
|
||||
up-flags: "--build"
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
||||
|
||||
- name: Install Playwright dependencies
|
||||
run: |
|
||||
|
||||
@@ -5,6 +5,56 @@ 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
|
||||
|
||||
- **ci**: bump helm chart version when MCP appVersion changes
|
||||
|
||||
## v0.61.1 (2026-01-15)
|
||||
|
||||
### Fix
|
||||
|
||||
- **astrolabe**: define appName and appVersion for @nextcloud/vue
|
||||
|
||||
## v0.61.0 (2026-01-14)
|
||||
|
||||
### Feat
|
||||
|
||||
- Add rate limiting and extract helpers for app password endpoints
|
||||
|
||||
### Fix
|
||||
|
||||
- Add missing annotations for deck remove/unassign operations
|
||||
- **auth**: Store app passwords locally for multi-user BasicAuth background sync
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use get_settings() for vector sync enabled check
|
||||
- Extract storage helper and improve PHP error handling
|
||||
|
||||
## v0.60.4 (2026-01-12)
|
||||
|
||||
### Fix
|
||||
|
||||
- **deck**: use correct endpoint for reorder_card to fix cross-stack moves
|
||||
|
||||
## v0.60.3 (2025-12-31)
|
||||
|
||||
### Fix
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
FROM docker.io/library/python:3.12-slim-trixie@sha256:fa48eefe2146644c2308b909d6bb7651a768178f84fc9550dcd495e4d6d84d01
|
||||
FROM docker.io/library/python:3.12-slim-trixie@sha256:d75c4b6cdd039ae966a34cd3ccab9e0e5f7299280ad76fe1744882d86eedce0b
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.18@sha256:5713fa8217f92b80223bc83aac7db36ec80a84437dbc0d04bbc659cae030d8c9 /uv /uvx /bin/
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.25@sha256:13e233d08517abdafac4ead26c16d881cd77504a2c40c38c905cf3a0d70131a6 /uv /uvx /bin/
|
||||
|
||||
# Install dependencies
|
||||
# 1. git (required for caldav dependency from git)
|
||||
|
||||
+2
-2
@@ -12,12 +12,12 @@
|
||||
# - Per-session app password authentication
|
||||
# - Multi-user support via Smithery session config
|
||||
|
||||
FROM docker.io/library/python:3.12-slim-trixie@sha256:fa48eefe2146644c2308b909d6bb7651a768178f84fc9550dcd495e4d6d84d01
|
||||
FROM docker.io/library/python:3.12-slim-trixie@sha256:d75c4b6cdd039ae966a34cd3ccab9e0e5f7299280ad76fe1744882d86eedce0b
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install uv for fast dependency management
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.18@sha256:5713fa8217f92b80223bc83aac7db36ec80a84437dbc0d04bbc659cae030d8c9 /uv /uvx /bin/
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.9.25@sha256:13e233d08517abdafac4ead26c16d881cd77504a2c40c38c905cf3a0d70131a6 /uv /uvx /bin/
|
||||
|
||||
# Install dependencies
|
||||
# 1. git (required for caldav dependency from git)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.commitizen]
|
||||
name = "cz_conventional_commits"
|
||||
version = "0.56.2"
|
||||
version = "0.57.4"
|
||||
tag_format = "nextcloud-mcp-server-$version"
|
||||
version_scheme = "semver"
|
||||
update_changelog_on_bump = true
|
||||
|
||||
@@ -14,6 +14,51 @@ 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.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)
|
||||
|
||||
### 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
|
||||
|
||||
- Add rate limiting and extract helpers for app password endpoints
|
||||
|
||||
### Fix
|
||||
|
||||
- Add missing annotations for deck remove/unassign operations
|
||||
- **auth**: Store app passwords locally for multi-user BasicAuth background sync
|
||||
- **deck**: use correct endpoint for reorder_card to fix cross-stack moves
|
||||
- **deck**: Always preserve fields in update_card for partial updates
|
||||
- **astrolabe**: Fix CSS loading for Nextcloud apps
|
||||
- **astrolabe**: Fix revoke access button HTTP method mismatch
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use get_settings() for vector sync enabled check
|
||||
- Extract storage helper and improve PHP error handling
|
||||
|
||||
## nextcloud-mcp-server-0.56.2 (2025-12-29)
|
||||
|
||||
### Fix
|
||||
|
||||
@@ -4,6 +4,6 @@ dependencies:
|
||||
version: 1.16.3
|
||||
- name: ollama
|
||||
repository: https://otwld.github.io/ollama-helm
|
||||
version: 1.36.0
|
||||
digest: sha256:7f0979ec4110ff41ebeb55bf586b41366a350cc39fe65a2da7d2da03f723fe9b
|
||||
generated: "2025-12-22T11:09:39.166328543Z"
|
||||
version: 1.37.0
|
||||
digest: sha256:0ce3bb4b5e95a3b8fde3f5f374d7b62aeafcb0dcf8a60b9d95978530b6c05b68
|
||||
generated: "2026-01-08T11:11:12.857375888Z"
|
||||
|
||||
@@ -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.56.2
|
||||
appVersion: "0.60.3"
|
||||
version: 0.57.4
|
||||
appVersion: "0.61.4"
|
||||
keywords:
|
||||
- nextcloud
|
||||
- mcp
|
||||
@@ -31,6 +31,6 @@ dependencies:
|
||||
repository: https://qdrant.github.io/qdrant-helm
|
||||
condition: qdrant.networkMode.deploySubchart
|
||||
- name: ollama
|
||||
version: "1.36.0"
|
||||
version: "1.37.0"
|
||||
repository: https://otwld.github.io/ollama-helm
|
||||
condition: ollama.enabled
|
||||
|
||||
+10
-14
@@ -23,7 +23,7 @@ services:
|
||||
restart: always
|
||||
|
||||
app:
|
||||
image: docker.io/library/nextcloud:32.0.3@sha256:53231a9fb9233af2c15bfe70fc03ebe639fd53243fa42a9369884b1e0008deae
|
||||
image: docker.io/library/nextcloud:32.0.3@sha256:b8658180f826242849b3f65c42a90529b582f9824bde0b7cc93fd08077bc4e14
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:8080:80
|
||||
@@ -54,14 +54,14 @@ services:
|
||||
retries: 30
|
||||
|
||||
recipes:
|
||||
image: docker.io/library/nginx:alpine@sha256:052b75ab72f690f33debaa51c7e08d9b969a0447a133eb2b99cc905d9188cb2b
|
||||
image: docker.io/library/nginx:alpine@sha256:66d420cc54ef85bcc1d72220e83d7aaa6c4850bd2904794e3a56f09fd4ccb66e
|
||||
restart: always
|
||||
volumes:
|
||||
- ./tests/fixtures/test_recipe.html:/usr/share/nginx/html/test_recipe.html:ro
|
||||
- ./tests/fixtures/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
|
||||
unstructured:
|
||||
image: downloads.unstructured.io/unstructured-io/unstructured-api:latest@sha256:54282d3a25f33fd6cf69bc45b3d37770f213593f58b6dfe5e85fe546376b2807
|
||||
image: downloads.unstructured.io/unstructured-io/unstructured-api:latest@sha256:db5fcc831eb673ec835c41e8d47f993fdde276562285d6837cebb03f958536a2
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:8002:8000
|
||||
@@ -88,8 +88,8 @@ services:
|
||||
- NEXTCLOUD_PASSWORD=admin
|
||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||
|
||||
# Vector sync configuration (ADR-007)
|
||||
#- VECTOR_SYNC_ENABLED=true
|
||||
# Semantic search configuration (ADR-007, ADR-021)
|
||||
#- ENABLE_SEMANTIC_SEARCH=true
|
||||
- VECTOR_SYNC_SCAN_INTERVAL=60
|
||||
- VECTOR_SYNC_PROCESSOR_WORKERS=1
|
||||
|
||||
@@ -140,14 +140,13 @@ services:
|
||||
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8003
|
||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
||||
- ENABLE_MULTI_USER_BASIC_AUTH=true
|
||||
#- ENABLE_OFFLINE_ACCESS=true
|
||||
- ENABLE_BACKGROUND_OPERATIONS=true
|
||||
|
||||
# Token storage (required for middleware initialization)
|
||||
- TOKEN_ENCRYPTION_KEY=ESF1BvEQdGYsCluwMx9Cxvw3uh5pFowPH7Rg_nIliyo=
|
||||
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||
|
||||
- VECTOR_SYNC_ENABLED=true
|
||||
- ENABLE_SEMANTIC_SEARCH=true
|
||||
- VECTOR_SYNC_SCAN_INTERVAL=60
|
||||
- VECTOR_SYNC_PROCESSOR_WORKERS=1
|
||||
|
||||
@@ -180,7 +179,6 @@ services:
|
||||
- NEXTCLOUD_OIDC_SCOPES=openid profile email notes:read notes:write calendar:read calendar:write contacts:read contacts:write cookbook:read cookbook:write deck:read deck:write tables:read tables:write files:read files:write sharing:read sharing:write todo:read todo:write
|
||||
|
||||
# Refresh token storage (ADR-002 Tier 1)
|
||||
#- ENABLE_OFFLINE_ACCESS=true
|
||||
- ENABLE_BACKGROUND_OPERATIONS=true
|
||||
- TOKEN_ENCRYPTION_KEY=ESF1BvEQdGYsCluwMx9Cxvw3uh5pFowPH7Rg_nIliyo=
|
||||
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||
@@ -189,9 +187,8 @@ services:
|
||||
# Tokens must contain BOTH MCP and Nextcloud audiences
|
||||
# No token exchange needed - tokens work for both MCP auth and Nextcloud APIs
|
||||
|
||||
# Vector sync configuration (ADR-007)
|
||||
# Semantic search configuration (ADR-007, ADR-021)
|
||||
- ENABLE_SEMANTIC_SEARCH=true
|
||||
#- VECTOR_SYNC_ENABLED=true
|
||||
- VECTOR_SYNC_SCAN_INTERVAL=60
|
||||
- VECTOR_SYNC_PROCESSOR_WORKERS=1
|
||||
|
||||
@@ -211,7 +208,7 @@ services:
|
||||
- oauth-tokens:/app/data
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:26.4.7@sha256:9409c59bdfb65dbffa20b11e6f18b8abb9281d480c7ca402f51ed3d5977e6007
|
||||
image: quay.io/keycloak/keycloak:26.5.0@sha256:5fdd7cda82e58775ed124294c7e16fabc33166d38dfc4aabebda7d64e7a964bf
|
||||
command:
|
||||
- "start-dev"
|
||||
- "--import-realm"
|
||||
@@ -259,7 +256,6 @@ services:
|
||||
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8888/realms/nextcloud-mcp
|
||||
|
||||
# Refresh token storage (ADR-002 Tier 1 & 2)
|
||||
#- ENABLE_OFFLINE_ACCESS=true
|
||||
- ENABLE_BACKGROUND_OPERATIONS=true
|
||||
- TOKEN_ENCRYPTION_KEY=ESF1BvEQdGYsCluwMx9Cxvw3uh5pFowPH7Rg_nIliyo=
|
||||
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
||||
@@ -293,13 +289,13 @@ services:
|
||||
- 127.0.0.1:8081:8081
|
||||
environment:
|
||||
- SMITHERY_DEPLOYMENT=true
|
||||
- VECTOR_SYNC_ENABLED=false
|
||||
- ENABLE_SEMANTIC_SEARCH=false
|
||||
- PORT=8081
|
||||
profiles:
|
||||
- smithery
|
||||
|
||||
qdrant:
|
||||
image: qdrant/qdrant:v1.16.2@sha256:dab6de32f7b2cc599985a7c764db3e8b062f70508fb85ca074aa856f829bf335
|
||||
image: docker.io/qdrant/qdrant:v1.16.3@sha256:0425e3e03e7fd9b3dc95c4214546afe19de2eb2e28ca621441a56663ac6e1f46
|
||||
restart: always
|
||||
ports:
|
||||
- 127.0.0.1:6333:6333 # REST API
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {}
|
||||
|
||||
|
||||
@@ -386,11 +386,17 @@ class DeckClient(BaseNextcloudClient):
|
||||
order: int,
|
||||
target_stack_id: int,
|
||||
) -> None:
|
||||
# Use the non-API route /cards/{cardId}/reorder which correctly reads
|
||||
# stackId from the body. The API route /api/.../stacks/{stackId}/cards/...
|
||||
# has a parameter conflict where URL stackId overrides body stackId.
|
||||
# See: https://github.com/cbcoutinho/nextcloud-mcp-server/issues/469
|
||||
json_data = {"order": order, "stackId": target_stack_id}
|
||||
headers = self._get_deck_headers()
|
||||
await self._make_request(
|
||||
"PUT",
|
||||
f"/apps/deck/api/v1.0/boards/{board_id}/stacks/{stack_id}/cards/{card_id}/reorder",
|
||||
f"/apps/deck/cards/{card_id}/reorder",
|
||||
json=json_data,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# Labels
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "nextcloud-mcp-server"
|
||||
version = "0.60.3"
|
||||
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,177 @@
|
||||
"""Integration tests for Deck card reorder functionality.
|
||||
|
||||
Tests issue #469: Moving Deck card from one column (stack) to another not working.
|
||||
https://github.com/cbcoutinho/nextcloud-mcp-server/issues/469
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
from nextcloud_mcp_server.client import NextcloudClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def board_with_two_stacks(nc_client: NextcloudClient):
|
||||
"""Create a temporary board with two stacks for testing card movement.
|
||||
|
||||
Yields:
|
||||
tuple: (board_data, source_stack_data, target_stack_data)
|
||||
"""
|
||||
unique_suffix = uuid.uuid4().hex[:8]
|
||||
board_title = f"Reorder Test Board {unique_suffix}"
|
||||
board = None
|
||||
|
||||
logger.info(f"Creating board with two stacks: {board_title}")
|
||||
try:
|
||||
board = await nc_client.deck.create_board(board_title, "0000FF")
|
||||
board_id = board.id
|
||||
|
||||
# Create source stack (stack 1)
|
||||
source_stack = await nc_client.deck.create_stack(
|
||||
board_id, f"Source Stack {unique_suffix}", order=1
|
||||
)
|
||||
source_stack_data = {
|
||||
"id": source_stack.id,
|
||||
"title": source_stack.title,
|
||||
"order": source_stack.order,
|
||||
}
|
||||
logger.info(f"Created source stack with ID: {source_stack.id}")
|
||||
|
||||
# Create target stack (stack 2)
|
||||
target_stack = await nc_client.deck.create_stack(
|
||||
board_id, f"Target Stack {unique_suffix}", order=2
|
||||
)
|
||||
target_stack_data = {
|
||||
"id": target_stack.id,
|
||||
"title": target_stack.title,
|
||||
"order": target_stack.order,
|
||||
}
|
||||
logger.info(f"Created target stack with ID: {target_stack.id}")
|
||||
|
||||
board_data = {
|
||||
"id": board_id,
|
||||
"title": board.title,
|
||||
"color": board.color,
|
||||
}
|
||||
|
||||
yield (board_data, source_stack_data, target_stack_data)
|
||||
|
||||
finally:
|
||||
if board:
|
||||
logger.info(f"Cleaning up board ID: {board.id}")
|
||||
try:
|
||||
await nc_client.deck.delete_board(board.id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error cleaning up board: {e}")
|
||||
|
||||
|
||||
async def test_reorder_card_move_to_different_stack(
|
||||
nc_client: NextcloudClient, board_with_two_stacks: tuple
|
||||
):
|
||||
"""Test moving a card from one stack to another (issue #469).
|
||||
|
||||
This test reproduces the bug where the reorder_card API reports success
|
||||
but the card doesn't actually move to the target stack.
|
||||
"""
|
||||
board_data, source_stack_data, target_stack_data = board_with_two_stacks
|
||||
board_id = board_data["id"]
|
||||
source_stack_id = source_stack_data["id"]
|
||||
target_stack_id = target_stack_data["id"]
|
||||
|
||||
# Create a card in the source stack
|
||||
unique_suffix = uuid.uuid4().hex[:8]
|
||||
card_title = f"Test Card {unique_suffix}"
|
||||
card = await nc_client.deck.create_card(
|
||||
board_id, source_stack_id, card_title, description="Card to be moved"
|
||||
)
|
||||
card_id = card.id
|
||||
logger.info(f"Created card ID: {card_id} in source stack ID: {source_stack_id}")
|
||||
|
||||
try:
|
||||
# Verify card is in source stack
|
||||
card_before = await nc_client.deck.get_card(board_id, source_stack_id, card_id)
|
||||
assert card_before.stackId == source_stack_id, (
|
||||
f"Card should start in source stack {source_stack_id}, "
|
||||
f"but is in {card_before.stackId}"
|
||||
)
|
||||
logger.info(f"Verified card is in source stack: {source_stack_id}")
|
||||
|
||||
# Move card to target stack
|
||||
logger.info(
|
||||
f"Moving card {card_id} from stack {source_stack_id} "
|
||||
f"to stack {target_stack_id}"
|
||||
)
|
||||
await nc_client.deck.reorder_card(
|
||||
board_id=board_id,
|
||||
stack_id=source_stack_id,
|
||||
card_id=card_id,
|
||||
order=0,
|
||||
target_stack_id=target_stack_id,
|
||||
)
|
||||
logger.info("reorder_card API call completed")
|
||||
|
||||
# Verify card moved to target stack
|
||||
# Note: After moving, the card should be accessible from the target stack
|
||||
card_after = await nc_client.deck.get_card(board_id, target_stack_id, card_id)
|
||||
assert card_after.stackId == target_stack_id, (
|
||||
f"Card should have moved to target stack {target_stack_id}, "
|
||||
f"but is in {card_after.stackId}"
|
||||
)
|
||||
logger.info(f"SUCCESS: Card moved to target stack {target_stack_id}")
|
||||
|
||||
finally:
|
||||
# Clean up - try to delete from target stack first, then source
|
||||
try:
|
||||
await nc_client.deck.delete_card(board_id, target_stack_id, card_id)
|
||||
except Exception:
|
||||
try:
|
||||
await nc_client.deck.delete_card(board_id, source_stack_id, card_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error cleaning up card: {e}")
|
||||
|
||||
|
||||
async def test_reorder_card_within_same_stack(
|
||||
nc_client: NextcloudClient, board_with_two_stacks: tuple
|
||||
):
|
||||
"""Test reordering a card within the same stack (should work)."""
|
||||
board_data, source_stack_data, _ = board_with_two_stacks
|
||||
board_id = board_data["id"]
|
||||
source_stack_id = source_stack_data["id"]
|
||||
|
||||
# Create two cards in the source stack
|
||||
unique_suffix = uuid.uuid4().hex[:8]
|
||||
card1 = await nc_client.deck.create_card(
|
||||
board_id, source_stack_id, f"Card 1 {unique_suffix}", order=0
|
||||
)
|
||||
card2 = await nc_client.deck.create_card(
|
||||
board_id, source_stack_id, f"Card 2 {unique_suffix}", order=1
|
||||
)
|
||||
logger.info(f"Created cards {card1.id} (order 0) and {card2.id} (order 1)")
|
||||
|
||||
try:
|
||||
# Reorder card1 to position after card2
|
||||
await nc_client.deck.reorder_card(
|
||||
board_id=board_id,
|
||||
stack_id=source_stack_id,
|
||||
card_id=card1.id,
|
||||
order=2, # Move to position 2
|
||||
target_stack_id=source_stack_id, # Same stack
|
||||
)
|
||||
logger.info(f"Reordered card {card1.id} to order 2")
|
||||
|
||||
# Verify card is still in the same stack
|
||||
card_after = await nc_client.deck.get_card(board_id, source_stack_id, card1.id)
|
||||
assert card_after.stackId == source_stack_id
|
||||
logger.info("Card reorder within same stack succeeded")
|
||||
|
||||
finally:
|
||||
try:
|
||||
await nc_client.deck.delete_card(board_id, source_stack_id, card1.id)
|
||||
await nc_client.deck.delete_card(board_id, source_stack_id, card2.id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error cleaning up cards: {e}")
|
||||
@@ -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]
|
||||
name = "cz_conventional_commits"
|
||||
version = "0.7.2"
|
||||
version = "0.8.1"
|
||||
tag_format = "astrolabe-v$version"
|
||||
version_scheme = "semver"
|
||||
update_changelog_on_bump = true
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
- name: Get version matrix
|
||||
id: versions
|
||||
uses: icewind1991/nextcloud-version-matrix@c2bf575a3516752db5ce2915499d3f694885e2c7 # v1.0.0
|
||||
uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1
|
||||
|
||||
php-lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
Vendored
+27
@@ -25,6 +25,33 @@ 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
|
||||
|
||||
- Add rate limiting and extract helpers for app password endpoints
|
||||
|
||||
### Fix
|
||||
|
||||
- **astrolabe**: define appName and appVersion for @nextcloud/vue
|
||||
- Add missing annotations for deck remove/unassign operations
|
||||
- **auth**: Store app passwords locally for multi-user BasicAuth background sync
|
||||
- **deck**: use correct endpoint for reorder_card to fix cross-stack moves
|
||||
- **deck**: Always preserve fields in update_card for partial updates
|
||||
|
||||
### Refactor
|
||||
|
||||
- Use get_settings() for vector sync enabled check
|
||||
- Extract storage helper and improve PHP error handling
|
||||
|
||||
## astrolabe-v0.7.2 (2025-12-30)
|
||||
|
||||
### Fix
|
||||
|
||||
+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.
|
||||
]]></description>
|
||||
<version>0.7.2</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
@@ -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
@@ -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(
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "astrolabe",
|
||||
"version": "0.7.2",
|
||||
"version": "0.8.1",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": "^22.0.0",
|
||||
|
||||
Vendored
+6
-8
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
|
||||
Vendored
+10
@@ -1,9 +1,19 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
// Read app info from info.xml for @nextcloud/vue
|
||||
const infoXml = readFileSync(resolve(__dirname, 'appinfo/info.xml'), 'utf-8')
|
||||
const appName = infoXml.match(/<id>([^<]+)<\/id>/)?.[1] || 'astrolabe'
|
||||
const appVersion = infoXml.match(/<version>([^<]+)<\/version>/)?.[1] || ''
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
define: {
|
||||
appName: JSON.stringify(appName),
|
||||
appVersion: JSON.stringify(appVersion),
|
||||
},
|
||||
build: {
|
||||
outDir: '.',
|
||||
emptyOutDir: false,
|
||||
|
||||
Reference in New Issue
Block a user