ci_report
Analyze run artifacts to generate a regression report that identifies breaking changes in MCP server capabilities.
Instructions
Generate a CI regression report from run artifacts.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| artifactsDir | No | Directory containing run artifacts. Defaults to .mcp-observatory/runs/ |
Implementation Reference
- src/commands/ci-report.ts:22-72 (handler)Core handler function that processes RunArtifact[] and builds a CiReport object with title, body, labels, regression detection, and counts.
export function buildCiReport(artifacts: RunArtifact[]): CiReport { const today = new Date().toISOString().slice(0, 10); const failing = artifacts.filter((a) => a.gate === "fail"); const failCount = failing.length; const hasRegressions = failCount > 0; const title = hasRegressions ? `MCP Observatory: ${failCount} regression${failCount === 1 ? "" : "s"} detected (${today})` : `MCP Observatory: all clear (${today})`; let body: string; if (!hasRegressions) { body = artifacts.length === 0 ? "No run artifacts found. Nothing to report." : `All ${artifacts.length} server${artifacts.length === 1 ? "" : "s"} passed on ${today}.`; } else { const sections: string[] = []; for (const artifact of failing) { const targetId = artifact.target.targetId; const lines: string[] = [`## ${targetId}`]; if (artifact.fatalError) { lines.push("", `> **Fatal error:** ${artifact.fatalError.split("\n")[0]}`); } const failingChecks = artifact.checks.filter( (ch) => ch.status === "fail" || ch.status === "partial", ); for (const check of failingChecks) { lines.push(`> **${check.id}:** ${check.message}`); } if (failingChecks.length === 0 && !artifact.fatalError) { lines.push("> Gate failed (no specific check failures recorded)."); } sections.push(lines.join("\n")); } body = sections.join("\n\n"); } return { title, body, labels: ["mcp-observatory"], hasRegressions, serverCount: artifacts.length, failCount, }; } - src/commands/ci-report.ts:11-18 (schema)Type definition for the CiReport output shape returned by buildCiReport.
export interface CiReport { title: string; body: string; labels: string[]; hasRegressions: boolean; serverCount: number; failCount: number; } - src/server.ts:721-752 (registration)MCP server tool registration of 'ci_report' that reads artifacts from a directory and calls buildCiReport.
server.tool( "ci_report", "Generate a CI regression report from run artifacts.", { artifactsDir: z.string().optional().describe("Directory containing run artifacts. Defaults to .mcp-observatory/runs/"), }, async ({ artifactsDir }) => { const startMs = Date.now(); try { const { readdir, readFile } = await import("node:fs/promises"); const dir = artifactsDir ?? path.join(process.cwd(), ".mcp-observatory", "runs"); const files = await readdir(dir); const artifacts: RunArtifact[] = []; for (const f of files) { if (!f.endsWith(".json")) continue; try { const raw = await readFile(path.join(dir, f), "utf8"); const parsed = JSON.parse(raw) as Record<string, unknown>; if (parsed["artifactType"] === "run") artifacts.push(parsed as unknown as RunArtifact); } catch { /* skip invalid */ } } const report = buildCiReport(artifacts); logRequest("ci_report", startMs); return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); logRequest("ci_report", startMs, true); return { content: [{ type: "text", text: `CI report failed: ${msg}` }], isError: true }; } }, ); - src/commands/ci-report.ts:76-112 (registration)CLI command registration for 'ci-report' that loads artifacts and outputs the report in JSON or markdown.
export function registerCiReportCommands(program: Command): void { program .command("ci-report") .description( "Generate a CI report from run artifacts for GitHub issue creation.", ) .option( "--artifacts-dir <path>", "Directory containing run artifacts.", defaultRunsDirectory(process.cwd()), ) .option("--format <format>", "Output format: json or markdown.", "json") .option("--no-color", "Disable colored output.") .action( async (options: { artifactsDir: string; format: string }) => { const artifacts = await loadArtifactsFromDir(options.artifactsDir); const report = buildCiReport(artifacts); if (options.format === "markdown") { process.stdout.write(report.body + "\n"); } else { process.stdout.write(JSON.stringify(report, null, 2) + "\n"); } recordEvent(buildEvent("command_complete", "ci-report", "cli", { nightlyScan: true, issueCreated: report.hasRegressions, matrixServerCount: report.serverCount, matrixFailCount: report.failCount, })); if (report.hasRegressions) { process.exitCode = 1; } }, ); } - src/commands/ci-report.ts:116-139 (helper)Helper that loads and validates run artifacts from a directory for the CLI command.
async function loadArtifactsFromDir(dir: string): Promise<RunArtifact[]> { let entries: string[]; try { entries = await readdir(dir); } catch { return []; } const jsonFiles = entries.filter((f) => f.endsWith(".json")).sort(); const artifacts: RunArtifact[] = []; for (const file of jsonFiles) { try { const content = await readFile(path.join(dir, file), "utf8"); const data: unknown = JSON.parse(content); const artifact = validateRunArtifact(data); artifacts.push(artifact); } catch { // Skip invalid files silently } } return artifacts; }