From b04de0a11224c106a0190563edb6532abda26e59 Mon Sep 17 00:00:00 2001 From: Justin Chang Date: Wed, 4 Mar 2026 18:58:59 -0800 Subject: [PATCH 1/3] Improve error message when pulling a nonexistent catalog Detect MANIFEST_UNKNOWN and NAME_UNKNOWN OCI registry errors and return a clear, concise message instead of the raw transport error. --- pkg/catalog_next/pull.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pkg/catalog_next/pull.go b/pkg/catalog_next/pull.go index 391ca2d16..2b5282a3f 100644 --- a/pkg/catalog_next/pull.go +++ b/pkg/catalog_next/pull.go @@ -2,10 +2,12 @@ package catalognext import ( "context" + "errors" "fmt" "time" "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/docker/mcp-gateway/pkg/db" "github.com/docker/mcp-gateway/pkg/oci" @@ -41,6 +43,9 @@ func pullCatalog(ctx context.Context, dao db.DAO, ociService oci.Service, refStr catalogArtifact, err := oci.ReadArtifact[CatalogArtifact](refStr, MCPCatalogArtifactType) if err != nil { + if isNotFoundError(err) { + return nil, fmt.Errorf("catalog not found: %s", oci.FullNameWithoutDigest(ref)) + } return nil, fmt.Errorf("failed to read OCI catalog: %w", err) } @@ -88,3 +93,18 @@ func pullCatalog(ctx context.Context, dao db.DAO, ociService oci.Service, refStr return &dbCatalog, nil } + +// isNotFoundError checks if the error is an OCI registry "not found" response +// (MANIFEST_UNKNOWN or NAME_UNKNOWN). +func isNotFoundError(err error) bool { + var transportErr *transport.Error + if !errors.As(err, &transportErr) { + return false + } + for _, diagnostic := range transportErr.Errors { + if diagnostic.Code == transport.ManifestUnknownErrorCode || diagnostic.Code == transport.NameUnknownErrorCode { + return true + } + } + return false +} From 139094acfa51f1fc73aa960007ac90c8679c6c6b Mon Sep 17 00:00:00 2001 From: Justin Chang Date: Wed, 4 Mar 2026 19:04:41 -0800 Subject: [PATCH 2/3] Add unit tests for isNotFoundError --- pkg/catalog_next/pull_test.go | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 pkg/catalog_next/pull_test.go diff --git a/pkg/catalog_next/pull_test.go b/pkg/catalog_next/pull_test.go new file mode 100644 index 000000000..08965c04e --- /dev/null +++ b/pkg/catalog_next/pull_test.go @@ -0,0 +1,63 @@ +package catalognext + +import ( + "fmt" + "net/http" + "testing" + + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/stretchr/testify/assert" +) + +func TestIsNotFoundError(t *testing.T) { + tests := []struct { + name string + err error + expected bool + }{ + { + name: "manifest unknown", + err: &transport.Error{Errors: []transport.Diagnostic{{Code: transport.ManifestUnknownErrorCode}}}, + expected: true, + }, + { + name: "name unknown", + err: &transport.Error{Errors: []transport.Diagnostic{{Code: transport.NameUnknownErrorCode}}}, + expected: true, + }, + { + name: "wrapped manifest unknown", + err: fmt.Errorf("fetch failed: %w", &transport.Error{Errors: []transport.Diagnostic{{Code: transport.ManifestUnknownErrorCode}}}), + expected: true, + }, + { + name: "unauthorized error", + err: &transport.Error{Errors: []transport.Diagnostic{{Code: transport.UnauthorizedErrorCode}}}, + expected: false, + }, + { + name: "non-transport error", + err: fmt.Errorf("network timeout"), + expected: false, + }, + { + name: "transport error with no diagnostics", + err: &transport.Error{StatusCode: http.StatusNotFound}, + expected: false, + }, + { + name: "multiple diagnostics with one match", + err: &transport.Error{Errors: []transport.Diagnostic{ + {Code: transport.UnauthorizedErrorCode}, + {Code: transport.ManifestUnknownErrorCode}, + }}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, isNotFoundError(tt.err)) + }) + } +} From bc713cc289eb565b9d37c0cb221ab193f96df367 Mon Sep 17 00:00:00 2001 From: Justin Chang Date: Wed, 4 Mar 2026 19:07:46 -0800 Subject: [PATCH 3/3] Use original input in catalog not-found error message FullNameWithoutDigest strips digests and defaults to :latest, which would misreport the identifier for digest-based pulls. --- pkg/catalog_next/pull.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/catalog_next/pull.go b/pkg/catalog_next/pull.go index 2b5282a3f..1f1165a96 100644 --- a/pkg/catalog_next/pull.go +++ b/pkg/catalog_next/pull.go @@ -44,7 +44,7 @@ func pullCatalog(ctx context.Context, dao db.DAO, ociService oci.Service, refStr catalogArtifact, err := oci.ReadArtifact[CatalogArtifact](refStr, MCPCatalogArtifactType) if err != nil { if isNotFoundError(err) { - return nil, fmt.Errorf("catalog not found: %s", oci.FullNameWithoutDigest(ref)) + return nil, fmt.Errorf("catalog not found: %s", refStr) } return nil, fmt.Errorf("failed to read OCI catalog: %w", err) }