@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 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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -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: ![alt text](url)
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
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const server: McpServer;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }