Skip to content

fix(publisher): record repository.id at init so renames stay resolvable - #1570

Open
UgaTheDev wants to merge 1 commit into
modelcontextprotocol:mainfrom
UgaTheDev:fix/publisher-record-repository-id
Open

fix(publisher): record repository.id at init so renames stay resolvable#1570
UgaTheDev wants to merge 1 commit into
modelcontextprotocol:mainfrom
UgaTheDev:fix/publisher-record-repository-id

Conversation

@UgaTheDev

Copy link
Copy Markdown

Towards #1484

Background

#1484 audited the repository.url of the 398 highest-graded remote servers and
found 38 (9.5%) pointing at repositories that have since been renamed or
transferred. No hard 404s — GitHub's REST API follows the redirect — but GraphQL
consumers resolve them as nonexistent (Could not resolve to a Repository), so
the source link is broken for a real class of tooling, and it stays broken once
the redirect eventually lapses.

The gap this closes

pkg/model/types.go already defines the field that solves this:

ID string `json:"id,omitempty" doc:"Repository identifier from the hosting service (e.g., GitHub repo ID). Owned and determined by the source forge. Should remain stable across repository renames and may be used to detect repository resurrection attacks …"`

Nothing populates it. mcp-publisher init builds the repository block from
detectRepoURL() and writes url and source only, so in practice every entry's
sole pointer at its source is a URL — and a URL is not a stable pointer.

This PR resolves the ID at init time and writes it alongside the URL. A rename
after that point no longer loses the entry: the ID still identifies the
repository, and it also gives the "deleted and recreated" detection the schema
doc already describes something to compare against.

Scope and deliberate limits

  • One request, at scaffolding time. Not in the publish path, so it adds no
    latency or GitHub rate-limit exposure to publishing.
  • Best effort. init is otherwise fully offline. Being off the network,
    rate limited, or pointed at a private repo leaves the field empty rather than
    failing the command. GITHUB_TOKEN is used when set, mostly so a rate-limited
    developer still gets an ID.
  • GitHub only. repository.source also allows gitlab, and detectRepoURL
    can produce a plain git source. Those return empty; GitLab's equivalent
    lookup is easy to add if you want it, but I did not want to guess at the
    ID semantics for a forge the audit did not cover.
  • Host matching is exact (github.com, optionally www.) so a lookalike host
    cannot get us to fetch an ID that belongs to somebody else. Covered by a test.

What this does not do

Two parts of #1484 are maintainer calls, not code I should pick unilaterally, and
I have written them up on the issue rather than guessing here:

  1. Backfilling the 38 existing entries. The registry's data is in the
    database, not this repo — CLAUDE.md is explicit that data/seed.json is
    local dev seed data and must not be used to publish. So a data fix cannot
    arrive as a PR to this repository at all; it needs an operator-run migration,
    and someone has to decide whether rewriting a publisher's repository.url
    without them re-publishing is acceptable.
  2. Publish-time behaviour on a redirecting URL — reject, warn, or accept and
    record. ValidateServerJSON is currently pure and offline; the only network
    validation lives in ValidatePublishRequest behind
    cfg.EnableRegistryValidation, and it returns a bare error with no channel
    for a warning even though ValidationIssueSeverityWarning exists elsewhere.
    Wiring a warning through is a real design decision about the publish contract.

Test

cmd/publisher/commands/init_internal_test.go:

  • TestParseGitHubOwnerRepo — the URL shapes detectRepoURL actually produces
    (plain, www., trailing slash, .git), plus the ones that must not be
    treated as GitHub: gitlab, a github.com.evil.example lookalike, a deep
    /tree/main/... path, an owner with no repo.
  • TestDetectRepoID — against an httptest stub: records the numeric id and
    requests /repos/{owner}/{repo}; and stays empty on 404 / non-GitHub source /
    empty URL, so a failed lookup can never fail init.
$ go test ./cmd/publisher/...
ok      github.com/modelcontextprotocol/registry/cmd/publisher/auth      2.363s
ok      github.com/modelcontextprotocol/registry/cmd/publisher/commands  4.838s

$ go build ./...      # clean
$ gofmt -l cmd/       # clean

golangci-lint was not available in my environment, so that gate is unverified
locally.

`repository.id` is documented in the schema as the forge's own identifier,
stable across renames and usable to detect a repository being deleted and
recreated. Nothing ever populates it: `mcp-publisher init` writes only `url`
and `source`, so in practice every entry's only pointer at its source is a
URL.

A URL is not a stable pointer. Renaming or transferring a GitHub repository
leaves the registered URL resolving only through GitHub's redirect — which
the REST API follows but GraphQL does not, so GraphQL consumers see
"Could not resolve to a Repository" and the source link is effectively
broken. modelcontextprotocol#1484 measured this across the 398 highest-graded remote servers:
38 of them (9.5%) already point at renamed or transferred repos.

Resolve the ID at init and write it alongside the URL. One request, at
scaffolding time rather than in the publish path, and the entry stays
re-resolvable afterwards even once its URL has gone stale.

Deliberately best effort: `init` is otherwise fully offline, so being off
the network, rate limited, or pointed at a private repo leaves the field
empty rather than failing the command. `GITHUB_TOKEN` is used when set.

This stops new entries from rotting. It does not backfill the 38 already
affected, and it does not decide whether publish should reject or warn on a
URL that redirects — both need a maintainer call, raised on the issue.

Signed-off-by: Kush Zingade <kush.zingade@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant