internal/mcpgateway/store_virtual.go
Rama main · réplica de lectura del repositorio original.
package mcpgateway
import (
"context"
"crypto/rand"
"slices"
"strconv"
"strings"
"time"
"github.com/enterpilot/gomodel/config"
)
// VirtualStore persists admin-managed virtual servers. The SQL and MongoDB
// stores implement it next to Store.
type VirtualStore interface {
ListVirtual(ctx context.Context) ([]ManagedVirtualServer, error)
GetVirtual(ctx context.Context, name string) (*ManagedVirtualServer, error)
UpsertVirtual(ctx context.Context, virtual ManagedVirtualServer) error
// UpdateVirtual rewrites an existing row and returns ErrNotFound when the
// row is gone, so an edit racing a delete cannot recreate it.
UpdateVirtual(ctx context.Context, virtual ManagedVirtualServer) error
// DeleteVirtual removes a row and returns it as it was deleted, in the
// same operation, so a caller learns which incarnation it removed even
// when another instance replaced the row just before.
DeleteVirtual(ctx context.Context, name string) (*ManagedVirtualServer, error)
}
// ManagedVirtualServer is one admin-managed virtual server row.
type ManagedVirtualServer struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Servers []string `json:"servers"`
ToolDiscovery string `json:"tool_discovery,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// Generation is a random token the store sets when it creates the row
// and keeps across edits, so it tells one creation of a name from the
// next. Rows created before it have none; see incarnation.
Generation string `json:"-"`
}
// Validate applies the declarative-config rules and normalizes the row in
// place. Member existence and name clashes depend on the running gateway, so
// Service.UpsertVirtual checks them.
func (v *ManagedVirtualServer) Validate() error {
name := strings.ToLower(strings.TrimSpace(v.Name))
if err := config.ValidateMCPVirtualServerName(name); err != nil {
return err
}
cfg := v.config()
if err := config.NormalizeMCPVirtualServer(&cfg); err != nil {
return err
}
v.Name = name
v.Description = cfg.Description
v.Servers = cfg.Servers
v.ToolDiscovery = cfg.ToolDiscovery
return nil
}
func (v ManagedVirtualServer) config() config.MCPVirtualServerConfig {
return config.MCPVirtualServerConfig{
Description: v.Description,
Servers: slices.Clone(v.Servers),
ToolDiscovery: v.ToolDiscovery,
}
}
// Spec converts the row into a runtime spec.
func (v ManagedVirtualServer) Spec() VirtualServerSpec {
spec := VirtualFromConfig(v.Name, v.config())
spec.Managed = false
spec.Incarnation = v.incarnation()
return spec
}
// incarnation identifies this creation of the row: its Generation, or for a
// row created before generations its creation time. Those times are whole
// seconds in SQL, but every row created since has a Generation, so two
// incarnations of one name never share one.
func (v ManagedVirtualServer) incarnation() string {
if v.Generation != "" {
return v.Generation
}
if v.CreatedAt.IsZero() {
return ""
}
return "created-" + strconv.FormatInt(v.CreatedAt.UnixNano(), 10)
}
// stampVirtualUpsert sets timestamps, and the generation an insert stores;
// an update keeps the row's own.
func stampVirtualUpsert(virtual *ManagedVirtualServer) {
now := time.Now().UTC()
if virtual.CreatedAt.IsZero() {
virtual.CreatedAt = now
}
virtual.UpdatedAt = now
if virtual.Generation == "" {
virtual.Generation = rand.Text()
}
}