Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Toggl API Configuration
TOGGL_API_KEY=your_api_key_here
TOGGL_API_TOKEN=your_api_token_here

# Cache Configuration (optional)
TOGGL_CACHE_TTL=3600000 # Cache time-to-live in milliseconds (default: 1 hour)
Expand All @@ -8,4 +8,7 @@ TOGGL_BATCH_SIZE=100 # Number of entries to fetch per request (default: 100

# Default Workspace (optional)
# If set, will be used as default for operations that require a workspace
# TOGGL_DEFAULT_WORKSPACE_ID=123456
# TOGGL_DEFAULT_WORKSPACE_ID=123456

# Project aliases (optional; defaults to config/project-aliases.json)
# TOGGL_PROJECT_ALIASES_FILE=/absolute/path/to/project-aliases.json
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ dist/
tmp/
*.code-workspace
*.tgz
config/project-aliases.json
14 changes: 14 additions & 0 deletions .mcp.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"mcpServers": {
"toggl": {
"command": "node",
"args": [
"/absolute/path/to/mcp-toggl/dist/index.js"
],
"env": {
"TOGGL_API_TOKEN": "${TOGGL_API_TOKEN}",
"TOGGL_PROJECT_ALIASES_FILE": "/absolute/path/to/mcp-toggl/config/project-aliases.json"
}
}
}
}
20 changes: 16 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Add this to `~/Library/Application Support/Claude/claude_desktop_config.json`:
"command": "npx",
"args": ["-y", "@verygoodplugins/mcp-toggl@latest"],
"env": {
"TOGGL_API_KEY": "your_api_key_here",
"TOGGL_API_TOKEN": "your_api_token_here",
"TOGGL_DEFAULT_WORKSPACE_ID": "123456"
}
}
Expand Down Expand Up @@ -130,6 +130,7 @@ mcp-toggl --help
| `toggl_get_current_entry` | Returns the running timer, elapsed seconds, and hydrated project/workspace context. |
| `toggl_start_timer` | Starts a timer with description, optional project/task, and tags. |
| `toggl_stop_timer` | Stops the currently running timer. |
| `toggl_create_time_entry` | Creates a completed entry with description, duration, and optional project ID or alias. |

### Lookups

Expand All @@ -138,6 +139,7 @@ mcp-toggl --help
| `toggl_check_auth` | Verifies token access and lists available workspaces without exposing the token. |
| `toggl_list_workspaces` | Lists all accessible workspaces. |
| `toggl_list_projects` | Lists projects for a workspace using cache-backed reads after first fetch. |
| `toggl_list_project_aliases` | Lists short aliases loaded from `config/project-aliases.json`. |
| `toggl_list_clients` | Lists clients for a workspace using cache-backed reads after first fetch. |

### Cache Management
Expand Down Expand Up @@ -192,14 +194,24 @@ When in doubt, use `include_events: false`.

| Env var | Required | Default | Notes |
| --- | --- | --- | --- |
| `TOGGL_API_KEY` | Yes | - | Preferred env var for your Toggl API token. |
| `TOGGL_API_TOKEN` | No | - | Supported alias for backwards compatibility. `TOGGL_API_KEY` is preferred. |
| `TOGGL_TOKEN` | No | - | Supported alias for backwards compatibility. `TOGGL_API_KEY` is preferred. |
| `TOGGL_API_TOKEN` | Yes | - | Preferred env var for your Toggl API token. |
| `TOGGL_API_KEY` | No | - | Supported alias for backwards compatibility. |
| `TOGGL_TOKEN` | No | - | Supported alias for backwards compatibility. |
| `TOGGL_DEFAULT_WORKSPACE_ID` | No | - | Used when a tool requires a workspace and none is passed. |
| `TOGGL_PROJECT_ALIASES_FILE` | No | `config/project-aliases.json` | JSON object mapping aliases such as `wotw` to Toggl project IDs. |
| `TOGGL_CACHE_TTL` | No | `3600000` | Cache TTL in milliseconds. Default is 1 hour. |
| `TOGGL_CACHE_SIZE` | No | `1000` | Maximum cached entity budget. |
| `TOGGL_BATCH_SIZE` | No | `100` | Batch size used by API pagination helpers. |

Project aliases are local configuration. Copy the tracked example and replace its sample IDs:

```bash
cp config/project-aliases.example.json config/project-aliases.json
```

`config/project-aliases.json` is gitignored. Set `TOGGL_PROJECT_ALIASES_FILE` when the alias file
lives elsewhere.

## Caveats

**Toggl rate limits and quotas**: Toggl may return rate-limit or quota errors during chatty sessions. The server returns structured retry information when Toggl provides it. Warm the cache before large reporting sessions to avoid repeated project/client/tag fetches.
Expand Down
79 changes: 79 additions & 0 deletions SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Toggl MCP Local Setup

## Requirements

- Node.js `^20.19.0` or `>=22.12.0`
- `TOGGL_API_TOKEN`: your token from <https://track.toggl.com/profile>

Optional:

- `TOGGL_DEFAULT_WORKSPACE_ID`: avoids workspace selection when multiple workspaces exist
- `TOGGL_PROJECT_ALIASES_FILE`: absolute path to an alternate alias JSON file

## Install And Verify

```bash
git clone https://github.com/verygoodplugins/mcp-toggl.git
cd mcp-toggl
npm install
npm run build
npm test
TOGGL_API_TOKEN="$(cat ~/.config/toggl/api_token)" npm run test:live
```

The live test creates temporary one-second entries, verifies the MCP tools against Toggl API v9,
and deletes the test entries afterward. It does not interrupt an already-running user timer.

## Project Aliases

Copy the example, then replace the sample IDs with project IDs from your Toggl workspace:

```bash
cp config/project-aliases.example.json config/project-aliases.json
```

The real `config/project-aliases.json` is gitignored so personal workspace IDs are not published.
Keys are case-insensitive short aliases and values are Toggl project IDs:

```json
{
"writing": 123456789,
"service": 987654321
}
```

Use aliases with `toggl_start_timer` or `toggl_create_time_entry` via `project_alias`.

## Claude Desktop

Merge the `mcpServers.toggl` object from `claude_desktop_config.example.json` into:

```text
~/Library/Application Support/Claude/claude_desktop_config.json
```

Replace `your_api_token_here`, then restart Claude Desktop.

## Claude Code

Claude Code defines project MCP servers in `.mcp.json`, not directly in `.claude/settings.json`.
Copy or merge `.mcp.json.example` into the project root as `.mcp.json`, replace its absolute paths,
export `TOGGL_API_TOKEN`, and approve the server when Claude Code prompts.

To explicitly enable this project MCP from `.claude/settings.json`, add:

```json
{
"enabledMcpjsonServers": ["toggl"]
}
```

## Core Tools

- `toggl_start_timer`: start a timer using `project_id` or `project_alias`
- `toggl_stop_timer`: stop the current timer
- `toggl_get_current_entry`: get the current timer
- `toggl_get_time_entries`: list recent entries
- `toggl_list_projects`: list projects
- `toggl_create_time_entry`: create a completed entry with description, duration, and project
- `toggl_list_project_aliases`: inspect configured aliases
14 changes: 14 additions & 0 deletions claude_desktop_config.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"mcpServers": {
"toggl": {
"command": "node",
"args": [
"/absolute/path/to/mcp-toggl/dist/index.js"
],
"env": {
"TOGGL_API_TOKEN": "your_api_token_here",
"TOGGL_PROJECT_ALIASES_FILE": "/absolute/path/to/mcp-toggl/config/project-aliases.json"
}
}
}
}
4 changes: 4 additions & 0 deletions config/project-aliases.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"writing": 123456789,
"service": 987654321
}
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"start": "node dist/index.js",
"setup": "node scripts/setup.js",
"test": "vitest run",
"test:live": "node scripts/live-smoke-test.js",
"test:coverage": "vitest run --coverage",
"lint": "eslint src/ tests/",
"format": "prettier --write src/**/*.ts",
Expand Down Expand Up @@ -47,6 +48,10 @@
},
"files": [
"dist/",
"config/project-aliases.example.json",
".mcp.json.example",
"claude_desktop_config.example.json",
"SETUP.md",
"docs/images/*.svg",
"README.md",
"LICENSE"
Expand Down
112 changes: 112 additions & 0 deletions scripts/live-smoke-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env node
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { config } from 'dotenv';
import { resolve } from 'node:path';
import { TogglAPI } from '../dist/toggl-api.js';

config({ quiet: true });

const token = (
process.env.TOGGL_API_TOKEN ||
process.env.TOGGL_API_KEY ||
process.env.TOGGL_TOKEN ||
''
).trim();
if (!token) throw new Error('Set TOGGL_API_TOKEN before running the live smoke test.');

const client = new Client({ name: 'toggl-live-smoke-test', version: '1.0.0' });
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve('dist/index.js')],
env: {
...process.env,
TOGGL_API_TOKEN: token,
},
});
const api = new TogglAPI(token);
const createdIds = [];
const smokePrefix = 'MCP live smoke test';

function parse(result) {
const text = result.content?.find((item) => item.type === 'text')?.text || '{}';
return JSON.parse(text);
}

async function call(name, args = {}) {
const payload = parse(await client.callTool({ name, arguments: args }));
if (payload.error) throw new Error(`${name}: ${payload.message}`);
console.log(`PASS ${name}`);
return payload;
}

await client.connect(transport);
try {
const tools = await client.listTools();
const required = [
'toggl_start_timer',
'toggl_stop_timer',
'toggl_get_current_entry',
'toggl_get_time_entries',
'toggl_list_projects',
'toggl_create_time_entry',
'toggl_list_project_aliases',
];
for (const name of required) {
if (!tools.tools.some((tool) => tool.name === name)) throw new Error(`Missing tool: ${name}`);
}
console.log('PASS required tool schemas');

await call('toggl_check_auth');
const workspaces = await call('toggl_list_workspaces');
const workspaceId = workspaces.workspaces[0]?.id;
if (!workspaceId) throw new Error('No Toggl workspace available.');

const projects = await call('toggl_list_projects', { workspace_id: workspaceId });
const aliases = await call('toggl_list_project_aliases');
const projectIds = new Set(projects.projects.map((project) => project.id));
for (const [alias, projectId] of Object.entries(aliases.aliases)) {
if (!projectIds.has(projectId)) throw new Error(`Alias ${alias} points to missing project ${projectId}`);
}
console.log('PASS project alias IDs');
await call('toggl_get_time_entries', { period: 'today' });
const current = await call('toggl_get_current_entry');

const completed = await call('toggl_create_time_entry', {
workspace_id: workspaceId,
project_alias: 'admin',
description: `${smokePrefix} - completed entry`,
duration_seconds: 1,
});
createdIds.push([workspaceId, completed.entry.id]);

if (current.running) {
console.log('SKIP toggl_start_timer/toggl_stop_timer: a user timer is already running');
} else {
const started = await call('toggl_start_timer', {
workspace_id: workspaceId,
project_alias: 'admin',
description: `${smokePrefix} - timer`,
});
createdIds.push([workspaceId, started.entry.id]);
await call('toggl_stop_timer');
}
} finally {
const recent = await api.getTimeEntries();
for (const entry of recent) {
if (entry.description?.startsWith(smokePrefix)) {
createdIds.push([entry.workspace_id, entry.id]);
}
}
const uniqueIds = new Map(createdIds.map(([workspaceId, entryId]) => [entryId, workspaceId]));
for (const [entryId, workspaceId] of uniqueIds) {
try {
await api.deleteTimeEntry(workspaceId, entryId);
console.log(`CLEANUP deleted test entry ${entryId}`);
} catch (error) {
if (!String(error).includes('404')) throw error;
console.log(`CLEANUP test entry ${entryId} was already absent`);
}
}
await client.close();
}
17 changes: 9 additions & 8 deletions scripts/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ async function setup() {
const envContent = await fs.readFile(envExamplePath, 'utf-8');
await fs.writeFile(envPath, envContent);
console.log('✅ .env file created');
console.log('\n⚠️ Please edit .env and add your Toggl API key');
console.log(' Get your API key from: https://track.toggl.com/profile\n');
console.log('\n⚠️ Please edit .env and add your Toggl API token');
console.log(' Get your API token from: https://track.toggl.com/profile\n');
}

// Install dependencies
Expand All @@ -44,28 +44,29 @@ async function setup() {
console.log('\n' + '='.repeat(50));
console.log('✨ Setup Complete!\n');
console.log('Next steps:');
console.log('1. Edit .env and add your TOGGL_API_KEY');
console.log('2. Add to your MCP configuration:');
console.log('1. Edit .env and add your TOGGL_API_TOKEN');
console.log('2. Optionally copy config/project-aliases.example.json to config/project-aliases.json');
console.log('3. Add to your MCP configuration:');
console.log('\n📋 For Claude Desktop:');
console.log('Edit: ~/Library/Application Support/Claude/claude_desktop_config.json');
console.log(JSON.stringify({
"mcp-toggl": {
"command": "node",
"args": [path.join(__dirname, '..', 'dist', 'index.js')],
"env": {
"TOGGL_API_KEY": "your_api_key_here"
"TOGGL_API_TOKEN": "your_api_token_here"
}
}
}, null, 2));

console.log('\n📋 For Cursor:');
console.log('\n📋 For Claude Code:');
console.log('Add to .mcp.json in your project:');
console.log(JSON.stringify({
"mcp-toggl": {
"command": "node",
"args": ["./mcp-servers/mcp-toggl/dist/index.js"],
"env": {
"TOGGL_API_KEY": "your_api_key_here"
"TOGGL_API_TOKEN": "your_api_token_here"
}
}
}, null, 2));
Expand All @@ -79,4 +80,4 @@ async function setup() {
}
}

setup();
setup();
Loading