4a5766b84e
Allows multi-user BasicAuth mode to use Dynamic Client Registration (DCR) for OAuth credentials when ENABLE_OFFLINE_ACCESS is enabled, making it consistent with OAuth modes and reducing configuration burden. **Changes:** Configuration Validation: - Relaxed OAuth credential requirements for multi-user BasicAuth - OAuth credentials now optional when offline access enabled - Will use DCR as fallback if NEXTCLOUD_OIDC_CLIENT_ID/SECRET not set - Updated validation to log info instead of error when DCR will be used Startup Logic (app.py): - Added DCR workflow for multi-user BasicAuth before uvicorn starts - Creates oauth_context for management APIs when offline access enabled - Allows Astrolabe to authenticate management API calls with OAuth - DCR runs synchronously at same lifecycle point as OAuth modes - Added traceback import for better error logging - Fixed type assertions for nextcloud_host - Fixed undefined variable references in vector sync logging Management API: - Improved auth mode detection using proper detect_auth_mode() - Added auth_mode field to /status endpoint: * "basic" - Single-user BasicAuth * "multi_user_basic" - Multi-user BasicAuth * "oauth" - OAuth modes * "smithery" - Smithery stateless - Added supports_app_passwords indicator for multi-user BasicAuth Docker Compose: - Updated mcp-multi-user-basic service configuration: * Enabled vector sync (VECTOR_SYNC_ENABLED=true) * Added ENABLE_OFFLINE_ACCESS=true for app password support * Added NEXTCLOUD_MCP_SERVER_URL for Astrolabe integration * Documented optional static OAuth credentials Testing: - Updated test_config_validators.py to expect DCR fallback - Enhanced configure_astrolabe_for_mcp_server fixture with verification - Added debug logging to test_users_setup fixture **Workflow:** 1. User configures ENABLE_OFFLINE_ACCESS=true 2. Server checks for static NEXTCLOUD_OIDC_CLIENT_ID/SECRET 3. If not found, performs DCR before uvicorn starts 4. DCR registers client with Nextcloud OIDC provider 5. OAuth credentials used for Astrolabe management API auth 6. Background sync can retrieve user app passwords via Astrolabe 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
324 lines
12 KiB
YAML
324 lines
12 KiB
YAML
services:
|
|
# Note: MariaDB is external service. You can find more information about the configuration here:
|
|
# https://hub.docker.com/_/mariadb
|
|
db:
|
|
# Note: Check the recommend version here: https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html#server
|
|
image: docker.io/library/mariadb:lts@sha256:1cac8492bd78b1ec693238dc600be173397efd7b55eabc725abc281dc855b482
|
|
restart: always
|
|
command: --transaction-isolation=READ-COMMITTED
|
|
volumes:
|
|
- db:/var/lib/mysql
|
|
environment:
|
|
- MYSQL_ROOT_PASSWORD=password
|
|
- MYSQL_PASSWORD=password
|
|
- MYSQL_DATABASE=nextcloud
|
|
- MYSQL_USER=nextcloud
|
|
|
|
# Note: Redis is an external service. You can find more information about the configuration here:
|
|
# https://hub.docker.com/_/redis
|
|
redis:
|
|
image: docker.io/library/redis:alpine@sha256:6cbef353e480a8a6e7f10ec545f13d7d3fa85a212cdcc5ffaf5a1c818b9d3798
|
|
restart: always
|
|
|
|
app:
|
|
image: docker.io/library/nextcloud:32.0.3@sha256:53231a9fb9233af2c15bfe70fc03ebe639fd53243fa42a9369884b1e0008deae
|
|
restart: always
|
|
ports:
|
|
- 0.0.0.0:8080:80
|
|
depends_on:
|
|
- redis
|
|
- db
|
|
- keycloak
|
|
volumes:
|
|
- nextcloud:/var/www/html
|
|
- ./app-hooks:/docker-entrypoint-hooks.d:ro
|
|
# Mount OIDC development directory outside /var/www/html to avoid rsync conflicts
|
|
# The post-installation hook will register /opt/apps as an additional app directory
|
|
#- ./third_party:/opt/apps:ro
|
|
- ./third_party/astrolabe:/opt/apps/astrolabe:ro
|
|
environment:
|
|
- NEXTCLOUD_TRUSTED_DOMAINS=app
|
|
- NEXTCLOUD_ADMIN_USER=admin
|
|
- NEXTCLOUD_ADMIN_PASSWORD=admin
|
|
- MYSQL_PASSWORD=password
|
|
- MYSQL_DATABASE=nextcloud
|
|
- MYSQL_USER=nextcloud
|
|
- MYSQL_HOST=db
|
|
- REDIS_HOST=redis
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "curl -Ss http://localhost/status.php | grep '\"installed\":true' || exit 1"]
|
|
interval: 10s
|
|
timeout: 30s
|
|
retries: 30
|
|
|
|
recipes:
|
|
image: docker.io/library/nginx:alpine@sha256:052b75ab72f690f33debaa51c7e08d9b969a0447a133eb2b99cc905d9188cb2b
|
|
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
|
|
restart: always
|
|
ports:
|
|
- 127.0.0.1:8002:8000
|
|
# Unstructured API runs on port 8000 internally
|
|
# We expose it on 8002 externally to avoid conflict
|
|
profiles:
|
|
- unstructured
|
|
|
|
mcp:
|
|
build: .
|
|
restart: always
|
|
command: ["--transport", "streamable-http"]
|
|
depends_on:
|
|
app:
|
|
condition: service_healthy
|
|
ports:
|
|
- 127.0.0.1:8000:8000
|
|
- 127.0.0.1:9090:9090
|
|
volumes:
|
|
- mcp-data:/app/data
|
|
environment:
|
|
- NEXTCLOUD_HOST=http://app:80
|
|
- NEXTCLOUD_USERNAME=admin
|
|
- NEXTCLOUD_PASSWORD=admin
|
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
|
|
|
# Vector sync configuration (ADR-007)
|
|
#- VECTOR_SYNC_ENABLED=true
|
|
- VECTOR_SYNC_SCAN_INTERVAL=60
|
|
- VECTOR_SYNC_PROCESSOR_WORKERS=1
|
|
|
|
#- LOG_FORMAT=json
|
|
|
|
# Qdrant configuration (three modes):
|
|
# 1. Network mode: Set QDRANT_URL=http://qdrant:6333 (requires qdrant service)
|
|
# 2. In-memory mode: Set QDRANT_LOCATION=:memory: (default if nothing set)
|
|
# 3. Persistent local: Set QDRANT_LOCATION=/app/data/qdrant (stored in mcp-data volume)
|
|
#- QDRANT_LOCATION=/app/data/qdrant # In-memory mode used if not set
|
|
#- QDRANT_URL=http://qdrant:6333 # Uncomment for network mode
|
|
#- QDRANT_API_KEY=${QDRANT_API_KEY:-my_secret_api_key} # Only for network mode
|
|
|
|
# Observability
|
|
#- OTEL_SERVICE_NAME=nextcloud-mcp-docker-compose
|
|
#- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
|
|
|
|
# Collection naming: Auto-generated as {deployment-id}-{model-name}
|
|
# - Deployment ID: OTEL_SERVICE_NAME (if set) or hostname (fallback)
|
|
# - Model name: OLLAMA_EMBEDDING_MODEL
|
|
# - Example: "nextcloud-mcp-server-nomic-embed-text"
|
|
# - Changing models creates new collection (requires re-embedding)
|
|
# - Set QDRANT_COLLECTION to override auto-generation:
|
|
#- QDRANT_COLLECTION=nextcloud_content
|
|
|
|
# Ollama configuration (optional - uses SimpleEmbeddingProvider if not set)
|
|
# - OLLAMA_BASE_URL=http://ollama:11434
|
|
# - OLLAMA_EMBEDDING_MODEL=nomic-embed-text # Changing this creates new collection
|
|
# - OLLAMA_VERIFY_SSL=false
|
|
|
|
# Document chunking configuration (for vector embeddings)
|
|
# Tune these based on your embedding model and content type
|
|
# - DOCUMENT_CHUNK_SIZE=512 # Words per chunk (default: 512)
|
|
# - DOCUMENT_CHUNK_OVERLAP=50 # Overlapping words (default: 50, recommended: 10-20% of chunk size)
|
|
|
|
mcp-multi-user-basic:
|
|
build: .
|
|
restart: always
|
|
command: ["--transport", "streamable-http"]
|
|
depends_on:
|
|
app:
|
|
condition: service_healthy
|
|
ports:
|
|
- 127.0.0.1:8003:8000
|
|
environment:
|
|
# Multi-user BasicAuth pass-through mode (ADR-020)
|
|
- NEXTCLOUD_HOST=http://app:80
|
|
- 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
|
|
- VECTOR_SYNC_SCAN_INTERVAL=60
|
|
- VECTOR_SYNC_PROCESSOR_WORKERS=1
|
|
|
|
# OAuth credentials for background sync (optional - uses DCR if not provided)
|
|
# Uncomment to avoid DCR:
|
|
# - NEXTCLOUD_OIDC_CLIENT_ID=your_client_id
|
|
# - NEXTCLOUD_OIDC_CLIENT_SECRET=your_client_secret
|
|
|
|
# NO admin credentials - credentials come from client Authorization header
|
|
volumes:
|
|
- multi-user-basic-data:/app/data
|
|
|
|
mcp-oauth:
|
|
build: .
|
|
command: ["--transport", "streamable-http", "--oauth", "--port", "8001", "--oauth-token-type", "jwt"]
|
|
restart: always
|
|
depends_on:
|
|
app:
|
|
condition: service_healthy
|
|
ports:
|
|
- 127.0.0.1:8001:8001
|
|
environment:
|
|
# Generic OIDC configuration (integrated mode - Nextcloud OIDC app)
|
|
# OIDC_DISCOVERY_URL not set - defaults to NEXTCLOUD_HOST/.well-known/openid-configuration
|
|
# OIDC_CLIENT_ID not set - uses Dynamic Client Registration (DCR)
|
|
- NEXTCLOUD_HOST=http://app:80
|
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8001
|
|
- NEXTCLOUD_RESOURCE_URI=http://localhost:8080 # ADR-005: Nextcloud resource identifier for audience validation
|
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8080
|
|
- 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
|
|
- TOKEN_ENCRYPTION_KEY=ESF1BvEQdGYsCluwMx9Cxvw3uh5pFowPH7Rg_nIliyo=
|
|
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
|
|
|
# ADR-005: Multi-audience mode (default - ENABLE_TOKEN_EXCHANGE=false)
|
|
# 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)
|
|
- VECTOR_SYNC_ENABLED=true
|
|
- VECTOR_SYNC_SCAN_INTERVAL=60
|
|
- VECTOR_SYNC_PROCESSOR_WORKERS=1
|
|
|
|
# Qdrant configuration - persistent local storage
|
|
- QDRANT_LOCATION=/app/data/qdrant
|
|
|
|
# Embedding provider for vector sync (use Simple provider as fallback)
|
|
# Ollama not available in CI/test environments
|
|
# - OLLAMA_BASE_URL=http://ollama:11434
|
|
# - OLLAMA_EMBEDDING_MODEL=nomic-embed-text
|
|
|
|
# NO admin credentials - using OAuth with Dynamic Client Registration (DCR)
|
|
# Client credentials registered via RFC 7591 and stored in volume
|
|
# JWT token type is used for testing (faster validation, scopes embedded in token)
|
|
volumes:
|
|
- oauth-client-storage:/app/.oauth
|
|
- oauth-tokens:/app/data
|
|
|
|
keycloak:
|
|
image: quay.io/keycloak/keycloak:26.4.7@sha256:9409c59bdfb65dbffa20b11e6f18b8abb9281d480c7ca402f51ed3d5977e6007
|
|
command:
|
|
- "start-dev"
|
|
- "--import-realm"
|
|
- "--hostname=http://localhost:8888"
|
|
- "--hostname-strict=false"
|
|
- "--hostname-backchannel-dynamic=true"
|
|
- "--features=preview" # Enable Legacy V1 token exchange (supports both Standard V2 and Legacy V1)
|
|
ports:
|
|
- 127.0.0.1:8888:8080
|
|
environment:
|
|
- KC_BOOTSTRAP_ADMIN_USERNAME=admin
|
|
- KC_BOOTSTRAP_ADMIN_PASSWORD=admin
|
|
volumes:
|
|
- ./keycloak/realm-export.json:/opt/keycloak/data/import/realm.json:ro
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/8080 && echo -e 'GET /realms/nextcloud-mcp HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3 && cat <&3 | grep -q 'HTTP/1.1 200'"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 30
|
|
|
|
mcp-keycloak:
|
|
build: .
|
|
command: ["--transport", "streamable-http", "--oauth", "--port", "8002"]
|
|
restart: always
|
|
depends_on:
|
|
keycloak:
|
|
condition: service_healthy
|
|
app:
|
|
condition: service_started
|
|
ports:
|
|
- 127.0.0.1:8002:8002
|
|
environment:
|
|
# Generic OIDC configuration (external IdP mode - Keycloak)
|
|
# Provider auto-detected from OIDC_DISCOVERY_URL issuer
|
|
# Using internal Docker hostname for discovery to get consistent issuer
|
|
- OIDC_DISCOVERY_URL=http://keycloak:8080/realms/nextcloud-mcp/.well-known/openid-configuration
|
|
- NEXTCLOUD_OIDC_CLIENT_ID=nextcloud-mcp-server
|
|
- NEXTCLOUD_OIDC_CLIENT_SECRET=mcp-secret-change-in-production
|
|
- OIDC_JWKS_URI=http://keycloak:8080/realms/nextcloud-mcp/protocol/openid-connect/certs
|
|
|
|
# Nextcloud API endpoint (for accessing APIs with validated token)
|
|
- NEXTCLOUD_HOST=http://app:80
|
|
- NEXTCLOUD_MCP_SERVER_URL=http://localhost:8002
|
|
- NEXTCLOUD_RESOURCE_URI=nextcloud # ADR-005: Keycloak uses client IDs as audiences, not URLs
|
|
- NEXTCLOUD_PUBLIC_ISSUER_URL=http://localhost:8888/realms/nextcloud-mcp
|
|
|
|
# Refresh token storage (ADR-002 Tier 1 & 2)
|
|
- ENABLE_OFFLINE_ACCESS=true
|
|
- TOKEN_ENCRYPTION_KEY=ESF1BvEQdGYsCluwMx9Cxvw3uh5pFowPH7Rg_nIliyo=
|
|
- TOKEN_STORAGE_DB=/app/data/tokens.db
|
|
|
|
# ADR-005: Token exchange mode (RFC 8693)
|
|
# Exchange MCP tokens (aud: nextcloud-mcp-server) for Nextcloud tokens (aud: http://localhost:8080)
|
|
# Provides strict audience separation between MCP session and Nextcloud API access
|
|
- ENABLE_TOKEN_EXCHANGE=true
|
|
- TOKEN_EXCHANGE_CACHE_TTL=300 # Cache exchanged tokens for 5 minutes (default)
|
|
|
|
# OAuth scopes (optional - uses defaults if not specified)
|
|
- NEXTCLOUD_OIDC_SCOPES=openid profile email offline_access 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
|
|
|
|
# NO admin credentials - using external IdP OAuth only!
|
|
volumes:
|
|
- keycloak-tokens:/app/data
|
|
- keycloak-oauth-storage:/app/.oauth
|
|
|
|
# Smithery stateless deployment mode (ADR-016)
|
|
# Test with: docker compose --profile smithery up smithery
|
|
# Then: curl http://localhost:8081/.well-known/mcp-config
|
|
smithery:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.smithery
|
|
restart: always
|
|
depends_on:
|
|
app:
|
|
condition: service_healthy
|
|
ports:
|
|
- 127.0.0.1:8081:8081
|
|
environment:
|
|
- SMITHERY_DEPLOYMENT=true
|
|
- VECTOR_SYNC_ENABLED=false
|
|
- PORT=8081
|
|
profiles:
|
|
- smithery
|
|
|
|
qdrant:
|
|
image: qdrant/qdrant:v1.16.2@sha256:dab6de32f7b2cc599985a7c764db3e8b062f70508fb85ca074aa856f829bf335
|
|
restart: always
|
|
ports:
|
|
- 127.0.0.1:6333:6333 # REST API
|
|
- 127.0.0.1:6334:6334 # gRPC (optional)
|
|
volumes:
|
|
- qdrant-data:/qdrant/storage
|
|
environment:
|
|
- QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY:-my_secret_api_key}
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "test -f /qdrant/.qdrant-initialized"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
profiles:
|
|
- qdrant
|
|
|
|
volumes:
|
|
nextcloud:
|
|
db:
|
|
oauth-client-storage:
|
|
oauth-tokens:
|
|
keycloak-tokens:
|
|
keycloak-oauth-storage:
|
|
qdrant-data:
|
|
mcp-data:
|
|
multi-user-basic-data:
|