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

  1. Extract the array first: jq '.tools' input.json > tools.json, then pass --toolsJson tools.json
  2. Ensure each entry has string name and description — non-conforming entries are filtered, but the top level must be an array
  3. 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

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


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)