2026-05-27 22:43:21 -10:00
//
// 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"
2026-06-04 23:25:09 -06:00
"context"
2026-05-27 22:43:21 -10:00
"encoding/json"
"errors"
"fmt"
2026-07-03 14:05:42 +08:00
"strconv"
2026-06-01 21:58:44 -10:00
"strings"
2026-06-01 15:37:05 -10:00
"time"
2026-05-27 22:43:21 -10:00
"ragflow/internal/common"
"ragflow/internal/dao"
"ragflow/internal/entity"
2026-06-04 23:25:09 -06:00
"ragflow/internal/utility"
2026-06-01 21:58:44 -10:00
"gorm.io/gorm"
2026-05-27 22:43:21 -10:00
)
const (
mcpServerTypeSSE = "sse"
mcpServerTypeStreamableHTTP = "streamable-http"
mcpServerNameLimit = 255
2026-06-04 23:25:09 -06:00
defaultMCPFetchTimeoutSec = 10
2026-06-01 15:37:05 -10:00
mcpServerDateFormat = "2006-01-02T15:04:05"
2026-05-27 22:43:21 -10:00
)
// 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" `
2026-07-03 14:05:42 +08:00
Timeout float64 ` json:"timeout,omitempty" `
2026-05-27 22:43:21 -10:00
}
// 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" `
}
2026-06-01 21:58:44 -10:00
// UpdateMCPServerRequest is the raw request payload for updating an MCP server.
type UpdateMCPServerRequest map [ string ] json . RawMessage
2026-06-01 15:37:05 -10:00
// MCPServerListItem is an MCP server item in the list response.
type MCPServerListItem struct {
ID string ` json:"id" `
Name string ` json:"name" `
ServerType string ` json:"server_type" `
URL string ` json:"url" `
Description * string ` json:"description" `
Variables entity . JSONMap ` json:"variables" `
CreateDate * string ` json:"create_date" `
UpdateDate * string ` json:"update_date" `
}
2026-06-18 11:09:22 +08:00
type ExportMCPServer struct {
Type string ` json:"type" `
URL string ` json:"url" `
Name string ` json:"name" `
AuthorizationToken interface { } ` json:"authorization_token" `
Tools interface { } ` json:"tools" `
}
type ExportMCPServerResponse struct {
MCPServers map [ string ] ExportMCPServer ` json:"mcpServers" `
}
2026-06-01 15:37:05 -10:00
// ListMCPServersResponse is the response payload for listing MCP servers.
type ListMCPServersResponse struct {
MCPServers [ ] * MCPServerListItem ` json:"mcp_servers" `
Total int64 ` json:"total" `
}
2026-07-03 14:05:42 +08:00
const maxMCPFetchTimeoutSec = 60
2026-05-27 22:43:21 -10:00
// CreateMCPServer creates an MCP server owned by a tenant.
func ( s * MCPService ) CreateMCPServer ( tenantID string , req CreateMCPServerRequest ) ( * CreateMCPServerResponse , common . ErrorCode , error ) {
2026-07-03 14:05:42 +08:00
if req . Timeout < 0 || req . Timeout > maxMCPFetchTimeoutSec {
return nil , common . CodeDataError , errors . New ( "Invalid timeout." )
}
2026-05-27 22:43:21 -10:00
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" )
2026-07-03 14:05:42 +08:00
tools , err := fetchMCPTools ( context . Background ( ) , req . URL , req . ServerType , headers , variables , req . Timeout )
if err != nil {
return nil , common . CodeDataError , err
}
variables [ "tools" ] = tools
2026-05-27 22:43:21 -10:00
server := & entity . MCPServer {
2026-07-07 17:22:08 +08:00
ID : utility . GenerateUUID ( ) ,
2026-05-27 22:43:21 -10:00
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
}
2026-07-03 14:05:42 +08:00
func optionalFloat64 ( req UpdateMCPServerRequest , key string , defaultValue float64 ) ( float64 , error ) {
raw , ok := req [ key ]
if ! ok || bytes . Equal ( bytes . TrimSpace ( raw ) , [ ] byte ( "null" ) ) {
return defaultValue , nil
}
var value float64
if err := json . Unmarshal ( raw , & value ) ; err == nil {
return value , nil
}
var text string
if err := json . Unmarshal ( raw , & text ) ; err != nil {
return defaultValue , fmt . Errorf ( "%s must be a number" , key )
}
value , err := strconv . ParseFloat ( strings . TrimSpace ( text ) , 64 )
if err != nil {
return defaultValue , fmt . Errorf ( "%s must be a number" , key )
}
return value , nil
}
func fetchMCPTools ( ctx context . Context , url , serverType string , headers , variables entity . JSONMap , timeoutSeconds float64 ) ( map [ string ] interface { } , error ) {
if timeoutSeconds <= 0 {
timeoutSeconds = defaultMCPFetchTimeoutSec
}
timeout := time . Duration ( timeoutSeconds * float64 ( time . Second ) )
tools , err := utility . FetchTools ( ctx , utility . FetchOptions {
URL : url ,
ServerType : serverType ,
Headers : jsonMapStringValues ( headers ) ,
Variables : jsonMapStringValues ( variables ) ,
Timeout : timeout ,
} )
if err != nil {
return nil , err
}
return toolsAsMap ( tools ) , nil
}
func jsonMapStringValues ( values entity . JSONMap ) map [ string ] string {
out := map [ string ] string { }
for key , value := range values {
if text , ok := value . ( string ) ; ok {
out [ key ] = text
}
}
return out
}
2026-06-18 11:09:22 +08:00
func ( s * MCPService ) GetMCPServer ( tenantID , mcpID string ) ( * entity . MCPServer , common . ErrorCode , error ) {
server , err := s . mcpServerDAO . GetByIDAndTenant ( mcpID , tenantID )
if err != nil {
if isMCPServerNotFound ( err ) {
return nil , common . CodeDataError , mcpServerNotFoundError ( mcpID , tenantID )
}
return nil , common . CodeServerError , fmt . Errorf ( "failed to get MCP server %s: %w" , mcpID , err )
}
if server == nil {
return nil , common . CodeDataError , mcpServerNotFoundError ( mcpID , tenantID )
}
return server , common . CodeSuccess , nil
}
func ( s * MCPService ) ExportMCPServer ( userID , mcpID string ) ( * ExportMCPServerResponse , common . ErrorCode , error ) {
server , code , err := s . GetMCPServer ( userID , mcpID )
if err != nil {
return nil , code , err
}
return newExportMCPServerResponse ( server ) , common . CodeSuccess , nil
}
func newExportMCPServerResponse ( server * entity . MCPServer ) * ExportMCPServerResponse {
vars := server . Variables
if vars == nil {
vars = entity . JSONMap { }
}
token := interface { } ( "" )
if value , ok := vars [ "authorization_token" ] ; ok {
token = value
}
tools := vars [ "tools" ]
if tools == nil {
tools = map [ string ] interface { } { }
}
return & ExportMCPServerResponse {
MCPServers : map [ string ] ExportMCPServer {
server . Name : {
Type : server . ServerType ,
URL : server . URL ,
Name : server . Name ,
AuthorizationToken : token ,
Tools : tools ,
} ,
} ,
}
}
2026-06-01 21:58:44 -10:00
// UpdateMCPServer updates an MCP server owned by a tenant.
func ( s * MCPService ) UpdateMCPServer ( tenantID , mcpID string , req UpdateMCPServerRequest ) ( * entity . MCPServer , common . ErrorCode , error ) {
server , err := s . mcpServerDAO . GetByIDAndTenant ( mcpID , tenantID )
if err != nil {
if isMCPServerNotFound ( err ) {
return nil , common . CodeDataError , mcpServerNotFoundError ( mcpID , tenantID )
}
return nil , common . CodeServerError , fmt . Errorf ( "failed to get MCP server %s: %w" , mcpID , err )
}
if server == nil {
return nil , common . CodeDataError , mcpServerNotFoundError ( mcpID , tenantID )
}
serverType := server . ServerType
serverTypeProvided := false
if value , ok , err := optionalString ( req , "server_type" ) ; err != nil {
return nil , common . CodeDataError , err
} else if ok {
serverType = value
serverTypeProvided = true
}
if serverTypeProvided && ! isValidMCPServerType ( serverType ) {
return nil , common . CodeDataError , errors . New ( "Unsupported MCP server type." )
}
serverName := server . Name
serverNameProvided := false
if value , ok , err := optionalString ( req , "name" ) ; err != nil {
return nil , common . CodeDataError , err
} else if ok {
serverName = value
serverNameProvided = true
}
if serverName != "" && len ( [ ] byte ( serverName ) ) > mcpServerNameLimit {
return nil , common . CodeDataError , fmt . Errorf ( "Invalid MCP name or length is %d which is large than 255." , len ( [ ] byte ( serverName ) ) )
}
serverURL := server . URL
serverURLProvided := false
if value , ok , err := optionalString ( req , "url" ) ; err != nil {
return nil , common . CodeDataError , err
} else if ok {
serverURL = strings . TrimSpace ( value )
if serverURL == "" {
return nil , common . CodeDataError , errors . New ( "Invalid url." )
}
serverURLProvided = true
}
if serverURL == "" {
return nil , common . CodeDataError , errors . New ( "Invalid url." )
}
headers := server . Headers
if raw , ok := req [ "headers" ] ; ok {
headers = safeJSONMap ( raw )
}
if headers == nil {
headers = entity . JSONMap { }
}
variables := server . Variables
if raw , ok := req [ "variables" ] ; ok {
variables = safeJSONMap ( raw )
}
if variables == nil {
variables = entity . JSONMap { }
}
2026-07-03 14:05:42 +08:00
existingTools := server . Variables [ "tools" ]
2026-06-01 21:58:44 -10:00
delete ( variables , "tools" )
2026-07-03 14:05:42 +08:00
needsRefresh := serverURLProvided || serverTypeProvided || headerOrVariablesChanged ( req )
if needsRefresh {
timeoutSeconds , err := optionalFloat64 ( req , "timeout" , defaultMCPFetchTimeoutSec )
if err != nil {
return nil , common . CodeDataError , err
}
tools , err := fetchMCPTools ( context . Background ( ) , serverURL , serverType , headers , variables , timeoutSeconds )
if err != nil {
return nil , common . CodeDataError , err
}
variables [ "tools" ] = tools
} else if existingTools != nil {
variables [ "tools" ] = existingTools
}
2026-06-01 21:58:44 -10:00
updates := map [ string ] interface { } {
"id" : mcpID ,
"tenant_id" : tenantID ,
"headers" : headers ,
"variables" : variables ,
}
if serverNameProvided {
updates [ "name" ] = serverName
}
if serverURLProvided {
updates [ "url" ] = serverURL
}
if serverTypeProvided {
updates [ "server_type" ] = serverType
}
if raw , ok := req [ "description" ] ; ok {
description , err := optionalNullableString ( raw , "description" )
if err != nil {
return nil , common . CodeDataError , err
}
updates [ "description" ] = description
}
if _ , err := s . mcpServerDAO . UpdateMCPServer ( mcpID , tenantID , updates ) ; err != nil {
return nil , common . CodeServerError , err
}
updatedServer , err := s . mcpServerDAO . GetByIDAndTenant ( mcpID , tenantID )
if err != nil {
if isMCPServerNotFound ( err ) {
return nil , common . CodeDataError , mcpServerNotFoundError ( mcpID , tenantID )
}
return nil , common . CodeServerError , fmt . Errorf ( "failed to fetch updated MCP server %s: %w" , mcpID , err )
}
if updatedServer == nil {
return nil , common . CodeDataError , mcpServerNotFoundError ( mcpID , tenantID )
}
return updatedServer , common . CodeSuccess , nil
}
2026-07-03 14:05:42 +08:00
func headerOrVariablesChanged ( req UpdateMCPServerRequest ) bool {
if _ , ok := req [ "headers" ] ; ok {
return true
}
if _ , ok := req [ "variables" ] ; ok {
return true
}
return false
}
2026-06-01 21:58:44 -10:00
func isMCPServerNotFound ( err error ) bool {
return errors . Is ( err , gorm . ErrRecordNotFound )
}
2026-06-01 15:37:05 -10:00
// ListMCPServers lists MCP servers owned by a tenant.
func ( s * MCPService ) ListMCPServers ( tenantID string , ids [ ] string , keywords string , page , pageSize int , orderby string , desc bool ) ( * ListMCPServersResponse , common . ErrorCode , error ) {
servers , total , err := s . mcpServerDAO . ListMCPServers ( tenantID , ids , keywords , orderby , desc )
if err != nil {
var orderbyErr * dao . InvalidMCPServerOrderByError
if errors . As ( err , & orderbyErr ) {
return nil , common . CodeExceptionError , err
}
return nil , common . CodeServerError , err
}
if servers == nil {
servers = [ ] * entity . MCPServer { }
}
servers = paginateMCPServers ( servers , page , pageSize )
items := make ( [ ] * MCPServerListItem , 0 , len ( servers ) )
for _ , server := range servers {
variables := server . Variables
if variables == nil {
variables = entity . JSONMap { }
}
items = append ( items , & MCPServerListItem {
ID : server . ID ,
Name : server . Name ,
ServerType : server . ServerType ,
URL : server . URL ,
Description : server . Description ,
Variables : variables ,
CreateDate : formatMCPServerDate ( server . CreateDate ) ,
UpdateDate : formatMCPServerDate ( server . UpdateDate ) ,
} )
}
return & ListMCPServersResponse {
MCPServers : items ,
Total : total ,
} , common . CodeSuccess , nil
}
2026-05-29 01:29:55 -10:00
// 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 )
}
2026-05-27 22:43:21 -10:00
func isValidMCPServerType ( serverType string ) bool {
return serverType == mcpServerTypeSSE || serverType == mcpServerTypeStreamableHTTP
}
2026-06-01 21:58:44 -10:00
func optionalString ( req UpdateMCPServerRequest , key string ) ( string , bool , error ) {
raw , ok := req [ key ]
if ! ok || bytes . Equal ( bytes . TrimSpace ( raw ) , [ ] byte ( "null" ) ) {
return "" , false , nil
}
var value string
if err := json . Unmarshal ( raw , & value ) ; err != nil {
return "" , true , fmt . Errorf ( "%s must be a string" , key )
}
return value , true , nil
}
func optionalNullableString ( raw json . RawMessage , key string ) ( * string , error ) {
if bytes . Equal ( bytes . TrimSpace ( raw ) , [ ] byte ( "null" ) ) {
return nil , nil
}
var value string
if err := json . Unmarshal ( raw , & value ) ; err != nil {
return nil , fmt . Errorf ( "%s must be a string" , key )
}
return & value , nil
}
2026-05-27 22:43:21 -10:00
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 )
}
2026-06-01 15:37:05 -10:00
2026-06-04 23:25:09 -06:00
// ---------- import + test (this PR's additions) ----------
// 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 = errors . New ( "unsupported MCP server type" )
ErrMCPInvalidName = errors . New ( "invalid MCP name" )
2026-07-03 14:05:07 +08:00
ErrMCPInvalidURL = errors . New ( "invalid url" )
ErrMCPTestFailed = errors . New ( "MCP test failed" )
2026-06-04 23:25:09 -06:00
)
// ImportResult is a single per-server outcome in the bulk import response,
// matching the shape returned by Python's import_multiple.
type ImportResult struct {
Server string ` json:"server" `
Success bool ` json:"success" `
Action string ` json:"action,omitempty" `
ID string ` json:"id,omitempty" `
NewName string ` json:"new_name,omitempty" `
Message string ` json:"message,omitempty" `
}
2026-07-03 14:05:07 +08:00
// ImportServers bulk-imports MCP servers from a {"mcpServers": {name: config}} map.
2026-06-04 23:25:09 -06:00
func ( s * MCPService ) ImportServers ( tenantID string , servers map [ string ] map [ string ] interface { } , timeoutSeconds float64 ) ( [ ] ImportResult , error ) {
if timeoutSeconds <= 0 {
timeoutSeconds = defaultMCPFetchTimeoutSec
}
timeout := time . Duration ( timeoutSeconds * float64 ( time . Second ) )
results := make ( [ ] ImportResult , 0 , len ( servers ) )
for serverName , config := range servers {
url , hasURL := config [ "url" ] . ( string )
stype , hasType := config [ "type" ] . ( string )
if ! hasType || ! hasURL {
results = append ( results , ImportResult { Server : serverName , Success : false , Message : "Missing required fields (type or url)" } )
continue
}
if serverName == "" || len ( [ ] byte ( serverName ) ) > mcpServerNameLimit {
results = append ( results , ImportResult { Server : serverName , Success : false , Message : fmt . Sprintf ( "Invalid MCP name or length is %d which is large than 255." , len ( serverName ) ) } )
continue
}
if ! isValidMCPServerType ( stype ) {
results = append ( results , ImportResult { Server : serverName , Success : false , Message : "Unsupported MCP server type." } )
continue
}
baseName := serverName
newName , err := s . nextAvailableMCPName ( baseName , tenantID )
if err != nil {
return nil , err
}
variables := map [ string ] interface { } { }
stringVars := map [ string ] string { }
for k , v := range config {
if k == "type" || k == "url" || k == "headers" {
continue
}
variables [ k ] = v
if sv , ok := v . ( string ) ; ok {
stringVars [ k ] = sv
}
}
delete ( variables , "tools" )
delete ( stringVars , "tools" )
// Headers can be provided either as a top-level "headers" map
// (preferred — matches the Python import shape) or as a flat
// "authorization_token" string at the entry root. Both go to the
// MCP client for tool discovery and to the persisted record so
// configs that depend on custom auth headers survive the round
// trip.
headers := map [ string ] string { }
headerVals := map [ string ] interface { } { }
if rawHeaders , ok := config [ "headers" ] . ( map [ string ] interface { } ) ; ok {
for k , v := range rawHeaders {
if sv , ok := v . ( string ) ; ok {
headers [ k ] = sv
}
headerVals [ k ] = v
}
}
2026-07-03 14:05:07 +08:00
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}"
2026-06-04 23:25:09 -06:00
}
2026-07-03 14:05:07 +08:00
if _ , exists := headerVals [ "Authorization" ] ; ! exists {
headerVals [ "Authorization" ] = "Bearer ${authorization_token}"
2026-06-04 23:25:09 -06:00
}
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , timeout )
2026-06-10 20:38:43 +08:00
tools , fetchErr := utility . FetchTools ( ctx , utility . FetchOptions {
2026-06-04 23:25:09 -06:00
URL : url ,
ServerType : stype ,
Headers : headers ,
Variables : stringVars ,
Timeout : timeout ,
} )
cancel ( )
if fetchErr != nil {
results = append ( results , ImportResult { Server : baseName , Success : false , Message : fetchErr . Error ( ) } )
continue
}
variables [ "tools" ] = toolsAsMap ( tools )
server := & entity . MCPServer {
2026-07-07 17:22:08 +08:00
ID : utility . GenerateUUID ( ) ,
2026-06-04 23:25:09 -06:00
TenantID : tenantID ,
Name : newName ,
URL : url ,
ServerType : stype ,
Variables : entity . JSONMap ( variables ) ,
Headers : entity . JSONMap ( headerVals ) ,
}
if err := s . mcpServerDAO . CreateMCPServer ( server ) ; err != nil {
results = append ( results , ImportResult { Server : serverName , Success : false , Message : "Failed to create MCP server." } )
continue
}
result := ImportResult { Server : serverName , Success : true , Action : "created" , ID : server . ID , NewName : newName }
if newName != baseName {
result . Message = fmt . Sprintf ( "Renamed from '%s' to '%s' avoid duplication" , baseName , newName )
}
results = append ( results , result )
}
return results , nil
}
func ( s * MCPService ) nextAvailableMCPName ( base , tenantID string ) ( string , error ) {
name := base
counter := 0
for {
exists , err := s . mcpServerDAO . ExistsByNameAndTenant ( name , tenantID )
if err != nil {
return "" , err
}
if ! exists {
return name , nil
}
name = fmt . Sprintf ( "%s_%d" , base , counter )
counter ++
}
}
// TestServerRequest is the body of POST /mcp/servers/:mcp_id/test. The mcp_id
// from the URL path is threaded through to the connect call for log
// correlation; the connection itself is opened from the request body so the
// user can preview unsaved edits — matching Python's test_mcp.
type TestServerRequest struct {
URL string ` json:"url" `
ServerType string ` json:"server_type" `
Headers map [ string ] interface { } ` json:"headers,omitempty" `
Variables map [ string ] interface { } ` json:"variables,omitempty" `
Timeout float64 ` json:"timeout,omitempty" `
}
2026-07-03 14:05:07 +08:00
// TestServer opens a live MCP session and returns the tools the server advertises.
2026-06-04 23:25:09 -06:00
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 )
}
if ! isValidMCPServerType ( req . ServerType ) {
return nil , ErrMCPInvalidType
}
// Run the SSRF guard up front so URL-shape failures (disallowed
// scheme, missing host, non-public address) surface as
// ErrMCPInvalidURL data errors instead of being swallowed inside the
// generic FetchTools error and re-classified by the handler as a 500.
// FetchTools repeats the check internally; the second call is cheap.
if _ , _ , err := utility . AssertURLSafe ( req . URL ) ; err != nil {
return nil , fmt . Errorf ( "%w: %s" , ErrMCPInvalidURL , err . Error ( ) )
}
timeoutSec := req . Timeout
if timeoutSec <= 0 {
timeoutSec = defaultMCPFetchTimeoutSec
}
timeout := time . Duration ( timeoutSec * float64 ( time . Second ) )
headers := map [ string ] string { }
for k , v := range req . Headers {
if sv , ok := v . ( string ) ; ok {
headers [ k ] = sv
}
}
vars := map [ string ] string { }
for k , v := range req . Variables {
if sv , ok := v . ( string ) ; ok {
vars [ k ] = sv
}
}
ctx , cancel := context . WithTimeout ( context . Background ( ) , timeout )
defer cancel ( )
2026-06-10 20:38:43 +08:00
tools , err := utility . FetchTools ( ctx , utility . FetchOptions {
2026-06-04 23:25:09 -06:00
URL : req . URL ,
ServerType : req . ServerType ,
Headers : headers ,
Variables : vars ,
Timeout : timeout ,
} )
if err != nil {
return nil , fmt . Errorf ( "%w: Test MCP error (id=%s): %v" , ErrMCPTestFailed , mcpID , err )
}
out := make ( [ ] map [ string ] interface { } , 0 , len ( tools ) )
for _ , t := range tools {
raw := t . Raw
if raw == nil {
raw = map [ string ] interface { } { "name" : t . Name }
if t . Description != "" {
raw [ "description" ] = t . Description
}
if t . InputSchema != nil {
raw [ "inputSchema" ] = t . InputSchema
}
}
raw [ "enabled" ] = true
out = append ( out , raw )
}
return out , nil
}
2026-06-10 20:38:43 +08:00
func toolsAsMap ( tools [ ] utility . Tool ) map [ string ] interface { } {
2026-06-04 23:25:09 -06:00
m := map [ string ] interface { } { }
for _ , t := range tools {
if t . Raw != nil {
m [ t . Name ] = t . Raw
continue
}
entry := map [ string ] interface { } { "name" : t . Name }
if t . Description != "" {
entry [ "description" ] = t . Description
}
if t . InputSchema != nil {
entry [ "inputSchema" ] = t . InputSchema
}
m [ t . Name ] = entry
}
return m
}
2026-06-01 15:37:05 -10:00
func formatMCPServerDate ( date * time . Time ) * string {
if date == nil {
return nil
}
formatted := date . Format ( mcpServerDateFormat )
return & formatted
}
func paginateMCPServers ( servers [ ] * entity . MCPServer , page , pageSize int ) [ ] * entity . MCPServer {
if page == 0 || pageSize == 0 {
return servers
}
start := ( page - 1 ) * pageSize
stop := page * pageSize
return sliceMCPServers ( servers , start , stop )
}
func sliceMCPServers ( servers [ ] * entity . MCPServer , start , stop int ) [ ] * entity . MCPServer {
length := len ( servers )
start = normalizeMCPServerSliceIndex ( start , length )
stop = normalizeMCPServerSliceIndex ( stop , length )
if stop < start {
return [ ] * entity . MCPServer { }
}
return servers [ start : stop ]
}
func normalizeMCPServerSliceIndex ( index , length int ) int {
if index < 0 {
index += length
}
if index < 0 {
return 0
}
if index > length {
return length
}
return index
}