@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/README.md +311 -39
- package/dist/index.js +453 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 = "
|
|
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
|
|
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
|
|
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 ??
|
|
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;
|