Skip to content

Commit 3805cc7

Browse files
authored
mcpd search --format (#84)
* Add support to mcpd search with a flag (--format) for outputting search results in json, yaml or plain text. * Tweak: Don't print Name in text results of search. * Change: JSON field names adjusted to use snake_case instead of camelCase. * Tidied up mcpd config export long description.
1 parent ce4d789 commit 3805cc7

20 files changed

+1586
-77
lines changed

cmd/add_test.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"bytes"
55
"errors"
6+
"slices"
67
"testing"
78

89
"github.com/mozilla-ai/mcpd/v2/internal/printer"
@@ -59,11 +60,7 @@ func (f *fakePrinter) PrintPackage(pkg packages.Package) error {
5960
}
6061

6162
func (f *fakePrinter) SetOptions(opt ...printer.PackagePrinterOption) error {
62-
opts := make([]printer.PackagePrinterOption, 0, len(opt))
63-
for i, o := range opt {
64-
opts[i] = o
65-
}
66-
f.opts = opts
63+
f.opts = slices.Clone(opt)
6764

6865
return nil
6966
}

cmd/config/export/export.go

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,42 +38,25 @@ func NewCmd(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Command, err
3838
cobraCmd := &cobra.Command{
3939
Use: "export",
4040
Short: "Exports current configuration, generating a pair of safe and portable configuration files",
41-
Long: "Exports current configuration, generating a pair of safe and portable configuration files.\n\n" +
42-
"Using a project's required configuration (e.g. .mcpd.toml) and the locally configured runtime values " +
43-
"from the execution context file (e.g. ~/.config/mcpd/secrets.dev.toml), outputs both an 'Environment Contract' " +
44-
"and 'Portable Execution Context' file." +
45-
"These files are safe to check into version control if required.\n\n" +
46-
"This allows running an mcpd project in any environment, cleanly separating the configuration structure " +
47-
"from the secret values.",
48-
RunE: c.run,
41+
Long: c.longDescription(),
42+
RunE: c.run,
4943
}
5044

51-
// Portable Execution Context:
52-
//
53-
// A new secrets.toml file that defines the runtime args and env sections for each server,
54-
// using the placeholders from the environment contract.
5545
cobraCmd.Flags().StringVar(
5646
&c.ContextOutput,
5747
"context-output",
5848
"secrets.prod.toml",
5949
"Optional, specify the output path for the templated execution context config file",
6050
)
6151

62-
// Environment Contract:
63-
//
64-
// Lists all required and configured environment variables as secure, namespaced placeholders
65-
// e.g. MCPD__{SERVER_NAME}__{ENV_VAR}
66-
// Creates placeholders for command line arguments to be populated with env vars
67-
// e.g. MCPD__{SERVER_NAME}__ARG_{ARG_NAME}
68-
// This file is intended for the platform operator or CI/CD system.
6952
cobraCmd.Flags().StringVar(
7053
&c.ContractOutput,
7154
"contract-output",
7255
".env",
7356
"Optional, specify the output path for the templated environment file",
7457
)
7558

76-
allowed := cmd.AllowedFormats()
59+
allowed := cmd.AllowedExportFormats()
7760
cobraCmd.Flags().Var(
7861
&c.Format,
7962
"format",
@@ -83,6 +66,25 @@ func NewCmd(baseCmd *cmd.BaseCmd, opt ...options.CmdOption) (*cobra.Command, err
8366
return cobraCmd, nil
8467
}
8568

69+
func (c *Cmd) longDescription() string {
70+
return `Exports current configuration, generating a pair of safe and portable configuration files.
71+
72+
Using a project's required configuration (e.g. .mcpd.toml) and the locally configured runtime values from the execution context file (e.g. ~/.config/mcpd/secrets.dev.toml), the export command outputs two files:
73+
74+
Environment Contract:
75+
- Lists all required and configured environment variables as secure, namespaced placeholders
76+
e.g. MCPD__{SERVER_NAME}__{ENV_VAR}
77+
- Creates placeholders for command line arguments to be populated with env vars
78+
e.g. MCPD__{SERVER_NAME}__ARG_{ARG_NAME}
79+
- This file is intended for the platform operator or CI/CD system
80+
81+
Portable Execution Context:
82+
- A new secrets .toml file that defines sanitized runtime args and env sections for each server using the placeholders aligned with the environment contract
83+
- These files are safe to check into version control if required.
84+
85+
"This allows running an mcpd project in any environment, cleanly separating the configuration structure from the secret values`
86+
}
87+
8688
func (c *Cmd) run(cmd *cobra.Command, args []string) error {
8789
contextPath := c.ContextOutput
8890
// contractPath := c.ContractOutput

cmd/search.go

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@ import (
66

77
"github.com/spf13/cobra"
88

9-
"github.com/mozilla-ai/mcpd/v2/internal/cmd"
9+
internalcmd "github.com/mozilla-ai/mcpd/v2/internal/cmd"
1010
cmdopts "github.com/mozilla-ai/mcpd/v2/internal/cmd/options"
11+
"github.com/mozilla-ai/mcpd/v2/internal/cmd/output"
12+
"github.com/mozilla-ai/mcpd/v2/internal/packages"
1113
"github.com/mozilla-ai/mcpd/v2/internal/printer"
1214
"github.com/mozilla-ai/mcpd/v2/internal/registry"
1315
"github.com/mozilla-ai/mcpd/v2/internal/registry/options"
1416
)
1517

1618
type SearchCmd struct {
17-
*cmd.BaseCmd
19+
*internalcmd.BaseCmd
1820
Version string
1921
Runtime string
2022
Tools []string
2123
Tags []string
2224
Categories []string
2325
License string
2426
Source string
27+
Format internalcmd.OutputFormat
2528
registryBuilder registry.Builder
2629
packagePrinter printer.Printer
2730
}
2831

29-
func NewSearchCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
32+
func NewSearchCmd(baseCmd *internalcmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Command, error) {
3033
opts, err := cmdopts.NewOptions(opt...)
3134
if err != nil {
3235
return nil, err
@@ -39,6 +42,7 @@ func NewSearchCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Comman
3942

4043
c := &SearchCmd{
4144
BaseCmd: baseCmd,
45+
Format: internalcmd.FormatText, // Default to plain text
4246
registryBuilder: opts.RegistryBuilder,
4347
packagePrinter: opts.Printer,
4448
}
@@ -101,6 +105,13 @@ func NewSearchCmd(baseCmd *cmd.BaseCmd, opt ...cmdopts.CmdOption) (*cobra.Comman
101105
"Optional, specify a partial match for required categories (can be repeated)",
102106
)
103107

108+
allowed := internalcmd.AllowedOutputFormats()
109+
cobraCommand.Flags().Var(
110+
&c.Format,
111+
"format",
112+
fmt.Sprintf("Specify the output format (one of: %s)", allowed.String()),
113+
)
114+
104115
return cobraCommand, nil
105116
}
106117

@@ -129,7 +140,21 @@ func (c *SearchCmd) filters() map[string]string {
129140
return f
130141
}
131142

132-
func (c *SearchCmd) run(cmd *cobra.Command, args []string) error {
143+
func (c *SearchCmd) run(cmd *cobra.Command, args []string) (err error) {
144+
// Configure the handler based on the requested format.
145+
var handler output.Handler[packages.Package]
146+
switch c.Format {
147+
case internalcmd.FormatJSON:
148+
handler = output.NewJSONHandler[packages.Package](cmd.OutOrStdout(), 2)
149+
case internalcmd.FormatYAML:
150+
handler = output.NewYAMLHandler[packages.Package](cmd.OutOrStdout(), 2)
151+
case internalcmd.FormatText:
152+
pkgListPrinter := printer.NewPackageListPrinter(c.packagePrinter)
153+
handler = output.NewTextHandler[packages.Package](cmd.OutOrStdout(), pkgListPrinter)
154+
default:
155+
return fmt.Errorf("unexpected error, no handler for output format: %s", c.Format)
156+
}
157+
133158
// Name not required, default to the wildcard.
134159
name := options.WildcardCharacter
135160
if len(args) > 0 && strings.TrimSpace(args[0]) != "" {
@@ -138,38 +163,13 @@ func (c *SearchCmd) run(cmd *cobra.Command, args []string) error {
138163

139164
reg, err := c.registryBuilder.Build()
140165
if err != nil {
141-
return err
166+
return handler.HandleError(err)
142167
}
143168

144169
results, err := reg.Search(name, c.filters(), []options.SearchOption{options.WithSearchSource(c.Source)}...)
145170
if err != nil {
146-
return err
147-
}
148-
if len(results) == 0 {
149-
fmt.Println("No packages found")
150-
return nil
151-
}
152-
153-
if _, err = fmt.Fprintf(cmd.OutOrStdout(), "\n🔎 Registry search results...\n"); err != nil {
154-
return err
155-
}
156-
if _, err = fmt.Fprintf(cmd.OutOrStdout(), "\n────────────────────────────────────────────\n\n"); err != nil {
157-
return err
158-
}
159-
160-
for _, pkg := range results {
161-
if err = c.packagePrinter.PrintPackage(pkg); err != nil {
162-
return err
163-
}
164-
}
165-
166-
if _, err := fmt.Fprintf(
167-
cmd.OutOrStdout(),
168-
"📦 Found %d package%s\n\n",
169-
len(results),
170-
map[bool]string{true: "s", false: ""}[len(results) > 1]); err != nil {
171-
return err
171+
return handler.HandleError(err)
172172
}
173173

174-
return nil
174+
return handler.HandleResults(results)
175175
}

0 commit comments

Comments
 (0)