Status: Proposed Date: 2025-01-22 Deciders: Development Team Related: ADR-004 (OAuth), ADR-007 (Background Vector Sync), ADR-015 (Unified Provider)
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
The current nextcloud-mcp-server architecture assumes a self-hosted deployment with:
-
Persistent Infrastructure
- Qdrant vector database for semantic search
- Background sync worker for content indexing
- Refresh token storage for offline access
-
Single-Tenant Configuration
- Environment variables configure one Nextcloud instance
NEXTCLOUD_HOST,NEXTCLOUD_USERNAME,NEXTCLOUD_PASSWORD- Or OAuth with a single IdP
-
Stateful Operations
- Vector sync maintains index state across requests
- Token storage persists between sessions
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
Many users have publicly accessible Nextcloud instances and want to:
- Try the MCP server without self-hosting infrastructure
- Connect multiple users to different Nextcloud instances
- Use basic Nextcloud tools without semantic search
- Benefit from Smithery's discovery and OAuth UI
Implement a stateless deployment mode for Smithery that:
- Disables stateful features (vector sync, semantic search)
- Creates clients per-session from Smithery configuration
- Supports multiple Nextcloud instances via session config
- Provides a useful subset of tools that work without infrastructure
┌─────────────────────────────────────────────────────────────────────────┐
│ 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 │ │
│ │◄───────────────────┤ │ │
│ │ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
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 | 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 | ✓ | ✗ |
# 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# 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)# 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 mcpThe /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 |
✓ | ✗ |
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()-
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
-
HTTPS Required
nextcloud_urlmust be HTTPS for production use- Validation rejects HTTP URLs in Smithery mode
-
No Credential Storage
- Credentials exist only for request duration
- No server-side persistence of user credentials
- Smithery handles secure config transmission
-
Scope Limitation
- Stateless mode cannot access offline_access
- No background operations on user's behalf
- Clear user expectation: tools work during session only
Users can start with Smithery stateless mode and migrate to self-hosted:
- Try on Smithery → Basic tools, no setup
- Self-host for semantic search → Add Qdrant, enable vector sync
- Full deployment → Background sync, webhooks, multi-user OAuth
- Lower barrier to entry - Users can try without infrastructure
- Multi-user support - Each session connects to different Nextcloud
- Smithery ecosystem - Discovery, observability, OAuth UI
- Clear feature tiers - Stateless (simple) vs self-hosted (full)
- No semantic search - Key differentiator unavailable on Smithery
- Per-request auth - Credentials sent with each request
- No offline access - Cannot perform background operations
- Maintenance burden - Two deployment modes to support
- Feature subset - May encourage users to self-host for full features
- Documentation needs - Clear guidance on mode differences required
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
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
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
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)