Skip to content

Commit c1a872e

Browse files
authored
UpstreamRegistry type, converters and test code (#2567)
* UpstreamRegistry type, converters and test code Signed-off-by: Daniele Martinoli <dmartino@redhat.com> * removed unnecessary functions Signed-off-by: Daniele Martinoli <dmartino@redhat.com> --------- Signed-off-by: Daniele Martinoli <dmartino@redhat.com>
1 parent 0f4c563 commit c1a872e

File tree

3 files changed

+270
-0
lines changed

3 files changed

+270
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package converters
2+
3+
import (
4+
"fmt"
5+
"time"
6+
7+
upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
8+
9+
"github.com/stacklok/toolhive/pkg/registry/types"
10+
)
11+
12+
// NewServerRegistryFromUpstream creates a ServerRegistry from upstream ServerJSON array.
13+
// This is used when ingesting data from upstream MCP Registry API endpoints.
14+
func NewServerRegistryFromUpstream(servers []upstreamv0.ServerJSON) *types.ServerRegistry {
15+
return &types.ServerRegistry{
16+
Version: "1.0.0",
17+
LastUpdated: time.Now().Format(time.RFC3339),
18+
Servers: servers,
19+
}
20+
}
21+
22+
// NewServerRegistryFromToolhive creates a ServerRegistry from ToolHive Registry.
23+
// This converts ToolHive format to upstream ServerJSON using the converters package.
24+
// Used when ingesting data from ToolHive-format sources (Git, File, API).
25+
func NewServerRegistryFromToolhive(toolhiveReg *types.Registry) (*types.ServerRegistry, error) {
26+
if toolhiveReg == nil {
27+
return nil, fmt.Errorf("toolhive registry cannot be nil")
28+
}
29+
30+
servers := make([]upstreamv0.ServerJSON, 0, len(toolhiveReg.Servers)+len(toolhiveReg.RemoteServers))
31+
32+
// Convert container servers using converters package
33+
for name, imgMeta := range toolhiveReg.Servers {
34+
serverJSON, err := ImageMetadataToServerJSON(name, imgMeta)
35+
if err != nil {
36+
return nil, fmt.Errorf("failed to convert server %s: %w", name, err)
37+
}
38+
servers = append(servers, *serverJSON)
39+
}
40+
41+
// Convert remote servers using converters package
42+
for name, remoteMeta := range toolhiveReg.RemoteServers {
43+
serverJSON, err := RemoteServerMetadataToServerJSON(name, remoteMeta)
44+
if err != nil {
45+
return nil, fmt.Errorf("failed to convert remote server %s: %w", name, err)
46+
}
47+
servers = append(servers, *serverJSON)
48+
}
49+
50+
return &types.ServerRegistry{
51+
Version: toolhiveReg.Version,
52+
LastUpdated: toolhiveReg.LastUpdated,
53+
Servers: servers,
54+
}, nil
55+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package converters
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
8+
"github.com/modelcontextprotocol/registry/pkg/model"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/stacklok/toolhive/pkg/registry/types"
13+
)
14+
15+
func TestNewServerRegistryFromToolhive(t *testing.T) {
16+
t.Parallel()
17+
18+
tests := []struct {
19+
name string
20+
toolhiveReg *types.Registry
21+
expectError bool
22+
validate func(*testing.T, *types.ServerRegistry)
23+
}{
24+
{
25+
name: "successful conversion with container servers",
26+
toolhiveReg: &types.Registry{
27+
Version: "1.0.0",
28+
LastUpdated: "2024-01-01T00:00:00Z",
29+
Servers: map[string]*types.ImageMetadata{
30+
"test-server": {
31+
BaseServerMetadata: types.BaseServerMetadata{
32+
Name: "test-server",
33+
Description: "A test server",
34+
Tier: "Community",
35+
Status: "Active",
36+
Transport: "stdio",
37+
Tools: []string{"test_tool"},
38+
},
39+
Image: "test/image:latest",
40+
},
41+
},
42+
RemoteServers: make(map[string]*types.RemoteServerMetadata),
43+
},
44+
expectError: false,
45+
validate: func(t *testing.T, sr *types.ServerRegistry) {
46+
t.Helper()
47+
assert.Equal(t, "1.0.0", sr.Version)
48+
assert.Equal(t, "2024-01-01T00:00:00Z", sr.LastUpdated)
49+
assert.Len(t, sr.Servers, 1)
50+
assert.Contains(t, sr.Servers[0].Name, "test-server")
51+
assert.Equal(t, "A test server", sr.Servers[0].Description)
52+
},
53+
},
54+
{
55+
name: "successful conversion with remote servers",
56+
toolhiveReg: &types.Registry{
57+
Version: "1.0.0",
58+
LastUpdated: "2024-01-01T00:00:00Z",
59+
Servers: make(map[string]*types.ImageMetadata),
60+
RemoteServers: map[string]*types.RemoteServerMetadata{
61+
"remote-server": {
62+
BaseServerMetadata: types.BaseServerMetadata{
63+
Name: "remote-server",
64+
Description: "A remote server",
65+
Tier: "Community",
66+
Status: "Active",
67+
Transport: "sse",
68+
Tools: []string{"remote_tool"},
69+
},
70+
URL: "https://example.com",
71+
},
72+
},
73+
},
74+
expectError: false,
75+
validate: func(t *testing.T, sr *types.ServerRegistry) {
76+
t.Helper()
77+
assert.Len(t, sr.Servers, 1)
78+
assert.Contains(t, sr.Servers[0].Name, "remote-server")
79+
},
80+
},
81+
{
82+
name: "empty registry",
83+
toolhiveReg: &types.Registry{
84+
Version: "1.0.0",
85+
LastUpdated: "2024-01-01T00:00:00Z",
86+
Servers: make(map[string]*types.ImageMetadata),
87+
RemoteServers: make(map[string]*types.RemoteServerMetadata),
88+
},
89+
expectError: false,
90+
validate: func(t *testing.T, sr *types.ServerRegistry) {
91+
t.Helper()
92+
assert.Empty(t, sr.Servers)
93+
},
94+
},
95+
}
96+
97+
for _, tt := range tests {
98+
t.Run(tt.name, func(t *testing.T) {
99+
t.Parallel()
100+
101+
result, err := NewServerRegistryFromToolhive(tt.toolhiveReg)
102+
103+
if tt.expectError {
104+
assert.Error(t, err)
105+
assert.Nil(t, result)
106+
} else {
107+
assert.NoError(t, err)
108+
assert.NotNil(t, result)
109+
if tt.validate != nil {
110+
tt.validate(t, result)
111+
}
112+
}
113+
})
114+
}
115+
}
116+
117+
func TestNewServerRegistryFromUpstream(t *testing.T) {
118+
t.Parallel()
119+
120+
tests := []struct {
121+
name string
122+
servers []upstreamv0.ServerJSON
123+
validate func(*testing.T, *types.ServerRegistry)
124+
}{
125+
{
126+
name: "create from upstream servers",
127+
servers: []upstreamv0.ServerJSON{
128+
{
129+
Schema: "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
130+
Name: "io.test/server1",
131+
Description: "Test server 1",
132+
Version: "1.0.0",
133+
Packages: []model.Package{
134+
{
135+
RegistryType: "oci",
136+
Identifier: "test/image:latest",
137+
Transport: model.Transport{Type: "stdio"},
138+
},
139+
},
140+
},
141+
},
142+
validate: func(t *testing.T, sr *types.ServerRegistry) {
143+
t.Helper()
144+
assert.Equal(t, "1.0.0", sr.Version)
145+
assert.NotEmpty(t, sr.LastUpdated)
146+
assert.Len(t, sr.Servers, 1)
147+
assert.Equal(t, "io.test/server1", sr.Servers[0].Name)
148+
},
149+
},
150+
{
151+
name: "create from empty slice",
152+
servers: []upstreamv0.ServerJSON{},
153+
validate: func(t *testing.T, sr *types.ServerRegistry) {
154+
t.Helper()
155+
assert.Empty(t, sr.Servers)
156+
},
157+
},
158+
}
159+
160+
for _, tt := range tests {
161+
t.Run(tt.name, func(t *testing.T) {
162+
t.Parallel()
163+
164+
result := NewServerRegistryFromUpstream(tt.servers)
165+
166+
assert.NotNil(t, result)
167+
if tt.validate != nil {
168+
tt.validate(t, result)
169+
}
170+
})
171+
}
172+
}
173+
174+
func TestNewServerRegistryFromUpstream_DefaultValues(t *testing.T) {
175+
t.Parallel()
176+
177+
servers := []upstreamv0.ServerJSON{
178+
{
179+
Schema: "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
180+
Name: "io.test/server1",
181+
Description: "Test server",
182+
Version: "1.0.0",
183+
},
184+
}
185+
186+
result := NewServerRegistryFromUpstream(servers)
187+
188+
// Verify defaults
189+
assert.Equal(t, "1.0.0", result.Version)
190+
assert.NotEmpty(t, result.LastUpdated)
191+
192+
// Verify timestamp is recent (within last minute)
193+
parsedTime, err := time.Parse(time.RFC3339, result.LastUpdated)
194+
require.NoError(t, err)
195+
assert.WithinDuration(t, time.Now(), parsedTime, time.Minute)
196+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package types
2+
3+
import (
4+
upstreamv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
5+
)
6+
7+
// ServerRegistry is the unified internal registry format.
8+
// It stores servers in upstream ServerJSON format while maintaining
9+
// ToolHive-compatible metadata fields for backward compatibility.
10+
type ServerRegistry struct {
11+
// Version is the schema version (ToolHive compatibility)
12+
Version string `json:"version"`
13+
14+
// LastUpdated is the timestamp when registry was last updated (ToolHive compatibility)
15+
LastUpdated string `json:"last_updated"`
16+
17+
// Servers contains the server definitions in upstream MCP format
18+
Servers []upstreamv0.ServerJSON `json:"servers"`
19+
}

0 commit comments

Comments
 (0)