@upstash/mcp-server 0.2.2-rc.1 → 0.2.3-rc.2

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
@@ -9,7 +9,22 @@ import { createServer } from "http";
9
9
  // src/server.ts
10
10
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
11
 
12
+ // src/config.ts
13
+ var config = {
14
+ apiKey: "",
15
+ email: "",
16
+ boxApiKey: "",
17
+ disableTelemetry: false,
18
+ readonly: false
19
+ };
20
+
12
21
  // src/log.ts
22
+ import { appendFileSync } from "fs";
23
+ import path from "path";
24
+ var debugLogPath = null;
25
+ function initDebugLog(projectRoot) {
26
+ debugLogPath = path.resolve(projectRoot, "upstash-debug.log");
27
+ }
13
28
  function log(...args) {
14
29
  const msg = `[DEBUG ${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg)).join(" ")}
15
30
  `;
@@ -17,13 +32,28 @@ function log(...args) {
17
32
  logs.push(msg);
18
33
  }
19
34
  process.stderr.write(msg);
35
+ if (debugLogPath) {
36
+ try {
37
+ appendFileSync(debugLogPath, msg);
38
+ } catch {
39
+ }
40
+ }
20
41
  }
21
42
  var logsStore = /* @__PURE__ */ new Map();
22
43
 
23
44
  // src/tools/utils.ts
24
45
  import { z } from "zod";
46
+
47
+ // src/tools/helpers.ts
48
+ var json = (value) => typeof value === "string" ? value : JSON.stringify(value, null, 2);
49
+ function tool(t) {
50
+ return t;
51
+ }
52
+
53
+ // src/tools/utils.ts
25
54
  var utilTools = {
26
55
  util_timestamps_to_date: tool({
56
+ readonly: true,
27
57
  description: `Use this tool to convert a timestamp to a human-readable date`,
28
58
  inputSchema: z.object({
29
59
  timestamps: z.array(z.number()).describe("Array of timestamps to convert")
@@ -33,6 +63,7 @@ var utilTools = {
33
63
  }
34
64
  }),
35
65
  util_dates_to_timestamps: tool({
66
+ readonly: true,
36
67
  description: `Use this tool to convert an array of ISO 8601 dates to an array of timestamps`,
37
68
  inputSchema: z.object({
38
69
  dates: z.array(z.string()).describe("Array of dates to convert")
@@ -46,12 +77,6 @@ var utilTools = {
46
77
  // src/tools/redis/backup.ts
47
78
  import { z as z2 } from "zod";
48
79
 
49
- // src/config.ts
50
- var config = {
51
- apiKey: "",
52
- email: ""
53
- };
54
-
55
80
  // src/middlewares.ts
56
81
  var formatTimestamps = (obj) => {
57
82
  if (!obj || typeof obj !== "object") {
@@ -91,27 +116,56 @@ var applyMiddlewares = async (req, func) => {
91
116
  return next();
92
117
  };
93
118
 
119
+ // src/version.ts
120
+ var VERSION = "0.2.3-rc.2";
121
+
122
+ // src/telemetry.ts
123
+ function getRuntime() {
124
+ var _a, _b, _c;
125
+ if (globalThis.Bun !== void 0) {
126
+ return `bun@${globalThis.Bun.version}`;
127
+ }
128
+ if (globalThis.Deno !== void 0) {
129
+ const denoVersion = (_b = (_a = globalThis.Deno) == null ? void 0 : _a.version) == null ? void 0 : _b.deno;
130
+ return denoVersion ? `deno@${denoVersion}` : "deno";
131
+ }
132
+ if (typeof process !== "undefined" && ((_c = process.versions) == null ? void 0 : _c.node)) {
133
+ return `node@${process.versions.node}`;
134
+ }
135
+ return "unknown";
136
+ }
137
+ function getPlatform() {
138
+ if (typeof process !== "undefined" && process.platform) {
139
+ return process.platform;
140
+ }
141
+ return "unknown";
142
+ }
143
+ var telemetry = {
144
+ runtime: getRuntime(),
145
+ platform: getPlatform(),
146
+ sdk: `@upstash/mcp-server@${VERSION}`
147
+ };
148
+
94
149
  // src/http.ts
95
- import fetch from "node-fetch";
96
150
  var HttpClient = class {
97
151
  constructor(config2) {
98
152
  this.baseUrl = config2.baseUrl.replace(/\/$/, "");
99
153
  this.qstashToken = config2.qstashToken;
100
154
  }
101
- async get(path, query) {
102
- return this.requestWithMiddleware({ method: "GET", path, query });
155
+ async get(path2, query) {
156
+ return this.requestWithMiddleware({ method: "GET", path: path2, query });
103
157
  }
104
- async post(path, body, headers) {
105
- return this.requestWithMiddleware({ method: "POST", path, body, headers });
158
+ async post(path2, body, headers) {
159
+ return this.requestWithMiddleware({ method: "POST", path: path2, body, headers });
106
160
  }
107
- async put(path, body, headers) {
108
- return this.requestWithMiddleware({ method: "PUT", path, body, headers });
161
+ async put(path2, body, headers) {
162
+ return this.requestWithMiddleware({ method: "PUT", path: path2, body, headers });
109
163
  }
110
- async patch(path, body) {
111
- return this.requestWithMiddleware({ method: "PATCH", path, body });
164
+ async patch(path2, body) {
165
+ return this.requestWithMiddleware({ method: "PATCH", path: path2, body });
112
166
  }
113
- async delete(path, body) {
114
- return this.requestWithMiddleware({ method: "DELETE", path, body });
167
+ async delete(path2, body) {
168
+ return this.requestWithMiddleware({ method: "DELETE", path: path2, body });
115
169
  }
116
170
  async requestWithMiddleware(req) {
117
171
  const res = await applyMiddlewares(req, async (req2) => {
@@ -145,11 +199,17 @@ var HttpClient = class {
145
199
  const token = [config.email, config.apiKey].join(":");
146
200
  authHeader = `Basic ${Buffer.from(token).toString("base64")}`;
147
201
  }
202
+ const telemetryHeaders = config.disableTelemetry ? {} : {
203
+ "Upstash-Telemetry-Runtime": telemetry.runtime,
204
+ "Upstash-Telemetry-Platform": telemetry.platform,
205
+ "Upstash-Telemetry-Sdk": telemetry.sdk
206
+ };
148
207
  const init = {
149
208
  method: req.method,
150
209
  headers: {
151
210
  "Content-Type": "application/json",
152
211
  Authorization: authHeader,
212
+ ...telemetryHeaders,
153
213
  ...req.headers
154
214
  }
155
215
  };
@@ -240,6 +300,7 @@ var redisBackupTools = {
240
300
  }
241
301
  }),
242
302
  redis_database_list_backups: tool({
303
+ readonly: true,
243
304
  // TODO: Add explanation for fields
244
305
  // TODO: Is this in bytes?
245
306
  description: `List all backups of a specific Upstash redis database.`,
@@ -269,9 +330,9 @@ var redisBackupTools = {
269
330
 
270
331
  // src/tools/redis/command.ts
271
332
  import { z as z3 } from "zod";
272
- import fetch2 from "node-fetch";
273
333
  var redisCommandTools = {
274
334
  redis_database_run_redis_commands: tool({
335
+ readonly: true,
275
336
  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.
276
337
  NOTE: For discovery, use SCAN over KEYS. Use TYPE to get the type of a key.
277
338
  NOTE: SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
@@ -300,7 +361,7 @@ NOTE: Multiple commands will be executed as a pipeline for better performance.`,
300
361
  log("Fetching database details for database_id:", database_id);
301
362
  const db = await http.get(["v2/redis/database", database_id]);
302
363
  restUrl = "https://" + db.endpoint;
303
- restToken = db.rest_token;
364
+ restToken = config.readonly ? db.read_only_rest_token : db.rest_token;
304
365
  }
305
366
  if (!restUrl || !restToken) {
306
367
  throw new Error("Could not determine REST URL and token for the database");
@@ -308,7 +369,7 @@ NOTE: Multiple commands will be executed as a pipeline for better performance.`,
308
369
  const isSingleCommand = commands.length === 1;
309
370
  const url = isSingleCommand ? restUrl : restUrl + "/pipeline";
310
371
  const body = isSingleCommand ? JSON.stringify(commands[0]) : JSON.stringify(commands);
311
- const req = await fetch2(url, {
372
+ const req = await fetch(url, {
312
373
  method: "POST",
313
374
  body,
314
375
  headers: {
@@ -393,6 +454,7 @@ NOTE: Ask user for the region and name of the database.${GENERIC_DATABASE_NOTES}
393
454
  }
394
455
  }),
395
456
  redis_database_list_databases: tool({
457
+ readonly: true,
396
458
  description: `List all Upstash redis databases. Only their names and ids.${GENERIC_DATABASE_NOTES}`,
397
459
  handler: async () => {
398
460
  const dbs = await http.get("v2/redis/databases");
@@ -419,6 +481,7 @@ NOTE: Ask user for the region and name of the database.${GENERIC_DATABASE_NOTES}
419
481
  }
420
482
  }),
421
483
  redis_database_get_details: tool({
484
+ readonly: true,
422
485
  description: `Get further details of a specific Upstash redis database. Includes all details of the database including usage statistics.
423
486
  db_disk_threshold: Total disk usage limit.
424
487
  db_memory_threshold: Maximum memory usage.
@@ -461,6 +524,7 @@ ${GENERIC_DATABASE_NOTES}
461
524
  }
462
525
  }),
463
526
  redis_database_get_statistics: tool({
527
+ readonly: true,
464
528
  description: `Get comprehensive usage statistics of an Upstash redis database. Returns both:
465
529
  1. PRECISE 5-day usage: Exact command count and bandwidth usage over the last 5 days
466
530
  2. SAMPLED period stats: Sampled statistics over a specified period (1h, 3h, 12h, 1d, 3d, 7d) for performance monitoring
@@ -525,6 +589,7 @@ var parseUsageData = (data) => {
525
589
  if (!Array.isArray(data)) return "INVALID DATA";
526
590
  if (data.length === 0 || data.length === 1) return "NO DATA";
527
591
  const filteredData = data.filter((d) => d.x && d.y);
592
+ if (filteredData.length === 0) return "NO DATA";
528
593
  return {
529
594
  start: filteredData[0].x,
530
595
  // last one can be null, so use the second last
@@ -543,57 +608,81 @@ var redisTools = {
543
608
  };
544
609
 
545
610
  // src/tools/qstash/qstash.ts
546
- import { z as z5 } from "zod";
611
+ import { z as z6 } from "zod";
547
612
 
548
613
  // src/tools/qstash/utils.ts
549
- var cachedToken = null;
550
- var tokenExpiry = 0;
551
- async function getQStashToken() {
614
+ var LOCAL_QSTASH_TOKEN = "eyJVc2VySUQiOiJkZWZhdWx0VXNlciIsIlBhc3N3b3JkIjoiZGVmYXVsdFBhc3N3b3JkIn0=";
615
+ var REGION_URLS = {
616
+ eu: "https://qstash-eu-central-1.upstash.io",
617
+ us: "https://qstash-us-east-1.upstash.io"
618
+ };
619
+ var REGION_API_NAMES = {
620
+ eu: "eu-central-1",
621
+ us: "us-east-1"
622
+ };
623
+ var cachedUsers = null;
624
+ var cacheExpiry = 0;
625
+ async function getQStashCredentials(region) {
552
626
  const now = Date.now();
553
- if (cachedToken && now < tokenExpiry) {
554
- return cachedToken;
627
+ if (!cachedUsers || now >= cacheExpiry) {
628
+ try {
629
+ cachedUsers = await http.get("v2/qstash/users");
630
+ cacheExpiry = now + 60 * 1e3;
631
+ } catch (error) {
632
+ throw new Error(
633
+ `Failed to get QStash credentials: ${error instanceof Error ? error.message : String(error)}`
634
+ );
635
+ }
555
636
  }
556
- try {
557
- const user = await http.get("qstash/user");
558
- cachedToken = user.token;
559
- tokenExpiry = now + 60 * 60 * 1e3;
560
- return user.token;
561
- } catch (error) {
637
+ const apiRegion = REGION_API_NAMES[region] ?? REGION_API_NAMES["eu"];
638
+ const match = cachedUsers.find((u) => u.region === apiRegion);
639
+ if (!match) {
640
+ throw new Error(`No QStash user found for region '${region}' (${apiRegion})`);
641
+ }
642
+ return { token: match.token };
643
+ }
644
+ async function createQStashClientWithToken(options) {
645
+ if (config.readonly) {
562
646
  throw new Error(
563
- `Failed to get QStash token: ${error instanceof Error ? error.message : String(error)}`
647
+ "QStash is not available in readonly mode yet. This feature will be implemented in the near future."
564
648
  );
565
649
  }
566
- }
567
- async function createQStashClientWithToken(creds = {}) {
568
- if (!(creds == null ? void 0 : creds.token)) {
569
- creds.token = await getQStashToken();
650
+ const { qstash_creds, region, local_mode_port } = options;
651
+ if (qstash_creds) {
652
+ return createQStashClient({ url: qstash_creds.url, token: qstash_creds.token });
570
653
  }
571
- return createQStashClient({
572
- url: creds == null ? void 0 : creds.url,
573
- token: creds == null ? void 0 : creds.token
574
- });
654
+ if (region === "local") {
655
+ return createQStashClient({
656
+ url: `http://localhost:${local_mode_port}`,
657
+ token: LOCAL_QSTASH_TOKEN
658
+ });
659
+ }
660
+ const effectiveRegion = region && REGION_URLS[region] ? region : "eu";
661
+ const fetched = await getQStashCredentials(effectiveRegion);
662
+ return createQStashClient({ url: REGION_URLS[effectiveRegion], token: fetched.token });
575
663
  }
576
664
 
577
- // src/tools/qstash/qstash.ts
578
- var qstashCreds = {
579
- qstash_creds: z5.undefined()
580
- // qstash_creds: z
581
- // .object({
582
- // url: z.string(),
583
- // token: z.string(),
584
- // })
585
- // .optional()
586
- // .describe(
587
- // "Optional qstash credentials. Use for local qstash connections and external qstash deployments"
588
- // ),
665
+ // src/tools/qstash/common.ts
666
+ import { z as z5 } from "zod";
667
+ var qstashCommon = {
668
+ region: z5.enum(["eu", "us", "local"]).default("eu").describe("QStash region to use. To use local mode, pick `local`"),
669
+ local_mode_port: z5.number().default(8080).describe(
670
+ "Only provide when using local mode and if default does not work, the port is usually in the .env file"
671
+ ),
672
+ qstash_creds: z5.object({
673
+ url: z5.string(),
674
+ token: z5.string()
675
+ }).optional().describe("Custom qstash credentials, overrides `region`")
589
676
  };
677
+
678
+ // src/tools/qstash/qstash.ts
590
679
  var qstashTools = {
591
680
  qstash_get_user_token: tool({
592
681
  description: `Get the QSTASH_TOKEN and QSTASH_URL of the current user. This
593
682
  is not needed for the mcp tools since the token is automatically fetched from
594
683
  the Upstash API for them.`,
595
684
  handler: async () => {
596
- const user = await http.get("qstash/user");
685
+ const user = await http.get("v2/qstash/user");
597
686
  return [json(user)];
598
687
  }
599
688
  }),
@@ -601,42 +690,42 @@ var qstashTools = {
601
690
  description: `Publish a message to a destination URL using QStash. This
602
691
  sends an HTTP request to the specified destination via QStash's message
603
692
  queue. This can also be used to trigger a upstash workflow run.`,
604
- inputSchema: z5.object({
605
- destination: z5.string().describe("The destination URL to send the message to (e.g., 'https://example.com')"),
606
- body: z5.string().optional().describe("Request body (JSON string or plain text)"),
607
- method: z5.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().describe("HTTP method (optional, defaults to POST)").default("POST"),
608
- delay: z5.string().optional().describe("Delay before message delivery (e.g., '10s', '5m', '1h')"),
609
- retries: z5.number().optional().describe("Number of retries on failure, default is 3"),
610
- callback: z5.string().optional().describe("Callback URL that will be called when the message is successfully delivered"),
611
- failureCallback: z5.string().optional().describe("Callback URL that will be called when the message is failed to deliver"),
612
- timeout: z5.string().optional().describe("Request timeout (e.g., '30s', '1h')"),
613
- queueName: z5.string().optional().describe(
693
+ inputSchema: z6.object({
694
+ destination: z6.string().describe("The destination URL to send the message to (e.g., 'https://example.com')"),
695
+ body: z6.string().optional().describe("Request body (JSON string or plain text)"),
696
+ method: z6.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().describe("HTTP method (optional, defaults to POST)").default("POST"),
697
+ delay: z6.string().optional().describe("Delay before message delivery (e.g., '10s', '5m', '1h')"),
698
+ retries: z6.number().optional().describe("Number of retries on failure, default is 3"),
699
+ callback: z6.string().optional().describe("Callback URL that will be called when the message is successfully delivered"),
700
+ failureCallback: z6.string().optional().describe("Callback URL that will be called when the message is failed to deliver"),
701
+ timeout: z6.string().optional().describe("Request timeout (e.g., '30s', '1h')"),
702
+ queueName: z6.string().optional().describe(
614
703
  "Queue name to use, you have to first create the queue in upstash. Prefer the flow control key instead"
615
704
  ),
616
- flow_control: z5.object({
617
- key: z5.string().describe("Unique identifier for grouping messages under same flow control rules"),
618
- parallelism: z5.number().optional().describe("Max concurrent active calls (default: unlimited)"),
619
- rate: z5.number().optional().describe("Max calls per period (default: unlimited)"),
620
- period: z5.string().optional().describe("Time window for rate limit (e.g., '1s', '1m', '1h', default: '1s')")
705
+ flow_control: z6.object({
706
+ key: z6.string().describe("Unique identifier for grouping messages under same flow control rules"),
707
+ parallelism: z6.number().optional().describe("Max concurrent active calls (default: unlimited)"),
708
+ rate: z6.number().optional().describe("Max calls per period (default: unlimited)"),
709
+ period: z6.string().optional().describe("Time window for rate limit (e.g., '1s', '1m', '1h', default: '1s')")
621
710
  }).optional().describe("Flow control for rate limiting and parallelism management"),
622
- extraHeaders: z5.record(z5.string()).optional().describe("Extra headers to add to the request"),
623
- ...qstashCreds
711
+ extraHeaders: z6.record(z6.string()).optional().describe("Extra headers to add to the request"),
712
+ ...qstashCommon
624
713
  }),
625
- handler: async ({
626
- destination,
627
- body,
628
- method,
629
- extraHeaders,
630
- delay,
631
- retries,
632
- callback,
633
- failureCallback,
634
- timeout,
635
- queueName,
636
- flow_control,
637
- qstash_creds
638
- }) => {
639
- const client = await createQStashClientWithToken(qstash_creds);
714
+ handler: async (params) => {
715
+ const {
716
+ destination,
717
+ body,
718
+ method,
719
+ extraHeaders,
720
+ delay,
721
+ retries,
722
+ callback,
723
+ failureCallback,
724
+ timeout,
725
+ queueName,
726
+ flow_control
727
+ } = params;
728
+ const client = await createQStashClientWithToken(params);
640
729
  const requestHeaders = {};
641
730
  if (method) {
642
731
  requestHeaders["Upstash-Method"] = method;
@@ -687,10 +776,10 @@ var qstashTools = {
687
776
  }),
688
777
  qstash_logs_list: tool({
689
778
  description: `List QStash logs with optional filtering. Returns a paginated list of message logs without their bodies.`,
690
- inputSchema: z5.object({
691
- cursor: z5.string().optional().describe("Cursor for pagination"),
692
- messageId: z5.string().optional().describe("Filter logs by message ID"),
693
- state: z5.enum([
779
+ inputSchema: z6.object({
780
+ cursor: z6.string().optional().describe("Cursor for pagination"),
781
+ messageId: z6.string().optional().describe("Filter logs by message ID"),
782
+ state: z6.enum([
694
783
  "CREATED",
695
784
  "ACTIVE",
696
785
  "RETRY",
@@ -701,21 +790,27 @@ var qstashTools = {
701
790
  "CANCEL_REQUESTED",
702
791
  "CANCELLED"
703
792
  ]).optional().describe("Filter logs by state"),
704
- url: z5.string().optional().describe("Filter logs by URL"),
705
- topicName: z5.string().optional().describe("Filter logs by topic name"),
706
- scheduleId: z5.string().optional().describe("Filter logs by schedule ID"),
707
- queueName: z5.string().optional().describe("Filter logs by queue name"),
708
- fromDate: z5.number().optional().describe("Filter logs from date (Unix timestamp in milliseconds)"),
709
- toDate: z5.number().optional().describe("Filter logs to date (Unix timestamp in milliseconds)"),
710
- count: z5.number().max(1e3).optional().describe("Number of logs to return (max 1000)"),
711
- ...qstashCreds
793
+ url: z6.string().optional().describe("Filter logs by URL"),
794
+ topicName: z6.string().optional().describe("Filter logs by topic name"),
795
+ scheduleId: z6.string().optional().describe("Filter logs by schedule ID"),
796
+ queueName: z6.string().optional().describe("Filter logs by queue name"),
797
+ fromDate: z6.number().optional().describe("Filter logs from date (Unix timestamp in milliseconds)"),
798
+ toDate: z6.number().optional().describe("Filter logs to date (Unix timestamp in milliseconds)"),
799
+ count: z6.number().max(1e3).optional().describe("Number of logs to return (max 1000)"),
800
+ ...qstashCommon
712
801
  }),
713
802
  handler: async (params) => {
714
- const client = await createQStashClientWithToken(params.qstash_creds);
803
+ const {
804
+ region: _region,
805
+ local_mode_port: _local_mode_port,
806
+ qstash_creds: _qstash_creds,
807
+ ...query
808
+ } = params;
809
+ const client = await createQStashClientWithToken(params);
715
810
  const response = await client.get("v2/logs", {
716
811
  trimBody: 0,
717
812
  groupBy: "messageId",
718
- ...params
813
+ ...query
719
814
  });
720
815
  const firstMessageFields = Object.fromEntries(
721
816
  Object.entries(response.messages[0] ?? {}).filter(
@@ -738,12 +833,13 @@ var qstashTools = {
738
833
  }),
739
834
  qstash_logs_get: tool({
740
835
  description: `Get details of a single QStash log item by message ID without trimming the body.`,
741
- inputSchema: z5.object({
742
- messageId: z5.string().describe("The message ID to get details for"),
743
- ...qstashCreds
836
+ inputSchema: z6.object({
837
+ messageId: z6.string().describe("The message ID to get details for"),
838
+ ...qstashCommon
744
839
  }),
745
- handler: async ({ messageId, qstash_creds }) => {
746
- const client = await createQStashClientWithToken(qstash_creds);
840
+ handler: async (params) => {
841
+ const { messageId } = params;
842
+ const client = await createQStashClientWithToken(params);
747
843
  const response = await client.get("v2/logs", { messageId });
748
844
  if (response.messages.length === 0) {
749
845
  return "No log entry found for the specified message ID";
@@ -754,25 +850,31 @@ var qstashTools = {
754
850
  }),
755
851
  qstash_dlq_list: tool({
756
852
  description: `List messages in the QStash Dead Letter Queue (DLQ) with optional filtering.`,
757
- inputSchema: z5.object({
758
- cursor: z5.string().optional().describe("Cursor for pagination"),
759
- messageId: z5.string().optional().describe("Filter DLQ messages by message ID"),
760
- url: z5.string().optional().describe("Filter DLQ messages by URL"),
761
- topicName: z5.string().optional().describe("Filter DLQ messages by topic name"),
762
- scheduleId: z5.string().optional().describe("Filter DLQ messages by schedule ID"),
763
- queueName: z5.string().optional().describe("Filter DLQ messages by queue name"),
764
- fromDate: z5.number().optional().describe("Filter from date (Unix timestamp in milliseconds)"),
765
- toDate: z5.number().optional().describe("Filter to date (Unix timestamp in milliseconds)"),
766
- responseStatus: z5.number().optional().describe("Filter by HTTP response status code"),
767
- callerIp: z5.string().optional().describe("Filter by IP address of the publisher"),
768
- count: z5.number().max(100).optional().describe("Number of messages to return (max 100)"),
769
- ...qstashCreds
853
+ inputSchema: z6.object({
854
+ cursor: z6.string().optional().describe("Cursor for pagination"),
855
+ messageId: z6.string().optional().describe("Filter DLQ messages by message ID"),
856
+ url: z6.string().optional().describe("Filter DLQ messages by URL"),
857
+ topicName: z6.string().optional().describe("Filter DLQ messages by topic name"),
858
+ scheduleId: z6.string().optional().describe("Filter DLQ messages by schedule ID"),
859
+ queueName: z6.string().optional().describe("Filter DLQ messages by queue name"),
860
+ fromDate: z6.number().optional().describe("Filter from date (Unix timestamp in milliseconds)"),
861
+ toDate: z6.number().optional().describe("Filter to date (Unix timestamp in milliseconds)"),
862
+ responseStatus: z6.number().optional().describe("Filter by HTTP response status code"),
863
+ callerIp: z6.string().optional().describe("Filter by IP address of the publisher"),
864
+ count: z6.number().max(100).optional().describe("Number of messages to return (max 100)"),
865
+ ...qstashCommon
770
866
  }),
771
867
  handler: async (params) => {
772
- const client = await createQStashClientWithToken(params.qstash_creds);
868
+ const {
869
+ region: _region,
870
+ local_mode_port: _local_mode_port,
871
+ qstash_creds: _qstash_creds,
872
+ ...query
873
+ } = params;
874
+ const client = await createQStashClientWithToken(params);
773
875
  const response = await client.get("v2/dlq", {
774
876
  trimBody: 0,
775
- ...params
877
+ ...query
776
878
  });
777
879
  return [
778
880
  `Found ${response.messages.length} DLQ messages`,
@@ -783,59 +885,63 @@ var qstashTools = {
783
885
  }),
784
886
  qstash_dlq_get: tool({
785
887
  description: `Get details of a single DLQ message by DLQ ID.`,
786
- inputSchema: z5.object({
787
- dlqId: z5.string().describe("The DLQ ID of the message to retrieve"),
788
- ...qstashCreds
888
+ inputSchema: z6.object({
889
+ dlqId: z6.string().describe("The DLQ ID of the message to retrieve"),
890
+ ...qstashCommon
789
891
  }),
790
- handler: async ({ dlqId, qstash_creds }) => {
791
- const client = await createQStashClientWithToken(qstash_creds);
892
+ handler: async (params) => {
893
+ const { dlqId } = params;
894
+ const client = await createQStashClientWithToken(params);
792
895
  const message = await client.get(`v2/dlq/${dlqId}`);
793
896
  return [`DLQ message details for ID: ${dlqId}`, json(message)];
794
897
  }
795
898
  }),
796
899
  qstash_schedules_list: tool({
797
900
  description: `List all QStash schedules.`,
798
- handler: async ({ qstash_creds }) => {
799
- const client = await createQStashClientWithToken(qstash_creds);
901
+ inputSchema: z6.object({
902
+ ...qstashCommon
903
+ }),
904
+ handler: async (params) => {
905
+ const client = await createQStashClientWithToken(params);
800
906
  const schedules = await client.get("v2/schedules");
801
907
  return [`Found ${schedules.length} schedules`, json(schedules)];
802
908
  }
803
909
  }),
804
910
  qstash_schedules_manage: tool({
805
911
  description: `Create, update, or manage QStash schedules. This tool handles create, update (by providing scheduleId), pause, resume, and delete operations in one unified interface.`,
806
- inputSchema: z5.object({
807
- operation: z5.enum(["create", "update", "pause", "resume", "delete"]).describe("The operation to perform"),
808
- scheduleId: z5.string().optional().describe("Schedule ID (required for update, pause, resume, delete operations)"),
809
- destination: z5.string().optional().describe("Destination URL or topic name (required for create/update)"),
810
- cron: z5.string().optional().describe("Cron expression (required for create/update)"),
811
- method: z5.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().describe("HTTP method (optional, defaults to POST)"),
812
- headers: z5.record(z5.string()).optional().describe("Request headers as key-value pairs"),
813
- body: z5.string().optional().describe("Request body"),
814
- delay: z5.string().optional().describe("Delay before message delivery (e.g., '10s', '5m', '1h')"),
815
- retries: z5.number().optional().describe("Number of retries on failure"),
816
- callback: z5.string().optional().describe("Callback URL for successful delivery"),
817
- failureCallback: z5.string().optional().describe("Callback URL for failed delivery"),
818
- timeout: z5.string().optional().describe("Request timeout (e.g., '30s')"),
819
- queueName: z5.string().optional().describe("Queue name to use"),
820
- ...qstashCreds
912
+ inputSchema: z6.object({
913
+ operation: z6.enum(["create", "update", "pause", "resume", "delete"]).describe("The operation to perform"),
914
+ scheduleId: z6.string().optional().describe("Schedule ID (required for update, pause, resume, delete operations)"),
915
+ destination: z6.string().optional().describe("Destination URL or topic name (required for create/update)"),
916
+ cron: z6.string().optional().describe("Cron expression (required for create/update)"),
917
+ method: z6.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().describe("HTTP method (optional, defaults to POST)"),
918
+ headers: z6.record(z6.string()).optional().describe("Request headers as key-value pairs"),
919
+ body: z6.string().optional().describe("Request body"),
920
+ delay: z6.string().optional().describe("Delay before message delivery (e.g., '10s', '5m', '1h')"),
921
+ retries: z6.number().optional().describe("Number of retries on failure"),
922
+ callback: z6.string().optional().describe("Callback URL for successful delivery"),
923
+ failureCallback: z6.string().optional().describe("Callback URL for failed delivery"),
924
+ timeout: z6.string().optional().describe("Request timeout (e.g., '30s')"),
925
+ queueName: z6.string().optional().describe("Queue name to use"),
926
+ ...qstashCommon
821
927
  }),
822
- handler: async ({
823
- operation,
824
- scheduleId,
825
- destination,
826
- cron,
827
- method = "POST",
828
- headers,
829
- body,
830
- delay,
831
- retries,
832
- callback,
833
- failureCallback,
834
- timeout,
835
- queueName,
836
- qstash_creds
837
- }) => {
838
- const client = await createQStashClientWithToken(qstash_creds);
928
+ handler: async (params) => {
929
+ const {
930
+ operation,
931
+ scheduleId,
932
+ destination,
933
+ cron,
934
+ method = "POST",
935
+ headers,
936
+ body,
937
+ delay,
938
+ retries,
939
+ callback,
940
+ failureCallback,
941
+ timeout,
942
+ queueName
943
+ } = params;
944
+ const client = await createQStashClientWithToken(params);
839
945
  switch (operation) {
840
946
  case "create":
841
947
  case "update": {
@@ -915,25 +1021,31 @@ var qstashTools = {
915
1021
  };
916
1022
 
917
1023
  // src/tools/qstash/workflow.ts
918
- import { z as z6 } from "zod";
1024
+ import { z as z7 } from "zod";
919
1025
  var workflowTools = {
920
1026
  workflow_logs_list: tool({
921
1027
  description: `List Upstash Workflow logs with optional filtering. Returns grouped workflow runs with their execution details.`,
922
- inputSchema: z6.object({
923
- cursor: z6.string().optional().describe("Cursor for pagination"),
924
- workflowRunId: z6.string().optional().describe("Filter by specific workflow run ID"),
925
- count: z6.number().optional().describe("Number of workflow runs to return"),
926
- state: z6.enum(["RUN_STARTED", "RUN_SUCCESS", "RUN_FAILED", "RUN_CANCELED"]).optional().describe("Filter by workflow state"),
927
- workflowUrl: z6.string().optional().describe("Filter by workflow URL (exact match)"),
928
- workflowCreatedAt: z6.number().optional().describe("Filter by workflow creation timestamp (Unix timestamp)"),
929
- ...qstashCreds
1028
+ inputSchema: z7.object({
1029
+ cursor: z7.string().optional().describe("Cursor for pagination"),
1030
+ workflowRunId: z7.string().optional().describe("Filter by specific workflow run ID"),
1031
+ count: z7.number().optional().describe("Number of workflow runs to return"),
1032
+ state: z7.enum(["RUN_STARTED", "RUN_SUCCESS", "RUN_FAILED", "RUN_CANCELED"]).optional().describe("Filter by workflow state"),
1033
+ workflowUrl: z7.string().optional().describe("Filter by workflow URL (exact match)"),
1034
+ workflowCreatedAt: z7.number().optional().describe("Filter by workflow creation timestamp (Unix timestamp)"),
1035
+ ...qstashCommon
930
1036
  }),
931
1037
  handler: async (params) => {
932
- const client = await createQStashClientWithToken(params.qstash_creds);
1038
+ const {
1039
+ region: _region,
1040
+ local_mode_port: _local_mode_port,
1041
+ qstash_creds: _qstash_creds,
1042
+ ...query
1043
+ } = params;
1044
+ const client = await createQStashClientWithToken(params);
933
1045
  const response = await client.get("v2/workflows/events", {
934
1046
  trimBody: 0,
935
1047
  groupBy: "workflowRunId",
936
- ...params
1048
+ ...query
937
1049
  });
938
1050
  const cleaned = response.runs.map(
939
1051
  (run) => Object.fromEntries(Object.entries(run).filter(([key, _value]) => key !== "steps"))
@@ -952,16 +1064,20 @@ var workflowTools = {
952
1064
  description: `Get details of a single workflow run by workflow run ID. There
953
1065
  could be multiple workflow runs with the same workflow run ID, so you can
954
1066
  use the workflowCreatedAt to get the details of the specific workflow run.`,
955
- inputSchema: z6.object({
956
- workflowRunId: z6.string().describe("The workflow run ID to get details for"),
957
- workflowCreatedAt: z6.number().optional().describe("The workflow creation timestamp (Unix timestamp)"),
958
- ...qstashCreds
1067
+ inputSchema: z7.object({
1068
+ workflowRunId: z7.string().describe("The workflow run ID to get details for"),
1069
+ workflowCreatedAt: z7.number().optional().describe("The workflow creation timestamp (Unix timestamp)"),
1070
+ ...qstashCommon
959
1071
  }),
960
1072
  handler: async (params) => {
961
- const client = await createQStashClientWithToken(params.qstash_creds);
962
- const response = await client.get("v2/workflows/logs", {
963
- ...params
964
- });
1073
+ const {
1074
+ region: _region,
1075
+ local_mode_port: _local_mode_port,
1076
+ qstash_creds: _qstash_creds,
1077
+ ...query
1078
+ } = params;
1079
+ const client = await createQStashClientWithToken(params);
1080
+ const response = await client.get("v2/workflows/logs", query);
965
1081
  if (response.runs.length === 0) {
966
1082
  return "No workflow run found";
967
1083
  }
@@ -974,22 +1090,28 @@ var workflowTools = {
974
1090
  }),
975
1091
  workflow_dlq_list: tool({
976
1092
  description: `List failed workflow runs in the Dead Letter Queue (DLQ) with optional filtering.`,
977
- inputSchema: z6.object({
978
- cursor: z6.string().optional().describe("Cursor for pagination"),
979
- workflowRunId: z6.string().optional().describe("Filter by workflow run ID"),
980
- workflowUrl: z6.string().optional().describe("Filter by workflow URL"),
981
- fromDate: z6.number().optional().describe("Filter from date (Unix timestamp in milliseconds)"),
982
- toDate: z6.number().optional().describe("Filter to date (Unix timestamp in milliseconds)"),
983
- responseStatus: z6.number().optional().describe("Filter by HTTP response status code"),
984
- callerIP: z6.string().optional().describe("Filter by IP address of the caller"),
985
- failureCallbackState: z6.string().optional().describe("Filter by failure callback state"),
986
- count: z6.number().optional().describe("Number of DLQ messages to return"),
987
- ...qstashCreds
1093
+ inputSchema: z7.object({
1094
+ cursor: z7.string().optional().describe("Cursor for pagination"),
1095
+ workflowRunId: z7.string().optional().describe("Filter by workflow run ID"),
1096
+ workflowUrl: z7.string().optional().describe("Filter by workflow URL"),
1097
+ fromDate: z7.number().optional().describe("Filter from date (Unix timestamp in milliseconds)"),
1098
+ toDate: z7.number().optional().describe("Filter to date (Unix timestamp in milliseconds)"),
1099
+ responseStatus: z7.number().optional().describe("Filter by HTTP response status code"),
1100
+ callerIP: z7.string().optional().describe("Filter by IP address of the caller"),
1101
+ failureCallbackState: z7.string().optional().describe("Filter by failure callback state"),
1102
+ count: z7.number().optional().describe("Number of DLQ messages to return"),
1103
+ ...qstashCommon
988
1104
  }),
989
1105
  handler: async (params) => {
990
- const client = await createQStashClientWithToken(params.qstash_creds);
1106
+ const {
1107
+ region: _region,
1108
+ local_mode_port: _local_mode_port,
1109
+ qstash_creds: _qstash_creds,
1110
+ ...query
1111
+ } = params;
1112
+ const client = await createQStashClientWithToken(params);
991
1113
  const response = await client.get("v2/workflows/dlq", {
992
- ...params,
1114
+ ...query,
993
1115
  trimBody: 0
994
1116
  });
995
1117
  const cleaned = response.messages.map(
@@ -1007,27 +1129,28 @@ var workflowTools = {
1007
1129
  }),
1008
1130
  workflow_dlq_get: tool({
1009
1131
  description: `Get details of a single failed workflow run from the DLQ by DLQ ID.`,
1010
- inputSchema: z6.object({
1011
- dlqId: z6.string().describe("The DLQ ID of the failed workflow run to retrieve"),
1012
- ...qstashCreds
1132
+ inputSchema: z7.object({
1133
+ dlqId: z7.string().describe("The DLQ ID of the failed workflow run to retrieve"),
1134
+ ...qstashCommon
1013
1135
  }),
1014
1136
  handler: async (params) => {
1015
- const client = await createQStashClientWithToken(params.qstash_creds);
1137
+ const client = await createQStashClientWithToken(params);
1016
1138
  const message = await client.get(`v2/workflows/dlq/${params.dlqId}`);
1017
1139
  return [`Failed workflow run details for DLQ ID: ${params.dlqId}`, json(message)];
1018
1140
  }
1019
1141
  }),
1020
1142
  workflow_dlq_manage: tool({
1021
1143
  description: `Delete, restart, and resume failed workflow runs in the DLQ using only the DLQ ID.`,
1022
- inputSchema: z6.object({
1023
- dlqId: z6.string().describe("The DLQ ID of the failed workflow run"),
1024
- action: z6.enum(["delete", "restart", "resume"]).describe(
1144
+ inputSchema: z7.object({
1145
+ dlqId: z7.string().describe("The DLQ ID of the failed workflow run"),
1146
+ action: z7.enum(["delete", "restart", "resume"]).describe(
1025
1147
  "The action to perform: delete (remove from DLQ), restart (from beginning), or resume (from the failed step)"
1026
1148
  ),
1027
- ...qstashCreds
1149
+ ...qstashCommon
1028
1150
  }),
1029
- handler: async ({ dlqId, action, qstash_creds }) => {
1030
- const client = await createQStashClientWithToken(qstash_creds);
1151
+ handler: async (params) => {
1152
+ const { dlqId, action } = params;
1153
+ const client = await createQStashClientWithToken(params);
1031
1154
  switch (action) {
1032
1155
  case "delete": {
1033
1156
  await client.delete(`v2/workflows/dlq/delete/${dlqId}`);
@@ -1060,15 +1183,425 @@ var qstashAllTools = {
1060
1183
  ...workflowTools
1061
1184
  };
1062
1185
 
1186
+ // src/tools/box/manage.ts
1187
+ import { z as z9 } from "zod";
1188
+
1189
+ // src/tools/box/common.ts
1190
+ import { z as z8 } from "zod";
1191
+ var BOX_BASE_URL = "https://us-east-1.box.upstash.com";
1192
+ function buildBoxCommon() {
1193
+ const hasConfigKey = Boolean(config.boxApiKey);
1194
+ return {
1195
+ box_api_key: hasConfigKey ? z8.string().optional().describe(
1196
+ "NOTE: The api key is already pre-configured at server startup; only pass this to override the configured key."
1197
+ ) : z8.string().describe(
1198
+ "Box API key (starts with 'box_'). Check the project's .env file for BOX_API_KEY, or ask the user for it."
1199
+ )
1200
+ };
1201
+ }
1202
+
1203
+ // src/tools/box/utils.ts
1204
+ function getBoxClient(params) {
1205
+ const apiKey = params.box_api_key || config.boxApiKey;
1206
+ if (!apiKey) {
1207
+ throw new Error(
1208
+ "No Box API key available. Pass box_api_key as a tool argument, or configure the server with --box-api-key / UPSTASH_BOX_API_KEY env var."
1209
+ );
1210
+ }
1211
+ return new HttpClient({
1212
+ baseUrl: BOX_BASE_URL,
1213
+ qstashToken: apiKey
1214
+ });
1215
+ }
1216
+
1217
+ // src/tools/box/manage.ts
1218
+ var boxManageTool = {
1219
+ box_manage: tool({
1220
+ description: `Manage Upstash Box containers. Supports creating, listing, getting, deleting, pausing, resuming, and forking boxes. Boxes are secure cloud containers with built-in AI agent capabilities.`,
1221
+ get inputSchema() {
1222
+ return z9.object({
1223
+ action: z9.enum(["create", "list", "get", "delete", "pause", "resume", "fork"]).describe("The action to perform"),
1224
+ box_id: z9.string().optional().describe("Box ID (required for get, delete, pause, resume, fork)"),
1225
+ // Create-specific fields
1226
+ name: z9.string().optional().describe("Display name for the box"),
1227
+ model: z9.string().optional().describe(
1228
+ "LLM model to use (e.g. 'claude/sonnet_4_6', 'openai/o4-mini'). Required for create"
1229
+ ),
1230
+ agent: z9.enum(["claude-code", "codex", "opencode"]).optional().default("claude-code").describe("Agent type (default: claude-code)"),
1231
+ runtime: z9.string().optional().default("node").describe("Runtime environment (e.g. 'node', 'python')"),
1232
+ agent_api_key: z9.string().optional().describe("API key for the AI agent provider. Empty uses managed key"),
1233
+ env_vars: z9.record(z9.string()).optional().describe("Environment variables to set in the box"),
1234
+ clone_repo: z9.string().optional().describe("Git repository URL to clone into the box"),
1235
+ clone_token: z9.string().optional().describe("Token for cloning private repositories"),
1236
+ ephemeral: z9.boolean().optional().describe("If true, box auto-deletes after TTL expires"),
1237
+ ttl: z9.number().optional().describe("Time-to-live in seconds for ephemeral boxes (max 259200 = 3 days)"),
1238
+ // List-specific fields
1239
+ status: z9.enum(["active", "deleted"]).optional().describe("Filter for list action: 'active' (default) or 'deleted'"),
1240
+ ...buildBoxCommon()
1241
+ });
1242
+ },
1243
+ handler: async (params) => {
1244
+ const { action, box_id } = params;
1245
+ const client = getBoxClient(params);
1246
+ switch (action) {
1247
+ case "create": {
1248
+ if (!params.model) {
1249
+ throw new Error("model is required for create action");
1250
+ }
1251
+ const body = {
1252
+ model: params.model
1253
+ };
1254
+ if (params.name) body.name = params.name;
1255
+ if (params.agent) body.agent = params.agent;
1256
+ if (params.runtime) body.runtime = params.runtime;
1257
+ if (params.agent_api_key) body.agent_api_key = params.agent_api_key;
1258
+ if (params.env_vars) body.env_vars = params.env_vars;
1259
+ if (params.clone_repo) body.clone_repo = params.clone_repo;
1260
+ if (params.clone_token) body.clone_token = params.clone_token;
1261
+ if (params.ephemeral !== void 0) body.ephemeral = params.ephemeral;
1262
+ if (params.ttl !== void 0) body.ttl = params.ttl;
1263
+ const box = await client.post("v2/box", body);
1264
+ return [
1265
+ `Box created successfully (status: ${box.status})`,
1266
+ `Box ID: ${box.id}`,
1267
+ json(box)
1268
+ ];
1269
+ }
1270
+ case "list": {
1271
+ const query = {};
1272
+ if (params.status === "deleted") query.status = "deleted";
1273
+ const boxes = await client.get("v2/box", query);
1274
+ return [`Found ${boxes.length} boxes`, json(boxes)];
1275
+ }
1276
+ case "get": {
1277
+ if (!box_id) throw new Error("box_id is required for get action");
1278
+ const box = await client.get(`v2/box/${box_id}`);
1279
+ return [`Box ${box_id} (status: ${box.status})`, json(box)];
1280
+ }
1281
+ case "delete": {
1282
+ if (!box_id) throw new Error("box_id is required for delete action");
1283
+ await client.delete(`v2/box/${box_id}`);
1284
+ return `Box ${box_id} deleted successfully`;
1285
+ }
1286
+ case "pause": {
1287
+ if (!box_id) throw new Error("box_id is required for pause action");
1288
+ await client.post(`v2/box/${box_id}/pause`);
1289
+ return `Box ${box_id} paused successfully`;
1290
+ }
1291
+ case "resume": {
1292
+ if (!box_id) throw new Error("box_id is required for resume action");
1293
+ await client.post(`v2/box/${box_id}/resume`);
1294
+ return `Box ${box_id} resumed successfully`;
1295
+ }
1296
+ case "fork": {
1297
+ if (!box_id) throw new Error("box_id is required for fork action");
1298
+ const forked = await client.post(`v2/box/${box_id}/fork`);
1299
+ return [`Box forked successfully`, `New Box ID: ${forked.id}`, json(forked)];
1300
+ }
1301
+ default: {
1302
+ throw new Error(`Unknown action: ${action}`);
1303
+ }
1304
+ }
1305
+ }
1306
+ })
1307
+ };
1308
+
1309
+ // src/tools/box/exec.ts
1310
+ import { z as z10 } from "zod";
1311
+ var boxExecTool = {
1312
+ box_exec: tool({
1313
+ description: `Execute a shell command inside an Upstash Box container. Use this for file operations, git commands, package installs, or any shell operation inside the box.`,
1314
+ get inputSchema() {
1315
+ return z10.object({
1316
+ box_id: z10.string().describe("The box ID to execute the command in"),
1317
+ command: z10.array(z10.string()).describe("Command and arguments as an array (e.g. ['bash', '-c', 'ls -la'])"),
1318
+ folder: z10.string().optional().describe("Working directory inside the box"),
1319
+ async: z10.boolean().optional().describe("If true, return immediately without waiting for completion"),
1320
+ ...buildBoxCommon()
1321
+ });
1322
+ },
1323
+ handler: async (params) => {
1324
+ const { box_id, command, folder, async: isAsync } = params;
1325
+ const client = getBoxClient(params);
1326
+ const body = { command };
1327
+ if (folder) body.folder = folder;
1328
+ if (isAsync) body.async = isAsync;
1329
+ const response = await client.post(`v2/box/${box_id}/exec`, body);
1330
+ if (response.exit_code !== 0) {
1331
+ return [
1332
+ `Command failed with exit code ${response.exit_code}`,
1333
+ response.output ? `stdout: ${response.output}` : "",
1334
+ response.error ? `stderr: ${response.error}` : ""
1335
+ ].filter(Boolean);
1336
+ }
1337
+ return [
1338
+ `Command executed successfully (exit code: ${response.exit_code})`,
1339
+ response.output || "(no output)"
1340
+ ];
1341
+ }
1342
+ })
1343
+ };
1344
+
1345
+ // src/tools/box/agent-run.ts
1346
+ import { z as z11 } from "zod";
1347
+ var boxAgentRunTool = {
1348
+ box_agent_run: tool({
1349
+ description: `Run an AI agent prompt inside an Upstash Box. The agent has access to shell, filesystem, and git inside the box. It reasons, executes commands, and iterates until the task is complete. This is a synchronous call that may take a while depending on the complexity of the prompt.`,
1350
+ get inputSchema() {
1351
+ return z11.object({
1352
+ box_id: z11.string().describe("The box ID to run the agent in"),
1353
+ prompt: z11.string().describe("The natural-language prompt for the agent to execute"),
1354
+ model: z11.string().optional().describe("Override the box's default LLM model for this run"),
1355
+ folder: z11.string().optional().describe("Working directory inside the box for the agent"),
1356
+ ...buildBoxCommon()
1357
+ });
1358
+ },
1359
+ handler: async (params) => {
1360
+ const { box_id, prompt, model, folder } = params;
1361
+ const client = getBoxClient(params);
1362
+ const body = { prompt };
1363
+ if (model) body.model = model;
1364
+ if (folder) body.folder = folder;
1365
+ const response = await client.post(`v2/box/${box_id}/run`, body);
1366
+ const result = [`Agent run completed`];
1367
+ if (response.run_id) {
1368
+ result.push(`Run ID: ${response.run_id}`);
1369
+ }
1370
+ result.push(response.output || "(no output)");
1371
+ if (response.metadata) {
1372
+ result.push(
1373
+ `Tokens: ${response.metadata.input_tokens ?? 0} in / ${response.metadata.output_tokens ?? 0} out` + (response.metadata.cost_usd ? ` ($${response.metadata.cost_usd.toFixed(4)})` : "")
1374
+ );
1375
+ }
1376
+ return result;
1377
+ }
1378
+ })
1379
+ };
1380
+
1381
+ // src/tools/box/logs.ts
1382
+ import { z as z12 } from "zod";
1383
+ var boxLogsTool = {
1384
+ box_logs: tool({
1385
+ description: `Get logs from an Upstash Box container. Useful for debugging what happened inside the box. Returns timestamped log entries from the system, user, and agent sources.`,
1386
+ get inputSchema() {
1387
+ return z12.object({
1388
+ box_id: z12.string().describe("The box ID to get logs for"),
1389
+ offset: z12.number().optional().default(0).describe("Starting position for log entries (default: 0)"),
1390
+ limit: z12.number().max(1e3).optional().default(100).describe("Maximum number of log entries to return (max 1000, default: 100)"),
1391
+ ...buildBoxCommon()
1392
+ });
1393
+ },
1394
+ handler: async (params) => {
1395
+ const { box_id, offset, limit } = params;
1396
+ const client = getBoxClient(params);
1397
+ const response = await client.get(`v2/box/${box_id}/logs`, {
1398
+ offset,
1399
+ limit
1400
+ });
1401
+ const logs = response.logs ?? [];
1402
+ if (logs.length === 0) {
1403
+ return "No logs found for this box";
1404
+ }
1405
+ return [`Found ${logs.length} log entries`, json(logs)];
1406
+ }
1407
+ })
1408
+ };
1409
+
1410
+ // src/tools/box/runs.ts
1411
+ import { z as z13 } from "zod";
1412
+ var boxRunsTool = {
1413
+ box_runs: tool({
1414
+ description: `List, get details, or cancel runs (execution history) for an Upstash Box. Useful for debugging past agent runs and shell executions, checking their status, output, token usage, and costs.`,
1415
+ get inputSchema() {
1416
+ return z13.object({
1417
+ action: z13.enum(["list", "get", "cancel"]).describe("The action to perform"),
1418
+ box_id: z13.string().describe("The box ID"),
1419
+ run_id: z13.string().optional().describe("Run ID (required for get and cancel actions)"),
1420
+ ...buildBoxCommon()
1421
+ });
1422
+ },
1423
+ handler: async (params) => {
1424
+ const { action, box_id, run_id } = params;
1425
+ const client = getBoxClient(params);
1426
+ switch (action) {
1427
+ case "list": {
1428
+ const response = await client.get(`v2/box/${box_id}/runs`);
1429
+ const runs = response.runs ?? [];
1430
+ return [`Found ${runs.length} runs`, json(runs)];
1431
+ }
1432
+ case "get": {
1433
+ if (!run_id) throw new Error("run_id is required for get action");
1434
+ const run = await client.get(`v2/box/${box_id}/runs/${run_id}`);
1435
+ return [`Run ${run_id} (status: ${run.status})`, json(run)];
1436
+ }
1437
+ case "cancel": {
1438
+ if (!run_id) throw new Error("run_id is required for cancel action");
1439
+ await client.post(`v2/box/${box_id}/runs/${run_id}/cancel`);
1440
+ return `Run ${run_id} cancelled successfully`;
1441
+ }
1442
+ default: {
1443
+ throw new Error(`Unknown action: ${action}`);
1444
+ }
1445
+ }
1446
+ }
1447
+ })
1448
+ };
1449
+
1450
+ // src/tools/box/preview.ts
1451
+ import { z as z14 } from "zod";
1452
+ var boxPreviewTool = {
1453
+ box_preview: tool({
1454
+ description: `Manage preview URLs for web applications running inside an Upstash Box. Create public URLs to access services running on specific ports, list existing previews, or delete them.`,
1455
+ get inputSchema() {
1456
+ return z14.object({
1457
+ action: z14.enum(["create", "list", "delete"]).describe("The action to perform"),
1458
+ box_id: z14.string().describe("The box ID"),
1459
+ port: z14.number().min(1).max(65535).optional().describe("Port number (required for create and delete)"),
1460
+ basic_auth: z14.boolean().optional().describe("Enable basic auth on the preview URL (create only)"),
1461
+ bearer_token: z14.boolean().optional().describe("Enable bearer token auth on the preview URL (create only)"),
1462
+ ...buildBoxCommon()
1463
+ });
1464
+ },
1465
+ handler: async (params) => {
1466
+ const { action, box_id, port, basic_auth, bearer_token } = params;
1467
+ const client = getBoxClient(params);
1468
+ switch (action) {
1469
+ case "create": {
1470
+ if (!port) throw new Error("port is required for create action");
1471
+ const body = { port };
1472
+ if (basic_auth) body.basic_auth = basic_auth;
1473
+ if (bearer_token) body.bearer_token = bearer_token;
1474
+ const response = await client.post(
1475
+ `v2/box/${box_id}/preview`,
1476
+ body
1477
+ );
1478
+ const result = [
1479
+ `Preview URL created: ${response.url}`,
1480
+ `Port: ${response.port}`
1481
+ ];
1482
+ if (response.username) result.push(`Username: ${response.username}`);
1483
+ if (response.password) result.push(`Password: ${response.password}`);
1484
+ if (response.token) result.push(`Token: ${response.token}`);
1485
+ return result;
1486
+ }
1487
+ case "list": {
1488
+ const response = await client.get(`v2/box/${box_id}/preview`);
1489
+ const previews = response.previews ?? [];
1490
+ return [`Found ${previews.length} preview URLs`, json(previews)];
1491
+ }
1492
+ case "delete": {
1493
+ if (!port) throw new Error("port is required for delete action");
1494
+ await client.delete(`v2/box/${box_id}/preview/${port}`);
1495
+ return `Preview for port ${port} deleted successfully`;
1496
+ }
1497
+ default: {
1498
+ throw new Error(`Unknown action: ${action}`);
1499
+ }
1500
+ }
1501
+ }
1502
+ })
1503
+ };
1504
+
1505
+ // src/tools/box/snapshots.ts
1506
+ import { z as z15 } from "zod";
1507
+ var boxSnapshotsTool = {
1508
+ box_snapshots: tool({
1509
+ description: `Manage Upstash Box snapshots. Create full filesystem snapshots of a box, list snapshots, delete them, or restore a box from a snapshot.`,
1510
+ get inputSchema() {
1511
+ return z15.object({
1512
+ action: z15.enum(["create", "list", "list_all", "delete", "restore"]).describe(
1513
+ "The action to perform. 'list' lists snapshots for a specific box, 'list_all' lists all your snapshots"
1514
+ ),
1515
+ box_id: z15.string().optional().describe("Box ID (required for create, list, delete)"),
1516
+ snapshot_id: z15.string().optional().describe("Snapshot ID (required for delete and restore)"),
1517
+ // Create-specific
1518
+ name: z15.string().optional().describe("Name for the snapshot (auto-generated if empty)"),
1519
+ // Restore-specific
1520
+ model: z15.string().optional().describe("LLM model for the restored box (required for restore)"),
1521
+ runtime: z15.string().optional().describe("Override the snapshot's runtime for the restored box"),
1522
+ env_vars: z15.record(z15.string()).optional().describe("Environment variables for the restored box"),
1523
+ ephemeral: z15.boolean().optional().describe("Create the restored box as ephemeral"),
1524
+ ttl: z15.number().optional().describe("TTL in seconds for the restored ephemeral box (max 259200)"),
1525
+ ...buildBoxCommon()
1526
+ });
1527
+ },
1528
+ handler: async (params) => {
1529
+ const { action, box_id, snapshot_id } = params;
1530
+ const client = getBoxClient(params);
1531
+ switch (action) {
1532
+ case "create": {
1533
+ if (!box_id) throw new Error("box_id is required for create action");
1534
+ const body = {};
1535
+ if (params.name) body.name = params.name;
1536
+ const snapshot = await client.post(`v2/box/${box_id}/snapshots`, body);
1537
+ return [
1538
+ `Snapshot created (status: ${snapshot.status})`,
1539
+ `Snapshot ID: ${snapshot.id}`,
1540
+ json(snapshot)
1541
+ ];
1542
+ }
1543
+ case "list": {
1544
+ if (!box_id) throw new Error("box_id is required for list action");
1545
+ const response = await client.get(
1546
+ `v2/box/${box_id}/snapshots`
1547
+ );
1548
+ const snapshots = response.snapshots ?? [];
1549
+ return [`Found ${snapshots.length} snapshots for box ${box_id}`, json(snapshots)];
1550
+ }
1551
+ case "list_all": {
1552
+ const response = await client.get("v2/box/snapshots");
1553
+ const snapshots = response.snapshots ?? [];
1554
+ return [`Found ${snapshots.length} snapshots total`, json(snapshots)];
1555
+ }
1556
+ case "delete": {
1557
+ if (!box_id) throw new Error("box_id is required for delete action");
1558
+ if (!snapshot_id) throw new Error("snapshot_id is required for delete action");
1559
+ await client.delete(`v2/box/${box_id}/snapshots/${snapshot_id}`);
1560
+ return `Snapshot ${snapshot_id} deleted successfully`;
1561
+ }
1562
+ case "restore": {
1563
+ if (!snapshot_id) throw new Error("snapshot_id is required for restore action");
1564
+ if (!params.model) throw new Error("model is required for restore action");
1565
+ const body = {
1566
+ snapshot_id,
1567
+ model: params.model
1568
+ };
1569
+ if (params.runtime) body.runtime = params.runtime;
1570
+ if (params.env_vars) body.env_vars = params.env_vars;
1571
+ if (params.ephemeral !== void 0) body.ephemeral = params.ephemeral;
1572
+ if (params.ttl !== void 0) body.ttl = params.ttl;
1573
+ const box = await client.post("v2/box/from-snapshot", body);
1574
+ return [
1575
+ `Box restored from snapshot (status: ${box.status})`,
1576
+ `New Box ID: ${box.id}`,
1577
+ json(box)
1578
+ ];
1579
+ }
1580
+ default: {
1581
+ throw new Error(`Unknown action: ${action}`);
1582
+ }
1583
+ }
1584
+ }
1585
+ })
1586
+ };
1587
+
1588
+ // src/tools/box/index.ts
1589
+ var boxTools = {
1590
+ ...boxManageTool,
1591
+ ...boxExecTool,
1592
+ ...boxAgentRunTool,
1593
+ ...boxLogsTool,
1594
+ ...boxRunsTool,
1595
+ ...boxPreviewTool,
1596
+ ...boxSnapshotsTool
1597
+ };
1598
+
1063
1599
  // src/tools/index.ts
1064
- var json = (json2) => typeof json2 === "string" ? json2 : JSON.stringify(json2, null, 2);
1065
1600
  var tools = {
1066
1601
  ...redisTools,
1067
- ...qstashAllTools
1602
+ ...qstashAllTools,
1603
+ ...boxTools
1068
1604
  };
1069
- function tool(tool2) {
1070
- return tool2;
1071
- }
1072
1605
 
1073
1606
  // src/settings.ts
1074
1607
  var MAX_MESSAGE_LENGTH = 3e4;
@@ -1087,7 +1620,7 @@ function handlerResponseToCallResult(response) {
1087
1620
  }
1088
1621
 
1089
1622
  // src/server.ts
1090
- import z7 from "zod";
1623
+ import z16 from "zod";
1091
1624
  function createServerInstance() {
1092
1625
  const server = new McpServer(
1093
1626
  { name: "upstash", version: "0.1.0" },
@@ -1098,7 +1631,8 @@ function createServerInstance() {
1098
1631
  }
1099
1632
  }
1100
1633
  );
1101
- const toolsList = Object.entries(tools).map(([name, tool2]) => ({
1634
+ const filteredTools = config.readonly ? Object.fromEntries(Object.entries(tools).filter(([_, tool2]) => tool2.readonly)) : tools;
1635
+ const toolsList = Object.entries(filteredTools).map(([name, tool2]) => ({
1102
1636
  name,
1103
1637
  description: tool2.description,
1104
1638
  inputSchema: tool2.inputSchema,
@@ -1111,7 +1645,7 @@ function createServerInstance() {
1111
1645
  toolName,
1112
1646
  {
1113
1647
  description: tool2.description,
1114
- inputSchema: (tool2.inputSchema ?? z7.object({})).shape
1648
+ inputSchema: (tool2.inputSchema ?? z16.object({})).shape
1115
1649
  },
1116
1650
  // @ts-expect-error - Just ignore the types here
1117
1651
  async (args) => {
@@ -1148,6 +1682,7 @@ Stack trace: ${error instanceof Error ? error.stack : "No stack trace available"
1148
1682
  }
1149
1683
 
1150
1684
  // src/test-connection.ts
1685
+ var READONLY_ERROR = "Readonly API key";
1151
1686
  async function testConnection() {
1152
1687
  log("\u{1F9EA} Testing connection to Upstash API");
1153
1688
  let dbs;
@@ -1162,6 +1697,14 @@ async function testConnection() {
1162
1697
  }
1163
1698
  if (!Array.isArray(dbs))
1164
1699
  throw new Error("Invalid response from Upstash API. Check your API key and email.");
1700
+ try {
1701
+ await http.delete("v2/redis/database/readonly-check-nonexistent");
1702
+ } catch (error) {
1703
+ if (error instanceof Error && error.message.includes(READONLY_ERROR)) {
1704
+ config.readonly = true;
1705
+ log("\u{1F512} Readonly API key detected. Write operations will be disabled.");
1706
+ }
1707
+ }
1165
1708
  log("\u2705 Connection to Upstash API is successful");
1166
1709
  }
1167
1710
 
@@ -1171,10 +1714,13 @@ var argv = process.argv.slice(2);
1171
1714
  if (argv.length >= 3 && argv[0] === "run") {
1172
1715
  argv = ["--email", argv[1], "--api-key", argv[2], ...argv.slice(3)];
1173
1716
  }
1174
- 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();
1717
+ 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("--box-api-key <key>", "Upstash Box API key (optional)").option("--debug", "Enable debug mode").option("--disable-telemetry", "Disable telemetry headers sent to Upstash APIs").allowUnknownOption();
1175
1718
  program.parse(argv, { from: "user" });
1176
1719
  var cliOptions = program.opts();
1177
1720
  var DEBUG = cliOptions.debug ?? false;
1721
+ if (DEBUG) {
1722
+ initDebugLog(new URL("../", import.meta.url).pathname);
1723
+ }
1178
1724
  var allowedTransports = ["stdio", "http"];
1179
1725
  if (!allowedTransports.includes(cliOptions.transport)) {
1180
1726
  console.error(
@@ -1203,6 +1749,8 @@ async function main() {
1203
1749
  }
1204
1750
  config.email = email;
1205
1751
  config.apiKey = apiKey;
1752
+ config.boxApiKey = cliOptions.boxApiKey || process.env.UPSTASH_BOX_API_KEY || "";
1753
+ config.disableTelemetry = cliOptions.disableTelemetry ?? false;
1206
1754
  await testConnection();
1207
1755
  const transportType = TRANSPORT_TYPE;
1208
1756
  if (transportType === "http") {