@hauptsache.net/clickup-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +89 -0
- package/dist/clickup-text.d.ts +45 -0
- package/dist/clickup-text.d.ts.map +1 -0
- package/dist/clickup-text.js +131 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +174 -0
- package/dist/test-utils.d.ts +23 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +44 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# ClickUp MCP (Model Context Protocol)
|
|
2
|
+
|
|
3
|
+
A minimal implementation of a Model Context Protocol (MCP) server for ClickUp integration, designed to allow Large Language Models (LLMs) to read ClickUp tasks and data.
|
|
4
|
+
|
|
5
|
+
The main focus is to represent tickets in a way that is easy for LLMs to process and understand.
|
|
6
|
+
This especially includes images in the description and in comments of tickets,
|
|
7
|
+
which are not that easy to understand by just passing the api responses.
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
This MCP provides a lightweight interface for LLMs to:
|
|
12
|
+
- Retrieve detailed task information by ID
|
|
13
|
+
- Search for tasks using keywords
|
|
14
|
+
- List open tasks assigned to the current user
|
|
15
|
+
|
|
16
|
+
The implementation includes image handling for task descriptions, allowing LLMs to process both text content and visual information from ClickUp tasks.
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
- Node.js (v16 or higher)
|
|
21
|
+
- A ClickUp account with API access
|
|
22
|
+
- ClickUp API key and Team ID
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
1. Clone this repository
|
|
27
|
+
2. Install dependencies:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
3. Build the TypeScript code:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm run build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
Set the following environment variables:
|
|
42
|
+
|
|
43
|
+
- `CLICKUP_API_KEY`: Your ClickUp API key
|
|
44
|
+
- `CLICKUP_TEAM_ID`: Your ClickUp team ID
|
|
45
|
+
|
|
46
|
+
You can set these variables in your environment or create a `.env` file at the root of the project.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### As an MCP Server
|
|
51
|
+
|
|
52
|
+
To use this as an MCP server with an LLM:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm start
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This will start the server using the standard input/output for communication, following the Model Context Protocol.
|
|
59
|
+
|
|
60
|
+
### CLI Usage
|
|
61
|
+
|
|
62
|
+
For testing or manual usage, you can use the CLI interface:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# List all available tools
|
|
66
|
+
npm run cli
|
|
67
|
+
|
|
68
|
+
# Get a task by ID
|
|
69
|
+
npm run cli getTaskById id=abc1234
|
|
70
|
+
|
|
71
|
+
# Search for tasks
|
|
72
|
+
npm run cli searchTask terms="feature|bug|enhancement"
|
|
73
|
+
|
|
74
|
+
# List open tasks for the current user
|
|
75
|
+
npm run cli listTodo
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Available Tools
|
|
79
|
+
|
|
80
|
+
### getTaskById
|
|
81
|
+
|
|
82
|
+
Retrieves a complete ClickUp task with its description, comments, and embedded images.
|
|
83
|
+
|
|
84
|
+
Parameters:
|
|
85
|
+
- `id`: The 7-9 character ClickUp task ID (without "#" or "CU-" prefix)
|
|
86
|
+
|
|
87
|
+
### searchTask
|
|
88
|
+
|
|
89
|
+
Searches for tasks by name with support for multiple search terms (using OR logic).
|
|
90
|
+
|
|
91
|
+
Parameters:
|
|
92
|
+
- `terms`: Search terms separated by '|' (e.g., 'term1|term2|term3')
|
|
93
|
+
|
|
94
|
+
### listTodo
|
|
95
|
+
|
|
96
|
+
Lists all open tasks assigned to the current user.
|
|
97
|
+
|
|
98
|
+
Parameters: None
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
For development with hot reloading:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm run dev
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Format code with Prettier:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm run prettier
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## How It Works
|
|
115
|
+
|
|
116
|
+
This MCP implementation:
|
|
117
|
+
|
|
118
|
+
1. Connects to the ClickUp API using your API key
|
|
119
|
+
2. Provides tools for LLMs to query and process ClickUp data
|
|
120
|
+
3. Handles image processing in Markdown descriptions
|
|
121
|
+
4. Returns structured data in a format optimized for LLM consumption
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
ISC
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_1 = require("./index");
|
|
5
|
+
async function main() {
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
if (args.length < 1) {
|
|
8
|
+
console.error("Usage: npm run cli <tool-name> [param1=value1 param2=value2 ...]");
|
|
9
|
+
console.error("\nAvailable tools:");
|
|
10
|
+
// @ts-ignore - Accessing private property for testing purposes
|
|
11
|
+
const tools = index_1.server._registeredTools;
|
|
12
|
+
if (tools) {
|
|
13
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
14
|
+
console.error(` - ${name}: ${tool.description}`);
|
|
15
|
+
console.error(" Parameters:");
|
|
16
|
+
// Get parameter information from the inputSchema
|
|
17
|
+
const shape = tool.inputSchema._def.shape();
|
|
18
|
+
for (const [paramName, schema] of Object.entries(shape)) {
|
|
19
|
+
// @ts-ignore - Accessing schema description
|
|
20
|
+
const description = schema.description || "No description";
|
|
21
|
+
console.error(` - ${paramName}: ${description}`);
|
|
22
|
+
}
|
|
23
|
+
console.error("");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const toolName = args[0];
|
|
29
|
+
const params = {};
|
|
30
|
+
// Parse parameters
|
|
31
|
+
for (let i = 1; i < args.length; i++) {
|
|
32
|
+
const arg = args[i];
|
|
33
|
+
const match = arg.match(/^([^=]+)=(.*)$/);
|
|
34
|
+
if (match) {
|
|
35
|
+
const [, key, value] = match;
|
|
36
|
+
// Try to parse as JSON if it looks like a JSON value
|
|
37
|
+
try {
|
|
38
|
+
if (value.startsWith('{') || value.startsWith('[') ||
|
|
39
|
+
value === 'true' || value === 'false' ||
|
|
40
|
+
!isNaN(Number(value))) {
|
|
41
|
+
params[key] = JSON.parse(value);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
params[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
params[key] = value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
// @ts-ignore - Accessing private property for testing purposes
|
|
54
|
+
const tools = index_1.server._registeredTools;
|
|
55
|
+
if (!tools || !tools[toolName]) {
|
|
56
|
+
console.error(`Unknown tool: ${toolName}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const tool = tools[toolName];
|
|
60
|
+
// Validate parameters using the tool's schema
|
|
61
|
+
try {
|
|
62
|
+
tool.inputSchema.parse(params);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const validationError = error;
|
|
66
|
+
console.error("Parameter validation error:", validationError.message);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
// Mock environment variables for testing if they're not set
|
|
70
|
+
if (!process.env.CLICKUP_API_KEY || !process.env.CLICKUP_TEAM_ID) {
|
|
71
|
+
console.warn("Warning: Using mock API credentials. This will not return real data.");
|
|
72
|
+
process.env.CLICKUP_API_KEY = process.env.CLICKUP_API_KEY || 'test_api_key';
|
|
73
|
+
process.env.CLICKUP_TEAM_ID = process.env.CLICKUP_TEAM_ID || 'test_team_id';
|
|
74
|
+
}
|
|
75
|
+
// Call the tool's callback function
|
|
76
|
+
const result = await tool.callback(params);
|
|
77
|
+
console.dir(result.content);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (error instanceof Error) {
|
|
81
|
+
console.error("Error:", error.message);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.error("Unknown error occurred");
|
|
85
|
+
}
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types";
|
|
2
|
+
/**
|
|
3
|
+
* Represents a ClickUp text item which can be plain text or an image
|
|
4
|
+
*/
|
|
5
|
+
export interface ClickUpTextItem {
|
|
6
|
+
text?: string;
|
|
7
|
+
type?: string;
|
|
8
|
+
image?: {
|
|
9
|
+
id?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
type?: string;
|
|
13
|
+
extension?: string;
|
|
14
|
+
thumbnail_large?: string;
|
|
15
|
+
thumbnail_medium?: string;
|
|
16
|
+
thumbnail_small?: string;
|
|
17
|
+
url: string;
|
|
18
|
+
uploaded?: boolean;
|
|
19
|
+
};
|
|
20
|
+
attributes?: any;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Represents a ClickUp attachment
|
|
24
|
+
*/
|
|
25
|
+
export interface ClickUpAttachment {
|
|
26
|
+
thumbnail_large?: string;
|
|
27
|
+
url: string;
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Process an array of ClickUp text items into a structured content format
|
|
32
|
+
* that includes both text and images in their original sequence
|
|
33
|
+
*
|
|
34
|
+
* @param textItems Array of text items from ClickUp API
|
|
35
|
+
* @returns Promise resolving to an array of content blocks (text and images)
|
|
36
|
+
*/
|
|
37
|
+
export declare function processClickUpText(textItems: ClickUpTextItem[]): Promise<CallToolResult["content"]>;
|
|
38
|
+
/**
|
|
39
|
+
* Splits markdown text at image references and converts them to image blocks
|
|
40
|
+
* @param markdownText The markdown text to process
|
|
41
|
+
* @param attachments Array of attachments from the Clickup API
|
|
42
|
+
* @returns Array of content blocks (text and images)
|
|
43
|
+
*/
|
|
44
|
+
export declare function processClickUpMarkdown(markdownText: string, attachments: ClickUpAttachment[]): Promise<CallToolResult["content"]>;
|
|
45
|
+
//# sourceMappingURL=clickup-text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clickup-text.d.ts","sourceRoot":"","sources":["../src/clickup-text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAGjE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE;QACN,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,GAAG,EAAE,MAAM,CAAC;QACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,UAAU,CAAC,EAAE,GAAG,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,eAAe,EAAE,GAC3B,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAiDpC;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,iBAAiB,EAAE,GAC/B,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAsEpC"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processClickUpText = processClickUpText;
|
|
4
|
+
exports.processClickUpMarkdown = processClickUpMarkdown;
|
|
5
|
+
const buffer_1 = require("buffer");
|
|
6
|
+
/**
|
|
7
|
+
* Process an array of ClickUp text items into a structured content format
|
|
8
|
+
* that includes both text and images in their original sequence
|
|
9
|
+
*
|
|
10
|
+
* @param textItems Array of text items from ClickUp API
|
|
11
|
+
* @returns Promise resolving to an array of content blocks (text and images)
|
|
12
|
+
*/
|
|
13
|
+
async function processClickUpText(textItems) {
|
|
14
|
+
const contentBlocks = [];
|
|
15
|
+
let currentTextBlock = "";
|
|
16
|
+
for (let i = 0; i < textItems.length; i++) {
|
|
17
|
+
const item = textItems[i];
|
|
18
|
+
// Handle image items
|
|
19
|
+
if (item.type === "image" && item.image && item.image.thumbnail_large) {
|
|
20
|
+
// If we have accumulated text, add it as a text block before adding the image
|
|
21
|
+
if (currentTextBlock.trim()) {
|
|
22
|
+
contentBlocks.push(Promise.resolve({
|
|
23
|
+
type: "text",
|
|
24
|
+
text: currentTextBlock.trim(),
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
// Reset current text block after pushing it
|
|
28
|
+
currentTextBlock = "";
|
|
29
|
+
// Add this image as an image block
|
|
30
|
+
if (typeof item.image.thumbnail_large === "string") {
|
|
31
|
+
contentBlocks.push(loadImageContentBlock(item.image.thumbnail_large, {
|
|
32
|
+
type: "text",
|
|
33
|
+
text: item.text || "Image",
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Handle text items
|
|
38
|
+
else if (item.text !== undefined) {
|
|
39
|
+
currentTextBlock += item.text;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Add any remaining text
|
|
43
|
+
if (currentTextBlock.trim()) {
|
|
44
|
+
contentBlocks.push(Promise.resolve({
|
|
45
|
+
type: "text",
|
|
46
|
+
text: currentTextBlock.trim(),
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
return Promise.all(contentBlocks);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Splits markdown text at image references and converts them to image blocks
|
|
53
|
+
* @param markdownText The markdown text to process
|
|
54
|
+
* @param attachments Array of attachments from the Clickup API
|
|
55
|
+
* @returns Array of content blocks (text and images)
|
|
56
|
+
*/
|
|
57
|
+
async function processClickUpMarkdown(markdownText, attachments) {
|
|
58
|
+
const contentBlocks = [];
|
|
59
|
+
let currentTextBlock = "";
|
|
60
|
+
// Create a map of attachment URLs to their thumbnail_large URLs for easy lookup
|
|
61
|
+
const thumbnailMap = new Map();
|
|
62
|
+
for (const attachment of attachments) {
|
|
63
|
+
if (typeof attachment.thumbnail_large === "string") {
|
|
64
|
+
thumbnailMap.set(attachment.url, attachment.thumbnail_large);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Regular expression to match markdown image syntax: 
|
|
68
|
+
const imageRegex = /!\[([^\]]*)\]\(([^\)]+)\)/g;
|
|
69
|
+
let lastIndex = 0;
|
|
70
|
+
let match;
|
|
71
|
+
while ((match = imageRegex.exec(markdownText)) !== null) {
|
|
72
|
+
const [fullMatch, altText, imageUrl] = match;
|
|
73
|
+
// Add text before the image reference to the current text block
|
|
74
|
+
currentTextBlock += markdownText.substring(lastIndex, match.index);
|
|
75
|
+
// Check if this image URL exists in our attachments
|
|
76
|
+
const thumbnailUrl = thumbnailMap.get(imageUrl);
|
|
77
|
+
if (thumbnailUrl) {
|
|
78
|
+
// If we have accumulated text, add it as a text block before adding the image
|
|
79
|
+
if (currentTextBlock.trim()) {
|
|
80
|
+
contentBlocks.push(Promise.resolve({
|
|
81
|
+
type: "text",
|
|
82
|
+
text: currentTextBlock.trim(),
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
// Reset current text block after pushing it
|
|
86
|
+
currentTextBlock = "";
|
|
87
|
+
// Add this image as an image block
|
|
88
|
+
contentBlocks.push(loadImageContentBlock(thumbnailUrl, {
|
|
89
|
+
type: "text",
|
|
90
|
+
text: fullMatch.trim(),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// If the image URL doesn't match any attachment, keep the original markdown in the current text block
|
|
95
|
+
currentTextBlock += fullMatch;
|
|
96
|
+
console.error(`Image URL ${imageUrl} not found in attachments`, thumbnailMap);
|
|
97
|
+
}
|
|
98
|
+
lastIndex = match.index + fullMatch.length;
|
|
99
|
+
}
|
|
100
|
+
// Add any remaining text after the last image
|
|
101
|
+
currentTextBlock += markdownText.substring(lastIndex);
|
|
102
|
+
if (currentTextBlock.trim()) {
|
|
103
|
+
contentBlocks.push(Promise.resolve({
|
|
104
|
+
type: "text",
|
|
105
|
+
text: currentTextBlock.trim(),
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
return Promise.all(contentBlocks);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Loads an image from a URL and converts it to a base64-encoded content block
|
|
112
|
+
*
|
|
113
|
+
* @param url URL of the image to load
|
|
114
|
+
* @param fallback Fallback content if image loading fails
|
|
115
|
+
* @returns Promise resolving to a content block (either image or fallback text)
|
|
116
|
+
*/
|
|
117
|
+
async function loadImageContentBlock(url, fallback) {
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(url);
|
|
120
|
+
const imageBuffer = await response.arrayBuffer();
|
|
121
|
+
return {
|
|
122
|
+
type: "image",
|
|
123
|
+
mimeType: response.headers.get("Content-Type") || "image/png",
|
|
124
|
+
data: buffer_1.Buffer.from(imageBuffer).toString("base64"),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error(`Error fetching image: ${error.message || "Unknown error"}`);
|
|
129
|
+
return fallback;
|
|
130
|
+
}
|
|
131
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAgBpE,eAAO,MAAM,MAAM,WAGjB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.server = void 0;
|
|
4
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
const clickup_text_1 = require("./clickup-text");
|
|
8
|
+
const CONFIG = {
|
|
9
|
+
apiKey: process.env.CLICKUP_API_KEY,
|
|
10
|
+
teamId: process.env.CLICKUP_TEAM_ID,
|
|
11
|
+
};
|
|
12
|
+
if (!CONFIG.apiKey || !CONFIG.teamId) {
|
|
13
|
+
throw new Error("Missing Clickup API key or team ID");
|
|
14
|
+
}
|
|
15
|
+
// Create an MCP server
|
|
16
|
+
exports.server = new mcp_js_1.McpServer({
|
|
17
|
+
name: "Clickup MCP",
|
|
18
|
+
version: "1.0.0",
|
|
19
|
+
});
|
|
20
|
+
exports.server.tool("getTaskById", "Get a Clickup task with images and comments by ID", {
|
|
21
|
+
id: zod_1.z
|
|
22
|
+
.string()
|
|
23
|
+
.min(7)
|
|
24
|
+
.max(9)
|
|
25
|
+
.describe(`The 7-9 character ID of the task to get without a prefix like "#" or "CU-"`),
|
|
26
|
+
}, async ({ id }) => {
|
|
27
|
+
const [content, comments] = await Promise.all([
|
|
28
|
+
loadTaskContent(id),
|
|
29
|
+
loadTaskComments(id),
|
|
30
|
+
]);
|
|
31
|
+
return {
|
|
32
|
+
content: [...content, ...comments],
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
async function loadTaskContent(id) {
|
|
36
|
+
const response = await fetch(`https://api.clickup.com/api/v2/task/${id}?include_markdown_description=true`, { headers: { Authorization: CONFIG.apiKey } });
|
|
37
|
+
const task = await response.json();
|
|
38
|
+
const content = await (0, clickup_text_1.processClickUpMarkdown)(task.markdown_description || "", task.attachments);
|
|
39
|
+
// Create the task metadata block
|
|
40
|
+
const taskMetadata = {
|
|
41
|
+
type: "text",
|
|
42
|
+
text: [
|
|
43
|
+
`task_id: ${task.id}`,
|
|
44
|
+
`name: ${task.name}`,
|
|
45
|
+
`status: ${task.status.status}`,
|
|
46
|
+
`date_created: ${new Date(+task.date_created)}`,
|
|
47
|
+
`date_updated: ${new Date(+task.date_updated)}`,
|
|
48
|
+
`creator: ${task.creator.username}`,
|
|
49
|
+
`list: ${task.list.name} (${task.list.id})`,
|
|
50
|
+
].join("\n"),
|
|
51
|
+
};
|
|
52
|
+
return [taskMetadata, ...content];
|
|
53
|
+
}
|
|
54
|
+
async function loadTaskComments(id) {
|
|
55
|
+
const response = await fetch(`https://api.clickup.com/api/v2/task/${id}/comment`, { headers: { Authorization: CONFIG.apiKey } });
|
|
56
|
+
const comments = await response.json();
|
|
57
|
+
return Promise.all(comments.comments
|
|
58
|
+
.sort((a, b) => +a.date - +b.date)
|
|
59
|
+
.map(async (comment) => {
|
|
60
|
+
// Create a header for the comment
|
|
61
|
+
const commentHeader = {
|
|
62
|
+
type: "text",
|
|
63
|
+
text: [
|
|
64
|
+
`comment_id: ${comment.id}`,
|
|
65
|
+
`date: ${new Date(+comment.date)}`,
|
|
66
|
+
`user: ${comment.user.username}`,
|
|
67
|
+
].join("\n"),
|
|
68
|
+
};
|
|
69
|
+
// Process comment items if they exist
|
|
70
|
+
if (comment.comment && Array.isArray(comment.comment)) {
|
|
71
|
+
const commentContentBlocks = await (0, clickup_text_1.processClickUpText)(comment.comment);
|
|
72
|
+
return [commentHeader, ...commentContentBlocks];
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
return [
|
|
76
|
+
{ type: "text", text: commentHeader.text },
|
|
77
|
+
{ type: "text", text: comment.comment_text },
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
let cachedTasks = [];
|
|
83
|
+
let lastTaskCacheUpdate = 0;
|
|
84
|
+
exports.server.tool("searchTask", [
|
|
85
|
+
"Searches tasks by name (case insensitive) with support for multiple search terms (OR logic).",
|
|
86
|
+
"You'll get a rough overview of the tasks that match the search terms.",
|
|
87
|
+
"Always use getTaskById to get more specific information if a task is relevant.",
|
|
88
|
+
].join("\n"), {
|
|
89
|
+
terms: zod_1.z
|
|
90
|
+
.string()
|
|
91
|
+
.min(3)
|
|
92
|
+
.describe("Search terms separated by '|' for OR logic (e.g., 'term1|term2|term3')"),
|
|
93
|
+
}, async ({ terms }) => {
|
|
94
|
+
const timeSinceLastUpdate = Date.now() - lastTaskCacheUpdate;
|
|
95
|
+
if (timeSinceLastUpdate > 10000) {
|
|
96
|
+
const taskLists = await Promise.all([...Array(30)].map((_, i) => {
|
|
97
|
+
return fetch(`https://api.clickup.com/api/v2/team/${CONFIG.teamId}/task?order_by=updated&page=${i}`, { headers: { Authorization: CONFIG.apiKey } }).then((res) => res.json());
|
|
98
|
+
}));
|
|
99
|
+
cachedTasks = taskLists.flatMap((taskList) => taskList.tasks);
|
|
100
|
+
lastTaskCacheUpdate = Date.now();
|
|
101
|
+
}
|
|
102
|
+
const searchTerms = terms
|
|
103
|
+
.split("|")
|
|
104
|
+
.map((term) => term.trim().toLowerCase());
|
|
105
|
+
const tasks = cachedTasks.filter((task) => {
|
|
106
|
+
const taskNameLower = task.name.toLowerCase();
|
|
107
|
+
return searchTerms.some((term) => taskNameLower.includes(term));
|
|
108
|
+
});
|
|
109
|
+
if (tasks.length === 0) {
|
|
110
|
+
return {
|
|
111
|
+
content: [
|
|
112
|
+
{
|
|
113
|
+
type: "text",
|
|
114
|
+
text: "No tasks found matching the search terms.",
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
content: tasks.map((task) => ({
|
|
121
|
+
type: "text",
|
|
122
|
+
text: [
|
|
123
|
+
`task_id: ${task.id}`,
|
|
124
|
+
`name: ${task.name}`,
|
|
125
|
+
`status: ${task.status.status}`,
|
|
126
|
+
`date_created: ${new Date(+task.date_created)}`,
|
|
127
|
+
`date_updated: ${new Date(+task.date_updated)}`,
|
|
128
|
+
`creator: ${task.creator.username}`,
|
|
129
|
+
`list: ${task.list.name} (${task.list.id})`,
|
|
130
|
+
].join("\n"),
|
|
131
|
+
})),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
exports.server.tool("listTodo", "Lists all open tasks for the current user.", {}, async () => {
|
|
135
|
+
// fetch current user ID
|
|
136
|
+
const userResp = await fetch("https://api.clickup.com/api/v2/user", {
|
|
137
|
+
headers: { Authorization: CONFIG.apiKey },
|
|
138
|
+
}).then((res) => res.json());
|
|
139
|
+
const userId = userResp.user.id;
|
|
140
|
+
// page through team tasks assigned to this user
|
|
141
|
+
const taskLists = await Promise.all([...Array(10)].map((_, i) => fetch(`https://api.clickup.com/api/v2/team/${CONFIG.teamId}/task?order_by=updated&page=${i}&assignees[]=${userId}`, { headers: { Authorization: CONFIG.apiKey } }).then((res) => res.json())));
|
|
142
|
+
const tasks = taskLists.flatMap((tl) => tl.tasks);
|
|
143
|
+
// filter out closed tasks
|
|
144
|
+
const openTasks = tasks
|
|
145
|
+
.filter((task) => task.status.type !== "done")
|
|
146
|
+
.slice(0, 100);
|
|
147
|
+
if (openTasks.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{ type: "text", text: "No open tasks found for the current user." },
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
content: openTasks.map((task) => ({
|
|
156
|
+
type: "text",
|
|
157
|
+
text: [
|
|
158
|
+
`task_id: ${task.id}`,
|
|
159
|
+
`name: ${task.name}`,
|
|
160
|
+
`status: ${task.status.status}`,
|
|
161
|
+
`date_created: ${new Date(+task.date_created)}`,
|
|
162
|
+
`date_updated: ${new Date(+task.date_updated)}`,
|
|
163
|
+
`creator: ${task.creator.username}`,
|
|
164
|
+
`list: ${task.list.name} (${task.list.id})`,
|
|
165
|
+
].join("\n"),
|
|
166
|
+
})),
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
// Only connect to the transport if this file is being run directly (not imported)
|
|
170
|
+
if (require.main === module) {
|
|
171
|
+
// Start receiving messages on stdin and sending messages on stdout
|
|
172
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
173
|
+
exports.server.connect(transport);
|
|
174
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { CallToolResult } from "@modelcontextprotocol/sdk/types";
|
|
3
|
+
/**
|
|
4
|
+
* A simple utility to test MCP tools directly without using the StdioServerTransport
|
|
5
|
+
*/
|
|
6
|
+
export declare class ToolTester {
|
|
7
|
+
private server;
|
|
8
|
+
constructor(server: McpServer);
|
|
9
|
+
/**
|
|
10
|
+
* Test a tool by name with the provided parameters
|
|
11
|
+
* @param toolName The name of the tool to test
|
|
12
|
+
* @param params The parameters to pass to the tool
|
|
13
|
+
* @returns A promise that resolves to the tool's result
|
|
14
|
+
*/
|
|
15
|
+
testTool(toolName: string, params: Record<string, any>): Promise<CallToolResult>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a tool tester for the provided server
|
|
19
|
+
* @param server The MCP server instance
|
|
20
|
+
* @returns A ToolTester instance
|
|
21
|
+
*/
|
|
22
|
+
export declare function createToolTester(server: McpServer): ToolTester;
|
|
23
|
+
//# sourceMappingURL=test-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-utils.d.ts","sourceRoot":"","sources":["../src/test-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEjE;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAY;gBAEd,MAAM,EAAE,SAAS;IAI7B;;;;;OAKG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,cAAc,CAAC;CAwBvF;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAE9D"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ToolTester = void 0;
|
|
4
|
+
exports.createToolTester = createToolTester;
|
|
5
|
+
/**
|
|
6
|
+
* A simple utility to test MCP tools directly without using the StdioServerTransport
|
|
7
|
+
*/
|
|
8
|
+
class ToolTester {
|
|
9
|
+
constructor(server) {
|
|
10
|
+
this.server = server;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Test a tool by name with the provided parameters
|
|
14
|
+
* @param toolName The name of the tool to test
|
|
15
|
+
* @param params The parameters to pass to the tool
|
|
16
|
+
* @returns A promise that resolves to the tool's result
|
|
17
|
+
*/
|
|
18
|
+
async testTool(toolName, params) {
|
|
19
|
+
// @ts-ignore - Accessing private property for testing purposes
|
|
20
|
+
const tools = this.server._registeredTools;
|
|
21
|
+
if (!tools || !tools[toolName]) {
|
|
22
|
+
throw new Error(`Tool "${toolName}" not found`);
|
|
23
|
+
}
|
|
24
|
+
const tool = tools[toolName];
|
|
25
|
+
// Validate parameters using the tool's schema
|
|
26
|
+
try {
|
|
27
|
+
tool.inputSchema.parse(params);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
throw new Error(`Parameter validation error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
31
|
+
}
|
|
32
|
+
// Call the tool's callback directly with the provided parameters
|
|
33
|
+
return await tool.callback(params);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.ToolTester = ToolTester;
|
|
37
|
+
/**
|
|
38
|
+
* Create a tool tester for the provided server
|
|
39
|
+
* @param server The MCP server instance
|
|
40
|
+
* @returns A ToolTester instance
|
|
41
|
+
*/
|
|
42
|
+
function createToolTester(server) {
|
|
43
|
+
return new ToolTester(server);
|
|
44
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hauptsache.net/clickup-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A minimal implementation of a Model Context Protocol (MCP) server for ClickUp integration",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"dev": "tsc -w & nodemon dist/index.js",
|
|
15
|
+
"cli": "npx ts-node src/cli.ts",
|
|
16
|
+
"prettier": "prettier --write src/**/*.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"clickup",
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"llm",
|
|
24
|
+
"ai"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "ISC",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.9.0",
|
|
30
|
+
"zod": "^3.24.2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.14.1",
|
|
34
|
+
"nodemon": "^3.1.9",
|
|
35
|
+
"prettier": "^3.5.3",
|
|
36
|
+
"typescript": "^5.8.3"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=16.0.0"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/hauptsacheNet/clickup-mcp.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/hauptsacheNet/clickup-mcp/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/hauptsacheNet/clickup-mcp#readme"
|
|
49
|
+
}
|