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.
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:
- Nextcloud backend API response times (most common)
- Database connection limits
- HTTP client connection pooling
- 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 frameworknextcloud_mcp_server/client/- HTTP client implementations for different Nextcloud APIsnextcloud_mcp_server/server/- MCP tool/resource definitions that expose client functionalitynextcloud_mcp_server/controllers/- Business logic controllers (e.g., notes search)
Client Architecture
NextcloudClient- Main orchestrating client that manages all app-specific clientsBaseNextcloudClient- 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:
- Defines MCP tools using
@mcp.tool()decorators - Defines MCP resources using
@mcp.resource()decorators - Uses the context pattern to access the
NextcloudClientinstance
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
- Environment-based configuration - Uses
NextcloudClient.from_env()to load credentials from environment variables - Async/await throughout - All operations are async using httpx
- Retry logic -
@retry_on_429decorator handles rate limiting - Context injection - MCP context provides access to the authenticated client instance
- 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:
- Client methods return
List[Dict](raw data) - MCP tools convert to Pydantic models and wrap in response object
- Response models inherit from
BaseResponse, includeresultsfield + metadata
Reference implementations:
SearchNotesResponseinnextcloud_mcp_server/models/notes.py:80SearchFilesResponseinnextcloud_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
- For basic auth tests: rebuild with
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
- For basic auth tests (most common):
- Use existing fixtures from
tests/conftest.pyto avoid duplicate setup work:nc_mcp_client- MCP client session for tool/resource testing (usesmcpcontainer)nc_mcp_oauth_client- MCP client session for OAuth testing (usesmcp-oauthcontainer)nc_client- Direct NextcloudClient for setup/cleanup operationstemporary_note- Creates and cleans up test notes automaticallytemporary_addressbook- Creates and cleans up test address bookstemporary_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 rebuildmcp-oauthcontainer)
- For Notes changes:
- 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 buildercreate_mock_note_response()- Pre-configured note responsecreate_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.pyfor 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_credentialsfixture intests/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_PASSWORDenvironment variables - Uses
pytest-playwright-asynciofor async Playwright fixtures - Playwright configuration: Use pytest CLI args like
--browser firefox --headedto customize - Install browsers:
uv run playwright install firefox(orchromium,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 testingmcp-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-oauthcontainer, notmcp
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(fromenv.sample) - Environment variables for Nextcloud connectiondocker-compose.yml- Complete development environment with Nextcloud + database
Integration testing with docker
Nextcloud
- The
appcontainer is running nextcloud. - Use
docker compose exec app php occ ...to get a list of available commands
Mariadb
- The
dbcontainer is running mariadb - Use
docker compose exec db mariadb -u [user] -p [password] [database]to execute queries. Check the docker-compose file for credentials