Skip to content

Commit 1b96888

Browse files
chambridgeclaude
andcommitted
feat(catalog): implement MCP server database models and repository layer
Add complete database persistence layer for MCP servers and tools with composite versioning strategy, comprehensive validation, and filter support. Database Models and Interfaces: - McpServer and McpServerTool models with typed attributes - Repository interfaces for CRUD operations and source management - Composite name versioning (base_name@version) with @ separator validation - Filter mappings for 19 McpServer properties and 2 McpServerTool properties Repository Implementation: - McpServerRepository with full CRUD operations (Save, GetByID, GetByNameAndVersion, List, Delete) - GetDistinctSourceIDs for source catalog management - Upsert logic based on (base_name, version) tuple - GORM query builder for database portability (MySQL/PostgreSQL) - Source-based bulk deletion for catalog loader cleanup 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Signed-off-by: Chris Hambridge <chambrid@redhat.com>
1 parent 4aeff70 commit 1b96888

13 files changed

Lines changed: 1620 additions & 28 deletions

File tree

catalog/cmd/catalog.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func runCatalogServer(cmd *cobra.Command, args []string) error {
6565
getRepo[models.CatalogMetricsArtifactRepository](repoSet),
6666
getRepo[models.CatalogSourceRepository](repoSet),
6767
getRepo[models.PropertyOptionsRepository](repoSet),
68+
getRepo[models.McpServerRepository](repoSet),
6869
)
6970

7071
loader := catalog.NewLoader(services, catalogCfg.ConfigPath)

catalog/internal/catalog/catalog_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func TestLoadCatalogSources(t *testing.T) {
4343
&MockCatalogMetricsArtifactRepository{},
4444
&MockCatalogSourceRepository{},
4545
&MockPropertyOptionsRepository{},
46+
nil, // McpServerRepository
4647
)
4748
loader := NewLoader(services, []string{tt.args.catalogsPath})
4849
err := loader.Start(context.Background())
@@ -104,6 +105,7 @@ func TestLoadCatalogSourcesEnabledDisabled(t *testing.T) {
104105
&MockCatalogMetricsArtifactRepository{},
105106
&MockCatalogSourceRepository{},
106107
&MockPropertyOptionsRepository{},
108+
nil, // McpServerRepository
107109
)
108110
loader := NewLoader(services, []string{tt.args.catalogsPath})
109111
err := loader.Start(context.Background())
@@ -131,6 +133,7 @@ func TestLabelsValidation(t *testing.T) {
131133
&MockCatalogMetricsArtifactRepository{},
132134
&MockCatalogSourceRepository{},
133135
&MockPropertyOptionsRepository{},
136+
nil, // McpServerRepository
134137
)
135138

136139
tests := []struct {
@@ -262,6 +265,7 @@ func TestCatalogSourceLabelsDefaultToEmptySlice(t *testing.T) {
262265
&MockCatalogMetricsArtifactRepository{},
263266
&MockCatalogSourceRepository{},
264267
&MockPropertyOptionsRepository{},
268+
nil, // McpServerRepository
265269
)
266270
loader := NewLoader(services, []string{tt.args.catalogsPath})
267271
err := loader.Start(context.Background())
@@ -301,6 +305,7 @@ func TestLoadCatalogSourcesWithMockRepositories(t *testing.T) {
301305
mockMetricsArtifactRepo,
302306
&MockCatalogSourceRepository{},
303307
&MockPropertyOptionsRepository{},
308+
nil, // McpServerRepository
304309
)
305310

306311
// Register a test provider that will create some test data
@@ -429,6 +434,7 @@ func TestLoadCatalogSourcesWithRepositoryErrors(t *testing.T) {
429434
mockMetricsArtifactRepo,
430435
&MockCatalogSourceRepository{},
431436
&MockPropertyOptionsRepository{},
437+
nil, // McpServerRepository
432438
)
433439

434440
// Register a test provider
@@ -505,6 +511,7 @@ func TestLoadCatalogSourcesWithNilEnabled(t *testing.T) {
505511
mockMetricsArtifactRepo,
506512
&MockCatalogSourceRepository{},
507513
&MockPropertyOptionsRepository{},
514+
nil, // McpServerRepository
508515
)
509516

510517
// Register a test provider
@@ -1086,6 +1093,7 @@ func TestAPIProviderGetPerformanceArtifacts(t *testing.T) {
10861093
&MockCatalogMetricsArtifactRepository{},
10871094
&MockCatalogSourceRepository{},
10881095
&MockPropertyOptionsRepository{},
1096+
nil, // McpServerRepository
10891097
)
10901098
provider := NewDBCatalog(services, nil)
10911099

@@ -1114,6 +1122,7 @@ func TestAPIProviderInterface(t *testing.T) {
11141122
&MockCatalogMetricsArtifactRepository{},
11151123
&MockCatalogSourceRepository{},
11161124
&MockPropertyOptionsRepository{},
1125+
nil, // McpServerRepository
11171126
)
11181127
var provider APIProvider = NewDBCatalog(services, nil)
11191128

catalog/internal/catalog/db_catalog_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func TestDBCatalog(t *testing.T) {
5252
metricsArtifactRepo,
5353
catalogSourceRepo,
5454
service.NewPropertyOptionsRepository(sharedDB),
55+
nil, // McpServerRepository
5556
)
5657

5758
// Create DB catalog instance
@@ -1437,6 +1438,7 @@ func TestDBCatalog_GetPerformanceArtifactsWithService(t *testing.T) {
14371438
metricsArtifactRepo,
14381439
catalogSourceRepo,
14391440
service.NewPropertyOptionsRepository(sharedDB),
1441+
nil, // McpServerRepository
14401442
)
14411443

14421444
sources := NewSourceCollection()
@@ -1989,6 +1991,7 @@ func TestFindModelsWithRecommendedLatency(t *testing.T) {
19891991
metricsArtifactRepo,
19901992
catalogSourceRepo,
19911993
service.NewPropertyOptionsRepository(sharedDB),
1994+
nil, // McpServerRepository
19921995
)
19931996

19941997
// Create DB catalog instance

catalog/internal/catalog/integration_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func setupIntegrationTestProvider(t *testing.T, ctx context.Context, sharedDB *g
3939
metricsArtifactRepo,
4040
catalogSourceRepo,
4141
service.NewPropertyOptionsRepository(sharedDB),
42+
nil, // McpServerRepository
4243
)
4344

4445
// Insert test data:
@@ -180,6 +181,7 @@ func setupBenchmarkProvider(b *testing.B, ctx context.Context, sharedDB *gorm.DB
180181
metricsArtifactRepo,
181182
catalogSourceRepo,
182183
service.NewPropertyOptionsRepository(sharedDB),
184+
nil, // McpServerRepository
183185
)
184186

185187
// Insert 100+ models with performance data for benchmarking

catalog/internal/catalog/loader_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ func TestRemoveModelsFromMissingSources(t *testing.T) {
105105
&MockCatalogMetricsArtifactRepository{},
106106
&MockCatalogSourceRepository{},
107107
&MockPropertyOptionsRepository{},
108+
nil, // McpServerRepository
108109
)
109110

110111
// Create loader and populate sources

catalog/internal/db/filter/entity_mappings.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ type CatalogRestEntityType string
1212
const (
1313
RestEntityCatalogModel CatalogRestEntityType = "CatalogModel"
1414
RestEntityCatalogArtifact CatalogRestEntityType = "CatalogArtifact"
15+
RestEntityMcpServer CatalogRestEntityType = "McpServer"
16+
RestEntityMcpServerTool CatalogRestEntityType = "McpServerTool"
1517
)
1618

1719
// catalogEntityMappings implements EntityMappingFunctions for the catalog package
@@ -27,6 +29,8 @@ func (c *catalogEntityMappings) GetMLMDEntityType(restEntityType filter.RestEnti
2729
switch restEntityType {
2830
case filter.RestEntityType(RestEntityCatalogArtifact):
2931
return filter.EntityTypeArtifact
32+
case filter.RestEntityType(RestEntityMcpServerTool):
33+
return filter.EntityTypeArtifact
3034
default:
3135
return filter.EntityTypeContext
3236
}
@@ -67,6 +71,20 @@ func (c *catalogEntityMappings) GetPropertyDefinitionForRestEntity(restEntityTyp
6771
}
6872
}
6973

74+
if restEntityType == filter.RestEntityType(RestEntityMcpServer) {
75+
if _, isWellKnown := mcpServerProperties[propertyName]; isWellKnown {
76+
// Use the well-known property definition
77+
return mcpServerProperties[propertyName]
78+
}
79+
}
80+
81+
if restEntityType == filter.RestEntityType(RestEntityMcpServerTool) {
82+
if _, isWellKnown := mcpServerToolProperties[propertyName]; isWellKnown {
83+
// Use the well-known property definition
84+
return mcpServerToolProperties[propertyName]
85+
}
86+
}
87+
7088
// Not a well-known property for this entity type, treat as custom
7189
return filter.PropertyDefinition{
7290
Location: filter.Custom,
@@ -119,3 +137,53 @@ var catalogArtifactProperties = map[string]filter.PropertyDefinition{
119137
// Artifact type (stored in type_id but we can filter by string representation)
120138
"artifactType": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "artifactType"},
121139
}
140+
141+
// mcpServerProperties defines the allowed properties for McpServer entities.
142+
// This follows the same pattern as catalogModelProperties - only properties that are:
143+
// 1. Entity table columns (required for core identity)
144+
// 2. Key filterable dimensions that need explicit type handling (arrays, bools)
145+
// 3. Common properties shared with CatalogModel for consistency
146+
// All other properties can be queried via custom property fallback.
147+
var mcpServerProperties = map[string]filter.PropertyDefinition{
148+
// Common Context properties (Entity Table - required)
149+
"id": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "id"},
150+
"name": {Location: filter.EntityTable, ValueType: filter.StringValueType, Column: "name"},
151+
"externalId": {Location: filter.EntityTable, ValueType: filter.StringValueType, Column: "external_id"},
152+
"createTimeSinceEpoch": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "create_time_since_epoch"},
153+
"lastUpdateTimeSinceEpoch": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "last_update_time_since_epoch"},
154+
155+
// Core properties matching CatalogModel pattern
156+
"source_id": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "source_id"},
157+
"base_name": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "base_name"},
158+
"description": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "description"},
159+
"provider": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "provider"},
160+
"license": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "license"},
161+
"license_link": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "license_link"},
162+
"logo": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "logo"},
163+
"readme": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "readme"},
164+
"version": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "version"},
165+
166+
// MCP-specific filterable dimensions (need explicit type for arrays)
167+
"tags": {Location: filter.PropertyTable, ValueType: filter.ArrayValueType, Column: "tags"},
168+
"transports": {Location: filter.PropertyTable, ValueType: filter.ArrayValueType, Column: "transports"},
169+
"deploymentMode": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "deploymentMode"},
170+
171+
// Security indicators (need explicit type for booleans)
172+
"verifiedSource": {Location: filter.PropertyTable, ValueType: filter.BoolValueType, Column: "verifiedSource"},
173+
"secureEndpoint": {Location: filter.PropertyTable, ValueType: filter.BoolValueType, Column: "secureEndpoint"},
174+
"sast": {Location: filter.PropertyTable, ValueType: filter.BoolValueType, Column: "sast"},
175+
"readOnlyTools": {Location: filter.PropertyTable, ValueType: filter.BoolValueType, Column: "readOnlyTools"},
176+
}
177+
178+
// mcpServerToolProperties defines the allowed properties for McpServerTool entities
179+
var mcpServerToolProperties = map[string]filter.PropertyDefinition{
180+
// Common Artifact properties
181+
"id": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "id"},
182+
"name": {Location: filter.EntityTable, ValueType: filter.StringValueType, Column: "name"},
183+
"createTimeSinceEpoch": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "create_time_since_epoch"},
184+
"lastUpdateTimeSinceEpoch": {Location: filter.EntityTable, ValueType: filter.IntValueType, Column: "last_update_time_since_epoch"},
185+
186+
// McpServerTool-specific properties stored in ArtifactProperty table
187+
"description": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "description"},
188+
"accessType": {Location: filter.PropertyTable, ValueType: filter.StringValueType, Column: "accessType"},
189+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package models
2+
3+
import (
4+
catalogfilter "github.com/kubeflow/model-registry/catalog/internal/db/filter"
5+
"github.com/kubeflow/model-registry/internal/db/filter"
6+
"github.com/kubeflow/model-registry/internal/db/models"
7+
)
8+
9+
// McpServerListOptions holds the options for listing MCP servers.
10+
type McpServerListOptions struct {
11+
models.Pagination
12+
Name *string
13+
SourceIDs *[]string
14+
Query *string
15+
FilterQuery *string
16+
NamedQuery *string
17+
}
18+
19+
// GetRestEntityType implements the FilterApplier interface.
20+
func (c *McpServerListOptions) GetRestEntityType() filter.RestEntityType {
21+
return filter.RestEntityType(catalogfilter.RestEntityMcpServer)
22+
}
23+
24+
// GetFilterQuery returns the filter query string for advanced filtering.
25+
func (c *McpServerListOptions) GetFilterQuery() string {
26+
if c.FilterQuery == nil {
27+
return ""
28+
}
29+
return *c.FilterQuery
30+
}
31+
32+
// McpServerAttributes holds the attributes for an MCP server record.
33+
type McpServerAttributes struct {
34+
Name *string
35+
ExternalID *string
36+
CreateTimeSinceEpoch *int64
37+
LastUpdateTimeSinceEpoch *int64
38+
}
39+
40+
// McpServer represents an MCP server stored in the database.
41+
type McpServer interface {
42+
models.Entity[McpServerAttributes]
43+
}
44+
45+
// McpServerImpl is the concrete implementation of McpServer.
46+
type McpServerImpl = models.BaseEntity[McpServerAttributes]
47+
48+
// McpServerRepository defines the interface for MCP server persistence.
49+
type McpServerRepository interface {
50+
GetByID(id int32) (McpServer, error)
51+
GetByNameAndVersion(name string, version string) (McpServer, error)
52+
List(listOptions McpServerListOptions) (*models.ListWrapper[McpServer], error)
53+
Save(server McpServer) (McpServer, error)
54+
DeleteBySource(sourceID string) error
55+
DeleteByID(id int32) error
56+
GetDistinctSourceIDs() ([]string, error)
57+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package models
2+
3+
import (
4+
"github.com/kubeflow/model-registry/internal/db/models"
5+
)
6+
7+
// McpServerToolAttributes holds the attributes for an MCP server tool record.
8+
type McpServerToolAttributes struct {
9+
Name *string
10+
CreateTimeSinceEpoch *int64
11+
LastUpdateTimeSinceEpoch *int64
12+
}
13+
14+
// McpServerTool represents an MCP server tool stored in the database.
15+
type McpServerTool interface {
16+
models.Entity[McpServerToolAttributes]
17+
}
18+
19+
// McpServerToolImpl is the concrete implementation of McpServerTool.
20+
type McpServerToolImpl = models.BaseEntity[McpServerToolAttributes]
21+
22+
// McpServerToolRepository defines the interface for MCP server tool persistence.
23+
type McpServerToolRepository interface {
24+
GetByID(id int32) (McpServerTool, error)
25+
List(parentID int32) ([]McpServerTool, error)
26+
Save(tool McpServerTool, parentID *int32) (McpServerTool, error)
27+
DeleteByParentID(parentID int32) error
28+
DeleteByID(id int32) error
29+
}

0 commit comments

Comments
 (0)