helper.go 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. package helper
  2. import (
  3. "fmt"
  4. "reflect"
  5. "github.com/mark3labs/mcp-go/mcp"
  6. )
  7. // GetStringSlice 从请求参数中读取字符串数组(结构化数组 schema,如 WithArray+WithStringItems)。
  8. // - 参数缺失或为 nil:返回 (nil, false)
  9. // - 参数为 []any:逐元素转字符串(仅接受 string 元素,遇到非字符串元素返回 ok=false)
  10. // - 其余类型:返回 ok=false
  11. //
  12. // 注意:本函数不接受 JSON 字符串形式的入参,参数应通过结构化数组 schema 传入。
  13. func GetStringSlice(req mcp.CallToolRequest, key string) ([]string, bool) {
  14. args := req.GetArguments()
  15. raw, exists := args[key]
  16. if !exists || raw == nil {
  17. return nil, false
  18. }
  19. arr, ok := raw.([]any)
  20. if !ok {
  21. return nil, false
  22. }
  23. out := make([]string, 0, len(arr))
  24. for _, item := range arr {
  25. s, ok := item.(string)
  26. if !ok {
  27. return nil, false
  28. }
  29. out = append(out, s)
  30. }
  31. return out, true
  32. }
  33. // JSONResult 将任意数据序列化为 JSON 文本返回,序列化前完成 PII 脱敏(A+B)。
  34. // 脱敏失败时返回错误,拒绝输出(fail-closed)。
  35. func JSONResult(data any) *mcp.CallToolResult {
  36. b, err := marshalSanitized(data)
  37. if err != nil {
  38. return mcp.NewToolResultError(err.Error())
  39. }
  40. return mcp.NewToolResultText(string(b))
  41. }
  42. // TextResult 直接返回文本
  43. func TextResult(text string) *mcp.CallToolResult {
  44. return mcp.NewToolResultText(text)
  45. }
  46. // ParamError 参数缺失错误
  47. func ParamError(name string) *mcp.CallToolResult {
  48. return mcp.NewToolResultError(fmt.Sprintf("参数 %s 必填", name))
  49. }
  50. // ParamInvalidError 参数格式错误
  51. func ParamInvalidError(name string, detail string) *mcp.CallToolResult {
  52. return mcp.NewToolResultError(fmt.Sprintf("参数 %s 格式错误: %s", name, detail))
  53. }
  54. // ServiceError 业务调用错误
  55. func ServiceError(action string, err error) *mcp.CallToolResult {
  56. return mcp.NewToolResultError(fmt.Sprintf("%s失败: %v", action, err))
  57. }
  58. // BatchValidationError 批量校验错误:返回结构化的 valid/invalid 分离结果,
  59. // 便于 Agent 精确定位哪些输入非法并只修正这些项,而无需重猜整批。
  60. // reason 为对非法项的统一说明(如 "SN 格式非法")。
  61. func BatchValidationError(action string, valid []string, invalid []string, reason string) *mcp.CallToolResult {
  62. return JSONResult(map[string]any{
  63. "error": true,
  64. "action": action,
  65. "reason": reason,
  66. "valid": valid,
  67. "invalid": invalid,
  68. "note": "invalid 中的项未通过校验,请修正后重试;valid 为已通过校验的项。",
  69. })
  70. }
  71. // EmptyResult 空结果
  72. func EmptyResult(msg string) *mcp.CallToolResult {
  73. return mcp.NewToolResultText(msg)
  74. }
  75. // PageResult 将分页查询结果包装为 AI 友好的结构:在原始 data 之外,附带
  76. // total(符合条件的总数)、pageSize、pageNum、returned(本页返回条数)与
  77. // hasMore(是否还有下一页),便于 Agent 判断是否翻完、如何继续翻页。
  78. //
  79. // hasMore = pageNum*pageSize < total(pageNum 从 1 开始)。
  80. // data 会经过 PII 脱敏并按大小决定内联或落盘(复用 JSONResultOrFile)。
  81. // hint 用于落盘文件名前缀。
  82. func PageResult(total, pageSize, pageNum uint, data any, hint string) *mcp.CallToolResult {
  83. returned := 0
  84. // 尽力统计本页条数:data 通常是切片
  85. switch d := data.(type) {
  86. case nil:
  87. returned = 0
  88. default:
  89. if arr, ok := toSliceLen(d); ok {
  90. returned = arr
  91. }
  92. }
  93. hasMore := uint(pageNum)*uint(pageSize) < uint(total)
  94. return JSONResultOrFile(map[string]any{
  95. "total": total,
  96. "pageSize": pageSize,
  97. "pageNum": pageNum,
  98. "returned": returned,
  99. "hasMore": hasMore,
  100. "data": data,
  101. }, hint)
  102. }
  103. // toSliceLen 若 v 是切片或数组,返回其长度与 true;否则返回 (0,false)。
  104. func toSliceLen(v any) (int, bool) {
  105. rv := reflect.ValueOf(v)
  106. switch rv.Kind() {
  107. case reflect.Slice, reflect.Array:
  108. return rv.Len(), true
  109. default:
  110. return 0, false
  111. }
  112. }