From aaa634ebb69ffd57643dd5efec6c2aa0575b9e4f Mon Sep 17 00:00:00 2001 From: Radoslav Dimitrov Date: Sat, 25 Apr 2026 10:14:11 -0400 Subject: [PATCH] fix(publisher): honor mcpName as the authoritative server name in init (#1195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Follow-up to #1145. When `mcpName` is set in `package.json`, it should be the server name used by `mcp-publisher init`. Two issues addressed: ### 1. `getNameFromPackageJSON` mangled `mcpName` through npm-name transformation `mcpName` values are already in MCP server name format (e.g. `io.github.foo/bar`). The existing code ran them through the `@scope/name → io.github.scope/name` transform and its `io.github./*` fallback, producing a broken double-prefix result. Example with `"mcpName": "io.github.acme/weather"`: - Before: `io.github./io.github.acme/weather` ❌ - After: `io.github.acme/weather` ✅ ### 2. `detectServerName` consulted git before `mcpName` Even with `mcpName` set in `package.json`, a github.com git remote (or a `repository` field) silently won — the function returned `io.github./` and never looked at `mcpName`. Per the [quickstart docs](https://github.com/modelcontextprotocol/registry/blob/main/docs/modelcontextprotocol-io/quickstart.mdx), *"The value of `mcpName` will be your server's name in the MCP Registry"*, so `mcpName` should take precedence. ## Changes - `cmd/publisher/commands/init.go`: - New `getMcpNameFromPackageJSON()` helper that returns `mcpName` or `""`. - `detectServerName` checks `mcpName` first, before git remote and the `name`-based fallback. - `getNameFromPackageJSON` simplified — the now-redundant `mcpName` branch was removed; it only handles the npm `name` transformation. - `cmd/publisher/commands/init_test.go` (new): 5 cases through the public `InitCommand` API covering `mcpName` as-is, fallback to scoped `name`, fallback to placeholder, version fallback, and `mcpName` winning over a GitHub `repository` URL. This completes the fix for #737. ## Test plan - [x] `go test ./cmd/publisher/commands/` passes (5 new subtests) - [x] `golangci-lint run ./cmd/publisher/commands/` clean - [ ] Manual: `mcp-publisher init` in a git-tracked directory with `package.json` containing `mcpName` produces a `server.json` whose `name` matches `mcpName`, not the repo slug 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) --- cmd/publisher/commands/init.go | 37 +++++++++--- cmd/publisher/commands/init_test.go | 92 +++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 cmd/publisher/commands/init_test.go diff --git a/cmd/publisher/commands/init.go b/cmd/publisher/commands/init.go index 8e645f5..92d6c8a 100644 --- a/cmd/publisher/commands/init.go +++ b/cmd/publisher/commands/init.go @@ -132,6 +132,26 @@ func detectSubfolder() string { return filepath.ToSlash(relPath) } +// getMcpNameFromPackageJSON returns the `mcpName` field from package.json, or +// "" if not set. mcpName is the authoritative MCP server name when present — +// see docs/modelcontextprotocol-io/quickstart.mdx. +func getMcpNameFromPackageJSON() string { + data, err := os.ReadFile("package.json") + if err != nil { + return "" + } + + var pkg map[string]any + if err := json.Unmarshal(data, &pkg); err != nil { + return "" + } + + if mcpName, ok := pkg["mcpName"].(string); ok && mcpName != "" { + return mcpName + } + return "" +} + func getNameFromPackageJSON() string { data, err := os.ReadFile("package.json") if err != nil { @@ -143,17 +163,13 @@ func getNameFromPackageJSON() string { return "" } - // Prefer mcpName over name, as the server name must match mcpName - name, ok := pkg["mcpName"].(string) + name, ok := pkg["name"].(string) if !ok || name == "" { - name, ok = pkg["name"].(string) - if !ok || name == "" { - return "" - } + return "" } // Convert npm package name to MCP server name - // @org/package -> io.npm.org/package + // @org/package -> io.github.org/package if strings.HasPrefix(name, "@") { parts := strings.Split(name[1:], "/") if len(parts) == 2 { @@ -183,6 +199,13 @@ func getVersionFromPackageJSON() string { } func detectServerName(subfolder string) string { + // mcpName in package.json is the authoritative server name when set, so it + // takes precedence over names inferred from the git remote or the npm + // `name` field. + if name := getMcpNameFromPackageJSON(); name != "" { + return name + } + // Try to get from git remote repoURL := detectRepoURL() if repoURL != "" && strings.Contains(repoURL, "github.com") { diff --git a/cmd/publisher/commands/init_test.go b/cmd/publisher/commands/init_test.go new file mode 100644 index 0000000..e7f3fdb --- /dev/null +++ b/cmd/publisher/commands/init_test.go @@ -0,0 +1,92 @@ +package commands_test + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/modelcontextprotocol/registry/cmd/publisher/commands" + apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInitCommand_PackageJSON(t *testing.T) { + tests := []struct { + name string + pkgJSON string + expectedName string + expectedVersion string + }{ + { + name: "mcpName is returned as-is without transformation", + pkgJSON: `{"name": "@acme/weather", "mcpName": "io.github.acme/weather-mcp", "version": "2.3.4", "repository": "https://gitlab.com/acme/weather"}`, + expectedName: "io.github.acme/weather-mcp", + expectedVersion: "2.3.4", + }, + { + name: "empty mcpName falls back to scoped name transformation", + pkgJSON: `{"name": "@acme/weather", "mcpName": "", "version": "1.2.3", "repository": "https://gitlab.com/acme/weather"}`, + expectedName: "io.github.acme/weather", + expectedVersion: "1.2.3", + }, + { + name: "missing mcpName falls back to scoped name transformation", + pkgJSON: `{"name": "@acme/weather", "version": "1.2.3", "repository": "https://gitlab.com/acme/weather"}`, + expectedName: "io.github.acme/weather", + expectedVersion: "1.2.3", + }, + { + name: "missing version falls back to 1.0.0", + pkgJSON: `{"name": "@acme/weather", "mcpName": "io.github.acme/weather", "repository": "https://gitlab.com/acme/weather"}`, + expectedName: "io.github.acme/weather", + expectedVersion: "1.0.0", + }, + { + name: "mcpName takes precedence over GitHub repository URL", + pkgJSON: `{"name": "weather", "mcpName": "io.github.acme/weather-mcp", "version": "1.0.0", "repository": "https://github.com/someone-else/some-repo"}`, + expectedName: "io.github.acme/weather-mcp", + expectedVersion: "1.0.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := withIsolatedPackageJSON(t, tt.pkgJSON) + + require.NoError(t, commands.InitCommand()) + + data, err := os.ReadFile(filepath.Join(dir, "server.json")) + require.NoError(t, err) + + var got apiv0.ServerJSON + require.NoError(t, json.Unmarshal(data, &got)) + + assert.Equal(t, tt.expectedName, got.Name) + assert.Equal(t, tt.expectedVersion, got.Version) + require.Len(t, got.Packages, 1) + assert.Equal(t, tt.expectedVersion, got.Packages[0].Version, + "package version should match server version") + }) + } +} + +// withIsolatedPackageJSON creates a temp dir outside any git repository, +// writes package.json with the given contents, chdirs into it, and returns +// the directory path. The HOME override prevents init from detecting the +// host repo's git state. +func withIsolatedPackageJSON(t *testing.T, contents string) string { + t.Helper() + + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(contents), 0600)) + + originalDir, err := os.Getwd() + require.NoError(t, err) + t.Cleanup(func() { _ = os.Chdir(originalDir) }) + + require.NoError(t, os.Chdir(dir)) + + return dir +}