@d4works/mcp-clickup
v0.8.0
Published
MCP server for ClickUp task management
Maintainers
Readme
@d4works/mcp-clickup
MCP (Model Context Protocol) server for ClickUp task management. Allows Claude to read task details from ClickUp and help with implementation.
Features
- Fetch task details (description, comments, attachments, custom fields, etc.)
- Create tasks and subtasks, with the target list derived from an existing task
- Support for multiple task ID formats (URL, custom ID, number)
- Instructions for Claude to analyze tasks before implementation
Configuration
1. Environment Variables
| Variable | Required | Description |
| --------------------- | -------- | ----------------------------------------------------------------------- |
| CLICKUP_API_TOKEN | Yes | Your ClickUp API token |
| CLICKUP_TEAM_ID | Yes | Your ClickUp team/workspace ID |
| CLICKUP_TASK_PREFIX | No | Task prefix for number-only input (e.g., "PROJ") |
| CLICKUP_SPACE_ID | No | Numeric space ID — skips the space lookup done by assign-release |
| CLICKUP_DEFAULT_LIST_ID | No | List ID create-task falls back to when the call names no target list |
| CLICKUP_READONLY | No | 1 or true — register only read tools and refuse every write to ClickUp (see Read-only mode) |
| CLICKUP_ALLOWED_TOOLS | No | Comma-separated allowlist of tool names to register; combined with CLICKUP_READONLY as an intersection |
2. Get Your API Token
- Open ClickUp → Click your avatar → Settings
- Go to Apps section
- Generate or copy your API Token
3. Find Your Team ID
- Open ClickUp → Go to any workspace
- Look at the URL:
https://app.clickup.com/{TEAM_ID}/... - The number in the URL is your Team ID
4. Create MCP Configuration
Create .mcp.json in your project root.
First, store your API token in a local file that's gitignored:
echo "export CLICKUP_API_TOKEN=pk_your_token_here" >> .claude/.env.localThen configure .mcp.json to source this file before running the server:
{
"mcpServers": {
"clickup": {
"command": "bash",
"args": [
"-c",
"export $(cat .claude/.env.local | xargs) && npx -y @d4works/mcp-clickup"
],
"env": {
"CLICKUP_TEAM_ID": "your_team_id",
"CLICKUP_TASK_PREFIX": "PROJ"
}
}
}
}Note: If you have
CLICKUP_API_TOKENexported in your global~/.zshrcor~/.bashrc, you can simplify the command to justnpx -y @d4works/mcp-clickup.
5. Read-only mode
For an agent that may look things up in ClickUp but must never change anything, start the
server with CLICKUP_READONLY=1:
{
"mcpServers": {
"clickup": {
"command": "bash",
"args": [
"-c",
"export $(cat .claude/.env.local | xargs) && npx -y @d4works/mcp-clickup"
],
"env": {
"CLICKUP_TEAM_ID": "your_team_id",
"CLICKUP_TASK_PREFIX": "PROJ",
"CLICKUP_READONLY": "1"
}
}
}
}In read-only mode:
- Only the read tools are registered:
test-connection,get-task,get-task-statuses,get-release-options,find-list. The write tools (create-task,set-task-status,set-task-release,rename-task,append-task-description,add-task-comment,assign-release,remove-release) are not listed at all, so the client never sees them. A call to one of them by name is answered with an error. - As a second line of defence, the HTTP client refuses every request to the ClickUp API
other than
GETwith aread-only modeerror, so nothing can be written even if a tool were mis-classified. - On start the server logs the mode and the active tools to stderr (never the token).
CLICKUP_ALLOWED_TOOLS narrows the set further, e.g. CLICKUP_ALLOWED_TOOLS=get-task,find-list.
When both variables are set the result is the intersection — the allowlist can never
re-enable a write tool in read-only mode. Names that match no tool are reported as a
warning on stderr and ignored.
Usage
Ask Claude to work on a task using any of these formats:
> Work on task ABC-123
> Work on task 123
> Work on task https://app.clickup.com/t/12345/ABC-123Claude will:
- Fetch task details from ClickUp
- Analyze the task (description, comments, attachments)
- Propose an implementation plan
- Wait for your approval before writing code
Available Tools
test-connection
Test connection to ClickUp API.
get-task
Get task details from ClickUp (description, status, comments, attachments, custom fields, checklists, etc.).
Parameters:
taskId(string, required): Task URL, custom ID (ABC-123), or number
create-task
Create a new task (or subtask) in ClickUp. Only used with explicit user confirmation.
The fastest way to use it is likeTaskId: point at any existing task in the right list — the task currently being worked on, for instance — and the new task is filed next to it. No list ID, no extra configuration, nothing to look up.
Parameters:
name(string, required): Task titledescription(string, optional): Task body, written as markdownlistId(string, optional): Target list ID (look one up withfind-list)likeTaskId(string, optional): Existing task whose list becomes the target — URL, custom ID (ABC-123), or numberparentTaskId(string, optional): Existing task to hang the new one under as a subtask — URL, custom ID (ABC-123), or numberstatus(string, optional): Initial status; must match one of the target list's statusesassignees(string[], optional): E-mail addresses or numeric user IDstags(string[], optional): Tag names — ClickUp only attaches tags that already exist in the spacepriority(number, optional): 1 = urgent, 2 = high, 3 = normal, 4 = low
Target list is taken from the first source that is given, in this order:
parentTaskId— a subtask always lives in its parent's list, so an additionallistIdis ignored and reportedlistIdlikeTaskIdCLICKUP_DEFAULT_LIST_IDfrom the environment
When none of them is available, nothing is created and the error names all four.
Behavior:
- The answer always carries the custom ID (ABC-123), the internal ID and the URL — the custom ID is what branch names, commit prefixes and time entries key off. When the space does not generate one, the answer says so outright instead of leaving it blank
- Everything checkable is checked before the task exists: an invalid status fails with the list's valid statuses, an unknown assignee e-mail fails with the workspace members, and neither creates anything
- The description is sent as
markdown_content, so headings, lists and links survive. A workspace that rejects the field falls back to the plain-textdescriptionand the answer says the formatting was lost - A custom ID passed as
parentTaskIdis translated to the internal ID the API requires - A rate-limited call (HTTP 429) is retried once, honoring
Retry-After
find-list
List the team's spaces, folders and lists with their IDs, so a listId can be found without opening ClickUp in a browser. Only needed when no existing task can serve as likeTaskId — a brand new project with no tasks yet, typically.
Parameters:
spaceId(string, optional): Restrict the walk to a single spacequery(string, optional): Case-insensitive substring matched against space, folder and list names. A hit on a space or folder name keeps all the lists below it
A space that cannot be read is reported in place, so restricted access to one space does not hide the rest of the workspace.
get-task-statuses
Get available statuses for a task. Use this before updating status to see valid options.
Parameters:
taskId(string, required): Task URL, custom ID (ABC-123), or number
set-task-status
Set the status of a ClickUp task. Only used with explicit user confirmation.
Parameters:
taskId(string, required): Task URL, custom ID (ABC-123), or numberstatus(string, required): New status name (must match one of the available statuses)
get-release-options
Get available Release dropdown options for a task. Use this to see valid values before setting Release.
Parameters:
taskId(string, required): Task URL, custom ID (ABC-123), or numberfieldName(string, optional): Name of the dropdown field (default: "Release")
set-task-release
Set the Release dropdown value on a ClickUp task. Only used with explicit user confirmation.
Parameters:
taskId(string, required): Task URL, custom ID (ABC-123), or numberrelease(string, required): Release value to set (must match one of the available options)fieldName(string, optional): Name of the dropdown field (default: "Release")
rename-task
Rename an existing ClickUp task. Changes the title only — the description, status, tags and custom fields stay untouched. The new name is written verbatim, so include any reference prefix (e.g. [ABC-123] ) yourself. The response shows the old and the new name. Only used with explicit user confirmation.
Parameters:
taskId(string, required): Task URL, custom ID (ABC-123), or numbername(string, required): New task name (non-empty)
append-task-description
Append markdown content below the existing description of a ClickUp task. The existing description is always kept — there is no replace mode. Only used with explicit user confirmation.
Parameters:
taskId(string, required): Task URL, custom ID (ABC-123), or numbercontent(string, required): Markdown content to append below the current description
add-task-comment
Add a comment to a ClickUp task. Only used with explicit user confirmation.
Parameters:
taskId(string, required): Task URL, custom ID (ABC-123), or numbercomment(string, required): The comment text to add to the tasknotifyAll(boolean, optional): Notify all task members about the comment (default: false)
assign-release
Tag one or more tasks with a release version and optionally move them to a status. Only used with explicit user confirmation.
The version is stored as a space tag, not as a dropdown custom field — the public ClickUp API cannot add options to a dropdown, but it can create tags. The tag is created on the space the first time a version is used, then attached to each task.
Parameters:
version(string, required): Release version used verbatim as the tag name — case and spaces matter (e.g."v1.17.4 (100)")taskIds(string[], required, min 1): Task URLs, custom IDs (ABC-123), or numbersstatus(string, optional): Status to set on every task (must match one of the available statuses)tagColor(object, optional):{ bg, fg }as 6-digit hex including#. Applied only when the tag is created for the first time (defaults:#4A78FF/#FFFFFF)
Behavior:
- Idempotent — re-running the same call neither duplicates the space tag nor the tag on a task
- Errors are collected per task, so one bad identifier does not abort the rest of the release; the call is only reported as failed when every task fails
- The space is resolved from the first task that can be read, unless
CLICKUP_SPACE_IDis set. All tasks in one call are expected to live in the same space — a task from another space is reported as failed, because its space does not have the tag
remove-release
The counterpart to assign-release: it detaches a release version tag from tasks instead of attaching it. Use it when a version was tagged by mistake — a failed build that never shipped, a typo in the version, or a re-tag that left both the old and the new version on the task. Only used with explicit user confirmation.
assign-release deliberately never replaces older version tags. Deleting them as a side effect of a write would mean a mis-derived commit range could destroy correct tags, so removal is this separate, explicit tool.
Parameters:
version(string, required): Release version matched verbatim against the tag name — case and spaces matter (e.g."v1.17.4 (101)")taskIds(string[], required, min 1): Task URLs, custom IDs (ABC-123), or numbersdeleteTagIfUnused(boolean, optional, defaultfalse): After removing the tag from the listed tasks, also delete it from the space — but only when no task uses it any more
Behavior:
- By default the tag is only detached from the listed tasks and stays available in the space
- Idempotent — re-running the same call changes nothing and reports every task as
removed: falsewith a reason - A tag that is not on a task is not an error: that task reports
removed: falsewithreason: "tag not present on task"and the rest of the batch continues - A tag that does not exist in the space is not an error either — nothing is changed and the result says so
- Errors are collected per task, so one bad identifier does not abort the rest; the call is only reported as failed when every task fails
Deleting the tag from the space is a separate, off-by-default step because it is irreversible and strips the tag from every task in the space at once. It only happens when all of the following hold, and the result explains which one blocked it otherwise:
- Every task in the call succeeded — an unreadable task might still carry the tag
- A search across the space (including closed and archived tasks) finds no other task using it, after subtracting the tasks this call just untagged
License
MIT
