Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -260,13 +260,6 @@ tasks:
cmds:
- ./{{.BUILD_DIR}}/registry-builder version

sync-schema:
desc: Sync schema reference with Go dependency version
cmds:
- echo "🔄 Syncing schema version with Go dependency..."
- ./scripts/sync-schema-version.sh
- echo "✅ Schema sync complete. Run 'task validate' to verify."

watch:
desc: Watch for changes and rebuild (requires entr)
cmds:
Expand Down
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
module github.com/stacklok/toolhive-registry

go 1.24.5
go 1.24.6

require (
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/modelcontextprotocol/registry v1.0.0
github.com/modelcontextprotocol/registry v1.3.5
github.com/spf13/cobra v1.10.1
github.com/stacklok/toolhive v0.3.7
github.com/stretchr/testify v1.11.1
Expand Down Expand Up @@ -60,7 +59,7 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-chi/chi/v5 v5.2.3 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
Expand Down Expand Up @@ -93,6 +92,7 @@ require (
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/go-containerregistry v0.20.6 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
Expand Down Expand Up @@ -165,7 +165,7 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/oauth2 v0.31.0 // indirect
golang.org/x/sync v0.17.0 // indirect
Expand Down
20 changes: 10 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -749,8 +749,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/coreos/go-oidc/v3 v3.15.0 h1:R6Oz8Z4bqWR7VFQ+sPSvZPQv4x8M+sJkDO5ojgwlyAg=
github.com/coreos/go-oidc/v3 v3.15.0/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q=
Expand Down Expand Up @@ -828,8 +828,8 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
Expand Down Expand Up @@ -1070,8 +1070,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b h1:ZGiXF8sz7PDk6RgkP+A/SFfUD0ZR/AgG6SpRNEDKZy8=
Expand Down Expand Up @@ -1131,8 +1131,8 @@ github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7z
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modelcontextprotocol/registry v1.0.0 h1:RxTSh2tC05Mlc3B2AzY/Oos1Fthuwe+OrK6a/17OCRE=
github.com/modelcontextprotocol/registry v1.0.0/go.mod h1:D6U1q6wYKYMA58q2gZz4eFsghr+fTkZQY8/ZFwTOT1Q=
github.com/modelcontextprotocol/registry v1.3.5 h1:M1ZTKPkxVICKlFBYio01/CkHbpjMcaGjLFF5M5QgZxc=
github.com/modelcontextprotocol/registry v1.3.5/go.mod h1:68KOBW2R5FX53BTrid2OFvPoCKEEYZk6z8LUa2ahLM0=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
Expand Down Expand Up @@ -1434,8 +1434,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
258 changes: 258 additions & 0 deletions pkg/registry/converters/converters_fixture_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package converters

import (
"encoding/json"
"os"
"path/filepath"
"testing"

upstream "github.com/modelcontextprotocol/registry/pkg/api/v0"
"github.com/stacklok/toolhive/pkg/registry"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestConverters_Fixtures validates converter functions using JSON fixture files
// This provides a clear, maintainable way to test conversions with real-world data
func TestConverters_Fixtures(t *testing.T) {
t.Parallel()

tests := []struct {
name string
fixtureDir string
inputFile string
expectedFile string
serverName string
convertFunc string // "ImageToServer", "ServerToImage", "RemoteToServer", "ServerToRemote"
validateFunc func(t *testing.T, input, output []byte)
}{
{
name: "ImageMetadata to ServerJSON - GitHub",
fixtureDir: "testdata/image_to_server",
inputFile: "input_github.json",
expectedFile: "expected_github.json",
serverName: "github",
convertFunc: "ImageToServer",
validateFunc: validateImageToServerConversion,
},
{
name: "ServerJSON to ImageMetadata - GitHub",
fixtureDir: "testdata/server_to_image",
inputFile: "input_github.json",
expectedFile: "expected_github.json",
serverName: "",
convertFunc: "ServerToImage",
validateFunc: validateServerToImageConversion,
},
{
name: "RemoteServerMetadata to ServerJSON - Example",
fixtureDir: "testdata/remote_to_server",
inputFile: "input_example.json",
expectedFile: "expected_example.json",
serverName: "example-remote",
convertFunc: "RemoteToServer",
validateFunc: validateRemoteToServerConversion,
},
{
name: "ServerJSON to RemoteServerMetadata - Example",
fixtureDir: "testdata/server_to_remote",
inputFile: "input_example.json",
expectedFile: "expected_example.json",
serverName: "",
convertFunc: "ServerToRemote",
validateFunc: validateServerToRemoteConversion,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

// Read input fixture
inputPath := filepath.Join(tt.fixtureDir, tt.inputFile)
inputData, err := os.ReadFile(inputPath)
require.NoError(t, err, "Failed to read input fixture: %s", inputPath)

// Read expected output fixture
expectedPath := filepath.Join(tt.fixtureDir, tt.expectedFile)
expectedData, err := os.ReadFile(expectedPath)
require.NoError(t, err, "Failed to read expected fixture: %s", expectedPath)

// Perform conversion based on type
var actualData []byte
switch tt.convertFunc {
case "ImageToServer":
actualData = convertImageToServer(t, inputData, tt.serverName)
case "ServerToImage":
actualData = convertServerToImage(t, inputData)
case "RemoteToServer":
actualData = convertRemoteToServer(t, inputData, tt.serverName)
case "ServerToRemote":
actualData = convertServerToRemote(t, inputData)
default:
t.Fatalf("Unknown conversion function: %s", tt.convertFunc)
}

// Compare output with expected
var expected, actual interface{}
require.NoError(t, json.Unmarshal(expectedData, &expected), "Failed to parse expected JSON")
require.NoError(t, json.Unmarshal(actualData, &actual), "Failed to parse actual JSON")

// Deep equal comparison
assert.Equal(t, expected, actual, "Conversion output doesn't match expected fixture")

// Run additional validation if provided
if tt.validateFunc != nil {
tt.validateFunc(t, inputData, actualData)
}
})
}
}

// Helper functions for conversions

func convertImageToServer(t *testing.T, inputData []byte, serverName string) []byte {
t.Helper()
var imageMetadata registry.ImageMetadata
require.NoError(t, json.Unmarshal(inputData, &imageMetadata))

serverJSON, err := ImageMetadataToServerJSON(serverName, &imageMetadata)
require.NoError(t, err)

output, err := json.MarshalIndent(serverJSON, "", " ")
require.NoError(t, err)
return output
}

func convertServerToImage(t *testing.T, inputData []byte) []byte {
t.Helper()
var serverJSON upstream.ServerJSON
require.NoError(t, json.Unmarshal(inputData, &serverJSON))

imageMetadata, err := ServerJSONToImageMetadata(&serverJSON)
require.NoError(t, err)

output, err := json.MarshalIndent(imageMetadata, "", " ")
require.NoError(t, err)
return output
}

func convertRemoteToServer(t *testing.T, inputData []byte, serverName string) []byte {
t.Helper()
var remoteMetadata registry.RemoteServerMetadata
require.NoError(t, json.Unmarshal(inputData, &remoteMetadata))

serverJSON, err := RemoteServerMetadataToServerJSON(serverName, &remoteMetadata)
require.NoError(t, err)

output, err := json.MarshalIndent(serverJSON, "", " ")
require.NoError(t, err)
return output
}

func convertServerToRemote(t *testing.T, inputData []byte) []byte {
t.Helper()
var serverJSON upstream.ServerJSON
require.NoError(t, json.Unmarshal(inputData, &serverJSON))

remoteMetadata, err := ServerJSONToRemoteServerMetadata(&serverJSON)
require.NoError(t, err)

output, err := json.MarshalIndent(remoteMetadata, "", " ")
require.NoError(t, err)
return output
}

// Validation functions - additional checks beyond JSON equality

func validateImageToServerConversion(t *testing.T, inputData, outputData []byte) {
t.Helper()
var input registry.ImageMetadata
var output upstream.ServerJSON

require.NoError(t, json.Unmarshal(inputData, &input))
require.NoError(t, json.Unmarshal(outputData, &output))

// Verify core mappings
assert.Equal(t, input.Description, output.Description, "Description should match")
assert.Len(t, output.Packages, 1, "Should have exactly one package")
assert.Equal(t, input.Image, output.Packages[0].Identifier, "Image identifier should match")
assert.Equal(t, input.Transport, output.Packages[0].Transport.Type, "Transport type should match")

// Verify environment variables count
assert.Len(t, output.Packages[0].EnvironmentVariables, len(input.EnvVars),
"Environment variables count should match")

// Verify publisher extensions exist
require.NotNil(t, output.Meta, "Meta should not be nil")
require.NotNil(t, output.Meta.PublisherProvided, "PublisherProvided should not be nil")

stacklokData, ok := output.Meta.PublisherProvided["io.github.stacklok"].(map[string]interface{})
require.True(t, ok, "Should have io.github.stacklok namespace")

extensions, ok := stacklokData[input.Image].(map[string]interface{})
require.True(t, ok, "Should have image-specific extensions")

// Verify key extension fields
assert.Equal(t, input.Status, extensions["status"], "Status should be in extensions")
assert.Equal(t, input.Tier, extensions["tier"], "Tier should be in extensions")
assert.NotNil(t, extensions["tools"], "Tools should be in extensions")
assert.NotNil(t, extensions["tags"], "Tags should be in extensions")
}

func validateServerToImageConversion(t *testing.T, inputData, outputData []byte) {
t.Helper()
var input upstream.ServerJSON
var output registry.ImageMetadata

require.NoError(t, json.Unmarshal(inputData, &input))
require.NoError(t, json.Unmarshal(outputData, &output))

// Verify core mappings
assert.Equal(t, input.Description, output.Description, "Description should match")
require.Len(t, input.Packages, 1, "Input should have exactly one package")
assert.Equal(t, input.Packages[0].Identifier, output.Image, "Image identifier should match")
assert.Equal(t, input.Packages[0].Transport.Type, output.Transport, "Transport type should match")

// Verify environment variables were extracted
assert.Len(t, output.EnvVars, len(input.Packages[0].EnvironmentVariables),
"Environment variables count should match")
}

func validateRemoteToServerConversion(t *testing.T, inputData, outputData []byte) {
t.Helper()
var input registry.RemoteServerMetadata
var output upstream.ServerJSON

require.NoError(t, json.Unmarshal(inputData, &input))
require.NoError(t, json.Unmarshal(outputData, &output))

// Verify core mappings
assert.Equal(t, input.Description, output.Description, "Description should match")
require.Len(t, output.Remotes, 1, "Should have exactly one remote")
assert.Equal(t, input.URL, output.Remotes[0].URL, "Remote URL should match")
assert.Equal(t, input.Transport, output.Remotes[0].Type, "Transport type should match")

// Verify headers count
assert.Len(t, output.Remotes[0].Headers, len(input.Headers),
"Headers count should match")
}

func validateServerToRemoteConversion(t *testing.T, inputData, outputData []byte) {
t.Helper()
var input upstream.ServerJSON
var output registry.RemoteServerMetadata

require.NoError(t, json.Unmarshal(inputData, &input))
require.NoError(t, json.Unmarshal(outputData, &output))

// Verify core mappings
assert.Equal(t, input.Description, output.Description, "Description should match")
require.Len(t, input.Remotes, 1, "Input should have exactly one remote")
assert.Equal(t, input.Remotes[0].URL, output.URL, "Remote URL should match")
assert.Equal(t, input.Remotes[0].Type, output.Transport, "Transport type should match")

// Verify headers were extracted
assert.Len(t, output.Headers, len(input.Remotes[0].Headers),
"Headers count should match")
}
Loading
Loading