From b2e4740acd51b0c81942a64bae91b44013fc8247 Mon Sep 17 00:00:00 2001 From: Haruko386 Date: Fri, 3 Jul 2026 14:05:07 +0800 Subject: [PATCH] fix: unable to import mcp from local (#16590) ### Summary As title --- internal/handler/mcp.go | 19 ++----------------- internal/service/mcp.go | 38 ++++++++++++-------------------------- 2 files changed, 14 insertions(+), 43 deletions(-) diff --git a/internal/handler/mcp.go b/internal/handler/mcp.go index 18901a3c11..b78f320e2a 100644 --- a/internal/handler/mcp.go +++ b/internal/handler/mcp.go @@ -303,12 +303,7 @@ type ImportMCPRequest struct { Timeout float64 `json:"timeout,omitempty"` } -// ImportMCPServers bulk-imports MCP servers from a JSON config, fetching the -// remote tool list for each entry and persisting it under variables.tools. -// Mirrors Python's import_multiple, including the same distinction between -// "mcpServers key missing" (101 ARGUMENT_ERROR) and "mcpServers key -// present but empty" (102 DATA_ERROR). -// +// ImportMCPServers bulk-imports MCP servers from a JSON config // @Summary Import MCP Servers // @Tags mcp // @Accept json @@ -322,10 +317,6 @@ func (h *MCPHandler) ImportMCPServers(c *gin.Context) { return } - // Read the raw body so we can distinguish "key absent" from "key - // present but empty" — the Python @validate_request("mcpServers") - // decorator returns RetCode.ARGUMENT_ERROR for the former, while the - // handler body returns RetCode.DATA_ERROR for the latter. body, err := io.ReadAll(c.Request.Body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": common.CodeBadRequest, "data": nil, "message": "Invalid request body: " + err.Error()}) @@ -378,11 +369,7 @@ func (h *MCPHandler) ImportMCPServers(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": common.CodeSuccess, "data": gin.H{"results": results}, "message": "success"}) } -// TestMCPServer opens a live MCP session and returns the tools the server -// advertises. The mcp_id path parameter identifies the stored record the -// user is trying to validate; the actual connection uses the request body -// so the user can preview unsaved edits — matching Python's test_mcp. -// +// TestMCPServer opens a live MCP session and returns the tools the server advertises. // @Summary Test MCP Server // @Tags mcp // @Accept json @@ -409,8 +396,6 @@ func (h *MCPHandler) TestMCPServer(c *gin.Context) { return } - // Mirror Python's @validate_request("url", "server_type"): missing - // required fields → code 101 (ARGUMENT_ERROR), not code 102. var missingFields []string if req.URL == "" { missingFields = append(missingFields, "url") diff --git a/internal/service/mcp.go b/internal/service/mcp.go index 65f48c0c87..77a51690ab 100644 --- a/internal/service/mcp.go +++ b/internal/service/mcp.go @@ -451,16 +451,10 @@ func safeJSONMap(raw json.RawMessage) entity.JSONMap { // Sentinel errors mapped by the handler to Python's response codes for the // import / test endpoints. Per-server CRUD errors stay inside CreateMCPServer. var ( - // ErrMCPInvalidType mirrors Python's "Unsupported MCP server type.". ErrMCPInvalidType = errors.New("unsupported MCP server type") - // ErrMCPInvalidName mirrors Python's invalid-name/length error. ErrMCPInvalidName = errors.New("invalid MCP name") - // ErrMCPInvalidURL mirrors Python's "Invalid url.". - ErrMCPInvalidURL = errors.New("invalid url") - // ErrMCPTestFailed is returned by TestServer when the live connection or - // tool-list fetch fails. The handler maps this to code 102 (DATA_ERROR), - // matching Python's test_mcp which never returns HTTP 500 for fetch errors. - ErrMCPTestFailed = errors.New("MCP test failed") + ErrMCPInvalidURL = errors.New("invalid url") + ErrMCPTestFailed = errors.New("MCP test failed") ) // ImportResult is a single per-server outcome in the bulk import response, @@ -474,14 +468,7 @@ type ImportResult struct { Message string `json:"message,omitempty"` } -// ImportServers bulk-imports MCP servers from a {"mcpServers": {name: config}} -// map. For each entry: validate type and URL, de-duplicate the name with a -// "_N" suffix, fetch the remote tool list via mcpclient (SSRF-guarded), and -// persist the server with tools stored under variables.tools. Mirrors -// Python's import_multiple. -// -// timeoutSeconds controls how long each tool-fetch call waits; <=0 falls back -// to the Python default of 10 s. +// ImportServers bulk-imports MCP servers from a {"mcpServers": {name: config}} map. func (s *MCPService) ImportServers(tenantID string, servers map[string]map[string]interface{}, timeoutSeconds float64) ([]ImportResult, error) { if timeoutSeconds <= 0 { timeoutSeconds = defaultMCPFetchTimeoutSec @@ -541,12 +528,15 @@ func (s *MCPService) ImportServers(tenantID string, servers map[string]map[strin headerVals[k] = v } } - if token, ok := config["authorization_token"].(string); ok { - if _, exists := headers["authorization_token"]; !exists { - headers["authorization_token"] = token + if token, ok := config["authorization_token"].(string); ok && strings.TrimSpace(token) != "" { + variables["authorization_token"] = token + stringVars["authorization_token"] = token + + if _, exists := headers["Authorization"]; !exists { + headers["Authorization"] = "Bearer ${authorization_token}" } - if _, exists := headerVals["authorization_token"]; !exists { - headerVals["authorization_token"] = token + if _, exists := headerVals["Authorization"]; !exists { + headerVals["Authorization"] = "Bearer ${authorization_token}" } } @@ -616,9 +606,7 @@ type TestServerRequest struct { Timeout float64 `json:"timeout,omitempty"` } -// TestServer opens a live MCP session and returns the tools the server -// advertises. Mirrors Python's test_mcp. mcpID is used for log correlation -// only. +// TestServer opens a live MCP session and returns the tools the server advertises. func (s *MCPService) TestServer(mcpID string, req *TestServerRequest) ([]map[string]interface{}, error) { if req == nil || req.URL == "" { return nil, fmt.Errorf("%w: Invalid MCP url.", ErrMCPInvalidURL) @@ -686,8 +674,6 @@ func (s *MCPService) TestServer(mcpID string, req *TestServerRequest) ([]map[str return out, nil } -// toolsAsMap mirrors Python's `{tool["name"]: tool ...}` shape used when -// persisting variables.tools. func toolsAsMap(tools []utility.Tool) map[string]interface{} { m := map[string]interface{}{} for _, t := range tools {