{"record":{"id":"1b22f30000f8c4ce","repo":"charmbracelet/crush","slug":"failed-to-decode-mcp-auth-url-w","errorCode":null,"errorMessage":"failed to decode MCP auth URL: %w","messagePattern":"failed to decode MCP auth URL: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/client/proto.go","lineNumber":363,"sourceCode":"\t}\n\treturn pending, nil\n}\n\n// MCPAuthURL retrieves the current OAuth authorization URL for a named MCP\n// server, if a flow is in progress.\nfunc (c *Client) MCPAuthURL(ctx context.Context, id, name string) (string, error) {\n\tq := url.Values{\"name\": []string{name}}\n\trsp, err := c.get(ctx, fmt.Sprintf(\"/workspaces/%s/mcp/auth-url\", id), q, nil)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get MCP auth URL: %w\", err)\n\t}\n\tdefer rsp.Body.Close()\n\tif rsp.StatusCode != http.StatusOK {\n\t\treturn \"\", fmt.Errorf(\"failed to get MCP auth URL: status code %d\", rsp.StatusCode)\n\t}\n\tvar resp proto.MCPAuthResponse\n\tif err := json.NewDecoder(rsp.Body).Decode(&resp); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to decode MCP auth URL: %w\", err)\n\t}\n\treturn resp.AuthURL, nil\n}\n\n// MCPAuthenticate runs the OAuth flow for a named MCP server. The server's\n// local browser is suppressed; the caller is responsible for surfacing the\n// authorization URL (via polling [Client.MCPPendingAuth] / state events)\n// and opening it on the user's machine. The call blocks until the flow\n// completes, fails, or ctx is cancelled.\nfunc (c *Client) MCPAuthenticate(ctx context.Context, id, name string) error {\n\trsp, err := c.post(ctx, fmt.Sprintf(\"/workspaces/%s/mcp/auth\", id), nil,\n\t\tjsonBody(proto.MCPNameRequest{Name: name}),\n\t\thttp.Header{\"Content-Type\": []string{\"application/json\"}})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to authenticate MCP: %w\", err)\n\t}\n\tdefer rsp.Body.Close()\n\tif rsp.StatusCode != http.StatusOK {","sourceCodeStart":345,"sourceCodeEnd":381,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/client/proto.go#L345-L381","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nvar resp proto.MCPAuthResponse\nif err := json.NewDecoder(rsp.Body).Decode(&resp); err != nil {\n    return \"\", fmt.Errorf(\"failed to decode MCP auth URL: %w\", err)\n}\n\n// after\nbody, err := io.ReadAll(rsp.Body)\nif err != nil {\n    return \"\", fmt.Errorf(\"failed to read MCP auth URL: %w\", err)\n}\nvar resp proto.MCPAuthResponse\nif err := json.Unmarshal(body, &resp); err != nil {\n    return \"\", fmt.Errorf(\"failed to decode MCP auth URL: %w (body: %.200s)\", err, body)\n}","handlingStrategy":"validation","validationCode":"// The decode happens inside the SDK; caller-side validation of the result:\nauthURL, err := client.MCPAuthURL(ctx, id, name)\nif err != nil {\n    return err\n}\nif authURL == \"\" {\n    return errors.New(\"server returned an empty auth URL\")\n}\nu, err := url.Parse(authURL)\nif err != nil || u.Scheme == \"\" {\n    return fmt.Errorf(\"invalid auth URL %q\", authURL)","typeGuard":"func hasAuthURL(resp proto.MCPAuthResponse) bool {\n    return resp.AuthURL != \"\"\n}","tryCatchPattern":"authURL, err := client.MCPAuthURL(ctx, id, name)\nif err != nil {\n    if strings.Contains(err.Error(), \"failed to decode MCP auth URL\") {\n        // 200 but unusable body: likely proxy interference or version skew\n        log.Warn(\"auth-url response not decodable; check proxy/server version\", \"err\", err)\n        return \"\", errBackoffRetryOrAbort(ctx)\n    }\n    return \"\", err\n}","preventionTips":["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."],"tags":["json","decoding","mcp","oauth","client"],"backgroundTag":"json-decode-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}