Skip to content

Commit 6ca9b3c

Browse files
fix(oauth): advertise only default scopes in metadata
Keep the full OAuth scope catalog available for per-tool step-up challenges, but limit protected resource discovery to the lower-risk default grant. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 9205304 commit 6ca9b3c

3 files changed

Lines changed: 34 additions & 10 deletions

File tree

‎docs/streamable-http.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,23 @@ The OAuth protected resource metadata's `resource` attribute will be populated w
7272
],
7373
"scopes_supported": [
7474
"repo",
75-
...
75+
"read:org",
76+
"read:user",
77+
"user:email",
78+
"read:packages",
79+
"write:packages",
80+
"read:project",
81+
"project",
82+
"gist",
83+
"notifications"
7684
],
7785
...
7886
}
7987
```
8088

8189
This allows OAuth clients to discover authentication requirements and endpoint information automatically.
90+
Scopes excluded from this default set, such as `delete_repo`, are requested only
91+
through a per-tool OAuth authorization challenge when needed.
8292

8393
The HTTP server is the OAuth protected resource, not the authorization server. It
8494
therefore serves `/.well-known/oauth-protected-resource` but does not serve

‎pkg/http/oauth/oauth.go‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,12 @@ const (
2020
OAuthProtectedResourcePrefix = "/.well-known/oauth-protected-resource"
2121
)
2222

23-
// SupportedScopes lists every OAuth scope that an MCP tool may require. HTTP
24-
// protected-resource metadata advertises this full set so clients can step up
25-
// authorization for tools excluded from the default grant.
23+
// SupportedScopes lists every OAuth scope that an MCP tool may require.
2624
var SupportedScopes = scopes.SupportedOAuthScopes()
2725

28-
// DefaultScopes are requested by stdio OAuth unless the operator explicitly
29-
// supplies --oauth-scopes. High-risk scopes such as delete_repo require opt-in.
26+
// DefaultScopes are advertised in protected-resource metadata and requested by
27+
// stdio OAuth unless the operator explicitly supplies --oauth-scopes. Other
28+
// scopes require opt-in through a per-tool authorization challenge.
3029
var DefaultScopes = scopes.DefaultOAuthScopes()
3130

3231
// Config holds the OAuth configuration for the MCP server.
@@ -128,7 +127,7 @@ func (h *AuthHandler) metadataHandler() http.Handler {
128127
Resource: resourceURL,
129128
AuthorizationServers: []string{authorizationServerURL},
130129
ResourceName: "GitHub MCP Server",
131-
ScopesSupported: SupportedScopes,
130+
ScopesSupported: DefaultScopes,
132131
BearerMethodsSupported: []string{"header"},
133132
}
134133

‎pkg/http/oauth/oauth_test.go‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ func TestHandleProtectedResource(t *testing.T) {
436436
host: "api.example.com",
437437
method: http.MethodGet,
438438
expectedStatusCode: http.StatusOK,
439-
expectedScopes: SupportedScopes,
439+
expectedScopes: DefaultScopes,
440440
validateResponse: func(t *testing.T, body map[string]any) {
441441
t.Helper()
442442
assert.Equal(t, "GitHub MCP Server", body["resource_name"])
@@ -573,7 +573,12 @@ func TestHandleProtectedResource(t *testing.T) {
573573
if tc.expectedScopes != nil {
574574
scopes, ok := body["scopes_supported"].([]any)
575575
require.True(t, ok)
576-
assert.Len(t, scopes, len(tc.expectedScopes))
576+
actualScopes := make([]string, len(scopes))
577+
for i, scope := range scopes {
578+
actualScopes[i], ok = scope.(string)
579+
require.True(t, ok)
580+
}
581+
assert.Equal(t, tc.expectedScopes, actualScopes)
577582
}
578583
}
579584
})
@@ -671,10 +676,20 @@ func TestSupportedScopes(t *testing.T) {
671676
assert.Equal(t, expectedScopes, SupportedScopes)
672677
}
673678

674-
func TestDefaultScopesRequiresExplicitDeleteRepoOptIn(t *testing.T) {
679+
func TestDefaultScopesRequireExplicitOptIn(t *testing.T) {
675680
assert.Subset(t, SupportedScopes, DefaultScopes)
676681
assert.Contains(t, SupportedScopes, "delete_repo")
677682
assert.NotContains(t, DefaultScopes, "delete_repo")
683+
assert.Contains(t, SupportedScopes, "workflow")
684+
assert.NotContains(t, DefaultScopes, "workflow")
685+
assert.Contains(t, SupportedScopes, "codespace")
686+
assert.NotContains(t, DefaultScopes, "codespace")
687+
assert.Contains(t, SupportedScopes, "admin:org")
688+
assert.NotContains(t, DefaultScopes, "admin:org")
689+
assert.Contains(t, SupportedScopes, "read:enterprise")
690+
assert.NotContains(t, DefaultScopes, "read:enterprise")
691+
assert.Contains(t, SupportedScopes, "admin:enterprise")
692+
assert.NotContains(t, DefaultScopes, "admin:enterprise")
678693
assert.Contains(t, DefaultScopes, "repo")
679694
}
680695

0 commit comments

Comments
 (0)