Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ type MemoryTokenStore = transport.MemoryTokenStore
// NewMemoryTokenStore is a convenience function that wraps transport.NewMemoryTokenStore
var NewMemoryTokenStore = transport.NewMemoryTokenStore

// OAuthProvider is a convenience type that wraps transport.OAuthProvider
type OAuthProvider = transport.OAuthProvider

// GitHubProvider is a convenience variable that wraps transport.GitHubProvider
var GitHubProvider = transport.GitHubProvider

// NewOAuthStreamableHttpClient creates a new streamable-http-based MCP client with OAuth support.
// Returns an error if the URL is invalid.
func NewOAuthStreamableHttpClient(baseURL string, oauthConfig OAuthConfig, options ...transport.StreamableHTTPCOption) (*Client, error) {
Expand Down
19 changes: 19 additions & 0 deletions client/transport/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type OAuthConfig struct {
// HTTPClient is an optional HTTP client to use for requests.
// If nil, a default HTTP client with a 30 second timeout will be used.
HTTPClient *http.Client
// Provider is an optional OAuth provider configuration.
// If set, it overrides the auto-discovery mechanism.
Provider *OAuthProvider
}

// TokenStore is an interface for storing and retrieving OAuth tokens.
Expand Down Expand Up @@ -358,6 +361,17 @@ type OAuthProtectedResource struct {
// getServerMetadata fetches the OAuth server metadata
func (h *OAuthHandler) getServerMetadata(ctx context.Context) (*AuthServerMetadata, error) {
h.metadataOnce.Do(func() {
// If Provider is explicitly configured, use it directly
// This bypasses RFC 8414 discovery which fails for split-domain providers like GitHub
if h.config.Provider != nil {
h.serverMetadata = &AuthServerMetadata{
Issuer: h.baseURL, // RFC 8414 issuers often don't match API domains
AuthorizationEndpoint: h.config.Provider.AuthorizationEndpoint,
TokenEndpoint: h.config.Provider.TokenEndpoint,
}
return
}

// If AuthServerMetadataURL is explicitly provided, use it directly
if h.config.AuthServerMetadataURL != "" {
h.fetchMetadataFromURL(ctx, h.config.AuthServerMetadataURL)
Expand Down Expand Up @@ -543,6 +557,11 @@ func (h *OAuthHandler) getDefaultEndpoints(baseURL string) (*AuthServerMetadata,

// RegisterClient performs dynamic client registration
func (h *OAuthHandler) RegisterClient(ctx context.Context, clientName string) error {
// Check if the provider explicitly forbids dynamic registration
if h.config.Provider != nil && !h.config.Provider.SupportsDynamicRegistration {
return errors.New("dynamic client registration is not supported by this provider")
}

metadata, err := h.getServerMetadata(ctx)
if err != nil {
return fmt.Errorf("failed to get server metadata: %w", err)
Expand Down
18 changes: 18 additions & 0 deletions client/transport/oauth_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package transport

// OAuthProvider defines the configuration for a specific OAuth provider.
// This is used to bypass auto-discovery for providers that do not support RFC 8414
// or have split domains (e.g. GitHub).
type OAuthProvider struct {
AuthorizationEndpoint string
TokenEndpoint string
SupportsDynamicRegistration bool
}

// GitHubProvider is the preset configuration for GitHub OAuth.
// GitHub does not support RFC 7591 dynamic registration and uses split domains for API and generic OAuth.
var GitHubProvider = &OAuthProvider{
AuthorizationEndpoint: "https://github.com/login/oauth/authorize",
TokenEndpoint: "https://github.com/login/oauth/access_token",
SupportsDynamicRegistration: false,
}
46 changes: 27 additions & 19 deletions examples/oauth_client/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
# OAuth Client Example

This example demonstrates how to use the OAuth capabilities of the MCP Go client to authenticate with an MCP server that requires OAuth authentication.
This example demonstrates how to use the OAuth capabilities of the MCP Go client to authenticate with an MCP server that requires OAuth authentication, specifically configured for GitHub.

## Features

- OAuth 2.1 authentication with PKCE support
- Dynamic client registration
- OAuth 2.0 authentication
- Preset configuration for GitHub (`client.GitHubProvider`)
- Authorization code flow
- Token refresh
- Local callback server for handling OAuth redirects

## Standard vs Manual Provider Configuration

By default, the client uses **RFC 8414** to automatically discover the authorization server metadata from the MCP server's base URL. This works for most standard OAuth implementations.

However, some providers (like GitHub) do not support RFC 8414 or host their OAuth endpoints on a different domain than their API. For these cases, you can provide an optional `Provider` configuration to manually specify the endpoints and capabilities.

This example uses the manual `GitHubProvider` configuration. For standard providers, you can omit the `Provider` field and `PKCEEnabled` is `true` by default.

## Usage

```bash
Expand All @@ -23,18 +31,17 @@ go run main.go

## How it Works

1. The client attempts to initialize a connection to the MCP server
2. If the server requires OAuth authentication, it will return a 401 Unauthorized response
3. The client detects this and starts the OAuth flow:
- Generates PKCE code verifier and challenge
1. The example initializes the client with `client.GitHubProvider` to bypass incorrect auto-discovery for GitHub.
2. It uses `read:user` scope as required by GitHub.
3. PKCE is disabled for simplicity (GitHub OAuth Apps support PKCE optionally).
4. The client starts the OAuth flow:
- Generates a state parameter for security
- Opens a browser to the authorization URL
- Starts a local server to handle the callback
4. The user authorizes the application in their browser
5. The authorization server redirects back to the local callback server
6. The client exchanges the authorization code for an access token
7. The client retries the initialization with the access token
8. The client can now make authenticated requests to the MCP server
- Opens a browser to the GitHub authorization URL
- Starts a local server to handle the callback at `http://localhost:8085/oauth/callback`
5. The user authorizes the application in their browser.
6. GitHub redirects back to the local callback server.
7. The client exchanges the authorization code for an access token.
8. The client initializes the connection to the MCP server with the authenticated token.

## Configuration

Expand All @@ -43,17 +50,18 @@ Edit the following constants in `main.go` to match your environment:
```go
const (
// Replace with your MCP server URL
serverURL = "https://api.example.com/v1/mcp"
serverURL = "https://api.githubcopilot.com/v1/mcp"
// Use a localhost redirect URI for this example
redirectURI = "http://localhost:8085/oauth/callback"
)
```

**Note:** Ensure your GitHub OAuth App is configured with `http://localhost:8085/oauth/callback` as the Authorization callback URL.

## OAuth Scopes

The example requests the following scopes:
The example requests the following scope:

- `mcp.read` - Read access to MCP resources
- `mcp.write` - Write access to MCP resources
- `read:user` - Read access to user profile (minimal scope for testing)

You can modify the scopes in the `oauthConfig` to match the requirements of your MCP server.
You can modify the scopes in the `oauthConfig` to match the requirements of your specific application or MCP server.
5 changes: 3 additions & 2 deletions examples/oauth_client/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ func main() {
ClientID: os.Getenv("MCP_CLIENT_ID"),
ClientSecret: os.Getenv("MCP_CLIENT_SECRET"),
RedirectURI: redirectURI,
Scopes: []string{"mcp.read", "mcp.write"},
Scopes: []string{"read:user"},
TokenStore: tokenStore,
PKCEEnabled: true, // Enable PKCE for public clients
PKCEEnabled: false, // Disable PKCE for GitHub (not supported)
Provider: client.GitHubProvider,
}

// Create the client with OAuth support
Expand Down