feat: 添加MCP时间服务器及相关工具

实现一个基于Golang的MCP时间服务器,提供获取当前时间和日期功能
包含客户端示例、安装脚本和详细文档

refactor: 优化磁盘巡检脚本以支持SAS和SSD硬盘

增强磁盘巡检脚本的兼容性,改进SMART信息解析逻辑
添加硬盘类型检测和更全面的错误处理

docs: 更新README和安装说明

添加MCP时间服务器的使用文档和API说明
完善磁盘巡检报告格式和内容
This commit is contained in:
cnphpbb
2025-09-12 13:45:08 +08:00
parent 45d8181205
commit 648faac289
12 changed files with 1160 additions and 20 deletions

152
mcpTimeServer/README.md Normal file
View File

@@ -0,0 +1,152 @@
# MCP Time Server
一个使用Golang实现的MCPModel Context Protocol服务器提供获取当前系统时间和SSEServer-Sent Events实时时间流功能。
## 项目概述
这个服务器实现了自定义的MCP协议允许客户端通过HTTP接口获取当前系统时间并且支持通过SSE技术订阅实时时间更新流。服务器使用slog库进行日志记录日志仅输出到控制台。
## 功能特性
- 提供获取当前系统时间的MCP工具`get_current_time`
- 支持自定义时间格式
- 实现SSEServer-Sent Events功能提供实时时间流订阅`subscribe_time_stream`
- 使用slog进行结构化日志记录
- 基于HTTP实现MCP协议
- 提供健康检查接口
- 支持优雅关闭
## 目录结构
```
mcpTimeServer/
├── main.go # 服务器主程序
├── go.mod # Go模块定义
├── install.sh # 安装脚本
└── README.md # 项目说明文档
```
## 安装与配置
### 前提条件
- Go 1.21或更高版本
### 安装步骤
1. 克隆项目或进入项目目录
2. 运行安装脚本:
```bash
chmod +x install.sh
./install.sh
```
安装脚本会检查Go环境、安装依赖并编译项目。
## 使用方法
### 启动服务器
```bash
./mcp_time_server
```
服务器启动后会在8080端口监听HTTP请求。
## API说明
### MCP协议接口
#### 提交MCP请求
- **URL**: `/mcp/v1/submit`
- **方法**: `POST`
- **Content-Type**: `application/json`
**请求体示例**:
```json
{
"data": {"format": "2006-01-02 15:04:05"}, // 可选参数
"type": "get_current_time",
"timestamp": 1699999999
}
```
#### 可用工具
1. **get_current_time**
- **参数**: `format`(可选)- 时间格式字符串使用Go的时间格式语法如"2006-01-02 15:04:05"
- **返回结果**:
```json
{
"current_time": "格式化的时间字符串",
"timestamp": 时间戳Unix时间
}
```
2. **subscribe_time_stream**
- **参数**:
- `interval`(可选)- 时间更新间隔默认为1秒
- `format`(可选)- 时间格式字符串
- **返回结果**:
```json
{
"stream_id": "唯一的流标识符",
"sse_url": "http://localhost:8080/sse",
"interval": 更新间隔(秒)
}
```
### SSE接口
- **URL**: `/sse`
- **方法**: `GET`
- **Content-Type**: `text/event-stream`
连接后,服务器会以指定的间隔发送时间更新事件。
### 健康检查接口
- **URL**: `/health`
- **方法**: `GET`
- **返回**: 状态码200表示服务器正常运行
## 测试方法
服务器启动后,可以使用以下方式测试:
1. **健康检查**:
```bash
curl http://localhost:8080/health
```
2. **获取当前时间**:
```bash
curl -X POST http://localhost:8080/mcp/v1/submit -H 'Content-Type: application/json' -d '{"data":{},"type":"get_current_time","timestamp":'$(date +%s)'}'
```
3. **使用自定义格式获取当前时间**:
```bash
curl -X POST http://localhost:8080/mcp/v1/submit -H 'Content-Type: application/json' -d '{"data":{"format":"2006-01-02 15:04:05"},"type":"get_current_time","timestamp":'$(date +%s)'}'
```
4. **订阅时间流**:
```bash
curl http://localhost:8080/sse
```
或直接在浏览器中访问 `http://localhost:8080/sse`
## 依赖说明
- github.com/google/uuid v1.6.0:用于生成唯一标识符
## 注意事项
- 服务器默认监听在8080端口
- 日志仅输出到控制台,不会写入文件
- SSE连接会在客户端断开或服务器关闭时终止
## License
MIT

5
mcpTimeServer/go.mod Normal file
View File

@@ -0,0 +1,5 @@
module mcpTimeServer
go 1.21
require github.com/google/uuid v1.6.0

71
mcpTimeServer/install.sh Normal file
View File

@@ -0,0 +1,71 @@
#!/bin/bash
# MCP Time Server 安装脚本
# 设置颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # 无颜色
# 检查是否安装了Go
echo -e "${BLUE}检查Go环境...${NC}"
if ! command -v go &> /dev/null
then
echo -e "${RED}错误: 未安装Go。请先安装Go 1.21或更高版本。${NC}"
exit 1
fi
# 检查Go版本
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
GO_MAJOR=$(echo $GO_VERSION | cut -d. -f1)
GO_MINOR=$(echo $GO_VERSION | cut -d. -f2)
if [ $GO_MAJOR -lt 1 ] || ([ $GO_MAJOR -eq 1 ] && [ $GO_MINOR -lt 21 ]); then
echo -e "${RED}错误: Go版本过低 ($GO_VERSION)。请安装Go 1.21或更高版本。${NC}"
exit 1
fi
echo -e "${GREEN}已安装Go ($GO_VERSION)${NC}"
# 检查当前目录
echo -e "${BLUE}\n检查项目目录...${NC}"
if [ ! -f "main.go" ]; then
echo -e "${RED}错误: 请在包含main.go的项目根目录下运行此脚本。${NC}"
exit 1
fi
echo -e "${GREEN}项目目录正确${NC}"
# 安装依赖
echo -e "${BLUE}\n安装项目依赖...${NC}"
go mod tidy
if [ $? -ne 0 ]; then
echo -e "${RED}安装依赖失败,请检查网络连接。${NC}"
exit 1
fi
echo -e "${GREEN}依赖安装成功${NC}"
# 编译项目
echo -e "${BLUE}\n编译项目...${NC}"
go build -o mcp_time_server
if [ $? -ne 0 ]; then
echo -e "${RED}编译失败。${NC}"
exit 1
fi
chmod +x mcp_time_server
echo -e "${GREEN}编译成功,生成可执行文件: mcp_time_server${NC}"
# 显示使用说明
echo -e "\n${GREEN}安装完成!${NC}"
echo -e "${BLUE}\n使用说明:${NC}"
echo -e "1. 启动服务器: ./mcp_time_server"
echo -e "2. 服务器启动后,可以通过以下方式测试:"
echo -e " - 健康检查: curl http://localhost:8080/health"
echo -e " - 获取当前时间: curl -X POST http://localhost:8080/mcp/v1/submit -H 'Content-Type: application/json' -d '{\"data\":{},\"type\":\"get_current_time\",\"timestamp\":$(date +%s)}'"
echo -e " - 订阅时间流: 使用浏览器或SSE客户端访问 http://localhost:8080/sse"
echo -e "\n${YELLOW}注意: 服务器默认监听在8080端口。${NC}"
echo -e "\n${GREEN}MCP Time Server安装成功${NC}"

385
mcpTimeServer/main.go Normal file
View File

@@ -0,0 +1,385 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/google/uuid"
)
// MCPRequest MCP请求结构体
type MCPRequest struct {
Data interface{} `json:"data"`
Type string `json:"type"`
Metadata map[string]string `json:"metadata,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// MCPResponse MCP响应结构体
type MCPResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data interface{} `json:"data,omitempty"`
RequestID string `json:"request_id,omitempty"`
Timestamp int64 `json:"timestamp"`
}
// SSEClient SSE客户端连接管理
type SSEClient struct {
ID string
Channel chan []byte
}
// SSEManager SSE管理器
type SSEManager struct {
clients map[string]*SSEClient
mutex sync.Mutex
}
// NewSSEManager 创建新的SSE管理器
func NewSSEManager() *SSEManager {
return &SSEManager{
clients: make(map[string]*SSEClient),
}
}
// AddClient 添加SSE客户端
func (m *SSEManager) AddClient(clientID string) *SSEClient {
m.mutex.Lock()
defer m.mutex.Unlock()
client := &SSEClient{
ID: clientID,
Channel: make(chan []byte, 10),
}
m.clients[clientID] = client
return client
}
// RemoveClient 移除SSE客户端
func (m *SSEManager) RemoveClient(clientID string) {
m.mutex.Lock()
defer m.mutex.Unlock()
if client, exists := m.clients[clientID]; exists {
close(client.Channel)
delete(m.clients, clientID)
}
}
// Broadcast 广播消息给所有SSE客户端
func (m *SSEManager) Broadcast(message []byte) {
m.mutex.Lock()
defer m.mutex.Unlock()
for _, client := range m.clients {
select {
case client.Channel <- message:
default:
// 如果客户端通道已满,跳过
}
}
}
// 格式化时间,处理可能的格式错误
func formatTime(t time.Time, format string) (string, error) {
if format == "" {
return t.Format(time.RFC3339), nil
}
// 尝试使用自定义格式
formatted := t.Format(format)
if formatted == format {
// 如果格式化后的结果与格式字符串相同,说明格式无效
return "", errors.New("无效的时间格式")
}
return formatted, nil
}
// 处理获取当前时间请求
func handleGetCurrentTime(data map[string]interface{}) (map[string]interface{}, error) {
// 获取当前时间
now := time.Now()
// 获取可选的格式参数
format, _ := data["format"].(string)
// 格式化时间
formattedTime, err := formatTime(now, format)
if err != nil {
// 如果格式无效,使用默认格式
formattedTime = now.Format(time.RFC3339)
}
// 返回结果
result := map[string]interface{}{
"current_time": formattedTime,
"timestamp": now.Unix(),
}
return result, nil
}
// 处理订阅时间流请求
func handleSubscribeTimeStream(data map[string]interface{}) (map[string]interface{}, error) {
// 生成唯一的流ID
streamID := uuid.New().String()
// 获取可选的间隔参数默认1秒
interval := 1.0
if intervalVal, ok := data["interval"].(float64); ok {
if intervalVal > 0 {
interval = intervalVal
}
}
// 获取可选的格式参数
format, _ := data["format"].(string)
// 存储订阅信息(实际项目中可能需要更复杂的存储机制)
// 这里我们只是返回流信息实际的SSE连接会在/sse端点建立
// 返回结果
result := map[string]interface{}{
"stream_id": streamID,
"sse_url": "http://localhost:8080/sse",
"interval": interval,
"format": format,
}
return result, nil
}
// 处理MCP请求提交
func handleSubmit(w http.ResponseWriter, r *http.Request) {
// 检查请求方法
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析请求体
var request MCPRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
response := MCPResponse{
Success: false,
Message: "Invalid request body",
Timestamp: time.Now().Unix(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
return
}
// 生成请求ID
requestID := uuid.New().String()
// 根据工具类型处理请求
var result map[string]interface{}
var err error
switch request.Type {
case "get_current_time":
// 确保data是map类型
dataMap, ok := request.Data.(map[string]interface{})
if !ok {
dataMap = make(map[string]interface{})
}
result, err = handleGetCurrentTime(dataMap)
case "subscribe_time_stream":
// 确保data是map类型
dataMap, ok := request.Data.(map[string]interface{})
if !ok {
dataMap = make(map[string]interface{})
}
result, err = handleSubscribeTimeStream(dataMap)
default:
err = errors.New("未知的工具类型")
}
// 构建响应
response := MCPResponse{
Success: err == nil,
Message: func() string { if err != nil { return err.Error() } return "" }(),
Data: result,
RequestID: requestID,
Timestamp: time.Now().Unix(),
}
// 返回响应
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
// 记录日志
slog.Debug("处理MCP请求", "request_id", requestID, "tool_type", request.Type, "success", err == nil)
}
// 处理SSE连接
func handleSSE(w http.ResponseWriter, r *http.Request, sseManager *SSEManager) {
// 设置响应头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
// 获取或生成客户端ID
clientID := r.URL.Query().Get("client_id")
if clientID == "" {
clientID = uuid.New().String()
}
// 添加客户端到管理器
client := sseManager.AddClient(clientID)
defer sseManager.RemoveClient(clientID)
// 发送初始连接确认事件
fmt.Fprintf(w, "event: connected\ndata: {\"client_id\":\"%s\"}\n\n", clientID)
flusher, ok := w.(http.Flusher)
if !ok {
slog.Error("不支持SSE", "client_id", clientID)
return
}
flusher.Flush()
// 获取时间格式参数(如果有)
format := r.URL.Query().Get("format")
// 获取时间间隔参数(如果有)
interval := 1.0
if intervalStr := r.URL.Query().Get("interval"); intervalStr != "" {
fmt.Sscanf(intervalStr, "%f", &interval)
if interval <= 0 {
interval = 1.0
}
}
// 创建上下文用于取消操作
ctx, cancel := context.WithCancel(r.Context())
defer cancel()
// 创建定时器
ticker := time.NewTicker(time.Duration(interval * float64(time.Second)))
defer ticker.Stop()
slog.Info("SSE连接已建立", "client_id", clientID, "interval", interval, "format", format)
// 发送时间更新
for {
select {
case <-ctx.Done():
// 客户端断开连接
slog.Info("SSE连接已断开", "client_id", clientID)
return
case now := <-ticker.C:
// 格式化时间
var formattedTime string
if format == "" {
formattedTime = now.Format(time.RFC3339)
} else {
var err error
formattedTime, err = formatTime(now, format)
if err != nil {
// 如果格式无效,使用默认格式
formattedTime = now.Format(time.RFC3339)
}
}
// 创建时间更新事件
timeData := map[string]interface{}{
"current_time": formattedTime,
"timestamp": now.Unix(),
"interval": interval,
}
// 转换为JSON
dataJSON, _ := json.Marshal(timeData)
// 发送SSE事件
fmt.Fprintf(w, "event: time_update\ndata: %s\n\n", string(dataJSON))
flusher.Flush()
slog.Debug("发送SSE时间更新", "client_id", clientID, "time", formattedTime)
case message := <-client.Channel:
// 发送广播消息
fmt.Fprintf(w, "event: broadcast\ndata: %s\n\n", string(message))
flusher.Flush()
}
}
}
// handleHealth 处理健康检查请求
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
func main() {
// 设置slog日志仅输出到控制台不写入文件
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
slog.SetDefault(logger)
logger.Info("MCP Time Server 启动")
// 创建SSE管理器
sseManager := NewSSEManager()
// 创建路由器
r := http.NewServeMux()
// 注册健康检查端点
r.HandleFunc("/health", handleHealth)
// 注册MCP提交端点
r.HandleFunc("/mcp/v1/submit", handleSubmit)
// 注册SSE端点
r.HandleFunc("/sse", func(w http.ResponseWriter, r *http.Request) {
handleSSE(w, r, sseManager)
})
// 创建HTTP服务器
srv := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// 启动服务器
go func() {
logger.Info("MCP Time Server 启动成功", "address", ":8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("服务器启动失败", "error", err)
os.Exit(1)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("MCP Time Server 正在关闭...")
// 创建超时上下文
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 优雅关闭服务器
if err := srv.Shutdown(ctx); err != nil {
logger.Error("服务器关闭失败", "error", err)
os.Exit(1)
}
logger.Info("MCP Time Server 已安全关闭")
}

BIN
mcpTimeServer/mcp_client_example Executable file

Binary file not shown.

View File

@@ -0,0 +1,190 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
// 这里使用通用的map类型表示请求和响应避免与main.go中的结构体冲突
func main() {
// 服务器地址
serverURL := "http://localhost:8080"
// 1. 测试健康检查
testHealthCheck(serverURL)
// 2. 测试获取当前时间(默认格式)
testGetCurrentTime(serverURL, "")
// 3. 测试获取当前时间(自定义格式)
testGetCurrentTime(serverURL, "2006-01-02 15:04:05")
// 4. 测试订阅时间流
testSubscribeTimeStream(serverURL)
}
// 测试健康检查
func testHealthCheck(serverURL string) {
url := fmt.Sprintf("%s/health", serverURL)
resp, err := http.Get(url)
if err != nil {
fmt.Printf("健康检查请求失败: %v\n", err)
return
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
fmt.Println("✅ 健康检查成功: 服务器正常运行")
} else {
fmt.Printf("❌ 健康检查失败: 状态码 %d\n", resp.StatusCode)
}
}
// 测试获取当前时间
func testGetCurrentTime(serverURL string, format string) {
url := fmt.Sprintf("%s/mcp/v1/submit", serverURL)
// 构建请求体
requestBody := map[string]interface{}{
"data": map[string]interface{}{},
"type": "get_current_time",
"timestamp": time.Now().Unix(),
}
// 添加格式参数(如果提供)
if format != "" {
data := requestBody["data"].(map[string]interface{})
data["format"] = format
}
// 序列化请求体
jsonData, err := json.Marshal(requestBody)
if err != nil {
fmt.Printf("序列化请求体失败: %v\n", err)
return
}
// 发送POST请求
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Printf("发送请求失败: %v\n", err)
return
}
defer resp.Body.Close()
// 读取响应
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取响应失败: %v\n", err)
return
}
// 解析响应为map
var response map[string]interface{}
if err := json.Unmarshal(body, &response); err != nil {
fmt.Printf("解析响应失败: %v\n", err)
return
}
// 检查响应状态
success, ok := response["success"].(bool)
if !ok || !success {
message := "未知错误"
if msg, ok := response["message"].(string); ok {
message = msg
}
fmt.Printf("❌ 获取当前时间失败: %s\n", message)
return
}
// 提取数据
data, ok := response["data"].(map[string]interface{})
if !ok {
fmt.Println("❌ 响应数据格式错误")
return
}
// 获取时间信息
currentTime, _ := data["current_time"].(string)
timestamp, _ := data["timestamp"].(float64)
fmt.Printf("✅ 获取当前时间成功 (格式: %s):\n", format)
fmt.Printf(" 时间: %s\n", currentTime)
fmt.Printf(" 时间戳: %.0f\n", timestamp)
}
// 测试订阅时间流
func testSubscribeTimeStream(serverURL string) {
url := fmt.Sprintf("%s/mcp/v1/submit", serverURL)
// 构建请求体
requestBody := map[string]interface{}{
"data": map[string]interface{}{
"interval": 2, // 每2秒更新一次
"format": "2006-01-02 15:04:05",
},
"type": "subscribe_time_stream",
"timestamp": time.Now().Unix(),
}
// 序列化请求体
jsonData, err := json.Marshal(requestBody)
if err != nil {
fmt.Printf("序列化请求体失败: %v\n", err)
return
}
// 发送POST请求
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Printf("发送请求失败: %v\n", err)
return
}
defer resp.Body.Close()
// 读取响应
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取响应失败: %v\n", err)
return
}
// 解析响应为map
var response map[string]interface{}
if err := json.Unmarshal(body, &response); err != nil {
fmt.Printf("解析响应失败: %v\n", err)
return
}
// 检查响应状态
success, ok := response["success"].(bool)
if !ok || !success {
message := "未知错误"
if msg, ok := response["message"].(string); ok {
message = msg
}
fmt.Printf("❌ 订阅时间流失败: %s\n", message)
return
}
// 提取数据
data, ok := response["data"].(map[string]interface{})
if !ok {
fmt.Println("❌ 响应数据格式错误")
return
}
// 获取流信息
streamID, _ := data["stream_id"].(string)
sseURL, _ := data["sse_url"].(string)
interval, _ := data["interval"].(float64)
fmt.Printf("✅ 订阅时间流成功:\n")
fmt.Printf(" 流ID: %s\n", streamID)
fmt.Printf(" SSE URL: %s\n", sseURL)
fmt.Printf(" 更新间隔: %.0f秒\n", interval)
fmt.Println(" 提示: 可以使用curl或浏览器访问SSE URL来接收实时时间更新")
}

BIN
mcpTimeServer/mcp_time_server Executable file

Binary file not shown.