Skip to content

Latest commit

 

History

History
492 lines (386 loc) · 17.4 KB

File metadata and controls

492 lines (386 loc) · 17.4 KB

ADR-016: Smithery Stateless Deployment for Multi-User Public Nextcloud Instances

Status: Proposed Date: 2025-01-22 Deciders: Development Team Related: ADR-004 (OAuth), ADR-007 (Background Vector Sync), ADR-015 (Unified Provider)

Context

Smithery is a hosting platform and marketplace for MCP servers that provides:

  • Discovery: Marketplace listing for MCP servers
  • Hosting: Containerized deployment with auto-scaling
  • Authentication UI: OAuth flow presentation for users
  • Session Configuration: Per-user settings passed via URL parameters
  • Observability: Usage logs and monitoring

Current Architecture Limitations

The current nextcloud-mcp-server architecture assumes a self-hosted deployment with:

  1. Persistent Infrastructure

    • Qdrant vector database for semantic search
    • Background sync worker for content indexing
    • Refresh token storage for offline access
  2. Single-Tenant Configuration

    • Environment variables configure one Nextcloud instance
    • NEXTCLOUD_HOST, NEXTCLOUD_USERNAME, NEXTCLOUD_PASSWORD
    • Or OAuth with a single IdP
  3. Stateful Operations

    • Vector sync maintains index state across requests
    • Token storage persists between sessions

Smithery Hosting Constraints

Smithery-hosted containers are stateless by design:

  • No persistent storage between requests
  • No background workers or cron jobs
  • No databases (Qdrant, Redis, etc.)
  • Containers may be recycled at any time
  • Configuration passed per-session via URL parameters

Opportunity

Many users have publicly accessible Nextcloud instances and want to:

  1. Try the MCP server without self-hosting infrastructure
  2. Connect multiple users to different Nextcloud instances
  3. Use basic Nextcloud tools without semantic search
  4. Benefit from Smithery's discovery and OAuth UI

Decision

Implement a stateless deployment mode for Smithery that:

  1. Disables stateful features (vector sync, semantic search)
  2. Creates clients per-session from Smithery configuration
  3. Supports multiple Nextcloud instances via session config
  4. Provides a useful subset of tools that work without infrastructure

Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                    Smithery-Hosted Stateless Mode                        │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  MCP Client                    Smithery                                  │
│  (Cursor, Claude)              Infrastructure                            │
│        │                            │                                    │
│        │ 1. Connect                 │                                    │
│        ├───────────────────────────►│                                    │
│        │                            │                                    │
│        │ 2. Config UI               │                                    │
│        │◄───────────────────────────┤  User enters:                      │
│        │    (Smithery presents)     │  - nextcloud_url                   │
│        │                            │  - auth_mode (basic/oauth)         │
│        │                            │  - credentials                     │
│        │ 3. Tool call               │                                    │
│        ├───────────────────────────►│                                    │
│        │    + session config        │                                    │
│        │                            │                                    │
│        │                    ┌───────┴───────┐                            │
│        │                    │  MCP Server   │                            │
│        │                    │  Container    │                            │
│        │                    │               │                            │
│        │                    │ 4. Create     │                            │
│        │                    │    client     │                            │
│        │                    │    from       │                            │
│        │                    │    config     │                            │
│        │                    │      │        │                            │
│        │                    │      ▼        │                            │
│        │                    │ 5. Call       │                            │
│        │                    │    Nextcloud  │───────► User's Nextcloud   │
│        │                    │    API        │         Instance           │
│        │                    │      │        │                            │
│        │                    │      ▼        │                            │
│        │ 6. Response        │ Return result │                            │
│        │◄───────────────────┤               │                            │
│        │                    └───────────────┘                            │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Session Configuration Schema

from pydantic import BaseModel, Field

class SmitheryConfigSchema(BaseModel):
    """Configuration schema for Smithery session."""

    # Required: Nextcloud instance
    nextcloud_url: str = Field(
        ...,
        description="Your Nextcloud instance URL (e.g., https://cloud.example.com)"
    )

    # Authentication mode
    auth_mode: str = Field(
        "app_password",
        description="Authentication method: 'app_password' or 'oauth'"
    )

    # App Password authentication (recommended for Smithery)
    username: str | None = Field(
        None,
        description="Nextcloud username (required for app_password auth)"
    )
    app_password: str | None = Field(
        None,
        description="Nextcloud app password (Settings → Security → App passwords)"
    )

    # OAuth authentication (advanced)
    # When auth_mode='oauth', Smithery handles the OAuth flow
    # and passes the access token automatically

Feature Matrix

Feature Self-Hosted Smithery Stateless
Notes
List/Search notes
Get/Create/Update notes
Semantic search
Calendar
List calendars
Get/Create events
Contacts
List address books
Search/Get contacts
Files (WebDAV)
List/Download files
Upload files
Search files ✓ (keyword only)
Deck
List boards/cards
Create/Update cards
Tables
List/Query tables
Create/Update rows
Cookbook
List/Get recipes
Semantic Search
Vector search
RAG answers
Background Sync
Auto-indexing
Webhook sync
Admin UI (/app)
Vector sync status
Vector visualization
Webhook management
Session management

Implementation

1. Deployment Mode Detection

# nextcloud_mcp_server/config.py

class DeploymentMode(Enum):
    SELF_HOSTED = "self_hosted"      # Full features, env-based config
    SMITHERY_STATELESS = "smithery"  # Stateless, session-based config

def get_deployment_mode() -> DeploymentMode:
    """Detect deployment mode from environment."""
    if os.getenv("SMITHERY_DEPLOYMENT") == "true":
        return DeploymentMode.SMITHERY_STATELESS
    return DeploymentMode.SELF_HOSTED

2. Session-Based Client Factory

# nextcloud_mcp_server/context.py

async def get_client(ctx: Context) -> NextcloudClient:
    """Get NextcloudClient - from session config or environment."""

    mode = get_deployment_mode()

    if mode == DeploymentMode.SMITHERY_STATELESS:
        # Create client from Smithery session config
        config = ctx.session_config
        if not config:
            raise McpError("Session configuration required")

        return NextcloudClient(
            base_url=config.nextcloud_url,
            username=config.username,
            password=config.app_password,
        )
    else:
        # Existing behavior: from environment or OAuth context
        return await _get_client_from_context(ctx)

3. Conditional Tool Registration

# nextcloud_mcp_server/app.py

def create_mcp_server(mode: DeploymentMode) -> FastMCP:
    """Create MCP server with mode-appropriate tools."""

    mcp = FastMCP("Nextcloud MCP")

    # Always register core tools
    configure_notes_tools(mcp)
    configure_calendar_tools(mcp)
    configure_contacts_tools(mcp)
    configure_webdav_tools(mcp)
    configure_deck_tools(mcp)
    configure_tables_tools(mcp)
    configure_cookbook_tools(mcp)

    # Only register stateful tools in self-hosted mode
    if mode == DeploymentMode.SELF_HOSTED:
        configure_semantic_tools(mcp)  # Requires Qdrant
        register_oauth_tools(mcp)       # Requires token storage

    return mcp

4. Exclude Admin UI Routes

The /app admin UI should not be installed in Smithery mode because:

  • Vector sync status - No vector sync in stateless mode
  • Vector visualization - No Qdrant to visualize
  • Webhook management - No webhook sync without background workers
  • Session management - No persistent sessions to manage
# nextcloud_mcp_server/app.py

def create_app(mode: DeploymentMode) -> Starlette:
    """Create Starlette app with mode-appropriate routes."""

    routes = [
        Route("/health/live", health_live, methods=["GET"]),
        Route("/health/ready", health_ready, methods=["GET"]),
    ]

    # Only mount admin UI in self-hosted mode
    if mode == DeploymentMode.SELF_HOSTED:
        browser_app = create_browser_app()
        routes.append(
            Route("/app", lambda r: RedirectResponse("/app/", status_code=307))
        )
        routes.append(Mount("/app", app=browser_app))
        logger.info("Admin UI mounted at /app")
    else:
        logger.info("Admin UI disabled in Smithery stateless mode")

    # Mount FastMCP at root
    mcp_app = create_mcp_server(mode).streamable_http_app()
    routes.append(Mount("/", app=mcp_app))

    return Starlette(routes=routes, lifespan=starlette_lifespan)

Endpoints by Mode:

Endpoint Self-Hosted Smithery
/mcp
/health/live
/health/ready
/.well-known/mcp-config
/app
/app/vector-sync/status
/app/vector-viz
/app/webhooks

5. Smithery Integration Files

smithery.yaml:

runtime: "container"
build:
  dockerfile: "Dockerfile.smithery"
  dockerBuildPath: "."
startCommand:
  type: "http"
  configSchema:
    type: "object"
    required: ["nextcloud_url", "username", "app_password"]
    properties:
      nextcloud_url:
        type: "string"
        title: "Nextcloud URL"
        description: "Your Nextcloud instance URL (e.g., https://cloud.example.com)"
      username:
        type: "string"
        title: "Username"
        description: "Your Nextcloud username"
      app_password:
        type: "string"
        title: "App Password"
        description: "Generate at Settings → Security → App passwords"
  exampleConfig:
    nextcloud_url: "https://cloud.example.com"
    username: "alice"
    app_password: "xxxxx-xxxxx-xxxxx-xxxxx-xxxxx"

Dockerfile.smithery:

FROM python:3.11-slim

WORKDIR /app

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv

# Copy project files
COPY pyproject.toml uv.lock ./
COPY nextcloud_mcp_server ./nextcloud_mcp_server

# Install dependencies (without vector/semantic extras)
RUN uv sync --frozen --no-dev

# Set Smithery mode
ENV SMITHERY_DEPLOYMENT=true
ENV VECTOR_SYNC_ENABLED=false

# Smithery sets PORT=8081
EXPOSE 8081

CMD ["uv", "run", "python", "-m", "nextcloud_mcp_server.smithery_main"]

nextcloud_mcp_server/smithery_main.py:

"""Smithery-specific entrypoint for stateless deployment."""

import os
import uvicorn
from starlette.middleware.cors import CORSMiddleware

from nextcloud_mcp_server.app import create_mcp_server
from nextcloud_mcp_server.config import DeploymentMode

def main():
    # Force stateless mode
    os.environ["SMITHERY_DEPLOYMENT"] = "true"
    os.environ["VECTOR_SYNC_ENABLED"] = "false"

    mcp = create_mcp_server(DeploymentMode.SMITHERY_STATELESS)
    app = mcp.streamable_http_app()

    # Add CORS for browser-based clients
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["GET", "POST", "OPTIONS"],
        allow_headers=["*"],
        expose_headers=["mcp-session-id", "mcp-protocol-version"],
    )

    # Smithery sets PORT environment variable
    port = int(os.environ.get("PORT", 8081))
    uvicorn.run(app, host="0.0.0.0", port=port)

if __name__ == "__main__":
    main()

Security Considerations

  1. App Passwords over User Passwords

    • Smithery config encourages app passwords (revocable, scoped)
    • Documentation guides users to create dedicated app passwords
    • App passwords can be revoked without changing main password
  2. HTTPS Required

    • nextcloud_url must be HTTPS for production use
    • Validation rejects HTTP URLs in Smithery mode
  3. No Credential Storage

    • Credentials exist only for request duration
    • No server-side persistence of user credentials
    • Smithery handles secure config transmission
  4. Scope Limitation

    • Stateless mode cannot access offline_access
    • No background operations on user's behalf
    • Clear user expectation: tools work during session only

Migration Path

Users can start with Smithery stateless mode and migrate to self-hosted:

  1. Try on Smithery → Basic tools, no setup
  2. Self-host for semantic search → Add Qdrant, enable vector sync
  3. Full deployment → Background sync, webhooks, multi-user OAuth

Consequences

Positive

  1. Lower barrier to entry - Users can try without infrastructure
  2. Multi-user support - Each session connects to different Nextcloud
  3. Smithery ecosystem - Discovery, observability, OAuth UI
  4. Clear feature tiers - Stateless (simple) vs self-hosted (full)

Negative

  1. No semantic search - Key differentiator unavailable on Smithery
  2. Per-request auth - Credentials sent with each request
  3. No offline access - Cannot perform background operations
  4. Maintenance burden - Two deployment modes to support

Neutral

  1. Feature subset - May encourage users to self-host for full features
  2. Documentation needs - Clear guidance on mode differences required

Alternatives Considered

1. External MCP Only

Approach: Only support self-hosted external MCP registration on Smithery.

Rejected because:

  • Higher barrier to entry for new users
  • Misses opportunity for Smithery marketplace visibility
  • Users want to try before committing to infrastructure

2. Embedded Vector DB (SQLite-vec)

Approach: Use SQLite with vector extensions for per-request indexing.

Rejected because:

  • No persistence between requests anyway
  • Indexing latency too high for synchronous requests
  • Complexity without benefit in stateless context

3. External Vector DB Service

Approach: Connect to Pinecone/Weaviate Cloud from Smithery container.

Rejected because:

  • Adds external dependency and cost
  • Per-user collections require complex multi-tenancy
  • Sync still impossible without background workers

4. Hybrid: Smithery + User's Qdrant

Approach: User provides their own Qdrant URL in session config.

Considered for future:

  • Could enable semantic search for advanced users
  • Adds complexity to session config
  • Sync still requires external trigger (manual or webhook)

References