Files
mcp-nextcloud/docs/ADR-011-hybrid-oauth-appapi-deployment.md
T
Chris CoutinhoandClaude ff3123a190 docs: Add ADR-011 for hybrid OAuth + AppAPI deployment architecture
This ADR documents the architectural decision to support both OAuth and
AppAPI (ExApp) deployment modes in a single codebase with 90%+ code sharing.

Key additions:
- Comprehensive analysis of AppAPI limitations and challenges
- Feature parity matrix comparing OAuth vs AppAPI modes
- Resolution of critical open questions via research:
  * Non-browser client authentication (app passwords/OAuth)
  * Streaming transport compatibility (buffered, not real-time)
  * Callbacks/webhooks (MCP notifications not possible in AppAPI)
- Detailed implementation plan with 4 phases (10 days)
- Mode-aware architecture with abstraction layer

Critical findings:
- AppAPI mode does NOT support MCP sampling (RAG features)
- No real-time progress updates (use Nextcloud notifications)
- Buffered streaming only (Streamable HTTP works, WebSocket doesn't)
- Requires app password support in AppAPI proxy

Deployment mode selection:
- OAuth: Multi-tenant, external clients, sampling/RAG, real-time updates
- AppAPI: Single-tenant, simplified install, native UI, admin-controlled

Related to investigation of ~/Software/app_api/ and ~/Software/nc_py_api/
for AppAPI integration patterns.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 13:10:21 +01:00

46 KiB

ADR-011: Hybrid OAuth and AppAPI Deployment Architecture

Status: Proposed Date: 2025-01-13 Related: ADR-004 (Progressive Consent), ADR-005 (Token Audience Validation)

Context

The Nextcloud MCP Server currently implements a sophisticated OAuth 2.0 / OIDC authentication architecture (ADR-004, ADR-005) that supports multi-tenant deployments, fine-grained scope-based permissions, and progressive consent. This architecture works well for standalone MCP server deployments where the server operates as a separate service from Nextcloud, requiring users to explicitly authenticate and authorize access to their Nextcloud data.

However, this OAuth-based approach has limitations for certain deployment scenarios:

Deployment Complexity: Setting up the OAuth flow requires:

  • Configuring an identity provider (Keycloak, Nextcloud's built-in OIDC, etc.)
  • Registering OAuth clients via Dynamic Client Registration (DCR)
  • Configuring redirect URIs, token endpoints, and JWKS endpoints
  • Setting up token storage for refresh tokens
  • Managing token lifecycle (refresh, expiration, revocation)

For administrators who simply want to enable MCP functionality within their existing Nextcloud instance, this setup burden is significant—especially compared to traditional Nextcloud apps that can be installed with a single click.

Integration Limitations: The current architecture treats Nextcloud as an external API, accessed via HTTP with bearer token authentication. This prevents the MCP server from:

  • Registering UI components in Nextcloud (menu items, file actions, dashboard widgets)
  • Responding to Nextcloud events and webhooks natively
  • Leveraging Nextcloud's built-in permission system
  • Being managed via Nextcloud's app management interface

Single-Tenant Overhead: For single-tenant deployments (one Nextcloud instance, one MCP server), the OAuth architecture's multi-tenant capabilities (separate tokens per user, per-user consent) add complexity without providing value. The admin already trusts the MCP server by deploying it—explicit per-user consent becomes redundant.

Nextcloud AppAPI (ExApp) Architecture

Nextcloud provides an AppAPI framework for running External Applications (ExApps)—applications that run outside the Nextcloud PHP process but integrate deeply with Nextcloud. ExApps are the evolution of traditional PHP-based Nextcloud apps, designed to support multiple programming languages (Python, Go, etc.) and run in separate containers for enhanced security and stability.

Key AppAPI Characteristics:

  1. Shared Secret Authentication: ExApps use a permanent shared secret instead of OAuth tokens. Nextcloud passes user context via the AUTHORIZATION-APP-API header: base64(userId:secret).

  2. Built-in Proxy: AppAPI provides a production-ready HTTP proxy at /index.php/apps/app_api/proxy/{appId}/{path} that:

    • Routes external requests through Nextcloud to the ExApp
    • Translates Nextcloud user sessions into authenticated ExApp requests
    • Supports HTTP streaming (including Server-Sent Events for MCP)
    • Enforces access control (PUBLIC, USER, ADMIN levels)
  3. Native Integration: ExApps can register:

    • Top menu items
    • File actions (right-click context menus)
    • Dashboard widgets
    • Settings pages
    • Background jobs (cron tasks)
    • Event listeners
  4. Admin-Controlled Permissions: Scopes are granted at installation time by the administrator (via info.xml manifest), not per-user. Users inherit the app's permissions based on their Nextcloud account privileges.

  5. Container Deployment: ExApps run as Docker containers managed by Nextcloud's daemon configuration, with automatic lifecycle management (start, stop, update, uninstall).

Example ExApp Deployment Flow:

# Admin registers ExApp via occ command
php occ app_api:app:register nextcloud_mcp_server \
  --json-info '{"id":"nextcloud_mcp_server","secret":"<generated>",...}'

# OR: One-click install from Nextcloud app store (future)

MCP Client Connection via Proxy:

User → Nextcloud (session auth) → AppAPI Proxy → ExApp MCP Server

The proxy handles user authentication and injects the AUTHORIZATION-APP-API header, so the ExApp receives pre-authenticated requests with user context.

Why Both Architectures Are Valuable

OAuth Mode Best For:

  • Multi-tenant SaaS: One MCP server instance serving multiple Nextcloud instances
  • External MCP clients: Clients not integrated with Nextcloud (Claude Desktop, custom clients)
  • Fine-grained consent: Users explicitly authorize which scopes the MCP client can access
  • Per-user token management: Background jobs that require long-lived refresh tokens
  • Standalone services: MCP server as a separate service with its own lifecycle

AppAPI Mode Best For:

  • Single-tenant deployments: One Nextcloud instance with integrated MCP functionality
  • Simplified administration: One-click installation via Nextcloud app store
  • Native Nextcloud integration: UI components, event listeners, dashboard widgets
  • Admin-controlled permissions: Trust model where admin installation implies consent
  • Nextcloud-managed lifecycle: Automatic container management and updates

Both deployment models serve legitimate use cases. Rather than choosing one over the other, a hybrid architecture supporting both modes maximizes flexibility and serves the broadest user base.

Design Challenges

Code Duplication Risk: Implementing two separate authentication mechanisms could lead to massive code duplication—separate tool implementations, separate clients, separate tests—doubling maintenance burden.

Abstraction Complexity: Creating an abstraction layer that works seamlessly for both OAuth (token-based, per-user scopes) and AppAPI (header-based, admin-granted permissions) risks over-engineering and introducing bugs.

Dependency Management: OAuth mode requires authlib, pyjwt, aiosqlite. AppAPI mode requires nc_py_api. Installing both adds bloat; making them mutually exclusive complicates development.

Testing Burden: Supporting both modes doubles the test matrix—each integration test must pass in both OAuth and AppAPI configurations.

Documentation Complexity: Users need clear guidance on when to use each mode, how to configure it, and how they differ in behavior.

Despite these challenges, the value proposition—supporting both standalone OAuth deployments and native Nextcloud integration—justifies the architectural investment.

AppAPI Limitations and Challenges

While AppAPI provides valuable benefits for Nextcloud integration, research has identified several technical limitations that constrain the MCP server's capabilities in AppAPI mode compared to OAuth mode. These limitations stem from fundamental architectural differences between the two approaches.

Authentication Challenges

Session Cookie Dependency: The AppAPI proxy relies on Nextcloud user sessions for authentication. When a user accesses /index.php/apps/app_api/proxy/{appId}/{path}, the proxy extracts the user ID from their Nextcloud session cookie and converts it to the AUTHORIZATION-APP-API header.

Non-Browser Client Problem: MCP clients like Claude Desktop are non-browser applications that cannot:

  • Handle browser-based session cookies
  • Interact with Nextcloud login forms
  • Complete standard OAuth browser flows

Workaround Required: To support non-browser MCP clients in AppAPI mode, one of the following solutions is needed:

  1. App Passwords (Simplest): Add BasicAuth support to the AppAPI proxy. Users generate app-specific passwords in Nextcloud's security settings, and MCP clients send Authorization: Basic base64(username:appPassword). The proxy validates credentials and converts to AUTHORIZATION-APP-API headers. Requires upstream AppAPI modification.

  2. OAuth 2.0 Integration (Most Secure): Integrate MCP's OAuth client with Nextcloud's OIDC provider. MCP clients obtain access tokens via standard OAuth flow, include Authorization: Bearer <token> headers, and the proxy validates tokens via introspection. Requires significant proxy modification and token caching for performance.

  3. PUBLIC Routes with Internal Auth (Bypass Proxy): Register ExApp routes as PUBLIC (no authentication) and implement validation within the ExApp. Less secure and duplicates authentication logic.

Recommendation: Implement app password support (Option 1) as Phase 1, with OAuth 2.0 (Option 2) as a long-term production-ready solution.

Streaming Limitations

Buffered, Not Real-Time: The AppAPI proxy supports HTTP streaming via Guzzle's stream: true option, which efficiently handles large response bodies. However, the streaming is buffered, not incremental:

  • ExApp generates response chunks
  • Guzzle streams response to proxy (low memory usage)
  • Proxy accumulates complete response
  • Client receives response only after completion

SSE Response Handling: While ExApps can return Content-Type: text/event-stream responses, the events arrive at the client as a complete buffered stream, not as individual events in real-time. This works for bounded operations (finite event streams) but not for true real-time streaming.

No WebSocket Support: The proxy does not support WebSocket protocol upgrades. WebSocket requires:

  • HTTP/1.1 Upgrade protocol handling
  • Different HTTP client library (Guzzle doesn't support WebSocket)
  • Persistent bidirectional connection management
  • Significant proxy architectural changes

Impact on MCP Transports:

Transport AppAPI Support Streaming Usability
Streamable HTTP Buffered Bounded Works today
stdio Future True Ideal but requires AppAPI changes
SSE (legacy) ⚠️ Buffered Bounded Deprecated by MCP
WebSocket Major rewrite True Not viable

Recommendation: Use Streamable HTTP transport (MCP 2025-03-26+), which works with current AppAPI proxy. Operations complete as bounded streams. For true real-time streaming, advocate for stdio transport support in AppAPI (requires docker exec -i integration, estimated 2-4 weeks development).

Notification and Progress Update Limitations

MCP's Notification Model: The MCP protocol supports server-to-client notifications over persistent connections (SSE or WebSocket). This enables:

  • Progress updates during long-running operations
  • Sampling requests (LLM generation via client)
  • Asynchronous events from server to client

AppAPI's Request/Response Architecture: The AppAPI proxy operates on a request/response model:

  • Client sends HTTP request through proxy
  • ExApp processes and returns response
  • Connection closes
  • No persistent connection to original client
  • ExApp doesn't know who the client is (only sees proxy)

Feature Incompatibility:

Feature OAuth/SSE Mode AppAPI Mode Notes
Tool execution Immediate Immediate Works fine
Progress updates Real-time via MCP No channel Must use Nextcloud notifications
Sampling (LLM/RAG) Via MCP protocol No protocol forwarding Critical limitation
Long operations With incremental progress ⚠️ Background tasks only Different pattern required
Bidirectional events Via persistent connection Request/response only Architectural incompatibility

Workarounds for AppAPI Mode:

  1. Short Operations (<30s): Direct request/response works fine. No changes needed.

  2. Long Operations: Use FastAPI BackgroundTasks + Nextcloud notifications:

    @mcp.tool()
    async def long_operation(ctx: NextcloudApp, background: BackgroundTasks):
        task_id = uuid.uuid4()
        background.add_task(process_long_operation, ctx, task_id)
        return {"status": "accepted", "task_id": task_id}
    
    def process_long_operation(nc: NextcloudApp, task_id: str):
        result = do_work()
        nc.notifications.create("Task completed", f"Result: {result}")
    
  3. Sampling/LLM Features: Not possible in AppAPI mode. The MCP sampling protocol (ctx.session.create_message()) requires bidirectional communication with the client. Tools using sampling must detect the deployment mode and gracefully degrade:

    @mcp.tool()
    async def semantic_search_answer(query: str, ctx: Context | NextcloudApp):
        if isinstance(ctx, NextcloudApp):
            # AppAPI mode: return documents only, no LLM generation
            return SearchResponse(results=documents, message="LLM generation not available in AppAPI mode")
        else:
            # OAuth mode: use sampling for LLM answer
            answer = await ctx.session.create_message(...)
            return SamplingResponse(answer=answer, sources=documents)
    

Recommendation: Document feature limitations clearly. Disable sampling-based tools in AppAPI mode with helpful error messages. Use Nextcloud's notification system for progress updates on long-running operations.

Webhook and Callback Support

Outbound Webhooks (Nextcloud → ExApp): Well Supported

ExApps can register webhooks via the webhooks_listener app:

nc.webhooks.register(
    http_method="POST",
    uri="/webhook/file_created",
    event="OCP\\Files\\Events\\Node\\NodeCreatedEvent",
    event_filter={...}
)

Inbound Notifications (ExApp → Nextcloud): Supported

ExApps can send notifications to users:

nc.notifications.create(
    subject="Task completed",
    message="Your file is ready",
    link=result_url
)

Limitation: ExApps cannot send MCP protocol notifications back through the proxy to the original MCP client. Communication is ExApp → Nextcloud → User UI, not ExApp → MCP Client.

Decision

We will implement a hybrid architecture that supports both OAuth and AppAPI deployment modes within a single codebase, achieving 90%+ code sharing through a carefully designed abstraction layer.

Core Principles

  1. Single Codebase: One repository, one Docker image build process, mode selection via environment variables
  2. Maximum Code Sharing: 100% sharing of MCP tool implementations, client libraries, and business logic
  3. Backwards Compatibility: OAuth mode remains the default; existing deployments continue working unchanged
  4. Mode Detection: Runtime mode selection via APPAPI_MODE=true environment variable
  5. Abstraction Over Duplication: Unified client interface that tools call regardless of mode
  6. Graceful Degradation: Features that only work in one mode (e.g., UI registration in AppAPI) degrade gracefully

Architecture Overview

┌────────────────────────────────────────────────────────────┐
│  MCP Tool Layer (100% Shared)                              │
│  - nc_notes_create_note()                                  │
│  - nc_calendar_get_events()                                │
│  - nc_semantic_search()                                    │
│  └─ Calls: get_client(ctx) → NextcloudClient              │
└────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌────────────────────────────────────────────────────────────┐
│  Client Abstraction Layer (New)                            │
│  - get_client(ctx: Context | NextcloudApp)                 │
│  - Returns NextcloudClient regardless of mode              │
└────────────────────────────────────────────────────────────┘
                           │
          ┌────────────────┴────────────────┐
          ▼                                 ▼
┌──────────────────────┐        ┌──────────────────────┐
│  OAuth Mode          │        │  AppAPI Mode         │
│  - Token validation  │        │  - Header validation │
│  - Token refresh     │        │  - User context from │
│  - DCR               │        │    AUTHORIZATION-    │
│  - Scope checking    │        │    APP-API header    │
└──────────────────────┘        └──────────────────────┘
          │                                 │
          ▼                                 ▼
┌──────────────────────┐        ┌──────────────────────┐
│  NextcloudClient     │        │  NextcloudClient     │
│  (token-based HTTP)  │        │  (via nc_py_api)     │
└──────────────────────┘        └──────────────────────┘

File Structure

nextcloud_mcp_server/
├── app.py                          # MODIFIED: Mode detection and delegation
├── app_oauth.py                    # NEW: OAuth-specific FastMCP setup
├── app_exapp.py                    # NEW: AppAPI-specific FastMCP setup
├── auth/
│   ├── bearer_auth.py              # OAuth token verification (existing)
│   ├── appapi_auth.py              # NEW: AppAPI header validation
│   ├── client_factory.py           # NEW: Mode-aware client abstraction
│   ├── scope_authorization.py      # MODIFIED: Mode-aware scope checking
│   └── context_helper.py           # MODIFIED: Support both Context and NextcloudApp
├── client/                         # NO CHANGES (100% shared)
│   ├── notes.py
│   ├── calendar.py
│   └── ...
├── server/                         # NO CHANGES (100% shared)
│   ├── notes.py
│   ├── calendar.py
│   └── ...
└── models/                         # NO CHANGES (100% shared)

appinfo/                            # NEW: ExApp manifest
├── info.xml                        # ExApp metadata and permissions
└── routes.xml                      # Proxy route configuration

Feature Parity Matrix

The following table documents feature support and differences between OAuth and AppAPI deployment modes. Understanding these differences is critical for users choosing a deployment mode.

Feature Category Feature OAuth Mode AppAPI Mode Notes
Authentication
Multi-user support Per-user tokens Per-request user context Both support multiple users
Non-browser clients OAuth flow ⚠️ Requires app passwords AppAPI needs BasicAuth support
Token management Refresh tokens N/A Shared secret OAuth uses refresh tokens
Fine-grained scopes Per-user consent ⚠️ Admin-granted AppAPI scopes set at install
Token revocation Per-user ⚠️ Uninstall app Different security models
Core Functionality
Tool execution Full support Full support All MCP tools work
Resource queries Full support Full support No functional differences
Error handling Full support Full support Same error reporting
Streaming & Communication
HTTP streaming Real-time ⚠️ Buffered AppAPI accumulates complete response
SSE transport Real-time events ⚠️ Buffered stream AppAPI receives all events at once
Streamable HTTP Real-time ⚠️ Buffered Works but not incremental
WebSocket ⚠️ Not implemented Not possible Neither mode supports currently
stdio transport Not applicable ⚠️ Future possibility Would require AppAPI changes
Progress & Notifications
Progress updates Real-time via MCP No channel Critical difference
MCP notifications Server → Client Impossible No persistent connection
Nextcloud notifications N/A ExApp → NC UI AppAPI advantage
Long-running ops With progress ⚠️ Background + notification Different UX patterns
Advanced Features
Sampling (LLM/RAG) Full support Not possible Critical limitation
Background sync workers With refresh tokens ⚠️ Via NC cron Different mechanisms
Vector database sync Background workers ⚠️ Webhook-driven Different sync strategies
Semantic search With sampling ⚠️ Without sampling Limited in AppAPI mode
Integration
Nextcloud UI components External service Native integration AppAPI advantage
File actions Not possible Right-click menus AppAPI advantage
Dashboard widgets Not possible Native widgets AppAPI advantage
Event listeners ⚠️ Via polling Via webhooks AppAPI advantage
Settings page Separate UI NC settings AppAPI advantage
Deployment
Installation ⚠️ Manual OAuth setup One-click install AppAPI advantage
Multi-tenant One server, many NC instances Single NC instance OAuth advantage
Standalone deployment Independent service Requires Nextcloud OAuth advantage
Container management Manual NC-managed AppAPI advantage
Updates Manual Via app store AppAPI advantage
Development & Testing
Local development Simple setup ⚠️ Requires NC instance OAuth easier for dev
Testing infrastructure Well established ⚠️ Needs work OAuth tests mature
Debug tooling Standard HTTP ⚠️ Via proxy OAuth simpler

Legend:

  • Full Support: Feature works as expected with no limitations
  • ⚠️ Partial/Different: Feature works but with limitations or different implementation
  • Not Supported: Feature not available or not possible
  • N/A Not Applicable: Feature doesn't apply to this mode

Key Takeaways:

  1. OAuth mode is best for:

    • Multi-tenant SaaS deployments
    • External MCP client access (Claude Desktop, custom clients)
    • Features requiring sampling/LLM generation (RAG)
    • Real-time progress updates
    • Standalone service deployments
  2. AppAPI mode is best for:

    • Single-tenant Nextcloud integration
    • Simplified installation and management
    • Native UI components and event listeners
    • Admin-controlled deployments
    • Nextcloud app store distribution
  3. Critical limitations in AppAPI mode:

    • No sampling support (RAG features unavailable)
    • No real-time progress updates (use Nextcloud notifications instead)
    • Buffered streaming only (not true real-time)
    • Requires app passwords for non-browser clients (needs AppAPI enhancement)

Implementation Details

1. Mode Detection (app.py)

import os
from typing import Literal

def get_deployment_mode() -> Literal["oauth", "exapp"]:
    """Detect deployment mode from environment."""
    if os.getenv("APPAPI_MODE") == "true":
        return "exapp"
    return "oauth"  # Default for backwards compatibility

def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
    """Get FastMCP app configured for the detected deployment mode."""
    mode = get_deployment_mode()

    if mode == "exapp":
        from nextcloud_mcp_server.app_exapp import create_exapp_app
        return create_exapp_app(transport, enabled_apps)
    else:
        from nextcloud_mcp_server.app_oauth import create_oauth_app
        return create_oauth_app(transport, enabled_apps)

# Entry point for MCP CLI
mcp = get_app()

2. Client Abstraction (auth/client_factory.py)

from typing import Union
from mcp.server.fastmcp import Context

# Conditional import (only available if nc_py_api installed)
try:
    from nc_py_api import NextcloudApp
except ImportError:
    NextcloudApp = None  # type: ignore

async def get_client(ctx: Union[Context, NextcloudApp]) -> NextcloudClient:
    """Get authenticated NextcloudClient for the current request (mode-aware)."""

    # AppAPI mode: ctx is NextcloudApp from nc_py_api
    if NextcloudApp and isinstance(ctx, NextcloudApp):
        return await _get_exapp_client(ctx)

    # OAuth mode: ctx is FastMCP Context
    if isinstance(ctx, Context):
        return await _get_oauth_client(ctx)

    raise TypeError(f"Unexpected context type: {type(ctx)}")

async def _get_exapp_client(nc: NextcloudApp) -> NextcloudClient:
    """Create NextcloudClient from nc_py_api NextcloudApp."""
    # Wrap nc_py_api session in NextcloudClient interface
    return NextcloudClient.from_nc_py_api(nc)

async def _get_oauth_client(ctx: Context) -> NextcloudClient:
    """Create NextcloudClient from OAuth access token."""
    # Existing OAuth logic from context_helper.py
    access_token = ctx.request_context.access_token
    username = access_token.claims.get("preferred_username")
    return NextcloudClient.from_token(
        base_url=settings.nextcloud_host,
        token=access_token.token,
        username=username
    )

3. Tool Signatures (server/notes.py)

from typing import Union
from mcp.server.fastmcp import Context

try:
    from nc_py_api import NextcloudApp
except ImportError:
    NextcloudApp = type(None)  # Fallback if not installed

@mcp.tool()
@require_scopes("notes:write")
async def nc_notes_create_note(
    title: str,
    content: str,
    category: str,
    ctx: Union[Context, NextcloudApp],  # Union type for both modes
) -> CreateNoteResponse:
    """Create a new note."""
    client = await get_client(ctx)  # Mode-aware helper
    result = await client.notes.create_note(
        title=title,
        content=content,
        category=category
    )
    return CreateNoteResponse(
        success=True,
        note=result,
        message=f"Created note '{title}'"
    )

Key Points:

  • Tools accept Context | NextcloudApp union type
  • No changes to business logic or client calls
  • get_client() abstraction handles mode detection

4. Scope Authorization (auth/scope_authorization.py)

def require_scopes(*required_scopes: str):
    """Decorator for scope/permission checking (mode-aware)."""

    def decorator(func: Callable) -> Callable:
        func._required_scopes = list(required_scopes)

        @wraps(func)
        async def wrapper(*args: Any, **kwargs: Any) -> Any:
            ctx = kwargs.get("ctx")

            # AppAPI mode: Check Nextcloud permissions
            if NextcloudApp and isinstance(ctx, NextcloudApp):
                await _check_exapp_permissions(ctx, required_scopes)

            # OAuth mode: Check token scopes (existing logic)
            elif isinstance(ctx, Context):
                await _check_oauth_scopes(ctx, required_scopes)

            return await func(*args, **kwargs)

        return wrapper
    return decorator

async def _check_exapp_permissions(nc: NextcloudApp, scopes: list[str]) -> None:
    """Validate ExApp has required permissions via Nextcloud capabilities."""
    # In AppAPI mode, permissions are granted at install time
    # Check against user's Nextcloud capabilities for additional validation
    # E.g., "notes:write" → verify user has notes app enabled
    pass  # Implementation depends on Nextcloud capability API

async def _check_oauth_scopes(ctx: Context, scopes: list[str]) -> None:
    """Validate OAuth token contains required scopes (existing logic)."""
    # ... existing implementation

5. Dependency Management (pyproject.toml)

[project]
dependencies = [
    "mcp[cli] >=1.21,<1.22",
    "httpx >=0.28.1,<0.29.0",
    "pydantic >=2.10.4,<2.11.0",
    # ... other shared dependencies
]

[dependency-groups]
oauth = [
    "authlib >=1.6.5",
    "pyjwt[crypto] >=2.8.0",
    "jwcrypto >=1.5.6",
    "aiosqlite >=0.20.0",
]
exapp = [
    "nc_py_api >=0.21.0",
]
dev = [
    # ... existing dev dependencies
]

Installation:

# OAuth mode (current default)
uv sync --group oauth

# AppAPI mode
uv sync --group exapp

# Development (both modes)
uv sync --group oauth --group exapp --group dev

6. Docker Multi-Stage Build (Dockerfile)

# Stage 1: Base image with shared dependencies
FROM python:3.11-slim AS base
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --no-dev

# Stage 2: OAuth variant (default)
FROM base AS oauth
RUN uv sync --group oauth --no-dev
COPY nextcloud_mcp_server/ ./nextcloud_mcp_server/
ENV DEPLOYMENT_MODE=oauth
EXPOSE 8000
CMD ["uv", "run", "mcp", "run", "nextcloud_mcp_server.app:mcp"]

# Stage 3: AppAPI variant
FROM base AS exapp
RUN uv sync --group exapp --no-dev
COPY nextcloud_mcp_server/ ./nextcloud_mcp_server/
COPY appinfo/ ./appinfo/
ENV APPAPI_MODE=true
EXPOSE 8000
CMD ["uv", "run", "python", "-m", "nextcloud_mcp_server.app_exapp"]

Build Commands:

# Build OAuth variant
docker build --target oauth -t nextcloud-mcp-server:oauth .

# Build AppAPI variant
docker build --target exapp -t nextcloud-mcp-server:exapp .

# Multi-platform builds
docker buildx build --platform linux/amd64,linux/arm64 \
  --target oauth -t ghcr.io/cbcoutinho/nextcloud-mcp-server:oauth --push .

7. ExApp Manifest (appinfo/info.xml)

<?xml version="1.0"?>
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
    <id>nextcloud_mcp_server</id>
    <name>Nextcloud MCP Server</name>
    <summary>Model Context Protocol server for AI assistant integration</summary>
    <description><![CDATA[
    Exposes Nextcloud functionality via the Model Context Protocol (MCP).
    Enables AI assistants like Claude to access Notes, Calendar, Contacts, Files, and more.
    Supports semantic search across all Nextcloud content via vector embeddings.
    ]]></description>

    <version>0.33.0</version>
    <licence>agpl</licence>
    <author>Chris Coutinho</author>

    <namespace>NextcloudMcpServer</namespace>
    <category>integration</category>
    <category>tools</category>

    <bugs>https://github.com/cbcoutinho/nextcloud-mcp-server/issues</bugs>
    <repository>https://github.com/cbcoutinho/nextcloud-mcp-server</repository>

    <dependencies>
        <nextcloud min-version="30" max-version="31"/>
    </dependencies>

    <external-app>
        <docker-install>
            <registry>ghcr.io</registry>
            <image>cbcoutinho/nextcloud-mcp-server</image>
            <image-tag>exapp</image-tag>
        </docker-install>

        <scopes>
            <value>FILES</value>
            <value>NOTIFICATIONS</value>
            <value>CALENDAR</value>
            <value>CONTACTS</value>
            <value>ALL</value>
        </scopes>

        <routes>
            <route>
                <url>/mcp.*</url>
                <verb>GET,POST</verb>
                <access_level>USER</access_level>
                <headers_to_exclude>[]</headers_to_exclude>
            </route>
        </routes>
    </external-app>
</info>

Proxy Route: /index.php/apps/app_api/proxy/nextcloud_mcp_server/mcp/sse

Configuration

OAuth Mode (Existing)

# Environment variables (unchanged)
NEXTCLOUD_HOST=https://nextcloud.example.com
OIDC_CLIENT_ID=abc123
OIDC_CLIENT_SECRET=xyz789
ENABLE_TOKEN_EXCHANGE=false

AppAPI Mode (New)

# Environment variables
APPAPI_MODE=true
APP_ID=nextcloud_mcp_server
APP_SECRET=<shared-secret-from-appapi>
APP_VERSION=0.33.0
NEXTCLOUD_URL=https://nextcloud.example.com

MCP Client Configuration

OAuth Mode

{
  "mcpServers": {
    "nextcloud": {
      "url": "https://mcp.example.com/sse",
      "transport": "sse"
    }
  }
}

AppAPI Mode (via Nextcloud Proxy)

{
  "mcpServers": {
    "nextcloud": {
      "url": "https://nextcloud.example.com/index.php/apps/app_api/proxy/nextcloud_mcp_server/mcp/sse",
      "transport": "sse",
      "headers": {
        "Cookie": "nc_session=<user-session-cookie>"
      }
    }
  }
}

Consequences

Positive

  1. Flexible Deployment: Users choose the mode that fits their use case—OAuth for multi-tenant SaaS, AppAPI for single-tenant integration.

  2. Simplified Administration: AppAPI mode enables one-click installation via Nextcloud app store, dramatically reducing setup complexity for single-tenant deployments.

  3. Native Nextcloud Integration: AppAPI mode unlocks UI components (menu items, file actions), event listeners, and dashboard widgets.

  4. No Breaking Changes: OAuth mode remains the default; all existing deployments continue working without modification.

  5. Maximum Code Sharing: 90%+ code reuse achieved through abstraction layer—tools, clients, models, and business logic fully shared.

  6. Future-Proof: As Nextcloud's ExApp ecosystem matures, we gain access to new integration capabilities without architectural changes.

  7. Ecosystem Compatibility: AppAPI mode makes the MCP server compatible with other Nextcloud ExApps, enabling potential cross-app integrations.

Negative

  1. Increased Complexity: Abstraction layer adds indirection and complexity to the codebase. Developers must understand both authentication mechanisms.

  2. Testing Burden: Must test both modes in CI/CD. Integration test matrix doubles in size.

  3. Dependency Management: Optional dependencies (authlib vs nc_py_api) complicate installation and development setup.

  4. Documentation Overhead: Users need clear guidance on when to use each mode, how they differ in behavior, and how to configure them.

  5. Maintenance Burden: Two authentication paths to maintain, two deployment modes to support, two sets of environment variables to document.

  6. Feature Parity Challenges: Some features may work better in one mode than the other (e.g., background jobs in OAuth mode, UI integration in AppAPI mode).

Neutral

  1. Separate Docker Images: Users explicitly choose via image tag (:oauth or :exapp), making mode selection clear but requiring separate builds.

  2. Mode-Specific Features: Some capabilities only work in specific modes (progressive consent in OAuth, UI registration in AppAPI).

  3. Community Bifurcation: User base may split between OAuth and AppAPI camps, potentially fragmenting community support.

  4. AppAPI Dependency: AppAPI mode's stability depends on Nextcloud's AppAPI maintenance and evolution.

Alternative Approaches Considered

Alternative 1: OAuth-Only (Status Quo)

Description: Continue with current OAuth architecture, do not add AppAPI support.

Pros:

  • No additional complexity
  • Single authentication mechanism to maintain
  • Works today with multi-tenant support

Cons:

  • Misses opportunity for simplified Nextcloud integration
  • Higher barrier to entry for single-tenant users
  • Cannot leverage ExApp ecosystem benefits
  • Requires separate deployment and management infrastructure

Rejected Because: Ignores clear demand for native Nextcloud integration and simpler deployment model.

Alternative 2: Migrate Fully to AppAPI

Description: Remove OAuth support entirely, implement only AppAPI authentication.

Pros:

  • Simpler codebase (single authentication mechanism)
  • Tight Nextcloud integration
  • Easier administration for Nextcloud users

Cons:

  • Breaking change: All existing OAuth deployments stop working
  • Loses multi-tenant capability: Cannot serve multiple Nextcloud instances from one MCP server
  • External client support lost: MCP clients not integrated with Nextcloud cannot connect
  • Background job complexity: Would need to reimplement background sync using Nextcloud cron

Rejected Because: Breaks existing deployments and eliminates valuable multi-tenant capabilities.

Alternative 3: Separate Repositories

Description: Fork codebase into nextcloud-mcp-server (OAuth) and nextcloud-mcp-exapp (AppAPI).

Pros:

  • Clean separation of concerns
  • No abstraction layer complexity
  • Independent versioning and release cycles

Cons:

  • Massive code duplication: 90%+ of code (tools, clients, models) duplicated
  • Maintenance nightmare: Bug fixes must be applied to both repos
  • Community fragmentation: Two projects instead of one unified effort
  • Feature divergence: Repositories inevitably drift apart over time

Rejected Because: Maintenance burden far outweighs benefits of clean separation.

Alternative 4: AppAPI Plugin to Existing OAuth Server

Description: Keep OAuth server unchanged, create separate AppAPI plugin that proxies to it.

Pros:

  • No changes to existing OAuth server
  • AppAPI support added as separate component

Cons:

  • Double proxying: Client → Nextcloud → AppAPI plugin → OAuth MCP server → Nextcloud
  • Authentication complexity: AppAPI plugin must translate headers to OAuth tokens
  • Performance overhead: Extra network hops add latency
  • Deployment complexity: Two containers to manage instead of one

Rejected Because: Architectural complexity and performance overhead unjustified.

Alternative 5: Runtime Mode Switching (Single Image)

Description: Single Docker image that detects mode at runtime via environment variable (no separate builds).

Pros:

  • Single image to build and distribute
  • Simplest CI/CD pipeline
  • Users can switch modes by changing environment variables

Cons:

  • Larger image size: Includes both authlib and nc_py_api dependencies
  • Unused code: OAuth-only deployments carry AppAPI code and vice versa
  • Security surface: All dependencies present even if not used

Decision: Implement multi-stage Docker builds instead for smaller, mode-specific images.

Implementation Plan

Phase 1: Foundation (Days 1-3)

Goal: Create abstraction layer without breaking existing OAuth functionality.

  1. Extract OAuth setup from app.py into app_oauth.py
  2. Add mode detection function to app.py
  3. Create auth/client_factory.py with get_client() abstraction
  4. Update context.py to accept Context | NextcloudApp union type
  5. Make @require_scopes decorator mode-aware
  6. Add conditional imports for nc_py_api (graceful degradation if not installed)
  7. Run full test suite—all existing OAuth tests must pass

Phase 2: AppAPI Implementation (Days 4-6)

Goal: Implement AppAPI mode and achieve feature parity with OAuth.

  1. Create app_exapp.py with FastAPI + AppAPIAuthMiddleware setup
  2. Implement auth/appapi_auth.py for header validation
  3. Add NextcloudClient.from_nc_py_api() adapter method
  4. Create appinfo/info.xml ExApp manifest
  5. Configure proxy routes for /mcp/* endpoints
  6. Implement ExApp-specific permission checking in scope_authorization.py
  7. Manual testing: Register ExApp in test Nextcloud instance, verify connection

Phase 3: Build & Deployment (Days 7-8)

Goal: Prepare distribution artifacts for both modes.

  1. Update Dockerfile with multi-stage builds (base → oauth, base → exapp)
  2. Add dependency groups to pyproject.toml (oauth, exapp)
  3. Update CI/CD pipeline to build both Docker images (:oauth, :exapp)
  4. Create AppAPI deployment documentation (docs/appapi-deployment.md)
  5. Update docs/installation.md with deployment options section
  6. Update docs/authentication.md with AppAPI mode details
  7. Test Docker builds locally for both modes

Phase 4: Testing & Documentation (Days 9-10)

Goal: Ensure both modes work correctly and are well-documented.

  1. Update tests/conftest.py to support mode-based fixtures
  2. Add TEST_MODE environment variable for test execution
  3. Run integration tests against both modes
  4. Add AppAPI-specific integration tests (proxy, header validation)
  5. Update README.md with deployment options comparison table
  6. Create this ADR (ADR-011)
  7. Write blog post / announcement explaining hybrid architecture

Total Estimated Time: 10 days

Success Criteria

  • All existing OAuth tests pass without modification
  • New AppAPI integration tests pass
  • Both Docker images build successfully
  • ExApp can be registered in test Nextcloud instance
  • MCP client can connect via AppAPI proxy
  • All MCP tools work in both modes
  • No breaking changes for existing OAuth deployments
  • Documentation covers both deployment modes

To Update

  • docs/installation.md - Add AppAPI installation section
  • docs/authentication.md - Document both authentication modes
  • docs/configuration.md - Add AppAPI environment variables
  • README.md - Add deployment options comparison

To Create

  • docs/appapi-deployment.md - Comprehensive AppAPI setup guide
  • docs/architecture-hybrid.md - Detailed hybrid architecture documentation
  • docs/comparison-oauth-vs-appapi.md - When to use each mode
  • ADR-004: Progressive Consent (OAuth-specific, note AppAPI differences)
  • ADR-005: Token Audience Validation (OAuth-specific)
  • ADR-007: Background Vector Sync (note implications for AppAPI mode)

Open Questions

Resolved via Research (2025-01-13)

The following questions have been investigated and resolved through detailed analysis of AppAPI and nc_py_api codebases:

1. Non-Browser Client Authentication Resolved

Question: How do MCP clients (Claude Desktop, CLI tools) authenticate to the AppAPI proxy when they can't handle browser-based session cookies?

Answer: AppAPI proxy currently relies on Nextcloud session cookies. Non-browser clients require one of:

  • App Passwords (Recommended Phase 1): Add BasicAuth support to AppAPI proxy. Users generate app passwords in Nextcloud UI, MCP clients send Authorization: Basic headers. Requires upstream contribution to AppAPI.
  • OAuth 2.0 (Long-term): Integrate MCP OAuth with Nextcloud OIDC provider, validate Bearer tokens via introspection.

Decision: Document app password requirement. Contribute BasicAuth support to AppAPI upstream. See "Authentication Challenges" section for implementation details.

2. Streaming Transport Compatibility Resolved

Question: How does the AppAPI proxy handle streamable HTTP? Will it work when MCP deprecates SSE?

Answer: AppAPI proxy supports buffered streaming via Guzzle's stream: true, not real-time streaming:

  • ExApp generates response chunks
  • Proxy accumulates complete response
  • Client receives full stream after completion
  • Works for bounded operations (finite event streams)
  • Does NOT support: Real-time incremental streaming, WebSocket protocol

Transport Compatibility:

  • Streamable HTTP (MCP 2025-03-26+): Works today, buffered
  • WebSocket: Would require major proxy rewrite (not viable)
  • ⚠️ stdio: Ideal for containerized ExApps but requires AppAPI changes (docker exec -i support)

Decision: Use Streamable HTTP transport for AppAPI mode. Document buffering limitations. Advocate for stdio transport support in future AppAPI versions. See "Streaming Limitations" section for details.

3. Callbacks and Progress Updates Resolved

Question: How do callbacks/webhooks work with ExApps? Can MCP's notification model work through the proxy?

Answer: Fundamental incompatibility between MCP's notification model and AppAPI's request/response architecture:

What DOES work:

  • Webhooks (Nextcloud → ExApp): Well supported via webhooks_listener
  • Notifications (ExApp → Nextcloud): Via nc.notifications.create()
  • Background tasks: Via FastAPI BackgroundTasks

What DOESN'T work:

  • MCP protocol notifications (server → client): No persistent connection through proxy
  • Real-time progress updates: No channel to original MCP client
  • Sampling/RAG features: Requires bidirectional MCP protocol

Decision:

  • Short operations (<30s): Direct request/response (works fine)
  • Long operations: Use BackgroundTasks + Nextcloud notifications
  • Sampling features: OAuth mode only (not possible in AppAPI)
  • Document limitations clearly in Feature Parity Matrix

See "Notification and Progress Update Limitations" section for workarounds.

Remaining Open Questions

4. Scope-to-Permission Mapping: How should MCP scopes (e.g., notes:write) map to Nextcloud capability checks? Need Nextcloud capability API documentation to implement robust permission validation in AppAPI mode.

5. Background Jobs Architecture: OAuth mode uses background workers with refresh tokens. AppAPI mode should likely use:

  • Webhook-driven processing (ADR-010 pattern)
  • Nextcloud cron jobs for periodic tasks
  • Different sync strategy than OAuth mode

6. UI Integration Priority: Should initial AppAPI implementation include UI components (menu items, file actions), or ship with proxy support only and add UI later? Recommend: Phase 1 without UI, add in Phase 2 if needed.

7. Version Compatibility: What range of Nextcloud versions should we support? info.xml currently specifies 30-31. Should we support older versions (28-29)?

These remaining questions should be resolved during implementation. Create follow-up issues to track decisions.

Conclusion

The hybrid OAuth + AppAPI architecture provides the best of both worlds: flexible multi-tenant OAuth deployments for standalone MCP servers, and simplified single-tenant AppAPI deployments for native Nextcloud integration. The abstraction layer achieves 90%+ code sharing, maintaining a single source of truth for business logic while supporting two distinct authentication mechanisms.

This approach respects existing OAuth deployments (no breaking changes), serves the community's desire for simpler Nextcloud integration, and positions the project for future growth as Nextcloud's ExApp ecosystem matures. While the hybrid architecture adds complexity, the value delivered—deployment flexibility, native integration, and broader user base—justifies the investment.