Skip to content

Commit d14a334

Browse files
authored
Fix the way we identify the URL registry type (#2576)
* Fix the way we identify the URL registry type Signed-off-by: Radoslav Dimitrov <[email protected]> * Add unit tests Signed-off-by: Radoslav Dimitrov <[email protected]> --------- Signed-off-by: Radoslav Dimitrov <[email protected]>
1 parent 6472796 commit d14a334

File tree

3 files changed

+395
-4
lines changed

3 files changed

+395
-4
lines changed

cmd/thv/app/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func unsetCACertCmdFunc(_ *cobra.Command, _ []string) error {
161161

162162
func setRegistryCmdFunc(_ *cobra.Command, args []string) error {
163163
input := args[0]
164-
registryType, cleanPath := config.DetectRegistryType(input)
164+
registryType, cleanPath := config.DetectRegistryType(input, allowPrivateRegistryIp)
165165

166166
provider := config.NewDefaultProvider()
167167

pkg/config/registry.go

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package config
22

33
import (
4+
"encoding/json"
45
"fmt"
6+
"net/http"
57
neturl "net/url"
68
"path/filepath"
79
"strings"
@@ -19,7 +21,7 @@ const (
1921
)
2022

2123
// DetectRegistryType determines if input is a URL or file path and returns cleaned path
22-
func DetectRegistryType(input string) (registryType string, cleanPath string) {
24+
func DetectRegistryType(input string, allowPrivateIPs bool) (registryType string, cleanPath string) {
2325
// Check for explicit file:// protocol
2426
if strings.HasPrefix(input, "file://") {
2527
return RegistryTypeFile, strings.TrimPrefix(input, "file://")
@@ -28,17 +30,72 @@ func DetectRegistryType(input string) (registryType string, cleanPath string) {
2830
// Check for HTTP/HTTPS URLs
2931
if networking.IsURL(input) {
3032
// If URL ends with .json, treat as static registry file
31-
// Otherwise, treat as MCP Registry API endpoint
3233
if strings.HasSuffix(input, ".json") {
3334
return RegistryTypeURL, input
3435
}
35-
return RegistryTypeAPI, input
36+
37+
// For URLs without .json extension, probe to determine the type
38+
registryType := probeRegistryURL(input, allowPrivateIPs)
39+
return registryType, input
3640
}
3741

3842
// Default: treat as file path
3943
return RegistryTypeFile, filepath.Clean(input)
4044
}
4145

46+
// probeRegistryURL attempts to determine if a URL is a static JSON file or an API endpoint
47+
// by checking if the URL returns valid ToolHive registry JSON or has an /openapi.yaml endpoint.
48+
func probeRegistryURL(url string, allowPrivateIPs bool) string {
49+
// Create HTTP client for probing with user's private IP preference
50+
client, err := networking.NewHttpClientBuilder().WithPrivateIPs(allowPrivateIPs).Build()
51+
if err != nil {
52+
// If we can't create a client, default to static JSON
53+
return RegistryTypeURL
54+
}
55+
56+
// First, try to fetch and parse as ToolHive registry JSON
57+
if isValidRegistryJSON(client, url) {
58+
return RegistryTypeURL
59+
}
60+
61+
// If not valid JSON, check for /openapi.yaml endpoint (MCP Registry API)
62+
openapiURL, err := neturl.JoinPath(url, "openapi.yaml")
63+
if err == nil {
64+
resp, err := client.Head(openapiURL)
65+
if err == nil {
66+
_ = resp.Body.Close()
67+
// If openapi.yaml exists (200 OK), treat as API endpoint
68+
if resp.StatusCode == http.StatusOK {
69+
return RegistryTypeAPI
70+
}
71+
}
72+
}
73+
74+
// Default to static JSON file (validation will catch errors later)
75+
return RegistryTypeURL
76+
}
77+
78+
// isValidRegistryJSON checks if a URL returns valid ToolHive registry JSON
79+
func isValidRegistryJSON(client *http.Client, url string) bool {
80+
resp, err := client.Get(url)
81+
if err != nil {
82+
return false
83+
}
84+
defer resp.Body.Close()
85+
86+
// Try to parse as JSON with registry structure
87+
// We just check for basic registry fields to avoid pulling in the full types package
88+
var data map[string]interface{}
89+
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
90+
return false
91+
}
92+
93+
// Check if it has registry-like structure (servers or remoteServers fields)
94+
_, hasServers := data["servers"]
95+
_, hasRemoteServers := data["remoteServers"]
96+
return hasServers || hasRemoteServers
97+
}
98+
4299
// setRegistryURL validates and sets a registry URL using the provided provider
43100
func setRegistryURL(provider Provider, registryURL string, allowPrivateRegistryIp bool) error {
44101
// Validate URL scheme

0 commit comments

Comments
 (0)