mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-01-12 00:06:29 +08:00
feat: Add MCP management (#8299)
This commit is contained in:
@@ -68,4 +68,6 @@ var (
|
||||
favoriteService = service.NewIFavoriteService()
|
||||
|
||||
websiteCAService = service.NewIWebsiteCAService()
|
||||
|
||||
mcpServerService = service.NewIMcpServerService()
|
||||
)
|
||||
|
||||
167
backend/app/api/v1/mcp_server.go
Normal file
167
backend/app/api/v1/mcp_server.go
Normal file
@@ -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)
|
||||
}
|
||||
10
backend/app/dto/mcp.go
Normal file
10
backend/app/dto/mcp.go
Normal file
@@ -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"`
|
||||
}
|
||||
57
backend/app/dto/request/mcp_server.go
Normal file
57
backend/app/dto/request/mcp_server.go
Normal file
@@ -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"`
|
||||
}
|
||||
26
backend/app/dto/response/mcp_server.go
Normal file
26
backend/app/dto/response/mcp_server.go
Normal file
@@ -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"`
|
||||
}
|
||||
18
backend/app/model/mcp_server.go
Normal file
18
backend/app/model/mcp_server.go
Normal file
@@ -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"`
|
||||
}
|
||||
56
backend/app/repo/mcp_server.go
Normal file
56
backend/app/repo/mcp_server.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -46,4 +46,6 @@ var (
|
||||
phpExtensionsRepo = repo.NewIPHPExtensionsRepo()
|
||||
|
||||
favoriteRepo = repo.NewIFavoriteRepo()
|
||||
|
||||
mcpServerRepo = repo.NewIMcpServerRepo()
|
||||
)
|
||||
|
||||
627
backend/app/service/mcp_server.go
Normal file
627
backend/app/service/mcp_server.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -285,6 +285,7 @@ SystemMode: "モード: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: 'まず Openresty をインストールしてください'
|
||||
ErrSSL: "証明書の内容が空です。証明書を確認してください!"
|
||||
ErrSsePath: "SSE パスが重複しています"
|
||||
|
||||
|
||||
#mobile app
|
||||
|
||||
@@ -288,6 +288,7 @@ SystemMode: "모드: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: '먼저 Openresty를 설치하세요'
|
||||
ErrSSL: "인증서 내용이 비어 있습니다. 인증서를 확인하세요!"
|
||||
ErrSsePath: "SSE 경로가 중복되었습니다"
|
||||
|
||||
|
||||
#mobile app
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -288,6 +288,7 @@ SystemMode: "режим: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: "Пожалуйста, установите Openresty сначала"
|
||||
ErrSSL: "Содержимое сертификата пустое, пожалуйста, проверьте сертификат!"
|
||||
ErrSsePath: "Путь SSE дублируется"
|
||||
|
||||
#mobile app
|
||||
ErrVerifyToken: 'шибка проверки токена, пожалуйста, сбросьте и отсканируйте снова.'
|
||||
|
||||
@@ -288,6 +288,7 @@ SystemMode: "模式: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: "請先安裝 Openresty"
|
||||
ErrSSL: "證書內容為空,請檢查證書!"
|
||||
ErrSsePath: "SSE 路徑重複"
|
||||
|
||||
#mobile app
|
||||
ErrVerifyToken: '令牌驗證錯誤,請重置後再次掃碼'
|
||||
|
||||
@@ -288,6 +288,7 @@ SystemMode: "模式: "
|
||||
#ai-tool
|
||||
ErrOpenrestyInstall: '请先安装 Openresty'
|
||||
ErrSSL: "证书内容为空,请检查证书!"
|
||||
ErrSsePath: "SSE 路径重复"
|
||||
|
||||
#mobile app
|
||||
ErrVerifyToken: '令牌验证错误,请重置后再次扫码'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -106,6 +106,8 @@ func Init() {
|
||||
migrations.AddAppMenu,
|
||||
migrations.AddAppPanelName,
|
||||
migrations.AddLicenseVerify,
|
||||
|
||||
migrations.AddMcpServer,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,5 +24,6 @@ func commonGroups() []CommonRouter {
|
||||
&ProcessRouter{},
|
||||
&WebsiteCARouter{},
|
||||
&AIToolsRouter{},
|
||||
&McpServerRouter{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
21
backend/router/ro_mcp.go
Normal file
21
backend/router/ro_mcp.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
19
cmd/server/mcp/compose.yml
Normal file
19
cmd/server/mcp/compose.yml
Normal file
@@ -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
|
||||
8
cmd/server/mcp/mcp.go
Normal file
8
cmd/server/mcp/mcp.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed compose.yml
|
||||
var DefaultMcpCompose []byte
|
||||
@@ -37,3 +37,6 @@ var DomainNotFoundHTML []byte
|
||||
|
||||
//go:embed stop.html
|
||||
var StopHTML []byte
|
||||
|
||||
//go:embed sse.conf
|
||||
var SSE []byte
|
||||
|
||||
7
cmd/server/nginx_conf/sse.conf
Normal file
7
cmd/server/nginx_conf/sse.conf
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ResPage<AI.McpServer>>(`/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.McpDomainRes>(`/ai/mcp/domain/get`);
|
||||
};
|
||||
|
||||
export const updateMcpDomain = (req: AI.McpBindDomainUpdate) => {
|
||||
return http.post(`/ai/mcp/domain/update`, req);
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
19
frontend/src/views/ai/mcp/index.vue
Normal file
19
frontend/src/views/ai/mcp/index.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterButton :buttons="buttons" />
|
||||
<LayoutContent>
|
||||
<router-view></router-view>
|
||||
</LayoutContent>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import i18n from '@/lang';
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('container.server'),
|
||||
path: '/ai/mcp/servers',
|
||||
},
|
||||
];
|
||||
</script>
|
||||
253
frontend/src/views/ai/mcp/server/bind/index.vue
Normal file
253
frontend/src/views/ai/mcp/server/bind/index.vue
Normal file
@@ -0,0 +1,253 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="open"
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
size="50%"
|
||||
>
|
||||
<template #header>
|
||||
<DrawerHeader :header="$t('mcp.bindDomain')" :back="handleClose" />
|
||||
</template>
|
||||
<div v-loading="loading">
|
||||
<el-form ref="formRef" label-position="top" @submit.prevent :model="req" :rules="rules">
|
||||
<el-row type="flex" justify="center">
|
||||
<el-col :span="22">
|
||||
<el-alert class="common-prompt" :closable="false" type="warning">
|
||||
<template #default>
|
||||
<ul>
|
||||
<li>{{ $t('aitool.proxyHelper1') }}</li>
|
||||
<li>{{ $t('aitool.proxyHelper2') }}</li>
|
||||
<li>{{ $t('aitool.proxyHelper3') }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</el-alert>
|
||||
<el-form-item :label="$t('website.domain')" prop="domain">
|
||||
<el-input v-model.trim="req.domain" :disabled="operate === 'update'" />
|
||||
<span class="input-help">
|
||||
{{ $t('aitool.proxyHelper4') }}
|
||||
</span>
|
||||
<span class="input-help">
|
||||
{{ $t('aitool.proxyHelper6') }}
|
||||
<el-link
|
||||
class="pageRoute"
|
||||
icon="Position"
|
||||
@click="toWebsite(req.websiteID)"
|
||||
type="primary"
|
||||
>
|
||||
{{ $t('firewall.quickJump') }}
|
||||
</el-link>
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('xpack.waf.whiteList') + ' IP'" prop="ipList">
|
||||
<el-input
|
||||
:rows="3"
|
||||
type="textarea"
|
||||
clearable
|
||||
v-model="req.ipList"
|
||||
:placeholder="$t('xpack.waf.ipGroupHelper')"
|
||||
/>
|
||||
<span class="input-help">
|
||||
{{ $t('aitool.whiteListHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="req.enableSSL" @change="changeSSL">
|
||||
{{ $t('website.enable') + ' ' + 'HTTPS' }}
|
||||
</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('website.acmeAccountManage')"
|
||||
prop="acmeAccountID"
|
||||
v-if="req.enableSSL"
|
||||
>
|
||||
<el-select
|
||||
v-model="req.acmeAccountID"
|
||||
:placeholder="$t('website.selectAcme')"
|
||||
@change="listSSL"
|
||||
>
|
||||
<el-option :key="0" :label="$t('website.imported')" :value="0"></el-option>
|
||||
<el-option
|
||||
v-for="(acme, index) in acmeAccounts"
|
||||
:key="index"
|
||||
:label="acme.email"
|
||||
:value="acme.id"
|
||||
>
|
||||
<span>
|
||||
{{ acme.email }}
|
||||
<el-tag class="ml-5">{{ getAccountName(acme.type) }}</el-tag>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('website.ssl')" prop="sslID" v-if="req.enableSSL">
|
||||
<el-select
|
||||
v-model="req.sslID"
|
||||
:placeholder="$t('website.selectSSL')"
|
||||
@change="changeSSl(req.sslID)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(ssl, index) in ssls"
|
||||
:key="index"
|
||||
:label="ssl.primaryDomain"
|
||||
:value="ssl.id"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="onSubmit(formRef)">
|
||||
{{ $t('commons.button.add') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Website } from '@/api/interface/website';
|
||||
import { ListSSL, SearchAcmeAccount } from '@/api/modules/website';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { FormInstance, FormRules } from 'element-plus';
|
||||
import { reactive, ref } from 'vue';
|
||||
import { getAccountName } from '@/utils/util';
|
||||
import { bindMcpDomain, getMcpDomain, updateMcpDomain } from '@/api/modules/ai';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const open = ref(false);
|
||||
const operate = ref('create');
|
||||
const loading = ref(false);
|
||||
const ssls = ref([]);
|
||||
const websiteSSL = ref<Website.SSL>();
|
||||
const acmeAccounts = ref();
|
||||
const formRef = ref();
|
||||
const req = ref({
|
||||
domain: '',
|
||||
sslID: undefined,
|
||||
ipList: '',
|
||||
acmeAccountID: 0,
|
||||
enableSSL: false,
|
||||
allowIPs: [],
|
||||
websiteID: 0,
|
||||
});
|
||||
const rules = reactive<FormRules>({
|
||||
domain: [Rules.domainWithPort],
|
||||
sslID: [Rules.requiredSelectBusiness],
|
||||
});
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const acceptParams = () => {
|
||||
search();
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const changeSSl = (sslid: number) => {
|
||||
const res = ssls.value.filter((element: Website.SSL) => {
|
||||
return element.id == sslid;
|
||||
});
|
||||
websiteSSL.value = res[0];
|
||||
};
|
||||
|
||||
const changeSSL = () => {
|
||||
if (!req.value.enableSSL) {
|
||||
req.value.sslID = undefined;
|
||||
} else {
|
||||
listAcmeAccount();
|
||||
}
|
||||
};
|
||||
|
||||
const listSSL = () => {
|
||||
const sslReq = {
|
||||
acmeAccountID: String(req.value.acmeAccountID),
|
||||
};
|
||||
ListSSL(sslReq).then((res) => {
|
||||
ssls.value = res.data || [];
|
||||
if (ssls.value.length > 0) {
|
||||
let exist = false;
|
||||
for (const ssl of ssls.value) {
|
||||
if (ssl.id === req.value.sslID) {
|
||||
exist = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!exist) {
|
||||
req.value.sslID = ssls.value[0].id;
|
||||
}
|
||||
changeSSl(req.value.sslID);
|
||||
} else {
|
||||
req.value.sslID = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const listAcmeAccount = () => {
|
||||
SearchAcmeAccount({ page: 1, pageSize: 100 }).then((res) => {
|
||||
acmeAccounts.value = res.data.items || [];
|
||||
listSSL();
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
formEl.validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
if (operate.value === 'update') {
|
||||
await updateMcpDomain(req.value);
|
||||
} else {
|
||||
await bindMcpDomain(req.value);
|
||||
}
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
handleClose();
|
||||
});
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
try {
|
||||
const res = await getMcpDomain();
|
||||
if (res.data.websiteID > 0) {
|
||||
operate.value = 'update';
|
||||
req.value.domain = res.data.domain;
|
||||
req.value.websiteID = res.data.websiteID;
|
||||
if (res.data.allowIPs && res.data.allowIPs.length > 0) {
|
||||
req.value.ipList = res.data.allowIPs.join('\n');
|
||||
}
|
||||
if (res.data.sslID > 0) {
|
||||
req.value.enableSSL = true;
|
||||
req.value.sslID = res.data.sslID;
|
||||
req.value.acmeAccountID = res.data.acmeAccountID;
|
||||
listAcmeAccount();
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const toWebsite = (websiteID: number) => {
|
||||
if (websiteID != undefined && websiteID > 0) {
|
||||
window.location.href = `/websites/${websiteID}/config/basic`;
|
||||
} else {
|
||||
window.location.href = '/websites';
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pageRoute {
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
||||
82
frontend/src/views/ai/mcp/server/import/index.vue
Normal file
82
frontend/src/views/ai/mcp/server/import/index.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<el-dialog v-model="submitVisible" :destroy-on-close="true" :close-on-click-modal="false" width="40%">
|
||||
<template #header>
|
||||
{{ $t('mcp.importMcpJson') }}
|
||||
</template>
|
||||
<div>
|
||||
<el-input
|
||||
v-model="mcpServerJson"
|
||||
type="textarea"
|
||||
:rows="15"
|
||||
placeholder='{
|
||||
"mcpServers": {
|
||||
"postgres": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-postgres",
|
||||
"postgresql://localhost/mydb"
|
||||
]
|
||||
}
|
||||
}
|
||||
}'
|
||||
></el-input>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="onCancel">
|
||||
{{ $t('commons.button.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="onConfirm">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import i18n from '@/lang';
|
||||
import { MsgError } from '@/utils/message';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const submitVisible = ref(false);
|
||||
const mcpServerJson = ref();
|
||||
const mcpServerConfig = ref();
|
||||
|
||||
const acceptParams = (): void => {
|
||||
mcpServerJson.value = '';
|
||||
submitVisible.value = true;
|
||||
};
|
||||
const emit = defineEmits(['confirm', 'cancel']);
|
||||
|
||||
const onConfirm = async () => {
|
||||
try {
|
||||
const data = JSON.parse(mcpServerJson.value);
|
||||
if (!data.mcpServers || typeof data.mcpServers !== 'object') {
|
||||
throw new Error(i18n.global.t('mcp.importMcpJsonError'));
|
||||
}
|
||||
mcpServerConfig.value = Object.entries(data.mcpServers).map(([name, config]: any) => ({
|
||||
name,
|
||||
command: [config.command, ...config.args].join(' '),
|
||||
environments: data.env ? Object.entries(data.env).map(([key, value]) => ({ key, value })) : [],
|
||||
ssePath: '/' + name,
|
||||
containerName: name,
|
||||
}));
|
||||
} catch (error) {
|
||||
MsgError(error);
|
||||
return;
|
||||
}
|
||||
emit('confirm', mcpServerConfig.value);
|
||||
submitVisible.value = false;
|
||||
};
|
||||
|
||||
const onCancel = async () => {
|
||||
emit('cancel');
|
||||
submitVisible.value = false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
244
frontend/src/views/ai/mcp/server/index.vue
Normal file
244
frontend/src/views/ai/mcp/server/index.vue
Normal file
@@ -0,0 +1,244 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterMenu />
|
||||
<LayoutContent :title="$t('container.server')" v-loading="loading">
|
||||
<template #toolbar>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<el-button type="primary" @click="openCreate">
|
||||
{{ $t('mcp.create') }}
|
||||
</el-button>
|
||||
<el-button type="primary" plain @click="openDomain">
|
||||
{{ $t('mcp.bindDomain') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #main>
|
||||
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
|
||||
<el-table-column
|
||||
:label="$t('commons.table.name')"
|
||||
fix
|
||||
prop="name"
|
||||
min-width="120px"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-text type="primary" class="cursor-pointer" @click="openDetail(row)">
|
||||
{{ row.name }}
|
||||
</el-text>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.port')" prop="port" max-width="50px">
|
||||
<template #default="{ row }">
|
||||
{{ row.port }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('mcp.externalUrl')" prop="baseUrl" min-width="200px">
|
||||
<template #default="{ row }">
|
||||
{{ row.baseUrl + row.ssePath }}
|
||||
<CopyButton :content="row.baseUrl + row.ssePath" type="icon" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.table.status')" prop="status" max-width="50px">
|
||||
<template #default="{ row }">
|
||||
<el-popover
|
||||
v-if="row.status === 'error'"
|
||||
placement="bottom"
|
||||
:width="400"
|
||||
trigger="hover"
|
||||
:content="row.message"
|
||||
popper-class="max-h-[300px] overflow-auto"
|
||||
>
|
||||
<template #reference>
|
||||
<Status :key="row.status" :status="row.status"></Status>
|
||||
</template>
|
||||
</el-popover>
|
||||
<div v-else>
|
||||
<Status :key="row.status" :status="row.status"></Status>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('commons.button.log')" prop="path" min-width="90px">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
@click="openLog(row)"
|
||||
link
|
||||
type="primary"
|
||||
:disabled="row.status !== 'running' && row.status !== 'error'"
|
||||
>
|
||||
{{ $t('website.check') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="createdAt"
|
||||
:label="$t('commons.table.date')"
|
||||
:formatter="dateFormat"
|
||||
show-overflow-tooltip
|
||||
min-width="120"
|
||||
fix
|
||||
/>
|
||||
<fu-table-operations
|
||||
:ellipsis="mobile ? 0 : 10"
|
||||
:min-width="mobile ? 'auto' : 400"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
</template>
|
||||
</LayoutContent>
|
||||
<McpServerOperate ref="createRef" @close="searchWithTimeOut" />
|
||||
<OpDialog ref="opRef" @search="search" />
|
||||
<ComposeLogs ref="composeLogRef" />
|
||||
<BindDomain ref="bindDomainRef" @close="searchWithTimeOut" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { deleteMcpServer, operateMcpServer, pageMcpServer } from '@/api/modules/ai';
|
||||
import RouterMenu from '@/views/ai/mcp/index.vue';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { dateFormat } from '@/utils/util';
|
||||
import McpServerOperate from './operate/index.vue';
|
||||
import ComposeLogs from '@/components/compose-log/index.vue';
|
||||
import { GlobalStore } from '@/store';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import BindDomain from './bind/index.vue';
|
||||
const globalStore = GlobalStore();
|
||||
|
||||
const loading = ref(false);
|
||||
const createRef = ref();
|
||||
const opRef = ref();
|
||||
const composeLogRef = ref();
|
||||
const bindDomainRef = ref();
|
||||
const items = ref<AI.McpServer[]>([]);
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'mcp-server-page-size',
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
});
|
||||
const mobile = computed(() => {
|
||||
return globalStore.isMobile();
|
||||
});
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: (row: AI.McpServer) => {
|
||||
openDetail(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.start'),
|
||||
click: (row: AI.McpServer) => {
|
||||
opServer(row, 'start');
|
||||
},
|
||||
disabled: (row: AI.McpServer) => {
|
||||
return row.status === 'running';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.stop'),
|
||||
click: (row: AI.McpServer) => {
|
||||
opServer(row, 'stop');
|
||||
},
|
||||
disabled: (row: AI.McpServer) => {
|
||||
return row.status === 'stopped';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.restart'),
|
||||
click: (row: AI.McpServer) => {
|
||||
opServer(row, 'restart');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.delete'),
|
||||
click: (row: AI.McpServer) => {
|
||||
deleteServer(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const searchWithTimeOut = () => {
|
||||
search();
|
||||
setTimeout(() => {
|
||||
search();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const search = () => {
|
||||
loading.value = true;
|
||||
pageMcpServer({
|
||||
page: paginationConfig.currentPage,
|
||||
pageSize: paginationConfig.pageSize,
|
||||
name: '',
|
||||
}).then((res) => {
|
||||
items.value = res.data.items;
|
||||
paginationConfig.total = res.data.total;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const openDetail = (row: AI.McpServer) => {
|
||||
createRef.value.acceptParams(row);
|
||||
};
|
||||
|
||||
const openCreate = () => {
|
||||
const maxPort = Math.max(...items.value.map((item) => item.port));
|
||||
createRef.value.acceptParams({ port: maxPort + 1 });
|
||||
};
|
||||
|
||||
const openLog = (row: AI.McpServer) => {
|
||||
composeLogRef.value.acceptParams({ compose: row.dir + '/docker-compose.yml', resource: row.name });
|
||||
};
|
||||
|
||||
const deleteServer = async (row: AI.McpServer) => {
|
||||
try {
|
||||
opRef.value.acceptParams({
|
||||
title: i18n.global.t('commons.button.delete'),
|
||||
names: [row.name],
|
||||
msg: i18n.global.t('commons.msg.operatorHelper', [
|
||||
i18n.global.t('mcp.server'),
|
||||
i18n.global.t('commons.button.delete'),
|
||||
]),
|
||||
api: deleteMcpServer,
|
||||
params: { id: row.id },
|
||||
});
|
||||
} catch (error) {
|
||||
MsgError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const opServer = async (row: AI.McpServer, operate: string) => {
|
||||
ElMessageBox.confirm(
|
||||
i18n.global.t('mcp.operatorHelper', [i18n.global.t('mcp.server'), i18n.global.t('commons.button.' + operate)]),
|
||||
i18n.global.t('commons.button.' + operate),
|
||||
{
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
).then(async () => {
|
||||
try {
|
||||
await operateMcpServer({ id: row.id, operate: operate });
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
search();
|
||||
} catch (error) {
|
||||
MsgError(error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const openDomain = () => {
|
||||
bindDomainRef.value.acceptParams();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
search();
|
||||
});
|
||||
</script>
|
||||
251
frontend/src/views/ai/mcp/server/operate/index.vue
Normal file
251
frontend/src/views/ai/mcp/server/operate/index.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:destroy-on-close="true"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
v-model="open"
|
||||
size="50%"
|
||||
>
|
||||
<template #header>
|
||||
<DrawerHeader
|
||||
:header="$t('mcp.' + mode)"
|
||||
:hideResource="mode == 'create'"
|
||||
:resource="mcpServer.name"
|
||||
:back="handleClose"
|
||||
/>
|
||||
</template>
|
||||
<el-row v-loading="loading">
|
||||
<el-col :span="22" :offset="1">
|
||||
<el-form ref="mcpServerForm" label-position="top" :model="mcpServer" label-width="125px" :rules="rules">
|
||||
<el-form-item>
|
||||
<el-button @click="importRef.acceptParams()" type="primary" plain>
|
||||
{{ $t('mcp.importMcpJson') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('commons.table.name')" prop="name">
|
||||
<el-input v-model="mcpServer.name" :disabled="mode == 'edit'" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('runtime.runScript')" prop="command">
|
||||
<el-input
|
||||
v-model="mcpServer.command"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="$t('mcp.commandPlaceHolder')"
|
||||
></el-input>
|
||||
<span class="input-help">
|
||||
{{ $t('mcp.commandHelper', ['@modelcontextprotocol/server-github']) }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<div>
|
||||
<el-text>{{ $t('mcp.environment') }}</el-text>
|
||||
<div class="mt-1">
|
||||
<el-row :gutter="20" v-for="(env, index) in mcpServer.environments" :key="index">
|
||||
<el-col :span="8">
|
||||
<el-form-item :prop="`environments.${index}.key`" :rules="rules.key">
|
||||
<el-input v-model="env.key" :placeholder="$t('mcp.envKey')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item :prop="`environments.${index}.value`" :rules="rules.value">
|
||||
<el-input v-model="env.value" :placeholder="$t('mcp.envValue')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="removeEnv(index)" link class="mt-1">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="4">
|
||||
<el-button class="mb-2" @click="addEnv">{{ $t('commons.button.add') }}</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<Volumes :volumes="mcpServer.volumes" />
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('commons.table.port')" prop="port">
|
||||
<el-input v-model.number="mcpServer.port" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item :label="$t('app.allowPort')" prop="hostIP">
|
||||
<el-switch
|
||||
v-model="mcpServer.hostIP"
|
||||
:active-value="'0.0.0.0'"
|
||||
:inactive-value="'127.0.0.1'"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item :label="$t('mcp.baseUrl')" prop="baseUrl">
|
||||
<el-input v-model.trim="mcpServer.baseUrl"></el-input>
|
||||
<span class="input-help">
|
||||
{{ $t('mcp.baseUrlHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('app.containerName')" prop="containerName">
|
||||
<el-input v-model.trim="mcpServer.containerName"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('mcp.ssePath')" prop="ssePath">
|
||||
<el-input v-model.trim="mcpServer.ssePath"></el-input>
|
||||
<span class="input-help">
|
||||
{{ $t('mcp.ssePathHelper') }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<template #footer>
|
||||
<span>
|
||||
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit(mcpServerForm)" :disabled="loading">
|
||||
{{ $t('commons.button.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-drawer>
|
||||
<Import ref="importRef" @confirm="getImport" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { AI } from '@/api/interface/ai';
|
||||
import { createMcpServer, getMcpDomain, updateMcpServer } from '@/api/modules/ai';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import i18n from '@/lang';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import { FormInstance } from 'element-plus';
|
||||
import { ref, watch } from 'vue';
|
||||
import Volumes from '../volume/index.vue';
|
||||
import Import from '../import/index.vue';
|
||||
|
||||
const open = ref(false);
|
||||
const mode = ref('create');
|
||||
const loading = ref(false);
|
||||
const mcpServerForm = ref();
|
||||
const importRef = ref();
|
||||
const newMcpServer = () => {
|
||||
return {
|
||||
id: 0,
|
||||
name: '',
|
||||
port: 8000,
|
||||
status: '',
|
||||
message: '',
|
||||
baseUrl: '',
|
||||
ssePath: '',
|
||||
command: '',
|
||||
containerName: '',
|
||||
environments: [],
|
||||
volumes: [],
|
||||
hostIP: '127.0.0.1',
|
||||
};
|
||||
};
|
||||
const em = defineEmits(['close']);
|
||||
const mcpServer = ref(newMcpServer());
|
||||
const rules = ref({
|
||||
name: [Rules.requiredInput, Rules.appName],
|
||||
command: [Rules.requiredInput],
|
||||
port: [Rules.requiredInput, Rules.port],
|
||||
containerName: [Rules.requiredInput],
|
||||
baseUrl: [Rules.requiredInput],
|
||||
ssePath: [Rules.requiredInput],
|
||||
key: [Rules.requiredInput],
|
||||
value: [Rules.requiredInput],
|
||||
});
|
||||
|
||||
const acceptParams = async (params: AI.McpServer) => {
|
||||
mode.value = params.id ? 'edit' : 'create';
|
||||
if (mode.value == 'edit') {
|
||||
mcpServer.value = params;
|
||||
if (!mcpServer.value.environments) {
|
||||
mcpServer.value.environments = [];
|
||||
}
|
||||
if (!mcpServer.value.volumes) {
|
||||
mcpServer.value.volumes = [];
|
||||
}
|
||||
} else {
|
||||
mcpServer.value = newMcpServer();
|
||||
if (params.port) {
|
||||
mcpServer.value.port = params.port;
|
||||
}
|
||||
try {
|
||||
const res = await getMcpDomain();
|
||||
mcpServer.value.baseUrl = res.data.connUrl;
|
||||
} catch (error) {
|
||||
MsgError(error);
|
||||
}
|
||||
}
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => mcpServer.value.name,
|
||||
(newVal) => {
|
||||
if (newVal && mode.value == 'create') {
|
||||
mcpServer.value.containerName = newVal;
|
||||
mcpServer.value.ssePath = '/' + newVal;
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const addEnv = () => {
|
||||
mcpServer.value.environments.push({
|
||||
key: '',
|
||||
value: '',
|
||||
});
|
||||
};
|
||||
|
||||
const removeEnv = (index: number) => {
|
||||
mcpServer.value.environments.splice(index, 1);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
em('close', false);
|
||||
};
|
||||
|
||||
const getImport = async (data: AI.ImportMcpServer[]) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const importServer = data[0];
|
||||
mcpServer.value.name = importServer.name;
|
||||
mcpServer.value.containerName = importServer.containerName;
|
||||
mcpServer.value.ssePath = importServer.ssePath;
|
||||
mcpServer.value.command = importServer.command;
|
||||
mcpServer.value.environments = importServer.environments || [];
|
||||
};
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return;
|
||||
await formEl.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
if (mode.value == 'create') {
|
||||
await createMcpServer(mcpServer.value);
|
||||
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
|
||||
} else {
|
||||
await updateMcpServer(mcpServer.value);
|
||||
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
|
||||
}
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
MsgError(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
acceptParams,
|
||||
});
|
||||
</script>
|
||||
60
frontend/src/views/ai/mcp/server/volume/index.vue
Normal file
60
frontend/src/views/ai/mcp/server/volume/index.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="mt-2">
|
||||
<el-text>{{ $t('container.mount') }}</el-text>
|
||||
<div class="mt-2">
|
||||
<el-row :gutter="20" v-for="(volume, index) in volumes" :key="index">
|
||||
<el-col :span="8">
|
||||
<el-form-item :prop="`volumes.${index}.source`" :rules="rules.value">
|
||||
<el-input v-model="volume.source" :placeholder="$t('container.hostOption')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item :prop="`volumes.${index}.target`" :rules="rules.value">
|
||||
<el-input v-model="volume.target" :placeholder="$t('container.containerDir')" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="removeEnv(index)" link class="mt-1">
|
||||
{{ $t('commons.button.delete') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="4">
|
||||
<el-button @click="addEnv">{{ $t('commons.button.add') }}</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, reactive } from 'vue';
|
||||
import { FormRules } from 'element-plus';
|
||||
import { Rules } from '@/global/form-rules';
|
||||
import { AI } from '@/api/interface/ai';
|
||||
|
||||
const props = defineProps({
|
||||
volumes: {
|
||||
type: Array<AI.Volume>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
value: [Rules.requiredInput],
|
||||
});
|
||||
|
||||
const addEnv = () => {
|
||||
props.volumes.push({
|
||||
source: '',
|
||||
target: '',
|
||||
});
|
||||
};
|
||||
|
||||
const removeEnv = (index: number) => {
|
||||
props.volumes.splice(index, 1);
|
||||
};
|
||||
</script>
|
||||
5
go.mod
5
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
Reference in New Issue
Block a user