{"record":{"id":"51b1f779f5e8e79d","repo":"siyuan-note/siyuan","slug":"attr-q-must-be-a-string-or-null-got-t","errorCode":null,"errorMessage":"attr %q must be a string or null (got %T)","messagePattern":"attr %q must be a string or null \\(got %T\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/tools/attr.go","lineNumber":116,"sourceCode":"\n\tif err := model.SetBlockAttrs(id, nameValues); err != nil {\n\t\treturn CallToolResult{Content: []ContentItem{{Type: \"text\", Text: \"set attrs failed: \" + err.Error()}}, IsError: true}, nil\n\t}\n\n\tutil.PushReloadFiletree()\n\treturn CallToolResult{Content: []ContentItem{{Type: \"text\", Text: \"attributes set for: \" + id}}}, nil\n}\n\nfunc parseAttrValues(attrs map[string]any) (map[string]string, error) {\n\tnameValues := make(map[string]string, len(attrs))\n\tfor name, value := range attrs {\n\t\tswitch typed := value.(type) {\n\t\tcase nil:\n\t\t\tnameValues[name] = \"\"\n\t\tcase string:\n\t\t\tnameValues[name] = typed\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"attr %q must be a string or null (got %T)\", name, value)\n\t\t}\n\t}\n\treturn nameValues, nil\n}\n\nfunc attrBatchGet(args map[string]any) (CallToolResult, error) {\n\tidsStr, _ := args[\"ids\"].(string)\n\tif idsStr == \"\" {\n\t\treturn CallToolResult{Content: []ContentItem{{Type: \"text\", Text: \"ids (comma-separated) is required\"}}, IsError: true}, nil\n\t}\n\tidList := strings.Split(idsStr, \",\")\n\tfor i := range idList {\n\t\tidList[i] = strings.TrimSpace(idList[i])\n\t}\n\n\tattrs := sql.BatchGetBlockAttrs(idList)\n\tif len(attrs) == 0 {\n\t\treturn CallToolResult{Content: []ContentItem{{Type: \"text\", Text: \"no attributes found\"}}}, nil","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/mcp/tools/attr.go#L98-L134","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nargs := map[string]any{\"custom-priority\": 3, \"custom-done\": true}\n// after\nargs := map[string]any{\"custom-priority\": \"3\", \"custom-done\": \"true\"}","handlingStrategy":"validation","validationCode":"function validateAttrValues(attrs) {\n  return Object.entries(attrs).every(([, v]) => v === null || typeof v === \"string\");\n}","typeGuard":"const isAttrValue = (v) => v === null || typeof v === \"string\";","tryCatchPattern":null,"preventionTips":["Always stringify attribute values before sending","Use null to clear attributes","Add a schema check on tool arguments"],"tags":["mcp","validation","type-mismatch"],"backgroundTag":"invalid-argument-value","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}