| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163 |
- package main
- import (
- "fmt"
- "os"
- "github.com/mark3labs/mcp-go/server"
- "go.uber.org/zap"
- "go.uber.org/zap/zapcore"
- "fwh-toolbox-server/cache"
- "fwh-toolbox-server/config"
- "fwh-toolbox-server/database"
- "fwh-toolbox-server/logger"
- "fwh-toolbox-server/repositories"
- minioUtil "fwh-toolbox-server/utils/minio"
- "fwh-toolbox-server/utils/agentfiles"
- "fwh-toolbox-server/cmd/mcp/tools"
- "fwh-toolbox-server/cmd/mcp/tools/middleware"
- )
- const (
- serverName = "fwh-tools"
- serverVersion = "2.0.0"
- logDir = "/var/log/fwh-mcp-server"
- logFile = "/var/log/fwh-mcp-server/mcp.log"
- )
- func main() {
- // 初始化 MCP 专用 logger(写入文件,不污染 stdio)
- initMCPLogger()
- defer zap.L().Sync()
- // 加载配置(MCP 使用独立配置文件,与后端主服务分开)
- cfg, err := config.LoadConfig("./config.mcp.env")
- if err != nil {
- zap.L().Fatal("加载配置失败", zap.Error(err))
- }
- // 初始化基础设施(与 HTTP Server 相同)
- if err := database.Init(&cfg.App); err != nil {
- zap.L().Fatal("数据库初始化失败", zap.Error(err))
- }
- cache.Init(cfg.App.RedisAddr)
- minioUtil.InitClient(&cfg.App)
- // 初始化 FWH Pro API 客户端(注入 host/账号,否则请求 URL 无 host)
- repositories.FwhHttpInit(&cfg.FwhApi)
- // 初始化 Plane(含底层 client + Feedback 专用实例 repositories.Plane)
- // 注意:必须用 InitPlane 而非仅 InitPlaneClient,否则 repositories.Plane 为 nil,
- // service_feedback_* 会空指针 panic。
- repositories.InitPlane(&cfg.PlaneWebApi)
- // 创建 MCP Server
- s := server.NewMCPServer(
- serverName,
- serverVersion,
- server.WithToolCapabilities(false),
- server.WithRecovery(),
- server.WithToolHandlerMiddleware(middleware.LoggingMiddleware()),
- server.WithInstructions(serverInstructions),
- )
- // 注册所有 Tools(唯一入口)
- tools.RegisterAll(s)
- // A 方案:进程启动时清理一次过期的本地落盘文件(results/ 与 downloads/)。
- // MCP 随会话频繁拉起,启动清理能覆盖大多数场景;与后端 cron 定时清理(B 方案)互为补充。
- agentfiles.Cleanup(agentfiles.DefaultMaxAge)
- // 启动 stdio 通信
- zap.L().Info("[fwh-mcp-server] started", zap.String("version", serverVersion))
- fmt.Fprintln(os.Stderr, "[fwh-mcp-server] started (stdio) v"+serverVersion)
- if err := server.ServeStdio(s); err != nil {
- zap.L().Fatal("[fwh-mcp-server] error", zap.Error(err))
- }
- }
- // initMCPLogger 初始化 MCP Server 专用 logger
- // 日志写入文件,不输出到 stdout/stderr(避免污染 stdio JSON-RPC 通道)
- func initMCPLogger() {
- // 确保日志目录存在
- if err := os.MkdirAll(logDir, 0755); err != nil {
- fmt.Fprintf(os.Stderr, "[fwh-mcp-server] 创建日志目录失败: %v\n", err)
- }
- config := zap.Config{
- Level: zap.NewAtomicLevelAt(zap.InfoLevel),
- Development: false,
- Encoding: "json",
- EncoderConfig: zapcore.EncoderConfig{
- TimeKey: "ts",
- LevelKey: "level",
- NameKey: "logger",
- CallerKey: "caller",
- MessageKey: "msg",
- StacktraceKey: "stacktrace",
- LineEnding: zapcore.DefaultLineEnding,
- EncodeLevel: zapcore.LowercaseLevelEncoder,
- EncodeTime: zapcore.ISO8601TimeEncoder,
- EncodeDuration: zapcore.SecondsDurationEncoder,
- EncodeCaller: zapcore.ShortCallerEncoder,
- },
- OutputPaths: []string{logFile},
- ErrorOutputPaths: []string{logFile},
- }
- zapLogger, err := config.Build(zap.AddStacktrace(zap.ErrorLevel))
- if err != nil {
- fmt.Fprintf(os.Stderr, "[fwh-mcp-server] logger 初始化失败: %v\n", err)
- // fallback: 使用 nop logger
- zapLogger = zap.NewNop()
- }
- // 设置全局 logger(zap.L() 和 zap.S())
- zap.ReplaceGlobals(zapLogger)
- // 同时设置 logger 包变量(database 等模块直接引用 logger.Sugar)
- logger.Log = zapLogger
- logger.Sugar = zapLogger.Sugar()
- logger.Std = zap.NewStdLog(zapLogger)
- }
- // serverInstructions MCP Server 使用说明,帮助 AI 理解工具分类和使用优先级
- const serverInstructions = `FranklinWH 工具箱 MCP Server — 内部工具平台的 AI 接口层。
- ## 工具分类(按访问层级)
- ### service_* — 业务逻辑层(优先使用)
- 调用后端 Service 层函数,包含业务校验、数据聚合和格式化。
- 适用场景:常规的业务数据查询和操作。
- - service_site_*: 站点设备管理(查详情、搜索、功率流)
- - service_sim_*: SIM 卡管理(查详情、搜索)
- - service_monitor_*: 运维监控(告警统计、停机统计)
- - service_feedback_*: 反馈协作(创建/查询 Plane Issue)
- ### data_* — 数据直查层(灵活查询)
- 直接读取数据库或缓存,跳过业务逻辑层。仅限只读操作。
- 适用场景:当 service 层工具无法满足查询需求时(自定义 filter、聚合统计等)。
- - data_mongo_*: MongoDB 集合查询
- - data_redis_*: Redis 缓存查看
- ### storage_* — 对象存储层
- 文件和对象存储操作。
- - storage_file_*: 用户上传文件的读取和写入
- - storage_minio_*: MinIO bucket 对象浏览和读取(只读)
- ### util_* — 辅助工具
- 纯函数工具,不涉及数据持久化。
- - util_parse_sn: 解析设备 SN 序列号含义
- ## 使用原则
- 1. 优先使用 service_* 工具,它们包含完整的业务逻辑
- 2. 仅当 service 层无法满足时才使用 data_* 直查
- 3. data_* 工具仅支持只读操作
- 4. storage_file_* 支持读写,storage_minio_* 仅只读
- 5. 查询 MongoDB 时注意 collection 白名单限制
- 6. MinIO 操作受 bucket 白名单限制
- 7. 描述中标注 [写操作] 的工具由 Agent 权限管控,执行前会请求用户确认
- 8. 带数据的结果一律落盘:查询/导出类工具(返回数据集合、记录、详情等)的返回体不含数据正文,而是一个 JSON 摘要——包含 "localPath"(完整结果已写入的本地文件路径)与 "summary"(数据规模与字段名摘要)。请使用文件读取工具按 "localPath" 读取完整内容;可先据 "summary" 判断是否真的需要读回完整文件,尽量节省 token。落盘文件位于 /tmp/fwh-toolbox-agent/ 目录下。分页类工具(PageResult)会直接返回 total/pageNum/pageSize/hasMore 等分页元信息,本页数据正文则落盘在 "data.localPath"。触发动作/任务或返回状态提示的工具(如创建/更新/删除、"xxx成功")仍直接返回文本,不落盘。
- 9. 大数据量导出:需要一次性拉取超过分页上限的大量数据时(如导出某集合的全部记录),优先使用 data_mongo_export 等 *_export 工具。它们流式将结果写入 NDJSON 文件(每行一个 JSON 对象,"format":"ndjson"),仅返回 "exported": true + "localPath" + "count" + "summary"(不返回数据正文),请使用文件读取工具按 localPath 逐行读取解析。为防误导出全表,无 filter 且数据量过大时会被拒绝,此时应补充 filter 或显式调高 max_docs`
|