Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.24.5
require (
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/modelcontextprotocol/registry v0.0.0-20250903150202-6ea3828e3ce6
github.com/modelcontextprotocol/registry v1.0.0
github.com/spf13/cobra v1.10.1
github.com/stacklok/toolhive v0.2.17
github.com/stretchr/testify v1.11.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
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 v0.0.0-20250903150202-6ea3828e3ce6 h1:/QevgO0aEnGJGmmePflfNkm4Y9pkdQyNPxFwqlY3qa8=
github.com/modelcontextprotocol/registry v0.0.0-20250903150202-6ea3828e3ce6/go.mod h1:D6U1q6wYKYMA58q2gZz4eFsghr+fTkZQY8/ZFwTOT1Q=
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/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
64 changes: 40 additions & 24 deletions pkg/registry/official.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ func (or *OfficialRegistry) WriteJSON(path string) error {
// Build the registry structure
registry := or.build()

// Validate the complete registry against schema
// Validate the complete registry against schema (warnings only for now)
if err := or.validateRegistry(registry); err != nil {
return fmt.Errorf("registry validation failed: %w", err)
fmt.Printf("⚠️ Schema validation warnings: %v\n", err)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why only warnings?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's temporary as the upstream schema enforces some requirements on values that we are not complying with, i.e. an sse/streamable-http server type don't have URLs on our side but is expected on the upstream side. It's actually an issue on our side because we made the assumption the MCP endpoints are on the root level of the server endpoint which may not always be the case

}

// Create the directory if it doesn't exist
Expand Down Expand Up @@ -164,18 +164,18 @@ func (or *OfficialRegistry) build() *ToolHiveRegistryType {
func (or *OfficialRegistry) transformEntry(name string, entry *types.RegistryEntry) upstream.ServerJSON {
// Create the flattened server JSON with _meta extensions
serverJSON := upstream.ServerJSON{
Name: name,
Description: entry.GetDescription(),
Status: or.convertStatus(entry.GetStatus()),
Repository: or.createRepository(entry),
VersionDetail: or.createVersionDetail(),
Name: or.convertNameToReverseDNS(name),
Description: entry.GetDescription(),
Status: or.convertStatus(entry.GetStatus()),
Repository: or.createRepository(entry),
Version: "1.0.0", // TODO: Default server version for now, fix this to use package/remote version
Meta: &upstream.ServerMeta{
Publisher: or.createXPublisherExtensions(entry),
PublisherProvided: or.createXPublisherExtensions(entry),
// The registry extensions are not supposed to be set by us.
// They are generated by the registry system.
// We include them here so we can start using them in toolhive,
// and they are available when we support an official MCP registry.
IOModelContextProtocolRegistry: or.createRegistryExtensions(),
Official: or.createRegistryExtensions(),
},
}

Expand Down Expand Up @@ -220,13 +220,6 @@ func (*OfficialRegistry) createRepository(entry *types.RegistryEntry) model.Repo
}
}

// createVersionDetail creates version information (fixed at 1.0.0 for now)
func (*OfficialRegistry) createVersionDetail() model.VersionDetail {
return model.VersionDetail{
Version: "1.0.0",
}
}

// createPackages creates Package entries for image-based servers
func (*OfficialRegistry) createPackages(entry *types.RegistryEntry) []model.Package {
if !entry.IsImage() || entry.Image == "" {
Expand Down Expand Up @@ -258,19 +251,32 @@ func (*OfficialRegistry) createPackages(entry *types.RegistryEntry) []model.Pack
version = ""
}

// Determine transport type - use entry's transport or default to stdio for containers
transportType := entry.GetTransport()
if transportType == "" {
transportType = "stdio"
}

transport := model.Transport{
Type: transportType,
}

// Todo: Add URL field for non-stdio transports (required by schema)

pkg := model.Package{
RegistryType: model.RegistryTypeOCI,
RegistryBaseURL: registryBaseURL,
Identifier: identifier,
Version: version,
EnvironmentVariables: envVars,
Transport: transport,
}

return []model.Package{pkg}
}

// createRemotes creates Remote entries for remote servers
func (*OfficialRegistry) createRemotes(entry *types.RegistryEntry) []model.Remote {
// createRemotes creates Transport entries for remote servers
func (*OfficialRegistry) createRemotes(entry *types.RegistryEntry) []model.Transport {
if !entry.IsRemote() || entry.URL == "" {
return nil
}
Expand All @@ -290,13 +296,13 @@ func (*OfficialRegistry) createRemotes(entry *types.RegistryEntry) []model.Remot
})
}

remote := model.Remote{
TransportType: entry.GetTransport(),
URL: entry.URL,
Headers: headers,
remote := model.Transport{
Type: entry.GetTransport(),
URL: entry.URL,
Headers: headers,
}

return []model.Remote{remote}
return []model.Transport{remote}
}

// createRegistryExtensions creates registry-generated metadata
Expand All @@ -307,7 +313,6 @@ func (*OfficialRegistry) createRegistryExtensions() *upstream.RegistryExtensions
PublishedAt: now,
UpdatedAt: now,
IsLatest: true,
ReleaseDate: now.Format("2006-01-02"),
}
}

Expand Down Expand Up @@ -499,3 +504,14 @@ func splitRegistryAndName(image string) (registryBaseURL, identifier string) {
// Otherwise assume Docker Hub with namespace
return "https://docker.io", image
}

// convertNameToReverseDNS converts simple server names to reverse-DNS format required by v1.0.0 schema
func (*OfficialRegistry) convertNameToReverseDNS(name string) string {
// If already in reverse-DNS format (contains '/'), return as-is
if strings.Contains(name, "/") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this enough to enforce that it's a reverse DNS? Looking at the current thv registry all the names are simple strings with just a-z just wondering if you meant to include the full io.stacklok.toolhive/ string in the check

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wasn't particularly sure about this but for the time being it's not a problem. To be honest I don't think any FE client will like seeing those names so I'll be doing a follow up change where we have a display name as part of the publisher-provider extensions (that is what we have now as name on our side) and leave this to be the DNS one just so we are compliant.

return name
}

// Convert simple names to toolhive namespace format
return "io.stacklok.toolhive/" + name
}
8 changes: 4 additions & 4 deletions schemas/registry.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"type": "array",
"description": "Array of MCP servers using the official MCP server schema",
"items": {
"$ref": "https://raw.githubusercontent.com/modelcontextprotocol/registry/6ea3828e3ce62cfd9815376cd6825453da011fa1/docs/server-json/server.schema.json#/$defs/ServerDetail"
"$ref": "https://raw.githubusercontent.com/modelcontextprotocol/registry/f975e68cf25c776160d4e837919884ca026027d6/docs/reference/server-json/server.schema.json#/$defs/ServerDetail"
}
},
"groups": {
Expand All @@ -61,9 +61,9 @@
}
},
"_schema_version": {
"mcp_registry_version": "v0.0.0-20250903150202-6ea3828e3ce6",
"mcp_registry_commit": "6ea3828e3ce6",
"updated_at": "2025-09-04T14:04:12Z",
"mcp_registry_version": "v1.0.0",
"mcp_registry_commit": "f975e68cf25c776160d4e837919884ca026027d6",
"updated_at": "2025-09-11T14:07:36Z",
"updated_by": "sync-schema-version.sh"
}
}
8 changes: 4 additions & 4 deletions scripts/sync-schema-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ get_current_commit_sha() {
echo "${BASH_REMATCH[1]}"
elif [[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
# For tagged versions, we need to resolve to commit SHA
warn "Tagged version detected: $version"
warn "Will attempt to resolve commit SHA from GitHub..."
warn "Tagged version detected: $version" >&2
warn "Will attempt to resolve commit SHA from GitHub..." >&2

# Try to get commit SHA for the tag
local sha
Expand Down Expand Up @@ -117,8 +117,8 @@ update_schema_reference() {
'
# Update the $ref URL
.properties.data.properties.servers.items["$ref"] |=
gsub("github.com/modelcontextprotocol/registry/[a-f0-9]*/";
"github.com/modelcontextprotocol/registry/" + $new_sha + "/") |
gsub("/registry/[^/]+/";
"/registry/" + $new_sha + "/") |

# Add or update _schema_version metadata
._schema_version = {
Expand Down
Loading