package helper import ( "fmt" "reflect" "github.com/mark3labs/mcp-go/mcp" ) // GetStringSlice 从请求参数中读取字符串数组(结构化数组 schema,如 WithArray+WithStringItems)。 // - 参数缺失或为 nil:返回 (nil, false) // - 参数为 []any:逐元素转字符串(仅接受 string 元素,遇到非字符串元素返回 ok=false) // - 其余类型:返回 ok=false // // 注意:本函数不接受 JSON 字符串形式的入参,参数应通过结构化数组 schema 传入。 func GetStringSlice(req mcp.CallToolRequest, key string) ([]string, bool) { args := req.GetArguments() raw, exists := args[key] if !exists || raw == nil { return nil, false } arr, ok := raw.([]any) if !ok { return nil, false } out := make([]string, 0, len(arr)) for _, item := range arr { s, ok := item.(string) if !ok { return nil, false } out = append(out, s) } return out, true } // JSONResult 将任意数据序列化为 JSON 文本返回,序列化前完成 PII 脱敏(A+B)。 // 脱敏失败时返回错误,拒绝输出(fail-closed)。 func JSONResult(data any) *mcp.CallToolResult { b, err := marshalSanitized(data) if err != nil { return mcp.NewToolResultError(err.Error()) } return mcp.NewToolResultText(string(b)) } // TextResult 直接返回文本 func TextResult(text string) *mcp.CallToolResult { return mcp.NewToolResultText(text) } // ParamError 参数缺失错误 func ParamError(name string) *mcp.CallToolResult { return mcp.NewToolResultError(fmt.Sprintf("参数 %s 必填", name)) } // ParamInvalidError 参数格式错误 func ParamInvalidError(name string, detail string) *mcp.CallToolResult { return mcp.NewToolResultError(fmt.Sprintf("参数 %s 格式错误: %s", name, detail)) } // ServiceError 业务调用错误 func ServiceError(action string, err error) *mcp.CallToolResult { return mcp.NewToolResultError(fmt.Sprintf("%s失败: %v", action, err)) } // BatchValidationError 批量校验错误:返回结构化的 valid/invalid 分离结果, // 便于 Agent 精确定位哪些输入非法并只修正这些项,而无需重猜整批。 // reason 为对非法项的统一说明(如 "SN 格式非法")。 func BatchValidationError(action string, valid []string, invalid []string, reason string) *mcp.CallToolResult { return JSONResult(map[string]any{ "error": true, "action": action, "reason": reason, "valid": valid, "invalid": invalid, "note": "invalid 中的项未通过校验,请修正后重试;valid 为已通过校验的项。", }) } // EmptyResult 空结果 func EmptyResult(msg string) *mcp.CallToolResult { return mcp.NewToolResultText(msg) } // PageResult 将分页查询结果包装为 AI 友好的结构:在原始 data 之外,附带 // total(符合条件的总数)、pageSize、pageNum、returned(本页返回条数)与 // hasMore(是否还有下一页),便于 Agent 判断是否翻完、如何继续翻页。 // // hasMore = pageNum*pageSize < total(pageNum 从 1 开始)。 // data 会经过 PII 脱敏并按大小决定内联或落盘(复用 JSONResultOrFile)。 // hint 用于落盘文件名前缀。 func PageResult(total, pageSize, pageNum uint, data any, hint string) *mcp.CallToolResult { returned := 0 // 尽力统计本页条数:data 通常是切片 switch d := data.(type) { case nil: returned = 0 default: if arr, ok := toSliceLen(d); ok { returned = arr } } hasMore := uint(pageNum)*uint(pageSize) < uint(total) return JSONResultOrFile(map[string]any{ "total": total, "pageSize": pageSize, "pageNum": pageNum, "returned": returned, "hasMore": hasMore, "data": data, }, hint) } // toSliceLen 若 v 是切片或数组,返回其长度与 true;否则返回 (0,false)。 func toSliceLen(v any) (int, bool) { rv := reflect.ValueOf(v) switch rv.Kind() { case reflect.Slice, reflect.Array: return rv.Len(), true default: return 0, false } }