Upstash MCP Server
Official
by upstash
import { z } from "zod";
import { json, tool } from "..";
import { http } from "../../http";
import type { RedisDatabase, RedisUsageResponse, UsageData } from "./types";
import { pruneFalsy } from "../../utils";
const readRegionSchema = z.union([
z.literal("us-east-1"),
z.literal("us-west-1"),
z.literal("us-west-2"),
z.literal("eu-west-1"),
z.literal("eu-central-1"),
z.literal("ap-southeast-1"),
z.literal("ap-southeast-2"),
z.literal("sa-east-1"),
]);
const GENERIC_DATABASE_NOTES =
"\nNOTE: Don't show the database ID from the response to the user unless explicitly asked or needed.\n";
export const redisDbOpsTools = {
redis_database_create_new: tool({
description: `Create a new Upstash redis database.
NOTE: Ask user for the region and name of the database.${GENERIC_DATABASE_NOTES}`,
inputSchema: z.object({
name: z.string().describe("Name of the database."),
primary_region: readRegionSchema.describe(`Primary Region of the Global Database.`),
read_regions: z
.array(readRegionSchema)
.optional()
.describe(`Array of read regions of the db`),
}),
handler: async ({ name, primary_region, read_regions }) => {
const newDb = await http.post<RedisDatabase>("v2/redis/database", {
name,
region: "global",
primary_region,
read_regions,
});
return [
json(newDb),
`Upstash console url: https://console.upstash.com/redis/${newDb.database_id}`,
];
},
}),
redis_database_delete: tool({
description: `Delete an Upstash redis database.`,
inputSchema: z.object({
database_id: z.string().describe("The ID of the database to delete."),
}),
handler: async ({ database_id }) => {
await http.delete(["v2/redis/database", database_id]);
return "Database deleted successfully.";
},
}),
redis_database_list_databases: tool({
description: `List all Upstash redis databases. Only their names and ids.${GENERIC_DATABASE_NOTES}`,
handler: async () => {
const dbs = await http.get<RedisDatabase[]>("v2/redis/databases");
const messages = [
json(
dbs.map((db) => {
const result = {
database_id: db.database_id,
database_name: db.database_name,
state: db.state === "active" ? undefined : db.state,
};
return pruneFalsy(result);
})
),
];
if (dbs.length > 2)
messages.push(
`NOTE: If the user did not specify a database name for the next command, ask them to choose a database from the list.`
);
messages.push(
"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."
);
return messages;
},
}),
redis_database_get_details: tool({
description: `Get further details of a specific Upstash redis database. Includes all details of the database including usage statistics.
db_disk_threshold: Total disk usage limit.
db_memory_threshold: Maximum memory usage.
db_daily_bandwidth_limit: Maximum daily network bandwidth usage.
db_request_limit: Total number of commands allowed.
All sizes are in bytes
${GENERIC_DATABASE_NOTES}
`,
inputSchema: z.object({
database_id: z.string().describe("The ID of the database to get details for."),
}),
handler: async ({ database_id }) => {
const db = await http.get<RedisDatabase>(["v2/redis/database", database_id]);
return json(db);
},
}),
redis_database_update_regions: tool({
description: `Update the read regions of an Upstash redis database.`,
inputSchema: z.object({
id: z.string().describe("The ID of your database."),
read_regions: z
.array(readRegionSchema)
.describe(
"Array of the new read regions of the database. This will replace the old regions array. 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"
),
}),
handler: async ({ id, read_regions }) => {
const updatedDb = await http.post<RedisDatabase>(["v2/redis/update-regions", id], {
read_regions,
});
return json(updatedDb);
},
}),
redis_database_reset_password: tool({
description: `Reset the password of an Upstash redis database.`,
inputSchema: z.object({
id: z.string().describe("The ID of your database."),
}),
handler: async ({ id }) => {
const updatedDb = await http.post<RedisDatabase>(["v2/redis/reset-password", id], {});
return json(updatedDb);
},
}),
redis_database_get_usage_last_5_days: tool({
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.
NOTE: Ask user first if they want to see stats for each database seperately or just for one.`,
inputSchema: z.object({
id: z.string().describe("The ID of your database."),
}),
handler: async ({ id }) => {
const stats = await http.get<RedisUsageResponse>(["v2/redis/stats", `${id}?period=3h`]);
return [
json({
days: stats.days,
command_usage: stats.dailyrequests,
bandwidth_usage: stats.bandwidths,
}),
`NOTE: Times are calculated according to UTC+0`,
];
},
}),
redis_database_get_stats: tool({
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.
Includes: read_latency_mean, write_latency_mean, keyspace, throughput (cmds/sec), diskusage
NOTE: If the user does not specify which stat to get, use throughput as default.`,
inputSchema: z.object({
id: z.string().describe("The ID of your database."),
period: z
.union([
z.literal("1h"),
z.literal("3h"),
z.literal("12h"),
z.literal("1d"),
z.literal("3d"),
z.literal("7d"),
])
.describe("The period of the stats."),
type: z
.union([
z.literal("read_latency_mean"),
z.literal("write_latency_mean"),
z.literal("keyspace").describe("Number of keys in db"),
z
.literal("throughput")
.describe("commands per second (sampled), calculate area for estimated count"),
z.literal("diskusage").describe("Current disk usage in bytes"),
])
.describe("The type of stat to get"),
}),
handler: async ({ id, period, type }) => {
const stats = await http.get<RedisUsageResponse>([
"v2/redis/stats",
`${id}?period=${period}`,
]);
const stat = stats[type];
if (!Array.isArray(stat))
throw new Error(
`Invalid key provided: ${type}. Valid keys are: ${Object.keys(stats).join(", ")}`
);
return [
JSON.stringify(parseUsageData(stat)),
`NOTE: Use the timestamps_to_date tool to parse timestamps if needed`,
`NOTE: Don't try to plot multiple stats in the same chart`,
];
},
}),
};
const parseUsageData = (data: UsageData) => {
if (!data) return "NO DATA";
if (!Array.isArray(data)) return "INVALID DATA";
if (data.length === 0 || data.length === 1) return "NO DATA";
const filteredData = data.filter((d) => d.x && d.y);
return {
start: filteredData[0].x,
// last one can be null, so use the second last
// eslint-disable-next-line unicorn/prefer-at
end: filteredData[filteredData.length - 1]?.x,
data: data.map((d) => [new Date(d.x).getTime(), d.y]),
};
};