{"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":1161,"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":1143,"sourceCodeEnd":1179,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/commands/security.ts#L1143-L1179","documentation":"The security command's --toolsJson flag expects a file whose top-level JSON value is an array of {name, description} objects (used to scan MCP tool descriptions for prompt-injection fragments). When the file parses successfully but is not an array — e.g. an object with a tools key — this error is thrown, naming the file path.","triggerScenarios":"Passing an MCP server config like {\"mcpServers\": {...}} or a manifest envelope {\"tools\": [...]} instead of the bare array; passing a file containing a single tool object.","commonSituations":"Feeding .claude/mcp-config JSON or a client tool-listing response whose top level is an object; manifest shape differing between CLI versions.","solutions":["Extract the array first: jq '.tools' input.json > tools.json, then pass --toolsJson tools.json","Ensure each entry has string name and description — non-conforming entries are filtered, but the top level must be an array","Or omit --toolsJson entirely so the command scans the CLI's own registered MCP tools"],"exampleFix":"# before\nruflo security <subcommand> --toolsJson mcp-config.json   # {\"mcpServers\":...}\n\n# after\njq '[.mcpServers[] | {name: .name, description: .description}]' mcp-config.json > tools.json\nruflo security <subcommand> --toolsJson tools.json","handlingStrategy":"validation","validationCode":"const parsed = JSON.parse(fs.readFileSync(toolsJsonPath, 'utf-8'));\nif (!Array.isArray(parsed)) {\n  throw new Error(`expected a top-level array in ${toolsJsonPath} — got ${typeof parsed}`);\n}\n// pass the file only after shape check, or normalize: fs.writeFileSync(p, JSON.stringify(parsed.tools ?? parsed))","typeGuard":"function isToolArray(v: unknown): v is Array<{ name: string; description: string }> {\n  return 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');\n}","tryCatchPattern":"try {\n  await runSecurityScan({ toolsJson: path });\n} catch (err) {\n  if (err instanceof Error && err.message.includes('must contain a JSON array')) {\n    // normalize the file with jq '.tools' and retry, or omit the flag to scan built-in tools\n  } else throw err;\n}","preventionTips":["Generate the tools file yourself from a known shape instead of reusing config files","Check Array.isArray plus element shape right after reading any JSON consumed by the CLI","In CI, omit --toolsJson so the command scans the CLI's registered MCP tools"],"tags":["cli","security","json","mcp","validation"],"backgroundTag":"unexpected-json-shape","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}