Skip to main content
Glama
ikido
by ikido
README.md
# Toggl MCP Server

A FastMCP-based Model Context Protocol (MCP) server that fetches and aggregates Toggl time tracking data with intelligent Fibery entity reference parsing and caching.

## Features

- **Time Entry Aggregation**: Fetch and aggregate time entries from Toggl API
- **Fibery Entity Parsing**: Automatically extract Fibery entity references from Toggl descriptions (#ID [DB] [TYPE])
- **Caching**: Smart caching with SQLite index and JSON file storage (1-hour TTL)
- **Rate Limiting**: Exponential backoff handling for Toggl API rate limits
- **User Filtering**: Optional filtering by single user or retrieve all workspace users
- **Date Range Validation**: Enforces ISO 8601 format and max 7-day ranges per request

## Installation

### Prerequisites

- Python 3.11+
- uv (package manager)

### Setup

1. Clone the repository:
```bash
git clone <repository-url>
cd toggl-mcp-custom
```

2. Create virtual environment:
```bash
uv venv
source .venv/bin/activate
```

3. Install dependencies:
```bash
uv pip install -r requirements.txt
```

4. Configure environment:
```bash
cp .env.example .env
# Edit .env with your Toggl API credentials
```

## Configuration

Create a `.env` file in the project root with your Toggl credentials:

```env
TOGGL_API_TOKEN=your_api_token_here
TOGGL_WORKSPACE_ID=your_workspace_id_here
```

Get your credentials:
- **API Token**: Available at https://track.toggl.com/app/profile
- **Workspace ID**: Available in Toggl settings or API responses

## Using with Claude Code

### Option 1: Project-Local Integration (Recommended)

The easiest way is to add the MCP server to your project's `.mcp.json` file:

```json
{
  "mcpServers": {
    "toggl": {
      "command": "/path/to/toggl-mcp-custom/.venv/bin/python",
      "args": ["-m", "toggl_mcp.server"],
      "cwd": "${workspaceRoot}"
    }
  }
}
```

Replace `/path/to/toggl-mcp-custom` with the actual path to this project.

Claude Code will automatically discover and use this server. The tools will be available in your conversations.

### Option 2: Global Integration

To use this server across all projects, add it to your global MCP config:

**macOS/Linux:**
```bash
mkdir -p ~/.config/claude-code
```

Then add to `~/.config/claude-code/mcp.json`:
```json
{
  "mcpServers": {
    "toggl": {
      "command": "/path/to/toggl-mcp-custom/.venv/bin/python",
      "args": ["-m", "toggl_mcp.server"],
      "cwd": "/path/to/toggl-mcp-custom"
    }
  }
}
```

### Using in Other Projects

To add this MCP server to another project:

1. **Copy the `.mcp.json` template:**
   ```bash
   cat > /path/to/other-project/.mcp.json << 'EOF'
   {
     "mcpServers": {
       "toggl": {
         "command": "/path/to/toggl-mcp-custom/.venv/bin/python",
         "args": ["-m", "toggl_mcp.server"],
         "cwd": "/path/to/toggl-mcp-custom"
       }
     }
   }
   EOF
   ```

2. **Update the paths** to point to your toggl-mcp-custom installation

3. **Restart Claude Code** - it will automatically discover the server

## Running the Server Manually

If you want to run the server directly:

```bash
source .venv/bin/activate
python -m toggl_mcp.server
```

The server will start and expose two MCP tools:

### 1. `get_workspace_users()`

Lists all users in the Toggl workspace.

**Response:**
```json
{
  "status": "success",
  "data": [
    {"id": "12345", "email": "user@example.com", "name": "John Doe"},
    {"id": "12346", "email": "jane@example.com", "name": "Jane Smith"}
  ],
  "error": null
}
```

### 2. `get_toggl_aggregated_data(start_date, end_date, user_id=None)`

Fetches and aggregates time entries for a date range.

**Parameters:**
- `start_date` (string): ISO 8601 format (YYYY-MM-DD), e.g., "2025-10-06"
- `end_date` (string): ISO 8601 format, must be >= start_date and <= start_date + 7 days
- `user_id` (optional string): Filter by single user ID

**Response:**
```json
{
  "status": "success",
  "data": {
    "users": {
      "user@example.com": {
        "user_email": "user@example.com",
        "matched_entities": [
          {
            "entity_database": "Scrum",
            "entity_type": "Task",
            "entity_id": "456",
            "description": "Design UI",
            "duration_hours": 7.5
          }
        ],
        "unmatched_activities": [
          {
            "description": "Team meeting",
            "duration_hours": 1.0
          }
        ],
        "statistics": {
          "total_duration_hours": 8.5,
          "matched_duration_hours": 7.5,
          "unmatched_duration_hours": 1.0
        }
      }
    },
    "statistics": {
      "total_users": 1,
      "total_duration_hours": 8.5,
      "total_matched_duration_hours": 7.5,
      "total_unmatched_duration_hours": 1.0
    }
  },
  "error": null,
  "metadata": {
    "source": "api",
    "entries_fetched": 15
  }
}
```

## Description Parsing

The server automatically parses Toggl entry descriptions to extract Fibery entity references.

**Format:** `"Description text #ID [DATABASE] [TYPE] [PROJECT]"`

**Examples:**
- `"Design UI #456 [Scrum] [Task] [Moneyball]"` → Matched entity
- `"#123 [Dev]"` → Minimal matched entity
- `"Team meeting"` → Unmatched activity

**Parsing Rules:**
- Extracts the rightmost `#ID` if multiple exist
- Captures first 3 bracket values as DATABASE, TYPE, and PROJECT
- Text before #ID becomes the description

## Architecture

### Services

- **CacheService**: Manages caching with MD5 hash keys, SQLite index, and JSON file storage
- **TogglService**: Handles Toggl API client with HTTP Basic Auth and rate limiting
- **ParserService**: Parses descriptions to extract Fibery entity references
- **AggregatorService**: Groups and aggregates time entries by user and entity

### Models

- **User**: Toggl workspace user
- **TimeEntry**: Raw Toggl time entry
- **ParsedEntry**: Entry with extracted metadata
- **UserAggregation**: Aggregated data for single user
- **AggregatedData**: Complete aggregated response

## Testing

Run unit tests:

```bash
python -m pytest tests/ -v
```

Test coverage:
- **26 unit tests** covering:
  - Description parsing (7 tests)
  - Data aggregation (5 tests)
  - Cache operations (5 tests)
  - Date validation and utilities (9 tests)

## Project Structure

```
toggl-mcp-custom/
├── src/toggl_mcp/
│   ├── __init__.py
│   ├── server.py              # FastMCP server + tools
│   ├── config.py              # Configuration from env vars
│   ├── models.py              # Pydantic models
│   ├── utils.py               # Utility functions
│   └── services/
│       ├── __init__.py
│       ├── cache_service.py   # SQLite + JSON caching
│       ├── toggl_service.py   # Toggl API client
│       ├── parser_service.py  # Description parsing
│       └── aggregator_service.py  # Data aggregation
├── tests/
│   ├── __init__.py
│   ├── test_parser_service.py
│   ├── test_aggregator_service.py
│   ├── test_cache_service.py
│   └── test_utils.py
├── cache/                     # Runtime cache (auto-created)
│   ├── data/                  # JSON cache files
│   └── index.db               # SQLite index
├── requirements.txt           # Python dependencies
├── .env.example              # Environment template
└── README.md                 # This file
```

## Performance Considerations

### Rate Limiting

- Toggl API limit: 3 requests per second
- Exponential backoff on 429 (rate limit) responses:
  - Attempt 1: 60 seconds
  - Attempt 2: 120 seconds
  - Attempt 3: 240 seconds
  - After 3 retries: failure

### Caching

- Default TTL: 1 hour
- Cache keys: MD5 hash of date range + optional user_id
- Storage: SQLite index + JSON files
- Automatic cleanup of expired entries on retrieval

### Date Range Limits

- Maximum range: 7 days per request
- For larger ranges, make multiple requests and aggregate client-side
- Day-by-day pagination to optimize API efficiency

## Error Handling

The server returns structured error responses with error codes:

- `INVALID_DATE_FORMAT` — Dates not in ISO 8601 format
- `INVALID_DATE_RANGE` — end_date < start_date
- `DATE_RANGE_EXCEEDS_LIMIT` — Range exceeds 7 days
- `USER_NOT_FOUND` — User ID not found in workspace
- `API_ERROR` — Toggl API error
- `SERVICE_NOT_INITIALIZED` — Services not properly initialized
- `INTERNAL_ERROR` — Server error

## Development

### Adding New Features

1. Add models to `models.py`
2. Implement service methods
3. Write unit tests in `tests/`
4. Update MCP tool in `server.py`

### Running Tests

```bash
# All tests
python -m pytest tests/ -v

# Specific test file
python -m pytest tests/test_parser_service.py -v

# With coverage
python -m pytest tests/ --cov=src/toggl_mcp
```

## License

MIT

## Support

For issues or questions, please refer to the implementation specification in `/docs/features/1-initial-implementation/IMPLEMENTATION.md`