@pripanggalih/clickup-mcp 1.6.1
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/LICENSE +22 -0
- package/README.md +295 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +184 -0
- package/dist/clickup-text.d.ts +83 -0
- package/dist/clickup-text.d.ts.map +1 -0
- package/dist/clickup-text.js +563 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +135 -0
- package/dist/resources/space-resources.d.ts +6 -0
- package/dist/resources/space-resources.d.ts.map +1 -0
- package/dist/resources/space-resources.js +95 -0
- package/dist/shared/config.d.ts +11 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +61 -0
- package/dist/shared/data-uri.d.ts +14 -0
- package/dist/shared/data-uri.d.ts.map +1 -0
- package/dist/shared/data-uri.js +34 -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 +199 -0
- package/dist/shared/types.d.ts +21 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/dist/shared/utils.d.ts +71 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +508 -0
- package/dist/test-utils.d.ts +23 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +44 -0
- package/dist/tools/admin-tools.d.ts +3 -0
- package/dist/tools/admin-tools.d.ts.map +1 -0
- package/dist/tools/admin-tools.js +288 -0
- package/dist/tools/doc-tools.d.ts +4 -0
- package/dist/tools/doc-tools.d.ts.map +1 -0
- package/dist/tools/doc-tools.js +436 -0
- package/dist/tools/list-tools.d.ts +4 -0
- package/dist/tools/list-tools.d.ts.map +1 -0
- package/dist/tools/list-tools.js +175 -0
- package/dist/tools/search-tools.d.ts +3 -0
- package/dist/tools/search-tools.d.ts.map +1 -0
- package/dist/tools/search-tools.js +161 -0
- package/dist/tools/space-tools.d.ts +3 -0
- package/dist/tools/space-tools.d.ts.map +1 -0
- package/dist/tools/space-tools.js +128 -0
- package/dist/tools/task-tools.d.ts +8 -0
- package/dist/tools/task-tools.d.ts.map +1 -0
- package/dist/tools/task-tools.js +329 -0
- 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 +567 -0
- package/dist/tools/time-tools.d.ts +4 -0
- package/dist/tools/time-tools.d.ts.map +1 -0
- package/dist/tools/time-tools.js +338 -0
- package/package.json +74 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import Fuse from 'fuse.js';
|
|
2
|
+
/**
|
|
3
|
+
* Checks if a string looks like a valid ClickUp task ID
|
|
4
|
+
* Valid task IDs are 6-9 characters long and contain only alphanumeric characters
|
|
5
|
+
*/
|
|
6
|
+
export declare function isTaskId(str: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Get current authenticated user information from ClickUp API
|
|
9
|
+
* Caches the promise to prevent race conditions on concurrent calls
|
|
10
|
+
*/
|
|
11
|
+
export declare function getCurrentUser(): Promise<any>;
|
|
12
|
+
export { downloadImages } from "./image-processing";
|
|
13
|
+
/**
|
|
14
|
+
* Function to get space details, using a cache to avoid redundant fetches
|
|
15
|
+
*/
|
|
16
|
+
export declare function getSpaceDetails(spaceId: string): Promise<any>;
|
|
17
|
+
/**
|
|
18
|
+
* Get or create a task search index with specified filters
|
|
19
|
+
* Caches promises to prevent race conditions on concurrent calls
|
|
20
|
+
*/
|
|
21
|
+
export declare function getTaskSearchIndex(space_ids?: string[], list_ids?: string[], assignees?: string[]): Promise<Fuse<any> | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Generate a ClickUp task URL from a task ID
|
|
24
|
+
*/
|
|
25
|
+
export declare function generateTaskUrl(taskId: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Generate a ClickUp list URL from a list ID
|
|
28
|
+
*/
|
|
29
|
+
export declare function generateListUrl(listId: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Generate a ClickUp space URL from a space ID
|
|
32
|
+
*/
|
|
33
|
+
export declare function generateSpaceUrl(spaceId: string): string;
|
|
34
|
+
/**
|
|
35
|
+
* Generate a ClickUp folder URL from a folder ID
|
|
36
|
+
*/
|
|
37
|
+
export declare function generateFolderUrl(folderId: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Generate a ClickUp document URL from a document ID and optional page ID
|
|
40
|
+
*/
|
|
41
|
+
export declare function generateDocumentUrl(docId: string, pageId?: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Format space content as tree structure
|
|
44
|
+
* Shared function used by both searchSpaces tool and space resources
|
|
45
|
+
*/
|
|
46
|
+
export declare function formatSpaceTree(space: any, lists: any[], folders: any[], documents: any[]): string;
|
|
47
|
+
/**
|
|
48
|
+
* Get or refresh the space search index
|
|
49
|
+
* Caches promise to prevent race conditions on concurrent calls
|
|
50
|
+
*/
|
|
51
|
+
export declare function getSpaceSearchIndex(): Promise<Fuse<any> | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Get lists, folders, and documents for a specific space with caching
|
|
54
|
+
*/
|
|
55
|
+
export declare function getSpaceContent(spaceId: string): Promise<{
|
|
56
|
+
lists: any[];
|
|
57
|
+
folders: any[];
|
|
58
|
+
documents: any[];
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Gets all team members from ClickUp API with caching
|
|
62
|
+
*/
|
|
63
|
+
export declare function getAllTeamMembers(): Promise<string[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Performs multi-term search with aggressive boosting for items matching multiple terms
|
|
66
|
+
* @param searchIndex Fuse search index to search within
|
|
67
|
+
* @param terms Array of search terms
|
|
68
|
+
* @returns Array of items sorted by relevance (multi-term matches ranked higher)
|
|
69
|
+
*/
|
|
70
|
+
export declare function performMultiTermSearch<T>(searchIndex: Fuse<T>, terms: string[]): Promise<T[]>;
|
|
71
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/shared/utils.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,SAAS,CAAC;AAI3B;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG7C;AAKD;;;GAGG;AACH,wBAAsB,cAAc,iBA6BnC;AAGD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAIpD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CA0B7D;AAKD;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,CAAC,EAAE,MAAM,EAAE,EACpB,QAAQ,CAAC,EAAE,MAAM,EAAE,EACnB,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CA+B3B;AAkED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1E;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,MAAM,CA4ElG;AAKD;;;GAGG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CA+CrE;AAKD;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,GAAG,EAAE,CAAC;IAAC,OAAO,EAAE,GAAG,EAAE,CAAC;IAAC,SAAS,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,CAgFlH;AAKD;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CA+C3D;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,WAAW,EAAE,IAAI,CAAC,CAAC,CAAC,EACpB,KAAK,EAAE,MAAM,EAAE,GACd,OAAO,CAAC,CAAC,EAAE,CAAC,CA8Dd"}
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.downloadImages = void 0;
|
|
7
|
+
exports.isTaskId = isTaskId;
|
|
8
|
+
exports.getCurrentUser = getCurrentUser;
|
|
9
|
+
exports.getSpaceDetails = getSpaceDetails;
|
|
10
|
+
exports.getTaskSearchIndex = getTaskSearchIndex;
|
|
11
|
+
exports.generateTaskUrl = generateTaskUrl;
|
|
12
|
+
exports.generateListUrl = generateListUrl;
|
|
13
|
+
exports.generateSpaceUrl = generateSpaceUrl;
|
|
14
|
+
exports.generateFolderUrl = generateFolderUrl;
|
|
15
|
+
exports.generateDocumentUrl = generateDocumentUrl;
|
|
16
|
+
exports.formatSpaceTree = formatSpaceTree;
|
|
17
|
+
exports.getSpaceSearchIndex = getSpaceSearchIndex;
|
|
18
|
+
exports.getSpaceContent = getSpaceContent;
|
|
19
|
+
exports.getAllTeamMembers = getAllTeamMembers;
|
|
20
|
+
exports.performMultiTermSearch = performMultiTermSearch;
|
|
21
|
+
const config_1 = require("./config");
|
|
22
|
+
const fuse_js_1 = __importDefault(require("fuse.js"));
|
|
23
|
+
const GLOBAL_REFRESH_INTERVAL = 60000; // 60 seconds - that is the rate limit time frame
|
|
24
|
+
/**
|
|
25
|
+
* Checks if a string looks like a valid ClickUp task ID
|
|
26
|
+
* Valid task IDs are 6-9 characters long and contain only alphanumeric characters
|
|
27
|
+
*/
|
|
28
|
+
function isTaskId(str) {
|
|
29
|
+
// Task IDs are 6-9 characters long and contain only alphanumeric characters
|
|
30
|
+
return /^[a-z0-9]{6,9}$/i.test(str);
|
|
31
|
+
}
|
|
32
|
+
// Cache for current user info to avoid repeated API calls and race conditions
|
|
33
|
+
let cachedUserPromise = null;
|
|
34
|
+
/**
|
|
35
|
+
* Get current authenticated user information from ClickUp API
|
|
36
|
+
* Caches the promise to prevent race conditions on concurrent calls
|
|
37
|
+
*/
|
|
38
|
+
async function getCurrentUser() {
|
|
39
|
+
// Return cached promise if available
|
|
40
|
+
if (cachedUserPromise) {
|
|
41
|
+
return cachedUserPromise;
|
|
42
|
+
}
|
|
43
|
+
// Create the fetch promise
|
|
44
|
+
const fetchPromise = (async () => {
|
|
45
|
+
const userResponse = await fetch("https://api.clickup.com/api/v2/user", {
|
|
46
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
47
|
+
});
|
|
48
|
+
if (!userResponse.ok) {
|
|
49
|
+
throw new Error(`Error fetching user info: ${userResponse.status} ${userResponse.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
return await userResponse.json();
|
|
52
|
+
})();
|
|
53
|
+
// Cache the promise
|
|
54
|
+
cachedUserPromise = fetchPromise;
|
|
55
|
+
// Auto-cleanup after 60 seconds
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
cachedUserPromise = null;
|
|
58
|
+
console.error(`Auto-cleaned user data cache`);
|
|
59
|
+
}, GLOBAL_REFRESH_INTERVAL);
|
|
60
|
+
return fetchPromise;
|
|
61
|
+
}
|
|
62
|
+
// Re-export image processing functions for backward compatibility
|
|
63
|
+
var image_processing_1 = require("./image-processing");
|
|
64
|
+
Object.defineProperty(exports, "downloadImages", { enumerable: true, get: function () { return image_processing_1.downloadImages; } });
|
|
65
|
+
const spaceCache = new Map(); // Global cache for space details promises
|
|
66
|
+
/**
|
|
67
|
+
* Function to get space details, using a cache to avoid redundant fetches
|
|
68
|
+
*/
|
|
69
|
+
function getSpaceDetails(spaceId) {
|
|
70
|
+
if (!spaceId) {
|
|
71
|
+
return Promise.reject(new Error('Invalid space ID'));
|
|
72
|
+
}
|
|
73
|
+
const cachedSpace = spaceCache.get(spaceId);
|
|
74
|
+
if (cachedSpace) {
|
|
75
|
+
return cachedSpace;
|
|
76
|
+
}
|
|
77
|
+
const fetchPromise = fetch(`https://api.clickup.com/api/v2/space/${spaceId}`, { headers: { Authorization: config_1.CONFIG.apiKey } })
|
|
78
|
+
.then(res => {
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
throw new Error(`Error fetching space ${spaceId}: ${res.status}`);
|
|
81
|
+
}
|
|
82
|
+
return res.json();
|
|
83
|
+
})
|
|
84
|
+
.catch(error => {
|
|
85
|
+
console.error(`Network error fetching space ${spaceId}:`, error);
|
|
86
|
+
throw new Error(`Error fetching space ${spaceId}: ${error}`);
|
|
87
|
+
});
|
|
88
|
+
spaceCache.set(spaceId, fetchPromise);
|
|
89
|
+
return fetchPromise;
|
|
90
|
+
}
|
|
91
|
+
// Task search index management - cache promises to prevent race conditions
|
|
92
|
+
const taskIndices = new Map();
|
|
93
|
+
/**
|
|
94
|
+
* Get or create a task search index with specified filters
|
|
95
|
+
* Caches promises to prevent race conditions on concurrent calls
|
|
96
|
+
*/
|
|
97
|
+
async function getTaskSearchIndex(space_ids, list_ids, assignees) {
|
|
98
|
+
// Create cache key from sorted filter arrays
|
|
99
|
+
const key = JSON.stringify({
|
|
100
|
+
space_ids: space_ids?.sort(),
|
|
101
|
+
list_ids: list_ids?.sort(),
|
|
102
|
+
assignees: assignees?.sort()
|
|
103
|
+
});
|
|
104
|
+
// Check for existing valid index promise
|
|
105
|
+
const cachedPromise = taskIndices.get(key);
|
|
106
|
+
if (cachedPromise) {
|
|
107
|
+
return cachedPromise;
|
|
108
|
+
}
|
|
109
|
+
// Create the fetch promise
|
|
110
|
+
const fetchPromise = (async () => {
|
|
111
|
+
console.error(`Refreshing task index for filters: ${key}`);
|
|
112
|
+
const tasks = await fetchTasks(space_ids, list_ids, assignees);
|
|
113
|
+
const index = createFuseIndex(tasks);
|
|
114
|
+
console.error(`Task index created with ${tasks.length} tasks`);
|
|
115
|
+
return index;
|
|
116
|
+
})();
|
|
117
|
+
// Store promise with auto-cleanup
|
|
118
|
+
taskIndices.set(key, fetchPromise);
|
|
119
|
+
setTimeout(() => {
|
|
120
|
+
taskIndices.delete(key);
|
|
121
|
+
console.error(`Auto-cleaned index for filters: ${key}`);
|
|
122
|
+
}, GLOBAL_REFRESH_INTERVAL);
|
|
123
|
+
return fetchPromise;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fetch tasks using team endpoint with dynamic filters
|
|
127
|
+
*/
|
|
128
|
+
async function fetchTasks(space_ids, list_ids, assignees) {
|
|
129
|
+
const queryParams = ['order_by=updated', 'subtasks=true'];
|
|
130
|
+
// Add filter parameters
|
|
131
|
+
if (space_ids?.length) {
|
|
132
|
+
space_ids.forEach(id => queryParams.push(`space_ids[]=${id}`));
|
|
133
|
+
}
|
|
134
|
+
if (list_ids?.length) {
|
|
135
|
+
list_ids.forEach(id => queryParams.push(`list_ids[]=${id}`));
|
|
136
|
+
}
|
|
137
|
+
if (assignees?.length) {
|
|
138
|
+
assignees.forEach(id => queryParams.push(`assignees[]=${id}`));
|
|
139
|
+
}
|
|
140
|
+
const queryString = queryParams.join('&');
|
|
141
|
+
// Fetch multiple pages in parallel
|
|
142
|
+
const maxPages = space_ids?.length || list_ids?.length || assignees?.length ? 10 : 30; // Fewer pages for filtered searches
|
|
143
|
+
const taskListsPromises = [...Array(maxPages)].map(async (_, i) => {
|
|
144
|
+
const url = `https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/task?${queryString}&page=${i}`;
|
|
145
|
+
try {
|
|
146
|
+
const res = await fetch(url, { headers: { Authorization: config_1.CONFIG.apiKey } });
|
|
147
|
+
return await res.json();
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
console.error(`Error fetching page ${i}:`, e);
|
|
151
|
+
return { tasks: [] };
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
const taskLists = await Promise.all(taskListsPromises);
|
|
155
|
+
return taskLists.flatMap(taskList => taskList.tasks || []);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Create a Fuse index from tasks array
|
|
159
|
+
*/
|
|
160
|
+
function createFuseIndex(tasks) {
|
|
161
|
+
return new fuse_js_1.default(tasks, {
|
|
162
|
+
keys: [
|
|
163
|
+
{ name: 'name', weight: 0.7 },
|
|
164
|
+
{ name: 'id', weight: 0.6 },
|
|
165
|
+
{ name: 'text_content', weight: 0.5 },
|
|
166
|
+
{ name: 'tags.name', weight: 0.4 },
|
|
167
|
+
{ name: 'assignees.username', weight: 0.4 },
|
|
168
|
+
{ name: 'list.name', weight: 0.3 },
|
|
169
|
+
{ name: 'folder.name', weight: 0.2 },
|
|
170
|
+
{ name: 'space.name', weight: 0.1 }
|
|
171
|
+
],
|
|
172
|
+
findAllMatches: true,
|
|
173
|
+
includeScore: true,
|
|
174
|
+
minMatchCharLength: 2,
|
|
175
|
+
threshold: 0.4,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// ===== LINK UTILITIES =====
|
|
179
|
+
/**
|
|
180
|
+
* Generate a ClickUp task URL from a task ID
|
|
181
|
+
*/
|
|
182
|
+
function generateTaskUrl(taskId) {
|
|
183
|
+
return `https://app.clickup.com/t/${taskId}`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Generate a ClickUp list URL from a list ID
|
|
187
|
+
*/
|
|
188
|
+
function generateListUrl(listId) {
|
|
189
|
+
return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/li/${listId}`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Generate a ClickUp space URL from a space ID
|
|
193
|
+
*/
|
|
194
|
+
function generateSpaceUrl(spaceId) {
|
|
195
|
+
return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/s/${spaceId}`;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Generate a ClickUp folder URL from a folder ID
|
|
199
|
+
*/
|
|
200
|
+
function generateFolderUrl(folderId) {
|
|
201
|
+
return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/f/${folderId}`;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Generate a ClickUp document URL from a document ID and optional page ID
|
|
205
|
+
*/
|
|
206
|
+
function generateDocumentUrl(docId, pageId) {
|
|
207
|
+
if (pageId) {
|
|
208
|
+
return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/dc/${docId}/${pageId}`;
|
|
209
|
+
}
|
|
210
|
+
return `https://app.clickup.com/${config_1.CONFIG.teamId}/v/dc/${docId}`;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Format space content as tree structure
|
|
214
|
+
* Shared function used by both searchSpaces tool and space resources
|
|
215
|
+
*/
|
|
216
|
+
function formatSpaceTree(space, lists, folders, documents) {
|
|
217
|
+
const spaceLines = [];
|
|
218
|
+
const totalLists = lists.length + folders.reduce((sum, f) => sum + (f.lists?.length || 0), 0);
|
|
219
|
+
// Space header
|
|
220
|
+
spaceLines.push(`🏢 SPACE: ${space.name} (space_id: ${space.id}${space.private ? ', private' : ''}${space.archived ? ', archived' : ''}) ${generateSpaceUrl(space.id)}`, ` ${totalLists} lists, ${folders.length} folders, ${documents.length} documents`);
|
|
221
|
+
// Create a tree structure
|
|
222
|
+
const hasDirectLists = lists.length > 0;
|
|
223
|
+
const hasFolders = folders.length > 0;
|
|
224
|
+
const hasDocuments = documents.length > 0;
|
|
225
|
+
// Direct lists (not in folders)
|
|
226
|
+
if (hasDirectLists) {
|
|
227
|
+
lists.forEach((list, listIndex) => {
|
|
228
|
+
const isLastDirectList = listIndex === lists.length - 1;
|
|
229
|
+
const isLastOverall = !hasFolders && !hasDocuments && isLastDirectList;
|
|
230
|
+
const treeChar = isLastOverall ? '└──' : '├──';
|
|
231
|
+
const extraInfo = [
|
|
232
|
+
...(list.task_count ? [`${list.task_count} tasks`] : []),
|
|
233
|
+
...(list.private ? ['private'] : []),
|
|
234
|
+
...(list.archived ? ['archived'] : [])
|
|
235
|
+
].join(', ');
|
|
236
|
+
const listLine = `${treeChar} 📝 ${list.name} (list_id: ${list.id}${extraInfo ? `, ${extraInfo}` : ''}) ${generateListUrl(list.id)}`;
|
|
237
|
+
spaceLines.push(listLine);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// Folders and their lists
|
|
241
|
+
if (hasFolders) {
|
|
242
|
+
folders.forEach((folder, folderIndex) => {
|
|
243
|
+
const isLastFolder = folderIndex === folders.length - 1;
|
|
244
|
+
const isLastOverall = !hasDocuments && isLastFolder;
|
|
245
|
+
const folderTreeChar = isLastOverall ? '└──' : '├──';
|
|
246
|
+
const folderContinuation = isLastOverall ? ' ' : '│ ';
|
|
247
|
+
const folderExtraInfo = [
|
|
248
|
+
...(folder.lists?.length ? [`${folder.lists.length} lists`] : []),
|
|
249
|
+
...(folder.private ? ['private'] : []),
|
|
250
|
+
...(folder.archived ? ['archived'] : [])
|
|
251
|
+
].join(', ');
|
|
252
|
+
const folderLine = `${folderTreeChar} 📂 ${folder.name} (folder_id: ${folder.id}${folderExtraInfo ? `, ${folderExtraInfo}` : ''}) ${generateFolderUrl(folder.id)}`;
|
|
253
|
+
spaceLines.push(folderLine);
|
|
254
|
+
// Lists within this folder
|
|
255
|
+
if (folder.lists && folder.lists.length > 0) {
|
|
256
|
+
folder.lists.forEach((list, listIndex) => {
|
|
257
|
+
const isLastListInFolder = listIndex === folder.lists.length - 1;
|
|
258
|
+
const listTreeChar = isLastListInFolder ? '└──' : '├──';
|
|
259
|
+
const listExtraInfo = [
|
|
260
|
+
...(list.task_count ? [`${list.task_count} tasks`] : []),
|
|
261
|
+
...(list.private ? ['private'] : []),
|
|
262
|
+
...(list.archived ? ['archived'] : [])
|
|
263
|
+
].join(', ');
|
|
264
|
+
const listLine = `${folderContinuation}${listTreeChar} 📝 ${list.name} (list_id: ${list.id}${listExtraInfo ? `, ${listExtraInfo}` : ''}) ${generateListUrl(list.id)}`;
|
|
265
|
+
spaceLines.push(listLine);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
// Documents attached to this space
|
|
271
|
+
if (hasDocuments) {
|
|
272
|
+
documents.forEach((document, docIndex) => {
|
|
273
|
+
const isLastDocument = docIndex === documents.length - 1;
|
|
274
|
+
const docTreeChar = isLastDocument ? '└──' : '├──';
|
|
275
|
+
const docLine = `${docTreeChar} 📄 ${document.name} (doc_id: ${document.id}) ${generateDocumentUrl(document.id)}`;
|
|
276
|
+
spaceLines.push(docLine);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return spaceLines.join('\n');
|
|
280
|
+
}
|
|
281
|
+
// Space search index cache - cache promise to prevent race conditions
|
|
282
|
+
let spaceSearchIndexPromise = null;
|
|
283
|
+
/**
|
|
284
|
+
* Get or refresh the space search index
|
|
285
|
+
* Caches promise to prevent race conditions on concurrent calls
|
|
286
|
+
*/
|
|
287
|
+
async function getSpaceSearchIndex() {
|
|
288
|
+
// Return cached promise if available
|
|
289
|
+
if (spaceSearchIndexPromise) {
|
|
290
|
+
return spaceSearchIndexPromise;
|
|
291
|
+
}
|
|
292
|
+
// Create the fetch promise
|
|
293
|
+
const fetchPromise = (async () => {
|
|
294
|
+
try {
|
|
295
|
+
const url = `https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/space`;
|
|
296
|
+
const response = await fetch(url, {
|
|
297
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
298
|
+
});
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
throw new Error(`Error fetching spaces: ${response.status} ${response.statusText}`);
|
|
301
|
+
}
|
|
302
|
+
const data = await response.json();
|
|
303
|
+
const spacesData = data.spaces || [];
|
|
304
|
+
// Create Fuse search index
|
|
305
|
+
return new fuse_js_1.default(spacesData, {
|
|
306
|
+
keys: [
|
|
307
|
+
{ name: 'name', weight: 0.7 },
|
|
308
|
+
{ name: 'id', weight: 0.6 }
|
|
309
|
+
],
|
|
310
|
+
includeScore: true,
|
|
311
|
+
threshold: 0.4,
|
|
312
|
+
minMatchCharLength: 1,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
console.error('Error creating space search index:', error);
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
})();
|
|
320
|
+
// Cache the promise
|
|
321
|
+
spaceSearchIndexPromise = fetchPromise;
|
|
322
|
+
// Auto-cleanup after 60 seconds
|
|
323
|
+
setTimeout(() => {
|
|
324
|
+
spaceSearchIndexPromise = null;
|
|
325
|
+
console.error('Auto-cleaned space search index');
|
|
326
|
+
}, GLOBAL_REFRESH_INTERVAL);
|
|
327
|
+
return fetchPromise;
|
|
328
|
+
}
|
|
329
|
+
const listCache = new Map(); // Cache for space lists/folders
|
|
330
|
+
/**
|
|
331
|
+
* Get lists, folders, and documents for a specific space with caching
|
|
332
|
+
*/
|
|
333
|
+
async function getSpaceContent(spaceId) {
|
|
334
|
+
const cacheKey = `space-content-${spaceId}`;
|
|
335
|
+
// Check cache first
|
|
336
|
+
const cachedContent = listCache.get(cacheKey);
|
|
337
|
+
if (cachedContent) {
|
|
338
|
+
return cachedContent;
|
|
339
|
+
}
|
|
340
|
+
// Fetch content with parallel requests
|
|
341
|
+
const fetchPromise = (async () => {
|
|
342
|
+
try {
|
|
343
|
+
const [folders, lists, documents] = await Promise.all([
|
|
344
|
+
fetch(`https://api.clickup.com/api/v2/space/${spaceId}/folder`, {
|
|
345
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
346
|
+
})
|
|
347
|
+
.then(response => response.json())
|
|
348
|
+
.then(json => json.folders || [])
|
|
349
|
+
.catch(e => {
|
|
350
|
+
console.error(e);
|
|
351
|
+
return [];
|
|
352
|
+
}),
|
|
353
|
+
fetch(`https://api.clickup.com/api/v2/space/${spaceId}/list`, {
|
|
354
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
355
|
+
})
|
|
356
|
+
.then(response => response.json())
|
|
357
|
+
.then(json => json.lists || [])
|
|
358
|
+
.catch(e => {
|
|
359
|
+
console.error(e);
|
|
360
|
+
return [];
|
|
361
|
+
}),
|
|
362
|
+
fetch(`https://api.clickup.com/api/v3/workspaces/${config_1.CONFIG.teamId}/docs?parent_id=${spaceId}`, {
|
|
363
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
364
|
+
})
|
|
365
|
+
.then(response => response.json())
|
|
366
|
+
.then(json => json.docs || [])
|
|
367
|
+
.catch(e => {
|
|
368
|
+
console.error(e);
|
|
369
|
+
return [];
|
|
370
|
+
})
|
|
371
|
+
]);
|
|
372
|
+
// For each folder, also fetch its lists
|
|
373
|
+
const folderListPromises = folders.map(async (folder) => {
|
|
374
|
+
try {
|
|
375
|
+
const folderListResponse = await fetch(`https://api.clickup.com/api/v2/folder/${folder.id}/list`, { headers: { Authorization: config_1.CONFIG.apiKey } });
|
|
376
|
+
if (folderListResponse.ok) {
|
|
377
|
+
const folderListData = await folderListResponse.json();
|
|
378
|
+
folder.lists = folderListData.lists || [];
|
|
379
|
+
}
|
|
380
|
+
return folder;
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
console.error(`Error fetching lists for folder ${folder.id}:`, error);
|
|
384
|
+
folder.lists = [];
|
|
385
|
+
return folder;
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
const foldersWithLists = await Promise.all(folderListPromises);
|
|
389
|
+
return { lists, folders: foldersWithLists, documents };
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
console.error(`Error fetching space content for ${spaceId}:`, error);
|
|
393
|
+
return { lists: [], folders: [], documents: [] };
|
|
394
|
+
}
|
|
395
|
+
})();
|
|
396
|
+
// Cache the promise
|
|
397
|
+
listCache.set(cacheKey, fetchPromise);
|
|
398
|
+
// Auto-cleanup after 60 seconds
|
|
399
|
+
setTimeout(() => {
|
|
400
|
+
listCache.delete(cacheKey);
|
|
401
|
+
console.error(`Auto-cleaned space content cache for ${spaceId}`);
|
|
402
|
+
}, GLOBAL_REFRESH_INTERVAL);
|
|
403
|
+
return fetchPromise;
|
|
404
|
+
}
|
|
405
|
+
// Cache for team members to avoid repeated API calls and race conditions
|
|
406
|
+
let cachedTeamMembersPromise = null;
|
|
407
|
+
/**
|
|
408
|
+
* Gets all team members from ClickUp API with caching
|
|
409
|
+
*/
|
|
410
|
+
async function getAllTeamMembers() {
|
|
411
|
+
// Return cached promise if available
|
|
412
|
+
if (cachedTeamMembersPromise) {
|
|
413
|
+
return cachedTeamMembersPromise;
|
|
414
|
+
}
|
|
415
|
+
// Create the fetch promise
|
|
416
|
+
const fetchPromise = (async () => {
|
|
417
|
+
try {
|
|
418
|
+
const response = await fetch(`https://api.clickup.com/api/v2/team`, {
|
|
419
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
420
|
+
});
|
|
421
|
+
if (!response.ok) {
|
|
422
|
+
console.error(`Error fetching teams: ${response.status} ${response.statusText}`);
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
const data = await response.json();
|
|
426
|
+
if (!data.teams || !Array.isArray(data.teams)) {
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
// Find the team that matches our configured team ID and extract all user IDs
|
|
430
|
+
const currentTeam = data.teams.find((team) => team.id === config_1.CONFIG.teamId);
|
|
431
|
+
if (!currentTeam || !currentTeam.members || !Array.isArray(currentTeam.members)) {
|
|
432
|
+
console.error(`Team ${config_1.CONFIG.teamId} not found or has no members`);
|
|
433
|
+
return [];
|
|
434
|
+
}
|
|
435
|
+
return currentTeam.members.map((member) => member.user?.id).filter(Boolean);
|
|
436
|
+
}
|
|
437
|
+
catch (error) {
|
|
438
|
+
console.error('Error fetching team members:', error);
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
})();
|
|
442
|
+
// Cache the promise
|
|
443
|
+
cachedTeamMembersPromise = fetchPromise;
|
|
444
|
+
// Auto-cleanup after 60 seconds
|
|
445
|
+
setTimeout(() => {
|
|
446
|
+
cachedTeamMembersPromise = null;
|
|
447
|
+
console.error(`Auto-cleaned team members cache`);
|
|
448
|
+
}, GLOBAL_REFRESH_INTERVAL);
|
|
449
|
+
return fetchPromise;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Performs multi-term search with aggressive boosting for items matching multiple terms
|
|
453
|
+
* @param searchIndex Fuse search index to search within
|
|
454
|
+
* @param terms Array of search terms
|
|
455
|
+
* @returns Array of items sorted by relevance (multi-term matches ranked higher)
|
|
456
|
+
*/
|
|
457
|
+
async function performMultiTermSearch(searchIndex, terms) {
|
|
458
|
+
// Filter valid search terms
|
|
459
|
+
const validTerms = terms.filter(term => term && term.trim().length > 0);
|
|
460
|
+
if (validTerms.length === 0) {
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
// Track multiple matches per item for aggressive boosting
|
|
464
|
+
const itemMatches = new Map();
|
|
465
|
+
// Collect all matches for each term
|
|
466
|
+
validTerms.forEach(term => {
|
|
467
|
+
const results = searchIndex.search(term);
|
|
468
|
+
results.forEach(result => {
|
|
469
|
+
if (result.item && typeof result.item.id === 'string') {
|
|
470
|
+
const itemId = result.item.id;
|
|
471
|
+
const currentScore = result.score ?? 1;
|
|
472
|
+
const existing = itemMatches.get(itemId);
|
|
473
|
+
if (!existing) {
|
|
474
|
+
itemMatches.set(itemId, {
|
|
475
|
+
item: result.item,
|
|
476
|
+
scores: [currentScore],
|
|
477
|
+
matchedTerms: [term]
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
existing.scores.push(currentScore);
|
|
482
|
+
existing.matchedTerms.push(term);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
// Calculate aggressively boosted scores for multi-term matches
|
|
488
|
+
const uniqueResults = new Map();
|
|
489
|
+
itemMatches.forEach((match, itemId) => {
|
|
490
|
+
const bestScore = Math.min(...match.scores);
|
|
491
|
+
const matchCount = match.scores.length;
|
|
492
|
+
const totalTerms = validTerms.length;
|
|
493
|
+
// Aggressive multi-term boost: exponential improvement for multiple matches
|
|
494
|
+
// 1 match: base score
|
|
495
|
+
// 2+ matches: exponentially better score based on match ratio
|
|
496
|
+
const matchRatio = matchCount / totalTerms;
|
|
497
|
+
const boostFactor = Math.pow(0.1, matchRatio * 4); // Very aggressive boost
|
|
498
|
+
const finalScore = bestScore * boostFactor;
|
|
499
|
+
uniqueResults.set(itemId, {
|
|
500
|
+
item: match.item,
|
|
501
|
+
score: finalScore
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
// Return sorted results (best scores first)
|
|
505
|
+
return Array.from(uniqueResults.values())
|
|
506
|
+
.sort((a, b) => a.score - b.score)
|
|
507
|
+
.map(entry => entry.item);
|
|
508
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"admin-tools.d.ts","sourceRoot":"","sources":["../../src/tools/admin-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAYpE,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,QAoOxD"}
|