@shortcut/mcp 0.15.2 → 0.17.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 +8 -3
- package/dist/index.js +5 -1564
- package/dist/server-http.js +430 -0
- package/dist/workflows-TjriXV16.js +1691 -0
- package/package.json +11 -4
|
@@ -0,0 +1,1691 @@
|
|
|
1
|
+
import { File } from "node:buffer";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
//#region src/client/cache.ts
|
|
8
|
+
/**
|
|
9
|
+
* Simple key/value cache.
|
|
10
|
+
*
|
|
11
|
+
* It only supports setting **all** values at once. You cannot add to the cache over time.
|
|
12
|
+
* It tracks staleness and is hard coded to expire after 5 minutes.
|
|
13
|
+
*/
|
|
14
|
+
var Cache = class {
|
|
15
|
+
cache;
|
|
16
|
+
age;
|
|
17
|
+
constructor() {
|
|
18
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
19
|
+
this.age = 0;
|
|
20
|
+
}
|
|
21
|
+
get(key) {
|
|
22
|
+
return this.cache.get(key) ?? null;
|
|
23
|
+
}
|
|
24
|
+
values() {
|
|
25
|
+
return Array.from(this.cache.values());
|
|
26
|
+
}
|
|
27
|
+
setMany(values) {
|
|
28
|
+
this.cache.clear();
|
|
29
|
+
for (const [key, value] of values) this.cache.set(key, value);
|
|
30
|
+
this.age = Date.now();
|
|
31
|
+
}
|
|
32
|
+
clear() {
|
|
33
|
+
this.cache.clear();
|
|
34
|
+
this.age = 0;
|
|
35
|
+
}
|
|
36
|
+
get isStale() {
|
|
37
|
+
return Date.now() - this.age > 1e3 * 60 * 5;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/client/shortcut.ts
|
|
43
|
+
/**
|
|
44
|
+
* This is a thin wrapper over the official Shortcut API client.
|
|
45
|
+
*
|
|
46
|
+
* Its main reasons for existing are:
|
|
47
|
+
* - Add a caching layer for common calls like fetching members or teams.
|
|
48
|
+
* - Unwrap and simplify some response types.
|
|
49
|
+
* - Only expose a subset of methods and a subset of the possible input parameters to those methods.
|
|
50
|
+
*/
|
|
51
|
+
var ShortcutClientWrapper = class {
|
|
52
|
+
currentUser = null;
|
|
53
|
+
userCache;
|
|
54
|
+
teamCache;
|
|
55
|
+
workflowCache;
|
|
56
|
+
customFieldCache;
|
|
57
|
+
constructor(client) {
|
|
58
|
+
this.client = client;
|
|
59
|
+
this.userCache = new Cache();
|
|
60
|
+
this.teamCache = new Cache();
|
|
61
|
+
this.workflowCache = new Cache();
|
|
62
|
+
this.customFieldCache = new Cache();
|
|
63
|
+
}
|
|
64
|
+
getNextPageToken(next) {
|
|
65
|
+
let next_page_token = null;
|
|
66
|
+
if (next) try {
|
|
67
|
+
const [, t] = /next=(.+)(?:&|$)/.exec(next) || [];
|
|
68
|
+
if (t) next_page_token = t;
|
|
69
|
+
} catch {}
|
|
70
|
+
return next_page_token;
|
|
71
|
+
}
|
|
72
|
+
async loadMembers() {
|
|
73
|
+
if (this.userCache.isStale) {
|
|
74
|
+
const response = await this.client.listMembers({});
|
|
75
|
+
const members = response?.data ?? null;
|
|
76
|
+
if (members) this.userCache.setMany(members.map((member) => [member.id, member]));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async loadTeams() {
|
|
80
|
+
if (this.teamCache.isStale) {
|
|
81
|
+
const response = await this.client.listGroups();
|
|
82
|
+
const groups = response?.data ?? null;
|
|
83
|
+
if (groups) this.teamCache.setMany(groups.map((group) => [group.id, group]));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async loadWorkflows() {
|
|
87
|
+
if (this.workflowCache.isStale) {
|
|
88
|
+
const response = await this.client.listWorkflows();
|
|
89
|
+
const workflows = response?.data ?? null;
|
|
90
|
+
if (workflows) this.workflowCache.setMany(workflows.map((workflow) => [workflow.id, workflow]));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async loadCustomFields() {
|
|
94
|
+
if (this.customFieldCache.isStale) {
|
|
95
|
+
const response = await this.client.listCustomFields();
|
|
96
|
+
const customFields = response?.data ?? null;
|
|
97
|
+
if (customFields) this.customFieldCache.setMany(customFields.map((customField) => [customField.id, customField]));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async getCurrentUser() {
|
|
101
|
+
if (this.currentUser) return this.currentUser;
|
|
102
|
+
const response = await this.client.getCurrentMemberInfo();
|
|
103
|
+
const user$1 = response?.data;
|
|
104
|
+
if (!user$1) return null;
|
|
105
|
+
this.currentUser = user$1;
|
|
106
|
+
return user$1;
|
|
107
|
+
}
|
|
108
|
+
async getUser(userId) {
|
|
109
|
+
const response = await this.client.getMember(userId, {});
|
|
110
|
+
const user$1 = response?.data;
|
|
111
|
+
if (!user$1) return null;
|
|
112
|
+
return user$1;
|
|
113
|
+
}
|
|
114
|
+
async getUserMap(userIds) {
|
|
115
|
+
await this.loadMembers();
|
|
116
|
+
return new Map(userIds.map((id) => [id, this.userCache.get(id)]).filter((user$1) => user$1[1] !== null));
|
|
117
|
+
}
|
|
118
|
+
async getUsers(userIds) {
|
|
119
|
+
await this.loadMembers();
|
|
120
|
+
return userIds.map((id) => this.userCache.get(id)).filter((user$1) => user$1 !== null);
|
|
121
|
+
}
|
|
122
|
+
async listMembers() {
|
|
123
|
+
await this.loadMembers();
|
|
124
|
+
const members = Array.from(this.userCache.values());
|
|
125
|
+
return members;
|
|
126
|
+
}
|
|
127
|
+
async getWorkflowMap(workflowIds) {
|
|
128
|
+
await this.loadWorkflows();
|
|
129
|
+
return new Map(workflowIds.map((id) => [id, this.workflowCache.get(id)]).filter((workflow) => workflow[1] !== null));
|
|
130
|
+
}
|
|
131
|
+
async getWorkflows() {
|
|
132
|
+
await this.loadWorkflows();
|
|
133
|
+
return Array.from(this.workflowCache.values());
|
|
134
|
+
}
|
|
135
|
+
async getWorkflow(workflowPublicId) {
|
|
136
|
+
const response = await this.client.getWorkflow(workflowPublicId);
|
|
137
|
+
const workflow = response?.data;
|
|
138
|
+
if (!workflow) return null;
|
|
139
|
+
return workflow;
|
|
140
|
+
}
|
|
141
|
+
async getTeams() {
|
|
142
|
+
await this.loadTeams();
|
|
143
|
+
const teams = Array.from(this.teamCache.values());
|
|
144
|
+
return teams;
|
|
145
|
+
}
|
|
146
|
+
async getTeamMap(teamIds) {
|
|
147
|
+
await this.loadTeams();
|
|
148
|
+
return new Map(teamIds.map((id) => [id, this.teamCache.get(id)]).filter((team) => team[1] !== null));
|
|
149
|
+
}
|
|
150
|
+
async getTeam(teamPublicId) {
|
|
151
|
+
const response = await this.client.getGroup(teamPublicId);
|
|
152
|
+
const group = response?.data;
|
|
153
|
+
if (!group) return null;
|
|
154
|
+
return group;
|
|
155
|
+
}
|
|
156
|
+
async createStory(params) {
|
|
157
|
+
const response = await this.client.createStory(params);
|
|
158
|
+
const story = response?.data ?? null;
|
|
159
|
+
if (!story) throw new Error(`Failed to create the story: ${response.status}`);
|
|
160
|
+
return story;
|
|
161
|
+
}
|
|
162
|
+
async updateStory(storyPublicId, params) {
|
|
163
|
+
const response = await this.client.updateStory(storyPublicId, params);
|
|
164
|
+
const story = response?.data ?? null;
|
|
165
|
+
if (!story) throw new Error(`Failed to update the story: ${response.status}`);
|
|
166
|
+
return story;
|
|
167
|
+
}
|
|
168
|
+
async getStory(storyPublicId) {
|
|
169
|
+
const response = await this.client.getStory(storyPublicId);
|
|
170
|
+
const story = response?.data ?? null;
|
|
171
|
+
if (!story) return null;
|
|
172
|
+
return story;
|
|
173
|
+
}
|
|
174
|
+
async getEpic(epicPublicId) {
|
|
175
|
+
const response = await this.client.getEpic(epicPublicId);
|
|
176
|
+
const epic = response?.data ?? null;
|
|
177
|
+
if (!epic) return null;
|
|
178
|
+
return epic;
|
|
179
|
+
}
|
|
180
|
+
async getIteration(iterationPublicId) {
|
|
181
|
+
const response = await this.client.getIteration(iterationPublicId);
|
|
182
|
+
const iteration = response?.data ?? null;
|
|
183
|
+
if (!iteration) return null;
|
|
184
|
+
return iteration;
|
|
185
|
+
}
|
|
186
|
+
async getMilestone(milestonePublicId) {
|
|
187
|
+
const response = await this.client.getMilestone(milestonePublicId);
|
|
188
|
+
const milestone = response?.data ?? null;
|
|
189
|
+
if (!milestone) return null;
|
|
190
|
+
return milestone;
|
|
191
|
+
}
|
|
192
|
+
async searchStories(query, nextToken) {
|
|
193
|
+
const response = await this.client.searchStories({
|
|
194
|
+
query,
|
|
195
|
+
page_size: 25,
|
|
196
|
+
detail: "full",
|
|
197
|
+
next: nextToken
|
|
198
|
+
});
|
|
199
|
+
const stories = response?.data?.data;
|
|
200
|
+
const total = response?.data?.total;
|
|
201
|
+
const next = response?.data?.next;
|
|
202
|
+
if (!stories) return {
|
|
203
|
+
stories: null,
|
|
204
|
+
total: null,
|
|
205
|
+
next_page_token: null
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
stories,
|
|
209
|
+
total,
|
|
210
|
+
next_page_token: this.getNextPageToken(next)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async searchIterations(query, nextToken) {
|
|
214
|
+
const response = await this.client.searchIterations({
|
|
215
|
+
query,
|
|
216
|
+
page_size: 25,
|
|
217
|
+
detail: "full",
|
|
218
|
+
next: nextToken
|
|
219
|
+
});
|
|
220
|
+
const iterations = response?.data?.data;
|
|
221
|
+
const total = response?.data?.total;
|
|
222
|
+
const next = response?.data?.next;
|
|
223
|
+
if (!iterations) return {
|
|
224
|
+
iterations: null,
|
|
225
|
+
total: null,
|
|
226
|
+
next_page_token: null
|
|
227
|
+
};
|
|
228
|
+
return {
|
|
229
|
+
iterations,
|
|
230
|
+
total,
|
|
231
|
+
next_page_token: this.getNextPageToken(next)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async getActiveIteration(teamIds) {
|
|
235
|
+
const response = await this.client.listIterations();
|
|
236
|
+
const iterations = response?.data;
|
|
237
|
+
if (!iterations) return /* @__PURE__ */ new Map();
|
|
238
|
+
const [today] = (/* @__PURE__ */ new Date()).toISOString().split("T");
|
|
239
|
+
const activeIterationByTeam = iterations.reduce((acc, iteration) => {
|
|
240
|
+
if (iteration.status !== "started") return acc;
|
|
241
|
+
const [startDate] = new Date(iteration.start_date).toISOString().split("T");
|
|
242
|
+
const [endDate] = new Date(iteration.end_date).toISOString().split("T");
|
|
243
|
+
if (!startDate || !endDate) return acc;
|
|
244
|
+
if (startDate > today || endDate < today) return acc;
|
|
245
|
+
if (!iteration.group_ids?.length) iteration.group_ids = ["none"];
|
|
246
|
+
for (const groupId of iteration.group_ids) {
|
|
247
|
+
if (groupId !== "none" && !teamIds.includes(groupId)) continue;
|
|
248
|
+
const prevIterations = acc.get(groupId);
|
|
249
|
+
if (prevIterations) acc.set(groupId, prevIterations.concat([iteration]));
|
|
250
|
+
else acc.set(groupId, [iteration]);
|
|
251
|
+
}
|
|
252
|
+
return acc;
|
|
253
|
+
}, /* @__PURE__ */ new Map());
|
|
254
|
+
return activeIterationByTeam;
|
|
255
|
+
}
|
|
256
|
+
async getUpcomingIteration(teamIds) {
|
|
257
|
+
const response = await this.client.listIterations();
|
|
258
|
+
const iterations = response?.data;
|
|
259
|
+
if (!iterations) return /* @__PURE__ */ new Map();
|
|
260
|
+
const [today] = (/* @__PURE__ */ new Date()).toISOString().split("T");
|
|
261
|
+
const upcomingIterationByTeam = iterations.reduce((acc, iteration) => {
|
|
262
|
+
if (iteration.status !== "unstarted") return acc;
|
|
263
|
+
const [startDate] = new Date(iteration.start_date).toISOString().split("T");
|
|
264
|
+
const [endDate] = new Date(iteration.end_date).toISOString().split("T");
|
|
265
|
+
if (!startDate || !endDate) return acc;
|
|
266
|
+
if (startDate < today) return acc;
|
|
267
|
+
if (!iteration.group_ids?.length) iteration.group_ids = ["none"];
|
|
268
|
+
for (const groupId of iteration.group_ids) {
|
|
269
|
+
if (groupId !== "none" && !teamIds.includes(groupId)) continue;
|
|
270
|
+
const prevIterations = acc.get(groupId);
|
|
271
|
+
if (prevIterations) acc.set(groupId, prevIterations.concat([iteration]));
|
|
272
|
+
else acc.set(groupId, [iteration]);
|
|
273
|
+
}
|
|
274
|
+
return acc;
|
|
275
|
+
}, /* @__PURE__ */ new Map());
|
|
276
|
+
return upcomingIterationByTeam;
|
|
277
|
+
}
|
|
278
|
+
async searchEpics(query, nextToken) {
|
|
279
|
+
const response = await this.client.searchEpics({
|
|
280
|
+
query,
|
|
281
|
+
page_size: 25,
|
|
282
|
+
detail: "full",
|
|
283
|
+
next: nextToken
|
|
284
|
+
});
|
|
285
|
+
const epics = response?.data?.data;
|
|
286
|
+
const total = response?.data?.total;
|
|
287
|
+
const next = response?.data?.next;
|
|
288
|
+
if (!epics) return {
|
|
289
|
+
epics: null,
|
|
290
|
+
total: null,
|
|
291
|
+
next_page_token: null
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
epics,
|
|
295
|
+
total,
|
|
296
|
+
next_page_token: this.getNextPageToken(next)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
async searchMilestones(query, nextToken) {
|
|
300
|
+
const response = await this.client.searchMilestones({
|
|
301
|
+
query,
|
|
302
|
+
page_size: 25,
|
|
303
|
+
detail: "full",
|
|
304
|
+
next: nextToken
|
|
305
|
+
});
|
|
306
|
+
const milestones = response?.data?.data;
|
|
307
|
+
const total = response?.data?.total;
|
|
308
|
+
const next = response?.data?.next;
|
|
309
|
+
if (!milestones) return {
|
|
310
|
+
milestones: null,
|
|
311
|
+
total: null,
|
|
312
|
+
next_page_token: null
|
|
313
|
+
};
|
|
314
|
+
return {
|
|
315
|
+
milestones,
|
|
316
|
+
total,
|
|
317
|
+
next_page_token: this.getNextPageToken(next)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async listIterationStories(iterationPublicId, includeDescription = false) {
|
|
321
|
+
const response = await this.client.listIterationStories(iterationPublicId, { includes_description: includeDescription });
|
|
322
|
+
const stories = response?.data;
|
|
323
|
+
if (!stories) return {
|
|
324
|
+
stories: null,
|
|
325
|
+
total: null
|
|
326
|
+
};
|
|
327
|
+
return {
|
|
328
|
+
stories,
|
|
329
|
+
total: stories.length
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
async createStoryComment(storyPublicId, params) {
|
|
333
|
+
const response = await this.client.createStoryComment(storyPublicId, params);
|
|
334
|
+
const storyComment = response?.data ?? null;
|
|
335
|
+
if (!storyComment) throw new Error(`Failed to create the comment: ${response.status}`);
|
|
336
|
+
return storyComment;
|
|
337
|
+
}
|
|
338
|
+
async createIteration(params) {
|
|
339
|
+
const response = await this.client.createIteration(params);
|
|
340
|
+
const iteration = response?.data ?? null;
|
|
341
|
+
if (!iteration) throw new Error(`Failed to create the iteration: ${response.status}`);
|
|
342
|
+
return iteration;
|
|
343
|
+
}
|
|
344
|
+
async createEpic(params) {
|
|
345
|
+
const response = await this.client.createEpic(params);
|
|
346
|
+
const epic = response?.data ?? null;
|
|
347
|
+
if (!epic) throw new Error(`Failed to create the epic: ${response.status}`);
|
|
348
|
+
return epic;
|
|
349
|
+
}
|
|
350
|
+
async addTaskToStory(storyPublicId, taskParams) {
|
|
351
|
+
const { description, ownerIds } = taskParams;
|
|
352
|
+
const params = {
|
|
353
|
+
description,
|
|
354
|
+
owner_ids: ownerIds
|
|
355
|
+
};
|
|
356
|
+
const response = await this.client.createTask(storyPublicId, params);
|
|
357
|
+
const task = response?.data ?? null;
|
|
358
|
+
if (!task) throw new Error(`Failed to create the task: ${response.status}`);
|
|
359
|
+
return task;
|
|
360
|
+
}
|
|
361
|
+
async addRelationToStory(storyPublicId, linkedStoryId, verb) {
|
|
362
|
+
const response = await this.client.createStoryLink({
|
|
363
|
+
object_id: linkedStoryId,
|
|
364
|
+
subject_id: storyPublicId,
|
|
365
|
+
verb
|
|
366
|
+
});
|
|
367
|
+
const storyLink = response?.data ?? null;
|
|
368
|
+
if (!storyLink) throw new Error(`Failed to create the story links: ${response.status}`);
|
|
369
|
+
return storyLink;
|
|
370
|
+
}
|
|
371
|
+
async getTask(storyPublicId, taskPublicId) {
|
|
372
|
+
const response = await this.client.getTask(storyPublicId, taskPublicId);
|
|
373
|
+
const task = response?.data ?? null;
|
|
374
|
+
if (!task) throw new Error(`Failed to get the task: ${response.status}`);
|
|
375
|
+
return task;
|
|
376
|
+
}
|
|
377
|
+
async updateTask(storyPublicId, taskPublicId, taskParams) {
|
|
378
|
+
const { description, ownerIds } = taskParams;
|
|
379
|
+
const params = {
|
|
380
|
+
description,
|
|
381
|
+
owner_ids: ownerIds,
|
|
382
|
+
complete: taskParams.isCompleted
|
|
383
|
+
};
|
|
384
|
+
const response = await this.client.updateTask(storyPublicId, taskPublicId, params);
|
|
385
|
+
const task = response?.data ?? null;
|
|
386
|
+
if (!task) throw new Error(`Failed to update the task: ${response.status}`);
|
|
387
|
+
return task;
|
|
388
|
+
}
|
|
389
|
+
async addExternalLinkToStory(storyPublicId, externalLink) {
|
|
390
|
+
const story = await this.getStory(storyPublicId);
|
|
391
|
+
if (!story) throw new Error(`Story ${storyPublicId} not found`);
|
|
392
|
+
const currentLinks = story.external_links || [];
|
|
393
|
+
if (currentLinks.some((link) => link.toLowerCase() === externalLink.toLowerCase())) return story;
|
|
394
|
+
const updatedLinks = [...currentLinks, externalLink];
|
|
395
|
+
return await this.updateStory(storyPublicId, { external_links: updatedLinks });
|
|
396
|
+
}
|
|
397
|
+
async removeExternalLinkFromStory(storyPublicId, externalLink) {
|
|
398
|
+
const story = await this.getStory(storyPublicId);
|
|
399
|
+
if (!story) throw new Error(`Story ${storyPublicId} not found`);
|
|
400
|
+
const currentLinks = story.external_links || [];
|
|
401
|
+
const updatedLinks = currentLinks.filter((link) => link.toLowerCase() !== externalLink.toLowerCase());
|
|
402
|
+
return await this.updateStory(storyPublicId, { external_links: updatedLinks });
|
|
403
|
+
}
|
|
404
|
+
async getStoriesByExternalLink(externalLink) {
|
|
405
|
+
const response = await this.client.getExternalLinkStories({ external_link: externalLink.toLowerCase() });
|
|
406
|
+
const stories = response?.data;
|
|
407
|
+
if (!stories) return {
|
|
408
|
+
stories: null,
|
|
409
|
+
total: null
|
|
410
|
+
};
|
|
411
|
+
return {
|
|
412
|
+
stories,
|
|
413
|
+
total: stories.length
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
async setStoryExternalLinks(storyPublicId, externalLinks) {
|
|
417
|
+
return await this.updateStory(storyPublicId, { external_links: externalLinks });
|
|
418
|
+
}
|
|
419
|
+
async createDoc(params) {
|
|
420
|
+
const response = await this.client.createDoc(params);
|
|
421
|
+
const doc = response?.data ?? null;
|
|
422
|
+
if (!doc) throw new Error(`Failed to create the document: ${response.status}`);
|
|
423
|
+
return doc;
|
|
424
|
+
}
|
|
425
|
+
async listDocs() {
|
|
426
|
+
const response = await this.client.listDocs();
|
|
427
|
+
if (response.status === 403) throw new Error("Docs feature disabled for this workspace.");
|
|
428
|
+
return response?.data ?? null;
|
|
429
|
+
}
|
|
430
|
+
async searchDocuments({ title, archived, createdByCurrentUser, followedByCurrentUser, pageSize = 25, nextPageToken }) {
|
|
431
|
+
const response = await this.client.searchDocuments({
|
|
432
|
+
title,
|
|
433
|
+
archived,
|
|
434
|
+
created_by_me: createdByCurrentUser,
|
|
435
|
+
followed_by_me: followedByCurrentUser,
|
|
436
|
+
page_size: pageSize,
|
|
437
|
+
next: nextPageToken
|
|
438
|
+
});
|
|
439
|
+
if (response.status === 403) throw new Error("Docs feature disabled for this workspace.");
|
|
440
|
+
const documents = response?.data?.data;
|
|
441
|
+
const total = response?.data?.total;
|
|
442
|
+
const next = response?.data?.next;
|
|
443
|
+
if (!documents) return {
|
|
444
|
+
documents: null,
|
|
445
|
+
total: null,
|
|
446
|
+
next_page_token: null
|
|
447
|
+
};
|
|
448
|
+
return {
|
|
449
|
+
documents,
|
|
450
|
+
total,
|
|
451
|
+
next_page_token: this.getNextPageToken(next)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
async getDocById(docId) {
|
|
455
|
+
const response = await this.client.getDoc(docId);
|
|
456
|
+
if (response.status === 403) throw new Error("Docs feature disabled for this workspace.");
|
|
457
|
+
return response?.data ?? null;
|
|
458
|
+
}
|
|
459
|
+
async uploadFile(storyId, filePath) {
|
|
460
|
+
const fileContent = readFileSync(filePath);
|
|
461
|
+
const fileName = basename(filePath);
|
|
462
|
+
const file = new File([fileContent], fileName);
|
|
463
|
+
const response = await this.client.uploadFiles({
|
|
464
|
+
story_id: storyId,
|
|
465
|
+
file0: file
|
|
466
|
+
});
|
|
467
|
+
const uploadedFile = response?.data ?? null;
|
|
468
|
+
if (!uploadedFile?.length) throw new Error(`Failed to upload the file: ${response.status}`);
|
|
469
|
+
return uploadedFile[0];
|
|
470
|
+
}
|
|
471
|
+
async getCustomFieldMap(customFieldIds) {
|
|
472
|
+
await this.loadCustomFields();
|
|
473
|
+
return new Map(customFieldIds.map((id) => [id, this.customFieldCache.get(id)]).filter((customField) => customField[1] !== null));
|
|
474
|
+
}
|
|
475
|
+
async getCustomFields() {
|
|
476
|
+
await this.loadCustomFields();
|
|
477
|
+
return Array.from(this.customFieldCache.values());
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region package.json
|
|
483
|
+
var name = "@shortcut/mcp";
|
|
484
|
+
var version = "0.17.0";
|
|
485
|
+
|
|
486
|
+
//#endregion
|
|
487
|
+
//#region src/mcp/CustomMcpServer.ts
|
|
488
|
+
var CustomMcpServer = class extends McpServer {
|
|
489
|
+
readonly;
|
|
490
|
+
tools;
|
|
491
|
+
constructor({ readonly, tools }) {
|
|
492
|
+
super({
|
|
493
|
+
name,
|
|
494
|
+
version
|
|
495
|
+
});
|
|
496
|
+
this.readonly = readonly;
|
|
497
|
+
this.tools = new Set(tools || []);
|
|
498
|
+
}
|
|
499
|
+
shouldAddTool(name$1) {
|
|
500
|
+
if (!this.tools.size) return true;
|
|
501
|
+
const [entityType] = name$1.split("-");
|
|
502
|
+
if (this.tools.has(entityType) || this.tools.has(name$1)) return true;
|
|
503
|
+
return false;
|
|
504
|
+
}
|
|
505
|
+
addToolWithWriteAccess(...args) {
|
|
506
|
+
if (this.readonly) return null;
|
|
507
|
+
if (!this.shouldAddTool(args[0])) return null;
|
|
508
|
+
return super.tool(...args);
|
|
509
|
+
}
|
|
510
|
+
addToolWithReadAccess(...args) {
|
|
511
|
+
if (!this.shouldAddTool(args[0])) return null;
|
|
512
|
+
return super.tool(...args);
|
|
513
|
+
}
|
|
514
|
+
tool() {
|
|
515
|
+
throw new Error("Call addToolWithReadAccess or addToolWithWriteAccess instead.");
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/tools/base.ts
|
|
521
|
+
/**
|
|
522
|
+
* Base class for all tools.
|
|
523
|
+
*/
|
|
524
|
+
var BaseTools = class {
|
|
525
|
+
constructor(client) {
|
|
526
|
+
this.client = client;
|
|
527
|
+
}
|
|
528
|
+
renameEntityProps(entity) {
|
|
529
|
+
if (!entity || typeof entity !== "object") return entity;
|
|
530
|
+
const renames = [
|
|
531
|
+
["team_id", null],
|
|
532
|
+
["entity_type", null],
|
|
533
|
+
["group_id", "team_id"],
|
|
534
|
+
["group_ids", "team_ids"],
|
|
535
|
+
["milestone_id", "objective_id"],
|
|
536
|
+
["milestone_ids", "objective_ids"]
|
|
537
|
+
];
|
|
538
|
+
for (const [from, to] of renames) if (from in entity) {
|
|
539
|
+
const value = entity[from];
|
|
540
|
+
delete entity[from];
|
|
541
|
+
if (to) entity = {
|
|
542
|
+
...entity,
|
|
543
|
+
[to]: value
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return entity;
|
|
547
|
+
}
|
|
548
|
+
mergeRelatedEntities(relatedEntities) {
|
|
549
|
+
return relatedEntities.reduce((acc, obj) => {
|
|
550
|
+
if (!obj) return acc;
|
|
551
|
+
for (const [key, value] of Object.entries(obj)) acc[key] = {
|
|
552
|
+
...acc[key] || {},
|
|
553
|
+
...value
|
|
554
|
+
};
|
|
555
|
+
return acc;
|
|
556
|
+
}, {});
|
|
557
|
+
}
|
|
558
|
+
getSimplifiedMember(entity) {
|
|
559
|
+
if (!entity) return null;
|
|
560
|
+
const { id, disabled, role, profile: { is_owner, name: name$1, email_address, mention_name } } = entity;
|
|
561
|
+
return {
|
|
562
|
+
id,
|
|
563
|
+
name: name$1,
|
|
564
|
+
email_address,
|
|
565
|
+
mention_name,
|
|
566
|
+
role,
|
|
567
|
+
disabled,
|
|
568
|
+
is_owner
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
getSimplifiedStory(entity, kind) {
|
|
572
|
+
if (!entity) return null;
|
|
573
|
+
const { id, name: name$1, app_url, archived, group_id, epic_id, iteration_id, workflow_id, workflow_state_id, owner_ids, requested_by_id, estimate, labels, comments, description, external_links, story_links, pull_requests, formatted_vcs_branch_name, branches, parent_story_id, sub_task_story_ids, tasks, custom_fields, blocked, blocker } = entity;
|
|
574
|
+
const additionalFields = {};
|
|
575
|
+
if (kind === "simple") {
|
|
576
|
+
additionalFields.description = description;
|
|
577
|
+
additionalFields.estimate = estimate ?? null;
|
|
578
|
+
additionalFields.comments = (comments || []).filter((c) => !c.deleted).map(({ id: id$1, author_id, text }) => ({
|
|
579
|
+
id: id$1,
|
|
580
|
+
author_id: author_id ?? null,
|
|
581
|
+
text: text ?? ""
|
|
582
|
+
}));
|
|
583
|
+
additionalFields.labels = (labels || []).map((l) => l.name);
|
|
584
|
+
additionalFields.external_links = external_links || [];
|
|
585
|
+
additionalFields.suggested_branch_name = formatted_vcs_branch_name ?? null;
|
|
586
|
+
additionalFields.pull_requests = (pull_requests || []).map((pr) => pr.url);
|
|
587
|
+
additionalFields.branches = (branches || []).map((b) => b.name);
|
|
588
|
+
additionalFields.related_stories = (story_links || []).map((sl) => sl.type === "object" ? sl.subject_id : sl.object_id);
|
|
589
|
+
additionalFields.tasks = (tasks || []).map((t) => ({
|
|
590
|
+
id: t.id,
|
|
591
|
+
description: t.description,
|
|
592
|
+
complete: t.complete
|
|
593
|
+
}));
|
|
594
|
+
additionalFields.custom_fields = (custom_fields || []).map((field) => field.value_id);
|
|
595
|
+
additionalFields.blocked = blocked;
|
|
596
|
+
additionalFields.blocker = blocker;
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
id,
|
|
600
|
+
name: name$1,
|
|
601
|
+
app_url,
|
|
602
|
+
archived,
|
|
603
|
+
team_id: group_id || null,
|
|
604
|
+
epic_id: epic_id || null,
|
|
605
|
+
iteration_id: iteration_id || null,
|
|
606
|
+
workflow_id,
|
|
607
|
+
workflow_state_id,
|
|
608
|
+
owner_ids,
|
|
609
|
+
requested_by_id,
|
|
610
|
+
parent_story_id: parent_story_id ?? null,
|
|
611
|
+
sub_task_ids: sub_task_story_ids ?? [],
|
|
612
|
+
...additionalFields
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
getSimplifiedWorkflow(entity) {
|
|
616
|
+
if (!entity) return null;
|
|
617
|
+
const { id, name: name$1, states } = entity;
|
|
618
|
+
return {
|
|
619
|
+
id,
|
|
620
|
+
name: name$1,
|
|
621
|
+
states: states.map((state) => ({
|
|
622
|
+
id: state.id,
|
|
623
|
+
name: state.name,
|
|
624
|
+
type: state.type
|
|
625
|
+
}))
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
getSimplifiedTeam(entity) {
|
|
629
|
+
if (!entity) return null;
|
|
630
|
+
const { archived, id, name: name$1, mention_name, member_ids, workflow_ids, default_workflow_id } = entity;
|
|
631
|
+
return {
|
|
632
|
+
id,
|
|
633
|
+
name: name$1,
|
|
634
|
+
archived,
|
|
635
|
+
mention_name,
|
|
636
|
+
member_ids,
|
|
637
|
+
workflow_ids,
|
|
638
|
+
default_workflow_id: default_workflow_id ?? null
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
getSimplifiedObjective(entity) {
|
|
642
|
+
if (!entity) return null;
|
|
643
|
+
const { app_url, id, name: name$1, archived, state, categories } = entity;
|
|
644
|
+
return {
|
|
645
|
+
app_url,
|
|
646
|
+
id,
|
|
647
|
+
name: name$1,
|
|
648
|
+
archived,
|
|
649
|
+
state,
|
|
650
|
+
categories: categories.map((cat) => cat.name)
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
getSimplifiedEpic(entity, kind) {
|
|
654
|
+
if (!entity) return null;
|
|
655
|
+
const { id, name: name$1, app_url, archived, group_id, state, milestone_id, comments, description, deadline, owner_ids } = entity;
|
|
656
|
+
const additionalFields = {};
|
|
657
|
+
if (kind === "simple") {
|
|
658
|
+
additionalFields.comments = (comments || []).filter((comment) => !comment.deleted).map(({ id: id$1, author_id, text }) => ({
|
|
659
|
+
id: id$1,
|
|
660
|
+
author_id,
|
|
661
|
+
text
|
|
662
|
+
}));
|
|
663
|
+
additionalFields.description = description;
|
|
664
|
+
additionalFields.deadline = deadline ?? null;
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
id,
|
|
668
|
+
name: name$1,
|
|
669
|
+
app_url,
|
|
670
|
+
archived,
|
|
671
|
+
state,
|
|
672
|
+
team_id: group_id || null,
|
|
673
|
+
objective_id: milestone_id || null,
|
|
674
|
+
owner_ids,
|
|
675
|
+
...additionalFields
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
getSimplifiedIteration(entity) {
|
|
679
|
+
if (!entity) return null;
|
|
680
|
+
const { id, name: name$1, app_url, group_ids, status, start_date, end_date } = entity;
|
|
681
|
+
return {
|
|
682
|
+
id,
|
|
683
|
+
name: name$1,
|
|
684
|
+
app_url,
|
|
685
|
+
team_ids: group_ids,
|
|
686
|
+
status,
|
|
687
|
+
start_date,
|
|
688
|
+
end_date
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
async getRelatedEntitiesForTeam(entity) {
|
|
692
|
+
if (!entity) return {
|
|
693
|
+
users: {},
|
|
694
|
+
workflows: {}
|
|
695
|
+
};
|
|
696
|
+
const { member_ids, workflow_ids } = entity;
|
|
697
|
+
const [users, workflows] = await Promise.all([this.client.getUserMap(member_ids), this.client.getWorkflowMap(workflow_ids)]);
|
|
698
|
+
return {
|
|
699
|
+
users: Object.fromEntries(member_ids.map((id) => this.getSimplifiedMember(users.get(id))).filter((member) => member !== null).map((member) => [member.id, member])),
|
|
700
|
+
workflows: Object.fromEntries(workflow_ids.map((id) => this.getSimplifiedWorkflow(workflows.get(id))).filter((workflow) => workflow !== null).map((workflow) => [workflow.id, workflow]))
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
async getRelatedEntitiesForIteration(entity) {
|
|
704
|
+
if (!entity) return {
|
|
705
|
+
teams: {},
|
|
706
|
+
users: {},
|
|
707
|
+
workflows: {}
|
|
708
|
+
};
|
|
709
|
+
const { group_ids } = entity;
|
|
710
|
+
const teams = await this.client.getTeamMap(group_ids || []);
|
|
711
|
+
const relatedEntitiesForTeams = await Promise.all(Array.from(teams.values()).map((team) => this.getRelatedEntitiesForTeam(team)));
|
|
712
|
+
const { users, workflows } = this.mergeRelatedEntities(relatedEntitiesForTeams);
|
|
713
|
+
return {
|
|
714
|
+
teams: Object.fromEntries([...teams.entries()].map(([id, team]) => [id, this.getSimplifiedTeam(team)]).filter(([_, team]) => !!team)),
|
|
715
|
+
users,
|
|
716
|
+
workflows
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
async getRelatedEntitiesForEpic(entity) {
|
|
720
|
+
if (!entity) return {
|
|
721
|
+
users: {},
|
|
722
|
+
workflows: {},
|
|
723
|
+
teams: {},
|
|
724
|
+
objectives: {}
|
|
725
|
+
};
|
|
726
|
+
const { group_id, owner_ids, milestone_id, requested_by_id, follower_ids, comments } = entity;
|
|
727
|
+
const [usersForEpicMap, teams, fullMilestone] = await Promise.all([
|
|
728
|
+
this.client.getUserMap([...new Set([
|
|
729
|
+
...owner_ids || [],
|
|
730
|
+
requested_by_id,
|
|
731
|
+
...follower_ids || [],
|
|
732
|
+
...(comments || []).map((comment) => comment.author_id)
|
|
733
|
+
].filter(Boolean))]),
|
|
734
|
+
this.client.getTeamMap(group_id ? [group_id] : []),
|
|
735
|
+
milestone_id ? this.client.getMilestone(milestone_id) : null
|
|
736
|
+
]);
|
|
737
|
+
const usersForEpic = Object.fromEntries([...usersForEpicMap.entries()].filter(([_, user$1]) => !!user$1).map(([id, user$1]) => [id, this.getSimplifiedMember(user$1)]));
|
|
738
|
+
const team = this.getSimplifiedTeam(teams.get(group_id || ""));
|
|
739
|
+
const { users, workflows } = await this.getRelatedEntitiesForTeam(teams.get(group_id || ""));
|
|
740
|
+
const milestone = this.getSimplifiedObjective(fullMilestone);
|
|
741
|
+
return {
|
|
742
|
+
users: this.mergeRelatedEntities([usersForEpic, users]),
|
|
743
|
+
teams: team ? { [team.id]: team } : {},
|
|
744
|
+
objectives: milestone ? { [milestone.id]: milestone } : {},
|
|
745
|
+
workflows
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
async getRelatedEntitiesForStory(entity, kind) {
|
|
749
|
+
const { group_id, iteration_id, epic_id, owner_ids, requested_by_id, follower_ids, workflow_id, comments, custom_fields } = entity;
|
|
750
|
+
const [fullUsersForStory, teamsForStory, workflowsForStory, iteration, epic] = await Promise.all([
|
|
751
|
+
this.client.getUserMap([...new Set([
|
|
752
|
+
...owner_ids || [],
|
|
753
|
+
requested_by_id,
|
|
754
|
+
...follower_ids || [],
|
|
755
|
+
...(comments || []).map((comment) => comment.author_id || "")
|
|
756
|
+
].filter(Boolean))]),
|
|
757
|
+
this.client.getTeamMap(group_id ? [group_id] : []),
|
|
758
|
+
this.client.getWorkflowMap(workflow_id ? [workflow_id] : []),
|
|
759
|
+
iteration_id ? this.client.getIteration(iteration_id) : null,
|
|
760
|
+
epic_id ? this.client.getEpic(epic_id) : null
|
|
761
|
+
]);
|
|
762
|
+
const usersForStory = Object.fromEntries([...fullUsersForStory.entries()].filter(([_, user$1]) => !!user$1).map(([id, user$1]) => [id, this.getSimplifiedMember(user$1)]));
|
|
763
|
+
const simplifiedIteration = this.getSimplifiedIteration(iteration);
|
|
764
|
+
const simplifiedEpic = this.getSimplifiedEpic(epic, kind);
|
|
765
|
+
const teamForStory = teamsForStory.get(group_id || "");
|
|
766
|
+
const workflowForStory = this.getSimplifiedWorkflow(workflowsForStory.get(workflow_id));
|
|
767
|
+
const { users: usersForTeam, workflows: workflowsForTeam } = await this.getRelatedEntitiesForTeam(teamForStory);
|
|
768
|
+
const { users: usersForIteration, workflows: workflowsForIteration, teams: teamsForIteration } = await this.getRelatedEntitiesForIteration(iteration);
|
|
769
|
+
const { users: usersForEpic, workflows: workflowsForEpic, teams: teamsForEpic, objectives } = await this.getRelatedEntitiesForEpic(epic);
|
|
770
|
+
const users = this.mergeRelatedEntities([
|
|
771
|
+
usersForTeam,
|
|
772
|
+
usersForStory,
|
|
773
|
+
usersForIteration,
|
|
774
|
+
usersForEpic
|
|
775
|
+
]);
|
|
776
|
+
const workflows = this.mergeRelatedEntities([
|
|
777
|
+
workflowsForTeam,
|
|
778
|
+
workflowsForIteration,
|
|
779
|
+
workflowsForEpic,
|
|
780
|
+
workflowForStory ? { [workflowForStory.id]: workflowForStory } : {}
|
|
781
|
+
]);
|
|
782
|
+
const simplifiedStoryTeam = this.getSimplifiedTeam(teamForStory);
|
|
783
|
+
const teams = this.mergeRelatedEntities([
|
|
784
|
+
teamsForIteration,
|
|
785
|
+
teamsForEpic,
|
|
786
|
+
simplifiedStoryTeam ? { [simplifiedStoryTeam.id]: simplifiedStoryTeam } : {}
|
|
787
|
+
]);
|
|
788
|
+
const epics = simplifiedEpic ? { [simplifiedEpic.id]: simplifiedEpic } : {};
|
|
789
|
+
const iterations = simplifiedIteration ? { [simplifiedIteration.id]: simplifiedIteration } : {};
|
|
790
|
+
const relatedCustomFields = {};
|
|
791
|
+
if (custom_fields?.length) {
|
|
792
|
+
const customFieldMap = await this.client.getCustomFieldMap(custom_fields.map((cf) => cf.field_id));
|
|
793
|
+
custom_fields.forEach((cf) => {
|
|
794
|
+
const field = customFieldMap.get(cf.field_id);
|
|
795
|
+
if (!field) return;
|
|
796
|
+
const value = field.values?.find((v) => v.id === cf.value_id);
|
|
797
|
+
if (!value) return;
|
|
798
|
+
relatedCustomFields[cf.value_id] = {
|
|
799
|
+
field_id: cf.field_id,
|
|
800
|
+
value_id: cf.value_id,
|
|
801
|
+
field_name: field.name,
|
|
802
|
+
value_name: value.value
|
|
803
|
+
};
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
users,
|
|
808
|
+
epics,
|
|
809
|
+
iterations,
|
|
810
|
+
workflows,
|
|
811
|
+
teams,
|
|
812
|
+
objectives,
|
|
813
|
+
custom_fields: relatedCustomFields
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
async getRelatedEntities(entity, kind) {
|
|
817
|
+
if (entity.entity_type === "group") return this.getRelatedEntitiesForTeam(entity);
|
|
818
|
+
if (entity.entity_type === "iteration") return this.getRelatedEntitiesForIteration(entity);
|
|
819
|
+
if (entity.entity_type === "epic") return this.getRelatedEntitiesForEpic(entity);
|
|
820
|
+
if (entity.entity_type === "story") return this.getRelatedEntitiesForStory(entity, kind);
|
|
821
|
+
return {};
|
|
822
|
+
}
|
|
823
|
+
getSimplifiedEntity(entity, kind) {
|
|
824
|
+
if (entity.entity_type === "group") return this.getSimplifiedTeam(entity);
|
|
825
|
+
if (entity.entity_type === "iteration") return this.getSimplifiedIteration(entity);
|
|
826
|
+
if (entity.entity_type === "epic") return this.getSimplifiedEpic(entity, kind);
|
|
827
|
+
if (entity.entity_type === "story") return this.getSimplifiedStory(entity, kind);
|
|
828
|
+
if (entity.entity_type === "milestone") return this.getSimplifiedObjective(entity);
|
|
829
|
+
if (entity.entity_type === "workflow") return this.getSimplifiedWorkflow(entity);
|
|
830
|
+
return entity;
|
|
831
|
+
}
|
|
832
|
+
async entityWithRelatedEntities(entity, entityType = "entity", full = false) {
|
|
833
|
+
const finalEntity = full ? entity : this.getSimplifiedEntity(entity, "simple");
|
|
834
|
+
const relatedEntities = await this.getRelatedEntities(entity, full ? "full" : "simple");
|
|
835
|
+
return {
|
|
836
|
+
[entityType]: this.renameEntityProps(finalEntity),
|
|
837
|
+
relatedEntities
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
async entitiesWithRelatedEntities(entities, entityType = "entities") {
|
|
841
|
+
const relatedEntities = await Promise.all(entities.map((entity) => this.getRelatedEntities(entity, "list")));
|
|
842
|
+
return {
|
|
843
|
+
[entityType]: entities.map((entity) => this.getSimplifiedEntity(entity, "list")),
|
|
844
|
+
relatedEntities: this.mergeRelatedEntities(relatedEntities)
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
toResult(message, data, paginationToken) {
|
|
848
|
+
return { content: [{
|
|
849
|
+
type: "text",
|
|
850
|
+
text: `${message}${data !== void 0 ? `\n\n<json>\n${JSON.stringify(data, null, 2)}\n</json>${paginationToken ? `\n\n<next-page-token>${paginationToken}</next-page-token>` : ""}` : ""}`
|
|
851
|
+
}] };
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/tools/documents.ts
|
|
857
|
+
var DocumentTools = class DocumentTools extends BaseTools {
|
|
858
|
+
static create(client, server) {
|
|
859
|
+
const tools = new DocumentTools(client);
|
|
860
|
+
server.addToolWithWriteAccess("documents-create", "Create a new document in Shortcut with a title and content. Returns the document's id, title, and app_url. Note: Use HTML markup for the content (e.g., <p>, <h1>, <ul>, <strong>) rather than Markdown.", {
|
|
861
|
+
title: z.string().max(256).describe("The title for the new document (max 256 characters)"),
|
|
862
|
+
content: z.string().describe("The content for the new document in HTML format (e.g., <p>Hello</p>, <h1>Title</h1>, <ul><li>Item</li></ul>)")
|
|
863
|
+
}, async ({ title, content }) => await tools.createDocument(title, content));
|
|
864
|
+
server.addToolWithReadAccess("documents-list", "List all documents in Shortcut.", async () => await tools.listDocuments());
|
|
865
|
+
server.addToolWithReadAccess("documents-search", "Find documents.", {
|
|
866
|
+
nextPageToken: z.string().optional().describe("If a next_page_token was returned from the search result, pass it in to get the next page of results. Should be combined with the original search parameters."),
|
|
867
|
+
title: z.string().describe("Find documents matching the specified name"),
|
|
868
|
+
archived: z.boolean().optional().describe("Find only documents matching the specified archived status"),
|
|
869
|
+
createdByCurrentUser: z.boolean().optional().describe("Find only documents created by current user"),
|
|
870
|
+
followedByCurrentUser: z.boolean().optional().describe("Find only documents followed by current user")
|
|
871
|
+
}, async ({ nextPageToken, title, archived, createdByCurrentUser, followedByCurrentUser }) => await tools.searchDocuments({
|
|
872
|
+
title,
|
|
873
|
+
archived,
|
|
874
|
+
createdByCurrentUser,
|
|
875
|
+
followedByCurrentUser
|
|
876
|
+
}, nextPageToken));
|
|
877
|
+
server.addToolWithReadAccess("documents-get-by-id", "Get a document as markdown by its ID", { docId: z.string().describe("The ID of the document to retrieve") }, async ({ docId }) => await tools.getDocumentById(docId));
|
|
878
|
+
return tools;
|
|
879
|
+
}
|
|
880
|
+
async createDocument(title, content) {
|
|
881
|
+
try {
|
|
882
|
+
const doc = await this.client.createDoc({
|
|
883
|
+
title,
|
|
884
|
+
content
|
|
885
|
+
});
|
|
886
|
+
return this.toResult("Document created successfully", {
|
|
887
|
+
id: doc.id,
|
|
888
|
+
title: doc.title,
|
|
889
|
+
app_url: doc.app_url
|
|
890
|
+
});
|
|
891
|
+
} catch (error) {
|
|
892
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
893
|
+
return this.toResult(`Failed to create document: ${errorMessage}`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
async listDocuments() {
|
|
897
|
+
try {
|
|
898
|
+
const docs = await this.client.listDocs();
|
|
899
|
+
if (!docs?.length) return this.toResult("No documents were found.");
|
|
900
|
+
return this.toResult(`Found ${docs.length} documents.`, docs);
|
|
901
|
+
} catch (error) {
|
|
902
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
903
|
+
return this.toResult(`Failed to list documents: ${errorMessage}`);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async searchDocuments(params, nextPageToken) {
|
|
907
|
+
try {
|
|
908
|
+
const { documents, total, next_page_token } = await this.client.searchDocuments({
|
|
909
|
+
...params,
|
|
910
|
+
nextPageToken
|
|
911
|
+
});
|
|
912
|
+
if (!documents) throw new Error(`Failed to search for document matching your query.`);
|
|
913
|
+
if (!documents.length) return this.toResult(`Result: No documents found.`);
|
|
914
|
+
return this.toResult(`Result (${documents.length} shown of ${total} total documents found):`, documents, next_page_token);
|
|
915
|
+
} catch (error) {
|
|
916
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
917
|
+
return this.toResult(`Failed to search documents: ${errorMessage}`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
async getDocumentById(docId) {
|
|
921
|
+
try {
|
|
922
|
+
const doc = await this.client.getDocById(docId);
|
|
923
|
+
if (!doc) return this.toResult(`Document with ID ${docId} not found.`);
|
|
924
|
+
return this.toResult(`Document with ID ${docId}`, doc);
|
|
925
|
+
} catch (error) {
|
|
926
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
927
|
+
return this.toResult(`Failed to get document: ${errorMessage}`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
//#endregion
|
|
933
|
+
//#region src/tools/utils/search.ts
|
|
934
|
+
const keyRenames = { name: "title" };
|
|
935
|
+
const mapKeyName = (key) => {
|
|
936
|
+
const lowercaseKey = key.toLowerCase();
|
|
937
|
+
return keyRenames[lowercaseKey] || lowercaseKey;
|
|
938
|
+
};
|
|
939
|
+
const getKey = (prop) => {
|
|
940
|
+
if (prop.startsWith("is")) return `is:${mapKeyName(prop.slice(2))}`;
|
|
941
|
+
if (prop.startsWith("has")) return `has:${mapKeyName(prop.slice(3))}`;
|
|
942
|
+
return mapKeyName(prop);
|
|
943
|
+
};
|
|
944
|
+
const buildSearchQuery = async (params, currentUser) => {
|
|
945
|
+
const query = Object.entries(params).map(([key, value]) => {
|
|
946
|
+
const q = getKey(key);
|
|
947
|
+
if (key === "owner" || key === "requester") {
|
|
948
|
+
if (value === "me") return `${q}:${currentUser?.mention_name || value}`;
|
|
949
|
+
return `${q}:${String(value || "").replace(/^@/, "")}`;
|
|
950
|
+
}
|
|
951
|
+
if (typeof value === "boolean") return value ? q : `!${q}`;
|
|
952
|
+
if (typeof value === "number") return `${q}:${value}`;
|
|
953
|
+
if (typeof value === "string" && value.includes(" ")) return `${q}:"${value}"`;
|
|
954
|
+
return `${q}:${value}`;
|
|
955
|
+
}).join(" ");
|
|
956
|
+
return query;
|
|
957
|
+
};
|
|
958
|
+
|
|
959
|
+
//#endregion
|
|
960
|
+
//#region src/tools/utils/validation.ts
|
|
961
|
+
const dateformat = "\\d{4}-\\d{2}-\\d{2}";
|
|
962
|
+
const range = ({ f = "\\*", t = "\\*" }) => `${f}\\.\\.${t}`;
|
|
963
|
+
const variations = [
|
|
964
|
+
"today",
|
|
965
|
+
"tomorrow",
|
|
966
|
+
"yesterday",
|
|
967
|
+
dateformat,
|
|
968
|
+
range({ f: "today" }),
|
|
969
|
+
range({ t: "today" }),
|
|
970
|
+
range({ f: "yesterday" }),
|
|
971
|
+
range({ t: "yesterday" }),
|
|
972
|
+
range({ f: "tomorrow" }),
|
|
973
|
+
range({ t: "tomorrow" }),
|
|
974
|
+
range({ f: dateformat }),
|
|
975
|
+
range({ t: dateformat }),
|
|
976
|
+
range({
|
|
977
|
+
f: dateformat,
|
|
978
|
+
t: dateformat
|
|
979
|
+
})
|
|
980
|
+
];
|
|
981
|
+
const DATE_REGEXP = /* @__PURE__ */ new RegExp(`^(${variations.join("|")})$`);
|
|
982
|
+
const date = () => z.string().regex(DATE_REGEXP).optional().describe("The date in \"YYYY-MM-DD\" format, or one of the keywords: \"yesterday\", \"today\", \"tomorrow\", or a date range in the format \"YYYY-MM-DD..YYYY-MM-DD\". The date range can also be open ended by using \"*\" for one of the bounds. Examples: \"2023-01-01\", \"today\", \"2023-01-01..*\" (from Jan 1, 2023 to any future date), \"*.2023-01-31\" (any date up to Jan 31, 2023), \"today..*\" (from today onwards), \"*.yesterday\" (any date up to yesterday). The keywords cannot be used to calculate relative dates (e.g. the following are not valid: \"today-1\" or \"tomorrow+1\").");
|
|
983
|
+
const is = (field) => z.boolean().optional().describe(`Find only entities that are ${field} when true, or only entities that are not ${field} when false.`);
|
|
984
|
+
const has = (field) => z.boolean().optional().describe(`Find only entities that have ${field} when true, or only entities that do not have ${field} when false. Example: hasOwner: true will find stories with an owner, hasOwner: false will find stories without an owner.`);
|
|
985
|
+
const user = (field) => z.string().optional().describe(`Find entities where the ${field} match the specified user. This must either be the user\'s mention name or the keyword "me" for the current user.`);
|
|
986
|
+
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/tools/epics.ts
|
|
989
|
+
var EpicTools = class EpicTools extends BaseTools {
|
|
990
|
+
static create(client, server) {
|
|
991
|
+
const tools = new EpicTools(client);
|
|
992
|
+
server.addToolWithReadAccess("epics-get-by-id", "Get a Shortcut epic by public ID", {
|
|
993
|
+
epicPublicId: z.number().positive().describe("The public ID of the epic to get"),
|
|
994
|
+
full: z.boolean().optional().default(false).describe("True to return all epic fields from the API. False to return a slim version that excludes uncommon fields")
|
|
995
|
+
}, async ({ epicPublicId, full }) => await tools.getEpic(epicPublicId, full));
|
|
996
|
+
server.addToolWithReadAccess("epics-search", "Find Shortcut epics.", {
|
|
997
|
+
nextPageToken: z.string().optional().describe("If a next_page_token was returned from the search result, pass it in to get the next page of results. Should be combined with the original search parameters."),
|
|
998
|
+
id: z.number().optional().describe("Find only epics with the specified public ID"),
|
|
999
|
+
name: z.string().optional().describe("Find only epics matching the specified name"),
|
|
1000
|
+
description: z.string().optional().describe("Find only epics matching the specified description"),
|
|
1001
|
+
state: z.enum([
|
|
1002
|
+
"unstarted",
|
|
1003
|
+
"started",
|
|
1004
|
+
"done"
|
|
1005
|
+
]).optional().describe("Find only epics matching the specified state"),
|
|
1006
|
+
objective: z.number().optional().describe("Find only epics matching the specified objective"),
|
|
1007
|
+
owner: user("owner"),
|
|
1008
|
+
requester: user("requester"),
|
|
1009
|
+
team: z.string().optional().describe("Find only epics matching the specified team. Should be a team's mention name."),
|
|
1010
|
+
comment: z.string().optional().describe("Find only epics matching the specified comment"),
|
|
1011
|
+
isUnstarted: is("unstarted"),
|
|
1012
|
+
isStarted: is("started"),
|
|
1013
|
+
isDone: is("completed"),
|
|
1014
|
+
isArchived: is("archived").default(false),
|
|
1015
|
+
isOverdue: is("overdue"),
|
|
1016
|
+
hasOwner: has("an owner"),
|
|
1017
|
+
hasComment: has("a comment"),
|
|
1018
|
+
hasDeadline: has("a deadline"),
|
|
1019
|
+
hasLabel: has("a label"),
|
|
1020
|
+
created: date(),
|
|
1021
|
+
updated: date(),
|
|
1022
|
+
completed: date(),
|
|
1023
|
+
due: date()
|
|
1024
|
+
}, async ({ nextPageToken,...params }) => await tools.searchEpics(params, nextPageToken));
|
|
1025
|
+
server.addToolWithWriteAccess("epics-create", "Create a new Shortcut epic.", {
|
|
1026
|
+
name: z.string().describe("The name of the epic"),
|
|
1027
|
+
owner: z.string().optional().describe("The user ID of the owner of the epic"),
|
|
1028
|
+
description: z.string().optional().describe("A description of the epic"),
|
|
1029
|
+
teamId: z.string().optional().describe("The ID of a team to assign the epic to")
|
|
1030
|
+
}, async (params) => await tools.createEpic(params));
|
|
1031
|
+
return tools;
|
|
1032
|
+
}
|
|
1033
|
+
async searchEpics(params, nextToken) {
|
|
1034
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1035
|
+
const query = await buildSearchQuery(params, currentUser);
|
|
1036
|
+
const { epics, total, next_page_token } = await this.client.searchEpics(query, nextToken);
|
|
1037
|
+
if (!epics) throw new Error(`Failed to search for epics matching your query: "${query}"`);
|
|
1038
|
+
if (!epics.length) return this.toResult(`Result: No epics found.`);
|
|
1039
|
+
return this.toResult(`Result (${epics.length} shown of ${total} total epics found):`, await this.entitiesWithRelatedEntities(epics, "epics"), next_page_token);
|
|
1040
|
+
}
|
|
1041
|
+
async getEpic(epicPublicId, full = false) {
|
|
1042
|
+
const epic = await this.client.getEpic(epicPublicId);
|
|
1043
|
+
if (!epic) throw new Error(`Failed to retrieve Shortcut epic with public ID: ${epicPublicId}`);
|
|
1044
|
+
return this.toResult(`Epic: ${epicPublicId}`, await this.entityWithRelatedEntities(epic, "epic", full));
|
|
1045
|
+
}
|
|
1046
|
+
async createEpic({ name: name$1, owner, teamId: group_id, description }) {
|
|
1047
|
+
const epic = await this.client.createEpic({
|
|
1048
|
+
name: name$1,
|
|
1049
|
+
group_id,
|
|
1050
|
+
owner_ids: owner ? [owner] : void 0,
|
|
1051
|
+
description
|
|
1052
|
+
});
|
|
1053
|
+
return this.toResult(`Epic created with ID: ${epic.id}.`);
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
//#endregion
|
|
1058
|
+
//#region src/tools/iterations.ts
|
|
1059
|
+
var IterationTools = class IterationTools extends BaseTools {
|
|
1060
|
+
static create(client, server) {
|
|
1061
|
+
const tools = new IterationTools(client);
|
|
1062
|
+
server.addToolWithReadAccess("iterations-get-stories", "Get stories in a specific iteration by iteration public ID", {
|
|
1063
|
+
iterationPublicId: z.number().positive().describe("The public ID of the iteration"),
|
|
1064
|
+
includeStoryDescriptions: z.boolean().optional().default(false).describe("Indicate whether story descriptions should be included. Including descriptions may take longer and will increase the size of the response.")
|
|
1065
|
+
}, async ({ iterationPublicId, includeStoryDescriptions }) => await tools.getIterationStories(iterationPublicId, includeStoryDescriptions));
|
|
1066
|
+
server.addToolWithReadAccess("iterations-get-by-id", "Get a Shortcut iteration by public ID", {
|
|
1067
|
+
iterationPublicId: z.number().positive().describe("The public ID of the iteration to get"),
|
|
1068
|
+
full: z.boolean().optional().default(false).describe("True to return all iteration fields from the API. False to return a slim version that excludes uncommon fields")
|
|
1069
|
+
}, async ({ iterationPublicId, full }) => await tools.getIteration(iterationPublicId, full));
|
|
1070
|
+
server.addToolWithReadAccess("iterations-search", "Find Shortcut iterations.", {
|
|
1071
|
+
nextPageToken: z.string().optional().describe("If a next_page_token was returned from the search result, pass it in to get the next page of results. Should be combined with the original search parameters."),
|
|
1072
|
+
id: z.number().optional().describe("Find only iterations with the specified public ID"),
|
|
1073
|
+
name: z.string().optional().describe("Find only iterations matching the specified name"),
|
|
1074
|
+
description: z.string().optional().describe("Find only iterations matching the specified description"),
|
|
1075
|
+
state: z.enum([
|
|
1076
|
+
"started",
|
|
1077
|
+
"unstarted",
|
|
1078
|
+
"done"
|
|
1079
|
+
]).optional().describe("Find only iterations matching the specified state"),
|
|
1080
|
+
team: z.string().optional().describe("Find only iterations matching the specified team. This can be a team ID or mention name."),
|
|
1081
|
+
created: date(),
|
|
1082
|
+
updated: date(),
|
|
1083
|
+
startDate: date(),
|
|
1084
|
+
endDate: date()
|
|
1085
|
+
}, async ({ nextPageToken,...params }) => await tools.searchIterations(params, nextPageToken));
|
|
1086
|
+
server.addToolWithWriteAccess("iterations-create", "Create a new Shortcut iteration", {
|
|
1087
|
+
name: z.string().describe("The name of the iteration"),
|
|
1088
|
+
startDate: z.string().describe("The start date of the iteration in YYYY-MM-DD format"),
|
|
1089
|
+
endDate: z.string().describe("The end date of the iteration in YYYY-MM-DD format"),
|
|
1090
|
+
teamId: z.string().optional().describe("The ID of a team to assign the iteration to"),
|
|
1091
|
+
description: z.string().optional().describe("A description of the iteration")
|
|
1092
|
+
}, async (params) => await tools.createIteration(params));
|
|
1093
|
+
server.addToolWithReadAccess("iterations-get-active", "Get the active Shortcut iterations for the current user based on their team memberships", { teamId: z.string().optional().describe("The ID of a team to filter iterations by") }, async ({ teamId }) => await tools.getActiveIterations(teamId));
|
|
1094
|
+
server.addToolWithReadAccess("iterations-get-upcoming", "Get the upcoming Shortcut iterations for the current user based on their team memberships", { teamId: z.string().optional().describe("The ID of a team to filter iterations by") }, async ({ teamId }) => await tools.getUpcomingIterations(teamId));
|
|
1095
|
+
return tools;
|
|
1096
|
+
}
|
|
1097
|
+
async getIterationStories(iterationPublicId, includeDescription) {
|
|
1098
|
+
const { stories } = await this.client.listIterationStories(iterationPublicId, includeDescription);
|
|
1099
|
+
if (!stories) throw new Error(`Failed to retrieve Shortcut stories in iteration with public ID: ${iterationPublicId}.`);
|
|
1100
|
+
return this.toResult(`Result (${stories.length} stories found):`, await this.entitiesWithRelatedEntities(stories, "stories"));
|
|
1101
|
+
}
|
|
1102
|
+
async searchIterations(params, nextToken) {
|
|
1103
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1104
|
+
const query = await buildSearchQuery(params, currentUser);
|
|
1105
|
+
const { iterations, total, next_page_token } = await this.client.searchIterations(query, nextToken);
|
|
1106
|
+
if (!iterations) throw new Error(`Failed to search for iterations matching your query: "${query}".`);
|
|
1107
|
+
if (!iterations.length) return this.toResult(`Result: No iterations found.`);
|
|
1108
|
+
return this.toResult(`Result (${iterations.length} shown of ${total} total iterations found):`, await this.entitiesWithRelatedEntities(iterations, "iterations"), next_page_token);
|
|
1109
|
+
}
|
|
1110
|
+
async getIteration(iterationPublicId, full = false) {
|
|
1111
|
+
const iteration = await this.client.getIteration(iterationPublicId);
|
|
1112
|
+
if (!iteration) throw new Error(`Failed to retrieve Shortcut iteration with public ID: ${iterationPublicId}.`);
|
|
1113
|
+
return this.toResult(`Iteration: ${iterationPublicId}`, await this.entityWithRelatedEntities(iteration, "iteration", full));
|
|
1114
|
+
}
|
|
1115
|
+
async createIteration({ name: name$1, startDate, endDate, teamId, description }) {
|
|
1116
|
+
const iteration = await this.client.createIteration({
|
|
1117
|
+
name: name$1,
|
|
1118
|
+
start_date: startDate,
|
|
1119
|
+
end_date: endDate,
|
|
1120
|
+
group_ids: teamId ? [teamId] : void 0,
|
|
1121
|
+
description
|
|
1122
|
+
});
|
|
1123
|
+
if (!iteration) throw new Error(`Failed to create the iteration.`);
|
|
1124
|
+
return this.toResult(`Iteration created with ID: ${iteration.id}.`);
|
|
1125
|
+
}
|
|
1126
|
+
async getActiveIterations(teamId) {
|
|
1127
|
+
if (teamId) {
|
|
1128
|
+
const team = await this.client.getTeam(teamId);
|
|
1129
|
+
if (!team) throw new Error(`No team found matching id: "${teamId}"`);
|
|
1130
|
+
const result = await this.client.getActiveIteration([teamId]);
|
|
1131
|
+
const iterations = result.get(teamId);
|
|
1132
|
+
if (!iterations?.length) return this.toResult(`Result: No active iterations found for team.`);
|
|
1133
|
+
if (iterations.length === 1) return this.toResult("The active iteration for the team is:", await this.entityWithRelatedEntities(iterations[0], "iteration"));
|
|
1134
|
+
return this.toResult("The active iterations for the team are:", await this.entitiesWithRelatedEntities(iterations, "iterations"));
|
|
1135
|
+
}
|
|
1136
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1137
|
+
if (!currentUser) throw new Error("Failed to retrieve current user.");
|
|
1138
|
+
const teams = await this.client.getTeams();
|
|
1139
|
+
const teamIds = teams.filter((team) => team.member_ids.includes(currentUser.id)).map((team) => team.id);
|
|
1140
|
+
if (!teamIds.length) throw new Error("Current user does not belong to any teams.");
|
|
1141
|
+
const resultsByTeam = await this.client.getActiveIteration(teamIds);
|
|
1142
|
+
const allActiveIterations = [...resultsByTeam.values()].flat();
|
|
1143
|
+
if (!allActiveIterations.length) return this.toResult("Result: No active iterations found for any of your teams.");
|
|
1144
|
+
return this.toResult(`You have ${allActiveIterations.length} active iterations for your teams:`, await this.entitiesWithRelatedEntities(allActiveIterations, "iterations"));
|
|
1145
|
+
}
|
|
1146
|
+
async getUpcomingIterations(teamId) {
|
|
1147
|
+
if (teamId) {
|
|
1148
|
+
const team = await this.client.getTeam(teamId);
|
|
1149
|
+
if (!team) throw new Error(`No team found matching id: "${teamId}"`);
|
|
1150
|
+
const result = await this.client.getUpcomingIteration([teamId]);
|
|
1151
|
+
const iterations = result.get(teamId);
|
|
1152
|
+
if (!iterations?.length) return this.toResult(`Result: No upcoming iterations found for team.`);
|
|
1153
|
+
if (iterations.length === 1) return this.toResult("The next upcoming iteration for the team is:", await this.entityWithRelatedEntities(iterations[0], "iteration"));
|
|
1154
|
+
return this.toResult("The next upcoming iterations for the team are:", await this.entitiesWithRelatedEntities(iterations, "iterations"));
|
|
1155
|
+
}
|
|
1156
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1157
|
+
if (!currentUser) throw new Error("Failed to retrieve current user.");
|
|
1158
|
+
const teams = await this.client.getTeams();
|
|
1159
|
+
const teamIds = teams.filter((team) => team.member_ids.includes(currentUser.id)).map((team) => team.id);
|
|
1160
|
+
if (!teamIds.length) throw new Error("Current user does not belong to any teams.");
|
|
1161
|
+
const resultsByTeam = await this.client.getUpcomingIteration(teamIds);
|
|
1162
|
+
const allUpcomingIterations = [...resultsByTeam.values()].flat();
|
|
1163
|
+
if (!allUpcomingIterations.length) return this.toResult("Result: No upcoming iterations found for any of your teams.");
|
|
1164
|
+
return this.toResult("The upcoming iterations for all your teams are:", await this.entitiesWithRelatedEntities(allUpcomingIterations, "iterations"));
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
//#endregion
|
|
1169
|
+
//#region src/tools/objectives.ts
|
|
1170
|
+
var ObjectiveTools = class ObjectiveTools extends BaseTools {
|
|
1171
|
+
static create(client, server) {
|
|
1172
|
+
const tools = new ObjectiveTools(client);
|
|
1173
|
+
server.addToolWithReadAccess("objectives-get-by-id", "Get a Shortcut objective by public ID", {
|
|
1174
|
+
objectivePublicId: z.number().positive().describe("The public ID of the objective to get"),
|
|
1175
|
+
full: z.boolean().optional().default(false).describe("True to return all objective fields from the API. False to return a slim version that excludes uncommon fields")
|
|
1176
|
+
}, async ({ objectivePublicId, full }) => await tools.getObjective(objectivePublicId, full));
|
|
1177
|
+
server.addToolWithReadAccess("objectives-search", "Find Shortcut objectives.", {
|
|
1178
|
+
nextPageToken: z.string().optional().describe("If a next_page_token was returned from the search result, pass it in to get the next page of results. Should be combined with the original search parameters."),
|
|
1179
|
+
id: z.number().optional().describe("Find objectives matching the specified id"),
|
|
1180
|
+
name: z.string().optional().describe("Find objectives matching the specified name"),
|
|
1181
|
+
description: z.string().optional().describe("Find objectives matching the specified description"),
|
|
1182
|
+
state: z.enum([
|
|
1183
|
+
"unstarted",
|
|
1184
|
+
"started",
|
|
1185
|
+
"done"
|
|
1186
|
+
]).optional().describe("Find objectives matching the specified state"),
|
|
1187
|
+
owner: user("owner"),
|
|
1188
|
+
requester: user("requester"),
|
|
1189
|
+
team: z.string().optional().describe("Find objectives matching the specified team. Should be a team mention name."),
|
|
1190
|
+
isUnstarted: is("unstarted"),
|
|
1191
|
+
isStarted: is("started"),
|
|
1192
|
+
isDone: is("completed"),
|
|
1193
|
+
isArchived: is("archived"),
|
|
1194
|
+
hasOwner: has("an owner"),
|
|
1195
|
+
created: date(),
|
|
1196
|
+
updated: date(),
|
|
1197
|
+
completed: date()
|
|
1198
|
+
}, async ({ nextPageToken,...params }) => await tools.searchObjectives(params, nextPageToken));
|
|
1199
|
+
return tools;
|
|
1200
|
+
}
|
|
1201
|
+
async searchObjectives(params, nextToken) {
|
|
1202
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1203
|
+
const query = await buildSearchQuery(params, currentUser);
|
|
1204
|
+
const { milestones, total, next_page_token } = await this.client.searchMilestones(query, nextToken);
|
|
1205
|
+
if (!milestones) throw new Error(`Failed to search for milestones matching your query: "${query}"`);
|
|
1206
|
+
if (!milestones.length) return this.toResult(`Result: No milestones found.`);
|
|
1207
|
+
return this.toResult(`Result (${milestones.length} shown of ${total} total milestones found):`, await this.entitiesWithRelatedEntities(milestones, "objectives"), next_page_token);
|
|
1208
|
+
}
|
|
1209
|
+
async getObjective(objectivePublicId, full = false) {
|
|
1210
|
+
const objective = await this.client.getMilestone(objectivePublicId);
|
|
1211
|
+
if (!objective) throw new Error(`Failed to retrieve Shortcut objective with public ID: ${objectivePublicId}`);
|
|
1212
|
+
return this.toResult(`Objective: ${objectivePublicId}`, await this.entityWithRelatedEntities(objective, "objective", full));
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
//#endregion
|
|
1217
|
+
//#region src/tools/stories.ts
|
|
1218
|
+
var StoryTools = class StoryTools extends BaseTools {
|
|
1219
|
+
static create(client, server) {
|
|
1220
|
+
const tools = new StoryTools(client);
|
|
1221
|
+
server.addToolWithReadAccess("stories-get-by-id", "Get a Shortcut story by public ID", {
|
|
1222
|
+
storyPublicId: z.number().positive().describe("The public ID of the story to get"),
|
|
1223
|
+
full: z.boolean().optional().default(false).describe("True to return all story fields from the API. False to return a slim version that excludes uncommon fields")
|
|
1224
|
+
}, async ({ storyPublicId, full }) => await tools.getStory(storyPublicId, full));
|
|
1225
|
+
server.addToolWithReadAccess("stories-search", "Find Shortcut stories.", {
|
|
1226
|
+
nextPageToken: z.string().optional().describe("If a next_page_token was returned from the search result, pass it in to get the next page of results. Should be combined with the original search parameters."),
|
|
1227
|
+
id: z.number().optional().describe("Find only stories with the specified public ID"),
|
|
1228
|
+
name: z.string().optional().describe("Find only stories matching the specified name"),
|
|
1229
|
+
description: z.string().optional().describe("Find only stories matching the specified description"),
|
|
1230
|
+
comment: z.string().optional().describe("Find only stories matching the specified comment"),
|
|
1231
|
+
type: z.enum([
|
|
1232
|
+
"feature",
|
|
1233
|
+
"bug",
|
|
1234
|
+
"chore"
|
|
1235
|
+
]).optional().describe("Find only stories of the specified type"),
|
|
1236
|
+
estimate: z.number().optional().describe("Find only stories matching the specified estimate"),
|
|
1237
|
+
branch: z.string().optional().describe("Find only stories matching the specified branch"),
|
|
1238
|
+
commit: z.string().optional().describe("Find only stories matching the specified commit"),
|
|
1239
|
+
pr: z.number().optional().describe("Find only stories matching the specified pull request"),
|
|
1240
|
+
project: z.number().optional().describe("Find only stories matching the specified project"),
|
|
1241
|
+
epic: z.number().optional().describe("Find only stories matching the specified epic"),
|
|
1242
|
+
objective: z.number().optional().describe("Find only stories matching the specified objective"),
|
|
1243
|
+
state: z.string().optional().describe("Find only stories matching the specified state"),
|
|
1244
|
+
label: z.string().optional().describe("Find only stories matching the specified label"),
|
|
1245
|
+
owner: user("owner"),
|
|
1246
|
+
requester: user("requester"),
|
|
1247
|
+
team: z.string().optional().describe("Find only stories matching the specified team. This can be a team mention name or team name."),
|
|
1248
|
+
skillSet: z.string().optional().describe("Find only stories matching the specified skill set"),
|
|
1249
|
+
productArea: z.string().optional().describe("Find only stories matching the specified product area"),
|
|
1250
|
+
technicalArea: z.string().optional().describe("Find only stories matching the specified technical area"),
|
|
1251
|
+
priority: z.string().optional().describe("Find only stories matching the specified priority"),
|
|
1252
|
+
severity: z.string().optional().describe("Find only stories matching the specified severity"),
|
|
1253
|
+
isDone: is("completed"),
|
|
1254
|
+
isStarted: is("started"),
|
|
1255
|
+
isUnstarted: is("unstarted"),
|
|
1256
|
+
isUnestimated: is("unestimated"),
|
|
1257
|
+
isOverdue: is("overdue"),
|
|
1258
|
+
isArchived: is("archived").default(false),
|
|
1259
|
+
isBlocker: is("blocking"),
|
|
1260
|
+
isBlocked: is("blocked"),
|
|
1261
|
+
hasComment: has("a comment"),
|
|
1262
|
+
hasLabel: has("a label"),
|
|
1263
|
+
hasDeadline: has("a deadline"),
|
|
1264
|
+
hasOwner: has("an owner"),
|
|
1265
|
+
hasPr: has("a pr"),
|
|
1266
|
+
hasCommit: has("a commit"),
|
|
1267
|
+
hasBranch: has("a branch"),
|
|
1268
|
+
hasEpic: has("an epic"),
|
|
1269
|
+
hasTask: has("a task"),
|
|
1270
|
+
hasAttachment: has("an attachment"),
|
|
1271
|
+
created: date(),
|
|
1272
|
+
updated: date(),
|
|
1273
|
+
completed: date(),
|
|
1274
|
+
due: date()
|
|
1275
|
+
}, async ({ nextPageToken,...params }) => await tools.searchStories(params, nextPageToken));
|
|
1276
|
+
server.addToolWithReadAccess("stories-get-branch-name", "Get a valid branch name for a specific story.", { storyPublicId: z.number().positive().describe("The public Id of the story") }, async ({ storyPublicId }) => await tools.getStoryBranchName(storyPublicId));
|
|
1277
|
+
server.addToolWithWriteAccess("stories-create", `Create a new Shortcut story.
|
|
1278
|
+
Name is required, and either a Team or Workflow must be specified:
|
|
1279
|
+
- If only Team is specified, we will use the default workflow for that team.
|
|
1280
|
+
- If Workflow is specified, it will be used regardless of Team.
|
|
1281
|
+
The story will be added to the default state for the workflow.
|
|
1282
|
+
`, {
|
|
1283
|
+
name: z.string().min(1).max(512).describe("The name of the story. Required."),
|
|
1284
|
+
description: z.string().max(1e4).optional().describe("The description of the story"),
|
|
1285
|
+
type: z.enum([
|
|
1286
|
+
"feature",
|
|
1287
|
+
"bug",
|
|
1288
|
+
"chore"
|
|
1289
|
+
]).default("feature").describe("The type of the story"),
|
|
1290
|
+
owner: z.string().optional().describe("The user id of the owner of the story"),
|
|
1291
|
+
epic: z.number().optional().describe("The epic id of the epic the story belongs to"),
|
|
1292
|
+
iteration: z.number().optional().describe("The iteration id of the iteration the story belongs to"),
|
|
1293
|
+
team: z.string().optional().describe("The team ID or mention name of the team the story belongs to. Required unless a workflow is specified."),
|
|
1294
|
+
workflow: z.number().optional().describe("The workflow ID to add the story to. Required unless a team is specified.")
|
|
1295
|
+
}, async ({ name: name$1, description, type, owner, epic, iteration, team, workflow }) => await tools.createStory({
|
|
1296
|
+
name: name$1,
|
|
1297
|
+
description,
|
|
1298
|
+
type,
|
|
1299
|
+
owner,
|
|
1300
|
+
epic,
|
|
1301
|
+
iteration,
|
|
1302
|
+
team,
|
|
1303
|
+
workflow
|
|
1304
|
+
}));
|
|
1305
|
+
server.addToolWithWriteAccess("stories-update", "Update an existing Shortcut story. Only provide fields you want to update. The story public ID will always be included in updates.", {
|
|
1306
|
+
storyPublicId: z.number().positive().describe("The public ID of the story to update"),
|
|
1307
|
+
name: z.string().max(512).optional().describe("The name of the story"),
|
|
1308
|
+
description: z.string().max(1e4).optional().describe("The description of the story"),
|
|
1309
|
+
type: z.enum([
|
|
1310
|
+
"feature",
|
|
1311
|
+
"bug",
|
|
1312
|
+
"chore"
|
|
1313
|
+
]).optional().describe("The type of the story"),
|
|
1314
|
+
epic: z.number().nullable().optional().describe("The epic id of the epic the story belongs to, or null to unset"),
|
|
1315
|
+
estimate: z.number().nullable().optional().describe("The point estimate of the story, or null to unset"),
|
|
1316
|
+
iteration: z.number().nullable().optional().describe("The iteration id of the iteration the story belongs to, or null to unset"),
|
|
1317
|
+
owner_ids: z.array(z.string()).optional().describe("Array of user UUIDs to assign as owners of the story"),
|
|
1318
|
+
workflow_state_id: z.number().optional().describe("The workflow state ID to move the story to"),
|
|
1319
|
+
labels: z.array(z.object({
|
|
1320
|
+
name: z.string().describe("The name of the label"),
|
|
1321
|
+
color: z.string().optional().describe("The color of the label"),
|
|
1322
|
+
description: z.string().optional().describe("The description of the label")
|
|
1323
|
+
})).optional().describe("Labels to assign to the story")
|
|
1324
|
+
}, async (params) => await tools.updateStory(params));
|
|
1325
|
+
server.addToolWithWriteAccess("stories-upload-file", "Upload a file and link it to a story.", {
|
|
1326
|
+
storyPublicId: z.number().positive().describe("The public ID of the story"),
|
|
1327
|
+
filePath: z.string().describe("The path to the file to upload")
|
|
1328
|
+
}, async ({ storyPublicId, filePath }) => await tools.uploadFileToStory(storyPublicId, filePath));
|
|
1329
|
+
server.addToolWithWriteAccess("stories-assign-current-user", "Assign the current user as the owner of a story", { storyPublicId: z.number().positive().describe("The public ID of the story") }, async ({ storyPublicId }) => await tools.assignCurrentUserAsOwner(storyPublicId));
|
|
1330
|
+
server.addToolWithWriteAccess("stories-unassign-current-user", "Unassign the current user as the owner of a story", { storyPublicId: z.number().positive().describe("The public ID of the story") }, async ({ storyPublicId }) => await tools.unassignCurrentUserAsOwner(storyPublicId));
|
|
1331
|
+
server.addToolWithWriteAccess("stories-create-comment", "Create a comment on a story", {
|
|
1332
|
+
storyPublicId: z.number().positive().describe("The public ID of the story"),
|
|
1333
|
+
text: z.string().min(1).describe("The text of the comment")
|
|
1334
|
+
}, async (params) => await tools.createStoryComment(params));
|
|
1335
|
+
server.addToolWithWriteAccess("stories-create-subtask", "Create a new story as a sub-task", {
|
|
1336
|
+
parentStoryPublicId: z.number().positive().describe("The public ID of the parent story"),
|
|
1337
|
+
name: z.string().min(1).max(512).describe("The name of the sub-task. Required."),
|
|
1338
|
+
description: z.string().max(1e4).optional().describe("The description of the sub-task")
|
|
1339
|
+
}, async (params) => await tools.createSubTask(params));
|
|
1340
|
+
server.addToolWithWriteAccess("stories-add-subtask", "Add an existing story as a sub-task", {
|
|
1341
|
+
parentStoryPublicId: z.number().positive().describe("The public ID of the parent story"),
|
|
1342
|
+
subTaskPublicId: z.number().positive().describe("The public ID of the sub-task story")
|
|
1343
|
+
}, async (params) => await tools.addStoryAsSubTask(params));
|
|
1344
|
+
server.addToolWithWriteAccess("stories-remove-subtask", "Remove a story from its parent. The sub-task will become a regular story.", { subTaskPublicId: z.number().positive().describe("The public ID of the sub-task story") }, async (params) => await tools.removeSubTaskFromParent(params));
|
|
1345
|
+
server.addToolWithWriteAccess("stories-add-task", "Add a task to a story", {
|
|
1346
|
+
storyPublicId: z.number().positive().describe("The public ID of the story"),
|
|
1347
|
+
taskDescription: z.string().min(1).describe("The description of the task"),
|
|
1348
|
+
taskOwnerIds: z.array(z.string()).optional().describe("Array of user IDs to assign as owners of the task")
|
|
1349
|
+
}, async (params) => await tools.addTaskToStory(params));
|
|
1350
|
+
server.addToolWithWriteAccess("stories-update-task", "Update a task in a story", {
|
|
1351
|
+
storyPublicId: z.number().positive().describe("The public ID of the story"),
|
|
1352
|
+
taskPublicId: z.number().positive().describe("The public ID of the task"),
|
|
1353
|
+
taskDescription: z.string().optional().describe("The description of the task"),
|
|
1354
|
+
taskOwnerIds: z.array(z.string()).optional().describe("Array of user IDs to assign as owners of the task"),
|
|
1355
|
+
isCompleted: z.boolean().optional().describe("Whether the task is completed or not")
|
|
1356
|
+
}, async (params) => await tools.updateTask(params));
|
|
1357
|
+
server.addToolWithWriteAccess("stories-add-relation", "Add a story relationship to a story", {
|
|
1358
|
+
storyPublicId: z.number().positive().describe("The public ID of the story"),
|
|
1359
|
+
relatedStoryPublicId: z.number().positive().describe("The public ID of the related story"),
|
|
1360
|
+
relationshipType: z.enum([
|
|
1361
|
+
"relates to",
|
|
1362
|
+
"blocks",
|
|
1363
|
+
"blocked by",
|
|
1364
|
+
"duplicates",
|
|
1365
|
+
"duplicated by"
|
|
1366
|
+
]).optional().default("relates to").describe("The type of relationship")
|
|
1367
|
+
}, async (params) => await tools.addRelationToStory(params));
|
|
1368
|
+
server.addToolWithWriteAccess("stories-add-external-link", "Add an external link to a Shortcut story", {
|
|
1369
|
+
storyPublicId: z.number().positive().describe("The public ID of the story"),
|
|
1370
|
+
externalLink: z.string().url().max(2048).describe("The external link URL to add")
|
|
1371
|
+
}, async ({ storyPublicId, externalLink }) => await tools.addExternalLinkToStory(storyPublicId, externalLink));
|
|
1372
|
+
server.addToolWithWriteAccess("stories-remove-external-link", "Remove an external link from a Shortcut story", {
|
|
1373
|
+
storyPublicId: z.number().positive().describe("The public ID of the story"),
|
|
1374
|
+
externalLink: z.string().url().max(2048).describe("The external link URL to remove")
|
|
1375
|
+
}, async ({ storyPublicId, externalLink }) => await tools.removeExternalLinkFromStory(storyPublicId, externalLink));
|
|
1376
|
+
server.addToolWithWriteAccess("stories-set-external-links", "Replace all external links on a story with a new set of links", {
|
|
1377
|
+
storyPublicId: z.number().positive().describe("The public ID of the story"),
|
|
1378
|
+
externalLinks: z.array(z.string().url().max(2048)).describe("Array of external link URLs to set (replaces all existing links)")
|
|
1379
|
+
}, async ({ storyPublicId, externalLinks }) => await tools.setStoryExternalLinks(storyPublicId, externalLinks));
|
|
1380
|
+
server.addToolWithReadAccess("stories-get-by-external-link", "Find all stories that contain a specific external link", { externalLink: z.string().url().max(2048).describe("The external link URL to search for") }, async ({ externalLink }) => await tools.getStoriesByExternalLink(externalLink));
|
|
1381
|
+
return tools;
|
|
1382
|
+
}
|
|
1383
|
+
async assignCurrentUserAsOwner(storyPublicId) {
|
|
1384
|
+
const story = await this.client.getStory(storyPublicId);
|
|
1385
|
+
if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
|
|
1386
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1387
|
+
if (!currentUser) throw new Error("Failed to retrieve current user");
|
|
1388
|
+
if (story.owner_ids.includes(currentUser.id)) return this.toResult(`Current user is already an owner of story sc-${storyPublicId}`);
|
|
1389
|
+
await this.client.updateStory(storyPublicId, { owner_ids: story.owner_ids.concat([currentUser.id]) });
|
|
1390
|
+
return this.toResult(`Assigned current user as owner of story sc-${storyPublicId}`);
|
|
1391
|
+
}
|
|
1392
|
+
async unassignCurrentUserAsOwner(storyPublicId) {
|
|
1393
|
+
const story = await this.client.getStory(storyPublicId);
|
|
1394
|
+
if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
|
|
1395
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1396
|
+
if (!currentUser) throw new Error("Failed to retrieve current user");
|
|
1397
|
+
if (!story.owner_ids.includes(currentUser.id)) return this.toResult(`Current user is not an owner of story sc-${storyPublicId}`);
|
|
1398
|
+
await this.client.updateStory(storyPublicId, { owner_ids: story.owner_ids.filter((ownerId) => ownerId !== currentUser.id) });
|
|
1399
|
+
return this.toResult(`Unassigned current user as owner of story sc-${storyPublicId}`);
|
|
1400
|
+
}
|
|
1401
|
+
createBranchName(currentUser, story) {
|
|
1402
|
+
return `${currentUser.mention_name}/sc-${story.id}/${story.name.toLowerCase().replace(/\s+/g, "-").replace(/[^\w-]/g, "")}`.substring(0, 50);
|
|
1403
|
+
}
|
|
1404
|
+
async getStoryBranchName(storyPublicId) {
|
|
1405
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1406
|
+
if (!currentUser) throw new Error("Unable to find current user");
|
|
1407
|
+
const story = await this.client.getStory(storyPublicId);
|
|
1408
|
+
if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
|
|
1409
|
+
const branchName = story.formatted_vcs_branch_name || this.createBranchName(currentUser, story);
|
|
1410
|
+
return this.toResult(`Branch name for story sc-${storyPublicId}: ${branchName}`);
|
|
1411
|
+
}
|
|
1412
|
+
async createStory({ name: name$1, description, type, owner, epic, iteration, team, workflow }) {
|
|
1413
|
+
if (!workflow && !team) throw new Error("Team or Workflow has to be specified");
|
|
1414
|
+
if (!workflow && team) {
|
|
1415
|
+
const fullTeam = await this.client.getTeam(team);
|
|
1416
|
+
workflow = fullTeam?.workflow_ids?.[0];
|
|
1417
|
+
}
|
|
1418
|
+
if (!workflow) throw new Error("Failed to find workflow for team");
|
|
1419
|
+
const fullWorkflow = await this.client.getWorkflow(workflow);
|
|
1420
|
+
if (!fullWorkflow) throw new Error("Failed to find workflow");
|
|
1421
|
+
const story = await this.client.createStory({
|
|
1422
|
+
name: name$1,
|
|
1423
|
+
description,
|
|
1424
|
+
story_type: type,
|
|
1425
|
+
owner_ids: owner ? [owner] : [],
|
|
1426
|
+
epic_id: epic,
|
|
1427
|
+
iteration_id: iteration,
|
|
1428
|
+
group_id: team,
|
|
1429
|
+
workflow_state_id: fullWorkflow.default_state_id
|
|
1430
|
+
});
|
|
1431
|
+
return this.toResult(`Created story: sc-${story.id}`);
|
|
1432
|
+
}
|
|
1433
|
+
async createSubTask({ parentStoryPublicId, name: name$1, description }) {
|
|
1434
|
+
if (!parentStoryPublicId) throw new Error("ID of parent story is required");
|
|
1435
|
+
if (!name$1) throw new Error("Sub-task name is required");
|
|
1436
|
+
const parentStory = await this.client.getStory(parentStoryPublicId);
|
|
1437
|
+
if (!parentStory) throw new Error(`Failed to retrieve parent story with public ID: ${parentStoryPublicId}`);
|
|
1438
|
+
const workflow = await this.client.getWorkflow(parentStory.workflow_id);
|
|
1439
|
+
if (!workflow) throw new Error("Failed to retrieve workflow of parent story");
|
|
1440
|
+
const workflowState = workflow.states[0];
|
|
1441
|
+
if (!workflowState) throw new Error("Failed to determine default state for sub-task");
|
|
1442
|
+
const subTask = await this.client.createStory({
|
|
1443
|
+
name: name$1,
|
|
1444
|
+
description,
|
|
1445
|
+
story_type: parentStory.story_type,
|
|
1446
|
+
epic_id: parentStory.epic_id,
|
|
1447
|
+
group_id: parentStory.group_id,
|
|
1448
|
+
workflow_state_id: workflowState.id,
|
|
1449
|
+
parent_story_id: parentStoryPublicId
|
|
1450
|
+
});
|
|
1451
|
+
return this.toResult(`Created sub-task: sc-${subTask.id}`);
|
|
1452
|
+
}
|
|
1453
|
+
async addStoryAsSubTask({ parentStoryPublicId, subTaskPublicId }) {
|
|
1454
|
+
if (!parentStoryPublicId) throw new Error("ID of parent story is required");
|
|
1455
|
+
if (!subTaskPublicId) throw new Error("ID of sub-task story is required");
|
|
1456
|
+
const subTask = await this.client.getStory(subTaskPublicId);
|
|
1457
|
+
if (!subTask) throw new Error(`Failed to retrieve story with public ID: ${subTaskPublicId}`);
|
|
1458
|
+
const parentStory = await this.client.getStory(parentStoryPublicId);
|
|
1459
|
+
if (!parentStory) throw new Error(`Failed to retrieve parent story with public ID: ${parentStoryPublicId}`);
|
|
1460
|
+
await this.client.updateStory(subTaskPublicId, { parent_story_id: parentStoryPublicId });
|
|
1461
|
+
return this.toResult(`Added story sc-${subTaskPublicId} as a sub-task of sc-${parentStoryPublicId}`);
|
|
1462
|
+
}
|
|
1463
|
+
async removeSubTaskFromParent({ subTaskPublicId }) {
|
|
1464
|
+
if (!subTaskPublicId) throw new Error("ID of sub-task story is required");
|
|
1465
|
+
const subTask = await this.client.getStory(subTaskPublicId);
|
|
1466
|
+
if (!subTask) throw new Error(`Failed to retrieve story with public ID: ${subTaskPublicId}`);
|
|
1467
|
+
await this.client.updateStory(subTaskPublicId, { parent_story_id: null });
|
|
1468
|
+
return this.toResult(`Removed story sc-${subTaskPublicId} from its parent story`);
|
|
1469
|
+
}
|
|
1470
|
+
async searchStories(params, nextToken) {
|
|
1471
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1472
|
+
const query = await buildSearchQuery(params, currentUser);
|
|
1473
|
+
const { stories, total, next_page_token } = await this.client.searchStories(query, nextToken);
|
|
1474
|
+
if (!stories) throw new Error(`Failed to search for stories matching your query: "${query}".`);
|
|
1475
|
+
if (!stories.length) return this.toResult(`Result: No stories found.`);
|
|
1476
|
+
return this.toResult(`Result (${stories.length} shown of ${total} total stories found):`, await this.entitiesWithRelatedEntities(stories, "stories"), next_page_token);
|
|
1477
|
+
}
|
|
1478
|
+
async getStory(storyPublicId, full = false) {
|
|
1479
|
+
const story = await this.client.getStory(storyPublicId);
|
|
1480
|
+
if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}.`);
|
|
1481
|
+
return this.toResult(`Story: sc-${storyPublicId}`, await this.entityWithRelatedEntities(story, "story", full));
|
|
1482
|
+
}
|
|
1483
|
+
async createStoryComment({ storyPublicId, text }) {
|
|
1484
|
+
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1485
|
+
if (!text) throw new Error("Story comment text is required");
|
|
1486
|
+
const story = await this.client.getStory(storyPublicId);
|
|
1487
|
+
if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
|
|
1488
|
+
const storyComment = await this.client.createStoryComment(storyPublicId, { text });
|
|
1489
|
+
return this.toResult(`Created comment on story sc-${storyPublicId}. Comment URL: ${storyComment.app_url}.`);
|
|
1490
|
+
}
|
|
1491
|
+
async updateStory({ storyPublicId,...updates }) {
|
|
1492
|
+
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1493
|
+
const story = await this.client.getStory(storyPublicId);
|
|
1494
|
+
if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
|
|
1495
|
+
const updateParams = {};
|
|
1496
|
+
if (updates.name !== void 0) updateParams.name = updates.name;
|
|
1497
|
+
if (updates.description !== void 0) updateParams.description = updates.description;
|
|
1498
|
+
if (updates.type !== void 0) updateParams.story_type = updates.type;
|
|
1499
|
+
if (updates.epic !== void 0) updateParams.epic_id = updates.epic;
|
|
1500
|
+
if (updates.estimate !== void 0) updateParams.estimate = updates.estimate;
|
|
1501
|
+
if (updates.iteration !== void 0) updateParams.iteration_id = updates.iteration;
|
|
1502
|
+
if (updates.owner_ids !== void 0) updateParams.owner_ids = updates.owner_ids;
|
|
1503
|
+
if (updates.workflow_state_id !== void 0) updateParams.workflow_state_id = updates.workflow_state_id;
|
|
1504
|
+
if (updates.labels !== void 0) updateParams.labels = updates.labels;
|
|
1505
|
+
const updatedStory = await this.client.updateStory(storyPublicId, updateParams);
|
|
1506
|
+
return this.toResult(`Updated story sc-${storyPublicId}. Story URL: ${updatedStory.app_url}`);
|
|
1507
|
+
}
|
|
1508
|
+
async uploadFileToStory(storyPublicId, filePath) {
|
|
1509
|
+
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1510
|
+
if (!filePath) throw new Error("File path is required");
|
|
1511
|
+
const story = await this.client.getStory(storyPublicId);
|
|
1512
|
+
if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
|
|
1513
|
+
const uploadedFile = await this.client.uploadFile(storyPublicId, filePath);
|
|
1514
|
+
if (!uploadedFile) throw new Error(`Failed to upload file to story sc-${storyPublicId}`);
|
|
1515
|
+
return this.toResult(`Uploaded file "${uploadedFile.name}" to story sc-${storyPublicId}. File ID is: ${uploadedFile.id}`);
|
|
1516
|
+
}
|
|
1517
|
+
async addTaskToStory({ storyPublicId, taskDescription, taskOwnerIds }) {
|
|
1518
|
+
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1519
|
+
if (!taskDescription) throw new Error("Task description is required");
|
|
1520
|
+
const story = await this.client.getStory(storyPublicId);
|
|
1521
|
+
if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
|
|
1522
|
+
if (taskOwnerIds?.length) {
|
|
1523
|
+
const owners = await this.client.getUserMap(taskOwnerIds);
|
|
1524
|
+
if (!owners) throw new Error(`Failed to retrieve users with IDs: ${taskOwnerIds.join(", ")}`);
|
|
1525
|
+
}
|
|
1526
|
+
const task = await this.client.addTaskToStory(storyPublicId, {
|
|
1527
|
+
description: taskDescription,
|
|
1528
|
+
ownerIds: taskOwnerIds
|
|
1529
|
+
});
|
|
1530
|
+
return this.toResult(`Created task for story sc-${storyPublicId}. Task ID: ${task.id}.`);
|
|
1531
|
+
}
|
|
1532
|
+
async updateTask({ storyPublicId, taskPublicId, taskDescription, taskOwnerIds, isCompleted }) {
|
|
1533
|
+
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1534
|
+
if (!taskPublicId) throw new Error("Task public ID is required");
|
|
1535
|
+
const story = await this.client.getStory(storyPublicId);
|
|
1536
|
+
if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
|
|
1537
|
+
const task = await this.client.getTask(storyPublicId, taskPublicId);
|
|
1538
|
+
if (!task) throw new Error(`Failed to retrieve Shortcut task with public ID: ${taskPublicId}`);
|
|
1539
|
+
const updatedTask = await this.client.updateTask(storyPublicId, taskPublicId, {
|
|
1540
|
+
description: taskDescription,
|
|
1541
|
+
ownerIds: taskOwnerIds,
|
|
1542
|
+
isCompleted
|
|
1543
|
+
});
|
|
1544
|
+
let message = `Updated task for story sc-${storyPublicId}. Task ID: ${updatedTask.id}.`;
|
|
1545
|
+
if (isCompleted) message = `Completed task for story sc-${storyPublicId}. Task ID: ${updatedTask.id}.`;
|
|
1546
|
+
return this.toResult(message);
|
|
1547
|
+
}
|
|
1548
|
+
async addRelationToStory({ storyPublicId, relatedStoryPublicId, relationshipType }) {
|
|
1549
|
+
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1550
|
+
if (!relatedStoryPublicId) throw new Error("Related story public ID is required");
|
|
1551
|
+
const story = await this.client.getStory(storyPublicId);
|
|
1552
|
+
if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
|
|
1553
|
+
const relatedStory = await this.client.getStory(relatedStoryPublicId);
|
|
1554
|
+
if (!relatedStory) throw new Error(`Failed to retrieve Shortcut story with public ID: ${relatedStoryPublicId}`);
|
|
1555
|
+
let subjectStoryId = storyPublicId;
|
|
1556
|
+
let objectStoryId = relatedStoryPublicId;
|
|
1557
|
+
if (relationshipType === "blocked by" || relationshipType === "duplicated by") {
|
|
1558
|
+
relationshipType = relationshipType === "blocked by" ? "blocks" : "duplicates";
|
|
1559
|
+
subjectStoryId = relatedStoryPublicId;
|
|
1560
|
+
objectStoryId = storyPublicId;
|
|
1561
|
+
}
|
|
1562
|
+
await this.client.addRelationToStory(subjectStoryId, objectStoryId, relationshipType);
|
|
1563
|
+
return this.toResult(relationshipType === "blocks" ? `Marked sc-${subjectStoryId} as a blocker to sc-${objectStoryId}.` : relationshipType === "duplicates" ? `Marked sc-${subjectStoryId} as a duplicate of sc-${objectStoryId}.` : `Added a relationship between sc-${subjectStoryId} and sc-${objectStoryId}.`);
|
|
1564
|
+
}
|
|
1565
|
+
async addExternalLinkToStory(storyPublicId, externalLink) {
|
|
1566
|
+
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1567
|
+
if (!externalLink) throw new Error("External link is required");
|
|
1568
|
+
const updatedStory = await this.client.addExternalLinkToStory(storyPublicId, externalLink);
|
|
1569
|
+
return this.toResult(`Added external link to story sc-${storyPublicId}. Story URL: ${updatedStory.app_url}`);
|
|
1570
|
+
}
|
|
1571
|
+
async removeExternalLinkFromStory(storyPublicId, externalLink) {
|
|
1572
|
+
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1573
|
+
if (!externalLink) throw new Error("External link is required");
|
|
1574
|
+
const updatedStory = await this.client.removeExternalLinkFromStory(storyPublicId, externalLink);
|
|
1575
|
+
return this.toResult(`Removed external link from story sc-${storyPublicId}. Story URL: ${updatedStory.app_url}`);
|
|
1576
|
+
}
|
|
1577
|
+
async getStoriesByExternalLink(externalLink) {
|
|
1578
|
+
if (!externalLink) throw new Error("External link is required");
|
|
1579
|
+
const { stories, total } = await this.client.getStoriesByExternalLink(externalLink);
|
|
1580
|
+
if (!stories || !stories.length) return this.toResult(`No stories found with external link: ${externalLink}`);
|
|
1581
|
+
return this.toResult(`Found ${total} stories with external link: ${externalLink}`, await this.entitiesWithRelatedEntities(stories, "stories"));
|
|
1582
|
+
}
|
|
1583
|
+
async setStoryExternalLinks(storyPublicId, externalLinks) {
|
|
1584
|
+
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1585
|
+
if (!Array.isArray(externalLinks)) throw new Error("External links must be an array");
|
|
1586
|
+
const updatedStory = await this.client.setStoryExternalLinks(storyPublicId, externalLinks);
|
|
1587
|
+
const linkCount = externalLinks.length;
|
|
1588
|
+
const message = linkCount === 0 ? `Removed all external links from story sc-${storyPublicId}` : `Set ${linkCount} external link${linkCount === 1 ? "" : "s"} on story sc-${storyPublicId}`;
|
|
1589
|
+
return this.toResult(`${message}. Story URL: ${updatedStory.app_url}`);
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
//#endregion
|
|
1594
|
+
//#region src/tools/teams.ts
|
|
1595
|
+
var TeamTools = class TeamTools extends BaseTools {
|
|
1596
|
+
static create(client, server) {
|
|
1597
|
+
const tools = new TeamTools(client);
|
|
1598
|
+
server.addToolWithReadAccess("teams-get-by-id", "Get a Shortcut team by public ID", {
|
|
1599
|
+
teamPublicId: z.string().describe("The public ID of the team to get"),
|
|
1600
|
+
full: z.boolean().optional().default(false).describe("True to return all team fields from the API. False to return a slim version that excludes uncommon fields")
|
|
1601
|
+
}, async ({ teamPublicId, full }) => await tools.getTeam(teamPublicId, full));
|
|
1602
|
+
server.addToolWithReadAccess("teams-list", "List all Shortcut teams", async () => await tools.getTeams());
|
|
1603
|
+
return tools;
|
|
1604
|
+
}
|
|
1605
|
+
async getTeam(teamPublicId, full = false) {
|
|
1606
|
+
const team = await this.client.getTeam(teamPublicId);
|
|
1607
|
+
if (!team) return this.toResult(`Team with public ID: ${teamPublicId} not found.`);
|
|
1608
|
+
return this.toResult(`Team: ${team.id}`, await this.entityWithRelatedEntities(team, "team", full));
|
|
1609
|
+
}
|
|
1610
|
+
async getTeams() {
|
|
1611
|
+
const teams = await this.client.getTeams();
|
|
1612
|
+
if (!teams.length) return this.toResult(`No teams found.`);
|
|
1613
|
+
return this.toResult(`Result (first ${teams.length} shown of ${teams.length} total teams found):`, await this.entitiesWithRelatedEntities(teams, "teams"));
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
|
|
1617
|
+
//#endregion
|
|
1618
|
+
//#region src/tools/user.ts
|
|
1619
|
+
var UserTools = class UserTools extends BaseTools {
|
|
1620
|
+
static create(client, server) {
|
|
1621
|
+
const tools = new UserTools(client);
|
|
1622
|
+
server.addToolWithReadAccess("users-get-current", "Get the current user", async () => await tools.getCurrentUser());
|
|
1623
|
+
server.addToolWithReadAccess("users-get-current-teams", "Get a list of teams where the current user is a member", async () => await tools.getCurrentUserTeams());
|
|
1624
|
+
server.addToolWithReadAccess("users-list", "Get all users", async () => await tools.listMembers());
|
|
1625
|
+
return tools;
|
|
1626
|
+
}
|
|
1627
|
+
async getCurrentUser() {
|
|
1628
|
+
const user$1 = await this.client.getCurrentUser();
|
|
1629
|
+
if (!user$1) throw new Error("Failed to retrieve current user.");
|
|
1630
|
+
return this.toResult(`Current user:`, user$1);
|
|
1631
|
+
}
|
|
1632
|
+
async getCurrentUserTeams() {
|
|
1633
|
+
const teams = await this.client.getTeams();
|
|
1634
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1635
|
+
if (!currentUser) throw new Error("Failed to get current user.");
|
|
1636
|
+
const userTeams = teams.filter((team) => !team.archived && team.member_ids.includes(currentUser.id));
|
|
1637
|
+
if (!userTeams.length) return this.toResult(`Current user is not a member of any teams.`);
|
|
1638
|
+
if (userTeams.length === 1) {
|
|
1639
|
+
const team = userTeams[0];
|
|
1640
|
+
return this.toResult(`Current user is a member of team "${team.name}":`, await this.entityWithRelatedEntities(team, "team"));
|
|
1641
|
+
}
|
|
1642
|
+
return this.toResult(`Current user is a member of ${userTeams.length} teams:`, await this.entitiesWithRelatedEntities(userTeams, "teams"));
|
|
1643
|
+
}
|
|
1644
|
+
async listMembers() {
|
|
1645
|
+
const members = await this.client.listMembers();
|
|
1646
|
+
return this.toResult(`Found ${members.length} members:`, members);
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1650
|
+
//#endregion
|
|
1651
|
+
//#region src/tools/workflows.ts
|
|
1652
|
+
var WorkflowTools = class WorkflowTools extends BaseTools {
|
|
1653
|
+
static create(client, server) {
|
|
1654
|
+
const tools = new WorkflowTools(client);
|
|
1655
|
+
server.addToolWithReadAccess("workflows-get-default", "Get the default workflow for a specific team or the global default if no team is specified.", { teamPublicId: z.string().optional().describe("The public ID of the team to get the default workflow for.") }, async ({ teamPublicId }) => await tools.getDefaultWorkflow(teamPublicId));
|
|
1656
|
+
server.addToolWithReadAccess("workflows-get-by-id", "Get a Shortcut workflow by public ID", {
|
|
1657
|
+
workflowPublicId: z.number().positive().describe("The public ID of the workflow to get"),
|
|
1658
|
+
full: z.boolean().optional().default(false).describe("True to return all workflow fields from the API. False to return a slim version that excludes uncommon fields")
|
|
1659
|
+
}, async ({ workflowPublicId, full }) => await tools.getWorkflow(workflowPublicId, full));
|
|
1660
|
+
server.addToolWithReadAccess("workflows-list", "List all Shortcut workflows", async () => await tools.listWorkflows());
|
|
1661
|
+
return tools;
|
|
1662
|
+
}
|
|
1663
|
+
async getDefaultWorkflow(teamPublicId) {
|
|
1664
|
+
if (teamPublicId) try {
|
|
1665
|
+
const teamDefaultWorkflowId = await this.client.getTeam(teamPublicId).then((t) => t?.default_workflow_id);
|
|
1666
|
+
if (teamDefaultWorkflowId) {
|
|
1667
|
+
const teamDefaultWorkflow = await this.client.getWorkflow(teamDefaultWorkflowId);
|
|
1668
|
+
if (teamDefaultWorkflow) return this.toResult(`Default workflow for team "${teamPublicId}" has id ${teamDefaultWorkflow.id}.`, await this.entityWithRelatedEntities(teamDefaultWorkflow, "workflow"));
|
|
1669
|
+
}
|
|
1670
|
+
} catch {}
|
|
1671
|
+
const currentUser = await this.client.getCurrentUser();
|
|
1672
|
+
if (!currentUser) throw new Error("Failed to retrieve current user.");
|
|
1673
|
+
const workspaceDefaultWorkflowId = currentUser.workspace2.default_workflow_id;
|
|
1674
|
+
const workspaceDefaultWorkflow = await this.client.getWorkflow(workspaceDefaultWorkflowId);
|
|
1675
|
+
if (workspaceDefaultWorkflow) return this.toResult(`${teamPublicId ? `No default workflow found for team with public ID "${teamPublicId}". The general default workflow has id ` : "Default workflow has id "}${workspaceDefaultWorkflow.id}.`, await this.entityWithRelatedEntities(workspaceDefaultWorkflow, "workflow"));
|
|
1676
|
+
return this.toResult("No default workflow found.");
|
|
1677
|
+
}
|
|
1678
|
+
async getWorkflow(workflowPublicId, full = false) {
|
|
1679
|
+
const workflow = await this.client.getWorkflow(workflowPublicId);
|
|
1680
|
+
if (!workflow) return this.toResult(`Workflow with public ID: ${workflowPublicId} not found.`);
|
|
1681
|
+
return this.toResult(`Workflow: ${workflow.id}`, await this.entityWithRelatedEntities(workflow, "workflow", full));
|
|
1682
|
+
}
|
|
1683
|
+
async listWorkflows() {
|
|
1684
|
+
const workflows = await this.client.getWorkflows();
|
|
1685
|
+
if (!workflows.length) return this.toResult(`No workflows found.`);
|
|
1686
|
+
return this.toResult(`Result (first ${workflows.length} shown of ${workflows.length} total workflows found):`, await this.entitiesWithRelatedEntities(workflows, "workflows"));
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
//#endregion
|
|
1691
|
+
export { CustomMcpServer, DocumentTools, EpicTools, IterationTools, ObjectiveTools, ShortcutClientWrapper, StoryTools, TeamTools, UserTools, WorkflowTools };
|