Skip to content

Commit ee42c1f

Browse files
committed
feat: add --no-tools option and server ls
To address issue where too much data was being printed when showing an entire catalog, introduce a option to specify --no-tools to exclude tools from the response. Additionally, add a `docker mcp catalog-next server ls` command so that we can get individual server information from a catalog that does include the tools.
1 parent 8f13ce3 commit ee42c1f

File tree

5 files changed

+967
-9
lines changed

5 files changed

+967
-9
lines changed

cmd/docker-mcp/commands/catalog_next.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func catalogNextCommand() *cobra.Command {
2626
cmd.AddCommand(pushCatalogNextCommand())
2727
cmd.AddCommand(pullCatalogNextCommand())
2828
cmd.AddCommand(tagCatalogNextCommand())
29+
cmd.AddCommand(catalogNextServerCommand())
2930

3031
return cmd
3132
}
@@ -83,6 +84,7 @@ func tagCatalogNextCommand() *cobra.Command {
8384
func showCatalogNextCommand() *cobra.Command {
8485
format := string(workingset.OutputFormatHumanReadable)
8586
pullOption := string(catalognext.PullOptionNever)
87+
var noTools bool
8688

8789
cmd := &cobra.Command{
8890
Use: "show <oci-reference> [--pull <pull-option>]",
@@ -98,13 +100,14 @@ func showCatalogNextCommand() *cobra.Command {
98100
return err
99101
}
100102
ociService := oci.NewService()
101-
return catalognext.Show(cmd.Context(), dao, ociService, args[0], workingset.OutputFormat(format), pullOption)
103+
return catalognext.Show(cmd.Context(), dao, ociService, args[0], workingset.OutputFormat(format), pullOption, noTools)
102104
},
103105
}
104106

105107
flags := cmd.Flags()
106108
flags.StringVar(&format, "format", string(workingset.OutputFormatHumanReadable), fmt.Sprintf("Supported: %s.", strings.Join(workingset.SupportedFormats(), ", ")))
107109
flags.StringVar(&pullOption, "pull", string(catalognext.PullOptionNever), fmt.Sprintf("Supported: %s, or duration (e.g. '1h', '1d'). Duration represents time since last update.", strings.Join(catalognext.SupportedPullOptions(), ", ")))
110+
flags.BoolVar(&noTools, "no-tools", false, "Exclude tools from output")
108111
return cmd
109112
}
110113

@@ -181,3 +184,62 @@ func pullCatalogNextCommand() *cobra.Command {
181184
},
182185
}
183186
}
187+
188+
func catalogNextServerCommand() *cobra.Command {
189+
cmd := &cobra.Command{
190+
Use: "server",
191+
Short: "Manage servers in catalogs",
192+
}
193+
194+
cmd.AddCommand(listCatalogNextServersCommand())
195+
196+
return cmd
197+
}
198+
199+
func listCatalogNextServersCommand() *cobra.Command {
200+
var opts struct {
201+
Filters []string
202+
Format string
203+
}
204+
205+
cmd := &cobra.Command{
206+
Use: "ls <oci-reference>",
207+
Aliases: []string{"list"},
208+
Short: "List servers in a catalog",
209+
Long: `List all servers in a catalog.
210+
211+
Use --filter to search for servers matching a query (case-insensitive substring matching on server names).
212+
Filters use key=value format (e.g., name=github).`,
213+
Example: ` # List all servers in a catalog
214+
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest
215+
216+
# Filter servers by name
217+
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest --filter name=github
218+
219+
# Combine multiple filters (using short flag)
220+
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest -f name=slack -f name=github
221+
222+
# Output in JSON format
223+
docker mcp catalog-next server ls mcp/docker-mcp-catalog:latest --format json`,
224+
Args: cobra.ExactArgs(1),
225+
RunE: func(cmd *cobra.Command, args []string) error {
226+
supported := slices.Contains(workingset.SupportedFormats(), opts.Format)
227+
if !supported {
228+
return fmt.Errorf("unsupported format: %s", opts.Format)
229+
}
230+
231+
dao, err := db.New()
232+
if err != nil {
233+
return err
234+
}
235+
236+
return catalognext.ListServers(cmd.Context(), dao, args[0], opts.Filters, workingset.OutputFormat(opts.Format))
237+
},
238+
}
239+
240+
flags := cmd.Flags()
241+
flags.StringArrayVarP(&opts.Filters, "filter", "f", []string{}, "Filter output (e.g., name=github)")
242+
flags.StringVar(&opts.Format, "format", string(workingset.OutputFormatHumanReadable), fmt.Sprintf("Supported: %s.", strings.Join(workingset.SupportedFormats(), ", ")))
243+
244+
return cmd
245+
}

pkg/catalog_next/server.go

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package catalognext
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"sort"
8+
"strings"
9+
10+
"github.com/goccy/go-yaml"
11+
12+
"github.com/docker/mcp-gateway/pkg/db"
13+
"github.com/docker/mcp-gateway/pkg/workingset"
14+
)
15+
16+
type serverFilter struct {
17+
key string
18+
value string
19+
}
20+
21+
// ListServers lists servers in a catalog with optional filtering
22+
func ListServers(ctx context.Context, dao db.DAO, catalogRef string, filters []string, format workingset.OutputFormat) error {
23+
parsedFilters, err := parseFilters(filters)
24+
if err != nil {
25+
return err
26+
}
27+
28+
// Get the catalog
29+
dbCatalog, err := dao.GetCatalog(ctx, catalogRef)
30+
if err != nil {
31+
return fmt.Errorf("failed to get catalog %s: %w", catalogRef, err)
32+
}
33+
34+
catalog := NewFromDb(dbCatalog)
35+
36+
// Apply name filter
37+
var nameFilter string
38+
for _, filter := range parsedFilters {
39+
switch filter.key {
40+
case "name":
41+
nameFilter = filter.value
42+
default:
43+
return fmt.Errorf("unsupported filter key: %s", filter.key)
44+
}
45+
}
46+
47+
// Filter servers
48+
servers := filterServers(catalog.Servers, nameFilter)
49+
50+
// Output results
51+
return outputServers(catalog.Ref, catalog.Title, servers, format)
52+
}
53+
54+
func parseFilters(filters []string) ([]serverFilter, error) {
55+
parsed := make([]serverFilter, 0, len(filters))
56+
for _, filter := range filters {
57+
parts := strings.SplitN(filter, "=", 2)
58+
if len(parts) != 2 {
59+
return nil, fmt.Errorf("invalid filter format: %s (expected key=value)", filter)
60+
}
61+
parsed = append(parsed, serverFilter{
62+
key: parts[0],
63+
value: parts[1],
64+
})
65+
}
66+
return parsed, nil
67+
}
68+
69+
func filterServers(servers []Server, nameFilter string) []Server {
70+
if nameFilter == "" {
71+
return servers
72+
}
73+
74+
nameLower := strings.ToLower(nameFilter)
75+
filtered := make([]Server, 0)
76+
77+
for _, server := range servers {
78+
if matchesNameFilter(server, nameLower) {
79+
filtered = append(filtered, server)
80+
}
81+
}
82+
83+
return filtered
84+
}
85+
86+
func matchesNameFilter(server Server, nameLower string) bool {
87+
if server.Snapshot == nil {
88+
return false
89+
}
90+
serverName := strings.ToLower(server.Snapshot.Server.Name)
91+
return strings.Contains(serverName, nameLower)
92+
}
93+
94+
func outputServers(catalogRef, catalogTitle string, servers []Server, format workingset.OutputFormat) error {
95+
// Sort servers by name
96+
sort.Slice(servers, func(i, j int) bool {
97+
if servers[i].Snapshot == nil || servers[j].Snapshot == nil {
98+
return false
99+
}
100+
return servers[i].Snapshot.Server.Name < servers[j].Snapshot.Server.Name
101+
})
102+
103+
var data []byte
104+
var err error
105+
106+
switch format {
107+
case workingset.OutputFormatHumanReadable:
108+
printServersHuman(catalogRef, catalogTitle, servers)
109+
return nil
110+
case workingset.OutputFormatJSON:
111+
output := map[string]any{
112+
"catalog": catalogRef,
113+
"title": catalogTitle,
114+
"servers": servers,
115+
}
116+
data, err = json.MarshalIndent(output, "", " ")
117+
case workingset.OutputFormatYAML:
118+
output := map[string]any{
119+
"catalog": catalogRef,
120+
"title": catalogTitle,
121+
"servers": servers,
122+
}
123+
data, err = yaml.Marshal(output)
124+
default:
125+
return fmt.Errorf("unsupported format: %s", format)
126+
}
127+
128+
if err != nil {
129+
return fmt.Errorf("failed to format servers: %w", err)
130+
}
131+
132+
fmt.Println(string(data))
133+
return nil
134+
}
135+
136+
func printServersHuman(catalogRef, catalogTitle string, servers []Server) {
137+
if len(servers) == 0 {
138+
fmt.Println("No servers found")
139+
return
140+
}
141+
142+
fmt.Printf("Catalog: %s\n", catalogRef)
143+
fmt.Printf("Title: %s\n", catalogTitle)
144+
fmt.Printf("Servers (%d):\n\n", len(servers))
145+
146+
for _, server := range servers {
147+
if server.Snapshot == nil {
148+
continue
149+
}
150+
srv := server.Snapshot.Server
151+
fmt.Printf(" %s\n", srv.Name)
152+
if srv.Title != "" {
153+
fmt.Printf(" Title: %s\n", srv.Title)
154+
}
155+
if srv.Description != "" {
156+
fmt.Printf(" Description: %s\n", srv.Description)
157+
}
158+
fmt.Printf(" Type: %s\n", server.Type)
159+
switch server.Type {
160+
case workingset.ServerTypeImage:
161+
fmt.Printf(" Image: %s\n", server.Image)
162+
case workingset.ServerTypeRegistry:
163+
fmt.Printf(" Source: %s\n", server.Source)
164+
case workingset.ServerTypeRemote:
165+
fmt.Printf(" Endpoint: %s\n", server.Endpoint)
166+
}
167+
if len(srv.Tools) > 0 {
168+
fmt.Printf(" Tools: %d\n", len(srv.Tools))
169+
}
170+
fmt.Println()
171+
}
172+
}

0 commit comments

Comments
 (0)