Skip to content

Commit 5dd97e4

Browse files
jchangxclaude
andcommitted
fix: include poci servers in catalog-next legacy catalog import
When converting a legacy catalog to catalog-next format, `createCatalogFromLegacyCatalog` only handled `server` and `remote` types, silently dropping `poci` servers (e.g. curl, ffmpeg, docker). This caused poci servers to disappear from the Docker Desktop catalog when the MCPWorkingSets feature flag was enabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 64e5b0f commit 5dd97e4

File tree

6 files changed

+135
-2
lines changed

6 files changed

+135
-2
lines changed

pkg/catalog_next/catalog.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ const (
5252
const CommunityRegistryCatalogRef = "mcp/community-registry:latest"
5353

5454
type Server struct {
55-
Type workingset.ServerType `yaml:"type" json:"type" validate:"required,oneof=registry image remote"`
55+
Type workingset.ServerType `yaml:"type" json:"type" validate:"required,oneof=registry image remote poci"`
5656
Tools []string `yaml:"tools,omitempty" json:"tools,omitempty"`
5757
// Policy describes the policy decision for this server.
5858
Policy *policy.Decision `yaml:"policy,omitempty" json:"policy,omitempty"`
@@ -85,6 +85,7 @@ func NewFromDb(dbCatalog *db.Catalog) CatalogWithDigest {
8585
if server.ServerType == "remote" {
8686
servers[i].Endpoint = server.Endpoint
8787
}
88+
// poci servers don't need image/endpoint/source — tools contain inline container specs
8889
if server.Snapshot != nil {
8990
servers[i].Snapshot = &workingset.ServerSnapshot{
9091
Server: server.Snapshot.Server,

pkg/catalog_next/create.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ func createCatalogFromLegacyCatalog(ctx context.Context, legacyCatalogURL string
168168
}
169169
s.Snapshot.Server.Name = name
170170
servers = append(servers, s)
171+
} else if server.Type == "poci" {
172+
s := Server{
173+
Type: workingset.ServerTypePoci,
174+
Snapshot: &workingset.ServerSnapshot{
175+
Server: server,
176+
},
177+
}
178+
s.Snapshot.Server.Name = name
179+
servers = append(servers, s)
171180
}
172181
}
173182

pkg/catalog_next/create_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,97 @@ registry:
405405
assert.Equal(t, "Another test server", catalog.Servers[1].Snapshot.Server.Description)
406406
}
407407

408+
func TestCreateFromLegacyCatalogWithPociServers(t *testing.T) {
409+
dao := setupTestDB(t)
410+
ctx := t.Context()
411+
412+
// Create a temporary legacy catalog file with server, remote, and poci types
413+
tempDir := t.TempDir()
414+
catalogFile := filepath.Join(tempDir, "test-catalog.yaml")
415+
416+
legacyCatalogYAML := `name: test-catalog
417+
registry:
418+
my-server:
419+
title: "My Server"
420+
type: "server"
421+
image: "docker/test-server:latest"
422+
description: "A regular server"
423+
my-remote:
424+
title: "My Remote"
425+
type: "remote"
426+
description: "A remote server"
427+
remote:
428+
url: "https://remote.example.com/mcp"
429+
curl:
430+
title: "Curl"
431+
type: "poci"
432+
description: "Standard curl tool."
433+
tools:
434+
- name: curl
435+
description: "Run a curl command."
436+
container:
437+
image: "alpine/curl"
438+
command:
439+
- "{{args|into}}"
440+
ffmpeg:
441+
title: "FFmpeg"
442+
type: "poci"
443+
description: "Use ffmpeg to process video files."
444+
tools:
445+
- name: ffmpeg
446+
description: "run the ffmpeg command"
447+
container:
448+
image: "linuxserver/ffmpeg:version-7.1-cli"
449+
command:
450+
- "{{args|into}}"
451+
`
452+
453+
err := os.WriteFile(catalogFile, []byte(legacyCatalogYAML), 0o644)
454+
require.NoError(t, err)
455+
456+
// Create catalog from legacy catalog
457+
output := captureStdout(t, func() {
458+
err := Create(ctx, dao, getMockRegistryClient(), getMockOciService(), "test/poci-test:latest", []string{}, "", catalogFile, "", "Poci Test Catalog", false)
459+
require.NoError(t, err)
460+
})
461+
462+
assert.Contains(t, output, "Catalog test/poci-test:latest created")
463+
464+
// Verify the catalog was created
465+
catalogs, err := dao.ListCatalogs(ctx)
466+
require.NoError(t, err)
467+
assert.Len(t, catalogs, 1)
468+
469+
catalog := NewFromDb(&catalogs[0])
470+
assert.Equal(t, "Poci Test Catalog", catalog.Title)
471+
assert.Len(t, catalog.Servers, 4)
472+
473+
// Verify all server types are present (order may vary due to map iteration)
474+
serversByName := map[string]Server{}
475+
for _, s := range catalog.Servers {
476+
serversByName[s.Snapshot.Server.Name] = s
477+
}
478+
479+
// Verify poci servers
480+
curlServer := serversByName["curl"]
481+
assert.Equal(t, workingset.ServerTypePoci, curlServer.Type)
482+
assert.Equal(t, "Curl", curlServer.Snapshot.Server.Title)
483+
assert.Equal(t, "Standard curl tool.", curlServer.Snapshot.Server.Description)
484+
assert.Empty(t, curlServer.Image)
485+
486+
ffmpegServer := serversByName["ffmpeg"]
487+
assert.Equal(t, workingset.ServerTypePoci, ffmpegServer.Type)
488+
assert.Equal(t, "FFmpeg", ffmpegServer.Snapshot.Server.Title)
489+
assert.Empty(t, ffmpegServer.Image)
490+
491+
// Verify other types still work
492+
remoteServer := serversByName["my-remote"]
493+
assert.Equal(t, workingset.ServerTypeRemote, remoteServer.Type)
494+
495+
imageServer := serversByName["my-server"]
496+
assert.Equal(t, workingset.ServerTypeImage, imageServer.Type)
497+
}
498+
408499
func TestCreateFromLegacyCatalogWithRemoveExistingWithSameContent(t *testing.T) {
409500
dao := setupTestDB(t)
410501
ctx := t.Context()

pkg/catalog_next/server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ func printServersHuman(catalogRef, catalogTitle string, catalogPolicy *policy.De
253253
fmt.Printf(" Source: %s\n", server.Source)
254254
case workingset.ServerTypeRemote:
255255
fmt.Printf(" Endpoint: %s\n", server.Endpoint)
256+
case workingset.ServerTypePoci:
257+
// poci servers have no top-level image/endpoint; tools define containers inline
256258
}
257259
if showPolicy {
258260
fmt.Printf(" Policy: %s\n", policycli.StatusMessage(server.Policy))
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
-- Add support for poci server type
2+
-- SQLite doesn't support modifying CHECK constraints, so we need to recreate the table
3+
4+
CREATE TABLE catalog_server_new (
5+
id integer primary key,
6+
server_type text check(server_type in ('registry', 'image', 'remote', 'poci')),
7+
tools text CHECK (json_valid(tools)),
8+
source text,
9+
image text,
10+
endpoint text,
11+
snapshot text CHECK (json_valid(snapshot)),
12+
catalog_ref text not null,
13+
foreign key (catalog_ref) references catalog(ref) on delete cascade
14+
);
15+
16+
-- Copy existing data
17+
INSERT INTO catalog_server_new (id, server_type, tools, source, image, endpoint, snapshot, catalog_ref)
18+
SELECT id, server_type, tools, source, image, endpoint, snapshot, catalog_ref
19+
FROM catalog_server;
20+
21+
DROP TABLE catalog_server;
22+
23+
ALTER TABLE catalog_server_new RENAME TO catalog_server;

pkg/workingset/workingset.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ const (
4646
ServerTypeRegistry ServerType = "registry"
4747
ServerTypeImage ServerType = "image"
4848
ServerTypeRemote ServerType = "remote"
49+
ServerTypePoci ServerType = "poci"
4950
)
5051

5152
// Server represents a server configuration in a working set
5253
type Server struct {
53-
Type ServerType `yaml:"type" json:"type" validate:"required,oneof=registry image remote"`
54+
Type ServerType `yaml:"type" json:"type" validate:"required,oneof=registry image remote poci"`
5455
Config map[string]any `yaml:"config,omitempty" json:"config,omitempty"`
5556
Secrets string `yaml:"secrets,omitempty" json:"secrets,omitempty"`
5657
Tools ToolList `yaml:"tools,omitempty" json:"tools"` // See IsZero() below
@@ -530,6 +531,12 @@ func ResolveFile(ctx context.Context, value string) ([]Server, error) {
530531
Secrets: "default",
531532
Snapshot: &ServerSnapshot{Server: server},
532533
}
534+
} else if server.Type == "poci" && server.Image == "" {
535+
serversResolved[i] = Server{
536+
Type: ServerTypePoci,
537+
Secrets: "default",
538+
Snapshot: &ServerSnapshot{Server: server},
539+
}
533540
} else if server.Type == "remote" {
534541
serversResolved[i] = Server{
535542
Type: ServerTypeRemote,

0 commit comments

Comments
 (0)