{"record":{"id":"c54f1fe55d6300b9","repo":"siyuan-note/siyuan","slug":"json-node-count-exceeds-d","errorCode":null,"errorMessage":"JSON node count exceeds %d","messagePattern":"JSON node count exceeds (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/mcp/tools/validation.go","lineNumber":198,"sourceCode":"\tselect {\n\tcase err := <-result:\n\t\treturn err\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-timer.C:\n\t\treturn fmt.Errorf(\"validation exceeded %s\", toolValidationTime)\n\t}\n}\n\nfunc validateJSONComplexity(value any, maxDepth, maxNodes int) error {\n\tnodes := 0\n\tvar walk func(any, int) error\n\twalk = func(current any, depth int) error {\n\t\tif depth > maxDepth {\n\t\t\treturn fmt.Errorf(\"JSON depth exceeds %d\", maxDepth)\n\t\t}\n\t\tnodes++\n\t\tif nodes > maxNodes {\n\t\t\treturn fmt.Errorf(\"JSON node count exceeds %d\", maxNodes)\n\t\t}\n\t\tswitch typed := current.(type) {\n\t\tcase map[string]any:\n\t\t\tfor _, child := range typed {\n\t\t\t\tif err := walk(child, depth+1); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\tcase []any:\n\t\t\tfor _, child := range typed {\n\t\t\t\tif err := walk(child, depth+1); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/mcp/tools/validation.go#L180-L216","documentation":"validateJSONComplexity counts every node visited while walking the JSON value and fails once the total exceeds maxNodes. This protects the MCP tool layer from very large argument payloads that would be expensive to validate, index, or persist. The counter is global across the whole tree, not per branch.","triggerScenarios":"An MCP tool call whose JSON argument contains more total nodes (keys, array elements, scalars) than maxNodes when passed to validateJSONComplexity.","commonSituations":"Bulk-import payloads with huge arrays, generated JSON from dumping an entire dataset into a tool argument, or loops that append entries without size checks.","solutions":["Reduce the payload size — split it into multiple smaller tool calls or paginate","Compress data structure: move bulk data out of JSON arguments (e.g. reference a file/block ID instead)","Raise maxNodes at the call site if legitimate large payloads must be accepted","Count JSON nodes client-side before sending and truncate"],"exampleFix":"// before\nawait callTool(\"import\", {items: hugeArray}) // node count exceeded\n// after\nfor (const chunk of _.chunk(hugeArray, 100)) await callTool(\"import\", {items: chunk})","handlingStrategy":"validation","validationCode":"function countNodes(v) { if (Array.isArray(v)) return 1 + v.reduce((s, x) => s + countNodes(x), 0); if (v && typeof v === 'object') return 1 + Object.values(v).reduce((s, x) => s + countNodes(x), 0); return 1; }\nif (countNodes(payload) > 100000) throw new Error('payload too large; split into batches');","typeGuard":"null","tryCatchPattern":"null","preventionTips":["Batch bulk data into multiple smaller calls","Serialize and measure payload size before sending","Never dump whole datasets into a single tool argument"],"tags":["json","validation","mcp","limit"],"backgroundTag":"payload-too-large","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"}