@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 +8 -0
- package/dist/index.js +47 -2
- package/dist/server-http.js +46 -23
- package/dist/{workflows-TjriXV16.js → workflows-DYvKtRHR.js} +102 -109
- package/package.json +2 -2
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 {
|
|
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 { };
|
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.");
|
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1129
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1149
|
-
|
|
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
|
|
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
|
|
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
|
|
1208
|
+
}, async ({ nextPageToken, ...params }) => await tools.searchObjectives(params, nextPageToken));
|
|
1199
1209
|
return tools;
|
|
1200
1210
|
}
|
|
1201
1211
|
async searchObjectives(params, nextToken) {
|
|
1202
|
-
const
|
|
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
|
|
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
|
-
|
|
1457
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1492
|
+
async updateStory({ storyPublicId, ...updates }) {
|
|
1492
1493
|
if (!storyPublicId) throw new Error("Story public ID is required");
|
|
1493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1536
|
-
if (!
|
|
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
|
-
|
|
1552
|
-
if (!
|
|
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 {
|
|
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",
|