@shortcut/mcp 0.18.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,8 @@ 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
291
+ - **documents-update** - Update content of an existing document by its ID
195
292
  - **documents-list** - List all documents in Shortcut
196
293
  - **documents-search** - Search for documents
197
294
  - **documents-get-by-id** - Retrieve a specific document in markdown format by its ID
@@ -203,7 +300,8 @@ You can limit the tools available to the LLM by setting the `SHORTCUT_TOOLS` env
203
300
  - Tools can be limited by entity type by just adding the entity, eg `stories` or `epics`.
204
301
  - Individual tools can also be limitied by their full name, eg `stories-get-by-id` or `epics-search`.
205
302
 
206
- By default, all tools are enabled.
303
+ > [!NOTE]
304
+ > By default, all tools are enabled.
207
305
 
208
306
  Example:
209
307
 
@@ -241,6 +339,9 @@ The following values are accepted in addition to the full tool names listed abov
241
339
 
242
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.
243
341
 
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).
344
+
244
345
  Example:
245
346
 
246
347
  ```json
@@ -263,7 +364,8 @@ Example:
263
364
 
264
365
  ## Issues and Troubleshooting
265
366
 
266
- 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!
267
369
 
268
370
  If you run into problems using this MCP server, you have a couple of options:
269
371
 
@@ -274,6 +376,10 @@ You can also check the list of [common issues](#common-issues) below to see if t
274
376
 
275
377
  ### Common Issues and Solutions
276
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
+
277
383
  #### NPX command not working when using MISE for version management
278
384
 
279
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 { BaseTools, CustomMcpServer, DocumentTools, EpicTools, IterationTools, ObjectiveTools, ShortcutClientWrapper, StoryTools, TeamTools, UserTools, WorkflowTools } from "./workflows-Dko3ibgz.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({
@@ -84,4 +85,5 @@ async function startServer() {
84
85
  }
85
86
  startServer();
86
87
 
87
- //#endregion
88
+ //#endregion
89
+ export { };
@@ -1,4 +1,4 @@
1
- import { CustomMcpServer, DocumentTools, EpicTools, IterationTools, ObjectiveTools, ShortcutClientWrapper, StoryTools, TeamTools, UserTools, WorkflowTools } from "./workflows-Dko3ibgz.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";
@@ -52,15 +52,18 @@ const logger = pino({
52
52
  function loadConfig() {
53
53
  let isReadonly = process.env.SHORTCUT_READONLY !== "false";
54
54
  let enabledTools = parseToolsList(process.env.SHORTCUT_TOOLS || "");
55
+ let httpDebug = process.env.SHORTCUT_HTTP_DEBUG === "true";
55
56
  if (process.argv.length >= 3) process.argv.slice(2).map((arg) => arg.split("=")).forEach(([name, value]) => {
56
57
  if (name === "SHORTCUT_READONLY") isReadonly = value !== "false";
57
58
  if (name === "SHORTCUT_TOOLS") enabledTools = parseToolsList(value);
59
+ if (name === "SHORTCUT_HTTP_DEBUG") httpDebug = value === "true";
58
60
  });
59
61
  return {
60
62
  port: Number.parseInt(process.env.PORT || String(DEFAULT_PORT), 10),
61
63
  isReadonly,
62
64
  enabledTools,
63
- sessionTimeoutMs: SESSION_TIMEOUT_MS
65
+ sessionTimeoutMs: SESSION_TIMEOUT_MS,
66
+ httpDebug
64
67
  };
65
68
  }
66
69
  function parseToolsList(toolsStr) {
@@ -91,8 +94,7 @@ var SessionManager = class {
91
94
  logger.info({ sessionId }, "Session initialized");
92
95
  }
93
96
  remove(sessionId) {
94
- const session = this.sessions.get(sessionId);
95
- if (session) {
97
+ if (this.sessions.get(sessionId)) {
96
98
  this.sessions.delete(sessionId);
97
99
  logger.info({ sessionId }, "Session removed");
98
100
  }
@@ -105,10 +107,7 @@ var SessionManager = class {
105
107
  cleanupStaleSessions() {
106
108
  const now = Date.now();
107
109
  const staleSessionIds = [];
108
- for (const [sessionId, session] of this.sessions.entries()) {
109
- const timeSinceLastAccess = now - session.lastAccessedAt.getTime();
110
- if (timeSinceLastAccess > this.timeoutMs) staleSessionIds.push(sessionId);
111
- }
110
+ for (const [sessionId, session] of this.sessions.entries()) if (now - session.lastAccessedAt.getTime() > this.timeoutMs) staleSessionIds.push(sessionId);
112
111
  if (staleSessionIds.length > 0) {
113
112
  logger.info({ count: staleSessionIds.length }, "Cleaning up stale sessions");
114
113
  for (const sessionId of staleSessionIds) {
@@ -156,8 +155,7 @@ function extractApiToken(req) {
156
155
  */
157
156
  async function validateApiToken(token) {
158
157
  try {
159
- const client = new ShortcutClient(token);
160
- await client.getCurrentMemberInfo();
158
+ await new ShortcutClient(token).getCurrentMemberInfo();
161
159
  return true;
162
160
  } catch (error) {
163
161
  logger.debug({ error: error instanceof Error ? error.message : error }, "API token validation failed");
@@ -242,8 +240,7 @@ async function createTransport(apiToken, config, sessionManager) {
242
240
  if (sid && sessionManager.has(sid)) sessionManager.remove(sid);
243
241
  }
244
242
  };
245
- const server = createServerInstance(apiToken, config);
246
- await server.connect(transport);
243
+ await createServerInstance(apiToken, config).connect(transport);
247
244
  return transport;
248
245
  }
249
246
  async function handleMcpPost(req, res, sessionManager, config) {
@@ -266,8 +263,7 @@ async function handleMcpPost(req, res, sessionManager, config) {
266
263
  sendUnauthorizedError(res, "API token does not match the session");
267
264
  return;
268
265
  }
269
- const session = sessionManager.get(sessionId);
270
- await session.transport.handleRequest(req, res, req.body);
266
+ await sessionManager.get(sessionId).transport.handleRequest(req, res, req.body);
271
267
  return;
272
268
  }
273
269
  if (isInitializeRequest(req.body)) {
@@ -276,15 +272,13 @@ async function handleMcpPost(req, res, sessionManager, config) {
276
272
  return;
277
273
  }
278
274
  reqLogger.info("Validating API token");
279
- const isValid = await validateApiToken(apiToken);
280
- if (!isValid) {
275
+ if (!await validateApiToken(apiToken)) {
281
276
  reqLogger.warn("API token validation failed");
282
277
  sendInvalidTokenError(res, requestId);
283
278
  return;
284
279
  }
285
280
  reqLogger.info("API token validated, creating session");
286
- const transport = await createTransport(apiToken, config, sessionManager);
287
- await transport.handleRequest(req, res, req.body);
281
+ await (await createTransport(apiToken, config, sessionManager)).handleRequest(req, res, req.body);
288
282
  return;
289
283
  }
290
284
  if (sessionId && !sessionManager.has(sessionId)) {
@@ -321,8 +315,7 @@ async function handleMcpGet(req, res, sessionManager) {
321
315
  if (lastEventId) reqLogger.info({ lastEventId }, "Client reconnecting with Last-Event-ID");
322
316
  else reqLogger.info("Establishing SSE stream");
323
317
  try {
324
- const session = sessionManager.get(sessionId);
325
- await session.transport.handleRequest(req, res);
318
+ await sessionManager.get(sessionId).transport.handleRequest(req, res);
326
319
  } catch (error) {
327
320
  reqLogger.error({ error }, "Error handling MCP GET request");
328
321
  if (!res.headersSent) res.status(500).send("Internal server error");
@@ -350,8 +343,7 @@ async function handleMcpDelete(req, res, sessionManager) {
350
343
  }
351
344
  reqLogger.info("Terminating session");
352
345
  try {
353
- const session = sessionManager.get(sessionId);
354
- await session.transport.handleRequest(req, res);
346
+ await sessionManager.get(sessionId).transport.handleRequest(req, res);
355
347
  } catch (error) {
356
348
  reqLogger.error({ error }, "Error handling session termination");
357
349
  if (!res.headersSent) res.status(500).send("Error processing session termination");
@@ -382,13 +374,43 @@ function loggingMiddleware(req, _res, next) {
382
374
  }, "Incoming request");
383
375
  next();
384
376
  }
377
+ function httpDebugRequestMiddleware(req, _res, next) {
378
+ const headers = { ...req.headers };
379
+ delete headers[HEADERS.AUTHORIZATION];
380
+ delete headers[HEADERS.X_SHORTCUT_API_TOKEN];
381
+ delete headers.cookie;
382
+ logger.info(JSON.stringify({
383
+ event: "http_request",
384
+ method: req.method,
385
+ path: req.path,
386
+ url: req.originalUrl,
387
+ query: req.query,
388
+ headers,
389
+ body: req.body
390
+ }));
391
+ next();
392
+ }
385
393
  async function startServer() {
386
394
  const config = loadConfig();
387
395
  const sessionManager = new SessionManager(config.sessionTimeoutMs);
388
396
  const app = express();
389
397
  app.use(express.json());
398
+ if (config.httpDebug) app.use(httpDebugRequestMiddleware);
390
399
  app.use(corsMiddleware);
391
400
  app.use(loggingMiddleware);
401
+ app.use((req, res, next) => {
402
+ const start = Date.now();
403
+ res.on("finish", () => {
404
+ if (res.statusCode >= 400) logger.info(JSON.stringify({
405
+ event: "http_request_failed",
406
+ method: req.method,
407
+ path: req.path,
408
+ status: res.statusCode,
409
+ ms: Date.now() - start
410
+ }));
411
+ });
412
+ next();
413
+ });
392
414
  app.get("/health", (_req, res) => {
393
415
  res.json({
394
416
  status: "ok",
@@ -427,4 +449,5 @@ startServer().catch((error) => {
427
449
  process.exit(1);
428
450
  });
429
451
 
430
- //#endregion
452
+ //#endregion
453
+ export { };