Skip to content

BD-ClickUp Integration Architecture

Overview

This project provides bidirectional sync between local bd (beads) issues and ClickUp tasks, exposed as MCP tools for AI-assisted development in Cursor IDE.

┌─────────────────────────────────────────────────────────────────────────┐
│                       CURSOR IDE / CLAUDE CODE                          │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  AI Assistant (Claude)                                            │  │
│  │  "Create a task for fixing the login bug"                         │  │
│  └──────────────────────────┬────────────────────────────────────────┘  │
│                             │ MCP Protocol                              │
│                             v                                           │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  BeadsMCPServer (beads_clickup/mcp_server.py)                     │  │
│  │  Tools: bd_create, bd_update, bd_close, bd_list, bd_show,         │  │
│  │         bd_search, bd_sync, bd_assign_subtask, bd_lifecycle_graph,│  │
│  │         bd_switch_workspace, bd_clickup_setup                     │  │
│  └──────────────────────────┬────────────────────────────────────────┘  │
└─────────────────────────────┼────────────────────────────────────────── ┘
                              v
┌─────────────────────────────────────────────────────────────────────────┐
│                         LOCAL MACHINE                                   │
│                                                                         │
│  ┌──────────────┐    ┌──────────────────┐    ┌────────────────────────┐ │
│  │  bd CLI      │    │  SyncEngine      │    │  .beads/               │ │
│  │  (wrapper    │───>│  (sync_engine.py)│───>│  ├── issues.jsonl      │ │
│  │   script)    │    │                  │    │  └── integrations/     │ │
│  └──────────────┘    │  FieldMapper     │    │      └── clickup/      │ │
│                      │  CustomFieldMapper│   │          ├── config    │ │
│  ┌──────────────┐    │  FieldInference  │    │          ├── sync_     │ │
│  │  bd-clickup  │    │  CommentSync     │    │          │   state     │ │
│  │  CLI         │    └────────┬─────────┘    │          └── caches    │ │
│  │  (cli.py)    │             │              └────────────────────────┘ │
│  └──────────────┘             │ REST API (HTTPS)                        │
│                               v                                         │
└───────────────────────────────┼─────────────────────────────────────────┘
                                v
┌─────────────────────────────────────────────────────────────────────────┐
│                        CLICKUP CLOUD                                    │
│  ┌───────────────────────────────────────────────────────────────────┐  │
│  │  ClickUp REST API v2 (api.clickup.com)                            │  │
│  │  Tasks, Custom Fields, Comments, Spaces, Lists                    │  │
│  └───────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘

Data Flow

Direction Trigger Path
Beads -> ClickUp bd_create, bd_update, bd_close via MCP or CLI MCP Server -> bd CLI -> SyncEngine -> FieldMapper -> ClickUpClient -> REST API
ClickUp -> Beads bd_sync SyncEngine fetches ClickUp tasks via REST API -> FieldMapper -> updates issues.jsonl
Full Sync bd_sync (full=true) SyncEngine reads all beads issues + all ClickUp tasks, reconciles both directions
Force Import bd_sync (force_import=true) Imports ClickUp tasks that weren't created by beads (skips beads-automation tag filter)

Project Structure

bd-clickup-integration/
├── beads_clickup/              # Main Python package
│   ├── __init__.py             # Public API exports, version
│   ├── __main__.py             # Entry: python -m beads_clickup -> MCP server
│   ├── mcp_server.py           # MCP server (JSON-RPC over stdio)
│   ├── cli.py                  # CLI commands (bd-clickup)
│   ├── sync_engine.py          # Core bidirectional sync logic
│   ├── clickup_client.py       # ClickUp REST API v2 client
│   ├── field_mapper.py         # Beads <-> ClickUp field mapping
│   ├── field_inference.py      # Infer custom field values from content
│   ├── provisioning.py         # ClickUp list/status/field provisioning
│   ├── comment_sync.py         # Bidirectional comment sync
│   ├── lifecycle_graph.py      # Task graph visualization (vis-network)
│   ├── lifecycle_stages.py     # Stage definitions and colors
│   ├── stage_classifier.py     # LLM-based lifecycle stage classification
│   ├── neighbor_classifier.py  # LLM-based task neighbor inference
│   ├── issue_template.py       # Issue description templates and validation
│   ├── models.py               # Data models (Issue, ClickUpTask, SyncState, etc.)
│   ├── constants.py            # Enums (IssueStatus, Priority, IssueType, etc.)
│   ├── exceptions.py           # Exception hierarchy
│   ├── env_detection.py        # Python environment detection
│   ├── mcp_client.py           # Deprecated alias for clickup_client
│   └── py.typed                # PEP 561 marker
├── tests/                      # Pytest test suite
│   ├── test_sync_engine.py
│   ├── test_field_mapper.py
│   ├── test_field_inference.py
│   ├── test_mcp_server.py
│   ├── test_mcp_client.py
│   ├── test_comment_sync.py
│   ├── test_issue_template.py
│   ├── test_env_detection.py
│   └── integration/
│       └── test_full_sync.py
├── scripts/
│   └── setup.sh                # Interactive project setup (venv, config, Cursor MCP, skills)
├── docs/                       # Documentation
│   ├── architecture.md         # This file
│   ├── quickstart.md
│   ├── setup.md
│   ├── mcp-setup.md
│   ├── deploy.md
│   ├── LIFECYCLE_STAGES.md
│   ├── issue-templates.md
│   └── ...
├── bd                          # Bash wrapper: intercepts bd commands, triggers sync
├── pyproject.toml              # Package config (hatchling), deps, tool config
├── uv.lock                     # Dependency lock file (uv)
├── config.yaml.example         # Example ClickUp config
├── templates.yaml.example      # Example issue template config
├── cursor-mcp-config.json      # Example Cursor MCP config
├── CLAUDE.md                   # Agent instructions (Claude Code)
├── AGENTS.md                   # Agent instructions (Cursor)
└── README.md

Module Architecture

Core Modules

                        ┌──────────────────┐
                        │  BeadsMCPServer   │ JSON-RPC over stdio
                        │  (mcp_server.py)  │ 11 tools exposed
                        └────────┬─────────┘
                                 │ delegates to
              ┌──────────────────┼──────────────────────┐
              │                  │                      │
              v                  v                      v
   ┌──────────────────┐  ┌──────────────┐  ┌────────────────────┐
   │  SyncEngine      │  │ Provisioner  │  │  lifecycle_graph   │
   │  (sync_engine)   │  │ (provision.) │  │  (lifecycle_graph) │
   └────────┬─────────┘  └──────┬───────┘  └────────┬───────────┘
            │                   │                    │
   ┌────────┼───────────┐      │           ┌────────┼───────────┐
   │        │           │      │           │        │           │
   v        v           v      v           v        v           v
┌───────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐
│ClickUp│ │  Field   │ │ Comment  │ │  Stage    │ │ Neighbor  │
│Client  │ │  Mapper  │ │  Sync   │ │Classifier │ │Classifier │
└───────┘ └────┬─────┘ └──────────┘ └───────────┘ └───────────┘
               v
         ┌───────────┐
         │  Field    │
         │ Inference │
         └───────────┘

Module Responsibilities

Module Responsibility
mcp_server.py MCP protocol handler. Receives JSON-RPC calls, dispatches to bd CLI and SyncEngine. Manages workspace discovery and switching.
cli.py CLI entry point (bd-clickup). Subcommands: init, sync, push, pull, link, status, provision, setup, lifecycle-graph, mcp-config.
sync_engine.py Core sync orchestrator. Reads issues.jsonl, manages sync_state.json, coordinates FieldMapper/ClickUpClient/CommentSync. Supports concurrent sync via max_workers.
clickup_client.py HTTP client wrapping ClickUp REST API v2. Handles auth, rate limiting, error mapping. Supports tasks, custom fields, comments, spaces, lists, folders.
field_mapper.py Two classes: FieldMapper (status/priority/labels mapping, full issue-to-task conversion) and CustomFieldMapper (dropdown/text/url/number/date type conversion with option ID resolution).
field_inference.py Rule-based inference of custom field values (lifecycle stage, category, effort, business value) from issue content. Used as fallback when fields aren't explicitly set.
provisioning.py ClickUpProvisioner creates/configures ClickUp lists with correct statuses and custom fields. Writes discovered field IDs back to config.yaml.
comment_sync.py Bidirectional comment sync. Stores comments as JSONL per issue. Tracks sync state per comment.
lifecycle_graph.py Builds interactive task dependency graph using vis-network. Coordinates LLM classifiers for stage/neighbor inference, exports standalone HTML.
stage_classifier.py LLM-powered (OpenAI/Anthropic) lifecycle stage assignment. Batch classification with JSON cache and TTL.
neighbor_classifier.py LLM-powered task relationship inference. Identifies related tasks, validates bidirectionality, caches results.
issue_template.py Issue description template loaded from .beads/templates.yaml (or built-in default). Validates description structure, parses sections, tracks DoD completion.
models.py Dataclass models: Issue, ClickUpTask, SyncState, SyncStats, Comment, CustomFieldConfig. All with from_dict()/to_dict() serialization.
constants.py Enums: IssueStatus, Priority, ClickUpPriority, IssueType, CustomFieldType, SyncDirection, LifecycleStage, Effort, BusinessValue, Category. Includes conversion helpers.
exceptions.py Hierarchy rooted at BeadsClickUpError with specific subtypes for API errors (401/403/404/429), sync conflicts, field mapping failures, and project errors.
env_detection.py Finds a valid Python executable with beads_clickup importable. Checks venv, system, and sys.executable.

Data Models

┌──────────────────────────────┐     ┌──────────────────────────────┐
│  Issue (beads)               │     │  ClickUpTask                 │
│  ─────────────               │     │  ────────────                │
│  id: str                     │     │  id: str                     │
│  title: str                  │◄───►│  name: str                   │
│  status: IssueStatus         │     │  status: str                 │
│  priority: Priority          │     │  priority: int               │
│  issue_type: IssueType       │     │  description: str            │
│  description: str            │     │  tags: list[str]             │
│  labels: list[str]           │     │  assignees: list             │
│  custom_fields: dict         │     │  custom_fields: list[dict]   │
│  external_ref: str           │     │  parent_id: str              │
│  created_at, updated_at      │     │  list_id, space_id           │
│  closed_at, close_reason     │     │  start_date, due_date, url   │
└──────────────┬───────────────┘     └──────────────┬───────────────┘
               │                                    │
               │         ┌──────────────────┐       │
               └────────►│  SyncState       │◄──────┘
                         │  ──────────      │
                         │  issue_id        │
                         │  clickup_task_id │
                         │  last_synced     │
                         │  sync_direction  │
                         └──────────────────┘

Local Storage

File Format Purpose
.beads/issues.jsonl JSONL All beads issues (one JSON object per line)
.beads/templates.yaml YAML Issue description template (optional, customizable)
.beads/integrations/clickup/config.yaml YAML ClickUp workspace, list, field mappings, sync options
.beads/integrations/clickup/sync_state.json JSON Map of issue_id -> {clickup_task_id, last_synced, ...}
.beads/integrations/clickup/comments/{id}.jsonl JSONL Comments per issue
.beads/integrations/clickup/stage_cache.json JSON LLM stage classification cache
.beads/integrations/clickup/neighbor_cache.json JSON LLM neighbor inference cache

Entry Points

Entry Point How to Run What it Does
MCP Server python -m beads_clickup Starts JSON-RPC stdio server for Cursor IDE
CLI bd-clickup <command> Direct CLI for sync, setup, provisioning
bd Wrapper ./bd <command> Intercepts bd CLI commands and triggers ClickUp sync

MCP Tools

The MCP server exposes 11 tools to AI assistants:

Tool Description
bd_create Create a new beads issue (auto-syncs to ClickUp)
bd_update Update issue status, priority, or fields (auto-syncs)
bd_close Close an issue with a reason (auto-syncs)
bd_list List issues with filters (status, priority)
bd_show Show full details of a specific issue
bd_search Search issues by text query
bd_assign_subtask Assign parent-child relationships between tasks
bd_sync Bidirectional sync (full, force_import options)
bd_lifecycle_graph Generate interactive task graph with LLM-inferred stages and neighbors
bd_switch_workspace Switch active beads workspace (or list available)
bd_clickup_setup One-command board setup: create list, provision statuses and custom fields

Sync Mechanism

State Tracking

The sync_state.json file maintains a mapping for each synced issue:

{
  "issue-abc": {
    "clickup_task_id": "abc123",
    "last_synced": "2025-01-15T10:30:00",
    "last_beads_update": "2025-01-15T10:29:00",
    "last_clickup_update": "2025-01-15T09:00:00",
    "sync_direction": "bidirectional"
  }
}

Sync Modes

Mode Description
Incremental Only syncs issues modified since last sync (timestamp-based)
Full Compares all beads issues against all ClickUp tasks
Force Import Imports ClickUp tasks not created by beads (bypasses beads-automation tag filter)

Conflict Resolution

When both sides have changed since last sync, the configured strategy applies: - beads_wins: Local changes overwrite ClickUp - clickup_wins: ClickUp changes overwrite local - manual: Log conflict to conflicts.jsonl for human review


Custom Field Pipeline

When syncing issues to ClickUp, custom fields go through a three-tier pipeline:

┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│  Config Defaults │────>│  Inferred Values │────>│  Explicit Values │
│  (lowest)        │     │  (medium)        │     │  (highest)       │
│                  │     │                  │     │                  │
│  config.yaml     │     │  FieldInference  │     │  issue.custom_   │
│  default: "..."  │     │  Engine rules    │     │  fields dict     │
└──────────────────┘     └──────────────────┘     └──────────────────┘
                                                          v
                                                  ┌──────────────────┐
                                                  │ CustomFieldMapper │
                                                  │ Converts to       │
                                                  │ ClickUp format    │
                                                  │ (dropdown IDs,    │
                                                  │  timestamps, etc) │
                                                  └──────────────────┘

Supported Field Types

Type Beads Value ClickUp API Value
drop_down Option name (string) Option ID (UUID or orderindex)
text / short_text String String
url URL string URL string
number Number Float
date ISO string or Unix ms Unix timestamp (ms)
users User ID or list List of user IDs
labels String or list List of label strings

Lenient vs Strict Mode

field_mapping:
  lenient_mode: true   # Log warnings, skip bad fields (default)
  # lenient_mode: false  # Raise exceptions on mapping failures

Issue Templates

When creating issues, descriptions are populated from a configurable template. The template is a single YAML file at .beads/templates.yaml. If the file doesn't exist, a built-in default is used.

# .beads/templates.yaml
template:
  objective: "[title]"
  plan: |
    1. Step one
    2. Step two
    3. Step three
  definition_of_done: |
    - [ ] Acceptance criterion 1
    - [ ] Acceptance criterion 2
    - [ ] Acceptance criterion 3
  testing_plan: "Manual verification"

Each key becomes a **Section Name:** header in the issue description. The [title] placeholder is replaced with the issue title. Sections can be added, removed, or reordered freely -- any YAML key is accepted. A **Results:** TBD section is appended automatically if not specified.

See templates.yaml.example at the repo root for the full reference with comments.


LLM-Powered Features

Two optional features use LLM APIs (OpenAI or Anthropic) for intelligent task analysis:

Stage Classification (stage_classifier.py)

  • Assigns lifecycle stages to tasks based on title, description, type, and status
  • Supports batch classification for efficiency
  • Results cached to JSON with configurable TTL
  • Providers: OpenAI (gpt-4o-mini) or Anthropic (claude-3-haiku)

Neighbor Inference (neighbor_classifier.py)

  • Identifies related/dependent tasks from the full issue set
  • Validates bidirectionality (if A relates to B, B relates to A)
  • Limits max neighbors per issue (configurable)
  • Results cached with TTL

Lifecycle Graph (lifecycle_graph.py)

  • Combines stage + neighbor data into an interactive vis-network graph
  • Groups tasks by lifecycle stage with color coding
  • Exports standalone HTML file
  • Optionally syncs inferred data back to ClickUp

Exception Hierarchy

BeadsClickUpError
├── ConfigurationError
│   ├── MissingConfigError
│   └── InvalidConfigError
├── ClickUpAPIError (status_code, endpoint)
│   ├── AuthenticationError (401)
│   ├── PermissionError (403)
│   ├── ResourceNotFoundError (404)
│   └── RateLimitError (429)
├── SyncError (issue_id, task_id, direction)
│   ├── IssueNotFoundError
│   ├── TaskNotFoundError
│   └── SyncConflictError
├── FieldMappingError (field_name, field_value)
│   ├── InvalidFieldValueError
│   ├── UnknownFieldError
│   └── MissingRequiredFieldError
└── ProjectError
    ├── ProjectNotFoundError
    └── InvalidProjectError

ClickUpAPIError.from_response() factory auto-selects the correct subclass by HTTP status code.


Cursor / Claude Code MCP Setup

Add to ~/.cursor/mcp.json (Cursor) or configure via .claude/settings.json (Claude Code):

{
  "mcpServers": {
    "beads": {
      "command": "beads-mcp-server",
      "env": {
        "BEADS_PROJECT_DIR": "/path/to/your/project",
        "CLICKUP_API_TOKEN": "pk_..."
      }
    }
  }
}

Run scripts/setup.sh to configure this automatically. See MCP Setup for details.

The BEADS_PROJECT_DIR environment variable sets the default workspace. The MCP server also supports runtime workspace switching via bd_switch_workspace.


Why Not ClickUp's Official MCP Server?

ClickUp has an official MCP server, but it requires OAuth authentication which doesn't work with personal API tokens. This project uses the REST API directly, giving you:

  • Works with personal API tokens (no OAuth flow)
  • Full control over sync logic and conflict resolution
  • Custom field mapping with type-safe conversion
  • Bidirectional sync with local issue storage
  • LLM-powered task analysis features
  • Offline-first: local .beads/ is the source of truth

Tech Stack

Component Technology
Language Python 3.11+
Build Hatchling
Package Manager uv
HTTP Client requests
Config PyYAML
Validation Pydantic (LLM response parsing)
LLM openai, anthropic SDKs
Environment python-dotenv
Testing pytest, pytest-cov
Linting ruff, mypy (strict)
Dev Environment Devbox

Testing

pytest                              # Run full test suite with coverage
pytest tests/test_sync_engine.py    # Run specific module tests
pytest tests/integration/           # Run integration tests (requires API token)

Test coverage spans: sync engine, field mapper, field inference, MCP server, ClickUp client, comment sync, issue templates, and environment detection.