Skip to content

Commit 92ccd15

Browse files
committed
feat: Add ability to add/remove servers
Add functionality to add and remove servers from a workingset. Can add/remove by OCI ref or registry URI.
1 parent dbfa331 commit 92ccd15

File tree

3 files changed

+471
-0
lines changed

3 files changed

+471
-0
lines changed

cmd/docker-mcp/commands/workingset.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func workingSetCommand() *cobra.Command {
2828
cmd.AddCommand(pullWorkingSetCommand())
2929
cmd.AddCommand(createWorkingSetCommand())
3030
cmd.AddCommand(removeWorkingSetCommand())
31+
cmd.AddCommand(workingsetServerCommand())
3132
cmd.AddCommand(configWorkingSetCommand())
3233
return cmd
3334
}
@@ -294,3 +295,85 @@ Use --workingset to show servers only from a specific working set.`,
294295

295296
return cmd
296297
}
298+
299+
func workingsetServerCommand() *cobra.Command {
300+
cmd := &cobra.Command{
301+
Use: "server",
302+
Short: "Manage servers in working sets",
303+
}
304+
305+
cmd.AddCommand(addServerCommand())
306+
cmd.AddCommand(removeServerCommand())
307+
308+
return cmd
309+
}
310+
311+
func addServerCommand() *cobra.Command {
312+
var servers []string
313+
314+
cmd := &cobra.Command{
315+
Use: "add <working-set-id> --server <ref1> --server <ref2> ...",
316+
Short: "Add MCP servers to a working set",
317+
Long: "Add MCP servers to a working set.",
318+
Example: ` # Add servers with OCI references
319+
docker mcp workingset server add my-working-set --server docker://mcp/github:latest --server docker://mcp/slack:latest
320+
321+
# Add servers with MCP Registry references
322+
docker mcp workingset server add my-working-set --server http://registry.modelcontextprotocol.io/v0/servers/71de5a2a-6cfb-4250-a196-f93080ecc860
323+
324+
# Mix MCP Registry references and OCI references
325+
docker mcp workingset server add my-working-set --server http://registry.modelcontextprotocol.io/v0/servers/71de5a2a-6cfb-4250-a196-f93080ecc860 --server docker://mcp/github:latest`,
326+
Args: cobra.ExactArgs(1),
327+
RunE: func(cmd *cobra.Command, args []string) error {
328+
if len(servers) == 0 {
329+
return fmt.Errorf("at least one --server flag must be specified")
330+
}
331+
dao, err := db.New()
332+
if err != nil {
333+
return err
334+
}
335+
registryClient := registryapi.NewClient()
336+
ociService := oci.NewService()
337+
return workingset.AddServers(cmd.Context(), dao, registryClient, ociService, args[0], servers)
338+
},
339+
}
340+
341+
flags := cmd.Flags()
342+
flags.StringArrayVar(&servers, "server", []string{}, "Server to include: MCP Registry reference or OCI reference with docker:// prefix (can be specified multiple times)")
343+
344+
return cmd
345+
}
346+
347+
func removeServerCommand() *cobra.Command {
348+
var servers []string
349+
350+
cmd := &cobra.Command{
351+
Use: "remove <working-set-id> --server <ref1> --server <ref2> ...",
352+
Short: "Remove MCP servers from a working set",
353+
Long: "Remove MCP servers from a working set by server reference.",
354+
Example: ` # Remove servers with OCI references
355+
docker mcp workingset server remove my-working-set --server docker://mcp/github:latest --server docker://mcp/slack:latest
356+
357+
# Remove servers with MCP Registry references
358+
docker mcp workingset server remove my-working-set --server http://registry.modelcontextprotocol.io/v0/servers/71de5a2a-6cfb-4250-a196-f93080ecc860
359+
360+
# Mix MCP Registry references and OCI references
361+
docker mcp workingset server remove my-working-set --server http://registry.modelcontextprotocol.io/v0/servers/71de5a2a-6cfb-4250-a196-f93080ecc860 --server docker://mcp/github:latest`,
362+
Args: cobra.ExactArgs(1),
363+
RunE: func(cmd *cobra.Command, args []string) error {
364+
if len(servers) == 0 {
365+
return fmt.Errorf("at least one --server flag must be specified")
366+
}
367+
dao, err := db.New()
368+
if err != nil {
369+
return err
370+
}
371+
return workingset.RemoveServers(cmd.Context(), dao, args[0], servers)
372+
},
373+
}
374+
375+
flags := cmd.Flags()
376+
flags.StringArrayVar(&servers, "server", []string{}, "Server to remove: MCP Registry reference or OCI reference with docker:// prefix (can be specified multiple times)")
377+
378+
return cmd
379+
}

pkg/workingset/server.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package workingset
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"fmt"
8+
"strings"
9+
10+
"github.com/docker/mcp-gateway/pkg/db"
11+
"github.com/docker/mcp-gateway/pkg/oci"
12+
"github.com/docker/mcp-gateway/pkg/registryapi"
13+
)
14+
15+
func AddServers(ctx context.Context, dao db.DAO, registryClient registryapi.Client, ociService oci.Service, id string, servers []string) error {
16+
if len(servers) == 0 {
17+
return fmt.Errorf("at least one server must be specified")
18+
}
19+
20+
dbWorkingSet, err := dao.GetWorkingSet(ctx, id)
21+
if err != nil {
22+
if errors.Is(err, sql.ErrNoRows) {
23+
return fmt.Errorf("working set %s not found", id)
24+
}
25+
return fmt.Errorf("failed to get working set: %w", err)
26+
}
27+
28+
workingSet := NewFromDb(dbWorkingSet)
29+
30+
if err := workingSet.EnsureSnapshotsResolved(ctx, ociService); err != nil {
31+
return fmt.Errorf("failed to resolve snapshots: %w", err)
32+
}
33+
34+
newServers := make([]Server, len(servers))
35+
for i, server := range servers {
36+
s, err := resolveServerFromString(ctx, registryClient, ociService, server)
37+
if err != nil {
38+
return fmt.Errorf("invalid server value: %w", err)
39+
}
40+
newServers[i] = s
41+
}
42+
43+
workingSet.Servers = append(workingSet.Servers, newServers...)
44+
45+
if err := workingSet.Validate(); err != nil {
46+
return fmt.Errorf("invalid working set: %w", err)
47+
}
48+
49+
err = dao.UpdateWorkingSet(ctx, workingSet.ToDb())
50+
if err != nil {
51+
return fmt.Errorf("failed to update working set: %w", err)
52+
}
53+
54+
fmt.Printf("Added %d server(s) to working set %s\n", len(newServers), id)
55+
56+
return nil
57+
}
58+
59+
func RemoveServers(ctx context.Context, dao db.DAO, id string, serverRefs []string) error {
60+
if len(serverRefs) == 0 {
61+
return fmt.Errorf("at least one server must be specified")
62+
}
63+
64+
dbWorkingSet, err := dao.GetWorkingSet(ctx, id)
65+
if err != nil {
66+
if errors.Is(err, sql.ErrNoRows) {
67+
return fmt.Errorf("working set %s not found", id)
68+
}
69+
return fmt.Errorf("failed to get working set: %w", err)
70+
}
71+
72+
workingSet := NewFromDb(dbWorkingSet)
73+
74+
// Build a set of servers to remove (strip protocol scheme for comparison)
75+
refsToRemove := make(map[string]bool)
76+
for _, ref := range serverRefs {
77+
normalized := stripProtocol(ref)
78+
refsToRemove[normalized] = true
79+
}
80+
81+
// Filter out the servers to remove
82+
originalCount := len(workingSet.Servers)
83+
filtered := make([]Server, 0, len(workingSet.Servers))
84+
for _, server := range workingSet.Servers {
85+
shouldKeep := true
86+
87+
switch server.Type {
88+
case ServerTypeImage:
89+
if refsToRemove[stripProtocol(server.Image)] {
90+
shouldKeep = false
91+
}
92+
case ServerTypeRegistry:
93+
if refsToRemove[stripProtocol(server.Source)] {
94+
shouldKeep = false
95+
}
96+
}
97+
98+
if shouldKeep {
99+
filtered = append(filtered, server)
100+
}
101+
}
102+
103+
removedCount := originalCount - len(filtered)
104+
if removedCount == 0 {
105+
return fmt.Errorf("no matching servers found to remove")
106+
}
107+
108+
workingSet.Servers = filtered
109+
110+
if err := workingSet.Validate(); err != nil {
111+
return fmt.Errorf("invalid working set: %w", err)
112+
}
113+
114+
err = dao.UpdateWorkingSet(ctx, workingSet.ToDb())
115+
if err != nil {
116+
return fmt.Errorf("failed to update working set: %w", err)
117+
}
118+
119+
fmt.Printf("Removed %d server(s) from working set %s\n", removedCount, id)
120+
121+
return nil
122+
}
123+
124+
// stripProtocol removes the protocol scheme (everything before and including "://") from a URI
125+
func stripProtocol(uri string) string {
126+
if idx := strings.Index(uri, "://"); idx != -1 {
127+
return uri[idx+3:]
128+
}
129+
return uri
130+
}

0 commit comments

Comments
 (0)