@shortcut/mcp 0.19.0 → 0.20.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
@@ -14,12 +14,70 @@ The MCP server for [Shortcut](https://shortcut.com).
14
14
 
15
15
  ## Usage
16
16
 
17
+ The only required input is your Shortcut API token. You can find it in your [Shortcut account settings](https://app.shortcut.com/settings/account/api-tokens).
18
+
19
+ Once you have a valid token, you can pass it to the MCP server as an environement variable or a CLI argument.
20
+
21
+ Examples:
22
+
23
+ ```json
24
+ "shortcut": {
25
+ "command": "npx",
26
+ "args": [
27
+ "-y",
28
+ "@shortcut/mcp@latest"
29
+ ],
30
+ "env": {
31
+ "SHORTCUT_API_TOKEN": "<YOUR_SHORTCUT_API_TOKEN>"
32
+ }
33
+ }
34
+ ```
35
+
36
+ ```json
37
+ "shortcut": {
38
+ "command": "npx",
39
+ "args": [
40
+ "-y",
41
+ "@shortcut/mcp@latest",
42
+ "SHORTCUT_API_TOKEN=<YOUR_SHORTCUT_API_TOKEN>"
43
+ ]
44
+ }
45
+ ```
46
+
47
+ Due to an issue in `gemini-cli` that redacts environment variables that contain the word "token", you can also use the alternative name `SHORTCUT_API_TKN` instead of `SHORTCUT_API_TOKEN`. This works for both the environment variable and the CLI argument:
48
+
49
+ ```json
50
+ "shortcut": {
51
+ "command": "npx",
52
+ "args": [
53
+ "-y",
54
+ "@shortcut/mcp@latest"
55
+ ],
56
+ "env": {
57
+ "SHORTCUT_API_TKN": "<YOUR_SHORTCUT_API_TOKEN>"
58
+ }
59
+ }
60
+ ```
61
+
62
+ ```json
63
+ "shortcut": {
64
+ "command": "npx",
65
+ "args": [
66
+ "-y",
67
+ "@shortcut/mcp@latest",
68
+ "SHORTCUT_API_TKN=<YOUR_SHORTCUT_API_TOKEN>"
69
+ ]
70
+ }
71
+ ```
72
+
73
+ For more information on how to setup the MCP for your tool of choice, see below.
74
+
17
75
  ### Windsurf
18
76
 
19
77
  See the [official Windsurf docs](https://docs.windsurf.com/windsurf/cascade/mcp) for more information.
20
78
 
21
- 1. Open the `Windsurf MCP Configuration Panel`
22
- 2. Click `Add custom server`.
79
+ 1. Open the MCP configuration by clicking the `MCPs` icon in the Cascade panel, or navigate to `Windsurf Settings` > `Cascade` > `MCP Servers`.
80
+ 2. Click `Add Custom Server` to edit the raw `mcp_config.json` file (located at `~/.codeium/windsurf/mcp_config.json`).
23
81
  3. Add the following details and save the file:
24
82
 
25
83
  ```json
@@ -66,35 +124,27 @@ See the [official Cursor docs](https://docs.cursor.com/context/model-context-pro
66
124
 
67
125
  ### Claude Code
68
126
 
69
- See the [official Claude Code docs](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/tutorials#set-up-model-context-protocol-mcp) for more information.
70
-
71
- _You can add a new MCP server from the Claude Code CLI. But modifying the json file directly is simpler!_
127
+ See the [official Claude Code docs](https://docs.anthropic.com/en/docs/claude-code/mcp) for more information.
72
128
 
73
- You can either add a new MCP server from the command line:
129
+ Add the MCP server from the command line:
74
130
 
75
131
  ```shell
76
- # Grab your Shortcut token here: https://app.shortcut.com/settings/account/api-tokens
77
- claude mcp add shortcut --transport=stdio -e SHORTCUT_API_TOKEN=$SHORTCUT_API_TOKEN -- npx -y @shortcut/mcp@latest
132
+ claude mcp add shortcut --transport stdio -e SHORTCUT_API_TOKEN=$SHORTCUT_API_TOKEN -- npx -y @shortcut/mcp@latest
78
133
  ```
79
134
 
80
- Or you can edit the local JSON file directly:
81
-
82
- 1. Open the Claude Code configuration file (it should be in `~/.claude.json`).
83
- 2. Find the `projects` > `mcpServers` section and add the following details and save the file:
135
+ Or you can create a `.mcp.json` file in your project root to share with your team:
84
136
 
85
137
  ```json
86
138
  {
87
- "projects": {
88
- "mcpServers": {
89
- "shortcut": {
90
- "command": "npx",
91
- "args": [
92
- "-y",
93
- "@shortcut/mcp@latest"
94
- ],
95
- "env": {
96
- "SHORTCUT_API_TOKEN": "<YOUR_SHORTCUT_API_TOKEN>"
97
- }
139
+ "mcpServers": {
140
+ "shortcut": {
141
+ "command": "npx",
142
+ "args": [
143
+ "-y",
144
+ "@shortcut/mcp@latest"
145
+ ],
146
+ "env": {
147
+ "SHORTCUT_API_TOKEN": "<YOUR_SHORTCUT_API_TOKEN>"
98
148
  }
99
149
  }
100
150
  }
@@ -102,26 +152,72 @@ Or you can edit the local JSON file directly:
102
152
  ```
103
153
 
104
154
  ### Zed
105
- [Zed MCP Documentation](https://zed.dev/docs/ai/mcp)
106
- 1. Open your `settings.json` file. Instructions [here](https://zed.dev/docs/configuring-zed#settings-files)
107
- 2. Add the following details and save the file:
155
+
156
+ See the [official Zed MCP docs](https://zed.dev/docs/ai/mcp) for more information.
157
+
158
+ 1. Open your `settings.json` file. Instructions [here](https://zed.dev/docs/configuring-zed#settings-files).
159
+ 2. Add the following to the `context_servers` section and save the file:
108
160
 
109
161
  ```json
162
+ {
110
163
  "context_servers": {
111
164
  "shortcut": {
112
- "settings":{},
113
- "command": {
114
- "path": "<PATH/TO/NPX>",
115
- "args": [
116
- "-y",
117
- "@shortcut/mcp@latest"
118
- ],
119
- "env": {
120
- "SHORTCUT_API_TOKEN": "<YOUR_SHORTCUT_API_TOKEN>"
121
- }
165
+ "command": "npx",
166
+ "args": [
167
+ "-y",
168
+ "@shortcut/mcp@latest"
169
+ ],
170
+ "env": {
171
+ "SHORTCUT_API_TOKEN": "<YOUR_SHORTCUT_API_TOKEN>"
172
+ }
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### VS Code
179
+
180
+ See the [official VS Code MCP docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more information.
181
+
182
+ 1. Create (or open) the `.vscode/mcp.json` file in your workspace.
183
+ 2. Add the following details and save the file:
184
+
185
+ ```json
186
+ {
187
+ "servers": {
188
+ "shortcut": {
189
+ "command": "npx",
190
+ "args": [
191
+ "-y",
192
+ "@shortcut/mcp@latest"
193
+ ],
194
+ "env": {
195
+ "SHORTCUT_API_TOKEN": "<YOUR_SHORTCUT_API_TOKEN>"
196
+ }
197
+ }
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### OpenCode
203
+
204
+ See the [official OpenCode MCP docs](https://opencode.ai/docs/mcp-servers/) for more information.
205
+
206
+ Add the following to your `opencode.json` configuration file:
207
+
208
+ ```json
209
+ {
210
+ "$schema": "https://opencode.ai/config.json",
211
+ "mcp": {
212
+ "shortcut": {
213
+ "type": "local",
214
+ "command": ["npx", "-y", "@shortcut/mcp@latest"],
215
+ "environment": {
216
+ "SHORTCUT_API_TOKEN": "<YOUR_SHORTCUT_API_TOKEN>"
122
217
  }
123
218
  }
124
219
  }
220
+ }
125
221
  ```
126
222
 
127
223
  ## Available Tools
@@ -191,7 +287,7 @@ Or you can edit the local JSON file directly:
191
287
 
192
288
  ### Documents
193
289
 
194
- - **documents-create** - Create a new document in Shortcut with HTML content
290
+ - **documents-create** - Create a new document in Shortcut with Markdown content
195
291
  - **documents-update** - Update content of an existing document by its ID
196
292
  - **documents-list** - List all documents in Shortcut
197
293
  - **documents-search** - Search for documents
@@ -204,7 +300,8 @@ You can limit the tools available to the LLM by setting the `SHORTCUT_TOOLS` env
204
300
  - Tools can be limited by entity type by just adding the entity, eg `stories` or `epics`.
205
301
  - Individual tools can also be limitied by their full name, eg `stories-get-by-id` or `epics-search`.
206
302
 
207
- By default, all tools are enabled.
303
+ > [!NOTE]
304
+ > By default, all tools are enabled.
208
305
 
209
306
  Example:
210
307
 
@@ -242,7 +339,8 @@ The following values are accepted in addition to the full tool names listed abov
242
339
 
243
340
  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.
244
341
 
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).
342
+ > [!TIP]
343
+ > Shortcut 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
344
 
247
345
  Example:
248
346
 
@@ -266,7 +364,8 @@ Example:
266
364
 
267
365
  ## Issues and Troubleshooting
268
366
 
269
- Before doing anything else, please make sure you are running the latest version!
367
+ > [!IMPORTANT]
368
+ > Before doing anything else, please make sure you are running the latest version!
270
369
 
271
370
  If you run into problems using this MCP server, you have a couple of options:
272
371
 
@@ -277,6 +376,10 @@ You can also check the list of [common issues](#common-issues) below to see if t
277
376
 
278
377
  ### Common Issues and Solutions
279
378
 
379
+ #### MCP fails on startup in Gemini CLI
380
+
381
+ If you are using the Gemini CLI and the MCP fails with the following error: `✕ Error during discovery for MCP server 'shortcut': MCP error -32000: Connection closed`, it might be due to an issue in Gemini where it redacts environment variables that contain the word `token`. You can either pass the Shortcut token as a CLI argument, or use the alternative name `SHORTCUT_API_TKN` instead of `SHORTCUT_API_TOKEN`. See the [Usage section](#usage) for more information.
382
+
280
383
  #### NPX command not working when using MISE for version management
281
384
 
282
385
  If you are using MISE for managing Node and NPM versions, you may encounter a "Client closed" error when trying to run the MCP server. Installing this extension into your IDE might help: https://github.com/hverlin/mise-vscode/.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
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";
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-GwezThPA.mjs";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { ShortcutClient } from "@shortcut/client";
5
5
  import { z } from "zod";
@@ -9,10 +9,10 @@ import { z } from "zod";
9
9
  * Tools for managing Shortcut labels.
10
10
  */
11
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.", {
12
+ static create(client, server) {
13
+ const tools = new LabelTools(client);
14
+ server.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.addToolWithWriteAccess("labels-create", "Create a new label in Shortcut.", {
16
16
  name: z.string().min(1).max(128).describe("The name of the new label. Required."),
17
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
18
  description: z.string().max(1024).optional().describe("A description of the label.")
@@ -47,16 +47,17 @@ var LabelTools = class LabelTools extends BaseTools {
47
47
 
48
48
  //#endregion
49
49
  //#region src/server.ts
50
- let apiToken = process.env.SHORTCUT_API_TOKEN;
50
+ let apiToken = process.env.SHORTCUT_API_TKN || process.env.SHORTCUT_API_TOKEN;
51
51
  let isReadonly = process.env.SHORTCUT_READONLY === "true";
52
52
  let enabledTools = (process.env.SHORTCUT_TOOLS || "").split(",").map((tool) => tool.trim()).filter(Boolean);
53
53
  if (process.argv.length >= 3) process.argv.slice(2).map((arg) => arg.split("=")).forEach(([name, value]) => {
54
+ if (name === "SHORTCUT_API_TKN") apiToken = value;
54
55
  if (name === "SHORTCUT_API_TOKEN") apiToken = value;
55
56
  if (name === "SHORTCUT_READONLY") isReadonly = value === "true";
56
57
  if (name === "SHORTCUT_TOOLS") enabledTools = value.split(",").map((tool) => tool.trim()).filter(Boolean);
57
58
  });
58
59
  if (!apiToken) {
59
- console.error("SHORTCUT_API_TOKEN is required");
60
+ console.error("A Shortcut api token is required.");
60
61
  process.exit(1);
61
62
  }
62
63
  const server = new CustomMcpServer({
@@ -1,4 +1,4 @@
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";
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-GwezThPA.mjs";
2
2
  import { ShortcutClient } from "@shortcut/client";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -95,23 +95,23 @@ var ShortcutClientWrapper = class {
95
95
  }
96
96
  async getCurrentUser() {
97
97
  if (this.currentUser) return this.currentUser;
98
- const user$1 = (await this.client.getCurrentMemberInfo())?.data;
99
- if (!user$1) return null;
100
- this.currentUser = user$1;
101
- return user$1;
98
+ const user = (await this.client.getCurrentMemberInfo())?.data;
99
+ if (!user) return null;
100
+ this.currentUser = user;
101
+ return user;
102
102
  }
103
103
  async getUser(userId) {
104
- const user$1 = (await this.client.getMember(userId, {}))?.data;
105
- if (!user$1) return null;
106
- return user$1;
104
+ const user = (await this.client.getMember(userId, {}))?.data;
105
+ if (!user) return null;
106
+ return user;
107
107
  }
108
108
  async getUserMap(userIds) {
109
109
  await this.loadMembers();
110
- return new Map(userIds.map((id) => [id, this.userCache.get(id)]).filter((user$1) => user$1[1] !== null));
110
+ return new Map(userIds.map((id) => [id, this.userCache.get(id)]).filter((user) => user[1] !== null));
111
111
  }
112
112
  async getUsers(userIds) {
113
113
  await this.loadMembers();
114
- return userIds.map((id) => this.userCache.get(id)).filter((user$1) => user$1 !== null);
114
+ return userIds.map((id) => this.userCache.get(id)).filter((user) => user !== null);
115
115
  }
116
116
  async listMembers() {
117
117
  await this.loadMembers();
@@ -477,7 +477,7 @@ var ShortcutClientWrapper = class {
477
477
  //#endregion
478
478
  //#region package.json
479
479
  var name = "@shortcut/mcp";
480
- var version = "0.19.0";
480
+ var version = "0.20.0";
481
481
 
482
482
  //#endregion
483
483
  //#region src/mcp/CustomMcpServer.ts
@@ -492,10 +492,10 @@ var CustomMcpServer = class extends McpServer {
492
492
  this.readonly = readonly;
493
493
  this.tools = new Set(tools || []);
494
494
  }
495
- shouldAddTool(name$1) {
495
+ shouldAddTool(name) {
496
496
  if (!this.tools.size) return true;
497
- const [entityType] = name$1.split("-");
498
- if (this.tools.has(entityType) || this.tools.has(name$1)) return true;
497
+ const [entityType] = name.split("-");
498
+ if (this.tools.has(entityType) || this.tools.has(name)) return true;
499
499
  return false;
500
500
  }
501
501
  addToolWithWriteAccess(...args) {
@@ -552,10 +552,10 @@ var BaseTools = class {
552
552
  }
553
553
  getSimplifiedMember(entity) {
554
554
  if (!entity) return null;
555
- const { id, disabled, role, profile: { is_owner, name: name$1, email_address, mention_name } } = entity;
555
+ const { id, disabled, role, profile: { is_owner, name, email_address, mention_name } } = entity;
556
556
  return {
557
557
  id,
558
- name: name$1,
558
+ name,
559
559
  email_address,
560
560
  mention_name,
561
561
  role,
@@ -565,13 +565,13 @@ var BaseTools = class {
565
565
  }
566
566
  getSimplifiedStory(entity, kind) {
567
567
  if (!entity) return null;
568
- const { id, name: name$1, app_url, archived, group_id, epic_id, iteration_id, workflow_id, workflow_state_id, owner_ids, requested_by_id, estimate, labels, comments, description, external_links, story_links, pull_requests, formatted_vcs_branch_name, branches, parent_story_id, sub_task_story_ids, tasks, custom_fields, blocked, blocker } = entity;
568
+ const { id, name, app_url, archived, group_id, epic_id, iteration_id, workflow_id, workflow_state_id, owner_ids, requested_by_id, estimate, labels, comments, description, external_links, story_links, pull_requests, formatted_vcs_branch_name, branches, parent_story_id, sub_task_story_ids, tasks, custom_fields, blocked, blocker } = entity;
569
569
  const additionalFields = {};
570
570
  if (kind === "simple") {
571
571
  additionalFields.description = description;
572
572
  additionalFields.estimate = estimate ?? null;
573
- additionalFields.comments = (comments || []).filter((c) => !c.deleted).map(({ id: id$1, author_id, text }) => ({
574
- id: id$1,
573
+ additionalFields.comments = (comments || []).filter((c) => !c.deleted).map(({ id, author_id, text }) => ({
574
+ id,
575
575
  author_id: author_id ?? null,
576
576
  text: text ?? ""
577
577
  }));
@@ -592,7 +592,7 @@ var BaseTools = class {
592
592
  }
593
593
  return {
594
594
  id,
595
- name: name$1,
595
+ name,
596
596
  app_url,
597
597
  archived,
598
598
  team_id: group_id || null,
@@ -609,10 +609,10 @@ var BaseTools = class {
609
609
  }
610
610
  getSimplifiedWorkflow(entity) {
611
611
  if (!entity) return null;
612
- const { id, name: name$1, states } = entity;
612
+ const { id, name, states } = entity;
613
613
  return {
614
614
  id,
615
- name: name$1,
615
+ name,
616
616
  states: states.map((state) => ({
617
617
  id: state.id,
618
618
  name: state.name,
@@ -620,26 +620,30 @@ var BaseTools = class {
620
620
  }))
621
621
  };
622
622
  }
623
- getSimplifiedTeam(entity) {
623
+ getSimplifiedTeam(entity, kind) {
624
624
  if (!entity) return null;
625
- const { archived, id, name: name$1, mention_name, member_ids, workflow_ids, default_workflow_id } = entity;
625
+ const { archived, id, name, mention_name, member_ids, workflow_ids, default_workflow_id } = entity;
626
+ const additionalFields = {};
627
+ if (kind === "simple") {
628
+ additionalFields.member_ids = member_ids;
629
+ additionalFields.workflow_ids = workflow_ids;
630
+ }
626
631
  return {
627
632
  id,
628
- name: name$1,
629
- archived,
633
+ name,
630
634
  mention_name,
631
- member_ids,
632
- workflow_ids,
633
- default_workflow_id: default_workflow_id ?? null
635
+ archived,
636
+ default_workflow_id: default_workflow_id ?? null,
637
+ ...additionalFields
634
638
  };
635
639
  }
636
640
  getSimplifiedObjective(entity) {
637
641
  if (!entity) return null;
638
- const { app_url, id, name: name$1, archived, state, categories } = entity;
642
+ const { app_url, id, name, archived, state, categories } = entity;
639
643
  return {
640
644
  app_url,
641
645
  id,
642
- name: name$1,
646
+ name,
643
647
  archived,
644
648
  state,
645
649
  categories: categories.map((cat) => cat.name)
@@ -647,11 +651,11 @@ var BaseTools = class {
647
651
  }
648
652
  getSimplifiedEpic(entity, kind) {
649
653
  if (!entity) return null;
650
- const { id, name: name$1, app_url, archived, group_id, state, milestone_id, comments, description, deadline, owner_ids } = entity;
654
+ const { id, name, app_url, archived, group_id, state, milestone_id, comments, description, deadline, owner_ids } = entity;
651
655
  const additionalFields = {};
652
656
  if (kind === "simple") {
653
- additionalFields.comments = (comments || []).filter((comment) => !comment.deleted).map(({ id: id$1, author_id, text }) => ({
654
- id: id$1,
657
+ additionalFields.comments = (comments || []).filter((comment) => !comment.deleted).map(({ id, author_id, text }) => ({
658
+ id,
655
659
  author_id,
656
660
  text
657
661
  }));
@@ -660,7 +664,7 @@ var BaseTools = class {
660
664
  }
661
665
  return {
662
666
  id,
663
- name: name$1,
667
+ name,
664
668
  app_url,
665
669
  archived,
666
670
  state,
@@ -672,10 +676,10 @@ var BaseTools = class {
672
676
  }
673
677
  getSimplifiedIteration(entity) {
674
678
  if (!entity) return null;
675
- const { id, name: name$1, app_url, group_ids, status, start_date, end_date } = entity;
679
+ const { id, name, app_url, group_ids, status, start_date, end_date } = entity;
676
680
  return {
677
681
  id,
678
- name: name$1,
682
+ name,
679
683
  app_url,
680
684
  team_ids: group_ids,
681
685
  status,
@@ -683,19 +687,35 @@ var BaseTools = class {
683
687
  end_date
684
688
  };
685
689
  }
686
- async getRelatedEntitiesForTeam(entity) {
690
+ async getRelatedEntitiesForTeam(entity, kind) {
687
691
  if (!entity) return {
688
692
  users: {},
689
693
  workflows: {}
690
694
  };
691
- const { member_ids, workflow_ids } = entity;
695
+ const { member_ids, workflow_ids, default_workflow_id } = entity;
696
+ if (kind === "list") {
697
+ if (!default_workflow_id) return {
698
+ users: {},
699
+ workflows: {}
700
+ };
701
+ const workflows = await this.client.getWorkflowMap([default_workflow_id]);
702
+ const simplifiedWorkflow = this.getSimplifiedWorkflow(workflows.get(default_workflow_id));
703
+ if (!simplifiedWorkflow) return {
704
+ users: {},
705
+ workflows: {}
706
+ };
707
+ return {
708
+ users: {},
709
+ workflows: simplifiedWorkflow ? { [default_workflow_id]: simplifiedWorkflow } : {}
710
+ };
711
+ }
692
712
  const [users, workflows] = await Promise.all([this.client.getUserMap(member_ids), this.client.getWorkflowMap(workflow_ids)]);
693
713
  return {
694
714
  users: Object.fromEntries(member_ids.map((id) => this.getSimplifiedMember(users.get(id))).filter((member) => member !== null).map((member) => [member.id, member])),
695
715
  workflows: Object.fromEntries(workflow_ids.map((id) => this.getSimplifiedWorkflow(workflows.get(id))).filter((workflow) => workflow !== null).map((workflow) => [workflow.id, workflow]))
696
716
  };
697
717
  }
698
- async getRelatedEntitiesForIteration(entity) {
718
+ async getRelatedEntitiesForIteration(entity, kind) {
699
719
  if (!entity) return {
700
720
  teams: {},
701
721
  users: {},
@@ -703,15 +723,15 @@ var BaseTools = class {
703
723
  };
704
724
  const { group_ids } = entity;
705
725
  const teams = await this.client.getTeamMap(group_ids || []);
706
- const relatedEntitiesForTeams = await Promise.all(Array.from(teams.values()).map((team) => this.getRelatedEntitiesForTeam(team)));
726
+ const relatedEntitiesForTeams = await Promise.all(Array.from(teams.values()).map((team) => this.getRelatedEntitiesForTeam(team, kind)));
707
727
  const { users, workflows } = this.mergeRelatedEntities(relatedEntitiesForTeams);
708
728
  return {
709
- teams: Object.fromEntries([...teams.entries()].map(([id, team]) => [id, this.getSimplifiedTeam(team)]).filter(([_, team]) => !!team)),
729
+ teams: Object.fromEntries([...teams.entries()].map(([id, team]) => [id, this.getSimplifiedTeam(team, kind)]).filter(([_, team]) => !!team)),
710
730
  users,
711
731
  workflows
712
732
  };
713
733
  }
714
- async getRelatedEntitiesForEpic(entity) {
734
+ async getRelatedEntitiesForEpic(entity, kind) {
715
735
  if (!entity) return {
716
736
  users: {},
717
737
  workflows: {},
@@ -729,9 +749,9 @@ var BaseTools = class {
729
749
  this.client.getTeamMap(group_id ? [group_id] : []),
730
750
  milestone_id ? this.client.getMilestone(milestone_id) : null
731
751
  ]);
732
- const usersForEpic = Object.fromEntries([...usersForEpicMap.entries()].filter(([_, user$1]) => !!user$1).map(([id, user$1]) => [id, this.getSimplifiedMember(user$1)]));
733
- const team = this.getSimplifiedTeam(teams.get(group_id || ""));
734
- const { users, workflows } = await this.getRelatedEntitiesForTeam(teams.get(group_id || ""));
752
+ const usersForEpic = Object.fromEntries([...usersForEpicMap.entries()].filter(([_, user]) => !!user).map(([id, user]) => [id, this.getSimplifiedMember(user)]));
753
+ const team = this.getSimplifiedTeam(teams.get(group_id || ""), kind);
754
+ const { users, workflows } = await this.getRelatedEntitiesForTeam(teams.get(group_id || ""), kind);
735
755
  const milestone = this.getSimplifiedObjective(fullMilestone);
736
756
  return {
737
757
  users: this.mergeRelatedEntities([usersForEpic, users]),
@@ -754,14 +774,14 @@ var BaseTools = class {
754
774
  iteration_id ? this.client.getIteration(iteration_id) : null,
755
775
  epic_id ? this.client.getEpic(epic_id) : null
756
776
  ]);
757
- const usersForStory = Object.fromEntries([...fullUsersForStory.entries()].filter(([_, user$1]) => !!user$1).map(([id, user$1]) => [id, this.getSimplifiedMember(user$1)]));
777
+ const usersForStory = Object.fromEntries([...fullUsersForStory.entries()].filter(([_, user]) => !!user).map(([id, user]) => [id, this.getSimplifiedMember(user)]));
758
778
  const simplifiedIteration = this.getSimplifiedIteration(iteration);
759
779
  const simplifiedEpic = this.getSimplifiedEpic(epic, kind);
760
780
  const teamForStory = teamsForStory.get(group_id || "");
761
781
  const workflowForStory = this.getSimplifiedWorkflow(workflowsForStory.get(workflow_id));
762
- const { users: usersForTeam, workflows: workflowsForTeam } = await this.getRelatedEntitiesForTeam(teamForStory);
763
- const { users: usersForIteration, workflows: workflowsForIteration, teams: teamsForIteration } = await this.getRelatedEntitiesForIteration(iteration);
764
- const { users: usersForEpic, workflows: workflowsForEpic, teams: teamsForEpic, objectives } = await this.getRelatedEntitiesForEpic(epic);
782
+ const { users: usersForTeam, workflows: workflowsForTeam } = await this.getRelatedEntitiesForTeam(teamForStory, kind);
783
+ const { users: usersForIteration, workflows: workflowsForIteration, teams: teamsForIteration } = await this.getRelatedEntitiesForIteration(iteration, kind);
784
+ const { users: usersForEpic, workflows: workflowsForEpic, teams: teamsForEpic, objectives } = await this.getRelatedEntitiesForEpic(epic, kind);
765
785
  const users = this.mergeRelatedEntities([
766
786
  usersForTeam,
767
787
  usersForStory,
@@ -774,7 +794,7 @@ var BaseTools = class {
774
794
  workflowsForEpic,
775
795
  workflowForStory ? { [workflowForStory.id]: workflowForStory } : {}
776
796
  ]);
777
- const simplifiedStoryTeam = this.getSimplifiedTeam(teamForStory);
797
+ const simplifiedStoryTeam = this.getSimplifiedTeam(teamForStory, kind);
778
798
  const teams = this.mergeRelatedEntities([
779
799
  teamsForIteration,
780
800
  teamsForEpic,
@@ -809,14 +829,14 @@ var BaseTools = class {
809
829
  };
810
830
  }
811
831
  async getRelatedEntities(entity, kind) {
812
- if (entity.entity_type === "group") return this.getRelatedEntitiesForTeam(entity);
813
- if (entity.entity_type === "iteration") return this.getRelatedEntitiesForIteration(entity);
814
- if (entity.entity_type === "epic") return this.getRelatedEntitiesForEpic(entity);
832
+ if (entity.entity_type === "group") return this.getRelatedEntitiesForTeam(entity, kind);
833
+ if (entity.entity_type === "iteration") return this.getRelatedEntitiesForIteration(entity, kind);
834
+ if (entity.entity_type === "epic") return this.getRelatedEntitiesForEpic(entity, kind);
815
835
  if (entity.entity_type === "story") return this.getRelatedEntitiesForStory(entity, kind);
816
836
  return {};
817
837
  }
818
838
  getSimplifiedEntity(entity, kind) {
819
- if (entity.entity_type === "group") return this.getSimplifiedTeam(entity);
839
+ if (entity.entity_type === "group") return this.getSimplifiedTeam(entity, kind);
820
840
  if (entity.entity_type === "iteration") return this.getSimplifiedIteration(entity);
821
841
  if (entity.entity_type === "epic") return this.getSimplifiedEpic(entity, kind);
822
842
  if (entity.entity_type === "story") return this.getSimplifiedStory(entity, kind);
@@ -998,11 +1018,11 @@ const variations = [
998
1018
  t: dateformat
999
1019
  })
1000
1020
  ];
1001
- const DATE_REGEXP = /* @__PURE__ */ new RegExp(`^(${variations.join("|")})$`);
1021
+ const DATE_REGEXP = new RegExp(`^(${variations.join("|")})$`);
1002
1022
  const date = () => z.string().regex(DATE_REGEXP).optional().describe("The date in \"YYYY-MM-DD\" format, or one of the keywords: \"yesterday\", \"today\", \"tomorrow\", or a date range in the format \"YYYY-MM-DD..YYYY-MM-DD\". The date range can also be open ended by using \"*\" for one of the bounds. Examples: \"2023-01-01\", \"today\", \"2023-01-01..*\" (from Jan 1, 2023 to any future date), \"*.2023-01-31\" (any date up to Jan 31, 2023), \"today..*\" (from today onwards), \"*.yesterday\" (any date up to yesterday). The keywords cannot be used to calculate relative dates (e.g. the following are not valid: \"today-1\" or \"tomorrow+1\").");
1003
1023
  const is = (field) => z.boolean().optional().describe(`Find only entities that are ${field} when true, or only entities that are not ${field} when false.`);
1004
1024
  const has = (field) => z.boolean().optional().describe(`Find only entities that have ${field} when true, or only entities that do not have ${field} when false. Example: hasOwner: true will find stories with an owner, hasOwner: false will find stories without an owner.`);
1005
- const user = (field) => z.string().optional().describe(`Find entities where the ${field} match the specified user. This must either be the user\'s mention name or the keyword "me" for the current user.`);
1025
+ const user = (field) => z.string().optional().describe(`Find entities where the ${field} match the specified user. This must either be the user's mention name or the keyword "me" for the current user.`);
1006
1026
 
1007
1027
  //#endregion
1008
1028
  //#region src/tools/epics.ts
@@ -1062,9 +1082,9 @@ var EpicTools = class EpicTools extends BaseTools {
1062
1082
  if (!epic) throw new Error(`Failed to retrieve Shortcut epic with public ID: ${epicPublicId}`);
1063
1083
  return this.toResult(`Epic: ${epicPublicId}`, await this.entityWithRelatedEntities(epic, "epic", full));
1064
1084
  }
1065
- async createEpic({ name: name$1, owner, teamId: group_id, description }) {
1085
+ async createEpic({ name, owner, teamId: group_id, description }) {
1066
1086
  const epic = await this.client.createEpic({
1067
- name: name$1,
1087
+ name,
1068
1088
  group_id,
1069
1089
  owner_ids: owner ? [owner] : void 0,
1070
1090
  description
@@ -1130,9 +1150,9 @@ var IterationTools = class IterationTools extends BaseTools {
1130
1150
  if (!iteration) throw new Error(`Failed to retrieve Shortcut iteration with public ID: ${iterationPublicId}.`);
1131
1151
  return this.toResult(`Iteration: ${iterationPublicId}`, await this.entityWithRelatedEntities(iteration, "iteration", full));
1132
1152
  }
1133
- async createIteration({ name: name$1, startDate, endDate, teamId, description }) {
1153
+ async createIteration({ name, startDate, endDate, teamId, description }) {
1134
1154
  const iteration = await this.client.createIteration({
1135
- name: name$1,
1155
+ name,
1136
1156
  start_date: startDate,
1137
1157
  end_date: endDate,
1138
1158
  group_ids: teamId ? [teamId] : void 0,
@@ -1301,8 +1321,8 @@ The story will be added to the default state for the workflow.
1301
1321
  iteration: z.number().optional().describe("The iteration id of the iteration the story belongs to"),
1302
1322
  team: z.string().optional().describe("The team ID or mention name of the team the story belongs to. Required unless a workflow is specified."),
1303
1323
  workflow: z.number().optional().describe("The workflow ID to add the story to. Required unless a team is specified.")
1304
- }, async ({ name: name$1, description, type, owner, epic, iteration, team, workflow }) => await tools.createStory({
1305
- name: name$1,
1324
+ }, async ({ name, description, type, owner, epic, iteration, team, workflow }) => await tools.createStory({
1325
+ name,
1306
1326
  description,
1307
1327
  type,
1308
1328
  owner,
@@ -1418,14 +1438,14 @@ The story will be added to the default state for the workflow.
1418
1438
  const branchName = story.formatted_vcs_branch_name || this.createBranchName(currentUser, story);
1419
1439
  return this.toResult(`Branch name for story sc-${storyPublicId}: ${branchName}`);
1420
1440
  }
1421
- async createStory({ name: name$1, description, type, owner, epic, iteration, team, workflow }) {
1441
+ async createStory({ name, description, type, owner, epic, iteration, team, workflow }) {
1422
1442
  if (!workflow && !team) throw new Error("Team or Workflow has to be specified");
1423
1443
  if (!workflow && team) workflow = (await this.client.getTeam(team))?.workflow_ids?.[0];
1424
1444
  if (!workflow) throw new Error("Failed to find workflow for team");
1425
1445
  const fullWorkflow = await this.client.getWorkflow(workflow);
1426
1446
  if (!fullWorkflow) throw new Error("Failed to find workflow");
1427
1447
  const story = await this.client.createStory({
1428
- name: name$1,
1448
+ name,
1429
1449
  description,
1430
1450
  story_type: type,
1431
1451
  owner_ids: owner ? [owner] : [],
@@ -1436,9 +1456,9 @@ The story will be added to the default state for the workflow.
1436
1456
  });
1437
1457
  return this.toResult(`Created story: sc-${story.id}`);
1438
1458
  }
1439
- async createSubTask({ parentStoryPublicId, name: name$1, description }) {
1459
+ async createSubTask({ parentStoryPublicId, name, description }) {
1440
1460
  if (!parentStoryPublicId) throw new Error("ID of parent story is required");
1441
- if (!name$1) throw new Error("Sub-task name is required");
1461
+ if (!name) throw new Error("Sub-task name is required");
1442
1462
  const parentStory = await this.client.getStory(parentStoryPublicId);
1443
1463
  if (!parentStory) throw new Error(`Failed to retrieve parent story with public ID: ${parentStoryPublicId}`);
1444
1464
  const workflow = await this.client.getWorkflow(parentStory.workflow_id);
@@ -1446,7 +1466,7 @@ The story will be added to the default state for the workflow.
1446
1466
  const workflowState = workflow.states[0];
1447
1467
  if (!workflowState) throw new Error("Failed to determine default state for sub-task");
1448
1468
  const subTask = await this.client.createStory({
1449
- name: name$1,
1469
+ name,
1450
1470
  description,
1451
1471
  story_type: parentStory.story_type,
1452
1472
  epic_id: parentStory.epic_id,
@@ -1592,7 +1612,7 @@ var TeamTools = class TeamTools extends BaseTools {
1592
1612
  teamPublicId: z.string().describe("The public ID of the team to get"),
1593
1613
  full: z.boolean().optional().default(false).describe("True to return all team fields from the API. False to return a slim version that excludes uncommon fields")
1594
1614
  }, async ({ teamPublicId, full }) => await tools.getTeam(teamPublicId, full));
1595
- server.addToolWithReadAccess("teams-list", "List all Shortcut teams", async () => await tools.getTeams());
1615
+ server.addToolWithReadAccess("teams-list", "List Shortcut teams", { includeArchived: z.boolean().describe("True to include archived teams.").optional().default(false) }, async ({ includeArchived }) => await tools.getTeams(includeArchived));
1596
1616
  return tools;
1597
1617
  }
1598
1618
  async getTeam(teamPublicId, full = false) {
@@ -1600,10 +1620,11 @@ var TeamTools = class TeamTools extends BaseTools {
1600
1620
  if (!team) return this.toResult(`Team with public ID: ${teamPublicId} not found.`);
1601
1621
  return this.toResult(`Team: ${team.id}`, await this.entityWithRelatedEntities(team, "team", full));
1602
1622
  }
1603
- async getTeams() {
1623
+ async getTeams(includeArchived) {
1604
1624
  const teams = await this.client.getTeams();
1605
1625
  if (!teams.length) return this.toResult(`No teams found.`);
1606
- return this.toResult(`Result (first ${teams.length} shown of ${teams.length} total teams found):`, await this.entitiesWithRelatedEntities(teams, "teams"));
1626
+ const filteredTeams = includeArchived ? teams : teams.filter((team) => !team.archived);
1627
+ return this.toResult(`Result (first ${filteredTeams.length} shown of ${filteredTeams.length} total teams found):`, await this.entitiesWithRelatedEntities(filteredTeams, "teams"));
1607
1628
  }
1608
1629
  };
1609
1630
 
@@ -1618,9 +1639,9 @@ var UserTools = class UserTools extends BaseTools {
1618
1639
  return tools;
1619
1640
  }
1620
1641
  async getCurrentUser() {
1621
- const user$1 = await this.client.getCurrentUser();
1622
- if (!user$1) throw new Error("Failed to retrieve current user.");
1623
- return this.toResult(`Current user:`, user$1);
1642
+ const user = await this.client.getCurrentUser();
1643
+ if (!user) throw new Error("Failed to retrieve current user.");
1644
+ return this.toResult(`Current user:`, user);
1624
1645
  }
1625
1646
  async getCurrentUserTeams() {
1626
1647
  const teams = await this.client.getTeams();
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@shortcut/mcp",
3
- "author": "Shortcut (https://www.shortcut.com)",
3
+ "version": "0.20.0",
4
4
  "description": "Shortcut MCP Server",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/useshortcut/mcp-server-shortcut.git"
8
- },
9
5
  "keywords": [
10
6
  "shortcut",
11
7
  "mcp",
12
8
  "modelcontextprotocol"
13
9
  ],
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/useshortcut/mcp-server-shortcut.git"
13
+ },
14
14
  "license": "MIT",
15
- "version": "0.19.0",
15
+ "author": "Shortcut (https://www.shortcut.com)",
16
16
  "type": "module",
17
17
  "main": "dist/index.js",
18
18
  "bin": {
@@ -21,17 +21,16 @@
21
21
  "files": [
22
22
  "dist"
23
23
  ],
24
- "devDependencies": {
25
- "@biomejs/biome": "2.0.5",
26
- "@types/bun": "latest",
27
- "@types/express": "^4.17.21",
28
- "bun": "^1.2.5",
29
- "husky": "^9.1.7",
30
- "pino-pretty": "^13.0.0",
31
- "tsdown": "^0.12.9"
32
- },
33
- "peerDependencies": {
34
- "typescript": "^5"
24
+ "scripts": {
25
+ "build": "tsdown",
26
+ "dev:http": "bun run src/server-http.ts",
27
+ "format": "biome check --write ./",
28
+ "lint": "biome check ./",
29
+ "prepare": "husky",
30
+ "prod:http": "node dist/server-http.js",
31
+ "prepublish": "bun run build",
32
+ "test": "bun test",
33
+ "ts": "tsc -b"
35
34
  },
36
35
  "dependencies": {
37
36
  "@modelcontextprotocol/sdk": "^1.25.1",
@@ -41,15 +40,16 @@
41
40
  "pino-http": "^10.3.0",
42
41
  "zod": "^3.24.4"
43
42
  },
44
- "scripts": {
45
- "test": "bun test",
46
- "format": "biome check --write ./",
47
- "lint": "biome check ./",
48
- "ts": "tsc -b",
49
- "build": "tsdown",
50
- "prepublish": "bun run build",
51
- "prepare": "husky",
52
- "dev:http": "bun run src/server-http.ts",
53
- "prod:http": "node dist/server-http.js"
43
+ "devDependencies": {
44
+ "@biomejs/biome": "2.3.14",
45
+ "@types/bun": "latest",
46
+ "@types/express": "^4.17.21",
47
+ "bun": "1.3.9",
48
+ "husky": "9.1.7",
49
+ "pino-pretty": "^13.0.0",
50
+ "tsdown": "0.20.3"
51
+ },
52
+ "peerDependencies": {
53
+ "typescript": "^5"
54
54
  }
55
55
  }