Explain Rego decision
rego_explain_decisionEvaluates a Rego query with full tracing to answer why a decision was made, returning a structured trace and per-rule summary.
Instructions
Evaluate a Rego query with full tracing and return a structured trace plus per-rule fired/not-fired summary. Use this when you need to answer "why was this denied?" — the agent reads the structured trace and narrates the cause without re-implementing the trace parser.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Rego query to evaluate, e.g. "data.example.allow". | |
| source | No | Inline Rego policy source. Mutually exclusive with `paths`. | |
| paths | No | Policy / data file or directory paths. Each must be inside an allowed root. | |
| input | No | Inline input document. | |
| inputPath | No | Path to a JSON input file. Mutually exclusive with `input`. | |
| unknowns | No | Refs to treat as unknown during partial evaluation. | |
| partial | No | Run partial evaluation rather than full evaluation. | |
| strictBuiltinErrors | No | Treat builtin errors as fatal instead of returning undefined. |
Implementation Reference
- The main handler function `registerRegoExplainDecision` that registers the 'rego_explain_decision' tool on the MCP server. It runs a Rego query with `--explain=full`, parses the trace, summarizes per-rule events (entered/exited/failed), and returns a structured trace plus aggregated summary.
export function registerRegoExplainDecision(server: McpServer, config: Config): void { const opa = new OpaCli(config); server.registerTool( 'rego_explain_decision', { title: 'Explain Rego decision', description: 'Evaluate a Rego query with full tracing and return a structured trace plus per-rule fired/not-fired summary. Use this when you need to answer "why was this denied?" — the agent reads the structured trace and narrates the cause without re-implementing the trace parser.', inputSchema: SharedEvalInput, }, async (args) => { return withToolEnvelope<RegoExplainDecisionOutput>(config, async () => { const evalEnvelope = await runEval(opa, config, args, { explain: 'full' }); if (!evalEnvelope.ok) { // Re-issue the same error under this tool's output type. return err(evalEnvelope.error!.code, evalEnvelope.error!.message, { hint: evalEnvelope.error!.hint, details: evalEnvelope.error!.details, }); } const data = evalEnvelope.data as RegoEvalOutput; const trace = (data.explanation ?? []) as TraceEvent[]; const summary = summarizeTrace(trace); return ok<RegoExplainDecisionOutput>({ result: data.result?.[0] !== undefined ? (data.result as Array<{ expressions?: Array<{ value?: unknown }> }>)[0] ?.expressions?.[0]?.value : undefined, errors: data.errors, rulesFired: [...summary.rulesFired], rulesEvaluated: [...summary.rulesEvaluated], trace, summary: { totalEvents: summary.totalEvents, enterEvents: summary.enterEvents, exitEvents: summary.exitEvents, failEvents: summary.failEvents, }, }); }); }, ); } - Output type `RegoExplainDecisionOutput` defining the shape of the tool's response: result, errors, rulesFired, rulesEvaluated, trace (raw events), and summary (total/enter/exit/fail event counts). The input schema is the shared `SharedEvalInput` from `_shared.ts`.
export interface RegoExplainDecisionOutput { result: unknown; errors?: unknown[]; rulesFired: string[]; rulesEvaluated: string[]; trace: TraceEvent[]; summary: { totalEvents: number; enterEvents: number; exitEvents: number; failEvents: number; }; } - The `summarizeTrace` helper function that iterates over trace events, counting enter/exit/fail operations and building sets of rule names that were evaluated vs. fired using regex matching on event messages.
function summarizeTrace(trace: TraceEvent[] | undefined): RegoExplainDecisionOutput['summary'] & { rulesEvaluated: Set<string>; rulesFired: Set<string>; } { const rulesEvaluated = new Set<string>(); const rulesFired = new Set<string>(); let enterEvents = 0; let exitEvents = 0; let failEvents = 0; for (const event of trace ?? []) { if (event.op === 'enter') { enterEvents += 1; const ruleMatch = event.message ? /^(?:eval|enter)\s+(.+)$/i.exec(event.message) : null; if (ruleMatch?.[1]) rulesEvaluated.add(ruleMatch[1]); } else if (event.op === 'exit') { exitEvents += 1; const ruleMatch = event.message ? /^(?:exit|matched)\s+(.+)$/i.exec(event.message) : null; if (ruleMatch?.[1]) rulesFired.add(ruleMatch[1]); } else if (event.op === 'fail') { failEvents += 1; } } return { totalEvents: trace?.length ?? 0, enterEvents, exitEvents, failEvents, rulesEvaluated, rulesFired, }; } - src/tools/helpers/index.ts:13-21 (registration)Registration of `registerRegoExplainDecision` within the helper tools module. Called from `registerHelperTools` which is invoked by the main `registerTools` entry point in `src/tools/index.ts` (line 42).
import { registerRegoExplainDecision } from './explain-decision.js'; import { registerRegoGenerateTestSkeleton } from './generate-test-skeleton.js'; import { registerRegoSuggestFix } from './suggest-fix.js'; export function registerHelperTools(server: McpServer, config: Config): void { registerRegoExplainDecision(server, config); registerRegoGenerateTestSkeleton(server, config); registerRegoDescribePolicy(server, config); registerRegoSuggestFix(server, config); - The `SharedEvalInput` Zod schema used as the input schema for rego_explain_decision. Defines shared input fields: query, source, paths, input, inputPath, unknowns, partial, strictBuiltinErrors.
export const SharedEvalInput = { query: z.string().min(1).describe('Rego query to evaluate, e.g. "data.example.allow".'), source: z .string() .optional() .describe('Inline Rego policy source. Mutually exclusive with `paths`.'), paths: z .array(z.string()) .optional() .describe('Policy / data file or directory paths. Each must be inside an allowed root.'), input: z.unknown().optional().describe('Inline input document.'), inputPath: z .string() .optional() .describe('Path to a JSON input file. Mutually exclusive with `input`.'), unknowns: z .array(z.string()) .optional() .describe('Refs to treat as unknown during partial evaluation.'), partial: z.boolean().optional().describe('Run partial evaluation rather than full evaluation.'), strictBuiltinErrors: z .boolean() .optional() .describe('Treat builtin errors as fatal instead of returning undefined.'), };