#
tokens: 11563/50000 29/29 files
lines: off (toggle) GitHub
raw markdown copy
# 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
[![smithery badge](https://smithery.ai/badge/redis-mcp)](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');
    });
  });
});
```