docs/mcp-go-oauth.md
This report provides a comprehensive technical analysis of implementing OAuth authentication flows with Model Context Protocol (MCP) servers using Go libraries. We examine redirect URI handling, Dynamic Client Registration (DCR), port allocation strategies, OAuth endpoint discovery, RFC 8252 compliance, practical Go implementations, and common pitfalls. The integration of OAuth 2.1 with MCP enables secure, user-consented access for AI agents to tools and data while adhering to IETF standards. Our investigation reveals that Go libraries like mcp-golang and mcp-go provide robust frameworks for implementing standards-compliant MCP servers with OAuth, though developers must carefully handle callback URIs, PKCE, and token management to avoid security vulnerabilities.
For native applications like CLI-based MCP clients, RFC 8252-compliant loopback redirects are essential. This approach binds a temporary HTTP server to the loopback interface (IPv4 127.0.0.1 or IPv6 ::1) using an OS-assigned ephemeral port. The authorization server must accept any port number since clients cannot predetermine available ports. In Go implementations, libraries like fastmcp automate this by launching a local callback server during OAuth flows, capturing authorization codes after user consent. A critical consideration is preventing port conflicts: Go's net/http package allows binding to port 0 to auto-assign available ports, though this requires coordination with authorization servers to allow dynamic redirect URIs.
Private-use URI schemes (e.g., com.example.app:/oauth2redirect) provide an alternative for desktop environments but introduce security risks from scheme hijacking. RFC 8252 mandates that authorization servers validate redirect URIs against registered patterns, rejecting mismatched ports or schemes. MCP clients should prioritize loopback over private schemes due to deterministic origin validation. For enhanced security, PKCE (Proof Key for Code Exchange) binds authorization requests to specific clients, mitigating interception attacks even if redirect URIs are compromised.
The mcp-golang library simplifies redirect handling through its transport/http module. Developers declare authorized redirect URIs during server registration, while the client's OAuth helper automatically manages browser interactions and code capture. For Cloudflare Workers-based MCP servers, Stytch's implementation validates redirect URIs against pre-registered patterns, rejecting unauthorized ports or domains.
RFC 7591-compliant DCR enables MCP clients to self-register with authorization servers at runtime. The client sends a POST request to the /register endpoint with JSON metadata including client_name, scopes, and redirect_uris. The authorization server responds with a unique client_id and client_secret (if applicable). MCP implementations should include the registration_access_token for future client metadata updates, enabling self-service management.
To prevent malicious registrations, MCP servers should require initial access tokens during registration, issued through out-of-band mechanisms. Metadata validation must enforce:
The godoc-mcp server exemplifies this by binding client registrations to PKCE-enhanced OAuth flows, ensuring only user-authorized agents gain access.
The mcp-go library supports DCR through its DynamicClientRegistration() method, which constructs RFC 7591-compliant requests. Developers supply client metadata, with optional JWT software statements for attested identity:
metadata := map[string]interface{}{
"client_name": "AI Agent",
"scope": "tasks:read tasks:write"
}
resp, err := client.Register(metadata, initialAccessToken)
Post-registration, the client uses issued credentials for subsequent OAuth flows, eliminating pre-registration bottlenecks.
For local callback servers, Go implementations should:
localhost:0 to auto-assign portsAs RFC 8252 notes, servers must accept any loopback port, requiring MCP servers to validate URIs using pattern matching (e.g., http://127.0.0.1:*) rather than exact matches.
Stytch's MCP implementation offloads callback handling to Cloudflare Workers, which:
This server-side strategy simplifies client implementation but requires public endpoints.
The fastmcp client demonstrates robust port handling:
func StartCallbackServer() (string, error) {
listener, _ := net.Listen("tcp", "127.0.0.1:0")
port := listener.Addr().(*net.TCPAddr).Port
go http.Serve(listener, callbackHandler)
return fmt.Sprintf("http://127.0.0.1:%d/callback", port), nil
}
This dynamically assigns ports, passing the URI to OAuth requests while handling conflicts.
RFC 8414 defines the /.well-known/oauth-authorization-server endpoint for disclosing OAuth configuration. MCP servers must publish:
authorization_endpointtoken_endpointregistration_endpointjwks_uriClients retrieve this document to configure OAuth flows dynamically.
The Model Context Protocol specification requires MCP servers to expose OAuth metadata via:
GET /.well-known/oauth-authorization-servermcp_version and tool_endpoints in responsesThis enables clients like godoc-mcp to auto-configure without hardcoded endpoints.
The mcp-golang library automates discovery:
func DiscoverOAuthConfig(serverURL string) (*OAuthMetadata, error) {
resp, _ := http.Get(serverURL + "/.well-known/oauth-authorization-server")
var metadata OAuthMetadata
json.NewDecoder(resp.Body).Decode(&metadata)
return &metadata, nil
}
This retrieves endpoints for dynamic client setup, supporting zero-configuration MCP deployments.
RFC 8252 mandates that for native apps (e.g., MCP CLI clients):
MCP servers violate compliance if they:
Non-compliance risks:
The Sigstore project's OIDC implementation faced vulnerabilities until adopting RFC 8252's port flexibility.
The mcp-go library enforces compliance through:
state parametersClients should validate server compliance during discovery by checking issuer and token_endpoint fields.
The mcp-golang library provides an OAuth-integrated server:
// Server-side
server := mcp.NewServer(transport)
server.RegisterOAuthProvider("google", OAuthConfig{
ClientID: "...",
ClientSecret: "...",
DiscoveryURL: "https://accounts.google.com/.well-known/openid-configuration",
})
// Client-side
client := mcp.NewClient(transport)
token, err := client.AuthenticateOAuth("google", "https://mcp-server/tasks")
This handles discovery, DCR, and token management automatically.
Stytch's Go-based MCP server uses Workers for OAuth:
func handleAuthorize(w http.ResponseWriter, r *http.Request) {
issuer := "https://oauth.example.com"
metadata := DiscoverOAuthMetadata(issuer)
redirectURI := BuildRedirectURI(r, metadata)
http.Redirect(w, r, metadata.AuthEndpoint+"?response_type=code&..."+redirectURI, 302)
}
This serverless pattern delegates token issuance while retaining MCP tool routing.
The mcp-go DCR workflow:
registrationRequest := DynamicRegistrationRequest{
ClientName: "TodoBot",
Scope: "tasks:read tasks:write",
}
resp, _ := client.Register(registrationRequest, initialAccessToken)
client.SetCredentials(resp.ClientID, resp.ClientSecret)
This enables runtime onboarding of AI agents.
Problem: Authorization servers reject dynamically generated URIs.
Solution:
http://127.0.0.1:*)Go Code:
// Auth server config
AllowedRedirectURIs: []string{"http://127.0.0.1:*", "http://[::1]:*"}
Problem: Access token expiration disrupts MCP sessions.
Solution:
context objectsGo Implementation:
func RefreshToken(client *mcp.Client) error {
newToken, err := client.OAuth.RefreshToken()
client.SetAccessToken(newToken)
return err
}
Problem: mcpproxy auth status shows server as "Authenticated & Connected" but token shows "⚠️ EXPIRED".
Explanation: This is expected behavior, not a bug:
auth status reads the original expiration from persistent storage (BBolt), which may lag behind in-memory state.When this occurs:
How to verify: Call a tool on the server. If it works, mcp-go is refreshing tokens automatically.
Contrast with "Disconnected + Expired": If status shows "Disconnected" with "EXPIRED" token, the refresh token also failed (expired or missing), requiring re-authentication via mcpproxy auth login.
Problem: Code interception via port sniffing.
Solution:
S256 PKCE methodcode_verifier to client sessioncode_challenge methodsMCP Spec Requirement: MCP servers must require PKCE for public clients.
RFC 8252 vs Implementation Reality: While RFC 8252 states that authorization servers "MUST allow any port to be specified at the time of the request for loopback IP redirect URIs", many OAuth providers including Cloudflare implement exact URI matching for security reasons.
MCPProxy's Solution: We've implemented a comprehensive Callback Server Coordination System that solves this exact matching requirement while maintaining RFC 8252 compliance.
MCPProxy successfully handles Cloudflare's strict validation through:
1. Callback Server Manager
type CallbackServerManager struct {
servers map[string]*CallbackServer
mu sync.RWMutex
logger *zap.Logger
}
func (m *CallbackServerManager) StartCallbackServer(serverName string) (*CallbackServer, error) {
// Allocate dynamic port
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, fmt.Errorf("failed to allocate dynamic port: %w", err)
}
// Extract port and create redirect URI
addr := listener.Addr().(*net.TCPAddr)
port := addr.Port
redirectURI := fmt.Sprintf("http://127.0.0.1:%d/oauth/callback", port)
// Create dedicated HTTP server for this callback
mux := http.NewServeMux()
server := &http.Server{
Addr: fmt.Sprintf("127.0.0.1:%d", port),
Handler: mux,
}
// Start server with proper callback handling
callbackServer := &CallbackServer{
Port: port,
RedirectURI: redirectURI,
Server: server,
CallbackChan: make(chan map[string]string, 1),
logger: m.logger.With(zap.String("server", serverName)),
}
// Set up callback handler
mux.HandleFunc("/oauth/callback", callbackServer.handleCallback)
// Start server on the allocated port
go server.Serve(listener)
return callbackServer, nil
}
2. OAuth Configuration with Dynamic URI
func CreateOAuthConfig(serverConfig *config.ServerConfig) *client.OAuthConfig {
// Start callback server first to get the exact port
callbackServer, err := globalCallbackManager.StartCallbackServer(serverConfig.Name)
if err != nil {
logger.Error("Failed to start OAuth callback server", zap.Error(err))
return nil
}
// Use the exact redirect URI in OAuth config
return &client.OAuthConfig{
ClientID: "", // Dynamic Client Registration
ClientSecret: "", // PKCE flow
RedirectURI: callbackServer.RedirectURI, // Exact URI with allocated port
Scopes: []string{"mcp.read", "mcp.write"},
PKCEEnabled: true,
AuthServerMetadataURL: buildMetadataURL(serverConfig.URL),
}
}
3. Coordinated OAuth Flow
// In upstream client initialization
func (c *Client) handleOAuthFlow(oauthHandler *client.OAuthHandler) error {
// Step 1: Dynamic Client Registration with exact URI
if err := oauthHandler.RegisterClient(ctx, "mcpproxy-go"); err != nil {
return fmt.Errorf("DCR failed: %w", err)
}
// Step 2: Generate PKCE and state parameters
codeVerifier, _ := client.GenerateCodeVerifier()
codeChallenge := client.GenerateCodeChallenge(codeVerifier)
state, _ := client.GenerateState()
// Step 3: Get authorization URL (uses exact redirect URI from DCR)
authURL, _ := oauthHandler.GetAuthorizationURL(ctx, state, codeChallenge)
// Step 4: Open browser and wait for callback
openBrowser(authURL)
callbackServer, _ := oauth.GetGlobalCallbackManager().GetCallbackServer(c.config.Name)
// Step 5: Wait for authorization code on our callback server
select {
case authParams := <-callbackServer.CallbackChan:
// Step 6: Validate state and exchange code for tokens
if authParams["state"] != state {
return fmt.Errorf("OAuth state mismatch")
}
return oauthHandler.ProcessAuthorizationResponse(ctx,
authParams["code"], state, codeVerifier)
case <-time.After(5 * time.Minute):
return fmt.Errorf("OAuth authorization timeout")
}
}
For Cloudflare MCP servers and other strict OAuth providers:
MCPProxy's implementation successfully handles:
127.0.0.1 loopback with PKCE securityExample Log Output:
2025-07-13T09:30:07.119 | INFO | OAuth callback server started successfully |
{"server": "cloudflare_autorag", "redirect_uri": "http://127.0.0.1:64020/oauth/callback", "port": 64020}
2025-07-13T09:30:07.119 | INFO | Opening browser for OAuth authentication |
{"auth_url": "https://autorag.mcp.cloudflare.com/oauth/authorize?...&redirect_uri=http%3A%2F%2F127.0.0.1%3A64020%2Foauth%2Fcallback..."}
2025-07-13T09:30:56.674 | INFO | OAuth callback received |
{"params": {"code": "...", "state": "..."}}
2025-07-13T09:30:57.507 | INFO | OAuth authentication completed successfully
The integration of OAuth 2.1 with MCP servers in Go requires strict adherence to IETF standards, particularly RFC 8252 for native apps and RFC 7591 for dynamic registration. Our analysis demonstrates that libraries like mcp-golang provide robust foundations, and MCPProxy has successfully implemented a production-ready solution that addresses all critical challenges:
127.0.0.1 loopback interface and OS-assigned ephemeral portsFor Cloudflare MCP OAuth (and other strict providers):
Based on MCPProxy's successful implementation:
MCPProxy's architecture enables future enhancements:
MCPProxy serves as a reference implementation for OAuth 2.1 with MCP servers, demonstrating:
mcp-go library's OAuth capabilitiesThe patterns documented and implemented in MCPProxy establish secure, scalable MCP-OAuth integrations for Go-based AI agent ecosystems, successfully balancing user consent with operational security while meeting the strict requirements of modern OAuth providers like Cloudflare.