From bac04636bfca510cc4ef8bf59558afda9634269b Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Tue, 21 Apr 2026 22:24:10 +0300 Subject: [PATCH] feat(config): add noOAuth flag to skip OAuth on public MCP servers - Add NoOAuth field to MCPServerConfig with JSON/YAML support - Guard OAuth error handling and transport setup with the new flag - Prevents failed dynamic client registration on servers like PubMed that do not support OAuth --- internal/config/config.go | 10 ++++++++++ internal/tools/connection_pool.go | 24 ++++++++++++------------ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index dc9aa347..39ae98f8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,6 +30,14 @@ type MCPServerConfig struct { OAuthClientSecret string `json:"oauthClientSecret,omitempty" yaml:"oauthClientSecret,omitempty"` OAuthScopes []string `json:"oauthScopes,omitempty" yaml:"oauthScopes,omitempty"` + // NoOAuth disables OAuth transport configuration for this server, even + // when the connection pool has an auth handler. Use this for public MCP + // servers (e.g. PubMed) that don't require authentication. Without this + // flag, the pool would attach OAuth transport to every remote server, + // causing proactive dynamic-client-registration attempts that fail on + // servers that don't support it. + NoOAuth bool `json:"noOAuth,omitempty" yaml:"noOAuth,omitempty"` + // InProcessServer holds a live *server.MCPServer for in-process transport. // When set (and Type is "inprocess"), the connection pool creates an // in-process client instead of spawning a subprocess or making HTTP calls. @@ -59,6 +67,7 @@ func (s *MCPServerConfig) UnmarshalJSON(data []byte) error { OAuthClientID string `json:"oauthClientId,omitempty" yaml:"oauthClientId,omitempty"` OAuthClientSecret string `json:"oauthClientSecret,omitempty" yaml:"oauthClientSecret,omitempty"` OAuthScopes []string `json:"oauthScopes,omitempty" yaml:"oauthScopes,omitempty"` + NoOAuth bool `json:"noOAuth,omitempty" yaml:"noOAuth,omitempty"` } // Also try legacy format @@ -86,6 +95,7 @@ func (s *MCPServerConfig) UnmarshalJSON(data []byte) error { s.OAuthClientID = newConfig.OAuthClientID s.OAuthClientSecret = newConfig.OAuthClientSecret s.OAuthScopes = newConfig.OAuthScopes + s.NoOAuth = newConfig.NoOAuth return nil } diff --git a/internal/tools/connection_pool.go b/internal/tools/connection_pool.go index ce58aa29..b8407e47 100644 --- a/internal/tools/connection_pool.go +++ b/internal/tools/connection_pool.go @@ -243,10 +243,12 @@ func (p *MCPConnectionPool) performHealthCheck(ctx context.Context, conn *MCPCon // createConnection creates a new connection func (p *MCPConnectionPool) createConnection(ctx context.Context, serverName string, serverConfig config.MCPServerConfig) (*MCPConnection, error) { + oauthEnabled := p.oauthFlow != nil && !serverConfig.NoOAuth + mcpClient, err := p.createMCPClient(ctx, serverName, serverConfig) if err != nil { // SSE transport can return OAuth error during Start() - if p.oauthFlow != nil && IsOAuthError(err) { + if oauthEnabled && IsOAuthError(err) { if flowErr := p.oauthFlow.RunAuthFlow(ctx, serverName, err); flowErr != nil { return nil, fmt.Errorf("OAuth authorization failed: %w", flowErr) } @@ -262,7 +264,7 @@ func (p *MCPConnectionPool) createConnection(ctx context.Context, serverName str if err := p.initializeClient(ctx, mcpClient); err != nil { // Streamable HTTP transport returns OAuth error during Initialize() - if p.oauthFlow != nil && IsOAuthError(err) { + if oauthEnabled && IsOAuthError(err) { if flowErr := p.oauthFlow.RunAuthFlow(ctx, serverName, err); flowErr != nil { _ = mcpClient.Close() return nil, fmt.Errorf("OAuth authorization failed: %w", flowErr) @@ -363,11 +365,11 @@ func (p *MCPConnectionPool) createSSEClient(ctx context.Context, serverConfig co } } - // Enable OAuth for remote transports when an auth handler is configured. - // The OAuthConfig uses PKCE and the handler's redirect URI. If the server - // config provides a pre-registered ClientID (for servers that don't support - // dynamic client registration, e.g. GitHub), it is passed through directly. - if p.oauthFlow != nil { + // Enable OAuth for remote transports when an auth handler is configured + // and the server hasn't opted out via NoOAuth. Public MCP servers (e.g. + // PubMed) set NoOAuth to skip dynamic client registration and token + // exchange, which would otherwise fail with a 404. + if p.oauthFlow != nil && !serverConfig.NoOAuth { tokenStore, tsErr := p.createTokenStore(serverConfig.URL) if tsErr != nil { return nil, fmt.Errorf("failed to create token store: %w", tsErr) @@ -420,11 +422,9 @@ func (p *MCPConnectionPool) createStreamableClient(ctx context.Context, serverCo } } - // Enable OAuth for remote transports when an auth handler is configured. - // The OAuthConfig uses PKCE and the handler's redirect URI. If the server - // config provides a pre-registered ClientID (for servers that don't support - // dynamic client registration, e.g. GitHub), it is passed through directly. - if p.oauthFlow != nil { + // Enable OAuth for remote transports when an auth handler is configured + // and the server hasn't opted out via NoOAuth. + if p.oauthFlow != nil && !serverConfig.NoOAuth { tokenStore, tsErr := p.createTokenStore(serverConfig.URL) if tsErr != nil { return nil, fmt.Errorf("failed to create token store: %w", tsErr)