@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 +3 -0
- package/dist/index.js +3 -2
- package/dist/server-http.js +46 -23
- package/dist/{workflows-Dko3ibgz.js → workflows-DYvKtRHR.js} +92 -111
- package/package.json +2 -2
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 {
|
|
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 { };
|
package/dist/server-http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1141
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1161
|
-
|
|
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
|
|
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
|
|
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
|
|
1208
|
+
}, async ({ nextPageToken, ...params }) => await tools.searchObjectives(params, nextPageToken));
|
|
1211
1209
|
return tools;
|
|
1212
1210
|
}
|
|
1213
1211
|
async searchObjectives(params, nextToken) {
|
|
1214
|
-
const
|
|
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
|
|
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
|
-
|
|
1469
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1492
|
+
async updateStory({ storyPublicId, ...updates }) {
|
|
1504
1493
|
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1505
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1548
|
-
if (!
|
|
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
|
-
|
|
1564
|
-
if (!
|
|
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 {
|
|
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.
|
|
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.
|
|
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",
|