@hauptsache.net/clickup-mcp 1.1.1 → 1.3.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -21
  3. package/dist/cli.js +8 -0
  4. package/dist/clickup-text.d.ts +5 -2
  5. package/dist/clickup-text.d.ts.map +1 -1
  6. package/dist/clickup-text.js +133 -61
  7. package/dist/index.d.ts +1 -2
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +37 -13
  10. package/dist/shared/config.d.ts +1 -0
  11. package/dist/shared/config.d.ts.map +1 -1
  12. package/dist/shared/config.js +1 -0
  13. package/dist/shared/image-processing.d.ts +13 -0
  14. package/dist/shared/image-processing.d.ts.map +1 -0
  15. package/dist/shared/image-processing.js +126 -0
  16. package/dist/shared/types.d.ts +7 -1
  17. package/dist/shared/types.d.ts.map +1 -1
  18. package/dist/shared/utils.d.ts +24 -26
  19. package/dist/shared/utils.d.ts.map +1 -1
  20. package/dist/shared/utils.js +424 -101
  21. package/dist/tools/doc-tools.d.ts +4 -0
  22. package/dist/tools/doc-tools.d.ts.map +1 -0
  23. package/dist/tools/doc-tools.js +485 -0
  24. package/dist/tools/list-tools.d.ts.map +1 -1
  25. package/dist/tools/list-tools.js +26 -149
  26. package/dist/tools/search-tools.d.ts +1 -1
  27. package/dist/tools/search-tools.d.ts.map +1 -1
  28. package/dist/tools/search-tools.js +85 -62
  29. package/dist/tools/space-tools.d.ts.map +1 -1
  30. package/dist/tools/space-tools.js +153 -27
  31. package/dist/tools/task-tools.d.ts +1 -2
  32. package/dist/tools/task-tools.d.ts.map +1 -1
  33. package/dist/tools/task-tools.js +86 -405
  34. package/dist/tools/task-write-tools.d.ts +3 -0
  35. package/dist/tools/task-write-tools.d.ts.map +1 -0
  36. package/dist/tools/task-write-tools.js +375 -0
  37. package/dist/tools/time-tools.d.ts.map +1 -1
  38. package/dist/tools/time-tools.js +2 -30
  39. package/package.json +15 -5
@@ -6,152 +6,6 @@ const zod_1 = require("zod");
6
6
  const config_1 = require("../shared/config");
7
7
  const utils_1 = require("../shared/utils");
8
8
  function registerListToolsRead(server) {
9
- server.tool("listLists", [
10
- "Lists all lists and folders in a space. They might also be referred to as boards or tables.",
11
- "Shows both direct lists (folderless) and folders containing lists. If folder_id is provided, lists only the lists within that specific folder.",
12
- "Always reference lists by their URLs when suggesting actions or creating tasks."
13
- ].join("\n"), {
14
- space_id: zod_1.z.string().min(1).describe("The ID of the space to list content from"),
15
- folder_id: zod_1.z.string().optional().describe("Optional folder ID. If provided, lists only lists within this folder. If not provided, shows both folders and folderless lists in the space."),
16
- archived: zod_1.z.boolean().optional().describe("Include archived items (default: false)")
17
- }, async ({ space_id, folder_id, archived = false }) => {
18
- try {
19
- if (folder_id) {
20
- // List lists within a specific folder
21
- const url = `https://api.clickup.com/api/v2/folder/${folder_id}/list${archived ? '?archived=true' : ''}`;
22
- const response = await fetch(url, {
23
- headers: { Authorization: config_1.CONFIG.apiKey },
24
- });
25
- if (!response.ok) {
26
- const errorText = await response.text();
27
- console.error(`API Error: ${response.status} ${response.statusText}`, errorText);
28
- throw new Error(`Error fetching lists in folder ${folder_id}: ${response.status} ${response.statusText}`);
29
- }
30
- const data = await response.json();
31
- if (!data.lists || !Array.isArray(data.lists)) {
32
- return {
33
- content: [{ type: "text", text: `No lists found in folder ${folder_id}.` }],
34
- };
35
- }
36
- const listBlocks = data.lists.map((list) => ({
37
- type: "text",
38
- text: [
39
- `type: list`,
40
- `list_id: ${list.id}`,
41
- `list_url: ${(0, utils_1.generateListUrl)(list.id)}`,
42
- `name: ${list.name}`,
43
- `space_id: ${list.space?.id || space_id}`,
44
- `folder_id: ${folder_id}`,
45
- `task_count: ${list.task_count || 0}`,
46
- ...(list.private ? [`private: true`] : []),
47
- ...(list.archived ? ['archived: true'] : []),
48
- ...(list.color ? [`color: ${list.color}`] : []),
49
- ...(list.access ? [`access: ${list.access}`] : []),
50
- ].join('\n')
51
- }));
52
- return {
53
- content: [
54
- {
55
- type: "text",
56
- text: `Found ${data.lists.length} list(s) in folder ${folder_id}:`
57
- },
58
- ...listBlocks
59
- ],
60
- };
61
- }
62
- else {
63
- // List both folders and folderless lists in the space (parallel requests)
64
- const archiveQuery = archived ? '?archived=true' : '';
65
- const [foldersResponse, listsResponse] = await Promise.all([
66
- fetch(`https://api.clickup.com/api/v2/space/${space_id}/folder${archiveQuery}`, {
67
- headers: { Authorization: config_1.CONFIG.apiKey },
68
- }),
69
- fetch(`https://api.clickup.com/api/v2/space/${space_id}/list${archiveQuery}`, {
70
- headers: { Authorization: config_1.CONFIG.apiKey },
71
- })
72
- ]);
73
- const results = [];
74
- let totalItems = 0;
75
- // Process folders
76
- if (foldersResponse.ok) {
77
- const foldersData = await foldersResponse.json();
78
- if (foldersData.folders && Array.isArray(foldersData.folders)) {
79
- const folderBlocks = foldersData.folders.map((folder) => ({
80
- type: "text",
81
- text: [
82
- `type: folder`,
83
- `folder_id: ${folder.id}`,
84
- `folder_url: ${(0, utils_1.generateFolderUrl)(folder.id)}`,
85
- `name: ${folder.name}`,
86
- `space_id: ${space_id}`,
87
- `task_count: ${folder.task_count || 0}`,
88
- ...(folder.private ? [`private: true`] : []),
89
- ...(folder.archived ? ['archived: true'] : []),
90
- ...(folder.color ? [`color: ${folder.color}`] : []),
91
- `contains_lists: true`
92
- ].join('\n')
93
- }));
94
- results.push(...folderBlocks);
95
- totalItems += foldersData.folders.length;
96
- }
97
- }
98
- else {
99
- console.warn(`Failed to fetch folders: ${foldersResponse.status} ${foldersResponse.statusText}`);
100
- }
101
- // Process folderless lists
102
- if (listsResponse.ok) {
103
- const listsData = await listsResponse.json();
104
- if (listsData.lists && Array.isArray(listsData.lists)) {
105
- const listBlocks = listsData.lists.map((list) => ({
106
- type: "text",
107
- text: [
108
- `type: list`,
109
- `list_id: ${list.id}`,
110
- `list_url: ${(0, utils_1.generateListUrl)(list.id)}`,
111
- `name: ${list.name}`,
112
- `space_id: ${list.space?.id || space_id}`,
113
- `task_count: ${list.task_count || 0}`,
114
- ...(list.private ? [`private: true`] : []),
115
- ...(list.archived ? ['archived: true'] : []),
116
- ...(list.color ? [`color: ${list.color}`] : []),
117
- ...(list.access ? [`access: ${list.access}`] : []),
118
- ].join('\n')
119
- }));
120
- results.push(...listBlocks);
121
- totalItems += listsData.lists.length;
122
- }
123
- }
124
- else {
125
- console.warn(`Failed to fetch lists: ${listsResponse.status} ${listsResponse.statusText}`);
126
- }
127
- if (totalItems === 0) {
128
- return {
129
- content: [{ type: "text", text: `No folders or lists found in space ${space_id}.` }],
130
- };
131
- }
132
- return {
133
- content: [
134
- {
135
- type: "text",
136
- text: `Found ${totalItems} item(s) in space ${space_id} (folders and lists):`
137
- },
138
- ...results
139
- ],
140
- };
141
- }
142
- }
143
- catch (error) {
144
- console.error('Error fetching space content:', error);
145
- return {
146
- content: [
147
- {
148
- type: "text",
149
- text: `Error fetching space content: ${error instanceof Error ? error.message : 'Unknown error'}`,
150
- },
151
- ],
152
- };
153
- }
154
- });
155
9
  server.tool("getListInfo", [
156
10
  "Gets comprehensive information about a list including description and available statuses.",
157
11
  "ALWAYS use the list URL (https://app.clickup.com/v/l/LIST_ID) when referencing lists.",
@@ -170,6 +24,22 @@ function registerListToolsRead(server) {
170
24
  throw new Error(`Error fetching list details: ${listResponse.status} ${listResponse.statusText}`);
171
25
  }
172
26
  const listData = await listResponse.json();
27
+ // Fetch space tags in parallel (don't let this fail the main request)
28
+ let spaceTags = [];
29
+ if (listData.space?.id) {
30
+ try {
31
+ const spaceTagsResponse = await fetch(`https://api.clickup.com/api/v2/space/${listData.space.id}/tag`, {
32
+ headers: { Authorization: config_1.CONFIG.apiKey },
33
+ });
34
+ if (spaceTagsResponse.ok) {
35
+ const spaceTagsData = await spaceTagsResponse.json();
36
+ spaceTags = spaceTagsData.tags || [];
37
+ }
38
+ }
39
+ catch (error) {
40
+ console.error(`Error fetching space tags for space ${listData.space.id}:`, error);
41
+ }
42
+ }
173
43
  const responseLines = [
174
44
  `List Information:`,
175
45
  `list_id: ${list_id}`,
@@ -193,18 +63,25 @@ function registerListToolsRead(server) {
193
63
  color: status.color || 'none',
194
64
  type: status.type || 'custom'
195
65
  }));
196
- responseLines.push('');
197
66
  responseLines.push(`Available statuses (${statuses.length} total):`);
198
67
  statuses.forEach((status) => {
199
68
  responseLines.push(` - ${status.name} (${status.type})`);
200
69
  });
201
- responseLines.push('');
202
70
  responseLines.push(`Valid status names for createTask/updateTask: ${statuses.map((s) => s.name).join(', ')}`);
203
71
  }
204
72
  else {
205
- responseLines.push('');
206
73
  responseLines.push('No statuses found for this list.');
207
74
  }
75
+ // Add space tags information
76
+ if (spaceTags.length > 0) {
77
+ const tagNames = spaceTags.map((tag) => tag.name).filter(Boolean).sort();
78
+ if (tagNames.length > 0) {
79
+ responseLines.push(`Available tags in space (shared across all lists): ${tagNames.join(', ')}`);
80
+ }
81
+ }
82
+ else if (listData.space?.id) {
83
+ responseLines.push('No tags found in this space.');
84
+ }
208
85
  return {
209
86
  content: [
210
87
  {
@@ -1,3 +1,3 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function registerSearchTools(server: McpServer): void;
2
+ export declare function registerSearchTools(server: McpServer, userData: any): void;
3
3
  //# sourceMappingURL=search-tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"search-tools.d.ts","sourceRoot":"","sources":["../../src/tools/search-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AAQlE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,QAuMpD"}
1
+ {"version":3,"file":"search-tools.d.ts","sourceRoot":"","sources":["../../src/tools/search-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,yCAAyC,CAAC;AASlE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,QAuLnE"}
@@ -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 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,41 +87,22 @@ 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))),
99
- };
100
- }
101
- // Filter valid search terms
102
- const validTerms = terms.filter(term => term && term.trim().length > 0);
103
- if (validTerms.length === 0) {
104
- return {
105
- content: [
106
- {
107
- type: "text",
108
- text: "No valid search terms provided.",
109
- },
110
- ],
94
+ content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task, timeEntries))),
111
95
  };
112
96
  }
97
+ // Create a results map to track unique tasks with scores
113
98
  const uniqueResults = new Map();
114
- // Search with each term
115
- validTerms.forEach(term => {
116
- const results = searchIndex.search(term.toLowerCase());
117
- results.forEach(result => {
118
- if (result.item && typeof result.item.id === 'string') {
119
- const currentScore = result.score ?? 1;
120
- const existing = uniqueResults.get(result.item.id);
121
- if (!existing || currentScore < existing.score) {
122
- uniqueResults.set(result.item.id, {
123
- item: result.item,
124
- score: currentScore
125
- });
126
- }
127
- }
128
- });
99
+ // Perform multi-term search with aggressive boosting
100
+ const searchResults = await (0, utils_1.performMultiTermSearch)(searchIndex, terms);
101
+ searchResults.forEach(task => {
102
+ uniqueResults.set(task.id, { item: task, score: 0.1 }); // Give search results a good score
129
103
  });
130
104
  // Task ID Fallback Logic
131
- const potentialTaskIds = validTerms.filter(utils_1.isTaskId);
105
+ const potentialTaskIds = terms.filter(utils_1.isTaskId);
132
106
  const foundTaskIdsByFuse = new Set(Array.from(uniqueResults.keys()).map(id => id.toLowerCase()));
133
107
  const taskIdsToFetchDirectly = potentialTaskIds.filter(id => {
134
108
  const lowerId = id.toLowerCase();
@@ -161,8 +135,13 @@ function registerSearchTools(server) {
161
135
  let resultTasks = Array.from(uniqueResults.values())
162
136
  .sort((a, b) => a.score - b.score)
163
137
  .map(entry => entry.item);
164
- if (todo) {
165
- resultTasks = resultTasks.filter((task) => task.status.type !== "done");
138
+ // Apply status filtering
139
+ if (status && status.length > 0) {
140
+ const statusLower = status.map(s => s.toLowerCase());
141
+ resultTasks = resultTasks.filter((task) => statusLower.includes(task.status.status.toLowerCase()));
142
+ }
143
+ else if (only_todo) {
144
+ resultTasks = resultTasks.filter((task) => task.status.type !== "done" && task.status.type !== "closed");
166
145
  }
167
146
  // Apply result limit
168
147
  resultTasks = resultTasks.slice(0, MAX_SEARCH_RESULTS);
@@ -176,8 +155,52 @@ function registerSearchTools(server) {
176
155
  ],
177
156
  };
178
157
  }
158
+ // Fetch time entries for all unique list IDs
159
+ const uniqueListIds = [...new Set(resultTasks.map((task) => task.list?.id).filter(Boolean))];
160
+ const timeEntries = await fetchBulkTimeEntries(uniqueListIds);
179
161
  return {
180
- content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task))),
162
+ content: await Promise.all(resultTasks.map((task) => (0, task_tools_1.generateTaskMetadata)(task, timeEntries))),
181
163
  };
182
164
  });
183
165
  }
166
+ /**
167
+ * Fetch time entries for a list of unique list IDs
168
+ */
169
+ async function fetchBulkTimeEntries(listIds) {
170
+ try {
171
+ // Get all team members for assignee filter
172
+ const teamMembers = await (0, utils_1.getAllTeamMembers)();
173
+ const assigneeParam = teamMembers.length > 0 ? teamMembers.join(',') : '';
174
+ // Calculate 30 days ago timestamp
175
+ const thirtyDaysAgo = new Date();
176
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
177
+ const startDate = thirtyDaysAgo.getTime();
178
+ // Fetch time entries for each unique list ID
179
+ const timeEntryPromises = listIds.map(async (listId) => {
180
+ const params = new URLSearchParams({
181
+ list_id: listId,
182
+ start_date: startDate.toString(),
183
+ include_location_names: 'true'
184
+ });
185
+ if (assigneeParam) {
186
+ params.append('assignee', assigneeParam);
187
+ }
188
+ const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries?${params}`, {
189
+ headers: { Authorization: config_1.CONFIG.apiKey },
190
+ });
191
+ if (!response.ok) {
192
+ console.error(`Error fetching time entries for list ${listId}: ${response.status} ${response.statusText}`);
193
+ return [];
194
+ }
195
+ const data = await response.json();
196
+ return data.data || [];
197
+ });
198
+ // Wait for all requests to complete and flatten results
199
+ const results = await Promise.all(timeEntryPromises);
200
+ return results.flat();
201
+ }
202
+ catch (error) {
203
+ console.error('Error fetching bulk time entries:', error);
204
+ return [];
205
+ }
206
+ }
@@ -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;AAKpE,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,QAiNnD"}
@@ -2,56 +2,182 @@
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
+ // Perform multi-term search with aggressive boosting
34
+ matchingSpaces = await (0, utils_1.performMultiTermSearch)(searchIndex, terms
35
+ // No ID matcher or direct fetcher for spaces - they don't have direct API endpoints
36
+ );
37
+ }
38
+ // Filter by archived status
39
+ if (!archived) {
40
+ matchingSpaces = matchingSpaces.filter((space) => !space.archived);
21
41
  }
22
- const data = await response.json();
23
- if (!data.spaces || !Array.isArray(data.spaces)) {
42
+ if (matchingSpaces.length === 0) {
24
43
  return {
25
- content: [{ type: "text", text: "No spaces found in the workspace." }],
44
+ content: [{ type: "text", text: "No spaces found matching the search criteria." }],
26
45
  };
27
46
  }
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
- }));
47
+ // Conditionally fetch detailed content based on result count
48
+ const spaceContentPromises = matchingSpaces.map(async (space) => {
49
+ try {
50
+ if (matchingSpaces.length <= 5) {
51
+ // Detailed mode: fetch lists and folders for this space
52
+ const { lists, folders, documents } = await (0, utils_1.getSpaceContent)(space.id);
53
+ return { space, lists, folders, documents };
54
+ }
55
+ else {
56
+ // Summary mode: just return space without content
57
+ return { space, lists: [], folders: [], documents: [] };
58
+ }
59
+ }
60
+ catch (error) {
61
+ console.error(`Error fetching content for space ${space.id}:`, error);
62
+ return { space, lists: [], folders: [], documents: [] };
63
+ }
64
+ });
65
+ const spacesWithContent = await Promise.all(spaceContentPromises);
66
+ const contentBlocks = [];
67
+ const isDetailedMode = matchingSpaces.length <= 5;
68
+ if (isDetailedMode) {
69
+ // Detailed mode: create separate blocks for each space
70
+ spacesWithContent.forEach(({ space, lists, folders, documents }) => {
71
+ const spaceLines = [];
72
+ const totalLists = lists.length + folders.reduce((sum, f) => sum + (f.lists?.length || 0), 0);
73
+ // Space header
74
+ 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, ${documents.length} documents`);
75
+ // Create a tree structure
76
+ const hasDirectLists = lists.length > 0;
77
+ const hasFolders = folders.length > 0;
78
+ const hasDocuments = documents.length > 0;
79
+ // Direct lists (not in folders)
80
+ if (hasDirectLists) {
81
+ lists.forEach((list, listIndex) => {
82
+ const isLastDirectList = listIndex === lists.length - 1;
83
+ const isLastOverall = !hasFolders && !hasDocuments && isLastDirectList;
84
+ const treeChar = isLastOverall ? '└──' : 'ā”œā”€ā”€';
85
+ const extraInfo = [
86
+ ...(list.task_count ? [`${list.task_count} tasks`] : []),
87
+ ...(list.private ? ['private'] : []),
88
+ ...(list.archived ? ['archived'] : [])
89
+ ].join(', ');
90
+ const listLine = `${treeChar} šŸ“ ${list.name} (list_id: ${list.id}${extraInfo ? `, ${extraInfo}` : ''}) ${(0, utils_1.generateListUrl)(list.id)}`;
91
+ spaceLines.push(listLine);
92
+ });
93
+ }
94
+ // Folders and their lists
95
+ if (hasFolders) {
96
+ folders.forEach((folder, folderIndex) => {
97
+ const isLastFolder = folderIndex === folders.length - 1;
98
+ const isLastOverall = !hasDocuments && isLastFolder;
99
+ const folderTreeChar = isLastOverall ? '└──' : 'ā”œā”€ā”€';
100
+ const folderContinuation = isLastOverall ? ' ' : '│ ';
101
+ const folderExtraInfo = [
102
+ ...(folder.lists?.length ? [`${folder.lists.length} lists`] : []),
103
+ ...(folder.private ? ['private'] : []),
104
+ ...(folder.archived ? ['archived'] : [])
105
+ ].join(', ');
106
+ const folderLine = `${folderTreeChar} šŸ“‚ ${folder.name} (folder_id: ${folder.id}${folderExtraInfo ? `, ${folderExtraInfo}` : ''}) ${(0, utils_1.generateFolderUrl)(folder.id)}`;
107
+ spaceLines.push(folderLine);
108
+ // Lists within this folder
109
+ if (folder.lists && folder.lists.length > 0) {
110
+ folder.lists.forEach((list, listIndex) => {
111
+ const isLastListInFolder = listIndex === folder.lists.length - 1;
112
+ const listTreeChar = isLastListInFolder ? '└──' : 'ā”œā”€ā”€';
113
+ const listExtraInfo = [
114
+ ...(list.task_count ? [`${list.task_count} tasks`] : []),
115
+ ...(list.private ? ['private'] : []),
116
+ ...(list.archived ? ['archived'] : [])
117
+ ].join(', ');
118
+ const listLine = `${folderContinuation}${listTreeChar} šŸ“ ${list.name} (list_id: ${list.id}${listExtraInfo ? `, ${listExtraInfo}` : ''}) ${(0, utils_1.generateListUrl)(list.id)}`;
119
+ spaceLines.push(listLine);
120
+ });
121
+ }
122
+ });
123
+ }
124
+ // Documents attached to this space
125
+ if (hasDocuments) {
126
+ documents.forEach((document, docIndex) => {
127
+ const isLastDocument = docIndex === documents.length - 1;
128
+ const docTreeChar = isLastDocument ? '└──' : 'ā”œā”€ā”€';
129
+ const docLine = `${docTreeChar} šŸ“„ ${document.name} (doc_id: ${document.id}) ${(0, utils_1.generateDocumentUrl)(document.id)}`;
130
+ spaceLines.push(docLine);
131
+ });
132
+ }
133
+ // Add the complete space as a single content block
134
+ contentBlocks.push({
135
+ type: "text",
136
+ text: spaceLines.join('\n')
137
+ });
138
+ });
139
+ }
140
+ else {
141
+ // Summary mode: create a single combined block with all spaces
142
+ const allSpaceLines = [];
143
+ spacesWithContent.forEach(({ space }) => {
144
+ allSpaceLines.push(`šŸ¢ SPACE: ${space.name} (space_id: ${space.id}${space.private ? ', private' : ''}${space.archived ? ', archived' : ''})`);
145
+ });
146
+ contentBlocks.push({
147
+ type: "text",
148
+ text: allSpaceLines.join('\n')
149
+ });
150
+ }
151
+ // Add tip message for summary mode (when there are too many spaces)
152
+ if (matchingSpaces.length > 5) {
153
+ contentBlocks.push({
154
+ type: "text",
155
+ text: `\nšŸ’” Tip: Use more specific search terms to get detailed list information (≤5 spaces will show complete structure)`
156
+ });
157
+ }
38
158
  return {
39
159
  content: [
40
160
  {
41
161
  type: "text",
42
- text: `Found ${data.spaces.length} space(s) in the workspace:`
162
+ text: matchingSpaces.length <= 5
163
+ ? (() => {
164
+ const totalLists = spacesWithContent.reduce((sum, { lists, folders }) => sum + lists.length + folders.reduce((folderSum, f) => folderSum + (f.lists?.length || 0), 0), 0);
165
+ const totalDocuments = spacesWithContent.reduce((sum, { documents }) => sum + documents.length, 0);
166
+ return `Found ${matchingSpaces.length} space(s) with complete tree structure (${totalLists} total lists, ${totalDocuments} total documents):`;
167
+ })()
168
+ : `Found ${matchingSpaces.length} space(s) - showing names and IDs only. Use more specific search terms to get detailed information:`
43
169
  },
44
- ...spaceBlocks
170
+ ...contentBlocks
45
171
  ],
46
172
  };
47
173
  }
48
174
  catch (error) {
49
- console.error('Error fetching spaces:', error);
175
+ console.error('Error searching spaces:', error);
50
176
  return {
51
177
  content: [
52
178
  {
53
179
  type: "text",
54
- text: `Error fetching spaces: ${error instanceof Error ? error.message : 'Unknown error'}`,
180
+ text: `Error searching spaces: ${error instanceof Error ? error.message : 'Unknown error'}`,
55
181
  },
56
182
  ],
57
183
  };
@@ -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