From 32d5bf9791518e5e26547a5752f77aec7d7a1fc4 Mon Sep 17 00:00:00 2001 From: Alexander Laurent <57456290+Helios531@users.noreply.github.com> Date: Wed, 27 May 2026 22:43:21 -1000 Subject: [PATCH] feat: add Go MCP server create API (#15260) ## What Implementation for POST /api/v1/mcp/servers #15240 --- cmd/server_main.go | 4 +- internal/dao/mcp.go | 43 +++++++++++ internal/handler/mcp.go | 65 ++++++++++++++++ internal/router/router.go | 8 ++ internal/service/mcp.go | 154 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 internal/dao/mcp.go create mode 100644 internal/handler/mcp.go create mode 100644 internal/service/mcp.go diff --git a/cmd/server_main.go b/cmd/server_main.go index e7d629216d..79e9d90cf4 100644 --- a/cmd/server_main.go +++ b/cmd/server_main.go @@ -195,6 +195,7 @@ func startServer(config *server.Config) { searchService := service.NewSearchService() fileService := service.NewFileService() memoryService := service.NewMemoryService() + mcpService := service.NewMCPService() modelProviderService := service.NewModelProviderService() // Initialize doc engine for skill search @@ -216,11 +217,12 @@ func startServer(config *server.Config) { searchHandler := handler.NewSearchHandler(searchService, userService) fileHandler := handler.NewFileHandler(fileService, userService) memoryHandler := handler.NewMemoryHandler(memoryService) + mcpHandler := handler.NewMCPHandler(mcpService) skillSearchHandler := handler.NewSkillSearchHandler(docEngine) providerHandler := handler.NewProviderHandler(userService, modelProviderService) // Initialize router - r := router.NewRouter(authHandler, userHandler, tenantHandler, documentHandler, datasetsHandler, systemHandler, knowledgebaseHandler, chunkHandler, llmHandler, chatHandler, chatSessionHandler, connectorHandler, searchHandler, fileHandler, memoryHandler, skillSearchHandler, providerHandler) + r := router.NewRouter(authHandler, userHandler, tenantHandler, documentHandler, datasetsHandler, systemHandler, knowledgebaseHandler, chunkHandler, llmHandler, chatHandler, chatSessionHandler, connectorHandler, searchHandler, fileHandler, memoryHandler, mcpHandler, skillSearchHandler, providerHandler) // Create Gin engine ginEngine := gin.New() diff --git a/internal/dao/mcp.go b/internal/dao/mcp.go new file mode 100644 index 0000000000..49ce5b3bc7 --- /dev/null +++ b/internal/dao/mcp.go @@ -0,0 +1,43 @@ +// +// Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package dao + +import "ragflow/internal/entity" + +// MCPServerDAO MCP server data access object. +type MCPServerDAO struct{} + +// NewMCPServerDAO creates an MCP server DAO. +func NewMCPServerDAO() *MCPServerDAO { + return &MCPServerDAO{} +} + +// ExistsByNameAndTenant returns whether an MCP server name already exists for a tenant. +func (dao *MCPServerDAO) ExistsByNameAndTenant(name, tenantID string) (bool, error) { + var count int64 + if err := DB.Model(&entity.MCPServer{}). + Where("name = ? AND tenant_id = ?", name, tenantID). + Count(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +// CreateMCPServer creates an MCP server. +func (dao *MCPServerDAO) CreateMCPServer(server *entity.MCPServer) error { + return DB.Create(server).Error +} diff --git a/internal/handler/mcp.go b/internal/handler/mcp.go new file mode 100644 index 0000000000..23f459aa2c --- /dev/null +++ b/internal/handler/mcp.go @@ -0,0 +1,65 @@ +// +// Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package handler + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "ragflow/internal/common" + "ragflow/internal/service" +) + +// MCPHandler handles MCP server requests. +type MCPHandler struct { + mcpService *service.MCPService +} + +// NewMCPHandler creates an MCP handler. +func NewMCPHandler(mcpService *service.MCPService) *MCPHandler { + return &MCPHandler{ + mcpService: mcpService, + } +} + +// CreateMCPServer creates an MCP server for the current user. +func (h *MCPHandler) CreateMCPServer(c *gin.Context) { + user, errorCode, errorMessage := GetUser(c) + if errorCode != common.CodeSuccess { + jsonError(c, errorCode, errorMessage) + return + } + + var req service.CreateMCPServerRequest + if err := c.ShouldBindJSON(&req); err != nil { + jsonError(c, common.CodeDataError, err.Error()) + return + } + + result, code, err := h.mcpService.CreateMCPServer(user.ID, req) + 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 191dc8dc7f..4b5d2f21a4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -38,6 +38,7 @@ type Router struct { searchHandler *handler.SearchHandler fileHandler *handler.FileHandler memoryHandler *handler.MemoryHandler + mcpHandler *handler.MCPHandler skillSearchHandler *handler.SkillSearchHandler providerHandler *handler.ProviderHandler } @@ -59,6 +60,7 @@ func NewRouter( searchHandler *handler.SearchHandler, fileHandler *handler.FileHandler, memoryHandler *handler.MemoryHandler, + mcpHandler *handler.MCPHandler, skillSearchHandler *handler.SkillSearchHandler, providerHandler *handler.ProviderHandler, ) *Router { @@ -78,6 +80,7 @@ func NewRouter( searchHandler: searchHandler, fileHandler: fileHandler, memoryHandler: memoryHandler, + mcpHandler: mcpHandler, skillSearchHandler: skillSearchHandler, providerHandler: providerHandler, } @@ -247,6 +250,11 @@ func (r *Router) Setup(engine *gin.Engine) { // message.GET("/:memory_id/:message_id/content", r.memoryHandler.GetMessageContent) // } + mcp := v1.Group("/mcp") + { + mcp.POST("/servers", r.mcpHandler.CreateMCPServer) + } + // Skill search routes skills := v1.Group("/skills") { diff --git a/internal/service/mcp.go b/internal/service/mcp.go new file mode 100644 index 0000000000..143144aba6 --- /dev/null +++ b/internal/service/mcp.go @@ -0,0 +1,154 @@ +// +// Copyright 2026 The InfiniFlow Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package service + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + + "ragflow/internal/common" + "ragflow/internal/dao" + "ragflow/internal/entity" +) + +const ( + mcpServerTypeSSE = "sse" + mcpServerTypeStreamableHTTP = "streamable-http" + mcpServerNameLimit = 255 +) + +// MCPService handles MCP server operations. +type MCPService struct { + mcpServerDAO *dao.MCPServerDAO + tenantDAO *dao.TenantDAO +} + +// NewMCPService creates an MCP service. +func NewMCPService() *MCPService { + return &MCPService{ + mcpServerDAO: dao.NewMCPServerDAO(), + tenantDAO: dao.NewTenantDAO(), + } +} + +// CreateMCPServerRequest is the request payload for creating an MCP server. +type CreateMCPServerRequest struct { + Name string `json:"name"` + URL string `json:"url"` + ServerType string `json:"server_type"` + Description *string `json:"description,omitempty"` + Variables json.RawMessage `json:"variables,omitempty"` + Headers json.RawMessage `json:"headers,omitempty"` +} + +// CreateMCPServerResponse is the response payload for creating an MCP server. +type CreateMCPServerResponse struct { + ID string `json:"id"` + TenantID string `json:"tenant_id"` + Name string `json:"name"` + URL string `json:"url"` + ServerType string `json:"server_type"` + Description *string `json:"description,omitempty"` + Variables entity.JSONMap `json:"variables"` + Headers entity.JSONMap `json:"headers"` +} + +// CreateMCPServer creates an MCP server owned by a tenant. +func (s *MCPService) CreateMCPServer(tenantID string, req CreateMCPServerRequest) (*CreateMCPServerResponse, common.ErrorCode, error) { + if !isValidMCPServerType(req.ServerType) { + return nil, common.CodeDataError, errors.New("Unsupported MCP server type.") + } + + if req.Name == "" || len([]byte(req.Name)) > mcpServerNameLimit { + return nil, common.CodeDataError, fmt.Errorf("Invalid MCP name or length is %d which is large than 255.", len([]byte(req.Name))) + } + + exists, err := s.mcpServerDAO.ExistsByNameAndTenant(req.Name, tenantID) + if err != nil { + return nil, common.CodeServerError, err + } + if exists { + return nil, common.CodeDataError, errors.New("Duplicated MCP server name.") + } + + if req.URL == "" { + return nil, common.CodeDataError, errors.New("Invalid url.") + } + + if _, err := s.tenantDAO.GetByID(tenantID); err != nil { + return nil, common.CodeDataError, errors.New("Tenant not found.") + } + + headers := safeJSONMap(req.Headers) + variables := safeJSONMap(req.Variables) + delete(variables, "tools") + variables["tools"] = map[string]interface{}{} + + server := &entity.MCPServer{ + ID: common.GenerateUUID(), + Name: req.Name, + TenantID: tenantID, + URL: req.URL, + ServerType: req.ServerType, + Description: req.Description, + Variables: variables, + Headers: headers, + } + + if err := s.mcpServerDAO.CreateMCPServer(server); err != nil { + return nil, common.CodeDataError, errors.New("Failed to create MCP server.") + } + + return &CreateMCPServerResponse{ + ID: server.ID, + TenantID: server.TenantID, + Name: server.Name, + URL: server.URL, + ServerType: server.ServerType, + Description: server.Description, + Variables: server.Variables, + Headers: server.Headers, + }, common.CodeSuccess, nil +} + +func isValidMCPServerType(serverType string) bool { + return serverType == mcpServerTypeSSE || serverType == mcpServerTypeStreamableHTTP +} + +func safeJSONMap(raw json.RawMessage) entity.JSONMap { + raw = bytes.TrimSpace(raw) + if len(raw) == 0 || bytes.Equal(raw, []byte("null")) { + return entity.JSONMap{} + } + + var value map[string]interface{} + if err := json.Unmarshal(raw, &value); err == nil && value != nil { + return entity.JSONMap(value) + } + + var textValue string + if err := json.Unmarshal(raw, &textValue); err != nil || textValue == "" { + return entity.JSONMap{} + } + + if err := json.Unmarshal([]byte(textValue), &value); err != nil || value == nil { + return entity.JSONMap{} + } + return entity.JSONMap(value) +}