我开发了一个 Redis MCP 服务,AI 直接操作缓存,排查问题快人一步!
让 AI 助手直接连接 Redis,查询缓存、分析性能、清理数据,一站式搞定。
一、为什么我开发了它?
作为后端开发,Redis 是我每天都在用的工具:
- 🔍 查看用户 Token 缓存是否存在
- ⏱️ 检查缓存过期时间设置是否正确
- 📊 分析缓存命中率
- 🧹 清理特定前缀的测试缓存
- 📈 监控 Redis 内存和性能
以前的痛苦:
- 打开终端
- 输入
redis-cli -h xxx -p xxx -a xxx - 执行命令
- 复制结果给 AI 看
- AI 说"能不能再看看这个 key"...
- 重复步骤 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:[调用 get,key="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:*"]
→ 找到 23 个 key
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 批量执行优化
- 缓存命中率自动分析
如果对你有帮助,欢迎 Star ⭐ 和 PR!
有问题可以在 Issues 留言,我会尽快回复。