fix(publisher): honor mcpName as the authoritative server name in init (#1195)

## 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.<your-username>/*` fallback, producing a broken double-prefix
result.

Example with `"mcpName": "io.github.acme/weather"`:
- Before: `io.github.<your-username>/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.<owner>/<repo>` 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) <noreply@anthropic.com>
This commit is contained in:
Radoslav Dimitrov 2026-04-25 10:14:11 -04:00 • committed by GitHub
commit aaa634ebb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 122 additions and 7 deletions

View file

@ -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") {

View file

@ -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
}