@shortcut/mcp 0.14.0 → 0.15.2

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.
Files changed (3) hide show
  1. package/README.md +44 -39
  2. package/dist/index.js +177 -150
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -129,67 +129,72 @@ Or you can edit the local JSON file directly:
129
129
 
130
130
  ### Stories
131
131
 
132
- - **get-story** - Get a single Shortcut story by ID
133
- - **search-stories** - Find Shortcut stories with filtering and search options
134
- - **get-story-branch-name** - Get the recommended branch name (based on workspace settings) for a specific story.
135
- - **create-story** - Create a new Shortcut story
136
- - **update-story** - Update an existing Shortcut story
137
- - **upload-file-to-story** - Upload a file and link it to a story
138
- - **assign-current-user-as-owner** - Assign the current user as the owner of a story
139
- - **unassign-current-user-as-owner** - Unassign the current user as the owner of a story
140
- - **create-story-comment** - Create a comment on a story
141
- - **add-task-to-story** - Add a task to a story
142
- - **update-task** - Update a task in a story
143
- - **add-relation-to-story** - Add a story relationship (relates to, blocks, duplicates, etc.)
144
- - **add-external-link-to-story** - Add an external link to a Shortcut story
145
- - **remove-external-link-from-story** - Remove an external link from a Shortcut story
146
- - **set-story-external-links** - Replace all external links on a story with a new set of links
147
- - **get-stories-by-external-link** - Find all stories that contain a specific external link
132
+ - **stories-get-by-id** - Get a single Shortcut story by ID
133
+ - **stories-search** - Find Shortcut stories with filtering and search options
134
+ - **stories-get-branch-name** - Get the recommended branch name (based on workspace settings) for a specific story.
135
+ - **stories-create** - Create a new Shortcut story
136
+ - **stories-update** - Update an existing Shortcut story
137
+ - **stories-upload-file** - Upload a file and link it to a story
138
+ - **stories-assign-current-user** - Assign the current user as the owner of a story
139
+ - **stories-unassign-current-user** - Unassign the current user as the owner of a story
140
+ - **stories-create-comment** - Create a comment on a story
141
+ - **stories-add-task** - Add a task to a story
142
+ - **stories-update-task** - Update a task in a story
143
+ - **stories-add-relation** - Add a story relationship (relates to, blocks, duplicates, etc.)
144
+ - **stories-add-external-link** - Add an external link to a Shortcut story
145
+ - **stories-remove-external-link** - Remove an external link from a Shortcut story
146
+ - **stories-set-external-links** - Replace all external links on a story with a new set of links
147
+ - **stories-get-by-external-link** - Find all stories that contain a specific external link
148
148
 
149
149
  ### Epics
150
150
 
151
- - **get-epic** - Get a Shortcut epic by ID
152
- - **search-epics** - Find Shortcut epics with filtering and search options
153
- - **create-epic** - Create a new Shortcut epic
151
+ - **epics-get-by-id** - Get a Shortcut epic by ID
152
+ - **epics-search** - Find Shortcut epics with filtering and search options
153
+ - **epics-create** - Create a new Shortcut epic
154
154
 
155
155
  ### Iterations
156
156
 
157
- - **get-iteration-stories** - Get stories in a specific iteration by iteration ID
158
- - **get-iteration** - Get a Shortcut iteration by ID
159
- - **search-iterations** - Find Shortcut iterations with filtering and search options
160
- - **create-iteration** - Create a new Shortcut iteration with start/end dates
161
- - **get-active-iterations** - Get active iterations for the current user based on team memberships
162
- - **get-upcoming-iterations** - Get upcoming iterations for the current user based on team memberships
157
+ - **iterations-get-stories** - Get stories in a specific iteration by iteration ID
158
+ - **iterations-get-by-id** - Get a Shortcut iteration by ID
159
+ - **iterations-search** - Find Shortcut iterations with filtering and search options
160
+ - **iterations-create** - Create a new Shortcut iteration with start/end dates
161
+ - **iterations-get-active** - Get active iterations for the current user based on team memberships
162
+ - **iterations-get-upcoming** - Get upcoming iterations for the current user based on team memberships
163
163
 
164
164
  ### Objectives
165
165
 
166
- - **get-objective** - Get a Shortcut objective by ID
167
- - **search-objectives** - Find Shortcut objectives with filtering and search options
166
+ - **objectives-get-by-id** - Get a Shortcut objective by ID
167
+ - **objectives-search** - Find Shortcut objectives with filtering and search options
168
168
 
169
169
  ### Teams
170
170
 
171
- - **get-team** - Get a Shortcut team by ID
172
- - **list-teams** - List all Shortcut teams
171
+ - **teams-get-by-id** - Get a Shortcut team by ID
172
+ - **teams-list** - List all Shortcut teams
173
173
 
174
174
  ### Workflows
175
175
 
176
- - **get-default-workflow** - Get the default workflow for a specific team or the workspace default
177
- - **get-workflow** - Get a Shortcut workflow by ID
178
- - **list-workflows** - List all Shortcut workflows
176
+ - **workflows-get-default** - Get the default workflow for a specific team or the workspace default
177
+ - **workflows-get-by-id** - Get a Shortcut workflow by ID
178
+ - **workflows-list** - List all Shortcut workflows
179
179
 
180
180
  ### Users
181
181
 
182
- - **get-current-user** - Get the current user information
183
- - **get-current-user-teams** - Get a list of teams where the current user is a member
184
- - **list-users** - Get all workspace users
182
+ - **users-get-current** - Get the current user information
183
+ - **users-get-current-teams** - Get a list of teams where the current user is a member
184
+ - **users-list** - Get all workspace users
185
185
 
186
186
  ### Documents
187
187
 
188
- - **create-document** - Create a new document in Shortcut with HTML content
188
+ - **documents-create** - Create a new document in Shortcut with HTML content
189
189
 
190
190
  ## Limit tools
191
191
 
192
- You can limit the tools available to the LLM by setting the `SHORTCUT_TOOLS` environment variable to a comma-separated list of entity types.
192
+ You can limit the tools available to the LLM by setting the `SHORTCUT_TOOLS` environment variable to a comma-separated list.
193
+
194
+ - Tools can be limited by entity type by just adding the entity, eg `stories` or `epics`.
195
+ - Individual tools can also be limitied by their full name, eg `stories-get-by-id` or `epics-search`.
196
+
197
+ By default, all tools are enabled.
193
198
 
194
199
  Example:
195
200
 
@@ -204,14 +209,14 @@ Example:
204
209
  ],
205
210
  "env": {
206
211
  "SHORTCUT_API_TOKEN": "<YOUR_SHORTCUT_API_TOKEN>",
207
- "SHORTCUT_TOOLS": "stories,epics"
212
+ "SHORTCUT_TOOLS": "stories,epics,iterations-create"
208
213
  }
209
214
  }
210
215
  }
211
216
  }
212
217
  ```
213
218
 
214
- The following values are accepted:
219
+ The following values are accepted in addition to the full tool names listed above under [Available Tools](#available-tools):
215
220
 
216
221
  - `users`
217
222
  - `stories`
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
3
  import { ShortcutClient } from "@shortcut/client";
5
4
  import { File } from "node:buffer";
6
5
  import { readFileSync } from "node:fs";
7
6
  import { basename } from "node:path";
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { z } from "zod";
9
9
 
10
10
  //#region src/client/cache.ts
@@ -450,7 +450,40 @@ var ShortcutClientWrapper = class {
450
450
  //#endregion
451
451
  //#region package.json
452
452
  var name = "@shortcut/mcp";
453
- var version = "0.14.0";
453
+ var version = "0.15.2";
454
+
455
+ //#endregion
456
+ //#region src/mcp/CustomMcpServer.ts
457
+ var CustomMcpServer = class extends McpServer {
458
+ readonly;
459
+ tools;
460
+ constructor({ readonly, tools }) {
461
+ super({
462
+ name,
463
+ version
464
+ });
465
+ this.readonly = readonly;
466
+ this.tools = new Set(tools || []);
467
+ }
468
+ shouldAddTool(name$1) {
469
+ if (!this.tools.size) return true;
470
+ const [entityType] = name$1.split("-");
471
+ if (this.tools.has(entityType) || this.tools.has(name$1)) return true;
472
+ return false;
473
+ }
474
+ addToolWithWriteAccess(...args) {
475
+ if (this.readonly) return null;
476
+ if (!this.shouldAddTool(args[0])) return null;
477
+ return super.tool(...args);
478
+ }
479
+ addToolWithReadAccess(...args) {
480
+ if (!this.shouldAddTool(args[0])) return null;
481
+ return super.tool(...args);
482
+ }
483
+ tool() {
484
+ throw new Error("Call addToolWithReadAccess or addToolWithWriteAccess instead.");
485
+ }
486
+ };
454
487
 
455
488
  //#endregion
456
489
  //#region src/tools/base.ts
@@ -458,9 +491,8 @@ var version = "0.14.0";
458
491
  * Base class for all tools.
459
492
  */
460
493
  var BaseTools = class {
461
- constructor(client$1, isReadonly$1 = false) {
494
+ constructor(client$1) {
462
495
  this.client = client$1;
463
- this.isReadonly = isReadonly$1;
464
496
  }
465
497
  renameEntityProps(entity) {
466
498
  if (!entity || typeof entity !== "object") return entity;
@@ -792,9 +824,9 @@ var BaseTools = class {
792
824
  //#endregion
793
825
  //#region src/tools/documents.ts
794
826
  var DocumentTools = class DocumentTools extends BaseTools {
795
- static create(client$1, server$1, isReadonly$1 = false) {
796
- const tools = new DocumentTools(client$1, isReadonly$1);
797
- if (!isReadonly$1) server$1.tool("create-document", "Create a new document in Shortcut with a title and content. Returns the document's id, title, and app_url. Note: Use HTML markup for the content (e.g., <p>, <h1>, <ul>, <strong>) rather than Markdown.", {
827
+ static create(client$1, server$1) {
828
+ const tools = new DocumentTools(client$1);
829
+ server$1.addToolWithWriteAccess("documents-create", "Create a new document in Shortcut with a title and content. Returns the document's id, title, and app_url. Note: Use HTML markup for the content (e.g., <p>, <h1>, <ul>, <strong>) rather than Markdown.", {
798
830
  title: z.string().max(256).describe("The title for the new document (max 256 characters)"),
799
831
  content: z.string().describe("The content for the new document in HTML format (e.g., <p>Hello</p>, <h1>Title</h1>, <ul><li>Item</li></ul>)")
800
832
  }, async ({ title, content }) => await tools.createDocument(title, content));
@@ -876,13 +908,13 @@ const user = (field) => z.string().optional().describe(`Find entities where the
876
908
  //#endregion
877
909
  //#region src/tools/epics.ts
878
910
  var EpicTools = class EpicTools extends BaseTools {
879
- static create(client$1, server$1, isReadonly$1 = false) {
880
- const tools = new EpicTools(client$1, isReadonly$1);
881
- server$1.tool("get-epic", "Get a Shortcut epic by public ID", {
911
+ static create(client$1, server$1) {
912
+ const tools = new EpicTools(client$1);
913
+ server$1.addToolWithReadAccess("epics-get-by-id", "Get a Shortcut epic by public ID", {
882
914
  epicPublicId: z.number().positive().describe("The public ID of the epic to get"),
883
915
  full: z.boolean().optional().default(false).describe("True to return all epic fields from the API. False to return a slim version that excludes uncommon fields")
884
916
  }, async ({ epicPublicId, full }) => await tools.getEpic(epicPublicId, full));
885
- server$1.tool("search-epics", "Find Shortcut epics.", {
917
+ server$1.addToolWithReadAccess("epics-search", "Find Shortcut epics.", {
886
918
  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."),
887
919
  id: z.number().optional().describe("Find only epics with the specified public ID"),
888
920
  name: z.string().optional().describe("Find only epics matching the specified name"),
@@ -911,7 +943,7 @@ var EpicTools = class EpicTools extends BaseTools {
911
943
  completed: date(),
912
944
  due: date()
913
945
  }, async ({ nextPageToken,...params }) => await tools.searchEpics(params, nextPageToken));
914
- if (!isReadonly$1) server$1.tool("create-epic", "Create a new Shortcut epic.", {
946
+ server$1.addToolWithWriteAccess("epics-create", "Create a new Shortcut epic.", {
915
947
  name: z.string().describe("The name of the epic"),
916
948
  owner: z.string().optional().describe("The user ID of the owner of the epic"),
917
949
  description: z.string().optional().describe("A description of the epic"),
@@ -946,17 +978,17 @@ var EpicTools = class EpicTools extends BaseTools {
946
978
  //#endregion
947
979
  //#region src/tools/iterations.ts
948
980
  var IterationTools = class IterationTools extends BaseTools {
949
- static create(client$1, server$1, isReadonly$1 = false) {
950
- const tools = new IterationTools(client$1, isReadonly$1);
951
- server$1.tool("get-iteration-stories", "Get stories in a specific iteration by iteration public ID", {
981
+ static create(client$1, server$1) {
982
+ const tools = new IterationTools(client$1);
983
+ server$1.addToolWithReadAccess("iterations-get-stories", "Get stories in a specific iteration by iteration public ID", {
952
984
  iterationPublicId: z.number().positive().describe("The public ID of the iteration"),
953
985
  includeStoryDescriptions: z.boolean().optional().default(false).describe("Indicate whether story descriptions should be included. Including descriptions may take longer and will increase the size of the response.")
954
986
  }, async ({ iterationPublicId, includeStoryDescriptions }) => await tools.getIterationStories(iterationPublicId, includeStoryDescriptions));
955
- server$1.tool("get-iteration", "Get a Shortcut iteration by public ID", {
987
+ server$1.addToolWithReadAccess("iterations-get-by-id", "Get a Shortcut iteration by public ID", {
956
988
  iterationPublicId: z.number().positive().describe("The public ID of the iteration to get"),
957
989
  full: z.boolean().optional().default(false).describe("True to return all iteration fields from the API. False to return a slim version that excludes uncommon fields")
958
990
  }, async ({ iterationPublicId, full }) => await tools.getIteration(iterationPublicId, full));
959
- server$1.tool("search-iterations", "Find Shortcut iterations.", {
991
+ server$1.addToolWithReadAccess("iterations-search", "Find Shortcut iterations.", {
960
992
  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."),
961
993
  id: z.number().optional().describe("Find only iterations with the specified public ID"),
962
994
  name: z.string().optional().describe("Find only iterations matching the specified name"),
@@ -972,15 +1004,15 @@ var IterationTools = class IterationTools extends BaseTools {
972
1004
  startDate: date(),
973
1005
  endDate: date()
974
1006
  }, async ({ nextPageToken,...params }) => await tools.searchIterations(params, nextPageToken));
975
- if (!isReadonly$1) server$1.tool("create-iteration", "Create a new Shortcut iteration", {
1007
+ server$1.addToolWithWriteAccess("iterations-create", "Create a new Shortcut iteration", {
976
1008
  name: z.string().describe("The name of the iteration"),
977
1009
  startDate: z.string().describe("The start date of the iteration in YYYY-MM-DD format"),
978
1010
  endDate: z.string().describe("The end date of the iteration in YYYY-MM-DD format"),
979
1011
  teamId: z.string().optional().describe("The ID of a team to assign the iteration to"),
980
1012
  description: z.string().optional().describe("A description of the iteration")
981
1013
  }, async (params) => await tools.createIteration(params));
982
- server$1.tool("get-active-iterations", "Get the active Shortcut iterations for the current user based on their team memberships", { teamId: z.string().optional().describe("The ID of a team to filter iterations by") }, async ({ teamId }) => await tools.getActiveIterations(teamId));
983
- server$1.tool("get-upcoming-iterations", "Get the upcoming Shortcut iterations for the current user based on their team memberships", { teamId: z.string().optional().describe("The ID of a team to filter iterations by") }, async ({ teamId }) => await tools.getUpcomingIterations(teamId));
1014
+ server$1.addToolWithReadAccess("iterations-get-active", "Get the active Shortcut iterations for the current user based on their team memberships", { teamId: z.string().optional().describe("The ID of a team to filter iterations by") }, async ({ teamId }) => await tools.getActiveIterations(teamId));
1015
+ server$1.addToolWithReadAccess("iterations-get-upcoming", "Get the upcoming Shortcut iterations for the current user based on their team memberships", { teamId: z.string().optional().describe("The ID of a team to filter iterations by") }, async ({ teamId }) => await tools.getUpcomingIterations(teamId));
984
1016
  return tools;
985
1017
  }
986
1018
  async getIterationStories(iterationPublicId, includeDescription) {
@@ -1057,13 +1089,13 @@ var IterationTools = class IterationTools extends BaseTools {
1057
1089
  //#endregion
1058
1090
  //#region src/tools/objectives.ts
1059
1091
  var ObjectiveTools = class ObjectiveTools extends BaseTools {
1060
- static create(client$1, server$1, isReadonly$1 = false) {
1061
- const tools = new ObjectiveTools(client$1, isReadonly$1);
1062
- server$1.tool("get-objective", "Get a Shortcut objective by public ID", {
1092
+ static create(client$1, server$1) {
1093
+ const tools = new ObjectiveTools(client$1);
1094
+ server$1.addToolWithReadAccess("objectives-get-by-id", "Get a Shortcut objective by public ID", {
1063
1095
  objectivePublicId: z.number().positive().describe("The public ID of the objective to get"),
1064
1096
  full: z.boolean().optional().default(false).describe("True to return all objective fields from the API. False to return a slim version that excludes uncommon fields")
1065
1097
  }, async ({ objectivePublicId, full }) => await tools.getObjective(objectivePublicId, full));
1066
- server$1.tool("search-objectives", "Find Shortcut objectives.", {
1098
+ server$1.addToolWithReadAccess("objectives-search", "Find Shortcut objectives.", {
1067
1099
  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."),
1068
1100
  id: z.number().optional().describe("Find objectives matching the specified id"),
1069
1101
  name: z.string().optional().describe("Find objectives matching the specified name"),
@@ -1105,13 +1137,13 @@ var ObjectiveTools = class ObjectiveTools extends BaseTools {
1105
1137
  //#endregion
1106
1138
  //#region src/tools/stories.ts
1107
1139
  var StoryTools = class StoryTools extends BaseTools {
1108
- static create(client$1, server$1, isReadonly$1 = false) {
1109
- const tools = new StoryTools(client$1, isReadonly$1);
1110
- server$1.tool("get-story", "Get a Shortcut story by public ID", {
1140
+ static create(client$1, server$1) {
1141
+ const tools = new StoryTools(client$1);
1142
+ server$1.addToolWithReadAccess("stories-get-by-id", "Get a Shortcut story by public ID", {
1111
1143
  storyPublicId: z.number().positive().describe("The public ID of the story to get"),
1112
1144
  full: z.boolean().optional().default(false).describe("True to return all story fields from the API. False to return a slim version that excludes uncommon fields")
1113
1145
  }, async ({ storyPublicId, full }) => await tools.getStory(storyPublicId, full));
1114
- server$1.tool("search-stories", "Find Shortcut stories.", {
1146
+ server$1.addToolWithReadAccess("stories-search", "Find Shortcut stories.", {
1115
1147
  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."),
1116
1148
  id: z.number().optional().describe("Find only stories with the specified public ID"),
1117
1149
  name: z.string().optional().describe("Find only stories matching the specified name"),
@@ -1162,103 +1194,101 @@ var StoryTools = class StoryTools extends BaseTools {
1162
1194
  completed: date(),
1163
1195
  due: date()
1164
1196
  }, async ({ nextPageToken,...params }) => await tools.searchStories(params, nextPageToken));
1165
- server$1.tool("get-story-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));
1166
- if (!isReadonly$1) {
1167
- server$1.tool("create-story", `Create a new Shortcut story.
1197
+ server$1.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));
1198
+ server$1.addToolWithWriteAccess("stories-create", `Create a new Shortcut story.
1168
1199
  Name is required, and either a Team or Workflow must be specified:
1169
1200
  - If only Team is specified, we will use the default workflow for that team.
1170
1201
  - If Workflow is specified, it will be used regardless of Team.
1171
1202
  The story will be added to the default state for the workflow.
1172
1203
  `, {
1173
- name: z.string().min(1).max(512).describe("The name of the story. Required."),
1174
- description: z.string().max(1e4).optional().describe("The description of the story"),
1175
- type: z.enum([
1176
- "feature",
1177
- "bug",
1178
- "chore"
1179
- ]).default("feature").describe("The type of the story"),
1180
- owner: z.string().optional().describe("The user id of the owner of the story"),
1181
- epic: z.number().optional().describe("The epic id of the epic the story belongs to"),
1182
- iteration: z.number().optional().describe("The iteration id of the iteration the story belongs to"),
1183
- team: z.string().optional().describe("The team ID or mention name of the team the story belongs to. Required unless a workflow is specified."),
1184
- workflow: z.number().optional().describe("The workflow ID to add the story to. Required unless a team is specified.")
1185
- }, async ({ name: name$1, description, type, owner, epic, iteration, team, workflow }) => await tools.createStory({
1186
- name: name$1,
1187
- description,
1188
- type,
1189
- owner,
1190
- epic,
1191
- iteration,
1192
- team,
1193
- workflow
1194
- }));
1195
- server$1.tool("update-story", "Update an existing Shortcut story. Only provide fields you want to update. The story public ID will always be included in updates.", {
1196
- storyPublicId: z.number().positive().describe("The public ID of the story to update"),
1197
- name: z.string().max(512).optional().describe("The name of the story"),
1198
- description: z.string().max(1e4).optional().describe("The description of the story"),
1199
- type: z.enum([
1200
- "feature",
1201
- "bug",
1202
- "chore"
1203
- ]).optional().describe("The type of the story"),
1204
- epic: z.number().nullable().optional().describe("The epic id of the epic the story belongs to, or null to unset"),
1205
- estimate: z.number().nullable().optional().describe("The point estimate of the story, or null to unset"),
1206
- iteration: z.number().nullable().optional().describe("The iteration id of the iteration the story belongs to, or null to unset"),
1207
- owner_ids: z.array(z.string()).optional().describe("Array of user UUIDs to assign as owners of the story"),
1208
- workflow_state_id: z.number().optional().describe("The workflow state ID to move the story to"),
1209
- labels: z.array(z.object({
1210
- name: z.string().describe("The name of the label"),
1211
- color: z.string().optional().describe("The color of the label"),
1212
- description: z.string().optional().describe("The description of the label")
1213
- })).optional().describe("Labels to assign to the story")
1214
- }, async (params) => await tools.updateStory(params));
1215
- server$1.tool("upload-file-to-story", "Upload a file and link it to a story.", {
1216
- storyPublicId: z.number().positive().describe("The public ID of the story"),
1217
- filePath: z.string().describe("The path to the file to upload")
1218
- }, async ({ storyPublicId, filePath }) => await tools.uploadFileToStory(storyPublicId, filePath));
1219
- server$1.tool("assign-current-user-as-owner", "Assign the current user as the owner of a story", { storyPublicId: z.number().positive().describe("The public ID of the story") }, async ({ storyPublicId }) => await tools.assignCurrentUserAsOwner(storyPublicId));
1220
- server$1.tool("unassign-current-user-as-owner", "Unassign the current user as the owner of a story", { storyPublicId: z.number().positive().describe("The public ID of the story") }, async ({ storyPublicId }) => await tools.unassignCurrentUserAsOwner(storyPublicId));
1221
- server$1.tool("create-story-comment", "Create a comment on a story", {
1222
- storyPublicId: z.number().positive().describe("The public ID of the story"),
1223
- text: z.string().min(1).describe("The text of the comment")
1224
- }, async (params) => await tools.createStoryComment(params));
1225
- server$1.tool("add-task-to-story", "Add a task to a story", {
1226
- storyPublicId: z.number().positive().describe("The public ID of the story"),
1227
- taskDescription: z.string().min(1).describe("The description of the task"),
1228
- taskOwnerIds: z.array(z.string()).optional().describe("Array of user IDs to assign as owners of the task")
1229
- }, async (params) => await tools.addTaskToStory(params));
1230
- server$1.tool("update-task", "Update a task in a story", {
1231
- storyPublicId: z.number().positive().describe("The public ID of the story"),
1232
- taskPublicId: z.number().positive().describe("The public ID of the task"),
1233
- taskDescription: z.string().optional().describe("The description of the task"),
1234
- taskOwnerIds: z.array(z.string()).optional().describe("Array of user IDs to assign as owners of the task"),
1235
- isCompleted: z.boolean().optional().describe("Whether the task is completed or not")
1236
- }, async (params) => await tools.updateTask(params));
1237
- server$1.tool("add-relation-to-story", "Add a story relationship to a story", {
1238
- storyPublicId: z.number().positive().describe("The public ID of the story"),
1239
- relatedStoryPublicId: z.number().positive().describe("The public ID of the related story"),
1240
- relationshipType: z.enum([
1241
- "relates to",
1242
- "blocks",
1243
- "blocked by",
1244
- "duplicates",
1245
- "duplicated by"
1246
- ]).optional().default("relates to").describe("The type of relationship")
1247
- }, async (params) => await tools.addRelationToStory(params));
1248
- server$1.tool("add-external-link-to-story", "Add an external link to a Shortcut story", {
1249
- storyPublicId: z.number().positive().describe("The public ID of the story"),
1250
- externalLink: z.string().url().max(2048).describe("The external link URL to add")
1251
- }, async ({ storyPublicId, externalLink }) => await tools.addExternalLinkToStory(storyPublicId, externalLink));
1252
- server$1.tool("remove-external-link-from-story", "Remove an external link from a Shortcut story", {
1253
- storyPublicId: z.number().positive().describe("The public ID of the story"),
1254
- externalLink: z.string().url().max(2048).describe("The external link URL to remove")
1255
- }, async ({ storyPublicId, externalLink }) => await tools.removeExternalLinkFromStory(storyPublicId, externalLink));
1256
- server$1.tool("set-story-external-links", "Replace all external links on a story with a new set of links", {
1257
- storyPublicId: z.number().positive().describe("The public ID of the story"),
1258
- externalLinks: z.array(z.string().url().max(2048)).describe("Array of external link URLs to set (replaces all existing links)")
1259
- }, async ({ storyPublicId, externalLinks }) => await tools.setStoryExternalLinks(storyPublicId, externalLinks));
1260
- }
1261
- server$1.tool("get-stories-by-external-link", "Find all stories that contain a specific external link", { externalLink: z.string().url().max(2048).describe("The external link URL to search for") }, async ({ externalLink }) => await tools.getStoriesByExternalLink(externalLink));
1204
+ name: z.string().min(1).max(512).describe("The name of the story. Required."),
1205
+ description: z.string().max(1e4).optional().describe("The description of the story"),
1206
+ type: z.enum([
1207
+ "feature",
1208
+ "bug",
1209
+ "chore"
1210
+ ]).default("feature").describe("The type of the story"),
1211
+ owner: z.string().optional().describe("The user id of the owner of the story"),
1212
+ epic: z.number().optional().describe("The epic id of the epic the story belongs to"),
1213
+ iteration: z.number().optional().describe("The iteration id of the iteration the story belongs to"),
1214
+ team: z.string().optional().describe("The team ID or mention name of the team the story belongs to. Required unless a workflow is specified."),
1215
+ workflow: z.number().optional().describe("The workflow ID to add the story to. Required unless a team is specified.")
1216
+ }, async ({ name: name$1, description, type, owner, epic, iteration, team, workflow }) => await tools.createStory({
1217
+ name: name$1,
1218
+ description,
1219
+ type,
1220
+ owner,
1221
+ epic,
1222
+ iteration,
1223
+ team,
1224
+ workflow
1225
+ }));
1226
+ server$1.addToolWithWriteAccess("stories-update", "Update an existing Shortcut story. Only provide fields you want to update. The story public ID will always be included in updates.", {
1227
+ storyPublicId: z.number().positive().describe("The public ID of the story to update"),
1228
+ name: z.string().max(512).optional().describe("The name of the story"),
1229
+ description: z.string().max(1e4).optional().describe("The description of the story"),
1230
+ type: z.enum([
1231
+ "feature",
1232
+ "bug",
1233
+ "chore"
1234
+ ]).optional().describe("The type of the story"),
1235
+ epic: z.number().nullable().optional().describe("The epic id of the epic the story belongs to, or null to unset"),
1236
+ estimate: z.number().nullable().optional().describe("The point estimate of the story, or null to unset"),
1237
+ iteration: z.number().nullable().optional().describe("The iteration id of the iteration the story belongs to, or null to unset"),
1238
+ owner_ids: z.array(z.string()).optional().describe("Array of user UUIDs to assign as owners of the story"),
1239
+ workflow_state_id: z.number().optional().describe("The workflow state ID to move the story to"),
1240
+ labels: z.array(z.object({
1241
+ name: z.string().describe("The name of the label"),
1242
+ color: z.string().optional().describe("The color of the label"),
1243
+ description: z.string().optional().describe("The description of the label")
1244
+ })).optional().describe("Labels to assign to the story")
1245
+ }, async (params) => await tools.updateStory(params));
1246
+ server$1.addToolWithWriteAccess("stories-upload-file", "Upload a file and link it to a story.", {
1247
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1248
+ filePath: z.string().describe("The path to the file to upload")
1249
+ }, async ({ storyPublicId, filePath }) => await tools.uploadFileToStory(storyPublicId, filePath));
1250
+ server$1.addToolWithWriteAccess("stories-assign-current-user", "Assign the current user as the owner of a story", { storyPublicId: z.number().positive().describe("The public ID of the story") }, async ({ storyPublicId }) => await tools.assignCurrentUserAsOwner(storyPublicId));
1251
+ server$1.addToolWithWriteAccess("stories-unassign-current-user", "Unassign the current user as the owner of a story", { storyPublicId: z.number().positive().describe("The public ID of the story") }, async ({ storyPublicId }) => await tools.unassignCurrentUserAsOwner(storyPublicId));
1252
+ server$1.addToolWithWriteAccess("stories-create-comment", "Create a comment on a story", {
1253
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1254
+ text: z.string().min(1).describe("The text of the comment")
1255
+ }, async (params) => await tools.createStoryComment(params));
1256
+ server$1.addToolWithWriteAccess("stories-add-task", "Add a task to a story", {
1257
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1258
+ taskDescription: z.string().min(1).describe("The description of the task"),
1259
+ taskOwnerIds: z.array(z.string()).optional().describe("Array of user IDs to assign as owners of the task")
1260
+ }, async (params) => await tools.addTaskToStory(params));
1261
+ server$1.addToolWithWriteAccess("stories-update-task", "Update a task in a story", {
1262
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1263
+ taskPublicId: z.number().positive().describe("The public ID of the task"),
1264
+ taskDescription: z.string().optional().describe("The description of the task"),
1265
+ taskOwnerIds: z.array(z.string()).optional().describe("Array of user IDs to assign as owners of the task"),
1266
+ isCompleted: z.boolean().optional().describe("Whether the task is completed or not")
1267
+ }, async (params) => await tools.updateTask(params));
1268
+ server$1.addToolWithWriteAccess("stories-add-relation", "Add a story relationship to a story", {
1269
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1270
+ relatedStoryPublicId: z.number().positive().describe("The public ID of the related story"),
1271
+ relationshipType: z.enum([
1272
+ "relates to",
1273
+ "blocks",
1274
+ "blocked by",
1275
+ "duplicates",
1276
+ "duplicated by"
1277
+ ]).optional().default("relates to").describe("The type of relationship")
1278
+ }, async (params) => await tools.addRelationToStory(params));
1279
+ server$1.addToolWithWriteAccess("stories-add-external-link", "Add an external link to a Shortcut story", {
1280
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1281
+ externalLink: z.string().url().max(2048).describe("The external link URL to add")
1282
+ }, async ({ storyPublicId, externalLink }) => await tools.addExternalLinkToStory(storyPublicId, externalLink));
1283
+ server$1.addToolWithWriteAccess("stories-remove-external-link", "Remove an external link from a Shortcut story", {
1284
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1285
+ externalLink: z.string().url().max(2048).describe("The external link URL to remove")
1286
+ }, async ({ storyPublicId, externalLink }) => await tools.removeExternalLinkFromStory(storyPublicId, externalLink));
1287
+ server$1.addToolWithWriteAccess("stories-set-external-links", "Replace all external links on a story with a new set of links", {
1288
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1289
+ externalLinks: z.array(z.string().url().max(2048)).describe("Array of external link URLs to set (replaces all existing links)")
1290
+ }, async ({ storyPublicId, externalLinks }) => await tools.setStoryExternalLinks(storyPublicId, externalLinks));
1291
+ server$1.addToolWithReadAccess("stories-get-by-external-link", "Find all stories that contain a specific external link", { externalLink: z.string().url().max(2048).describe("The external link URL to search for") }, async ({ externalLink }) => await tools.getStoriesByExternalLink(externalLink));
1262
1292
  return tools;
1263
1293
  }
1264
1294
  async assignCurrentUserAsOwner(storyPublicId) {
@@ -1437,13 +1467,13 @@ The story will be added to the default state for the workflow.
1437
1467
  //#endregion
1438
1468
  //#region src/tools/teams.ts
1439
1469
  var TeamTools = class TeamTools extends BaseTools {
1440
- static create(client$1, server$1, isReadonly$1 = false) {
1441
- const tools = new TeamTools(client$1, isReadonly$1);
1442
- server$1.tool("get-team", "Get a Shortcut team by public ID", {
1470
+ static create(client$1, server$1) {
1471
+ const tools = new TeamTools(client$1);
1472
+ server$1.addToolWithReadAccess("teams-get-by-id", "Get a Shortcut team by public ID", {
1443
1473
  teamPublicId: z.string().describe("The public ID of the team to get"),
1444
1474
  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")
1445
1475
  }, async ({ teamPublicId, full }) => await tools.getTeam(teamPublicId, full));
1446
- server$1.tool("list-teams", "List all Shortcut teams", async () => await tools.getTeams());
1476
+ server$1.addToolWithReadAccess("teams-list", "List all Shortcut teams", async () => await tools.getTeams());
1447
1477
  return tools;
1448
1478
  }
1449
1479
  async getTeam(teamPublicId, full = false) {
@@ -1461,11 +1491,11 @@ var TeamTools = class TeamTools extends BaseTools {
1461
1491
  //#endregion
1462
1492
  //#region src/tools/user.ts
1463
1493
  var UserTools = class UserTools extends BaseTools {
1464
- static create(client$1, server$1, isReadonly$1 = false) {
1465
- const tools = new UserTools(client$1, isReadonly$1);
1466
- server$1.tool("get-current-user", "Get the current user", async () => await tools.getCurrentUser());
1467
- server$1.tool("get-current-user-teams", "Get a list of teams where the current user is a member", async () => await tools.getCurrentUserTeams());
1468
- server$1.tool("list-users", "Get all users", async () => await tools.listMembers());
1494
+ static create(client$1, server$1) {
1495
+ const tools = new UserTools(client$1);
1496
+ server$1.addToolWithReadAccess("users-get-current", "Get the current user", async () => await tools.getCurrentUser());
1497
+ server$1.addToolWithReadAccess("users-get-current-teams", "Get a list of teams where the current user is a member", async () => await tools.getCurrentUserTeams());
1498
+ server$1.addToolWithReadAccess("users-list", "Get all users", async () => await tools.listMembers());
1469
1499
  return tools;
1470
1500
  }
1471
1501
  async getCurrentUser() {
@@ -1494,14 +1524,14 @@ var UserTools = class UserTools extends BaseTools {
1494
1524
  //#endregion
1495
1525
  //#region src/tools/workflows.ts
1496
1526
  var WorkflowTools = class WorkflowTools extends BaseTools {
1497
- static create(client$1, server$1, isReadonly$1 = false) {
1498
- const tools = new WorkflowTools(client$1, isReadonly$1);
1499
- server$1.tool("get-default-workflow", "Get the default workflow for a specific team or the global default if no team is specified.", { teamPublicId: z.string().optional().describe("The public ID of the team to get the default workflow for.") }, async ({ teamPublicId }) => await tools.getDefaultWorkflow(teamPublicId));
1500
- server$1.tool("get-workflow", "Get a Shortcut workflow by public ID", {
1527
+ static create(client$1, server$1) {
1528
+ const tools = new WorkflowTools(client$1);
1529
+ server$1.addToolWithReadAccess("workflows-get-default", "Get the default workflow for a specific team or the global default if no team is specified.", { teamPublicId: z.string().optional().describe("The public ID of the team to get the default workflow for.") }, async ({ teamPublicId }) => await tools.getDefaultWorkflow(teamPublicId));
1530
+ server$1.addToolWithReadAccess("workflows-get-by-id", "Get a Shortcut workflow by public ID", {
1501
1531
  workflowPublicId: z.number().positive().describe("The public ID of the workflow to get"),
1502
1532
  full: z.boolean().optional().default(false).describe("True to return all workflow fields from the API. False to return a slim version that excludes uncommon fields")
1503
1533
  }, async ({ workflowPublicId, full }) => await tools.getWorkflow(workflowPublicId, full));
1504
- server$1.tool("list-workflows", "List all Shortcut workflows", async () => await tools.listWorkflows());
1534
+ server$1.addToolWithReadAccess("workflows-list", "List all Shortcut workflows", async () => await tools.listWorkflows());
1505
1535
  return tools;
1506
1536
  }
1507
1537
  async getDefaultWorkflow(teamPublicId) {
@@ -1535,32 +1565,29 @@ var WorkflowTools = class WorkflowTools extends BaseTools {
1535
1565
  //#region src/server.ts
1536
1566
  let apiToken = process.env.SHORTCUT_API_TOKEN;
1537
1567
  let isReadonly = process.env.SHORTCUT_READONLY === "true";
1538
- let enabledTools = process.env.SHORTCUT_TOOLS?.length ? process.env.SHORTCUT_TOOLS.split(",").map((tool) => tool.trim()) : null;
1568
+ let enabledTools = (process.env.SHORTCUT_TOOLS || "").split(",").map((tool) => tool.trim()).filter(Boolean);
1539
1569
  if (process.argv.length >= 3) process.argv.slice(2).map((arg) => arg.split("=")).forEach(([name$1, value]) => {
1540
1570
  if (name$1 === "SHORTCUT_API_TOKEN") apiToken = value;
1541
1571
  if (name$1 === "SHORTCUT_READONLY") isReadonly = value === "true";
1542
- if (name$1 === "SHORTCUT_TOOLS") enabledTools = value.split(",").map((tool) => tool.trim());
1572
+ if (name$1 === "SHORTCUT_TOOLS") enabledTools = value.split(",").map((tool) => tool.trim()).filter(Boolean);
1543
1573
  });
1544
1574
  if (!apiToken) {
1545
1575
  console.error("SHORTCUT_API_TOKEN is required");
1546
1576
  process.exit(1);
1547
1577
  }
1548
- const server = new McpServer({
1549
- name,
1550
- version
1578
+ const server = new CustomMcpServer({
1579
+ readonly: isReadonly,
1580
+ tools: enabledTools
1551
1581
  });
1552
1582
  const client = new ShortcutClientWrapper(new ShortcutClient(apiToken));
1553
- const areToolsEnabled = (toolName) => {
1554
- return !enabledTools || enabledTools.includes(toolName);
1555
- };
1556
- if (areToolsEnabled("users")) UserTools.create(client, server, isReadonly);
1557
- if (areToolsEnabled("stories")) StoryTools.create(client, server, isReadonly);
1558
- if (areToolsEnabled("iterations")) IterationTools.create(client, server, isReadonly);
1559
- if (areToolsEnabled("epics")) EpicTools.create(client, server, isReadonly);
1560
- if (areToolsEnabled("objectives")) ObjectiveTools.create(client, server, isReadonly);
1561
- if (areToolsEnabled("teams")) TeamTools.create(client, server, isReadonly);
1562
- if (areToolsEnabled("workflows")) WorkflowTools.create(client, server, isReadonly);
1563
- if (areToolsEnabled("documents")) DocumentTools.create(client, server, isReadonly);
1583
+ UserTools.create(client, server);
1584
+ StoryTools.create(client, server);
1585
+ IterationTools.create(client, server);
1586
+ EpicTools.create(client, server);
1587
+ ObjectiveTools.create(client, server);
1588
+ TeamTools.create(client, server);
1589
+ WorkflowTools.create(client, server);
1590
+ DocumentTools.create(client, server);
1564
1591
  async function startServer() {
1565
1592
  try {
1566
1593
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "modelcontextprotocol"
13
13
  ],
14
14
  "license": "MIT",
15
- "version": "0.14.0",
15
+ "version": "0.15.2",
16
16
  "type": "module",
17
17
  "main": "dist/index.js",
18
18
  "bin": {