| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- package data
- import (
- "context"
- "fmt"
- "strings"
- "github.com/mark3labs/mcp-go/mcp"
- "github.com/mark3labs/mcp-go/server"
- "fwh-toolbox-server/cache/redisx"
- "fwh-toolbox-server/cmd/mcp/tools/helper"
- )
- // Redis 只读工具集。
- // 说明:本工具集仅提供只读命令,不包含任何写操作。数据均位于业务约定的 Redis DB 12
- //(业务缓存 + asynq 任务队列),由 MCP 进程启动时的 cache.Init 固定连接。
- func registerRedisTools(s *server.MCPServer) {
- s.AddTool(
- mcp.NewTool("data_redis_get",
- mcp.WithDescription("读取 Redis 中指定 key 的值(只读)。支持 string/hash/list/set/zset;type=auto 时自动探测类型。"),
- mcp.WithString("key", mcp.Required(), mcp.Description("Redis key")),
- mcp.WithString("type", mcp.Description("数据类型:auto(默认,自动探测)、string、hash、list、set、zset")),
- mcp.WithNumber("limit", mcp.Description("list/set/zset 返回的最大元素数,默认100,最大1000。string/hash 忽略。")),
- ),
- handleRedisGet,
- )
- s.AddTool(
- mcp.NewTool("data_redis_mget",
- mcp.WithDescription("批量读取多个 string key 的值(只读)。"),
- mcp.WithString("keys", mcp.Required(), mcp.Description("多个 key,用英文逗号分隔")),
- ),
- handleRedisMGet,
- )
- s.AddTool(
- mcp.NewTool("data_redis_hget",
- mcp.WithDescription("读取 hash 中指定 field 的值(只读)。"),
- mcp.WithString("key", mcp.Required(), mcp.Description("Redis hash key")),
- mcp.WithString("field", mcp.Required(), mcp.Description("hash field 名")),
- ),
- handleRedisHGet,
- )
- s.AddTool(
- mcp.NewTool("data_redis_type",
- mcp.WithDescription("查看 key 的类型及规模概览(只读):类型、TTL、以及对应类型的长度/元素数。"),
- mcp.WithString("key", mcp.Required(), mcp.Description("Redis key")),
- ),
- handleRedisType,
- )
- s.AddTool(
- mcp.NewTool("data_redis_exists",
- mcp.WithDescription("检查 Redis key 是否存在,并返回 TTL 信息(只读)。key 支持精确匹配。"),
- mcp.WithString("key", mcp.Required(), mcp.Description("Redis key")),
- ),
- handleRedisExists,
- )
- s.AddTool(
- mcp.NewTool("data_redis_ttl",
- mcp.WithDescription("查看 key 的存活时间 TTL(只读)。"),
- mcp.WithString("key", mcp.Required(), mcp.Description("Redis key")),
- ),
- handleRedisTTL,
- )
- s.AddTool(
- mcp.NewTool("data_redis_scan",
- mcp.WithDescription("游标遍历 key(只读,非阻塞,推荐用于扫描)。返回本批 keys 与下一个游标(cursor=0 表示结束)。"),
- mcp.WithString("match", mcp.Description("匹配模式,如 user:* ;为空则匹配全部")),
- mcp.WithNumber("cursor", mcp.Description("起始游标,首次传 0,后续传上次返回的 next_cursor")),
- mcp.WithNumber("count", mcp.Description("每批返回数量提示,默认100,最大1000")),
- ),
- handleRedisScan,
- )
- s.AddTool(
- mcp.NewTool("data_redis_keys",
- mcp.WithDescription("按 pattern 返回匹配的 key(只读)。注意:KEYS 为阻塞命令,大库慎用,建议优先用 data_redis_scan。"),
- mcp.WithString("pattern", mcp.Required(), mcp.Description("匹配模式,如 user:* 或 * ")),
- mcp.WithNumber("limit", mcp.Description("返回的最大 key 数,默认1000,最大10000")),
- ),
- handleRedisKeys,
- )
- s.AddTool(
- mcp.NewTool("data_redis_stream_range",
- mcp.WithDescription("读取 Stream 指定范围内的消息(只读)。"),
- mcp.WithString("key", mcp.Required(), mcp.Description("Stream key")),
- mcp.WithString("start", mcp.Description("起始 ID,默认 '-'(最早)")),
- mcp.WithString("stop", mcp.Description("结束 ID,默认 '+'(最新)")),
- mcp.WithNumber("count", mcp.Description("返回的最大消息数,默认100,最大1000")),
- ),
- handleRedisStreamRange,
- )
- s.AddTool(
- mcp.NewTool("data_redis_stream_info",
- mcp.WithDescription("查看 Stream 元信息与消费者组信息(只读):长度、首末消息、消费者组列表。"),
- mcp.WithString("key", mcp.Required(), mcp.Description("Stream key")),
- ),
- handleRedisStreamInfo,
- )
- }
- // clampLimit 将 limit 限制在 [1, max],非法值回退到 def。
- func clampLimit(v, def, max int) int {
- if v < 1 {
- return def
- }
- if v > max {
- return max
- }
- return v
- }
- func handleRedisGet(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- key, err := req.RequireString("key")
- if err != nil {
- return helper.ParamError("key"), nil
- }
- keyType := req.GetString("type", "auto")
- limit := clampLimit(int(req.GetFloat("limit", 100)), 100, 1000)
- // auto:先探测类型
- if keyType == "auto" {
- t, terr := redisx.Type(ctx, key)
- if terr != nil {
- return helper.ServiceError("探测Redis key类型", terr), nil
- }
- if t == "none" {
- return helper.EmptyResult(fmt.Sprintf("key 不存在: %s", key)), nil
- }
- keyType = t
- }
- switch keyType {
- case "string":
- val, err := redisx.Get(ctx, key)
- if err != nil {
- return helper.EmptyResult(fmt.Sprintf("key 不存在或读取失败: %s (%v)", key, err)), nil
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "key": key, "type": "string", "value": val,
- }), nil
- case "hash":
- val, err := redisx.HGetAll(ctx, key)
- if err != nil {
- return helper.ServiceError("读取Redis hash", err), nil
- }
- if len(val) == 0 {
- return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "key": key, "type": "hash", "count": len(val), "value": val,
- }), nil
- case "list":
- val, err := redisx.LRange(ctx, key, 0, int64(limit-1))
- if err != nil {
- return helper.ServiceError("读取Redis list", err), nil
- }
- if len(val) == 0 {
- return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "key": key, "type": "list", "count": len(val),
- "limit": limit, "truncated": len(val) == limit, "value": val,
- }), nil
- case "set":
- val, err := redisx.SMembers(ctx, key)
- if err != nil {
- return helper.ServiceError("读取Redis set", err), nil
- }
- if len(val) == 0 {
- return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
- }
- total := len(val)
- truncated := false
- if total > limit {
- val = val[:limit]
- truncated = true
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "key": key, "type": "set", "total": total, "count": len(val),
- "limit": limit, "truncated": truncated, "value": val,
- }), nil
- case "zset":
- val, err := redisx.ZRangeWithScores(ctx, key, 0, int64(limit-1))
- if err != nil {
- return helper.ServiceError("读取Redis zset", err), nil
- }
- if len(val) == 0 {
- return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
- }
- members := make([]map[string]any, 0, len(val))
- for _, z := range val {
- members = append(members, map[string]any{"member": z.Member, "score": z.Score})
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "key": key, "type": "zset", "count": len(members),
- "limit": limit, "truncated": len(members) == limit, "value": members,
- }), nil
- default:
- return helper.ParamInvalidError("type", "可选值: auto, string, hash, list, set, zset"), nil
- }
- }
- func handleRedisMGet(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- raw, err := req.RequireString("keys")
- if err != nil {
- return helper.ParamError("keys"), nil
- }
- keys := make([]string, 0)
- for _, k := range strings.Split(raw, ",") {
- if s := strings.TrimSpace(k); s != "" {
- keys = append(keys, s)
- }
- }
- if len(keys) == 0 {
- return helper.ParamError("keys"), nil
- }
- vals, err := redisx.MGet(ctx, keys...)
- if err != nil {
- return helper.ServiceError("批量读取Redis string", err), nil
- }
- result := make(map[string]any, len(keys))
- for i, k := range keys {
- if i < len(vals) {
- result[k] = vals[i] // nil 表示 key 不存在
- }
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "type": "mget", "count": len(keys), "value": result,
- }), nil
- }
- func handleRedisHGet(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- key, err := req.RequireString("key")
- if err != nil {
- return helper.ParamError("key"), nil
- }
- field, err := req.RequireString("field")
- if err != nil {
- return helper.ParamError("field"), nil
- }
- val, err := redisx.HGet(ctx, key, field)
- if err != nil {
- return helper.EmptyResult(fmt.Sprintf("field 不存在或读取失败: %s.%s (%v)", key, field, err)), nil
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "key": key, "type": "hash", "field": field, "value": val,
- }), nil
- }
- func handleRedisType(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- key, err := req.RequireString("key")
- if err != nil {
- return helper.ParamError("key"), nil
- }
- t, err := redisx.Type(ctx, key)
- if err != nil {
- return helper.ServiceError("获取Redis key类型", err), nil
- }
- if t == "none" {
- return helper.EmptyResult(fmt.Sprintf("key 不存在: %s", key)), nil
- }
- out := map[string]any{"key": key, "type": t}
- // TTL
- if ttl, e := redisx.TTL(ctx, key); e == nil {
- out["ttl"] = ttlString(ttl.Seconds(), ttl == -1, ttl == -2)
- }
- // 各类型规模概览
- switch t {
- case "string":
- if n, e := redisx.StrLen(ctx, key); e == nil {
- out["length"] = n
- }
- case "hash":
- if n, e := redisx.HLen(ctx, key); e == nil {
- out["fields"] = n
- }
- case "list":
- if n, e := redisx.LLen(ctx, key); e == nil {
- out["length"] = n
- }
- case "set":
- if n, e := redisx.SCard(ctx, key); e == nil {
- out["members"] = n
- }
- case "zset":
- if n, e := redisx.ZCard(ctx, key); e == nil {
- out["members"] = n
- }
- case "stream":
- if n, e := redisx.XLen(ctx, key); e == nil {
- out["entries"] = n
- }
- }
- return helper.JSONResultOrFileAuto(out), nil
- }
- func handleRedisExists(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- key, err := req.RequireString("key")
- if err != nil {
- return helper.ParamError("key"), nil
- }
- exists, err := redisx.Exists(ctx, key)
- if err != nil {
- return helper.ServiceError("检查Redis key", err), nil
- }
- if !exists {
- return helper.EmptyResult(fmt.Sprintf("key 不存在: %s", key)), nil
- }
- ttl, err := redisx.TTL(ctx, key)
- if err != nil {
- return helper.ServiceError("获取TTL", err), nil
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "key": key, "exists": true,
- "ttl": ttlString(ttl.Seconds(), ttl == -1, ttl == -2),
- }), nil
- }
- func handleRedisTTL(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- key, err := req.RequireString("key")
- if err != nil {
- return helper.ParamError("key"), nil
- }
- ttl, err := redisx.TTL(ctx, key)
- if err != nil {
- return helper.ServiceError("获取TTL", err), nil
- }
- if ttl == -2 {
- return helper.EmptyResult(fmt.Sprintf("key 不存在: %s", key)), nil
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "key": key,
- "ttl": ttlString(ttl.Seconds(), ttl == -1, ttl == -2),
- "ttl_seconds": int64(ttl.Seconds()),
- }), nil
- }
- func handleRedisScan(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- cursor := uint64(req.GetFloat("cursor", 0))
- match := req.GetString("match", "")
- count := int64(clampLimit(int(req.GetFloat("count", 100)), 100, 1000))
- keys, next, err := redisx.Scan(ctx, cursor, match, count)
- if err != nil {
- return helper.ServiceError("SCAN 遍历", err), nil
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "type": "scan", "match": match, "count": len(keys),
- "next_cursor": next, "done": next == 0, "keys": keys,
- }), nil
- }
- func handleRedisKeys(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- pattern, err := req.RequireString("pattern")
- if err != nil {
- return helper.ParamError("pattern"), nil
- }
- limit := clampLimit(int(req.GetFloat("limit", 1000)), 1000, 10000)
- keys, err := redisx.Keys(ctx, pattern)
- if err != nil {
- return helper.ServiceError("KEYS 匹配", err), nil
- }
- total := len(keys)
- truncated := false
- if total > limit {
- keys = keys[:limit]
- truncated = true
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "type": "keys", "pattern": pattern, "total": total,
- "count": len(keys), "limit": limit, "truncated": truncated, "keys": keys,
- }), nil
- }
- func handleRedisStreamRange(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- key, err := req.RequireString("key")
- if err != nil {
- return helper.ParamError("key"), nil
- }
- start := req.GetString("start", "-")
- stop := req.GetString("stop", "+")
- count := int64(clampLimit(int(req.GetFloat("count", 100)), 100, 1000))
- msgs, err := redisx.XRangeN(ctx, key, start, stop, count)
- if err != nil {
- return helper.ServiceError("读取Stream消息", err), nil
- }
- if len(msgs) == 0 {
- return helper.EmptyResult(fmt.Sprintf("Stream 无消息或不存在: %s", key)), nil
- }
- items := make([]map[string]any, 0, len(msgs))
- for _, m := range msgs {
- items = append(items, map[string]any{"id": m.ID, "values": m.Values})
- }
- return helper.JSONResultOrFileAuto(map[string]any{
- "key": key, "type": "stream", "count": len(items),
- "limit": count, "truncated": int64(len(items)) == count, "value": items,
- }), nil
- }
- func handleRedisStreamInfo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- key, err := req.RequireString("key")
- if err != nil {
- return helper.ParamError("key"), nil
- }
- xlen, err := redisx.XLen(ctx, key)
- if err != nil {
- return helper.ServiceError("获取Stream长度", err), nil
- }
- out := map[string]any{"key": key, "type": "stream", "length": xlen}
- if groups, e := redisx.XInfoGroups(ctx, key); e == nil {
- gs := make([]map[string]any, 0, len(groups))
- for _, g := range groups {
- gs = append(gs, map[string]any{
- "name": g.Name,
- "consumers": g.Consumers,
- "pending": g.Pending,
- "last_delivered_id": g.LastDeliveredID,
- })
- }
- out["groups"] = gs
- }
- return helper.JSONResultOrFileAuto(out), nil
- }
- // ttlString 将 TTL 秒数格式化为可读字符串。
- // persist 表示无过期时间(-1),missing 表示 key 不存在(-2)。
- func ttlString(seconds float64, persist, missing bool) string {
- if missing {
- return "key 不存在"
- }
- if persist || seconds < 0 {
- return "永不过期"
- }
- return fmt.Sprintf("%ds", int64(seconds))
- }
|