Skip to content

Commit f6e8702

Browse files
committed
Add plugins 'add' command
Implement 'mcpd config plugins add' command to add new plugins to a category.
1 parent 723be7c commit f6e8702

File tree

5 files changed

+700
-13
lines changed

5 files changed

+700
-13
lines changed

cmd/config/plugins/add.go

Lines changed: 149 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,163 @@ package plugins
22

33
import (
44
"fmt"
5+
"maps"
6+
"slices"
7+
"strings"
58

69
"github.com/spf13/cobra"
710

8-
"github.com/mozilla-ai/mcpd/v2/internal/cmd"
9-
"github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
11+
internalcmd "github.com/mozilla-ai/mcpd/v2/internal/cmd"
12+
cmdopts "github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
13+
"github.com/mozilla-ai/mcpd/v2/internal/config"
1014
)
1115

12-
// NewAddCmd creates the add command for plugins.
13-
// TODO: Implement in a future PR.
14-
func NewAddCmd(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Command, error) {
16+
const (
17+
flagFlow = "flow"
18+
flagRequired = "required"
19+
flagCommitHash = "commit-hash"
20+
)
21+
22+
// AddCmd represents the command for adding a new plugin entry.
23+
// NOTE: Use NewAddCmd to create instances of AddCmd.
24+
type AddCmd struct {
25+
*internalcmd.BaseCmd
26+
27+
// cfgLoader is used to load the configuration.
28+
cfgLoader config.Loader
29+
30+
// category is the category to add the plugin to.
31+
category config.Category
32+
33+
// flows is the list of flows.
34+
flows []string
35+
36+
// required indicates if the plugin is required.
37+
required bool
38+
39+
// commitHash is the optional commit hash for version validation.
40+
commitHash string
41+
}
42+
43+
// NewAddCmd creates a new add command for plugin entries.
44+
func NewAddCmd(baseCmd *internalcmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
45+
opts, err := cmdopts.NewOptions(opt...)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
c := &AddCmd{
51+
BaseCmd: baseCmd,
52+
cfgLoader: opts.ConfigLoader,
53+
}
54+
1555
cobraCmd := &cobra.Command{
16-
Use: "add",
56+
Use: "add <plugin-name>",
1757
Short: "Add a new plugin entry to a category",
18-
Long: "Add a new plugin entry to a category. The configuration is saved automatically.",
19-
RunE: func(cmd *cobra.Command, args []string) error {
20-
return fmt.Errorf("not yet implemented")
21-
},
58+
Long: `Add a new plugin entry to a category. The configuration is saved automatically.
59+
60+
The plugin name must exactly match the name of the plugin binary file.
61+
62+
This command creates new plugin entries only. If a plugin with the same name already exists
63+
in the category, the command fails with an error. To update an existing plugin, use the 'set' command.`,
64+
Example: ` # Add new plugin with all fields
65+
mcpd config plugins add jwt-auth --category=authentication --flow=request --required
66+
67+
# Add plugin with multiple flows
68+
mcpd config plugins add metrics --category=observability --flow=request --flow=response --commit-hash=abc123
69+
70+
# Add without required flag (defaults to false)
71+
mcpd config plugins add rbac --category=authorization --flow=response`,
72+
RunE: c.run,
73+
Args: cobra.ExactArgs(1), // plugin-name
2274
}
2375

76+
allowedCategories := config.OrderedCategories()
77+
cobraCmd.Flags().Var(
78+
&c.category,
79+
flagCategory,
80+
fmt.Sprintf("Specify the category (one of: %s)", allowedCategories.String()),
81+
)
82+
_ = cobraCmd.MarkFlagRequired(flagCategory)
83+
84+
cobraCmd.Flags().StringArrayVar(
85+
&c.flows,
86+
flagFlow,
87+
nil,
88+
"Flow to execute plugin in: request, response (can be repeated)",
89+
)
90+
_ = cobraCmd.MarkFlagRequired(flagFlow)
91+
92+
cobraCmd.Flags().BoolVar(
93+
&c.required,
94+
flagRequired,
95+
false,
96+
"Optional, mark plugin as required",
97+
)
98+
99+
cobraCmd.Flags().StringVar(
100+
&c.commitHash,
101+
flagCommitHash,
102+
"",
103+
"Optional, commit hash for runtime version validation",
104+
)
105+
24106
return cobraCmd, nil
25107
}
108+
109+
func (c *AddCmd) run(cmd *cobra.Command, args []string) error {
110+
pluginName := strings.TrimSpace(args[0])
111+
if pluginName == "" {
112+
return fmt.Errorf("plugin name cannot be empty")
113+
}
114+
115+
cfg, err := c.LoadConfig(c.cfgLoader)
116+
if err != nil {
117+
return err
118+
}
119+
120+
if _, exists := cfg.Plugin(c.category, pluginName); exists {
121+
return fmt.Errorf(
122+
"plugin '%s' already exists in category '%s'\n\n"+
123+
"To update an existing plugin, use: mcpd config plugins set %s --category=%s [flags]",
124+
pluginName,
125+
c.category,
126+
pluginName,
127+
c.category,
128+
)
129+
}
130+
131+
flowsMap := config.ParseFlows(c.flows)
132+
if len(flowsMap) == 0 {
133+
return fmt.Errorf("at least one valid flow is required (must be 'request' or 'response')")
134+
}
135+
136+
parsedFlows := slices.Sorted(maps.Keys(flowsMap))
137+
138+
entry := config.PluginEntry{
139+
Name: pluginName,
140+
Flows: parsedFlows,
141+
}
142+
143+
// Set optional fields only if they were provided.
144+
if cmd.Flags().Changed(flagRequired) {
145+
entry.Required = &c.required
146+
}
147+
148+
if c.commitHash != "" {
149+
entry.CommitHash = &c.commitHash
150+
}
151+
152+
if _, err := cfg.UpsertPlugin(c.category, entry); err != nil {
153+
return err
154+
}
155+
156+
_, _ = fmt.Fprintf(
157+
cmd.OutOrStdout(),
158+
"✓ Plugin '%s' added to category '%s'\n",
159+
pluginName,
160+
c.category,
161+
)
162+
163+
return nil
164+
}

0 commit comments

Comments
 (0)