Skip to content

Commit 916cc51

Browse files
authored
Revision 1 (#3)
* fix: use node15 and es2015 when building * fix: install node@15 types * fix: improve stats * fix: long scan loop when asking for keys * chore: remove lint flag * chore: remove .js suffixes from imports * fix: add node15 target to tsup * chore: remove unnecessary code * fix: don't use Array.at for node15 support * fix: add hint for list-dbs * add a timestamps_to_date tool * fix: add hint for truncate * chore: move timestamp_to_date to utils * fix: add hint for teams * chore: rename commands * fix: remove bun-types from tsconfig * fix: modify hints for stats * fix: remove dot from hint
1 parent d2e7397 commit 916cc51

15 files changed

+126
-77
lines changed

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Upstash MCP Server
2+
23
[![smithery badge](https://smithery.ai/badge/@upstash/mcp-server-upstash)](https://smithery.ai/server/@upstash/mcp-server-upstash)
34

45
Model Context Protocol (MCP) is a [new, standardized protocol](https://modelcontextprotocol.io/introduction) for managing context between large language models (LLMs) and external systems. In this repository, we provide an installer as well as an MCP Server for [Upstash Developer API's](https://upstash.com/docs/devops/developer-api).
@@ -20,6 +21,7 @@ This lets you use Claude Desktop, or any MCP Client, to use natural language to
2021
- [Upstash API key](https://upstash.com/docs/devops/developer-api) - You can create one from [here](https://console.upstash.com/account/api).
2122

2223
## How to use locally
24+
2325
### Installing via Smithery
2426

2527
To install Upstash for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@upstash/mcp-server-upstash):
@@ -29,6 +31,7 @@ npx -y @smithery/cli install @upstash/mcp-server-upstash --client claude
2931
```
3032

3133
### Installing manually
34+
3235
1. Run `npx @upstash/mcp-server-upstash init <UPSTASH_EMAIL> <UPSTASH_API_KEY>`
3336
2. Restart Claude Desktop
3437
3. You should now be able to use Upstash commands in Claude Desktop
@@ -54,7 +57,8 @@ See the [troubleshooting guide](https://modelcontextprotocol.io/quickstart#troub
5457
- `redis_database_run_single_redis_command`
5558
- `redis_database_set_daily_backup`
5659
- `redis_database_update_regions`
57-
- `redis_database_get_usage_stats`
60+
- `redis_database_get_usage_last_5_days`
61+
- `redis_database_get_stats`
5862

5963
## Development
6064

bun.lockb

-1.76 KB
Binary file not shown.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@
3030
"devDependencies": {
3131
"@typescript-eslint/eslint-plugin": "8.4.0",
3232
"@typescript-eslint/parser": "8.4.0",
33-
"bun-types": "^1.1.38",
3433
"eslint": "9.10.0",
3534
"eslint-plugin-unicorn": "55.0.0",
3635
"prettier": "^3.4.2",
3736
"tsup": "^8.3.5"
3837
},
3938
"dependencies": {
4039
"@modelcontextprotocol/sdk": "^1.0.3",
40+
"@types/node": "15",
4141
"chalk": "^5.3.0",
4242
"dotenv": "^16.4.7",
4343
"node-fetch": "^3.3.2",

src/http.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { config } from "./config";
22
import { log } from "./log";
33
import { applyMiddlewares } from "./middlewares";
4+
import type { RequestInit } from "node-fetch";
45
import fetch from "node-fetch";
56

67
export type UpstashRequest = {

src/index.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
#!/usr/bin/env node
22

3-
/* eslint-disable unicorn/no-process-exit */
43
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5-
import { init } from "./init.js";
6-
import { log } from "./log.js";
7-
import { server } from "./server.js";
8-
import { config } from "./config.js";
4+
import { init } from "./init";
5+
import { log } from "./log";
6+
import { server } from "./server";
7+
import { config } from "./config";
98
import "dotenv/config";
10-
import { testConnection } from "./test-connection.js";
9+
import { testConnection } from "./test-connection";
1110

1211
process.on("uncaughtException", (error) => {
1312
log("Uncaught exception:", error.name, error.message, error.stack);

src/init.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
import path from "node:path";
2-
import os from "node:os";
3-
import fs from "node:fs";
1+
/* eslint-disable unicorn/prefer-node-protocol */
2+
import path from "path";
3+
import os from "os";
4+
import fs from "fs";
45
import chalk from "chalk";
5-
import { fileURLToPath } from "node:url";
6+
67
import { log } from "./log";
78
import { config } from "./config";
89

9-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
10-
const _packageJson = JSON.parse(
11-
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")
12-
);
1310
const claudeConfigPath = path.join(
1411
os.homedir(),
1512
"Library",

src/tool.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function handlerResponseToCallResult(
3434
// Truncate messages that are too long
3535
const truncatedArray = array.map((item) =>
3636
item.length > MAX_MESSAGE_LENGTH
37-
? `${item.slice(0, MAX_MESSAGE_LENGTH)}... (truncated)`
37+
? `${item.slice(0, MAX_MESSAGE_LENGTH)}... (MESSAGE TRUNCATED, MENTION THIS TO USER)`
3838
: item
3939
);
4040

src/tools/redis/command.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,14 @@ NOTE: SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]`,
4444
throw new Error("Redis error: " + result.error);
4545
}
4646

47-
return json(result);
47+
const isScanCommand = command[0].toLocaleLowerCase().includes("scan");
48+
const messages = [json(result)];
49+
50+
if (isScanCommand)
51+
messages.push(`NOTE: Use the returned cursor to get the next set of keys.
52+
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.`);
53+
54+
return messages;
4855
},
4956
}),
5057

src/tools/redis/db.ts

+72-53
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { z } from "zod";
22
import { json, tool } from "..";
33
import { http } from "../../http";
44
import type { RedisDatabase, RedisUsageResponse, UsageData } from "./types";
5+
import { pruneFalsy } from "../../utils";
56

67
const readRegionSchema = z.union([
78
z.literal("us-east-1"),
@@ -14,24 +15,20 @@ const readRegionSchema = z.union([
1415
z.literal("sa-east-1"),
1516
]);
1617

17-
const READ_REGIONS_DESCRIPTION =
18-
"Available regions: us-east-1, us-west-1, us-west-2, eu-west-1, eu-central-1, ap-southeast-1, ap-southeast-2, sa-east-1";
19-
20-
const GENERIC_DATABASE_NOTES = "\nNOTE: Don't show the database ID from the response to the user unless explicitly asked or needed.\n";
18+
const GENERIC_DATABASE_NOTES =
19+
"\nNOTE: Don't show the database ID from the response to the user unless explicitly asked or needed.\n";
2120

2221
export const redisDbOpsTools = {
2322
redis_database_create_new: tool({
2423
description: `Create a new Upstash redis database.
2524
NOTE: Ask user for the region and name of the database.${GENERIC_DATABASE_NOTES}`,
2625
inputSchema: z.object({
2726
name: z.string().describe("Name of the database."),
28-
primary_region: readRegionSchema.describe(
29-
`Primary Region of the Global Database. ${READ_REGIONS_DESCRIPTION}`
30-
),
27+
primary_region: readRegionSchema.describe(`Primary Region of the Global Database.`),
3128
read_regions: z
3229
.array(readRegionSchema)
3330
.optional()
34-
.describe(`Array of Read Regions of the Database. ${READ_REGIONS_DESCRIPTION}`),
31+
.describe(`Array of read regions of the db`),
3532
}),
3633
handler: async ({ name, primary_region, read_regions }) => {
3734
const newDb = await http.post<RedisDatabase>("v2/redis/database", {
@@ -61,32 +58,32 @@ NOTE: Ask user for the region and name of the database.${GENERIC_DATABASE_NOTES}
6158
}),
6259

6360
redis_database_list_databases: tool({
64-
description:
65-
`List all Upstash redis databases. Includes names, regions, password, creation time and more.${GENERIC_DATABASE_NOTES}`,
61+
description: `List all Upstash redis databases. Only their names and ids.${GENERIC_DATABASE_NOTES}`,
6662
handler: async () => {
6763
const dbs = await http.get<RedisDatabase[]>("v2/redis/databases");
6864

69-
return json(
70-
// Only the important fields
71-
dbs.map((db) => ({
72-
database_id: db.database_id,
73-
database_name: db.database_name,
74-
database_type: db.database_type,
75-
region: db.region,
76-
type: db.type,
77-
primary_region: db.primary_region,
78-
read_regions: db.read_regions,
79-
creation_time: db.creation_time,
80-
budget: db.budget,
81-
state: db.state,
82-
password: db.password,
83-
endpoint: db.endpoint,
84-
rest_token: db.rest_token,
85-
read_only_rest_token: db.read_only_rest_token,
86-
db_acl_enabled: db.db_acl_enabled,
87-
db_acl_default_user_status: db.db_acl_default_user_status,
88-
}))
65+
const messages = [
66+
json(
67+
dbs.map((db) => {
68+
const result = {
69+
database_id: db.database_id,
70+
database_name: db.database_name,
71+
state: db.state === "active" ? undefined : db.state,
72+
};
73+
return pruneFalsy(result);
74+
})
75+
),
76+
];
77+
78+
if (dbs.length > 2)
79+
messages.push(
80+
`NOTE: If the user did not specify a database name for the next command, ask them to choose a database from the list.`
81+
);
82+
messages.push(
83+
"NOTE: If the user wants to see dbs in another team, mention that they need to create a new management api key for that team and initialize MCP server with the newly created key."
8984
);
85+
86+
return messages;
9087
},
9188
}),
9289

@@ -140,9 +137,30 @@ ${GENERIC_DATABASE_NOTES}
140137
},
141138
}),
142139

143-
redis_database_get_usage_stats: tool({
144-
description: `Get usage statistics of an Upstash redis database over a period of time.
145-
Available stats: read_latency_mean, write_latency_mean, keyspace, throughput (cmds per second), daily_net_commands, diskusage, command_counts (stats of every command seperately).`,
140+
redis_database_get_usage_last_5_days: tool({
141+
description: `Get PRECISE command count and bandwidth usage statistics of an Upstash redis database over the last 5 days. This is a precise stat, not an average.
142+
NOTE: Ask user first if they want to see stats for each database seperately or just for one.`,
143+
inputSchema: z.object({
144+
id: z.string().describe("The ID of your database."),
145+
}),
146+
handler: async ({ id }) => {
147+
const stats = await http.get<RedisUsageResponse>(["v2/redis/stats", `${id}?period=3h`]);
148+
149+
return [
150+
json({
151+
days: stats.days,
152+
command_usage: stats.dailyrequests,
153+
bandwidth_usage: stats.bandwidths,
154+
}),
155+
`NOTE: Times are calculated according to UTC+0`,
156+
];
157+
},
158+
}),
159+
160+
redis_database_get_stats: tool({
161+
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.
162+
Includes: read_latency_mean, write_latency_mean, keyspace, throughput (cmds/sec), diskusage
163+
NOTE: If the user does not specify which stat to get, use throughput as default.`,
146164
inputSchema: z.object({
147165
id: z.string().describe("The ID of your database."),
148166
period: z
@@ -159,11 +177,11 @@ Available stats: read_latency_mean, write_latency_mean, keyspace, throughput (cm
159177
.union([
160178
z.literal("read_latency_mean"),
161179
z.literal("write_latency_mean"),
162-
z.literal("keyspace"),
163-
z.literal("throughput"),
164-
z.literal("daily_net_commands"),
165-
z.literal("diskusage"),
166-
z.literal("command_counts"),
180+
z.literal("keyspace").describe("Number of keys in db"),
181+
z
182+
.literal("throughput")
183+
.describe("commands per second (sampled), calculate area for estimated count"),
184+
z.literal("diskusage").describe("Current disk usage in bytes"),
167185
])
168186
.describe("The type of stat to get"),
169187
}),
@@ -173,31 +191,32 @@ Available stats: read_latency_mean, write_latency_mean, keyspace, throughput (cm
173191
`${id}?period=${period}`,
174192
]);
175193

176-
if (type === "command_counts") {
177-
return JSON.stringify(
178-
stats.command_counts.map((c) => ({
179-
command: c.metric_identifier,
180-
...parseUsageData(c.data_points),
181-
}))
182-
);
183-
}
184-
185194
const stat = stats[type];
186195

187-
if (Array.isArray(stat)) {
188-
return JSON.stringify(parseUsageData(stat));
189-
}
196+
if (!Array.isArray(stat))
197+
throw new Error(
198+
`Invalid key provided: ${type}. Valid keys are: ${Object.keys(stats).join(", ")}`
199+
);
190200

191-
return json(stats);
201+
return [
202+
JSON.stringify(parseUsageData(stat)),
203+
`NOTE: Use the timestamps_to_date tool to parse timestamps if needed`,
204+
`NOTE: Don't try to plot multiple stats in the same chart`,
205+
];
192206
},
193207
}),
194208
};
195209

196210
const parseUsageData = (data: UsageData) => {
211+
if (!data) return "NO DATA";
212+
if (!Array.isArray(data)) return "INVALID DATA";
213+
if (data.length === 0 || data.length === 1) return "NO DATA";
214+
const filteredData = data.filter((d) => d.x && d.y);
197215
return {
198-
start: data[0].x,
216+
start: filteredData[0].x,
199217
// last one can be null, so use the second last
200-
end: data.at(-1)?.x || data.at(-2)?.x,
218+
// eslint-disable-next-line unicorn/prefer-at
219+
end: filteredData[filteredData.length - 1]?.x,
201220
data: data.map((d) => [new Date(d.x).getTime(), d.y]),
202221
};
203222
};

src/tools/redis/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { utilTools } from "../utils";
12
import { redisBackupTools } from "./backup";
23
import { redisCommandTools } from "./command";
34
import { redisDbOpsTools } from "./db";
@@ -6,4 +7,5 @@ export const redisTools = {
67
...redisDbOpsTools,
78
...redisBackupTools,
89
...redisCommandTools,
10+
...utilTools,
911
};

src/tools/redis/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,9 @@ export type RedisUsageResponse = {
6565
metric_identifier: string;
6666
data_points: UsageData;
6767
}[];
68+
69+
// For last 5 days
70+
dailyrequests: UsageData;
71+
bandwidths: UsageData;
72+
days: string[];
6873
};

src/tools/utils.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { z } from "zod";
2+
import { tool } from ".";
3+
4+
export const utilTools = {
5+
timestamps_to_date: tool({
6+
description: `Use this tool to convert a timestamp to a human-readable date`,
7+
inputSchema: z.object({
8+
timestamps: z.array(z.number()).describe("Array of timestamps to convert"),
9+
}),
10+
handler: async ({ timestamps }) => {
11+
return timestamps.map((timestamp) => new Date(timestamp).toUTCString());
12+
},
13+
}),
14+
}

src/utils.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function pruneFalsy(obj: Record<string, any>) {
2+
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value));
3+
}

tsconfig.json

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
22
"compilerOptions": {
3-
"lib": ["ESNext"],
3+
"lib": ["ES2015"],
44
"module": "esnext",
5-
"target": "esnext",
5+
"target": "ES2015",
66
"moduleResolution": "bundler",
77
"allowImportingTsExtensions": true,
88
"noEmit": true,
@@ -12,9 +12,6 @@
1212
"jsx": "react-jsx",
1313
"allowSyntheticDefaultImports": true,
1414
"forceConsistentCasingInFileNames": true,
15-
"allowJs": true,
16-
"types": [
17-
"bun-types" // add Bun global
18-
]
15+
"allowJs": true
1916
}
2017
}

tsup.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export default defineConfig({
66
clean: true,
77
dts: true,
88
sourcemap: true,
9+
target: "node15",
910
});

0 commit comments

Comments
 (0)