mvanhorn/last30days-skill · error

%s must be a boolean

Error message

%s must be a boolean

What it means

Thrown by boolArgument (mcp/internal/tools/research.go:143) when an optional boolean argument is present but its type is not a Go bool. On the 'research' tool the only boolean is 'save', so the message reads "save must be a boolean". Because the argument is optional, omission is fine (defaults to false); the error fires only when a value IS supplied and JSON-decodes to a string ("true"), number (1/0), or null-like non-bool. There is no string-coercion: the server requires a real JSON boolean.

Source

Thrown at mcp/internal/tools/research.go:143

	}
	switch value {
	case "":
		return "compact", nil
	case "compact", "html":
		return value, nil
	default:
		return "", fmt.Errorf("emit must be 'compact' or 'html', got %q", value)
	}
}

func boolArgument(args map[string]any, name string) (bool, error) {
	raw, ok := args[name]
	if !ok {
		return false, nil
	}
	value, ok := raw.(bool)
	if !ok {
		return false, fmt.Errorf("%s must be a boolean", name)
	}
	return value, nil
}

// formatRunError flattens engine.Run's distinct error shapes into a single
// user-facing message that includes the relevant stderr context.
func formatRunError(runErr error, res *engine.RunResult) string {
	var msg strings.Builder
	msg.WriteString(runErr.Error())
	if res != nil && len(res.Stderr) > 0 {
		msg.WriteString("\nengine stderr:\n")
		msg.Write(res.Stderr)
	}
	return msg.String()
}

View on GitHub (pinned to c7460f6114)

Solutions

  1. Pass a real JSON boolean: {"save": true} or {"save": false} — no quotes, no 1/0.
  2. If the flag comes from a string env var or config, convert it yourself before the call: save := flagStr == "1" || strings.EqualFold(flagStr, "true").
  3. Build the arguments map with Go bools (map[string]any{"save": true}) or a real JSON serializer so booleans stay unquoted.

Example fix

// before (string-interpolated JSON)
body := fmt.Sprintf(`{"topic":"x","save":"%t"}`, saveFlag)
// -> error: save must be a boolean

// after (typed map serialized with encoding/json)
args := map[string]any{"topic": "x", "save": saveFlag} // saveFlag is bool
payload, _ := json.Marshal(args)
Defensive patterns

Strategy: type-guard

Validate before calling

// Coerce loose flag representations to a real bool before dispatch.
func toBool(v any) (bool, error) {
	switch t := v.(type) {
	case nil:
		return false, nil
	case bool:
		return t, nil
	case string:
		switch strings.ToLower(t) {
		case "true", "1", "yes":
			return true, nil
		case "false", "0", "no", "":
			return false, nil
		}
	}
	return false, fmt.Errorf("cannot use %v as boolean for save", v)
}

Type guard

func isBool(v any) bool {
	_, ok := v.(bool)
	return ok
}

Prevention

When it happens

Trigger: Calling 'research' with {"save": "true"}, {"save": "false"}, {"save": 1}, or {"save": 0} instead of a bare JSON true/false. Typical when arguments are built with string formatting instead of a JSON encoder, when a model quotes the boolean, or when a config-driven pipeline stores flags as strings.

Common situations: Quoted booleans from LLM tool calls ({"save": "true"}); templates that interpolate booleans into JSON text; configs/env vars carrying "1"/"0" or "yes"/"no" being forwarded verbatim; strongly-typed clients mapping ints to the flag.

Related errors


AI-assisted analysis of mvanhorn/last30days-skill@c7460f6114 (2026-08-15). Data as JSON: /api/errors/4b22ff5fa6546c82. Report an issue: GitHub.