package mcp import ( "fmt" "math" "slices" "strconv" "strings" "github.com/sophotechlabs/spinoza/internal/api" ) const ( argResource = "resource" argName = "name" argNamespace = "namespace" argGroup = "group" argLimit = "limit" argQuery = "query" argAction = "action" argYAML = "yaml" argReplicas = "replicas" argRevision = "revision" argEngine = "engine" keyError = "error" ) type arguments map[string]any func (a arguments) text(key string) string { raw, held := a[key] if !held { return "" } switch value := raw.(type) { case string: return value case float64: return strconv.FormatFloat(value, 'f', -1, 64) case bool: return strconv.FormatBool(value) default: return "" } } func (a arguments) required(key string) (string, error) { found := a.text(key) if strings.TrimSpace(found) == "" { return "", fmt.Errorf("%s is required", key) } return found, nil } func (a arguments) number(key string, fallback int) int { raw, held := a[key] if !held { return fallback } switch value := raw.(type) { case float64: return int(value) case string: parsed, err := strconv.Atoi(value) if err != nil { return fallback } return parsed default: return fallback } } func (a arguments) limit(key string, fallback int) int { found := a.number(key, fallback) if found <= 0 { return fallback } if found > maxRows { return maxRows } return found } func (a arguments) count(key string) (int64, error) { raw, held := a[key] if !held { return 0, fmt.Errorf("%s is required", key) } switch value := raw.(type) { case float64: if value != math.Trunc(value) { return 0, fmt.Errorf("%s must be a whole number, not %v", key, value) } return int64(value), nil case string: parsed, err := strconv.ParseInt(value, 10, 64) if err != nil { return 0, fmt.Errorf("%s must be a whole number, not %q", key, value) } return parsed, nil default: return 0, fmt.Errorf("%s must be a whole number", key) } } func (a arguments) flag(key string) bool { raw, held := a[key] if !held { return false } switch value := raw.(type) { case bool: return value case string: return value == "true" default: return false } } func (a arguments) oneOf(key string, allowed ...string) (string, error) { found, err := a.required(key) if err != nil { return "", err } if slices.Contains(allowed, found) { return found, nil } return "", fmt.Errorf("%s must be one of %s", key, strings.Join(allowed, ", ")) } func (a arguments) ref(catalog api.ResourceCatalog) (api.ObjectRef, error) { resource, err := a.required(argResource) if err != nil { return api.ObjectRef{}, err } name, err := a.required(argName) if err != nil { return api.ObjectRef{}, err } found, err := resolve(catalog, resource, a.text(argGroup)) if err != nil { return api.ObjectRef{}, err } return api.ObjectRef{ Group: found.Group, Version: found.Version, Resource: found.Resource, Namespace: a.text(argNamespace), Name: name, }, nil } func (a arguments) refIn(catalog api.ResourceCatalog) (api.ObjectRef, error) { ref, err := a.ref(catalog) if err != nil { return api.ObjectRef{}, err } if ref.Namespace == "" { return api.ObjectRef{}, fmt.Errorf("%s is required", argNamespace) } return ref, nil } func (a arguments) kind(catalog api.ResourceCatalog) (api.ObjectRef, error) { resource, err := a.required(argResource) if err != nil { return api.ObjectRef{}, err } found, err := resolve(catalog, resource, a.text(argGroup)) if err != nil { return api.ObjectRef{}, err } return api.ObjectRef{ Group: found.Group, Version: found.Version, Resource: found.Resource, Namespace: a.text(argNamespace), }, nil } func resolve(catalog api.ResourceCatalog, resource, group string) (api.ResourceDescriptor, error) { matches := []api.ResourceDescriptor{} wanted := strings.ToLower(resource) for _, category := range catalog.Categories { for _, desc := range category.Resources { if !names(desc, wanted) { continue } if group != "" && desc.Group != group { continue } matches = append(matches, desc) } } if len(matches) == 0 { return api.ResourceDescriptor{}, fmt.Errorf("this cluster reports no resource type called %q", resource) } if len(matches) > 1 { return api.ResourceDescriptor{}, fmt.Errorf("%q is served by %s; name the group", resource, groupList(matches)) } return matches[0], nil } func names(desc api.ResourceDescriptor, wanted string) bool { if strings.EqualFold(desc.Resource, wanted) { return true } return strings.EqualFold(desc.Kind, wanted) } func groupList(matches []api.ResourceDescriptor) string { out := make([]string, 0, len(matches)) for _, desc := range matches { label := desc.Group if label == "" { label = "the core group" } out = append(out, label) } return strings.Join(out, " and ") }