package helper import ( "encoding/json" "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 友好的结构:分页元信息 // total(符合条件的总数)、pageSize、pageNum、returned(本页返回条数)与 // hasMore(是否还有下一页)直接返回,便于 Agent 判断是否翻完、如何继续翻页; // data 正文(带数据)经 PII 脱敏后无条件落盘,只在返回里给出 localPath 与 summary // (不含数据正文),避免分页数据进入模型上下文。 // // hasMore = pageNum*pageSize < total(pageNum 从 1 开始)。 // 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) meta := map[string]any{ "total": total, "pageSize": pageSize, "pageNum": pageNum, "returned": returned, "hasMore": hasMore, } // data 正文无条件落盘:脱敏 → 写文件 → 只回 localPath + summary。 if returned == 0 { // 本页无数据:无需落盘,直接标注空数据。 meta["data"] = nil meta["note"] = "本页无数据。" } else if b, err := marshalSanitized(data); err != nil { return mcp.NewToolResultError(err.Error()) } else if path, werr := writeSpillFile(hint, b); werr != nil { return mcp.NewToolResultError(fmt.Sprintf("分页数据落盘失败: %v", werr)) } else { dataMeta := map[string]any{ "localPath": path, "bytes": len(b), } if summary := summarizeJSON(b); summary != nil { dataMeta["summary"] = summary } meta["data"] = dataMeta meta["note"] = "本页数据已写入本地文件,请使用文件读取工具按 data.localPath 读取完整内容。" } mb, err := json.MarshalIndent(meta, "", " ") if err != nil { return mcp.NewToolResultError(fmt.Sprintf("分页元信息序列化失败: %v", err)) } return mcp.NewToolResultText(string(mb)) } // 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 } }