@shortcut/mcp 0.18.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
@@ -192,6 +192,7 @@ Or you can edit the local JSON file directly:
192
192
  ### Documents
193
193
 
194
194
  - **documents-create** - Create a new document in Shortcut with HTML content
195
+ - **documents-update** - Update content of an existing document by its ID
195
196
  - **documents-list** - List all documents in Shortcut
196
197
  - **documents-search** - Search for documents
197
198
  - **documents-get-by-id** - Retrieve a specific document in markdown format by its ID
@@ -241,6 +242,8 @@ The following values are accepted in addition to the full tool names listed abov
241
242
 
242
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.
243
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
+
244
247
  Example:
245
248
 
246
249
  ```json
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { BaseTools, CustomMcpServer, DocumentTools, EpicTools, IterationTools, ObjectiveTools, ShortcutClientWrapper, StoryTools, TeamTools, UserTools, WorkflowTools } from "./workflows-Dko3ibgz.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
5
  import { z } from "zod";
@@ -84,4 +84,5 @@ async function startServer() {
84
84
  }
85
85
  startServer();
86
86
 
87
- //#endregion
87
+ //#endregion
88
+ export { };
@@ -1,4 +1,4 @@
1
- import { CustomMcpServer, DocumentTools, EpicTools, IterationTools, ObjectiveTools, ShortcutClientWrapper, StoryTools, TeamTools, UserTools, WorkflowTools } from "./workflows-Dko3ibgz.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.");
@@ -477,8 +462,7 @@ var ShortcutClientWrapper = class {
477
462
  return Array.from(this.customFieldCache.values());
478
463
  }
479
464
  async listLabels({ includeArchived = false }) {
480
- const response = await this.client.listLabels({ slim: false });
481
- const allLabels = response?.data ?? [];
465
+ const allLabels = (await this.client.listLabels({ slim: false }))?.data ?? [];
482
466
  if (includeArchived) return allLabels;
483
467
  return allLabels.filter((label) => !label.archived);
484
468
  }
@@ -493,7 +477,7 @@ var ShortcutClientWrapper = class {
493
477
  //#endregion
494
478
  //#region package.json
495
479
  var name = "@shortcut/mcp";
496
- var version = "0.18.0";
480
+ var version = "0.19.0";
497
481
 
498
482
  //#endregion
499
483
  //#region src/mcp/CustomMcpServer.ts
@@ -539,15 +523,14 @@ var BaseTools = class {
539
523
  }
540
524
  renameEntityProps(entity) {
541
525
  if (!entity || typeof entity !== "object") return entity;
542
- const renames = [
526
+ for (const [from, to] of [
543
527
  ["team_id", null],
544
528
  ["entity_type", null],
545
529
  ["group_id", "team_id"],
546
530
  ["group_ids", "team_ids"],
547
531
  ["milestone_id", "objective_id"],
548
532
  ["milestone_ids", "objective_ids"]
549
- ];
550
- for (const [from, to] of renames) if (from in entity) {
533
+ ]) if (from in entity) {
551
534
  const value = entity[from];
552
535
  delete entity[from];
553
536
  if (to) entity = {
@@ -869,10 +852,15 @@ var BaseTools = class {
869
852
  var DocumentTools = class DocumentTools extends BaseTools {
870
853
  static create(client, server) {
871
854
  const tools = new DocumentTools(client);
872
- 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.", {
873
856
  title: z.string().max(256).describe("The title for the new document (max 256 characters)"),
874
- 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.")
875
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));
876
864
  server.addToolWithReadAccess("documents-list", "List all documents in Shortcut.", async () => await tools.listDocuments());
877
865
  server.addToolWithReadAccess("documents-search", "Find documents.", {
878
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."),
@@ -893,7 +881,8 @@ var DocumentTools = class DocumentTools extends BaseTools {
893
881
  try {
894
882
  const doc = await this.client.createDoc({
895
883
  title,
896
- content
884
+ content,
885
+ content_format: "markdown"
897
886
  });
898
887
  return this.toResult("Document created successfully", {
899
888
  id: doc.id,
@@ -905,6 +894,26 @@ var DocumentTools = class DocumentTools extends BaseTools {
905
894
  return this.toResult(`Failed to create document: ${errorMessage}`);
906
895
  }
907
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
+ }
908
917
  async listDocuments() {
909
918
  try {
910
919
  const docs = await this.client.listDocs();
@@ -954,7 +963,7 @@ const getKey = (prop) => {
954
963
  return mapKeyName(prop);
955
964
  };
956
965
  const buildSearchQuery = async (params, currentUser) => {
957
- const query = Object.entries(params).map(([key, value]) => {
966
+ return Object.entries(params).map(([key, value]) => {
958
967
  const q = getKey(key);
959
968
  if (key === "owner" || key === "requester") {
960
969
  if (value === "me") return `${q}:${currentUser?.mention_name || value}`;
@@ -965,7 +974,6 @@ const buildSearchQuery = async (params, currentUser) => {
965
974
  if (typeof value === "string" && value.includes(" ")) return `${q}:"${value}"`;
966
975
  return `${q}:${value}`;
967
976
  }).join(" ");
968
- return query;
969
977
  };
970
978
 
971
979
  //#endregion
@@ -1033,7 +1041,7 @@ var EpicTools = class EpicTools extends BaseTools {
1033
1041
  updated: date(),
1034
1042
  completed: date(),
1035
1043
  due: date()
1036
- }, async ({ nextPageToken,...params }) => await tools.searchEpics(params, nextPageToken));
1044
+ }, async ({ nextPageToken, ...params }) => await tools.searchEpics(params, nextPageToken));
1037
1045
  server.addToolWithWriteAccess("epics-create", "Create a new Shortcut epic.", {
1038
1046
  name: z.string().describe("The name of the epic"),
1039
1047
  owner: z.string().optional().describe("The user ID of the owner of the epic"),
@@ -1043,8 +1051,7 @@ var EpicTools = class EpicTools extends BaseTools {
1043
1051
  return tools;
1044
1052
  }
1045
1053
  async searchEpics(params, nextToken) {
1046
- const currentUser = await this.client.getCurrentUser();
1047
- const query = await buildSearchQuery(params, currentUser);
1054
+ const query = await buildSearchQuery(params, await this.client.getCurrentUser());
1048
1055
  const { epics, total, next_page_token } = await this.client.searchEpics(query, nextToken);
1049
1056
  if (!epics) throw new Error(`Failed to search for epics matching your query: "${query}"`);
1050
1057
  if (!epics.length) return this.toResult(`Result: No epics found.`);
@@ -1094,7 +1101,7 @@ var IterationTools = class IterationTools extends BaseTools {
1094
1101
  updated: date(),
1095
1102
  startDate: date(),
1096
1103
  endDate: date()
1097
- }, async ({ nextPageToken,...params }) => await tools.searchIterations(params, nextPageToken));
1104
+ }, async ({ nextPageToken, ...params }) => await tools.searchIterations(params, nextPageToken));
1098
1105
  server.addToolWithWriteAccess("iterations-create", "Create a new Shortcut iteration", {
1099
1106
  name: z.string().describe("The name of the iteration"),
1100
1107
  startDate: z.string().describe("The start date of the iteration in YYYY-MM-DD format"),
@@ -1112,8 +1119,7 @@ var IterationTools = class IterationTools extends BaseTools {
1112
1119
  return this.toResult(`Result (${stories.length} stories found):`, await this.entitiesWithRelatedEntities(stories, "stories"));
1113
1120
  }
1114
1121
  async searchIterations(params, nextToken) {
1115
- const currentUser = await this.client.getCurrentUser();
1116
- const query = await buildSearchQuery(params, currentUser);
1122
+ const query = await buildSearchQuery(params, await this.client.getCurrentUser());
1117
1123
  const { iterations, total, next_page_token } = await this.client.searchIterations(query, nextToken);
1118
1124
  if (!iterations) throw new Error(`Failed to search for iterations matching your query: "${query}".`);
1119
1125
  if (!iterations.length) return this.toResult(`Result: No iterations found.`);
@@ -1137,41 +1143,33 @@ var IterationTools = class IterationTools extends BaseTools {
1137
1143
  }
1138
1144
  async getActiveIterations(teamId) {
1139
1145
  if (teamId) {
1140
- const team = await this.client.getTeam(teamId);
1141
- if (!team) throw new Error(`No team found matching id: "${teamId}"`);
1142
- const result = await this.client.getActiveIteration([teamId]);
1143
- 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);
1144
1148
  if (!iterations?.length) return this.toResult(`Result: No active iterations found for team.`);
1145
1149
  if (iterations.length === 1) return this.toResult("The active iteration for the team is:", await this.entityWithRelatedEntities(iterations[0], "iteration"));
1146
1150
  return this.toResult("The active iterations for the team are:", await this.entitiesWithRelatedEntities(iterations, "iterations"));
1147
1151
  }
1148
1152
  const currentUser = await this.client.getCurrentUser();
1149
1153
  if (!currentUser) throw new Error("Failed to retrieve current user.");
1150
- const teams = await this.client.getTeams();
1151
- 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);
1152
1155
  if (!teamIds.length) throw new Error("Current user does not belong to any teams.");
1153
- const resultsByTeam = await this.client.getActiveIteration(teamIds);
1154
- const allActiveIterations = [...resultsByTeam.values()].flat();
1156
+ const allActiveIterations = [...(await this.client.getActiveIteration(teamIds)).values()].flat();
1155
1157
  if (!allActiveIterations.length) return this.toResult("Result: No active iterations found for any of your teams.");
1156
1158
  return this.toResult(`You have ${allActiveIterations.length} active iterations for your teams:`, await this.entitiesWithRelatedEntities(allActiveIterations, "iterations"));
1157
1159
  }
1158
1160
  async getUpcomingIterations(teamId) {
1159
1161
  if (teamId) {
1160
- const team = await this.client.getTeam(teamId);
1161
- if (!team) throw new Error(`No team found matching id: "${teamId}"`);
1162
- const result = await this.client.getUpcomingIteration([teamId]);
1163
- 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);
1164
1164
  if (!iterations?.length) return this.toResult(`Result: No upcoming iterations found for team.`);
1165
1165
  if (iterations.length === 1) return this.toResult("The next upcoming iteration for the team is:", await this.entityWithRelatedEntities(iterations[0], "iteration"));
1166
1166
  return this.toResult("The next upcoming iterations for the team are:", await this.entitiesWithRelatedEntities(iterations, "iterations"));
1167
1167
  }
1168
1168
  const currentUser = await this.client.getCurrentUser();
1169
1169
  if (!currentUser) throw new Error("Failed to retrieve current user.");
1170
- const teams = await this.client.getTeams();
1171
- 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);
1172
1171
  if (!teamIds.length) throw new Error("Current user does not belong to any teams.");
1173
- const resultsByTeam = await this.client.getUpcomingIteration(teamIds);
1174
- const allUpcomingIterations = [...resultsByTeam.values()].flat();
1172
+ const allUpcomingIterations = [...(await this.client.getUpcomingIteration(teamIds)).values()].flat();
1175
1173
  if (!allUpcomingIterations.length) return this.toResult("Result: No upcoming iterations found for any of your teams.");
1176
1174
  return this.toResult("The upcoming iterations for all your teams are:", await this.entitiesWithRelatedEntities(allUpcomingIterations, "iterations"));
1177
1175
  }
@@ -1207,12 +1205,11 @@ var ObjectiveTools = class ObjectiveTools extends BaseTools {
1207
1205
  created: date(),
1208
1206
  updated: date(),
1209
1207
  completed: date()
1210
- }, async ({ nextPageToken,...params }) => await tools.searchObjectives(params, nextPageToken));
1208
+ }, async ({ nextPageToken, ...params }) => await tools.searchObjectives(params, nextPageToken));
1211
1209
  return tools;
1212
1210
  }
1213
1211
  async searchObjectives(params, nextToken) {
1214
- const currentUser = await this.client.getCurrentUser();
1215
- const query = await buildSearchQuery(params, currentUser);
1212
+ const query = await buildSearchQuery(params, await this.client.getCurrentUser());
1216
1213
  const { milestones, total, next_page_token } = await this.client.searchMilestones(query, nextToken);
1217
1214
  if (!milestones) throw new Error(`Failed to search for milestones matching your query: "${query}"`);
1218
1215
  if (!milestones.length) return this.toResult(`Result: No milestones found.`);
@@ -1284,7 +1281,7 @@ var StoryTools = class StoryTools extends BaseTools {
1284
1281
  updated: date(),
1285
1282
  completed: date(),
1286
1283
  due: date()
1287
- }, async ({ nextPageToken,...params }) => await tools.searchStories(params, nextPageToken));
1284
+ }, async ({ nextPageToken, ...params }) => await tools.searchStories(params, nextPageToken));
1288
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));
1289
1286
  server.addToolWithWriteAccess("stories-create", `Create a new Shortcut story.
1290
1287
  Name is required, and either a Team or Workflow must be specified:
@@ -1423,10 +1420,7 @@ The story will be added to the default state for the workflow.
1423
1420
  }
1424
1421
  async createStory({ name: name$1, description, type, owner, epic, iteration, team, workflow }) {
1425
1422
  if (!workflow && !team) throw new Error("Team or Workflow has to be specified");
1426
- if (!workflow && team) {
1427
- const fullTeam = await this.client.getTeam(team);
1428
- workflow = fullTeam?.workflow_ids?.[0];
1429
- }
1423
+ if (!workflow && team) workflow = (await this.client.getTeam(team))?.workflow_ids?.[0];
1430
1424
  if (!workflow) throw new Error("Failed to find workflow for team");
1431
1425
  const fullWorkflow = await this.client.getWorkflow(workflow);
1432
1426
  if (!fullWorkflow) throw new Error("Failed to find workflow");
@@ -1465,23 +1459,19 @@ The story will be added to the default state for the workflow.
1465
1459
  async addStoryAsSubTask({ parentStoryPublicId, subTaskPublicId }) {
1466
1460
  if (!parentStoryPublicId) throw new Error("ID of parent story is required");
1467
1461
  if (!subTaskPublicId) throw new Error("ID of sub-task story is required");
1468
- const subTask = await this.client.getStory(subTaskPublicId);
1469
- if (!subTask) throw new Error(`Failed to retrieve story with public ID: ${subTaskPublicId}`);
1470
- const parentStory = await this.client.getStory(parentStoryPublicId);
1471
- 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}`);
1472
1464
  await this.client.updateStory(subTaskPublicId, { parent_story_id: parentStoryPublicId });
1473
1465
  return this.toResult(`Added story sc-${subTaskPublicId} as a sub-task of sc-${parentStoryPublicId}`);
1474
1466
  }
1475
1467
  async removeSubTaskFromParent({ subTaskPublicId }) {
1476
1468
  if (!subTaskPublicId) throw new Error("ID of sub-task story is required");
1477
- const subTask = await this.client.getStory(subTaskPublicId);
1478
- 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}`);
1479
1470
  await this.client.updateStory(subTaskPublicId, { parent_story_id: null });
1480
1471
  return this.toResult(`Removed story sc-${subTaskPublicId} from its parent story`);
1481
1472
  }
1482
1473
  async searchStories(params, nextToken) {
1483
- const currentUser = await this.client.getCurrentUser();
1484
- const query = await buildSearchQuery(params, currentUser);
1474
+ const query = await buildSearchQuery(params, await this.client.getCurrentUser());
1485
1475
  const { stories, total, next_page_token } = await this.client.searchStories(query, nextToken);
1486
1476
  if (!stories) throw new Error(`Failed to search for stories matching your query: "${query}".`);
1487
1477
  if (!stories.length) return this.toResult(`Result: No stories found.`);
@@ -1495,15 +1485,13 @@ The story will be added to the default state for the workflow.
1495
1485
  async createStoryComment({ storyPublicId, text }) {
1496
1486
  if (!storyPublicId) throw new Error("Story public ID is required");
1497
1487
  if (!text) throw new Error("Story comment text is required");
1498
- const story = await this.client.getStory(storyPublicId);
1499
- 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}`);
1500
1489
  const storyComment = await this.client.createStoryComment(storyPublicId, { text });
1501
1490
  return this.toResult(`Created comment on story sc-${storyPublicId}. Comment URL: ${storyComment.app_url}.`);
1502
1491
  }
1503
- async updateStory({ storyPublicId,...updates }) {
1492
+ async updateStory({ storyPublicId, ...updates }) {
1504
1493
  if (!storyPublicId) throw new Error("Story public ID is required");
1505
- const story = await this.client.getStory(storyPublicId);
1506
- 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}`);
1507
1495
  const updateParams = {};
1508
1496
  if (updates.name !== void 0) updateParams.name = updates.name;
1509
1497
  if (updates.description !== void 0) updateParams.description = updates.description;
@@ -1520,8 +1508,7 @@ The story will be added to the default state for the workflow.
1520
1508
  async uploadFileToStory(storyPublicId, filePath) {
1521
1509
  if (!storyPublicId) throw new Error("Story public ID is required");
1522
1510
  if (!filePath) throw new Error("File path is required");
1523
- const story = await this.client.getStory(storyPublicId);
1524
- 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}`);
1525
1512
  const uploadedFile = await this.client.uploadFile(storyPublicId, filePath);
1526
1513
  if (!uploadedFile) throw new Error(`Failed to upload file to story sc-${storyPublicId}`);
1527
1514
  return this.toResult(`Uploaded file "${uploadedFile.name}" to story sc-${storyPublicId}. File ID is: ${uploadedFile.id}`);
@@ -1529,11 +1516,9 @@ The story will be added to the default state for the workflow.
1529
1516
  async addTaskToStory({ storyPublicId, taskDescription, taskOwnerIds }) {
1530
1517
  if (!storyPublicId) throw new Error("Story public ID is required");
1531
1518
  if (!taskDescription) throw new Error("Task description is required");
1532
- const story = await this.client.getStory(storyPublicId);
1533
- 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}`);
1534
1520
  if (taskOwnerIds?.length) {
1535
- const owners = await this.client.getUserMap(taskOwnerIds);
1536
- 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(", ")}`);
1537
1522
  }
1538
1523
  const task = await this.client.addTaskToStory(storyPublicId, {
1539
1524
  description: taskDescription,
@@ -1544,10 +1529,8 @@ The story will be added to the default state for the workflow.
1544
1529
  async updateTask({ storyPublicId, taskPublicId, taskDescription, taskOwnerIds, isCompleted }) {
1545
1530
  if (!storyPublicId) throw new Error("Story public ID is required");
1546
1531
  if (!taskPublicId) throw new Error("Task public ID is required");
1547
- const story = await this.client.getStory(storyPublicId);
1548
- if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
1549
- const task = await this.client.getTask(storyPublicId, taskPublicId);
1550
- 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}`);
1551
1534
  const updatedTask = await this.client.updateTask(storyPublicId, taskPublicId, {
1552
1535
  description: taskDescription,
1553
1536
  ownerIds: taskOwnerIds,
@@ -1560,10 +1543,8 @@ The story will be added to the default state for the workflow.
1560
1543
  async addRelationToStory({ storyPublicId, relatedStoryPublicId, relationshipType }) {
1561
1544
  if (!storyPublicId) throw new Error("Story public ID is required");
1562
1545
  if (!relatedStoryPublicId) throw new Error("Related story public ID is required");
1563
- const story = await this.client.getStory(storyPublicId);
1564
- if (!story) throw new Error(`Failed to retrieve Shortcut story with public ID: ${storyPublicId}`);
1565
- const relatedStory = await this.client.getStory(relatedStoryPublicId);
1566
- 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}`);
1567
1548
  let subjectStoryId = storyPublicId;
1568
1549
  let objectStoryId = relatedStoryPublicId;
1569
1550
  if (relationshipType === "blocked by" || relationshipType === "duplicated by") {
@@ -1700,4 +1681,4 @@ var WorkflowTools = class WorkflowTools extends BaseTools {
1700
1681
  };
1701
1682
 
1702
1683
  //#endregion
1703
- export { BaseTools, 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.18.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",