Skip to main content
Glama
Nam088

Multi-Database MCP Server

by Nam088
index.ts35.4 kB
import { z } from 'zod'; import { createClient, type RedisClientType } from 'redis'; import { PluginBase, PluginMode, type PluginConfig, type PluginContext } from '@nam088/mcp-core'; import type { RedisPluginConfig } from './types.js'; /** * Redis MCP Plugin * Provides tools for interacting with Redis */ export class RedisPlugin extends PluginBase { readonly metadata: PluginConfig = { name: 'redis', version: '1.0.0', description: 'Redis database tools for MCP', }; private redisConfig: RedisPluginConfig; private client: RedisClientType | null = null; constructor(config?: Record<string, unknown>) { super(config); // Support mode from config or environment variable const modeFromEnv = process.env.REDIS_MODE; if (config?.mode && typeof config.mode === 'string' && config.mode in PluginMode) { this.metadata.mode = config.mode as PluginMode; } else if (modeFromEnv && modeFromEnv in PluginMode) { this.metadata.mode = modeFromEnv as PluginMode; } else { this.metadata.mode = PluginMode.READONLY; } // Support connection string (URL) - takes precedence over individual settings const url = (config?.url as string) || (config?.connectionString as string) || process.env.REDIS_URL; // Support environment variables with config override const password = (config?.password as string) || process.env.REDIS_PASSWORD; const tls = config?.tls !== undefined ? (config.tls as boolean) : process.env.REDIS_TLS === 'true'; const rejectUnauthorized = config?.rejectUnauthorized !== undefined ? (config.rejectUnauthorized as boolean) : process.env.REDIS_REJECT_UNAUTHORIZED !== 'false'; // Default true for security this.redisConfig = { ...(url && { url }), host: (config?.host as string) || process.env.REDIS_HOST || 'localhost', port: (config?.port as number) || parseInt(process.env.REDIS_PORT || '6379'), ...(password && { password }), db: (config?.db as number) || parseInt(process.env.REDIS_DB || '0'), connectionTimeout: (config?.connectionTimeout as number) || parseInt(process.env.REDIS_TIMEOUT || '5000'), commandTimeout: (config?.commandTimeout as number) || 5000, tls, rejectUnauthorized, lazyConnect: (config?.lazyConnect as boolean) || false, maxRetries: (config?.maxRetries as number) || 3, enableAutoPipelining: (config?.enableAutoPipelining as boolean) !== false, // Default true keepAlive: (config?.keepAlive as number) || 30000, }; } /** * Initialize Redis connection */ async initialize(context: PluginContext): Promise<void> { await super.initialize(context); try { // If connection URL is provided, use it directly if (this.redisConfig.url) { const clientConfig: { url: string; socket?: { connectTimeout?: number; keepAlive?: number; reconnectStrategy?: (retries: number) => number | Error; tls?: boolean; rejectUnauthorized?: boolean; }; commandsQueueMaxLength?: number; } = { url: this.redisConfig.url, commandsQueueMaxLength: 10000, }; // Add socket options if specified const socketConfig: { connectTimeout?: number; keepAlive?: number; reconnectStrategy?: (retries: number) => number | Error; tls?: boolean; rejectUnauthorized?: boolean; } = {}; if (this.redisConfig.connectionTimeout) { socketConfig.connectTimeout = this.redisConfig.connectionTimeout; } if (this.redisConfig.keepAlive) { socketConfig.keepAlive = this.redisConfig.keepAlive; } // Add retry strategy const maxRetries = this.redisConfig.maxRetries || 3; socketConfig.reconnectStrategy = (retries: number): number | Error => { if (retries > maxRetries) { console.error(`[ERROR] [Redis] Max retries (${maxRetries}) exceeded`); return new Error('Max retries exceeded'); } const delay = Math.min(retries * 100, 3000); // Max 3s delay return delay; }; // Add TLS configuration if URL doesn't already specify it // (rediss:// means TLS, redis:// means no TLS) if (this.redisConfig.tls && !this.redisConfig.url.startsWith('rediss://')) { socketConfig.tls = true; if (this.redisConfig.rejectUnauthorized !== undefined) { socketConfig.rejectUnauthorized = this.redisConfig.rejectUnauthorized; } } else if ( this.redisConfig.url.startsWith('rediss://') && this.redisConfig.rejectUnauthorized !== undefined ) { socketConfig.tls = true; socketConfig.rejectUnauthorized = this.redisConfig.rejectUnauthorized; } if (Object.keys(socketConfig).length > 0) { clientConfig.socket = socketConfig; } this.client = createClient(clientConfig); } else { // Use individual config settings (existing behavior) const socketConfig: { host: string; port: number; connectTimeout?: number; tls?: boolean; rejectUnauthorized?: boolean; keepAlive?: number; reconnectStrategy?: (retries: number) => number | Error; } = { host: this.redisConfig.host || 'localhost', port: this.redisConfig.port || 6379, }; if (this.redisConfig.connectionTimeout) { socketConfig.connectTimeout = this.redisConfig.connectionTimeout; } if (this.redisConfig.keepAlive) { socketConfig.keepAlive = this.redisConfig.keepAlive; } // Add retry strategy const maxRetries = this.redisConfig.maxRetries || 3; socketConfig.reconnectStrategy = (retries: number): number | Error => { if (retries > maxRetries) { console.error(`[ERROR] [Redis] Max retries (${maxRetries}) exceeded`); return new Error('Max retries exceeded'); } const delay = Math.min(retries * 100, 3000); // Max 3s delay return delay; }; // Add TLS configuration if (this.redisConfig.tls) { socketConfig.tls = true; if (this.redisConfig.rejectUnauthorized !== undefined) { socketConfig.rejectUnauthorized = this.redisConfig.rejectUnauthorized; } } const clientConfig: { socket: typeof socketConfig; password?: string; database?: number; commandsQueueMaxLength?: number; } = { socket: socketConfig, commandsQueueMaxLength: 10000, // Prevent memory issues }; if (this.redisConfig.password) { clientConfig.password = this.redisConfig.password; } if (this.redisConfig.db !== undefined) { clientConfig.database = this.redisConfig.db; } this.client = createClient(clientConfig); } // Add event listeners this.client.on('error', (err) => { console.error('[ERROR] [Redis] Client Error:', err); }); // Connect based on lazyConnect option if (!this.redisConfig.lazyConnect) { await this.client.connect(); } } catch (error) { console.error('[ERROR] [Redis] Failed to connect:', error); throw error; } } /** * Register Redis tools */ register(context: PluginContext): void { // Tool: redis_get (readonly operation) this.registerTool({ context, name: 'redis_get', schema: { description: 'Get value from Redis by key', inputSchema: { key: z.string().describe('Redis key to retrieve'), }, }, handler: async ({ key }: { key: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const value = await this.client.get(key); return { content: [ { type: 'text', text: JSON.stringify({ key, value, exists: value !== null }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); // Tool: redis_set (write operation) this.registerTool({ context, name: 'redis_set', schema: { description: 'Set value in Redis (requires FULL mode)', inputSchema: { key: z.string().describe('Redis key'), value: z.string().describe('Value to set'), ttl: z.number().optional().describe('TTL in seconds (optional)'), }, }, handler: async ({ key, value, ttl }: { key: string; value: string; ttl?: number }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } let result: string | null; if (ttl) { result = await this.client.setEx(key, ttl, value); } else { result = await this.client.set(key, value); } return { content: [ { type: 'text', text: JSON.stringify({ key, value, ttl: ttl || null, result }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_del (write operation) this.registerTool({ context, name: 'redis_del', schema: { description: 'Delete key from Redis (requires FULL mode)', inputSchema: { key: z.string().describe('Redis key to delete'), }, }, handler: async ({ key }: { key: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const deletedCount = await this.client.del(key); return { content: [ { type: 'text', text: JSON.stringify({ key, deleted: deletedCount > 0, deletedCount }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_keys this.registerTool({ context, name: 'redis_keys', schema: { description: 'Find keys matching a pattern', inputSchema: { pattern: z.string().describe('Pattern to match (e.g., "user:*")'), }, }, handler: async ({ pattern }: { pattern: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const keys = await this.client.keys(pattern); return { content: [ { type: 'text', text: JSON.stringify({ pattern, keys, count: keys.length }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); // Tool: redis_info this.registerTool({ context, name: 'redis_info', schema: { description: 'Get Redis server information', inputSchema: { section: z .string() .optional() .describe('Info section (server, clients, memory, stats, etc.)'), }, }, handler: async ({ section }: { section?: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const infoStr = section ? await this.client.info(section) : await this.client.info(); // Parse INFO output into structured format const info: Record<string, string> = {}; const lines = infoStr.split('\r\n'); for (const line of lines) { if (line && !line.startsWith('#')) { const [key, value] = line.split(':'); if (key && value) { info[key] = value; } } } return { content: [ { type: 'text', text: JSON.stringify({ section: section || 'default', info }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); // Tool: redis_exists this.registerTool({ context, name: 'redis_exists', schema: { description: 'Check if one or more keys exist', inputSchema: { keys: z.array(z.string()).describe('Array of keys to check'), }, }, handler: async ({ keys }: { keys: string[] }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const exists = await this.client.exists(keys); return { content: [ { type: 'text', text: JSON.stringify( { keys, existsCount: exists, allExist: exists === keys.length }, null, 2, ), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); // Tool: redis_ttl this.registerTool({ context, name: 'redis_ttl', schema: { description: 'Get the time to live for a key', inputSchema: { key: z.string().describe('Redis key'), }, }, handler: async ({ key }: { key: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const ttl = await this.client.ttl(key); let status: string; if (ttl === -2) { status = 'key does not exist'; } else if (ttl === -1) { status = 'key has no expiration'; } else { status = 'key expires in seconds'; } return { content: [ { type: 'text', text: JSON.stringify({ key, ttl, status }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); // Tool: redis_expire this.registerTool({ context, name: 'redis_expire', schema: { description: 'Set a timeout on a key (requires FULL mode)', inputSchema: { key: z.string().describe('Redis key'), seconds: z.number().describe('TTL in seconds'), }, }, handler: async ({ key, seconds }: { key: string; seconds: number }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const result = await this.client.expire(key, seconds); return { content: [ { type: 'text', text: JSON.stringify({ key, seconds, success: result }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_incr this.registerTool({ context, name: 'redis_incr', schema: { description: 'Increment the integer value of a key by 1 (requires FULL mode)', inputSchema: { key: z.string().describe('Redis key'), }, }, handler: async ({ key }: { key: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const newValue = await this.client.incr(key); return { content: [ { type: 'text', text: JSON.stringify({ key, newValue }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_decr this.registerTool({ context, name: 'redis_decr', schema: { description: 'Decrement the integer value of a key by 1 (requires FULL mode)', inputSchema: { key: z.string().describe('Redis key'), }, }, handler: async ({ key }: { key: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const newValue = await this.client.decr(key); return { content: [ { type: 'text', text: JSON.stringify({ key, newValue }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_mget this.registerTool({ context, name: 'redis_mget', schema: { description: 'Get values of multiple keys', inputSchema: { keys: z.array(z.string()).describe('Array of Redis keys'), }, }, handler: async ({ keys }: { keys: string[] }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const values = await this.client.mGet(keys); const result: Record<string, string | null> = {}; keys.forEach((key: string, index: number) => { result[key] = values[index]; }); return { content: [ { type: 'text', text: JSON.stringify({ keys, values: result }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); // Tool: redis_hget this.registerTool({ context, name: 'redis_hget', schema: { description: 'Get the value of a hash field', inputSchema: { key: z.string().describe('Redis hash key'), field: z.string().describe('Hash field name'), }, }, handler: async ({ key, field }: { key: string; field: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const value = await this.client.hGet(key, field); return { content: [ { type: 'text', text: JSON.stringify({ key, field, value, exists: value !== null }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); // Tool: redis_hgetall this.registerTool({ context, name: 'redis_hgetall', schema: { description: 'Get all fields and values in a hash', inputSchema: { key: z.string().describe('Redis hash key'), }, }, handler: async ({ key }: { key: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const hash = await this.client.hGetAll(key); return { content: [ { type: 'text', text: JSON.stringify({ key, hash, fieldCount: Object.keys(hash).length }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); // Tool: redis_hset this.registerTool({ context, name: 'redis_hset', schema: { description: 'Set the value of a hash field (requires FULL mode)', inputSchema: { key: z.string().describe('Redis hash key'), field: z.string().describe('Hash field name'), value: z.string().describe('Value to set'), }, }, handler: async ({ key, field, value }: { key: string; field: string; value: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const result = await this.client.hSet(key, field, value); return { content: [ { type: 'text', text: JSON.stringify({ key, field, value, newField: result === 1 }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_hdel this.registerTool({ context, name: 'redis_hdel', schema: { description: 'Delete one or more hash fields (requires FULL mode)', inputSchema: { key: z.string().describe('Redis hash key'), fields: z.array(z.string()).describe('Array of field names to delete'), }, }, handler: async ({ key, fields }: { key: string; fields: string[] }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const deletedCount = await this.client.hDel(key, fields); return { content: [ { type: 'text', text: JSON.stringify({ key, fields, deletedCount }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_lpush this.registerTool({ context, name: 'redis_lpush', schema: { description: 'Prepend one or more values to a list (requires FULL mode)', inputSchema: { key: z.string().describe('Redis list key'), values: z.array(z.string()).describe('Array of values to prepend'), }, }, handler: async ({ key, values }: { key: string; values: string[] }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const newLength = await this.client.lPush(key, values); return { content: [ { type: 'text', text: JSON.stringify({ key, values, newLength }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_rpush this.registerTool({ context, name: 'redis_rpush', schema: { description: 'Append one or more values to a list (requires FULL mode)', inputSchema: { key: z.string().describe('Redis list key'), values: z.array(z.string()).describe('Array of values to append'), }, }, handler: async ({ key, values }: { key: string; values: string[] }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const newLength = await this.client.rPush(key, values); return { content: [ { type: 'text', text: JSON.stringify({ key, values, newLength }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_lrange this.registerTool({ context, name: 'redis_lrange', schema: { description: 'Get a range of elements from a list', inputSchema: { key: z.string().describe('Redis list key'), start: z.number().describe('Start index (0-based)'), stop: z.number().describe('Stop index (-1 for end of list)'), }, }, handler: async ({ key, start, stop }: { key: string; start: number; stop: number }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const values = await this.client.lRange(key, start, stop); return { content: [ { type: 'text', text: JSON.stringify({ key, start, stop, values, count: values.length }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); // Tool: redis_sadd this.registerTool({ context, name: 'redis_sadd', schema: { description: 'Add one or more members to a set (requires FULL mode)', inputSchema: { key: z.string().describe('Redis set key'), members: z.array(z.string()).describe('Array of members to add'), }, }, handler: async ({ key, members }: { key: string; members: string[] }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const addedCount = await this.client.sAdd(key, members); return { content: [ { type: 'text', text: JSON.stringify({ key, members, addedCount }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_smembers this.registerTool({ context, name: 'redis_smembers', schema: { description: 'Get all members in a set', inputSchema: { key: z.string().describe('Redis set key'), }, }, handler: async ({ key }: { key: string }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const members = await this.client.sMembers(key); return { content: [ { type: 'text', text: JSON.stringify({ key, members, count: members.length }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); // Tool: redis_srem this.registerTool({ context, name: 'redis_srem', schema: { description: 'Remove one or more members from a set (requires FULL mode)', inputSchema: { key: z.string().describe('Redis set key'), members: z.array(z.string()).describe('Array of members to remove'), }, }, handler: async ({ key, members }: { key: string; members: string[] }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const removedCount = await this.client.sRem(key, members); return { content: [ { type: 'text', text: JSON.stringify({ key, members, removedCount }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_zadd this.registerTool({ context, name: 'redis_zadd', schema: { description: 'Add one or more members to a sorted set (requires FULL mode)', inputSchema: { key: z.string().describe('Redis sorted set key'), members: z .array( z.object({ score: z.number().describe('Score for sorting'), value: z.string().describe('Member value'), }), ) .describe('Array of members with scores'), }, }, handler: async ({ key, members, }: { key: string; members: Array<{ score: number; value: string }>; }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const addedCount = await this.client.zAdd(key, members); return { content: [ { type: 'text', text: JSON.stringify({ key, members, addedCount }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, isWriteTool: true, }); // Tool: redis_zrange this.registerTool({ context, name: 'redis_zrange', schema: { description: 'Get a range of members from a sorted set by index', inputSchema: { key: z.string().describe('Redis sorted set key'), start: z.number().describe('Start index'), stop: z.number().describe('Stop index'), withScores: z.boolean().optional().describe('Include scores in response'), }, }, handler: async ({ key, start, stop, withScores, }: { key: string; start: number; stop: number; withScores?: boolean; }) => { try { if (!this.client) { throw new Error('Redis client not initialized'); } const result = withScores ? await this.client.zRangeWithScores(key, start, stop) : await this.client.zRange(key, start, stop); return { content: [ { type: 'text', text: JSON.stringify( { key, start, stop, result, count: Array.isArray(result) ? result.length : 0 }, null, 2, ), }, ], }; } catch (error) { return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], isError: true, }; } }, }); } /** * Cleanup Redis connection */ async cleanup(): Promise<void> { await super.cleanup(); if (this.client) { await this.client.quit(); } } /** * Health check */ async healthCheck(): Promise<boolean> { try { if (!this.client) { return false; } const result = await this.client.ping(); return result === 'PONG'; } catch { return false; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Nam088/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server