Skip to content

Commit cf12950

Browse files
authored
fix(registries): versioned User-Agent for Pulse + Fleur stdio not remote (#568)
Two registry-parser bugs in internal/registries/search.go: - Pulse (#566): fetchServers sent no User-Agent; api.pulsemcp.com now 410s empty/bare UAs and only 200s a versioned one. Set a "mcpproxy/<version>" User-Agent (wired SetVersion from main, mirroring guesser.go). - Fleur (#567): constructServerURL synthesised an https endpoint for every Fleur app, making local stdio apps (InstallCmd set) show as "Remote" in the frontend — same class as Docker #483. Return "" when InstallCmd is set. Test-first: httptest 410s on empty/bare UA and asserts fetchServers sends a versioned UA; TestConstructServerURL gains a Fleur-with-InstallCmd case. Related #566 Related #567
1 parent c2a0117 commit cf12950

3 files changed

Lines changed: 74 additions & 0 deletions

File tree

cmd/mcpproxy/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,8 @@ func runServer(cmd *cobra.Command, _ []string) error {
502502
server.SetMCPServerVersion(version)
503503
// Spec 042: surface header for outbound CLI HTTP requests.
504504
cliclient.SetClientVersion(version)
505+
// Issue #566: registries (e.g. Pulse) require a versioned User-Agent.
506+
registries.SetVersion(version)
505507

506508
// Override other settings from command line
507509
cfg.DebugSearch = cmdDebugSearch

internal/registries/search.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,24 @@ const (
4141
// GitHub URL pattern for matching https://github.com/<author|org>/<repo>
4242
var githubURLPattern = regexp.MustCompile(`^https://github\.com/([^/]+)/([^/]+)(?:/.*)?$`)
4343

44+
// buildVersion holds the build-time version used in the outbound User-Agent.
45+
// Set via SetVersion at process startup. Defaults to "dev" so tests run
46+
// without setup. Some registries (e.g. Pulse, issue #566) reject requests
47+
// with an empty or bare User-Agent and require a versioned one.
48+
var buildVersion = "dev"
49+
50+
// SetVersion sets the version reported in the registry User-Agent header.
51+
func SetVersion(v string) {
52+
if v != "" {
53+
buildVersion = v
54+
}
55+
}
56+
57+
// registryUserAgent returns the versioned User-Agent for registry HTTP requests.
58+
func registryUserAgent() string {
59+
return "mcpproxy/" + buildVersion
60+
}
61+
4462
// SearchServers searches the given registry for servers matching optional tag and query
4563
// with optional repository guessing and result limiting
4664
func SearchServers(ctx context.Context, registryID, tag, query string, limit int, guesser *experiments.Guesser) ([]ServerEntry, error) {
@@ -135,6 +153,10 @@ func fetchServers(ctx context.Context, reg *RegistryEntry, guesser *experiments.
135153
return nil, fmt.Errorf("failed to create request: %w", err)
136154
}
137155

156+
// Some registries (e.g. Pulse, issue #566) reject requests with an empty
157+
// or bare User-Agent and require a versioned one (mirror guesser.go).
158+
req.Header.Set("User-Agent", registryUserAgent())
159+
138160
resp, err := client.Do(req)
139161
if err != nil {
140162
return nil, fmt.Errorf("failed to fetch servers: %w", err)
@@ -628,6 +650,13 @@ func constructServerURL(server *ServerEntry, reg *RegistryEntry) string {
628650
// transport (issue #483).
629651
return ""
630652
case protocolFleur:
653+
// Issue #567 (same class as Docker #483): Fleur apps that ship a local
654+
// stdio install command carry it in InstallCmd. The URL field is for
655+
// HTTP/SSE remote endpoints only — synthesising one makes the frontend
656+
// register the server as a remote transport. Leave it empty for stdio.
657+
if server.InstallCmd != "" {
658+
return ""
659+
}
631660
if server.ID != "" {
632661
return fmt.Sprintf("https://api.fleurmcp.com/apps/%s/mcp", server.ID)
633662
}

internal/registries/search_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,17 @@ func TestConstructServerURL(t *testing.T) {
793793
RegistryEntry{Protocol: "custom/fleur"},
794794
"https://api.fleurmcp.com/apps/app1/mcp",
795795
},
796+
{
797+
// Issue #567: Fleur apps that ship a local stdio install command
798+
// (InstallCmd populated by parseFleur) must NOT get a synthesised
799+
// https endpoint — same class as the Docker #483 bug. A non-empty
800+
// URL makes the frontend register them as "Remote". Launch info
801+
// travels via InstallCmd, so the connection URL must stay empty.
802+
"fleur protocol with InstallCmd returns empty (stdio not remote)",
803+
ServerEntry{ID: "app1", URL: "", InstallCmd: "npx -y @fleur/app1"},
804+
RegistryEntry{Protocol: "custom/fleur"},
805+
"",
806+
},
796807
{
797808
"unknown protocol",
798809
ServerEntry{ID: "test", URL: ""},
@@ -811,6 +822,38 @@ func TestConstructServerURL(t *testing.T) {
811822
}
812823
}
813824

825+
// TestFetchServers_SendsVersionedUserAgent is a regression for issue #566:
826+
// Pulse's api.pulsemcp.com/v0beta/servers now returns 410 to requests with an
827+
// empty or bare User-Agent and only 200s a versioned one (e.g. "mcpproxy/0.35.0").
828+
// fetchServers previously sent no User-Agent at all. This stands up a server
829+
// that mirrors Pulse's behaviour (410 unless a versioned UA is present) and
830+
// asserts fetchServers both sends a versioned UA and succeeds.
831+
func TestFetchServers_SendsVersionedUserAgent(t *testing.T) {
832+
var gotUserAgent string
833+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
834+
gotUserAgent = r.Header.Get("User-Agent")
835+
// Mirror Pulse: reject empty or bare (no version segment) UAs with 410.
836+
if gotUserAgent == "" || !strings.Contains(gotUserAgent, "mcpproxy/") {
837+
w.WriteHeader(http.StatusGone) // 410
838+
return
839+
}
840+
w.Header().Set("Content-Type", "application/json")
841+
_, _ = w.Write([]byte(`{"servers":[]}`))
842+
}))
843+
defer srv.Close()
844+
845+
reg := &RegistryEntry{
846+
ID: "pulse",
847+
Protocol: "custom/pulse",
848+
ServersURL: srv.URL,
849+
}
850+
851+
_, err := fetchServers(context.Background(), reg, nil)
852+
require.NoError(t, err, "fetchServers should not get a 410 — it must send a versioned User-Agent")
853+
require.NotEmpty(t, gotUserAgent, "fetchServers must send a User-Agent header")
854+
require.Contains(t, gotUserAgent, "mcpproxy/", "User-Agent must be versioned (mcpproxy/<version>)")
855+
}
856+
814857
// Helper function for testing
815858
func TestProtocolParsersWithMissingData(t *testing.T) {
816859
// Test all parsers with null/invalid data

0 commit comments

Comments
 (0)