fix: handle oauth2.RetrieveError during token refresh (#917)

Previously, an expired refresh token in the oauth2.Token returned from
OAuthHandler.TokenSource() would cause the connection to fail.

From the client perspective, this meant that the MCP connection was in a
hard-failed state with no way to re-authorize.

The change in this commit causes Authorize() to be called in the event
of both an oauth2.RetrieveError from the token endpoint, as well as in
the pre-existing case of a 401/403 HTTP response from the MCP server.
Clients will handle this in their existing Authorize() flows to get a
new valid token for the connection.

Supersedes and closes: #909.

Co-authored-by: Yaroslav <yarolegovich@gmail.com>
This commit is contained in:
Scott Leggett 2026-05-04 19:07:03 +08:00 • committed by GitHub
commit 4f1b3e5062
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -37,6 +37,7 @@ import (
"github.com/modelcontextprotocol/go-sdk/internal/util"
"github.com/modelcontextprotocol/go-sdk/internal/xcontext"
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
"golang.org/x/oauth2"
)
// A StreamableHTTPHandler is an http.Handler that serves streamable MCP
@ -1803,6 +1804,7 @@ func (c *streamableClientConn) Write(ctx context.Context, msg jsonrpc.Message) e
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
if err := c.setMCPHeaders(req); err != nil {
// Failure to set headers means that the request was not sent.
// Wrap with ErrRejected so the jsonrpc2 connection doesn't set writeErr
@ -1934,9 +1936,20 @@ func (c *streamableClientConn) setMCPHeaders(req *http.Request) error {
if ts != nil {
token, err := ts.Token()
if err != nil {
return err
}
if token != nil {
// If the error is an invalid_grant oauth2.RetrieveError it indicates
// that the token source doesn't have valid authorization for the token
// endpoint, per RFC 6749 section 5.2. For example, the refresh token
// may be expired or invalid.
//
// In that case, ignore the error, skip setting the Authorization
// header, and proceed with the request. Callers that support
// authorization flows get a 401/403 response and trigger the
// Authorize() flow to refresh their token.
var retrieveErr *oauth2.RetrieveError
if !errors.As(err, &retrieveErr) || retrieveErr.ErrorCode != "invalid_grant" {
return err
}
} else if token != nil {
req.Header.Set("Authorization", "Bearer "+token.AccessToken)
}
}