@upstash/mcp-server 0.2.2 → 0.2.3

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,6 +9,15 @@ 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
13
22
  import { appendFileSync } from "fs";
14
23
  import path from "path";
@@ -44,6 +53,7 @@ function tool(t) {
44
53
  // src/tools/utils.ts
45
54
  var utilTools = {
46
55
  util_timestamps_to_date: tool({
56
+ readonly: true,
47
57
  description: `Use this tool to convert a timestamp to a human-readable date`,
48
58
  inputSchema: z.object({
49
59
  timestamps: z.array(z.number()).describe("Array of timestamps to convert")
@@ -53,6 +63,7 @@ var utilTools = {
53
63
  }
54
64
  }),
55
65
  util_dates_to_timestamps: tool({
66
+ readonly: true,
56
67
  description: `Use this tool to convert an array of ISO 8601 dates to an array of timestamps`,
57
68
  inputSchema: z.object({
58
69
  dates: z.array(z.string()).describe("Array of dates to convert")
@@ -66,13 +77,6 @@ var utilTools = {
66
77
  // src/tools/redis/backup.ts
67
78
  import { z as z2 } from "zod";
68
79
 
69
- // src/config.ts
70
- var config = {
71
- apiKey: "",
72
- email: "",
73
- disableTelemetry: false
74
- };
75
-
76
80
  // src/middlewares.ts
77
81
  var formatTimestamps = (obj) => {
78
82
  if (!obj || typeof obj !== "object") {
@@ -113,7 +117,7 @@ var applyMiddlewares = async (req, func) => {
113
117
  };
114
118
 
115
119
  // src/version.ts
116
- var VERSION = "v0.2.2";
120
+ var VERSION = "0.2.3";
117
121
 
118
122
  // src/telemetry.ts
119
123
  function getRuntime() {
@@ -296,6 +300,7 @@ var redisBackupTools = {
296
300
  }
297
301
  }),
298
302
  redis_database_list_backups: tool({
303
+ readonly: true,
299
304
  // TODO: Add explanation for fields
300
305
  // TODO: Is this in bytes?
301
306
  description: `List all backups of a specific Upstash redis database.`,
@@ -327,6 +332,7 @@ var redisBackupTools = {
327
332
  import { z as z3 } from "zod";
328
333
  var redisCommandTools = {
329
334
  redis_database_run_redis_commands: tool({
335
+ readonly: true,
330
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.
331
337
  NOTE: For discovery, use SCAN over KEYS. Use TYPE to get the type of a key.
332
338
  NOTE: SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
@@ -355,7 +361,7 @@ NOTE: Multiple commands will be executed as a pipeline for better performance.`,
355
361
  log("Fetching database details for database_id:", database_id);
356
362
  const db = await http.get(["v2/redis/database", database_id]);
357
363
  restUrl = "https://" + db.endpoint;
358
- restToken = db.rest_token;
364
+ restToken = config.readonly ? db.read_only_rest_token : db.rest_token;
359
365
  }
360
366
  if (!restUrl || !restToken) {
361
367
  throw new Error("Could not determine REST URL and token for the database");
@@ -448,6 +454,7 @@ NOTE: Ask user for the region and name of the database.${GENERIC_DATABASE_NOTES}
448
454
  }
449
455
  }),
450
456
  redis_database_list_databases: tool({
457
+ readonly: true,
451
458
  description: `List all Upstash redis databases. Only their names and ids.${GENERIC_DATABASE_NOTES}`,
452
459
  handler: async () => {
453
460
  const dbs = await http.get("v2/redis/databases");
@@ -474,6 +481,7 @@ NOTE: Ask user for the region and name of the database.${GENERIC_DATABASE_NOTES}
474
481
  }
475
482
  }),
476
483
  redis_database_get_details: tool({
484
+ readonly: true,
477
485
  description: `Get further details of a specific Upstash redis database. Includes all details of the database including usage statistics.
478
486
  db_disk_threshold: Total disk usage limit.
479
487
  db_memory_threshold: Maximum memory usage.
@@ -516,6 +524,7 @@ ${GENERIC_DATABASE_NOTES}
516
524
  }
517
525
  }),
518
526
  redis_database_get_statistics: tool({
527
+ readonly: true,
519
528
  description: `Get comprehensive usage statistics of an Upstash redis database. Returns both:
520
529
  1. PRECISE 5-day usage: Exact command count and bandwidth usage over the last 5 days
521
530
  2. SAMPLED period stats: Sampled statistics over a specified period (1h, 3h, 12h, 1d, 3d, 7d) for performance monitoring
@@ -633,6 +642,11 @@ async function getQStashCredentials(region) {
633
642
  return { token: match.token };
634
643
  }
635
644
  async function createQStashClientWithToken(options) {
645
+ if (config.readonly) {
646
+ throw new Error(
647
+ "QStash is not available in readonly mode yet. This feature will be implemented in the near future."
648
+ );
649
+ }
636
650
  const { qstash_creds, region, local_mode_port } = options;
637
651
  if (qstash_creds) {
638
652
  return createQStashClient({ url: qstash_creds.url, token: qstash_creds.token });
@@ -1169,10 +1183,424 @@ var qstashAllTools = {
1169
1183
  ...workflowTools
1170
1184
  };
1171
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
+
1172
1599
  // src/tools/index.ts
1173
1600
  var tools = {
1174
1601
  ...redisTools,
1175
- ...qstashAllTools
1602
+ ...qstashAllTools,
1603
+ ...boxTools
1176
1604
  };
1177
1605
 
1178
1606
  // src/settings.ts
@@ -1192,7 +1620,7 @@ function handlerResponseToCallResult(response) {
1192
1620
  }
1193
1621
 
1194
1622
  // src/server.ts
1195
- import z8 from "zod";
1623
+ import z16 from "zod";
1196
1624
  function createServerInstance() {
1197
1625
  const server = new McpServer(
1198
1626
  { name: "upstash", version: "0.1.0" },
@@ -1203,7 +1631,8 @@ function createServerInstance() {
1203
1631
  }
1204
1632
  }
1205
1633
  );
1206
- 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]) => ({
1207
1636
  name,
1208
1637
  description: tool2.description,
1209
1638
  inputSchema: tool2.inputSchema,
@@ -1216,7 +1645,7 @@ function createServerInstance() {
1216
1645
  toolName,
1217
1646
  {
1218
1647
  description: tool2.description,
1219
- inputSchema: (tool2.inputSchema ?? z8.object({})).shape
1648
+ inputSchema: (tool2.inputSchema ?? z16.object({})).shape
1220
1649
  },
1221
1650
  // @ts-expect-error - Just ignore the types here
1222
1651
  async (args) => {
@@ -1253,6 +1682,7 @@ Stack trace: ${error instanceof Error ? error.stack : "No stack trace available"
1253
1682
  }
1254
1683
 
1255
1684
  // src/test-connection.ts
1685
+ var READONLY_ERROR = "Readonly API key";
1256
1686
  async function testConnection() {
1257
1687
  log("\u{1F9EA} Testing connection to Upstash API");
1258
1688
  let dbs;
@@ -1267,6 +1697,14 @@ async function testConnection() {
1267
1697
  }
1268
1698
  if (!Array.isArray(dbs))
1269
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
+ }
1270
1708
  log("\u2705 Connection to Upstash API is successful");
1271
1709
  }
1272
1710
 
@@ -1276,7 +1714,7 @@ var argv = process.argv.slice(2);
1276
1714
  if (argv.length >= 3 && argv[0] === "run") {
1277
1715
  argv = ["--email", argv[1], "--api-key", argv[2], ...argv.slice(3)];
1278
1716
  }
1279
- 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").option("--disable-telemetry", "Disable telemetry headers sent to Upstash APIs").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();
1280
1718
  program.parse(argv, { from: "user" });
1281
1719
  var cliOptions = program.opts();
1282
1720
  var DEBUG = cliOptions.debug ?? false;
@@ -1311,6 +1749,7 @@ async function main() {
1311
1749
  }
1312
1750
  config.email = email;
1313
1751
  config.apiKey = apiKey;
1752
+ config.boxApiKey = cliOptions.boxApiKey || process.env.UPSTASH_BOX_API_KEY || "";
1314
1753
  config.disableTelemetry = cliOptions.disableTelemetry ?? false;
1315
1754
  await testConnection();
1316
1755
  const transportType = TRANSPORT_TYPE;