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
+20 -1
View File
@@ -288,10 +288,28 @@ async def provision_app_password(request: Request) -> JSONResponse:
status_code=500,
)
# Parse optional scopes and username from request body
scopes = None
nc_username = None
try:
body = await request.json()
scopes = body.get("scopes") # list[str] | None
nc_username = body.get("username") # Nextcloud loginName
except Exception:
pass # No JSON body = legacy call without scopes
# Store the validated app password
try:
storage = await _get_app_password_storage(request)
await storage.store_app_password(username, app_password)
if scopes is not None or nc_username is not None:
# New path: store with scopes and username
await storage.store_app_password_with_scopes(
username, app_password, scopes=scopes, username=nc_username
)
else:
# Legacy path: store without scopes
await storage.store_app_password(username, app_password)
_record_rate_limit_attempt(path_user_id, success=True)
logger.info(f"Provisioned app password for user: {username}")
@@ -300,6 +318,7 @@ async def provision_app_password(request: Request) -> JSONResponse:
{
"success": True,
"message": f"App password stored for {username}",
"scopes": scopes,
}
)