{"record":{"id":"a5084a8cf2466e7a","repo":"siyuan-note/siyuan","slug":"prev-must-be-a-string","errorCode":null,"errorMessage":"prev must be a string","messagePattern":"prev must be a string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/tools/database.go","lineNumber":338,"sourceCode":"\tkey := &av.Key{ID: keyID, Name: name, Type: av.KeyType(keyType), Icon: icon, DateFormat: av.DateDisplayFormatFull}\n\tif nil != attrView {\n\t\tif storedKey, getErr := attrView.GetKey(keyID); nil == getErr {\n\t\t\tkey = storedKey\n\t\t}\n\t}\n\treturn databaseSuccess(\"key_add\", map[string]any{\"id\": id, \"key\": key})\n}\n\nfunc databasePreviousKeyID(attrView *av.AttributeView, args map[string]any) (ret string, err error) {\n\tview, err := attrView.GetFirstView()\n\tif nil != err {\n\t\treturn \"\", err\n\t}\n\tfieldIDs := databaseViewFieldIDs(view)\n\tif value, specified := args[\"prev\"]; specified {\n\t\tprev, ok := value.(string)\n\t\tif !ok {\n\t\t\treturn \"\", errors.New(\"prev must be a string\")\n\t\t}\n\t\tif \"\" == prev {\n\t\t\treturn \"\", nil\n\t\t}\n\t\tfor _, fieldID := range fieldIDs {\n\t\t\tif fieldID == prev {\n\t\t\t\treturn prev, nil\n\t\t\t}\n\t\t}\n\t\treturn \"\", fmt.Errorf(\"previous key not found in current view: %s\", prev)\n\t}\n\tif 0 < len(fieldIDs) {\n\t\treturn fieldIDs[len(fieldIDs)-1], nil\n\t}\n\treturn \"\", nil\n}\n\nfunc databaseViewFieldIDs(view *av.View) (ret []string) {","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/mcp/tools/database.go#L320-L356","documentation":"databasePreviousKeyID resolves where a new key will be placed. If the caller supplies the optional 'prev' argument, it must be a JSON string; any other JSON type triggers 'prev must be a string'. prev names the field ID after which the new key should be inserted in the view.","triggerScenarios":"Calling databaseKeyAdd with prev set to a number, boolean, object, or array instead of a string field ID.","commonSituations":"Agents echoing back a field order index (e.g. prev: 2) instead of the field's ID; clients passing null explicitly (which is treated as specified but not a string); confusion between key names and field IDs.","solutions":["Pass prev as the string field ID of an existing key in the view.","Omit prev entirely to place the new key at the end of the current view.","Fetch the view's field IDs first (e.g. via database tools that list keys) and use one of them verbatim.","Fix the client schema so prev is typed as string-or-absent."],"exampleFix":"// before\nawait mcp.call(\"databaseKeyAdd\", { databaseID, prev: 2 });\n// after\nawait mcp.call(\"databaseKeyAdd\", { databaseID, prev: \"20240101120000-abcdefg\" });","handlingStrategy":"type-guard","validationCode":"function normalizePrev(prev) {\n  if (prev == null || prev === undefined) return undefined; // omit -> view end\n  if (typeof prev !== 'string') throw new TypeError('prev must be a string field ID');\n  return prev;\n}","typeGuard":"function isStringOrUndefined(v) { return typeof v === 'string' || v === undefined; }","tryCatchPattern":"try {\n  await mcp.call(\"databaseKeyAdd\", { databaseID, prev });\n} catch (e) {\n  if (e.message === \"prev must be a string\") {\n    // coerce index->ID or drop prev and re-send\n  }\n}","preventionTips":["Never pass positional indices as prev — always the string field ID","Omit prev instead of passing null when you want default (view end) placement","Type the argument as optional string in the client schema"],"tags":["mcp","type-mismatch","database"],"backgroundTag":"type-mismatch","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}