@kryptosai/mcp-observatory 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,13 +12,15 @@
12
12
 
13
13
  [![CI](https://github.com/KryptosAI/mcp-observatory/actions/workflows/ci.yml/badge.svg)](https://github.com/KryptosAI/mcp-observatory/actions/workflows/ci.yml)
14
14
  [![npm](https://img.shields.io/npm/v/@kryptosai/mcp-observatory)](https://www.npmjs.com/package/@kryptosai/mcp-observatory)
15
+ [![npm downloads](https://img.shields.io/npm/dm/@kryptosai/mcp-observatory)](https://www.npmjs.com/package/@kryptosai/mcp-observatory)
15
16
  [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
16
17
  [![Node >= 20](https://img.shields.io/badge/node-%3E%3D20-339933)](./package.json)
18
+ [![Smithery](https://smithery.ai/badge/@kryptosai/mcp-observatory)](https://smithery.ai/server/@kryptosai/mcp-observatory)
17
19
  [![mcp-observatory MCP server](https://glama.ai/mcp/servers/KryptosAI/mcp-observatory/badges/score.svg)](https://glama.ai/mcp/servers/KryptosAI/mcp-observatory)
18
20
 
19
- Find problems in your MCP servers before your users do.
21
+ **The first testing tool that is itself an MCP server.** Your AI agent can scan, test, record, replay, and verify other MCP servers autonomously catching regressions, schema drift, and security issues without human intervention.
20
22
 
21
- You update a server, a tool silently breaks, and your agent starts failing. MCP Observatory catches that. It connects to your servers, checks every capability, actually calls tools to make sure they work, and diffs runs to catch what changed.
23
+ Use it as a CLI, a CI action, or give it to your agent as an MCP server and let it test your other servers for you.
22
24
 
23
25
  <p align="center">
24
26
  <img src="./docs/demo.svg" alt="MCP Observatory scan output" width="820">
@@ -164,19 +166,25 @@ The action runs checks on every PR, comments a markdown report, and blocks merge
164
166
 
165
167
  ## MCP Server Mode
166
168
 
167
- When running as an MCP server (`serve`), your AI agent gets the same capabilities as the CLI:
169
+ **No other testing tool is itself an MCP server.** Add Observatory as a server and your AI agent can autonomously test, diagnose, and monitor your other MCP servers.
168
170
 
169
- | Tool | What it does |
170
- |------|-------------|
171
- | `scan` | Discover and check all configured servers |
172
- | `check_server` | Check a specific server by command |
173
- | `record` | Record a server session to a cassette file |
174
- | `replay` | Replay a cassette offline — no live server needed |
175
- | `verify` | Verify a live server still matches a cassette |
176
- | `watch` | Run checks and diff against the previous run |
177
- | `diff_runs` | Compare two saved run artifacts |
178
- | `get_last_run` | Return the most recent run for a target |
179
- | `suggest_servers` | Scan your environment and recommend servers you're missing |
171
+ ```bash
172
+ claude mcp add mcp-observatory -- npx -y @kryptosai/mcp-observatory serve
173
+ ```
174
+
175
+ Your agent gets 9 tools:
176
+
177
+ | Tool | When to use it |
178
+ |------|---------------|
179
+ | `scan` | Check if all your configured MCP servers are healthy |
180
+ | `check_server` | Test a specific server before installing or after updating |
181
+ | `record` | Capture a baseline of a working server for future comparison |
182
+ | `replay` | Test against a recorded session — no live server needed |
183
+ | `verify` | Confirm a server update didn't break anything |
184
+ | `watch` | Check a server and see what changed since the last check |
185
+ | `diff_runs` | Find regressions between two check results |
186
+ | `get_last_run` | Retrieve previous check results for a server |
187
+ | `suggest_servers` | Discover MCP servers that match your project stack |
180
188
 
181
189
  An AI tool that checks other AI tools. It's a tool testing tools that serve tools.*
182
190
 
@@ -259,7 +267,7 @@ npx @kryptosai/mcp-observatory run --target ./target.json
259
267
  | Benchmarking / latency | — | — | ✅ | — |
260
268
  | Jest integration | — | — | — | ✅ |
261
269
  | MCP proxy mode | — | ✅ | — | — |
262
- | Works as MCP server | | — | — | — |
270
+ | **Works as MCP server** | **✅** | — | — | — |
263
271
 
264
272
  Each tool has strengths. Observatory focuses on regression detection and CI-friendly workflows. mcp-recorder is great as a transparent proxy. MCPBench is the go-to for performance benchmarking. mcp-jest is ideal if you're already in a Jest workflow.
265
273
 
@@ -0,0 +1,7 @@
1
+ import type { HealthGrade } from "./types.js";
2
+ export interface BadgeOptions {
3
+ label?: string;
4
+ score: number;
5
+ grade: HealthGrade;
6
+ }
7
+ export declare function generateBadgeSvg(options: BadgeOptions): string;
@@ -0,0 +1,36 @@
1
+ const GRADE_COLORS = {
2
+ A: "#4c1",
3
+ B: "#97ca00",
4
+ C: "#dfb317",
5
+ D: "#fe7d37",
6
+ F: "#e05d44",
7
+ };
8
+ export function generateBadgeSvg(options) {
9
+ const label = options.label ?? "MCP Health";
10
+ const value = `${options.score}/100`;
11
+ const color = GRADE_COLORS[options.grade];
12
+ // Approximate text widths (7px per character for the 11px Verdana used by shields.io)
13
+ const labelWidth = label.length * 7 + 10;
14
+ const valueWidth = value.length * 7 + 10;
15
+ const totalWidth = labelWidth + valueWidth;
16
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img" aria-label="${label}: ${value}">
17
+ <title>${label}: ${value}</title>
18
+ <linearGradient id="s" x2="0" y2="100%">
19
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
20
+ <stop offset="1" stop-opacity=".1"/>
21
+ </linearGradient>
22
+ <clipPath id="r"><rect width="${totalWidth}" height="20" rx="3" fill="#fff"/></clipPath>
23
+ <g clip-path="url(#r)">
24
+ <rect width="${labelWidth}" height="20" fill="#555"/>
25
+ <rect x="${labelWidth}" width="${valueWidth}" height="20" fill="${color}"/>
26
+ <rect width="${totalWidth}" height="20" fill="url(#s)"/>
27
+ </g>
28
+ <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
29
+ <text aria-hidden="true" x="${labelWidth * 5}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)">${label}</text>
30
+ <text x="${labelWidth * 5}" y="140" transform="scale(.1)">${label}</text>
31
+ <text aria-hidden="true" x="${(labelWidth + valueWidth / 2) * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)">${value}</text>
32
+ <text x="${(labelWidth + valueWidth / 2) * 10}" y="140" transform="scale(.1)">${value}</text>
33
+ </g>
34
+ </svg>`;
35
+ }
36
+ //# sourceMappingURL=badge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"badge.js","sourceRoot":"","sources":["../../src/badge.ts"],"names":[],"mappings":"AAEA,MAAM,YAAY,GAAgC;IAChD,CAAC,EAAE,MAAM;IACT,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;CACb,CAAC;AAQF,MAAM,UAAU,gBAAgB,CAAC,OAAqB;IACpD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,YAAY,CAAC;IAC5C,MAAM,KAAK,GAAG,GAAG,OAAO,CAAC,KAAK,MAAM,CAAC;IACrC,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAE1C,sFAAsF;IACtF,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;IACzC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;IACzC,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;IAE3C,OAAO,kDAAkD,UAAU,wCAAwC,KAAK,KAAK,KAAK;WACjH,KAAK,KAAK,KAAK;;;;;kCAKQ,UAAU;;mBAEzB,UAAU;eACd,UAAU,YAAY,UAAU,uBAAuB,KAAK;mBACxD,UAAU;;;kCAGK,UAAU,GAAG,CAAC,oEAAoE,KAAK;eAC1G,UAAU,GAAG,CAAC,mCAAmC,KAAK;kCACnC,CAAC,UAAU,GAAG,UAAU,GAAG,CAAC,CAAC,GAAG,EAAE,oEAAoE,KAAK;eAC9H,CAAC,UAAU,GAAG,UAAU,GAAG,CAAC,CAAC,GAAG,EAAE,mCAAmC,KAAK;;OAElF,CAAC;AACR,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { type CheckContext, type ObservedCheck } from "./base.js";
2
+ export declare function runConformanceCheck(context: CheckContext): Promise<ObservedCheck>;
@@ -0,0 +1,162 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { isCapabilityAdvertised, makeCheckResult } from "./base.js";
3
+ function checkCapabilitiesPresent(context) {
4
+ const caps = context.serverCapabilities;
5
+ if (caps === undefined) {
6
+ return { rule: "capabilities-present", passed: false, detail: "Server did not return capabilities during initialization." };
7
+ }
8
+ return { rule: "capabilities-present", passed: true, detail: "Server returned capabilities object." };
9
+ }
10
+ function checkServerInfo(context) {
11
+ const caps = context.serverCapabilities;
12
+ if (!caps) {
13
+ return { rule: "server-info", passed: false, detail: "Cannot verify server info — no capabilities returned." };
14
+ }
15
+ return { rule: "server-info", passed: true, detail: "Server provided initialization info." };
16
+ }
17
+ async function checkToolsEndpoint(context) {
18
+ if (!isCapabilityAdvertised(context.serverCapabilities, "tools")) {
19
+ return { rule: "tools-capability-match", passed: true, detail: "Tools not advertised — endpoint check skipped." };
20
+ }
21
+ try {
22
+ const resp = await context.client.listTools(undefined, { timeout: context.timeoutMs });
23
+ if (!Array.isArray(resp.tools)) {
24
+ return { rule: "tools-capability-match", passed: false, detail: "tools/list did not return an array of tools." };
25
+ }
26
+ return { rule: "tools-capability-match", passed: true, detail: `tools/list returned ${resp.tools.length} tool(s).` };
27
+ }
28
+ catch (error) {
29
+ const msg = error instanceof Error ? error.message : String(error);
30
+ return { rule: "tools-capability-match", passed: false, detail: `Advertised tools but tools/list failed: ${msg}` };
31
+ }
32
+ }
33
+ async function checkPromptsEndpoint(context) {
34
+ if (!isCapabilityAdvertised(context.serverCapabilities, "prompts")) {
35
+ return { rule: "prompts-capability-match", passed: true, detail: "Prompts not advertised — endpoint check skipped." };
36
+ }
37
+ try {
38
+ const resp = await context.client.listPrompts(undefined, { timeout: context.timeoutMs });
39
+ if (!Array.isArray(resp.prompts)) {
40
+ return { rule: "prompts-capability-match", passed: false, detail: "prompts/list did not return an array of prompts." };
41
+ }
42
+ return { rule: "prompts-capability-match", passed: true, detail: `prompts/list returned ${resp.prompts.length} prompt(s).` };
43
+ }
44
+ catch (error) {
45
+ const msg = error instanceof Error ? error.message : String(error);
46
+ return { rule: "prompts-capability-match", passed: false, detail: `Advertised prompts but prompts/list failed: ${msg}` };
47
+ }
48
+ }
49
+ async function checkResourcesEndpoint(context) {
50
+ if (!isCapabilityAdvertised(context.serverCapabilities, "resources")) {
51
+ return { rule: "resources-capability-match", passed: true, detail: "Resources not advertised — endpoint check skipped." };
52
+ }
53
+ try {
54
+ const resp = await context.client.listResources(undefined, { timeout: context.timeoutMs });
55
+ if (!Array.isArray(resp.resources)) {
56
+ return { rule: "resources-capability-match", passed: false, detail: "resources/list did not return an array of resources." };
57
+ }
58
+ return { rule: "resources-capability-match", passed: true, detail: `resources/list returned ${resp.resources.length} resource(s).` };
59
+ }
60
+ catch (error) {
61
+ const msg = error instanceof Error ? error.message : String(error);
62
+ return { rule: "resources-capability-match", passed: false, detail: `Advertised resources but resources/list failed: ${msg}` };
63
+ }
64
+ }
65
+ async function checkToolResponseContent(context) {
66
+ if (!isCapabilityAdvertised(context.serverCapabilities, "tools")) {
67
+ return { rule: "tool-response-content", passed: true, detail: "No tools — content check skipped." };
68
+ }
69
+ try {
70
+ const { tools } = await context.client.listTools(undefined, { timeout: context.timeoutMs });
71
+ // Find a tool safe to invoke (no required params)
72
+ const safeTool = tools.find(t => {
73
+ const schema = t.inputSchema;
74
+ const required = schema?.["required"];
75
+ return !required || required.length === 0;
76
+ });
77
+ if (!safeTool) {
78
+ return { rule: "tool-response-content", passed: true, detail: "No safe tool to invoke — content validation skipped." };
79
+ }
80
+ const result = await context.client.callTool({ name: safeTool.name, arguments: {} }, undefined, { timeout: context.timeoutMs });
81
+ if (!Array.isArray(result.content)) {
82
+ return { rule: "tool-response-content", passed: false, detail: `Tool "${safeTool.name}" response.content is not an array.` };
83
+ }
84
+ for (const item of result.content) {
85
+ const typed = item;
86
+ if (typeof typed["type"] !== "string") {
87
+ return { rule: "tool-response-content", passed: false, detail: `Tool "${safeTool.name}" response content item missing 'type' field.` };
88
+ }
89
+ }
90
+ return { rule: "tool-response-content", passed: true, detail: `Tool "${safeTool.name}" response has valid content array.` };
91
+ }
92
+ catch (error) {
93
+ const msg = error instanceof Error ? error.message : String(error);
94
+ return { rule: "tool-response-content", passed: false, detail: `Tool response content check failed: ${msg}` };
95
+ }
96
+ }
97
+ async function checkErrorHandling(context) {
98
+ try {
99
+ // Try calling a method that shouldn't exist
100
+ await context.client.request({ method: "observatory/nonexistent", params: {} }, { method: "object" }, { timeout: Math.min(context.timeoutMs, 5000) });
101
+ // If we get here, server didn't error — that's actually acceptable per JSON-RPC
102
+ return { rule: "error-handling", passed: true, detail: "Server responded to unknown method without crashing." };
103
+ }
104
+ catch (error) {
105
+ // Getting an error is the expected behavior
106
+ const err = error;
107
+ const code = err["code"];
108
+ if (typeof code === "number") {
109
+ return { rule: "error-handling", passed: true, detail: `Server returned proper error code ${String(code)} for unknown method.` };
110
+ }
111
+ if (code !== undefined && code !== null) {
112
+ return { rule: "error-handling", passed: true, detail: "Server returned an error response for unknown method." };
113
+ }
114
+ // Connection-level error is ok — server closed cleanly
115
+ return { rule: "error-handling", passed: true, detail: "Server handled unknown method gracefully." };
116
+ }
117
+ }
118
+ export async function runConformanceCheck(context) {
119
+ const startedAt = performance.now();
120
+ const asyncFindings = await Promise.all([
121
+ checkToolsEndpoint(context),
122
+ checkPromptsEndpoint(context),
123
+ checkResourcesEndpoint(context),
124
+ checkToolResponseContent(context),
125
+ checkErrorHandling(context),
126
+ ]);
127
+ const findings = [
128
+ checkCapabilitiesPresent(context),
129
+ checkServerInfo(context),
130
+ ...asyncFindings,
131
+ ];
132
+ const passed = findings.filter(f => f.passed).length;
133
+ const failed = findings.filter(f => !f.passed).length;
134
+ const total = findings.length;
135
+ let status;
136
+ if (failed === 0) {
137
+ status = "pass";
138
+ }
139
+ else if (passed > failed) {
140
+ status = "partial";
141
+ }
142
+ else {
143
+ status = "fail";
144
+ }
145
+ const message = failed === 0
146
+ ? `All ${total} conformance checks passed.`
147
+ : `${passed}/${total} conformance checks passed, ${failed} failed.`;
148
+ const diagnostics = findings.map(f => `[${f.passed ? "pass" : "FAIL"}] ${f.rule}: ${f.detail}`);
149
+ const evidence = {
150
+ endpoint: "conformance/check",
151
+ advertised: true,
152
+ responded: true,
153
+ minimalShapePresent: failed === 0,
154
+ itemCount: total,
155
+ identifiers: findings.filter(f => !f.passed).map(f => f.rule),
156
+ diagnostics,
157
+ };
158
+ return {
159
+ result: makeCheckResult("conformance", status, performance.now() - startedAt, message, [evidence]),
160
+ };
161
+ }
162
+ //# sourceMappingURL=conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.js","sourceRoot":"","sources":["../../../src/checks/conformance.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { type CheckContext, type ObservedCheck } from "./base.js";
2
+ export interface QualityFinding {
3
+ itemType: "tool" | "prompt" | "resource";
4
+ itemName: string;
5
+ issue: string;
6
+ severity: "warning" | "info";
7
+ }
8
+ export declare function runSchemaQualityCheck(context: CheckContext): Promise<ObservedCheck>;
@@ -0,0 +1,132 @@
1
+ import { performance } from "node:perf_hooks";
2
+ import { isCapabilityAdvertised, makeCheckResult } from "./base.js";
3
+ function checkToolQuality(tool) {
4
+ const findings = [];
5
+ const name = tool.name;
6
+ if (!tool.description || tool.description.trim().length === 0) {
7
+ findings.push({ itemType: "tool", itemName: name, issue: "Missing description", severity: "warning" });
8
+ }
9
+ if (/\s/.test(name)) {
10
+ findings.push({ itemType: "tool", itemName: name, issue: "Tool name contains whitespace", severity: "warning" });
11
+ }
12
+ if (name.length > 64) {
13
+ findings.push({ itemType: "tool", itemName: name, issue: "Tool name exceeds 64 characters", severity: "info" });
14
+ }
15
+ const schema = tool.inputSchema;
16
+ if (!schema) {
17
+ findings.push({ itemType: "tool", itemName: name, issue: "Missing input schema", severity: "warning" });
18
+ return findings;
19
+ }
20
+ const properties = schema["properties"];
21
+ if (properties && Object.keys(properties).length > 0) {
22
+ const required = schema["required"];
23
+ if (!required || !Array.isArray(required)) {
24
+ findings.push({ itemType: "tool", itemName: name, issue: "Has properties but no 'required' array declared", severity: "info" });
25
+ }
26
+ for (const [propName, propDef] of Object.entries(properties)) {
27
+ if (!propDef["description"] || (typeof propDef["description"] === "string" && propDef["description"].trim().length === 0)) {
28
+ findings.push({ itemType: "tool", itemName: name, issue: `Property '${propName}' missing description`, severity: "info" });
29
+ }
30
+ }
31
+ }
32
+ return findings;
33
+ }
34
+ function checkPromptQuality(prompt) {
35
+ const findings = [];
36
+ const name = prompt.name;
37
+ if (!prompt.description || prompt.description.trim().length === 0) {
38
+ findings.push({ itemType: "prompt", itemName: name, issue: "Missing description", severity: "warning" });
39
+ }
40
+ if (prompt.arguments) {
41
+ for (const arg of prompt.arguments) {
42
+ if (!arg.description || arg.description.trim().length === 0) {
43
+ findings.push({ itemType: "prompt", itemName: name, issue: `Argument '${arg.name}' missing description`, severity: "info" });
44
+ }
45
+ }
46
+ }
47
+ return findings;
48
+ }
49
+ function checkResourceQuality(resource) {
50
+ const findings = [];
51
+ const name = resource.name;
52
+ if (!resource.description || resource.description.trim().length === 0) {
53
+ findings.push({ itemType: "resource", itemName: name, issue: "Missing description", severity: "warning" });
54
+ }
55
+ return findings;
56
+ }
57
+ export async function runSchemaQualityCheck(context) {
58
+ const startedAt = performance.now();
59
+ const findings = [];
60
+ let totalItems = 0;
61
+ // Check tools
62
+ if (isCapabilityAdvertised(context.serverCapabilities, "tools")) {
63
+ try {
64
+ const { tools } = await context.client.listTools(undefined, { timeout: context.timeoutMs });
65
+ totalItems += tools.length;
66
+ for (const tool of tools) {
67
+ findings.push(...checkToolQuality(tool));
68
+ }
69
+ }
70
+ catch {
71
+ // tools check already reports this failure
72
+ }
73
+ }
74
+ // Check prompts
75
+ if (isCapabilityAdvertised(context.serverCapabilities, "prompts")) {
76
+ try {
77
+ const { prompts } = await context.client.listPrompts(undefined, { timeout: context.timeoutMs });
78
+ totalItems += prompts.length;
79
+ for (const prompt of prompts) {
80
+ findings.push(...checkPromptQuality(prompt));
81
+ }
82
+ }
83
+ catch {
84
+ // prompts check already reports this failure
85
+ }
86
+ }
87
+ // Check resources
88
+ if (isCapabilityAdvertised(context.serverCapabilities, "resources")) {
89
+ try {
90
+ const { resources } = await context.client.listResources(undefined, { timeout: context.timeoutMs });
91
+ totalItems += resources.length;
92
+ for (const resource of resources) {
93
+ findings.push(...checkResourceQuality(resource));
94
+ }
95
+ }
96
+ catch {
97
+ // resources check already reports this failure
98
+ }
99
+ }
100
+ const warnings = findings.filter(f => f.severity === "warning").length;
101
+ const infos = findings.filter(f => f.severity === "info").length;
102
+ let status;
103
+ if (totalItems === 0) {
104
+ status = "pass";
105
+ }
106
+ else if (warnings > totalItems * 0.5) {
107
+ status = "fail";
108
+ }
109
+ else if (findings.length > 0) {
110
+ status = "partial";
111
+ }
112
+ else {
113
+ status = "pass";
114
+ }
115
+ const message = findings.length === 0
116
+ ? `All ${totalItems} item(s) have good schema quality.`
117
+ : `Found ${findings.length} quality finding(s) across ${totalItems} item(s): ${warnings} warnings, ${infos} info.`;
118
+ const diagnostics = findings.map(f => `[${f.severity}] ${f.itemType} "${f.itemName}": ${f.issue}`);
119
+ const evidence = {
120
+ endpoint: "schema-quality/scan",
121
+ advertised: true,
122
+ responded: true,
123
+ minimalShapePresent: true,
124
+ itemCount: findings.length,
125
+ identifiers: [...new Set(findings.map(f => f.itemName))],
126
+ diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
127
+ };
128
+ return {
129
+ result: makeCheckResult("schema-quality", status, performance.now() - startedAt, message, [evidence]),
130
+ };
131
+ }
132
+ //# sourceMappingURL=schema-quality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-quality.js","sourceRoot":"","sources":["../../../src/checks/schema-quality.ts"],"names":[],"mappings":"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"}
package/dist/src/cli.js CHANGED
@@ -11,7 +11,10 @@ import { runPromptsCheck } from "./checks/prompts.js";
11
11
  import { runResourcesCheck } from "./checks/resources.js";
12
12
  import { runToolsCheck } from "./checks/tools.js";
13
13
  import { runToolsInvokeCheck } from "./checks/tools-invoke.js";
14
+ import { generateBadgeSvg } from "./badge.js";
14
15
  import { diffArtifacts, readArtifact, renderHtml, renderMarkdown, renderTerminal, runTarget, writeRunArtifact } from "./index.js";
16
+ import { renderJUnit } from "./reporters/junit.js";
17
+ import { renderSarif } from "./reporters/sarif.js";
15
18
  import { runTargetRecording } from "./runner.js";
16
19
  import { defaultRunsDirectory } from "./storage.js";
17
20
  import { ReplayTransport } from "./transport/replay-transport.js";
@@ -102,6 +105,10 @@ function formatOutput(artifact, format) {
102
105
  return renderMarkdown(artifact);
103
106
  if (format === "html")
104
107
  return renderHtml(artifact);
108
+ if (format === "junit" && artifact.artifactType === "run")
109
+ return renderJUnit(artifact);
110
+ if (format === "sarif" && artifact.artifactType === "run")
111
+ return renderSarif(artifact);
105
112
  return renderTerminal(artifact);
106
113
  }
107
114
  async function writeOutput(content, format, outputPath) {
@@ -286,6 +293,8 @@ async function main() {
286
293
  [" verify cassette.json npx server-foo", "Verify server still matches cassette"],
287
294
  [" diff run-a.json run-b.json", "Compare two runs for regressions"],
288
295
  [" suggest", "Detect your stack and recommend MCP servers"],
296
+ [" score npx server-foo", "Score a server's health (0-100)"],
297
+ [" badge npx server-foo --output badge.svg", "Generate a health badge for README"],
289
298
  ];
290
299
  const maxCmd = Math.max(...examples.map(([cmd]) => (bin + cmd).length));
291
300
  const pad = (cmd) => " ".repeat(Math.max(2, maxCmd - (bin + cmd).length + 3));
@@ -353,7 +362,7 @@ async function main() {
353
362
  .description("Compare two runs and show regressions and schema drift.")
354
363
  .argument("<base>", "Base run artifact JSON file.")
355
364
  .argument("<head>", "Head run artifact JSON file.")
356
- .option("--format <format>", "terminal, json, markdown, or html", "terminal")
365
+ .option("--format <format>", "terminal, json, markdown, html, junit, or sarif", "terminal")
357
366
  .option("--output <file>", "Write to file instead of stdout.")
358
367
  .option("--no-color", "Disable colored output.")
359
368
  .option("--fail-on-regression", "Exit with code 1 when regressions are present.", false)
@@ -745,6 +754,85 @@ async function main() {
745
754
  const output = formatOutput(artifact, options.format);
746
755
  await writeOutput(output, options.format, options.output);
747
756
  });
757
+ // ── score ────────────────────────────────────────────────────────────
758
+ program
759
+ .command("score")
760
+ .passThroughOptions()
761
+ .description("Score an MCP server's health (0-100).")
762
+ .argument("<command...>", "Server command and arguments to run.")
763
+ .option("--format <format>", "terminal, json, junit, markdown, html, or sarif", "terminal")
764
+ .option("--output <file>", "Write to file instead of stdout.")
765
+ .option("--no-color", "Disable colored output.")
766
+ .action(async (commandArgs, options) => {
767
+ const target = targetFromCommand(commandArgs);
768
+ process.stdout.write(`${c(ANSI.dim, "⟳")} Scoring ${c(ANSI.bold, target.targetId)}...\n\n`);
769
+ const artifact = await runTarget(target, { invokeTools: true, securityCheck: true });
770
+ await writeRunArtifact(artifact, defaultRunsDirectory(process.cwd()));
771
+ if (options.format !== "terminal") {
772
+ const output = formatOutput(artifact, options.format);
773
+ await writeOutput(output, options.format, options.output);
774
+ return;
775
+ }
776
+ const score = artifact.healthScore;
777
+ if (!score) {
778
+ process.stdout.write(" Could not compute health score.\n\n");
779
+ return;
780
+ }
781
+ const gradeColor = score.grade === "A" || score.grade === "B" ? ANSI.green
782
+ : score.grade === "C" ? ANSI.yellow
783
+ : ANSI.red;
784
+ process.stdout.write(c(ANSI.bold, ` MCP Health Score: ${c(gradeColor, `${score.overall}/100`)} (${c(gradeColor, score.grade)})\n\n`));
785
+ for (const dim of score.dimensions) {
786
+ const filled = Math.round(dim.score / 5);
787
+ const empty = 20 - filled;
788
+ const bar = "█".repeat(filled) + "░".repeat(empty);
789
+ const dimColor = dim.score >= 80 ? ANSI.green : dim.score >= 60 ? ANSI.yellow : ANSI.red;
790
+ const weightPct = Math.round(dim.weight * 100);
791
+ process.stdout.write(` ${dim.name.padEnd(22)} ${c(dimColor, bar)} ${String(dim.score).padStart(3)} ${c(ANSI.dim, `(weight: ${weightPct}%)`)}\n`);
792
+ }
793
+ process.stdout.write("\n");
794
+ // Show details for dimensions that aren't perfect
795
+ for (const dim of score.dimensions) {
796
+ if (dim.score < 100 && dim.details.length > 0) {
797
+ process.stdout.write(` ${c(ANSI.dim, dim.name + ":")}\n`);
798
+ for (const detail of dim.details) {
799
+ process.stdout.write(` ${c(ANSI.dim, "→")} ${detail}\n`);
800
+ }
801
+ }
802
+ }
803
+ process.stdout.write("\n");
804
+ if (artifact.gate === "fail") {
805
+ process.exitCode = 1;
806
+ }
807
+ });
808
+ // ── badge ───────────────────────────────────────────────────────────
809
+ program
810
+ .command("badge")
811
+ .passThroughOptions()
812
+ .description("Generate an SVG health score badge for your README.")
813
+ .argument("<command...>", "Server command and arguments to run.")
814
+ .option("--output <file>", "Write SVG to file (default: stdout).")
815
+ .option("--label <text>", "Badge label text.", "MCP Health")
816
+ .action(async (commandArgs, options) => {
817
+ const target = targetFromCommand(commandArgs);
818
+ process.stderr.write(`${c(ANSI.dim, "⟳")} Scoring ${c(ANSI.bold, target.targetId)}...\n`);
819
+ const artifact = await runTarget(target, { invokeTools: true, securityCheck: true });
820
+ const score = artifact.healthScore;
821
+ if (!score) {
822
+ process.stderr.write(" Could not compute health score.\n");
823
+ process.exitCode = 1;
824
+ return;
825
+ }
826
+ const svg = generateBadgeSvg({ score: score.overall, grade: score.grade, label: options.label });
827
+ if (options.output) {
828
+ await mkdir(path.dirname(options.output), { recursive: true });
829
+ await writeFile(options.output, svg, "utf8");
830
+ process.stderr.write(` Badge written to ${options.output}\n`);
831
+ }
832
+ else {
833
+ process.stdout.write(svg);
834
+ }
835
+ });
748
836
  // ── telemetry ──────────────────────────────────────────────────────────
749
837
  program
750
838
  .command("telemetry")