@upstash/mcp-server 0.1.0 → 0.2.0-canary-4
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 +107 -70
- package/dist/index.d.ts +3 -0
- package/dist/index.js +975 -316
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,90 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { createServer } from "http";
|
|
5
9
|
|
|
6
|
-
// src/
|
|
7
|
-
import
|
|
8
|
-
import os from "os";
|
|
9
|
-
import fs from "fs";
|
|
10
|
-
import chalk from "chalk";
|
|
10
|
+
// src/server.ts
|
|
11
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
12
|
|
|
12
13
|
// src/log.ts
|
|
13
14
|
function log(...args) {
|
|
14
15
|
const msg = `[DEBUG ${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).join(" ")}
|
|
15
16
|
`;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// src/config.ts
|
|
20
|
-
var config = {
|
|
21
|
-
apiKey: "",
|
|
22
|
-
email: ""
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// src/init.ts
|
|
26
|
-
var claudeConfigPath = path.join(
|
|
27
|
-
os.homedir(),
|
|
28
|
-
"Library",
|
|
29
|
-
"Application Support",
|
|
30
|
-
"Claude",
|
|
31
|
-
"claude_desktop_config.json"
|
|
32
|
-
);
|
|
33
|
-
var UPSTASH_MCP_KEY = "upstash";
|
|
34
|
-
async function init({ executablePath }) {
|
|
35
|
-
const isLocal = executablePath.includes("dist/index.js");
|
|
36
|
-
const upstashConfig = isLocal ? {
|
|
37
|
-
command: "node",
|
|
38
|
-
args: [executablePath, "run", config.email, config.apiKey]
|
|
39
|
-
} : {
|
|
40
|
-
command: "npx",
|
|
41
|
-
args: ["-y", "@upstash/mcp-server", "run", config.email, config.apiKey]
|
|
42
|
-
};
|
|
43
|
-
const configDir = path.dirname(claudeConfigPath);
|
|
44
|
-
if (!fs.existsSync(configDir)) {
|
|
45
|
-
log(chalk.blue("Creating Claude config directory..."));
|
|
46
|
-
fs.mkdirSync(configDir, { recursive: true });
|
|
47
|
-
}
|
|
48
|
-
const existingConfig = fs.existsSync(claudeConfigPath) ? JSON.parse(fs.readFileSync(claudeConfigPath, "utf8")) : { mcpServers: {} };
|
|
49
|
-
if (UPSTASH_MCP_KEY in ((existingConfig == null ? void 0 : existingConfig.mcpServers) || {})) {
|
|
50
|
-
log(chalk.yellow("Upstash entry already exists. Overriding it."));
|
|
51
|
-
}
|
|
52
|
-
if (isLocal) {
|
|
53
|
-
log(
|
|
54
|
-
chalk.yellow(
|
|
55
|
-
"Local executable detected. Using 'node' and absolute path instead of 'npx' for development."
|
|
56
|
-
)
|
|
57
|
-
);
|
|
17
|
+
for (const [, logs] of logsStore.entries()) {
|
|
18
|
+
logs.push(msg);
|
|
58
19
|
}
|
|
59
|
-
|
|
60
|
-
...existingConfig,
|
|
61
|
-
mcpServers: {
|
|
62
|
-
...existingConfig.mcpServers,
|
|
63
|
-
[UPSTASH_MCP_KEY]: upstashConfig
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
fs.writeFileSync(claudeConfigPath, JSON.stringify(newConfig, null, 2));
|
|
67
|
-
log(
|
|
68
|
-
chalk.blue(
|
|
69
|
-
"\n" + JSON.stringify(
|
|
70
|
-
{
|
|
71
|
-
[UPSTASH_MCP_KEY]: upstashConfig
|
|
72
|
-
},
|
|
73
|
-
null,
|
|
74
|
-
2
|
|
75
|
-
).replaceAll(config.apiKey, "********")
|
|
76
|
-
)
|
|
77
|
-
);
|
|
78
|
-
log(chalk.green(`Config written to: "${claudeConfigPath.replace(os.homedir(), "~")}"`));
|
|
20
|
+
process.stderr.write(msg);
|
|
79
21
|
}
|
|
80
|
-
|
|
81
|
-
// src/server.ts
|
|
82
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
83
|
-
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
+
var logsStore = /* @__PURE__ */ new Map();
|
|
84
23
|
|
|
85
24
|
// src/tools/utils.ts
|
|
86
25
|
import { z } from "zod";
|
|
87
26
|
var utilTools = {
|
|
88
|
-
|
|
27
|
+
util_timestamps_to_date: tool({
|
|
89
28
|
description: `Use this tool to convert a timestamp to a human-readable date`,
|
|
90
29
|
inputSchema: z.object({
|
|
91
30
|
timestamps: z.array(z.number()).describe("Array of timestamps to convert")
|
|
@@ -93,29 +32,54 @@ var utilTools = {
|
|
|
93
32
|
handler: async ({ timestamps }) => {
|
|
94
33
|
return timestamps.map((timestamp) => new Date(timestamp).toUTCString());
|
|
95
34
|
}
|
|
35
|
+
}),
|
|
36
|
+
util_dates_to_timestamps: tool({
|
|
37
|
+
description: `Use this tool to convert an array of ISO 8601 dates to an array of timestamps`,
|
|
38
|
+
inputSchema: z.object({
|
|
39
|
+
dates: z.array(z.string()).describe("Array of dates to convert")
|
|
40
|
+
}),
|
|
41
|
+
handler: async ({ dates }) => {
|
|
42
|
+
return dates.map((date) => new Date(date).getTime()).join(",");
|
|
43
|
+
}
|
|
96
44
|
})
|
|
97
45
|
};
|
|
98
46
|
|
|
99
47
|
// src/tools/redis/backup.ts
|
|
100
48
|
import { z as z2 } from "zod";
|
|
101
49
|
|
|
50
|
+
// src/config.ts
|
|
51
|
+
var config = {
|
|
52
|
+
apiKey: "",
|
|
53
|
+
email: ""
|
|
54
|
+
};
|
|
55
|
+
|
|
102
56
|
// src/middlewares.ts
|
|
103
|
-
var
|
|
104
|
-
if (obj
|
|
105
|
-
|
|
57
|
+
var formatTimestamps = (obj) => {
|
|
58
|
+
if (!obj || typeof obj !== "object") {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(obj)) {
|
|
62
|
+
for (const item of obj) {
|
|
63
|
+
formatTimestamps(item);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
68
|
+
const shouldFormat = typeof value === "number" && (key === "creationTime" || key === "creation_time" || key === "time" || key.toLowerCase().includes("createdat"));
|
|
69
|
+
if (shouldFormat && typeof value === "number" && value > 0) {
|
|
70
|
+
const timestamp = value > 1e12 ? value : value * 1e3;
|
|
71
|
+
const formatted = new Date(timestamp).toLocaleString("en-US", { timeZoneName: "short" });
|
|
72
|
+
obj[key] = `${formatted} (${value})`;
|
|
73
|
+
} else if (value && typeof value === "object") {
|
|
74
|
+
formatTimestamps(value);
|
|
75
|
+
}
|
|
106
76
|
}
|
|
107
77
|
};
|
|
108
78
|
var middlewares = [
|
|
109
|
-
// Middleware to format
|
|
79
|
+
// Middleware to format timestamp fields to human readable format
|
|
110
80
|
async (req, next) => {
|
|
111
81
|
const res = await next();
|
|
112
|
-
|
|
113
|
-
for (const element of res) {
|
|
114
|
-
formatCreationTime(element);
|
|
115
|
-
}
|
|
116
|
-
} else {
|
|
117
|
-
formatCreationTime(res);
|
|
118
|
-
}
|
|
82
|
+
formatTimestamps(res);
|
|
119
83
|
return res;
|
|
120
84
|
}
|
|
121
85
|
];
|
|
@@ -133,18 +97,22 @@ import fetch from "node-fetch";
|
|
|
133
97
|
var HttpClient = class {
|
|
134
98
|
constructor(config2) {
|
|
135
99
|
this.baseUrl = config2.baseUrl.replace(/\/$/, "");
|
|
100
|
+
this.qstashToken = config2.qstashToken;
|
|
101
|
+
}
|
|
102
|
+
async get(path, query) {
|
|
103
|
+
return this.requestWithMiddleware({ method: "GET", path, query });
|
|
136
104
|
}
|
|
137
|
-
async
|
|
138
|
-
return this.requestWithMiddleware({ method: "
|
|
105
|
+
async post(path, body, headers) {
|
|
106
|
+
return this.requestWithMiddleware({ method: "POST", path, body, headers });
|
|
139
107
|
}
|
|
140
|
-
async
|
|
141
|
-
return this.requestWithMiddleware({ method: "
|
|
108
|
+
async put(path, body, headers) {
|
|
109
|
+
return this.requestWithMiddleware({ method: "PUT", path, body, headers });
|
|
142
110
|
}
|
|
143
|
-
async patch(
|
|
144
|
-
return this.requestWithMiddleware({ method: "PATCH", path
|
|
111
|
+
async patch(path, body) {
|
|
112
|
+
return this.requestWithMiddleware({ method: "PATCH", path, body });
|
|
145
113
|
}
|
|
146
|
-
async delete(
|
|
147
|
-
return this.requestWithMiddleware({ method: "DELETE", path
|
|
114
|
+
async delete(path, body) {
|
|
115
|
+
return this.requestWithMiddleware({ method: "DELETE", path, body });
|
|
148
116
|
}
|
|
149
117
|
async requestWithMiddleware(req) {
|
|
150
118
|
const res = await applyMiddlewares(req, async (req2) => {
|
|
@@ -158,70 +126,131 @@ var HttpClient = class {
|
|
|
158
126
|
} else if (typeof req.path === "string") {
|
|
159
127
|
req.path = [req.path];
|
|
160
128
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
129
|
+
let url = [this.baseUrl, ...req.path].join("/");
|
|
130
|
+
if (req.query) {
|
|
131
|
+
const queryPairs = [];
|
|
132
|
+
for (const [key, value] of Object.entries(req.query)) {
|
|
133
|
+
if (value !== void 0 && value !== null && typeof value !== "object") {
|
|
134
|
+
queryPairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (queryPairs.length > 0) {
|
|
138
|
+
url += `?${queryPairs.join("&")}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const qstashToken = req.qstashToken || this.qstashToken;
|
|
142
|
+
let authHeader;
|
|
143
|
+
if (qstashToken) {
|
|
144
|
+
authHeader = `Bearer ${qstashToken}`;
|
|
145
|
+
} else {
|
|
146
|
+
const token = [config.email, config.apiKey].join(":");
|
|
147
|
+
authHeader = `Basic ${Buffer.from(token).toString("base64")}`;
|
|
148
|
+
}
|
|
149
|
+
const init = {
|
|
164
150
|
method: req.method,
|
|
165
151
|
headers: {
|
|
166
152
|
"Content-Type": "application/json",
|
|
167
|
-
Authorization:
|
|
153
|
+
Authorization: authHeader,
|
|
154
|
+
...req.headers
|
|
168
155
|
}
|
|
169
156
|
};
|
|
170
|
-
if (req.method !== "GET") {
|
|
171
|
-
|
|
157
|
+
if (req.method !== "GET" && req.body !== void 0) {
|
|
158
|
+
init.body = JSON.stringify(req.body);
|
|
172
159
|
}
|
|
173
|
-
log("
|
|
160
|
+
log("-> sending request", {
|
|
174
161
|
url,
|
|
175
|
-
...
|
|
176
|
-
headers: {
|
|
162
|
+
...init,
|
|
163
|
+
headers: {
|
|
164
|
+
...init.headers
|
|
165
|
+
// , Authorization: "***"
|
|
166
|
+
}
|
|
177
167
|
});
|
|
178
|
-
const res = await fetch(url,
|
|
168
|
+
const res = await fetch(url, init);
|
|
179
169
|
if (!res.ok) {
|
|
180
170
|
throw new Error(`Request failed (${res.status} ${res.statusText}): ${await res.text()}`);
|
|
181
171
|
}
|
|
182
|
-
|
|
172
|
+
const text = await res.text();
|
|
173
|
+
if (!text) {
|
|
174
|
+
return {};
|
|
175
|
+
}
|
|
176
|
+
const result = safeParseJson(text);
|
|
177
|
+
if (result) {
|
|
178
|
+
log("<- received response", json(result));
|
|
179
|
+
} else {
|
|
180
|
+
log("<- received text response", text);
|
|
181
|
+
}
|
|
182
|
+
return result || text;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var safeParseJson = (text) => {
|
|
186
|
+
try {
|
|
187
|
+
return JSON.parse(text);
|
|
188
|
+
} catch {
|
|
189
|
+
return;
|
|
183
190
|
}
|
|
184
191
|
};
|
|
185
192
|
var http = new HttpClient({ baseUrl: "https://api.upstash.com" });
|
|
193
|
+
function createQStashClient({ url, token }) {
|
|
194
|
+
return new HttpClient({
|
|
195
|
+
baseUrl: url ?? "https://qstash.upstash.io",
|
|
196
|
+
qstashToken: token
|
|
197
|
+
});
|
|
198
|
+
}
|
|
186
199
|
|
|
187
200
|
// src/tools/redis/backup.ts
|
|
188
201
|
var redisBackupTools = {
|
|
189
|
-
|
|
190
|
-
description: `Create
|
|
191
|
-
NOTE: Ask user to choose a name for the backup`,
|
|
192
|
-
inputSchema: z2.object({
|
|
193
|
-
database_id: z2.string().describe("The ID of the database to create a backup for."),
|
|
194
|
-
backup_name: z2.string().describe("A name for the backup.")
|
|
195
|
-
}),
|
|
196
|
-
handler: async ({ database_id, backup_name }) => {
|
|
197
|
-
await http.post(["v2/redis/create-backup", database_id], {
|
|
198
|
-
name: backup_name
|
|
199
|
-
});
|
|
200
|
-
return "OK";
|
|
201
|
-
}
|
|
202
|
-
}),
|
|
203
|
-
redis_database_delete_backup: tool({
|
|
204
|
-
description: `Delete a backup of a specific Upstash redis database.`,
|
|
205
|
-
inputSchema: z2.object({
|
|
206
|
-
database_id: z2.string().describe("The ID of the database to delete a backup from."),
|
|
207
|
-
backup_id: z2.string().describe("The ID of the backup to delete.")
|
|
208
|
-
}),
|
|
209
|
-
handler: async ({ database_id, backup_id }) => {
|
|
210
|
-
await http.delete(["v2/redis/delete-backup", database_id, backup_id]);
|
|
211
|
-
return "OK";
|
|
212
|
-
}
|
|
213
|
-
}),
|
|
214
|
-
redis_database_restore_backup: tool({
|
|
215
|
-
description: `Restore a backup of a specific Upstash redis database. A backup can only be restored to the same database it was created from.`,
|
|
202
|
+
redis_database_manage_backup: tool({
|
|
203
|
+
description: `Create, delete, or restore backups for a specific Upstash redis database. This tool handles all backup operations in one unified interface.`,
|
|
216
204
|
inputSchema: z2.object({
|
|
217
|
-
database_id: z2.string().describe("The ID of the database to
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
205
|
+
database_id: z2.string().describe("The ID of the database to manage backups for."),
|
|
206
|
+
operation: z2.union([z2.literal("create"), z2.literal("delete"), z2.literal("restore")]).describe("The backup operation to perform: create, delete, or restore."),
|
|
207
|
+
backup_name: z2.string().optional().describe("Name for the backup (required for create operation)."),
|
|
208
|
+
backup_id: z2.string().optional().describe("ID of the backup (required for delete and restore operations).")
|
|
209
|
+
}).refine(
|
|
210
|
+
(data) => {
|
|
211
|
+
if (data.operation === "create" && !data.backup_name) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if ((data.operation === "delete" || data.operation === "restore") && !data.backup_id) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
message: "backup_name is required for create operation, backup_id is required for delete and restore operations"
|
|
221
|
+
}
|
|
222
|
+
),
|
|
223
|
+
handler: async ({ database_id, operation, backup_name, backup_id }) => {
|
|
224
|
+
switch (operation) {
|
|
225
|
+
case "create": {
|
|
226
|
+
if (!backup_name) {
|
|
227
|
+
throw new Error("backup_name is required for create operation");
|
|
228
|
+
}
|
|
229
|
+
await http.post(["v2/redis/create-backup", database_id], {
|
|
230
|
+
name: backup_name
|
|
231
|
+
});
|
|
232
|
+
return `Backup "${backup_name}" created successfully for database ${database_id}.`;
|
|
233
|
+
}
|
|
234
|
+
case "delete": {
|
|
235
|
+
if (!backup_id) {
|
|
236
|
+
throw new Error("backup_id is required for delete operation");
|
|
237
|
+
}
|
|
238
|
+
await http.delete(["v2/redis/delete-backup", database_id, backup_id]);
|
|
239
|
+
return `Backup ${backup_id} deleted successfully from database ${database_id}.`;
|
|
240
|
+
}
|
|
241
|
+
case "restore": {
|
|
242
|
+
if (!backup_id) {
|
|
243
|
+
throw new Error("backup_id is required for restore operation");
|
|
244
|
+
}
|
|
245
|
+
await http.post(["v2/redis/restore-backup", database_id], {
|
|
246
|
+
backup_id
|
|
247
|
+
});
|
|
248
|
+
return `Backup ${backup_id} restored successfully to database ${database_id}.`;
|
|
249
|
+
}
|
|
250
|
+
default: {
|
|
251
|
+
throw new Error(`Invalid operation: ${operation}. Use create, delete, or restore.`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
225
254
|
}
|
|
226
255
|
}),
|
|
227
256
|
redis_database_list_backups: tool({
|
|
@@ -256,59 +285,74 @@ NOTE: Ask user to choose a name for the backup`,
|
|
|
256
285
|
import { z as z3 } from "zod";
|
|
257
286
|
import fetch2 from "node-fetch";
|
|
258
287
|
var redisCommandTools = {
|
|
259
|
-
|
|
260
|
-
description: `Run
|
|
288
|
+
redis_database_run_redis_commands: tool({
|
|
289
|
+
description: `Run one or more Redis commands on a specific Upstash redis database. Either provide database_id OR both database_rest_url and database_rest_token.
|
|
261
290
|
NOTE: For discovery, use SCAN over KEYS. Use TYPE to get the type of a key.
|
|
262
|
-
NOTE: SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
|
|
291
|
+
NOTE: SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
|
|
292
|
+
NOTE: Multiple commands will be executed as a pipeline for better performance.`,
|
|
263
293
|
inputSchema: z3.object({
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
294
|
+
database_id: z3.string().optional().describe("The ID of the database to run commands on."),
|
|
295
|
+
database_rest_url: z3.string().optional().describe("The REST URL of the database. Example: https://***.upstash.io"),
|
|
296
|
+
database_rest_token: z3.string().optional().describe("The REST token of the database."),
|
|
297
|
+
commands: z3.array(z3.array(z3.string())).describe(
|
|
298
|
+
"The Redis commands to run. For single command: [['SET', 'foo', 'bar']], for multiple: [['SET', 'foo', 'bar'], ['GET', 'foo']]"
|
|
299
|
+
)
|
|
300
|
+
}).refine(
|
|
301
|
+
(data) => {
|
|
302
|
+
return data.database_id || data.database_rest_url && data.database_rest_token;
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
message: "Either provide database_id OR both database_rest_url and database_rest_token"
|
|
306
|
+
}
|
|
307
|
+
),
|
|
308
|
+
handler: async ({ database_id, database_rest_url, database_rest_token, commands }) => {
|
|
309
|
+
if (database_id && (database_rest_url || database_rest_token)) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
"Either provide database_id OR both database_rest_url and database_rest_token"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
let restUrl = database_rest_url;
|
|
315
|
+
let restToken = database_rest_token;
|
|
316
|
+
if (database_id && (!database_rest_url || !database_rest_token)) {
|
|
317
|
+
log("Fetching database details for database_id:", database_id);
|
|
318
|
+
const db = await http.get(["v2/redis/database", database_id]);
|
|
319
|
+
restUrl = db.endpoint;
|
|
320
|
+
restToken = db.rest_token;
|
|
321
|
+
}
|
|
322
|
+
if (!restUrl || !restToken) {
|
|
323
|
+
throw new Error("Could not determine REST URL and token for the database");
|
|
324
|
+
}
|
|
325
|
+
const isSingleCommand = commands.length === 1;
|
|
326
|
+
const url = isSingleCommand ? restUrl : restUrl + "/pipeline";
|
|
327
|
+
const body = isSingleCommand ? JSON.stringify(commands[0]) : JSON.stringify(commands);
|
|
328
|
+
const req = await fetch2(url, {
|
|
270
329
|
method: "POST",
|
|
271
|
-
body
|
|
330
|
+
body,
|
|
272
331
|
headers: {
|
|
273
332
|
"Content-Type": "application/json",
|
|
274
|
-
Authorization: `Bearer ${
|
|
333
|
+
Authorization: `Bearer ${restToken}`
|
|
275
334
|
}
|
|
276
335
|
});
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
336
|
+
if (isSingleCommand) {
|
|
337
|
+
const result = await req.json();
|
|
338
|
+
log("command result:", result);
|
|
339
|
+
if ("error" in result) {
|
|
340
|
+
throw new Error("Redis error: " + result.error);
|
|
341
|
+
}
|
|
342
|
+
const isScanCommand = commands[0][0].toLocaleLowerCase().includes("scan");
|
|
343
|
+
const messages = [json(result)];
|
|
344
|
+
if (isScanCommand)
|
|
345
|
+
messages.push(`NOTE: Use the returned cursor to get the next set of keys.
|
|
286
346
|
NOTE: The result might be too large to be returned. If applicable, stop after the second SCAN command and ask the user if they want to continue.`);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
database_rest_url: z3.string().describe("The REST URL of the database. Example: https://***.upstash.io"),
|
|
294
|
-
database_rest_token: z3.string().describe("The REST token of the database."),
|
|
295
|
-
commands: z3.array(z3.array(z3.string())).describe("The Redis commands to run. Example: [['SET', 'foo', 'bar'], ['GET', 'foo']]")
|
|
296
|
-
}),
|
|
297
|
-
handler: async ({ database_rest_url, database_rest_token, commands }) => {
|
|
298
|
-
const req = await fetch2(database_rest_url + "/pipeline", {
|
|
299
|
-
method: "POST",
|
|
300
|
-
body: JSON.stringify(commands),
|
|
301
|
-
headers: {
|
|
302
|
-
"Content-Type": "application/json",
|
|
303
|
-
Authorization: `Bearer ${database_rest_token}`
|
|
347
|
+
return messages;
|
|
348
|
+
} else {
|
|
349
|
+
const result = await req.json();
|
|
350
|
+
log("commands result:", result);
|
|
351
|
+
if (result.some((r) => "error" in r)) {
|
|
352
|
+
throw new Error("Some commands in the pipeline resulted in an error:\n" + json(result));
|
|
304
353
|
}
|
|
305
|
-
|
|
306
|
-
const result = await req.json();
|
|
307
|
-
log("commands result:", result);
|
|
308
|
-
if (result.some((r) => "error" in r)) {
|
|
309
|
-
throw new Error("Some commands in the pipeline resulted in an error:\n" + json(result));
|
|
354
|
+
return json(result);
|
|
310
355
|
}
|
|
311
|
-
return json(result);
|
|
312
356
|
}
|
|
313
357
|
})
|
|
314
358
|
};
|
|
@@ -433,28 +477,14 @@ ${GENERIC_DATABASE_NOTES}
|
|
|
433
477
|
return json(updatedDb);
|
|
434
478
|
}
|
|
435
479
|
}),
|
|
436
|
-
|
|
437
|
-
description: `Get
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
return [
|
|
445
|
-
json({
|
|
446
|
-
days: stats.days,
|
|
447
|
-
command_usage: stats.dailyrequests,
|
|
448
|
-
bandwidth_usage: stats.bandwidths
|
|
449
|
-
}),
|
|
450
|
-
`NOTE: Times are calculated according to UTC+0`
|
|
451
|
-
];
|
|
452
|
-
}
|
|
453
|
-
}),
|
|
454
|
-
redis_database_get_stats: tool({
|
|
455
|
-
description: `Get SAMPLED usage statistics of an Upstash redis database over a period of time (1h, 3h, 12h, 1d, 3d, 7d). Use this to check for peak usages and latency problems.
|
|
456
|
-
Includes: read_latency_mean, write_latency_mean, keyspace, throughput (cmds/sec), diskusage
|
|
457
|
-
NOTE: If the user does not specify which stat to get, use throughput as default.`,
|
|
480
|
+
redis_database_get_statistics: tool({
|
|
481
|
+
description: `Get comprehensive usage statistics of an Upstash redis database. Returns both:
|
|
482
|
+
1. PRECISE 5-day usage: Exact command count and bandwidth usage over the last 5 days
|
|
483
|
+
2. SAMPLED period stats: Sampled statistics over a specified period (1h, 3h, 12h, 1d, 3d, 7d) for performance monitoring
|
|
484
|
+
|
|
485
|
+
For sampled stats, includes: read_latency_mean, write_latency_mean, keyspace, throughput (cmds/sec), diskusage
|
|
486
|
+
NOTE: If user doesn't specify stat_type, defaults to "throughput" for sampled stats.
|
|
487
|
+
NOTE: Ask user first if they want to see stats for each database separately or just for one.`,
|
|
458
488
|
inputSchema: z4.object({
|
|
459
489
|
id: z4.string().describe("The ID of your database."),
|
|
460
490
|
period: z4.union([
|
|
@@ -464,27 +494,42 @@ NOTE: If the user does not specify which stat to get, use throughput as default.
|
|
|
464
494
|
z4.literal("1d"),
|
|
465
495
|
z4.literal("3d"),
|
|
466
496
|
z4.literal("7d")
|
|
467
|
-
]).describe("The period
|
|
468
|
-
|
|
497
|
+
]).describe("The period for sampled stats."),
|
|
498
|
+
stat_type: z4.union([
|
|
469
499
|
z4.literal("read_latency_mean"),
|
|
470
500
|
z4.literal("write_latency_mean"),
|
|
471
501
|
z4.literal("keyspace").describe("Number of keys in db"),
|
|
472
502
|
z4.literal("throughput").describe("commands per second (sampled), calculate area for estimated count"),
|
|
473
503
|
z4.literal("diskusage").describe("Current disk usage in bytes")
|
|
474
|
-
]).describe("The type of stat to get")
|
|
504
|
+
]).optional().describe("The type of sampled stat to get (defaults to 'throughput')")
|
|
475
505
|
}),
|
|
476
|
-
handler: async ({ id, period,
|
|
506
|
+
handler: async ({ id, period, stat_type }) => {
|
|
507
|
+
const statType = stat_type || "throughput";
|
|
477
508
|
const stats = await http.get([
|
|
478
509
|
"v2/redis/stats",
|
|
479
510
|
`${id}?period=${period}`
|
|
480
511
|
]);
|
|
481
|
-
const stat = stats[
|
|
512
|
+
const stat = stats[statType];
|
|
482
513
|
if (!Array.isArray(stat))
|
|
483
514
|
throw new Error(
|
|
484
|
-
`Invalid
|
|
515
|
+
`Invalid stat_type provided: ${statType}. Valid keys are: ${Object.keys(stats).join(", ")}`
|
|
485
516
|
);
|
|
486
517
|
return [
|
|
487
|
-
|
|
518
|
+
json({
|
|
519
|
+
// 5-day precise usage stats
|
|
520
|
+
usage_last_5_days: {
|
|
521
|
+
days: stats.days,
|
|
522
|
+
command_usage: stats.dailyrequests,
|
|
523
|
+
bandwidth_usage: stats.bandwidths
|
|
524
|
+
},
|
|
525
|
+
// Sampled stats for the specified period and type
|
|
526
|
+
sampled_stats: {
|
|
527
|
+
period,
|
|
528
|
+
stat_type: statType,
|
|
529
|
+
data: parseUsageData(stat)
|
|
530
|
+
}
|
|
531
|
+
}),
|
|
532
|
+
`NOTE: Times are calculated according to UTC+0`,
|
|
488
533
|
`NOTE: Use the timestamps_to_date tool to parse timestamps if needed`,
|
|
489
534
|
`NOTE: Don't try to plot multiple stats in the same chart`
|
|
490
535
|
];
|
|
@@ -514,21 +559,536 @@ var redisTools = {
|
|
|
514
559
|
...utilTools
|
|
515
560
|
};
|
|
516
561
|
|
|
562
|
+
// src/tools/qstash/qstash.ts
|
|
563
|
+
import { z as z5 } from "zod";
|
|
564
|
+
|
|
565
|
+
// src/tools/qstash/utils.ts
|
|
566
|
+
var cachedToken = null;
|
|
567
|
+
var tokenExpiry = 0;
|
|
568
|
+
async function getQStashToken() {
|
|
569
|
+
const now = Date.now();
|
|
570
|
+
if (cachedToken && now < tokenExpiry) {
|
|
571
|
+
return cachedToken;
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
const user = await http.get("qstash/user");
|
|
575
|
+
cachedToken = user.token;
|
|
576
|
+
tokenExpiry = now + 60 * 60 * 1e3;
|
|
577
|
+
return user.token;
|
|
578
|
+
} catch (error) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Failed to get QStash token: ${error instanceof Error ? error.message : String(error)}`
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function createQStashClientWithToken(creds = {}) {
|
|
585
|
+
if (!(creds == null ? void 0 : creds.token)) {
|
|
586
|
+
creds.token = await getQStashToken();
|
|
587
|
+
}
|
|
588
|
+
return createQStashClient({
|
|
589
|
+
url: creds == null ? void 0 : creds.url,
|
|
590
|
+
token: creds == null ? void 0 : creds.token
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/tools/qstash/qstash.ts
|
|
595
|
+
var qstashCreds = {
|
|
596
|
+
qstash_creds: z5.undefined()
|
|
597
|
+
// qstash_creds: z
|
|
598
|
+
// .object({
|
|
599
|
+
// url: z.string(),
|
|
600
|
+
// token: z.string(),
|
|
601
|
+
// })
|
|
602
|
+
// .optional()
|
|
603
|
+
// .describe(
|
|
604
|
+
// "Optional qstash credentials. Use for local qstash connections and external qstash deployments"
|
|
605
|
+
// ),
|
|
606
|
+
};
|
|
607
|
+
var qstashTools = {
|
|
608
|
+
qstash_get_user_token: tool({
|
|
609
|
+
description: `Get the QSTASH_TOKEN and QSTASH_URL of the current user. This
|
|
610
|
+
is not needed for the mcp tools since the token is automatically fetched from
|
|
611
|
+
the Upstash API for them.`,
|
|
612
|
+
handler: async () => {
|
|
613
|
+
const user = await http.get("qstash/user");
|
|
614
|
+
return [json(user)];
|
|
615
|
+
}
|
|
616
|
+
}),
|
|
617
|
+
qstash_publish_message: tool({
|
|
618
|
+
description: `Publish a message to a destination URL using QStash. This
|
|
619
|
+
sends an HTTP request to the specified destination via QStash's message
|
|
620
|
+
queue. This can also be used to trigger a upstash workflow run.`,
|
|
621
|
+
inputSchema: z5.object({
|
|
622
|
+
destination: z5.string().describe("The destination URL to send the message to (e.g., 'https://example.com')"),
|
|
623
|
+
body: z5.string().optional().describe("Request body (JSON string or plain text)"),
|
|
624
|
+
method: z5.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().describe("HTTP method (optional, defaults to POST)").default("POST"),
|
|
625
|
+
delay: z5.string().optional().describe("Delay before message delivery (e.g., '10s', '5m', '1h')"),
|
|
626
|
+
retries: z5.number().optional().describe("Number of retries on failure, default is 3"),
|
|
627
|
+
callback: z5.string().optional().describe("Callback URL that will be called when the message is successfully delivered"),
|
|
628
|
+
failureCallback: z5.string().optional().describe("Callback URL that will be called when the message is failed to deliver"),
|
|
629
|
+
timeout: z5.string().optional().describe("Request timeout (e.g., '30s', '1h')"),
|
|
630
|
+
queueName: z5.string().optional().describe(
|
|
631
|
+
"Queue name to use, you have to first create the queue in upstash. Prefer the flow control key instead"
|
|
632
|
+
),
|
|
633
|
+
flow_control: z5.object({
|
|
634
|
+
key: z5.string().describe("Unique identifier for grouping messages under same flow control rules"),
|
|
635
|
+
parallelism: z5.number().optional().describe("Max concurrent active calls (default: unlimited)"),
|
|
636
|
+
rate: z5.number().optional().describe("Max calls per period (default: unlimited)"),
|
|
637
|
+
period: z5.string().optional().describe("Time window for rate limit (e.g., '1s', '1m', '1h', default: '1s')")
|
|
638
|
+
}).optional().describe("Flow control for rate limiting and parallelism management"),
|
|
639
|
+
extraHeaders: z5.record(z5.string()).optional().describe("Extra headers to add to the request"),
|
|
640
|
+
...qstashCreds
|
|
641
|
+
}),
|
|
642
|
+
handler: async ({
|
|
643
|
+
destination,
|
|
644
|
+
body,
|
|
645
|
+
method,
|
|
646
|
+
extraHeaders,
|
|
647
|
+
delay,
|
|
648
|
+
retries,
|
|
649
|
+
callback,
|
|
650
|
+
failureCallback,
|
|
651
|
+
timeout,
|
|
652
|
+
queueName,
|
|
653
|
+
flow_control,
|
|
654
|
+
qstash_creds
|
|
655
|
+
}) => {
|
|
656
|
+
const client = await createQStashClientWithToken(qstash_creds);
|
|
657
|
+
const requestHeaders = {};
|
|
658
|
+
if (method) {
|
|
659
|
+
requestHeaders["Upstash-Method"] = method;
|
|
660
|
+
}
|
|
661
|
+
if (delay) {
|
|
662
|
+
requestHeaders["Upstash-Delay"] = delay;
|
|
663
|
+
}
|
|
664
|
+
if (retries !== void 0) {
|
|
665
|
+
requestHeaders["Upstash-Retries"] = retries.toString();
|
|
666
|
+
}
|
|
667
|
+
if (callback) {
|
|
668
|
+
requestHeaders["Upstash-Callback"] = callback;
|
|
669
|
+
}
|
|
670
|
+
if (failureCallback) {
|
|
671
|
+
requestHeaders["Upstash-Failure-Callback"] = failureCallback;
|
|
672
|
+
}
|
|
673
|
+
if (timeout) {
|
|
674
|
+
requestHeaders["Upstash-Timeout"] = timeout;
|
|
675
|
+
}
|
|
676
|
+
if (queueName) {
|
|
677
|
+
requestHeaders["Upstash-Queue-Name"] = queueName;
|
|
678
|
+
}
|
|
679
|
+
if (flow_control) {
|
|
680
|
+
requestHeaders["Upstash-Flow-Control-Key"] = flow_control.key;
|
|
681
|
+
const value = [
|
|
682
|
+
flow_control.parallelism === void 0 ? void 0 : `parallelism=${flow_control.parallelism}`,
|
|
683
|
+
flow_control.rate === void 0 ? void 0 : `rate=${flow_control.rate}`,
|
|
684
|
+
flow_control.period === void 0 ? void 0 : `period=${flow_control.period}`
|
|
685
|
+
].filter(Boolean).join(",");
|
|
686
|
+
requestHeaders["Upstash-Flow-Control-Value"] = value;
|
|
687
|
+
}
|
|
688
|
+
if (extraHeaders) {
|
|
689
|
+
for (const [key, value] of Object.entries(extraHeaders)) {
|
|
690
|
+
requestHeaders[key] = value;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const response = await client.post(
|
|
694
|
+
`v2/publish/${destination}`,
|
|
695
|
+
body || {},
|
|
696
|
+
requestHeaders
|
|
697
|
+
);
|
|
698
|
+
return [
|
|
699
|
+
"Message published successfully",
|
|
700
|
+
`Message ID: ${response.messageId}`,
|
|
701
|
+
json(response)
|
|
702
|
+
];
|
|
703
|
+
}
|
|
704
|
+
}),
|
|
705
|
+
qstash_logs_list: tool({
|
|
706
|
+
description: `List QStash logs with optional filtering. Returns a paginated list of message logs without their bodies.`,
|
|
707
|
+
inputSchema: z5.object({
|
|
708
|
+
cursor: z5.string().optional().describe("Cursor for pagination"),
|
|
709
|
+
messageId: z5.string().optional().describe("Filter logs by message ID"),
|
|
710
|
+
state: z5.enum([
|
|
711
|
+
"CREATED",
|
|
712
|
+
"ACTIVE",
|
|
713
|
+
"RETRY",
|
|
714
|
+
"ERROR",
|
|
715
|
+
"IN_PROGRESS",
|
|
716
|
+
"DELIVERED",
|
|
717
|
+
"FAILED",
|
|
718
|
+
"CANCEL_REQUESTED",
|
|
719
|
+
"CANCELLED"
|
|
720
|
+
]).optional().describe("Filter logs by state"),
|
|
721
|
+
url: z5.string().optional().describe("Filter logs by URL"),
|
|
722
|
+
topicName: z5.string().optional().describe("Filter logs by topic name"),
|
|
723
|
+
scheduleId: z5.string().optional().describe("Filter logs by schedule ID"),
|
|
724
|
+
queueName: z5.string().optional().describe("Filter logs by queue name"),
|
|
725
|
+
fromDate: z5.number().optional().describe("Filter logs from date (Unix timestamp in milliseconds)"),
|
|
726
|
+
toDate: z5.number().optional().describe("Filter logs to date (Unix timestamp in milliseconds)"),
|
|
727
|
+
count: z5.number().max(1e3).optional().describe("Number of logs to return (max 1000)"),
|
|
728
|
+
...qstashCreds
|
|
729
|
+
}),
|
|
730
|
+
handler: async (params) => {
|
|
731
|
+
const client = await createQStashClientWithToken(params.qstash_creds);
|
|
732
|
+
const response = await client.get("v2/logs", {
|
|
733
|
+
trimBody: 0,
|
|
734
|
+
groupBy: "messageId",
|
|
735
|
+
...params
|
|
736
|
+
});
|
|
737
|
+
const firstMessageFields = Object.fromEntries(
|
|
738
|
+
Object.entries(response.messages[0] ?? {}).filter(
|
|
739
|
+
([key, _value]) => !key.toLocaleLowerCase().includes("headers")
|
|
740
|
+
)
|
|
741
|
+
);
|
|
742
|
+
const cleanedEvents = response.messages.map((message) => ({
|
|
743
|
+
messageId: message.messageId,
|
|
744
|
+
events: message.events.map((event) => ({
|
|
745
|
+
state: event.state,
|
|
746
|
+
time: event.time
|
|
747
|
+
}))
|
|
748
|
+
}));
|
|
749
|
+
return [
|
|
750
|
+
`Found ${response.messages.length} log entries`,
|
|
751
|
+
response.cursor ? `Pagination cursor: ${response.cursor}` : "No more entries",
|
|
752
|
+
json({ ...firstMessageFields, events: cleanedEvents })
|
|
753
|
+
];
|
|
754
|
+
}
|
|
755
|
+
}),
|
|
756
|
+
qstash_logs_get: tool({
|
|
757
|
+
description: `Get details of a single QStash log item by message ID without trimming the body.`,
|
|
758
|
+
inputSchema: z5.object({
|
|
759
|
+
messageId: z5.string().describe("The message ID to get details for"),
|
|
760
|
+
...qstashCreds
|
|
761
|
+
}),
|
|
762
|
+
handler: async ({ messageId, qstash_creds }) => {
|
|
763
|
+
const client = await createQStashClientWithToken(qstash_creds);
|
|
764
|
+
const response = await client.get("v2/logs", { messageId });
|
|
765
|
+
if (response.messages.length === 0) {
|
|
766
|
+
return "No log entry found for the specified message ID";
|
|
767
|
+
}
|
|
768
|
+
const logEntry = response.messages[0];
|
|
769
|
+
return [`Log details for message ID: ${messageId}`, json(logEntry)];
|
|
770
|
+
}
|
|
771
|
+
}),
|
|
772
|
+
qstash_dlq_list: tool({
|
|
773
|
+
description: `List messages in the QStash Dead Letter Queue (DLQ) with optional filtering.`,
|
|
774
|
+
inputSchema: z5.object({
|
|
775
|
+
cursor: z5.string().optional().describe("Cursor for pagination"),
|
|
776
|
+
messageId: z5.string().optional().describe("Filter DLQ messages by message ID"),
|
|
777
|
+
url: z5.string().optional().describe("Filter DLQ messages by URL"),
|
|
778
|
+
topicName: z5.string().optional().describe("Filter DLQ messages by topic name"),
|
|
779
|
+
scheduleId: z5.string().optional().describe("Filter DLQ messages by schedule ID"),
|
|
780
|
+
queueName: z5.string().optional().describe("Filter DLQ messages by queue name"),
|
|
781
|
+
fromDate: z5.number().optional().describe("Filter from date (Unix timestamp in milliseconds)"),
|
|
782
|
+
toDate: z5.number().optional().describe("Filter to date (Unix timestamp in milliseconds)"),
|
|
783
|
+
responseStatus: z5.number().optional().describe("Filter by HTTP response status code"),
|
|
784
|
+
callerIp: z5.string().optional().describe("Filter by IP address of the publisher"),
|
|
785
|
+
count: z5.number().max(100).optional().describe("Number of messages to return (max 100)"),
|
|
786
|
+
...qstashCreds
|
|
787
|
+
}),
|
|
788
|
+
handler: async (params) => {
|
|
789
|
+
const client = await createQStashClientWithToken(params.qstash_creds);
|
|
790
|
+
const response = await client.get("v2/dlq", {
|
|
791
|
+
trimBody: 0,
|
|
792
|
+
...params
|
|
793
|
+
});
|
|
794
|
+
return [
|
|
795
|
+
`Found ${response.messages.length} DLQ messages`,
|
|
796
|
+
response.cursor ? `Pagination cursor: ${response.cursor}` : "No more entries",
|
|
797
|
+
json(response.messages)
|
|
798
|
+
];
|
|
799
|
+
}
|
|
800
|
+
}),
|
|
801
|
+
qstash_dlq_get: tool({
|
|
802
|
+
description: `Get details of a single DLQ message by DLQ ID.`,
|
|
803
|
+
inputSchema: z5.object({
|
|
804
|
+
dlqId: z5.string().describe("The DLQ ID of the message to retrieve"),
|
|
805
|
+
...qstashCreds
|
|
806
|
+
}),
|
|
807
|
+
handler: async ({ dlqId, qstash_creds }) => {
|
|
808
|
+
const client = await createQStashClientWithToken(qstash_creds);
|
|
809
|
+
const message = await client.get(`v2/dlq/${dlqId}`);
|
|
810
|
+
return [`DLQ message details for ID: ${dlqId}`, json(message)];
|
|
811
|
+
}
|
|
812
|
+
}),
|
|
813
|
+
qstash_schedules_list: tool({
|
|
814
|
+
description: `List all QStash schedules.`,
|
|
815
|
+
handler: async ({ qstash_creds }) => {
|
|
816
|
+
const client = await createQStashClientWithToken(qstash_creds);
|
|
817
|
+
const schedules = await client.get("v2/schedules");
|
|
818
|
+
return [`Found ${schedules.length} schedules`, json(schedules)];
|
|
819
|
+
}
|
|
820
|
+
}),
|
|
821
|
+
qstash_schedules_manage: tool({
|
|
822
|
+
description: `Create, update, or manage QStash schedules. This tool handles create, update (by providing scheduleId), pause, resume, and delete operations in one unified interface.`,
|
|
823
|
+
inputSchema: z5.object({
|
|
824
|
+
operation: z5.enum(["create", "update", "pause", "resume", "delete"]).describe("The operation to perform"),
|
|
825
|
+
scheduleId: z5.string().optional().describe("Schedule ID (required for update, pause, resume, delete operations)"),
|
|
826
|
+
destination: z5.string().optional().describe("Destination URL or topic name (required for create/update)"),
|
|
827
|
+
cron: z5.string().optional().describe("Cron expression (required for create/update)"),
|
|
828
|
+
method: z5.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().describe("HTTP method (optional, defaults to POST)"),
|
|
829
|
+
headers: z5.record(z5.string()).optional().describe("Request headers as key-value pairs"),
|
|
830
|
+
body: z5.string().optional().describe("Request body"),
|
|
831
|
+
delay: z5.string().optional().describe("Delay before message delivery (e.g., '10s', '5m', '1h')"),
|
|
832
|
+
retries: z5.number().optional().describe("Number of retries on failure"),
|
|
833
|
+
callback: z5.string().optional().describe("Callback URL for successful delivery"),
|
|
834
|
+
failureCallback: z5.string().optional().describe("Callback URL for failed delivery"),
|
|
835
|
+
timeout: z5.string().optional().describe("Request timeout (e.g., '30s')"),
|
|
836
|
+
queueName: z5.string().optional().describe("Queue name to use"),
|
|
837
|
+
...qstashCreds
|
|
838
|
+
}),
|
|
839
|
+
handler: async ({
|
|
840
|
+
operation,
|
|
841
|
+
scheduleId,
|
|
842
|
+
destination,
|
|
843
|
+
cron,
|
|
844
|
+
method = "POST",
|
|
845
|
+
headers,
|
|
846
|
+
body,
|
|
847
|
+
delay,
|
|
848
|
+
retries,
|
|
849
|
+
callback,
|
|
850
|
+
failureCallback,
|
|
851
|
+
timeout,
|
|
852
|
+
queueName,
|
|
853
|
+
qstash_creds
|
|
854
|
+
}) => {
|
|
855
|
+
const client = await createQStashClientWithToken(qstash_creds);
|
|
856
|
+
switch (operation) {
|
|
857
|
+
case "create":
|
|
858
|
+
case "update": {
|
|
859
|
+
if (!destination || !cron) {
|
|
860
|
+
throw new Error("destination and cron are required for create/update operations");
|
|
861
|
+
}
|
|
862
|
+
const requestHeaders = {
|
|
863
|
+
"Upstash-Cron": cron
|
|
864
|
+
};
|
|
865
|
+
if (method !== "POST") {
|
|
866
|
+
requestHeaders["Upstash-Method"] = method;
|
|
867
|
+
}
|
|
868
|
+
if (delay) {
|
|
869
|
+
requestHeaders["Upstash-Delay"] = delay;
|
|
870
|
+
}
|
|
871
|
+
if (retries !== void 0) {
|
|
872
|
+
requestHeaders["Upstash-Retries"] = retries.toString();
|
|
873
|
+
}
|
|
874
|
+
if (callback) {
|
|
875
|
+
requestHeaders["Upstash-Callback"] = callback;
|
|
876
|
+
}
|
|
877
|
+
if (failureCallback) {
|
|
878
|
+
requestHeaders["Upstash-Failure-Callback"] = failureCallback;
|
|
879
|
+
}
|
|
880
|
+
if (timeout) {
|
|
881
|
+
requestHeaders["Upstash-Timeout"] = timeout;
|
|
882
|
+
}
|
|
883
|
+
if (queueName) {
|
|
884
|
+
requestHeaders["Upstash-Queue-Name"] = queueName;
|
|
885
|
+
}
|
|
886
|
+
if (scheduleId && operation === "update") {
|
|
887
|
+
requestHeaders["Upstash-Schedule-Id"] = scheduleId;
|
|
888
|
+
}
|
|
889
|
+
if (headers) {
|
|
890
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
891
|
+
requestHeaders[key] = value;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const response = await client.post(
|
|
895
|
+
`v2/schedules/${destination}`,
|
|
896
|
+
body || {},
|
|
897
|
+
requestHeaders
|
|
898
|
+
);
|
|
899
|
+
return [
|
|
900
|
+
operation === "create" ? "Schedule created successfully" : "Schedule updated successfully",
|
|
901
|
+
`Schedule ID: ${response.scheduleId}`,
|
|
902
|
+
json(response)
|
|
903
|
+
];
|
|
904
|
+
}
|
|
905
|
+
case "pause": {
|
|
906
|
+
if (!scheduleId) {
|
|
907
|
+
throw new Error("scheduleId is required for pause operation");
|
|
908
|
+
}
|
|
909
|
+
await client.post(`v2/schedules/${scheduleId}/pause`);
|
|
910
|
+
return `Schedule ${scheduleId} paused successfully`;
|
|
911
|
+
}
|
|
912
|
+
case "resume": {
|
|
913
|
+
if (!scheduleId) {
|
|
914
|
+
throw new Error("scheduleId is required for resume operation");
|
|
915
|
+
}
|
|
916
|
+
await client.post(`v2/schedules/${scheduleId}/resume`);
|
|
917
|
+
return `Schedule ${scheduleId} resumed successfully`;
|
|
918
|
+
}
|
|
919
|
+
case "delete": {
|
|
920
|
+
if (!scheduleId) {
|
|
921
|
+
throw new Error("scheduleId is required for delete operation");
|
|
922
|
+
}
|
|
923
|
+
await client.delete(`v2/schedules/${scheduleId}`);
|
|
924
|
+
return `Schedule ${scheduleId} deleted successfully`;
|
|
925
|
+
}
|
|
926
|
+
default: {
|
|
927
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
})
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// src/tools/qstash/workflow.ts
|
|
935
|
+
import { z as z6 } from "zod";
|
|
936
|
+
var workflowTools = {
|
|
937
|
+
workflow_logs_list: tool({
|
|
938
|
+
description: `List Upstash Workflow logs with optional filtering. Returns grouped workflow runs with their execution details.`,
|
|
939
|
+
inputSchema: z6.object({
|
|
940
|
+
cursor: z6.string().optional().describe("Cursor for pagination"),
|
|
941
|
+
workflowRunId: z6.string().optional().describe("Filter by specific workflow run ID"),
|
|
942
|
+
count: z6.number().optional().describe("Number of workflow runs to return"),
|
|
943
|
+
state: z6.enum(["RUN_STARTED", "RUN_SUCCESS", "RUN_FAILED", "RUN_CANCELED"]).optional().describe("Filter by workflow state"),
|
|
944
|
+
workflowUrl: z6.string().optional().describe("Filter by workflow URL (exact match)"),
|
|
945
|
+
workflowCreatedAt: z6.number().optional().describe("Filter by workflow creation timestamp (Unix timestamp)"),
|
|
946
|
+
...qstashCreds
|
|
947
|
+
}),
|
|
948
|
+
handler: async (params) => {
|
|
949
|
+
const client = await createQStashClientWithToken(params.qstash_creds);
|
|
950
|
+
const response = await client.get("v2/workflows/events", {
|
|
951
|
+
trimBody: 0,
|
|
952
|
+
groupBy: "workflowRunId",
|
|
953
|
+
...params
|
|
954
|
+
});
|
|
955
|
+
const cleaned = response.runs.map(
|
|
956
|
+
(run) => Object.fromEntries(Object.entries(run).filter(([key, _value]) => key !== "steps"))
|
|
957
|
+
);
|
|
958
|
+
return [
|
|
959
|
+
`Found ${response.runs.length} workflow runs`,
|
|
960
|
+
response.cursor ? `Pagination cursor: ${response.cursor}` : "No more entries",
|
|
961
|
+
json({
|
|
962
|
+
...response,
|
|
963
|
+
runs: cleaned
|
|
964
|
+
})
|
|
965
|
+
];
|
|
966
|
+
}
|
|
967
|
+
}),
|
|
968
|
+
workflow_logs_get: tool({
|
|
969
|
+
description: `Get details of a single workflow run by workflow run ID. There
|
|
970
|
+
could be multiple workflow runs with the same workflow run ID, so you can
|
|
971
|
+
use the workflowCreatedAt to get the details of the specific workflow run.`,
|
|
972
|
+
inputSchema: z6.object({
|
|
973
|
+
workflowRunId: z6.string().describe("The workflow run ID to get details for"),
|
|
974
|
+
workflowCreatedAt: z6.number().optional().describe("The workflow creation timestamp (Unix timestamp)"),
|
|
975
|
+
...qstashCreds
|
|
976
|
+
}),
|
|
977
|
+
handler: async (params) => {
|
|
978
|
+
const client = await createQStashClientWithToken(params.qstash_creds);
|
|
979
|
+
const response = await client.get("v2/workflows/logs", {
|
|
980
|
+
...params
|
|
981
|
+
});
|
|
982
|
+
if (response.runs.length === 0) {
|
|
983
|
+
return "No workflow run found";
|
|
984
|
+
}
|
|
985
|
+
const workflowRun = response.runs[0];
|
|
986
|
+
return [
|
|
987
|
+
`Workflow run details for ID: ${params.workflowRunId} created at: ${params.workflowCreatedAt}`,
|
|
988
|
+
json(workflowRun)
|
|
989
|
+
];
|
|
990
|
+
}
|
|
991
|
+
}),
|
|
992
|
+
workflow_dlq_list: tool({
|
|
993
|
+
description: `List failed workflow runs in the Dead Letter Queue (DLQ) with optional filtering.`,
|
|
994
|
+
inputSchema: z6.object({
|
|
995
|
+
cursor: z6.string().optional().describe("Cursor for pagination"),
|
|
996
|
+
workflowRunId: z6.string().optional().describe("Filter by workflow run ID"),
|
|
997
|
+
workflowUrl: z6.string().optional().describe("Filter by workflow URL"),
|
|
998
|
+
fromDate: z6.number().optional().describe("Filter from date (Unix timestamp in milliseconds)"),
|
|
999
|
+
toDate: z6.number().optional().describe("Filter to date (Unix timestamp in milliseconds)"),
|
|
1000
|
+
responseStatus: z6.number().optional().describe("Filter by HTTP response status code"),
|
|
1001
|
+
callerIP: z6.string().optional().describe("Filter by IP address of the caller"),
|
|
1002
|
+
failureCallbackState: z6.string().optional().describe("Filter by failure callback state"),
|
|
1003
|
+
count: z6.number().optional().describe("Number of DLQ messages to return"),
|
|
1004
|
+
...qstashCreds
|
|
1005
|
+
}),
|
|
1006
|
+
handler: async (params) => {
|
|
1007
|
+
const client = await createQStashClientWithToken(params.qstash_creds);
|
|
1008
|
+
const response = await client.get("v2/workflows/dlq", {
|
|
1009
|
+
...params,
|
|
1010
|
+
trimBody: 0
|
|
1011
|
+
});
|
|
1012
|
+
const cleaned = response.messages.map(
|
|
1013
|
+
(message) => Object.fromEntries(Object.entries(message).filter(([key]) => !key.includes("header")))
|
|
1014
|
+
);
|
|
1015
|
+
return [
|
|
1016
|
+
`Found ${response.messages.length} failed workflow runs in DLQ`,
|
|
1017
|
+
response.cursor ? `Pagination cursor: ${response.cursor}` : "No more entries",
|
|
1018
|
+
json({
|
|
1019
|
+
...response,
|
|
1020
|
+
messages: cleaned
|
|
1021
|
+
})
|
|
1022
|
+
];
|
|
1023
|
+
}
|
|
1024
|
+
}),
|
|
1025
|
+
workflow_dlq_get: tool({
|
|
1026
|
+
description: `Get details of a single failed workflow run from the DLQ by DLQ ID.`,
|
|
1027
|
+
inputSchema: z6.object({
|
|
1028
|
+
dlqId: z6.string().describe("The DLQ ID of the failed workflow run to retrieve"),
|
|
1029
|
+
...qstashCreds
|
|
1030
|
+
}),
|
|
1031
|
+
handler: async (params) => {
|
|
1032
|
+
const client = await createQStashClientWithToken(params.qstash_creds);
|
|
1033
|
+
const message = await client.get(`v2/workflows/dlq/${params.dlqId}`);
|
|
1034
|
+
return [`Failed workflow run details for DLQ ID: ${params.dlqId}`, json(message)];
|
|
1035
|
+
}
|
|
1036
|
+
}),
|
|
1037
|
+
workflow_dlq_manage: tool({
|
|
1038
|
+
description: `Delete, restart, and resume failed workflow runs in the DLQ using only the DLQ ID.`,
|
|
1039
|
+
inputSchema: z6.object({
|
|
1040
|
+
dlqId: z6.string().describe("The DLQ ID of the failed workflow run"),
|
|
1041
|
+
action: z6.enum(["delete", "restart", "resume"]).describe(
|
|
1042
|
+
"The action to perform: delete (remove from DLQ), restart (from beginning), or resume (from the failed step)"
|
|
1043
|
+
),
|
|
1044
|
+
...qstashCreds
|
|
1045
|
+
}),
|
|
1046
|
+
handler: async ({ dlqId, action, qstash_creds }) => {
|
|
1047
|
+
const client = await createQStashClientWithToken(qstash_creds);
|
|
1048
|
+
switch (action) {
|
|
1049
|
+
case "delete": {
|
|
1050
|
+
await client.delete(`v2/workflows/dlq/delete/${dlqId}`);
|
|
1051
|
+
return `Failed workflow run with DLQ ID ${dlqId} deleted successfully`;
|
|
1052
|
+
}
|
|
1053
|
+
case "restart": {
|
|
1054
|
+
const restartResponse = await client.post(`v2/workflows/dlq/restart/${dlqId}`);
|
|
1055
|
+
return [
|
|
1056
|
+
`Workflow run restarted successfully from DLQ ID: ${dlqId}`,
|
|
1057
|
+
json(restartResponse)
|
|
1058
|
+
];
|
|
1059
|
+
}
|
|
1060
|
+
case "resume": {
|
|
1061
|
+
const resumeResponse = await client.post(`v2/workflows/dlq/resume/${dlqId}`);
|
|
1062
|
+
return [`Workflow run resumed successfully from DLQ ID: ${dlqId}`, json(resumeResponse)];
|
|
1063
|
+
}
|
|
1064
|
+
default: {
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
`Invalid action: ${action}. Supported actions are: delete, restart, resume`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
})
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
// src/tools/qstash/index.ts
|
|
1075
|
+
var qstashAllTools = {
|
|
1076
|
+
...qstashTools,
|
|
1077
|
+
...workflowTools
|
|
1078
|
+
};
|
|
1079
|
+
|
|
517
1080
|
// src/tools/index.ts
|
|
518
1081
|
var json = (json2) => typeof json2 === "string" ? json2 : JSON.stringify(json2, null, 2);
|
|
519
1082
|
var tools = {
|
|
520
|
-
...redisTools
|
|
1083
|
+
...redisTools,
|
|
1084
|
+
...qstashAllTools
|
|
521
1085
|
};
|
|
522
1086
|
function tool(tool2) {
|
|
523
1087
|
return tool2;
|
|
524
1088
|
}
|
|
525
1089
|
|
|
526
|
-
// src/tool.ts
|
|
527
|
-
import { z as z5 } from "zod";
|
|
528
|
-
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
529
|
-
|
|
530
1090
|
// src/settings.ts
|
|
531
|
-
var MAX_MESSAGE_LENGTH =
|
|
1091
|
+
var MAX_MESSAGE_LENGTH = 3e4;
|
|
532
1092
|
|
|
533
1093
|
// src/tool.ts
|
|
534
1094
|
function handlerResponseToCallResult(response) {
|
|
@@ -542,66 +1102,67 @@ function handlerResponseToCallResult(response) {
|
|
|
542
1102
|
};
|
|
543
1103
|
} else return response;
|
|
544
1104
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
1105
|
+
|
|
1106
|
+
// src/server.ts
|
|
1107
|
+
import z7 from "zod";
|
|
1108
|
+
function createServerInstance() {
|
|
1109
|
+
const server = new McpServer(
|
|
1110
|
+
{ name: "upstash", version: "0.1.0" },
|
|
1111
|
+
{
|
|
1112
|
+
capabilities: {
|
|
1113
|
+
tools: {},
|
|
1114
|
+
logging: {}
|
|
1115
|
+
}
|
|
553
1116
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
return jsonSchema;
|
|
557
|
-
}
|
|
558
|
-
function convertToTools(tools2) {
|
|
559
|
-
return Object.entries(tools2).map(([name, tool2]) => ({
|
|
1117
|
+
);
|
|
1118
|
+
const toolsList = Object.entries(tools).map(([name, tool2]) => ({
|
|
560
1119
|
name,
|
|
561
1120
|
description: tool2.description,
|
|
562
|
-
inputSchema:
|
|
1121
|
+
inputSchema: tool2.inputSchema,
|
|
1122
|
+
tool: tool2
|
|
563
1123
|
}));
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
1124
|
+
for (const toolDef of toolsList) {
|
|
1125
|
+
const toolName = toolDef.name;
|
|
1126
|
+
const tool2 = toolDef.tool;
|
|
1127
|
+
server.registerTool(
|
|
1128
|
+
toolName,
|
|
1129
|
+
{
|
|
1130
|
+
description: tool2.description,
|
|
1131
|
+
inputSchema: (tool2.inputSchema ?? z7.object({})).shape
|
|
1132
|
+
},
|
|
1133
|
+
// @ts-expect-error - Just ignore the types here
|
|
1134
|
+
async (args) => {
|
|
1135
|
+
log("< received tool call:", toolName, args);
|
|
1136
|
+
try {
|
|
1137
|
+
const result = await tool2.handler(args);
|
|
1138
|
+
const response = handlerResponseToCallResult(result);
|
|
1139
|
+
log("> tool result:", response.content.map((item) => item.text).join("\n"));
|
|
1140
|
+
return response;
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
1143
|
+
log("> error in tool call:", msg);
|
|
1144
|
+
return {
|
|
1145
|
+
content: [
|
|
1146
|
+
{
|
|
1147
|
+
type: "text",
|
|
1148
|
+
text: `${error instanceof Error ? error.name : "Error"}: ${msg}`
|
|
1149
|
+
},
|
|
1150
|
+
...DEBUG ? [
|
|
1151
|
+
{
|
|
1152
|
+
type: "text",
|
|
1153
|
+
text: `
|
|
1154
|
+
Stack trace: ${error instanceof Error ? error.stack : "No stack trace available"}`
|
|
1155
|
+
}
|
|
1156
|
+
] : []
|
|
1157
|
+
],
|
|
1158
|
+
isError: true
|
|
1159
|
+
};
|
|
596
1160
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
};
|
|
1161
|
+
}
|
|
1162
|
+
);
|
|
600
1163
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
// src/index.ts
|
|
604
|
-
import "dotenv/config";
|
|
1164
|
+
return server;
|
|
1165
|
+
}
|
|
605
1166
|
|
|
606
1167
|
// src/test-connection.ts
|
|
607
1168
|
async function testConnection() {
|
|
@@ -613,57 +1174,155 @@ async function testConnection() {
|
|
|
613
1174
|
}
|
|
614
1175
|
|
|
615
1176
|
// src/index.ts
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
var
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
await testConnection();
|
|
655
|
-
await main();
|
|
656
|
-
} else {
|
|
657
|
-
throw new Error(`Unknown command: ${cmd}. Expected 'init' or 'run'. ${USAGE_GENERAL}`);
|
|
1177
|
+
import "dotenv/config";
|
|
1178
|
+
var program = new Command().option("--transport <stdio|http>", "transport type", "stdio").option("--port <number>", "port for HTTP transport", "3000").option("--email <email>", "Upstash email").option("--api-key <key>", "Upstash API key").option("--debug", "Enable debug mode").allowUnknownOption().parse(process.argv);
|
|
1179
|
+
var cliOptions = program.opts();
|
|
1180
|
+
var DEBUG = cliOptions.debug ?? false;
|
|
1181
|
+
var allowedTransports = ["stdio", "http"];
|
|
1182
|
+
if (!allowedTransports.includes(cliOptions.transport)) {
|
|
1183
|
+
console.error(
|
|
1184
|
+
`Invalid --transport value: '${cliOptions.transport}'. Must be one of: stdio, http.`
|
|
1185
|
+
);
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
var TRANSPORT_TYPE = cliOptions.transport || "stdio";
|
|
1189
|
+
var passedPortFlag = process.argv.includes("--port");
|
|
1190
|
+
if (TRANSPORT_TYPE === "stdio" && passedPortFlag) {
|
|
1191
|
+
console.error("The --port flag is not allowed when using --transport stdio.");
|
|
1192
|
+
process.exit(1);
|
|
1193
|
+
}
|
|
1194
|
+
var CLI_PORT = (() => {
|
|
1195
|
+
const parsed = Number.parseInt(cliOptions.port, 10);
|
|
1196
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
1197
|
+
})();
|
|
1198
|
+
var sseTransports = {};
|
|
1199
|
+
function getClientIp(req) {
|
|
1200
|
+
var _a;
|
|
1201
|
+
const forwardedFor = req.headers["x-forwarded-for"] || req.headers["X-Forwarded-For"];
|
|
1202
|
+
if (forwardedFor) {
|
|
1203
|
+
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
|
|
1204
|
+
const ipList = ips.split(",").map((ip) => ip.trim());
|
|
1205
|
+
for (const ip of ipList) {
|
|
1206
|
+
const plainIp = ip.replace(/^::ffff:/, "");
|
|
1207
|
+
if (!plainIp.startsWith("10.") && !plainIp.startsWith("192.168.") && !/^172\.(1[6-9]|2\d|3[01])\./.test(plainIp)) {
|
|
1208
|
+
return plainIp;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return ipList[0].replace(/^::ffff:/, "");
|
|
1212
|
+
}
|
|
1213
|
+
if ((_a = req.socket) == null ? void 0 : _a.remoteAddress) {
|
|
1214
|
+
return req.socket.remoteAddress.replace(/^::ffff:/, "");
|
|
658
1215
|
}
|
|
1216
|
+
return void 0;
|
|
659
1217
|
}
|
|
660
1218
|
async function main() {
|
|
661
|
-
const
|
|
662
|
-
|
|
1219
|
+
const email = cliOptions.email || process.env.UPSTASH_EMAIL;
|
|
1220
|
+
const apiKey = cliOptions.apiKey || process.env.UPSTASH_API_KEY;
|
|
1221
|
+
if (!email || !apiKey) {
|
|
1222
|
+
console.error(
|
|
1223
|
+
"Missing required credentials. Provide --email and --api-key or set UPSTASH_EMAIL and UPSTASH_API_KEY environment variables."
|
|
1224
|
+
);
|
|
1225
|
+
process.exit(1);
|
|
1226
|
+
}
|
|
1227
|
+
config.email = email;
|
|
1228
|
+
config.apiKey = apiKey;
|
|
1229
|
+
await testConnection();
|
|
1230
|
+
const transportType = TRANSPORT_TYPE;
|
|
1231
|
+
if (transportType === "http") {
|
|
1232
|
+
const initialPort = CLI_PORT ?? 3e3;
|
|
1233
|
+
let actualPort = initialPort;
|
|
1234
|
+
const httpServer = createServer(async (req, res) => {
|
|
1235
|
+
const url = new globalThis.URL(req.url || "", `http://${req.headers.host}`).pathname;
|
|
1236
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1237
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,DELETE");
|
|
1238
|
+
res.setHeader(
|
|
1239
|
+
"Access-Control-Allow-Headers",
|
|
1240
|
+
"Content-Type, MCP-Session-Id, MCP-Protocol-Version, X-Upstash-API-Key, Upstash-API-Key, X-API-Key, Authorization"
|
|
1241
|
+
);
|
|
1242
|
+
res.setHeader("Access-Control-Expose-Headers", "MCP-Session-Id");
|
|
1243
|
+
if (req.method === "OPTIONS") {
|
|
1244
|
+
res.writeHead(200);
|
|
1245
|
+
res.end();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
try {
|
|
1249
|
+
const _clientIp = getClientIp(req);
|
|
1250
|
+
const requestServer = createServerInstance();
|
|
1251
|
+
if (url === "/mcp") {
|
|
1252
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1253
|
+
sessionIdGenerator: void 0
|
|
1254
|
+
});
|
|
1255
|
+
await requestServer.connect(transport);
|
|
1256
|
+
await transport.handleRequest(req, res);
|
|
1257
|
+
} else if (url === "/sse" && req.method === "GET") {
|
|
1258
|
+
const sseTransport = new SSEServerTransport("/messages", res);
|
|
1259
|
+
sseTransports[sseTransport.sessionId] = sseTransport;
|
|
1260
|
+
res.on("close", () => {
|
|
1261
|
+
delete sseTransports[sseTransport.sessionId];
|
|
1262
|
+
});
|
|
1263
|
+
await requestServer.connect(sseTransport);
|
|
1264
|
+
} else if (url === "/messages" && req.method === "POST") {
|
|
1265
|
+
const sessionId = new globalThis.URL(
|
|
1266
|
+
req.url || "",
|
|
1267
|
+
`http://${req.headers.host}`
|
|
1268
|
+
).searchParams.get("sessionId") ?? "";
|
|
1269
|
+
if (!sessionId) {
|
|
1270
|
+
res.writeHead(400);
|
|
1271
|
+
res.end("Missing sessionId parameter");
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const sseTransport = sseTransports[sessionId];
|
|
1275
|
+
if (!sseTransport) {
|
|
1276
|
+
res.writeHead(400);
|
|
1277
|
+
res.end(`No transport found for sessionId: ${sessionId}`);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
await sseTransport.handlePostMessage(req, res);
|
|
1281
|
+
} else if (url === "/ping") {
|
|
1282
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
1283
|
+
res.end("pong");
|
|
1284
|
+
} else {
|
|
1285
|
+
res.writeHead(404);
|
|
1286
|
+
res.end("Not found");
|
|
1287
|
+
}
|
|
1288
|
+
} catch (error) {
|
|
1289
|
+
console.error("Error handling request:", error);
|
|
1290
|
+
if (!res.headersSent) {
|
|
1291
|
+
res.writeHead(500);
|
|
1292
|
+
res.end("Internal Server Error");
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
const startServer = (port, maxAttempts = 10) => {
|
|
1297
|
+
httpServer.once("error", (err) => {
|
|
1298
|
+
if (err.code === "EADDRINUSE" && port < initialPort + maxAttempts) {
|
|
1299
|
+
console.warn(`Port ${port} is in use, trying port ${port + 1}...`);
|
|
1300
|
+
startServer(port + 1, maxAttempts);
|
|
1301
|
+
} else {
|
|
1302
|
+
console.error(`Failed to start server: ${err.message}`);
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
httpServer.listen(port, () => {
|
|
1307
|
+
actualPort = port;
|
|
1308
|
+
console.error(
|
|
1309
|
+
`Upstash MCP Server running on ${transportType.toUpperCase()} at http://localhost:${actualPort}/mcp with SSE endpoint at /sse`
|
|
1310
|
+
);
|
|
1311
|
+
});
|
|
1312
|
+
};
|
|
1313
|
+
startServer(initialPort);
|
|
1314
|
+
} else {
|
|
1315
|
+
const server = createServerInstance();
|
|
1316
|
+
const transport = new StdioServerTransport();
|
|
1317
|
+
await server.connect(transport);
|
|
1318
|
+
console.error("Upstash MCP Server running on stdio");
|
|
1319
|
+
}
|
|
663
1320
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
console.error(error.message);
|
|
1321
|
+
main().catch((error) => {
|
|
1322
|
+
console.error("Fatal error in main():", error);
|
|
667
1323
|
process.exit(1);
|
|
668
1324
|
});
|
|
1325
|
+
export {
|
|
1326
|
+
DEBUG
|
|
1327
|
+
};
|
|
669
1328
|
//# sourceMappingURL=index.js.map
|