package workbench import ( "context" "encoding/json" "errors" "fmt" "strings" "time" "aigateway.local/core/internal/platform/cryptox" "aigateway.local/core/internal/provider" "github.com/jackc/pgx/v5" ) type MCPServer struct { ID string `json:"id"` Code string `json:"code"` Name string `json:"name"` Description string `json:"description"` Transport string `json:"transport"` EndpointURL string `json:"endpoint_url"` Status string `json:"status"` CategoryID *string `json:"category_id,omitempty"` CategoryName string `json:"category_name"` Tags []string `json:"tags"` DepartmentIDs []string `json:"department_ids"` Enabled bool `json:"enabled"` HasSecretHeaders bool `json:"has_secret_headers"` Revision int64 `json:"revision"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` EncryptedHeaders []byte `json:"-"` HeadersKEKVersion int `json:"-"` } type MCPServerInput struct { Code string `json:"code"` Name string `json:"name"` Description string `json:"description"` Transport string `json:"transport"` EndpointURL string `json:"endpoint_url"` Headers map[string]string `json:"headers,omitempty"` Status string `json:"status"` CategoryID *string `json:"category_id,omitempty"` Tags []string `json:"tags"` DepartmentIDs []string `json:"department_ids"` Enabled bool `json:"enabled"` } // MCPServerService manages registered MCP servers: lifecycle CRUD plus header // decryption for the MCP client. type MCPServerService struct { assets *Service cipher cryptox.Cipher allowPrivate bool } func NewMCPServerService(assets *Service, cipher cryptox.Cipher, allowPrivate bool) *MCPServerService { return &MCPServerService{assets: assets, cipher: cipher, allowPrivate: allowPrivate} } func (s *MCPServerService) validate(ctx context.Context, input *MCPServerInput, create bool) error { input.Code = strings.ToLower(strings.TrimSpace(input.Code)) input.Name = strings.TrimSpace(input.Name) input.Description = strings.TrimSpace(input.Description) input.Transport = strings.ToLower(strings.TrimSpace(input.Transport)) if !codeRE.MatchString(input.Code) || input.Name == "" || len(input.Name) > 128 || len(input.Description) > 4000 { return errors.New("MCP 服务器编码、名称或描述格式无效") } switch input.Transport { case "", "streamable-http": input.Transport = "streamable-http" case "sse": default: return errors.New("不支持的 MCP 传输方式") } validated, err := provider.ValidateBaseURL(ctx, input.EndpointURL, s.allowPrivate) if err != nil { return fmt.Errorf("MCP 端点校验失败: %w", err) } input.EndpointURL = validated switch input.Status { case "", "draft": input.Status = "draft" case "published", "archived": default: return errors.New("无效的资源状态") } if input.CategoryID != nil && strings.TrimSpace(*input.CategoryID) == "" { input.CategoryID = nil } var categoryErr error input.CategoryID, categoryErr = validCategoryID(ctx, s.assets.pool, input.CategoryID, "") if categoryErr != nil { return categoryErr } input.Tags, err = normalizeStrings(input.Tags, 30) if err != nil { return err } input.DepartmentIDs, err = normalizeStrings(input.DepartmentIDs, 100) if err != nil { return err } if create && input.Headers == nil { input.Headers = map[string]string{} } for key, value := range input.Headers { if strings.TrimSpace(key) == "" || len(key) > 128 || strings.ContainsAny(key, "\r\n") || len(value) > 8192 || strings.ContainsAny(value, "\r\n") { return errors.New("MCP 请求头格式无效") } } return nil } const mcpServerSelect = `SELECT m.id::text,m.code,m.name,m.description,m.transport,m.endpoint_url,m.status,m.category_id::text,coalesce(c.name,''),m.tags,m.department_ids::text[],m.enabled,octet_length(m.encrypted_headers)>0,m.revision,m.created_at,m.updated_at,m.encrypted_headers,m.headers_kek_version FROM gateway.mcp_servers m LEFT JOIN gateway.marketplace_categories c ON c.id=m.category_id` func scanMCPServer(row pgx.Row) (MCPServer, error) { var s MCPServer err := row.Scan(&s.ID, &s.Code, &s.Name, &s.Description, &s.Transport, &s.EndpointURL, &s.Status, &s.CategoryID, &s.CategoryName, &s.Tags, &s.DepartmentIDs, &s.Enabled, &s.HasSecretHeaders, &s.Revision, &s.CreatedAt, &s.UpdatedAt, &s.EncryptedHeaders, &s.HeadersKEKVersion) return s, mapNotFound(err) } func (s *MCPServerService) List(ctx context.Context) ([]MCPServer, error) { rows, err := s.assets.pool.Query(ctx, mcpServerSelect+` ORDER BY m.updated_at DESC`) if err != nil { return nil, err } defer rows.Close() items := []MCPServer{} for rows.Next() { server, err := scanMCPServer(rows) if err != nil { return nil, err } items = append(items, server) } return items, rows.Err() } func (s *MCPServerService) Get(ctx context.Context, id string) (MCPServer, error) { return scanMCPServer(s.assets.pool.QueryRow(ctx, mcpServerSelect+` WHERE m.id=$1`, id)) } func (s *MCPServerService) GetByCode(ctx context.Context, code string) (MCPServer, error) { return scanMCPServer(s.assets.pool.QueryRow(ctx, mcpServerSelect+` WHERE m.code=$1`, code)) } // GetPublishedByCode returns a published, enabled server by code. func (s *MCPServerService) GetPublishedByCode(ctx context.Context, code string) (MCPServer, error) { return scanMCPServer(s.assets.pool.QueryRow(ctx, mcpServerSelect+` WHERE m.code=$1 AND m.status='published' AND m.enabled`, code)) } func (s *MCPServerService) Save(ctx context.Context, id string, input MCPServerInput, actorID string, create bool) (MCPServer, error) { if err := s.validate(ctx, &input, create); err != nil { return MCPServer{}, err } tx, err := s.assets.pool.Begin(ctx) if err != nil { return MCPServer{}, err } defer rollback(ctx, tx) var encrypted []byte var version int if input.Headers != nil { raw, _ := json.Marshal(input.Headers) encrypted, version, err = s.cipher.Encrypt(raw) if err != nil { return MCPServer{}, err } } if create { id, err = newUUID() if err != nil { return MCPServer{}, err } _, err = tx.Exec(ctx, `INSERT INTO gateway.mcp_servers(id,code,name,description,transport,endpoint_url,encrypted_headers,headers_kek_version,status,category_id,tags,department_ids,enabled,created_by) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)`, id, input.Code, input.Name, input.Description, input.Transport, input.EndpointURL, encrypted, version, input.Status, input.CategoryID, input.Tags, input.DepartmentIDs, input.Enabled, actorID) } else if input.Headers == nil { tag, updateErr := tx.Exec(ctx, `UPDATE gateway.mcp_servers SET code=$2,name=$3,description=$4,transport=$5,endpoint_url=$6,status=$7,category_id=$8,tags=$9,department_ids=$10,enabled=$11,revision=revision+1,updated_at=clock_timestamp() WHERE id=$1`, id, input.Code, input.Name, input.Description, input.Transport, input.EndpointURL, input.Status, input.CategoryID, input.Tags, input.DepartmentIDs, input.Enabled) err = updateErr if err == nil && tag.RowsAffected() == 0 { return MCPServer{}, ErrNotFound } } else { tag, updateErr := tx.Exec(ctx, `UPDATE gateway.mcp_servers SET code=$2,name=$3,description=$4,transport=$5,endpoint_url=$6,encrypted_headers=$7,headers_kek_version=$8,status=$9,category_id=$10,tags=$11,department_ids=$12,enabled=$13,revision=revision+1,updated_at=clock_timestamp() WHERE id=$1`, id, input.Code, input.Name, input.Description, input.Transport, input.EndpointURL, encrypted, version, input.Status, input.CategoryID, input.Tags, input.DepartmentIDs, input.Enabled) err = updateErr if err == nil && tag.RowsAffected() == 0 { return MCPServer{}, ErrNotFound } } if err != nil { return MCPServer{}, err } event := "mcp_server.updated" if create { event = "mcp_server.created" } if err = emit(ctx, tx, event, "mcp_server", id, actorID, nil); err != nil { return MCPServer{}, err } if err = tx.Commit(ctx); err != nil { return MCPServer{}, err } return s.Get(ctx, id) } func (s *MCPServerService) Delete(ctx context.Context, id, actorID string) error { tx, err := s.assets.pool.Begin(ctx) if err != nil { return err } defer rollback(ctx, tx) var used bool if err = tx.QueryRow(ctx, `SELECT EXISTS( SELECT 1 FROM gateway.skills WHERE $1 = ANY(mcp_server_ids) UNION ALL SELECT 1 FROM gateway.digital_employees WHERE $1 = ANY(mcp_server_ids) UNION ALL SELECT 1 FROM gateway.marketplace_installations WHERE resource_type='mcp_server' AND resource_id=$1 )`, id).Scan(&used); err != nil { return err } if used { return ErrConflict } tag, err := tx.Exec(ctx, `DELETE FROM gateway.mcp_servers WHERE id=$1`, id) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrNotFound } if err = emit(ctx, tx, "mcp_server.deleted", "mcp_server", id, actorID, nil); err != nil { return err } return tx.Commit(ctx) } // Headers decrypts the stored request headers for an MCP server so the client // can authenticate to it. func (s *MCPServerService) Headers(server MCPServer) (map[string]string, error) { if len(server.EncryptedHeaders) == 0 { return map[string]string{}, nil } plain, err := s.cipher.Decrypt(server.EncryptedHeaders, server.HeadersKEKVersion) if err != nil { return nil, err } headers := map[string]string{} if err = json.Unmarshal(plain, &headers); err != nil { return nil, errors.New("MCP 请求头密文内容无效") } return headers, nil }