Skip to content

Commit f9e501b

Browse files
committed
wip
1 parent 10d3c1e commit f9e501b

File tree

7 files changed

+388
-0
lines changed

7 files changed

+388
-0
lines changed

cmd/docker-mcp/commands/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ func Root(ctx context.Context, cwd string, dockerCli command.Cli) *cobra.Command
8282
cmd.AddCommand(serverCommand(dockerClient, dockerCli))
8383
cmd.AddCommand(toolsCommand(dockerClient))
8484
cmd.AddCommand(versionCommand())
85+
cmd.AddCommand(workingSetCommand())
8586

8687
if os.Getenv("DOCKER_MCP_SHOW_HIDDEN") == "1" {
8788
unhideHiddenCommands(cmd)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
8+
workingset "github.com/docker/mcp-gateway/cmd/docker-mcp/working-set"
9+
)
10+
11+
func workingSetCommand() *cobra.Command {
12+
cmd := &cobra.Command{
13+
Use: "working-set",
14+
Aliases: []string{"ws"},
15+
Short: "Manage MCP server working-sets",
16+
Long: `Manage working-sets of MCP servers for organizing and grouping servers together.`,
17+
}
18+
cmd.AddCommand(createWorkingSetCommand())
19+
cmd.AddCommand(listWorkingSetCommand())
20+
cmd.AddCommand(showWorkingSetCommand())
21+
return cmd
22+
}
23+
24+
func createWorkingSetCommand() *cobra.Command {
25+
var opts struct {
26+
Name string
27+
Description string
28+
Servers []string
29+
}
30+
cmd := &cobra.Command{
31+
Use: "create --name <name> [--description <description>] --server <server1> --server <server2> ...",
32+
Short: "Create a new working-set of MCP servers",
33+
Long: `Create a new working-set that groups multiple MCP servers together.
34+
A working-set allows you to organize and manage related servers as a single unit.`,
35+
Example: ` # Create a working-set with multiple servers
36+
docker mcp working-set create --name dev-tools --description "Development tools" --server github --server slack
37+
38+
# Create a working-set with a single server
39+
docker mcp working-set create --name docker-only --server docker`,
40+
RunE: func(cmd *cobra.Command, args []string) error {
41+
return workingset.Create(opts.Name, opts.Description, opts.Servers)
42+
},
43+
}
44+
flags := cmd.Flags()
45+
flags.StringVar(&opts.Name, "name", "", "Name of the working-set (required)")
46+
flags.StringVar(&opts.Description, "description", "", "Description of the working-set")
47+
flags.StringArrayVar(&opts.Servers, "server", []string{}, "Server to include in the working-set (can be specified multiple times)")
48+
49+
_ = cmd.MarkFlagRequired("name")
50+
_ = cmd.MarkFlagRequired("server")
51+
52+
return cmd
53+
}
54+
55+
func listWorkingSetCommand() *cobra.Command {
56+
var opts struct {
57+
Format workingset.Format
58+
}
59+
cmd := &cobra.Command{
60+
Use: "ls",
61+
Aliases: []string{"list"},
62+
Short: "List all working-sets",
63+
Long: `List all configured working-sets and their associated servers.`,
64+
Example: ` # List all working-sets in human-readable format
65+
docker mcp working-set ls
66+
67+
# List working-sets in JSON format
68+
docker mcp working-set ls --format json
69+
70+
# List working-sets in YAML format
71+
docker mcp working-set ls --format yaml`,
72+
Args: cobra.NoArgs,
73+
RunE: func(cmd *cobra.Command, args []string) error {
74+
return workingset.List(opts.Format)
75+
},
76+
}
77+
flags := cmd.Flags()
78+
flags.Var(&opts.Format, "format", fmt.Sprintf("Output format. Supported: %s.", workingset.SupportedFormats()))
79+
return cmd
80+
}
81+
82+
func showWorkingSetCommand() *cobra.Command {
83+
var opts struct {
84+
Format workingset.Format
85+
}
86+
cmd := &cobra.Command{
87+
Use: "show <name>",
88+
Short: "Display working-set details",
89+
Long: `Display the details of a specific working-set including all its servers.`,
90+
Example: ` # Show a working-set in human-readable format
91+
docker mcp working-set show my-dev-tools
92+
93+
# Show a working-set in JSON format
94+
docker mcp working-set show my-dev-tools --format json
95+
96+
# Show a working-set in YAML format
97+
docker mcp working-set show my-dev-tools --format yaml`,
98+
Args: cobra.ExactArgs(1),
99+
RunE: func(cmd *cobra.Command, args []string) error {
100+
return workingset.Show(args[0], opts.Format)
101+
},
102+
}
103+
flags := cmd.Flags()
104+
flags.Var(&opts.Format, "format", fmt.Sprintf("Output format. Supported: %s.", workingset.SupportedFormats()))
105+
return cmd
106+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package workingset
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/docker/mcp-gateway/pkg/config"
7+
)
8+
9+
type Config struct {
10+
WorkingSets map[string]WorkingSet `json:"workingSets"`
11+
}
12+
13+
type WorkingSet struct {
14+
Name string `json:"name"`
15+
Description string `json:"description,omitempty"`
16+
Servers []string `json:"servers"`
17+
}
18+
19+
func ReadConfig() (*Config, error) {
20+
buf, err := config.ReadWorkingSets()
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
var result Config
26+
if len(buf) > 0 {
27+
if err := json.Unmarshal(buf, &result); err != nil {
28+
return nil, err
29+
}
30+
}
31+
32+
if result.WorkingSets == nil {
33+
result.WorkingSets = map[string]WorkingSet{}
34+
}
35+
36+
return &result, nil
37+
}
38+
39+
func WriteConfig(cfg *Config) error {
40+
if cfg.WorkingSets == nil {
41+
cfg.WorkingSets = map[string]WorkingSet{}
42+
}
43+
44+
data, err := json.MarshalIndent(cfg, "", " ")
45+
if err != nil {
46+
return err
47+
}
48+
49+
return config.WriteWorkingSets(data)
50+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package workingset
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
func Create(name, description string, servers []string) error {
8+
// Validate input
9+
if name == "" {
10+
return fmt.Errorf("working-set name cannot be empty")
11+
}
12+
13+
if len(servers) == 0 {
14+
return fmt.Errorf("at least one server must be specified")
15+
}
16+
17+
// Read existing config
18+
cfg, err := ReadConfig()
19+
if err != nil {
20+
return fmt.Errorf("failed to read working-sets config: %w", err)
21+
}
22+
23+
// Check if working-set already exists
24+
if _, exists := cfg.WorkingSets[name]; exists {
25+
return fmt.Errorf("working-set %q already exists", name)
26+
}
27+
28+
// Create new working-set
29+
cfg.WorkingSets[name] = WorkingSet{
30+
Name: name,
31+
Description: description,
32+
Servers: servers,
33+
}
34+
35+
// Write config
36+
if err := WriteConfig(cfg); err != nil {
37+
return fmt.Errorf("failed to write working-sets config: %w", err)
38+
}
39+
40+
fmt.Printf("Created working-set %q with %d server(s)\n", name, len(servers))
41+
return nil
42+
}

cmd/docker-mcp/working-set/list.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package workingset
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"sort"
7+
"strings"
8+
9+
"gopkg.in/yaml.v3"
10+
)
11+
12+
type Format string
13+
14+
const (
15+
JSON Format = "json"
16+
YAML Format = "yaml"
17+
)
18+
19+
var supportedFormats = []Format{JSON, YAML}
20+
21+
func (e *Format) String() string {
22+
return string(*e)
23+
}
24+
25+
func (e *Format) Set(v string) error {
26+
actual := Format(v)
27+
for _, allowed := range supportedFormats {
28+
if allowed == actual {
29+
*e = actual
30+
return nil
31+
}
32+
}
33+
return fmt.Errorf("must be one of %s", SupportedFormats())
34+
}
35+
36+
// Type is only used in help text
37+
func (e *Format) Type() string {
38+
return "format"
39+
}
40+
41+
func SupportedFormats() string {
42+
var quoted []string
43+
for _, v := range supportedFormats {
44+
quoted = append(quoted, "\""+string(v)+"\"")
45+
}
46+
return strings.Join(quoted, ", ")
47+
}
48+
49+
// WorkingSetMetadata contains summary info about a working-set (excluding full server list)
50+
type WorkingSetMetadata struct {
51+
Description string `json:"description,omitempty" yaml:"description,omitempty"`
52+
ServerCount int `json:"serverCount" yaml:"serverCount"`
53+
}
54+
55+
type ListOutput struct {
56+
WorkingSets map[string]WorkingSetMetadata `json:"workingSets" yaml:"workingSets"`
57+
}
58+
59+
func List(format Format) error {
60+
cfg, err := ReadConfig()
61+
if err != nil {
62+
return fmt.Errorf("failed to read working-sets config: %w", err)
63+
}
64+
65+
switch format {
66+
case JSON:
67+
output := buildListOutput(cfg.WorkingSets)
68+
data, err := json.MarshalIndent(output, "", " ")
69+
if err != nil {
70+
return fmt.Errorf("failed to marshal to JSON: %w", err)
71+
}
72+
fmt.Println(string(data))
73+
case YAML:
74+
output := buildListOutput(cfg.WorkingSets)
75+
data, err := yaml.Marshal(output)
76+
if err != nil {
77+
return fmt.Errorf("failed to marshal to YAML: %w", err)
78+
}
79+
fmt.Print(string(data))
80+
default:
81+
humanPrintWorkingSetsList(cfg.WorkingSets)
82+
}
83+
84+
return nil
85+
}
86+
87+
func buildListOutput(workingSets map[string]WorkingSet) ListOutput {
88+
output := ListOutput{
89+
WorkingSets: make(map[string]WorkingSetMetadata),
90+
}
91+
92+
for name, ws := range workingSets {
93+
output.WorkingSets[name] = WorkingSetMetadata{
94+
Description: ws.Description,
95+
ServerCount: len(ws.Servers),
96+
}
97+
}
98+
99+
return output
100+
}
101+
102+
func humanPrintWorkingSetsList(workingSets map[string]WorkingSet) {
103+
if len(workingSets) == 0 {
104+
fmt.Println("No working-sets configured.")
105+
return
106+
}
107+
108+
// Sort by name for consistent output
109+
names := make([]string, 0, len(workingSets))
110+
for name := range workingSets {
111+
names = append(names, name)
112+
}
113+
sort.Strings(names)
114+
115+
for _, name := range names {
116+
ws := workingSets[name]
117+
if ws.Description != "" {
118+
fmt.Printf("%s: %s\n", name, ws.Description)
119+
} else {
120+
fmt.Println(name)
121+
}
122+
}
123+
}

cmd/docker-mcp/working-set/show.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package workingset
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"gopkg.in/yaml.v3"
8+
)
9+
10+
func Show(name string, format Format) error {
11+
cfg, err := ReadConfig()
12+
if err != nil {
13+
return fmt.Errorf("failed to read working-sets config: %w", err)
14+
}
15+
16+
ws, exists := cfg.WorkingSets[name]
17+
if !exists {
18+
return fmt.Errorf("working-set %q not found", name)
19+
}
20+
21+
switch format {
22+
case JSON:
23+
data, err := json.MarshalIndent(ws, "", " ")
24+
if err != nil {
25+
return fmt.Errorf("failed to marshal to JSON: %w", err)
26+
}
27+
fmt.Println(string(data))
28+
case YAML:
29+
data, err := yaml.Marshal(ws)
30+
if err != nil {
31+
return fmt.Errorf("failed to marshal to YAML: %w", err)
32+
}
33+
fmt.Print(string(data))
34+
default:
35+
humanPrintWorkingSet(name, ws)
36+
}
37+
38+
return nil
39+
}
40+
41+
func humanPrintWorkingSet(name string, ws WorkingSet) {
42+
fmt.Println()
43+
fmt.Printf(" \033[1m%s\033[0m\n", name)
44+
if ws.Description != "" {
45+
fmt.Printf(" Description: %s\n", ws.Description)
46+
}
47+
fmt.Println()
48+
fmt.Println(" Servers:")
49+
for _, server := range ws.Servers {
50+
fmt.Printf(" - %s\n", server)
51+
}
52+
fmt.Println()
53+
}

0 commit comments

Comments
 (0)