Skip to main content
Glama

diff_results

Compare before and after accessibility analysis results to identify improvements, regressions, and penalty changes per target. Verify that fixes addressed issues effectively.

Instructions

Compare two Tactual analysis results (before/after). Shows what improved, regressed, which penalties were resolved or added, and severity band changes per target. Returns a JSON array of {targetId, baselineScore, candidateScore, status, penalties}.

Read-only, no side effects. Use after fixing accessibility issues to verify improvements. Both inputs must be JSON strings from analyze_url (format='json'). Not useful for SARIF output — use analyze_url directly for before/after SARIF comparisons.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
baselineYesBaseline analysis result as JSON string
candidateYesCandidate analysis result as JSON string

Implementation Reference

  • Core handler that diffs two Tactual analysis results. Iterates over all target IDs, computes score deltas, identifies penalties resolved/added, and returns a structured DiffResult with summary counts and up to 20 changes.
    export function runDiffResults(baseline: unknown, candidate: unknown): DiffResult {
      let baseFindings;
      let candFindings;
      try {
        baseFindings = extractFindings(baseline);
        candFindings = extractFindings(candidate);
      } catch (err) {
        throw new DiffResultsError(
          "bad-input",
          `Error parsing results: ${err instanceof Error ? err.message : String(err)}`,
        );
      }
    
      const baseMap = new Map(baseFindings.map((f) => [f.targetId, f]));
      const candMap = new Map(candFindings.map((f) => [f.targetId, f]));
      const allIds = new Set([...baseMap.keys(), ...candMap.keys()]);
    
      const changes: DiffChange[] = [];
      let improved = 0;
      let regressed = 0;
      let added = 0;
      let removed = 0;
    
      for (const id of allIds) {
        const b = baseMap.get(id);
        const c = candMap.get(id);
        const bScore = b?.overall ?? null;
        const cScore = c?.overall ?? null;
        const delta = (cScore ?? 0) - (bScore ?? 0);
    
        const bPenalties = new Set(b?.penalties ?? []);
        const cPenalties = new Set(c?.penalties ?? []);
        const penaltiesResolved = [...bPenalties].filter((p) => !cPenalties.has(p));
        const penaltiesAdded = [...cPenalties].filter((p) => !bPenalties.has(p));
    
        let status: DiffChange["status"];
        if (!b) {
          status = "new";
          added++;
        } else if (!c) {
          status = "removed";
          removed++;
        } else if (delta > 0) {
          status = "improved";
          improved++;
        } else if (delta < 0) {
          status = "regressed";
          regressed++;
        } else {
          continue;
        }
    
        changes.push({
          targetId: id,
          baselineScore: bScore,
          candidateScore: cScore,
          delta,
          baselineSeverity: b?.severity ?? null,
          candidateSeverity: c?.severity ?? null,
          severityChanged: (b?.severity ?? null) !== (c?.severity ?? null),
          penaltiesResolved,
          penaltiesAdded,
          status,
        });
      }
    
      const statusOrder = { regressed: 0, new: 1, removed: 2, improved: 3, unchanged: 4 };
      changes.sort((a, b) => statusOrder[a.status] - statusOrder[b.status] || a.delta - b.delta);
    
      const allResolved = changes.flatMap((c) => c.penaltiesResolved);
      const allAdded = changes.flatMap((c) => c.penaltiesAdded);
    
      return {
        summary: { improved, regressed, added, removed },
        penaltiesResolved: [...new Set(allResolved)].slice(0, 5),
        penaltiesAdded: [...new Set(allAdded)].slice(0, 5),
        changes: changes.slice(0, 20),
      };
    }
  • Type definitions for DiffChange and DiffResult interfaces used by the tool.
    export interface DiffChange {
      targetId: string;
      baselineScore: number | null;
      candidateScore: number | null;
      delta: number;
      baselineSeverity: string | null;
      candidateSeverity: string | null;
      severityChanged: boolean;
      penaltiesResolved: string[];
      penaltiesAdded: string[];
      status: "improved" | "regressed" | "new" | "removed" | "unchanged";
    }
    
    export interface DiffResult {
      summary: {
        improved: number;
        regressed: number;
        added: number;
        removed: number;
      };
      penaltiesResolved: string[];
      penaltiesAdded: string[];
      changes: DiffChange[];
    }
  • MCP tool registration of 'diff_results' with Zod inputSchema (baseline, candidate as JSON strings) and async handler that calls runDiffResults.
    export function registerDiffResults(server: McpServer): void {
      server.registerTool(
        "diff_results",
        {
          description:
            "Compare two Tactual analysis results (before/after). Shows what improved, regressed, " +
            "which penalties were resolved or added, and severity band changes per target. " +
            "Returns a JSON array of {targetId, baselineScore, candidateScore, status, penalties}.\n\n" +
            "Read-only, no side effects. Use after fixing accessibility issues to verify improvements. " +
            "Both inputs must be JSON strings from analyze_url (format='json'). " +
            "Not useful for SARIF output — use analyze_url directly for before/after SARIF comparisons.",
          inputSchema: {
            baseline: z.string().describe("Baseline analysis result as JSON string"),
            candidate: z.string().describe("Candidate analysis result as JSON string"),
          },
        },
        async ({ baseline, candidate }) => {
          try {
            const result = runDiffResults(JSON.parse(baseline), JSON.parse(candidate));
            return {
              content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
            };
          } catch (err) {
            const text =
              err instanceof DiffResultsError
                ? err.message
                : `Error parsing results: ${err instanceof Error ? err.message : String(err)}`;
            return { content: [{ type: "text" as const, text }], isError: true };
          }
        },
      );
    }
  • src/mcp/index.ts:14-40 (registration)
    Import and registration call (line 37) of registerDiffResults into the MCP server.
    import { registerDiffResults } from "./tools/diff-results.js";
    import { registerSuggestRemediations } from "./tools/suggest-remediations.js";
    import { registerTracePath } from "./tools/trace-path.js";
    import { registerSaveAuth } from "./tools/save-auth.js";
    import { registerAnalyzePages } from "./tools/analyze-pages.js";
    
    export { extractFindings, getOverallScore } from "./helpers.js";
    export { closeSharedBrowser } from "./browser.js";
    
    /**
     * Construct the Tactual MCP server with all tools registered.
     * Each tool lives in its own file under tools/; shared infrastructure
     * (browser pool, probe-budget helpers) lives alongside this index.
     */
    export function createMcpServer(): McpServer {
      const server = new McpServer({
        name: "tactual",
        version: VERSION,
      });
    
      registerAnalyzeUrl(server);
      registerValidateUrl(server);
      registerListProfiles(server);
      registerDiffResults(server);
      registerSuggestRemediations(server);
      registerTracePath(server);
      registerSaveAuth(server);
  • extractFindings helper used by runDiffResults to normalize both raw AnalysisResult, SummarizedResult, and SARIF inputs into a common NormalizedFinding format.
    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.",
      );
    }
    
    /**
     * Extract the overall score from either Finding or DetailedFinding shape.
     * Finding:         { scores: { overall: N } }
     * DetailedFinding: { overall: N, scores: { discoverability, ... } }
     */
    export function getOverallScore(f: Record<string, unknown>): number {
      if (typeof f.overall === "number") return f.overall;
      const scores = f.scores as Record<string, unknown> | undefined;
      if (scores && typeof scores.overall === "number") return scores.overall;
      return 0;
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Declares read-only, no side effects. Describes return format (JSON array). Adequately discloses behavior beyond any annotations (none provided).

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Concise, front-loaded with purpose, then usage guidance, then constraints. Every sentence is necessary and informative.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with 2 parameters and no output schema, the description fully covers usage, constraints, return format, and when to use. No gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema has 100% coverage, but description adds critical context: inputs must be JSON strings from analyze_url (format='json'), which is not in schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states it compares two Tactual analysis results (before/after) and lists what it shows. Differentiates itself from analyze_url by specifying it's not for SARIF output.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly advises using after fixing accessibility issues and requires inputs from analyze_url. Provides a clear when-not-to-use case for SARIF output.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tactual-dev/tactual'

If you have feedback or need assistance with the MCP directory API, please join our Discord server