Skip to content

fix(publish): correct misleading org-namespace guidance in 403 and docs - #1483

Open
rdimitrov wants to merge 1 commit into
mainfrom
rdimitrov/validate-issue-1468
Open

fix(publish): correct misleading org-namespace guidance in 403 and docs#1483
rdimitrov wants to merge 1 commit into
mainfrom
rdimitrov/validate-issue-1468

Conversation

@rdimitrov

Copy link
Copy Markdown
Member

The problem

When a publish is refused for an io.github.* namespace, the 403 tells you to make your GitHub organization membership public:

If you're trying to publish to a GitHub organization, you may need to make your organization membership public in your GitHub settings: https://docs.github.com/...

That advice has been wrong since #1383. It replaced the public-membership lookup (GET /users/{login}/orgs) with a check on the caller's organization role (GET /user/memberships/orgs). Public membership no longer factors into the decision at all.

The cost is not theoretical. In #1468 the reporter is an Owner of the org, and their membership is public — confirmed via GET /orgs/qatouch/public_members. The message sent them, and a maintainer, through a week of re-checking GitHub settings that were never the problem.

The docs have the mirror-image defect: authentication.mdx walks the reader through mcp-publisher login github, then explains that org publishing requires Owner, and qualifies only the PAT cases — never saying that the interactive flow cannot grant an org namespace at all.

Root cause of the underlying limitation

mcp-publisher login github authenticates against a GitHub App (Iv23liUydBbI7Z2Q9bOZmcp-registry-login-prod), not an OAuth App. GitHub Apps ignore the OAuth scope parameter, so the read:org read:user requested in cmd/publisher/auth/github-at.go:144 has no effect, and GitHub App user tokens cannot read organization memberships.

Critically, GitHub signals this with 200 OK and an empty array, not a 403 — so none of the safeguards #1383 added (rate-limit detection, X-GitHub-SSO, the deliberate fail-closed path) ever execute. The registry simply concludes "owns no organizations."

Verified against production, same account and same request within the same hour:

credential GET /user/memberships/orgs?state=active&per_page=100&page=1
OAuth App token with read:org 16 orgs, 8 as Owner
GitHub App user token from the prod device flow 0 orgs

The resulting prod JWT carried exactly one permission: io.github.<user>/*.

Consistent with that timeline: io.github.qatouch/qatouch published successfully on 2026-07-03, and v1.8.0 (the first release containing #1383) reached production 2026-07-13.

What this PR changes

Nothing about authorization. Message and documentation text only.

  • publish.go — the org hint now states the two real requirements (Owner role, and a credential that can read that role) and names the credentials that work: GitHub Actions OIDC, or mcp-publisher login github --token <classic PAT with read:org>. The docs URL is extracted to orgNamespaceDocsURL and points at modelcontextprotocol.io/registry/authentication (verified 200).
  • authentication.mdx — a <Warning> stating plainly that the device flow cannot publish to an org namespace, why, that it affects everyone regardless of role or membership visibility, and the two paths that do work.
  • publish_internal_test.go — new in-package tests (_internal_test.go per the repo convention that satisfies testpackage).

Tests

Written test-first; both behaviour tests were watched failing against the old message before the fix:

--- FAIL: TestBuildPermissionErrorMessageDoesNotAdvisePublicisingOrgMembership
--- FAIL: TestBuildPermissionErrorMessageNamesWorkingOrgAuthPaths

Four tests now cover: the misleading advice is gone; the working auth paths are named; the attempted-vs-granted namespaces are still reported (the diagnostic core); and the GitHub guidance does not leak onto DNS/HTTP-verified namespaces.

go build ./..., go test ./internal/... (with PostgreSQL up) and gofmt are clean. golangci-lint reports no new findings — the only hit in a touched file is the pre-existing goconst on publish.go:35, which is also present on main.

Deliberately out of scope

  • The actual fix for org publishing. That needs the login client to become a real OAuth App, which requires an Owner of the modelcontextprotocol org to create it plus a githubClientId change in the three Pulumi stacks. Still under discussion.
  • Observability. fetchOrgMembershipsPage returns nil, nil on a 403 with no log line (github_at.go:270), which is why this was undiagnosable server-side. Worth fixing under any outcome; kept out to keep this reviewable, and I'll follow up with a separate PR.

Closes nothing on its own — #1468 stays open until org publishing actually works.

🤖 Generated with Claude Code

The 403 returned for an unauthorized `io.github.*` publish advised making
your GitHub organization membership public. That stopped being how org
namespaces are authorized in #1383, which replaced the public-membership
lookup with a check on the caller's organization *role*. The advice now
sends reporters off to re-check GitHub settings that were never the
problem -- see #1468, where it cost a week of back-and-forth.

Replace it with the two real requirements (Owner role, plus a credential
that can read that role) and name the credentials that actually can:
GitHub Actions OIDC, or a PAT supplied via --token.

Document the underlying limitation in authentication.mdx. The interactive
device flow authenticates against a GitHub App, and GitHub App user tokens
cannot read organization memberships -- GitHub returns 200 with an empty
list, so the registry concludes you own no organizations. Verified against
production: an OAuth token with read:org returns 16 orgs (8 as Owner) for
the same account where the device-flow token returns 0. The page
previously walked through the device flow and then described org
publishing as though it worked, qualifying only the PAT cases.

Authorization logic is untouched; this changes message and doc text only.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 27, 2026 14:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates publish-time guidance and documentation to accurately explain why GitHub organization namespace publishes can be refused (especially for device-flow logins), without changing authorization behavior.

Changes:

  • Updates the /publish 403 permission error hint for io.github.* publishes and centralizes the docs link in orgNamespaceDocsURL.
  • Adds in-package tests covering the updated permission error messaging behavior.
  • Updates authentication.mdx with an explicit warning that the interactive device flow cannot currently grant org namespaces, and points to working alternatives.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
internal/api/handlers/v0/publish.go Updates the GitHub-related 403 guidance and extracts the auth docs URL constant.
internal/api/handlers/v0/publish_internal_test.go Adds unit tests validating the new error-message content and scoping.
docs/modelcontextprotocol-io/authentication.mdx Documents the device-flow limitation for org namespaces and lists working auth paths.

Comment on lines +104 to +108
errorMsg += ". Publishing under a GitHub organization namespace requires both that you are an Owner of that organization" +
" and that you authenticate with a credential that can read your organization role." +
" The interactive 'mcp-publisher login github' flow cannot read organization roles; instead publish from GitHub Actions" +
" (which uses OIDC), or run 'mcp-publisher login github --token <classic PAT with the read:org scope>'." +
" See " + orgNamespaceDocsURL
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.

2 participants