@shortcut/mcp 0.17.0 → 0.19.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 CHANGED
@@ -148,6 +148,10 @@ Or you can edit the local JSON file directly:
148
148
  - **stories-set-external-links** - Replace all external links on a story with a new set of links
149
149
  - **stories-get-by-external-link** - Find all stories that contain a specific external link
150
150
 
151
+ ### Labels
152
+ - **labels-list** - List all labels in the Shortcut workspace.
153
+ - **labels-create** - Create a new label in Shortcut.
154
+
151
155
  ### Epics
152
156
 
153
157
  - **epics-get-by-id** - Get a Shortcut epic by ID
@@ -188,6 +192,7 @@ Or you can edit the local JSON file directly:
188
192
  ### Documents
189
193
 
190
194
  - **documents-create** - Create a new document in Shortcut with HTML content
195
+ - **documents-update** - Update content of an existing document by its ID
191
196
  - **documents-list** - List all documents in Shortcut
192
197
  - **documents-search** - Search for documents
193
198
  - **documents-get-by-id** - Retrieve a specific document in markdown format by its ID
@@ -227,6 +232,7 @@ The following values are accepted in addition to the full tool names listed abov
227
232
  - `stories`
228
233
  - `epics`
229
234
  - `iterations`
235
+ - `labels`
230
236
  - `objectives`
231
237
  - `teams`
232
238
  - `workflows`
@@ -236,6 +242,8 @@ The following values are accepted in addition to the full tool names listed abov
236
242
 
237
243
  You can run the MCP server in read-only mode by setting the `SHORTCUT_READONLY` environment variable to `true`. This will disable all tools that modify data in Shortcut.
238
244
 
245
+ Additionally, Shortcut now supports **read-only API tokens**, which you can use to ensure that the MCP server is limited to read-only operations at the API level. This provides an additional layer of security since the restriction is enforced by the Shortcut API itself, not just the MCP server. You can create a read-only token from your [Shortcut API tokens settings](https://app.shortcut.com/settings/account/api-tokens).
246
+
239
247
  Example:
240
248
 
241
249
  ```json
package/dist/index.js CHANGED
@@ -1,8 +1,51 @@
1
1
  #!/usr/bin/env node
2
- import { CustomMcpServer, DocumentTools, EpicTools, IterationTools, ObjectiveTools, ShortcutClientWrapper, StoryTools, TeamTools, UserTools, WorkflowTools } from "./workflows-TjriXV16.js";
2
+ import { a as ObjectiveTools, c as DocumentTools, d as ShortcutClientWrapper, i as StoryTools, l as BaseTools, n as UserTools, o as IterationTools, r as TeamTools, s as EpicTools, t as WorkflowTools, u as CustomMcpServer } from "./workflows-DYvKtRHR.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { ShortcutClient } from "@shortcut/client";
5
+ import { z } from "zod";
5
6
 
7
+ //#region src/tools/labels.ts
8
+ /**
9
+ * Tools for managing Shortcut labels.
10
+ */
11
+ var LabelTools = class LabelTools extends BaseTools {
12
+ static create(client$1, server$1) {
13
+ const tools = new LabelTools(client$1);
14
+ server$1.addToolWithReadAccess("labels-list", "List all labels in the Shortcut workspace.", { includeArchived: z.boolean().optional().describe("Whether to include archived labels in the list.").default(false) }, async (params) => await tools.listLabels(params));
15
+ server$1.addToolWithWriteAccess("labels-create", "Create a new label in Shortcut.", {
16
+ name: z.string().min(1).max(128).describe("The name of the new label. Required."),
17
+ color: z.string().regex(/^#[a-fA-F0-9]{6}$/).optional().describe("The hex color to be displayed with the label (e.g., \"#ff0000\")."),
18
+ description: z.string().max(1024).optional().describe("A description of the label.")
19
+ }, async (params) => await tools.createLabel(params));
20
+ return tools;
21
+ }
22
+ formatLabel(label, { includeDescription = false, includeArchived = false } = {}) {
23
+ return {
24
+ id: label.id,
25
+ name: label.name,
26
+ app_url: label.app_url,
27
+ ...includeDescription ? { description: label.description ?? null } : {},
28
+ ...includeArchived ? { archived: label.archived } : {},
29
+ stats: Object.fromEntries(Object.entries(label.stats || {}).filter(([key, value]) => !key.match(/(unestimated|total)$/) && !!value))
30
+ };
31
+ }
32
+ async listLabels({ includeArchived = false }) {
33
+ const labels = await this.client.listLabels({ includeArchived });
34
+ if (!labels.length) return this.toResult("Result: No labels found.");
35
+ const formattedLabels = labels.map((label) => this.formatLabel(label, { includeArchived }));
36
+ return this.toResult(`Result (${labels.length} labels found):`, { labels: formattedLabels });
37
+ }
38
+ async createLabel({ name, color, description }) {
39
+ const label = await this.client.createLabel({
40
+ name,
41
+ color,
42
+ description
43
+ });
44
+ return this.toResult(`Label created with ID: ${label.id}.`, { label: this.formatLabel(label, { includeDescription: true }) });
45
+ }
46
+ };
47
+
48
+ //#endregion
6
49
  //#region src/server.ts
7
50
  let apiToken = process.env.SHORTCUT_API_TOKEN;
8
51
  let isReadonly = process.env.SHORTCUT_READONLY === "true";
@@ -29,6 +72,7 @@ ObjectiveTools.create(client, server);
29
72
  TeamTools.create(client, server);
30
73
  WorkflowTools.create(client, server);
31
74
  DocumentTools.create(client, server);
75
+ LabelTools.create(client, server);
32
76
  async function startServer() {
33
77
  try {
34
78
  const transport = new StdioServerTransport();
@@ -40,4 +84,5 @@ async function startServer() {
40
84
  }
41
85
  startServer();
42
86
 
43
- //#endregion
87
+ //#endregion
88
+ export { };
@@ -1,4 +1,4 @@
1
- import { CustomMcpServer, DocumentTools, EpicTools, IterationTools, ObjectiveTools, ShortcutClientWrapper, StoryTools, TeamTools, UserTools, WorkflowTools } from "./workflows-TjriXV16.js";
1
+ import { a as ObjectiveTools, c as DocumentTools, d as ShortcutClientWrapper, i as StoryTools, n as UserTools, o as IterationTools, r as TeamTools, s as EpicTools, t as WorkflowTools, u as CustomMcpServer } from "./workflows-DYvKtRHR.js";
2
2
  import { ShortcutClient } from "@shortcut/client";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -52,15 +52,18 @@ const logger = pino({
52
52
  function loadConfig() {
53
53
  let isReadonly = process.env.SHORTCUT_READONLY !== "false";
54
54
  let enabledTools = parseToolsList(process.env.SHORTCUT_TOOLS || "");
55
+ let httpDebug = process.env.SHORTCUT_HTTP_DEBUG === "true";
55
56
  if (process.argv.length >= 3) process.argv.slice(2).map((arg) => arg.split("=")).forEach(([name, value]) => {
56
57
  if (name === "SHORTCUT_READONLY") isReadonly = value !== "false";
57
58
  if (name === "SHORTCUT_TOOLS") enabledTools = parseToolsList(value);
59
+ if (name === "SHORTCUT_HTTP_DEBUG") httpDebug = value === "true";
58
60
  });
59
61
  return {
60
62
  port: Number.parseInt(process.env.PORT || String(DEFAULT_PORT), 10),
61
63
  isReadonly,
62
64
  enabledTools,
63
- sessionTimeoutMs: SESSION_TIMEOUT_MS
65
+ sessionTimeoutMs: SESSION_TIMEOUT_MS,
66
+ httpDebug
64
67
  };
65
68
  }
66
69
  function parseToolsList(toolsStr) {
@@ -91,8 +94,7 @@ var SessionManager = class {
91
94
  logger.info({ sessionId }, "Session initialized");
92
95
  }
93
96
  remove(sessionId) {
94
- const session = this.sessions.get(sessionId);
95
- if (session) {
97
+ if (this.sessions.get(sessionId)) {
96
98
  this.sessions.delete(sessionId);
97
99
  logger.info({ sessionId }, "Session removed");
98
100
  }
@@ -105,10 +107,7 @@ var SessionManager = class {
105
107
  cleanupStaleSessions() {
106
108
  const now = Date.now();
107
109
  const staleSessionIds = [];
108
- for (const [sessionId, session] of this.sessions.entries()) {
109
- const timeSinceLastAccess = now - session.lastAccessedAt.getTime();
110
- if (timeSinceLastAccess > this.timeoutMs) staleSessionIds.push(sessionId);
111
- }
110
+ for (const [sessionId, session] of this.sessions.entries()) if (now - session.lastAccessedAt.getTime() > this.timeoutMs) staleSessionIds.push(sessionId);
112
111
  if (staleSessionIds.length > 0) {
113
112
  logger.info({ count: staleSessionIds.length }, "Cleaning up stale sessions");
114
113
  for (const sessionId of staleSessionIds) {
@@ -156,8 +155,7 @@ function extractApiToken(req) {
156
155
  */
157
156
  async function validateApiToken(token) {
158
157
  try {
159
- const client = new ShortcutClient(token);
160
- await client.getCurrentMemberInfo();
158
+ await new ShortcutClient(token).getCurrentMemberInfo();
161
159
  return true;
162
160
  } catch (error) {
163
161
  logger.debug({ error: error instanceof Error ? error.message : error }, "API token validation failed");
@@ -242,8 +240,7 @@ async function createTransport(apiToken, config, sessionManager) {
242
240
  if (sid && sessionManager.has(sid)) sessionManager.remove(sid);
243
241
  }
244
242
  };
245
- const server = createServerInstance(apiToken, config);
246
- await server.connect(transport);
243
+ await createServerInstance(apiToken, config).connect(transport);
247
244
  return transport;
248
245
  }
249
246
  async function handleMcpPost(req, res, sessionManager, config) {
@@ -266,8 +263,7 @@ async function handleMcpPost(req, res, sessionManager, config) {
266
263
  sendUnauthorizedError(res, "API token does not match the session");
267
264
  return;
268
265
  }
269
- const session = sessionManager.get(sessionId);
270
- await session.transport.handleRequest(req, res, req.body);
266
+ await sessionManager.get(sessionId).transport.handleRequest(req, res, req.body);
271
267
  return;
272
268
  }
273
269
  if (isInitializeRequest(req.body)) {
@@ -276,15 +272,13 @@ async function handleMcpPost(req, res, sessionManager, config) {
276
272
  return;
277
273
  }
278
274
  reqLogger.info("Validating API token");
279
- const isValid = await validateApiToken(apiToken);
280
- if (!isValid) {
275
+ if (!await validateApiToken(apiToken)) {
281
276
  reqLogger.warn("API token validation failed");
282
277
  sendInvalidTokenError(res, requestId);
283
278
  return;
284
279
  }
285
280
  reqLogger.info("API token validated, creating session");
286
- const transport = await createTransport(apiToken, config, sessionManager);
287
- await transport.handleRequest(req, res, req.body);
281
+ await (await createTransport(apiToken, config, sessionManager)).handleRequest(req, res, req.body);
288
282
  return;
289
283
  }
290
284
  if (sessionId && !sessionManager.has(sessionId)) {
@@ -321,8 +315,7 @@ async function handleMcpGet(req, res, sessionManager) {
321
315
  if (lastEventId) reqLogger.info({ lastEventId }, "Client reconnecting with Last-Event-ID");
322
316
  else reqLogger.info("Establishing SSE stream");
323
317
  try {
324
- const session = sessionManager.get(sessionId);
325
- await session.transport.handleRequest(req, res);
318
+ await sessionManager.get(sessionId).transport.handleRequest(req, res);
326
319
  } catch (error) {
327
320
  reqLogger.error({ error }, "Error handling MCP GET request");
328
321
  if (!res.headersSent) res.status(500).send("Internal server error");
@@ -350,8 +343,7 @@ async function handleMcpDelete(req, res, sessionManager) {
350
343
  }
351
344
  reqLogger.info("Terminating session");
352
345
  try {
353
- const session = sessionManager.get(sessionId);
354
- await session.transport.handleRequest(req, res);
346
+ await sessionManager.get(sessionId).transport.handleRequest(req, res);
355
347
  } catch (error) {
356
348
  reqLogger.error({ error }, "Error handling session termination");
357
349
  if (!res.headersSent) res.status(500).send("Error processing session termination");
@@ -382,13 +374,43 @@ function loggingMiddleware(req, _res, next) {
382
374
  }, "Incoming request");
383
375
  next();
384
376
  }
377
+ function httpDebugRequestMiddleware(req, _res, next) {
378
+ const headers = { ...req.headers };
379
+ delete headers[HEADERS.AUTHORIZATION];
380
+ delete headers[HEADERS.X_SHORTCUT_API_TOKEN];
381
+ delete headers.cookie;
382
+ logger.info(JSON.stringify({
383
+ event: "http_request",
384
+ method: req.method,
385
+ path: req.path,
386
+ url: req.originalUrl,
387
+ query: req.query,
388
+ headers,
389
+ body: req.body
390
+ }));
391
+ next();
392
+ }
385
393
  async function startServer() {
386
394
  const config = loadConfig();
387
395
  const sessionManager = new SessionManager(config.sessionTimeoutMs);
388
396
  const app = express();
389
397
  app.use(express.json());
398
+ if (config.httpDebug) app.use(httpDebugRequestMiddleware);
390
399
  app.use(corsMiddleware);
391
400
  app.use(loggingMiddleware);
401
+ app.use((req, res, next) => {
402
+ const start = Date.now();
403
+ res.on("finish", () => {
404
+ if (res.statusCode >= 400) logger.info(JSON.stringify({
405
+ event: "http_request_failed",
406
+ method: req.method,
407
+ path: req.path,
408
+ status: res.statusCode,
409
+ ms: Date.now() - start
410
+ }));
411
+ });
412
+ next();
413
+ });
392
414
  app.get("/health", (_req, res) => {
393
415
  res.json({
394
416
  status: "ok",
@@ -427,4 +449,5 @@ startServer().catch((error) => {
427
449
  process.exit(1);
428
450
  });
429
451
 
430
- //#endregion
452
+ //#endregion
453
+ export { };
@@ -71,43 +71,37 @@ var ShortcutClientWrapper = class {
71
71
  }
72
72
  async loadMembers() {
73
73
  if (this.userCache.isStale) {
74
- const response = await this.client.listMembers({});
75
- const members = response?.data ?? null;
74
+ const members = (await this.client.listMembers({}))?.data ?? null;
76
75
  if (members) this.userCache.setMany(members.map((member) => [member.id, member]));
77
76
  }
78
77
  }
79
78
  async loadTeams() {
80
79
  if (this.teamCache.isStale) {
81
- const response = await this.client.listGroups();
82
- const groups = response?.data ?? null;
80
+ const groups = (await this.client.listGroups())?.data ?? null;
83
81
  if (groups) this.teamCache.setMany(groups.map((group) => [group.id, group]));
84
82
  }
85
83
  }
86
84
  async loadWorkflows() {
87
85
  if (this.workflowCache.isStale) {
88
- const response = await this.client.listWorkflows();
89
- const workflows = response?.data ?? null;
86
+ const workflows = (await this.client.listWorkflows())?.data ?? null;
90
87
  if (workflows) this.workflowCache.setMany(workflows.map((workflow) => [workflow.id, workflow]));
91
88
  }
92
89
  }
93
90
  async loadCustomFields() {
94
91
  if (this.customFieldCache.isStale) {
95
- const response = await this.client.listCustomFields();
96
- const customFields = response?.data ?? null;
92
+ const customFields = (await this.client.listCustomFields())?.data ?? null;
97
93
  if (customFields) this.customFieldCache.setMany(customFields.map((customField) => [customField.id, customField]));
98
94
  }
99
95
  }
100
96
  async getCurrentUser() {
101
97
  if (this.currentUser) return this.currentUser;
102
- const response = await this.client.getCurrentMemberInfo();
103
- const user$1 = response?.data;
98
+ const user$1 = (await this.client.getCurrentMemberInfo())?.data;
104
99
  if (!user$1) return null;
105
100
  this.currentUser = user$1;
106
101
  return user$1;
107
102
  }
108
103
  async getUser(userId) {
109
- const response = await this.client.getMember(userId, {});
110
- const user$1 = response?.data;
104
+ const user$1 = (await this.client.getMember(userId, {}))?.data;
111
105
  if (!user$1) return null;
112
106
  return user$1;
113
107
  }
@@ -121,8 +115,7 @@ var ShortcutClientWrapper = class {
121
115
  }
122
116
  async listMembers() {
123
117
  await this.loadMembers();
124
- const members = Array.from(this.userCache.values());
125
- return members;
118
+ return Array.from(this.userCache.values());
126
119
  }
127
120
  async getWorkflowMap(workflowIds) {
128
121
  await this.loadWorkflows();
@@ -133,23 +126,20 @@ var ShortcutClientWrapper = class {
133
126
  return Array.from(this.workflowCache.values());
134
127
  }
135
128
  async getWorkflow(workflowPublicId) {
136
- const response = await this.client.getWorkflow(workflowPublicId);
137
- const workflow = response?.data;
129
+ const workflow = (await this.client.getWorkflow(workflowPublicId))?.data;
138
130
  if (!workflow) return null;
139
131
  return workflow;
140
132
  }
141
133
  async getTeams() {
142
134
  await this.loadTeams();
143
- const teams = Array.from(this.teamCache.values());
144
- return teams;
135
+ return Array.from(this.teamCache.values());
145
136
  }
146
137
  async getTeamMap(teamIds) {
147
138
  await this.loadTeams();
148
139
  return new Map(teamIds.map((id) => [id, this.teamCache.get(id)]).filter((team) => team[1] !== null));
149
140
  }
150
141
  async getTeam(teamPublicId) {
151
- const response = await this.client.getGroup(teamPublicId);
152
- const group = response?.data;
142
+ const group = (await this.client.getGroup(teamPublicId))?.data;
153
143
  if (!group) return null;
154
144
  return group;
155
145
  }
@@ -166,26 +156,22 @@ var ShortcutClientWrapper = class {
166
156
  return story;
167
157
  }
168
158
  async getStory(storyPublicId) {
169
- const response = await this.client.getStory(storyPublicId);
170
- const story = response?.data ?? null;
159
+ const story = (await this.client.getStory(storyPublicId))?.data ?? null;
171
160
  if (!story) return null;
172
161
  return story;
173
162
  }
174
163
  async getEpic(epicPublicId) {
175
- const response = await this.client.getEpic(epicPublicId);
176
- const epic = response?.data ?? null;
164
+ const epic = (await this.client.getEpic(epicPublicId))?.data ?? null;
177
165
  if (!epic) return null;
178
166
  return epic;
179
167
  }
180
168
  async getIteration(iterationPublicId) {
181
- const response = await this.client.getIteration(iterationPublicId);
182
- const iteration = response?.data ?? null;
169
+ const iteration = (await this.client.getIteration(iterationPublicId))?.data ?? null;
183
170
  if (!iteration) return null;
184
171
  return iteration;
185
172
  }
186
173
  async getMilestone(milestonePublicId) {
187
- const response = await this.client.getMilestone(milestonePublicId);
188
- const milestone = response?.data ?? null;
174
+ const milestone = (await this.client.getMilestone(milestonePublicId))?.data ?? null;
189
175
  if (!milestone) return null;
190
176
  return milestone;
191
177
  }
@@ -232,11 +218,10 @@ var ShortcutClientWrapper = class {
232
218
  };
233
219
  }
234
220
  async getActiveIteration(teamIds) {
235
- const response = await this.client.listIterations();
236
- const iterations = response?.data;
221
+ const iterations = (await this.client.listIterations())?.data;
237
222
  if (!iterations) return /* @__PURE__ */ new Map();
238
223
  const [today] = (/* @__PURE__ */ new Date()).toISOString().split("T");
239
- const activeIterationByTeam = iterations.reduce((acc, iteration) => {
224
+ return iterations.reduce((acc, iteration) => {
240
225
  if (iteration.status !== "started") return acc;
241
226
  const [startDate] = new Date(iteration.start_date).toISOString().split("T");
242
227
  const [endDate] = new Date(iteration.end_date).toISOString().split("T");
@@ -251,14 +236,12 @@ var ShortcutClientWrapper = class {
251
236
  }
252
237
  return acc;
253
238
  }, /* @__PURE__ */ new Map());
254
- return activeIterationByTeam;
255
239
  }
256
240
  async getUpcomingIteration(teamIds) {
257
- const response = await this.client.listIterations();
258
- const iterations = response?.data;
241
+ const iterations = (await this.client.listIterations())?.data;
259
242
  if (!iterations) return /* @__PURE__ */ new Map();
260
243
  const [today] = (/* @__PURE__ */ new Date()).toISOString().split("T");
261
- const upcomingIterationByTeam = iterations.reduce((acc, iteration) => {
244
+ return iterations.reduce((acc, iteration) => {
262
245
  if (iteration.status !== "unstarted") return acc;
263
246
  const [startDate] = new Date(iteration.start_date).toISOString().split("T");
264
247
  const [endDate] = new Date(iteration.end_date).toISOString().split("T");
@@ -273,7 +256,6 @@ var ShortcutClientWrapper = class {
273
256
  }
274
257
  return acc;
275
258
  }, /* @__PURE__ */ new Map());
276
- return upcomingIterationByTeam;
277
259
  }
278
260
  async searchEpics(query, nextToken) {
279
261
  const response = await this.client.searchEpics({
@@ -318,8 +300,7 @@ var ShortcutClientWrapper = class {
318
300
  };
319
301
  }
320
302
  async listIterationStories(iterationPublicId, includeDescription = false) {
321
- const response = await this.client.listIterationStories(iterationPublicId, { includes_description: includeDescription });
322
- const stories = response?.data;
303
+ const stories = (await this.client.listIterationStories(iterationPublicId, { includes_description: includeDescription }))?.data;
323
304
  if (!stories) return {
324
305
  stories: null,
325
306
  total: null
@@ -397,13 +378,11 @@ var ShortcutClientWrapper = class {
397
378
  async removeExternalLinkFromStory(storyPublicId, externalLink) {
398
379
  const story = await this.getStory(storyPublicId);
399
380
  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());
381
+ const updatedLinks = (story.external_links || []).filter((link) => link.toLowerCase() !== externalLink.toLowerCase());
402
382
  return await this.updateStory(storyPublicId, { external_links: updatedLinks });
403
383
  }
404
384
  async getStoriesByExternalLink(externalLink) {
405
- const response = await this.client.getExternalLinkStories({ external_link: externalLink.toLowerCase() });
406
- const stories = response?.data;
385
+ const stories = (await this.client.getExternalLinkStories({ external_link: externalLink.toLowerCase() }))?.data;
407
386
  if (!stories) return {
408
387
  stories: null,
409
388
  total: null
@@ -422,6 +401,12 @@ var ShortcutClientWrapper = class {
422
401
  if (!doc) throw new Error(`Failed to create the document: ${response.status}`);
423
402
  return doc;
424
403
  }
404
+ async updateDoc(docPublicId, params) {
405
+ const response = await this.client.updateDoc(docPublicId, params);
406
+ const doc = response?.data ?? null;
407
+ if (!doc) throw new Error(`Failed to update the document: ${response.status}`);
408
+ return doc;
409
+ }
425
410
  async listDocs() {
426
411
  const response = await this.client.listDocs();
427
412
  if (response.status === 403) throw new Error("Docs feature disabled for this workspace.");
@@ -476,12 +461,23 @@ var ShortcutClientWrapper = class {
476
461
  await this.loadCustomFields();
477
462
  return Array.from(this.customFieldCache.values());
478
463
  }
464
+ async listLabels({ includeArchived = false }) {
465
+ const allLabels = (await this.client.listLabels({ slim: false }))?.data ?? [];
466
+ if (includeArchived) return allLabels;
467
+ return allLabels.filter((label) => !label.archived);
468
+ }
469
+ async createLabel(params) {
470
+ const response = await this.client.createLabel(params);
471
+ const label = response?.data ?? null;
472
+ if (!label) throw new Error(`Failed to create the label: ${response.status}`);
473
+ return label;
474
+ }
479
475
  };
480
476
 
481
477
  //#endregion
482
478
  //#region package.json
483
479
  var name = "@shortcut/mcp";
484
- var version = "0.17.0";
480
+ var version = "0.19.0";
485
481
 
486
482
  //#endregion
487
483
  //#region src/mcp/CustomMcpServer.ts
@@ -527,15 +523,14 @@ var BaseTools = class {
527
523
  }
528
524
  renameEntityProps(entity) {
529
525
  if (!entity || typeof entity !== "object") return entity;
530
- const renames = [
526
+ for (const [from, to] of [
531
527
  ["team_id", null],
532
528
  ["entity_type", null],
533
529
  ["group_id", "team_id"],
534
530
  ["group_ids", "team_ids"],
535
531
  ["milestone_id", "objective_id"],
536
532
  ["milestone_ids", "objective_ids"]
537
- ];
538
- for (const [from, to] of renames) if (from in entity) {
533
+ ]) if (from in entity) {
539
534
  const value = entity[from];
540
535
  delete entity[from];
541
536
  if (to) entity = {
@@ -857,10 +852,15 @@ var BaseTools = class {
857
852
  var DocumentTools = class DocumentTools extends BaseTools {
858
853
  static create(client, server) {
859
854
  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.", {
855
+ 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 Markdown format for the content.", {
861
856
  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>)")
857
+ content: z.string().describe("The content for the new document in Markdown format.")
863
858
  }, async ({ title, content }) => await tools.createDocument(title, content));
859
+ server.addToolWithWriteAccess("documents-update", "Update the content and/or title of an existing document in Shortcut.", {
860
+ docId: z.string().describe("The ID of the document to retrieve"),
861
+ title: z.string().max(256).describe("The title for the document (max 256 characters)").optional(),
862
+ content: z.string().describe("The updated content for the document in Markdown format").optional()
863
+ }, async ({ docId, content, title }) => await tools.updateDocument(docId, title, content));
864
864
  server.addToolWithReadAccess("documents-list", "List all documents in Shortcut.", async () => await tools.listDocuments());
865
865
  server.addToolWithReadAccess("documents-search", "Find documents.", {
866
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."),
@@ -881,7 +881,8 @@ var DocumentTools = class DocumentTools extends BaseTools {
881
881
  try {
882
882
  const doc = await this.client.createDoc({
883
883
  title,
884
- content
884
+ content,
885
+ content_format: "markdown"
885
886
  });
886
887
  return this.toResult("Document created successfully", {
887
888
  id: doc.id,
@@ -893,6 +894,26 @@ var DocumentTools = class DocumentTools extends BaseTools {
893
894
  return this.toResult(`Failed to create document: ${errorMessage}`);
894
895
  }
895
896
  }
897
+ async updateDocument(docId, title, content) {
898
+ try {
899
+ const doc = await this.client.getDocById(docId);
900
+ if (!doc) return this.toResult(`Document with ID ${docId} not found.`);
901
+ const result = await this.client.updateDoc(docId, {
902
+ title: title ?? doc.title ?? "",
903
+ content: content ?? doc.content_markdown ?? "",
904
+ content_format: "markdown"
905
+ });
906
+ return this.toResult("Document updated successfully", {
907
+ id: result.id,
908
+ title: result.title,
909
+ content: result.content_markdown,
910
+ app_url: result.app_url
911
+ });
912
+ } catch (error) {
913
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
914
+ return this.toResult(`Failed to update document: ${errorMessage}`);
915
+ }
916
+ }
896
917
  async listDocuments() {
897
918
  try {
898
919
  const docs = await this.client.listDocs();
@@ -942,7 +963,7 @@ const getKey = (prop) => {
942
963
  return mapKeyName(prop);
943
964
  };
944
965
  const buildSearchQuery = async (params, currentUser) => {
945
- const query = Object.entries(params).map(([key, value]) => {
966
+ return Object.entries(params).map(([key, value]) => {
946
967
  const q = getKey(key);
947
968
  if (key === "owner" || key === "requester") {
948
969
  if (value === "me") return `${q}:${currentUser?.mention_name || value}`;
@@ -953,7 +974,6 @@ const buildSearchQuery = async (params, currentUser) => {
953
974
  if (typeof value === "string" && value.includes(" ")) return `${q}:"${value}"`;
954
975
  return `${q}:${value}`;
955
976
  }).join(" ");
956
- return query;
957
977
  };
958
978
 
959
979
  //#endregion
@@ -1021,7 +1041,7 @@ var EpicTools = class EpicTools extends BaseTools {
1021
1041
  updated: date(),
1022
1042
  completed: date(),
1023
1043
  due: date()
1024
- }, async ({ nextPageToken,...params }) => await tools.searchEpics(params, nextPageToken));
1044
+ }, async ({ nextPageToken, ...params }) => await tools.searchEpics(params, nextPageToken));
1025
1045
  server.addToolWithWriteAccess("epics-create", "Create a new Shortcut epic.", {
1026
1046
  name: z.string().describe("The name of the epic"),
1027
1047
  owner: z.string().optional().describe("The user ID of the owner of the epic"),
@@ -1031,8 +1051,7 @@ var EpicTools = class EpicTools extends BaseTools {
1031
1051
  return tools;
1032
1052
  }
1033
1053
  async searchEpics(params, nextToken) {
1034
- const currentUser = await this.client.getCurrentUser();
1035
- const query = await buildSearchQuery(params, currentUser);
1054
+ const query = await buildSearchQuery(params, await this.client.getCurrentUser());
1036
1055
  const { epics, total, next_page_token } = await this.client.searchEpics(query, nextToken);
1037
1056
  if (!epics) throw new Error(`Failed to search for epics matching your query: "${query}"`);
1038
1057
  if (!epics.length) return this.toResult(`Result: No epics found.`);
@@ -1082,7 +1101,7 @@ var IterationTools = class IterationTools extends BaseTools {
1082
1101
  updated: date(),
1083
1102
  startDate: date(),
1084
1103
  endDate: date()
1085
- }, async ({ nextPageToken,...params }) => await tools.searchIterations(params, nextPageToken));
1104
+ }, async ({ nextPageToken, ...params }) => await tools.searchIterations(params, nextPageToken));
1086
1105
  server.addToolWithWriteAccess("iterations-create", "Create a new Shortcut iteration", {
1087
1106
  name: z.string().describe("The name of the iteration"),
1088
1107
  startDate: z.string().describe("The start date of the iteration in YYYY-MM-DD format"),
@@ -1100,8 +1119,7 @@ var IterationTools = class IterationTools extends BaseTools {
1100
1119
  return this.toResult(`Result (${stories.length} stories found):`, await this.entitiesWithRelatedEntities(stories, "stories"));
1101
1120
  }
1102
1121
  async searchIterations(params, nextToken) {
1103
- const currentUser = await this.client.getCurrentUser();
1104
- const query = await buildSearchQuery(params, currentUser);
1122
+ const query = await buildSearchQuery(params, await this.client.getCurrentUser());
1105
1123
  const { iterations, total, next_page_token } = await this.client.searchIterations(query, nextToken);
1106
1124
  if (!iterations) throw new Error(`Failed to search for iterations matching your query: "${query}".`);
1107
1125
  if (!iterations.length) return this.toResult(`Result: No iterations found.`);
@@ -1125,41 +1143,33 @@ var IterationTools = class IterationTools extends BaseTools {
1125
1143
  }
1126
1144
  async getActiveIterations(teamId) {
1127
1145
  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);
1146
+ if (!await this.client.getTeam(teamId)) throw new Error(`No team found matching id: "${teamId}"`);
1147
+ const iterations = (await this.client.getActiveIteration([teamId])).get(teamId);
1132
1148
  if (!iterations?.length) return this.toResult(`Result: No active iterations found for team.`);
1133
1149
  if (iterations.length === 1) return this.toResult("The active iteration for the team is:", await this.entityWithRelatedEntities(iterations[0], "iteration"));
1134
1150
  return this.toResult("The active iterations for the team are:", await this.entitiesWithRelatedEntities(iterations, "iterations"));
1135
1151
  }
1136
1152
  const currentUser = await this.client.getCurrentUser();
1137
1153
  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);
1154
+ const teamIds = (await this.client.getTeams()).filter((team) => team.member_ids.includes(currentUser.id)).map((team) => team.id);
1140
1155
  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();
1156
+ const allActiveIterations = [...(await this.client.getActiveIteration(teamIds)).values()].flat();
1143
1157
  if (!allActiveIterations.length) return this.toResult("Result: No active iterations found for any of your teams.");
1144
1158
  return this.toResult(`You have ${allActiveIterations.length} active iterations for your teams:`, await this.entitiesWithRelatedEntities(allActiveIterations, "iterations"));
1145
1159
  }
1146
1160
  async getUpcomingIterations(teamId) {
1147
1161
  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);
1162
+ if (!await this.client.getTeam(teamId)) throw new Error(`No team found matching id: "${teamId}"`);
1163
+ const iterations = (await this.client.getUpcomingIteration([teamId])).get(teamId);
1152
1164
  if (!iterations?.length) return this.toResult(`Result: No upcoming iterations found for team.`);
1153
1165
  if (iterations.length === 1) return this.toResult("The next upcoming iteration for the team is:", await this.entityWithRelatedEntities(iterations[0], "iteration"));
1154
1166
  return this.toResult("The next upcoming iterations for the team are:", await this.entitiesWithRelatedEntities(iterations, "iterations"));
1155
1167
  }
1156
1168
  const currentUser = await this.client.getCurrentUser();
1157
1169
  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);
1170
+ const teamIds = (await this.client.getTeams()).filter((team) => team.member_ids.includes(currentUser.id)).map((team) => team.id);
1160
1171
  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();
1172
+ const allUpcomingIterations = [...(await this.client.getUpcomingIteration(teamIds)).values()].flat();
1163
1173
  if (!allUpcomingIterations.length) return this.toResult("Result: No upcoming iterations found for any of your teams.");
1164
1174
  return this.toResult("The upcoming iterations for all your teams are:", await this.entitiesWithRelatedEntities(allUpcomingIterations, "iterations"));
1165
1175
  }
@@ -1195,12 +1205,11 @@ var ObjectiveTools = class ObjectiveTools extends BaseTools {
1195
1205
  created: date(),
1196
1206
  updated: date(),
1197
1207
  completed: date()
1198
- }, async ({ nextPageToken,...params }) => await tools.searchObjectives(params, nextPageToken));
1208
+ }, async ({ nextPageToken, ...params }) => await tools.searchObjectives(params, nextPageToken));
1199
1209
  return tools;
1200
1210
  }
1201
1211
  async searchObjectives(params, nextToken) {
1202
- const currentUser = await this.client.getCurrentUser();
1203
- const query = await buildSearchQuery(params, currentUser);
1212
+ const query = await buildSearchQuery(params, await this.client.getCurrentUser());
1204
1213
  const { milestones, total, next_page_token } = await this.client.searchMilestones(query, nextToken);
1205
1214
  if (!milestones) throw new Error(`Failed to search for milestones matching your query: "${query}"`);
1206
1215
  if (!milestones.length) return this.toResult(`Result: No milestones found.`);
@@ -1272,7 +1281,7 @@ var StoryTools = class StoryTools extends BaseTools {
1272
1281
  updated: date(),
1273
1282
  completed: date(),
1274
1283
  due: date()
1275
- }, async ({ nextPageToken,...params }) => await tools.searchStories(params, nextPageToken));
1284
+ }, async ({ nextPageToken, ...params }) => await tools.searchStories(params, nextPageToken));
1276
1285
  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
1286
  server.addToolWithWriteAccess("stories-create", `Create a new Shortcut story.
1278
1287
  Name is required, and either a Team or Workflow must be specified:
@@ -1411,10 +1420,7 @@ The story will be added to the default state for the workflow.
1411
1420
  }
1412
1421
  async createStory({ name: name$1, description, type, owner, epic, iteration, team, workflow }) {
1413
1422
  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
- }
1423
+ if (!workflow && team) workflow = (await this.client.getTeam(team))?.workflow_ids?.[0];
1418
1424
  if (!workflow) throw new Error("Failed to find workflow for team");
1419
1425
  const fullWorkflow = await this.client.getWorkflow(workflow);
1420
1426
  if (!fullWorkflow) throw new Error("Failed to find workflow");
@@ -1453,23 +1459,19 @@ The story will be added to the default state for the workflow.
1453
1459
  async addStoryAsSubTask({ parentStoryPublicId, subTaskPublicId }) {
1454
1460
  if (!parentStoryPublicId) throw new Error("ID of parent story is required");
1455
1461
  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}`);
1462
+ if (!await this.client.getStory(subTaskPublicId)) throw new Error(`Failed to retrieve story with public ID: ${subTaskPublicId}`);
1463
+ if (!await this.client.getStory(parentStoryPublicId)) throw new Error(`Failed to retrieve parent story with public ID: ${parentStoryPublicId}`);
1460
1464
  await this.client.updateStory(subTaskPublicId, { parent_story_id: parentStoryPublicId });
1461
1465
  return this.toResult(`Added story sc-${subTaskPublicId} as a sub-task of sc-${parentStoryPublicId}`);
1462
1466
  }
1463
1467
  async removeSubTaskFromParent({ subTaskPublicId }) {
1464
1468
  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}`);
1469
+ if (!await this.client.getStory(subTaskPublicId)) throw new Error(`Failed to retrieve story with public ID: ${subTaskPublicId}`);
1467
1470
  await this.client.updateStory(subTaskPublicId, { parent_story_id: null });
1468
1471
  return this.toResult(`Removed story sc-${subTaskPublicId} from its parent story`);
1469
1472
  }
1470
1473
  async searchStories(params, nextToken) {
1471
- const currentUser = await this.client.getCurrentUser();
1472
- const query = await buildSearchQuery(params, currentUser);
1474
+ const query = await buildSearchQuery(params, await this.client.getCurrentUser());
1473
1475
  const { stories, total, next_page_token } = await this.client.searchStories(query, nextToken);
1474
1476
  if (!stories) throw new Error(`Failed to search for stories matching your query: "${query}".`);
1475
1477
  if (!stories.length) return this.toResult(`Result: No stories found.`);
@@ -1483,15 +1485,13 @@ The story will be added to the default state for the workflow.
1483
1485
  async createStoryComment({ storyPublicId, text }) {
1484
1486
  if (!storyPublicId) throw new Error("Story public ID is required");
1485
1487
  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
+ if (!await this.client.getStory(storyPublicId)) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
1488
1489
  const storyComment = await this.client.createStoryComment(storyPublicId, { text });
1489
1490
  return this.toResult(`Created comment on story sc-${storyPublicId}. Comment URL: ${storyComment.app_url}.`);
1490
1491
  }
1491
- async updateStory({ storyPublicId,...updates }) {
1492
+ async updateStory({ storyPublicId, ...updates }) {
1492
1493
  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}`);
1494
+ if (!await this.client.getStory(storyPublicId)) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
1495
1495
  const updateParams = {};
1496
1496
  if (updates.name !== void 0) updateParams.name = updates.name;
1497
1497
  if (updates.description !== void 0) updateParams.description = updates.description;
@@ -1508,8 +1508,7 @@ The story will be added to the default state for the workflow.
1508
1508
  async uploadFileToStory(storyPublicId, filePath) {
1509
1509
  if (!storyPublicId) throw new Error("Story public ID is required");
1510
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}`);
1511
+ if (!await this.client.getStory(storyPublicId)) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
1513
1512
  const uploadedFile = await this.client.uploadFile(storyPublicId, filePath);
1514
1513
  if (!uploadedFile) throw new Error(`Failed to upload file to story sc-${storyPublicId}`);
1515
1514
  return this.toResult(`Uploaded file "${uploadedFile.name}" to story sc-${storyPublicId}. File ID is: ${uploadedFile.id}`);
@@ -1517,11 +1516,9 @@ The story will be added to the default state for the workflow.
1517
1516
  async addTaskToStory({ storyPublicId, taskDescription, taskOwnerIds }) {
1518
1517
  if (!storyPublicId) throw new Error("Story public ID is required");
1519
1518
  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}`);
1519
+ if (!await this.client.getStory(storyPublicId)) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
1522
1520
  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(", ")}`);
1521
+ if (!await this.client.getUserMap(taskOwnerIds)) throw new Error(`Failed to retrieve users with IDs: ${taskOwnerIds.join(", ")}`);
1525
1522
  }
1526
1523
  const task = await this.client.addTaskToStory(storyPublicId, {
1527
1524
  description: taskDescription,
@@ -1532,10 +1529,8 @@ The story will be added to the default state for the workflow.
1532
1529
  async updateTask({ storyPublicId, taskPublicId, taskDescription, taskOwnerIds, isCompleted }) {
1533
1530
  if (!storyPublicId) throw new Error("Story public ID is required");
1534
1531
  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}`);
1532
+ if (!await this.client.getStory(storyPublicId)) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
1533
+ if (!await this.client.getTask(storyPublicId, taskPublicId)) throw new Error(`Failed to retrieve Shortcut task with public ID: ${taskPublicId}`);
1539
1534
  const updatedTask = await this.client.updateTask(storyPublicId, taskPublicId, {
1540
1535
  description: taskDescription,
1541
1536
  ownerIds: taskOwnerIds,
@@ -1548,10 +1543,8 @@ The story will be added to the default state for the workflow.
1548
1543
  async addRelationToStory({ storyPublicId, relatedStoryPublicId, relationshipType }) {
1549
1544
  if (!storyPublicId) throw new Error("Story public ID is required");
1550
1545
  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}`);
1546
+ if (!await this.client.getStory(storyPublicId)) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
1547
+ if (!await this.client.getStory(relatedStoryPublicId)) throw new Error(`Failed to retrieve Shortcut story with public ID: ${relatedStoryPublicId}`);
1555
1548
  let subjectStoryId = storyPublicId;
1556
1549
  let objectStoryId = relatedStoryPublicId;
1557
1550
  if (relationshipType === "blocked by" || relationshipType === "duplicated by") {
@@ -1688,4 +1681,4 @@ var WorkflowTools = class WorkflowTools extends BaseTools {
1688
1681
  };
1689
1682
 
1690
1683
  //#endregion
1691
- export { CustomMcpServer, DocumentTools, EpicTools, IterationTools, ObjectiveTools, ShortcutClientWrapper, StoryTools, TeamTools, UserTools, WorkflowTools };
1684
+ export { ObjectiveTools as a, DocumentTools as c, ShortcutClientWrapper as d, StoryTools as i, BaseTools as l, UserTools as n, IterationTools as o, TeamTools as r, EpicTools as s, WorkflowTools as t, CustomMcpServer as u };
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "modelcontextprotocol"
13
13
  ],
14
14
  "license": "MIT",
15
- "version": "0.17.0",
15
+ "version": "0.19.0",
16
16
  "type": "module",
17
17
  "main": "dist/index.js",
18
18
  "bin": {
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@modelcontextprotocol/sdk": "^1.25.1",
38
- "@shortcut/client": "^3.1.0",
38
+ "@shortcut/client": "^3.2.0",
39
39
  "express": "^4.18.2",
40
40
  "pino": "^9.5.0",
41
41
  "pino-http": "^10.3.0",