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