ruvnet/ruflo · error · Error
must contain a JSON array of
Error message
${toolsJson} must contain a JSON array of {name, description} What it means
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.
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
Example fix
# before
ruflo security <subcommand> --toolsJson mcp-config.json # {"mcpServers":...}
# after
jq '[.mcpServers[] | {name: .name, description: .description}]' mcp-config.json > tools.json
ruflo security <subcommand> --toolsJson tools.json Defensive patterns
Strategy: validation
Validate before calling
const parsed = JSON.parse(fs.readFileSync(toolsJsonPath, 'utf-8'));
if (!Array.isArray(parsed)) {
throw new Error(`expected a top-level array in ${toolsJsonPath} — got ${typeof parsed}`);
}
// pass the file only after shape check, or normalize: fs.writeFileSync(p, JSON.stringify(parsed.tools ?? parsed)) Type guard
function isToolArray(v: unknown): v is Array<{ name: string; description: string }> {
return Array.isArray(v) && v.every((t) =>
typeof t === 'object' && t !== null &&
typeof (t as any).name === 'string' &&
typeof (t as any).description === 'string');
} Try / catch
try {
await runSecurityScan({ toolsJson: path });
} catch (err) {
if (err instanceof Error && err.message.includes('must contain a JSON array')) {
// normalize the file with jq '.tools' and retry, or omit the flag to scan built-in tools
} else throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Feeding .claude/mcp-config JSON or a client tool-listing response whose top level is an object; manifest shape differing between CLI versions.
Related errors
- Dangerous key segment rejected
- FORBIDDEN_PROTOCOL
- Key contains disallowed characters
- memory path contains disallowed characters
- Namespace contains disallowed characters
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/a8f49d65f962cb90.
Report an issue: GitHub.
Appendix: source
Thrown at v3/@claude-flow/cli/src/commands/security.ts:1161
{ 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)' },
],
examples: [
{ command: 'claude-flow security composition-scan', description: 'Scan the CLI\'s own registered MCP tool descriptions' },
{ command: 'claude-flow security composition-scan --tools-json ./external-mcp-registry.json --top 50', description: 'Scan a third-party MCP registry' },
],
action: async (ctx: CommandContext): Promise<CommandResult> => {
const minFragment = (ctx.flags.minFragment as number) || 20;
const top = (ctx.flags.top as number) || 20;
const toolsJson = ctx.flags.toolsJson as string | undefined;
let tools: Array<{ name: string; description: string }> = [];
try {
if (toolsJson) {
const fs = await import('node:fs');
const path = await import('node:path');
const raw = fs.readFileSync(path.resolve(toolsJson), 'utf-8');
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) throw new Error(`${toolsJson} must contain a JSON array of {name, description}`);
tools = parsed
.filter((t: unknown): t is { name: string; description: string } =>
typeof t === 'object' && t !== null &&
typeof (t as { name?: unknown }).name === 'string' &&
typeof (t as { description?: unknown }).description === 'string')
.map((t) => ({ name: t.name, description: t.description }));
} else {
// Scan the CLI's own registered MCP tools via the client registry.
const { listMCPTools } = await import('../mcp-client.js');
tools = listMCPTools().map((t) => ({ name: t.name, description: t.description }));
}
} catch (err) {
output.printError(`Failed to load tools: ${err instanceof Error ? err.message : String(err)}`);
return { success: false, exitCode: 1 };
}
const { scanToolDescriptions } = await import('../security/mcp-composition-inspector.js');
const result = scanToolDescriptions(tools, { minFragment });View on GitHub (pinned to fa13ee4ad6)