charmbracelet/crush · error
failed to decode MCP auth URL: %w
Error message
failed to decode MCP auth URL: %w
What it means
On a 200 response from /workspaces/{id}/mcp/auth-url, the client decodes the body into proto.MCPAuthResponse and returns resp.AuthURL. If the body cannot be decoded as that struct, this error wraps the json failure. It points to a response-body contract mismatch: the server replied successfully but with unexpected content.
Source
Thrown at internal/client/proto.go:363
}
return pending, nil
}
// MCPAuthURL retrieves the current OAuth authorization URL for a named MCP
// server, if a flow is in progress.
func (c *Client) MCPAuthURL(ctx context.Context, id, name string) (string, error) {
q := url.Values{"name": []string{name}}
rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/mcp/auth-url", id), q, nil)
if err != nil {
return "", fmt.Errorf("failed to get MCP auth URL: %w", err)
}
defer rsp.Body.Close()
if rsp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to get MCP auth URL: status code %d", rsp.StatusCode)
}
var resp proto.MCPAuthResponse
if err := json.NewDecoder(rsp.Body).Decode(&resp); err != nil {
return "", fmt.Errorf("failed to decode MCP auth URL: %w", err)
}
return resp.AuthURL, nil
}
// MCPAuthenticate runs the OAuth flow for a named MCP server. The server's
// local browser is suppressed; the caller is responsible for surfacing the
// authorization URL (via polling [Client.MCPPendingAuth] / state events)
// and opening it on the user's machine. The call blocks until the flow
// completes, fails, or ctx is cancelled.
func (c *Client) MCPAuthenticate(ctx context.Context, id, name string) error {
rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/auth", id), nil,
jsonBody(proto.MCPNameRequest{Name: name}),
http.Header{"Content-Type": []string{"application/json"}})
if err != nil {
return fmt.Errorf("failed to authenticate MCP: %w", err)
}
defer rsp.Body.Close()
if rsp.StatusCode != http.StatusOK {View on GitHub (pinned to 7944b8e522)
Solutions
- Capture and log the raw response body to see exactly what the server returned.
- Check Content-Type is application/json; an HTML 200 means you are talking to a proxy/login page, not the API.
- Rebuild/upgrade the client so proto.MCPAuthResponse matches the running server's schema.
- Retry once on transient io.ErrUnexpectedEOF; if persistent, inspect the server handler for this route.
Example fix
// before
var resp proto.MCPAuthResponse
if err := json.NewDecoder(rsp.Body).Decode(&resp); err != nil {
return "", fmt.Errorf("failed to decode MCP auth URL: %w", err)
}
// after
body, err := io.ReadAll(rsp.Body)
if err != nil {
return "", fmt.Errorf("failed to read MCP auth URL: %w", err)
}
var resp proto.MCPAuthResponse
if err := json.Unmarshal(body, &resp); err != nil {
return "", fmt.Errorf("failed to decode MCP auth URL: %w (body: %.200s)", err, body)
} Defensive patterns
Strategy: validation
Validate before calling
// The decode happens inside the SDK; caller-side validation of the result:
authURL, err := client.MCPAuthURL(ctx, id, name)
if err != nil {
return err
}
if authURL == "" {
return errors.New("server returned an empty auth URL")
}
u, err := url.Parse(authURL)
if err != nil || u.Scheme == "" {
return fmt.Errorf("invalid auth URL %q", authURL) Type guard
func hasAuthURL(resp proto.MCPAuthResponse) bool {
return resp.AuthURL != ""
} Try / catch
authURL, err := client.MCPAuthURL(ctx, id, name)
if err != nil {
if strings.Contains(err.Error(), "failed to decode MCP auth URL") {
// 200 but unusable body: likely proxy interference or version skew
log.Warn("auth-url response not decodable; check proxy/server version", "err", err)
return "", errBackoffRetryOrAbort(ctx)
}
return "", err
} Prevention
- Bypass or correctly configure proxies for API hosts so 200 responses are always JSON.
- Deploy matching client and server versions; schema drift is the usual cause.
- On the server, ensure the auth-url handler always writes a complete JSON body before returning.
- Log raw bodies (bounded) on decode failures to speed up contract-mismatch triage.
When it happens
Trigger: Calling MCPAuthURL when a 200 response contains HTML/empty body instead of JSON (proxy or captive portal), or JSON that cannot unmarshal into proto.MCPAuthResponse (missing/renamed auth_url field, wrong types), often after a client/server version skew.
Common situations: Development proxy returning a 200 HTML page; a stub/mock route returning an empty 200; server upgraded with a changed MCPAuthResponse schema while an older client binary is still deployed; truncate/body-EOF errors from flaky connections.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to decode MCP pending auth: %w
- failed to decode MCP prompt response: %w
- failed to decode workspaces: %w
- failed to get MCP auth URL: status code %d
- interactive OAuth authorization required
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/1b22f30000f8c4ce.
Report an issue: GitHub.