Files
nextcloud-mcp-server/CLAUDE.md
T
Chris Coutinho 1e877f17f7 test: Replace persistent OAuth client cache with session-scoped fixtures
Remove file-based caching of OAuth client credentials and implement automatic
client lifecycle management for test fixtures.

Changes:
- Add RFC 7592 client deletion function in auth/client_registration.py
- Remove cache_file parameter from _create_oauth_client_with_scopes helper
- Update all OAuth credential fixtures to use yield/finalizer pattern
- Add automatic client cleanup at end of test session (best-effort)
- Remove persistent .nextcloud_oauth_*.json cache files

Benefits:
- No persistent cache files cluttering repository
- Fresh OAuth clients created for each test session via DCR
- Automatic cleanup attempts (RFC 7592 DELETE endpoint)
- Cleaner test environment with proper fixture lifecycle

Note: Client deletion may fail due to Nextcloud authentication middleware
(logged as warning). The key improvement is removing persistent cache files.
OAuth clients may accumulate in Nextcloud but can be cleaned manually.
2025-10-24 08:11:22 +02:00

15 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Development Commands

Testing

The test suite is organized in layers for fast feedback:

# FAST FEEDBACK (recommended for development)
# Unit tests only - ~5 seconds
uv run pytest tests/unit/ -v

# Smoke tests - critical path validation - ~30-60 seconds
uv run pytest -m smoke -v

# INTEGRATION TESTS
# Integration tests without OAuth - ~2-3 minutes
uv run pytest -m "integration and not oauth" -v

# Full test suite - ~4-5 minutes
uv run pytest

# OAuth tests only (slowest, requires Playwright) - ~3 minutes
uv run pytest -m oauth -v

# COVERAGE
# Run tests with coverage
uv run pytest --cov

# LEGACY COMMANDS (still work)
# Run all integration tests
uv run pytest -m integration -v

# Skip integration tests
uv run pytest -m "not integration" -v

Load Testing

# Run benchmark with default settings (10 workers, 30 seconds)
uv run python -m tests.load.benchmark

# Quick test with custom concurrency and duration
uv run python -m tests.load.benchmark --concurrency 20 --duration 60

# Extended load test (50 workers for 5 minutes)
uv run python -m tests.load.benchmark -c 50 -d 300

# Export results to JSON for analysis
uv run python -m tests.load.benchmark -c 20 -d 60 --output results.json

# Test OAuth server on port 8001
uv run python -m tests.load.benchmark --url http://127.0.0.1:8001/mcp

# Verbose mode with detailed logging
uv run python -m tests.load.benchmark -c 10 -d 30 --verbose

Load Testing Features:

  • Mixed workload simulating realistic MCP usage (40% reads, 20% writes, 15% search, 25% other operations)
  • Real-time progress bar with live RPS and error counts
  • Detailed metrics:
    • Throughput (requests/second)
    • Latency percentiles (p50, p90, p95, p99)
    • Per-operation breakdown
    • Error rates and types
  • Automatic cleanup of test data
  • JSON export for CI/CD integration
  • Server health checks before starting

Understanding Results:

  • Requests/Second (RPS): Higher is better. Expected baseline: 50-200 RPS for mixed workload
  • Latency:
    • p50 (median): Should be <100ms for most operations
    • p95: Should be <500ms
    • p99: Should be <1000ms
  • Error Rate: Should be <1% under normal load

Common Bottlenecks:

  1. Nextcloud backend API response times (most common)
  2. Database connection limits
  3. HTTP client connection pooling
  4. Network I/O between containers

Code Quality

# Format and lint code
uv run ruff check
uv run ruff format

# Type checking
# No explicit type checker configured - this is a Python project using ruff for linting

Running the Server

# Local development - load environment variables and run
export $(grep -v '^#' .env | xargs)
mcp run --transport sse nextcloud_mcp_server.app:mcp

# Docker development environment with Nextcloud instance
docker-compose up

# After code changes, rebuild and restart the appropriate MCP server container:
# For basic auth changes (most common) - uses admin credentials
docker-compose up --build -d mcp

# For OAuth changes - uses OAuth authentication with JWT tokens
docker-compose up --build -d mcp-oauth

# Build Docker image
docker build -t nextcloud-mcp-server .

Important: MCP Server Containers

  • mcp (port 8000): Uses basic auth with admin credentials. Use this for most development and testing.
  • mcp-oauth (port 8001): Uses OAuth authentication with JWT tokens. Use this when working on OAuth-specific features or tests.
    • JWT tokens are used for testing (faster validation, scopes embedded in token)
    • The server can handle both JWT and opaque tokens via the token verifier

Environment Setup

# Install dependencies
uv sync

# Install development dependencies
uv sync --group dev

Architecture Overview

This is a Python MCP (Model Context Protocol) server that provides LLM integration with Nextcloud. The architecture follows a layered pattern:

Core Components

  • nextcloud_mcp_server/app.py - Main MCP server entry point using FastMCP framework
  • nextcloud_mcp_server/client/ - HTTP client implementations for different Nextcloud APIs
  • nextcloud_mcp_server/server/ - MCP tool/resource definitions that expose client functionality
  • nextcloud_mcp_server/controllers/ - Business logic controllers (e.g., notes search)

Client Architecture

  • NextcloudClient - Main orchestrating client that manages all app-specific clients
  • BaseNextcloudClient - Abstract base class providing common HTTP functionality and retry logic
  • App-specific clients: NotesClient, CalendarClient, ContactsClient, TablesClient, WebDAVClient

Server Integration

Each Nextcloud app has a corresponding server module that:

  1. Defines MCP tools using @mcp.tool() decorators
  2. Defines MCP resources using @mcp.resource() decorators
  3. Uses the context pattern to access the NextcloudClient instance

Supported Nextcloud Apps

  • Notes - Full CRUD operations and search
  • Calendar - CalDAV integration with events, recurring events, attendees, and tasks (VTODO)
    • Calendar Operations: List, create, delete calendars
    • Event Operations: Full CRUD, recurring events, attendees, reminders, bulk operations
    • Task Operations (VTODO): Full CRUD for CalDAV tasks with:
      • Status tracking (NEEDS-ACTION, IN-PROCESS, COMPLETED, CANCELLED)
      • Priority levels (0-9, 1=highest, 9=lowest)
      • Due dates, start dates, completion tracking
      • Percent complete (0-100%)
      • Categories and filtering
      • Search across all calendars
    • Note: Calendar implementation uses caldav library's AsyncDavClient
  • Contacts - CardDAV integration with address book operations
  • Tables - Row-level operations on Nextcloud Tables
  • WebDAV - Complete file system access

Key Patterns

  1. Environment-based configuration - Uses NextcloudClient.from_env() to load credentials from environment variables
  2. Async/await throughout - All operations are async using httpx
  3. Retry logic - @retry_on_429 decorator handles rate limiting
  4. Context injection - MCP context provides access to the authenticated client instance
  5. Modular design - Each Nextcloud app is isolated in its own client/server pair

MCP Response Patterns

CRITICAL: Never return raw List[Dict] from MCP tools - always wrap in Pydantic response models

FastMCP serialization issue: raw lists get mangled into dicts with numeric string keys.

Pattern:

  1. Client methods return List[Dict] (raw data)
  2. MCP tools convert to Pydantic models and wrap in response object
  3. Response models inherit from BaseResponse, include results field + metadata

Reference implementations:

  • SearchNotesResponse in nextcloud_mcp_server/models/notes.py:80
  • SearchFilesResponse in nextcloud_mcp_server/models/webdav.py:113
  • Tool examples: nextcloud_mcp_server/server/{notes,webdav}.py

Testing: Extract data["results"] from MCP responses, not data directly.

Testing Structure

The test suite follows a layered architecture for fast feedback:

tests/
├── unit/                    # Fast unit tests (~5s total)
│   ├── test_scope_decorator.py
│   └── test_response_models.py
├── smoke/                   # Critical path tests (~30-60s)
│   └── test_smoke.py
├── integration/
│   ├── client/             # Direct API layer tests
│   │   ├── notes/
│   │   ├── calendar/
│   │   └── ...
│   └── server/             # MCP tool layer tests
│       ├── oauth/          # OAuth-specific tests (slow, ~3min)
│       │   ├── test_oauth_core.py
│       │   ├── test_scope_authorization.py
│       │   └── ...
│       ├── test_mcp.py
│       └── ...
└── load/                   # Performance tests

Test Markers:

  • @pytest.mark.unit - Fast unit tests with mocked dependencies
  • @pytest.mark.integration - Integration tests requiring Docker containers
  • @pytest.mark.oauth - OAuth tests requiring Playwright (slowest)
  • @pytest.mark.smoke - Critical path smoke tests

Fixtures in tests/conftest.py - Shared test setup and utilities

  • Important: Integration tests run against live Docker containers. After making code changes:
    • For basic auth tests: rebuild with docker-compose up --build -d mcp
    • For OAuth tests: rebuild with docker-compose up --build -d mcp-oauth

Testing Best Practices

  • MANDATORY: Always run tests after implementing features or fixing bugs
    • Run tests to completion before considering any task complete
    • If tests require modifications to pass, ask for permission before proceeding
    • Rebuild the correct container after code changes:
      • For basic auth tests (most common): docker-compose up --build -d mcp
      • For OAuth tests: docker-compose up --build -d mcp-oauth
  • Use existing fixtures from tests/conftest.py to avoid duplicate setup work:
    • nc_mcp_client - MCP client session for tool/resource testing (uses mcp container)
    • nc_mcp_oauth_client - MCP client session for OAuth testing (uses mcp-oauth container)
    • nc_client - Direct NextcloudClient for setup/cleanup operations
    • temporary_note - Creates and cleans up test notes automatically
    • temporary_addressbook - Creates and cleans up test address books
    • temporary_contact - Creates and cleans up test contacts
  • Test specific functionality after changes:
    • For Notes changes: uv run pytest tests/server/test_mcp.py -k "notes" -v
    • For specific API changes: uv run pytest tests/client/notes/test_notes_api.py -v
    • For OAuth changes: uv run pytest tests/server/test_oauth*.py -v (remember to rebuild mcp-oauth container)
  • Avoid creating standalone test scripts - use pytest with proper fixtures instead

Writing Mocked Unit Tests

For client-layer tests that verify response parsing logic, use mocked HTTP responses instead of real network calls:

Pattern:

import httpx
import pytest
from nextcloud_mcp_server.client.notes import NotesClient
from tests.conftest import create_mock_note_response

async def test_notes_api_get_note(mocker):
    """Test that get_note correctly parses the API response."""
    # Create mock response using helper functions
    mock_response = create_mock_note_response(
        note_id=123,
        title="Test Note",
        content="Test content",
        category="Test",
        etag="abc123",
    )

    # Mock the _make_request method
    mock_client = mocker.AsyncMock(spec=httpx.AsyncClient)
    mock_make_request = mocker.patch.object(
        NotesClient, "_make_request", return_value=mock_response
    )

    # Create client and test
    client = NotesClient(mock_client, "testuser")
    note = await client.get_note(note_id=123)

    # Verify the response was parsed correctly
    assert note["id"] == 123
    assert note["title"] == "Test Note"
    # Verify the correct API endpoint was called
    mock_make_request.assert_called_once_with("GET", "/apps/notes/api/v1/notes/123")

Mock Response Helpers in tests/conftest.py:

  • create_mock_response() - Generic HTTP response builder
  • create_mock_note_response() - Pre-configured note response
  • create_mock_error_response() - Error responses (404, 412, etc.)

Benefits:

  • Fast execution (~0.1s vs minutes for integration tests)
  • 🔒 No Docker dependency
  • 🎯 Tests focus on response parsing logic
  • ♻️ Repeatable and deterministic

When to use:

  • Testing client methods that parse JSON responses
  • Testing error handling (404, 412, etc.)
  • Testing request parameter building

When NOT to use (keep as integration tests):

  • Complex protocol interactions (CalDAV, CardDAV, WebDAV)
  • Multi-component workflows (Notes + WebDAV attachments)
  • OAuth flows
  • End-to-end MCP tool testing

Reference Implementation:

  • See tests/client/notes/test_notes_api.py for complete examples
  • Mark unit tests with pytestmark = pytest.mark.unit
  • Run with: uv run pytest tests/unit/ tests/client/notes/test_notes_api.py -v

OAuth/OIDC Testing

OAuth integration tests use automated Playwright browser automation to complete the OAuth flow programmatically.

OAuth Testing Setup:

  • Main fixtures: nc_oauth_client, nc_mcp_oauth_client - Use Playwright automation
  • Shared OAuth Client: All test users authenticate using a single OAuth client
    • Created fresh for each test session via Dynamic Client Registration (DCR)
    • Matches production MCP server behavior (one client, multiple user tokens)
    • Each user gets their own unique access token
    • Automatic cleanup: Client is registered at session start, deleted at session end (RFC 7592)
    • Implementation: shared_oauth_client_credentials fixture in tests/conftest.py
    • Note: Client deletion may fail due to Nextcloud middleware (logged as warning). This doesn't affect tests.
  • Available fixtures: playwright_oauth_token, nc_oauth_client, nc_mcp_oauth_client
  • Multi-user fixtures: alice_oauth_token, bob_oauth_token, charlie_oauth_token, diana_oauth_token
  • Requirements: NEXTCLOUD_HOST, NEXTCLOUD_USERNAME, NEXTCLOUD_PASSWORD environment variables
  • Uses pytest-playwright-asyncio for async Playwright fixtures
  • Playwright configuration: Use pytest CLI args like --browser firefox --headed to customize
  • Install browsers: uv run playwright install firefox (or chromium, webkit)

Example Commands:

# Run all OAuth tests with Playwright automation using Firefox
uv run pytest tests/server/oauth/ --browser firefox -v

# Run specific OAuth test file with visible browser for debugging
uv run pytest tests/server/oauth/test_oauth_core.py --browser firefox --headed -v

# Run with Chromium (default) - use -m oauth marker for all OAuth tests
uv run pytest -m oauth -v

Test Environment:

  • Two MCP server containers are available:
    • mcp (port 8000): Uses basic auth with admin credentials - for most testing
    • mcp-oauth (port 8001): Uses OAuth authentication - for OAuth-specific testing
  • Start OAuth MCP server: docker-compose up --build -d mcp-oauth
  • Important: When working on OAuth functionality, always rebuild mcp-oauth container, not mcp

CI/CD Notes:

  • Playwright tests run in CI/CD environments
  • Use Firefox browser in CI: --browser firefox (Chromium may have issues with localhost redirects)

Configuration Files

  • pyproject.toml - Python project configuration using uv for dependency management
  • .env (from env.sample) - Environment variables for Nextcloud connection
  • docker-compose.yml - Complete development environment with Nextcloud + database

Integration testing with docker

Nextcloud

  • The app container is running nextcloud.
  • Use docker compose exec app php occ ... to get a list of available commands

Mariadb

  • The db container is running mariadb
  • Use docker compose exec db mariadb -u [user] -p [password] [database] to execute queries. Check the docker-compose file for credentials