feat: add Go MCP server delete API (#15262)

## What

#15240
Implementation for DELETE /api/v1/mcp/servers/:mcp_id
This commit is contained in:
Alexander Laurent
2026-05-29 01:29:55 -10:00
committed by GitHub
parent 09e91a8e61
commit faa9c5469e
5 changed files with 115 additions and 4 deletions

View File

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

View File

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

View File

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

View File

@@ -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

View File

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