我开发了一个 Redis MCP 服务,AI 直接操作缓存,排查问题快人一步!

0 阅读6分钟

我开发了一个 Redis MCP 服务,AI 直接操作缓存,排查问题快人一步!

项目地址github.com/wenit/redis…

让 AI 助手直接连接 Redis,查询缓存、分析性能、清理数据,一站式搞定。

一、为什么我开发了它?

作为后端开发,Redis 是我每天都在用的工具:

  • 🔍 查看用户 Token 缓存是否存在
  • ⏱️ 检查缓存过期时间设置是否正确
  • 📊 分析缓存命中率
  • 🧹 清理特定前缀的测试缓存
  • 📈 监控 Redis 内存和性能

以前的痛苦

  1. 打开终端
  2. 输入 redis-cli -h xxx -p xxx -a xxx
  3. 执行命令
  4. 复制结果给 AI 看
  5. AI 说"能不能再看看这个 key"...
  6. 重复步骤 3-5

现在的高效

我:用户 10086 的 token 缓存还有多久过期?
AI:[直接查询] → 还剩 1800 秒

我:所有 user:* 的 key 有哪些?
AI:[直接扫描] → 找到 156 个 key

二、什么是 MCP?

MCP(Model Context Protocol)是 Anthropic 推出的开放协议,可以理解为 AI 的 USB-C 接口

特性说明
标准化统一的数据交换格式
即插即用一次开发,Claude、Kimi、GPT 都能用
双向通信AI 能读数据,也能执行操作

简单说,MCP 让 AI 从"聊天"升级为"动手"。

三、16 个工具全覆盖

我实现了 Redis 5 大数据类型 + 管理操作,共 16 个工具:

📝 字符串(最常用)

工具功能
get获取键值
set设置键值,支持过期时间、NX 条件
del删除一个或多个键

🗂️ 哈希(存储对象)

工具功能
hgetall获取哈希表所有字段
hmset批量设置哈希字段

📋 列表(队列/栈)

工具功能
lrange获取列表范围元素
lpush左侧推入(栈)
lpop左侧弹出

🎯 集合(去重)

工具功能
smembers获取所有成员
sadd添加成员

🏆 有序集合(排行榜)

工具功能
zrange按排名获取成员(带分数)
zadd添加成员(带分数)

⚙️ 管理操作

工具功能
keys查找匹配模式的键(SCAN 实现,不阻塞)
expire设置过期时间
ttl查看剩余时间
info服务器信息(内存、命中率等)
ping测试连接

四、核心代码实现

4.1 项目结构

redis-mcp-server/
├── src/
│   ├── index.ts          # 服务入口
│   ├── redis.ts          # Redis 客户端
│   └── tools/
│       ├── string.ts     # 字符串操作
│       ├── hash.ts       # 哈希操作
│       ├── list.ts       # 列表操作
│       ├── set.ts        # 集合操作
│       ├── zset.ts       # 有序集合操作
│       └── admin.ts      # 管理操作
├── package.json
└── tsconfig.json

4.2 Redis 连接

// src/redis.ts
import Redis from 'ioredis';

export const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD || undefined,
  db: parseInt(process.env.REDIS_DB || '0'),
  retryStrategy: (times) => {
    const delay = Math.min(times * 50, 2000);
    return delay;
  }
});

redis.on('error', (err) => {
  console.error('Redis connection error:', err);
});

4.3 字符串操作(get/set/del)

// src/tools/string.ts
import { redis } from '../redis';

export const stringTools = [
  {
    name: 'get',
    description: '获取指定键的字符串值',
    inputSchema: {
      type: 'object',
      properties: {
        key: { type: 'string', description: '键名' }
      },
      required: ['key']
    },
    async handler(args: { key: string }) {
      const value = await redis.get(args.key);
      return {
        content: [{
          type: 'text',
          text: value !== null ? value : '(nil)'
        }]
      };
    }
  },
  {
    name: 'set',
    description: '设置字符串值,支持过期时间和条件选项',
    inputSchema: {
      type: 'object',
      properties: {
        key: { type: 'string', description: '键名' },
        value: { type: 'string', description: '值' },
        ex: { type: 'number', description: '过期时间(秒)' },
        nx: { type: 'boolean', description: '仅在键不存在时才设置(NX选项)' }
      },
      required: ['key', 'value']
    },
    async handler(args: { key: string; value: string; ex?: number; nx?: boolean }) {
      const options: string[] = [];
      if (args.ex) options.push('EX', String(args.ex));
      if (args.nx) options.push('NX');
      
      const result = await redis.set(args.key, args.value, ...options);
      return {
        content: [{
          type: 'text',
          text: result === 'OK' ? 'OK' : result === null ? '(nil)' : String(result)
        }]
      };
    }
  },
  {
    name: 'del',
    description: '删除一个或多个键',
    inputSchema: {
      type: 'object',
      properties: {
        keys: { 
          type: 'array', 
          description: '要删除的键名列表',
          items: { type: 'string' }
        }
      },
      required: ['keys']
    },
    async handler(args: { keys: string[] }) {
      const result = await redis.del(...args.keys);
      return {
        content: [{ type: 'text', text: `(integer) ${result}` }]
      };
    }
  }
];

4.4 哈希操作(存储对象)

// src/tools/hash.ts
import { redis } from '../redis';

export const hashTools = [
  {
    name: 'hgetall',
    description: '获取哈希表中所有字段和值',
    inputSchema: {
      type: 'object',
      properties: {
        key: { type: 'string', description: '哈希键名' }
      },
      required: ['key']
    },
    async handler(args: { key: string }) {
      const result = await redis.hgetall(args.key);
      const formatted = Object.keys(result).length > 0 
        ? JSON.stringify(result, null, 2)
        : '(empty hash)';
      return { content: [{ type: 'text', text: formatted }] };
    }
  },
  {
    name: 'hmset',
    description: '批量设置哈希表的字段值',
    inputSchema: {
      type: 'object',
      properties: {
        key: { type: 'string', description: '哈希键名' },
        fields: { 
          type: 'object', 
          description: '字段值对象,key为字段名,value为字段值'
        }
      },
      required: ['key', 'fields']
    },
    async handler(args: { key: string; fields: Record<string, string> }) {
      const entries = Object.entries(args.fields).flat();
      const result = await redis.hmset(args.key, ...entries);
      return { content: [{ type: 'text', text: result }] };
    }
  }
];

4.5 列表操作(队列)

// src/tools/list.ts
import { redis } from '../redis';

export const listTools = [
  {
    name: 'lrange',
    description: '获取列表指定范围的元素',
    inputSchema: {
      type: 'object',
      properties: {
        key: { type: 'string', description: '列表键名' },
        start: { type: 'number', description: '起始索引(从0开始)', default: 0 },
        stop: { type: 'number', description: '结束索引(-1表示到最后)', default: -1 }
      },
      required: ['key']
    },
    async handler(args: { key: string; start?: number; stop?: number }) {
      const result = await redis.lrange(args.key, args.start || 0, args.stop ?? -1);
      const formatted = result.length > 0 
        ? result.map((v, i) => `${i + 1}) "${v}"`).join('\n')
        : '(empty list)';
      return { content: [{ type: 'text', text: formatted }] };
    }
  },
  {
    name: 'lpush',
    description: '将一个或多个值从左侧推入列表',
    inputSchema: {
      type: 'object',
      properties: {
        key: { type: 'string', description: '列表键名' },
        values: { 
          type: 'array', 
          description: '要推入的值列表',
          items: { type: 'string' }
        }
      },
      required: ['key', 'values']
    },
    async handler(args: { key: string; values: string[] }) {
      const result = await redis.lpush(args.key, ...args.values);
      return { content: [{ type: 'text', text: `(integer) ${result}` }] };
    }
  },
  {
    name: 'lpop',
    description: '从列表左侧弹出一个元素',
    inputSchema: {
      type: 'object',
      properties: {
        key: { type: 'string', description: '列表键名' }
      },
      required: ['key']
    },
    async handler(args: { key: string }) {
      const result = await redis.lpop(args.key);
      return { content: [{ type: 'text', text: result !== null ? `"${result}"` : '(nil)' }] };
    }
  }
];

4.6 有序集合(排行榜)

// src/tools/zset.ts
import { redis } from '../redis';

export const zsetTools = [
  {
    name: 'zrange',
    description: '获取有序集合指定排名范围的成员(按分数从小到大排序)',
    inputSchema: {
      type: 'object',
      properties: {
        key: { type: 'string', description: '有序集合键名' },
        start: { type: 'number', description: '起始排名(从0开始)', default: 0 },
        stop: { type: 'number', description: '结束排名(-1表示到最后)', default: -1 },
        withscores: { type: 'boolean', description: '是否同时返回分数', default: false }
      },
      required: ['key']
    },
    async handler(args: { key: string; start?: number; stop?: number; withscores?: boolean }) {
      const result = args.withscores
        ? await redis.zrange(args.key, args.start || 0, args.stop ?? -1, 'WITHSCORES')
        : await redis.zrange(args.key, args.start || 0, args.stop ?? -1);
      
      let formatted;
      if (args.withscores) {
        const pairs = [];
        for (let i = 0; i < result.length; i += 2) {
          pairs.push(`"${result[i]}" -> ${result[i + 1]}`);
        }
        formatted = pairs.join('\n') || '(empty zset)';
      } else {
        formatted = result.length > 0 
          ? result.map(v => `"${v}"`).join('\n')
          : '(empty zset)';
      }
      
      return { content: [{ type: 'text', text: formatted }] };
    }
  },
  {
    name: 'zadd',
    description: '向有序集合中添加成员(带分数)',
    inputSchema: {
      type: 'object',
      properties: {
        key: { type: 'string', description: '有序集合键名' },
        members: {
          type: 'array',
          description: '成员列表,每个成员包含值和分数',
          items: {
            type: 'object',
            properties: {
              member: { type: 'string', description: '成员值' },
              score: { type: 'number', description: '分数' }
            },
            required: ['member', 'score']
          }
        }
      },
      required: ['key', 'members']
    },
    async handler(args: { key: string; members: { member: string; score: number }[] }) {
      const entries = args.members.flatMap(m => [String(m.score), m.member]);
      const result = await redis.zadd(args.key, ...entries);
      return { content: [{ type: 'text', text: `(integer) ${result}` }] };
    }
  }
];

4.7 安全的 keys 查询

生产环境不能用 KEYS *,必须用 SCAN

// src/tools/admin.ts
export const keysTool = {
  name: 'keys',
  description: '查找匹配模式的键(使用SCAN实现,不会阻塞服务器)',
  inputSchema: {
    type: 'object',
    properties: {
      pattern: { type: 'string', description: '匹配模式,如 user:*', default: '*' },
      count: { type: 'number', description: '每次迭代返回的大致数量' }
    }
  },
  async handler(args: { pattern?: string; count?: number }) {
    const stream = redis.scanStream({
      match: args.pattern || '*',
      count: args.count || 100
    });
    
    const keys = [];
    for await (const chunk of stream) {
      keys.push(...chunk);
    }
    
    return {
      content: [{
        type: 'text',
        text: keys.length > 0 
          ? `找到 ${keys.length} 个键:\n${keys.join('\n')}`
          : '(empty set)'
      }]
    };
  }
};

五、真实使用场景

场景 1:查看用户缓存

我:用户 10086 的 token 是什么?

AI:[调用 getkey="user:10086:token"]
→ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

场景 2:检查过期时间

我:这个 token 还有多久过期?

AI:[调用 ttl]
→ (integer) 1785 秒(约 29 分钟)

我:给它续 1 小时

AI:[调用 expire,seconds=3600]
→ (integer) 1(设置成功)

场景 3:批量清理缓存

我:清理所有 test:* 的 key

AI:[调用 keys,pattern="test:*"]
→ 找到 23key

AI:[调用 del]
→ (integer) 23(删除成功)

场景 4:查看排行榜

我:积分排行榜前 10 名

AI:[调用 zrange,withscores=true]
1) "user_1003" -> 9850
2) "user_1021" -> 8720
3) "user_1001" -> 8650
...

场景 5:性能监控

我:Redis 内存使用情况?

AI:[调用 info,section="memory"]
# Memory
used_memory:1048576
used_memory_human:1.00M
used_memory_rss:2097152
mem_fragmentation_ratio:2.00

分析:内存使用正常,碎片率 2.0,建议关注。

六、安装配置

6.1 Kimi Code CLI

在项目 .kimi/mcp.json 中添加:

{
  "mcpServers": {
    "redis": {
      "command": "node",
      "args": ["./redis-mcp-server/dist/index.js"],
      "env": {
        "REDIS_HOST": "localhost",
        "REDIS_PORT": "6379",
        "REDIS_PASSWORD": "",
        "REDIS_DB": "0"
      }
    }
  }
}

6.2 Claude Desktop

claude_desktop_config.json 中:

{
  "mcpServers": {
    "redis": {
      "command": "node",
      "args": ["/absolute/path/to/redis-mcp-server/dist/index.js"],
      "env": {
        "REDIS_HOST": "localhost",
        "REDIS_PORT": "6379",
        "REDIS_PASSWORD": "",
        "REDIS_DB": "0"
      }
    }
  }
}

6.3 启动

git clone https://github.com/wenit/redis-mcp-server.git
cd redis-mcp-server
npm install
npm run build
npm start

七、技术亮点

7.1 生产安全

  • keys 使用 SCAN 实现,不阻塞服务器
  • ✅ 支持密码认证
  • ✅ 连接断线自动重连

7.2 开发者友好

  • ✅ 输出格式模拟 Redis CLI,熟悉感强
  • ✅ 支持批量操作(del 多个 key)
  • ✅ 清晰的错误提示

7.3 完整的数据类型支持

  • ✅ String、Hash、List、Set、Sorted Set 全覆盖
  • ✅ 支持过期时间、条件设置等高级特性

八、踩坑记录

8.1 不要用 KEYS *

一开始图省事用了 KEYS *,被同事提醒生产环境会阻塞。

解决方案:改用 SCAN 流式遍历,封装在 keys 工具里。

8.2 连接重连

网络抖动时连接会断开,需要配置 retryStrategy

8.3 最大收获

最大的改变是 排查问题的速度

  • 以前:切终端 → 连 Redis → 执行命令 → 复制结果 → 发给 AI
  • 现在:一句话,AI 自动执行并分析

原来 5 分钟的事,现在 10 秒搞定。

九、后续规划

  • 支持 Redis Stream(消息队列)
  • 支持 Pub/Sub 订阅发布
  • Pipeline 批量执行优化
  • 缓存命中率自动分析

项目地址github.com/wenit/redis…

如果对你有帮助,欢迎 Star ⭐PR

有问题可以在 Issues 留言,我会尽快回复。