diff --git a/backend/app/api/v1/entry.go b/backend/app/api/v1/entry.go index f9b179a37..1c194d269 100644 --- a/backend/app/api/v1/entry.go +++ b/backend/app/api/v1/entry.go @@ -68,4 +68,6 @@ var ( favoriteService = service.NewIFavoriteService() websiteCAService = service.NewIWebsiteCAService() + + mcpServerService = service.NewIMcpServerService() ) diff --git a/backend/app/api/v1/mcp_server.go b/backend/app/api/v1/mcp_server.go new file mode 100644 index 000000000..1a73b2e89 --- /dev/null +++ b/backend/app/api/v1/mcp_server.go @@ -0,0 +1,167 @@ +package v1 + +import ( + "github.com/1Panel-dev/1Panel/backend/app/api/v1/helper" + "github.com/1Panel-dev/1Panel/backend/app/dto/request" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/gin-gonic/gin" +) + +// @Tags McpServer +// @Summary List mcp servers +// @Accept json +// @Param request body request.McpServerSearch true "request" +// @Success 200 {object} response.McpServersRes +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /mcp/search [post] +func (b *BaseApi) PageMcpServers(c *gin.Context) { + var req request.McpServerSearch + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + list := mcpServerService.Page(req) + helper.SuccessWithData(c, list) +} + +// @Tags McpServer +// @Summary Create mcp server +// @Accept json +// @Param request body request.McpServerCreate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /mcp/server [post] +func (b *BaseApi) CreateMcpServer(c *gin.Context) { + var req request.McpServerCreate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := mcpServerService.Create(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags McpServer +// @Summary Update mcp server +// @Accept json +// @Param request body request.McpServerUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /mcp/server/update [post] +func (b *BaseApi) UpdateMcpServer(c *gin.Context) { + var req request.McpServerUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := mcpServerService.Update(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags McpServer +// @Summary Delete mcp server +// @Accept json +// @Param request body request.McpServerDelete true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /mcp/server/del [post] +func (b *BaseApi) DeleteMcpServer(c *gin.Context) { + var req request.McpServerDelete + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := mcpServerService.Delete(req.ID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags McpServer +// @Summary Operate mcp server +// @Accept json +// @Param request body request.McpServerOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /mcp/server/op [post] +func (b *BaseApi) OperateMcpServer(c *gin.Context) { + var req request.McpServerOperate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := mcpServerService.Operate(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags McpServer +// @Summary Bind Domain for mcp server +// @Accept json +// @Param request body request.McpBindDomain true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /mcp/domain/bind [post] +func (b *BaseApi) BindMcpDomain(c *gin.Context) { + var req request.McpBindDomain + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := mcpServerService.BindDomain(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags McpServer +// @Summary Update bind Domain for mcp server +// @Accept json +// @Param request body request.McpBindDomainUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /mcp/domain/update [post] +func (b *BaseApi) UpdateMcpBindDomain(c *gin.Context) { + var req request.McpBindDomainUpdate + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + err := mcpServerService.UpdateBindDomain(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} + +// @Tags McpServer +// @Summary Get bin Domain for mcp server +// @Accept json +// @Success 200 {object} response.McpBindDomainRes +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /mcp/domain/get [get] +func (b *BaseApi) GetMcpBindDomain(c *gin.Context) { + res, err := mcpServerService.GetBindDomain() + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} diff --git a/backend/app/dto/mcp.go b/backend/app/dto/mcp.go new file mode 100644 index 000000000..d961a1c45 --- /dev/null +++ b/backend/app/dto/mcp.go @@ -0,0 +1,10 @@ +package dto + +type DockerComposeService struct { + Image string `yaml:"image"` + ContainerName string `yaml:"container_name"` + Restart string `yaml:"restart"` + Ports []string `yaml:"ports"` + Environment []string `yaml:"environment"` + Command []string `yaml:"command"` +} diff --git a/backend/app/dto/request/mcp_server.go b/backend/app/dto/request/mcp_server.go new file mode 100644 index 000000000..fb1fcc88d --- /dev/null +++ b/backend/app/dto/request/mcp_server.go @@ -0,0 +1,57 @@ +package request + +import "github.com/1Panel-dev/1Panel/backend/app/dto" + +type McpServerSearch struct { + dto.PageInfo + Name string `json:"name"` + Sync bool `json:"sync"` +} + +type McpServerCreate struct { + Name string `json:"name" validate:"required"` + Command string `json:"command" validate:"required"` + Environments []Environment `json:"environments"` + Volumes []Volume `json:"volumes"` + Port int `json:"port" validate:"required"` + ContainerName string `json:"containerName"` + BaseURL string `json:"baseUrl"` + SsePath string `json:"ssePath"` + HostIP string `json:"hostIP"` +} + +type McpServerUpdate struct { + ID uint `json:"id" validate:"required"` + McpServerCreate +} + +type Environment struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type Volume struct { + Source string `json:"source"` + Target string `json:"target"` +} + +type McpServerDelete struct { + ID uint `json:"id" validate:"required"` +} + +type McpServerOperate struct { + ID uint `json:"id" validate:"required"` + Operate string `json:"operate" validate:"required"` +} + +type McpBindDomain struct { + Domain string `json:"domain" validate:"required"` + SSLID uint `json:"sslID"` + IPList string `json:"ipList"` +} + +type McpBindDomainUpdate struct { + WebsiteID uint `json:"websiteID" validate:"required"` + SSLID uint `json:"sslID"` + IPList string `json:"ipList"` +} diff --git a/backend/app/dto/response/mcp_server.go b/backend/app/dto/response/mcp_server.go new file mode 100644 index 000000000..fd03d8a48 --- /dev/null +++ b/backend/app/dto/response/mcp_server.go @@ -0,0 +1,26 @@ +package response + +import ( + "github.com/1Panel-dev/1Panel/backend/app/dto/request" + "github.com/1Panel-dev/1Panel/backend/app/model" +) + +type McpServersRes struct { + Items []McpServerDTO `json:"items"` + Total int64 `json:"total"` +} + +type McpServerDTO struct { + model.McpServer + Environments []request.Environment `json:"environments"` + Volumes []request.Volume `json:"volumes"` +} + +type McpBindDomainRes struct { + Domain string `json:"domain"` + SSLID uint `json:"sslID"` + AcmeAccountID uint `json:"acmeAccountID"` + AllowIPs []string `json:"allowIPs"` + WebsiteID uint `json:"websiteID"` + ConnUrl string `json:"connUrl"` +} diff --git a/backend/app/model/mcp_server.go b/backend/app/model/mcp_server.go new file mode 100644 index 000000000..61ce66b65 --- /dev/null +++ b/backend/app/model/mcp_server.go @@ -0,0 +1,18 @@ +package model + +type McpServer struct { + BaseModel + Name string `json:"name"` + DockerCompose string `json:"dockerCompose"` + Command string `json:"command"` + ContainerName string `json:"containerName"` + Message string `json:"message"` + Port int `json:"port"` + Status string `json:"status"` + Env string `json:"env"` + BaseURL string `json:"baseUrl"` + SsePath string `json:"ssePath"` + WebsiteID int `json:"websiteID"` + Dir string `json:"dir"` + HostIP string `json:"hostIP"` +} diff --git a/backend/app/repo/mcp_server.go b/backend/app/repo/mcp_server.go new file mode 100644 index 000000000..a55c21070 --- /dev/null +++ b/backend/app/repo/mcp_server.go @@ -0,0 +1,56 @@ +package repo + +import "github.com/1Panel-dev/1Panel/backend/app/model" + +type McpServerRepo struct { +} + +type IMcpServerRepo interface { + Page(page, size int, opts ...DBOption) (int64, []model.McpServer, error) + GetFirst(opts ...DBOption) (*model.McpServer, error) + Create(mcpServer *model.McpServer) error + Save(mcpServer *model.McpServer) error + DeleteBy(opts ...DBOption) error + List(opts ...DBOption) ([]model.McpServer, error) +} + +func NewIMcpServerRepo() IMcpServerRepo { + return &McpServerRepo{} +} + +func (m McpServerRepo) Page(page, size int, opts ...DBOption) (int64, []model.McpServer, error) { + var servers []model.McpServer + db := getDb(opts...).Model(&model.McpServer{}) + count := int64(0) + db = db.Count(&count) + err := db.Limit(size).Offset(size * (page - 1)).Find(&servers).Error + return count, servers, err +} + +func (m McpServerRepo) GetFirst(opts ...DBOption) (*model.McpServer, error) { + var mcpServer model.McpServer + if err := getDb(opts...).First(&mcpServer).Error; err != nil { + return nil, err + } + return &mcpServer, nil +} + +func (m McpServerRepo) List(opts ...DBOption) ([]model.McpServer, error) { + var mcpServers []model.McpServer + if err := getDb(opts...).Find(&mcpServers).Error; err != nil { + return nil, err + } + return mcpServers, nil +} + +func (m McpServerRepo) Create(mcpServer *model.McpServer) error { + return getDb().Create(mcpServer).Error +} + +func (m McpServerRepo) Save(mcpServer *model.McpServer) error { + return getDb().Save(mcpServer).Error +} + +func (m McpServerRepo) DeleteBy(opts ...DBOption) error { + return getDb(opts...).Delete(&model.McpServer{}).Error +} diff --git a/backend/app/service/entry.go b/backend/app/service/entry.go index 0b91e5346..93fba4f26 100644 --- a/backend/app/service/entry.go +++ b/backend/app/service/entry.go @@ -46,4 +46,6 @@ var ( phpExtensionsRepo = repo.NewIPHPExtensionsRepo() favoriteRepo = repo.NewIFavoriteRepo() + + mcpServerRepo = repo.NewIMcpServerRepo() ) diff --git a/backend/app/service/mcp_server.go b/backend/app/service/mcp_server.go new file mode 100644 index 000000000..155f6ed59 --- /dev/null +++ b/backend/app/service/mcp_server.go @@ -0,0 +1,627 @@ +package service + +import ( + "context" + "errors" + "fmt" + "github.com/1Panel-dev/1Panel/backend/app/dto" + "github.com/1Panel-dev/1Panel/backend/app/dto/request" + "github.com/1Panel-dev/1Panel/backend/app/dto/response" + "github.com/1Panel-dev/1Panel/backend/app/model" + "github.com/1Panel-dev/1Panel/backend/buserr" + "github.com/1Panel-dev/1Panel/backend/constant" + "github.com/1Panel-dev/1Panel/backend/global" + "github.com/1Panel-dev/1Panel/backend/utils/common" + "github.com/1Panel-dev/1Panel/backend/utils/compose" + "github.com/1Panel-dev/1Panel/backend/utils/docker" + "github.com/1Panel-dev/1Panel/backend/utils/files" + "github.com/1Panel-dev/1Panel/backend/utils/nginx" + "github.com/1Panel-dev/1Panel/backend/utils/nginx/components" + "github.com/1Panel-dev/1Panel/backend/utils/nginx/parser" + "github.com/1Panel-dev/1Panel/cmd/server/mcp" + "github.com/1Panel-dev/1Panel/cmd/server/nginx_conf" + "github.com/subosito/gotenv" + "gopkg.in/yaml.v3" + "path" + "strconv" + "strings" +) + +type McpServerService struct{} + +type IMcpServerService interface { + Page(req request.McpServerSearch) response.McpServersRes + Create(create request.McpServerCreate) error + Update(req request.McpServerUpdate) error + Delete(id uint) error + Operate(req request.McpServerOperate) 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 { + serverDTO := response.McpServerDTO{ + McpServer: item, + Environments: make([]request.Environment, 0), + Volumes: make([]request.Volume, 0), + } + project, _ := docker.GetComposeProject(item.Name, path.Join(constant.McpDir, item.Name), []byte(item.DockerCompose), []byte(item.Env), true) + for _, service := range project.Services { + if service.Environment != nil { + for key, value := range service.Environment { + serverDTO.Environments = append(serverDTO.Environments, request.Environment{ + Key: key, + Value: *value, + }) + } + } + if service.Volumes != nil { + for _, volume := range service.Volumes { + serverDTO.Volumes = append(serverDTO.Volumes, request.Volume{ + Source: volume.Source, + Target: volume.Target, + }) + } + } + } + items = append(items, serverDTO) + } + res.Total = total + res.Items = items + return res +} + +func (m McpServerService) Update(req request.McpServerUpdate) error { + mcpServer, err := mcpServerRepo.GetFirst(commonRepo.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 + } + } + mcpServer.Name = req.Name + mcpServer.ContainerName = req.ContainerName + mcpServer.Port = req.Port + mcpServer.Command = req.Command + mcpServer.BaseURL = req.BaseURL + mcpServer.SsePath = req.SsePath + mcpServer.HostIP = req.HostIP + if err := handleCreateParams(mcpServer, req.Environments, req.Volumes); err != nil { + return err + } + env := handleEnv(mcpServer) + mcpDir := path.Join(constant.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 + } + startMcp(mcpServer) + return syncMcpServerContainerStatus(mcpServer) +} + +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(constant.ErrNameIsExist) + } + if server.SsePath == create.SsePath { + return buserr.New("ErrSsePath") + } + } + if err := checkPortExist(create.Port); err != nil { + return err + } + if err := checkContainerName(create.ContainerName); err != nil { + return err + } + mcpDir := path.Join(constant.McpDir, create.Name) + mcpServer := &model.McpServer{ + Name: create.Name, + ContainerName: create.ContainerName, + Port: create.Port, + Command: create.Command, + Status: constant.RuntimeNormal, + BaseURL: create.BaseURL, + SsePath: create.SsePath, + Dir: mcpDir, + } + 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 + } + startMcp(mcpServer) + addProxy(mcpServer) + return syncMcpServerContainerStatus(mcpServer) +} + +func (m McpServerService) Delete(id uint) error { + mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return err + } + composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml") + _, _ = compose.Down(composePath) + _ = files.NewFileOp().DeleteDir(path.Join(constant.McpDir, mcpServer.Name)) + + websiteID := GetWebsiteID() + if websiteID > 0 { + websiteService := NewIWebsiteService() + delProxyReq := request.WebsiteProxyDel{ + ID: websiteID, + Name: mcpServer.Name, + } + _ = websiteService.DeleteProxy(delProxyReq) + } + return mcpServerRepo.DeleteBy(commonRepo.WithByID(id)) +} + +func (m McpServerService) Operate(req request.McpServerOperate) error { + mcpServer, err := mcpServerRepo.GetFirst(commonRepo.WithByID(req.ID)) + if 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.RuntimeRunning + case "stop": + out, err = compose.Down(composePath) + mcpServer.Status = constant.RuntimeStopped + case "restart": + out, err = compose.Restart(composePath) + mcpServer.Status = constant.RuntimeRunning + } + if err != nil { + mcpServer.Status = constant.RuntimeError + mcpServer.Message = out + } + return mcpServerRepo.Save(mcpServer) +} + +func (m McpServerService) GetBindDomain() (response.McpBindDomainRes, error) { + var res response.McpBindDomainRes + websiteID := GetWebsiteID() + if websiteID == 0 { + return res, nil + } + website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID)) + if err != nil { + return res, err + } + res.WebsiteID = website.ID + res.Domain = website.PrimaryDomain + if website.WebsiteSSLID > 0 { + res.SSLID = website.WebsiteSSLID + ssl, _ := websiteSSLRepo.GetFirst(commonRepo.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(commonRepo.WithByID(req.SSLID)) + if err != nil { + return err + } + if ssl.Pem == "" { + return buserr.New("ErrSSL") + } + } + createWebsiteReq := request.WebsiteCreate{ + PrimaryDomain: req.Domain, + Alias: strings.ToLower(req.Domain), + Type: constant.Static, + } + 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 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 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(commonRepo.WithByID(req.SSLID)) + if err != nil { + return err + } + if ssl.Pem == "" { + return buserr.New("ErrSSL") + } + } + websiteService := NewIWebsiteService() + website, err := websiteRepo.GetFirst(commonRepo.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(commonRepo.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) + } + for _, server := range servers { + if server.BaseURL != baseUrl { + server.BaseURL = baseUrl + go updateMcpServer(&server) + } + } +} + +func addProxy(server *model.McpServer) { + websiteID := GetWebsiteID() + website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID)) + if err != nil { + global.LOG.Errorf("[mcp] add proxy failed, err: %v", err) + return + } + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + global.LOG.Errorf("[mcp] add proxy failed, err: %v", err) + return + } + fileOp := files.NewFileOp() + includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy") + 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 + } + location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.1:%d%s", server.Port, server.SsePath)}) + location.ChangePath("^~", server.SsePath) + if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + global.LOG.Errorf("write config failed, err: %v", buserr.WithErr(constant.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 + } + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID)) + if err != nil { + return err + } + fileOp := files.NewFileOp() + includeDir := path.Join(nginxInstall.GetPath(), "www", "sites", website.Alias, "proxy") + 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 + } + location.UpdateDirective("proxy_pass", []string{fmt.Sprintf("http://127.0.1:%d%s", server.Port, server.SsePath)}) + location.ChangePath("^~", server.SsePath) + if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return buserr.WithErr(constant.ErrUpdateBuWebsite, err) + } + server.BaseURL = baseUrl + 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 { + env := handleEnv(mcpServer) + if err := gotenv.Write(env, path.Join(mcpServer.Dir, ".env")); err != nil { + return err + } + composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml") + _, _ = compose.Down(composePath) + if _, err := compose.Up(composePath); err != nil { + mcpServer.Status = constant.RuntimeError + mcpServer.Message = err.Error() + } + + return mcpServerRepo.Save(mcpServer) +} + +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 + 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 = mcp.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(constant.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) + } + delete(serviceValue, "environment") + if len(environments) > 0 { + envMap := make(map[string]string) + for _, env := range environments { + envMap[env.Key] = env.Value + } + serviceValue["environment"] = envMap + } + delete(serviceValue, "volumes") + if len(volumes) > 0 { + volumeList := make([]string, 0) + for _, volume := range volumes { + volumeList = append(volumeList, fmt.Sprintf("%s:%s", volume.Source, volume.Target)) + } + serviceValue["volumes"] = volumeList + } + + services[mcpServer.Name] = serviceValue + composeByte, err := yaml.Marshal(composeMap) + if err != nil { + return err + } + mcpServer.DockerCompose = string(composeByte) + return nil +} + +func startMcp(mcpServer *model.McpServer) { + composePath := path.Join(constant.McpDir, mcpServer.Name, "docker-compose.yml") + if mcpServer.Status != constant.RuntimeNormal { + _, _ = compose.Down(composePath) + } + if out, err := compose.Up(composePath); err != nil { + mcpServer.Status = constant.RuntimeError + mcpServer.Message = out + } else { + mcpServer.Status = constant.RuntimeRunning + mcpServer.Message = "" + } +} + +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 { + mcpServer.Status = constant.RuntimeStopped + return mcpServerRepo.Save(mcpServer) + } + container := containers[0] + switch container.State { + case "exited": + mcpServer.Status = constant.RuntimeError + case "running": + mcpServer.Status = constant.RuntimeRunning + case "paused": + mcpServer.Status = constant.RuntimeStopped + default: + if mcpServer.Status != constant.RuntimeBuildIng { + mcpServer.Status = constant.RuntimeStopped + } + } + return mcpServerRepo.Save(mcpServer) +} + +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) +} diff --git a/backend/app/service/website.go b/backend/app/service/website.go index 841bbbcaa..271ee702b 100644 --- a/backend/app/service/website.go +++ b/backend/app/service/website.go @@ -496,6 +496,10 @@ func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error { if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebsiteId(req.ID)); err != nil { return err } + websiteID := GetWebsiteID() + if req.ID == websiteID { + _ = settingRepo.UpdateOrCreate("MCP_WEBSITE_ID", "0") + } tx.Commit() uploadDir := path.Join(global.CONF.System.BaseDir, fmt.Sprintf("1panel/uploads/website/%s", website.Alias)) diff --git a/backend/constant/dir.go b/backend/constant/dir.go index 1b474a63e..9c960a82a 100644 --- a/backend/constant/dir.go +++ b/backend/constant/dir.go @@ -17,4 +17,5 @@ var ( RuntimeDir = path.Join(DataDir, "runtime") RecycleBinDir = "/.1panel_clash" SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl") + McpDir = path.Join(DataDir, "mcp") ) diff --git a/backend/i18n/lang/en.yaml b/backend/i18n/lang/en.yaml index bde74da55..8f1902029 100644 --- a/backend/i18n/lang/en.yaml +++ b/backend/i18n/lang/en.yaml @@ -288,6 +288,7 @@ SystemMode: "mode: " #ai-tool ErrOpenrestyInstall: 'Please install Openresty first' ErrSSL: "Certificate content is empty, please check the certificate!" +ErrSsePath: "SSE path is duplicated" #mobile app ErrVerifyToken: 'Token verification error, please reset and scan again.' diff --git a/backend/i18n/lang/ja.yaml b/backend/i18n/lang/ja.yaml index 6ca47890d..8f7b444f7 100644 --- a/backend/i18n/lang/ja.yaml +++ b/backend/i18n/lang/ja.yaml @@ -285,6 +285,7 @@ SystemMode: "モード: " #ai-tool ErrOpenrestyInstall: 'まず Openresty をインストールしてください' ErrSSL: "証明書の内容が空です。証明書を確認してください!" +ErrSsePath: "SSE パスが重複しています" #mobile app diff --git a/backend/i18n/lang/ko.yaml b/backend/i18n/lang/ko.yaml index f65cbdf3a..2f04ff27d 100644 --- a/backend/i18n/lang/ko.yaml +++ b/backend/i18n/lang/ko.yaml @@ -288,6 +288,7 @@ SystemMode: "모드: " #ai-tool ErrOpenrestyInstall: '먼저 Openresty를 설치하세요' ErrSSL: "인증서 내용이 비어 있습니다. 인증서를 확인하세요!" +ErrSsePath: "SSE 경로가 중복되었습니다" #mobile app diff --git a/backend/i18n/lang/ms.yml b/backend/i18n/lang/ms.yml index d6d9a0272..fda011c8a 100644 --- a/backend/i18n/lang/ms.yml +++ b/backend/i18n/lang/ms.yml @@ -287,7 +287,7 @@ SystemMode: "Mod: " #ai-tool ErrOpenrestyInstall: 'Sila pasang Openresty terlebih dahulu' ErrSSL: "Kandungan sijil kosong, sila periksa sijil!" - +ErrSsePath: "Laluan SSE bertindan" #mobile app ErrVerifyToken: 'Ralat pengesahan token, sila tetapkan semula dan imbas semula.' diff --git a/backend/i18n/lang/pt-BR.yaml b/backend/i18n/lang/pt-BR.yaml index f4e8890f5..05f18bcbd 100644 --- a/backend/i18n/lang/pt-BR.yaml +++ b/backend/i18n/lang/pt-BR.yaml @@ -285,6 +285,7 @@ SystemMode: "modo: " #ai-tool ErrOpenrestyInstall: 'Por favor, instale o Openresty primeiro' ErrSSL: "O conteúdo do certificado está vazio, por favor, verifique o certificado!" +ErrSsePath: "Caminho SSE duplicado" #mobile app ErrVerifyToken: 'Erro de verificação do token, por favor, reinicie e escaneie novamente.' diff --git a/backend/i18n/lang/ru.yaml b/backend/i18n/lang/ru.yaml index 986ca78ce..088bb8f8e 100644 --- a/backend/i18n/lang/ru.yaml +++ b/backend/i18n/lang/ru.yaml @@ -288,6 +288,7 @@ SystemMode: "режим: " #ai-tool ErrOpenrestyInstall: "Пожалуйста, установите Openresty сначала" ErrSSL: "Содержимое сертификата пустое, пожалуйста, проверьте сертификат!" +ErrSsePath: "Путь SSE дублируется" #mobile app ErrVerifyToken: 'шибка проверки токена, пожалуйста, сбросьте и отсканируйте снова.' diff --git a/backend/i18n/lang/zh-Hant.yaml b/backend/i18n/lang/zh-Hant.yaml index 4f164b052..8496e5848 100644 --- a/backend/i18n/lang/zh-Hant.yaml +++ b/backend/i18n/lang/zh-Hant.yaml @@ -288,6 +288,7 @@ SystemMode: "模式: " #ai-tool ErrOpenrestyInstall: "請先安裝 Openresty" ErrSSL: "證書內容為空,請檢查證書!" +ErrSsePath: "SSE 路徑重複" #mobile app ErrVerifyToken: '令牌驗證錯誤,請重置後再次掃碼' diff --git a/backend/i18n/lang/zh.yaml b/backend/i18n/lang/zh.yaml index d5b2e7bac..a64d35986 100644 --- a/backend/i18n/lang/zh.yaml +++ b/backend/i18n/lang/zh.yaml @@ -288,6 +288,7 @@ SystemMode: "模式: " #ai-tool ErrOpenrestyInstall: '请先安装 Openresty' ErrSSL: "证书内容为空,请检查证书!" +ErrSsePath: "SSE 路径重复" #mobile app ErrVerifyToken: '令牌验证错误,请重置后再次扫码' diff --git a/backend/init/app/app.go b/backend/init/app/app.go index a7152f301..794bde105 100644 --- a/backend/init/app/app.go +++ b/backend/init/app/app.go @@ -23,8 +23,11 @@ func Init() { constant.SSLLogDir = path.Join(global.CONF.System.DataDir, "log", "ssl") + constant.McpDir = path.Join(constant.DataDir, "mcp") + dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir, - global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir, constant.SSLLogDir} + global.CONF.System.Backup, constant.RuntimeDir, constant.LocalAppResourceDir, constant.RemoteAppResourceDir, + constant.SSLLogDir, constant.McpDir} fileOp := files.NewFileOp() for _, dir := range dirs { diff --git a/backend/init/migration/migrate.go b/backend/init/migration/migrate.go index 01874d677..74ff10e38 100644 --- a/backend/init/migration/migrate.go +++ b/backend/init/migration/migrate.go @@ -106,6 +106,8 @@ func Init() { migrations.AddAppMenu, migrations.AddAppPanelName, migrations.AddLicenseVerify, + + migrations.AddMcpServer, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/backend/init/migration/migrations/v_1_10.go b/backend/init/migration/migrations/v_1_10.go index 823deb9d8..6e4449d32 100644 --- a/backend/init/migration/migrations/v_1_10.go +++ b/backend/init/migration/migrations/v_1_10.go @@ -459,3 +459,13 @@ var AddLicenseVerify = &gormigrate.Migration{ return nil }, } + +var AddMcpServer = &gormigrate.Migration{ + ID: "20250401-add-mcp-server", + Migrate: func(tx *gorm.DB) error { + if err := tx.AutoMigrate(&model.McpServer{}); err != nil { + return err + } + return nil + }, +} diff --git a/backend/router/common.go b/backend/router/common.go index 320032032..17e27f1f2 100644 --- a/backend/router/common.go +++ b/backend/router/common.go @@ -24,5 +24,6 @@ func commonGroups() []CommonRouter { &ProcessRouter{}, &WebsiteCARouter{}, &AIToolsRouter{}, + &McpServerRouter{}, } } diff --git a/backend/router/ro_ai.go b/backend/router/ro_ai.go index 1fba58452..159015b70 100644 --- a/backend/router/ro_ai.go +++ b/backend/router/ro_ai.go @@ -26,5 +26,14 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/domain/bind", baseApi.BindDomain) aiToolsRouter.POST("/domain/get", baseApi.GetBindDomain) aiToolsRouter.POST("/domain/update", baseApi.UpdateBindDomain) + + aiToolsRouter.POST("/mcp/search", baseApi.PageMcpServers) + aiToolsRouter.POST("/mcp/server", baseApi.CreateMcpServer) + aiToolsRouter.POST("/mcp/server/update", baseApi.UpdateMcpServer) + aiToolsRouter.POST("/mcp/server/del", baseApi.DeleteMcpServer) + aiToolsRouter.POST("/mcp/server/op", baseApi.OperateMcpServer) + aiToolsRouter.POST("/mcp/domain/bind", baseApi.BindMcpDomain) + aiToolsRouter.GET("/mcp/domain/get", baseApi.GetMcpBindDomain) + aiToolsRouter.POST("/mcp/domain/update", baseApi.UpdateMcpBindDomain) } } diff --git a/backend/router/ro_mcp.go b/backend/router/ro_mcp.go new file mode 100644 index 000000000..ecfa8b782 --- /dev/null +++ b/backend/router/ro_mcp.go @@ -0,0 +1,21 @@ +package router + +import ( + v1 "github.com/1Panel-dev/1Panel/backend/app/api/v1" + "github.com/1Panel-dev/1Panel/backend/middleware" + "github.com/gin-gonic/gin" +) + +type McpServerRouter struct { +} + +func (m *McpServerRouter) InitRouter(Router *gin.RouterGroup) { + mcpRouter := Router.Group("mcp") + mcpRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth()).Use(middleware.PasswordExpired()) + + baseApi := v1.ApiGroupApp.BaseApi + { + mcpRouter.POST("/search", baseApi.PageMcpServers) + mcpRouter.POST("/server", baseApi.CreateMcpServer) + } +} diff --git a/backend/utils/nginx/components/location.go b/backend/utils/nginx/components/location.go index 80620500d..eb21588e9 100644 --- a/backend/utils/nginx/components/location.go +++ b/backend/utils/nginx/components/location.go @@ -46,7 +46,9 @@ func NewLocation(directive IDirective) *Location { location.Host = params[1] } case "proxy_cache": - location.Cache = true + if params[0] != "off" { + location.Cache = true + } case "if": if params[0] == "(" && params[1] == "$uri" && params[2] == "~*" { dirs := dir.GetBlock().GetDirectives() diff --git a/cmd/server/mcp/compose.yml b/cmd/server/mcp/compose.yml new file mode 100644 index 000000000..9aef87157 --- /dev/null +++ b/cmd/server/mcp/compose.yml @@ -0,0 +1,19 @@ +services: + mcp-server: + image: supercorp/supergateway:latest + container_name: ${CONTAINER_NAME} + restart: unless-stopped + ports: + - "${HOST_IP}:${PANEL_APP_PORT_HTTP}:${PANEL_APP_PORT_HTTP}" + command: [ + "--stdio", "${COMMAND}", + "--port", "${PANEL_APP_PORT_HTTP}", + "--baseUrl", "${BASE_URL}", + "--ssePath", "${SSE_PATH}", + "--messagePath", "${SSE_PATH}/messages" + ] + networks: + - 1panel-network +networks: + 1panel-network: + external: true \ No newline at end of file diff --git a/cmd/server/mcp/mcp.go b/cmd/server/mcp/mcp.go new file mode 100644 index 000000000..0e87d1335 --- /dev/null +++ b/cmd/server/mcp/mcp.go @@ -0,0 +1,8 @@ +package mcp + +import ( + _ "embed" +) + +//go:embed compose.yml +var DefaultMcpCompose []byte diff --git a/cmd/server/nginx_conf/nginx_conf.go b/cmd/server/nginx_conf/nginx_conf.go index 7b376a9b0..ec0557a0f 100644 --- a/cmd/server/nginx_conf/nginx_conf.go +++ b/cmd/server/nginx_conf/nginx_conf.go @@ -37,3 +37,6 @@ var DomainNotFoundHTML []byte //go:embed stop.html var StopHTML []byte + +//go:embed sse.conf +var SSE []byte diff --git a/cmd/server/nginx_conf/sse.conf b/cmd/server/nginx_conf/sse.conf new file mode 100644 index 000000000..1a03ba96d --- /dev/null +++ b/cmd/server/nginx_conf/sse.conf @@ -0,0 +1,7 @@ +location ^~ /github { + proxy_pass http://127.0.0.1:8001/github; + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Connection ''; + chunked_transfer_encoding off; +} \ No newline at end of file diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index 46444710d..43fbd8248 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -109,4 +109,73 @@ export namespace AI { websiteID?: number; connUrl: string; } + + export interface Environment { + key: string; + value: string; + } + + export interface Volume { + source: string; + target: string; + } + + export interface McpServer { + id: number; + name: string; + status: string; + baseUrl: string; + ssePath: string; + command: string; + port: number; + message: string; + createdAt?: string; + containerName: string; + environments: Environment[]; + volumes: Volume[]; + dir?: string; + hostIP: string; + } + + export interface McpServerSearch extends ReqPage { + name: string; + } + + export interface McpServerDelete { + id: number; + } + + export interface McpServerOperate { + id: number; + operate: string; + } + + export interface McpBindDomain { + domain: string; + sslID: number; + ipList: string; + } + + export interface McpDomainRes { + domain: string; + sslID: number; + acmeAccountID: number; + allowIPs: string[]; + websiteID?: number; + connUrl: string; + } + + export interface McpBindDomainUpdate { + websiteID: number; + sslID: number; + ipList: string; + } + + export interface ImportMcpServer { + name: string; + command: string; + ssePath: string; + containerName: string; + environments: Environment[]; + } } diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index 464f553cd..16b674fa4 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -39,3 +39,35 @@ export const getBindDomain = (req: AI.BindDomainReq) => { export const updateBindDomain = (req: AI.BindDomain) => { return http.post(`/ai/domain/update`, req); }; + +export const pageMcpServer = (req: AI.McpServerSearch) => { + return http.post>(`/ai/mcp/search`, req); +}; + +export const createMcpServer = (req: AI.McpServer) => { + return http.post(`/ai/mcp/server`, req); +}; + +export const updateMcpServer = (req: AI.McpServer) => { + return http.post(`/ai/mcp/server/update`, req); +}; + +export const deleteMcpServer = (req: AI.McpServerDelete) => { + return http.post(`/ai/mcp/server/del`, req); +}; + +export const operateMcpServer = (req: AI.McpServerOperate) => { + return http.post(`/ai/mcp/server/op`, req); +}; + +export const bindMcpDomain = (req: AI.McpBindDomain) => { + return http.post(`/ai/mcp/domain/bind`, req); +}; + +export const getMcpDomain = () => { + return http.get(`/ai/mcp/domain/get`); +}; + +export const updateMcpDomain = (req: AI.McpBindDomainUpdate) => { + return http.post(`/ai/mcp/domain/update`, req); +}; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 8431387bc..581c4ed54 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -2601,6 +2601,27 @@ const message = { proxyHelper6: 'To disable proxy configuration, you can delete it from the website list.', whiteListHelper: 'Restrict access to only IPs in the whitelist', }, + mcp: { + server: 'MCP Server', + create: 'Add Server', + edit: 'Edit Server', + commandHelper: 'For example: npx -y {0}', + baseUrl: 'External Access Path', + baseUrlHelper: 'For example: https://127.0.0.1:8080', + ssePath: 'SSE Path', + ssePathHelper: 'For example: /sse, note not to duplicate with other servers', + environment: 'Environment Variables', + envKey: 'Variable Name', + envValue: 'Variable Value', + externalUrl: 'External Connection Address', + operatorHelper: 'Will perform {1} operation on {0}, continue?', + domain: 'Default Access Address', + domainHelper: 'For example: 192.168.1.1 or example.com', + bindDomain: 'Bind Website', + commandPlaceHolder: 'Currently only supports commands started with npx and binary', + importMcpJson: 'Import MCP Server Configuration', + importMcpJsonError: 'mcpServers structure is incorrect', + }, }; export default { diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 6905bfa55..e59717d92 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -2570,6 +2570,27 @@ const message = { proxyHelper6: 'プロキシ設定を無効にするには、ウェブサイトリストから削除できます。', whiteListHelper: 'ホワイトリスト内のIPのみアクセスを許可する', }, + mcp: { + server: 'MCP サーバー', + create: 'サーバーを追加', + edit: 'サーバーを編集', + commandHelper: '例: npx -y {0}', + baseUrl: '外部アクセスパス', + baseUrlHelper: '例: https://127.0.0.1:8080', + ssePath: 'SSE パス', + ssePathHelper: '例: /sse, 他のサーバーと重複しないように注意してください', + environment: '環境変数', + envKey: '変数名', + envValue: '変数値', + externalUrl: '外部接続アドレス', + operatorHelper: '{0} に {1} 操作を実行します、続行しますか?', + domain: 'デフォルトアクセスアドレス', + domainHelper: '例: 192.168.1.1 または example.com', + bindDomain: 'ウェブサイトをバインド', + commandPlaceHolder: '現在、npx およびバイナリ起動のコマンドのみをサポートしています', + importMcpJson: 'MCP サーバー設定をインポート', + importMcpJsonError: 'mcpServers 構造が正しくありません', + }, }; export default { ...fit2cloudJaLocale, diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 3c1f4b642..34c717fb0 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -2530,6 +2530,27 @@ const message = { proxyHelper6: '프록시 구성을 비활성화하려면 웹사이트 목록에서 삭제할 수 있습니다.', whiteListHelper: '화이트리스트에 있는 IP만 접근 허용', }, + mcp: { + server: 'MCP サーバー', + create: 'サーバーを追加', + edit: 'サーバーを編集', + commandHelper: '例: npx -y {0}', + baseUrl: '外部アクセスパス', + baseUrlHelper: '例: https://127.0.0.1:8080', + ssePath: 'SSE パス', + ssePathHelper: '例: /sse, 他のサーバーと重複しないように注意してください', + environment: '環境変数', + envKey: '変数名', + envValue: '変数値', + externalUrl: '外部接続アドレス', + operatorHelper: '{0} に {1} 操作を実行します、続行しますか?', + domain: 'デフォルトアクセスアドレス', + domainHelper: '例: 192.168.1.1 または example.com', + bindDomain: 'ウェブサイトをバインド', + commandPlaceHolder: '現在、npx およびバイナリ起動のコマンドのみをサポートしています', + importMcpJson: 'MCP サーバー設定をインポート', + importMcpJsonError: 'mcpServers 構造が正しくありません', + }, }; export default { diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index 4876e4e29..3598ce7fe 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -2629,6 +2629,27 @@ const message = { proxyHelper6: 'Untuk melumpuhkan konfigurasi proksi, anda boleh memadamnya dari senarai laman web.', whiteListHelper: 'Hadkan akses kepada hanya IP dalam senarai putih', }, + mcp: { + server: 'Pelayan MCP', + create: 'Tambah Pelayan', + edit: 'Edit Pelayan', + commandHelper: 'Contoh: npx -y {0}', + baseUrl: 'Laluan Akses Luar', + baseUrlHelper: 'Contoh: https://127.0.0.1:8080', + ssePath: 'Laluan SSE', + ssePathHelper: 'Contoh: /sse, berhati-hati jangan bertindan dengan pelayan lain', + environment: 'Pemboleh Ubah Persekitaran', + envKey: 'Nama Pemboleh Ubah', + envValue: 'Nilai Pemboleh Ubah', + externalUrl: 'Alamat Sambungan Luar', + operatorHelper: 'Akan melakukan operasi {1} pada {0}, teruskan?', + domain: 'Alamat Akses Lalai', + domainHelper: 'Contoh: 192.168.1.1 atau example.com', + bindDomain: 'Sematkan Laman Web', + commandPlaceHolder: 'Kini hanya menyokong perintah yang bermula dengan npx dan binari', + importMcpJson: 'Import Konfigurasi Pelayan MCP', + importMcpJsonError: 'Struktur mcpServers tidak betul', + }, }; export default { diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 349f39d7a..7dc23f932 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -2626,6 +2626,27 @@ const message = { proxyHelper6: 'Para desativar a configuração de proxy, você pode excluí-la da lista de sites.', whiteListHelper: 'Restringir o acesso apenas aos IPs na lista branca', }, + mcp: { + server: 'Servidor MCP', + create: 'Adicionar Servidor', + edit: 'Editar Servidor', + commandHelper: 'Por exemplo: npx -y {0}', + baseUrl: 'Caminho de Acesso Externo', + baseUrlHelper: 'Por exemplo: https://127.0.0.1:8080', + ssePath: 'Caminho SSE', + ssePathHelper: 'Por exemplo: /sse, tome cuidado para não duplicar com outros servidores', + environment: 'Variáveis de Ambiente', + envKey: 'Nome da Variável', + envValue: 'Valor da Variável', + externalUrl: 'Endereço de Conexão Externo', + operatorHelper: 'Será realizada a operação {1} no {0}, continuar?', + domain: 'Endereço de Acesso Padrão', + domainHelper: 'Por exemplo: 192.168.1.1 ou example.com', + bindDomain: 'Vincular Site', + commandPlaceHolder: 'Atualmente, apenas comandos iniciados com npx e binário são suportados', + importMcpJson: 'Importar Configuração do Servidor MCP', + importMcpJsonError: 'A estrutura mcpServers está incorreta', + }, }; export default { diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 5565a24e0..fb6220710 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -2623,6 +2623,28 @@ const message = { proxyHelper6: 'Чтобы отключить настройку прокси, вы можете удалить её из списка сайтов.', whiteListHelper: 'Ограничить доступ только для IP-адресов из белого списка', }, + mcp: { + server: 'Сервер MCP', + create: 'Добавить сервер', + edit: 'Редактировать сервер', + commandHelper: 'Например: npx -y {0}', + baseUrl: 'Внешний путь доступа', + baseUrlHelper: 'Например: https://127.0.0.1:8080', + ssePath: 'Путь SSE', + ssePathHelper: 'Например: /sse, будьте осторожны, чтобы не дублировать с другими серверами', + environment: 'Переменные среды', + envKey: 'Имя переменной', + envValue: 'Значение переменной', + externalUrl: 'Внешний адрес подключения', + operatorHelper: 'Будет выполнена операция {1} на {0}, продолжить?', + domain: 'Адрес доступа по умолчанию', + domainHelper: 'Например: 192.168.1.1 или example.com', + bindDomain: 'Привязать сайт', + commandPlaceHolder: + 'В настоящее время поддерживаются только команды, запускаемые с помощью npx и бинарных файлов', + importMcpJson: 'Импортировать конфигурацию сервера MCP', + importMcpJsonError: 'Структура mcpServers некорректна', + }, }; export default { diff --git a/frontend/src/lang/modules/tw.ts b/frontend/src/lang/modules/tw.ts index b86b20e19..0f822351c 100644 --- a/frontend/src/lang/modules/tw.ts +++ b/frontend/src/lang/modules/tw.ts @@ -2436,6 +2436,27 @@ const message = { proxyHelper6: '如需關閉代理配置,可以在網站列表中刪除', whiteListHelper: '限制僅白名單中的 IP 可瀏覽', }, + mcp: { + server: 'MCP 伺服器', + create: '添加伺服器', + edit: '編輯伺服器', + commandHelper: '例如:npx -y {0}', + baseUrl: '外部訪問路徑', + baseUrlHelper: '例如:https://127.0.0.1:8080', + ssePath: 'SSE 路徑', + ssePathHelper: '例如:/sse,注意不要與其他伺服器重複', + environment: '環境變數', + envKey: '變數名', + envValue: '變數值', + externalUrl: '外部連接地址', + operatorHelper: '將對 {0} 進行 {1} 操作,是否繼續?', + domain: '默認訪問地址', + domainHelper: '例如:192.168.1.1 或者 example.com', + bindDomain: '綁定網站', + commandPlaceHolder: '當前僅支持 npx 和二進制啟動的命令', + importMcpJson: '導入 MCP 伺服器配置', + importMcpJsonError: 'mcpServers 結構不正確', + }, }; export default { ...fit2cloudTwLocale, diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 58c25f264..c51d6a965 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -2438,6 +2438,27 @@ const message = { proxyHelper6: '如需关闭代理配置,可以在网站列表中删除', whiteListHelper: '限制仅白名单中的 IP 可访问', }, + mcp: { + server: 'MCP 服务器', + create: '添加服务器', + edit: '编辑服务器', + commandHelper: '例如:npx -y {0}', + baseUrl: '外部访问路径', + baseUrlHelper: '例如:https://127.0.0.1:8080', + ssePath: 'SSE 路径', + ssePathHelper: '例如:/sse,注意不要与其他服务器重复', + environment: '环境变量', + envKey: '变量名', + envValue: '变量值', + externalUrl: '外部连接地址', + operatorHelper: '将对 {0} 进行 {1} 操作,是否继续?', + domain: '默认访问地址', + domainHelper: '例如:192.168.1.1 或者 example.com', + bindDomain: '绑定网站', + commandPlaceHolder: '当前仅支持 npx 和二进制启动的命令', + importMcpJson: '导入 MCP 服务器配置', + importMcpJsonError: 'mcpServers 结构不正确', + }, }; export default { ...fit2cloudZhLocale, diff --git a/frontend/src/routers/modules/ai.ts b/frontend/src/routers/modules/ai.ts index 0750f4c37..2ce49e936 100644 --- a/frontend/src/routers/modules/ai.ts +++ b/frontend/src/routers/modules/ai.ts @@ -28,6 +28,15 @@ const databaseRouter = { requiresAuth: true, }, }, + { + path: '/ai/mcp', + name: 'MCPServer', + component: () => import('@/views/ai/mcp/server/index.vue'), + meta: { + title: 'MCP', + requiresAuth: true, + }, + }, ], }; diff --git a/frontend/src/views/ai/mcp/index.vue b/frontend/src/views/ai/mcp/index.vue new file mode 100644 index 000000000..868e214fb --- /dev/null +++ b/frontend/src/views/ai/mcp/index.vue @@ -0,0 +1,19 @@ + + + diff --git a/frontend/src/views/ai/mcp/server/bind/index.vue b/frontend/src/views/ai/mcp/server/bind/index.vue new file mode 100644 index 000000000..33c78af08 --- /dev/null +++ b/frontend/src/views/ai/mcp/server/bind/index.vue @@ -0,0 +1,253 @@ + + + + + diff --git a/frontend/src/views/ai/mcp/server/import/index.vue b/frontend/src/views/ai/mcp/server/import/index.vue new file mode 100644 index 000000000..748d77b03 --- /dev/null +++ b/frontend/src/views/ai/mcp/server/import/index.vue @@ -0,0 +1,82 @@ + + + diff --git a/frontend/src/views/ai/mcp/server/index.vue b/frontend/src/views/ai/mcp/server/index.vue new file mode 100644 index 000000000..d24956369 --- /dev/null +++ b/frontend/src/views/ai/mcp/server/index.vue @@ -0,0 +1,244 @@ + + + diff --git a/frontend/src/views/ai/mcp/server/operate/index.vue b/frontend/src/views/ai/mcp/server/operate/index.vue new file mode 100644 index 000000000..c674421d5 --- /dev/null +++ b/frontend/src/views/ai/mcp/server/operate/index.vue @@ -0,0 +1,251 @@ + + + diff --git a/frontend/src/views/ai/mcp/server/volume/index.vue b/frontend/src/views/ai/mcp/server/volume/index.vue new file mode 100644 index 000000000..3661506bb --- /dev/null +++ b/frontend/src/views/ai/mcp/server/volume/index.vue @@ -0,0 +1,60 @@ + + + diff --git a/go.mod b/go.mod index 7272252b8..d344e7ca4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/1Panel-dev/1Panel -go 1.22.3 +go 1.23 + +toolchain go1.23.7 require ( github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible @@ -170,6 +172,7 @@ require ( github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mark3labs/mcp-go v0.14.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/miekg/dns v1.1.62 // indirect diff --git a/go.sum b/go.sum index 64363058a..17ce90829 100644 --- a/go.sum +++ b/go.sum @@ -658,6 +658,8 @@ github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0V github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mark3labs/mcp-go v0.14.1 h1:NsieyFbuWQaeZSWSHPvJ5TwJdQwu+1jmivAIVljeouY= +github.com/mark3labs/mcp-go v0.14.1/go.mod h1:xBB350hekQsJAK7gJAii8bcEoWemboLm2mRm5/+KBaU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=