Skip to content

Commit f772e49

Browse files
authored
fix(registry/483): Docker MCP Catalog → docker run install (no fake docker:// URL) (#484)
Two stacked bugs were preventing 'Add server from Docker MCP Catalog' from ever working: 1. internal/registries/search.go — parseDocker left ServerEntry.URL empty, then constructServerURL synthesised 'docker://mcp/<name>'. The frontend treated any non-empty URL as an HTTP transport and issued POST 'docker://mcp/sqlite' → 'unsupported protocol scheme: docker' (the user-visible error in #483). Fix: parseDocker now emits InstallCmd 'docker run -i --rm mcp/<name>' (matching the parseFleur pattern for OCI servers) and constructServerURL no longer fabricates URLs for protocolDocker. 2. frontend/src/services/api.ts + types/api.ts + views/Repositories.vue — the backend contract serialises install_cmd / connect_url in snake_case, but the Vue code read server.installCmd / server.connectUrl (camelCase), so the value was always undefined. With #483's URL gone, the second bug surfaced as 'Either url or command parameter is required'. Fix: read snake_case consistently (matches source_code_url etc. already used elsewhere in the same file). Regression tests: - TestParseDocker asserts InstallCmd populated, URL empty. - TestConstructServerURL/docker_protocol_returns_empty asserts no synthetic URL. End-to-end verified via curl (REST + MCP tool call) and Playwright- style Chrome flow: a sqlite catalog entry now lands as {protocol:stdio, command:docker, args:[run,-i,--rm,mcp/sqlite]}, auto-quarantined for review. Refs: #483
1 parent 538c1a9 commit f772e49

5 files changed

Lines changed: 47 additions & 17 deletions

File tree

frontend/src/services/api.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -642,9 +642,15 @@ class APIService {
642642
protocol: 'stdio'
643643
}
644644

645-
// Determine command and args from installCmd or connectUrl
646-
if (server.installCmd) {
647-
const parts = server.installCmd.split(' ')
645+
// Determine command and args from install_cmd or connect_url.
646+
// NB: backend contracts.RepositoryServer serialises these as snake_case
647+
// (install_cmd, connect_url). Reading the wrong casing here is the
648+
// second half of issue #483 — for a stdio entry the install command
649+
// was silently undefined and the call fell through to "neither url nor
650+
// command supplied", surfaced as "Either 'url' or 'command' parameter
651+
// is required".
652+
if (server.install_cmd) {
653+
const parts = server.install_cmd.split(' ').filter(Boolean)
648654
args.command = parts[0]
649655
if (parts.length > 1) {
650656
args.args_json = JSON.stringify(parts.slice(1))
@@ -653,9 +659,9 @@ class APIService {
653659
// Remote server with HTTP protocol
654660
args.protocol = 'http'
655661
args.url = server.url
656-
} else if (server.connectUrl) {
662+
} else if (server.connect_url) {
657663
args.protocol = 'http'
658-
args.url = server.connectUrl
664+
args.url = server.connect_url
659665
}
660666

661667
return this.callTool('upstream_servers', args)

frontend/src/types/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -531,8 +531,8 @@ export interface RepositoryServer {
531531
description: string
532532
url?: string // MCP endpoint for remote servers only
533533
source_code_url?: string // Source repository URL
534-
installCmd?: string // Installation command
535-
connectUrl?: string // Alternative connection URL
534+
install_cmd?: string // Installation command (matches backend contracts.RepositoryServer)
535+
connect_url?: string // Alternative connection URL (matches backend contracts.RepositoryServer)
536536
updatedAt?: string
537537
createdAt?: string
538538
registry?: string // Which registry this came from

frontend/src/views/Repositories.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,11 @@
131131
</div>
132132

133133
<!-- Install Command -->
134-
<div v-if="server.installCmd" class="mt-3">
134+
<div v-if="server.install_cmd" class="mt-3">
135135
<div class="flex items-center justify-between bg-base-200 rounded px-2 py-1">
136-
<code class="text-xs flex-1 overflow-x-auto">{{ server.installCmd }}</code>
136+
<code class="text-xs flex-1 overflow-x-auto">{{ server.install_cmd }}</code>
137137
<button
138-
@click="copyToClipboard(server.installCmd)"
138+
@click="copyToClipboard(server.install_cmd)"
139139
class="btn btn-ghost btn-xs ml-2"
140140
title="Copy install command"
141141
>

internal/registries/search.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,9 +326,17 @@ func parseDocker(rawData interface{}) []ServerEntry {
326326
continue
327327
}
328328
if name, ok := itemMap["name"].(string); ok && name != "" {
329+
// Docker Hub's mcp/ namespace returns image repo names (e.g. "sqlite",
330+
// "git", "fetch"). The full reference is mcp/<name>. These images are
331+
// stdio MCP servers, so the canonical launch is `docker run -i --rm`.
332+
// We MUST set InstallCmd (not URL) — the URL field is reserved for HTTP/SSE
333+
// remote endpoints. See issue #483: synthesising "docker://mcp/<name>" as a
334+
// URL caused the frontend to register it as an http transport, leading to
335+
// `Post "docker://mcp/sqlite": unsupported protocol scheme "docker"`.
329336
server := ServerEntry{
330-
ID: name,
331-
Name: name,
337+
ID: name,
338+
Name: name,
339+
InstallCmd: fmt.Sprintf("docker run -i --rm mcp/%s", name),
332340
}
333341

334342
// Try to get description from images array
@@ -563,9 +571,12 @@ func constructServerURL(server *ServerEntry, reg *RegistryEntry) string {
563571
return fmt.Sprintf("https://api.mcpstore.co/servers/%s/mcp", server.ID)
564572
}
565573
case protocolDocker:
566-
if server.ID != "" {
567-
return fmt.Sprintf("docker://mcp/%s", server.ID)
568-
}
574+
// Docker MCP catalog entries are stdio servers launched via `docker run`
575+
// (see parseDocker — InstallCmd is populated). Do NOT synthesise a URL:
576+
// the URL field is for HTTP/SSE remote endpoints only, and any non-empty
577+
// value here causes the frontend to register the server as an http
578+
// transport (issue #483).
579+
return ""
569580
case protocolFleur:
570581
if server.ID != "" {
571582
return fmt.Sprintf("https://api.fleurmcp.com/apps/%s/mcp", server.ID)

internal/registries/search_test.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,16 @@ func TestParseDocker(t *testing.T) {
296296
if servers[0].Description != "Weather MCP server" {
297297
t.Errorf("expected description 'Weather MCP server', got '%s'", servers[0].Description)
298298
}
299+
// Regression for issue #483: Docker catalog entries must surface as a docker
300+
// run install command, NOT a synthetic "docker://" URL (which the frontend
301+
// would mis-classify as an http transport).
302+
if servers[0].URL != "" {
303+
t.Errorf("expected empty URL for Docker catalog entry, got '%s'", servers[0].URL)
304+
}
305+
wantCmd := "docker run -i --rm mcp/mcp-weather"
306+
if servers[0].InstallCmd != wantCmd {
307+
t.Errorf("expected InstallCmd %q, got %q", wantCmd, servers[0].InstallCmd)
308+
}
299309
}
300310

301311
func TestParseFleur(t *testing.T) {
@@ -769,10 +779,13 @@ func TestConstructServerURL(t *testing.T) {
769779
"https://api.mcpstore.co/servers/news/mcp",
770780
},
771781
{
772-
"docker protocol",
782+
// Issue #483: Docker catalog must NOT synthesise a docker:// URL —
783+
// the URL field is reserved for HTTP/SSE remote endpoints. The launch
784+
// info travels via InstallCmd populated by parseDocker.
785+
"docker protocol returns empty (install via docker run)",
773786
ServerEntry{ID: "scraper", URL: ""},
774787
RegistryEntry{Protocol: "custom/docker"},
775-
"docker://mcp/scraper",
788+
"",
776789
},
777790
{
778791
"fleur protocol",

0 commit comments

Comments
 (0)