@modelcontextprotocol/server-redis
Version:
MCP server for using Redis
266 lines (265 loc) • 9.02 kB
JavaScript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { createClient } from 'redis';
// Configuration
const REDIS_URL = process.argv[2] || "redis://localhost:6379";
const MAX_RETRIES = 5;
const MIN_RETRY_DELAY = 1000; // 1 second
const MAX_RETRY_DELAY = 30000; // 30 seconds
// Create Redis client with retry strategy
const redisClient = createClient({
url: REDIS_URL,
socket: {
reconnectStrategy: (retries) => {
if (retries >= MAX_RETRIES) {
console.error(`[Redis Error] Maximum retries (${MAX_RETRIES}) reached. Giving up.`);
console.error(`[Redis Error] Connection: ${REDIS_URL}`);
return new Error('Max retries reached');
}
const delay = Math.min(Math.pow(2, retries) * MIN_RETRY_DELAY, MAX_RETRY_DELAY);
console.error(`[Redis Retry] Attempt ${retries + 1}/${MAX_RETRIES} failed`);
console.error(`[Redis Retry] Next attempt in ${delay}ms`);
console.error(`[Redis Retry] Connection: ${REDIS_URL}`);
return delay;
}
}
});
// Define Zod schemas for validation
const SetArgumentsSchema = z.object({
key: z.string(),
value: z.string(),
expireSeconds: z.number().optional(),
});
const GetArgumentsSchema = z.object({
key: z.string(),
});
const DeleteArgumentsSchema = z.object({
key: z.string().or(z.array(z.string())),
});
const ListArgumentsSchema = z.object({
pattern: z.string().default("*"),
});
// Create server instance
const server = new Server({
name: "redis",
version: "0.0.1"
}, {
capabilities: {
tools: {}
}
});
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "set",
description: "Set a Redis key-value pair with optional expiration",
inputSchema: {
type: "object",
properties: {
key: {
type: "string",
description: "Redis key",
},
value: {
type: "string",
description: "Value to store",
},
expireSeconds: {
type: "number",
description: "Optional expiration time in seconds",
},
},
required: ["key", "value"],
},
},
{
name: "get",
description: "Get value by key from Redis",
inputSchema: {
type: "object",
properties: {
key: {
type: "string",
description: "Redis key to retrieve",
},
},
required: ["key"],
},
},
{
name: "delete",
description: "Delete one or more keys from Redis",
inputSchema: {
type: "object",
properties: {
key: {
oneOf: [
{ type: "string" },
{ type: "array", items: { type: "string" } }
],
description: "Key or array of keys to delete",
},
},
required: ["key"],
},
},
{
name: "list",
description: "List Redis keys matching a pattern",
inputSchema: {
type: "object",
properties: {
pattern: {
type: "string",
description: "Pattern to match keys (default: *)",
},
},
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "set") {
const { key, value, expireSeconds } = SetArgumentsSchema.parse(args);
if (expireSeconds) {
await redisClient.setEx(key, expireSeconds, value);
}
else {
await redisClient.set(key, value);
}
return {
content: [
{
type: "text",
text: `Successfully set key: ${key}`,
},
],
};
}
else if (name === "get") {
const { key } = GetArgumentsSchema.parse(args);
const value = await redisClient.get(key);
if (value === null) {
return {
content: [
{
type: "text",
text: `Key not found: ${key}`,
},
],
};
}
return {
content: [
{
type: "text",
text: `${value}`,
},
],
};
}
else if (name === "delete") {
const { key } = DeleteArgumentsSchema.parse(args);
if (Array.isArray(key)) {
await redisClient.del(key);
return {
content: [
{
type: "text",
text: `Successfully deleted ${key.length} keys`,
},
],
};
}
else {
await redisClient.del(key);
return {
content: [
{
type: "text",
text: `Successfully deleted key: ${key}`,
},
],
};
}
}
else if (name === "list") {
const { pattern } = ListArgumentsSchema.parse(args);
const keys = await redisClient.keys(pattern);
return {
content: [
{
type: "text",
text: keys.length > 0
? `Found keys:\n${keys.join('\n')}`
: "No keys found matching pattern",
},
],
};
}
else {
throw new Error(`Unknown tool: ${name}`);
}
}
catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid arguments: ${error.errors
.map((e) => `${e.path.join(".")}: ${e.message}`)
.join(", ")}`);
}
throw error;
}
});
// Set up Redis event handlers
redisClient.on('error', (err) => {
console.error(`[Redis Error] ${err.name}: ${err.message}`);
console.error(`[Redis Error] Connection: ${REDIS_URL}`);
console.error(`[Redis Error] Stack: ${err.stack}`);
});
redisClient.on('connect', () => {
console.error(`[Redis Connected] Successfully connected to ${REDIS_URL}`);
});
redisClient.on('reconnecting', () => {
console.error('[Redis Reconnecting] Connection lost, attempting to reconnect...');
});
redisClient.on('end', () => {
console.error('[Redis Disconnected] Connection closed');
});
async function runServer() {
try {
// Connect to Redis
await redisClient.connect();
// Set up MCP server
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Redis MCP Server running on stdio");
}
catch (error) {
const err = error;
console.error("[Redis Fatal] Server initialization failed");
console.error(`[Redis Fatal] Error: ${err.name}: ${err.message}`);
console.error(`[Redis Fatal] Connection: ${REDIS_URL}`);
console.error(`[Redis Fatal] Stack: ${err.stack}`);
await redisClient.quit().catch(() => { });
process.exit(1);
}
}
// Handle process termination
process.on('SIGINT', async () => {
await redisClient.quit().catch(() => { });
process.exit(0);
});
process.on('SIGTERM', async () => {
await redisClient.quit().catch(() => { });
process.exit(0);
});
runServer();