@shortcut/mcp 0.13.0 → 0.15.1

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 +76 -36
  2. package/dist/index.js +178 -145
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -129,63 +129,103 @@ 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
+
190
+ ## Limit tools
191
+
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.
198
+
199
+ Example:
200
+
201
+ ```json
202
+ {
203
+ "mcpServers": {
204
+ "shortcut": {
205
+ "command": "npx",
206
+ "args": [
207
+ "-y",
208
+ "@shortcut/mcp@latest"
209
+ ],
210
+ "env": {
211
+ "SHORTCUT_API_TOKEN": "<YOUR_SHORTCUT_API_TOKEN>",
212
+ "SHORTCUT_TOOLS": "stories,epics,iterations-create"
213
+ }
214
+ }
215
+ }
216
+ }
217
+ ```
218
+
219
+ The following values are accepted in addition to the full tool names listed above under [Available Tools](#available-tools):
220
+
221
+ - `users`
222
+ - `stories`
223
+ - `epics`
224
+ - `iterations`
225
+ - `objectives`
226
+ - `teams`
227
+ - `workflows`
228
+ - `documents`
189
229
 
190
230
  ## Read-only mode
191
231
 
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,41 @@ var ShortcutClientWrapper = class {
450
450
  //#endregion
451
451
  //#region package.json
452
452
  var name = "@shortcut/mcp";
453
- var version = "0.13.0";
453
+ var version = "0.15.1";
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
+ console.log("Checking tool:", name$1, this.tools.size);
470
+ if (!this.tools.size) return true;
471
+ const [entityType] = name$1.split("-");
472
+ if (this.tools.has(entityType) || this.tools.has(name$1)) return true;
473
+ return false;
474
+ }
475
+ addToolWithWriteAccess(...args) {
476
+ if (this.readonly) return null;
477
+ if (!this.shouldAddTool(args[0])) return null;
478
+ return super.tool(...args);
479
+ }
480
+ addToolWithReadAccess(...args) {
481
+ if (!this.shouldAddTool(args[0])) return null;
482
+ return super.tool(...args);
483
+ }
484
+ tool() {
485
+ throw new Error("Call addToolWithReadAccess or addToolWithWriteAccess instead.");
486
+ }
487
+ };
454
488
 
455
489
  //#endregion
456
490
  //#region src/tools/base.ts
@@ -458,9 +492,8 @@ var version = "0.13.0";
458
492
  * Base class for all tools.
459
493
  */
460
494
  var BaseTools = class {
461
- constructor(client$1, isReadonly$1 = false) {
495
+ constructor(client$1) {
462
496
  this.client = client$1;
463
- this.isReadonly = isReadonly$1;
464
497
  }
465
498
  renameEntityProps(entity) {
466
499
  if (!entity || typeof entity !== "object") return entity;
@@ -792,9 +825,9 @@ var BaseTools = class {
792
825
  //#endregion
793
826
  //#region src/tools/documents.ts
794
827
  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.", {
828
+ static create(client$1, server$1) {
829
+ const tools = new DocumentTools(client$1);
830
+ 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
831
  title: z.string().max(256).describe("The title for the new document (max 256 characters)"),
799
832
  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
833
  }, async ({ title, content }) => await tools.createDocument(title, content));
@@ -876,13 +909,13 @@ const user = (field) => z.string().optional().describe(`Find entities where the
876
909
  //#endregion
877
910
  //#region src/tools/epics.ts
878
911
  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", {
912
+ static create(client$1, server$1) {
913
+ const tools = new EpicTools(client$1);
914
+ server$1.addToolWithReadAccess("epics-get-by-id", "Get a Shortcut epic by public ID", {
882
915
  epicPublicId: z.number().positive().describe("The public ID of the epic to get"),
883
916
  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
917
  }, async ({ epicPublicId, full }) => await tools.getEpic(epicPublicId, full));
885
- server$1.tool("search-epics", "Find Shortcut epics.", {
918
+ server$1.addToolWithReadAccess("epics-search", "Find Shortcut epics.", {
886
919
  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
920
  id: z.number().optional().describe("Find only epics with the specified public ID"),
888
921
  name: z.string().optional().describe("Find only epics matching the specified name"),
@@ -911,7 +944,7 @@ var EpicTools = class EpicTools extends BaseTools {
911
944
  completed: date(),
912
945
  due: date()
913
946
  }, async ({ nextPageToken,...params }) => await tools.searchEpics(params, nextPageToken));
914
- if (!isReadonly$1) server$1.tool("create-epic", "Create a new Shortcut epic.", {
947
+ server$1.addToolWithWriteAccess("epics-create", "Create a new Shortcut epic.", {
915
948
  name: z.string().describe("The name of the epic"),
916
949
  owner: z.string().optional().describe("The user ID of the owner of the epic"),
917
950
  description: z.string().optional().describe("A description of the epic"),
@@ -946,17 +979,17 @@ var EpicTools = class EpicTools extends BaseTools {
946
979
  //#endregion
947
980
  //#region src/tools/iterations.ts
948
981
  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", {
982
+ static create(client$1, server$1) {
983
+ const tools = new IterationTools(client$1);
984
+ server$1.addToolWithReadAccess("iterations-get-stories", "Get stories in a specific iteration by iteration public ID", {
952
985
  iterationPublicId: z.number().positive().describe("The public ID of the iteration"),
953
986
  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
987
  }, async ({ iterationPublicId, includeStoryDescriptions }) => await tools.getIterationStories(iterationPublicId, includeStoryDescriptions));
955
- server$1.tool("get-iteration", "Get a Shortcut iteration by public ID", {
988
+ server$1.addToolWithReadAccess("iterations-get-by-id", "Get a Shortcut iteration by public ID", {
956
989
  iterationPublicId: z.number().positive().describe("The public ID of the iteration to get"),
957
990
  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
991
  }, async ({ iterationPublicId, full }) => await tools.getIteration(iterationPublicId, full));
959
- server$1.tool("search-iterations", "Find Shortcut iterations.", {
992
+ server$1.addToolWithReadAccess("iterations-search", "Find Shortcut iterations.", {
960
993
  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
994
  id: z.number().optional().describe("Find only iterations with the specified public ID"),
962
995
  name: z.string().optional().describe("Find only iterations matching the specified name"),
@@ -972,15 +1005,15 @@ var IterationTools = class IterationTools extends BaseTools {
972
1005
  startDate: date(),
973
1006
  endDate: date()
974
1007
  }, async ({ nextPageToken,...params }) => await tools.searchIterations(params, nextPageToken));
975
- if (!isReadonly$1) server$1.tool("create-iteration", "Create a new Shortcut iteration", {
1008
+ server$1.addToolWithWriteAccess("iterations-create", "Create a new Shortcut iteration", {
976
1009
  name: z.string().describe("The name of the iteration"),
977
1010
  startDate: z.string().describe("The start date of the iteration in YYYY-MM-DD format"),
978
1011
  endDate: z.string().describe("The end date of the iteration in YYYY-MM-DD format"),
979
1012
  teamId: z.string().optional().describe("The ID of a team to assign the iteration to"),
980
1013
  description: z.string().optional().describe("A description of the iteration")
981
1014
  }, 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));
1015
+ 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));
1016
+ 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
1017
  return tools;
985
1018
  }
986
1019
  async getIterationStories(iterationPublicId, includeDescription) {
@@ -1057,13 +1090,13 @@ var IterationTools = class IterationTools extends BaseTools {
1057
1090
  //#endregion
1058
1091
  //#region src/tools/objectives.ts
1059
1092
  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", {
1093
+ static create(client$1, server$1) {
1094
+ const tools = new ObjectiveTools(client$1);
1095
+ server$1.addToolWithReadAccess("objectives-get-by-id", "Get a Shortcut objective by public ID", {
1063
1096
  objectivePublicId: z.number().positive().describe("The public ID of the objective to get"),
1064
1097
  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
1098
  }, async ({ objectivePublicId, full }) => await tools.getObjective(objectivePublicId, full));
1066
- server$1.tool("search-objectives", "Find Shortcut objectives.", {
1099
+ server$1.addToolWithReadAccess("objectives-search", "Find Shortcut objectives.", {
1067
1100
  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
1101
  id: z.number().optional().describe("Find objectives matching the specified id"),
1069
1102
  name: z.string().optional().describe("Find objectives matching the specified name"),
@@ -1105,13 +1138,13 @@ var ObjectiveTools = class ObjectiveTools extends BaseTools {
1105
1138
  //#endregion
1106
1139
  //#region src/tools/stories.ts
1107
1140
  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", {
1141
+ static create(client$1, server$1) {
1142
+ const tools = new StoryTools(client$1);
1143
+ server$1.addToolWithReadAccess("stories-get-by-id", "Get a Shortcut story by public ID", {
1111
1144
  storyPublicId: z.number().positive().describe("The public ID of the story to get"),
1112
1145
  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
1146
  }, async ({ storyPublicId, full }) => await tools.getStory(storyPublicId, full));
1114
- server$1.tool("search-stories", "Find Shortcut stories.", {
1147
+ server$1.addToolWithReadAccess("stories-search", "Find Shortcut stories.", {
1115
1148
  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
1149
  id: z.number().optional().describe("Find only stories with the specified public ID"),
1117
1150
  name: z.string().optional().describe("Find only stories matching the specified name"),
@@ -1162,103 +1195,101 @@ var StoryTools = class StoryTools extends BaseTools {
1162
1195
  completed: date(),
1163
1196
  due: date()
1164
1197
  }, 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.
1198
+ 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));
1199
+ server$1.addToolWithWriteAccess("stories-create", `Create a new Shortcut story.
1168
1200
  Name is required, and either a Team or Workflow must be specified:
1169
1201
  - If only Team is specified, we will use the default workflow for that team.
1170
1202
  - If Workflow is specified, it will be used regardless of Team.
1171
1203
  The story will be added to the default state for the workflow.
1172
1204
  `, {
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));
1205
+ name: z.string().min(1).max(512).describe("The name of the story. Required."),
1206
+ description: z.string().max(1e4).optional().describe("The description of the story"),
1207
+ type: z.enum([
1208
+ "feature",
1209
+ "bug",
1210
+ "chore"
1211
+ ]).default("feature").describe("The type of the story"),
1212
+ owner: z.string().optional().describe("The user id of the owner of the story"),
1213
+ epic: z.number().optional().describe("The epic id of the epic the story belongs to"),
1214
+ iteration: z.number().optional().describe("The iteration id of the iteration the story belongs to"),
1215
+ team: z.string().optional().describe("The team ID or mention name of the team the story belongs to. Required unless a workflow is specified."),
1216
+ workflow: z.number().optional().describe("The workflow ID to add the story to. Required unless a team is specified.")
1217
+ }, async ({ name: name$1, description, type, owner, epic, iteration, team, workflow }) => await tools.createStory({
1218
+ name: name$1,
1219
+ description,
1220
+ type,
1221
+ owner,
1222
+ epic,
1223
+ iteration,
1224
+ team,
1225
+ workflow
1226
+ }));
1227
+ 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.", {
1228
+ storyPublicId: z.number().positive().describe("The public ID of the story to update"),
1229
+ name: z.string().max(512).optional().describe("The name of the story"),
1230
+ description: z.string().max(1e4).optional().describe("The description of the story"),
1231
+ type: z.enum([
1232
+ "feature",
1233
+ "bug",
1234
+ "chore"
1235
+ ]).optional().describe("The type of the story"),
1236
+ epic: z.number().nullable().optional().describe("The epic id of the epic the story belongs to, or null to unset"),
1237
+ estimate: z.number().nullable().optional().describe("The point estimate of the story, or null to unset"),
1238
+ iteration: z.number().nullable().optional().describe("The iteration id of the iteration the story belongs to, or null to unset"),
1239
+ owner_ids: z.array(z.string()).optional().describe("Array of user UUIDs to assign as owners of the story"),
1240
+ workflow_state_id: z.number().optional().describe("The workflow state ID to move the story to"),
1241
+ labels: z.array(z.object({
1242
+ name: z.string().describe("The name of the label"),
1243
+ color: z.string().optional().describe("The color of the label"),
1244
+ description: z.string().optional().describe("The description of the label")
1245
+ })).optional().describe("Labels to assign to the story")
1246
+ }, async (params) => await tools.updateStory(params));
1247
+ server$1.addToolWithWriteAccess("stories-upload-file", "Upload a file and link it to a story.", {
1248
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1249
+ filePath: z.string().describe("The path to the file to upload")
1250
+ }, async ({ storyPublicId, filePath }) => await tools.uploadFileToStory(storyPublicId, filePath));
1251
+ 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));
1252
+ 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));
1253
+ server$1.addToolWithWriteAccess("stories-create-comment", "Create a comment on a story", {
1254
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1255
+ text: z.string().min(1).describe("The text of the comment")
1256
+ }, async (params) => await tools.createStoryComment(params));
1257
+ server$1.addToolWithWriteAccess("stories-add-task", "Add a task to a story", {
1258
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1259
+ taskDescription: z.string().min(1).describe("The description of the task"),
1260
+ taskOwnerIds: z.array(z.string()).optional().describe("Array of user IDs to assign as owners of the task")
1261
+ }, async (params) => await tools.addTaskToStory(params));
1262
+ server$1.addToolWithWriteAccess("stories-update-task", "Update a task in a story", {
1263
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1264
+ taskPublicId: z.number().positive().describe("The public ID of the task"),
1265
+ taskDescription: z.string().optional().describe("The description of the task"),
1266
+ taskOwnerIds: z.array(z.string()).optional().describe("Array of user IDs to assign as owners of the task"),
1267
+ isCompleted: z.boolean().optional().describe("Whether the task is completed or not")
1268
+ }, async (params) => await tools.updateTask(params));
1269
+ server$1.addToolWithWriteAccess("stories-add-relation", "Add a story relationship to a story", {
1270
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1271
+ relatedStoryPublicId: z.number().positive().describe("The public ID of the related story"),
1272
+ relationshipType: z.enum([
1273
+ "relates to",
1274
+ "blocks",
1275
+ "blocked by",
1276
+ "duplicates",
1277
+ "duplicated by"
1278
+ ]).optional().default("relates to").describe("The type of relationship")
1279
+ }, async (params) => await tools.addRelationToStory(params));
1280
+ server$1.addToolWithWriteAccess("stories-add-external-link", "Add an external link to a Shortcut story", {
1281
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1282
+ externalLink: z.string().url().max(2048).describe("The external link URL to add")
1283
+ }, async ({ storyPublicId, externalLink }) => await tools.addExternalLinkToStory(storyPublicId, externalLink));
1284
+ server$1.addToolWithWriteAccess("stories-remove-external-link", "Remove an external link from a Shortcut story", {
1285
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1286
+ externalLink: z.string().url().max(2048).describe("The external link URL to remove")
1287
+ }, async ({ storyPublicId, externalLink }) => await tools.removeExternalLinkFromStory(storyPublicId, externalLink));
1288
+ server$1.addToolWithWriteAccess("stories-set-external-links", "Replace all external links on a story with a new set of links", {
1289
+ storyPublicId: z.number().positive().describe("The public ID of the story"),
1290
+ externalLinks: z.array(z.string().url().max(2048)).describe("Array of external link URLs to set (replaces all existing links)")
1291
+ }, async ({ storyPublicId, externalLinks }) => await tools.setStoryExternalLinks(storyPublicId, externalLinks));
1292
+ 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
1293
  return tools;
1263
1294
  }
1264
1295
  async assignCurrentUserAsOwner(storyPublicId) {
@@ -1437,13 +1468,13 @@ The story will be added to the default state for the workflow.
1437
1468
  //#endregion
1438
1469
  //#region src/tools/teams.ts
1439
1470
  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", {
1471
+ static create(client$1, server$1) {
1472
+ const tools = new TeamTools(client$1);
1473
+ server$1.addToolWithReadAccess("teams-get-by-id", "Get a Shortcut team by public ID", {
1443
1474
  teamPublicId: z.string().describe("The public ID of the team to get"),
1444
1475
  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
1476
  }, async ({ teamPublicId, full }) => await tools.getTeam(teamPublicId, full));
1446
- server$1.tool("list-teams", "List all Shortcut teams", async () => await tools.getTeams());
1477
+ server$1.addToolWithReadAccess("teams-list", "List all Shortcut teams", async () => await tools.getTeams());
1447
1478
  return tools;
1448
1479
  }
1449
1480
  async getTeam(teamPublicId, full = false) {
@@ -1461,11 +1492,11 @@ var TeamTools = class TeamTools extends BaseTools {
1461
1492
  //#endregion
1462
1493
  //#region src/tools/user.ts
1463
1494
  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());
1495
+ static create(client$1, server$1) {
1496
+ const tools = new UserTools(client$1);
1497
+ server$1.addToolWithReadAccess("users-get-current", "Get the current user", async () => await tools.getCurrentUser());
1498
+ server$1.addToolWithReadAccess("users-get-current-teams", "Get a list of teams where the current user is a member", async () => await tools.getCurrentUserTeams());
1499
+ server$1.addToolWithReadAccess("users-list", "Get all users", async () => await tools.listMembers());
1469
1500
  return tools;
1470
1501
  }
1471
1502
  async getCurrentUser() {
@@ -1494,14 +1525,14 @@ var UserTools = class UserTools extends BaseTools {
1494
1525
  //#endregion
1495
1526
  //#region src/tools/workflows.ts
1496
1527
  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", {
1528
+ static create(client$1, server$1) {
1529
+ const tools = new WorkflowTools(client$1);
1530
+ 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));
1531
+ server$1.addToolWithReadAccess("workflows-get-by-id", "Get a Shortcut workflow by public ID", {
1501
1532
  workflowPublicId: z.number().positive().describe("The public ID of the workflow to get"),
1502
1533
  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
1534
  }, async ({ workflowPublicId, full }) => await tools.getWorkflow(workflowPublicId, full));
1504
- server$1.tool("list-workflows", "List all Shortcut workflows", async () => await tools.listWorkflows());
1535
+ server$1.addToolWithReadAccess("workflows-list", "List all Shortcut workflows", async () => await tools.listWorkflows());
1505
1536
  return tools;
1506
1537
  }
1507
1538
  async getDefaultWorkflow(teamPublicId) {
@@ -1535,27 +1566,29 @@ var WorkflowTools = class WorkflowTools extends BaseTools {
1535
1566
  //#region src/server.ts
1536
1567
  let apiToken = process.env.SHORTCUT_API_TOKEN;
1537
1568
  let isReadonly = process.env.SHORTCUT_READONLY === "true";
1569
+ let enabledTools = (process.env.SHORTCUT_TOOLS || "").split(",").map((tool) => tool.trim()).filter(Boolean);
1538
1570
  if (process.argv.length >= 3) process.argv.slice(2).map((arg) => arg.split("=")).forEach(([name$1, value]) => {
1539
1571
  if (name$1 === "SHORTCUT_API_TOKEN") apiToken = value;
1540
1572
  if (name$1 === "SHORTCUT_READONLY") isReadonly = value === "true";
1573
+ if (name$1 === "SHORTCUT_TOOLS") enabledTools = value.split(",").map((tool) => tool.trim()).filter(Boolean);
1541
1574
  });
1542
1575
  if (!apiToken) {
1543
1576
  console.error("SHORTCUT_API_TOKEN is required");
1544
1577
  process.exit(1);
1545
1578
  }
1546
- const server = new McpServer({
1547
- name,
1548
- version
1579
+ const server = new CustomMcpServer({
1580
+ readonly: isReadonly,
1581
+ tools: enabledTools
1549
1582
  });
1550
1583
  const client = new ShortcutClientWrapper(new ShortcutClient(apiToken));
1551
- UserTools.create(client, server, isReadonly);
1552
- StoryTools.create(client, server, isReadonly);
1553
- IterationTools.create(client, server, isReadonly);
1554
- EpicTools.create(client, server, isReadonly);
1555
- ObjectiveTools.create(client, server, isReadonly);
1556
- TeamTools.create(client, server, isReadonly);
1557
- WorkflowTools.create(client, server, isReadonly);
1558
- DocumentTools.create(client, server, isReadonly);
1584
+ UserTools.create(client, server);
1585
+ StoryTools.create(client, server);
1586
+ IterationTools.create(client, server);
1587
+ EpicTools.create(client, server);
1588
+ ObjectiveTools.create(client, server);
1589
+ TeamTools.create(client, server);
1590
+ WorkflowTools.create(client, server);
1591
+ DocumentTools.create(client, server);
1559
1592
  async function startServer() {
1560
1593
  try {
1561
1594
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "modelcontextprotocol"
13
13
  ],
14
14
  "license": "MIT",
15
- "version": "0.13.0",
15
+ "version": "0.15.1",
16
16
  "type": "module",
17
17
  "main": "dist/index.js",
18
18
  "bin": {