@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/time-tools.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.registerTimeToolsRead = registerTimeToolsRead;
|
|
|
4
4
|
exports.registerTimeToolsWrite = registerTimeToolsWrite;
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
const config_1 = require("../shared/config");
|
|
7
|
+
const utils_1 = require("../shared/utils");
|
|
7
8
|
/**
|
|
8
9
|
* Converts ISO date string to Unix timestamp in milliseconds
|
|
9
10
|
*/
|
|
@@ -29,12 +30,35 @@ function timestampToIso(timestamp) {
|
|
|
29
30
|
const timezoneOffset = sign + String(offsetHours).padStart(2, '0') + ':' + String(offsetMinutes).padStart(2, '0');
|
|
30
31
|
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${timezoneOffset}`;
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Formats duration in milliseconds to human readable format
|
|
35
|
+
*/
|
|
36
|
+
function formatDuration(durationMs) {
|
|
37
|
+
const hours = durationMs / (1000 * 60 * 60);
|
|
38
|
+
const displayHours = Math.floor(hours);
|
|
39
|
+
const displayMinutes = Math.round((hours - displayHours) * 60);
|
|
40
|
+
return displayHours > 0 ? `${displayHours}h ${displayMinutes}m` : `${displayMinutes}m`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Formats timestamp to simple date and time for entry display
|
|
44
|
+
*/
|
|
45
|
+
function formatEntryTime(timestamp) {
|
|
46
|
+
const date = new Date(timestamp);
|
|
47
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
48
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
49
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
50
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
51
|
+
return `${date.getFullYear()}-${month}-${day} ${hours}:${minutes}`;
|
|
52
|
+
}
|
|
32
53
|
function registerTimeToolsRead(server) {
|
|
33
54
|
server.tool("getTimeEntries", "Gets time entries for a specific task or all user's time entries. Returns last 30 days by default if no dates specified.", {
|
|
34
55
|
task_id: zod_1.z.string().min(6).max(9).optional().describe("Optional 6-9 character task ID to filter entries. If not provided, returns all user's time entries."),
|
|
35
56
|
start_date: zod_1.z.string().optional().describe("Optional start date filter as ISO date string (e.g., '2024-10-06T00:00:00+02:00'). Defaults to 30 days ago."),
|
|
36
|
-
end_date: zod_1.z.string().optional().describe("Optional end date filter as ISO date string (e.g., '2024-10-06T23:59:59+02:00'). Defaults to current date.")
|
|
37
|
-
|
|
57
|
+
end_date: zod_1.z.string().optional().describe("Optional end date filter as ISO date string (e.g., '2024-10-06T23:59:59+02:00'). Defaults to current date."),
|
|
58
|
+
list_id: zod_1.z.string().optional().describe("Optional single list ID to filter time entries by a specific list"),
|
|
59
|
+
space_id: zod_1.z.string().optional().describe("Optional single space ID to filter time entries by a specific space"),
|
|
60
|
+
include_all_users: zod_1.z.boolean().optional().describe("Optional flag to include time entries from all team members (default: false, only current user)")
|
|
61
|
+
}, async ({ task_id, start_date, end_date, list_id, space_id, include_all_users }) => {
|
|
38
62
|
try {
|
|
39
63
|
// Build query parameters
|
|
40
64
|
const params = new URLSearchParams();
|
|
@@ -47,122 +71,37 @@ function registerTimeToolsRead(server) {
|
|
|
47
71
|
if (end_date) {
|
|
48
72
|
params.append('end_date', isoToTimestamp(end_date).toString());
|
|
49
73
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (!response.ok) {
|
|
54
|
-
throw new Error(`Error fetching time entries: ${response.status} ${response.statusText}`);
|
|
74
|
+
// Add single list_id or space_id filter (not both)
|
|
75
|
+
if (list_id) {
|
|
76
|
+
params.append('list_id', list_id);
|
|
55
77
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const noEntriesMsg = task_id ?
|
|
59
|
-
`No time entries found for task ${task_id}.` :
|
|
60
|
-
'No time entries found for current user.';
|
|
61
|
-
return {
|
|
62
|
-
content: [{ type: "text", text: noEntriesMsg }],
|
|
63
|
-
};
|
|
78
|
+
else if (space_id) {
|
|
79
|
+
params.append('space_id', space_id);
|
|
64
80
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const entryDurationMs = parseInt(entry.duration) || 0;
|
|
75
|
-
timeByUser.set(username, currentTime + entryDurationMs);
|
|
76
|
-
if (!entriesByUser.has(username)) {
|
|
77
|
-
entriesByUser.set(username, []);
|
|
81
|
+
// Always include location names to get list information
|
|
82
|
+
params.append('include_location_names', 'true');
|
|
83
|
+
// Handle include_all_users by fetching all team members and adding them as assignees filter
|
|
84
|
+
// Note: This only works for Workspace Owners/Admins
|
|
85
|
+
if (include_all_users) {
|
|
86
|
+
try {
|
|
87
|
+
const teamMembers = await (0, utils_1.getAllTeamMembers)();
|
|
88
|
+
if (teamMembers.length > 0) {
|
|
89
|
+
params.append('assignee', teamMembers.join(','));
|
|
78
90
|
}
|
|
79
|
-
entriesByUser.get(username).push(entry);
|
|
80
|
-
});
|
|
81
|
-
summaryLines = [`Time entries for task ${task_id}:`];
|
|
82
|
-
for (const [username, totalMs] of timeByUser.entries()) {
|
|
83
|
-
const hours = totalMs / (1000 * 60 * 60);
|
|
84
|
-
const displayHours = Math.floor(hours);
|
|
85
|
-
const displayMinutes = Math.round((hours - displayHours) * 60);
|
|
86
|
-
const timeDisplay = displayHours > 0 ?
|
|
87
|
-
`${displayHours}h ${displayMinutes}m` :
|
|
88
|
-
`${displayMinutes}m`;
|
|
89
|
-
summaryLines.push(` ${username}: ${timeDisplay}`);
|
|
90
|
-
totalTimeMs += totalMs;
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const timeByTask = new Map();
|
|
96
|
-
const entriesByTask = new Map();
|
|
97
|
-
data.data.forEach((entry) => {
|
|
98
|
-
const taskInfo = entry.task ?
|
|
99
|
-
`${entry.task.name} (${entry.task.id})` :
|
|
100
|
-
'No task';
|
|
101
|
-
const currentTime = timeByTask.get(taskInfo) || 0;
|
|
102
|
-
const entryDurationMs = parseInt(entry.duration) || 0;
|
|
103
|
-
timeByTask.set(taskInfo, currentTime + entryDurationMs);
|
|
104
|
-
if (!entriesByTask.has(taskInfo)) {
|
|
105
|
-
entriesByTask.set(taskInfo, []);
|
|
106
|
-
}
|
|
107
|
-
entriesByTask.get(taskInfo).push(entry);
|
|
108
|
-
});
|
|
109
|
-
summaryLines = ['Time entries for current user:'];
|
|
110
|
-
for (const [taskInfo, totalMs] of timeByTask.entries()) {
|
|
111
|
-
const hours = totalMs / (1000 * 60 * 60);
|
|
112
|
-
const displayHours = Math.floor(hours);
|
|
113
|
-
const displayMinutes = Math.round((hours - displayHours) * 60);
|
|
114
|
-
const timeDisplay = displayHours > 0 ?
|
|
115
|
-
`${displayHours}h ${displayMinutes}m` :
|
|
116
|
-
`${displayMinutes}m`;
|
|
117
|
-
summaryLines.push(` ${taskInfo}: ${timeDisplay}`);
|
|
118
|
-
totalTimeMs += totalMs;
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error('Warning: Could not fetch all team members. This feature requires Workspace Owner/Admin permissions.');
|
|
94
|
+
// Continue without all users - will only show current user's entries
|
|
119
95
|
}
|
|
120
96
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
const totalDisplayMinutes = Math.round((totalHours - totalDisplayHours) * 60);
|
|
124
|
-
const totalDisplay = totalDisplayHours > 0 ?
|
|
125
|
-
`${totalDisplayHours}h ${totalDisplayMinutes}m` :
|
|
126
|
-
`${totalDisplayMinutes}m`;
|
|
127
|
-
summaryLines.push(`Total: ${totalDisplay}`);
|
|
128
|
-
// Create detailed entries
|
|
129
|
-
const detailBlocks = [];
|
|
130
|
-
data.data.forEach((entry) => {
|
|
131
|
-
const entryHours = (parseInt(entry.duration) || 0) / (1000 * 60 * 60);
|
|
132
|
-
const entryDisplayHours = Math.floor(entryHours);
|
|
133
|
-
const entryDisplayMinutes = Math.round((entryHours - entryDisplayHours) * 60);
|
|
134
|
-
const entryTimeDisplay = entryDisplayHours > 0 ?
|
|
135
|
-
`${entryDisplayHours}h ${entryDisplayMinutes}m` :
|
|
136
|
-
`${entryDisplayMinutes}m`;
|
|
137
|
-
const entryInfo = [
|
|
138
|
-
`entry_id: ${entry.id}`,
|
|
139
|
-
`user: ${entry.user?.username || 'Unknown User'}`,
|
|
140
|
-
`duration: ${entryTimeDisplay}`,
|
|
141
|
-
`start: ${timestampToIso(parseInt(entry.start))}`,
|
|
142
|
-
...(entry.description ? [`description: ${entry.description}`] : []),
|
|
143
|
-
`billable: ${entry.billable || false}`
|
|
144
|
-
];
|
|
145
|
-
// Add task info only when showing all user entries (no task filter)
|
|
146
|
-
if (!task_id && entry.task) {
|
|
147
|
-
entryInfo.splice(2, 0, `task: ${entry.task.name} (${entry.task.id})`);
|
|
148
|
-
}
|
|
149
|
-
detailBlocks.push({
|
|
150
|
-
type: "text",
|
|
151
|
-
text: entryInfo.join('\n')
|
|
152
|
-
});
|
|
97
|
+
const response = await fetch(`https://api.clickup.com/api/v2/team/${config_1.CONFIG.teamId}/time_entries?${params}`, {
|
|
98
|
+
headers: { Authorization: config_1.CONFIG.apiKey },
|
|
153
99
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
},
|
|
160
|
-
...(detailBlocks.length > 0 ? [
|
|
161
|
-
{ type: "text", text: "\nDetailed entries:" },
|
|
162
|
-
...detailBlocks
|
|
163
|
-
] : [])
|
|
164
|
-
],
|
|
165
|
-
};
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(`Error fetching time entries: ${response.status} ${response.statusText}`);
|
|
102
|
+
}
|
|
103
|
+
const data = await response.json();
|
|
104
|
+
return processTimeEntriesData(data, task_id, start_date, end_date, include_all_users);
|
|
166
105
|
}
|
|
167
106
|
catch (error) {
|
|
168
107
|
console.error('Error fetching time entries:', error);
|
|
@@ -177,6 +116,147 @@ function registerTimeToolsRead(server) {
|
|
|
177
116
|
}
|
|
178
117
|
});
|
|
179
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Process the time entries data and return formatted hierarchical output
|
|
121
|
+
*/
|
|
122
|
+
function processTimeEntriesData(data, task_id, start_date, end_date, include_all_users) {
|
|
123
|
+
if (!data.data || !Array.isArray(data.data)) {
|
|
124
|
+
const noEntriesMsg = task_id ?
|
|
125
|
+
`No time entries found for task ${task_id}.` :
|
|
126
|
+
'No time entries found.';
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: "text", text: noEntriesMsg }],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const filteredEntries = data.data;
|
|
132
|
+
// Create hierarchical structure: List → Task → User → Individual entries
|
|
133
|
+
const hierarchy = new Map();
|
|
134
|
+
let totalTimeMs = 0;
|
|
135
|
+
filteredEntries.forEach((entry) => {
|
|
136
|
+
const taskId = entry.task?.id || 'no-task';
|
|
137
|
+
// Use location names from include_location_names parameter
|
|
138
|
+
const listId = entry.task_location?.list_id || 'no-list';
|
|
139
|
+
const listName = entry.task_location?.list_name || 'No List';
|
|
140
|
+
const taskName = entry.task?.name || 'No Task';
|
|
141
|
+
const userId = entry.user?.id || 'no-user';
|
|
142
|
+
const userName = entry.user?.username || 'Unknown User';
|
|
143
|
+
// Handle running timers (negative duration)
|
|
144
|
+
let entryDurationMs = parseInt(entry.duration) || 0;
|
|
145
|
+
const isRunningTimer = entryDurationMs < 0;
|
|
146
|
+
if (isRunningTimer) {
|
|
147
|
+
// For running timers, calculate current duration from start time
|
|
148
|
+
entryDurationMs = Date.now() - parseInt(entry.start);
|
|
149
|
+
}
|
|
150
|
+
totalTimeMs += entryDurationMs;
|
|
151
|
+
// Initialize list level
|
|
152
|
+
if (!hierarchy.has(listId)) {
|
|
153
|
+
hierarchy.set(listId, {
|
|
154
|
+
name: listName,
|
|
155
|
+
id: listId,
|
|
156
|
+
totalTime: 0,
|
|
157
|
+
tasks: new Map()
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const listData = hierarchy.get(listId);
|
|
161
|
+
listData.totalTime += entryDurationMs;
|
|
162
|
+
// Initialize task level
|
|
163
|
+
if (!listData.tasks.has(taskId)) {
|
|
164
|
+
listData.tasks.set(taskId, {
|
|
165
|
+
name: taskName,
|
|
166
|
+
id: taskId,
|
|
167
|
+
totalTime: 0,
|
|
168
|
+
users: new Map()
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
const taskData = listData.tasks.get(taskId);
|
|
172
|
+
taskData.totalTime += entryDurationMs;
|
|
173
|
+
// Initialize user level
|
|
174
|
+
if (!taskData.users.has(userId)) {
|
|
175
|
+
taskData.users.set(userId, {
|
|
176
|
+
name: userName,
|
|
177
|
+
id: userId,
|
|
178
|
+
totalTime: 0,
|
|
179
|
+
entries: []
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const userData = taskData.users.get(userId);
|
|
183
|
+
userData.totalTime += entryDurationMs;
|
|
184
|
+
userData.entries.push(entry);
|
|
185
|
+
});
|
|
186
|
+
// Count total tasks across all lists
|
|
187
|
+
let totalTasks = 0;
|
|
188
|
+
for (const [listId, listData] of hierarchy.entries()) {
|
|
189
|
+
totalTasks += listData.tasks.size;
|
|
190
|
+
}
|
|
191
|
+
// Format the hierarchical output
|
|
192
|
+
const outputLines = [];
|
|
193
|
+
// Header with date range and total
|
|
194
|
+
const dateRange = start_date && end_date ?
|
|
195
|
+
` (${start_date.split('T')[0]} to ${end_date.split('T')[0]})` :
|
|
196
|
+
start_date ? ` (from ${start_date.split('T')[0]})` :
|
|
197
|
+
end_date ? ` (until ${end_date.split('T')[0]})` : '';
|
|
198
|
+
outputLines.push(`Time Entries Summary${dateRange}`);
|
|
199
|
+
outputLines.push(`Total: ${formatDuration(totalTimeMs)}`);
|
|
200
|
+
outputLines.push('');
|
|
201
|
+
// Check if result is too large (>100 tasks)
|
|
202
|
+
const TASK_LIMIT = 100;
|
|
203
|
+
const isTruncated = totalTasks > TASK_LIMIT;
|
|
204
|
+
if (isTruncated) {
|
|
205
|
+
// Show only list-level summary
|
|
206
|
+
outputLines.push(`⚠️ Large result detected (${totalTasks} tasks). Showing summary only.`);
|
|
207
|
+
outputLines.push(`💡 Use list_id, space_id, or date filters for detailed view.`);
|
|
208
|
+
outputLines.push('');
|
|
209
|
+
for (const [listId, listData] of hierarchy.entries()) {
|
|
210
|
+
const taskCount = listData.tasks.size;
|
|
211
|
+
outputLines.push(`📋 ${listData.name} (List: ${listId}) - ${formatDuration(listData.totalTime)} across ${taskCount} task${taskCount === 1 ? '' : 's'}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// Show full hierarchical display
|
|
216
|
+
for (const [listId, listData] of hierarchy.entries()) {
|
|
217
|
+
outputLines.push(`📋 ${listData.name} (List: ${listId}) - ${formatDuration(listData.totalTime)}`);
|
|
218
|
+
for (const [taskId, taskData] of listData.tasks.entries()) {
|
|
219
|
+
outputLines.push(` ├─ 🎯 ${taskData.name} (Task: ${taskId}) - ${formatDuration(taskData.totalTime)}`);
|
|
220
|
+
const userEntries = Array.from(taskData.users.entries());
|
|
221
|
+
for (let userIndex = 0; userIndex < userEntries.length; userIndex++) {
|
|
222
|
+
const [userId, userData] = userEntries[userIndex];
|
|
223
|
+
const isLastUser = userIndex === userEntries.length - 1;
|
|
224
|
+
const userPrefix = isLastUser ? ' └─' : ' ├─';
|
|
225
|
+
outputLines.push(`${userPrefix} ${userData.name}: ${formatDuration(userData.totalTime)}`);
|
|
226
|
+
// Add individual entries
|
|
227
|
+
userData.entries.forEach((entry, entryIndex) => {
|
|
228
|
+
const isLastEntry = entryIndex === userData.entries.length - 1;
|
|
229
|
+
const entryPrefix = isLastUser ?
|
|
230
|
+
(isLastEntry ? ' └─' : ' ├─') :
|
|
231
|
+
(isLastEntry ? ' │ └─' : ' │ ├─');
|
|
232
|
+
const entryStart = formatEntryTime(parseInt(entry.start));
|
|
233
|
+
// Handle running timers
|
|
234
|
+
const rawDuration = parseInt(entry.duration) || 0;
|
|
235
|
+
const isRunningTimer = rawDuration < 0;
|
|
236
|
+
let entryDuration;
|
|
237
|
+
if (isRunningTimer) {
|
|
238
|
+
const currentDuration = Date.now() - parseInt(entry.start);
|
|
239
|
+
entryDuration = `${formatDuration(currentDuration)} (running)`;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
entryDuration = formatDuration(rawDuration);
|
|
243
|
+
}
|
|
244
|
+
outputLines.push(`${entryPrefix} ${entryStart} - ${entryDuration}`);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
outputLines.push('');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
content: [
|
|
253
|
+
{
|
|
254
|
+
type: "text",
|
|
255
|
+
text: outputLines.join('\n')
|
|
256
|
+
}
|
|
257
|
+
],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
180
260
|
function registerTimeToolsWrite(server) {
|
|
181
261
|
server.tool("createTimeEntry", [
|
|
182
262
|
"Creates a time entry (books time) on a task for the current user.",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hauptsache.net/clickup-mcp",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Transform your AI assistant into a powerful ClickUp integration for both agentic coding and productivity management. Enables seamless task context sharing, intelligent search, time tracking, and complete project management workflows.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"bin": "dist/index.js",
|
|
@@ -23,9 +23,19 @@
|
|
|
23
23
|
"mcp",
|
|
24
24
|
"model-context-protocol",
|
|
25
25
|
"llm",
|
|
26
|
-
"ai"
|
|
26
|
+
"ai",
|
|
27
|
+
"ticket",
|
|
28
|
+
"time-tracking",
|
|
29
|
+
"productivity",
|
|
30
|
+
"project-management",
|
|
31
|
+
"task-management",
|
|
32
|
+
"agentic-coding",
|
|
33
|
+
"automation"
|
|
27
34
|
],
|
|
28
|
-
"author":
|
|
35
|
+
"author": {
|
|
36
|
+
"name": "Marco Pfeiffer",
|
|
37
|
+
"email": "marco@hauptsache.net"
|
|
38
|
+
},
|
|
29
39
|
"license": "ISC",
|
|
30
40
|
"dependencies": {
|
|
31
41
|
"@modelcontextprotocol/sdk": "^1.9.0",
|