MSPbotsAIFramework

clickup-mcp

Community MSPbotsAIFramework
Updated

clickup-mcp

clickup-mcp

ClickUp MCP server for Claude — exposes ClickUp tasks, spaces, folders, lists, and comments as MCP tools.

Tech stack: Python 3.12 + uv + FastMCP (Starlette/FastAPI)

MSPbots Connector

The MCP Gateway built for MSPs: visible, controlled, and your data stays where it belongs.

Get started at connect.mspbots.ai →

MSPbots Connector gives your AI clients one MCP URL to reach your entire MSP stack (RMM, PSA, documentation, security, finance), and gives you control over who can call which tool and a record of everything that was called.

Hosted service: connect.mspbots.ai · Free to use.

Why Connector

MSPbots Connector

Visible Every tool call is logged: who, which vendor, when. Usage dashboard, CSV export, SIEM streaming.
Controlled Role-based, per-tool allowlist. Deny by default.
Protected Credentials encrypted per record. AI clients never see your vendor passwords or tokens.
MSP-native Built for the MSP stack only: RMM, PSA, documentation, security, finance.

Quick start

  1. Sign up at connect.mspbots.ai.
  2. Connect your vendors. Pick from 150+ connectors and authorize each one on the vendor's own page, or bring your own API keys. No extra vendor accounts needed.
  3. Set access. Create roles and decide which tools each role may call. Nothing is allowed until you allow it.
  4. Add your MCP URL to your AI client. Copy the URL from your Connector dashboard into Claude, ChatGPT, Cursor, VS Code, Gemini, Codex, or any MCP-capable client.
  5. Ask your AI to do real work, and watch every call appear in the audit log.

Features

Unified gateway

  • One MCP URL for all your AI clients
  • 150+ connectors across the MSP stack
  • Custom MCP servers supported, free
  • Authorization happens on each vendor's own page; the AI never sees passwords or tokens
  • Bring your own credentials: use the vendor API keys you already have
  • Multiple accounts per app; connect once, reuse across AI clients
  • Tool search: finds the right action for the task
  • One sign-in; vendor credentials are injected behind the scenes and OAuth tokens refresh automatically

Visibility

  • Audit log for every tool call (who, which vendor, when)
  • Usage dashboard: call volume, top vendors, error rate, per-user activity
  • Export audit logs to CSV, or stream to Loki, Graylog, or LogScale (SIEM)
  • Optional recording of tool arguments and result summaries (off by default, enabled per organization)

Control

  • Per-role, per-tool allowlist, deny by default
  • owner / admin / member roles
  • SCIM provisioning with Entra ID, Okta, JumpCloud, and Google Workspace

Security

  • Credentials encrypted one record at a time (AES-256-GCM, per-record key)
  • AI clients authenticate with OAuth 2.1 + PKCE
  • Vendor responses are scanned for prompt-injection patterns and returned as structured data. This is one layer of defense, not a guarantee against every attack.
  • Static egress IP for customer firewall and API allowlisting

Pricing

Free.

Links: connect.mspbots.ai · mspbots.ai

Quick Start

# Install dependencies
cd D:\leo\mcp-server\clickup-mcp
uv sync

# Run in HTTP/gateway mode (the default) — each request supplies its own
# token via the X-Clickup-Token header, nothing to set locally
uv run clickup-mcp

# Or run in stdio mode for local single-user tools like Claude Desktop
$env:MCP_TRANSPORT="stdio"
$env:CLICKUP_API_TOKEN="pk_xxxxx"
uv run clickup-mcp

Configuration

Copy .env.example to .env and fill in your values:

Variable Default Description
MCP_TRANSPORT http http (gateway, default) or stdio (Claude Desktop / local dev only)
CLICKUP_API_TOKEN — ClickUp personal API token (pk_xxxxx). Only read when MCP_TRANSPORT=stdio — the HTTP/gateway path never reads it, by design (no header = 401, never a silent fallback to this value)
MCP_HTTP_PORT 8080 HTTP server port
CLICKUP_BASE_URL https://api.clickup.com/api/v2 API base URL

Note: there is no AUTH_MODE setting anymore. HTTP transport is alwaysgateway mode (per-request X-Clickup-Token header, enforced by middleware);stdio transport is always local-token mode. Credential source now followstransport directly, so there is no way to accidentally run an HTTP/gatewaydeployment that falls back to a shared env-var token.

Get your API token: ClickUp → Settings → Apps → API Token

Claude Desktop Setup

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "clickup": {
      "command": "uv",
      "args": ["run", "--directory", "D:/leo/mcp-server/clickup-mcp", "clickup-mcp"],
      "env": {
        "CLICKUP_API_TOKEN": "pk_xxxxx"
      }
    }
  }
}

Transport Modes

HTTP — gateway / multi-tenant (default)

uv run clickup-mcp
# Each request must include: X-Clickup-Token: pk_xxxxx
# A request with no header gets 401 — there is no env-var fallback.

stdio (Claude Desktop / CLI, local dev only)

$env:MCP_TRANSPORT="stdio"
$env:CLICKUP_API_TOKEN="pk_xxxxx"
uv run clickup-mcp

Available Tools (28)

Tool Description
clickup_get_workspaces List all workspaces/teams
clickup_list_members List workspace members, flattened to id/username/email/team_id/role — resolve a person's email to the user_id assignees filters expect
clickup_list_spaces List spaces in a workspace, or across every workspace the token can see when team_id is omitted
clickup_get_space Get space details
clickup_get_space_folders List folders in a space
clickup_get_space_lists List folderless lists in a space
clickup_get_folder Get folder details
clickup_get_folder_lists List lists in a folder
clickup_create_folder Create a folder
clickup_update_folder Update a folder
clickup_delete_folder Delete a folder
clickup_get_list Get list details
clickup_create_list_in_folder Create list in a folder
clickup_create_folderless_list Create list in a space
clickup_update_list Update a list
clickup_get_task Get task by native ID, or by custom ID with the workspace resolved automatically
clickup_search_tasks Search tasks with filters; searches every workspace the token can see when team_id is omitted
clickup_list_tasks_for_person List a person's tasks across ALL visible workspaces in one call, by email or user_id — no team_id, no manual pagination/dedup needed
clickup_create_task Create a task
clickup_update_task Update a task
clickup_delete_task Delete a task
clickup_move_task Move task to a different list
clickup_get_task_comments Get task comments
clickup_create_task_comment Add a comment to a task
clickup_get_doc_page Get a single page from a Doc (v3); workspace_id optional
clickup_attach_task_file Upload a file (e.g. an image) as an attachment on a task
clickup_create_comment_with_image Upload a file and post it inline inside a new task comment, in one call
clickup_list_rocks_for_org List all EOS Rocks (quarterly goals) org-wide in one call, normalized to a fixed status enum

Workspace (team) IDs are resolved from the token

No tool requires a workspace/team ID. GET /team already tells the server everyworkspace the API token is authorized for, so the server resolves it rather thanasking the caller to supply an ID it has no way of knowing.

  • Omit team_id and the call covers every workspace the token can reach. Readsfan out and merge; each returned row carries its own team_id.
  • Pass a team_id the token cannot see and, when only one workspace exists, itis substituted and the response reports team_id_corrected.
  • When several workspaces exist and the given ID matches none, the error liststhe legal ones in authorized_workspaces — so one retry is enough. Writes(clickup_attach_task_file) never guess between workspaces.

A workspace the token cannot reach is reported as invalid_argument, notunauthorized; only a genuinely bad token yields unauthorized.

Finding a person's ClickUp user ID

Use clickup_list_members. ClickUp's native GET /team response embeds afull member list per team (teams[].members[].user.{id,username,email}), butclickup_get_workspaces strips that out to keep its response small, so it isnot the place to look up people. clickup_list_members reads the sameunderlying endpoint and projects the member list to a flat, purpose-builtshape (id/username/email/team_id/role) so callers don't have to dig it out ofthe full workspace/team object themselves. clickup_list_tasks_for_personuses the same underlying lookup internally to resolve email -> user_id.

Known gap: ClickUp's team-member object has no reliable "is this memberdeactivated" field — clickup_list_members does not return an activefield, since nothing real would back it (the only status field presenton the raw object, invited_by.status, describes the inviter, not themember).

clickup_search_tasks already returns status.type

Like every other read tool here, clickup_search_tasks andclickup_get_task pass through ClickUp's raw task object unmodified —including the status object's type field (open / custom / closed /done), which is the only reliable way to tell whether a custom-namedstatus counts as done. No code change was needed for this; it was alreadythere. clickup_list_tasks_for_person surfaces it explicitly asstatus_type on each returned task for convenience.

How EOS Rocks are represented in this ClickUp workspace

Confirmed 2026-08-18 by inspecting a real rock task's fields directly (notguessed): Rocks are regular ClickUp tasks living in a list literally named"Rocks" (found under Space "Company" > Folder "EOS Traction"), eachcarrying dedicated custom fields: Quarter (dropdown, "Q1 2024".."Q4 2026"),Rocks Status (On Hold / Off Track / On Track / Completed / Blocked / AtRisk), Rock Type (Company / Individual / Departmental / Team Rock),Department, and progress via either Progress (manual) or Progress %(auto, checklist-rollup). This is neither the ClickUp Goals API nor aplain task list with no metadata — it's tasks-plus-custom-fields.

clickup_list_rocks_for_org discovers every list named "Rocks" (by name,not a hardcoded ID, in case spaces/folders get reorganized) across everyworkspace visible to the token, reads these fields, and normalizes them:

  • quarter: ClickUp's "Q3 2026" label is converted to 2026-Q3 (and back,for the quarter input filter).
  • status: ClickUp's 6 raw options are mapped down to the 5-value contract(on_track/off_track/done/missed/open) — see the _STATUS_MAPcomment in rocks.py for the exact mapping and why missed is neveremitted (nothing in ClickUp's data distinguishes "ran out of time" fromgeneric "off track"; deriving it from an overdue due_date would be anunconfirmed business-logic assumption, so it isn't done here). The rawClickUp label (e.g. "At Risk") is also returned as status_raw,alongside the normalized status, so a UI can show ClickUp's own wordingwithout it looking out of sync with the mapped value.
  • measurable: no dedicated field exists on this list. Falls back tothe task description; null if that's empty too (never fabricated).
  • weekly_status: no structured source was found anywhere (not acustom field, nothing comment-derived either) — always returned as [].If the org starts tracking this in ClickUp some other way, revisit.
  • owner_email / owner_user_id (optional filter args): this tool isorg-wide by default (every rock returned), but a caller building asingle-person view can pass either to scope the results to one owner'srocks — the org-wide fetch itself still happens (this doesn't reduceupstream ClickUp API calls), it just narrows what's returned. Omit bothfor the original org-wide behavior.

Discovery performance: parallel, not sequential

clickup_list_rocks_for_org's list-discovery walk (every workspace, everyspace, each space's folderless lists + folder lists) runs concurrently viaasyncio.gather, not one request after another. A sequential version ofthis was measured to time out against MSPbots' own workspace (15+ spacesx 2 calls each, run one at a time, comfortably exceeded the caller's MCPtimeout) — parallelizing brought it down to ~7s. ClickUp's rate limit(developer.clickup.com/docs/rate-limits) is a per-minute budget with noseparate burst cap (100/min on the lowest plan tier), and this fires onthe order of 2 x (space count) requests once, so a few dozen concurrentcalls stays well inside it even combined with other concurrent usage ofthe same token — api_client.py's shared retry logic (see below) alsobacks off on a real 429 rather than assuming this burst is the onlytraffic on the token.

Rate-limit retries are centralized, not per-tool

Every tool's HTTP calls go through ClickUpClient's shared _requestmethod (api_client.py), which retries 429/500/502/503/504with backoff — added here once so every tool benefits, since a token'srate-limit budget is shared across whatever else is calling the ClickUpAPI with it, not dedicated to any single tool. On a 429, the delayprefers ClickUp's own X-RateLimit-Reset header (a Unix timestamp forwhen the per-minute window resets — the header ClickUp's rate-limit docsactually document) over a generic Retry-After or blind exponentialbackoff, so retries wait exactly as long as ClickUp says to, not a guess.

Attachments and images

ClickUp's REST API has no way to attach a file directly to a comment —only to a task (POST /task/{task_id}/attachment, whatclickup_attach_task_file wraps). There is also no delete/updateattachment endpoint; re-uploading adds a new attachment rather thanreplacing the old one, and removing one requires the ClickUp web/desktopapp. Confirmed by checking ClickUp's own official MCP server's tooldescriptions too — same split (a Create Task Comment tool with noattachment support, and a separate Attach File to Task tool).

To make an image show up inline inside a comment, the underlyingtrick is: upload the file to the task first, then reference thereturned URL from the file's response using Markdown image syntax inthe comment text — ClickUp's comment renderer inlines it as a realimage, not just a link. clickup_create_comment_with_image does bothsteps in one call:

clickup_create_comment_with_image(task_id, file_content_base64, filename)
# internally:
#   1. POST /task/{task_id}/attachment  -> {"url": "...", ...}
#   2. POST /task/{task_id}/comment     comment_text = "![filename](url)"

To do it manually instead (e.g. to add other text around the image),call the two tools yourself:

1. result = clickup_attach_task_file(task_id, file_content_base64, filename)
   -> result["url"] is the uploaded file's URL

2. clickup_create_task_comment(
     task_id,
     comment_text=f"![{filename}]({result['url']})"
   )

API Reference

MCP Server · Populars

MCP Server · New

    bex-co

    Beancount.io Backend

    💰 Double-entry bookkeeping made easy — plain-text accounting for humans and AI agents. Polished iOS & Android app built with React Native + Expo.

    Community bex-co
    nkapila6

    mcpsurfr

    Local web search MCP server, formerly mcp-local-rag. Multi-engine deep research with on-device re-ranking. ✨ no API keys ✨

    Community nkapila6
    convex

    mcp.ts

    The open-source reactive database for app developers

    Community convex
    convex

    convex-backend

    The open-source reactive database for app developers

    Community convex
    Tripo-Developer

    Tripo MCP Server

    Official MCP server for Tripo

    Community Tripo-Developer