Skip to content

Commit 4e9dc30

Browse files
authored
Support --format for mcpd add (#102)
1 parent 387e836 commit 4e9dc30

22 files changed

+422
-406
lines changed

cmd/add.go

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package cmd
22

33
import (
44
"fmt"
5+
"io"
56
"strings"
67

78
"github.com/spf13/cobra"
89

9-
"github.com/mozilla-ai/mcpd/v2/internal/cmd"
10+
internalcmd "github.com/mozilla-ai/mcpd/v2/internal/cmd"
1011
cmdopts "github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
12+
"github.com/mozilla-ai/mcpd/v2/internal/cmd/output"
1113
"github.com/mozilla-ai/mcpd/v2/internal/config"
1214
"github.com/mozilla-ai/mcpd/v2/internal/filter"
1315
"github.com/mozilla-ai/mcpd/v2/internal/flags"
@@ -20,27 +22,29 @@ import (
2022

2123
// AddCmd should be used to represent the 'add' command.
2224
type AddCmd struct {
23-
*cmd.BaseCmd
25+
*internalcmd.BaseCmd
2426
Version string
2527
Tools []string
2628
Runtime string
2729
Source string
30+
Format internalcmd.OutputFormat
2831
cfgLoader config.Loader
29-
packagePrinter printer.Printer
32+
packagePrinter output.Printer[config.ServerEntry]
3033
registryBuilder registry.Builder
3134
}
3235

3336
// NewAddCmd creates a newly configured (Cobra) command.
34-
func NewAddCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
37+
func NewAddCmd(baseCmd *internalcmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
3538
opts, err := cmdopts.NewOptions(opt...)
3639
if err != nil {
3740
return nil, err
3841
}
3942

4043
c := &AddCmd{
4144
BaseCmd: baseCmd,
45+
Format: internalcmd.FormatText, // Default to plain text
4246
cfgLoader: opts.ConfigLoader,
43-
packagePrinter: opts.Printer,
47+
packagePrinter: &printer.ServerEntryPrinter{},
4448
registryBuilder: opts.RegistryBuilder,
4549
}
4650

@@ -81,26 +85,38 @@ func NewAddCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command,
8185
"Optional, specify the source registry of the server package (e.g. `mcpm`)",
8286
)
8387

88+
allowed := internalcmd.AllowedOutputFormats()
89+
cobraCommand.Flags().Var(
90+
&c.Format,
91+
"format",
92+
fmt.Sprintf("Specify the output format (one of: %s)", allowed.String()),
93+
)
94+
8495
return cobraCommand, nil
8596
}
8697

8798
// run is configured (via NewAddCmd) to be called by the Cobra framework when the command is executed.
8899
// It may return an error (or nil, when there is no error).
89100
func (c *AddCmd) run(cmd *cobra.Command, args []string) error {
101+
handler, err := internalcmd.FormatHandler(cmd.OutOrStdout(), c.Format, c.packagePrinter)
102+
if err != nil {
103+
return err
104+
}
105+
90106
if len(args) == 0 || strings.TrimSpace(args[0]) == "" {
91-
return fmt.Errorf("server name is required and cannot be empty")
107+
return handler.HandleError(fmt.Errorf("server name is required and cannot be empty"))
92108
}
93109

94110
name := strings.TrimSpace(args[0])
95111

96112
logger, err := c.Logger()
97113
if err != nil {
98-
return err
114+
return handler.HandleError(err)
99115
}
100116

101117
reg, err := c.registryBuilder.Build()
102118
if err != nil {
103-
return err
119+
return handler.HandleError(err)
104120
}
105121

106122
pkg, err := reg.Resolve(name, c.options()...)
@@ -114,35 +130,37 @@ func (c *AddCmd) run(cmd *cobra.Command, args []string) error {
114130
"source", c.Source,
115131
"error", err,
116132
)
117-
return fmt.Errorf("⚠️ Failed to get package '%s@%s' from registry: %w", name, c.Version, err)
133+
return handler.HandleError(fmt.Errorf(
134+
"⚠️ Failed to get package '%s@%s' from registry: %w",
135+
name,
136+
c.Version,
137+
err),
138+
)
118139
}
119140

120141
entry, err := parseServerEntry(pkg, runtime.Runtime(c.Runtime), c.Tools, c.MCPDSupportedRuntimes())
121142
if err != nil {
122-
return fmt.Errorf("error parsing server entry: %w", err)
143+
return handler.HandleError(fmt.Errorf("error parsing server entry: %w", err))
123144
}
124145

125146
cfg, err := c.cfgLoader.Load(flags.ConfigFile)
126147
if err != nil {
127-
return err
148+
return handler.HandleError(err)
128149
}
129150

130151
err = cfg.AddServer(entry)
131152
if err != nil {
132-
return err
153+
return handler.HandleError(err)
133154
}
134155

135-
// User-friendly output + logging
136-
_, err = fmt.Fprintf(
137-
cmd.OutOrStdout(),
138-
"✓ Added server '%s' (version: %s), tools: %s\n",
139-
name,
140-
entry.PackageVersion(),
141-
strings.Join(entry.Tools, ", "),
142-
)
143-
if err != nil {
144-
return err
145-
}
156+
// User-friendly output for text format.
157+
c.packagePrinter.SetHeader(func(w io.Writer, count int) {
158+
_, _ = fmt.Fprintln(w)
159+
})
160+
c.packagePrinter.SetFooter(func(w io.Writer, count int) {
161+
_, _ = fmt.Fprintln(w)
162+
})
163+
146164
logger.Debug(
147165
"Server added",
148166
"name", name,
@@ -152,16 +170,7 @@ func (c *AddCmd) run(cmd *cobra.Command, args []string) error {
152170
)
153171

154172
// Print the package info.
155-
if err = c.packagePrinter.PrintPackage(pkg); err != nil {
156-
return err
157-
}
158-
159-
_, err = fmt.Fprintln(cmd.OutOrStdout())
160-
if err != nil {
161-
return err
162-
}
163-
164-
return nil
173+
return handler.HandleResult(entry)
165174
}
166175

167176
// selectRuntime returns the most appropriate runtime from a set of installations,

cmd/add_test.go

Lines changed: 7 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ package cmd
33
import (
44
"bytes"
55
"errors"
6-
"slices"
76
"testing"
87

9-
"github.com/mozilla-ai/mcpd/v2/internal/printer"
10-
118
"github.com/stretchr/testify/assert"
129
"github.com/stretchr/testify/require"
1310

@@ -48,23 +45,6 @@ func (f *fakeLoader) Load(_ string) (config.Modifier, error) {
4845
return f.cfg, f.err
4946
}
5047

51-
type fakePrinter struct {
52-
printed packages.Package
53-
opts []printer.PackagePrinterOption
54-
err error
55-
}
56-
57-
func (f *fakePrinter) PrintPackage(pkg packages.Package) error {
58-
f.printed = pkg
59-
return f.err
60-
}
61-
62-
func (f *fakePrinter) SetOptions(opt ...printer.PackagePrinterOption) error {
63-
f.opts = slices.Clone(opt)
64-
65-
return nil
66-
}
67-
6848
type fakeRegistry struct {
6949
pkg packages.Package
7050
err error
@@ -103,6 +83,7 @@ func TestAddCmd_Success(t *testing.T) {
10383
Version: "1.2.3",
10484
InstallationDetails: map[runtime.Runtime]packages.Installation{
10585
runtime.UVX: {
86+
Command: "uvx",
10687
Package: "mcp-server-1",
10788
Recommended: true,
10889
},
@@ -114,13 +95,12 @@ func TestAddCmd_Success(t *testing.T) {
11495
&cmd.BaseCmd{},
11596
cmdopts.WithConfigLoader(&fakeLoader{cfg: cfg}),
11697
cmdopts.WithRegistryBuilder(&fakeBuilder{reg: &fakeRegistry{pkg: pkg}}),
117-
cmdopts.WithPrinter(&fakePrinter{}),
11898
)
11999
require.NoError(t, err)
120100
require.NotNil(t, cmdObj)
121101

122102
cmdObj.SetOut(buf)
123-
cmdObj.SetArgs([]string{"mcp-server-1", "--version=1.2.3", "--tool=toolA", "--runtime=uvx"})
103+
cmdObj.SetArgs([]string{"server1", "--version=1.2.3", "--tool=toolA", "--runtime=uvx"})
124104

125105
err = cmdObj.Execute()
126106
require.NoError(t, err)
@@ -134,7 +114,6 @@ func TestAddCmd_MissingArgs(t *testing.T) {
134114
cmdObj, err := NewAddCmd(&cmd.BaseCmd{},
135115
cmdopts.WithConfigLoader(&fakeLoader{}),
136116
cmdopts.WithRegistryBuilder(&fakeBuilder{}),
137-
cmdopts.WithPrinter(&fakePrinter{}),
138117
)
139118
require.NoError(t, err)
140119

@@ -149,7 +128,6 @@ func TestAddCmd_RegistryFails(t *testing.T) {
149128
cmdObj, err := NewAddCmd(&cmd.BaseCmd{},
150129
cmdopts.WithConfigLoader(&fakeLoader{}),
151130
cmdopts.WithRegistryBuilder(&fakeBuilder{err: errors.New("registry error")}),
152-
cmdopts.WithPrinter(&fakePrinter{}),
153131
)
154132
require.NoError(t, err)
155133

@@ -160,7 +138,7 @@ func TestAddCmd_RegistryFails(t *testing.T) {
160138
}
161139

162140
func TestAddCmd_BasicServerAdd(t *testing.T) {
163-
output := &bytes.Buffer{}
141+
o := &bytes.Buffer{}
164142

165143
pkg := packages.Package{
166144
ID: "testserver",
@@ -173,32 +151,31 @@ func TestAddCmd_BasicServerAdd(t *testing.T) {
173151
},
174152
InstallationDetails: map[runtime.Runtime]packages.Installation{
175153
"uvx": {
154+
Command: "uvx",
176155
Package: "mcp-server-testserver",
177156
Recommended: true,
178157
},
179158
},
180159
}
181160

182161
cfg := &fakeConfig{}
183-
fp := &fakePrinter{}
184162
cmdObj, err := NewAddCmd(
185163
&cmd.BaseCmd{},
186164
cmdopts.WithConfigLoader(&fakeLoader{cfg: cfg}),
187165
cmdopts.WithRegistryBuilder(&fakeBuilder{reg: &fakeRegistry{pkg: pkg}}),
188-
cmdopts.WithPrinter(fp),
189166
)
190167
require.NoError(t, err)
191168

192-
cmdObj.SetOut(output)
193-
cmdObj.SetErr(output)
169+
cmdObj.SetOut(o)
170+
cmdObj.SetErr(o)
194171
cmdObj.SetArgs([]string{"testserver"})
195172

196173
// Run the command
197174
err = cmdObj.Execute()
198175
require.NoError(t, err)
199176

200177
// Output assertions
201-
outStr := output.String()
178+
outStr := o.String()
202179
assert.Contains(t, outStr, "✓ Added server 'testserver'")
203180
assert.Contains(t, outStr, "version: latest")
204181

cmd/root.go

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"github.com/mozilla-ai/mcpd/v2/internal/cmd"
1010
"github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
1111
"github.com/mozilla-ai/mcpd/v2/internal/flags"
12-
"github.com/mozilla-ai/mcpd/v2/internal/printer"
1312
)
1413

1514
type RootCmd struct {
@@ -74,17 +73,7 @@ func NewRootCmd(c *RootCmd) (*cobra.Command, error) {
7473
}
7574

7675
for _, fn := range fns {
77-
p, err := printer.NewPrinter(rootCmd.OutOrStdout())
78-
if err != nil {
79-
return nil, err
80-
}
81-
82-
opts := []options.CmdOption{
83-
options.WithPrinter(p),
84-
options.WithRegistryBuilder(c.BaseCmd),
85-
}
86-
87-
tempCmd, err := fn(c.BaseCmd, opts...)
76+
tempCmd, err := fn(c.BaseCmd, options.WithRegistryBuilder(c.BaseCmd))
8877
if err != nil {
8978
return nil, err
9079
}

cmd/search.go

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type SearchCmd struct {
2727
Format internalcmd.OutputFormat
2828
IsOfficial bool
2929
registryBuilder registry.Builder
30-
packagePrinter printer.Printer
30+
packagePrinter output.Printer[packages.Package]
3131
}
3232

3333
func NewSearchCmd(baseCmd *internalcmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
@@ -36,16 +36,13 @@ func NewSearchCmd(baseCmd *internalcmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobr
3636
return nil, err
3737
}
3838

39-
// Override printer options to show separator for search.
40-
if err = opts.Printer.SetOptions(printer.WithSeparator(true)); err != nil {
41-
return nil, err
42-
}
39+
pkgPrinter := printer.NewPackagePrinter()
4340

4441
c := &SearchCmd{
4542
BaseCmd: baseCmd,
4643
Format: internalcmd.FormatText, // Default to plain text
4744
registryBuilder: opts.RegistryBuilder,
48-
packagePrinter: opts.Printer,
45+
packagePrinter: printer.NewPackageResultsPrinter(pkgPrinter),
4946
}
5047

5148
cobraCommand := &cobra.Command{
@@ -152,18 +149,9 @@ func (c *SearchCmd) filters() map[string]string {
152149
}
153150

154151
func (c *SearchCmd) run(cmd *cobra.Command, args []string) (err error) {
155-
// Configure the handler based on the requested format.
156-
var handler output.Handler[packages.Package]
157-
switch c.Format {
158-
case internalcmd.FormatJSON:
159-
handler = output.NewJSONHandler[packages.Package](cmd.OutOrStdout(), 2)
160-
case internalcmd.FormatYAML:
161-
handler = output.NewYAMLHandler[packages.Package](cmd.OutOrStdout(), 2)
162-
case internalcmd.FormatText:
163-
pkgListPrinter := printer.NewPackageListPrinter(c.packagePrinter)
164-
handler = output.NewTextHandler[packages.Package](cmd.OutOrStdout(), pkgListPrinter)
165-
default:
166-
return fmt.Errorf("unexpected error, no handler for output format: %s", c.Format)
152+
handler, err := internalcmd.FormatHandler(cmd.OutOrStdout(), c.Format, c.packagePrinter)
153+
if err != nil {
154+
return err
167155
}
168156

169157
// Name not required, default to the wildcard.
@@ -182,5 +170,5 @@ func (c *SearchCmd) run(cmd *cobra.Command, args []string) (err error) {
182170
return handler.HandleError(err)
183171
}
184172

185-
return handler.HandleResults(results)
173+
return handler.HandleResults(results...)
186174
}

0 commit comments

Comments
 (0)