Files

1047 lines
29 KiB
Go

// Copyright 2026 Alibaba Group
// 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 app
import (
"encoding/json"
"fmt"
"log/slog"
"os"
"sort"
"strconv"
"strings"
"unicode"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/cmdutil"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/mcptypes"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type pluginFlagKind uint8
const (
pluginFlagString pluginFlagKind = iota
pluginFlagInt
pluginFlagFloat
pluginFlagBool
pluginFlagStringSlice
pluginFlagJSON
)
type pluginFlagBinding struct {
property string
names []string
kind pluginFlagKind
required bool
defaultProvided bool
defaultValue string
envDefault string
positional bool
positionalIndex int
omitWhen string
}
type pluginFlagReservations struct {
names map[string]bool
shorthands map[string]bool
}
// buildPluginCommands compiles only manifest-authored CLI overlays. It never
// starts a transport or calls tools/list, so plugin help stays available when
// a local subprocess or remote MCP endpoint is offline.
func buildPluginCommands(
servers []mcptypes.ServerDescriptor,
runner executor.Runner,
hostRoot *cobra.Command,
) []*cobra.Command {
reservations := pluginReservedFlags(hostRoot)
sorted := append([]mcptypes.ServerDescriptor(nil), servers...)
sort.SliceStable(sorted, func(i, j int) bool {
left := strings.TrimSpace(sorted[i].DisplayName) + "\x00" + strings.TrimSpace(sorted[i].Key)
right := strings.TrimSpace(sorted[j].DisplayName) + "\x00" + strings.TrimSpace(sorted[j].Key)
return left < right
})
roots := make(map[string]*cobra.Command)
for _, server := range sorted {
overlay := server.CLI
if overlay.Skip || len(overlay.ToolOverrides) == 0 {
continue
}
if reason := unsupportedPluginOverlay(overlay); reason != "" {
slog.Warn("plugin: unsupported overlay semantics, skipping command tree",
"plugin", server.DisplayName, "field", reason)
continue
}
name := firstNonEmptyPluginString(overlay.Command, overlay.ID, server.Key)
if !validPluginCommandName(name) {
slog.Warn("plugin: invalid overlay command name, skipping",
"plugin", server.DisplayName, "command", name)
continue
}
root := buildPluginOverlayRoot(server, runner, reservations)
if root == nil {
continue
}
if existing := roots[name]; existing != nil {
mergePluginRoot(existing, root)
continue
}
roots[name] = root
}
names := make([]string, 0, len(roots))
for name := range roots {
names = append(names, name)
}
sort.Strings(names)
commands := make([]*cobra.Command, 0, len(names))
for _, name := range names {
commands = append(commands, roots[name])
}
return commands
}
func buildPluginOverlayRoot(
server mcptypes.ServerDescriptor,
runner executor.Runner,
reservations pluginFlagReservations,
) *cobra.Command {
overlay := server.CLI
name := firstNonEmptyPluginString(overlay.Command, overlay.ID, server.Key)
short := firstNonEmptyPluginString(overlay.Description, server.Description, server.DisplayName, name)
root := cobracmd.NewGroupCommand(name, short)
root.Hidden = overlay.Hidden
root.Aliases = safePluginAliases(overlay.Aliases, name)
cmdutil.MarkPluginSource(root)
groups := make(map[string]*cobra.Command)
groupNames := make([]string, 0, len(overlay.Groups))
for groupName := range overlay.Groups {
groupNames = append(groupNames, groupName)
}
sort.Strings(groupNames)
for _, groupName := range groupNames {
group := overlay.Groups[groupName]
ensurePluginGroup(root, groupName, group.Description, groups)
}
toolNames := make([]string, 0, len(overlay.ToolOverrides))
for toolName := range overlay.ToolOverrides {
toolNames = append(toolNames, toolName)
}
sort.Strings(toolNames)
leafCount := 0
for _, toolName := range toolNames {
override := overlay.ToolOverrides[toolName]
if override.Hidden || strings.TrimSpace(toolName) == "" {
continue
}
leaf := buildPluginLeaf(server, toolName, override, runner, reservations)
if leaf == nil {
continue
}
parent := root
if groupPath := strings.TrimSpace(override.Group); groupPath != "" {
parent = ensurePluginGroup(root, groupPath, groupPath, groups)
if parent == nil {
slog.Warn("plugin: invalid overlay group path, skipping leaf",
"plugin", server.DisplayName, "tool", toolName, "group", groupPath)
continue
}
}
if existing := cobracmd.ChildByName(parent, leaf.Name()); existing != nil {
slog.Warn("plugin: duplicate overlay leaf, keeping first",
"plugin", server.DisplayName,
"command", strings.TrimSpace(parent.CommandPath()+" "+leaf.Name()))
continue
}
parent.AddCommand(leaf)
leafCount++
}
if leafCount == 0 {
return nil
}
pruneEmptyPluginGroups(root)
return root
}
func buildPluginLeaf(
server mcptypes.ServerDescriptor,
toolName string,
override mcptypes.CLIToolOverride,
runner executor.Runner,
reservations pluginFlagReservations,
) *cobra.Command {
overlay := server.CLI
if reason := unsupportedPluginToolOverride(override); reason != "" {
slog.Warn("plugin: unsupported tool override semantics, skipping leaf",
"plugin", server.DisplayName, "tool", toolName, "field", reason)
return nil
}
name := strings.TrimSpace(override.CLIName)
if name == "" {
name = derivePluginCommandName(toolName, overlay.Prefixes)
}
if !validPluginCommandName(name) {
slog.Warn("plugin: invalid overlay leaf name, skipping",
"plugin", server.DisplayName, "tool", toolName, "command", name)
return nil
}
short := firstNonEmptyPluginString(override.Description, toolName)
bindings, use, argsValidator, ok := registerPluginBindings(name, override, reservations)
if !ok {
return nil
}
if !validPluginFlagConstraints(bindings, override) {
slog.Warn("plugin: invalid flag constraints, skipping leaf",
"plugin", server.DisplayName, "tool", toolName)
return nil
}
canonicalProduct := firstNonEmptyPluginString(overlay.ID, server.Key)
cmd := &cobra.Command{
Use: use,
Short: short,
Example: strings.TrimRight(override.Example, " \t\r\n"),
Args: argsValidator,
DisableAutoGenTag: true,
}
cmdutil.MarkPluginSource(cmd)
registerPluginFlags(cmd, bindings, override, reservations)
applyPluginFlagConstraints(cmd, override)
cmd.RunE = func(cmd *cobra.Command, args []string) error {
jsonPayload, err := cmd.Flags().GetString("json")
if err != nil {
return apperrors.NewInternal("failed to read --json")
}
paramsPayload, err := cmd.Flags().GetString("params")
if err != nil {
return apperrors.NewInternal("failed to read --params")
}
params, err := executor.MergePayloads(jsonPayload, paramsPayload, nil)
if err != nil {
return err
}
if err := collectPluginBindings(cmd, args, bindings, params); err != nil {
return err
}
if wrapper := strings.TrimSpace(override.BodyWrapper); wrapper != "" {
wrapPluginParams(params, wrapper)
}
dryRun := pluginRootBoolFlag(cmd, "dry-run")
if override.IsSensitive && !dryRun && !pluginRootBoolFlag(cmd, "yes") {
return pluginConfirmationRequired(cmd.CommandPath())
}
if runner == nil {
return apperrors.NewInternal("plugin command runner is not configured")
}
invocation := executor.NewCompatibilityInvocation(
cobracmd.LegacyCommandPath(cmd),
canonicalProduct,
toolName,
params,
)
invocation.DryRun = dryRun
result, err := runner.Run(cmd.Context(), invocation)
if err != nil {
return err
}
return output.WriteCommandPayload(cmd, result, output.FormatJSON)
}
return cmd
}
func registerPluginBindings(
name string,
override mcptypes.CLIToolOverride,
reservations pluginFlagReservations,
) ([]pluginFlagBinding, string, cobra.PositionalArgs, bool) {
properties := make([]string, 0, len(override.Flags))
for property := range override.Flags {
properties = append(properties, property)
}
sort.Strings(properties)
bindings := make([]pluginFlagBinding, 0, len(properties))
usedNames := make(map[string]bool, len(reservations.names))
for reserved := range reservations.names {
usedNames[reserved] = true
}
usedPositions := make(map[int]bool)
maxPositionals := 0
minPositionals := 0
type positionalLabel struct {
index int
name string
}
var labels []positionalLabel
for _, property := range properties {
flagOverride := override.Flags[property]
if strings.Contains(property, ".") {
slog.Warn("plugin: dotted flag properties are unsupported, skipping leaf",
"command", name, "property", property)
return nil, "", nil, false
}
if reason := unsupportedPluginFlagOverride(flagOverride); reason != "" {
slog.Warn("plugin: unsupported flag override semantics, skipping leaf",
"command", name, "property", property, "field", reason)
return nil, "", nil, false
}
primary := strings.TrimSpace(flagOverride.Alias)
if primary == "" && !flagOverride.Positional {
primary = pluginKebabName(property)
}
if !flagOverride.Positional &&
(!validPluginFlagName(primary) || usedNames[primary]) {
slog.Warn("plugin: primary overlay flag conflicts with the host CLI, skipping leaf",
"command", name, "flag", primary, "property", property)
return nil, "", nil, false
}
names := make([]string, 0, 1+len(flagOverride.Aliases))
for _, candidate := range append([]string{primary}, flagOverride.Aliases...) {
candidate = strings.TrimSpace(candidate)
if !validPluginFlagName(candidate) || usedNames[candidate] {
if candidate != "" {
slog.Warn("plugin: duplicate or invalid overlay flag, skipping",
"command", name, "flag", candidate, "property", property)
}
continue
}
usedNames[candidate] = true
names = append(names, candidate)
}
if flagOverride.Positional {
index := flagOverride.PositionalIndex
if index < 0 || usedPositions[index] {
slog.Warn("plugin: invalid or duplicate positional binding, skipping leaf",
"command", name, "property", property, "index", index)
return nil, "", nil, false
}
usedPositions[index] = true
if index+1 > maxPositionals {
maxPositionals = index + 1
}
if flagOverride.Required && len(names) == 0 && index+1 > minPositionals {
minPositionals = index + 1
}
labels = append(labels, positionalLabel{index: index, name: property})
}
bindings = append(bindings, pluginFlagBinding{
property: property,
names: names,
kind: pluginFlagKindFromString(flagOverride.Type),
required: flagOverride.Required,
defaultProvided: flagOverride.Default != "",
defaultValue: flagOverride.Default,
envDefault: strings.TrimSpace(flagOverride.EnvDefault),
positional: flagOverride.Positional,
positionalIndex: flagOverride.PositionalIndex,
omitWhen: strings.ToLower(strings.TrimSpace(flagOverride.OmitWhen)),
})
}
for index := 0; index < maxPositionals; index++ {
if !usedPositions[index] {
slog.Warn("plugin: positional bindings must use contiguous indexes, skipping leaf",
"command", name, "missingIndex", index)
return nil, "", nil, false
}
}
use := name
sort.SliceStable(labels, func(i, j int) bool { return labels[i].index < labels[j].index })
for _, label := range labels {
use += " [" + label.name + "]"
}
var validator cobra.PositionalArgs = cobra.NoArgs
switch {
case maxPositionals == 0:
validator = cobra.NoArgs
case minPositionals == maxPositionals:
validator = cobra.ExactArgs(maxPositionals)
case minPositionals > 0:
validator = cobra.RangeArgs(minPositionals, maxPositionals)
default:
validator = cobra.MaximumNArgs(maxPositionals)
}
return bindings, use, validator, true
}
func registerPluginFlags(
cmd *cobra.Command,
bindings []pluginFlagBinding,
override mcptypes.CLIToolOverride,
reservations pluginFlagReservations,
) {
usedShorthands := make(map[string]bool)
for _, binding := range bindings {
flagOverride := override.Flags[binding.property]
for index, name := range binding.names {
shorthand := ""
if index == 0 {
shorthand = safePluginShorthand(
flagOverride.Shorthand,
usedShorthands,
reservations.shorthands,
)
}
usage := firstNonEmptyPluginString(flagOverride.Description, binding.property)
registerPluginFlag(cmd.Flags(), name, shorthand, usage, binding.kind, flagOverride.Default)
if index > 0 || flagOverride.Hidden {
_ = cmd.Flags().MarkHidden(name)
}
}
}
cmd.Flags().String("json", "", "Base JSON object payload for this command")
cmd.Flags().String("params", "", "Additional JSON object payload merged after --json")
_ = cmd.Flags().MarkHidden("json")
_ = cmd.Flags().MarkHidden("params")
}
func registerPluginFlag(flags *pflag.FlagSet, name, shorthand, usage string, kind pluginFlagKind, rawDefault string) {
switch kind {
case pluginFlagInt:
value, _ := strconv.Atoi(strings.TrimSpace(rawDefault))
flags.IntP(name, shorthand, value, usage)
case pluginFlagFloat:
value, _ := strconv.ParseFloat(strings.TrimSpace(rawDefault), 64)
flags.Float64P(name, shorthand, value, usage)
case pluginFlagBool:
value, _ := strconv.ParseBool(strings.TrimSpace(rawDefault))
flags.BoolP(name, shorthand, value, usage)
case pluginFlagStringSlice:
var value []string
if strings.TrimSpace(rawDefault) != "" {
for _, item := range strings.Split(rawDefault, ",") {
if item = strings.TrimSpace(item); item != "" {
value = append(value, item)
}
}
}
flags.StringSliceP(name, shorthand, value, usage)
default:
flags.StringP(name, shorthand, rawDefault, usage)
}
}
func collectPluginBindings(
cmd *cobra.Command,
args []string,
bindings []pluginFlagBinding,
params map[string]any,
) error {
for _, binding := range bindings {
var (
value any
set bool
err error
)
for _, name := range binding.names {
if flag := cmd.Flags().Lookup(name); flag != nil && flag.Changed {
value, err = readPluginFlag(cmd.Flags(), name, binding.kind)
set = err == nil
break
}
}
if err != nil {
return apperrors.NewValidation(fmt.Sprintf("invalid plugin flag for %s: %v", binding.property, err))
}
if !set {
if existing, exists := params[binding.property]; exists {
value = existing
set = true
}
}
if !set && binding.positional && binding.positionalIndex >= 0 && binding.positionalIndex < len(args) {
value, err = parsePluginValue(args[binding.positionalIndex], binding.kind)
if err != nil {
return apperrors.NewValidation(fmt.Sprintf(
"invalid positional value for %s: %v",
binding.property,
err,
))
}
set = true
}
if !set && binding.defaultProvided {
value, err = parsePluginValue(binding.defaultValue, binding.kind)
if err != nil {
return apperrors.NewValidation(fmt.Sprintf("invalid default for %s: %v", binding.property, err))
}
set = true
}
if !set && binding.envDefault != "" {
if raw := strings.TrimSpace(os.Getenv(binding.envDefault)); raw != "" {
value, err = parsePluginValue(raw, binding.kind)
if err != nil {
return apperrors.NewValidation(fmt.Sprintf("invalid %s value from %s: %v", binding.property, binding.envDefault, err))
}
set = true
}
}
if !set {
if binding.required {
label := binding.property
if len(binding.names) > 0 {
label = "--" + binding.names[0]
}
return apperrors.NewValidation("missing required plugin parameter: " + label)
}
continue
}
if shouldOmitPluginValue(value, binding.omitWhen) {
delete(params, binding.property)
if binding.required {
return apperrors.NewValidation("required plugin parameter is empty: " + binding.property)
}
continue
}
params[binding.property] = value
}
return nil
}
func readPluginFlag(flags *pflag.FlagSet, name string, kind pluginFlagKind) (any, error) {
switch kind {
case pluginFlagInt:
return flags.GetInt(name)
case pluginFlagFloat:
return flags.GetFloat64(name)
case pluginFlagBool:
return flags.GetBool(name)
case pluginFlagStringSlice:
return flags.GetStringSlice(name)
case pluginFlagJSON:
raw, err := flags.GetString(name)
if err != nil {
return nil, err
}
var value any
if err := json.Unmarshal([]byte(raw), &value); err != nil {
return nil, err
}
return value, nil
default:
return flags.GetString(name)
}
}
func parsePluginValue(raw string, kind pluginFlagKind) (any, error) {
switch kind {
case pluginFlagInt:
return strconv.Atoi(strings.TrimSpace(raw))
case pluginFlagFloat:
return strconv.ParseFloat(strings.TrimSpace(raw), 64)
case pluginFlagBool:
return strconv.ParseBool(strings.TrimSpace(raw))
case pluginFlagStringSlice:
var values []string
for _, item := range strings.Split(raw, ",") {
if item = strings.TrimSpace(item); item != "" {
values = append(values, item)
}
}
return values, nil
case pluginFlagJSON:
var value any
if err := json.Unmarshal([]byte(raw), &value); err != nil {
return nil, err
}
return value, nil
default:
return raw, nil
}
}
func shouldOmitPluginValue(value any, mode string) bool {
if mode == "never" {
return false
}
switch typed := value.(type) {
case nil:
return true
case string:
return strings.TrimSpace(typed) == ""
case []string:
return len(typed) == 0
}
if mode != "zero" {
return false
}
switch typed := value.(type) {
case bool:
return !typed
case int:
return typed == 0
case float64:
return typed == 0
case []any:
return len(typed) == 0
case map[string]any:
return len(typed) == 0
default:
return false
}
}
func wrapPluginParams(params map[string]any, wrapper string) {
if params == nil || strings.TrimSpace(wrapper) == "" {
return
}
body := make(map[string]any)
if existing, ok := params[wrapper].(map[string]any); ok {
for key, value := range existing {
body[key] = value
}
delete(params, wrapper)
}
for key, value := range params {
if strings.HasPrefix(key, "_") {
continue
}
body[key] = value
delete(params, key)
}
params[wrapper] = body
}
func applyPluginFlagConstraints(cmd *cobra.Command, override mcptypes.CLIToolOverride) {
validate := func(group []string) []string {
names := make([]string, 0, len(group))
for _, name := range group {
name = strings.TrimSpace(name)
if name == "" || cmd.Flags().Lookup(name) == nil {
return nil
}
names = append(names, name)
}
return names
}
for _, group := range override.MutuallyExclusive {
if names := validate(group); len(names) > 1 {
cmd.MarkFlagsMutuallyExclusive(names...)
}
}
for _, group := range override.RequireOneOf {
if names := validate(group); len(names) > 0 {
cmd.MarkFlagsOneRequired(names...)
}
}
for _, group := range override.RequireTogether {
if names := validate(group); len(names) > 1 {
cmd.MarkFlagsRequiredTogether(names...)
}
}
}
func validPluginFlagConstraints(bindings []pluginFlagBinding, override mcptypes.CLIToolOverride) bool {
known := make(map[string]bool)
for _, binding := range bindings {
for _, name := range binding.names {
known[name] = true
}
}
validate := func(groups [][]string, minimum int) bool {
for _, group := range groups {
if len(group) < minimum {
return false
}
seen := make(map[string]bool, len(group))
for _, rawName := range group {
name := strings.TrimSpace(rawName)
if name == "" || !known[name] || seen[name] {
return false
}
seen[name] = true
}
}
return true
}
return validate(override.MutuallyExclusive, 2) &&
validate(override.RequireOneOf, 1) &&
validate(override.RequireTogether, 2)
}
func ensurePluginGroup(
root *cobra.Command,
rawPath string,
description string,
groups map[string]*cobra.Command,
) *cobra.Command {
if !validPluginGroupPath(rawPath) {
return nil
}
parts := strings.Split(strings.TrimSpace(rawPath), ".")
parent := root
var pathParts []string
for index, rawPart := range parts {
part := strings.TrimSpace(rawPart)
pathParts = append(pathParts, part)
path := strings.Join(pathParts, ".")
if existing := groups[path]; existing != nil {
parent = existing
continue
}
short := part
if index == len(parts)-1 {
short = firstNonEmptyPluginString(description, part)
}
group := cobracmd.NewGroupCommand(part, short)
cmdutil.MarkPluginSource(group)
parent.AddCommand(group)
groups[path] = group
parent = group
}
return parent
}
func validPluginGroupPath(rawPath string) bool {
parts := strings.Split(strings.TrimSpace(rawPath), ".")
for _, rawPart := range parts {
if !validPluginCommandName(strings.TrimSpace(rawPart)) {
return false
}
}
return true
}
func mergePluginRoot(destination, source *cobra.Command) {
if destination == nil || source == nil {
return
}
aliases := append(append([]string(nil), destination.Aliases...), source.Aliases...)
destination.Aliases = safePluginAliases(aliases, destination.Name())
cobracmd.MergeCommandTree(destination, source)
}
func pruneEmptyPluginGroups(parent *cobra.Command) {
if parent == nil {
return
}
for _, child := range append([]*cobra.Command(nil), parent.Commands()...) {
pruneEmptyPluginGroups(child)
_, group, err := corecmd.GroupPolicyFor(child)
if err != nil {
panic(fmt.Sprintf("prune plugin group %q: %v", child.CommandPath(), err))
}
if group && len(child.Commands()) == 0 {
parent.RemoveCommand(child)
}
}
}
func pluginConfirmationRequired(commandPath string) error {
return apperrors.NewValidation(
commandPath+" is sensitive; preview with --dry-run, then obtain user confirmation and retry with --yes",
apperrors.WithReason("confirmation_required"),
apperrors.WithHint("run the same command with --dry-run first; after the user confirms, add --yes"),
apperrors.WithActions("preview with --dry-run", "obtain user confirmation", "execute with --yes"),
)
}
func pluginRootBoolFlag(cmd *cobra.Command, name string) bool {
if cmd == nil || cmd.Root() == nil {
return false
}
flags := cmd.Root().PersistentFlags()
if flags == nil || flags.Lookup(name) == nil {
return false
}
value, err := flags.GetBool(name)
return err == nil && value
}
func pluginFlagKindFromString(raw string) pluginFlagKind {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "int", "integer":
return pluginFlagInt
case "float", "float64", "number":
return pluginFlagFloat
case "bool", "boolean":
return pluginFlagBool
case "stringslice", "string_slice", "array", "[]string":
return pluginFlagStringSlice
case "json", "object":
return pluginFlagJSON
default:
return pluginFlagString
}
}
func safePluginShorthand(raw string, used, reserved map[string]bool) string {
value := strings.TrimSpace(raw)
if len(value) != 1 || used[value] || reserved[value] {
return ""
}
used[value] = true
return value
}
func pluginReservedFlags(root *cobra.Command) pluginFlagReservations {
reservations := pluginFlagReservations{
names: map[string]bool{
"client-id": true, "client-secret": true, "debug": true,
"dry-run": true, "fields": true, "format": true, "help": true,
"jq": true, "json": true, "mock": true, "output": true,
"params": true, "profile": true, "timeout": true, "token": true,
"verbose": true, "yes": true,
},
shorthands: map[string]bool{
"f": true, "h": true, "o": true, "v": true, "y": true,
},
}
if root == nil {
return reservations
}
root.PersistentFlags().VisitAll(func(flag *pflag.Flag) {
reservations.names[flag.Name] = true
if shorthand := strings.TrimSpace(flag.Shorthand); shorthand != "" {
reservations.shorthands[shorthand] = true
}
})
return reservations
}
func unsupportedPluginOverlay(overlay mcptypes.CLIOverlay) string {
switch {
case strings.TrimSpace(overlay.Parent) != "":
return "parent"
case strings.TrimSpace(overlay.Group) != "":
return "group"
case len(overlay.ServerDeps) > 0:
return "serverDeps"
case len(overlay.Hints) > 0:
return "hintCommands"
case strings.TrimSpace(overlay.RedirectTo) != "":
return "redirectTo"
default:
return ""
}
}
func unsupportedPluginDescriptor(root *cobra.Command, descriptor mcptypes.ServerDescriptor) string {
overlay := descriptor.CLI
if reason := unsupportedPluginOverlay(overlay); reason != "" {
return reason
}
if len(overlay.ToolOverrides) == 0 {
return ""
}
if !validPluginCommandName(pluginDescriptorRootName(descriptor)) {
return "command"
}
for groupPath := range overlay.Groups {
if !validPluginGroupPath(groupPath) {
return "groups"
}
}
reservations := pluginReservedFlags(root)
for toolName, override := range overlay.ToolOverrides {
if override.Hidden {
continue
}
if strings.TrimSpace(toolName) == "" {
return "tool"
}
if reason := unsupportedPluginToolOverride(override); reason != "" {
return reason
}
leafName := strings.TrimSpace(override.CLIName)
if leafName == "" {
leafName = derivePluginCommandName(toolName, overlay.Prefixes)
}
if !validPluginCommandName(leafName) {
return "cliName"
}
if groupPath := strings.TrimSpace(override.Group); groupPath != "" &&
!validPluginGroupPath(groupPath) {
return "group"
}
bindings, _, _, ok := registerPluginBindings(leafName, override, reservations)
if !ok {
return "flags"
}
if !validPluginFlagConstraints(bindings, override) {
return "constraints"
}
}
return ""
}
func unsupportedPluginToolOverride(override mcptypes.CLIToolOverride) string {
switch {
case len(override.CLIAliases) > 0:
return "cliAliases"
case len(override.OutputFormat) > 0:
return "outputFormat"
case strings.TrimSpace(override.ServerOverride) != "":
return "serverOverride"
case strings.TrimSpace(override.RedirectTo) != "":
return "redirectTo"
case len(override.Pipeline) > 0:
return "pipeline"
default:
return ""
}
}
func unsupportedPluginFlagOverride(override mcptypes.CLIFlagOverride) string {
switch {
case strings.TrimSpace(override.MapsTo) != "":
return "mapsTo"
case strings.TrimSpace(override.Transform) != "":
return "transform"
case len(override.TransformArgs) > 0:
return "transformArgs"
case strings.TrimSpace(override.RuntimeDefault) != "":
return "runtimeDefault"
case override.PipelineLocal:
return "pipelineLocal"
case !supportedPluginFlagType(override.Type):
return "type"
case !supportedPluginOmitMode(override.OmitWhen):
return "omitWhen"
default:
return ""
}
}
func supportedPluginFlagType(raw string) bool {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "", "string", "int", "integer", "float", "float64", "number",
"bool", "boolean", "stringslice", "string_slice", "array", "[]string",
"json", "object":
return true
default:
return false
}
}
func supportedPluginOmitMode(raw string) bool {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "", "empty", "zero", "never":
return true
default:
return false
}
}
func safePluginAliases(rawAliases []string, commandName string) []string {
seen := map[string]bool{commandName: true}
aliases := make([]string, 0, len(rawAliases))
for _, raw := range rawAliases {
alias := strings.TrimSpace(raw)
if !validPluginCommandName(alias) || reservedCommands[alias] || seen[alias] {
continue
}
seen[alias] = true
aliases = append(aliases, alias)
}
return aliases
}
func derivePluginCommandName(toolName string, prefixes []string) string {
name := strings.TrimSpace(toolName)
for _, rawPrefix := range prefixes {
prefix := strings.TrimSpace(rawPrefix)
if prefix != "" && strings.HasPrefix(name, prefix+"_") {
name = strings.TrimPrefix(name, prefix+"_")
break
}
}
return pluginKebabName(name)
}
func pluginKebabName(raw string) string {
var result strings.Builder
lastDash := false
var previous rune
for index, current := range raw {
separator := current == '_' || current == '.' || unicode.IsSpace(current)
if separator {
if result.Len() > 0 && !lastDash {
result.WriteByte('-')
lastDash = true
}
previous = current
continue
}
if unicode.IsUpper(current) && index > 0 && !lastDash &&
(unicode.IsLower(previous) || unicode.IsDigit(previous)) {
result.WriteByte('-')
}
result.WriteRune(unicode.ToLower(current))
lastDash = false
previous = current
}
return strings.Trim(result.String(), "-")
}
func validPluginCommandName(name string) bool {
name = strings.TrimSpace(name)
return name != "help" && validPluginKebabName(name)
}
func validPluginFlagName(name string) bool {
name = strings.TrimSpace(name)
return name != "json" && name != "params" && validPluginKebabName(name)
}
func validPluginKebabName(name string) bool {
name = strings.TrimSpace(name)
if name == "" || name[0] < 'a' || name[0] > 'z' {
return false
}
previousDash := false
for _, current := range name {
switch {
case current >= 'a' && current <= 'z':
previousDash = false
case current >= '0' && current <= '9':
previousDash = false
case current == '-' && !previousDash:
previousDash = true
default:
return false
}
}
return !previousDash
}
func firstNonEmptyPluginString(values ...string) string {
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
return ""
}