diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 61576b0ec4..408e737c19 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -171,9 +171,46 @@ jobs: RUNNER_NUM=$(sudo docker inspect $(hostname) --format '{{index .Config.Labels "com.docker.compose.container-number"}}' 2>/dev/null || true) RUNNER_NUM=${RUNNER_NUM:-1} - # Per-runner base plus per-workflow-run offset avoids port clashes when - # multiple CI jobs share the same self-hosted runner concurrently. - PORT_OFFSET=$(( (GITHUB_RUN_ID % 400) + RUNNER_NUM * 10 )) + # Per-runner seed plus per-workflow-run offset avoids most clashes when + # multiple CI jobs share the same self-hosted runner concurrently. Probe + # the final host ports too, because stale compose projects can still hold + # a deterministic port from a previous run. + PORT_BASES=(1200 1201 23817 23820 5432 5455 9000 9001 6379 6380 6601 9380 9381 9382 9384 9383 9385 80 443) + MAX_PORT_OFFSET=$((65000 - 23820)) + PORT_OFFSET=$(( (GITHUB_RUN_ID % 4000) + RUNNER_NUM * 1000 )) + OFFSET_FOUND=false + + port_offset_available() { + local offset=$1 + local base port + for base in "${PORT_BASES[@]}"; do + port=$((base + offset)) + if ss -ltnH "sport = :${port}" | grep -q .; then + return 1 + fi + done + return 0 + } + + for ATTEMPT in $(seq 0 9); do + CANDIDATE_OFFSET=$(( (PORT_OFFSET + ATTEMPT * 4000) % MAX_PORT_OFFSET )) + if [ "${CANDIDATE_OFFSET}" -lt 1000 ]; then + CANDIDATE_OFFSET=$((CANDIDATE_OFFSET + 1000)) + fi + + if port_offset_available "${CANDIDATE_OFFSET}"; then + PORT_OFFSET=${CANDIDATE_OFFSET} + OFFSET_FOUND=true + break + fi + done + + if [ "${OFFSET_FOUND}" != "true" ]; then + echo "Failed to find a free host port range for docker compose" >&2 + exit 1 + fi + + echo "Using host port offset ${PORT_OFFSET}" ES_PORT=$((1200 + PORT_OFFSET)) OS_PORT=$((1201 + PORT_OFFSET)) INFINITY_THRIFT_PORT=$((23817 + PORT_OFFSET)) diff --git a/internal/dao/mcp.go b/internal/dao/mcp.go index 49ce5b3bc7..8ac75c4be5 100644 --- a/internal/dao/mcp.go +++ b/internal/dao/mcp.go @@ -16,7 +16,13 @@ package dao -import "ragflow/internal/entity" +import ( + "errors" + + "ragflow/internal/entity" + + "gorm.io/gorm" +) // MCPServerDAO MCP server data access object. type MCPServerDAO struct{} @@ -26,6 +32,18 @@ func NewMCPServerDAO() *MCPServerDAO { return &MCPServerDAO{} } +// GetByID returns an MCP server by ID. +func (dao *MCPServerDAO) GetByID(id string) (*entity.MCPServer, error) { + var server entity.MCPServer + if err := DB.Where("id = ?", id).First(&server).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return &server, nil +} + // ExistsByNameAndTenant returns whether an MCP server name already exists for a tenant. func (dao *MCPServerDAO) ExistsByNameAndTenant(name, tenantID string) (bool, error) { var count int64 @@ -41,3 +59,12 @@ func (dao *MCPServerDAO) ExistsByNameAndTenant(name, tenantID string) (bool, err func (dao *MCPServerDAO) CreateMCPServer(server *entity.MCPServer) error { return DB.Create(server).Error } + +// DeleteMCPServer deletes an MCP server owned by a tenant. +func (dao *MCPServerDAO) DeleteMCPServer(id, tenantID string) (bool, error) { + result := DB.Where("id = ? AND tenant_id = ?", id, tenantID).Delete(&entity.MCPServer{}) + if result.Error != nil { + return false, result.Error + } + return result.RowsAffected > 0, nil +} diff --git a/internal/handler/mcp.go b/internal/handler/mcp.go index 23f459aa2c..23612b6fb6 100644 --- a/internal/handler/mcp.go +++ b/internal/handler/mcp.go @@ -63,3 +63,24 @@ func (h *MCPHandler) CreateMCPServer(c *gin.Context) { "data": result, }) } + +// DeleteMCPServer deletes an MCP server for the current user. +func (h *MCPHandler) DeleteMCPServer(c *gin.Context) { + user, errorCode, errorMessage := GetUser(c) + if errorCode != common.CodeSuccess { + jsonError(c, errorCode, errorMessage) + return + } + + result, code, err := h.mcpService.DeleteMCPServer(user.ID, c.Param("mcp_id")) + if err != nil { + jsonError(c, code, err.Error()) + return + } + + c.JSON(http.StatusOK, gin.H{ + "code": common.CodeSuccess, + "message": "success", + "data": result, + }) +} diff --git a/internal/router/router.go b/internal/router/router.go index e5b4910688..a6a8fe9e6c 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -260,6 +260,7 @@ func (r *Router) Setup(engine *gin.Engine) { mcp := v1.Group("/mcp") { mcp.POST("/servers", r.mcpHandler.CreateMCPServer) + mcp.DELETE("/servers/:mcp_id", r.mcpHandler.DeleteMCPServer) } // Skill search routes diff --git a/internal/service/mcp.go b/internal/service/mcp.go index 143144aba6..c1bc86fe69 100644 --- a/internal/service/mcp.go +++ b/internal/service/mcp.go @@ -127,6 +127,31 @@ func (s *MCPService) CreateMCPServer(tenantID string, req CreateMCPServerRequest }, common.CodeSuccess, nil } +// DeleteMCPServer deletes an MCP server owned by a tenant. +func (s *MCPService) DeleteMCPServer(tenantID, mcpID string) (bool, common.ErrorCode, error) { + server, err := s.mcpServerDAO.GetByID(mcpID) + if err != nil { + return false, common.CodeServerError, fmt.Errorf("failed to get MCP server %s: %w", mcpID, err) + } + if server == nil || server.TenantID != tenantID { + return false, common.CodeDataError, mcpServerNotFoundError(mcpID, tenantID) + } + + deleted, err := s.mcpServerDAO.DeleteMCPServer(mcpID, tenantID) + if err != nil { + return false, common.CodeServerError, err + } + if !deleted { + return false, common.CodeDataError, mcpServerNotFoundError(mcpID, tenantID) + } + + return true, common.CodeSuccess, nil +} + +func mcpServerNotFoundError(mcpID, tenantID string) error { + return fmt.Errorf("Cannot find MCP server %s for user %s", mcpID, tenantID) +} + func isValidMCPServerType(serverType string) bool { return serverType == mcpServerTypeSSE || serverType == mcpServerTypeStreamableHTTP }