toggl-focus-mcp
# toggl-focus-mcp
MCP server for **[Toggl Focus (2.0)](https://engineering.toggl.com/docs/focus/)** — not Toggl Track 1.0.
Connect Claude, Cursor, Codex, or OpenCode to your Focus workspace with a Bearer API key (`toggl_sk_…`) and `npx`. No OAuth browser flow required.
```bash
npx -y toggl-focus-mcp
```
| | |
| --- | --- |
| **Auth** | Focus Bearer API key (`Authorization: Bearer toggl_sk_…`) |
| **API** | `https://focus.toggl.com/api` |
| **Transport** | stdio MCP |
| **Node** | ≥ 20 (CI / `.nvmrc` → latest Current) |
> Track “API Token” values (Basic Auth / profile token for Track) **do not work**. Create a Focus / Toggl 2.0 key in settings. Official OAuth MCP: [`@togglhq/mcp`](https://www.npmjs.com/package/@togglhq/mcp).
---
## Prerequisites
1. A **Toggl Focus / 2.0 API key** (`toggl_sk_…`) — shown once when created
2. Your **organization ID** and **workspace ID** (strongly recommended)
3. **Node.js 20+** (for `npx`)
Find org/workspace IDs in the Focus URL, e.g.
`https://focus.toggl.com/.../organizations/21504582/workspaces/21503801/...`
Setting both IDs avoids extra discovery calls and protects against Focus hourly quota limits.
---
## Environment variables
| Variable | Required | Description |
| --- | --- | --- |
| `TOGGL_FOCUS_API_TOKEN` | Yes\* | Focus API key (`toggl_sk_…`) |
| `TOGGL_API_KEY` | Yes\* | Alias for the Focus API key |
| `TOGGL_API_TOKEN` | Yes\* | Additional alias |
| `TOGGL_ORGANIZATION_ID` | Recommended | Default organization ID |
| `TOGGL_WORKSPACE_ID` | Recommended | Default workspace ID |
| `TOGGL_FOCUS_BASE_URL` | No | API base (default `https://focus.toggl.com/api`) |
\* Exactly one of `TOGGL_FOCUS_API_TOKEN`, `TOGGL_API_KEY`, or `TOGGL_API_TOKEN` is required.
Most tools accept optional `organization_id` / `workspace_id` arguments that override the env defaults for that call.
---
## Install
Replace `toggl_sk_...`, `123`, and `456` with your real key and IDs.
### Claude Code
```bash
claude mcp add toggl-focus \
-e TOGGL_FOCUS_API_TOKEN=toggl_sk_... \
-e TOGGL_ORGANIZATION_ID=123 \
-e TOGGL_WORKSPACE_ID=456 \
-- npx -y toggl-focus-mcp
```
Verify:
```bash
claude mcp list
```
### Claude Desktop
Edit `claude_desktop_config.json`, then restart Claude Desktop.
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"toggl-focus": {
"command": "npx",
"args": ["-y", "toggl-focus-mcp"],
"env": {
"TOGGL_FOCUS_API_TOKEN": "toggl_sk_...",
"TOGGL_ORGANIZATION_ID": "123",
"TOGGL_WORKSPACE_ID": "456"
}
}
}
}
```
### Cursor
Add to project `.cursor/mcp.json` or your global Cursor MCP settings:
```json
{
"mcpServers": {
"toggl-focus": {
"command": "npx",
"args": ["-y", "toggl-focus-mcp"],
"env": {
"TOGGL_FOCUS_API_TOKEN": "toggl_sk_...",
"TOGGL_ORGANIZATION_ID": "123",
"TOGGL_WORKSPACE_ID": "456"
}
}
}
}
```
Enable the server in **Cursor Settings → MCP**, then confirm tools like `focus_whoami` appear.
### Codex
CLI (recommended):
```bash
codex mcp add toggl-focus \
--env TOGGL_FOCUS_API_TOKEN=toggl_sk_... \
--env TOGGL_ORGANIZATION_ID=123 \
--env TOGGL_WORKSPACE_ID=456 \
-- npx -y toggl-focus-mcp
```
Or edit `~/.codex/config.toml` (or project `.codex/config.toml` in a trusted repo):
```toml
[mcp_servers.toggl-focus]
command = "npx"
args = ["-y", "toggl-focus-mcp"]
[mcp_servers.toggl-focus.env]
TOGGL_FOCUS_API_TOKEN = "toggl_sk_..."
TOGGL_ORGANIZATION_ID = "123"
TOGGL_WORKSPACE_ID = "456"
```
To keep secrets out of the file, set them in your shell and forward by name:
```toml
[mcp_servers.toggl-focus]
command = "npx"
args = ["-y", "toggl-focus-mcp"]
env_vars = ["TOGGL_FOCUS_API_TOKEN", "TOGGL_ORGANIZATION_ID", "TOGGL_WORKSPACE_ID"]
```
Shared across Codex CLI, IDE extension, and ChatGPT desktop Codex host. Docs: [developers.openai.com/codex/mcp](https://developers.openai.com/codex/mcp).
### OpenCode
Add to `opencode.json` / `opencode.jsonc` (project or global):
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"toggl-focus": {
"type": "local",
"command": ["npx", "-y", "toggl-focus-mcp"],
"enabled": true,
"environment": {
"TOGGL_FOCUS_API_TOKEN": "toggl_sk_...",
"TOGGL_ORGANIZATION_ID": "123",
"TOGGL_WORKSPACE_ID": "456"
}
}
}
}
```
You can also reference shell env vars with OpenCode’s `{env:VAR}` syntax where supported. Docs: [opencode.ai/docs/mcp-servers](https://opencode.ai/docs/mcp-servers/).
---
## Quick start prompts
Once the server is connected, try:
- “Who am I in Toggl Focus?”
- “List my projects”
- “Start a timer on project X”
- “Create a task called Ship MCP docs on project Y”
- “Show time entries for this week”
Creates, updates, deletes, and timer start/stop apply **immediately** (no confirmation token).
---
## Tools
24 tools. All are Focus/2.0 only. Shared optional args on almost every tool:
| Arg | Type | Notes |
| --- | --- | --- |
| `organization_id` | number | Overrides `TOGGL_ORGANIZATION_ID` |
| `workspace_id` | number | Overrides `TOGGL_WORKSPACE_ID` |
| `page` | number | Default `1` (list tools) |
| `per_page` | number | Default `50`, max `100` (list tools) |
### Account & context
#### `focus_whoami`
Account metadata + user settings for the authenticated Focus key.
| Arg | Type | Required | Description |
| --- | --- | --- | --- |
| `include_settings` | boolean | No | Default `true` — also fetch `GET /users/me/settings` |
| `organization_id` / `workspace_id` | number | No | Optional scope |
#### `focus_get_workspace_context`
Resolve and cache `organization_id` / `workspace_id` for later tools. Prefer setting both env vars. Does **not** call the session-only `/workspaces/{id}/context` endpoint (that returns 403 for API keys).
#### `focus_list_org_users`
List organization users (assignee lookup). Read-only.
| Arg | Type | Required | Description |
| --- | --- | --- | --- |
| `filter` | string | No | Search name/email |
| `active_status` | string | No | Active-status filter when supported |
| `page` / `per_page` | number | No | Pagination |
### Projects
#### `focus_list_projects`
List workspace projects.
| Arg | Type | Description |
| --- | --- | --- |
| `name` | string | Filter by project or client name |
| `archived` / `completed` / `pinned` | boolean | Status filters |
| `client_id` | number[] | Filter by client IDs |
| `tag_id` | number[] | Filter by tag IDs |
#### `focus_get_project`
| Arg | Type | Required |
| --- | --- | --- |
| `project_id` | number | Yes |
#### `focus_create_project`
Creates immediately. Requires `name`.
| Arg | Type | Description |
| --- | --- | --- |
| `name` | string | **Required** |
| `description` | string | |
| `client_id` | number | Supported on create |
| `color` | string | |
| `private` / `pinned` / `billable` | boolean | |
| `start_date` / `end_date` | string | `YYYY-MM-DD` |
| `tag_ids` | number[] | Applied via follow-up PATCH; may be forbidden on some plans/roles — project still creates and a `warning` is returned |
| `parent_project_id` | number | |
#### `focus_update_project`
Partial update (`PATCH`). Requires `project_id`. Same optional fields as create, plus nullable `client_id` and optional `archived`.
### Tasks
#### `focus_list_tasks`
| Arg | Type | Description |
| --- | --- | --- |
| `name` | string | Name filter |
| `project_id` | number[] | Project IDs |
| `status_id` | number[] | Status IDs |
| `assignee_user_id` | number[] | Assignees |
| `tag_id` | number[] | Tags |
| `pinned` / `archived` | boolean | |
| `parent_task_id` | number[] | Parent tasks |
| `include_assignees` | boolean | Expand assignees |
#### `focus_get_task`
Requires `task_id`. Optional `include_assignees`.
#### `focus_create_task`
Creates immediately. Requires `name`.
| Arg | Type | Description |
| --- | --- | --- |
| `name` | string | **Required** |
| `description` / `notes` | string | |
| `project_id` / `parent_task_id` / `status_id` | number | |
| `assignee_user_ids` / `tag_ids` | number[] | |
| `estimated_mins` | number | |
| `priority` | `"none"` \| `"low"` \| `"medium"` \| `"high"` | |
| `pinned` / `private` / `billable` | boolean | |
| `start_date` / `end_date` | string | `YYYY-MM-DD` |
#### `focus_update_task`
Partial update. Requires `task_id`. Same mutable fields as create (all optional).
### Live tracking
#### `focus_get_current_timer`
Returns the running timer for the workspace, or `{ "running": false }`.
#### `focus_start_timer`
Starts immediately. `type` defaults to `"activity"`.
| Arg | Type | Description |
| --- | --- | --- |
| `type` | `"activity"` \| `"break"` | Default `activity` |
| `task_id` / `project_id` | number | |
| `description` | string | |
| `billable` | boolean | |
| `tag_ids` | number[] | |
| `time_block_id` | number | |
| `start` | string | RFC3339; server default = now |
#### `focus_stop_timer`
Stops the running timer. Optional `end` (RFC3339); defaults to current UTC.
### Time entries
#### `focus_list_time_entries`
Requires a date range. Accepts `YYYY-MM-DD` or RFC3339 (`YYYY-MM-DD` is expanded to start/end of day UTC).
| Arg | Type | Required | Description |
| --- | --- | --- | --- |
| `date_from` | string | Yes | Range start |
| `date_to` | string | Yes | Range end |
| `task_id` | number | No | |
| `type` | `"activity"` \| `"break"` | No | |
| `archived` / `include_taskless` | boolean | No | |
| `time_block_id` | number | No | |
#### `focus_create_time_entry`
Creates a completed entry immediately. If `task_id` is set, uses the task-scoped endpoint; otherwise creates a taskless entry. `type` defaults to `"activity"`.
| Arg | Type | Description |
| --- | --- | --- |
| `task_id` / `project_id` | number | |
| `description` | string | |
| `start` | string | RFC3339 |
| `duration` | number | Seconds |
| `billable` | boolean | |
| `tag_ids` | number[] | |
| `time_block_id` | number | |
| `type` | `"activity"` \| `"break"` | |
#### `focus_update_time_entry`
Partial update. Requires `time_entry_id`. Optional `task_id` selects the task-scoped path.
#### `focus_delete_time_entry`
Deletes immediately. Requires `time_entry_id`. Optional `task_id` for the task-scoped path.
### Clients
#### `focus_list_clients`
Optional `name` filter plus pagination.
#### `focus_create_client`
Requires `name`. Optional `currency` (ISO 4217).
### Tags
#### `focus_list_tags`
Optional `name`, `tag_id` (array), pagination.
#### `focus_create_tag`
Requires `name` and `color` (hex or Focus color token).
### Statuses & search
#### `focus_list_statuses`
List task statuses. Optional `name`, `type`, pagination.
#### `focus_search`
Unified search across time entries, tasks, and projects.
| Arg | Type | Required | Description |
| --- | --- | --- | --- |
| `keyword` | string | Yes | Search text |
| `per_group` | number | No | Results per group |
| `project_id` / `task_id` | number | No | Scope |
| `only_me` / `include_drafts` | boolean | No | |
---
## Behavior notes
- **Mutations are immediate** — create / update / delete / start / stop have no confirmation step.
- **Project tags on create** — some Focus plans/roles reject `tag_ids` on `POST`. The tool creates the project first, then PATCHes tags; on failure you still get the project plus a `warning`.
- **Workspace context** — session cookie endpoints are not used; set org + workspace env vars.
- **Quota** — Focus enforces per-user-per-org hourly limits (e.g. Free ~30/hour). HTTP **402** means quota exhausted; wait for `X-Toggl-Quota-Resets-In`. Prefer env IDs so tools do not rediscover context every call.
---
## Development
```bash
git clone https://github.com/copperline-ai/toggl-focus-mcp.git
cd toggl-focus-mcp
npm install
npm run check # typecheck + unit/mocked MCP tests + build (CI gate)
npm run test:live # optional — real Focus API; local only, needs .env
node dist/index.js
```
`npm run check` is the same gate used by GitHub Actions on PRs and before publish. It uses **mocked** Focus responses — CI never calls your real Toggl account.
### Live MCP tests (local only)
Optional suite against a **real** Focus workspace. Not run in CI.
1. Copy `.env.example` → `.env` and set your Focus key + org/workspace IDs
2. Run:
```bash
npm run test:live
```
Reuses stable fixtures (client **Toggl Focus**, project **Toggl Focus MCP**, task **MCP live test**, tag **MCP live**). Disposable time entries are deleted. Paces ~1 req/sec for rate limits. `.env` is gitignored and not published.
### Publishing
Releases use **release-it** + AI Gateway (semver bump, changelog, GitHub Release, npm publish):
```bash
npm run release:dry-run
npm pack --dry-run
```
CI secrets: `NPM_ACCESS_TOKEN`, `AI_GATEWAY_API_KEY`.
Vendored OpenAPI: [`openapi/focus.json`](openapi/focus.json) from [Focus OpenAPI](https://engineering.toggl.com/docs/focus/openapi/).
---
## Related
- Focus docs: [engineering.toggl.com/docs/focus](https://engineering.toggl.com/docs/focus/)
- Focus auth: [Bearer API keys](https://engineering.toggl.com/docs/focus/start/authentication/)
- Official OAuth MCP: [`@togglhq/mcp`](https://www.npmjs.com/package/@togglhq/mcp)
## License
MIT
TDQS
Scored across 24 tools
Each tool targets a distinct resource and action (e.g., get_current_timer vs. list_time_entries, start_timer vs. create_time_entry). Even with overlapping domains, the descriptions make boundaries clear.
All tools share the 'focus_' prefix and almost all follow a verb_noun pattern (list_projects, create_task, update_time_entry). The few exceptions (whoami, search) are standard single-word commands and don't break the overall consistency.
24 tools is on the higher end but justified by the broad Toggl Focus domain covering projects, tasks, time entries, clients, tags, statuses, and user/context helpers. It feels comprehensive without redundant or trivial tools.
Core resources have strong coverage: projects and tasks support list/get/create/update, and time entries support list/create/update/delete. However, projects and tasks lack delete operations, and clients/tags only support list/create, leaving notable lifecycle gaps for these resources.