charmbracelet/crush · error

failed to decode MCP states: %w

Error message

failed to decode MCP states: %w

What it means

MCPGetStates received HTTP 200 but the body failed to decode into map[string]proto.MCPClientInfo. The JSON did not match the expected MCP client-state schema — shape drift, truncation, or non-JSON content.

Source

Thrown at internal/client/proto.go:326

	if err := json.NewDecoder(rsp.Body).Decode(&lsps); err != nil {
		return nil, fmt.Errorf("failed to decode LSPs: %w", err)
	}
	return lsps, nil
}

// MCPGetStates retrieves the MCP client states for a workspace.
func (c *Client) MCPGetStates(ctx context.Context, id string) (map[string]proto.MCPClientInfo, error) {
	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/mcp/states", id), nil, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get MCP states: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to get MCP states: status code %d", rsp.StatusCode)
	}
	var states map[string]proto.MCPClientInfo
	if err := json.NewDecoder(rsp.Body).Decode(&states); err != nil {
		return nil, fmt.Errorf("failed to decode MCP states: %w", err)
	}
	return states, nil
}

// MCPPendingAuth retrieves the MCP servers awaiting OAuth authentication
// for a workspace.
func (c *Client) MCPPendingAuth(ctx context.Context, id string) ([]proto.MCPPendingAuthServer, error) {
	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/mcp/pending-auth", id), nil, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to get MCP pending auth: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to get MCP pending auth: status code %d", rsp.StatusCode)
	}
	var pending []proto.MCPPendingAuthServer
	if err := json.NewDecoder(rsp.Body).Decode(&pending); err != nil {
		return nil, fmt.Errorf("failed to decode MCP pending auth: %w", err)

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Sync client and server versions so proto.MCPClientInfo matches the server output.
  2. Log the raw response body with the decode error to pinpoint the mismatched field.
  3. Remove/bypass intermediaries that rewrite the body.
  4. Retry after server restart if truncation is the cause.

Example fix

// before
var states map[string]proto.MCPClientInfo
if err := json.NewDecoder(rsp.Body).Decode(&states); err != nil { return nil, err }
// after
body, _ := io.ReadAll(rsp.Body)
var states map[string]proto.MCPClientInfo
if err := json.Unmarshal(body, &states); err != nil {
    return nil, fmt.Errorf("failed to decode MCP states: %w (body: %.200s)", err, body)
}
Defensive patterns

Strategy: validation

Validate before calling

body, _ := io.ReadAll(rsp.Body)
ct := rsp.Header.Get("Content-Type")
if !strings.Contains(ct, "application/json") || len(bytes.TrimSpace(body)) == 0 {
	return errors.New("MCP states response missing or not JSON")
}

Type guard

func isMCPStatesShape(v any) bool {
	m, ok := v.(map[string]proto.MCPClientInfo)
	return ok && m != nil
}

Try / catch

var states map[string]proto.MCPClientInfo
if err := json.Unmarshal(body, &states); err != nil {
	return nil, fmt.Errorf("MCP states decode failed; check client/server versions: %w", err)
}

Prevention

When it happens

Trigger: Calling MCPGetStates when the server emits a changed/incompatible MCPClientInfo JSON shape, a proxy injects content, the body is truncated, or an error page slipped through with a 200 status.

Common situations: Partial upgrade: new server with old client proto structs (or vice versa); auth proxies adding fields that break strict decoding is rare but HTML injection is common; huge state maps getting truncated by an intermediary.

Understand the failure class

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/b858bd267f2a6632. Report an issue: GitHub.