@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.
@@ -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 };