diff --git a/mcp_server_go/.dockerignore b/mcp_server_go/.dockerignore new file mode 100644 index 0000000..37ad6f0 --- /dev/null +++ b/mcp_server_go/.dockerignore @@ -0,0 +1,41 @@ +# 编译产物 +*.o +*.a +*.so +*.exe +*.out +mcp_server + +# 依赖缓存 +vendor/ + +# 日志文件 +logs/ +*.log + +# 测试文件 +*.test +coverage.out + +# IDE配置 +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# 操作系统文件 +.DS_Store +Thumbs.db + +# 环境变量 +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# 临时文件 +/tmp +/temp +.cache \ No newline at end of file diff --git a/mcp_server_go/README.md b/mcp_server_go/README.md new file mode 100644 index 0000000..8dba1b6 --- /dev/null +++ b/mcp_server_go/README.md @@ -0,0 +1,297 @@ +# MCP Server Go + +一个使用Golang实现的MCP(Master Control Program)服务器,能够对接开放的AI API(如OpenAI),提供数据分析和处理功能。 + +## 项目特点 + +- 基于Golang开发,高性能、低资源占用 +- 使用slog标准库进行日志管理,支持多级别、多格式日志输出 +- 使用TOML格式配置文件,配置简单明了 +- 支持对接OpenAI API,可扩展支持其他AI服务 +- 提供健康检查、Prometheus监控指标 +- 实现CORS跨域支持、请求速率限制、请求日志等中间件 +- 支持配置文件热重载 +- 优雅关闭机制 + +## 目录结构 + +``` +mcp_server_go/ +├── main.go # 主程序文件 +├── config.toml # 配置文件 +├── README.md # 项目说明文档 +├── go.mod # Go模块定义 +├── go.sum # 依赖版本锁定 +└── logs/ # 日志文件目录 + └── mcp_server.log # 日志文件 +``` + +## 配置说明 + +配置文件`config.toml`包含以下主要配置项: + +```toml +# 服务器基本配置 +[server] +listen_addr = "0.0.0.0:8080" # 监听地址和端口 +read_timeout = 30 # 读取超时时间(秒) +write_timeout = 30 # 写入超时时间(秒) +max_header_bytes = 1048576 # 最大请求头大小(字节) + +# OpenAI API 配置 +[openai] +api_key = "your_api_key_here" # OpenAI API密钥 +base_url = "https://api.openai.com/v1" # API基础URL +model = "gpt-3.5-turbo" # 使用的模型 +temperature = 0.7 # 生成内容的随机性 +max_tokens = 1000 # 最大生成token数 +request_timeout = 60 # 请求超时时间(秒) + +# 日志配置 +[logging] +level = "info" # 日志级别: debug, info, warn, error +format = "text" # 日志格式: text, json +output_path = "logs/mcp_server.log" # 日志文件路径 +max_size = 100 # 单个日志文件最大大小(MB) +max_age = 7 # 日志保留天数 +max_backups = 5 # 最大备份文件数 +compress = false # 是否压缩归档日志 + +# 安全配置 +[security] +allowed_origins = ["*"] # 允许的源 +allowed_methods = ["GET", "POST", "OPTIONS"] # 允许的HTTP方法 +allowed_headers = ["Content-Type", "Authorization"] # 允许的HTTP头 +``` + +## 安装与依赖 + +### 前提条件 + +- Go 1.21或更高版本 +- 有效的OpenAI API密钥(或其他兼容的AI服务API密钥) + +### 安装依赖 + +```bash +# 初始化Go模块(如果尚未初始化) +go mod init mcp_server_go + +# 安装依赖包 +go get github.com/sashabaranov/go-openai +go get github.com/spf13/viper +go get github.com/fsnotify/fsnotify +go get github.com/prometheus/client_golang/prometheus +go get github.com/prometheus/client_golang/prometheus/promhttp +go get github.com/google/uuid +go get golang.org/x/time/rate + +# 生成go.sum文件 +go mod tidy +``` + +## 运行方法 + +### 直接运行 + +```bash +# 确保配置文件正确设置 +# 启动服务器 +go run main.go +``` + +### 编译后运行 + +```bash +# 编译项目 +go build -o mcp_server +sudo chmod +x mcp_server + +# 运行编译后的二进制文件 +./mcp_server +``` + +### 作为系统服务运行 + +可以创建一个systemd服务文件来管理MCP服务器: + +```bash +sudo nano /etc/systemd/system/mcp_server.service +``` + +添加以下内容(根据实际路径修改): + +```ini +[Unit] +Description=MCP Server Go +After=network.target + +[Service] +Type=simple +User=your_user +WorkingDirectory=/home/geng/mydate/deploy.stack/mcp_server_go +ExecStart=/home/geng/mydate/deploy.stack/mcp_server_go/mcp_server +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target +``` + +然后启用并启动服务: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable mcp_server +sudo systemctl start mcp_server +``` + +## API接口文档 + +### 健康检查接口 + +**GET /health** + +检查服务器和OpenAI连接状态 + +**响应示例**: + +```json +{ + "status": "ok", + "version": "1.0.0", + "timestamp": 1634567890, + "openai_health": true +} +``` + +### MCP数据提交接口 + +**POST /mcp/v1/submit** + +提交数据到MCP服务器进行处理和AI分析 + +**请求体示例**: + +```json +{ + "data": { + "disk_id": "sda", + "smart_data": { + "temperature": 38, + "power_on_hours": 12345, + "read_errors": 0, + "write_errors": 0 + }, + "performance_data": { + "read_speed": 120, + "write_speed": 90 + } + }, + "type": "disk_inspection", + "metadata": { + "server_id": "server-001", + "location": "data_center_a" + }, + "timestamp": 1634567890 +} +``` + +**响应示例**: + +```json +{ + "success": true, + "message": "数据提交成功", + "data": {"disk_id": "sda", ...}, // 原始提交的数据 + "ai_result": { + "analysis": "硬盘状态良好,温度正常,无错误记录。", + "recommendations": "建议定期进行数据备份,继续监控硬盘健康状态。", + "health_score": 98 + }, + "request_id": "req-1634567890-ab12cd34", + "timestamp": 1634567891 +} +``` + +### Prometheus监控指标 + +**GET /metrics** + +提供Prometheus格式的监控指标 + +## 日志说明 + +日志配置在`[logging]`部分,支持以下特性: + +- 可配置日志级别(debug、info、warn、error) +- 支持文本和JSON两种日志格式 +- 日志文件自动轮转(基于大小和时间) +- 同时输出到控制台和文件 + +## 安全配置 + +- 配置CORS策略,限制允许的源、方法和头部 +- 实现请求速率限制,防止滥用 +- 支持配置文件中的安全设置热重载 + +## 与disk_inspection.py的对接 + +MCP服务器可以直接接收`disk_inspection.py`脚本发送的数据: + +1. 确保`disk_inspection.py`中的`submit_to_mcp`方法配置正确的MCP服务器地址(http://localhost:8080/mcp/v1/submit) +2. 确保`disk_inspection.py`安装了requests依赖:`pip install requests` +3. 运行MCP服务器和disk_inspection.py脚本 + +## 故障排除 + +### 常见问题 + +1. **OpenAI API调用失败** + - 检查API密钥是否正确配置 + - 确认网络连接正常,特别是可以访问OpenAI API + - 查看日志文件获取详细错误信息 + +2. **配置文件不生效** + - 确认配置文件路径正确 + - 检查配置项格式是否符合TOML规范 + +3. **端口被占用** + - 修改`config.toml`中的`listen_addr`配置,使用其他可用端口 + +4. **请求速率限制** + - 如果遇到"请求过于频繁"的错误,可以调整代码中的速率限制参数 + +### 日志分析 + +日志文件默认位于`logs/mcp_server.log`,包含详细的请求处理信息、错误信息和OpenAI API调用情况。可以使用以下命令查看日志: + +```bash +# 实时查看日志 +tail -f logs/mcp_server.log + +# 搜索错误信息 +grep -i error logs/mcp_server.log +``` + +## 开发与扩展 + +### 添加新的AI服务支持 + +可以扩展代码以支持其他AI服务提供商,只需实现相应的客户端初始化和请求处理逻辑。 + +### 自定义数据处理逻辑 + +可以修改`analyzeWithOpenAI`函数,根据不同的数据类型和需求定制AI分析的提示词和处理逻辑。 + +### 添加新的API端点 + +可以在`main.go`中添加新的HTTP处理函数并注册到路由器,扩展服务器功能。 + +## 许可证 + +MIT License + +## 版本历史 + +- v1.0.0: 初始版本,支持OpenAI API对接、基本的MCP功能和监控 \ No newline at end of file diff --git a/mcp_server_go/client_example.go b/mcp_server_go/client_example.go new file mode 100644 index 0000000..3f8f640 --- /dev/null +++ b/mcp_server_go/client_example.go @@ -0,0 +1,125 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// 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"` AIResult interface{} `json:"ai_result,omitempty"` RequestID string `json:"request_id,omitempty"` Timestamp int64 `json:"timestamp"` +} + +func main() { + // 准备测试数据 + diskData := map[string]interface{}{ + "disk_id": "sda", + "smart_data": map[string]interface{}{ + "temperature": 38, + "power_on_hours": 12345, + "read_errors": 0, + "write_errors": 0, + "reallocated_sectors": 0, + }, + "performance_data": map[string]interface{}{ + "read_speed": 120, + "write_speed": 90, + "io_wait": 0.5, + }, + } + + // 创建MCP请求 + mcpRequest := MCPRequest{ + Data: diskData, + Type: "disk_inspection", + Metadata: map[string]string{"server_id": "test-server", "location": "test-lab"}, + Timestamp: time.Now().Unix(), + } + + // 序列化请求数据 + jsonData, err := json.Marshal(mcpRequest) + if err != nil { + fmt.Printf("序列化请求数据失败: %v\n", err) + return + } + + // 发送HTTP请求到MCP服务器 + resp, err := http.Post("http://localhost:8080/mcp/v1/submit", "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + fmt.Printf("发送请求失败: %v\n", err) + fmt.Println("请确保MCP服务器正在运行,并且监听在正确的端口上") + return + } + defer resp.Body.Close() + + // 读取响应 + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("读取响应失败: %v\n", err) + return + } + + // 打印响应状态和内容 + fmt.Printf("响应状态码: %d\n", resp.StatusCode) + + // 如果响应成功,解析并显示详细信息 + if resp.StatusCode == http.StatusOK { + var mcpResponse MCPResponse + if err := json.Unmarshal(body, &mcpResponse); err != nil { + fmt.Printf("解析响应数据失败: %v\n", err) + fmt.Printf("原始响应内容: %s\n", string(body)) + return + } + + // 格式化输出响应数据 + fmt.Println("\nMCP服务器响应:") + fmt.Printf(" 成功状态: %v\n", mcpResponse.Success) + fmt.Printf(" 消息: %s\n", mcpResponse.Message) + fmt.Printf(" 请求ID: %s\n", mcpResponse.RequestID) + fmt.Printf(" 时间戳: %d (%s)\n", + mcpResponse.Timestamp, + time.Unix(mcpResponse.Timestamp, 0).Format("2006-01-02 15:04:05")) + + // 输出AI分析结果(如果有) + if mcpResponse.AIResult != nil { + fmt.Println("\nAI分析结果:") + aiResultJSON, _ := json.MarshalIndent(mcpResponse.AIResult, " ", " ") + fmt.Println(string(aiResultJSON)) + } + } else { + fmt.Printf("请求失败,响应内容: %s\n", string(body)) + } + + // 测试健康检查接口 + testHealthCheck() +} + +// 测试健康检查接口 +func testHealthCheck() { + fmt.Println("\n测试健康检查接口...") + + resp, err := http.Get("http://localhost:8080/health") + if err != nil { + fmt.Printf("健康检查失败: %v\n", err) + return + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Printf("读取健康检查响应失败: %v\n", err) + return + } + + fmt.Printf("健康检查状态码: %d\n", resp.StatusCode) + fmt.Printf("健康检查响应: %s\n", string(body)) +} \ No newline at end of file diff --git a/mcp_server_go/config.toml b/mcp_server_go/config.toml new file mode 100644 index 0000000..dda18ad --- /dev/null +++ b/mcp_server_go/config.toml @@ -0,0 +1,31 @@ +# MCP Server Go 配置文件 +# 服务器基本配置 +[server] +listen_addr = "0.0.0.0:8080" +read_timeout = 30 write_timeout = 30 # 超时时间(秒) +max_header_bytes = 1048576 # 1MB + +# OpenAI API 配置 +[openai] +api_key = "your_api_key_here" # OpenAI API密钥 +base_url = "https://api.openai.com/v1" # API基础URL +model = "gpt-3.5-turbo" # 使用的模型 +temperature = 0.7 # 生成内容的随机性 +max_tokens = 1000 # 最大生成token数 +request_timeout = 60 # 请求超时时间(秒) + +# 日志配置 +[logging] +level = "info" # 日志级别: debug, info, warn, error +format = "text" # 日志格式: text, json +output_path = "logs/mcp_server.log" # 日志文件路径 +max_size = 100 # 单个日志文件最大大小(MB) +max_age = 7 # 日志保留天数 +max_backups = 5 # 最大备份文件数 +compress = false # 是否压缩归档日志 + +# 安全配置 +[security] +allowed_origins = ["*"] +allowed_methods = ["GET", "POST", "OPTIONS"] +allowed_headers = ["Content-Type", "Authorization"] \ No newline at end of file diff --git a/mcp_server_go/go.mod b/mcp_server_go/go.mod new file mode 100644 index 0000000..72eb4bc --- /dev/null +++ b/mcp_server_go/go.mod @@ -0,0 +1,107 @@ +module mcp_server_go + +go 1.21 + +require ( + github.com/fsnotify/fsnotify v1.7.0 + github.com/google/uuid v1.6.0 + github.com/prometheus/client_golang v1.18.0 + github.com/sashabaranov/go-openai v1.17.4 + github.com/spf13/viper v1.17.0 + golang.org/x/time v0.5.0 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/OneOfOne/xxhash v1.2.2 // indirect + github.com/alecthomas/kingpin v1.3.1 // indirect + github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.8.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fatih/color v1.15.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gopherjs/gopherjs v1.17.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/itchyny/gojq v0.12.13 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jessevdk/go-flags v1.5.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pierrec/lz4 v4.1.17+incompatible // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.11.1 // indirect + github.com/rivo/uniseg v0.4.4 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sashabaranov/go-openai v1.17.4 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/afero v1.10.0 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.17.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/gjson v1.17.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/toml-lang/toml v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/yosssi/gohtml v0.0.0-20230121090043-345651034a4d // indirect + github.com/yosssi/gi v0.0.0-20180507055002-ea47d430d263 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.24.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.13.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231019153415-0f05c00091f9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231019153415-0f05c00091f9 // indirect + google.golang.org/grpc v1.58.3 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/square/go-jose.v2 v2.6.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) \ No newline at end of file diff --git a/mcp_server_go/install.sh b/mcp_server_go/install.sh new file mode 100644 index 0000000..6b98cf0 --- /dev/null +++ b/mcp_server_go/install.sh @@ -0,0 +1,132 @@ +#!/bin/bash + +# MCP Server Go 安装脚本 + +# 设置颜色 +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}" +mkdir -p logs +echo -e "${GREEN}日志目录已创建: logs/${NC}" + +# 安装依赖 +echo -e "${BLUE}\n安装项目依赖...${NC}" +if [ ! -f "go.mod" ]; then + echo -e "${YELLOW}未找到go.mod文件,正在初始化Go模块...${NC}" + go mod init mcp_server_go +fi + +go mod tidy +if [ $? -ne 0 ]; then + echo -e "${RED}安装依赖失败,请检查网络连接和go.mod文件。${NC}" + exit 1 +fi + +echo -e "${GREEN}依赖安装成功${NC}" + +# 编译项目 +echo -e "${BLUE}\n编译项目...${NC}" +go build -o mcp_server +if [ $? -ne 0 ]; then + echo -e "${RED}编译失败,请检查代码是否有错误。${NC}" + exit 1 +fi + +chmod +x mcp_server +echo -e "${GREEN}编译成功: mcp_server (可执行文件)${NC}" + +# 检查配置文件 +echo -e "${BLUE}\n检查配置文件...${NC}" +if [ ! -f "config.toml" ]; then + echo -e "${YELLOW}警告: 未找到config.toml文件。请根据config.toml.example创建配置文件。${NC}" +else + # 检查是否设置了API密钥 + API_KEY=$(grep "api_key" config.toml | awk -F'=' '{print $2}' | tr -d ' "') + if [ "$API_KEY" = "your_api_key_here" ]; then + echo -e "${YELLOW}警告: 配置文件中的API密钥尚未设置,请编辑config.toml并填入有效的OpenAI API密钥。${NC}" + else + echo -e "${GREEN}配置文件已找到,API密钥已设置${NC}" + fi +fi + +# 创建systemd服务文件 +echo -e "${BLUE}\n创建systemd服务文件 (可选)...${NC}" +read -p "是否创建systemd服务文件以便作为系统服务运行?(y/n): " CREATE_SERVICE + +if [[ $CREATE_SERVICE == [Yy]* ]]; then + # 获取当前路径 + CURRENT_DIR=$(pwd) + # 获取当前用户 + CURRENT_USER=$(whoami) + + # 创建systemd服务文件 + SERVICE_FILE="/etc/systemd/system/mcp_server.service" + + echo -e "${YELLOW}创建systemd服务文件: $SERVICE_FILE${NC}" + cat > mcp_server.service << EOF +[Unit] +Description=MCP Server Go +After=network.target + +[Service] +Type=simple +User=$CURRENT_USER +WorkingDirectory=$CURRENT_DIR +ExecStart=$CURRENT_DIR/mcp_server +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target +EOF + + echo -e "${BLUE}\n请使用以下命令安装systemd服务:${NC}" + echo -e "sudo mv mcp_server.service $SERVICE_FILE" + echo -e "sudo systemctl daemon-reload" + echo -e "sudo systemctl enable mcp_server" + echo -e "sudo systemctl start mcp_server" +fi + +# 显示启动说明 +echo -e "\n${GREEN}安装完成!${NC}" +echo -e "${BLUE}\n使用说明:${NC}" +echo -e "1. 确保编辑config.toml文件,设置正确的OpenAI API密钥" +echo -e "2. 直接运行: ./mcp_server" +echo -e "3. 或按照上述说明设置为系统服务" +echo -e "\n${YELLOW}注意: 首次运行前,请确保config.toml中的配置正确无误。${NC}" + +echo -e "\n${GREEN}MCP Server Go安装成功!${NC}" \ No newline at end of file diff --git a/mcp_server_go/main.go b/mcp_server_go/main.go new file mode 100644 index 0000000..920f145 --- /dev/null +++ b/mcp_server_go/main.go @@ -0,0 +1,473 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + "os" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/fsnotify/fsnotify" + "github.com/google/uuid" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/spf13/viper" + "golang.org/x/time/rate" + + openai "github.com/sashabaranov/go-openai" +) + +// Config 结构体用于存储配置 +type Config struct { + Server ServerConfig `mapstructure:"server"` OpenAI OpenAIConfig `mapstructure:"openai"` Logging LoggingConfig `mapstructure:"logging"` Security SecurityConfig `mapstructure:"security"` +} + +// ServerConfig 服务器配置 +type ServerConfig struct { + ListenAddr string `mapstructure:"listen_addr"` ReadTimeout int `mapstructure:"read_timeout"` WriteTimeout int `mapstructure:"write_timeout"` MaxHeaderBytes int `mapstructure:"max_header_bytes"` +} + +// OpenAIConfig OpenAI API配置 +type OpenAIConfig struct { + APIKey string `mapstructure:"api_key"` BaseURL string `mapstructure:"base_url"` Model string `mapstructure:"model"` Temperature float64 `mapstructure:"temperature"` MaxTokens int `mapstructure:"max_tokens"` RequestTimeout int `mapstructure:"request_timeout"` +} + +// LoggingConfig 日志配置 +type LoggingConfig struct { + Level string `mapstructure:"level"` Format string `mapstructure:"format"` OutputPath string `mapstructure:"output_path"` MaxSize int `mapstructure:"max_size"` MaxAge int `mapstructure:"max_age"` MaxBackups int `mapstructure:"max_backups"` Compress bool `mapstructure:"compress"` +} + +// SecurityConfig 安全配置 +type SecurityConfig struct { + AllowedOrigins []string `mapstructure:"allowed_origins"` AllowedMethods []string `mapstructure:"allowed_methods"` AllowedHeaders []string `mapstructure:"allowed_headers"` +} + +// 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"` AIResult interface{} `json:"ai_result,omitempty"` RequestID string `json:"request_id,omitempty"` Timestamp int64 `json:"timestamp"` +} + +var ( + config Config + logger *slog.Logger + openaiClient *openai.Client + // 速率限制器 + limiter = rate.NewLimiter(rate.Limit(10), 20) + // Prometheus指标 + requestCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "mcp_server_requests_total", + Help: "Total number of MCP server requests", + }, + []string{"endpoint", "status"}, + ) + requestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "mcp_server_request_duration_seconds", + Help: "Duration of MCP server requests in seconds", + Buckets: prometheus.DefBuckets, + }, + []string{"endpoint"}, + ) +) + +func init() { + // 注册Prometheus指标 + prometheus.MustRegister(requestCounter) + prometheus.MustRegister(requestDuration) +} + +// 加载配置 +func loadConfig() error { + viper.SetConfigName("config") + viper.SetConfigType("toml") + viper.AddConfigPath(".") + viper.AddConfigPath("/etc/mcp_server/") + viper.AddConfigPath("$HOME/.mcp_server/") + + if err := viper.ReadInConfig(); err != nil { + return fmt.Errorf("读取配置文件失败: %w", err) + } + + if err := viper.Unmarshal(&config); err != nil { + return fmt.Errorf("解析配置文件失败: %w", err) + } + + // 配置文件热重载 + viper.WatchConfig() + viper.OnConfigChange(func(e fsnotify.Event) { + logger.Info("配置文件已变更,重新加载", "file", e.Name) + if err := viper.Unmarshal(&config); err != nil { + logger.Error("重新解析配置文件失败", "error", err) + } + // 重新初始化OpenAI客户端 + initOpenAIClient() + }) + + return nil +} + +// 初始化日志 +func initLogger() error { + // 创建日志目录 + logDir := filepath.Dir(config.Logging.OutputPath) + if err := os.MkdirAll(logDir, 0755); err != nil { + return fmt.Errorf("创建日志目录失败: %w", err) + } + + // 打开日志文件 + logFile, err := os.OpenFile( + config.Logging.OutputPath, + os.O_CREATE|os.O_WRONLY|os.O_APPEND, + 0644, + ) + if err != nil { + return fmt.Errorf("打开日志文件失败: %w", err) + } + + // 设置日志级别 + var logLevel slog.Level + switch config.Logging.Level { + case "debug": + logLevel = slog.LevelDebug + case "warn": + logLevel = slog.LevelWarn + case "error": + logLevel = slog.LevelError + default: + logLevel = slog.LevelInfo + } + + // 创建日志处理器 + var handler slog.Handler + if config.Logging.Format == "json" { + handler = slog.NewJSONHandler(logFile, &slog.HandlerOptions{ + Level: logLevel, + }) + } else { + handler = slog.NewTextHandler(logFile, &slog.HandlerOptions{ + Level: logLevel, + }) + } + + // 同时输出到控制台和文件 + multiHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: logLevel}) + logger = slog.New(slog.NewTextHandler( + io.MultiWriter(os.Stdout, logFile), + &slog.HandlerOptions{Level: logLevel}, + )) + slog.SetDefault(logger) + + return nil +} + +// 初始化OpenAI客户端 +func initOpenAIClient() { + cfg := openai.DefaultConfig(config.OpenAI.APIKey) + if config.OpenAI.BaseURL != "" { + cfg.BaseURL = config.OpenAI.BaseURL + } + openaiClient = openai.NewClientWithConfig(cfg) +} + +// 创建CORS中间件 +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 设置CORS头 + for _, origin := range config.Security.AllowedOrigins { + w.Header().Set("Access-Control-Allow-Origin", origin) + } + w.Header().Set("Access-Control-Allow-Methods", strings.Join(config.Security.AllowedMethods, ",")) + w.Header().Set("Access-Control-Allow-Headers", strings.Join(config.Security.AllowedHeaders, ",")) + w.Header().Set("Access-Control-Allow-Credentials", "true") + + // 处理预检请求 + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} + +// 速率限制中间件 +func rateLimitMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !limiter.Allow() { + http.Error(w, "请求过于频繁,请稍后再试", http.StatusTooManyRequests) + return + } + next.ServeHTTP(w, r) + }) +} + +// 日志中间件 +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + logger.Info("收到请求", + slog.String("method", r.Method), + slog.String("path", r.URL.Path), + slog.String("remote_addr", r.RemoteAddr), + ) + + wrappedWriter := &responseWriter{ + ResponseWriter: w, + statusCode: http.StatusOK, + } + + next.ServeHTTP(wrappedWriter, r) + + duration := time.Since(startTime) + logger.Info("请求处理完成", + slog.String("method", r.Method), + slog.String("path", r.URL.Path), + slog.Int("status", wrappedWriter.statusCode), + slog.Duration("duration", duration), + ) + }) +} + +// 响应写入器包装器,用于捕获状态码 +type responseWriter struct { + http.ResponseWriter + statusCode int +} + +func (rw *responseWriter) WriteHeader(code int) { + rw.statusCode = code + rw.ResponseWriter.WriteHeader(code) +} + +// 健康检查端点 +func healthCheckHandler(w http.ResponseWriter, r *http.Request) { + start := time.Now() + defer func() { + requestDuration.WithLabelValues("/health").Observe(time.Since(start).Seconds()) + }() + + w.Header().Set("Content-Type", "application/json") + + // 检查OpenAI客户端连接 + oaiHealthy := false + if openaiClient != nil { + // 创建一个轻量级的测试请求 + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + _, err := openaiClient.ListModels(ctx) + oaiHealthy = (err == nil) + } + + response := map[string]interface{}{ + "status": "ok", + "version": "1.0.0", + "timestamp": time.Now().Unix(), + "openai_health": oaiHealthy, + } + + json.NewEncoder(w).Encode(response) + requestCounter.WithLabelValues("/health", "200").Inc() +} + +// MCP数据提交端点 +func submitHandler(w http.ResponseWriter, r *http.Request) { + start := time.Now() + defer func() { + requestDuration.WithLabelValues("/mcp/v1/submit").Observe(time.Since(start).Seconds()) + }() + + // 生成请求ID + requestID := fmt.Sprintf("req-%d-%s", time.Now().UnixNano(), uuid.New().String()[:8]) + logger.Info("处理MCP提交请求", slog.String("request_id", requestID)) + + // 解析请求体 + var mcpRequest MCPRequest + if err := json.NewDecoder(r.Body).Decode(&mcpRequest); err != nil { + logger.Error("解析请求体失败", slog.String("request_id", requestID), slog.Error(err)) + http.Error(w, "无效的请求体", http.StatusBadRequest) + requestCounter.WithLabelValues("/mcp/v1/submit", "400").Inc() + return + } + + // 设置默认时间戳 + if mcpRequest.Timestamp == 0 { + mcpRequest.Timestamp = time.Now().Unix() + } + + // 调用OpenAI API进行分析 + aIResult, err := analyzeWithOpenAI(r.Context(), mcpRequest, requestID) + if err != nil { + logger.Error("OpenAI API调用失败", slog.String("request_id", requestID), slog.Error(err)) + http.Error(w, "AI分析失败", http.StatusInternalServerError) + requestCounter.WithLabelValues("/mcp/v1/submit", "500").Inc() + return + } + + // 构建响应 + response := MCPResponse{ + Success: true, + Message: "数据提交成功", + Data: mcpRequest.Data, + AIResult: aIResult, + RequestID: requestID, + Timestamp: time.Now().Unix(), + } + + // 发送响应 + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(response); err != nil { + logger.Error("发送响应失败", slog.String("request_id", requestID), slog.Error(err)) + } + + requestCounter.WithLabelValues("/mcp/v1/submit", "200").Inc() + logger.Info("MCP提交请求处理完成", slog.String("request_id", requestID)) +} + +// 使用OpenAI API进行数据分析 +func analyzeWithOpenAI(ctx context.Context, request MCPRequest, requestID string) (interface{}, error) { + // 准备超时上下文 + timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(config.OpenAI.RequestTimeout)*time.Second) + defer cancel() + + // 将请求数据转换为字符串 + dataJSON, err := json.Marshal(request.Data) + if err != nil { + return nil, fmt.Errorf("数据序列化失败: %w", err) + } + + // 构建提示词 + prompt := fmt.Sprintf(`请分析以下数据并提供见解: + +数据类型: %s + +数据内容: +%s + +请提供结构化的分析结果。`, request.Type, string(dataJSON)) + + // 创建聊天完成请求 + chatReq := openai.ChatCompletionRequest{ + Model: config.OpenAI.Model, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleSystem, + Content: "你是一个数据分析助手,负责分析和解读各种类型的数据。", + }, + { + Role: openai.ChatMessageRoleUser, + Content: prompt, + }, + }, + Temperature: config.OpenAI.Temperature, + MaxTokens: config.OpenAI.MaxTokens, + } + + // 调用OpenAI API + logger.Debug("调用OpenAI API", slog.String("request_id", requestID), slog.String("model", config.OpenAI.Model)) + resp, err := openaiClient.CreateChatCompletion(timeoutCtx, chatReq) + if err != nil { + return nil, fmt.Errorf("OpenAI API调用失败: %w", err) + } + + // 解析AI响应 + if len(resp.Choices) == 0 { + return nil, fmt.Errorf("OpenAI未返回有效结果") + } + + // 尝试将响应内容解析为JSON + var aiResult interface{} + if err := json.Unmarshal([]byte(resp.Choices[0].Message.Content), &aiResult); err != nil { + // 如果解析失败,直接返回原始文本 + aiResult = resp.Choices[0].Message.Content + } + + logger.Debug("OpenAI API调用成功", slog.String("request_id", requestID), slog.Int("token_usage", resp.Usage.TotalTokens)) + + return aiResult, nil +} + +// 主函数 +func main() { + // 加载配置 + if err := loadConfig(); err != nil { + fmt.Fprintf(os.Stderr, "加载配置失败: %v\n", err) + os.Exit(1) + } + + // 初始化日志 + if err := initLogger(); err != nil { + fmt.Fprintf(os.Stderr, "初始化日志失败: %v\n", err) + os.Exit(1) + } + + // 初始化OpenAI客户端 + initOpenAIClient() + + logger.Info("MCP服务器启动", slog.String("listen_addr", config.Server.ListenAddr)) + + // 创建路由器 + r := http.NewServeMux() + + // 注册健康检查端点 + r.HandleFunc("/health", healthCheckHandler) + + // 注册MCP提交端点 + r.HandleFunc("/mcp/v1/submit", submitHandler) + + // 注册Prometheus指标端点 + r.Handle("/metrics", promhttp.Handler()) + + // 创建中间件链 + handler := loggingMiddleware(rateLimitMiddleware(corsMiddleware(r))) + + // 创建HTTP服务器 + srv := &http.Server{ + Addr: config.Server.ListenAddr, + Handler: handler, + ReadTimeout: time.Duration(config.Server.ReadTimeout) * time.Second, + WriteTimeout: time.Duration(config.Server.WriteTimeout) * time.Second, + IdleTimeout: 120 * time.Second, + } + + // 启动服务器 + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + logger.Error("服务器启动失败", slog.Error(err)) + os.Exit(1) + } + }() + + logger.Info("MCP服务器已成功启动") + + // 等待中断信号 + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + logger.Info("MCP服务器正在关闭...") + + // 创建超时上下文 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // 优雅关闭服务器 + if err := srv.Shutdown(ctx); err != nil { + logger.Error("服务器关闭失败", slog.Error(err)) + os.Exit(1) + } + + logger.Info("MCP服务器已安全关闭") +} \ No newline at end of file