clockodo-mcp-server
# Clockodo MCP Server
MCP server wrapper for the Clockodo time tracking API with configurable feature sets.
[](https://lobehub.com/mcp/pfaeffli-clockodo-mcp-server)
[](https://github.com/pfaeffli/clockodo-mcp-server/pkgs/container/clockodo-mcp-server)
[](https://github.com/pfaeffli/clockodo-mcp-server/actions)
**š³ Docker Image:** `ghcr.io/pfaeffli/clockodo-mcp-server:latest`
## Table of Contents
- [Features](#features)
- [Architecture & Patterns](#architecture--patterns)
- [Setup](#setup)
- [Environment Variables](#environment-variables)
- [Available Features](#available-features)
- [Development](#development)
- [Manual Testing](#manual-testing)
## Features
This MCP server provides comprehensive time tracking capabilities through:
- **Tools**: 25+ tools for time tracking, HR analytics, and team management
- **Prompts**: Interactive prompt templates for common workflows
- **Resources**: Real-time access to time entries, customers, and services
- **Role-Based Access**: Configurable permission levels (employee, team_leader, hr_analytics, admin)
## Architecture & Patterns
This project follows specific architectural patterns to maintain clean, testable, and maintainable code.
### 1. **Layered Architecture**
```
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā MCP Server Layer (server.py) ā ā Tool registration, MCP protocol
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Service Layer (services/) ā ā Business logic, orchestration
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā Client Layer (client.py) ā ā HTTP API communication
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā¤
ā External API (Clockodo REST API) ā ā Third-party service
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
```
**Rules:**
- **Server Layer**: Only handles MCP tool registration and protocol. No business logic.
- **Service Layer**: Contains all business logic. Services use clients but never handle MCP directly.
- **Client Layer**: Pure HTTP/API client. No business logic, only request/response handling.
- **Dependencies flow downward only**: Server ā Service ā Client (never upward)
### 2. **Configuration Management**
**Pattern**: Feature Flags with Environment Variables
```python
# config.py - Central configuration
class ServerConfig:
hr_readonly: bool = True # Default safe
user_read: bool = False # Opt-in
admin_edit: bool = False # Explicit opt-in
@classmethod
def from_env(cls) -> "ServerConfig":
"""Load from environment with safe defaults"""
```
**Rules:**
- All configuration comes from environment variables
- Safe defaults (read-only, minimal permissions)
- Preset configurations available (readonly, user, admin)
- No hardcoded credentials or API keys
### 3. **Dependency Injection**
**Pattern**: Constructor Injection
```python
class HRService:
def __init__(self, client: ClockodoClient):
"""Inject dependencies explicitly"""
self.client = client
def check_overtime_compliance(self, year: int) -> dict:
# Use injected client
reports = self.client.get_user_reports(year=year)
```
**Rules:**
- Services receive their dependencies through constructors
- Makes testing easy (mock the dependencies)
- Clear dependency graph
- No global state or singletons (except config)
### 4. **Separation of Concerns**
**Pattern**: Single Responsibility Principle
```
client.py ā HTTP communication only
hr_analyzer.py ā Pure data analysis (no I/O)
hr_service.py ā Orchestration (client + analyzer)
hr_tools.py ā MCP tool wrappers (service ā MCP)
server.py ā Tool registration
```
**Rules:**
- Each module has ONE clear purpose
- Analyzers are pure functions (input ā output, no side effects)
- Services handle orchestration
- Tools are thin wrappers
### 5. **API Version Handling**
**Pattern**: Resource-Specific Versioning
Clockodo uses a resource-specific versioning scheme. This server always targets the most recent stable version for each resource:
- **v4**: Projects, Services, Absences
- **v3**: Users, Customers
- **v2**: Clock, Entries
- **v1**: User Reports (Legacy reports with no newer version available)
**Rules:**
- Base URL is normalized to end with `/api/`
- All client methods explicitly use the required version prefix (e.g., `v3/users`)
- Responses are normalized to maintain internal consistency (e.g., mapping `data` key to resource-specific keys)
- Legacy v1 endpoints are called without a version prefix
### 6. **Error Handling**
**Pattern**: Let Errors Bubble Up with Context
```python
def _request(self, method: str, endpoint: str) -> dict:
resp = httpx.request(...)
resp.raise_for_status() # Let HTTPStatusError bubble up
return resp.json()
```
**Rules:**
- Don't catch exceptions unless you can handle them
- Use httpx's built-in error handling
- Add context when re-raising
- Let MCP framework handle final error presentation
### 7. **Type Safety**
**Pattern**: Type Hints Everywhere
```python
def check_overtime_compliance(
self, year: int, max_overtime_hours: float = 80
) -> dict:
"""
Clear input/output types
Args:
year: Year to check (e.g., 2024)
max_overtime_hours: Maximum allowed overtime hours
Returns:
Dictionary with overtime violations
"""
```
**Rules:**
- All functions have type hints
- Use `from __future__ import annotations` for forward references
- Docstrings explain the structure of complex dicts
- mypy validation in CI/CD
### 8. **Testing Strategy**
**Pattern**: Layered Testing
```
Unit Tests ā Pure functions (analyzers)
Integration Tests ā Services with mocked clients
Manual Tests ā Jupyter notebooks for real API
```
**Rules:**
- Mock external HTTP calls (use respx)
- Test business logic in isolation
- Use pytest fixtures for common setup
- Manual testing with real credentials in notebooks
### 9. **Documentation as Code**
**Pattern**: Self-Documenting Code
```python
@mcp.tool()
def check_overtime_compliance(year: int, max_overtime_hours: float = 80) -> dict:
"""
Check which employees have excessive overtime.
This docstring becomes the MCP tool description.
"""
```
**Rules:**
- Docstrings on all public functions
- Type hints provide inline documentation
- README explains patterns and architecture
- Examples in manual-test/ folder
### 10. **Environment-Based Behavior**
**Pattern**: Configuration Over Code
```python
# Don't do this:
if production_mode:
do_something()
# Do this:
config = ServerConfig.from_env()
if config.is_enabled(FeatureGroup.ADMIN_EDIT):
register_admin_tools()
```
**Rules:**
- Feature flags control behavior
- No if/else for environments in code
- Test different configurations via env vars
- Document all environment variables
### 11. **Project Versioning**
**Pattern**: Automated Git Tag Versioning
The project version is automatically managed using `setuptools-scm` based on Git tags. This ensures that the version in `pyproject.toml` and at runtime always matches the latest Git tag.
**Rules:**
- Version is NOT hardcoded in `pyproject.toml` (uses `dynamic = ["version"]`)
- `src/clockodo_mcp/__init__.py` retrieves the version at runtime using `importlib.metadata` or a generated `_version.py` file
- New releases are created by pushing a signed tag (e.g., `git tag -s v0.3.0`), then publishing the GitHub release for it with `gh release create v0.3.0 --verify-tag`
- Publish the release before the tag's build finishes (about 7 minutes): the `attach-sbom` job uploads the SBOMs to that release. If it ran too early, publish the release and re-run only `attach-sbom`
- The version matches semantic versioning principles
---
## Setup
### Option 1: Using Pre-built Docker Image from GitHub Container Registry
#### For Local MCP Clients (Claude Desktop, IDEs) - stdio transport
Add configuration to your IDE's MCP settings (e.g., Claude Desktop):
```json
{
"mcpServers": {
"clockodo": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"-e",
"CLOCKODO_API_USER=your@email.com",
"-e",
"CLOCKODO_API_KEY=your_api_key",
"-e",
"CLOCKODO_USER_AGENT=my-company/1.0",
"-e",
"CLOCKODO_BASE_URL=https://my.clockodo.com/api/",
"-e",
"CLOCKODO_EXTERNAL_APP_CONTACT=dev@company.com",
"-e",
"CLOCKODO_MCP_ROLE=employee",
"ghcr.io/pfaeffli/clockodo-mcp-server:latest"
]
}
}
}
```
#### For Remote Access (Web Apps) - HTTP/SSE transport
> **ā ļø Note:** SSE transport is currently experimental and has known issues. Not recommended for production use.
```bash
docker run -d \
-p 127.0.0.1:8000:8000 \
-e CLOCKODO_API_USER=your@email.com \
-e CLOCKODO_API_KEY=your_api_key \
-e CLOCKODO_MCP_ROLE=employee \
-e CLOCKODO_MCP_TRANSPORT=sse \
-e CLOCKODO_MCP_HOST=0.0.0.0 \
-e CLOCKODO_MCP_AUTH_TOKEN=change-me-long-random-secret \
-e CLOCKODO_MCP_ALLOWED_HOSTS='localhost:*,127.0.0.1:*' \
-e CLOCKODO_MCP_PORT=8000 \
ghcr.io/pfaeffli/clockodo-mcp-server:latest
```
Clients must send `Authorization: Bearer <CLOCKODO_MCP_AUTH_TOKEN>`. The port is
published on `127.0.0.1` only; put a TLS-terminating reverse proxy in front for
anything beyond the local machine and add its hostname to
`CLOCKODO_MCP_ALLOWED_HOSTS`.
**Available image tags:**
- `latest` - Latest stable release
- `v1.0.0`, `v1.0`, `v1` - Semantic version tags
- `main-<sha>` - Latest main branch build
### Option 2: Build Locally
1. Build the Docker image:
```bash
make build-mcp
```
2. Add configuration to your IDE's MCP settings using `clockodo-mcp:latest` instead of the ghcr.io image.
## Environment Variables
### API Credentials (Required)
- `CLOCKODO_API_USER` - Your Clockodo email
- `CLOCKODO_API_KEY` - Your Clockodo API key
### API Configuration (Optional)
- `CLOCKODO_USER_AGENT` - Custom user agent string (default: "clockodo-mcp/unknown")
- `CLOCKODO_BASE_URL` - API base URL (default: "https://my.clockodo.com/api/")
- `CLOCKODO_EXTERNAL_APP_CONTACT` - Contact info for external app header (default: API user email)
### Time Zone (Optional)
- `CLOCKODO_TIMEZONE` - IANA zone used for times without an offset (default: "Europe/Zurich"); all times are sent to Clockodo as UTC
### Transport Configuration (Optional)
- `CLOCKODO_MCP_TRANSPORT` - Transport protocol (default: "stdio")
- `stdio` - Standard input/output for local processes (Claude Desktop, IDEs) **[Recommended]**
- `sse` - HTTP/SSE for remote access **[Experimental - Known Issues]**
- `CLOCKODO_MCP_HOST` - Host address to bind to (default: "127.0.0.1"; use "0.0.0.0" inside Docker)
- `CLOCKODO_MCP_PORT` - Port for SSE transport (default: 8000)
- `CLOCKODO_MCP_ALLOWED_HOSTS` - Comma-separated `Host` header allow-list for DNS-rebinding protection, always enabled (default: "127.0.0.1:*,localhost:*")
- `CLOCKODO_MCP_AUTH_TOKEN` - Bearer token for SSE. **Required** when the host is not loopback (the server refuses to start without it); optional but enforced when set on loopback. Compared in constant time.
Unknown `CLOCKODO_MCP_ROLE` or `CLOCKODO_MCP_TRANSPORT` values abort startup with an error. `clockodo-mcp --version` prints the version and exits.
> **ā ļø SSE Transport Limitation:** The SSE transport is experimental and currently has issues with the MCP library (mcp >= 2.3). The server accepts connections and messages but does not properly send responses back through the event stream, causing client initialization timeouts. **Use stdio transport for production.** SSE support depends on upstream fixes in the MCP library.
### Role Configuration (Recommended)
Use `CLOCKODO_MCP_ROLE` to set the user's role:
```bash
CLOCKODO_MCP_ROLE=employee # Default - Track your own time
CLOCKODO_MCP_ROLE=team_leader # Employee + approve vacations & edit team entries
CLOCKODO_MCP_ROLE=hr_analytics # View HR compliance reports only
CLOCKODO_MCP_ROLE=admin # Full access to everything
```
| Role | Can Do | Tools |
|------|--------|-------|
| **employee** | Track own time, request vacation | 16 |
| **team_leader** | Everything employee can + see users + approve team vacations + edit team entries | 24 |
| **hr_analytics** | View HR compliance reports (overtime, vacation violations) for all employees | 5 |
| **admin** | Full access to all features (incl. `get_raw_user_reports`) | 28 |
Everything is registered according to the role; tools, resources and prompts outside it do not exist for the client. Write tools carry MCP annotations (`destructiveHint` for edit/delete/approve/reject/adjust, `readOnlyHint` for reads) so clients can ask for confirmation. Tools returning Clockodo free text say so in their description: the text is user-provided data, not instructions.
### Legacy Configuration (Deprecated)
The following are still supported but deprecated. Use `CLOCKODO_MCP_ROLE` instead:
**Legacy Presets:**
- `CLOCKODO_MCP_PRESET=readonly` - Maps to hr_analytics role
- `CLOCKODO_MCP_PRESET=user` - Maps to employee role
- `CLOCKODO_MCP_PRESET=team_leader` - Maps to team_leader role
- `CLOCKODO_MCP_PRESET=admin` - Maps to admin role
**Legacy Granular Flags:**
- `CLOCKODO_MCP_ENABLE_HR_READONLY=true`
- `CLOCKODO_MCP_ENABLE_USER_READ=true`
- `CLOCKODO_MCP_ENABLE_USER_EDIT=true`
- `CLOCKODO_MCP_ENABLE_TEAM_LEADER=true`
- `CLOCKODO_MCP_ENABLE_ADMIN_READ=true`
- `CLOCKODO_MCP_ENABLE_ADMIN_EDIT=true`
## Available Features
### Core Tools
- `health` - Health check (always available; shows enabled features)
- `list_customers`, `list_services`, `list_projects` - Master data (`USER_READ`, `USER_EDIT`, `TEAM_LEADER` or `ADMIN_READ`)
- `list_users` - List all Clockodo users (`TEAM_LEADER`, `HR_READONLY` or `ADMIN_READ`; not for employees)
- `get_raw_user_reports(year)` - Raw API response for debugging (`ADMIN_READ` only)
### Prompts (when `USER_EDIT` enabled)
- `start_tracking` - Start tracking time for a customer and service (uses `start_my_clock`)
- `stop_tracking` - Stop tracking the current time entry (uses `stop_my_clock`)
- `request_vacation` - Request vacation time (uses `add_my_vacation`)
### Resources
- `clockodo://current-entry` - Currently running time entry (`USER_READ`)
- `clockodo://recent-entries` - Recent time entries, last 7 days (`USER_READ`)
- `clockodo://customers`, `clockodo://services`, `clockodo://projects` - Master data (same groups as the `list_*` tools)
### HR Analytics (when `HR_READONLY` enabled)
- `check_overtime_compliance(year, max_overtime_hours)` - Check employee overtime
- `check_vacation_compliance(year, min_vacation_days, max_vacation_remaining)` - Check vacation usage
- `get_hr_summary(year, ...)` - Complete HR compliance report
### User Tools (when `USER_READ` or `USER_EDIT` enabled)
- `get_my_clock()` - Get currently running clock
- `get_my_time_entries(time_since, time_until)` - Get your time entries
- `get_my_absences(year, absence_type=None)` - List your absences for a year (all statuses), including the `id` needed to delete or adjust them
- `start_my_clock(...)` - Start tracking time
- `stop_my_clock()` - Stop tracking time
- `add_my_time_entry(...)` - Add a manual time entry
- `edit_my_time_entry(entry_id, time_since=None, time_until=None, text=None, customers_id=None, services_id=None, projects_id=None, billable=None)` - Edit your time entry; only passed fields change (`billable`: 0/1/2; times as in `add_my_time_entry`)
- `delete_my_time_entry(entry_id)` - Delete your time entry
- `add_my_vacation(date_since, date_until, half_day=False)` - Request vacation; `half_day=True` books a half day (single day only)
- `add_my_sick_day(date_since, date_until, sick_note=False, child=False)` - Report a sick day; `child=True` books a sick day of a child
- `edit_my_vacation(absence_id, date_since=None, date_until=None, half_day=None)` - Change the dates or half-day flag of your absence
- `delete_my_vacation(absence_id)` - Delete an absence; approved absences are withdrawn (cancelled) first
### Team Leader Tools (when `TEAM_LEADER` enabled)
- `list_pending_vacation_requests(year)` - List all pending vacation requests
- `approve_vacation_request(absence_id)` - Approve a vacation request (refused for your own absence)
- `reject_vacation_request(absence_id)` - Reject a vacation request (refused for your own absence)
- `adjust_vacation_dates(absence_id, new_date_since, new_date_until)` - Adjust vacation length (refused for your own absence; use `edit_my_vacation`)
- `create_team_member_vacation(user_id, date_since, date_until, ...)` - Create vacation for team member; pending unless `auto_approve=True` (default `False`, refused for yourself)
- `edit_team_member_entry(entry_id, time_since=None, time_until=None, text=None, customers_id=None, services_id=None, projects_id=None, billable=None)` - Edit a team member's time entry; only passed fields change, the entry can't change user
- `delete_team_member_entry(entry_id)` - Delete team member's time entry
## Development
```bash
# Build
make build-mcp
# Run tests
make test
# Type checking
make type
# Linting
make lint
# Style check
make format-check
```
### Security Scanning
Run comprehensive security scans on the Docker image:
```bash
# Run all security scans (vulnerability, Docker best practices, licenses, SBOM)
make all-scans
# Individual scans
make vulnerability-scan # Trivy vulnerability scanning
make docker-scan # Dockle Docker best practices
make license-check # Python dependency license check
make sbom # Generate Software Bill of Materials
```
All security tools run via Docker containers - no local installation required.
## Manual Testing
For manual testing with real Clockodo API credentials, use the Jupyter notebook:
```bash
make manual-test
```
Open `http://localhost:8888` and navigate to `work/manual-test/test_clockodo.ipynb`.
See [manual-test/JUPYTER_TESTING.md](manual-test/JUPYTER_TESTING.md) for detailed instructions.
A live end-to-end QA suite against a Clockodo **trial** company is available via `make live-test`; see [manual-test/LIVE_TESTS.md](manual-test/LIVE_TESTS.md) (it writes and deletes data, never use production).
## Project Structure
```
clockodo-mcp/
āāā src/clockodo_mcp/
ā āāā server.py # MCP tool registration
ā āāā client.py # Clockodo API client
ā āāā config.py # Feature flag configuration
ā āāā hr_analyzer.py # Pure data analysis functions
ā āāā services/
ā ā āāā hr_service.py # Business logic orchestration
ā ā āāā user_service.py # User operations
ā ā āāā team_leader_service.py # Team leader operations
ā āāā tools/
ā āāā hr_tools.py # MCP tool wrappers
ā āāā user_tools.py # User tool wrappers
ā āāā team_leader_tools.py # Team leader tool wrappers
ā āāā debug_tools.py # Debugging utilities
āāā tests/ # Unit and integration tests
āāā manual-test/ # Jupyter notebooks for manual testing
āāā docker-compose.yml # Dev and server services
āāā docker-compose.test.yml # Test and Jupyter services
āāā makefile # Build and test targets
```
## Contributing
When adding new features, follow these patterns:
1. **New API Endpoint**: Add method to `client.py`
2. **Business Logic**: Create/update service in `services/`
3. **MCP Tool**: Add tool registration in `server.py`
4. **Tests**: Add unit tests in `tests/`
5. **Documentation**: Update README and docstrings
Always maintain the layered architecture: Server ā Service ā Client
TDQS
Scored across 16 tools
Tools target distinct resources and actions (time entries, clock control, absences, reference lists). Some overlap remains between add_my_time_entry and start_my_clock (both create entries) and between add_my_vacation and add_my_sick_day (both add absences), but descriptions clarify boundaries. Overall mostly distinct.
Strong verb_noun pattern with my_ for user-specific resources (get_my_*, add_my_*, edit_my_*, delete_my_*, start_my_clock/stop_my_clock) and list_* for global reference data. Minor inconsistencies: health lacks a verb, list_customers/projects/services omit my_ while get_my_absences includes it, and edit/delete_my_vacation actually operate on any absence.
16 tools for a personal time-tracking and absence-management integration is slightly above the ideal 15 but each tool maps to a distinct operation; no redundant tools. Health check is a minor meta addition. Scope is reasonable.
Core CRUD for time entries and clock start/stop/get is complete, and absences support add, edit, delete, and list. Notable gap: absence creation covers only vacation and sick day, not special leave (type 2) or overtime reduction (type 3), though those types can be listed and edited/deleted.