{"record":{"id":"a8f49d65f962cb90","repo":"ruvnet/ruflo","slug":"toolsjson-must-contain-a-json-array-of-name-d","errorCode":null,"errorMessage":"${toolsJson} must contain a JSON array of {name, description}","messagePattern":"(.+?) must contain a JSON array of (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/commands/security.ts","lineNumber":1154,"sourceCode":"    { name: 'tools-json', type: 'string', description: 'Path to a JSON file of {name, description}[] to scan (default: scan the CLI\\'s own registered MCP tools)' },\n  ],\n  examples: [\n    { command: 'claude-flow security composition-scan', description: 'Scan the CLI\\'s own registered MCP tool descriptions' },\n    { command: 'claude-flow security composition-scan --tools-json ./external-mcp-registry.json --top 50', description: 'Scan a third-party MCP registry' },\n  ],\n  action: async (ctx: CommandContext): Promise<CommandResult> => {\n    const minFragment = (ctx.flags.minFragment as number) || 20;\n    const top = (ctx.flags.top as number) || 20;\n    const toolsJson = ctx.flags.toolsJson as string | undefined;\n\n    let tools: Array<{ name: string; description: string }> = [];\n    try {\n      if (toolsJson) {\n        const fs = await import('node:fs');\n        const path = await import('node:path');\n        const raw = fs.readFileSync(path.resolve(toolsJson), 'utf-8');\n        const parsed = JSON.parse(raw);\n        if (!Array.isArray(parsed)) throw new Error(`${toolsJson} must contain a JSON array of {name, description}`);\n        tools = parsed\n          .filter((t: unknown): t is { name: string; description: string } =>\n            typeof t === 'object' && t !== null &&\n            typeof (t as { name?: unknown }).name === 'string' &&\n            typeof (t as { description?: unknown }).description === 'string')\n          .map((t) => ({ name: t.name, description: t.description }));\n      } else {\n        // Scan the CLI's own registered MCP tools via the client registry.\n        const { listMCPTools } = await import('../mcp-client.js');\n        tools = listMCPTools().map((t) => ({ name: t.name, description: t.description }));\n      }\n    } catch (err) {\n      output.printError(`Failed to load tools: ${err instanceof Error ? err.message : String(err)}`);\n      return { success: false, exitCode: 1 };\n    }\n\n    const { scanToolDescriptions } = await import('../security/mcp-composition-inspector.js');\n    const result = scanToolDescriptions(tools, { minFragment });","sourceCodeStart":1136,"sourceCodeEnd":1172,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/cli/src/commands/security.ts#L1136-L1172","documentation":"Thrown by the security command when the --toolsJson file parses successfully but its top-level value is not a JSON array. The fragment-overlap scanner expects an array of {name, description} tool descriptors; an object or scalar is rejected so the subsequent `.filter().map()` does not silently produce an empty tool set.","triggerScenarios":"Pointing --toolsJson at a file that contains a single tool object (`{...}`) rather than an array (`[{...}]`), or a file wrapping tools under a key (`{\"tools\":[...]}`) without unwrapping.","commonSituations":"Exporting one MCP server's tool list as an object, a manifest format that nests tools under a property, or hand-editing the file and forgetting the outer `[ ]`.","solutions":["Wrap the tool list in an array at the top level of the JSON file.","If the source is `{\"tools\":[...]}`, unwrap it: `jq '.tools' manifest.json > tools.json` before passing.","Validate with `jq type tools.json` — it should print `array`."],"exampleFix":"// before — tools.json contains {\"name\":\"x\",\"description\":\"y\"}\n// after — tools.json contains [{\"name\":\"x\",\"description\":\"y\"}]","handlingStrategy":"type-guard","validationCode":"function loadToolsArray(filePath: string): Array<{name:string;description:string}> {\n  const parsed = JSON.parse(require('fs').readFileSync(filePath, 'utf-8'));\n  const arr = Array.isArray(parsed) ? parsed : (parsed?.tools ?? null);\n  if (!Array.isArray(arr)) {\n    throw new Error(`${filePath} must contain a JSON array of {name, description}`);\n  }\n  return arr;\n}","typeGuard":"const isToolArray = (v: unknown): v is Array<{name:string;description:string}> =>\n  Array.isArray(v) && v.every(t =>\n    typeof t === 'object' && t !== null &&\n    typeof (t as any).name === 'string' &&\n    typeof (t as any).description === 'string');","tryCatchPattern":"try {\n  await runSecurityScan({ toolsJson });\n} catch (e) {\n  const msg = e instanceof Error ? e.message : String(e);\n  if (msg.endsWith('must contain a JSON array of {name, description}')) {\n    console.error('Wrap the tool list in [ ] or unwrap a {\"tools\":[...]} envelope.');\n    process.exit(2);\n  }\n  throw e;\n}","preventionTips":["Validate the file with `jq type` (should print array) before passing.","Unwrap `{\"tools\":[...]}` envelopes before writing the file.","Generate the manifest from a trusted exporter."],"tags":["cli","validation","json","security","tools"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}