mcp: Add client-side scope accumulation (#944)

## Description

This PR implements
https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2350

Fixes #943

---------

Co-authored-by: Maciej Kisiel <mkisiel@google.com>
This commit is contained in:
Guglielmo Colombo 2026-05-18 10:26:02 +02:00 • committed by GitHub
commit bf69179066
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -85,6 +85,9 @@ type Config struct {
// ScopesSupported is an optional list of scopes to advertise in the
// authorization server metadata.
ScopesSupported []string
// TokenScopeFunc, if set, is called with the scope from the authorization
// request and returns the scope string to include in the token response.
TokenScopeFunc func(requestedScope string) string
}
// FakeAuthorizationServer is a fake OAuth 2.0 Authorization Server for testing.
@ -98,6 +101,7 @@ type FakeAuthorizationServer struct {
type codeInfo struct {
CodeChallenge string
Scope string
}
// NewFakeAuthorizationServer creates a new FakeAuthorizationServer.
@ -259,6 +263,7 @@ func (s *FakeAuthorizationServer) handleAuthorize(w http.ResponseWriter, r *http
code := rand.Text()
s.codes[code] = codeInfo{
CodeChallenge: codeChallenge,
Scope: r.URL.Query().Get("scope"),
}
state := r.URL.Query().Get("state")
@ -317,12 +322,18 @@ func (s *FakeAuthorizationServer) handleAuthorizationCodeGrant(w http.ResponseWr
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
resp := map[string]any{
"access_token": "test_access_token",
"token_type": "Bearer",
"expires_in": 3600,
})
}
if s.config.TokenScopeFunc != nil {
if scope := s.config.TokenScopeFunc(codeInfo.Scope); scope != "" {
resp["scope"] = scope
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func (s *FakeAuthorizationServer) handleJWTBearerGrant(w http.ResponseWriter, r *http.Request) {
@ -355,13 +366,18 @@ func (s *FakeAuthorizationServer) handleClientCredentialsGrant(w http.ResponseWr
http.Error(w, "client_credentials grant not supported", http.StatusBadRequest)
return
}
// Client was already authenticated in handleToken.
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{
resp := map[string]any{
"access_token": "test_access_token",
"token_type": "Bearer",
"expires_in": 3600,
})
}
if s.config.TokenScopeFunc != nil {
if scope := s.config.TokenScopeFunc(r.Form.Get("scope")); scope != "" {
resp["scope"] = scope
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func (s *FakeAuthorizationServer) authenticateClient(r *http.Request) error {