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