Skip to content

OAuth fails for remote MCP servers without dynamic client registration (e.g. GitHub) — no way to provide ClientID #4

Description

@ezynda3

Bug Description

When connecting to remote MCP servers that require OAuth but do not support dynamic client registration (e.g. GitHub's https://api.githubcopilot.com/mcp/), the OAuth flow hard-fails because there is no mechanism to provide a pre-registered ClientID through Kit's configuration.

The mcp-go library's transport.OAuthConfig already supports a ClientID field, but Kit's connection pool never populates it. And Kit's OAuthFlowRunner.RunAuthFlow() treats dynamic registration failure as a fatal error with no fallback.

Root Cause

There are two related sub-issues:

1. Connection pool never passes ClientID to the transport

In internal/tools/connection_pool.go, both createStreamableClient() (line ~423) and createSSEClient() (line ~376) construct the OAuthConfig with only RedirectURI, PKCEEnabled, and TokenStore — but never ClientID:

options = append(options, transport.WithHTTPOAuth(transport.OAuthConfig{
	RedirectURI: p.oauthFlow.handler.RedirectURI(),
	PKCEEnabled: true,
	TokenStore:  tokenStore,
	// ClientID is never set!
}))

The mcp-go OAuthConfig struct already has a ClientID field that would work if populated:

// In mcp-go/client/transport/oauth.go
type OAuthConfig struct {
	ClientID     string     // ← Already exists, never populated by Kit
	ClientURI    string
	ClientSecret string
	RedirectURI  string
	Scopes       []string
	TokenStore   TokenStore
	PKCEEnabled  bool
	// ...
}

2. OAuthFlowRunner hard-fails when dynamic registration isn't supported

In internal/tools/oauth_flow.go, RunAuthFlow() (line ~48) calls RegisterClient() when GetClientID() == "" and treats failure as fatal:

if oauthHandler.GetClientID() == "" {
	if err := oauthHandler.RegisterClient(ctx, "kit"); err != nil {
		return fmt.Errorf("oauth flow: dynamic client registration failed: %w", err)
		// ← Hard error, no fallback
	}
}

For servers like GitHub, the OAuth metadata (discovered via RFC 9728) has no registration_endpoint:

// GET https://github.com/.well-known/oauth-authorization-server/login/oauth
{
  "issuer": "https://github.com/login/oauth",
  "authorization_endpoint": "https://github.com/login/oauth/authorize",
  "token_endpoint": "https://github.com/login/oauth/access_token",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"]
  // No registration_endpoint!
}

This causes mcp-go's RegisterClient() to correctly return "server does not support dynamic client registration", which Kit then treats as a fatal error.

Note: mcp-go is behaving correctly here — it returns a clear error when the server doesn't support dynamic registration. The issue is that Kit has no way to provide a ClientID upfront and no graceful fallback when registration fails.

Impact

Any remote MCP server that requires OAuth but doesn't support dynamic client registration is completely unusable with Kit. This includes:

  • GitHub MCP Server (https://api.githubcopilot.com/mcp/) — the official GitHub MCP server
  • Potentially any OAuth server using standard GitHub OAuth, Google OAuth, etc. that requires pre-registered apps

This affects both the Kit CLI/TUI and SDK users.

Reproduction

# .kit.yml
mcpServers:
  github:
    type: remote
    url: https://api.githubcopilot.com/mcp/
$ kit
Warning: Failed to load MCP server 'github': failed to get connection from pool:
  failed to create connection for github: OAuth authorization failed: oauth flow:
  dynamic client registration failed: server does not support dynamic client registration

Suggested Fix

Option A: Add OAuth fields to MCPServerConfig (recommended)

Add optional OAuth configuration to MCPServerConfig in internal/config/config.go:

type MCPServerConfig struct {
	Type          string            `json:"type"`
	Command       []string          `json:"command,omitempty"`
	Environment   map[string]string `json:"environment,omitempty"`
	URL           string            `json:"url,omitempty"`
	Headers       []string          `json:"headers,omitempty"`
	AllowedTools  []string          `json:"allowedTools,omitempty"`
	ExcludedTools []string          `json:"excludedTools,omitempty"`

	// OAuth configuration for remote servers
	OAuthClientID     string   `json:"oauthClientId,omitempty" yaml:"oauthClientId,omitempty"`
	OAuthClientSecret string   `json:"oauthClientSecret,omitempty" yaml:"oauthClientSecret,omitempty"`
	OAuthScopes       []string `json:"oauthScopes,omitempty" yaml:"oauthScopes,omitempty"`

	// Legacy fields...
}

Then in connection_pool.go, pass it through:

oauthCfg := transport.OAuthConfig{
	RedirectURI: p.oauthFlow.handler.RedirectURI(),
	PKCEEnabled: true,
	TokenStore:  tokenStore,
}
if serverConfig.OAuthClientID != "" {
	oauthCfg.ClientID = serverConfig.OAuthClientID
}
if serverConfig.OAuthClientSecret != "" {
	oauthCfg.ClientSecret = serverConfig.OAuthClientSecret
}
if len(serverConfig.OAuthScopes) > 0 {
	oauthCfg.Scopes = serverConfig.OAuthScopes
}
options = append(options, transport.WithHTTPOAuth(oauthCfg))

This would enable configuration like:

mcpServers:
  github:
    type: remote
    url: https://api.githubcopilot.com/mcp/
    oauthClientId: "Ov23liXXXXXXXXXXXXXX"  # From a registered GitHub OAuth App

And for SDK users:

host.AddMCPServer(ctx, "github", kit.MCPServerConfig{
	Type: "remote",
	URL:  "https://api.githubcopilot.com/mcp/",
	OAuthClientID: os.Getenv("GITHUB_OAUTH_CLIENT_ID"),
})

Option B: Add a client registration store

Add a persistent store for client registrations (separate from token store) so that once dynamic registration succeeds, the ClientID is remembered for future connections. This wouldn't help for servers that never support dynamic registration (like GitHub), but would prevent repeated registration for servers that do.

Option C: Graceful fallback in OAuthFlowRunner

When RegisterClient() fails, instead of hard-failing, the flow could check if the server's metadata provides enough information to proceed without a client ID (some OAuth servers allow public clients without registration). This is more of a stretch but worth considering.

Options A and B are complementary — A solves the immediate problem for servers like GitHub, and B improves the experience for servers that do support dynamic registration.

Related

Environment

  • Kit: v0.48.0
  • mcp-go: v0.47.1
  • Go: 1.26.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions