mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 08:03:55 +08:00
1062 lines
30 KiB
Go
1062 lines
30 KiB
Go
package service
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
|
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
|
"github.com/1Panel-dev/1Panel/agent/cmd/server/ai"
|
|
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
|
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
|
"github.com/1Panel-dev/1Panel/agent/global"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
|
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx/parser"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/mattn/go-shellwords"
|
|
"github.com/subosito/gotenv"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type McpServerService struct{}
|
|
|
|
const (
|
|
defaultMcpGatewayImageNpx = "supercorp/supergateway:3.4.3"
|
|
defaultMcpGatewayImageUvx = "supercorp/supergateway:uvx"
|
|
defaultMcpProtocolVersion = "2025-06-18"
|
|
mcpOutputTransportSSE = "sse"
|
|
mcpOutputTransportStreamableHTTP = "streamableHttp"
|
|
)
|
|
|
|
type IMcpServerService interface {
|
|
Page(req request.McpServerSearch, readOnly ...bool) response.McpServersRes
|
|
Detail(req request.McpServerDetail) (response.McpServerDTO, error)
|
|
Create(create request.McpServerCreate) error
|
|
Update(req request.McpServerUpdate) error
|
|
Delete(id uint) error
|
|
Operate(req request.McpServerOperate) error
|
|
SyncStatus(req request.McpServerStatusSync) ([]response.McpServerStatusDTO, error)
|
|
TestConnection(req request.McpServerConnectionTest) (response.McpServerConnectionTestRes, error)
|
|
GetBindDomain() (response.McpBindDomainRes, error)
|
|
BindDomain(req request.McpBindDomain) error
|
|
UpdateBindDomain(req request.McpBindDomainUpdate) error
|
|
}
|
|
|
|
func NewIMcpServerService() IMcpServerService {
|
|
return &McpServerService{}
|
|
}
|
|
|
|
func (m McpServerService) Page(req request.McpServerSearch, readOnly ...bool) response.McpServersRes {
|
|
var (
|
|
res response.McpServersRes
|
|
items []response.McpServerDTO
|
|
)
|
|
|
|
total, data, _ := mcpServerRepo.Page(req.PageInfo.Page, req.PageInfo.PageSize)
|
|
for _, item := range data {
|
|
normalizeMcpServerGateway(&item)
|
|
if isDemoReadOnly(readOnly...) {
|
|
item.DockerCompose = ""
|
|
item.Env = ""
|
|
}
|
|
items = append(items, response.McpServerDTO{
|
|
McpServer: item,
|
|
Environments: make([]request.Environment, 0),
|
|
Volumes: make([]request.Volume, 0),
|
|
})
|
|
}
|
|
res.Total = total
|
|
res.Items = items
|
|
return res
|
|
}
|
|
|
|
func (m McpServerService) Detail(req request.McpServerDetail) (response.McpServerDTO, error) {
|
|
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
|
|
if err != nil {
|
|
return response.McpServerDTO{}, err
|
|
}
|
|
normalizeMcpServerGateway(mcpServer)
|
|
serverDTO := response.McpServerDTO{
|
|
McpServer: *mcpServer,
|
|
Environments: make([]request.Environment, 0),
|
|
Volumes: make([]request.Volume, 0),
|
|
}
|
|
if err := loadMcpServerComposeConfig(&serverDTO); err != nil {
|
|
return response.McpServerDTO{}, err
|
|
}
|
|
return serverDTO, nil
|
|
}
|
|
|
|
func (m McpServerService) Update(req request.McpServerUpdate) error {
|
|
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if mcpServer.Port != req.Port {
|
|
if err := checkPortExist(req.Port); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if mcpServer.ContainerName != req.ContainerName {
|
|
if err := checkContainerName(req.ContainerName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
req.Command = strings.TrimSuffix(req.Command, "\n")
|
|
mcpServer.Name = req.Name
|
|
mcpServer.ContainerName = req.ContainerName
|
|
mcpServer.Port = req.Port
|
|
mcpServer.Command = req.Command
|
|
mcpServer.BaseURL = req.BaseURL
|
|
mcpServer.HostIP = req.HostIP
|
|
mcpServer.OutputTransport = req.OutputTransport
|
|
mcpServer.Type = req.Type
|
|
mcpServer.GatewayImage = req.GatewayImage
|
|
mcpServer.ProtocolVersion = req.ProtocolVersion
|
|
mcpServer.GatewayArgs = req.GatewayArgs
|
|
normalizeMcpServerGateway(mcpServer)
|
|
if req.OutputTransport == mcpOutputTransportSSE {
|
|
mcpServer.SsePath = req.SsePath
|
|
} else {
|
|
mcpServer.StreamableHttpPath = req.StreamableHttpPath
|
|
}
|
|
if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil {
|
|
return err
|
|
}
|
|
env := handleEnv(mcpServer)
|
|
mcpDir := path.Join(global.Dir.McpDir, mcpServer.Name)
|
|
envPath := path.Join(mcpDir, ".env")
|
|
if err := gotenv.Write(env, envPath); err != nil {
|
|
return err
|
|
}
|
|
dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
|
|
if err := files.NewFileOp().SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
|
|
return err
|
|
}
|
|
mcpServer.Status = constant.StatusStarting
|
|
if err := mcpServerRepo.Save(mcpServer); err != nil {
|
|
return err
|
|
}
|
|
go runMcpServerTask(mcpServer, req.TaskID, task.TaskUpdate)
|
|
return nil
|
|
}
|
|
|
|
func (m McpServerService) Create(create request.McpServerCreate) error {
|
|
servers, _ := mcpServerRepo.List()
|
|
for _, server := range servers {
|
|
if server.Port == create.Port {
|
|
return buserr.New("ErrPortInUsed")
|
|
}
|
|
if server.ContainerName == create.ContainerName {
|
|
return buserr.New("ErrContainerName")
|
|
}
|
|
if server.Name == create.Name {
|
|
return buserr.New("ErrNameIsExist")
|
|
}
|
|
if server.SsePath == create.SsePath {
|
|
return buserr.New("ErrSsePath")
|
|
}
|
|
}
|
|
create.Command = strings.TrimSuffix(create.Command, "\n")
|
|
if err := checkPortExist(create.Port); err != nil {
|
|
return err
|
|
}
|
|
if err := checkContainerName(create.ContainerName); err != nil {
|
|
return err
|
|
}
|
|
mcpDir := path.Join(global.Dir.McpDir, create.Name)
|
|
mcpServer := &model.McpServer{
|
|
Name: create.Name,
|
|
ContainerName: create.ContainerName,
|
|
Port: create.Port,
|
|
Command: create.Command,
|
|
Status: constant.StatusStarting,
|
|
BaseURL: create.BaseURL,
|
|
Dir: mcpDir,
|
|
HostIP: create.HostIP,
|
|
OutputTransport: create.OutputTransport,
|
|
Type: create.Type,
|
|
GatewayImage: create.GatewayImage,
|
|
ProtocolVersion: create.ProtocolVersion,
|
|
GatewayArgs: create.GatewayArgs,
|
|
}
|
|
normalizeMcpServerGateway(mcpServer)
|
|
if create.OutputTransport == mcpOutputTransportSSE {
|
|
mcpServer.SsePath = create.SsePath
|
|
} else {
|
|
mcpServer.StreamableHttpPath = create.StreamableHttpPath
|
|
}
|
|
|
|
if err := handleCreateParams(mcpServer, create.Environments, create.Volumes); err != nil {
|
|
return err
|
|
}
|
|
|
|
env := handleEnv(mcpServer)
|
|
filesOP := files.NewFileOp()
|
|
if !filesOP.Stat(mcpDir) {
|
|
_ = filesOP.CreateDir(mcpDir, 0644)
|
|
}
|
|
envPath := path.Join(mcpDir, ".env")
|
|
if err := gotenv.Write(env, envPath); err != nil {
|
|
return err
|
|
}
|
|
dockerComposePath := path.Join(mcpDir, "docker-compose.yml")
|
|
if err := filesOP.SaveFile(dockerComposePath, mcpServer.DockerCompose, 0644); err != nil {
|
|
return err
|
|
}
|
|
if err := mcpServerRepo.Create(mcpServer); err != nil {
|
|
return err
|
|
}
|
|
addProxy(mcpServer)
|
|
go runMcpServerTask(mcpServer, create.TaskID, task.TaskCreate)
|
|
return nil
|
|
}
|
|
|
|
func (m McpServerService) Delete(id uint) error {
|
|
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(id))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
|
|
_, _ = compose.Down(composePath)
|
|
_ = files.NewFileOp().DeleteDir(path.Join(global.Dir.McpDir, mcpServer.Name))
|
|
|
|
websiteID := GetWebsiteID()
|
|
if websiteID > 0 {
|
|
websiteService := NewIWebsiteService()
|
|
delProxyReq := request.WebsiteProxyDel{
|
|
ID: websiteID,
|
|
Name: mcpServer.Name,
|
|
}
|
|
_ = websiteService.DeleteProxy(delProxyReq)
|
|
}
|
|
return mcpServerRepo.DeleteBy(repo.WithByID(id))
|
|
}
|
|
|
|
func (m McpServerService) Operate(req request.McpServerOperate) error {
|
|
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := refreshMcpServerFiles(mcpServer); err != nil {
|
|
return err
|
|
}
|
|
composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
|
|
var out string
|
|
switch req.Operate {
|
|
case "start":
|
|
out, err = compose.Up(composePath)
|
|
mcpServer.Status = constant.StatusRunning
|
|
case "stop":
|
|
out, err = compose.Down(composePath)
|
|
mcpServer.Status = constant.StatusStopped
|
|
case "restart":
|
|
out, err = compose.Restart(composePath)
|
|
mcpServer.Status = constant.StatusRunning
|
|
}
|
|
if err != nil {
|
|
mcpServer.Status = constant.StatusError
|
|
mcpServer.Message = out
|
|
}
|
|
return mcpServerRepo.Save(mcpServer)
|
|
}
|
|
|
|
func (m McpServerService) SyncStatus(req request.McpServerStatusSync) ([]response.McpServerStatusDTO, error) {
|
|
var (
|
|
servers []model.McpServer
|
|
err error
|
|
)
|
|
if len(req.IDs) > 0 {
|
|
servers, err = mcpServerRepo.List(repo.WithByIDs(req.IDs))
|
|
} else {
|
|
servers, err = mcpServerRepo.List()
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(servers) == 0 {
|
|
return []response.McpServerStatusDTO{}, nil
|
|
}
|
|
|
|
containerNames := make([]string, 0, len(servers))
|
|
for _, server := range servers {
|
|
if server.ContainerName != "" {
|
|
containerNames = append(containerNames, server.ContainerName)
|
|
}
|
|
}
|
|
containerMap := make(map[string]*container.Summary)
|
|
if len(containerNames) > 0 {
|
|
cli, err := docker.NewClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer cli.Close()
|
|
containers, err := cli.ListContainersByName(containerNames)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range containers {
|
|
for _, name := range containers[i].Names {
|
|
containerMap[strings.TrimPrefix(name, "/")] = &containers[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
res := make([]response.McpServerStatusDTO, 0, len(servers))
|
|
for i := range servers {
|
|
oldStatus := servers[i].Status
|
|
applyMcpContainerStatus(&servers[i], containerMap[servers[i].ContainerName])
|
|
if servers[i].Status != oldStatus {
|
|
if err := mcpServerRepo.Save(&servers[i]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
res = append(res, response.McpServerStatusDTO{
|
|
ID: servers[i].ID,
|
|
Status: servers[i].Status,
|
|
Message: servers[i].Message,
|
|
})
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (m McpServerService) TestConnection(req request.McpServerConnectionTest) (response.McpServerConnectionTestRes, error) {
|
|
mcpServer, err := mcpServerRepo.GetFirst(repo.WithByID(req.ID))
|
|
if err != nil {
|
|
return response.McpServerConnectionTestRes{}, err
|
|
}
|
|
normalizeMcpServerGateway(mcpServer)
|
|
endpoint := buildMcpEndpoint(mcpServer)
|
|
res := response.McpServerConnectionTestRes{
|
|
Endpoint: endpoint,
|
|
OutputTransport: mcpServer.OutputTransport,
|
|
ProtocolVersion: mcpServer.ProtocolVersion,
|
|
}
|
|
if endpoint == "" {
|
|
res.Message = "empty endpoint"
|
|
return res, nil
|
|
}
|
|
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
if mcpServer.OutputTransport == mcpOutputTransportSSE {
|
|
httpReq, err := http.NewRequest(http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
res.Message = err.Error()
|
|
return res, nil
|
|
}
|
|
httpRes, err := client.Do(httpReq)
|
|
if err != nil {
|
|
res.Message = err.Error()
|
|
return res, nil
|
|
}
|
|
defer httpRes.Body.Close()
|
|
contentType := httpRes.Header.Get("Content-Type")
|
|
if httpRes.StatusCode >= 200 && httpRes.StatusCode < 300 && strings.Contains(contentType, "text/event-stream") {
|
|
res.Success = true
|
|
res.Message = "ok"
|
|
return res, nil
|
|
}
|
|
res.Message = fmt.Sprintf("unexpected response: status=%d content-type=%s", httpRes.StatusCode, contentType)
|
|
return res, nil
|
|
}
|
|
|
|
payload := map[string]interface{}{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "initialize",
|
|
"params": map[string]interface{}{
|
|
"protocolVersion": mcpServer.ProtocolVersion,
|
|
"capabilities": map[string]interface{}{},
|
|
"clientInfo": map[string]string{
|
|
"name": "1Panel",
|
|
"version": "1.0.0",
|
|
},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(payload)
|
|
httpReq, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
res.Message = err.Error()
|
|
return res, nil
|
|
}
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
httpReq.Header.Set("Accept", "application/json, text/event-stream")
|
|
httpRes, err := client.Do(httpReq)
|
|
if err != nil {
|
|
res.Message = err.Error()
|
|
return res, nil
|
|
}
|
|
defer httpRes.Body.Close()
|
|
res.Success, res.Message = handleMcpConnectionHTTPResponse(httpRes)
|
|
return res, nil
|
|
}
|
|
|
|
func handleMcpConnectionHTTPResponse(httpRes *http.Response) (bool, string) {
|
|
if httpRes.StatusCode >= 200 && httpRes.StatusCode < 300 {
|
|
return true, "ok"
|
|
}
|
|
respBody, _ := io.ReadAll(httpRes.Body)
|
|
message := strings.TrimSpace(string(respBody))
|
|
if message == "" {
|
|
message = httpRes.Status
|
|
}
|
|
return false, message
|
|
}
|
|
|
|
func (m McpServerService) GetBindDomain() (response.McpBindDomainRes, error) {
|
|
var res response.McpBindDomainRes
|
|
websiteID := GetWebsiteID()
|
|
if websiteID == 0 {
|
|
return res, nil
|
|
}
|
|
website, err := websiteRepo.GetFirst(repo.WithByID(websiteID))
|
|
if err != nil {
|
|
return res, nil
|
|
}
|
|
res.WebsiteID = website.ID
|
|
res.Domain = website.PrimaryDomain
|
|
if website.WebsiteSSLID > 0 {
|
|
res.SSLID = website.WebsiteSSLID
|
|
ssl, _ := websiteSSLRepo.GetFirst(repo.WithByID(website.WebsiteSSLID))
|
|
res.AcmeAccountID = ssl.AcmeAccountID
|
|
}
|
|
res.ConnUrl = fmt.Sprintf("%s://%s", strings.ToLower(website.Protocol), website.PrimaryDomain)
|
|
res.AllowIPs = GetAllowIps(website)
|
|
return res, nil
|
|
|
|
}
|
|
|
|
func (m McpServerService) BindDomain(req request.McpBindDomain) error {
|
|
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
|
|
if nginxInstall.ID == 0 {
|
|
return buserr.New("ErrOpenrestyInstall")
|
|
}
|
|
var (
|
|
ipList []string
|
|
err error
|
|
)
|
|
if len(req.IPList) > 0 {
|
|
ipList, err = common.HandleIPList(req.IPList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if req.SSLID > 0 {
|
|
ssl, err := websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ssl.Pem == "" {
|
|
return buserr.New("ErrSSL")
|
|
}
|
|
}
|
|
group, _ := groupRepo.Get(groupRepo.WithByWebsiteDefault())
|
|
|
|
domain, err := ParseDomain(req.Domain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if domain.Port == 0 {
|
|
domain.Port = nginxInstall.HttpPort
|
|
}
|
|
createWebsiteReq := request.WebsiteCreate{
|
|
Domains: []request.WebsiteDomain{
|
|
{
|
|
Domain: domain.Domain,
|
|
Port: domain.Port,
|
|
},
|
|
},
|
|
Alias: strings.ToLower(req.Domain),
|
|
Type: constant.Static,
|
|
WebsiteGroupID: group.ID,
|
|
}
|
|
if req.SSLID > 0 {
|
|
createWebsiteReq.WebsiteSSLID = req.SSLID
|
|
createWebsiteReq.EnableSSL = true
|
|
}
|
|
websiteService := NewIWebsiteService()
|
|
if err := websiteService.CreateWebsite(createWebsiteReq); err != nil {
|
|
return err
|
|
}
|
|
website, err := websiteRepo.GetFirst(websiteRepo.WithAlias(strings.ToLower(req.Domain)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_ = settingRepo.UpdateOrCreate("MCP_WEBSITE_ID", fmt.Sprintf("%d", website.ID))
|
|
if len(ipList) > 0 {
|
|
if err = ConfigAllowIPs(ipList, website); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err = addMCPProxy(website.ID); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m McpServerService) UpdateBindDomain(req request.McpBindDomainUpdate) error {
|
|
nginxInstall, _ := getAppInstallByKey(constant.AppOpenresty)
|
|
if nginxInstall.ID == 0 {
|
|
return buserr.New("ErrOpenrestyInstall")
|
|
}
|
|
var (
|
|
ipList []string
|
|
err error
|
|
)
|
|
if len(req.IPList) > 0 {
|
|
ipList, err = common.HandleIPList(req.IPList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if req.SSLID > 0 {
|
|
ssl, err := websiteSSLRepo.GetFirst(repo.WithByID(req.SSLID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ssl.Pem == "" {
|
|
return buserr.New("ErrSSL")
|
|
}
|
|
}
|
|
websiteService := NewIWebsiteService()
|
|
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = ConfigAllowIPs(ipList, website); err != nil {
|
|
return err
|
|
}
|
|
if req.SSLID > 0 {
|
|
sslReq := request.WebsiteHTTPSOp{
|
|
WebsiteID: website.ID,
|
|
Enable: true,
|
|
Type: "existed",
|
|
WebsiteSSLID: req.SSLID,
|
|
HttpConfig: "HTTPSOnly",
|
|
}
|
|
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if website.WebsiteSSLID > 0 && req.SSLID == 0 {
|
|
sslReq := request.WebsiteHTTPSOp{
|
|
WebsiteID: website.ID,
|
|
Enable: false,
|
|
}
|
|
if _, err = websiteService.OpWebsiteHTTPS(context.Background(), sslReq); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
go updateMcpConfig(website.ID)
|
|
return nil
|
|
}
|
|
|
|
func updateMcpConfig(websiteID uint) {
|
|
servers, _ := mcpServerRepo.List()
|
|
if len(servers) == 0 {
|
|
return
|
|
}
|
|
website, _ := websiteRepo.GetFirst(repo.WithByID(websiteID))
|
|
websiteDomain := website.Domains[0]
|
|
var baseUrl string
|
|
if website.Protocol == constant.ProtocolHTTP {
|
|
baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
|
|
} else {
|
|
baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
|
|
}
|
|
|
|
go func() {
|
|
for _, server := range servers {
|
|
if server.BaseURL != baseUrl {
|
|
server.BaseURL = baseUrl
|
|
server.HostIP = "127.0.0.1"
|
|
_ = updateMcpServer(&server)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func addProxy(server *model.McpServer) {
|
|
websiteID := GetWebsiteID()
|
|
website, err := websiteRepo.GetFirst(repo.WithByID(websiteID))
|
|
if err != nil {
|
|
global.LOG.Errorf("[mcp] add proxy failed, err: %v", err)
|
|
return
|
|
}
|
|
fileOp := files.NewFileOp()
|
|
includeDir := GetSitePath(website, SiteProxyDir)
|
|
if !fileOp.Stat(includeDir) {
|
|
if err = fileOp.CreateDir(includeDir, 0644); err != nil {
|
|
return
|
|
}
|
|
}
|
|
config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
|
|
if err != nil {
|
|
return
|
|
}
|
|
includePath := path.Join(includeDir, server.Name+".conf")
|
|
config.FilePath = includePath
|
|
directives := config.Directives
|
|
location, ok := directives[0].(*components.Location)
|
|
if !ok {
|
|
return
|
|
}
|
|
var proxyPath string
|
|
if server.OutputTransport == "sse" {
|
|
proxyPath = server.SsePath
|
|
} else {
|
|
proxyPath = server.StreamableHttpPath
|
|
}
|
|
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath)})
|
|
location.ChangePath("^~", proxyPath)
|
|
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
|
global.LOG.Errorf("write config failed, err: %v", buserr.WithErr("ErrUpdateBuWebsite", err))
|
|
return
|
|
}
|
|
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
|
|
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
|
|
global.LOG.Errorf("update nginx config failed, err: %v", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
func addMCPProxy(websiteID uint) error {
|
|
servers, _ := mcpServerRepo.List()
|
|
if len(servers) == 0 {
|
|
return nil
|
|
}
|
|
website, err := websiteRepo.GetFirst(repo.WithByID(websiteID))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fileOp := files.NewFileOp()
|
|
includeDir := GetSitePath(website, SiteProxyDir)
|
|
if !fileOp.Stat(includeDir) {
|
|
if err = fileOp.CreateDir(includeDir, 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
config, err := parser.NewStringParser(string(nginx_conf.SSE)).Parse()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
websiteDomain := website.Domains[0]
|
|
var baseUrl string
|
|
if website.Protocol == constant.ProtocolHTTP {
|
|
baseUrl = fmt.Sprintf("http://%s", websiteDomain.Domain)
|
|
} else {
|
|
baseUrl = fmt.Sprintf("https://%s", websiteDomain.Domain)
|
|
}
|
|
if websiteDomain.Port != 80 && websiteDomain.Port != 443 {
|
|
baseUrl = fmt.Sprintf("%s:%d", baseUrl, websiteDomain.Port)
|
|
}
|
|
for _, server := range servers {
|
|
includePath := path.Join(includeDir, server.Name+".conf")
|
|
config.FilePath = includePath
|
|
directives := config.Directives
|
|
location, ok := directives[0].(*components.Location)
|
|
if !ok {
|
|
err = errors.New("error")
|
|
return err
|
|
}
|
|
var proxyPath string
|
|
if server.OutputTransport == "sse" {
|
|
proxyPath = server.SsePath
|
|
} else {
|
|
proxyPath = server.StreamableHttpPath
|
|
}
|
|
location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.0.1:%d%s", server.Port, proxyPath)})
|
|
location.ChangePath("^~", proxyPath)
|
|
if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil {
|
|
return buserr.WithErr("ErrUpdateBuWebsite", err)
|
|
}
|
|
server.BaseURL = baseUrl
|
|
server.HostIP = "127.0.0.1"
|
|
go updateMcpServer(&server)
|
|
}
|
|
nginxInclude := fmt.Sprintf("/www/sites/%s/proxy/*.conf", website.Alias)
|
|
if err = updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func updateMcpServer(mcpServer *model.McpServer) error {
|
|
if err := refreshMcpServerFiles(mcpServer); err != nil {
|
|
return err
|
|
}
|
|
composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
|
|
_, _ = compose.Down(composePath)
|
|
if _, err := compose.Up(composePath); err != nil {
|
|
mcpServer.Status = constant.StatusError
|
|
mcpServer.Message = err.Error()
|
|
}
|
|
return mcpServerRepo.Save(mcpServer)
|
|
}
|
|
|
|
func refreshMcpServerFiles(mcpServer *model.McpServer) error {
|
|
normalizeMcpServerGateway(mcpServer)
|
|
if err := handleCreateParams(mcpServer, nil, nil); err != nil {
|
|
return err
|
|
}
|
|
env := handleEnv(mcpServer)
|
|
if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil {
|
|
return err
|
|
}
|
|
composePath := path.Join(mcpServer.Dir, "docker-compose.yml")
|
|
if err := files.NewFileOp().SaveFile(composePath, mcpServer.DockerCompose, 0644); err != nil {
|
|
return err
|
|
}
|
|
return mcpServerRepo.Save(mcpServer)
|
|
}
|
|
|
|
func normalizeMcpServerGateway(mcpServer *model.McpServer) {
|
|
if mcpServer.Type == "" {
|
|
mcpServer.Type = "npx"
|
|
}
|
|
if strings.TrimSpace(mcpServer.GatewayImage) == "" {
|
|
if mcpServer.Type == "uvx" {
|
|
mcpServer.GatewayImage = defaultMcpGatewayImageUvx
|
|
} else {
|
|
mcpServer.GatewayImage = defaultMcpGatewayImageNpx
|
|
}
|
|
}
|
|
if strings.TrimSpace(mcpServer.ProtocolVersion) == "" {
|
|
mcpServer.ProtocolVersion = defaultMcpProtocolVersion
|
|
}
|
|
}
|
|
|
|
var managedMcpGatewayArgs = map[string]struct{}{
|
|
"--": {},
|
|
"stdio": {},
|
|
"sse": {},
|
|
"streamablehttp": {},
|
|
"outputtransport": {},
|
|
"port": {},
|
|
"baseurl": {},
|
|
"ssepath": {},
|
|
"messagepath": {},
|
|
"streamablehttppath": {},
|
|
"protocolversion": {},
|
|
}
|
|
|
|
func normalizeMcpGatewayArgName(name string) string {
|
|
if name == "--" || !strings.HasPrefix(name, "--") {
|
|
return name
|
|
}
|
|
name = strings.TrimPrefix(name, "--")
|
|
name = strings.TrimPrefix(name, "no-")
|
|
name = strings.SplitN(name, ".", 2)[0]
|
|
return strings.ToLower(strings.ReplaceAll(name, "-", ""))
|
|
}
|
|
|
|
func parseMcpGatewayArgs(raw string) ([]string, error) {
|
|
if strings.TrimSpace(raw) == "" {
|
|
return nil, nil
|
|
}
|
|
parser := shellwords.NewParser()
|
|
args, err := parser.Parse(raw)
|
|
if err != nil {
|
|
return nil, buserr.New("ErrMcpGatewayArgsParse")
|
|
}
|
|
if parser.Position >= 0 {
|
|
return nil, buserr.New("ErrMcpGatewayArgsParse")
|
|
}
|
|
for _, arg := range args {
|
|
name := strings.SplitN(arg, "=", 2)[0]
|
|
if _, ok := managedMcpGatewayArgs[normalizeMcpGatewayArgName(name)]; ok {
|
|
return nil, buserr.WithName("ErrMcpGatewayArgsConflict", name)
|
|
}
|
|
}
|
|
return args, nil
|
|
}
|
|
|
|
func buildSupergatewayCommand(mcpServer *model.McpServer) ([]string, error) {
|
|
normalizeMcpServerGateway(mcpServer)
|
|
command := []string{
|
|
"--stdio", mcpServer.Command,
|
|
"--outputTransport", mcpServer.OutputTransport,
|
|
"--port", strconv.Itoa(mcpServer.Port),
|
|
}
|
|
if mcpServer.OutputTransport == mcpOutputTransportSSE {
|
|
command = append(command,
|
|
"--baseUrl", mcpServer.BaseURL,
|
|
"--ssePath", mcpServer.SsePath,
|
|
"--messagePath", fmt.Sprintf("%s/messages", mcpServer.SsePath),
|
|
)
|
|
} else {
|
|
command = append(command,
|
|
"--streamableHttpPath", mcpServer.StreamableHttpPath,
|
|
"--protocolVersion", mcpServer.ProtocolVersion,
|
|
)
|
|
}
|
|
gatewayArgs, err := parseMcpGatewayArgs(mcpServer.GatewayArgs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return append(command, gatewayArgs...), nil
|
|
}
|
|
|
|
func buildMcpEndpoint(mcpServer *model.McpServer) string {
|
|
if mcpServer.BaseURL == "" {
|
|
return ""
|
|
}
|
|
if mcpServer.OutputTransport == mcpOutputTransportSSE {
|
|
return strings.TrimRight(mcpServer.BaseURL, "/") + "/" + strings.TrimLeft(mcpServer.SsePath, "/")
|
|
}
|
|
return strings.TrimRight(mcpServer.BaseURL, "/") + "/" + strings.TrimLeft(mcpServer.StreamableHttpPath, "/")
|
|
}
|
|
|
|
func loadMcpServerComposeConfig(serverDTO *response.McpServerDTO) error {
|
|
serverDTO.Environments = make([]request.Environment, 0)
|
|
serverDTO.Volumes = make([]request.Volume, 0)
|
|
dir := serverDTO.Dir
|
|
if dir == "" {
|
|
dir = path.Join(global.Dir.McpDir, serverDTO.Name)
|
|
}
|
|
project, err := docker.GetComposeProject(serverDTO.Name, dir, []byte(serverDTO.DockerCompose), []byte(serverDTO.Env), true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, service := range project.Services {
|
|
if service.Environment != nil {
|
|
for key, value := range service.Environment {
|
|
if value == nil {
|
|
continue
|
|
}
|
|
serverDTO.Environments = append(serverDTO.Environments, request.Environment{
|
|
Key: key,
|
|
Value: *value,
|
|
})
|
|
}
|
|
}
|
|
if service.Volumes != nil {
|
|
for _, volume := range service.Volumes {
|
|
mode := "rw"
|
|
if volume.ReadOnly {
|
|
mode = "ro"
|
|
}
|
|
serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{
|
|
Source: volume.Source,
|
|
Target: volume.Target,
|
|
Mode: mode,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func handleEnv(mcpServer *model.McpServer) gotenv.Env {
|
|
env := make(gotenv.Env)
|
|
env["CONTAINER_NAME"] = mcpServer.ContainerName
|
|
env["COMMAND"] = mcpServer.Command
|
|
env["PANEL_APP_PORT_HTTP"] = strconv.Itoa(mcpServer.Port)
|
|
env["BASE_URL"] = mcpServer.BaseURL
|
|
env["SSE_PATH"] = mcpServer.SsePath
|
|
env["HOST_IP"] = mcpServer.HostIP
|
|
env["STREAMABLE_HTTP_PATH"] = mcpServer.StreamableHttpPath
|
|
env["OUTPUT_TRANSPORT"] = mcpServer.OutputTransport
|
|
env["GATEWAY_IMAGE"] = mcpServer.GatewayImage
|
|
envStr, _ := gotenv.Marshal(env)
|
|
mcpServer.Env = envStr
|
|
return env
|
|
}
|
|
|
|
func handleCreateParams(mcpServer *model.McpServer, environments []request.Environment, volumes []request.Volume) error {
|
|
var composeContent []byte
|
|
if mcpServer.ID == 0 {
|
|
composeContent = ai.DefaultMcpCompose
|
|
} else {
|
|
composeContent = []byte(mcpServer.DockerCompose)
|
|
}
|
|
composeMap := make(map[string]interface{})
|
|
if err := yaml.Unmarshal(composeContent, &composeMap); err != nil {
|
|
return err
|
|
}
|
|
services, serviceValid := composeMap["services"].(map[string]interface{})
|
|
if !serviceValid {
|
|
return buserr.New("ErrFileParse")
|
|
}
|
|
serviceName := ""
|
|
serviceValue := make(map[string]interface{})
|
|
|
|
if mcpServer.ID > 0 {
|
|
serviceName = mcpServer.Name
|
|
serviceValue = services[serviceName].(map[string]interface{})
|
|
} else {
|
|
for name, service := range services {
|
|
serviceName = name
|
|
serviceValue = service.(map[string]interface{})
|
|
break
|
|
}
|
|
delete(services, serviceName)
|
|
}
|
|
normalizeMcpServerGateway(mcpServer)
|
|
command, err := buildSupergatewayCommand(mcpServer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serviceValue["command"] = command
|
|
serviceValue["image"] = mcpServer.GatewayImage
|
|
serviceValue["ports"] = []string{
|
|
formatComposePortMapping("HOST_IP", "PANEL_APP_PORT_HTTP", "PANEL_APP_PORT_HTTP", ""),
|
|
}
|
|
oldEnv, hasOldEnv := serviceValue["environment"]
|
|
delete(serviceValue, "environment")
|
|
if environments != nil && len(environments) > 0 {
|
|
envMap := make(map[string]string)
|
|
for _, env := range environments {
|
|
envMap[env.Key] = env.Value
|
|
}
|
|
serviceValue["environment"] = envMap
|
|
} else if environments == nil {
|
|
// Preserve existing custom environment when regenerating compose during start/restart.
|
|
if mcpServer.ID > 0 && hasOldEnv {
|
|
serviceValue["environment"] = oldEnv
|
|
}
|
|
}
|
|
oldVolumes, hasOldVolumes := serviceValue["volumes"]
|
|
delete(serviceValue, "volumes")
|
|
if volumes != nil && len(volumes) > 0 {
|
|
volumeList := make([]string, 0)
|
|
for _, volume := range volumes {
|
|
volumeList = append(volumeList, formatComposeVolume(volume.Source, volume.Target, volume.Mode))
|
|
}
|
|
serviceValue["volumes"] = volumeList
|
|
} else if volumes == nil {
|
|
// Preserve existing custom volumes when regenerating compose during start/restart.
|
|
if mcpServer.ID > 0 && hasOldVolumes {
|
|
serviceValue["volumes"] = oldVolumes
|
|
}
|
|
}
|
|
|
|
services[mcpServer.Name] = serviceValue
|
|
composeByte, err := yaml.Marshal(composeMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mcpServer.DockerCompose = string(composeByte)
|
|
return nil
|
|
}
|
|
|
|
func runMcpServerTask(mcpServer *model.McpServer, taskID, operate string) {
|
|
mcpTask, err := task.NewTaskWithOps(mcpServer.Name, operate, task.TaskScopeAI, taskID, mcpServer.ID)
|
|
if err != nil {
|
|
mcpServer.Status = constant.StatusError
|
|
mcpServer.Message = err.Error()
|
|
_ = mcpServerRepo.Save(mcpServer)
|
|
return
|
|
}
|
|
mcpTask.AddSubTask(task.GetTaskName(mcpServer.Name, operate, task.TaskScopeAI), func(t *task.Task) error {
|
|
return startMcpWithTask(mcpServer, t)
|
|
}, nil)
|
|
_ = mcpTask.Execute()
|
|
}
|
|
|
|
func startMcp(mcpServer *model.McpServer) {
|
|
_ = startMcpWithTask(mcpServer, nil)
|
|
}
|
|
|
|
func startMcpWithTask(mcpServer *model.McpServer, taskItem *task.Task) error {
|
|
if err := refreshMcpServerFiles(mcpServer); err != nil {
|
|
mcpServer.Status = constant.StatusError
|
|
mcpServer.Message = err.Error()
|
|
_ = mcpServerRepo.Save(mcpServer)
|
|
return err
|
|
}
|
|
composePath := path.Join(global.Dir.McpDir, mcpServer.Name, "docker-compose.yml")
|
|
if mcpServer.Status != constant.StatusNormal {
|
|
_, _ = compose.Down(composePath)
|
|
}
|
|
var err error
|
|
var out string
|
|
if taskItem != nil {
|
|
err = compose.UpWithTask(composePath, taskItem, false)
|
|
} else {
|
|
out, err = compose.Up(composePath)
|
|
}
|
|
if err != nil {
|
|
mcpServer.Status = constant.StatusError
|
|
if out != "" {
|
|
mcpServer.Message = out
|
|
} else {
|
|
mcpServer.Message = err.Error()
|
|
}
|
|
_ = mcpServerRepo.Save(mcpServer)
|
|
return err
|
|
} else {
|
|
mcpServer.Status = constant.StatusRunning
|
|
mcpServer.Message = ""
|
|
}
|
|
return syncMcpServerContainerStatus(mcpServer)
|
|
}
|
|
|
|
func syncMcpServerContainerStatus(mcpServer *model.McpServer) error {
|
|
containerNames := []string{mcpServer.ContainerName}
|
|
cli, err := docker.NewClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
containers, err := cli.ListContainersByName(containerNames)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(containers) == 0 {
|
|
applyMcpContainerStatus(mcpServer, nil)
|
|
return mcpServerRepo.Save(mcpServer)
|
|
}
|
|
applyMcpContainerStatus(mcpServer, &containers[0])
|
|
return mcpServerRepo.Save(mcpServer)
|
|
}
|
|
|
|
func applyMcpContainerStatus(mcpServer *model.McpServer, containerSummary *container.Summary) {
|
|
if containerSummary == nil {
|
|
mcpServer.Status = constant.StatusStopped
|
|
return
|
|
}
|
|
switch containerSummary.State {
|
|
case "exited":
|
|
mcpServer.Status = constant.StatusError
|
|
case "running":
|
|
mcpServer.Status = constant.StatusRunning
|
|
case "paused":
|
|
mcpServer.Status = constant.StatusStopped
|
|
case "restarting":
|
|
mcpServer.Status = constant.StatusRestarting
|
|
default:
|
|
if mcpServer.Status != constant.StatusStarting {
|
|
mcpServer.Status = constant.StatusStopped
|
|
}
|
|
}
|
|
}
|
|
|
|
func GetWebsiteID() uint {
|
|
websiteID, _ := settingRepo.Get(settingRepo.WithByKey("MCP_WEBSITE_ID"))
|
|
if websiteID.Value == "" {
|
|
return 0
|
|
}
|
|
websiteIDUint, _ := strconv.ParseUint(websiteID.Value, 10, 64)
|
|
return uint(websiteIDUint)
|
|
}
|