@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/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/init.ts
7
- import path from "path";
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
- process.stderr.write(msg);
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
- const newConfig = {
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
- timestamps_to_date: tool({
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 formatCreationTime = (obj) => {
104
- if (obj && typeof obj === "object" && "creation_time" in obj && typeof obj.creation_time === "number" && obj.creation_time > 0) {
105
- obj.creation_time = new Date(obj.creation_time * 1e3).toLocaleString();
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 creation_time field to human readable format
79
+ // Middleware to format timestamp fields to human readable format
110
80
  async (req, next) => {
111
81
  const res = await next();
112
- if (Array.isArray(res)) {
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 get(path2) {
138
- return this.requestWithMiddleware({ method: "GET", path: path2 });
105
+ async post(path, body, headers) {
106
+ return this.requestWithMiddleware({ method: "POST", path, body, headers });
139
107
  }
140
- async post(path2, body) {
141
- return this.requestWithMiddleware({ method: "POST", path: path2, body });
108
+ async put(path, body, headers) {
109
+ return this.requestWithMiddleware({ method: "PUT", path, body, headers });
142
110
  }
143
- async patch(path2, body) {
144
- return this.requestWithMiddleware({ method: "PATCH", path: path2, body });
111
+ async patch(path, body) {
112
+ return this.requestWithMiddleware({ method: "PATCH", path, body });
145
113
  }
146
- async delete(path2) {
147
- return this.requestWithMiddleware({ method: "DELETE", path: path2 });
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
- const url = [this.baseUrl, ...req.path].join("/");
162
- const token = [config.email, config.apiKey].join(":");
163
- const init2 = {
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: `Basic ${Buffer.from(token).toString("base64")}`
153
+ Authorization: authHeader,
154
+ ...req.headers
168
155
  }
169
156
  };
170
- if (req.method !== "GET") {
171
- init2.body = JSON.stringify(req.body);
157
+ if (req.method !== "GET" && req.body !== void 0) {
158
+ init.body = JSON.stringify(req.body);
172
159
  }
173
- log("Sending request", {
160
+ log("-> sending request", {
174
161
  url,
175
- ...init2,
176
- headers: { ...init2.headers, Authorization: "***" }
162
+ ...init,
163
+ headers: {
164
+ ...init.headers
165
+ // , Authorization: "***"
166
+ }
177
167
  });
178
- const res = await fetch(url, init2);
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
- return await res.json();
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
- redis_database_create_backup: tool({
190
- description: `Create a backup of a specific Upstash redis database.
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 restore a backup to."),
218
- backup_id: z2.string().describe("The ID of the backup to restore.")
219
- }),
220
- handler: async ({ database_id, backup_id }) => {
221
- await http.post(["v2/redis/restore-backup", database_id], {
222
- backup_id
223
- });
224
- return "OK";
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
- redis_database_run_single_redis_command: tool({
260
- description: `Run a single Redis command on a specific Upstash redis database.
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
- database_rest_url: z3.string().describe("The REST URL of the database. Example: https://***.upstash.io"),
265
- database_rest_token: z3.string().describe("The REST token of the database."),
266
- command: z3.array(z3.string()).describe("The Redis command to run. Example: ['SET', 'foo', 'bar', 'EX', 100]")
267
- }),
268
- handler: async ({ database_rest_url, database_rest_token, command }) => {
269
- const req = await fetch2(database_rest_url, {
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: JSON.stringify(command),
330
+ body,
272
331
  headers: {
273
332
  "Content-Type": "application/json",
274
- Authorization: `Bearer ${database_rest_token}`
333
+ Authorization: `Bearer ${restToken}`
275
334
  }
276
335
  });
277
- const result = await req.json();
278
- log("command result:", result);
279
- if ("error" in result) {
280
- throw new Error("Redis error: " + result.error);
281
- }
282
- const isScanCommand = command[0].toLocaleLowerCase().includes("scan");
283
- const messages = [json(result)];
284
- if (isScanCommand)
285
- messages.push(`NOTE: Use the returned cursor to get the next set of keys.
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
- return messages;
288
- }
289
- }),
290
- redis_database_run_multiple_redis_commands: tool({
291
- description: `Run multiple Redis commands on a specific Upstash redis database`,
292
- inputSchema: z3.object({
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
- redis_database_get_usage_last_5_days: tool({
437
- description: `Get PRECISE command count and bandwidth usage statistics of an Upstash redis database over the last 5 days. This is a precise stat, not an average.
438
- NOTE: Ask user first if they want to see stats for each database seperately or just for one.`,
439
- inputSchema: z4.object({
440
- id: z4.string().describe("The ID of your database.")
441
- }),
442
- handler: async ({ id }) => {
443
- const stats = await http.get(["v2/redis/stats", `${id}?period=3h`]);
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 of the stats."),
468
- type: z4.union([
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, type }) => {
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[type];
512
+ const stat = stats[statType];
482
513
  if (!Array.isArray(stat))
483
514
  throw new Error(
484
- `Invalid key provided: ${type}. Valid keys are: ${Object.keys(stats).join(", ")}`
515
+ `Invalid stat_type provided: ${statType}. Valid keys are: ${Object.keys(stats).join(", ")}`
485
516
  );
486
517
  return [
487
- JSON.stringify(parseUsageData(stat)),
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 = 8e3;
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
- function convertToJsonSchema(schema) {
546
- const jsonSchema = zodToJsonSchema(schema);
547
- delete jsonSchema.$schema;
548
- const removeAdditionalProperties = (schema2) => {
549
- if (schema2.type !== "object") return;
550
- delete schema2.additionalProperties;
551
- for (const value of Object.values(schema2.properties)) {
552
- removeAdditionalProperties(value);
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
- removeAdditionalProperties(jsonSchema);
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: convertToJsonSchema(tool2.inputSchema ?? z5.object({}))
1121
+ inputSchema: tool2.inputSchema,
1122
+ tool: tool2
563
1123
  }));
564
- }
565
-
566
- // src/server.ts
567
- var server = new Server(
568
- { name: "upstash", version: "0.1.0" },
569
- { capabilities: { tools: {} } }
570
- );
571
- var toolsList = convertToTools(tools);
572
- server.setRequestHandler(ListToolsRequestSchema, async () => {
573
- log("Received list tools request", toolsList);
574
- return { tools: toolsList };
575
- });
576
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
577
- const toolName = request.params.name;
578
- log("< received tool call:", toolName, request.params.arguments);
579
- try {
580
- if (toolName in tools) {
581
- const tool2 = tools[toolName];
582
- const result = await tool2.handler(request.params.arguments);
583
- const response = handlerResponseToCallResult(result);
584
- log("> tool result:", response.content.map((item) => item.text).join("\n"));
585
- return response;
586
- }
587
- throw new Error(`Unknown tool: ${toolName}`);
588
- } catch (error) {
589
- const msg = error instanceof Error ? error.message : String(error);
590
- log("> error in tool call:", msg);
591
- return {
592
- content: [
593
- {
594
- type: "text",
595
- text: `Error: ${msg}`
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
- isError: true
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
- process.on("uncaughtException", (error) => {
617
- log("Uncaught exception:", error.name, error.message, error.stack);
618
- });
619
- process.on("unhandledRejection", (error) => {
620
- if (error instanceof Error) log("Unhandled rejection:", error.name, error.message, error.stack);
621
- else log("Unhandled rejection:", error);
622
- });
623
- var envApiKey = process.env.UPSTASH_API_KEY;
624
- var envEmail = process.env.UPSTASH_EMAIL;
625
- var USAGE_GENERAL = `Usage: npx @upstash/mcp-server (init|run) <UPSTASH_EMAIL> <UPSTASH_API_KEY>`;
626
- var USAGE_RUN = `Usage: npx @upstash/mcp-server run <UPSTASH_EMAIL> <UPSTASH_API_KEY>`;
627
- var USAGE_INIT = `Usage: npx @upstash/mcp-server init <UPSTASH_EMAIL> <UPSTASH_API_KEY>`;
628
- async function parseArguments() {
629
- const [cmd, ...args] = process.argv.slice(2);
630
- if (!cmd) throw new Error(USAGE_GENERAL);
631
- else if (cmd === "init") {
632
- const [email, apiKey, ...rest] = args;
633
- const finalApiKey = apiKey || envApiKey;
634
- const finalEmail = email || envEmail;
635
- if (rest.length > 0) throw new Error(`Too many arguments. ${USAGE_INIT}`);
636
- if (!finalApiKey) throw new Error(`Missing API key. ${USAGE_INIT}`);
637
- if (!finalEmail) throw new Error(`Missing email. ${USAGE_INIT}`);
638
- config.apiKey = finalApiKey;
639
- config.email = finalEmail;
640
- await testConnection();
641
- await init({
642
- executablePath: process.argv[1]
643
- });
644
- } else if (cmd === "run") {
645
- const [email, apiKey, ...rest] = args;
646
- const finalApiKey = apiKey || envApiKey;
647
- const finalEmail = email || envEmail;
648
- if (!finalApiKey) throw new Error(`Missing API key. ${USAGE_RUN}`);
649
- if (!finalEmail) throw new Error(`Missing email. ${USAGE_RUN}`);
650
- if (rest.length > 0) throw new Error(`Too many arguments. ${USAGE_RUN}`);
651
- log("Starting MCP server");
652
- config.apiKey = finalApiKey;
653
- config.email = finalEmail;
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 transport = new StdioServerTransport();
662
- await server.connect(transport);
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
- parseArguments().catch((error) => {
665
- if (!(error instanceof Error)) throw error;
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