@kryptosai/mcp-observatory 0.11.0 → 0.12.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.
@@ -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":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAE,sBAAsB,EAAE,eAAe,EAAyC,MAAM,WAAW,CAAC;AAS3G,SAAS,wBAAwB,CAAC,OAAqB;IACrD,MAAM,IAAI,GAAG,OAAO,CAAC,kBAAkB,CAAC;IACxC,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,EAAE,IAAI,EAAE,sBAAsB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,2DAA2D,EAAE,CAAC;IAC9H,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,sBAAsB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,sCAAsC,EAAE,CAAC;AACxG,CAAC;AAED,SAAS,eAAe,CAAC,OAAqB;IAC5C,MAAM,IAAI,GAAG,OAAO,CAAC,kBAAkB,CAAC;IACxC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,uDAAuD,EAAE,CAAC;IACjH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,sCAAsC,EAAE,CAAC;AAC/F,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,OAAqB;IACrD,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,kBAAkB,EAAE,OAAO,CAAC,EAAE,CAAC;QACjE,OAAO,EAAE,IAAI,EAAE,wBAAwB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,gDAAgD,EAAE,CAAC;IACpH,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;QACvF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,EAAE,IAAI,EAAE,wBAAwB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,8CAA8C,EAAE,CAAC;QACnH,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,wBAAwB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,uBAAuB,IAAI,CAAC,KAAK,CAAC,MAAM,WAAW,EAAE,CAAC;IACvH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,wBAAwB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,2CAA2C,GAAG,EAAE,EAAE,CAAC;IACrH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,oBAAoB,CAAC,OAAqB;IACvD,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,kBAAkB,EAAE,SAAS,CAAC,EAAE,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,0BAA0B,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,kDAAkD,EAAE,CAAC;IACxH,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;QACzF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACjC,OAAO,EAAE,IAAI,EAAE,0BAA0B,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,kDAAkD,EAAE,CAAC;QACzH,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,0BAA0B,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,yBAAyB,IAAI,CAAC,OAAO,CAAC,MAAM,aAAa,EAAE,CAAC;IAC/H,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,0BAA0B,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,+CAA+C,GAAG,EAAE,EAAE,CAAC;IAC3H,CAAC;AACH,CAAC;AAED,KAAK,UAAU,sBAAsB,CAAC,OAAqB;IACzD,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC,EAAE,CAAC;QACrE,OAAO,EAAE,IAAI,EAAE,4BAA4B,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,oDAAoD,EAAE,CAAC;IAC5H,CAAC;IACD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;QAC3F,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YACnC,OAAO,EAAE,IAAI,EAAE,4BAA4B,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,sDAAsD,EAAE,CAAC;QAC/H,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,4BAA4B,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,2BAA2B,IAAI,CAAC,SAAS,CAAC,MAAM,eAAe,EAAE,CAAC;IACvI,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,4BAA4B,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,mDAAmD,GAAG,EAAE,EAAE,CAAC;IACjI,CAAC;AACH,CAAC;AAED,KAAK,UAAU,wBAAwB,CAAC,OAAqB;IAC3D,IAAI,CAAC,sBAAsB,CAAC,OAAO,CAAC,kBAAkB,EAAE,OAAO,CAAC,EAAE,CAAC;QACjE,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,mCAAmC,EAAE,CAAC;IACtG,CAAC;IACD,IAAI,CAAC;QACH,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;QAC5F,kDAAkD;QAClD,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;YAC9B,MAAM,MAAM,GAAG,CAAC,CAAC,WAAkD,CAAC;YACpE,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,UAAU,CAAyB,CAAC;YAC9D,OAAO,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,sDAAsD,EAAE,CAAC;QACzH,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;QAChI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YACnC,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,QAAQ,CAAC,IAAI,qCAAqC,EAAE,CAAC;QAC/H,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAClC,MAAM,KAAK,GAAG,IAA+B,CAAC;YAC9C,IAAI,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ,EAAE,CAAC;gBACtC,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,QAAQ,CAAC,IAAI,+CAA+C,EAAE,CAAC;YACzI,CAAC;QACH,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,QAAQ,CAAC,IAAI,qCAAqC,EAAE,CAAC;IAC9H,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnE,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,uCAAuC,GAAG,EAAE,EAAE,CAAC;IAChH,CAAC;AACH,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,OAAqB;IACrD,IAAI,CAAC;QACH,4CAA4C;QAC5C,MAAM,OAAO,CAAC,MAAM,CAAC,OAAO,CAC1B,EAAE,MAAM,EAAE,yBAAyB,EAAE,MAAM,EAAE,EAAE,EAAE,EACjD,EAAE,MAAM,EAAE,QAAQ,EAAW,EAC7B,EAAE,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAC/C,CAAC;QACF,gFAAgF;QAChF,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,sDAAsD,EAAE,CAAC;IAClH,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,4CAA4C;QAC5C,MAAM,GAAG,GAAG,KAAgC,CAAC;QAC7C,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;QACzB,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,qCAAqC,MAAM,CAAC,IAAI,CAAC,sBAAsB,EAAE,CAAC;QACnI,CAAC;QACD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACxC,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,uDAAuD,EAAE,CAAC;QACnH,CAAC;QACD,uDAAuD;QACvD,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,2CAA2C,EAAE,CAAC;IACvG,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,OAAqB;IAC7D,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAEpC,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACtC,kBAAkB,CAAC,OAAO,CAAC;QAC3B,oBAAoB,CAAC,OAAO,CAAC;QAC7B,sBAAsB,CAAC,OAAO,CAAC;QAC/B,wBAAwB,CAAC,OAAO,CAAC;QACjC,kBAAkB,CAAC,OAAO,CAAC;KAC5B,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG;QACf,wBAAwB,CAAC,OAAO,CAAC;QACjC,eAAe,CAAC,OAAO,CAAC;QACxB,GAAG,aAAa;KACjB,CAAC;IAEF,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IACrD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IACtD,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC;IAE9B,IAAI,MAAmC,CAAC;IACxC,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;QACjB,MAAM,GAAG,MAAM,CAAC;IAClB,CAAC;SAAM,IAAI,MAAM,GAAG,MAAM,EAAE,CAAC;QAC3B,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,MAAM,CAAC;IAClB,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC;QAC1B,CAAC,CAAC,OAAO,KAAK,6BAA6B;QAC3C,CAAC,CAAC,GAAG,MAAM,IAAI,KAAK,+BAA+B,MAAM,UAAU,CAAC;IAEtE,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAEhG,MAAM,QAAQ,GAAoB;QAChC,QAAQ,EAAE,mBAAmB;QAC7B,UAAU,EAAE,IAAI;QAChB,SAAS,EAAE,IAAI;QACf,mBAAmB,EAAE,MAAM,KAAK,CAAC;QACjC,SAAS,EAAE,KAAK;QAChB,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7D,WAAW;KACZ,CAAC;IAEF,OAAO;QACL,MAAM,EAAE,eAAe,CACrB,aAAa,EACb,MAAM,EACN,WAAW,CAAC,GAAG,EAAE,GAAG,SAAS,EAC7B,OAAO,EACP,CAAC,QAAQ,CAAC,CACX;KACF,CAAC;AACJ,CAAC"}
@@ -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")