415 lines
16 KiB
Markdown
415 lines
16 KiB
Markdown
# 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:
|
|
|
|
```bash
|
|
# 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
|
|
```
|
|
|
|
! Hint: If the tests are failing due to missing environment variables, then usually the correct .env has not been created or not correctly configured yet.
|
|
|
|
### Load Testing
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# Install dependencies
|
|
uv sync
|
|
|
|
# Install development dependencies
|
|
uv sync --group dev
|
|
```
|
|
|
|
### Database Inspection
|
|
|
|
**Docker Compose Database Credentials:**
|
|
- Root user: `root` / password: `password`
|
|
- App user: `nextcloud` / password: `password`
|
|
- Database: `nextcloud`
|
|
|
|
**Common Database Commands:**
|
|
```bash
|
|
# Connect to database as root (most common for inspection)
|
|
docker compose exec db mariadb -u root -ppassword nextcloud
|
|
|
|
# Check OAuth clients
|
|
docker compose exec db mariadb -u root -ppassword nextcloud -e "SELECT id, name, token_type FROM oc_oidc_clients ORDER BY id DESC LIMIT 10;"
|
|
|
|
# Check OAuth client scopes
|
|
docker compose exec db mariadb -u root -ppassword nextcloud -e "SELECT c.id, c.name, s.scope FROM oc_oidc_clients c LEFT JOIN oc_oidc_client_scopes s ON c.id = s.client_id WHERE c.name LIKE '%MCP%';"
|
|
|
|
# Check OAuth access tokens
|
|
docker compose exec db mariadb -u root -ppassword nextcloud -e "SELECT id, client_id, user_id, created_at FROM oc_oidc_access_tokens ORDER BY created_at DESC LIMIT 10;"
|
|
```
|
|
|
|
**Important Tables:**
|
|
- `oc_oidc_clients` - OAuth client registrations (DCR clients)
|
|
- `oc_oidc_client_scopes` - Client allowed scopes
|
|
- `oc_oidc_access_tokens` - Issued access tokens
|
|
- `oc_oidc_authorization_codes` - Authorization codes
|
|
- `oc_oidc_registration_tokens` - RFC 7592 registration tokens for client management
|
|
- `oc_oidc_redirect_uris` - Redirect URIs for each client
|
|
|
|
## 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:**
|
|
```python
|
|
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:**
|
|
```bash
|
|
# 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
|