@hauptsache.net/clickup-mcp 1.1.0 ā 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.
- package/README.md +6 -1
- package/dist/cli.js +1 -0
- package/dist/clickup-text.d.ts +5 -2
- package/dist/clickup-text.d.ts.map +1 -1
- package/dist/clickup-text.js +129 -61
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/shared/config.d.ts +1 -0
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +1 -0
- package/dist/shared/image-processing.d.ts +13 -0
- package/dist/shared/image-processing.d.ts.map +1 -0
- package/dist/shared/image-processing.js +126 -0
- package/dist/shared/types.d.ts +7 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/utils.d.ts +62 -11
- package/dist/shared/utils.d.ts.map +1 -1
- package/dist/shared/utils.js +281 -63
- package/dist/tools/list-tools.d.ts.map +1 -1
- package/dist/tools/list-tools.js +34 -143
- package/dist/tools/search-tools.d.ts +1 -1
- package/dist/tools/search-tools.d.ts.map +1 -1
- package/dist/tools/search-tools.js +79 -33
- package/dist/tools/space-tools.d.ts.map +1 -1
- package/dist/tools/space-tools.js +160 -31
- package/dist/tools/task-tools.d.ts +1 -2
- package/dist/tools/task-tools.d.ts.map +1 -1
- package/dist/tools/task-tools.js +92 -380
- package/dist/tools/task-write-tools.d.ts +3 -0
- package/dist/tools/task-write-tools.d.ts.map +1 -0
- package/dist/tools/task-write-tools.js +375 -0
- package/dist/tools/time-tools.d.ts.map +1 -1
- package/dist/tools/time-tools.js +191 -111
- package/package.json +14 -4
package/dist/tools/list-tools.js
CHANGED
|
@@ -4,150 +4,14 @@ exports.registerListToolsRead = registerListToolsRead;
|
|
|
4
4
|
exports.registerListToolsWrite = registerListToolsWrite;
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const config_1 = require("../shared/config");
|
|
7
|
+
const utils_1 = require("../shared/utils");
|
|
7
8
|
function registerListToolsRead(server) {
|
|
8
|
-
server.tool("listLists", "Lists all lists and folders in a space. They might also be referred to as boards or tables. Shows both direct lists (folderless) and folders containing lists. If folder_id is provided, lists only the lists within that specific folder.", {
|
|
9
|
-
space_id: zod_1.z.string().min(1).describe("The ID of the space to list content from"),
|
|
10
|
-
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."),
|
|
11
|
-
archived: zod_1.z.boolean().optional().describe("Include archived items (default: false)")
|
|
12
|
-
}, async ({ space_id, folder_id, archived = false }) => {
|
|
13
|
-
try {
|
|
14
|
-
if (folder_id) {
|
|
15
|
-
// List lists within a specific folder
|
|
16
|
-
const url = `https://api.clickup.com/api/v2/folder/${folder_id}/list${archived ? '?archived=true' : ''}`;
|
|
17
|
-
const response = await fetch(url, {
|
|
18
|
-
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
19
|
-
});
|
|
20
|
-
if (!response.ok) {
|
|
21
|
-
const errorText = await response.text();
|
|
22
|
-
console.error(`API Error: ${response.status} ${response.statusText}`, errorText);
|
|
23
|
-
throw new Error(`Error fetching lists in folder ${folder_id}: ${response.status} ${response.statusText}`);
|
|
24
|
-
}
|
|
25
|
-
const data = await response.json();
|
|
26
|
-
if (!data.lists || !Array.isArray(data.lists)) {
|
|
27
|
-
return {
|
|
28
|
-
content: [{ type: "text", text: `No lists found in folder ${folder_id}.` }],
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
const listBlocks = data.lists.map((list) => ({
|
|
32
|
-
type: "text",
|
|
33
|
-
text: [
|
|
34
|
-
`type: list`,
|
|
35
|
-
`list_id: ${list.id}`,
|
|
36
|
-
`name: ${list.name}`,
|
|
37
|
-
`space_id: ${list.space?.id || space_id}`,
|
|
38
|
-
`folder_id: ${folder_id}`,
|
|
39
|
-
`task_count: ${list.task_count || 0}`,
|
|
40
|
-
`private: ${list.private || false}`,
|
|
41
|
-
...(list.archived ? ['archived: true'] : []),
|
|
42
|
-
...(list.color ? [`color: ${list.color}`] : []),
|
|
43
|
-
...(list.access ? [`access: ${list.access}`] : []),
|
|
44
|
-
].join('\n')
|
|
45
|
-
}));
|
|
46
|
-
return {
|
|
47
|
-
content: [
|
|
48
|
-
{
|
|
49
|
-
type: "text",
|
|
50
|
-
text: `Found ${data.lists.length} list(s) in folder ${folder_id}:`
|
|
51
|
-
},
|
|
52
|
-
...listBlocks
|
|
53
|
-
],
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
// List both folders and folderless lists in the space (parallel requests)
|
|
58
|
-
const archiveQuery = archived ? '?archived=true' : '';
|
|
59
|
-
const [foldersResponse, listsResponse] = await Promise.all([
|
|
60
|
-
fetch(`https://api.clickup.com/api/v2/space/${space_id}/folder${archiveQuery}`, {
|
|
61
|
-
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
62
|
-
}),
|
|
63
|
-
fetch(`https://api.clickup.com/api/v2/space/${space_id}/list${archiveQuery}`, {
|
|
64
|
-
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
65
|
-
})
|
|
66
|
-
]);
|
|
67
|
-
const results = [];
|
|
68
|
-
let totalItems = 0;
|
|
69
|
-
// Process folders
|
|
70
|
-
if (foldersResponse.ok) {
|
|
71
|
-
const foldersData = await foldersResponse.json();
|
|
72
|
-
if (foldersData.folders && Array.isArray(foldersData.folders)) {
|
|
73
|
-
const folderBlocks = foldersData.folders.map((folder) => ({
|
|
74
|
-
type: "text",
|
|
75
|
-
text: [
|
|
76
|
-
`type: folder`,
|
|
77
|
-
`folder_id: ${folder.id}`,
|
|
78
|
-
`name: ${folder.name}`,
|
|
79
|
-
`space_id: ${space_id}`,
|
|
80
|
-
`task_count: ${folder.task_count || 0}`,
|
|
81
|
-
`private: ${folder.private || false}`,
|
|
82
|
-
...(folder.archived ? ['archived: true'] : []),
|
|
83
|
-
...(folder.color ? [`color: ${folder.color}`] : []),
|
|
84
|
-
`contains_lists: true`
|
|
85
|
-
].join('\n')
|
|
86
|
-
}));
|
|
87
|
-
results.push(...folderBlocks);
|
|
88
|
-
totalItems += foldersData.folders.length;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
console.warn(`Failed to fetch folders: ${foldersResponse.status} ${foldersResponse.statusText}`);
|
|
93
|
-
}
|
|
94
|
-
// Process folderless lists
|
|
95
|
-
if (listsResponse.ok) {
|
|
96
|
-
const listsData = await listsResponse.json();
|
|
97
|
-
if (listsData.lists && Array.isArray(listsData.lists)) {
|
|
98
|
-
const listBlocks = listsData.lists.map((list) => ({
|
|
99
|
-
type: "text",
|
|
100
|
-
text: [
|
|
101
|
-
`type: list`,
|
|
102
|
-
`list_id: ${list.id}`,
|
|
103
|
-
`name: ${list.name}`,
|
|
104
|
-
`space_id: ${list.space?.id || space_id}`,
|
|
105
|
-
`task_count: ${list.task_count || 0}`,
|
|
106
|
-
`private: ${list.private || false}`,
|
|
107
|
-
...(list.archived ? ['archived: true'] : []),
|
|
108
|
-
...(list.color ? [`color: ${list.color}`] : []),
|
|
109
|
-
...(list.access ? [`access: ${list.access}`] : []),
|
|
110
|
-
].join('\n')
|
|
111
|
-
}));
|
|
112
|
-
results.push(...listBlocks);
|
|
113
|
-
totalItems += listsData.lists.length;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
console.warn(`Failed to fetch lists: ${listsResponse.status} ${listsResponse.statusText}`);
|
|
118
|
-
}
|
|
119
|
-
if (totalItems === 0) {
|
|
120
|
-
return {
|
|
121
|
-
content: [{ type: "text", text: `No folders or lists found in space ${space_id}.` }],
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
return {
|
|
125
|
-
content: [
|
|
126
|
-
{
|
|
127
|
-
type: "text",
|
|
128
|
-
text: `Found ${totalItems} item(s) in space ${space_id} (folders and lists):`
|
|
129
|
-
},
|
|
130
|
-
...results
|
|
131
|
-
],
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
console.error('Error fetching space content:', error);
|
|
137
|
-
return {
|
|
138
|
-
content: [
|
|
139
|
-
{
|
|
140
|
-
type: "text",
|
|
141
|
-
text: `Error fetching space content: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
142
|
-
},
|
|
143
|
-
],
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
});
|
|
147
9
|
server.tool("getListInfo", [
|
|
148
10
|
"Gets comprehensive information about a list including description and available statuses.",
|
|
11
|
+
"ALWAYS use the list URL (https://app.clickup.com/v/l/LIST_ID) when referencing lists.",
|
|
149
12
|
"Use this before creating tasks to understand the list context and available statuses for new tasks.",
|
|
150
|
-
"IMPORTANT: The list description often contains valuable project context, requirements, or guidelines - read and consider this information when creating or updating tasks in this list."
|
|
13
|
+
"IMPORTANT: The list description often contains valuable project context, requirements, or guidelines - read and consider this information when creating or updating tasks in this list.",
|
|
14
|
+
"Share the clickable list URL when suggesting list-related actions."
|
|
151
15
|
].join("\n"), {
|
|
152
16
|
list_id: zod_1.z.string().min(1).describe("The list ID to get information for")
|
|
153
17
|
}, async ({ list_id }) => {
|
|
@@ -160,12 +24,30 @@ function registerListToolsRead(server) {
|
|
|
160
24
|
throw new Error(`Error fetching list details: ${listResponse.status} ${listResponse.statusText}`);
|
|
161
25
|
}
|
|
162
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
|
+
}
|
|
163
43
|
const responseLines = [
|
|
164
44
|
`List Information:`,
|
|
165
45
|
`list_id: ${list_id}`,
|
|
46
|
+
`list_url: ${(0, utils_1.generateListUrl)(list_id)}`,
|
|
166
47
|
`name: ${listData.name}`,
|
|
167
48
|
`folder: ${listData.folder?.name || 'No folder'}`,
|
|
168
49
|
`space: ${listData.space?.name || 'Unknown'} (${listData.space?.id || 'N/A'})`,
|
|
50
|
+
`space_url: ${(0, utils_1.generateSpaceUrl)(listData.space?.id || '')}`,
|
|
169
51
|
`archived: ${listData.archived || false}`,
|
|
170
52
|
`task_count: ${listData.task_count || 0}`,
|
|
171
53
|
];
|
|
@@ -181,18 +63,25 @@ function registerListToolsRead(server) {
|
|
|
181
63
|
color: status.color || 'none',
|
|
182
64
|
type: status.type || 'custom'
|
|
183
65
|
}));
|
|
184
|
-
responseLines.push('');
|
|
185
66
|
responseLines.push(`Available statuses (${statuses.length} total):`);
|
|
186
67
|
statuses.forEach((status) => {
|
|
187
68
|
responseLines.push(` - ${status.name} (${status.type})`);
|
|
188
69
|
});
|
|
189
|
-
responseLines.push('');
|
|
190
70
|
responseLines.push(`Valid status names for createTask/updateTask: ${statuses.map((s) => s.name).join(', ')}`);
|
|
191
71
|
}
|
|
192
72
|
else {
|
|
193
|
-
responseLines.push('');
|
|
194
73
|
responseLines.push('No statuses found for this list.');
|
|
195
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
|
+
}
|
|
196
85
|
return {
|
|
197
86
|
content: [
|
|
198
87
|
{
|
|
@@ -218,8 +107,10 @@ function registerListToolsRead(server) {
|
|
|
218
107
|
function registerListToolsWrite(server) {
|
|
219
108
|
server.tool("updateListInfo", [
|
|
220
109
|
"Appends documentation or context to a list's description.",
|
|
110
|
+
"ALWAYS reference the list URL (https://app.clickup.com/v/l/LIST_ID) when updating or discussing lists.",
|
|
221
111
|
"SAFETY FEATURE: Description updates are APPEND-ONLY to prevent data loss - existing content is preserved.",
|
|
222
112
|
"Use this to add project context, requirements, or guidelines that LLMs should consider when working with tasks in this list.",
|
|
113
|
+
"Include links to related tasks, spaces, or external resources in the appended content.",
|
|
223
114
|
"Content is appended in markdown format with timestamp for tracking changes."
|
|
224
115
|
].join("\n"), {
|
|
225
116
|
list_id: zod_1.z.string().min(1).describe("The list ID to update"),
|
|
@@ -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;
|
|
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,QA8MnE"}
|
|
@@ -6,18 +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
|
-
|
|
15
|
-
"You'll get a rough overview of the tasks that match the search terms, sorted by relevance.",
|
|
16
|
-
"Always use getTaskById to get more specific information if a task is relevant.",
|
|
14
|
+
"Can also be used to find tasks for the current user by providing the assigned_to_me flag."
|
|
17
15
|
];
|
|
18
16
|
if (config_1.CONFIG.primaryLanguageHint && config_1.CONFIG.primaryLanguageHint.toLowerCase() !== 'en') {
|
|
19
|
-
searchTasksDescriptionBase.
|
|
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}'.`);
|
|
20
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.");
|
|
21
22
|
server.tool("searchTasks", searchTasksDescriptionBase.join("\n"), {
|
|
22
23
|
terms: zod_1.z
|
|
23
24
|
.array(zod_1.z.string())
|
|
@@ -31,33 +32,21 @@ function registerSearchTools(server) {
|
|
|
31
32
|
.array(zod_1.z.string())
|
|
32
33
|
.optional()
|
|
33
34
|
.describe("Filter tasks to specific space IDs"),
|
|
34
|
-
|
|
35
|
+
only_todo: zod_1.z
|
|
35
36
|
.boolean()
|
|
36
37
|
.optional()
|
|
37
|
-
.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)"),
|
|
38
43
|
assigned_to_me: zod_1.z
|
|
39
44
|
.boolean()
|
|
40
45
|
.optional()
|
|
41
|
-
.describe(
|
|
42
|
-
}, async ({ terms, list_ids, space_ids,
|
|
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 }) => {
|
|
43
48
|
// Get current user ID if filtering by assigned_to_me
|
|
44
|
-
|
|
45
|
-
if (assigned_to_me) {
|
|
46
|
-
try {
|
|
47
|
-
const userData = await (0, utils_1.getCurrentUser)();
|
|
48
|
-
assignees = [userData.user.id];
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
return {
|
|
52
|
-
content: [
|
|
53
|
-
{
|
|
54
|
-
type: "text",
|
|
55
|
-
text: "Error: Failed to get current user information.",
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
49
|
+
const assignees = assigned_to_me ? [userData.user.id] : [];
|
|
61
50
|
const searchIndex = await (0, utils_1.getTaskSearchIndex)(space_ids, list_ids, assignees);
|
|
62
51
|
if (!searchIndex) {
|
|
63
52
|
return {
|
|
@@ -72,8 +61,13 @@ function registerSearchTools(server) {
|
|
|
72
61
|
// Early return for no search terms
|
|
73
62
|
if (!terms || terms.length === 0) {
|
|
74
63
|
let allTasks = searchIndex._docs || [];
|
|
75
|
-
|
|
76
|
-
|
|
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");
|
|
77
71
|
}
|
|
78
72
|
// Sort by updated date (most recent first) and limit
|
|
79
73
|
const resultTasks = allTasks
|
|
@@ -93,8 +87,11 @@ function registerSearchTools(server) {
|
|
|
93
87
|
],
|
|
94
88
|
};
|
|
95
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);
|
|
96
93
|
return {
|
|
97
|
-
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))),
|
|
98
95
|
};
|
|
99
96
|
}
|
|
100
97
|
// Filter valid search terms
|
|
@@ -160,8 +157,13 @@ function registerSearchTools(server) {
|
|
|
160
157
|
let resultTasks = Array.from(uniqueResults.values())
|
|
161
158
|
.sort((a, b) => a.score - b.score)
|
|
162
159
|
.map(entry => entry.item);
|
|
163
|
-
|
|
164
|
-
|
|
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");
|
|
165
167
|
}
|
|
166
168
|
// Apply result limit
|
|
167
169
|
resultTasks = resultTasks.slice(0, MAX_SEARCH_RESULTS);
|
|
@@ -175,8 +177,52 @@ function registerSearchTools(server) {
|
|
|
175
177
|
],
|
|
176
178
|
};
|
|
177
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);
|
|
178
183
|
return {
|
|
179
|
-
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))),
|
|
180
185
|
};
|
|
181
186
|
});
|
|
182
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;
|
|
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,57 +2,186 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerSpaceTools = registerSpaceTools;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
-
const
|
|
5
|
+
const utils_1 = require("../shared/utils");
|
|
6
6
|
function registerSpaceTools(server) {
|
|
7
|
-
server.tool("
|
|
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.",
|
|
12
|
+
"Always reference spaces by their URLs when discussing projects or suggesting actions."
|
|
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."),
|
|
8
18
|
archived: zod_1.z.boolean().optional().describe("Include archived spaces (default: false)")
|
|
9
|
-
}, async ({ archived = false }) => {
|
|
19
|
+
}, async ({ terms, archived = false }) => {
|
|
10
20
|
try {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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);
|
|
17
63
|
}
|
|
18
|
-
|
|
19
|
-
if (!
|
|
64
|
+
// Filter by archived status
|
|
65
|
+
if (!archived) {
|
|
66
|
+
matchingSpaces = matchingSpaces.filter((space) => !space.archived);
|
|
67
|
+
}
|
|
68
|
+
if (matchingSpaces.length === 0) {
|
|
20
69
|
return {
|
|
21
|
-
content: [{ type: "text", text: "No spaces found
|
|
70
|
+
content: [{ type: "text", text: "No spaces found matching the search criteria." }],
|
|
22
71
|
};
|
|
23
72
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
+
}
|
|
39
166
|
return {
|
|
40
167
|
content: [
|
|
41
168
|
{
|
|
42
169
|
type: "text",
|
|
43
|
-
text:
|
|
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:`
|
|
44
173
|
},
|
|
45
|
-
...
|
|
174
|
+
...contentBlocks
|
|
46
175
|
],
|
|
47
176
|
};
|
|
48
177
|
}
|
|
49
178
|
catch (error) {
|
|
50
|
-
console.error('Error
|
|
179
|
+
console.error('Error searching spaces:', error);
|
|
51
180
|
return {
|
|
52
181
|
content: [
|
|
53
182
|
{
|
|
54
183
|
type: "text",
|
|
55
|
-
text: `Error
|
|
184
|
+
text: `Error searching spaces: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
56
185
|
},
|
|
57
186
|
],
|
|
58
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,
|
|
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"}
|