mongo.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. package data
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "github.com/mark3labs/mcp-go/mcp"
  7. "github.com/mark3labs/mcp-go/server"
  8. "go.mongodb.org/mongo-driver/bson"
  9. "go.mongodb.org/mongo-driver/mongo/options"
  10. "fwh-toolbox-server/cmd/mcp/tools/helper"
  11. "fwh-toolbox-server/database"
  12. )
  13. // mongoAllowedCollections 允许查询的 collection 白名单
  14. var mongoAllowedCollections = []string{
  15. // 站点信息管理
  16. "SiteInfoManageData",
  17. "SiteInfoManageRecord",
  18. // SIM卡管理
  19. "SimCardManage",
  20. "SimCardMonthlyData",
  21. // 批量下载
  22. "BatchDownloadConfig",
  23. // 站点分析
  24. "SiteAnalysisConfig",
  25. // 统计
  26. "StatisticsShutdown",
  27. "StatisticsAlarm",
  28. // 站点监控
  29. "SiteMonitorConfig",
  30. // 自动升级
  31. "AutoUpdateConfigOption",
  32. "AutoUpdateConfig",
  33. // 注意:UserInfo(内部用户身份数据)与 AiChatSessions(历史对话自由文本,
  34. // 字段级脱敏无效)已从白名单移除,禁止通过 data_mongo_* 直查以防隐私泄露。
  35. }
  36. func registerMongoTools(s *server.MCPServer) {
  37. s.AddTool(
  38. mcp.NewTool("data_mongo_find",
  39. mcp.WithDescription("直接查询 MongoDB 集合数据(只读)。用于 service 层无法满足的灵活查询场景。"),
  40. mcp.WithString("collection", mcp.Required(), mcp.Description(
  41. fmt.Sprintf("集合名,可选值: %v", mongoAllowedCollections),
  42. )),
  43. mcp.WithString("filter", mcp.Description("查询条件,JSON 格式的 MongoDB filter(如 {\"sn\": \"xxx\"}),不传则查全部")),
  44. mcp.WithArray("fields", mcp.WithStringItems(),
  45. mcp.Description("字段投影:要返回的字段名数组(如 [\"sn\",\"status\"]),留空返回全部字段。用于大数据量场景下减少返回体积、节省 token。")),
  46. mcp.WithNumber("limit", mcp.Description("返回数量限制,默认10,最大100")),
  47. mcp.WithNumber("skip", mcp.Description("跳过数量(分页偏移),默认0")),
  48. mcp.WithString("sort", mcp.Description("排序条件,JSON 格式(如 {\"createdAt\": -1})")),
  49. ),
  50. handleMongoFind,
  51. )
  52. s.AddTool(
  53. mcp.NewTool("data_mongo_count",
  54. mcp.WithDescription("统计 MongoDB 集合中符合条件的文档数量"),
  55. mcp.WithString("collection", mcp.Required(), mcp.Description(
  56. fmt.Sprintf("集合名,可选值: %v", mongoAllowedCollections),
  57. )),
  58. mcp.WithString("filter", mcp.Description("查询条件,JSON 格式的 MongoDB filter,不传则统计全部")),
  59. ),
  60. handleMongoCount,
  61. )
  62. s.AddTool(
  63. mcp.NewTool("data_mongo_export",
  64. mcp.WithDescription("全量导出 MongoDB 集合数据到本地文件(只读)。用于大数据量导出场景:不受 find 的 100 条上限限制,游标流式拉取符合条件的全部文档,边脱敏边写入 NDJSON 文件(每行一个 JSON 对象),内存占用为常数级。仅返回 localPath、count 与结构化摘要(不返回数据正文,避免占用上下文)。为防止误导出全表,无 filter 时若总数超过上限会被拒绝。"),
  65. mcp.WithString("collection", mcp.Required(), mcp.Description(
  66. fmt.Sprintf("集合名,可选值: %v", mongoAllowedCollections),
  67. )),
  68. mcp.WithString("filter", mcp.Description("查询条件,JSON 格式的 MongoDB filter,不传则导出全部(受上限保护)")),
  69. mcp.WithArray("fields", mcp.WithStringItems(),
  70. mcp.Description("字段投影:只导出这些字段(如 [\"sn\",\"status\"]),留空导出全部字段。")),
  71. mcp.WithString("sort", mcp.Description("排序条件,JSON 格式(如 {\"createdAt\": -1})")),
  72. mcp.WithNumber("max_docs", mcp.Description(fmt.Sprintf("最大导出条数上限,默认 %d,硬上限 %d。超过则拒绝,请缩小 filter。", mongoExportDefaultMax, mongoExportHardMax))),
  73. ),
  74. handleMongoExport,
  75. )
  76. }
  77. // mongoExportDefaultMax 全量导出默认上限(防误导出全表的软保护,可由 max_docs 覆盖)
  78. const mongoExportDefaultMax = 50000
  79. // mongoExportHardMax 全量导出硬上限。流式导出内存为常数级,此上限仅用于防止
  80. // 误导出超大规模数据,而非内存保护,故设置较高。
  81. const mongoExportHardMax = 5000000
  82. func handleMongoFind(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  83. collection, err := req.RequireString("collection")
  84. if err != nil {
  85. return helper.ParamError("collection"), nil
  86. }
  87. if !isAllowedCollection(collection) {
  88. return helper.ParamInvalidError("collection", fmt.Sprintf("不允许查询此集合,可选: %v", mongoAllowedCollections)), nil
  89. }
  90. // 解析 filter
  91. filterStr := req.GetString("filter", "{}")
  92. var filter bson.M
  93. if err := json.Unmarshal([]byte(filterStr), &filter); err != nil {
  94. return helper.ParamInvalidError("filter", fmt.Sprintf("JSON 解析失败: %v", err)), nil
  95. }
  96. // limit
  97. limit := int64(req.GetFloat("limit", 10))
  98. if limit > 100 {
  99. limit = 100
  100. }
  101. if limit < 1 {
  102. limit = 10
  103. }
  104. // skip
  105. skip := int64(req.GetFloat("skip", 0))
  106. if skip < 0 {
  107. skip = 0
  108. }
  109. // sort
  110. var sort bson.M
  111. sortStr := req.GetString("sort", "")
  112. if sortStr != "" {
  113. if err := json.Unmarshal([]byte(sortStr), &sort); err != nil {
  114. return helper.ParamInvalidError("sort", fmt.Sprintf("JSON 解析失败: %v", err)), nil
  115. }
  116. }
  117. // 查询
  118. coll := database.GetCollection(collection)
  119. findOpts := options.Find().SetLimit(limit).SetSkip(skip)
  120. if sort != nil {
  121. findOpts.SetSort(sort)
  122. }
  123. // fields 字段投影:留空返回全部字段;指定则只返回这些字段(源头减量,节省 token)
  124. if fields, ok := helper.GetStringSlice(req, "fields"); ok && len(fields) > 0 {
  125. projection := bson.M{}
  126. for _, f := range fields {
  127. projection[f] = 1
  128. }
  129. findOpts.SetProjection(projection)
  130. }
  131. cursor, err := coll.Find(ctx, filter, findOpts)
  132. if err != nil {
  133. return helper.ServiceError("MongoDB查询", err), nil
  134. }
  135. defer cursor.Close(ctx)
  136. var results []bson.M
  137. if err := cursor.All(ctx, &results); err != nil {
  138. return helper.ServiceError("读取MongoDB结果", err), nil
  139. }
  140. // total:符合 filter 的文档总数(不受 limit/skip 影响),便于大数据量遍历时判断进度
  141. total, err := coll.CountDocuments(ctx, filter)
  142. if err != nil {
  143. return helper.ServiceError("统计MongoDB总数", err), nil
  144. }
  145. returned := int64(len(results))
  146. hasMore := skip+returned < total
  147. // 结构化返回:附带 total/returned/skip/limit/hasMore,便于 AI 判断是否翻完、如何翻页
  148. return helper.JSONResultOrFileAuto(map[string]any{
  149. "total": total,
  150. "returned": returned,
  151. "skip": skip,
  152. "limit": limit,
  153. "hasMore": hasMore,
  154. "items": results,
  155. }), nil
  156. }
  157. func handleMongoCount(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  158. collection, err := req.RequireString("collection")
  159. if err != nil {
  160. return helper.ParamError("collection"), nil
  161. }
  162. if !isAllowedCollection(collection) {
  163. return helper.ParamInvalidError("collection", fmt.Sprintf("不允许查询此集合,可选: %v", mongoAllowedCollections)), nil
  164. }
  165. filterStr := req.GetString("filter", "{}")
  166. var filter bson.M
  167. if err := json.Unmarshal([]byte(filterStr), &filter); err != nil {
  168. return helper.ParamInvalidError("filter", fmt.Sprintf("JSON 解析失败: %v", err)), nil
  169. }
  170. coll := database.GetCollection(collection)
  171. count, err := coll.CountDocuments(ctx, filter)
  172. if err != nil {
  173. return helper.ServiceError("MongoDB统计", err), nil
  174. }
  175. return helper.JSONResultOrFileAuto(map[string]any{
  176. "collection": collection,
  177. "count": count,
  178. }), nil
  179. }
  180. func handleMongoExport(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  181. collection, err := req.RequireString("collection")
  182. if err != nil {
  183. return helper.ParamError("collection"), nil
  184. }
  185. if !isAllowedCollection(collection) {
  186. return helper.ParamInvalidError("collection", fmt.Sprintf("不允许查询此集合,可选: %v", mongoAllowedCollections)), nil
  187. }
  188. // 解析 filter
  189. filterStr := req.GetString("filter", "{}")
  190. var filter bson.M
  191. if err := json.Unmarshal([]byte(filterStr), &filter); err != nil {
  192. return helper.ParamInvalidError("filter", fmt.Sprintf("JSON 解析失败: %v", err)), nil
  193. }
  194. hasFilter := len(filter) > 0
  195. // max_docs 上限
  196. maxDocs := int64(req.GetFloat("max_docs", float64(mongoExportDefaultMax)))
  197. if maxDocs < 1 {
  198. maxDocs = mongoExportDefaultMax
  199. }
  200. if maxDocs > mongoExportHardMax {
  201. maxDocs = mongoExportHardMax
  202. }
  203. coll := database.GetCollection(collection)
  204. // 先统计总量,做防误导出保护
  205. total, err := coll.CountDocuments(ctx, filter)
  206. if err != nil {
  207. return helper.ServiceError("统计导出总数", err), nil
  208. }
  209. if total == 0 {
  210. return helper.EmptyResult("符合条件的文档为 0,无需导出"), nil
  211. }
  212. if total > maxDocs {
  213. hint := "请缩小 filter 或调高 max_docs"
  214. if !hasFilter {
  215. hint = "未提供 filter 且数据量过大,请提供 filter 缩小范围,或显式调高 max_docs"
  216. }
  217. return helper.ParamInvalidError("max_docs", fmt.Sprintf(
  218. "符合条件的文档数 %d 超过上限 %d,%s(硬上限 %d)", total, maxDocs, hint, mongoExportHardMax)), nil
  219. }
  220. // 组装查询选项
  221. findOpts := options.Find()
  222. if sortStr := req.GetString("sort", ""); sortStr != "" {
  223. var sort bson.M
  224. if err := json.Unmarshal([]byte(sortStr), &sort); err != nil {
  225. return helper.ParamInvalidError("sort", fmt.Sprintf("JSON 解析失败: %v", err)), nil
  226. }
  227. findOpts.SetSort(sort)
  228. }
  229. if fields, ok := helper.GetStringSlice(req, "fields"); ok && len(fields) > 0 {
  230. projection := bson.M{}
  231. for _, f := range fields {
  232. projection[f] = 1
  233. }
  234. findOpts.SetProjection(projection)
  235. }
  236. // 游标流式导出:逐条 decode → 脱敏 → 写 NDJSON,内存常数级
  237. cursor, err := coll.Find(ctx, filter, findOpts)
  238. if err != nil {
  239. return helper.ServiceError("MongoDB导出查询", err), nil
  240. }
  241. defer cursor.Close(ctx)
  242. res := helper.ExportStreamNDJSON(func() (any, bool, error) {
  243. if !cursor.Next(ctx) {
  244. // Next 返回 false:可能是正常结束,也可能是出错
  245. return nil, false, cursor.Err()
  246. }
  247. var doc bson.M
  248. if err := cursor.Decode(&doc); err != nil {
  249. return nil, false, err
  250. }
  251. return doc, true, nil
  252. }, "mongo_export_"+collection)
  253. return res, nil
  254. }
  255. func isAllowedCollection(name string) bool {
  256. for _, c := range mongoAllowedCollections {
  257. if c == name {
  258. return true
  259. }
  260. }
  261. return false
  262. }