ソースを参照

refactor:重构MCP Server框架结构

Jerry Liang 2 ヶ月 前
親
コミット
995598586d

+ 489 - 0
service/cmd/mcp/DESIGN.md

@@ -0,0 +1,489 @@
+# FranklinWH MCP Server 设计文档
+
+## 1. 概述
+
+FranklinWH MCP Server (`fwh-tools`) 是基于 [Model Context Protocol](https://modelcontextprotocol.io) 实现的内部工具服务器,为 AI Agent(Kiro CLI)提供对 FranklinWH 内部工具平台数据和服务的结构化访问能力。
+
+| 项目 | 说明 |
+|------|------|
+| 服务名 | `fwh-tools` |
+| 版本 | `2.0.0` |
+| 语言 | Go |
+| SDK | `github.com/mark3labs/mcp-go` |
+| 传输协议 | stdio (JSON-RPC 2.0 over stdin/stdout) |
+| 构建产物 | 单二进制 `fwh-mcp-server` |
+| 调用方 | Kiro CLI(通过 ACP 协议启动子进程) |
+
+---
+
+## 2. 架构设计
+
+### 2.1 整体架构
+
+```
+┌─────────────────────────────────────────────────────┐
+│                   Kiro CLI (AI Agent)                 │
+│              通过 ACP 协议管理 MCP Server              │
+└────────────────────────┬────────────────────────────┘
+                         │ stdio (JSON-RPC 2.0)
+                         ▼
+┌─────────────────────────────────────────────────────┐
+│              fwh-mcp-server (本进程)                  │
+│                                                      │
+│  ┌────────────────────────────────────────────────┐  │
+│  │              MCP Protocol Layer                 │  │
+│  │         (mcp-go SDK: 请求路由、序列化)          │  │
+│  └─────────────────────┬──────────────────────────┘  │
+│                        │                             │
+│  ┌─────────┬───────────┼───────────┬─────────────┐  │
+│  │service/ │  data/    │  storage/ │    util/     │  │
+│  │业务逻辑 │ 数据直查   │ 对象存储  │   辅助工具   │  │
+│  └────┬────┴─────┬─────┴─────┬─────┴──────┬──────┘  │
+│       │          │           │            │          │
+└───────┼──────────┼───────────┼────────────┼──────────┘
+        │          │           │            │
+        ▼          ▼           ▼            │ (纯函数)
+┌──────────┐ ┌──────────┐ ┌──────────┐     │
+│ services/│ │ MongoDB  │ │  MinIO   │     │
+│层(复用后 │ │ Redis    │ │          │     │
+│端业务代 │ │          │ │          │     │
+│码)       │ │          │ │          │     │
+└──────────┘ └──────────┘ └──────────┘
+```
+
+### 2.2 与现有后端的关系
+
+MCP Server 是现有后端 Service 层的**另一个入口**,不是独立系统:
+
+| 对比项 | HTTP Server (`main.go`) | MCP Server (`cmd/mcp/main.go`) |
+|--------|------------------------|-------------------------------|
+| 入口 | Fiber HTTP 路由 | stdio JSON-RPC |
+| 调用方 | 前端浏览器 | Kiro CLI (AI Agent) |
+| 认证 | Casdoor JWT | 无需(本地 stdio 进程) |
+| 复用层 | controllers → services → repositories | **直接调用 services/repositories** |
+| 输出格式 | JSON HTTP Response | MCP Tool Result (text/JSON) |
+
+### 2.3 代码目录结构
+
+```
+service/cmd/mcp/
+├── main.go                      # 入口:初始化基础设施 + 创建 MCP Server + 注册工具
+├── tools/
+│   ├── register.go              # RegisterAll:唯一对外注册入口
+│   ├── helper/
+│   │   └── helper.go            # 统一响应工具函数
+│   ├── service/                 # 业务逻辑层工具
+│   │   ├── register.go          # RegisterServiceTools
+│   │   ├── site.go              # 站点相关工具
+│   │   ├── sim.go               # SIM 卡相关工具
+│   │   ├── monitor.go           # 监控统计工具
+│   │   └── feedback.go          # 反馈协作工具
+│   ├── data/                    # 数据直查层工具
+│   │   ├── register.go          # RegisterDataTools
+│   │   ├── mongo.go             # MongoDB 直查
+│   │   └── redis.go             # Redis 缓存读取
+│   ├── storage/                 # 对象存储层工具
+│   │   ├── register.go          # RegisterStorageTools
+│   │   ├── file.go              # 用户上传文件读取
+│   │   └── minio.go             # MinIO 对象浏览/读取
+│   └── util/                    # 辅助工具层
+│       ├── register.go          # RegisterUtilTools
+│       └── format.go            # SN 解析等纯函数
+```
+
+---
+
+## 3. 工具分层架构
+
+采用四层分类设计,按访问层级递进:
+
+```
+┌───────────────────────────────────────────────────┐
+│  Layer 1: service_*  — 业务逻辑层(优先使用)        │
+│  调用后端 Service 层函数,包含业务校验和数据聚合      │
+├───────────────────────────────────────────────────┤
+│  Layer 2: data_*     — 数据直查层(灵活查询)        │
+│  直接读取 DB/Cache,跳过业务逻辑,仅限只读           │
+├───────────────────────────────────────────────────┤
+│  Layer 3: storage_*  — 对象存储层(文件操作)        │
+│  MinIO 对象浏览和用户上传文件读取,仅限只读           │
+├───────────────────────────────────────────────────┤
+│  Layer 4: util_*     — 辅助工具层(纯函数)          │
+│  格式解析等不涉及数据持久化的工具                     │
+└───────────────────────────────────────────────────┘
+```
+
+**命名规范:** `{layer}_{domain}_{action}`
+
+示例:`service_site_query_info`、`data_mongo_find`、`storage_minio_list_objects`、`util_parse_sn`
+
+---
+
+## 4. 工具清单
+
+### 4.1 Service 层(业务逻辑)
+
+| 工具名 | 说明 | 参数 |
+|--------|------|------|
+| `service_site_query_info` | 查询站点设备详情 | `sn` (必填) |
+| `service_site_search` | 搜索站点列表 | `page`, `size`, `keyword` |
+| `service_site_power_flow` | 查询功率流数据 | `sn` (必填), `date` (必填, yyyy-MM-dd) |
+| `service_sim_query` | 查询 SIM 卡详情 | `giccid` (必填) |
+| `service_sim_search` | 搜索 SIM 卡列表 | `page`, `size`, `search`, `status`, `platform` |
+| `service_monitor_alarm_stats` | 查询告警统计 | `date` |
+| `service_monitor_shutdown_stats` | 查询停机统计 | `date` |
+| `service_feedback_create` | 创建 Plane Intake Issue | `title` (必填), `type` (必填), `priority`, `description`, `category` |
+| `service_feedback_history` | 查询反馈历史 | (无参数) |
+
+### 4.2 Data 层(数据直查)
+
+| 工具名 | 说明 | 参数 |
+|--------|------|------|
+| `data_mongo_find` | MongoDB 集合查询 | `collection` (必填), `filter`, `limit`, `skip`, `sort` |
+| `data_mongo_count` | MongoDB 文档计数 | `collection` (必填), `filter` |
+| `data_redis_get` | 读取 Redis key | `key` (必填), `type` |
+| `data_redis_exists` | 检查 key 存在性及 TTL | `key` (必填) |
+
+### 4.3 Storage 层(对象存储)
+
+| 工具名 | 说明 | 参数 |
+|--------|------|------|
+| `storage_file_read` | 读取用户上传文件 | `path` (必填) |
+| `storage_file_list` | 列出用户上传文件 | (无参数) |
+| `storage_minio_list_objects` | 列出 MinIO 对象 | `bucket` (必填), `prefix`, `limit` |
+| `storage_minio_read_object` | 读取 MinIO 文本文件 | `bucket` (必填), `key` (必填) |
+
+### 4.4 Util 层(辅助工具)
+
+| 工具名 | 说明 | 参数 |
+|--------|------|------|
+| `util_parse_sn` | 解析设备 SN 序列号含义 | `sn` (必填) |
+
+---
+
+## 5. 安全设计
+
+### 5.1 访问控制
+
+| 机制 | 说明 |
+|------|------|
+| 传输层 | stdio 本地进程通信,无网络暴露 |
+| 认证 | 无需(由 ACP 管理器控制进程生命周期) |
+| Collection 白名单 | MongoDB 查询限制在指定集合内 |
+| Bucket 白名单 | MinIO 访问限制在指定 bucket 内 |
+| 只读限制 | data_* 和 storage_* 层所有操作均为只读 |
+| 文件大小限制 | MinIO 读取限制 1MB,用户上传限制 2MB |
+| 查询结果限制 | MongoDB 单次最多返回 100 条 |
+| Redis 安全 | 不支持通配符 key,防止 KEYS * 类操作 |
+
+### 5.2 白名单配置
+
+**MongoDB Collections:**
+```go
+var mongoAllowedCollections = []string{
+    "site_info",
+    "sim_card_manage",
+    "batch_download",
+    "site_analysis",
+    "shutdown_statistics",
+    "alarm_statistics",
+    "sites_monitor_config",
+    "user_config",
+}
+```
+
+**MinIO Buckets:**
+```go
+var minioAllowedBuckets = []string{
+    "fwh-toolbox-batch-download",
+    "fwh-toolbox-ai-file",
+}
+```
+
+### 5.3 写操作控制
+
+| 层级 | 写操作 | 说明 |
+|------|--------|------|
+| service | `service_feedback_create` | 唯一写操作,仅创建 Plane Intake Issue |
+| data | 无 | 全部只读 |
+| storage | 无 | 全部只读(文件上传通过 HTTP API,不通过 MCP) |
+| util | 无 | 纯函数 |
+
+---
+
+## 6. 文件存储设计
+
+### 6.1 FileStore 架构
+
+```
+MinIO Bucket: fwh-toolbox-ai-file
+├── acp/                          ← ACP (AI Chat) 产生的文件
+│   └── {userId}/{sessionId}/{uuid}_{fileName}
+│
+└── mcp/                          ← MCP (用户上传) 的文件
+    └── {userId}/{sessionId}/{uuid}_{fileName}
+```
+
+### 6.2 文件生命周期
+
+| 阶段 | 操作 | 说明 |
+|------|------|------|
+| 上传 | `POST /ai/files/upload` | 前端上传 → 后端存入 MinIO(prefix=acp) |
+| 读取 | Agent `readTextFile` 或 `storage_file_read` | AI 请求读取 → FileStore 从 MinIO 获取 |
+| 过期 | 自动清理 | 文件设置 7 天过期 |
+
+### 6.3 共享模块
+
+通用 FileStore 位于 `service/utils/filestore/filestore.go`,ACP 和 MCP 通过不同 prefix 实例化:
+
+```go
+// ACP 使用
+acpFS := filestore.NewFileStore("acp")
+
+// MCP 使用
+mcpFS := filestore.NewFileStore("mcp")
+```
+
+---
+
+## 7. 响应格式
+
+### 7.1 统一响应工具 (`helper/helper.go`)
+
+| 函数 | 用途 | 返回类型 |
+|------|------|----------|
+| `JSONResult(data)` | 序列化为格式化 JSON | `CallToolResult` (text) |
+| `TextResult(text)` | 直接返回文本 | `CallToolResult` (text) |
+| `ParamError(name)` | 参数缺失错误 | `CallToolResult` (error) |
+| `ParamInvalidError(name, detail)` | 参数格式错误 | `CallToolResult` (error) |
+| `ServiceError(action, err)` | 业务调用失败 | `CallToolResult` (error) |
+| `EmptyResult(msg)` | 空结果提示 | `CallToolResult` (text) |
+
+### 7.2 错误响应格式
+
+```
+参数 {name} 必填
+参数 {name} 格式错误: {detail}
+{action}失败: {error}
+```
+
+---
+
+## 8. 初始化流程
+
+```go
+func main() {
+    // 1. 加载配置(与 HTTP Server 共享)
+    cfg := config.LoadConfig()
+
+    // 2. 初始化基础设施
+    logger.Init(cfg.App.AppEnv)
+    database.Init(&cfg.App)          // MongoDB
+    cache.Init(cfg.App.RedisAddr)    // Redis
+    repositories.PlaneWebHttpInit()  // Plane API
+
+    // 3. 初始化 MCP FileStore (prefix="mcp")
+    mcpFS := filestore.NewFileStore("mcp")
+    storage.InitFileStore(mcpFS)
+
+    // 4. 创建 MCP Server
+    s := server.NewMCPServer("fwh-tools", "2.0.0",
+        server.WithToolCapabilities(false),
+        server.WithRecovery(),
+        server.WithInstructions(serverInstructions),
+    )
+
+    // 5. 注册所有 Tools(单一入口)
+    tools.RegisterAll(s)
+
+    // 6. 启动 stdio 通信
+    server.ServeStdio(s)
+}
+```
+
+---
+
+## 9. 部署配置
+
+### 9.1 构建
+
+```bash
+cd service
+go build -o fwh-mcp-server ./cmd/mcp
+```
+
+### 9.2 Kiro MCP 配置
+
+**文件:`service/.kiro/settings/mcp.json`**
+
+```json
+{
+  "mcpServers": {
+    "fwh-tools": {
+      "command": "./fwh-mcp-server",
+      "args": [],
+      "env": {
+        "MONGO_DSN": "${MONGO_DSN}",
+        "REDIS_ADDR": "${REDIS_ADDR}",
+        "MINIO_ENDPOINT": "${MINIO_ENDPOINT}",
+        "MINIO_ACCESS_KEY": "${MINIO_ACCESS_KEY}",
+        "MINIO_SECRET_KEY": "${MINIO_SECRET_KEY}",
+        "PLANE_WEB_HOST": "${PLANE_WEB_HOST}",
+        "PLANE_WEB_EMAIL": "${PLANE_WEB_EMAIL}",
+        "PLANE_WEB_PASSWORD": "${PLANE_WEB_PASSWORD}",
+        "PLANE_WEB_WORKSPACE_SLUG": "${PLANE_WEB_WORKSPACE_SLUG}",
+        "PLANE_WEB_PROJECT_ID": "${PLANE_WEB_PROJECT_ID}"
+      }
+    }
+  }
+}
+```
+
+### 9.3 部署目录
+
+```
+/var/www/fwh-toolbox/server/
+├── fwh-toolbox-server         # HTTP Server
+├── fwh-mcp-server             # MCP Server
+├── .kiro/
+│   ├── settings/mcp.json      # MCP 配置
+│   ├── agents/                # Agent 配置
+│   └── steering/              # Steering 文件
+└── .env                       # 环境变量
+```
+
+---
+
+## 10. Instructions 系统提示
+
+MCP Server 通过 `server.WithInstructions()` 向 AI Agent 提供使用指南:
+
+```
+FranklinWH 工具箱 MCP Server — 内部工具平台的 AI 接口层。
+
+使用原则:
+1. 优先使用 service_* 工具,它们包含完整的业务逻辑
+2. 仅当 service 层无法满足时才使用 data_* 直查
+3. 所有 data_* 和 storage_* 工具仅支持只读操作
+4. 查询 MongoDB 时注意 collection 白名单限制
+```
+
+---
+
+## 11. 扩展指南
+
+### 11.1 添加新的 Service 层工具
+
+1. 在 `tools/service/` 下创建新文件(如 `newdomain.go`)
+2. 实现 `registerXxxTools(s *server.MCPServer)` 函数
+3. 在 `tools/service/register.go` 的 `RegisterServiceTools` 中调用
+4. 工具命名遵循 `service_{domain}_{action}` 格式
+
+```go
+// tools/service/newdomain.go
+package service
+
+func registerNewDomainTools(s *server.MCPServer) {
+    s.AddTool(
+        mcp.NewTool("service_newdomain_query",
+            mcp.WithDescription("..."),
+            mcp.WithString("id", mcp.Required(), mcp.Description("...")),
+        ),
+        handleNewDomainQuery,
+    )
+}
+
+func handleNewDomainQuery(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+    id, err := req.RequireString("id")
+    if err != nil {
+        return helper.ParamError("id"), nil
+    }
+    data, err := services.GetSomething(id)
+    if err != nil {
+        return helper.ServiceError("查询", err), nil
+    }
+    return helper.JSONResult(data), nil
+}
+```
+
+### 11.2 添加新的 MongoDB Collection 访问
+
+在 `tools/data/mongo.go` 的 `mongoAllowedCollections` 白名单中添加新集合名:
+
+```go
+var mongoAllowedCollections = []string{
+    // ... 现有集合
+    "new_collection_name",  // ← 添加这一行
+}
+```
+
+### 11.3 添加新的 MinIO Bucket 访问
+
+在 `tools/storage/minio.go` 的 `minioAllowedBuckets` 白名单中添加:
+
+```go
+var minioAllowedBuckets = []string{
+    // ... 现有 bucket
+    "new-bucket-name",  // ← 添加这一行
+}
+```
+
+---
+
+## 12. 测试
+
+### 12.1 本地测试
+
+```bash
+# 构建
+cd service && go build -o fwh-mcp-server ./cmd/mcp
+
+# 测试 tools/list
+echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | ./fwh-mcp-server
+
+# 测试单个工具调用
+echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"util_parse_sn","arguments":{"sn":"10060006A03X25310091"}}}' | ./fwh-mcp-server
+```
+
+### 12.2 通过 Kiro CLI 测试
+
+```bash
+# 在 service/ 目录下(含 .kiro/ 配置)
+kiro-cli chat
+
+# 对话中使用工具
+> 帮我查一下 SN 为 10060006A03X25310091 的站点信息
+> 统计一下 site_info 集合中有多少条记录
+> 解析一下这个 SN: 10050004B02X24450023
+```
+
+---
+
+## 13. 依赖关系
+
+| 依赖 | 版本 | 用途 |
+|------|------|------|
+| `github.com/mark3labs/mcp-go` | latest | MCP 协议 SDK |
+| `go.mongodb.org/mongo-driver` | - | MongoDB 驱动 |
+| `github.com/minio/minio-go/v7` | - | MinIO 客户端 |
+| `github.com/google/uuid` | - | UUID 生成 |
+| `go.uber.org/zap` | - | 日志 |
+
+共享后端模块:
+- `fwh-toolbox-server/services` — 业务逻辑层
+- `fwh-toolbox-server/database` — 数据库连接
+- `fwh-toolbox-server/cache` — Redis 缓存
+- `fwh-toolbox-server/repositories` — 数据访问层
+- `fwh-toolbox-server/utils/filestore` — 文件存储
+- `fwh-toolbox-server/utils/minio` — MinIO 客户端
+- `fwh-toolbox-server/config` — 配置加载
+- `fwh-toolbox-server/dto` — 数据传输对象
+
+---
+
+## 14. 变更历史
+
+| 版本 | 日期 | 变更内容 |
+|------|------|----------|
+| 1.0.0 | 2026-08-03 | 初版:6 个 service 层工具(site/sim/alarm/shutdown/plane) |
+| 2.0.0 | 2026-08-06 | 重构为四层架构,新增 data/storage/util 层,命名规范化 |

+ 50 - 15
service/cmd/mcp/main.go

@@ -15,6 +15,12 @@ import (
 	"fwh-toolbox-server/utils/filestore"
 
 	"fwh-toolbox-server/cmd/mcp/tools"
+	"fwh-toolbox-server/cmd/mcp/tools/storage"
+)
+
+const (
+	serverName    = "fwh-tools"
+	serverVersion = "2.0.0"
 )
 
 func main() {
@@ -35,30 +41,59 @@ func main() {
 	cache.Init(cfg.App.RedisAddr)
 	repositories.PlaneWebHttpInit(&cfg.PlaneWebApi)
 
+	// 初始化 MCP FileStore
+	mcpFS := filestore.NewFileStore("mcp")
+	storage.InitFileStore(mcpFS)
+
 	// 创建 MCP Server
 	s := server.NewMCPServer(
-		"fwh-tools",
-		"1.0.0",
+		serverName,
+		serverVersion,
 		server.WithToolCapabilities(false),
 		server.WithRecovery(),
+		server.WithInstructions(serverInstructions),
 	)
 
-	// 初始化 MCP FileStore
-	mcpFS := filestore.NewFileStore("mcp")
-	tools.InitFileStore(mcpFS)
-
-	// 注册所有 Tools
-	tools.RegisterSiteInfoTools(s)
-	tools.RegisterSimCardTools(s)
-	tools.RegisterPowerFlowTools(s)
-	tools.RegisterAlarmTools(s)
-	tools.RegisterShutdownTools(s)
-	tools.RegisterPlaneTools(s)
-	tools.RegisterFileTools(s)
+	// 注册所有 Tools(唯一入口)
+	tools.RegisterAll(s)
 
 	// 启动 stdio 通信
-	fmt.Fprintln(os.Stderr, "[fwh-mcp-server] started (stdio)")
+	fmt.Fprintln(os.Stderr, "[fwh-mcp-server] started (stdio) v"+serverVersion)
 	if err := server.ServeStdio(s); err != nil {
 		log.Fatalf("[fwh-mcp-server] error: %v", err)
 	}
 }
+
+// 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_* — 对象存储层
+读取 MinIO 对象存储或用户上传的文件。仅限只读操作。
+- storage_file_*: 用户上传文件读取
+- storage_minio_*: MinIO bucket 对象浏览和读取
+
+### util_* — 辅助工具
+纯函数工具,不涉及数据持久化。
+- util_parse_sn: 解析设备 SN 序列号含义
+
+## 使用原则
+1. 优先使用 service_* 工具,它们包含完整的业务逻辑
+2. 仅当 service 层无法满足时才使用 data_* 直查
+3. 所有 data_* 和 storage_* 工具仅支持只读操作
+4. 查询 MongoDB 时注意 collection 白名单限制`

+ 0 - 39
service/cmd/mcp/tools/alarm.go

@@ -1,39 +0,0 @@
-package tools
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-
-	"fwh-toolbox-server/services"
-)
-
-func RegisterAlarmTools(s *server.MCPServer) {
-	s.AddTool(
-		mcp.NewTool("query_alarm_statistics",
-			mcp.WithDescription("查询告警统计数据(按日期汇总各类告警数量)"),
-			mcp.WithString("date", mcp.Description("统计日期,格式 yyyy-MM-dd(不传则查最新)")),
-		),
-		handleQueryAlarmStatistics,
-	)
-}
-
-func handleQueryAlarmStatistics(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	date := req.GetString("date", "")
-
-	var datePtr *string
-	if date != "" {
-		datePtr = &date
-	}
-
-	data, err := services.GetAlarmStatistics(datePtr)
-	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("查询告警统计失败: %v", err)), nil
-	}
-
-	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
-	return mcp.NewToolResultText(string(jsonBytes)), nil
-}

+ 157 - 0
service/cmd/mcp/tools/data/mongo.go

@@ -0,0 +1,157 @@
+package data
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo/options"
+
+	"fwh-toolbox-server/cmd/mcp/tools/helper"
+	"fwh-toolbox-server/database"
+)
+
+// mongoAllowedCollections 允许查询的 collection 白名单
+var mongoAllowedCollections = []string{
+	"site_info",
+	"sim_card_manage",
+	"batch_download",
+	"site_analysis",
+	"shutdown_statistics",
+	"alarm_statistics",
+	"sites_monitor_config",
+	"user_config",
+}
+
+func registerMongoTools(s *server.MCPServer) {
+	s.AddTool(
+		mcp.NewTool("data_mongo_find",
+			mcp.WithDescription("直接查询 MongoDB 集合数据(只读)。用于 service 层无法满足的灵活查询场景。"),
+			mcp.WithString("collection", mcp.Required(), mcp.Description(
+				fmt.Sprintf("集合名,可选值: %v", mongoAllowedCollections),
+			)),
+			mcp.WithString("filter", mcp.Description("查询条件,JSON 格式的 MongoDB filter(如 {\"sn\": \"xxx\"}),不传则查全部")),
+			mcp.WithNumber("limit", mcp.Description("返回数量限制,默认10,最大100")),
+			mcp.WithNumber("skip", mcp.Description("跳过数量(分页偏移),默认0")),
+			mcp.WithString("sort", mcp.Description("排序条件,JSON 格式(如 {\"createdAt\": -1})")),
+		),
+		handleMongoFind,
+	)
+
+	s.AddTool(
+		mcp.NewTool("data_mongo_count",
+			mcp.WithDescription("统计 MongoDB 集合中符合条件的文档数量"),
+			mcp.WithString("collection", mcp.Required(), mcp.Description(
+				fmt.Sprintf("集合名,可选值: %v", mongoAllowedCollections),
+			)),
+			mcp.WithString("filter", mcp.Description("查询条件,JSON 格式的 MongoDB filter,不传则统计全部")),
+		),
+		handleMongoCount,
+	)
+}
+
+func handleMongoFind(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	collection, err := req.RequireString("collection")
+	if err != nil {
+		return helper.ParamError("collection"), nil
+	}
+
+	if !isAllowedCollection(collection) {
+		return helper.ParamInvalidError("collection", fmt.Sprintf("不允许查询此集合,可选: %v", mongoAllowedCollections)), nil
+	}
+
+	// 解析 filter
+	filterStr := req.GetString("filter", "{}")
+	var filter bson.M
+	if err := json.Unmarshal([]byte(filterStr), &filter); err != nil {
+		return helper.ParamInvalidError("filter", fmt.Sprintf("JSON 解析失败: %v", err)), nil
+	}
+
+	// limit
+	limit := int64(req.GetFloat("limit", 10))
+	if limit > 100 {
+		limit = 100
+	}
+	if limit < 1 {
+		limit = 10
+	}
+
+	// skip
+	skip := int64(req.GetFloat("skip", 0))
+	if skip < 0 {
+		skip = 0
+	}
+
+	// sort
+	var sort bson.M
+	sortStr := req.GetString("sort", "")
+	if sortStr != "" {
+		if err := json.Unmarshal([]byte(sortStr), &sort); err != nil {
+			return helper.ParamInvalidError("sort", fmt.Sprintf("JSON 解析失败: %v", err)), nil
+		}
+	}
+
+	// 查询
+	coll := database.GetCollection(collection)
+	findOpts := options.Find().SetLimit(limit).SetSkip(skip)
+	if sort != nil {
+		findOpts.SetSort(sort)
+	}
+
+	cursor, err := coll.Find(ctx, filter, findOpts)
+	if err != nil {
+		return helper.ServiceError("MongoDB查询", err), nil
+	}
+	defer cursor.Close(ctx)
+
+	var results []bson.M
+	if err := cursor.All(ctx, &results); err != nil {
+		return helper.ServiceError("读取MongoDB结果", err), nil
+	}
+
+	if len(results) == 0 {
+		return helper.EmptyResult("查询结果为空"), nil
+	}
+
+	return helper.JSONResult(results), nil
+}
+
+func handleMongoCount(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	collection, err := req.RequireString("collection")
+	if err != nil {
+		return helper.ParamError("collection"), nil
+	}
+
+	if !isAllowedCollection(collection) {
+		return helper.ParamInvalidError("collection", fmt.Sprintf("不允许查询此集合,可选: %v", mongoAllowedCollections)), nil
+	}
+
+	filterStr := req.GetString("filter", "{}")
+	var filter bson.M
+	if err := json.Unmarshal([]byte(filterStr), &filter); err != nil {
+		return helper.ParamInvalidError("filter", fmt.Sprintf("JSON 解析失败: %v", err)), nil
+	}
+
+	coll := database.GetCollection(collection)
+	count, err := coll.CountDocuments(ctx, filter)
+	if err != nil {
+		return helper.ServiceError("MongoDB统计", err), nil
+	}
+
+	return helper.JSONResult(map[string]any{
+		"collection": collection,
+		"count":      count,
+	}), nil
+}
+
+func isAllowedCollection(name string) bool {
+	for _, c := range mongoAllowedCollections {
+		if c == name {
+			return true
+		}
+	}
+	return false
+}

+ 140 - 0
service/cmd/mcp/tools/data/redis.go

@@ -0,0 +1,140 @@
+package data
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/cache/redisx"
+	"fwh-toolbox-server/cmd/mcp/tools/helper"
+)
+
+func registerRedisTools(s *server.MCPServer) {
+	s.AddTool(
+		mcp.NewTool("data_redis_get",
+			mcp.WithDescription("读取 Redis 中指定 key 的值(只读,仅支持 string 和 hash 类型)"),
+			mcp.WithString("key", mcp.Required(), mcp.Description("Redis key")),
+			mcp.WithString("type", mcp.Description("key 的数据类型:string(默认)、hash、list、set")),
+		),
+		handleRedisGet,
+	)
+
+	s.AddTool(
+		mcp.NewTool("data_redis_exists",
+			mcp.WithDescription("检查 Redis key 是否存在,并返回 TTL 信息"),
+			mcp.WithString("key", mcp.Required(), mcp.Description("Redis key")),
+		),
+		handleRedisExists,
+	)
+}
+
+func handleRedisGet(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	key, err := req.RequireString("key")
+	if err != nil {
+		return helper.ParamError("key"), nil
+	}
+
+	keyType := req.GetString("type", "string")
+
+	switch keyType {
+	case "string":
+		val, err := redisx.Get(ctx, key)
+		if err != nil {
+			return helper.EmptyResult(fmt.Sprintf("key 不存在或读取失败: %s (%v)", key, err)), nil
+		}
+		return helper.JSONResult(map[string]any{
+			"key":   key,
+			"type":  "string",
+			"value": val,
+		}), nil
+
+	case "hash":
+		val, err := redisx.HGetAll(ctx, key)
+		if err != nil {
+			return helper.ServiceError("读取Redis hash", err), nil
+		}
+		if len(val) == 0 {
+			return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
+		}
+		return helper.JSONResult(map[string]any{
+			"key":   key,
+			"type":  "hash",
+			"value": val,
+		}), nil
+
+	case "list":
+		val, err := redisx.LRange(ctx, key, 0, 99)
+		if err != nil {
+			return helper.ServiceError("读取Redis list", err), nil
+		}
+		if len(val) == 0 {
+			return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
+		}
+		return helper.JSONResult(map[string]any{
+			"key":   key,
+			"type":  "list",
+			"count": len(val),
+			"value": val,
+		}), nil
+
+	case "set":
+		val, err := redisx.SMembers(ctx, key)
+		if err != nil {
+			return helper.ServiceError("读取Redis set", err), nil
+		}
+		if len(val) == 0 {
+			return helper.EmptyResult(fmt.Sprintf("key 不存在或为空: %s", key)), nil
+		}
+		return helper.JSONResult(map[string]any{
+			"key":   key,
+			"type":  "set",
+			"count": len(val),
+			"value": val,
+		}), nil
+
+	default:
+		return helper.ParamInvalidError("type", "可选值: string, hash, list, set"), nil
+	}
+}
+
+func handleRedisExists(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	key, err := req.RequireString("key")
+	if err != nil {
+		return helper.ParamError("key"), nil
+	}
+
+	// 安全检查:不允许通配符
+	if strings.Contains(key, "*") {
+		return helper.ParamInvalidError("key", "不支持通配符,请提供精确 key"), nil
+	}
+
+	exists, err := redisx.Exists(ctx, key)
+	if err != nil {
+		return helper.ServiceError("检查Redis key", err), nil
+	}
+
+	if !exists {
+		return helper.EmptyResult(fmt.Sprintf("key 不存在: %s", key)), nil
+	}
+
+	ttl, err := redisx.TTL(ctx, key)
+	if err != nil {
+		return helper.ServiceError("获取TTL", err), nil
+	}
+
+	ttlStr := "永不过期"
+	if ttl > 0 {
+		ttlStr = ttl.String()
+	} else if ttl == -2 {
+		ttlStr = "key 不存在"
+	}
+
+	return helper.JSONResult(map[string]any{
+		"key":    key,
+		"exists": true,
+		"ttl":    ttlStr,
+	}), nil
+}

+ 11 - 0
service/cmd/mcp/tools/data/register.go

@@ -0,0 +1,11 @@
+package data
+
+import "github.com/mark3labs/mcp-go/server"
+
+// RegisterDataTools 注册所有数据直查层 MCP Tools
+func RegisterDataTools(s *server.MCPServer) {
+	registerMongoTools(s)
+	registerRedisTools(s)
+	// registerPostgresTools(s)  // 后续按需启用
+	// registerMysqlTools(s)     // 后续按需启用
+}

+ 0 - 80
service/cmd/mcp/tools/fileUpload.go

@@ -1,80 +0,0 @@
-package tools
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-
-	"fwh-toolbox-server/utils/filestore"
-)
-
-// mcpFileStore MCP 专用的文件存储实例
-var mcpFileStore *filestore.FileStore
-
-// InitFileStore 初始化 MCP FileStore
-func InitFileStore(fs *filestore.FileStore) {
-	mcpFileStore = fs
-}
-
-// GetFileStore 获取 MCP FileStore 实例(供外部使用)
-func GetFileStore() *filestore.FileStore {
-	return mcpFileStore
-}
-
-func RegisterFileTools(s *server.MCPServer) {
-	// Tool: 读取用户上传的文件
-	s.AddTool(
-		mcp.NewTool("read_uploaded_file",
-			mcp.WithDescription("读取用户上传的文件内容。用户通过前端上传的文件会存储在 /uploads/ 路径下,使用此工具读取文件内容进行分析。"),
-			mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
-		),
-		handleReadUploadedFile,
-	)
-
-	// Tool: 列出用户上传的文件
-	s.AddTool(
-		mcp.NewTool("list_uploaded_files",
-			mcp.WithDescription("列出当前会话中用户上传的所有文件,返回文件名、路径、大小等信息。"),
-		),
-		handleListUploadedFiles,
-	)
-}
-
-func handleReadUploadedFile(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	path, err := req.RequireString("path")
-	if err != nil {
-		return mcp.NewToolResultError("参数 path 必填"), nil
-	}
-
-	if mcpFileStore == nil {
-		return mcp.NewToolResultError("文件存储未初始化"), nil
-	}
-
-	if !mcpFileStore.HasFile(path) {
-		return mcp.NewToolResultError(fmt.Sprintf("文件不存在: %s", path)), nil
-	}
-
-	content, err := mcpFileStore.ReadFile(path)
-	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("读取文件失败: %v", err)), nil
-	}
-
-	return mcp.NewToolResultText(content), nil
-}
-
-func handleListUploadedFiles(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	if mcpFileStore == nil {
-		return mcp.NewToolResultError("文件存储未初始化"), nil
-	}
-
-	files := mcpFileStore.GetFiles()
-	if len(files) == 0 {
-		return mcp.NewToolResultText("当前没有上传的文件"), nil
-	}
-
-	jsonBytes, _ := json.MarshalIndent(files, "", "  ")
-	return mcp.NewToolResultText(string(jsonBytes)), nil
-}

+ 42 - 0
service/cmd/mcp/tools/helper/helper.go

@@ -0,0 +1,42 @@
+package helper
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+)
+
+// JSONResult 将任意数据序列化为 JSON 文本返回
+func JSONResult(data any) *mcp.CallToolResult {
+	bytes, err := json.MarshalIndent(data, "", "  ")
+	if err != nil {
+		return mcp.NewToolResultError(fmt.Sprintf("序列化失败: %v", err))
+	}
+	return mcp.NewToolResultText(string(bytes))
+}
+
+// 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))
+}
+
+// EmptyResult 空结果
+func EmptyResult(msg string) *mcp.CallToolResult {
+	return mcp.NewToolResultText(msg)
+}

+ 0 - 49
service/cmd/mcp/tools/powerFlow.go

@@ -1,49 +0,0 @@
-package tools
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"time"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-
-	"fwh-toolbox-server/services"
-)
-
-func RegisterPowerFlowTools(s *server.MCPServer) {
-	s.AddTool(
-		mcp.NewTool("query_power_flow",
-			mcp.WithDescription("查询站点指定日期的功率流数据(PV、Grid、aPower、HomeLoad、Generator)"),
-			mcp.WithString("sn", mcp.Required(), mcp.Description("站点 SN 序列号")),
-			mcp.WithString("date", mcp.Required(), mcp.Description("日期,格式 yyyy-MM-dd")),
-		),
-		handleQueryPowerFlow,
-	)
-}
-
-func handleQueryPowerFlow(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	sn, err := req.RequireString("sn")
-	if err != nil {
-		return mcp.NewToolResultError("参数 sn 必填"), nil
-	}
-	dateStr, err := req.RequireString("date")
-	if err != nil {
-		return mcp.NewToolResultError("参数 date 必填"), nil
-	}
-
-	// 解析日期
-	day, err := time.Parse("2006-01-02", dateStr)
-	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("日期格式错误,需要 yyyy-MM-dd: %v", err)), nil
-	}
-
-	data, err := services.QuerySitePowerFlow(sn, day)
-	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("查询功率流失败: %v", err)), nil
-	}
-
-	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
-	return mcp.NewToolResultText(string(jsonBytes)), nil
-}

+ 24 - 0
service/cmd/mcp/tools/register.go

@@ -0,0 +1,24 @@
+package tools
+
+import (
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/cmd/mcp/tools/data"
+	"fwh-toolbox-server/cmd/mcp/tools/service"
+	"fwh-toolbox-server/cmd/mcp/tools/storage"
+	toolutil "fwh-toolbox-server/cmd/mcp/tools/util"
+)
+
+// RegisterAll 注册所有 MCP Tools(唯一对外导出入口)
+//
+// 按访问层级分类:
+//   - service: 业务逻辑层,调用 Service 层函数,有业务校验和数据聚合
+//   - data:    数据直查层,直接读取 DB/Cache,用于灵活查询
+//   - storage: 对象存储层,MinIO/文件读取操作
+//   - util:    辅助工具层,格式解析等纯函数
+func RegisterAll(s *server.MCPServer) {
+	service.RegisterServiceTools(s)
+	data.RegisterDataTools(s)
+	storage.RegisterStorageTools(s)
+	toolutil.RegisterUtilTools(s)
+}

+ 16 - 21
service/cmd/mcp/tools/plane.go → service/cmd/mcp/tools/service/feedback.go

@@ -1,21 +1,19 @@
-package tools
+package service
 
 import (
 	"context"
-	"encoding/json"
-	"fmt"
 
 	"github.com/mark3labs/mcp-go/mcp"
 	"github.com/mark3labs/mcp-go/server"
 
+	"fwh-toolbox-server/cmd/mcp/tools/helper"
 	"fwh-toolbox-server/dto"
 	"fwh-toolbox-server/services"
 )
 
-func RegisterPlaneTools(s *server.MCPServer) {
-	// Tool: 创建反馈
+func registerFeedbackTools(s *server.MCPServer) {
 	s.AddTool(
-		mcp.NewTool("create_feedback",
+		mcp.NewTool("service_feedback_create",
 			mcp.WithDescription("在 Plane 创建 Intake Issue(问题反馈或需求提交)"),
 			mcp.WithString("title", mcp.Required(), mcp.Description("标题")),
 			mcp.WithString("type", mcp.Required(), mcp.Description("类型:bug 或 feature")),
@@ -23,29 +21,28 @@ func RegisterPlaneTools(s *server.MCPServer) {
 			mcp.WithString("description", mcp.Description("详细描述")),
 			mcp.WithString("category", mcp.Description("分类:bug类(bug,ui,performance,other) / feature类(new,improvement,experience)")),
 		),
-		handleCreateFeedback,
+		handleFeedbackCreate,
 	)
 
-	// Tool: 查询反馈历史
 	s.AddTool(
-		mcp.NewTool("query_feedback_history",
+		mcp.NewTool("service_feedback_history",
 			mcp.WithDescription("查询 Plane Intake Issue 提交历史(带 Feedback 标签的记录)"),
 		),
-		handleQueryFeedbackHistory,
+		handleFeedbackHistory,
 	)
 }
 
-func handleCreateFeedback(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+func handleFeedbackCreate(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 	title, err := req.RequireString("title")
 	if err != nil {
-		return mcp.NewToolResultError("参数 title 必填"), nil
+		return helper.ParamError("title"), nil
 	}
 	feedbackType, err := req.RequireString("type")
 	if err != nil {
-		return mcp.NewToolResultError("参数 type 必填"), nil
+		return helper.ParamError("type"), nil
 	}
 	if feedbackType != "bug" && feedbackType != "feature" {
-		return mcp.NewToolResultError("type 必须是 bug 或 feature"), nil
+		return helper.ParamInvalidError("type", "必须是 bug 或 feature"), nil
 	}
 
 	priority := req.GetString("priority", "medium")
@@ -62,19 +59,17 @@ func handleCreateFeedback(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
 
 	result, err := services.SubmitFeedback(body, "MCP-Agent")
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("创建失败: %v", err)), nil
+		return helper.ServiceError("创建反馈", err), nil
 	}
 
-	jsonBytes, _ := json.MarshalIndent(result, "", "  ")
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return helper.JSONResult(result), nil
 }
 
-func handleQueryFeedbackHistory(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+func handleFeedbackHistory(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 	data, err := services.GetFeedbackHistory("")
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("查询失败: %v", err)), nil
+		return helper.ServiceError("查询反馈历史", err), nil
 	}
 
-	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return helper.JSONResult(data), nil
 }

+ 61 - 0
service/cmd/mcp/tools/service/monitor.go

@@ -0,0 +1,61 @@
+package service
+
+import (
+	"context"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/cmd/mcp/tools/helper"
+	"fwh-toolbox-server/services"
+)
+
+func registerMonitorTools(s *server.MCPServer) {
+	s.AddTool(
+		mcp.NewTool("service_monitor_alarm_stats",
+			mcp.WithDescription("查询告警统计数据(按日期汇总各类告警数量)"),
+			mcp.WithString("date", mcp.Description("统计日期,格式 yyyy-MM-dd(不传则查最新)")),
+		),
+		handleAlarmStats,
+	)
+
+	s.AddTool(
+		mcp.NewTool("service_monitor_shutdown_stats",
+			mcp.WithDescription("查询停机统计数据(按日期汇总停机站点信息)"),
+			mcp.WithString("date", mcp.Description("统计日期,格式 yyyy-MM-dd(不传则查最新)")),
+		),
+		handleShutdownStats,
+	)
+}
+
+func handleAlarmStats(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	date := req.GetString("date", "")
+
+	var datePtr *string
+	if date != "" {
+		datePtr = &date
+	}
+
+	data, err := services.GetAlarmStatistics(datePtr)
+	if err != nil {
+		return helper.ServiceError("查询告警统计", err), nil
+	}
+
+	return helper.JSONResult(data), nil
+}
+
+func handleShutdownStats(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	date := req.GetString("date", "")
+
+	var datePtr *string
+	if date != "" {
+		datePtr = &date
+	}
+
+	data, err := services.GetShutdownStatistics(datePtr)
+	if err != nil {
+		return helper.ServiceError("查询停机统计", err), nil
+	}
+
+	return helper.JSONResult(data), nil
+}

+ 11 - 0
service/cmd/mcp/tools/service/register.go

@@ -0,0 +1,11 @@
+package service
+
+import "github.com/mark3labs/mcp-go/server"
+
+// RegisterServiceTools 注册所有 Service 层 MCP Tools
+func RegisterServiceTools(s *server.MCPServer) {
+	registerSiteTools(s)
+	registerSimTools(s)
+	registerMonitorTools(s)
+	registerFeedbackTools(s)
+}

+ 14 - 19
service/cmd/mcp/tools/simCard.go → service/cmd/mcp/tools/service/sim.go

@@ -1,29 +1,26 @@
-package tools
+package service
 
 import (
 	"context"
-	"encoding/json"
-	"fmt"
 
 	"github.com/mark3labs/mcp-go/mcp"
 	"github.com/mark3labs/mcp-go/server"
 
+	"fwh-toolbox-server/cmd/mcp/tools/helper"
 	"fwh-toolbox-server/services"
 )
 
-func RegisterSimCardTools(s *server.MCPServer) {
-	// Tool: 查询单张SIM卡
+func registerSimTools(s *server.MCPServer) {
 	s.AddTool(
-		mcp.NewTool("query_sim_card",
+		mcp.NewTool("service_sim_query",
 			mcp.WithDescription("查询单张 SIM 卡的详细信息,包括状态、平台、用量等"),
 			mcp.WithString("giccid", mcp.Required(), mcp.Description("SIM 卡 GICCID(19位)")),
 		),
-		handleQuerySimCard,
+		handleSimQuery,
 	)
 
-	// Tool: 搜索SIM卡列表
 	s.AddTool(
-		mcp.NewTool("search_sim_cards",
+		mcp.NewTool("service_sim_search",
 			mcp.WithDescription("搜索 SIM 卡列表,支持按状态和平台过滤"),
 			mcp.WithNumber("page", mcp.Description("页码,从1开始,默认1")),
 			mcp.WithNumber("size", mcp.Description("每页数量,默认10")),
@@ -31,26 +28,25 @@ func RegisterSimCardTools(s *server.MCPServer) {
 			mcp.WithString("status", mcp.Description("状态过滤:UNKNOWN, NOT_ACTIVATED, ACTIVATED, DEACTIVATED, RETIRED(逗号分隔多选)")),
 			mcp.WithString("platform", mcp.Description("平台过滤:SmartHub, Acceleronix(逗号分隔多选)")),
 		),
-		handleSearchSimCards,
+		handleSimSearch,
 	)
 }
 
-func handleQuerySimCard(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+func handleSimQuery(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 	giccid, err := req.RequireString("giccid")
 	if err != nil {
-		return mcp.NewToolResultError("参数 giccid 必填"), nil
+		return helper.ParamError("giccid"), nil
 	}
 
 	data, err := services.GetSimCardInfo(giccid)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("查询失败: %v", err)), nil
+		return helper.ServiceError("查询SIM卡信息", err), nil
 	}
 
-	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return helper.JSONResult(data), nil
 }
 
-func handleSearchSimCards(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+func handleSimSearch(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
 	page := uint(req.GetFloat("page", 1))
 	size := uint(req.GetFloat("size", 10))
 	search := req.GetString("search", "")
@@ -68,9 +64,8 @@ func handleSearchSimCards(ctx context.Context, req mcp.CallToolRequest) (*mcp.Ca
 
 	data, err := services.GetSimCardBatchInfo(page, size, search, statusPtr, platformPtr)
 	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("搜索失败: %v", err)), nil
+		return helper.ServiceError("搜索SIM卡列表", err), nil
 	}
 
-	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
-	return mcp.NewToolResultText(string(jsonBytes)), nil
+	return helper.JSONResult(data), nil
 }

+ 99 - 0
service/cmd/mcp/tools/service/site.go

@@ -0,0 +1,99 @@
+package service
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/cmd/mcp/tools/helper"
+	"fwh-toolbox-server/dto"
+	"fwh-toolbox-server/services"
+)
+
+func registerSiteTools(s *server.MCPServer) {
+	s.AddTool(
+		mcp.NewTool("service_site_query_info",
+			mcp.WithDescription("查询单个站点设备的详细信息,包括版本、网络状态、激活状态、固件、SN 等"),
+			mcp.WithString("sn", mcp.Required(), mcp.Description("站点 SN 序列号")),
+		),
+		handleSiteQueryInfo,
+	)
+
+	s.AddTool(
+		mcp.NewTool("service_site_search",
+			mcp.WithDescription("搜索站点设备列表,支持分页和关键字搜索"),
+			mcp.WithNumber("page", mcp.Description("页码,从1开始,默认1")),
+			mcp.WithNumber("size", mcp.Description("每页数量,默认10")),
+			mcp.WithString("keyword", mcp.Description("搜索关键字(匹配 SN、设备名)")),
+		),
+		handleSiteSearch,
+	)
+
+	s.AddTool(
+		mcp.NewTool("service_site_power_flow",
+			mcp.WithDescription("查询站点指定日期的功率流数据(PV、Grid、aPower、HomeLoad、Generator)"),
+			mcp.WithString("sn", mcp.Required(), mcp.Description("站点 SN 序列号")),
+			mcp.WithString("date", mcp.Required(), mcp.Description("日期,格式 yyyy-MM-dd")),
+		),
+		handleSitePowerFlow,
+	)
+}
+
+func handleSiteQueryInfo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	sn, err := req.RequireString("sn")
+	if err != nil {
+		return helper.ParamError("sn"), nil
+	}
+
+	data, err := services.GetSiteInfoData(sn)
+	if err != nil {
+		return helper.ServiceError("查询站点信息", err), nil
+	}
+
+	return helper.JSONResult(data), nil
+}
+
+func handleSiteSearch(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	page := uint(req.GetFloat("page", 1))
+	size := uint(req.GetFloat("size", 10))
+	keyword := req.GetString("keyword", "")
+
+	param := dto.SiteInfoRequest{
+		PageNum:  page,
+		PageSize: size,
+		Search:   keyword,
+	}
+
+	data, err := services.QuerySiteInfoDataList(param)
+	if err != nil {
+		return helper.ServiceError("搜索站点列表", err), nil
+	}
+
+	return helper.JSONResult(data), nil
+}
+
+func handleSitePowerFlow(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	sn, err := req.RequireString("sn")
+	if err != nil {
+		return helper.ParamError("sn"), nil
+	}
+	dateStr, err := req.RequireString("date")
+	if err != nil {
+		return helper.ParamError("date"), nil
+	}
+
+	day, err := time.Parse("2006-01-02", dateStr)
+	if err != nil {
+		return helper.ParamInvalidError("date", fmt.Sprintf("需要 yyyy-MM-dd 格式: %v", err)), nil
+	}
+
+	data, err := services.QuerySitePowerFlow(sn, day)
+	if err != nil {
+		return helper.ServiceError("查询功率流", err), nil
+	}
+
+	return helper.JSONResult(data), nil
+}

+ 0 - 39
service/cmd/mcp/tools/shutdown.go

@@ -1,39 +0,0 @@
-package tools
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-
-	"fwh-toolbox-server/services"
-)
-
-func RegisterShutdownTools(s *server.MCPServer) {
-	s.AddTool(
-		mcp.NewTool("query_shutdown_statistics",
-			mcp.WithDescription("查询停机统计数据(按日期汇总停机站点信息)"),
-			mcp.WithString("date", mcp.Description("统计日期,格式 yyyy-MM-dd(不传则查最新)")),
-		),
-		handleQueryShutdownStatistics,
-	)
-}
-
-func handleQueryShutdownStatistics(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	date := req.GetString("date", "")
-
-	var datePtr *string
-	if date != "" {
-		datePtr = &date
-	}
-
-	data, err := services.GetShutdownStatistics(datePtr)
-	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("查询停机统计失败: %v", err)), nil
-	}
-
-	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
-	return mcp.NewToolResultText(string(jsonBytes)), nil
-}

+ 0 - 70
service/cmd/mcp/tools/siteInfo.go

@@ -1,70 +0,0 @@
-package tools
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-
-	"fwh-toolbox-server/dto"
-	"fwh-toolbox-server/services"
-)
-
-func RegisterSiteInfoTools(s *server.MCPServer) {
-	// Tool: 查询站点信息
-	s.AddTool(
-		mcp.NewTool("query_site_info",
-			mcp.WithDescription("查询单个站点设备的详细信息,包括版本、网络状态、激活状态、固件、SN 等"),
-			mcp.WithString("sn", mcp.Required(), mcp.Description("站点 SN 序列号")),
-		),
-		handleQuerySiteInfo,
-	)
-
-	// Tool: 搜索站点列表
-	s.AddTool(
-		mcp.NewTool("search_sites",
-			mcp.WithDescription("搜索站点设备列表,支持分页和关键字搜索"),
-			mcp.WithNumber("page", mcp.Description("页码,从1开始,默认1")),
-			mcp.WithNumber("size", mcp.Description("每页数量,默认10")),
-			mcp.WithString("keyword", mcp.Description("搜索关键字(匹配 SN、设备名)")),
-		),
-		handleSearchSites,
-	)
-}
-
-func handleQuerySiteInfo(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	sn, err := req.RequireString("sn")
-	if err != nil {
-		return mcp.NewToolResultError("参数 sn 必填"), nil
-	}
-
-	data, err := services.GetSiteInfoData(sn)
-	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("查询失败: %v", err)), nil
-	}
-
-	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
-	return mcp.NewToolResultText(string(jsonBytes)), nil
-}
-
-func handleSearchSites(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-	page := uint(req.GetFloat("page", 1))
-	size := uint(req.GetFloat("size", 10))
-	keyword := req.GetString("keyword", "")
-
-	param := dto.SiteInfoRequest{
-		PageNum:  page,
-		PageSize: size,
-		Search:   keyword,
-	}
-
-	data, err := services.QuerySiteInfoDataList(param)
-	if err != nil {
-		return mcp.NewToolResultError(fmt.Sprintf("搜索失败: %v", err)), nil
-	}
-
-	jsonBytes, _ := json.MarshalIndent(data, "", "  ")
-	return mcp.NewToolResultText(string(jsonBytes)), nil
-}

+ 77 - 0
service/cmd/mcp/tools/storage/file.go

@@ -0,0 +1,77 @@
+package storage
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/cmd/mcp/tools/helper"
+	"fwh-toolbox-server/utils/filestore"
+)
+
+// mcpFileStore MCP 专用的文件存储实例
+var mcpFileStore *filestore.FileStore
+
+// InitFileStore 初始化 MCP FileStore(由 main.go 调用)
+func InitFileStore(fs *filestore.FileStore) {
+	mcpFileStore = fs
+}
+
+// GetFileStore 获取 MCP FileStore 实例
+func GetFileStore() *filestore.FileStore {
+	return mcpFileStore
+}
+
+func registerFileTools(s *server.MCPServer) {
+	s.AddTool(
+		mcp.NewTool("storage_file_read",
+			mcp.WithDescription("读取用户上传的文件内容。用户通过前端上传的文件存储在 /uploads/ 路径下。"),
+			mcp.WithString("path", mcp.Required(), mcp.Description("文件路径,格式为 /uploads/{filename}")),
+		),
+		handleFileRead,
+	)
+
+	s.AddTool(
+		mcp.NewTool("storage_file_list",
+			mcp.WithDescription("列出当前会话中用户上传的所有文件,返回文件名、路径、大小等信息。"),
+		),
+		handleFileList,
+	)
+}
+
+func handleFileRead(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	path, err := req.RequireString("path")
+	if err != nil {
+		return helper.ParamError("path"), nil
+	}
+
+	if mcpFileStore == nil {
+		return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
+	}
+
+	if !mcpFileStore.HasFile(path) {
+		return helper.EmptyResult(fmt.Sprintf("文件不存在: %s", path)), nil
+	}
+
+	content, err := mcpFileStore.ReadFile(path)
+	if err != nil {
+		return helper.ServiceError("读取文件", err), nil
+	}
+
+	return helper.TextResult(content), nil
+}
+
+func handleFileList(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	if mcpFileStore == nil {
+		return helper.ServiceError("文件存储", fmt.Errorf("未初始化")), nil
+	}
+
+	files := mcpFileStore.GetFiles()
+	if len(files) == 0 {
+		return helper.EmptyResult("当前没有上传的文件"), nil
+	}
+
+	return helper.JSONResult(files), nil
+}

+ 172 - 0
service/cmd/mcp/tools/storage/minio.go

@@ -0,0 +1,172 @@
+package storage
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"strings"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+	"github.com/minio/minio-go/v7"
+
+	"fwh-toolbox-server/cmd/mcp/tools/helper"
+	minioUtil "fwh-toolbox-server/utils/minio"
+)
+
+// minioAllowedBuckets 允许访问的 bucket 白名单
+var minioAllowedBuckets = []string{
+	"fwh-toolbox-batch-download",
+	"fwh-toolbox-ai-file",
+}
+
+func registerMinioTools(s *server.MCPServer) {
+	s.AddTool(
+		mcp.NewTool("storage_minio_list_objects",
+			mcp.WithDescription("列出 MinIO bucket 中的对象(只读),用于查看已下载的文件"),
+			mcp.WithString("bucket", mcp.Required(), mcp.Description(
+				fmt.Sprintf("Bucket 名称,可选: %v", minioAllowedBuckets),
+			)),
+			mcp.WithString("prefix", mcp.Description("对象前缀过滤(如 SN 号或日期路径)")),
+			mcp.WithNumber("limit", mcp.Description("返回数量限制,默认20,最大100")),
+		),
+		handleMinioListObjects,
+	)
+
+	s.AddTool(
+		mcp.NewTool("storage_minio_read_object",
+			mcp.WithDescription("读取 MinIO 中的文本文件内容(只读,仅支持文本类型文件,最大 1MB)"),
+			mcp.WithString("bucket", mcp.Required(), mcp.Description(
+				fmt.Sprintf("Bucket 名称,可选: %v", minioAllowedBuckets),
+			)),
+			mcp.WithString("key", mcp.Required(), mcp.Description("对象 key(完整路径)")),
+		),
+		handleMinioReadObject,
+	)
+}
+
+func handleMinioListObjects(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	bucket, err := req.RequireString("bucket")
+	if err != nil {
+		return helper.ParamError("bucket"), nil
+	}
+
+	if !isAllowedBucket(bucket) {
+		return helper.ParamInvalidError("bucket", fmt.Sprintf("不允许访问此 bucket,可选: %v", minioAllowedBuckets)), nil
+	}
+
+	prefix := req.GetString("prefix", "")
+	limit := int(req.GetFloat("limit", 20))
+	if limit > 100 {
+		limit = 100
+	}
+
+	opts := minio.ListObjectsOptions{
+		Prefix:    prefix,
+		Recursive: true,
+	}
+
+	type ObjectInfo struct {
+		Key          string `json:"key"`
+		Size         int64  `json:"size"`
+		LastModified string `json:"lastModified"`
+		ContentType  string `json:"contentType,omitempty"`
+	}
+
+	var objects []ObjectInfo
+	for obj := range minioUtil.Client.ListObjects(ctx, bucket, opts) {
+		if obj.Err != nil {
+			return helper.ServiceError("列出MinIO对象", obj.Err), nil
+		}
+		objects = append(objects, ObjectInfo{
+			Key:          obj.Key,
+			Size:         obj.Size,
+			LastModified: obj.LastModified.Format("2006-01-02 15:04:05"),
+			ContentType:  obj.ContentType,
+		})
+		if len(objects) >= limit {
+			break
+		}
+	}
+
+	if len(objects) == 0 {
+		return helper.EmptyResult(fmt.Sprintf("bucket %s 中无匹配对象 (prefix: %s)", bucket, prefix)), nil
+	}
+
+	return helper.JSONResult(map[string]any{
+		"bucket": bucket,
+		"prefix": prefix,
+		"count":  len(objects),
+		"objects": objects,
+	}), nil
+}
+
+func handleMinioReadObject(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	bucket, err := req.RequireString("bucket")
+	if err != nil {
+		return helper.ParamError("bucket"), nil
+	}
+	key, err := req.RequireString("key")
+	if err != nil {
+		return helper.ParamError("key"), nil
+	}
+
+	if !isAllowedBucket(bucket) {
+		return helper.ParamInvalidError("bucket", fmt.Sprintf("不允许访问此 bucket,可选: %v", minioAllowedBuckets)), nil
+	}
+
+	// 获取对象信息
+	stat, err := minioUtil.Client.StatObject(ctx, bucket, key, minio.StatObjectOptions{})
+	if err != nil {
+		return helper.ServiceError("获取对象信息", err), nil
+	}
+
+	// 限制大小:仅允许读取 1MB 以内的文本文件
+	const maxSize = 1 * 1024 * 1024
+	if stat.Size > maxSize {
+		return helper.ParamInvalidError("key", fmt.Sprintf("文件过大 (%d bytes),仅支持读取 1MB 以内的文本文件", stat.Size)), nil
+	}
+
+	// 检查是否为文本类型
+	if !isTextContentType(stat.ContentType) {
+		return helper.ParamInvalidError("key", fmt.Sprintf("非文本文件 (content-type: %s),不支持读取", stat.ContentType)), nil
+	}
+
+	obj, err := minioUtil.Client.GetObject(ctx, bucket, key, minio.GetObjectOptions{})
+	if err != nil {
+		return helper.ServiceError("读取MinIO对象", err), nil
+	}
+	defer obj.Close()
+
+	data, err := io.ReadAll(obj)
+	if err != nil {
+		return helper.ServiceError("读取对象内容", err), nil
+	}
+
+	return helper.TextResult(string(data)), nil
+}
+
+func isAllowedBucket(name string) bool {
+	for _, b := range minioAllowedBuckets {
+		if b == name {
+			return true
+		}
+	}
+	return false
+}
+
+func isTextContentType(ct string) bool {
+	textTypes := []string{
+		"text/",
+		"application/json",
+		"application/xml",
+		"application/csv",
+		"application/javascript",
+	}
+	for _, t := range textTypes {
+		if strings.HasPrefix(ct, t) || strings.Contains(ct, t) {
+			return true
+		}
+	}
+	return false
+}

+ 9 - 0
service/cmd/mcp/tools/storage/register.go

@@ -0,0 +1,9 @@
+package storage
+
+import "github.com/mark3labs/mcp-go/server"
+
+// RegisterStorageTools 注册所有对象存储层 MCP Tools
+func RegisterStorageTools(s *server.MCPServer) {
+	registerFileTools(s)
+	registerMinioTools(s)
+}

+ 83 - 0
service/cmd/mcp/tools/util/format.go

@@ -0,0 +1,83 @@
+package util
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/mark3labs/mcp-go/mcp"
+	"github.com/mark3labs/mcp-go/server"
+
+	"fwh-toolbox-server/cmd/mcp/tools/helper"
+)
+
+func registerFormatTools(s *server.MCPServer) {
+	s.AddTool(
+		mcp.NewTool("util_parse_sn",
+			mcp.WithDescription(`解析 FranklinWH 设备 SN 序列号的含义。
+SN 格式: {产品代码}{版本}{工厂}{年}{周}{序号}
+示例: 10060006A03X25310091
+- 1006: 产品代码 (aGate=1006, aPower=1005, aHub/PowerLink=1002)
+- 0006: 硬件版本
+- A: 工厂代码
+- 03: 版本标识
+- X: 预留
+- 25: 年份(2025)
+- 31: 周数
+- 0091: 序号`),
+			mcp.WithString("sn", mcp.Required(), mcp.Description("设备 SN 序列号")),
+		),
+		handleParseSN,
+	)
+}
+
+func handleParseSN(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+	sn, err := req.RequireString("sn")
+	if err != nil {
+		return helper.ParamError("sn"), nil
+	}
+
+	sn = strings.TrimSpace(sn)
+	if len(sn) < 20 {
+		return helper.ParamInvalidError("sn", "SN 长度不足,标准长度为 20 位"), nil
+	}
+
+	// 解析产品类型
+	productCode := sn[0:4]
+	var productName string
+	switch productCode {
+	case "1006":
+		productName = "aGate (网关)"
+	case "1005":
+		productName = "aPower (储能电池)"
+	case "1002":
+		productName = "aHub / PowerLink"
+	case "1003":
+		productName = "MPPT"
+	default:
+		productName = "未知产品"
+	}
+
+	hwVersion := sn[4:8]
+	factory := string(sn[8])
+	verFlag := sn[9:11]
+	year := sn[12:14]
+	week := sn[14:16]
+	serial := sn[16:]
+
+	result := map[string]any{
+		"sn":           sn,
+		"productCode":  productCode,
+		"productName":  productName,
+		"hwVersion":    hwVersion,
+		"factory":      factory,
+		"versionFlag":  verFlag,
+		"year":         fmt.Sprintf("20%s", year),
+		"week":         week,
+		"serialNumber": serial,
+		"summary": fmt.Sprintf("%s | HW:%s | 工厂:%s | 20%s年第%s周 | 序号:%s",
+			productName, hwVersion, factory, year, week, serial),
+	}
+
+	return helper.JSONResult(result), nil
+}

+ 8 - 0
service/cmd/mcp/tools/util/register.go

@@ -0,0 +1,8 @@
+package util
+
+import "github.com/mark3labs/mcp-go/server"
+
+// RegisterUtilTools 注册所有辅助工具层 MCP Tools
+func RegisterUtilTools(s *server.MCPServer) {
+	registerFormatTools(s)
+}