feat: add Docker Compose profiles and Login Flow v2 service

Add selective service startup via Docker Compose profiles so each MCP
deployment mode runs independently. Also add the new mcp-login-flow
service (port 8004) for Login Flow v2 authentication (ADR-022).

Profile assignments:
- single-user: mcp (port 8000)
- multi-user-basic: mcp-multi-user-basic (port 8003)
- oauth: mcp-oauth (port 8001)
- keycloak: keycloak + mcp-keycloak (port 8002)
- login-flow: mcp-login-flow (port 8004)

Infrastructure services (db, redis, app, recipes) always start.

Integration tests cover the full Login Flow v2 provisioning flow:
OAuth → browser login → app password → Nextcloud API access for
notes, calendar, contacts, files, deck, and cookbook operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-02-27 20:33:54 +01:00
co-authored by Claude Opus 4.6
parent 5796e2ba54
commit 8b5c2395b5
21 changed files with 3156 additions and 5 deletions
+25
View File
@@ -181,6 +181,7 @@ class Settings:
nextcloud_host: Optional[str] = None
nextcloud_username: Optional[str] = None
nextcloud_password: Optional[str] = None
nextcloud_app_password: Optional[str] = None # Preferred over nextcloud_password
# Nextcloud SSL/TLS settings
nextcloud_verify_ssl: bool = True
@@ -204,6 +205,13 @@ class Settings:
# and passes them through to Nextcloud APIs (no storage, stateless)
enable_multi_user_basic_auth: bool = False
# Login Flow v2 settings (ADR-022)
enable_login_flow: bool = False
login_flow_poll_interval: int = 2 # seconds between polls
login_flow_poll_timeout: int = 300 # max seconds to wait for completion
login_flow_cleanup_interval: int = 3600 # seconds between expired session cleanup
app_password_max_age_days: int = 0 # 0 = no expiration
# Token exchange cache settings
token_exchange_cache_ttl: int = 300 # seconds (5 minutes default)
@@ -260,6 +268,14 @@ class Settings:
"""Validate configuration and set defaults."""
logger = logging.getLogger(__name__)
# Deprecation warning: NEXTCLOUD_PASSWORD without NEXTCLOUD_APP_PASSWORD
if self.nextcloud_password and not self.nextcloud_app_password:
logger.warning(
"NEXTCLOUD_PASSWORD is deprecated for app password usage. "
"Please use NEXTCLOUD_APP_PASSWORD instead. "
"Support for NEXTCLOUD_PASSWORD as app password will be removed in v1.0.0."
)
# Validate SSL/TLS configuration
if not self.nextcloud_verify_ssl:
logger.warning(
@@ -523,6 +539,7 @@ def get_settings() -> Settings:
nextcloud_host=os.getenv("NEXTCLOUD_HOST"),
nextcloud_username=os.getenv("NEXTCLOUD_USERNAME"),
nextcloud_password=os.getenv("NEXTCLOUD_PASSWORD"),
nextcloud_app_password=os.getenv("NEXTCLOUD_APP_PASSWORD"),
# Nextcloud SSL/TLS settings
nextcloud_verify_ssl=(
os.getenv("NEXTCLOUD_VERIFY_SSL", "true").lower() == "true"
@@ -544,6 +561,14 @@ def get_settings() -> Settings:
enable_multi_user_basic_auth=(
os.getenv("ENABLE_MULTI_USER_BASIC_AUTH", "false").lower() == "true"
),
# Login Flow v2 settings (ADR-022)
enable_login_flow=(os.getenv("ENABLE_LOGIN_FLOW", "false").lower() == "true"),
login_flow_poll_interval=int(os.getenv("LOGIN_FLOW_POLL_INTERVAL", "2")),
login_flow_poll_timeout=int(os.getenv("LOGIN_FLOW_POLL_TIMEOUT", "300")),
login_flow_cleanup_interval=int(
os.getenv("LOGIN_FLOW_CLEANUP_INTERVAL", "3600")
),
app_password_max_age_days=int(os.getenv("APP_PASSWORD_MAX_AGE_DAYS", "0")),
# Token exchange cache settings
token_exchange_cache_ttl=int(os.getenv("TOKEN_EXCHANGE_CACHE_TTL", "300")),
# Token and webhook storage settings (encryption key optional for webhook-only usage)