suggest_remediations
Extract prioritized remediation suggestions from Tactual analysis results, ranked by severity, to identify the most critical fixes first.
Instructions
Extract the top unique remediation suggestions from a Tactual analysis result, ranked by severity. Returns a JSON array of {targetId, severity, score, fix, penalties}.
Read-only, no side effects. Most useful with large JSON results where you want a prioritized shortlist of what to fix first. For SARIF results, the findings already contain fix suggestions inline — this tool is redundant in that case. Input must be a JSON string from analyze_url (format='json').
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| analysis | Yes | Analysis result as JSON string | |
| maxSuggestions | No | Maximum number of suggestions to return |
Implementation Reference
- Core handler: runs the suggest_remediations logic — extracts findings via extractFindings, deduplicates by fix string, sorts by score (ascending, so worst first), and returns up to maxSuggestions results.
export function runSuggestRemediations( opts: SuggestRemediationsOptions, ): Suggestion[] { let findings; try { findings = extractFindings(opts.analysis); } catch (err) { throw new SuggestRemediationsError( "bad-input", `Error parsing analysis: ${err instanceof Error ? err.message : String(err)}`, ); } const max = opts.maxSuggestions ?? 10; const sorted = [...findings].sort((a, b) => a.overall - b.overall); const suggestions: Suggestion[] = []; const seenFixes = new Set<string>(); for (const finding of sorted) { for (const fix of finding.suggestedFixes) { if (seenFixes.has(fix)) continue; seenFixes.add(fix); suggestions.push({ targetId: finding.targetId, severity: finding.severity, score: finding.overall, fix, penalties: finding.penalties, }); if (suggestions.length >= max) return suggestions; } } return suggestions; } - Suggestion interface (targetId, severity, score, fix, penalties) and SuggestRemediationsOptions interface (analysis, maxSuggestions).
export interface Suggestion { targetId: string; severity: string; score: number; fix: string; penalties: string[]; } export interface SuggestRemediationsOptions { analysis: unknown; maxSuggestions?: number; } - src/mcp/tools/suggest-remediations.ts:8-43 (registration)MCP registration: registers the 'suggest_remediations' tool on the McpServer with Zod inputSchema describing 'analysis' (string) and 'maxSuggestions' (number, default 10). The handler calls runSuggestRemediations and returns formatted JSON text.
export function registerSuggestRemediations(server: McpServer): void { server.registerTool( "suggest_remediations", { description: "Extract the top unique remediation suggestions from a Tactual analysis result, " + "ranked by severity. Returns a JSON array of {targetId, severity, score, fix, penalties}.\n\n" + "Read-only, no side effects. Most useful with large JSON results where you want a prioritized " + "shortlist of what to fix first. For SARIF results, the findings already contain fix " + "suggestions inline — this tool is redundant in that case. " + "Input must be a JSON string from analyze_url (format='json').", inputSchema: { analysis: z.string().describe("Analysis result as JSON string"), maxSuggestions: z.number().default(10).describe("Maximum number of suggestions to return"), }, }, async ({ analysis, maxSuggestions }) => { try { const suggestions = runSuggestRemediations({ analysis: JSON.parse(analysis), maxSuggestions, }); return { content: [ { type: "text" as const, text: JSON.stringify(suggestions, null, 2) }, ], }; } catch (err) { const text = err instanceof SuggestRemediationsError ? err.message : `Error: ${err instanceof Error ? err.message : String(err)}`; return { content: [{ type: "text" as const, text }], isError: true }; } }, ); - src/cli/commands/suggest-remediations.ts:7-51 (registration)CLI registration: registers a 'suggest-remediations' command on Commander, reads a file path for analysis JSON, parses it, and calls runSuggestRemediations.
export function registerSuggestRemediations(program: Command): void { program .command("suggest-remediations") .description("Extract top remediation suggestions from an analysis result") .argument("<file>", "Path to analysis JSON file") .option( "-n, --max-suggestions <n>", "Maximum suggestions to show", "10", ) // Legacy alias; parity plan renames this `maxSuggestions` everywhere. .option("--max <n>", "(deprecated - use --max-suggestions)") .action( async (file: string, opts: { maxSuggestions?: string; max?: string }) => { const fs = await import("fs/promises"); try { const data = JSON.parse(await fs.readFile(file, "utf-8")); const max = parseInt(opts.maxSuggestions ?? opts.max ?? "10", 10); const suggestions = runSuggestRemediations({ analysis: data, maxSuggestions: max, }); console.log(""); if (suggestions.length === 0) { console.log(" No fix suggestions found."); console.log(""); return; } for (const s of suggestions) { console.log(` ${s.score}/100 ${s.targetId}`); console.log(` \x1b[2m-> ${s.fix}\x1b[0m`); console.log(""); } } catch (err) { if (err instanceof SuggestRemediationsError) { console.error(err.message); } else { console.error(`Error: ${err}`); } process.exit(1); } }, ); } - src/core/result-extraction.ts:22-84 (helper)extractFindings helper: normalizes analysis results from raw AnalysisResult, SummarizedResult (worstFindings), or SARIF format into NormalizedFinding[] used by the pipeline handler.
export function extractFindings(data: unknown): NormalizedFinding[] { const obj = data as Record<string, unknown>; if (Array.isArray(obj.findings)) { return (obj.findings as Array<Record<string, unknown>>).map((f) => ({ targetId: String(f.targetId ?? ""), overall: getOverallScore(f), severity: String(f.severity ?? "unknown"), penalties: (f.penalties as string[] | undefined) ?? [], suggestedFixes: (f.suggestedFixes as string[] | undefined) ?? [], })); } if (Array.isArray(obj.worstFindings)) { return (obj.worstFindings as Array<Record<string, unknown>>).map((f) => ({ targetId: String(f.targetId ?? ""), overall: getOverallScore(f), severity: String(f.severity ?? "unknown"), penalties: (f.penalties as string[] | undefined) ?? [], suggestedFixes: (f.suggestedFixes as string[] | undefined) ?? [], })); } if (Array.isArray(obj.runs)) { const runs = obj.runs as Array<Record<string, unknown>>; const results = (runs[0]?.results ?? []) as Array<Record<string, unknown>>; return results .filter((r) => { const props = r.properties as Record<string, unknown> | undefined; return !props?.truncated; }) .map((r) => { const props = (r.properties ?? {}) as Record<string, unknown>; const scores = (props.scores ?? {}) as Record<string, unknown>; const locs = (r.locations ?? []) as Array<Record<string, unknown>>; const logLocs = (locs[0]?.logicalLocations ?? []) as Array<Record<string, unknown>>; const targetId = String(logLocs[0]?.name ?? ""); const level = String(r.level ?? "note"); const severity = level === "error" ? "high" : level === "warning" ? "moderate" : "acceptable"; const msgText = String((r.message as Record<string, unknown>)?.text ?? ""); const penalties: string[] = []; const suggestedFixes: string[] = []; const issuesMatch = msgText.match(/Issues:\s*(.+?)(?:\.\s*Fixes:|$)/); if (issuesMatch) penalties.push(...issuesMatch[1].split(/;\s*/).filter(Boolean)); const fixesMatch = msgText.match(/Fixes:\s*(.+)/); if (fixesMatch) suggestedFixes.push(...fixesMatch[1].split(/;\s*/).filter(Boolean)); return { targetId, overall: Number(scores.overall ?? 0), severity, penalties, suggestedFixes, }; }); } throw new Error( 'Input must contain "findings" (raw), "worstFindings" (summarized), or "runs" (SARIF) array. ' + "Pass the full analysis result object, not a sub-field.", ); }