Visitar URL original
fix(publish): correct misleading org-namespace guidance in 403 and docs · modelcontextprotocol/registry@ba3117c · GitHub
Skip to content

Commit ba3117c

Browse files
rdimitrovclaude
andcommitted
fix(publish): correct misleading org-namespace guidance in 403 and docs
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>
1 parent 82441f0 commit ba3117c

3 files changed

Lines changed: 98 additions & 3 deletions

File tree

‎docs/modelcontextprotocol-io/authentication.mdx‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,18 @@ GitHub authentication always grants your personal namespace, `io.github.<your-us
5353

5454
To publish under an **organization** namespace (`io.github.<orgname>/*`), you must be an **Owner** of that organization. Ordinary org membership is no longer sufficient: the registry checks your membership role and only grants the org namespace to admins. This prevents anyone who merely belongs to an org from publishing — or overwriting — servers under the org's name.
5555

56-
If you authenticate with a Personal Access Token (for example in CI), the token must let the registry read your organization role. A token that can't will still publish to your personal namespace, but org publishing will be silently unavailable. The exact requirement depends on the token type:
56+
<Warning>
57+
**`mcp-publisher login github` cannot currently publish to an organization namespace.**
58+
59+
The interactive device flow authenticates against a GitHub App, and GitHub App user tokens cannot read organization memberships — GitHub returns an empty list, so the registry sees no organizations you own and grants only your personal namespace. This affects everyone, regardless of your role in the organization or whether your membership is public. Tracking issue: [#1468](https://github.com/modelcontextprotocol/registry/issues/1468).
60+
61+
To publish an organization server today, use one of the two paths that can prove your organization role:
62+
63+
- **[GitHub Actions](./github-actions)** — publishes via OIDC, which is unaffected.
64+
- **A Personal Access Token** — `mcp-publisher login github --token <YOUR_TOKEN>`, meeting the requirements below.
65+
</Warning>
66+
67+
If you authenticate with a Personal Access Token (in CI, or to work around the limitation above), the token must let the registry read your organization role. A token that can't will still publish to your personal namespace, but org publishing will be silently unavailable. The exact requirement depends on the token type:
5768

5869
- **Classic PAT**: grant the `read:org` scope.
5970
- **Fine-grained PAT**: grant the **Organization permissions → Members → Read-only** permission (the fine-grained equivalent of `read:org`). Without it, GitHub returns no organization membership for the token and you'll get your personal namespace only. Note that a fine-grained PAT is bound to a single resource owner, so it can only see the organization it was created for.

‎internal/api/handlers/v0/publish.go‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ func RegisterPublishEndpoint(api huma.API, pathPrefix string, registry service.R
7373
})
7474
}
7575

76+
// orgNamespaceDocsURL documents which credentials can prove a GitHub organization role,
77+
// and is where an operator should look when an org-namespace publish is refused.
78+
const orgNamespaceDocsURL = "https://modelcontextprotocol.io/registry/authentication"
79+
7680
// buildPermissionErrorMessage creates a detailed error message showing what permissions
7781
// the user has and what they're trying to publish
7882
func buildPermissionErrorMessage(attemptedResource string, permissions []auth.Permission) string {
@@ -91,9 +95,17 @@ func buildPermissionErrorMessage(attemptedResource string, permissions []auth.Pe
9195
}
9296
errorMsg += ". Attempting to publish: " + attemptedResource
9397

94-
// Add helpful hint for GitHub organization publishing issues
98+
// Add guidance for GitHub namespace failures. An org namespace is granted from the
99+
// caller's org *role*, and only some credential types can prove that role — so the
100+
// actionable advice is which credential to authenticate with, not how to configure
101+
// GitHub. (Publicising org membership, which this hint used to recommend, stopped
102+
// being relevant when the role check replaced the public-membership lookup.)
95103
if strings.HasPrefix(attemptedResource, "io.github.") {
96-
errorMsg += ". 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/en/account-and-profile/how-tos/organization-membership/publicizing-or-hiding-organization-membership"
104+
errorMsg += ". If you're trying to publish under a GitHub organization namespace, you must be an Owner of that organization" +
105+
" and authenticate with a credential that can read your organization role." +
106+
" The interactive 'mcp-publisher login github' flow cannot read organization roles; instead publish from GitHub Actions" +
107+
" (which uses OIDC), or run 'mcp-publisher login github --token <classic PAT with the read:org scope>'." +
108+
" See " + orgNamespaceDocsURL
97109
}
98110

99111
return errorMsg
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package v0
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/modelcontextprotocol/registry/internal/auth"
8+
)
9+
10+
// personalOnlyPermissions models what the github-at exchange actually issues for a
11+
// device-flow login: the caller's own namespace and nothing else. This is the exact
12+
// shape that produces the 403 reported in issue #1468.
13+
func personalOnlyPermissions(username string) []auth.Permission {
14+
return []auth.Permission{{
15+
Action: auth.PermissionActionPublish,
16+
ResourcePattern: "io.github." + username + "/*",
17+
}}
18+
}
19+
20+
func TestBuildPermissionErrorMessageDoesNotAdvisePublicisingOrgMembership(t *testing.T) {
21+
msg := buildPermissionErrorMessage("io.github.qatouch/qatouch", personalOnlyPermissions("premnathm"))
22+
23+
// Public org membership stopped being how org namespaces are authorised once the
24+
// registry switched to checking the caller's org *role*. Still advising it sends
25+
// reporters off to re-check GitHub settings that were never the problem.
26+
if strings.Contains(msg, "publicizing-or-hiding-organization-membership") {
27+
t.Errorf("message still links the publicise-membership doc:\n%s", msg)
28+
}
29+
if strings.Contains(msg, "membership public") {
30+
t.Errorf("message still advises making org membership public:\n%s", msg)
31+
}
32+
}
33+
34+
func TestBuildPermissionErrorMessageNamesWorkingOrgAuthPaths(t *testing.T) {
35+
msg := buildPermissionErrorMessage("io.github.qatouch/qatouch", personalOnlyPermissions("premnathm"))
36+
37+
// Both real requirements, and both credentials that can actually satisfy them.
38+
for _, want := range []string{
39+
"Owner",
40+
"read:org",
41+
"--token",
42+
"GitHub Actions",
43+
} {
44+
if !strings.Contains(msg, want) {
45+
t.Errorf("message does not mention %q:\n%s", want, msg)
46+
}
47+
}
48+
}
49+
50+
func TestBuildPermissionErrorMessageKeepsAttemptedAndGrantedNamespaces(t *testing.T) {
51+
msg := buildPermissionErrorMessage("io.github.qatouch/qatouch", personalOnlyPermissions("premnathm"))
52+
53+
// The diagnostic core of the message: what you asked for vs what you hold.
54+
if !strings.Contains(msg, "io.github.qatouch/qatouch") {
55+
t.Errorf("message omits the attempted resource:\n%s", msg)
56+
}
57+
if !strings.Contains(msg, "io.github.premnathm/*") {
58+
t.Errorf("message omits the granted pattern:\n%s", msg)
59+
}
60+
}
61+
62+
func TestBuildPermissionErrorMessageOmitsGitHubGuidanceForOtherNamespaces(t *testing.T) {
63+
msg := buildPermissionErrorMessage("com.example/server", []auth.Permission{{
64+
Action: auth.PermissionActionPublish,
65+
ResourcePattern: "com.other/*",
66+
}})
67+
68+
// DNS/HTTP-verified namespaces have nothing to do with GitHub org roles.
69+
if strings.Contains(msg, "GitHub organization") {
70+
t.Errorf("GitHub org guidance leaked into a non-GitHub namespace:\n%s", msg)
71+
}
72+
}

0 commit comments

Comments
 (0)