ruvnet/ruflo · error · Error

${toolsJson} must contain a JSON array of {name, description

Error message

${toolsJson} must contain a JSON array of {name, description}

What it means

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.

Source

Thrown at v3/@claude-flow/cli/src/commands/security.ts:1154

    { 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 6b01dc5a68)

Solutions

  1. Wrap the tool list in an array at the top level of the JSON file.
  2. If the source is `{"tools":[...]}`, unwrap it: `jq '.tools' manifest.json > tools.json` before passing.
  3. Validate with `jq type tools.json` — it should print `array`.

Example fix

// before — tools.json contains {"name":"x","description":"y"}
// after — tools.json contains [{"name":"x","description":"y"}]
Defensive patterns

Strategy: type-guard

Validate before calling

function loadToolsArray(filePath: string): Array<{name:string;description:string}> {
  const parsed = JSON.parse(require('fs').readFileSync(filePath, 'utf-8'));
  const arr = Array.isArray(parsed) ? parsed : (parsed?.tools ?? null);
  if (!Array.isArray(arr)) {
    throw new Error(`${filePath} must contain a JSON array of {name, description}`);
  }
  return arr;
}

Type guard

const isToolArray = (v: unknown): v is Array<{name:string;description:string}> =>
  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 });
} catch (e) {
  const msg = e instanceof Error ? e.message : String(e);
  if (msg.endsWith('must contain a JSON array of {name, description}')) {
    console.error('Wrap the tool list in [ ] or unwrap a {"tools":[...]} envelope.');
    process.exit(2);
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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 `[ ]`.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/a8f49d65f962cb90. Report an issue: GitHub.