# Directory Structure ``` ├── .clinerules ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── jest.config.js ├── jest.setup.ts ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── evals │ │ └── evals.ts │ ├── interfaces │ │ └── types.ts │ ├── redis_server.ts │ └── tools │ ├── __tests__ │ │ └── scan_tool.test.ts │ ├── base_tool.ts │ ├── del_tool.ts │ ├── get_tool.ts │ ├── hget_tool.ts │ ├── hgetall_tool.ts │ ├── hmset_tool.ts │ ├── sadd_tool.ts │ ├── scan_tool.ts │ ├── set_tool.ts │ ├── smembers_tool.ts │ ├── tool_registry.ts │ ├── zadd_tool.ts │ ├── zrange_tool.ts │ ├── zrangebyscore_tool.ts │ └── zrem_tool.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` node_modules/ dist/ ``` -------------------------------------------------------------------------------- /.clinerules: -------------------------------------------------------------------------------- ``` This is a Typescript project is named redis-mcp, and is published as an npm package. It is a CLI tool starts nodejs server compatable with the Model Context Protocol (MCP) and provides access to a Redis database. See @https://modelcontextprotocol.io/introduction and https://modelcontextprotocol.io/docs/concepts/tools for an introduction to Model Context Protocol (MCP). Clients like Claude Desktop and Cline connect to this MCP server using configuration like: { "mcpServers": { "redis": { "command": "npx", "args": ["redis-mcp", "--redis-host", "localhost", "--redis-port", "6379"], "disabled": false } } } ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Redis MCP Server [](https://smithery.ai/server/redis-mcp) A Model Context Protocol (MCP) server that provides access to Redis database operations. <a href="https://glama.ai/mcp/servers/cbn7lsbp7h"><img width="380" height="200" src="https://glama.ai/mcp/servers/cbn7lsbp7h/badge" alt="Redis Server MCP server" /></a> ## Project Structure ``` src/ ├── interfaces/ │ └── types.ts # Shared TypeScript interfaces and types ├── tools/ │ ├── base_tool.ts # Abstract base class for Redis tools │ ├── tool_registry.ts # Registry managing all available Redis tools │ ├── hmset_tool.ts # HMSET Redis operation │ ├── hget_tool.ts # HGET Redis operation │ ├── hgetall_tool.ts # HGETALL Redis operation │ ├── scan_tool.ts # SCAN Redis operation │ ├── set_tool.ts # SET Redis operation │ ├── get_tool.ts # GET Redis operation │ ├── del_tool.ts # DEL Redis operation │ ├── zadd_tool.ts # ZADD Redis operation │ ├── zrange_tool.ts # ZRANGE Redis operation │ ├── zrangebyscore_tool.ts # ZRANGEBYSCORE Redis operation │ └── zrem_tool.ts # ZREM Redis operation └── redis_server.ts # Main server implementation ``` ## Available Tools | Tool | Type | Description | Input Schema | |------|------|-------------|--------------| | hmset | Hash Command | Set multiple hash fields to multiple values | `key`: string (Hash key)<br>`fields`: object (Field-value pairs to set) | | hget | Hash Command | Get the value of a hash field | `key`: string (Hash key)<br>`field`: string (Field to get) | | hgetall | Hash Command | Get all fields and values in a hash | `key`: string (Hash key) | | scan | Key Command | Scan Redis keys matching a pattern | `pattern`: string (Pattern to match, e.g., "user:*")<br>`count`: number, optional (Number of keys to return) | | set | String Command | Set string value with optional NX and PX options | `key`: string (Key to set)<br>`value`: string (Value to set)<br>`nx`: boolean, optional (Only set if not exists)<br>`px`: number, optional (Expiry in milliseconds) | | get | String Command | Get string value | `key`: string (Key to get) | | del | Key Command | Delete a key | `key`: string (Key to delete) | | zadd | Sorted Set Command | Add one or more members to a sorted set | `key`: string (Sorted set key)<br>`members`: array of objects with `score`: number and `value`: string | | zrange | Sorted Set Command | Return a range of members from a sorted set by index | `key`: string (Sorted set key)<br>`start`: number (Start index)<br>`stop`: number (Stop index)<br>`withScores`: boolean, optional (Include scores in output) | | zrangebyscore | Sorted Set Command | Return members from a sorted set with scores between min and max | `key`: string (Sorted set key)<br>`min`: number (Minimum score)<br>`max`: number (Maximum score)<br>`withScores`: boolean, optional (Include scores in output) | | zrem | Sorted Set Command | Remove one or more members from a sorted set | `key`: string (Sorted set key)<br>`members`: array of strings (Members to remove) | | sadd | Set Command | Add one or more members to a set | `key`: string (Set key)<br>`members`: array of strings (Members to add to the set) | | smembers | Set Command | Get all members in a set | `key`: string (Set key) | ## Usage Configure in your MCP client (e.g., Claude Desktop, Cline): ```json { "mcpServers": { "redis": { "command": "npx", "args": ["redis-mcp", "--redis-host", "localhost", "--redis-port", "6379"], "disabled": false } } } ``` ## Command Line Arguments - `--redis-host`: Redis server host (default: localhost) - `--redis-port`: Redis server port (default: 6379) ### Installing via Smithery To install Redis Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/redis-mcp): ```bash npx -y @smithery/cli install redis-mcp --client claude ``` ## Development To add a new Redis tool: 1. Create a new tool class in `src/tools/` extending `RedisTool` 2. Define the tool's interface in `src/interfaces/types.ts` 3. Register the tool in `src/tools/tool_registry.ts` Example tool implementation: ```typescript export class MyTool extends RedisTool { name = 'mytool'; description = 'Description of what the tool does'; inputSchema = { type: 'object', properties: { // Define input parameters }, required: ['requiredParam'] }; validateArgs(args: unknown): args is MyToolArgs { // Implement argument validation } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { // Implement tool logic } } ``` ## Running evals The evals package loads an mcp client that then runs the index.ts file, so there is no need to rebuild between tests. You can load environment variables by prefixing the npx command. Full documentation can be found [here](https://www.mcpevals.io/docs). ```bash OPENAI_API_KEY=your-key npx mcp-eval src/evals/evals.ts src/tools/zrangebyscore_tool.ts ``` ## License MIT: https://opensource.org/license/mit ``` -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- ```typescript import { jest } from '@jest/globals'; global.jest = jest; ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript /** @type {import('ts-jest').JestConfigWithTsJest} */ export default { preset: 'ts-jest/presets/default-esm', testEnvironment: 'node', extensionsToTreatAsEsm: ['.ts'], moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1', }, transform: { '^.+\\.tsx?$': ['ts-jest', { useESM: true, tsconfig: 'tsconfig.json' }], }, setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'] }; ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml startCommand: type: stdio configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: - redisHost - redisPort properties: redisHost: type: string description: The host address of the Redis server. redisPort: type: string description: The port number of the Redis server. commandFunction: # A function that produces the CLI command to start the MCP on stdio. |- (config) => ({ command: 'node', args: ['dist/redis_server.js', '--redis-host', config.redisHost, '--redis-port', config.redisPort] }) ``` -------------------------------------------------------------------------------- /src/tools/base_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { BaseTool, ToolResponse } from '../interfaces/types.js'; export abstract class RedisTool implements BaseTool { abstract name: string; abstract description: string; abstract inputSchema: object; abstract validateArgs(args: unknown): boolean; abstract execute(args: unknown, client: RedisClientType): Promise<ToolResponse>; protected createSuccessResponse(text: string): ToolResponse { return { content: [{ type: 'text', text }] }; } protected createErrorResponse(error: string): ToolResponse { return { content: [{ type: 'text', text: error }], _meta: { error: true } }; } } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile # Use a Node.js image as the base image FROM node:20-alpine AS builder # Set the working directory WORKDIR /app # Copy package.json and package-lock.json to the working directory COPY package.json package-lock.json ./ # Install dependencies RUN npm install # Copy the rest of the application code to the working directory COPY src ./src COPY tsconfig.json ./ # Build the TypeScript files RUN npm run build # Create the release image FROM node:20-alpine # Set the working directory WORKDIR /app # Copy the built files from the builder stage COPY --from=builder /app/dist ./dist COPY --from=builder /app/package.json ./ # Install only production dependencies RUN npm ci --omit=dev # Set the command to run the application ENTRYPOINT ["node", "dist/redis_server.js"] ``` -------------------------------------------------------------------------------- /src/tools/get_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { GetArgs, ToolResponse } from '../interfaces/types.js'; export class GetTool extends RedisTool { name = 'get'; description = 'Get string value'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Key to get' } }, required: ['key'] }; validateArgs(args: unknown): args is GetArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string'; } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for get'); } try { const value = await client.get(args.key); if (value === null) { return this.createSuccessResponse('Key not found'); } return this.createSuccessResponse(value); } catch (error) { return this.createErrorResponse(`Failed to get key: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/del_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { DelArgs, ToolResponse } from '../interfaces/types.js'; export class DelTool extends RedisTool { name = 'del'; description = 'Delete a key'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Key to delete' } }, required: ['key'] }; validateArgs(args: unknown): args is DelArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string'; } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for del'); } try { const count = await client.del(args.key); if (count === 0) { return this.createSuccessResponse('Key did not exist'); } return this.createSuccessResponse('Key deleted'); } catch (error) { return this.createErrorResponse(`Failed to delete key: ${error}`); } } } ``` -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.0.4] - 2025-01-06 ### Changed - Limited SCAN command results to maximum 10 keys for better performance - Added comprehensive unit tests for SCAN command ## [0.0.3] - 2025-01-06 ### Added - String Commands - GET: Retrieve string values - SET: Store string values - Set Commands - SADD: Add members to a set - SMEMBERS: Get all members of a set - Sorted Set Commands - ZADD: Add members with scores to a sorted set - ZRANGE: Get range of members from a sorted set - ZRANGEBYSCORE: Get members within a score range - ZREM: Remove members from a sorted set ## [0.0.2] - Initial Release - Hash Commands - HGET: Get value of a hash field - HGETALL: Get all fields and values in a hash - HMSET: Set multiple hash fields to multiple values - Key Commands - DEL: Delete keys - SCAN: Iterate over keys matching a pattern ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "redis-mcp", "version": "0.0.4", "type": "module", "main": "dist/redis_server.js", "bin": { "redis-mcp": "dist/redis_server.js" }, "scripts": { "build": "tsc && node -e \"require('fs').chmodSync('dist/redis_server.js', '755')\"", "start": "node dist/redis_server.js", "dev": "NODE_OPTIONS=\"--loader ts-node/esm\" node src/redis_server.ts", "test": "NODE_OPTIONS=--experimental-vm-modules jest", "prepublishOnly": "npm run build" }, "author": "fahrankaz", "license": "ISC", "description": "Redis MCP server providing tools and resources for interacting with Redis databases through the Model Context Protocol", "repository": { "type": "git", "url": "https://github.com/farhankaz/redis-mcp" }, "keywords": [ "mcp", "redis", "database", "cli" ], "dependencies": { "@modelcontextprotocol/sdk": "^1.0.4", "mcp-evals": "^1.0.18", "redis": "^4.6.12" }, "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "^20.10.6", "jest": "^29.7.0", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", "typescript": "^5.3.3" } } ``` -------------------------------------------------------------------------------- /src/tools/smembers_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { SMembersArgs, ToolResponse } from '../interfaces/types.js'; export class SMembersTool extends RedisTool { name = 'smembers'; description = 'Get all members in a set'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Set key' } }, required: ['key'] }; validateArgs(args: unknown): args is SMembersArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string'; } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for smembers'); } try { const members = await client.sMembers(args.key); if (members.length === 0) { return this.createSuccessResponse('Set is empty or does not exist'); } return this.createSuccessResponse(JSON.stringify(members, null, 2)); } catch (error) { return this.createErrorResponse(`Failed to get set members: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/hgetall_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { HGetAllArgs, ToolResponse } from '../interfaces/types.js'; export class HGetAllTool extends RedisTool { name = 'hgetall'; description = 'Get all the fields and values in a hash'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Hash key' } }, required: ['key'] }; validateArgs(args: unknown): args is HGetAllArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string'; } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for hgetall'); } try { const value = await client.hGetAll(args.key); if (Object.keys(value).length === 0) { return this.createSuccessResponse('Hash not found or empty'); } return this.createSuccessResponse(JSON.stringify(value, null, 2)); } catch (error) { return this.createErrorResponse(`Failed to get hash: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/hget_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { HGetArgs, ToolResponse } from '../interfaces/types.js'; export class HGetTool extends RedisTool { name = 'hget'; description = 'Get the value of a hash field'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Hash key' }, field: { type: 'string', description: 'Field to get' } }, required: ['key', 'field'] }; validateArgs(args: unknown): args is HGetArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string' && 'field' in args && typeof (args as any).field === 'string'; } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for hget'); } try { const value = await client.hGet(args.key, args.field); if (value === null || value === undefined) { return this.createSuccessResponse('Field not found'); } return this.createSuccessResponse(value); } catch (error) { return this.createErrorResponse(`Failed to get hash field: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/hmset_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { HMSetArgs, ToolResponse } from '../interfaces/types.js'; export class HMSetTool extends RedisTool { name = 'hmset'; description = 'Set multiple hash fields to multiple values'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Hash key' }, fields: { type: 'object', description: 'Field-value pairs to set', additionalProperties: { type: 'string' } } }, required: ['key', 'fields'] }; validateArgs(args: unknown): args is HMSetArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string' && 'fields' in args && typeof (args as any).fields === 'object'; } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for hmset'); } try { await client.hSet(args.key, args.fields); return this.createSuccessResponse('Hash fields set successfully'); } catch (error) { return this.createErrorResponse(`Failed to set hash fields: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/sadd_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { SAddArgs, ToolResponse } from '../interfaces/types.js'; export class SAddTool extends RedisTool { name = 'sadd'; description = 'Add one or more members to a set'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Set key' }, members: { type: 'array', items: { type: 'string' }, description: 'Members to add to the set' } }, required: ['key', 'members'] }; validateArgs(args: unknown): args is SAddArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string' && 'members' in args && Array.isArray((args as any).members) && (args as any).members.every((member: unknown) => typeof member === 'string'); } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for sadd'); } try { const result = await client.sAdd(args.key, args.members); return this.createSuccessResponse(`Added ${result} new member(s) to the set`); } catch (error) { return this.createErrorResponse(`Failed to add members to set: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/zrem_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { ZRemArgs, ToolResponse } from '../interfaces/types.js'; export class ZRemTool extends RedisTool { name = 'zrem'; description = 'Remove one or more members from a sorted set'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Sorted set key' }, members: { type: 'array', description: 'Array of members to remove', items: { type: 'string' } } }, required: ['key', 'members'] }; validateArgs(args: unknown): args is ZRemArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string' && 'members' in args && Array.isArray((args as any).members) && (args as any).members.every((member: any) => typeof member === 'string'); } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for zrem'); } try { const result = await client.zRem(args.key, args.members); return this.createSuccessResponse(`Removed ${result} members from the sorted set`); } catch (error) { return this.createErrorResponse(`Failed to remove members from sorted set: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/tool_registry.ts: -------------------------------------------------------------------------------- ```typescript import { BaseTool } from '../interfaces/types.js'; import { HMSetTool } from './hmset_tool.js'; import { HGetTool } from './hget_tool.js'; import { HGetAllTool } from './hgetall_tool.js'; import { ScanTool } from './scan_tool.js'; import { SetTool } from './set_tool.js'; import { GetTool } from './get_tool.js'; import { DelTool } from './del_tool.js'; import { ZAddTool } from './zadd_tool.js'; import { ZRangeTool } from './zrange_tool.js'; import { ZRangeByScoreTool } from './zrangebyscore_tool.js'; import { ZRemTool } from './zrem_tool.js'; import { SAddTool } from './sadd_tool.js'; import { SMembersTool } from './smembers_tool.js'; export class ToolRegistry { private tools: Map<string, BaseTool>; constructor() { this.tools = new Map(); this.registerDefaultTools(); } private registerDefaultTools() { const defaultTools = [ new HMSetTool(), new HGetTool(), new HGetAllTool(), new ScanTool(), new SetTool(), new GetTool(), new DelTool(), new ZAddTool(), new ZRangeTool(), new ZRangeByScoreTool(), new ZRemTool(), new SAddTool(), new SMembersTool(), ]; for (const tool of defaultTools) { this.registerTool(tool); } } registerTool(tool: BaseTool) { this.tools.set(tool.name, tool); } getTool(name: string): BaseTool | undefined { return this.tools.get(name); } getAllTools(): BaseTool[] { return Array.from(this.tools.values()); } hasToolWithName(name: string): boolean { return this.tools.has(name); } } ``` -------------------------------------------------------------------------------- /src/interfaces/types.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType as RedisClient } from 'redis'; // Extend RedisClientType to handle the specific client type export type RedisClientType = RedisClient<any, any, any>; export interface HMSetArgs { key: string; fields: Record<string, string>; } export interface HGetArgs { key: string; field: string; } export interface HGetAllArgs { key: string; } export interface HSetArgs { key: string; field: string; value: string; } export interface SetArgs { key: string; value: string; nx?: boolean; px?: number; } export interface GetArgs { key: string; } export interface DelArgs { key: string; } export interface ScanArgs { pattern: string; count?: number; } export interface ZAddArgs { key: string; members: Array<{score: number; value: string}>; } export interface ZRangeArgs { key: string; start: number; stop: number; withScores?: boolean; } export interface ZRangeByScoreArgs { key: string; min: number; max: number; withScores?: boolean; } export interface ZRemArgs { key: string; members: string[]; } export interface SAddArgs { key: string; members: string[]; } export interface SMembersArgs { key: string; } // Update ToolResponse to match MCP SDK expectations export interface ToolResponse { content: Array<{ type: string; text: string; }>; _meta?: Record<string, unknown>; } export interface BaseTool { name: string; description: string; inputSchema: object; validateArgs(args: unknown): boolean; execute(args: unknown, client: RedisClientType): Promise<ToolResponse>; } ``` -------------------------------------------------------------------------------- /src/tools/scan_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { ScanArgs, ToolResponse } from '../interfaces/types.js'; export class ScanTool extends RedisTool { name = 'scan'; description = 'Scan Redis keys matching a pattern'; inputSchema = { type: 'object', properties: { pattern: { type: 'string', description: 'Pattern to match (e.g., "user:*" or "schedule:*")' }, count: { type: 'number', description: 'Number of keys to return per iteration (optional)', minimum: 1 } }, required: ['pattern'] }; validateArgs(args: unknown): args is ScanArgs { return typeof args === 'object' && args !== null && 'pattern' in args && typeof (args as any).pattern === 'string' && (!('count' in args) || typeof (args as any).count === 'number'); } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for scan'); } try { const { pattern, count = 100 } = args; const keys = await client.keys(pattern); if (keys.length === 0) { return this.createSuccessResponse('No keys found matching pattern'); } // Limit keys to at most 10, or less if count is specified and smaller const maxKeys = Math.min(count || 10, 10); const limitedKeys = keys.slice(0, maxKeys); return this.createSuccessResponse(JSON.stringify(limitedKeys, null, 2)); } catch (error) { return this.createErrorResponse(`Failed to scan keys: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/set_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { SetArgs, ToolResponse } from '../interfaces/types.js'; export class SetTool extends RedisTool { name = 'set'; description = 'Set string value with optional NX (only if not exists) and PX (expiry in milliseconds) options'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Key to set' }, value: { type: 'string', description: 'Value to set' }, nx: { type: 'boolean', description: 'Only set if key does not exist' }, px: { type: 'number', description: 'Set expiry in milliseconds' } }, required: ['key', 'value'] }; validateArgs(args: unknown): args is SetArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string' && 'value' in args && typeof (args as any).value === 'string' && (!('nx' in args) || typeof (args as any).nx === 'boolean') && (!('px' in args) || typeof (args as any).px === 'number'); } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for set'); } try { const options: any = {}; if (args.nx) { options.NX = true; } if (args.px) { options.PX = args.px; } const result = await client.set(args.key, args.value, options); if (result === null) { return this.createSuccessResponse('Key not set (NX condition not met)'); } return this.createSuccessResponse('OK'); } catch (error) { return this.createErrorResponse(`Failed to set key: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/zadd_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { ZAddArgs, ToolResponse } from '../interfaces/types.js'; export class ZAddTool extends RedisTool { name = 'zadd'; description = 'Add one or more members to a sorted set'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Sorted set key' }, members: { type: 'array', description: 'Array of score-value pairs to add', items: { type: 'object', properties: { score: { type: 'number', description: 'Score for the member' }, value: { type: 'string', description: 'Member value' } }, required: ['score', 'value'] } } }, required: ['key', 'members'] }; validateArgs(args: unknown): args is ZAddArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string' && 'members' in args && Array.isArray((args as any).members) && (args as any).members.every((member: any) => typeof member === 'object' && member !== null && 'score' in member && typeof member.score === 'number' && 'value' in member && typeof member.value === 'string' ); } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for zadd'); } try { const members = args.members.map(member => ({ score: member.score, value: member.value })); const result = await client.zAdd(args.key, members); return this.createSuccessResponse(`Added ${result} new members to the sorted set`); } catch (error) { return this.createErrorResponse(`Failed to add members to sorted set: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/zrange_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { ZRangeArgs, ToolResponse } from '../interfaces/types.js'; export class ZRangeTool extends RedisTool { name = 'zrange'; description = 'Return a range of members from a sorted set by index'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Sorted set key' }, start: { type: 'number', description: 'Start index (0-based)' }, stop: { type: 'number', description: 'Stop index (inclusive)' }, withScores: { type: 'boolean', description: 'Include scores in output', default: false } }, required: ['key', 'start', 'stop'] }; validateArgs(args: unknown): args is ZRangeArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string' && 'start' in args && typeof (args as any).start === 'number' && 'stop' in args && typeof (args as any).stop === 'number' && (!('withScores' in args) || typeof (args as any).withScores === 'boolean'); } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for zrange'); } try { const result = await client.sendCommand([ 'ZRANGE', args.key, args.start.toString(), args.stop.toString(), ...(args.withScores ? ['WITHSCORES'] : []) ]) as string[]; if (!Array.isArray(result) || result.length === 0) { return this.createSuccessResponse('No members found in the specified range'); } if (args.withScores) { // Format result with scores when WITHSCORES is used const pairs = []; for (let i = 0; i < result.length; i += 2) { pairs.push(`${result[i]} (score: ${result[i + 1]})`); } return this.createSuccessResponse(pairs.join('\n')); } return this.createSuccessResponse(result.join('\n')); } catch (error) { return this.createErrorResponse(`Failed to get range from sorted set: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/tools/zrangebyscore_tool.ts: -------------------------------------------------------------------------------- ```typescript import { RedisClientType } from 'redis'; import { RedisTool } from './base_tool.js'; import { ZRangeByScoreArgs, ToolResponse } from '../interfaces/types.js'; export class ZRangeByScoreTool extends RedisTool { name = 'zrangebyscore'; description = 'Return members from a sorted set with scores between min and max'; inputSchema = { type: 'object', properties: { key: { type: 'string', description: 'Sorted set key' }, min: { type: 'number', description: 'Minimum score' }, max: { type: 'number', description: 'Maximum score' }, withScores: { type: 'boolean', description: 'Include scores in output', default: false } }, required: ['key', 'min', 'max'] }; validateArgs(args: unknown): args is ZRangeByScoreArgs { return typeof args === 'object' && args !== null && 'key' in args && typeof (args as any).key === 'string' && 'min' in args && typeof (args as any).min === 'number' && 'max' in args && typeof (args as any).max === 'number' && (!('withScores' in args) || typeof (args as any).withScores === 'boolean'); } async execute(args: unknown, client: RedisClientType): Promise<ToolResponse> { if (!this.validateArgs(args)) { return this.createErrorResponse('Invalid arguments for zrangebyscore'); } try { const result = await client.sendCommand([ 'ZRANGEBYSCORE', args.key, args.min.toString(), args.max.toString(), ...(args.withScores ? ['WITHSCORES'] : []) ]) as string[]; if (!Array.isArray(result) || result.length === 0) { return this.createSuccessResponse('No members found in the specified score range'); } if (args.withScores) { // Format result with scores when WITHSCORES is used const pairs = []; for (let i = 0; i < result.length; i += 2) { pairs.push(`${result[i]} (score: ${result[i + 1]})`); } return this.createSuccessResponse(pairs.join('\n')); } return this.createSuccessResponse(result.join('\n')); } catch (error) { return this.createErrorResponse(`Failed to get range by score from sorted set: ${error}`); } } } ``` -------------------------------------------------------------------------------- /src/evals/evals.ts: -------------------------------------------------------------------------------- ```typescript //evals.ts import { EvalConfig } from 'mcp-evals'; import { openai } from "@ai-sdk/openai"; import { grade, EvalFunction } from "mcp-evals"; const ZRangeByScoreToolEval: EvalFunction = { name: 'ZRangeByScoreTool Evaluation', description: 'Evaluates retrieving members from a sorted set by score range', run: async () => { const result = await grade(openai("gpt-4"), "Retrieve all members from the sorted set 'mySortedSet' with scores between 10 and 30, including their scores"); return JSON.parse(result); } }; const zaddEval: EvalFunction = { name: 'ZAddTool', description: 'Evaluates the functionality of adding members to a sorted set', run: async () => { const result = await grade(openai("gpt-4"), "Add the members {score: 1, value: 'Alice'} and {score: 2, value: 'Bob'} to the sorted set 'leaderboard'."); return JSON.parse(result); } }; const zremEval: EvalFunction = { name: 'zremEval', description: 'Evaluates removing members from a sorted set using ZRemTool', run: async () => { const result = await grade(openai("gpt-4"), "Remove the members 'alex' and 'sara' from the sorted set 'myzset'."); return JSON.parse(result); } }; const hmsetEval: EvalFunction = { name: 'hmset Tool Evaluation', description: 'Evaluates the functionality of setting multiple fields in a Redis hash using hmset', run: async () => { const result = await grade(openai("gpt-4"), "Use the hmset tool to set the fields 'name'='John Doe' and 'email'='[email protected]' on a Redis hash key 'user:42'."); return JSON.parse(result); } }; const hgetallEval: EvalFunction = { name: 'HGetAllTool Evaluation', description: 'Evaluates the functionality of the hgetall tool', run: async () => { const result = await grade(openai("gpt-4"), "Retrieve all fields and values from the Redis hash with key 'myHash' using the hgetall tool."); return JSON.parse(result); } }; const config: EvalConfig = { model: openai("gpt-4"), evals: [ZRangeByScoreToolEval, zaddEval, zremEval, hmsetEval, hgetallEval] }; export default config; export const evals = [ZRangeByScoreToolEval, zaddEval, zremEval, hmsetEval, hgetallEval]; ``` -------------------------------------------------------------------------------- /src/redis_server.ts: -------------------------------------------------------------------------------- ```typescript #!/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 { createClient } from 'redis'; import { ToolRegistry } from './tools/tool_registry.js'; import { RedisClientType } from './interfaces/types.js'; // Parse command line arguments let redisHost = 'localhost'; let redisPort = 6379; for (let i = 2; i < process.argv.length; i++) { if (process.argv[i] === '--redis-host') { if (!process.argv[i + 1] || process.argv[i + 1].trim() === '') { console.error('Error: --redis-host requires a non-empty value'); process.exit(1); } redisHost = process.argv[i + 1].trim(); i++; } else if (process.argv[i] === '--redis-port') { if (!process.argv[i + 1]) { console.error('Error: --redis-port requires a numeric value'); process.exit(1); } const port = parseInt(process.argv[i + 1], 10); if (isNaN(port) || port < 1 || port > 65535) { console.error('Error: --redis-port must be a valid port number between 1 and 65535'); process.exit(1); } redisPort = port; i++; } } class RedisServer { private server: Server; private redisClient: RedisClientType; private toolRegistry: ToolRegistry; constructor() { this.server = new Server( { name: "redis-server", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); this.redisClient = createClient({ url: `redis://${redisHost}:${redisPort}` }); this.toolRegistry = new ToolRegistry(); this.setupToolHandlers(); this.server.onerror = (error) => console.error('[MCP Error]', error); // Handle process termination const cleanup = async () => { try { if (this.redisClient.isOpen) { await this.redisClient.quit(); } await this.server.close(); } catch (error) { console.error('Error during cleanup:', error); } process.exit(0); }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: this.toolRegistry.getAllTools().map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })) })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { await this.ensureConnected(); const tool = this.toolRegistry.getTool(request.params.name); if (!tool) { return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], _meta: { error: true } }; } try { return await tool.execute(request.params.arguments, this.redisClient); } catch (error) { return { content: [{ type: 'text', text: `Error executing tool: ${error}` }], _meta: { error: true } }; } }); } private async ensureConnected() { if (!this.redisClient.isOpen) { await this.redisClient.connect(); } } async run() { try { const transport = new StdioServerTransport(); await this.server.connect(transport); console.info(`Redis MCP server is running and connected to Redis using url redis://${redisHost}:${redisPort}`); } catch (error) { console.error('Error starting server:', error); process.exit(1); } } } const server = new RedisServer(); server.run().catch(console.error); ``` -------------------------------------------------------------------------------- /src/tools/__tests__/scan_tool.test.ts: -------------------------------------------------------------------------------- ```typescript import { ScanTool } from '../scan_tool.js'; import { RedisClientType } from 'redis'; import { jest, describe, it, expect, beforeEach } from '@jest/globals'; describe('ScanTool', () => { let scanTool: ScanTool; let mockRedisClient: jest.Mocked<RedisClientType>; beforeEach(() => { scanTool = new ScanTool(); mockRedisClient = { keys: jest.fn(), } as unknown as jest.Mocked<RedisClientType>; }); describe('validateArgs', () => { it('should return true for valid arguments with pattern only', () => { expect(scanTool.validateArgs({ pattern: 'user:*' })).toBe(true); }); it('should return true for valid arguments with pattern and count', () => { expect(scanTool.validateArgs({ pattern: 'user:*', count: 5 })).toBe(true); }); it('should return false for missing pattern', () => { expect(scanTool.validateArgs({})).toBe(false); }); it('should return false for invalid count type', () => { expect(scanTool.validateArgs({ pattern: 'user:*', count: '5' })).toBe(false); }); it('should return false for null input', () => { expect(scanTool.validateArgs(null)).toBe(false); }); }); describe('execute', () => { it('should return error for invalid arguments', async () => { const result = await scanTool.execute({}, mockRedisClient); expect(result._meta?.error).toBe(true); expect(result.content[0].text).toBe('Invalid arguments for scan'); }); it('should return success message for no matching keys', async () => { mockRedisClient.keys.mockResolvedValue([]); const result = await scanTool.execute({ pattern: 'user:*' }, mockRedisClient); expect(result._meta?.error).toBeUndefined(); expect(result.content[0].text).toBe('No keys found matching pattern'); }); it('should limit results to 10 keys when more exist', async () => { const keys = Array.from({ length: 20 }, (_, i) => `key${i}`); mockRedisClient.keys.mockResolvedValue(keys); const result = await scanTool.execute({ pattern: 'key*' }, mockRedisClient); const parsedKeys = JSON.parse(result.content[0].text); expect(result._meta?.error).toBeUndefined(); expect(parsedKeys.length).toBe(10); expect(parsedKeys).toEqual(keys.slice(0, 10)); }); it('should respect count parameter when less than 10', async () => { const keys = Array.from({ length: 20 }, (_, i) => `key${i}`); mockRedisClient.keys.mockResolvedValue(keys); const result = await scanTool.execute({ pattern: 'key*', count: 5 }, mockRedisClient); const parsedKeys = JSON.parse(result.content[0].text); expect(result._meta?.error).toBeUndefined(); expect(parsedKeys.length).toBe(5); expect(parsedKeys).toEqual(keys.slice(0, 5)); }); it('should limit to 10 keys when count is greater than 10', async () => { const keys = Array.from({ length: 20 }, (_, i) => `key${i}`); mockRedisClient.keys.mockResolvedValue(keys); const result = await scanTool.execute({ pattern: 'key*', count: 15 }, mockRedisClient); const parsedKeys = JSON.parse(result.content[0].text); expect(result._meta?.error).toBeUndefined(); expect(parsedKeys.length).toBe(10); expect(parsedKeys).toEqual(keys.slice(0, 10)); }); it('should handle Redis client errors', async () => { const error = new Error('Redis connection failed'); mockRedisClient.keys.mockRejectedValue(error); const result = await scanTool.execute({ pattern: 'key*' }, mockRedisClient); expect(result._meta?.error).toBe(true); expect(result.content[0].text).toBe('Failed to scan keys: Error: Redis connection failed'); }); }); }); ```