@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 +145 -39
- package/dist/{index.js → index.mjs} +10 -8
- package/dist/{server-http.js → server-http.mjs} +46 -23
- package/dist/{workflows-Dko3ibgz.js → workflows-GwezThPA.mjs} +183 -181
- package/package.json +28 -28
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
|
|
22
|
-
2. Click `Add
|
|
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/
|
|
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
|
-
|
|
129
|
+
Add the MCP server from the command line:
|
|
74
130
|
|
|
75
131
|
```shell
|
|
76
|
-
|
|
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
|
|
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
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
13
|
-
const tools = new LabelTools(client
|
|
14
|
-
server
|
|
15
|
-
server
|
|
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("
|
|
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 {
|
|
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
|
-
|
|
95
|
-
if (session) {
|
|
97
|
+
if (this.sessions.get(sessionId)) {
|
|
96
98
|
this.sessions.delete(sessionId);
|
|
97
99
|
logger.info({ sessionId }, "Session removed");
|
|
98
100
|
}
|
|
@@ -105,10 +107,7 @@ var SessionManager = class {
|
|
|
105
107
|
cleanupStaleSessions() {
|
|
106
108
|
const now = Date.now();
|
|
107
109
|
const staleSessionIds = [];
|
|
108
|
-
for (const [sessionId, session] of this.sessions.entries())
|
|
109
|
-
const timeSinceLastAccess = now - session.lastAccessedAt.getTime();
|
|
110
|
-
if (timeSinceLastAccess > this.timeoutMs) staleSessionIds.push(sessionId);
|
|
111
|
-
}
|
|
110
|
+
for (const [sessionId, session] of this.sessions.entries()) if (now - session.lastAccessedAt.getTime() > this.timeoutMs) staleSessionIds.push(sessionId);
|
|
112
111
|
if (staleSessionIds.length > 0) {
|
|
113
112
|
logger.info({ count: staleSessionIds.length }, "Cleaning up stale sessions");
|
|
114
113
|
for (const sessionId of staleSessionIds) {
|
|
@@ -156,8 +155,7 @@ function extractApiToken(req) {
|
|
|
156
155
|
*/
|
|
157
156
|
async function validateApiToken(token) {
|
|
158
157
|
try {
|
|
159
|
-
|
|
160
|
-
await client.getCurrentMemberInfo();
|
|
158
|
+
await new ShortcutClient(token).getCurrentMemberInfo();
|
|
161
159
|
return true;
|
|
162
160
|
} catch (error) {
|
|
163
161
|
logger.debug({ error: error instanceof Error ? error.message : error }, "API token validation failed");
|
|
@@ -242,8 +240,7 @@ async function createTransport(apiToken, config, sessionManager) {
|
|
|
242
240
|
if (sid && sessionManager.has(sid)) sessionManager.remove(sid);
|
|
243
241
|
}
|
|
244
242
|
};
|
|
245
|
-
|
|
246
|
-
await server.connect(transport);
|
|
243
|
+
await createServerInstance(apiToken, config).connect(transport);
|
|
247
244
|
return transport;
|
|
248
245
|
}
|
|
249
246
|
async function handleMcpPost(req, res, sessionManager, config) {
|
|
@@ -266,8 +263,7 @@ async function handleMcpPost(req, res, sessionManager, config) {
|
|
|
266
263
|
sendUnauthorizedError(res, "API token does not match the session");
|
|
267
264
|
return;
|
|
268
265
|
}
|
|
269
|
-
|
|
270
|
-
await session.transport.handleRequest(req, res, req.body);
|
|
266
|
+
await sessionManager.get(sessionId).transport.handleRequest(req, res, req.body);
|
|
271
267
|
return;
|
|
272
268
|
}
|
|
273
269
|
if (isInitializeRequest(req.body)) {
|
|
@@ -276,15 +272,13 @@ async function handleMcpPost(req, res, sessionManager, config) {
|
|
|
276
272
|
return;
|
|
277
273
|
}
|
|
278
274
|
reqLogger.info("Validating API token");
|
|
279
|
-
|
|
280
|
-
if (!isValid) {
|
|
275
|
+
if (!await validateApiToken(apiToken)) {
|
|
281
276
|
reqLogger.warn("API token validation failed");
|
|
282
277
|
sendInvalidTokenError(res, requestId);
|
|
283
278
|
return;
|
|
284
279
|
}
|
|
285
280
|
reqLogger.info("API token validated, creating session");
|
|
286
|
-
|
|
287
|
-
await transport.handleRequest(req, res, req.body);
|
|
281
|
+
await (await createTransport(apiToken, config, sessionManager)).handleRequest(req, res, req.body);
|
|
288
282
|
return;
|
|
289
283
|
}
|
|
290
284
|
if (sessionId && !sessionManager.has(sessionId)) {
|
|
@@ -321,8 +315,7 @@ async function handleMcpGet(req, res, sessionManager) {
|
|
|
321
315
|
if (lastEventId) reqLogger.info({ lastEventId }, "Client reconnecting with Last-Event-ID");
|
|
322
316
|
else reqLogger.info("Establishing SSE stream");
|
|
323
317
|
try {
|
|
324
|
-
|
|
325
|
-
await session.transport.handleRequest(req, res);
|
|
318
|
+
await sessionManager.get(sessionId).transport.handleRequest(req, res);
|
|
326
319
|
} catch (error) {
|
|
327
320
|
reqLogger.error({ error }, "Error handling MCP GET request");
|
|
328
321
|
if (!res.headersSent) res.status(500).send("Internal server error");
|
|
@@ -350,8 +343,7 @@ async function handleMcpDelete(req, res, sessionManager) {
|
|
|
350
343
|
}
|
|
351
344
|
reqLogger.info("Terminating session");
|
|
352
345
|
try {
|
|
353
|
-
|
|
354
|
-
await session.transport.handleRequest(req, res);
|
|
346
|
+
await sessionManager.get(sessionId).transport.handleRequest(req, res);
|
|
355
347
|
} catch (error) {
|
|
356
348
|
reqLogger.error({ error }, "Error handling session termination");
|
|
357
349
|
if (!res.headersSent) res.status(500).send("Error processing session termination");
|
|
@@ -382,13 +374,43 @@ function loggingMiddleware(req, _res, next) {
|
|
|
382
374
|
}, "Incoming request");
|
|
383
375
|
next();
|
|
384
376
|
}
|
|
377
|
+
function httpDebugRequestMiddleware(req, _res, next) {
|
|
378
|
+
const headers = { ...req.headers };
|
|
379
|
+
delete headers[HEADERS.AUTHORIZATION];
|
|
380
|
+
delete headers[HEADERS.X_SHORTCUT_API_TOKEN];
|
|
381
|
+
delete headers.cookie;
|
|
382
|
+
logger.info(JSON.stringify({
|
|
383
|
+
event: "http_request",
|
|
384
|
+
method: req.method,
|
|
385
|
+
path: req.path,
|
|
386
|
+
url: req.originalUrl,
|
|
387
|
+
query: req.query,
|
|
388
|
+
headers,
|
|
389
|
+
body: req.body
|
|
390
|
+
}));
|
|
391
|
+
next();
|
|
392
|
+
}
|
|
385
393
|
async function startServer() {
|
|
386
394
|
const config = loadConfig();
|
|
387
395
|
const sessionManager = new SessionManager(config.sessionTimeoutMs);
|
|
388
396
|
const app = express();
|
|
389
397
|
app.use(express.json());
|
|
398
|
+
if (config.httpDebug) app.use(httpDebugRequestMiddleware);
|
|
390
399
|
app.use(corsMiddleware);
|
|
391
400
|
app.use(loggingMiddleware);
|
|
401
|
+
app.use((req, res, next) => {
|
|
402
|
+
const start = Date.now();
|
|
403
|
+
res.on("finish", () => {
|
|
404
|
+
if (res.statusCode >= 400) logger.info(JSON.stringify({
|
|
405
|
+
event: "http_request_failed",
|
|
406
|
+
method: req.method,
|
|
407
|
+
path: req.path,
|
|
408
|
+
status: res.statusCode,
|
|
409
|
+
ms: Date.now() - start
|
|
410
|
+
}));
|
|
411
|
+
});
|
|
412
|
+
next();
|
|
413
|
+
});
|
|
392
414
|
app.get("/health", (_req, res) => {
|
|
393
415
|
res.json({
|
|
394
416
|
status: "ok",
|
|
@@ -427,4 +449,5 @@ startServer().catch((error) => {
|
|
|
427
449
|
process.exit(1);
|
|
428
450
|
});
|
|
429
451
|
|
|
430
|
-
//#endregion
|
|
452
|
+
//#endregion
|
|
453
|
+
export { };
|