@hauptsache.net/clickup-mcp 1.1.1 → 1.2.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.
@@ -6,19 +6,19 @@ const config_1 = require("../shared/config");
6
6
  const utils_1 = require("../shared/utils");
7
7
  const task_tools_1 = require("./task-tools");
8
8
  const MAX_SEARCH_RESULTS = 50;
9
- function registerSearchTools(server) {
9
+ function registerSearchTools(server, userData) {
10
10
  // Dynamically construct the searchTasks description
11
11
  const searchTasksDescriptionBase = [
12
- "Searches tasks by name, content, assignees, and ID (case insensitive) with fuzzy matching and support for multiple search terms (OR logic).",
12
+ "Searches tasks (sometimes called Tickets or Cards) by name, content, assignees, and ID (case insensitive) with fuzzy matching and support for multiple search terms (OR logic).",
13
13
  "Can filter by multiple list_ids, space_ids, todo status, or tasks assigned to the current user. If no search terms provided, returns most recently updated tasks.",
14
- // Placeholder for language-specific guidance
15
- "Always reference tasks by their URLs when discussing search results or suggesting actions.",
16
- "You'll get a rough overview of the tasks that match the search terms, sorted by relevance.",
17
- "Always use getTaskById to get more specific information if a task is relevant, and always share the task URL.",
14
+ "Can also be used to find tasks for the current user by providing the assigned_to_me flag."
18
15
  ];
19
16
  if (config_1.CONFIG.primaryLanguageHint && config_1.CONFIG.primaryLanguageHint.toLowerCase() !== 'en') {
20
- searchTasksDescriptionBase.splice(2, 0, `For optimal results, as your ClickUp tasks may be primarily in '${config_1.CONFIG.primaryLanguageHint}', consider providing search terms in English and '${config_1.CONFIG.primaryLanguageHint}'.`);
17
+ searchTasksDescriptionBase.push(`For optimal results, as your ClickUp tasks may be primarily in '${config_1.CONFIG.primaryLanguageHint}', consider providing search terms in English and '${config_1.CONFIG.primaryLanguageHint}'.`);
21
18
  }
19
+ searchTasksDescriptionBase.push("Always reference tasks by their URLs when discussing search results or suggesting actions.");
20
+ searchTasksDescriptionBase.push("You'll get a rough overview of the tasks that match the search terms, sorted by relevance.");
21
+ searchTasksDescriptionBase.push("Always use getTaskById to get more specific information if a task is relevant, and always share the task URL.");
22
22
  server.tool("searchTasks", searchTasksDescriptionBase.join("\n"), {
23
23
  terms: zod_1.z
24
24
  .array(zod_1.z.string())
@@ -32,33 +32,21 @@ function registerSearchTools(server) {
32
32
  .array(zod_1.z.string())
33
33
  .optional()
34
34
  .describe("Filter tasks to specific space IDs"),
35
- todo: zod_1.z
35
+ only_todo: zod_1.z
36
36
  .boolean()
37
37
  .optional()
38
- .describe("Filter for open/todo tasks only (exclude done tasks)"),
38
+ .describe("Filter for open/todo tasks only (exclude done and closed tasks)"),
39
+ status: zod_1.z
40
+ .array(zod_1.z.string())
41
+ .optional()
42
+ .describe("Filter for tasks with specific status names (overrides only_todo if provided)"),
39
43
  assigned_to_me: zod_1.z
40
44
  .boolean()
41
45
  .optional()
42
- .describe("Filter for tasks assigned to the current user"),
43
- }, async ({ terms, list_ids, space_ids, todo, assigned_to_me }) => {
46
+ .describe(`Filter for tasks assigned to the current user (${userData.user.username} (${userData.user.id}))`),
47
+ }, async ({ terms, list_ids, space_ids, only_todo, status, assigned_to_me }) => {
44
48
  // Get current user ID if filtering by assigned_to_me
45
- let assignees;
46
- if (assigned_to_me) {
47
- try {
48
- const userData = await (0, utils_1.getCurrentUser)();
49
- assignees = [userData.user.id];
50
- }
51
- catch (error) {
52
- return {
53
- content: [
54
- {
55
- type: "text",
56
- text: "Error: Failed to get current user information.",
57
- },
58
- ],
59
- };
60
- }
61
- }
49
+ const assignees = assigned_to_me ? [userData.user.id] : [];
62
50
  const searchIndex = await (0, utils_1.getTaskSearchIndex)(space_ids, list_ids, assignees);
63
51
  if (!searchIndex) {
64
52
  return {
@@ -73,8 +61,13 @@ function registerSearchTools(server) {
73
61
  // Early return for no search terms
74
62
  if (!terms || terms.length === 0) {
75
63
  let allTasks = searchIndex._docs || [];
76
- if (todo) {
77
- allTasks = allTasks.filter((task) => task.status.type !== "done");
64
+ // Apply status filtering
65
+ if (status && status.length > 0) {
66
+ const statusLower = status.map(s => s.toLowerCase());
67
+ allTasks = allTasks.filter((task) => statusLower.includes(task.status.status.toLowerCase()));
68
+ }
69
+ else if (only_todo) {
70
+ allTasks = allTasks.filter((task) => task.status.type !== "done" && task.status.type !== "closed");
78
71
  }
79
72
  // Sort by updated date (most recent first) and limit
80
73
  const resultTasks = allTasks
@@ -94,8 +87,11 @@ function registerSearchTools(server) {
94
87
  ],
95
88
  };
96
89
  }
90
+ // Fetch time entries for all unique list IDs
91
+ const uniqueListIds = [...new Set(resultTasks.map((task) => task.list?.id).filter(Boolean))];
92
+ const timeEntries = await fetchBulkTimeEntries(uniqueListIds);
97
93
  return {
98
- content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task))),
94
+ content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task, timeEntries))),
99
95
  };
100
96
  }
101
97
  // Filter valid search terms
@@ -161,8 +157,13 @@ function registerSearchTools(server) {
161
157
  let resultTasks = Array.from(uniqueResults.values())
162
158
  .sort((a, b) => a.score - b.score)
163
159
  .map(entry => entry.item);
164
- if (todo) {
165
- resultTasks = resultTasks.filter((task) => task.status.type !== "done");
160
+ // Apply status filtering
161
+ if (status && status.length > 0) {
162
+ const statusLower = status.map(s => s.toLowerCase());
163
+ resultTasks = resultTasks.filter((task) => statusLower.includes(task.status.status.toLowerCase()));
164
+ }
165
+ else if (only_todo) {
166
+ resultTasks = resultTasks.filter((task) => task.status.type !== "done" && task.status.type !== "closed");
166
167
  }
167
168
  // Apply result limit
168
169
  resultTasks = resultTasks.slice(0, MAX_SEARCH_RESULTS);
@@ -176,8 +177,52 @@ function registerSearchTools(server) {
176
177
  ],
177
178
  };
178
179
  }
180
+ // Fetch time entries for all unique list IDs
181
+ const uniqueListIds = [...new Set(resultTasks.map((task) => task.list?.id).filter(Boolean))];
182
+ const timeEntries = await fetchBulkTimeEntries(uniqueListIds);
179
183
  return {
180
- content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task))),
184
+ content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task, timeEntries))),
181
185
  };
182
186
  });
183
187
  }
188
+ /**
189
+ * Fetch time entries for a list of unique list IDs
190
+ */
191
+ async function fetchBulkTimeEntries(listIds) {
192
+ try {
193
+ // Get all team members for assignee filter
194
+ const teamMembers = await (0, utils_1.getAllTeamMembers)();
195
+ const assigneeParam = teamMembers.length > 0 ? teamMembers.join(',') : '';
196
+ // Calculate 30 days ago timestamp
197
+ const thirtyDaysAgo = new Date();
198
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
199
+ const startDate = thirtyDaysAgo.getTime();
200
+ // Fetch time entries for each unique list ID
201
+ const timeEntryPromises = listIds.map(async (listId) => {
202
+ const params = new URLSearchParams({
203
+ list_id: listId,
204
+ start_date: startDate.toString(),
205
+ include_location_names: 'true'
206
+ });
207
+ if (assigneeParam) {
208
+ params.append('assignee', assigneeParam);
209
+ }
210
+ const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries?${params}`, {
211
+ headers: { Authorization: config_1.CONFIG.apiKey },
212
+ });
213
+ if (!response.ok) {
214
+ console.error(`Error fetching time entries for list ${listId}: ${response.status} ${response.statusText}`);
215
+ return [];
216
+ }
217
+ const data = await response.json();
218
+ return data.data || [];
219
+ });
220
+ // Wait for all requests to complete and flatten results
221
+ const results = await Promise.all(timeEntryPromises);
222
+ return results.flat();
223
+ }
224
+ catch (error) {
225
+ console.error('Error fetching bulk time entries:', error);
226
+ return [];
227
+ }
228
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"space-tools.d.ts","sourceRoot":"","sources":["../../src/tools/space-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAMpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,QA+DnD"}
1
+ {"version":3,"file":"space-tools.d.ts","sourceRoot":"","sources":["../../src/tools/space-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAMpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,QAqNnD"}
@@ -2,56 +2,186 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.registerSpaceTools = registerSpaceTools;
4
4
  const zod_1 = require("zod");
5
- const config_1 = require("../shared/config");
6
5
  const utils_1 = require("../shared/utils");
7
6
  function registerSpaceTools(server) {
8
- server.tool("listSpaces", [
9
- "Lists all spaces in the workspace. These might also be referred to as customers or projects.",
7
+ server.tool("searchSpaces", [
8
+ "Searches spaces (sometimes called projects) by name or ID with fuzzy matching.",
9
+ "If 5 or fewer spaces match, automatically fetches all lists (sometimes called boards) and folders within those spaces to provide a complete tree structure.",
10
+ "If more than 5 spaces match, returns only space information with guidance to search more precisely.",
11
+ "You can search by space name (fuzzy matching) or provide an exact space ID.",
10
12
  "Always reference spaces by their URLs when discussing projects or suggesting actions."
11
13
  ].join("\n"), {
14
+ terms: zod_1.z
15
+ .array(zod_1.z.string())
16
+ .optional()
17
+ .describe("Array of search terms to match against space names or IDs. If not provided, returns all spaces."),
12
18
  archived: zod_1.z.boolean().optional().describe("Include archived spaces (default: false)")
13
- }, async ({ archived = false }) => {
19
+ }, async ({ terms, archived = false }) => {
14
20
  try {
15
- const url = `https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/space${archived ? '?archived=true' : ''}`;
16
- const response = await fetch(url, {
17
- headers: { Authorization: config_1.CONFIG.apiKey },
18
- });
19
- if (!response.ok) {
20
- throw new Error(`Error fetching spaces: ${response.status} ${response.statusText}`);
21
+ const searchIndex = await (0, utils_1.getSpaceSearchIndex)();
22
+ if (!searchIndex) {
23
+ return {
24
+ content: [{ type: "text", text: "Error: Could not build space search index." }],
25
+ };
26
+ }
27
+ let matchingSpaces = [];
28
+ if (!terms || terms.length === 0) {
29
+ // Return all spaces if no search terms
30
+ matchingSpaces = searchIndex._docs || [];
31
+ }
32
+ else {
33
+ // Search with fuzzy matching
34
+ const uniqueResults = new Map();
35
+ terms.forEach(term => {
36
+ const trimmedTerm = term.trim();
37
+ if (trimmedTerm.length === 0)
38
+ return;
39
+ // Check if it's an exact space ID first
40
+ const exactMatch = searchIndex._docs.find((space) => space.id === trimmedTerm);
41
+ if (exactMatch) {
42
+ uniqueResults.set(exactMatch.id, { item: exactMatch, score: 0 });
43
+ return;
44
+ }
45
+ // Fuzzy search
46
+ const results = searchIndex.search(trimmedTerm);
47
+ results.forEach(result => {
48
+ if (result.item && typeof result.item.id === 'string') {
49
+ const currentScore = result.score ?? 1;
50
+ const existing = uniqueResults.get(result.item.id);
51
+ if (!existing || currentScore < existing.score) {
52
+ uniqueResults.set(result.item.id, {
53
+ item: result.item,
54
+ score: currentScore
55
+ });
56
+ }
57
+ }
58
+ });
59
+ });
60
+ matchingSpaces = Array.from(uniqueResults.values())
61
+ .sort((a, b) => a.score - b.score)
62
+ .map(entry => entry.item);
21
63
  }
22
- const data = await response.json();
23
- if (!data.spaces || !Array.isArray(data.spaces)) {
64
+ // Filter by archived status
65
+ if (!archived) {
66
+ matchingSpaces = matchingSpaces.filter((space) => !space.archived);
67
+ }
68
+ if (matchingSpaces.length === 0) {
24
69
  return {
25
- content: [{ type: "text", text: "No spaces found in the workspace." }],
70
+ content: [{ type: "text", text: "No spaces found matching the search criteria." }],
26
71
  };
27
72
  }
28
- const spaceBlocks = data.spaces.map((space) => ({
29
- type: "text",
30
- text: [
31
- `space_id: ${space.id}`,
32
- `space_url: ${(0, utils_1.generateSpaceUrl)(space.id)}`,
33
- `name: ${space.name}`,
34
- ...(space.private ? [`private: true`] : []),
35
- ...(space.archived ? ['archived: true'] : []),
36
- ].join('\n')
37
- }));
73
+ // Conditionally fetch detailed content based on result count
74
+ const spaceContentPromises = matchingSpaces.map(async (space) => {
75
+ try {
76
+ if (matchingSpaces.length <= 5) {
77
+ // Detailed mode: fetch lists and folders for this space
78
+ const { lists, folders } = await (0, utils_1.getSpaceContent)(space.id);
79
+ return { space, lists, folders };
80
+ }
81
+ else {
82
+ // Summary mode: just return space without content
83
+ return { space, lists: [], folders: [] };
84
+ }
85
+ }
86
+ catch (error) {
87
+ console.error(`Error fetching content for space ${space.id}:`, error);
88
+ return { space, lists: [], folders: [] };
89
+ }
90
+ });
91
+ const spacesWithContent = await Promise.all(spaceContentPromises);
92
+ const contentBlocks = [];
93
+ spacesWithContent.forEach(({ space, lists, folders }, index) => {
94
+ const spaceLines = [];
95
+ const totalLists = lists.length + folders.reduce((sum, f) => sum + (f.lists?.length || 0), 0);
96
+ // Space header
97
+ spaceLines.push(`šŸ¢ SPACE: ${space.name} (space_id: ${space.id}${space.private ? ', private' : ''}${space.archived ? ', archived' : ''}) ${(0, utils_1.generateSpaceUrl)(space.id)}`, ` ${totalLists} lists, ${folders.length} folders`);
98
+ // Create a tree structure
99
+ const hasDirectLists = lists.length > 0;
100
+ const hasFolders = folders.length > 0;
101
+ // Direct lists (not in folders)
102
+ if (hasDirectLists) {
103
+ lists.forEach((list, listIndex) => {
104
+ const isLastDirectList = listIndex === lists.length - 1;
105
+ const isLastOverall = !hasFolders && isLastDirectList;
106
+ const treeChar = isLastOverall ? '└──' : 'ā”œā”€ā”€';
107
+ const extraInfo = [
108
+ ...(list.task_count ? [`${list.task_count} tasks`] : []),
109
+ ...(list.private ? ['private'] : []),
110
+ ...(list.archived ? ['archived'] : [])
111
+ ].join(', ');
112
+ const listLine = `${treeChar} šŸ“ ${list.name} (list_id: ${list.id}${extraInfo ? `, ${extraInfo}` : ''}) ${(0, utils_1.generateListUrl)(list.id)}`;
113
+ spaceLines.push(listLine);
114
+ });
115
+ }
116
+ // Folders and their lists
117
+ if (hasFolders) {
118
+ folders.forEach((folder, folderIndex) => {
119
+ const isLastFolder = folderIndex === folders.length - 1;
120
+ const folderTreeChar = isLastFolder ? '└──' : 'ā”œā”€ā”€';
121
+ const folderContinuation = isLastFolder ? ' ' : '│ ';
122
+ const folderExtraInfo = [
123
+ ...(folder.lists?.length ? [`${folder.lists.length} lists`] : []),
124
+ ...(folder.private ? ['private'] : []),
125
+ ...(folder.archived ? ['archived'] : [])
126
+ ].join(', ');
127
+ const folderLine = `${folderTreeChar} šŸ“‚ ${folder.name} (folder_id: ${folder.id}${folderExtraInfo ? `, ${folderExtraInfo}` : ''}) ${(0, utils_1.generateFolderUrl)(folder.id)}`;
128
+ spaceLines.push(folderLine);
129
+ // Lists within this folder
130
+ if (folder.lists && folder.lists.length > 0) {
131
+ folder.lists.forEach((list, listIndex) => {
132
+ const isLastListInFolder = listIndex === folder.lists.length - 1;
133
+ const listTreeChar = isLastListInFolder ? '└──' : 'ā”œā”€ā”€';
134
+ const listExtraInfo = [
135
+ ...(list.task_count ? [`${list.task_count} tasks`] : []),
136
+ ...(list.private ? ['private'] : []),
137
+ ...(list.archived ? ['archived'] : [])
138
+ ].join(', ');
139
+ const listLine = `${folderContinuation}${listTreeChar} šŸ“ ${list.name} (list_id: ${list.id}${listExtraInfo ? `, ${listExtraInfo}` : ''}) ${(0, utils_1.generateListUrl)(list.id)}`;
140
+ spaceLines.push(listLine);
141
+ });
142
+ }
143
+ });
144
+ }
145
+ // Add the complete space as a single content block
146
+ contentBlocks.push({
147
+ type: "text",
148
+ text: spaceLines.join('\n')
149
+ });
150
+ // Add separator between spaces (except for the last one)
151
+ if (index < spacesWithContent.length - 1) {
152
+ contentBlocks.push({
153
+ type: "text",
154
+ text: '─'.repeat(50)
155
+ });
156
+ }
157
+ });
158
+ const totalLists = spacesWithContent.reduce((sum, { lists, folders }) => sum + lists.length + folders.reduce((folderSum, f) => folderSum + (f.lists?.length || 0), 0), 0);
159
+ // Add tip message for summary mode (when there are too many spaces)
160
+ if (matchingSpaces.length > 5) {
161
+ contentBlocks.push({
162
+ type: "text",
163
+ text: `\nšŸ’” Tip: Use more specific search terms to get detailed list information (≤5 spaces will show complete structure)`
164
+ });
165
+ }
38
166
  return {
39
167
  content: [
40
168
  {
41
169
  type: "text",
42
- text: `Found ${data.spaces.length} space(s) in the workspace:`
170
+ text: matchingSpaces.length <= 5
171
+ ? `Found ${matchingSpaces.length} space(s) with complete tree structure (${totalLists} total lists):`
172
+ : `Found ${matchingSpaces.length} space(s) - too many to show detailed list information. Please search more precisely to get complete tree structure with lists and folders:`
43
173
  },
44
- ...spaceBlocks
174
+ ...contentBlocks
45
175
  ],
46
176
  };
47
177
  }
48
178
  catch (error) {
49
- console.error('Error fetching spaces:', error);
179
+ console.error('Error searching spaces:', error);
50
180
  return {
51
181
  content: [
52
182
  {
53
183
  type: "text",
54
- text: `Error fetching spaces: ${error instanceof Error ? error.message : 'Unknown error'}`,
184
+ text: `Error searching spaces: ${error instanceof Error ? error.message : 'Unknown error'}`,
55
185
  },
56
186
  ],
57
187
  };
@@ -1,9 +1,8 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { ContentBlock } from "../shared/types";
3
3
  export declare function registerTaskToolsRead(server: McpServer, userData: any): void;
4
- export declare function registerTaskToolsWrite(server: McpServer, userData: any): void;
5
4
  /**
6
5
  * Helper function to generate consistent task metadata
7
6
  */
8
- export declare function generateTaskMetadata(task: any): Promise<ContentBlock>;
7
+ export declare function generateTaskMetadata(task: any, timeEntries?: any[], isDetailView?: boolean): Promise<ContentBlock>;
9
8
  //# sourceMappingURL=task-tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAqB,MAAM,iBAAiB,CAAC;AAwIlE,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAwDrE;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QA6StE;AAoLD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,YAAY,CAAC,CA4G3E"}
1
+ {"version":3,"file":"task-tools.d.ts","sourceRoot":"","sources":["../../src/tools/task-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAGpE,OAAO,EAAE,YAAY,EAAyC,MAAM,iBAAiB,CAAC;AAOtF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAwDrE;AA+MD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,GAAG,EAAE,WAAW,CAAC,EAAE,GAAG,EAAE,EAAE,YAAY,GAAE,OAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CA+G/H"}