diff --git a/docs/streamable-http.md b/docs/streamable-http.md index a1e6d9890a..ae86dba8ef 100644 --- a/docs/streamable-http.md +++ b/docs/streamable-http.md @@ -72,13 +72,23 @@ The OAuth protected resource metadata's `resource` attribute will be populated w ], "scopes_supported": [ "repo", - ... + "read:org", + "read:user", + "user:email", + "read:packages", + "write:packages", + "read:project", + "project", + "gist", + "notifications" ], ... } ``` This allows OAuth clients to discover authentication requirements and endpoint information automatically. +Scopes excluded from this default set, such as `delete_repo`, are requested only +through a per-tool OAuth authorization challenge when needed. The HTTP server is the OAuth protected resource, not the authorization server. It therefore serves `/.well-known/oauth-protected-resource` but does not serve diff --git a/pkg/http/oauth/oauth.go b/pkg/http/oauth/oauth.go index 1d8ba70afa..43d57d57bf 100644 --- a/pkg/http/oauth/oauth.go +++ b/pkg/http/oauth/oauth.go @@ -20,13 +20,12 @@ const ( OAuthProtectedResourcePrefix = "/.well-known/oauth-protected-resource" ) -// SupportedScopes lists every OAuth scope that an MCP tool may require. HTTP -// protected-resource metadata advertises this full set so clients can step up -// authorization for tools excluded from the default grant. +// SupportedScopes lists every OAuth scope that an MCP tool may require. var SupportedScopes = scopes.SupportedOAuthScopes() -// DefaultScopes are requested by stdio OAuth unless the operator explicitly -// supplies --oauth-scopes. High-risk scopes such as delete_repo require opt-in. +// DefaultScopes are advertised in protected-resource metadata and requested by +// stdio OAuth unless the operator explicitly supplies --oauth-scopes. Other +// scopes require opt-in through a per-tool authorization challenge. var DefaultScopes = scopes.DefaultOAuthScopes() // Config holds the OAuth configuration for the MCP server. @@ -128,7 +127,7 @@ func (h *AuthHandler) metadataHandler() http.Handler { Resource: resourceURL, AuthorizationServers: []string{authorizationServerURL}, ResourceName: "GitHub MCP Server", - ScopesSupported: SupportedScopes, + ScopesSupported: DefaultScopes, BearerMethodsSupported: []string{"header"}, } diff --git a/pkg/http/oauth/oauth_test.go b/pkg/http/oauth/oauth_test.go index d2176880de..c2ef660104 100644 --- a/pkg/http/oauth/oauth_test.go +++ b/pkg/http/oauth/oauth_test.go @@ -436,7 +436,18 @@ func TestHandleProtectedResource(t *testing.T) { host: "api.example.com", method: http.MethodGet, expectedStatusCode: http.StatusOK, - expectedScopes: SupportedScopes, +expectedScopes: []string{ + "repo", + "read:org", + "read:user", + "user:email", + "read:packages", + "write:packages", + "read:project", + "project", + "gist", + "notifications", + }, validateResponse: func(t *testing.T, body map[string]any) { t.Helper() assert.Equal(t, "GitHub MCP Server", body["resource_name"]) @@ -573,7 +584,12 @@ func TestHandleProtectedResource(t *testing.T) { if tc.expectedScopes != nil { scopes, ok := body["scopes_supported"].([]any) require.True(t, ok) - assert.Len(t, scopes, len(tc.expectedScopes)) + actualScopes := make([]string, len(scopes)) + for i, scope := range scopes { + actualScopes[i], ok = scope.(string) + require.True(t, ok) + } + assert.Equal(t, tc.expectedScopes, actualScopes) } } }) @@ -671,10 +687,20 @@ func TestSupportedScopes(t *testing.T) { assert.Equal(t, expectedScopes, SupportedScopes) } -func TestDefaultScopesRequiresExplicitDeleteRepoOptIn(t *testing.T) { +func TestDefaultScopesRequireExplicitOptIn(t *testing.T) { assert.Subset(t, SupportedScopes, DefaultScopes) assert.Contains(t, SupportedScopes, "delete_repo") assert.NotContains(t, DefaultScopes, "delete_repo") + assert.Contains(t, SupportedScopes, "workflow") + assert.NotContains(t, DefaultScopes, "workflow") + assert.Contains(t, SupportedScopes, "codespace") + assert.NotContains(t, DefaultScopes, "codespace") + assert.Contains(t, SupportedScopes, "admin:org") + assert.NotContains(t, DefaultScopes, "admin:org") + assert.Contains(t, SupportedScopes, "read:enterprise") + assert.NotContains(t, DefaultScopes, "read:enterprise") + assert.Contains(t, SupportedScopes, "admin:enterprise") + assert.NotContains(t, DefaultScopes, "admin:enterprise") assert.Contains(t, DefaultScopes, "repo") }