mcp-medic 1.2.2 → 1.2.4
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 +82 -8
- package/dist/checks/index.d.ts +8 -0
- package/dist/checks/index.js +24 -0
- package/dist/checks/malformed-schema.js +17 -0
- package/dist/checks/protocol-connection-health.d.ts +17 -0
- package/dist/checks/protocol-connection-health.js +78 -0
- package/dist/checks/quality-prompts.d.ts +8 -0
- package/dist/checks/quality-prompts.js +121 -0
- package/dist/checks/quality-resources.d.ts +8 -0
- package/dist/checks/quality-resources.js +92 -0
- package/dist/checks/quality-tool-annotations.d.ts +10 -0
- package/dist/checks/quality-tool-annotations.js +63 -0
- package/dist/checks/quality-tool-descriptions.d.ts +2 -0
- package/dist/checks/quality-tool-descriptions.js +106 -0
- package/dist/checks/quality-tool-names.d.ts +35 -0
- package/dist/checks/quality-tool-names.js +160 -0
- package/dist/checks/quality-tool-output-schema.d.ts +10 -0
- package/dist/checks/quality-tool-output-schema.js +76 -0
- package/dist/checks/quality-tool-surface.d.ts +13 -0
- package/dist/checks/quality-tool-surface.js +99 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +35 -3
- package/dist/diagnostics.d.ts +11 -0
- package/dist/diagnostics.js +28 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/orchestrator.js +7 -1
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +72 -0
- package/dist/protocol/connect.js +42 -1
- package/dist/protocol/quality-rules.d.ts +46 -0
- package/dist/protocol/quality-rules.js +37 -0
- package/dist/quality-score.d.ts +76 -0
- package/dist/quality-score.js +242 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +54 -0
- package/dist/types.d.ts +81 -1
- package/package.json +1 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { categoryOf, inferDiagnosticCategory } from './diagnostics.js';
|
|
2
|
+
import { allChecks } from './checks/index.js';
|
|
3
|
+
export const QUALITY_DIMENSIONS = [
|
|
4
|
+
'protocol',
|
|
5
|
+
'schema',
|
|
6
|
+
'usability',
|
|
7
|
+
'security',
|
|
8
|
+
'reliability',
|
|
9
|
+
];
|
|
10
|
+
/** Suggested weights from the product spec; documented here since every
|
|
11
|
+
* point deduction must be explainable in terms of these. Do not change
|
|
12
|
+
* without a documented reason — see .agent-room/DECISIONS.md. */
|
|
13
|
+
export const QUALITY_DIMENSION_WEIGHTS = {
|
|
14
|
+
protocol: 0.25,
|
|
15
|
+
schema: 0.2,
|
|
16
|
+
usability: 0.2,
|
|
17
|
+
security: 0.2,
|
|
18
|
+
reliability: 0.15,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The score is a measurement, not a certification: it reports what
|
|
22
|
+
* mcp-medic's passive checks actually found (or, per `coverage`, actually
|
|
23
|
+
* looked for) — never a formal audit of the server's real security or
|
|
24
|
+
* behavior. Surfaced in both JSON (`ReportQualityScore.disclaimer`) and the
|
|
25
|
+
* human report.
|
|
26
|
+
*/
|
|
27
|
+
export const QUALITY_SCORE_DISCLAIMER = "This score reflects issues detectable by mcp-medic's passive inspection (see \"coverage\") — " +
|
|
28
|
+
'it is not a certification of the server\'s actual security, correctness, or behavior.';
|
|
29
|
+
// Deduction weights: chosen so a handful of real problems meaningfully move
|
|
30
|
+
// the score, while a single noisy checkId can never wipe out a whole
|
|
31
|
+
// dimension on its own (the per-checkId cap) — this directly implements
|
|
32
|
+
// "avoid double-counting" / "sensible caps" from the product spec: e.g. 20
|
|
33
|
+
// tools sharing one description problem cost at most 15 points total, not 60.
|
|
34
|
+
const SEVERITY_POINTS = { error: 8, warning: 3, info: 1 };
|
|
35
|
+
const SEVERITY_CAP_PER_CHECK = { error: 25, warning: 15, info: 5 };
|
|
36
|
+
function categoryToDimension(category) {
|
|
37
|
+
switch (category) {
|
|
38
|
+
case 'protocol':
|
|
39
|
+
return 'protocol';
|
|
40
|
+
case 'schema':
|
|
41
|
+
return 'schema';
|
|
42
|
+
case 'security':
|
|
43
|
+
return 'security';
|
|
44
|
+
case 'reliability':
|
|
45
|
+
return 'reliability';
|
|
46
|
+
// 'usability', 'quality' (the broad bucket most tool/resource/prompt
|
|
47
|
+
// quality checks use), and 'configuration' (policy.* checks) all land
|
|
48
|
+
// under the "Agent usability" dimension from the product spec.
|
|
49
|
+
case 'usability':
|
|
50
|
+
case 'quality':
|
|
51
|
+
case 'configuration':
|
|
52
|
+
default:
|
|
53
|
+
return 'usability';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function describeDeduction(checkId, severity, count) {
|
|
57
|
+
const plural = count === 1 ? '' : 's';
|
|
58
|
+
return `${count} ${severity}${plural} from "${checkId}"`;
|
|
59
|
+
}
|
|
60
|
+
function deductionsFromDiagnostics(dimension, diagnostics) {
|
|
61
|
+
const grouped = new Map();
|
|
62
|
+
for (const d of diagnostics) {
|
|
63
|
+
if (categoryToDimension(categoryOf(d)) !== dimension)
|
|
64
|
+
continue;
|
|
65
|
+
const key = `${d.checkId}|${d.severity}`;
|
|
66
|
+
const entry = grouped.get(key) ?? { checkId: d.checkId, severity: d.severity, count: 0 };
|
|
67
|
+
entry.count += 1;
|
|
68
|
+
grouped.set(key, entry);
|
|
69
|
+
}
|
|
70
|
+
const deductions = [];
|
|
71
|
+
for (const { checkId, severity, count } of grouped.values()) {
|
|
72
|
+
const points = Math.min(count * SEVERITY_POINTS[severity], SEVERITY_CAP_PER_CHECK[severity]);
|
|
73
|
+
deductions.push({ dimension, checkId, severity, count, points, description: describeDeduction(checkId, severity, count) });
|
|
74
|
+
}
|
|
75
|
+
return deductions;
|
|
76
|
+
}
|
|
77
|
+
function dimensionScore(deductions) {
|
|
78
|
+
const totalPoints = deductions.reduce((sum, d) => sum + d.points, 0);
|
|
79
|
+
return Math.max(0, Math.min(100, Math.round(100 - totalPoints)));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Computes a quality score for one connection, purely from `diagnostics`
|
|
83
|
+
* (filtered to that server) — no connection metadata is consulted
|
|
84
|
+
* directly. Returns `undefined` for a connection that never connected —
|
|
85
|
+
* there's nothing meaningful to score (no tools/schema/capabilities were
|
|
86
|
+
* ever observed); see `computeReportQualityScore` for how the report-level
|
|
87
|
+
* score surfaces that instead of silently dropping it.
|
|
88
|
+
*/
|
|
89
|
+
export function computeConnectionQualityScore(connection, diagnostics) {
|
|
90
|
+
if (connection.status !== 'connected')
|
|
91
|
+
return undefined;
|
|
92
|
+
const relevant = diagnostics.filter((d) => d.serverName === connection.server.name);
|
|
93
|
+
const allDeductions = QUALITY_DIMENSIONS.flatMap((dim) => deductionsFromDiagnostics(dim, relevant));
|
|
94
|
+
const dimensions = Object.fromEntries(QUALITY_DIMENSIONS.map((dim) => [dim, dimensionScore(allDeductions.filter((d) => d.dimension === dim))]));
|
|
95
|
+
const overall = Math.round(QUALITY_DIMENSIONS.reduce((sum, dim) => sum + dimensions[dim] * QUALITY_DIMENSION_WEIGHTS[dim], 0));
|
|
96
|
+
return {
|
|
97
|
+
overall,
|
|
98
|
+
dimensions,
|
|
99
|
+
deductions: allDeductions.filter((d) => d.points > 0).sort((a, b) => b.points - a.points),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const COVERAGE_WEIGHT = { covered: 1, partial: 0.5, 'not-covered': 0 };
|
|
103
|
+
/**
|
|
104
|
+
* Coverage answers "was this dimension actually evaluated," independent of
|
|
105
|
+
* what score it got — a 100 from zero checks run means "nothing was
|
|
106
|
+
* looked for," not "nothing is wrong." Derived from the *ids* of the
|
|
107
|
+
* checks that actually ran (`executedChecks`), compared against the
|
|
108
|
+
* built-in `allChecks` reference set grouped by dimension. Reliability is
|
|
109
|
+
* special-cased to always 'covered': its signal (did the connection
|
|
110
|
+
* succeed) isn't produced by any optional `Check` — it's inherent to
|
|
111
|
+
* attempting a connection at all, so it's never "not covered."
|
|
112
|
+
*/
|
|
113
|
+
export function computeQualityCoverage(executedChecks) {
|
|
114
|
+
const referenceByDimension = new Map();
|
|
115
|
+
for (const check of allChecks) {
|
|
116
|
+
const dim = categoryToDimension(inferDiagnosticCategory(check.id));
|
|
117
|
+
if (dim === 'reliability')
|
|
118
|
+
continue; // no built-in check drives reliability; handled unconditionally below
|
|
119
|
+
if (!referenceByDimension.has(dim))
|
|
120
|
+
referenceByDimension.set(dim, new Set());
|
|
121
|
+
referenceByDimension.get(dim).add(check.id);
|
|
122
|
+
}
|
|
123
|
+
const coverage = {};
|
|
124
|
+
for (const dim of QUALITY_DIMENSIONS) {
|
|
125
|
+
if (dim === 'reliability') {
|
|
126
|
+
coverage[dim] = 'covered';
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const reference = referenceByDimension.get(dim) ?? new Set();
|
|
130
|
+
// Every executed check that maps to this dimension — including custom/
|
|
131
|
+
// community checks not in the built-in reference set, so an unmapped
|
|
132
|
+
// check still counts as (at least partial) coverage for the dimension
|
|
133
|
+
// it was inferred into, rather than being invisible to this count.
|
|
134
|
+
const executedForDim = executedChecks.filter((c) => categoryToDimension(inferDiagnosticCategory(c.id)) === dim);
|
|
135
|
+
if (executedForDim.length === 0) {
|
|
136
|
+
coverage[dim] = 'not-covered';
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (reference.size === 0) {
|
|
140
|
+
// No built-in check exists for this dimension at all, but something
|
|
141
|
+
// (necessarily a custom check) ran for it — can't call that "full"
|
|
142
|
+
// coverage without a reference to compare against.
|
|
143
|
+
coverage[dim] = 'partial';
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const executedKnownIds = new Set(executedForDim.map((c) => c.id).filter((id) => reference.has(id)));
|
|
147
|
+
coverage[dim] = executedKnownIds.size === reference.size ? 'covered' : 'partial';
|
|
148
|
+
}
|
|
149
|
+
return coverage;
|
|
150
|
+
}
|
|
151
|
+
function coveragePercent(coverage) {
|
|
152
|
+
const total = QUALITY_DIMENSIONS.reduce((sum, dim) => sum + COVERAGE_WEIGHT[coverage[dim]], 0);
|
|
153
|
+
return Math.round((total / QUALITY_DIMENSIONS.length) * 100);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Aggregate score across every connected server in a report (simple mean —
|
|
157
|
+
* deliberately not weighted by tool count, so one huge server can't drown
|
|
158
|
+
* out the rest of a fleet). `executedChecks` should be the exact list
|
|
159
|
+
* passed to `runChecks({ checks })` for this report, so `coverage` reflects
|
|
160
|
+
* what actually ran (defaults to the full built-in set if omitted, for
|
|
161
|
+
* callers that haven't been updated to pass it through).
|
|
162
|
+
* Returns `undefined` if no server connected — nothing to score at all.
|
|
163
|
+
*/
|
|
164
|
+
export function computeReportQualityScore(report, executedChecks = allChecks) {
|
|
165
|
+
const perServer = {};
|
|
166
|
+
for (const connection of report.connections) {
|
|
167
|
+
const breakdown = computeConnectionQualityScore(connection, report.diagnostics);
|
|
168
|
+
if (breakdown)
|
|
169
|
+
perServer[connection.server.name] = breakdown;
|
|
170
|
+
}
|
|
171
|
+
const scoredServers = Object.keys(perServer);
|
|
172
|
+
const unscoredServers = report.connections
|
|
173
|
+
.map((c) => c.server.name)
|
|
174
|
+
.filter((name) => !perServer[name]);
|
|
175
|
+
if (scoredServers.length === 0)
|
|
176
|
+
return undefined;
|
|
177
|
+
const dimensions = Object.fromEntries(QUALITY_DIMENSIONS.map((dim) => [
|
|
178
|
+
dim,
|
|
179
|
+
Math.round(scoredServers.reduce((sum, n) => sum + perServer[n].dimensions[dim], 0) / scoredServers.length),
|
|
180
|
+
]));
|
|
181
|
+
const overall = Math.round(scoredServers.reduce((sum, n) => sum + perServer[n].overall, 0) / scoredServers.length);
|
|
182
|
+
const deductions = scoredServers
|
|
183
|
+
.flatMap((n) => perServer[n].deductions)
|
|
184
|
+
.sort((a, b) => b.points - a.points);
|
|
185
|
+
const coverage = computeQualityCoverage(executedChecks);
|
|
186
|
+
return {
|
|
187
|
+
overall,
|
|
188
|
+
dimensions,
|
|
189
|
+
deductions,
|
|
190
|
+
perServer,
|
|
191
|
+
coverage,
|
|
192
|
+
coveragePercent: coveragePercent(coverage),
|
|
193
|
+
scoredServers,
|
|
194
|
+
unscoredServers,
|
|
195
|
+
disclaimer: QUALITY_SCORE_DISCLAIMER,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Enforces `.mcp-medic-policy.json`'s `quality.minimumScore` as a CI gate.
|
|
200
|
+
* Pure function so it's directly unit-testable without needing a real CLI
|
|
201
|
+
* invocation or a restricted check set — see PHASE 12 ("policy/CI") in
|
|
202
|
+
* .agent-room/DECISIONS.md.
|
|
203
|
+
*
|
|
204
|
+
* Returns diagnostics to append to the report (0, 1, or 2):
|
|
205
|
+
* - an 'error' if the score is below `minimumScore` (fails the run).
|
|
206
|
+
* - a 'warning' if the score was computed from a partial assessment
|
|
207
|
+
* (incomplete coverage, or a server that couldn't be scored) — a
|
|
208
|
+
* minimumScore gate must never silently "pass" as if a full check had
|
|
209
|
+
* run when it didn't.
|
|
210
|
+
*/
|
|
211
|
+
export function checkMinimumScorePolicy(quality, minimumScore, serverNamesLabel) {
|
|
212
|
+
const results = [];
|
|
213
|
+
if (quality.overall < minimumScore) {
|
|
214
|
+
results.push({
|
|
215
|
+
checkId: 'policy.minimum-quality-score',
|
|
216
|
+
severity: 'error',
|
|
217
|
+
message: `MCP quality score ${quality.overall} is below the policy minimum of ${minimumScore}.`,
|
|
218
|
+
serverName: serverNamesLabel,
|
|
219
|
+
category: 'configuration',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
if (quality.coveragePercent < 100 || quality.unscoredServers.length > 0) {
|
|
223
|
+
const gaps = Object.entries(quality.coverage)
|
|
224
|
+
.filter(([, status]) => status !== 'covered')
|
|
225
|
+
.map(([dim, status]) => `${dim}: ${status}`)
|
|
226
|
+
.join(', ');
|
|
227
|
+
const parts = [
|
|
228
|
+
gaps ? `coverage is ${quality.coveragePercent}% (${gaps})` : undefined,
|
|
229
|
+
quality.unscoredServers.length > 0
|
|
230
|
+
? `${quality.unscoredServers.length} server(s) could not be scored: ${quality.unscoredServers.join(', ')}`
|
|
231
|
+
: undefined,
|
|
232
|
+
].filter(Boolean);
|
|
233
|
+
results.push({
|
|
234
|
+
checkId: 'policy.partial-coverage-with-minimum-score',
|
|
235
|
+
severity: 'warning',
|
|
236
|
+
message: `quality.minimumScore was evaluated against a partial assessment (${parts.join('; ')}) — it may not reflect a full check of this server.`,
|
|
237
|
+
serverName: serverNamesLabel,
|
|
238
|
+
category: 'configuration',
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return results;
|
|
242
|
+
}
|
package/dist/report.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { RunReport } from './types.js';
|
|
2
2
|
export interface FormatReportOptions {
|
|
3
3
|
showFixes?: boolean;
|
|
4
|
+
/** Include the MCP quality score section (Phase 7). Off by default so a
|
|
5
|
+
* plain `check` report stays short; `--score`/`score` turn it on. */
|
|
6
|
+
showScore?: boolean;
|
|
4
7
|
}
|
|
5
8
|
/** Human-readable plain text report formatter. */
|
|
6
9
|
export declare function formatReportHuman(report: RunReport, options?: FormatReportOptions): string;
|
package/dist/report.js
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
const DIMENSION_LABELS = {
|
|
2
|
+
protocol: 'Protocol',
|
|
3
|
+
schema: 'Schema',
|
|
4
|
+
usability: 'Agent usability',
|
|
5
|
+
security: 'Security',
|
|
6
|
+
reliability: 'Reliability',
|
|
7
|
+
};
|
|
8
|
+
function formatScoreBlock(label, score) {
|
|
9
|
+
const lines = [];
|
|
10
|
+
lines.push(label);
|
|
11
|
+
for (const [dim, value] of Object.entries(score.dimensions)) {
|
|
12
|
+
const name = (DIMENSION_LABELS[dim] ?? dim).padEnd(17, ' ');
|
|
13
|
+
lines.push(` ${name} ${String(value).padStart(3, ' ')}/100`);
|
|
14
|
+
}
|
|
15
|
+
lines.push(` ${'MCP QUALITY SCORE'.padEnd(17, ' ')} ${String(score.overall).padStart(3, ' ')}/100`);
|
|
16
|
+
if (score.deductions.length > 0) {
|
|
17
|
+
lines.push(' Deductions:');
|
|
18
|
+
for (const d of score.deductions) {
|
|
19
|
+
lines.push(` -${d.points} ${d.description}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return lines;
|
|
23
|
+
}
|
|
24
|
+
/** Printed once per report (not per-server) — coverage and the disclaimer
|
|
25
|
+
* are report-level facts, and repeating them per server would overwhelm
|
|
26
|
+
* the output for a multi-server fleet. */
|
|
27
|
+
function formatCoverageAndDisclaimer(quality) {
|
|
28
|
+
const lines = [];
|
|
29
|
+
const gaps = Object.entries(quality.coverage).filter(([, status]) => status !== 'covered');
|
|
30
|
+
const coverageLine = gaps.length === 0
|
|
31
|
+
? `Coverage: ${quality.coveragePercent}% (all dimensions fully evaluated)`
|
|
32
|
+
: `Coverage: ${quality.coveragePercent}% (${gaps
|
|
33
|
+
.map(([dim, status]) => `${DIMENSION_LABELS[dim] ?? dim}: ${status}`)
|
|
34
|
+
.join(', ')})`;
|
|
35
|
+
lines.push(coverageLine);
|
|
36
|
+
if (quality.unscoredServers.length > 0) {
|
|
37
|
+
lines.push(`Note: ${quality.unscoredServers.length} server(s) could not be scored (connection failed): ${quality.unscoredServers.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
lines.push(quality.disclaimer);
|
|
40
|
+
return lines;
|
|
41
|
+
}
|
|
1
42
|
/** Human-readable plain text report formatter. */
|
|
2
43
|
export function formatReportHuman(report, options = {}) {
|
|
3
44
|
const lines = [];
|
|
@@ -30,6 +71,19 @@ export function formatReportHuman(report, options = {}) {
|
|
|
30
71
|
if (conn.error) {
|
|
31
72
|
lines.push(` ${conn.error.stage}: ${conn.error.message}`);
|
|
32
73
|
}
|
|
74
|
+
const perServerScore = options.showScore ? report.quality?.perServer[conn.server.name] : undefined;
|
|
75
|
+
if (perServerScore) {
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(...formatScoreBlock('QUALITY', perServerScore));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (options.showScore && report.quality) {
|
|
81
|
+
if (Object.keys(report.quality.perServer).length > 1) {
|
|
82
|
+
lines.push('');
|
|
83
|
+
lines.push(...formatScoreBlock('OVERALL QUALITY (all servers)', report.quality));
|
|
84
|
+
}
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push(...formatCoverageAndDisclaimer(report.quality));
|
|
33
87
|
}
|
|
34
88
|
if (report.diagnostics.length > 0) {
|
|
35
89
|
lines.push('');
|
package/dist/types.d.ts
CHANGED
|
@@ -14,21 +14,48 @@ export interface MCPConfig {
|
|
|
14
14
|
servers: MCPServerConfig[];
|
|
15
15
|
sourcePath?: string;
|
|
16
16
|
}
|
|
17
|
+
/** Per the MCP spec's `ToolAnnotations`: optional client *hints*, not
|
|
18
|
+
* authoritative security guarantees — a server can lie about any of these. */
|
|
19
|
+
export interface MCPToolAnnotations {
|
|
20
|
+
title?: string;
|
|
21
|
+
readOnlyHint?: boolean;
|
|
22
|
+
destructiveHint?: boolean;
|
|
23
|
+
idempotentHint?: boolean;
|
|
24
|
+
openWorldHint?: boolean;
|
|
25
|
+
}
|
|
17
26
|
export interface MCPToolDefinition {
|
|
18
27
|
name: string;
|
|
28
|
+
title?: string;
|
|
19
29
|
description?: string;
|
|
20
30
|
inputSchema: unknown;
|
|
31
|
+
/** Optional per spec; same shape as `inputSchema` when present. */
|
|
32
|
+
outputSchema?: unknown;
|
|
33
|
+
annotations?: MCPToolAnnotations;
|
|
21
34
|
}
|
|
22
35
|
export interface MCPResourceDefinition {
|
|
23
36
|
uri: string;
|
|
37
|
+
/** Required by the MCP spec's `Resource` (extends `BaseMetadata`) — kept
|
|
38
|
+
* optional here because mcp-medic reports a missing name as a diagnostic
|
|
39
|
+
* rather than failing the whole `resources/list` response over it. */
|
|
24
40
|
name?: string;
|
|
41
|
+
title?: string;
|
|
25
42
|
description?: string;
|
|
26
43
|
mimeType?: string;
|
|
44
|
+
size?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface MCPPromptArgument {
|
|
47
|
+
/** Required by the MCP spec's `PromptArgument` (extends `BaseMetadata`) —
|
|
48
|
+
* kept optional here for the same reason as `MCPResourceDefinition.name`. */
|
|
49
|
+
name?: string;
|
|
50
|
+
title?: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
required?: boolean;
|
|
27
53
|
}
|
|
28
54
|
export interface MCPPromptDefinition {
|
|
29
55
|
name: string;
|
|
56
|
+
title?: string;
|
|
30
57
|
description?: string;
|
|
31
|
-
arguments?:
|
|
58
|
+
arguments?: MCPPromptArgument[];
|
|
32
59
|
}
|
|
33
60
|
export interface ProtocolVersionInfo {
|
|
34
61
|
/** The protocolVersion this client sent in `initialize`. */
|
|
@@ -68,6 +95,10 @@ export interface MCPConnection {
|
|
|
68
95
|
latencyMs?: number;
|
|
69
96
|
}
|
|
70
97
|
export type Severity = 'error' | 'warning' | 'info';
|
|
98
|
+
/** Stable diagnostic taxonomy. Optional and additive — existing checks that
|
|
99
|
+
* don't set this are categorized by `inferDiagnosticCategory()` (src/diagnostics.ts)
|
|
100
|
+
* from their `checkId` prefix, so nothing that already works needs to change. */
|
|
101
|
+
export type DiagnosticCategory = 'protocol' | 'schema' | 'quality' | 'security' | 'reliability' | 'usability' | 'configuration';
|
|
71
102
|
export interface SuggestedFix {
|
|
72
103
|
description: string;
|
|
73
104
|
patch?: unknown;
|
|
@@ -80,6 +111,12 @@ export interface DiagnosticResult {
|
|
|
80
111
|
toolName?: string;
|
|
81
112
|
details?: unknown;
|
|
82
113
|
suggestedFix?: SuggestedFix;
|
|
114
|
+
/** Optional; see `DiagnosticCategory`. */
|
|
115
|
+
category?: DiagnosticCategory;
|
|
116
|
+
/** Optional: how confident the check is that this is a real problem (vs. a heuristic guess). */
|
|
117
|
+
confidence?: 'low' | 'medium' | 'high';
|
|
118
|
+
/** Optional: link to a doc explaining the rule this diagnostic enforces. */
|
|
119
|
+
documentationUrl?: string;
|
|
83
120
|
}
|
|
84
121
|
export interface Check {
|
|
85
122
|
id: string;
|
|
@@ -94,6 +131,46 @@ export interface RunOptions {
|
|
|
94
131
|
/** "auto" (default) requests the newest protocol version this client supports; an explicit version string requests that version instead. */
|
|
95
132
|
protocolVersion?: string;
|
|
96
133
|
}
|
|
134
|
+
/** The five scored dimensions of MCP quality (see src/quality-score.ts for the scoring model). */
|
|
135
|
+
export type QualityDimension = 'protocol' | 'schema' | 'usability' | 'security' | 'reliability';
|
|
136
|
+
export interface QualityDeduction {
|
|
137
|
+
dimension: QualityDimension;
|
|
138
|
+
checkId: string;
|
|
139
|
+
severity: Severity;
|
|
140
|
+
/** How many diagnostics from this checkId (at this severity) contributed. */
|
|
141
|
+
count: number;
|
|
142
|
+
/** Points actually deducted, after the per-checkId cap. */
|
|
143
|
+
points: number;
|
|
144
|
+
description: string;
|
|
145
|
+
}
|
|
146
|
+
export interface QualityScoreBreakdown {
|
|
147
|
+
overall: number;
|
|
148
|
+
dimensions: Record<QualityDimension, number>;
|
|
149
|
+
/** Only deductions with points > 0, most-costly first — every deduction here explains itself. */
|
|
150
|
+
deductions: QualityDeduction[];
|
|
151
|
+
}
|
|
152
|
+
/** Whether the checks that feed a dimension actually ran this time.
|
|
153
|
+
* 'covered': every built-in check for this dimension ran.
|
|
154
|
+
* 'partial': at least one check contributing to this dimension ran, but not all of them.
|
|
155
|
+
* 'not-covered': no check contributing to this dimension ran — a 100 in
|
|
156
|
+
* that dimension means "nothing flagged it," not "nothing wrong exists." */
|
|
157
|
+
export type CoverageStatus = 'covered' | 'partial' | 'not-covered';
|
|
158
|
+
export type QualityCoverage = Record<QualityDimension, CoverageStatus>;
|
|
159
|
+
export interface ReportQualityScore extends QualityScoreBreakdown {
|
|
160
|
+
/** Per-connected-server breakdown; a server that never connected has no entry (nothing to score). */
|
|
161
|
+
perServer: Record<string, QualityScoreBreakdown>;
|
|
162
|
+
/** Per-dimension coverage, derived from which checks actually ran (`RunOptions.checks`) —
|
|
163
|
+
* never assume a dimension was fully evaluated just because it scored 100. */
|
|
164
|
+
coverage: QualityCoverage;
|
|
165
|
+
/** 0-100: 'covered' dimensions count as 1, 'partial' as 0.5, 'not-covered' as 0, averaged across all 5. */
|
|
166
|
+
coveragePercent: number;
|
|
167
|
+
/** Names of servers actually included in this score (connected + scored). */
|
|
168
|
+
scoredServers: string[];
|
|
169
|
+
/** Names of servers that could NOT be scored (failed to connect) — never silently dropped from view. */
|
|
170
|
+
unscoredServers: string[];
|
|
171
|
+
/** A short, load-bearing reminder of what this number does and doesn't mean — see src/quality-score.ts. */
|
|
172
|
+
disclaimer: string;
|
|
173
|
+
}
|
|
97
174
|
export interface RunReport {
|
|
98
175
|
configSource?: string;
|
|
99
176
|
connections: MCPConnection[];
|
|
@@ -105,4 +182,7 @@ export interface RunReport {
|
|
|
105
182
|
errors: number;
|
|
106
183
|
warnings: number;
|
|
107
184
|
};
|
|
185
|
+
/** Deterministic quality score derived from `diagnostics`/`connections` — see src/quality-score.ts.
|
|
186
|
+
* `undefined` when no server connected (nothing to score). */
|
|
187
|
+
quality?: ReportQualityScore;
|
|
108
188
|
}
|