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
- Stringify non-string values before calling the tool (e.g. use "true", "123" as strings)
- Use JSON null when you intend to remove/clear an attribute
- Inspect the %T in the message to find which attribute and its offending type
- 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
- Always stringify attribute values before sending
- Use null to clear attributes
- Add a schema check on tool arguments
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
- prev must be a string
- block [ ] type is locked: expected , got
- createDocTree definition must be a list
- createDocTree document must be a dictionary
- date display format is only available for date fields
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"}}}, nilView on GitHub (pinned to 9f775e8a12)