redis.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. package data
  2. import (
  3. "context"
  4. "fmt"
  5. "strings"
  6. "github.com/mark3labs/mcp-go/mcp"
  7. "github.com/mark3labs/mcp-go/server"
  8. "fwh-toolbox-server/cache/redisx"
  9. "fwh-toolbox-server/cmd/mcp/tools/helper"
  10. )
  11. // Redis 只读工具集。
  12. // 说明:本工具集仅提供只读命令,不包含任何写操作。数据均位于业务约定的 Redis DB 12
  13. //(业务缓存 + asynq 任务队列),由 MCP 进程启动时的 cache.Init 固定连接。
  14. func registerRedisTools(s *server.MCPServer) {
  15. s.AddTool(
  16. mcp.NewTool("data_redis_get",
  17. mcp.WithDescription("读取 Redis 中指定 key 的值(只读)。支持 string/hash/list/set/zset;type=auto 时自动探测类型。"),
  18. mcp.WithString("key", mcp.Required(), mcp.Description("Redis key")),
  19. mcp.WithString("type", mcp.Description("数据类型:auto(默认,自动探测)、string、hash、list、set、zset")),
  20. mcp.WithNumber("limit", mcp.Description("list/set/zset 返回的最大元素数,默认100,最大1000。string/hash 忽略。")),
  21. ),
  22. handleRedisGet,
  23. )
  24. s.AddTool(
  25. mcp.NewTool("data_redis_mget",
  26. mcp.WithDescription("批量读取多个 string key 的值(只读)。"),
  27. mcp.WithString("keys", mcp.Required(), mcp.Description("多个 key,用英文逗号分隔")),
  28. ),
  29. handleRedisMGet,
  30. )
  31. s.AddTool(
  32. mcp.NewTool("data_redis_hget",
  33. mcp.WithDescription("读取 hash 中指定 field 的值(只读)。"),
  34. mcp.WithString("key", mcp.Required(), mcp.Description("Redis hash key")),
  35. mcp.WithString("field", mcp.Required(), mcp.Description("hash field 名")),
  36. ),
  37. handleRedisHGet,
  38. )
  39. s.AddTool(
  40. mcp.NewTool("data_redis_type",
  41. mcp.WithDescription("查看 key 的类型及规模概览(只读):类型、TTL、以及对应类型的长度/元素数。"),
  42. mcp.WithString("key", mcp.Required(), mcp.Description("Redis key")),
  43. ),
  44. handleRedisType,
  45. )
  46. s.AddTool(
  47. mcp.NewTool("data_redis_exists",
  48. mcp.WithDescription("检查 Redis key 是否存在,并返回 TTL 信息(只读)。key 支持精确匹配。"),
  49. mcp.WithString("key", mcp.Required(), mcp.Description("Redis key")),
  50. ),
  51. handleRedisExists,
  52. )
  53. s.AddTool(
  54. mcp.NewTool("data_redis_ttl",
  55. mcp.WithDescription("查看 key 的存活时间 TTL(只读)。"),
  56. mcp.WithString("key", mcp.Required(), mcp.Description("Redis key")),
  57. ),
  58. handleRedisTTL,
  59. )
  60. s.AddTool(
  61. mcp.NewTool("data_redis_scan",
  62. mcp.WithDescription("游标遍历 key(只读,非阻塞,推荐用于扫描)。返回本批 keys 与下一个游标(cursor=0 表示结束)。"),
  63. mcp.WithString("match", mcp.Description("匹配模式,如 user:* ;为空则匹配全部")),
  64. mcp.WithNumber("cursor", mcp.Description("起始游标,首次传 0,后续传上次返回的 next_cursor")),
  65. mcp.WithNumber("count", mcp.Description("每批返回数量提示,默认100,最大1000")),
  66. ),
  67. handleRedisScan,
  68. )
  69. s.AddTool(
  70. mcp.NewTool("data_redis_keys",
  71. mcp.WithDescription("按 pattern 返回匹配的 key(只读)。注意:KEYS 为阻塞命令,大库慎用,建议优先用 data_redis_scan。"),
  72. mcp.WithString("pattern", mcp.Required(), mcp.Description("匹配模式,如 user:* 或 * ")),
  73. mcp.WithNumber("limit", mcp.Description("返回的最大 key 数,默认1000,最大10000")),
  74. ),
  75. handleRedisKeys,
  76. )
  77. s.AddTool(
  78. mcp.NewTool("data_redis_stream_range",
  79. mcp.WithDescription("读取 Stream 指定范围内的消息(只读)。"),
  80. mcp.WithString("key", mcp.Required(), mcp.Description("Stream key")),
  81. mcp.WithString("start", mcp.Description("起始 ID,默认 '-'(最早)")),
  82. mcp.WithString("stop", mcp.Description("结束 ID,默认 '+'(最新)")),
  83. mcp.WithNumber("count", mcp.Description("返回的最大消息数,默认100,最大1000")),
  84. ),
  85. handleRedisStreamRange,
  86. )
  87. s.AddTool(
  88. mcp.NewTool("data_redis_stream_info",
  89. mcp.WithDescription("查看 Stream 元信息与消费者组信息(只读):长度、首末消息、消费者组列表。"),
  90. mcp.WithString("key", mcp.Required(), mcp.Description("Stream key")),
  91. ),
  92. handleRedisStreamInfo,
  93. )
  94. }
  95. // clampLimit 将 limit 限制在 [1, max],非法值回退到 def。
  96. func clampLimit(v, def, max int) int {
  97. if v < 1 {
  98. return def
  99. }
  100. if v > max {
  101. return max
  102. }
  103. return v
  104. }
  105. func handleRedisGet(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  106. key, err := req.RequireString("key")
  107. if err != nil {
  108. return helper.ParamError("key"), nil
  109. }
  110. keyType := req.GetString("type", "auto")
  111. limit := clampLimit(int(req.GetFloat("limit", 100)), 100, 1000)
  112. // auto:先探测类型
  113. if keyType == "auto" {
  114. t, terr := redisx.Type(ctx, key)
  115. if terr != nil {
  116. return helper.ServiceError("探测Redis key类型", terr), nil
  117. }
  118. if t == "none" {
  119. return helper.EmptyResult(fmt.Sprintf("key 不存在: %s", key)), nil
  120. }
  121. keyType = t
  122. }
  123. switch keyType {
  124. case "string":
  125. val, err := redisx.Get(ctx, key)
  126. if err != nil {
  127. return helper.EmptyResult(fmt.Sprintf("key 不存在或读取失败: %s (%v)", key, err)), nil
  128. }
  129. return helper.JSONResultOrFileAuto(map[string]any{
  130. "key": key, "type": "string", "value": val,
  131. }), nil
  132. case "hash":
  133. val, err := redisx.HGetAll(ctx, key)
  134. if err != nil {
  135. return helper.ServiceError("读取Redis hash", err), nil
  136. }
  137. if len(val) == 0 {
  138. return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
  139. }
  140. return helper.JSONResultOrFileAuto(map[string]any{
  141. "key": key, "type": "hash", "count": len(val), "value": val,
  142. }), nil
  143. case "list":
  144. val, err := redisx.LRange(ctx, key, 0, int64(limit-1))
  145. if err != nil {
  146. return helper.ServiceError("读取Redis list", err), nil
  147. }
  148. if len(val) == 0 {
  149. return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
  150. }
  151. return helper.JSONResultOrFileAuto(map[string]any{
  152. "key": key, "type": "list", "count": len(val),
  153. "limit": limit, "truncated": len(val) == limit, "value": val,
  154. }), nil
  155. case "set":
  156. val, err := redisx.SMembers(ctx, key)
  157. if err != nil {
  158. return helper.ServiceError("读取Redis set", err), nil
  159. }
  160. if len(val) == 0 {
  161. return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
  162. }
  163. total := len(val)
  164. truncated := false
  165. if total > limit {
  166. val = val[:limit]
  167. truncated = true
  168. }
  169. return helper.JSONResultOrFileAuto(map[string]any{
  170. "key": key, "type": "set", "total": total, "count": len(val),
  171. "limit": limit, "truncated": truncated, "value": val,
  172. }), nil
  173. case "zset":
  174. val, err := redisx.ZRangeWithScores(ctx, key, 0, int64(limit-1))
  175. if err != nil {
  176. return helper.ServiceError("读取Redis zset", err), nil
  177. }
  178. if len(val) == 0 {
  179. return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
  180. }
  181. members := make([]map[string]any, 0, len(val))
  182. for _, z := range val {
  183. members = append(members, map[string]any{"member": z.Member, "score": z.Score})
  184. }
  185. return helper.JSONResultOrFileAuto(map[string]any{
  186. "key": key, "type": "zset", "count": len(members),
  187. "limit": limit, "truncated": len(members) == limit, "value": members,
  188. }), nil
  189. default:
  190. return helper.ParamInvalidError("type", "可选值: auto, string, hash, list, set, zset"), nil
  191. }
  192. }
  193. func handleRedisMGet(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  194. raw, err := req.RequireString("keys")
  195. if err != nil {
  196. return helper.ParamError("keys"), nil
  197. }
  198. keys := make([]string, 0)
  199. for _, k := range strings.Split(raw, ",") {
  200. if s := strings.TrimSpace(k); s != "" {
  201. keys = append(keys, s)
  202. }
  203. }
  204. if len(keys) == 0 {
  205. return helper.ParamError("keys"), nil
  206. }
  207. vals, err := redisx.MGet(ctx, keys...)
  208. if err != nil {
  209. return helper.ServiceError("批量读取Redis string", err), nil
  210. }
  211. result := make(map[string]any, len(keys))
  212. for i, k := range keys {
  213. if i < len(vals) {
  214. result[k] = vals[i] // nil 表示 key 不存在
  215. }
  216. }
  217. return helper.JSONResultOrFileAuto(map[string]any{
  218. "type": "mget", "count": len(keys), "value": result,
  219. }), nil
  220. }
  221. func handleRedisHGet(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  222. key, err := req.RequireString("key")
  223. if err != nil {
  224. return helper.ParamError("key"), nil
  225. }
  226. field, err := req.RequireString("field")
  227. if err != nil {
  228. return helper.ParamError("field"), nil
  229. }
  230. val, err := redisx.HGet(ctx, key, field)
  231. if err != nil {
  232. return helper.EmptyResult(fmt.Sprintf("field 不存在或读取失败: %s.%s (%v)", key, field, err)), nil
  233. }
  234. return helper.JSONResultOrFileAuto(map[string]any{
  235. "key": key, "type": "hash", "field": field, "value": val,
  236. }), nil
  237. }
  238. func handleRedisType(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  239. key, err := req.RequireString("key")
  240. if err != nil {
  241. return helper.ParamError("key"), nil
  242. }
  243. t, err := redisx.Type(ctx, key)
  244. if err != nil {
  245. return helper.ServiceError("获取Redis key类型", err), nil
  246. }
  247. if t == "none" {
  248. return helper.EmptyResult(fmt.Sprintf("key 不存在: %s", key)), nil
  249. }
  250. out := map[string]any{"key": key, "type": t}
  251. // TTL
  252. if ttl, e := redisx.TTL(ctx, key); e == nil {
  253. out["ttl"] = ttlString(ttl.Seconds(), ttl == -1, ttl == -2)
  254. }
  255. // 各类型规模概览
  256. switch t {
  257. case "string":
  258. if n, e := redisx.StrLen(ctx, key); e == nil {
  259. out["length"] = n
  260. }
  261. case "hash":
  262. if n, e := redisx.HLen(ctx, key); e == nil {
  263. out["fields"] = n
  264. }
  265. case "list":
  266. if n, e := redisx.LLen(ctx, key); e == nil {
  267. out["length"] = n
  268. }
  269. case "set":
  270. if n, e := redisx.SCard(ctx, key); e == nil {
  271. out["members"] = n
  272. }
  273. case "zset":
  274. if n, e := redisx.ZCard(ctx, key); e == nil {
  275. out["members"] = n
  276. }
  277. case "stream":
  278. if n, e := redisx.XLen(ctx, key); e == nil {
  279. out["entries"] = n
  280. }
  281. }
  282. return helper.JSONResultOrFileAuto(out), nil
  283. }
  284. func handleRedisExists(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  285. key, err := req.RequireString("key")
  286. if err != nil {
  287. return helper.ParamError("key"), nil
  288. }
  289. exists, err := redisx.Exists(ctx, key)
  290. if err != nil {
  291. return helper.ServiceError("检查Redis key", err), nil
  292. }
  293. if !exists {
  294. return helper.EmptyResult(fmt.Sprintf("key 不存在: %s", key)), nil
  295. }
  296. ttl, err := redisx.TTL(ctx, key)
  297. if err != nil {
  298. return helper.ServiceError("获取TTL", err), nil
  299. }
  300. return helper.JSONResultOrFileAuto(map[string]any{
  301. "key": key, "exists": true,
  302. "ttl": ttlString(ttl.Seconds(), ttl == -1, ttl == -2),
  303. }), nil
  304. }
  305. func handleRedisTTL(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  306. key, err := req.RequireString("key")
  307. if err != nil {
  308. return helper.ParamError("key"), nil
  309. }
  310. ttl, err := redisx.TTL(ctx, key)
  311. if err != nil {
  312. return helper.ServiceError("获取TTL", err), nil
  313. }
  314. if ttl == -2 {
  315. return helper.EmptyResult(fmt.Sprintf("key 不存在: %s", key)), nil
  316. }
  317. return helper.JSONResultOrFileAuto(map[string]any{
  318. "key": key,
  319. "ttl": ttlString(ttl.Seconds(), ttl == -1, ttl == -2),
  320. "ttl_seconds": int64(ttl.Seconds()),
  321. }), nil
  322. }
  323. func handleRedisScan(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  324. cursor := uint64(req.GetFloat("cursor", 0))
  325. match := req.GetString("match", "")
  326. count := int64(clampLimit(int(req.GetFloat("count", 100)), 100, 1000))
  327. keys, next, err := redisx.Scan(ctx, cursor, match, count)
  328. if err != nil {
  329. return helper.ServiceError("SCAN 遍历", err), nil
  330. }
  331. return helper.JSONResultOrFileAuto(map[string]any{
  332. "type": "scan", "match": match, "count": len(keys),
  333. "next_cursor": next, "done": next == 0, "keys": keys,
  334. }), nil
  335. }
  336. func handleRedisKeys(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  337. pattern, err := req.RequireString("pattern")
  338. if err != nil {
  339. return helper.ParamError("pattern"), nil
  340. }
  341. limit := clampLimit(int(req.GetFloat("limit", 1000)), 1000, 10000)
  342. keys, err := redisx.Keys(ctx, pattern)
  343. if err != nil {
  344. return helper.ServiceError("KEYS 匹配", err), nil
  345. }
  346. total := len(keys)
  347. truncated := false
  348. if total > limit {
  349. keys = keys[:limit]
  350. truncated = true
  351. }
  352. return helper.JSONResultOrFileAuto(map[string]any{
  353. "type": "keys", "pattern": pattern, "total": total,
  354. "count": len(keys), "limit": limit, "truncated": truncated, "keys": keys,
  355. }), nil
  356. }
  357. func handleRedisStreamRange(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  358. key, err := req.RequireString("key")
  359. if err != nil {
  360. return helper.ParamError("key"), nil
  361. }
  362. start := req.GetString("start", "-")
  363. stop := req.GetString("stop", "+")
  364. count := int64(clampLimit(int(req.GetFloat("count", 100)), 100, 1000))
  365. msgs, err := redisx.XRangeN(ctx, key, start, stop, count)
  366. if err != nil {
  367. return helper.ServiceError("读取Stream消息", err), nil
  368. }
  369. if len(msgs) == 0 {
  370. return helper.EmptyResult(fmt.Sprintf("Stream 无消息或不存在: %s", key)), nil
  371. }
  372. items := make([]map[string]any, 0, len(msgs))
  373. for _, m := range msgs {
  374. items = append(items, map[string]any{"id": m.ID, "values": m.Values})
  375. }
  376. return helper.JSONResultOrFileAuto(map[string]any{
  377. "key": key, "type": "stream", "count": len(items),
  378. "limit": count, "truncated": int64(len(items)) == count, "value": items,
  379. }), nil
  380. }
  381. func handleRedisStreamInfo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  382. key, err := req.RequireString("key")
  383. if err != nil {
  384. return helper.ParamError("key"), nil
  385. }
  386. xlen, err := redisx.XLen(ctx, key)
  387. if err != nil {
  388. return helper.ServiceError("获取Stream长度", err), nil
  389. }
  390. out := map[string]any{"key": key, "type": "stream", "length": xlen}
  391. if groups, e := redisx.XInfoGroups(ctx, key); e == nil {
  392. gs := make([]map[string]any, 0, len(groups))
  393. for _, g := range groups {
  394. gs = append(gs, map[string]any{
  395. "name": g.Name,
  396. "consumers": g.Consumers,
  397. "pending": g.Pending,
  398. "last_delivered_id": g.LastDeliveredID,
  399. })
  400. }
  401. out["groups"] = gs
  402. }
  403. return helper.JSONResultOrFileAuto(out), nil
  404. }
  405. // ttlString 将 TTL 秒数格式化为可读字符串。
  406. // persist 表示无过期时间(-1),missing 表示 key 不存在(-2)。
  407. func ttlString(seconds float64, persist, missing bool) string {
  408. if missing {
  409. return "key 不存在"
  410. }
  411. if persist || seconds < 0 {
  412. return "永不过期"
  413. }
  414. return fmt.Sprintf("%ds", int64(seconds))
  415. }