siyuan-note/siyuan · error

attr must be a string or null (got %T)

Error message

attr %q must be a string or null (got %T)

What it means

MCP attr.set tool argument validation in parseAttrValues (kernel/mcp/tools/attr.go). Each attribute value supplied by the MCP client must be a JSON string or JSON null; anything else (number, boolean, object, array) is rejected because SiYuan block attributes are string-keyed string values. The error names the offending attribute and the actual Go type received.

Solutions

  1. Stringify non-string values before calling the tool (e.g. use "true", "123" as strings)
  2. Use JSON null when you intend to remove/clear an attribute
  3. Inspect the %T in the message to find which attribute and its offending type
  4. Update the MCP client wrapper to coerce all attribute values with toString/JSON.stringify before sending

Example fix

// before
args := map[string]any{"custom-priority": 3, "custom-done": true}
// after
args := map[string]any{"custom-priority": "3", "custom-done": "true"}
Defensive patterns

Strategy: validation

Validate before calling

function validateAttrValues(attrs) {
  return Object.entries(attrs).every(([, v]) => v === null || typeof v === "string");
}

Type guard

const isAttrValue = (v) => v === null || typeof v === "string";

Prevention

When it happens

Trigger: Calling the attr set MCP tool with an arguments map where an attribute value is a non-string JSON value, e.g. {"attr-name": 123}, {"custom-checked": true}, or a nested object/array.

Common situations: LLM clients or MCP client code passing native JSON numbers/booleans as attribute values instead of stringified values; tooling that builds attributes from untyped maps without coercing to string.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/51b1f779f5e8e79d. Report an issue: GitHub.

Appendix: source

Thrown at kernel/mcp/tools/attr.go:116

	if err := model.SetBlockAttrs(id, nameValues); err != nil {
		return CallToolResult{Content: []ContentItem{{Type: "text", Text: "set attrs failed: " + err.Error()}}, IsError: true}, nil
	}

	util.PushReloadFiletree()
	return CallToolResult{Content: []ContentItem{{Type: "text", Text: "attributes set for: " + id}}}, nil
}

func parseAttrValues(attrs map[string]any) (map[string]string, error) {
	nameValues := make(map[string]string, len(attrs))
	for name, value := range attrs {
		switch typed := value.(type) {
		case nil:
			nameValues[name] = ""
		case string:
			nameValues[name] = typed
		default:
			return nil, fmt.Errorf("attr %q must be a string or null (got %T)", name, value)
		}
	}
	return nameValues, nil
}

func attrBatchGet(args map[string]any) (CallToolResult, error) {
	idsStr, _ := args["ids"].(string)
	if idsStr == "" {
		return CallToolResult{Content: []ContentItem{{Type: "text", Text: "ids (comma-separated) is required"}}, IsError: true}, nil
	}
	idList := strings.Split(idsStr, ",")
	for i := range idList {
		idList[i] = strings.TrimSpace(idList[i])
	}

	attrs := sql.BatchGetBlockAttrs(idList)
	if len(attrs) == 0 {
		return CallToolResult{Content: []ContentItem{{Type: "text", Text: "no attributes found"}}}, nil

View on GitHub (pinned to 9f775e8a12)