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) 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) 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) 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) }