mcp-medic 1.2.0 → 1.2.2
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/dist/cli.d.ts +3 -0
- package/dist/cli.js +43 -1
- package/dist/fleet.js +5 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/orchestrator.js +7 -1
- package/dist/protocol/connect.js +140 -16
- package/dist/protocol/index.d.ts +1 -0
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/versions.d.ts +43 -0
- package/dist/protocol/versions.js +51 -0
- package/dist/redact.d.ts +34 -0
- package/dist/redact.js +61 -0
- package/dist/report.js +19 -0
- package/dist/sarif.d.ts +11 -0
- package/dist/sarif.js +77 -0
- package/dist/types.d.ts +38 -0
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface ParsedArgs {
|
|
|
8
8
|
policyPath?: string;
|
|
9
9
|
exportJunit?: string;
|
|
10
10
|
exportJson?: string;
|
|
11
|
+
exportSarif?: string;
|
|
11
12
|
snapshotPath?: string;
|
|
12
13
|
updateSnapshotPath?: string;
|
|
13
14
|
json: boolean;
|
|
@@ -17,6 +18,8 @@ export interface ParsedArgs {
|
|
|
17
18
|
failOn: 'error' | 'warning';
|
|
18
19
|
checkFilter?: string;
|
|
19
20
|
dryRun: boolean;
|
|
21
|
+
/** "auto" (default) or an explicit MCP protocolVersion string, e.g. "2025-06-18". */
|
|
22
|
+
protocolVersion: string;
|
|
20
23
|
}
|
|
21
24
|
/** Prompts the user with `question` and resolves true for an explicit "y"/"yes" answer. */
|
|
22
25
|
export type ConfirmFn = (question: string) => Promise<boolean>;
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,8 @@ import { isConfigPatch, diffConfigPatch, applyConfigPatch } from './fix.js';
|
|
|
13
13
|
import { loadPolicy, createPolicyChecks } from './policy.js';
|
|
14
14
|
import { runFleetChecks, diffConfigs, filterDiagnosticsByBaseline } from './fleet.js';
|
|
15
15
|
import { formatReportJUnit, formatFleetReportJUnit } from './junit.js';
|
|
16
|
+
import { formatReportSarif } from './sarif.js';
|
|
17
|
+
import { SUPPORTED_PROTOCOL_VERSIONS } from './protocol/versions.js';
|
|
16
18
|
import pc from 'picocolors';
|
|
17
19
|
export function parseArgs(argv) {
|
|
18
20
|
const args = {
|
|
@@ -22,6 +24,7 @@ export function parseArgs(argv) {
|
|
|
22
24
|
verbose: false,
|
|
23
25
|
failOn: 'error',
|
|
24
26
|
dryRun: false,
|
|
27
|
+
protocolVersion: 'auto',
|
|
25
28
|
};
|
|
26
29
|
const positional = [];
|
|
27
30
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -67,6 +70,9 @@ export function parseArgs(argv) {
|
|
|
67
70
|
else if (arg === '--export-json') {
|
|
68
71
|
args.exportJson = argv[++i];
|
|
69
72
|
}
|
|
73
|
+
else if (arg === '--export-sarif') {
|
|
74
|
+
args.exportSarif = argv[++i];
|
|
75
|
+
}
|
|
70
76
|
else if (arg === '--snapshot') {
|
|
71
77
|
args.snapshotPath = argv[++i];
|
|
72
78
|
}
|
|
@@ -95,6 +101,13 @@ export function parseArgs(argv) {
|
|
|
95
101
|
}
|
|
96
102
|
args.timeoutMs = parsed;
|
|
97
103
|
}
|
|
104
|
+
else if (arg === '--protocol-version') {
|
|
105
|
+
const val = argv[++i];
|
|
106
|
+
if (!val) {
|
|
107
|
+
throw new Error(`--protocol-version requires a value: "auto" or an explicit version (e.g. ${SUPPORTED_PROTOCOL_VERSIONS[0]})`);
|
|
108
|
+
}
|
|
109
|
+
args.protocolVersion = val;
|
|
110
|
+
}
|
|
98
111
|
else {
|
|
99
112
|
positional.push(arg);
|
|
100
113
|
}
|
|
@@ -182,14 +195,23 @@ OPTIONS
|
|
|
182
195
|
--update-snapshot <p> Save diagnostic report as new baseline snapshot
|
|
183
196
|
--export-junit <file> Export report in JUnit XML format
|
|
184
197
|
--export-json <file> Export report in JSON format
|
|
198
|
+
--export-sarif <file> Export report in SARIF 2.1.0 format (GitHub Code Scanning, etc.)
|
|
185
199
|
--show-fixes Print actionable suggested fixes under diagnostics
|
|
186
200
|
--fail-on <severity> Exit with code 1 on 'error' (default) or 'warning'
|
|
187
201
|
--verbose, -v Print raw JSON-RPC traffic and debug messages
|
|
188
202
|
--json Output report in JSON format
|
|
189
203
|
--timeout <ms> Per-server handshake timeout in milliseconds (default: 5000)
|
|
204
|
+
--protocol-version <v> MCP protocolVersion to request: "auto" (default, latest supported)
|
|
205
|
+
or an explicit version, e.g. ${SUPPORTED_PROTOCOL_VERSIONS[SUPPORTED_PROTOCOL_VERSIONS.length - 1]}
|
|
190
206
|
--help, -h Show help
|
|
191
207
|
--version, -V Print the installed mcp-medic version
|
|
192
208
|
|
|
209
|
+
PROTOCOL VERSIONS
|
|
210
|
+
This client supports: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')}
|
|
211
|
+
"auto" requests ${SUPPORTED_PROTOCOL_VERSIONS[0]} (the newest). The server may negotiate an
|
|
212
|
+
older version instead; mcp-medic reports both and fails cleanly if the
|
|
213
|
+
negotiated version isn't one this client supports.
|
|
214
|
+
|
|
193
215
|
FIX OPTIONS (mcp-medic fix)
|
|
194
216
|
--check <id> Only offer fixes from this check id (e.g. security.untrusted-remote)
|
|
195
217
|
--dry-run Show every available fix as a diff; apply nothing, prompt for nothing
|
|
@@ -222,6 +244,10 @@ function colorizeHumanReport(text) {
|
|
|
222
244
|
return pc.yellow(line);
|
|
223
245
|
if (/Suggested fix:/.test(line))
|
|
224
246
|
return pc.cyan(line);
|
|
247
|
+
if (/^\s*Protocol:.*✗ incompatible/.test(line))
|
|
248
|
+
return pc.red(line);
|
|
249
|
+
if (/^\s*Protocol:/.test(line))
|
|
250
|
+
return pc.dim(line);
|
|
225
251
|
return line;
|
|
226
252
|
})
|
|
227
253
|
.join('\n');
|
|
@@ -232,6 +258,7 @@ async function executeCheck(config, args) {
|
|
|
232
258
|
timeoutMs: args.timeoutMs,
|
|
233
259
|
checks,
|
|
234
260
|
verbose: args.verbose,
|
|
261
|
+
protocolVersion: args.protocolVersion,
|
|
235
262
|
});
|
|
236
263
|
// Handle baseline snapshot comparison
|
|
237
264
|
if (args.snapshotPath) {
|
|
@@ -275,6 +302,15 @@ async function executeCheck(config, args) {
|
|
|
275
302
|
console.error(pc.red(`Failed to write JSON export: ${String(err)}`));
|
|
276
303
|
}
|
|
277
304
|
}
|
|
305
|
+
// Handle SARIF export
|
|
306
|
+
if (args.exportSarif) {
|
|
307
|
+
try {
|
|
308
|
+
writeFileSync(resolve(args.exportSarif), formatReportSarif(report));
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
console.error(pc.red(`Failed to write SARIF export: ${String(err)}`));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
278
314
|
if (args.json) {
|
|
279
315
|
console.log(formatReportJSON(report));
|
|
280
316
|
}
|
|
@@ -338,7 +374,12 @@ async function defaultConfirm(question) {
|
|
|
338
374
|
*/
|
|
339
375
|
async function executeFix(configPath, rawText, rawJson, config, args, confirm) {
|
|
340
376
|
const [checks] = await Promise.all([loadChecks(args.policyPath), loadProtocol()]);
|
|
341
|
-
const report = await runChecks(config, {
|
|
377
|
+
const report = await runChecks(config, {
|
|
378
|
+
timeoutMs: args.timeoutMs,
|
|
379
|
+
checks,
|
|
380
|
+
verbose: args.verbose,
|
|
381
|
+
protocolVersion: args.protocolVersion,
|
|
382
|
+
});
|
|
342
383
|
let fixable = report.diagnostics.filter((d) => isConfigPatch(d.suggestedFix?.patch));
|
|
343
384
|
if (args.checkFilter) {
|
|
344
385
|
fixable = fixable.filter((d) => d.checkId === args.checkFilter);
|
|
@@ -496,6 +537,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
496
537
|
checks,
|
|
497
538
|
timeoutMs: args.timeoutMs,
|
|
498
539
|
verbose: args.verbose,
|
|
540
|
+
protocolVersion: args.protocolVersion,
|
|
499
541
|
});
|
|
500
542
|
if (args.exportJunit) {
|
|
501
543
|
try {
|
package/dist/fleet.js
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { loadConfig } from './config-loader.js';
|
|
4
4
|
import { runChecks } from './orchestrator.js';
|
|
5
5
|
import { allChecks } from './checks/index.js';
|
|
6
|
+
import { redactRecord } from './redact.js';
|
|
6
7
|
function findFilesMatching(dir, pattern, results = []) {
|
|
7
8
|
if (!existsSync(dir))
|
|
8
9
|
return results;
|
|
@@ -114,7 +115,10 @@ export function diffConfigs(configA, configB) {
|
|
|
114
115
|
changes.push({ field: 'url', from: serverA.url, to: serverB.url });
|
|
115
116
|
}
|
|
116
117
|
if (JSON.stringify(serverA.env) !== JSON.stringify(serverB.env)) {
|
|
117
|
-
|
|
118
|
+
// Report that env changed and which keys, but never raw values —
|
|
119
|
+
// env vars routinely carry API keys/tokens and diff output gets
|
|
120
|
+
// pasted into PRs and CI logs.
|
|
121
|
+
changes.push({ field: 'env', from: redactRecord(serverA.env), to: redactRecord(serverB.env) });
|
|
118
122
|
}
|
|
119
123
|
if (changes.length > 0) {
|
|
120
124
|
entries.push({ serverName: name, kind: 'modified', changes });
|
package/dist/index.d.ts
CHANGED
|
@@ -16,5 +16,9 @@ export type { MCPDoctorPolicy } from './policy.js';
|
|
|
16
16
|
export { runFleetChecks, diffConfigs, filterDiagnosticsByBaseline, findConfigFiles, } from './fleet.js';
|
|
17
17
|
export type { FleetReport, ConfigDiffResult, ConfigDiffEntry, FileRunResult } from './fleet.js';
|
|
18
18
|
export { formatReportJUnit, formatFleetReportJUnit } from './junit.js';
|
|
19
|
+
export { formatReportSarif } from './sarif.js';
|
|
19
20
|
export { allChecks } from './checks/index.js';
|
|
21
|
+
export { SUPPORTED_PROTOCOL_VERSIONS, LATEST_SUPPORTED_PROTOCOL_VERSION, KNOWN_UNSUPPORTED_PROTOCOL_VERSIONS, isSupportedProtocolVersion, resolveRequestedProtocolVersion, } from './protocol/versions.js';
|
|
22
|
+
export type { SupportedProtocolVersion, ProtocolVersionNegotiation } from './protocol/versions.js';
|
|
23
|
+
export { isSecretKey, redactRecord, redactDeep, sanitizeServerConfig } from './redact.js';
|
|
20
24
|
export * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -9,5 +9,8 @@ export { isMCPConfigFile, validateMCPDocument, activateExtension } from './exten
|
|
|
9
9
|
export { loadPolicy, createPolicyChecks } from './policy.js';
|
|
10
10
|
export { runFleetChecks, diffConfigs, filterDiagnosticsByBaseline, findConfigFiles, } from './fleet.js';
|
|
11
11
|
export { formatReportJUnit, formatFleetReportJUnit } from './junit.js';
|
|
12
|
+
export { formatReportSarif } from './sarif.js';
|
|
12
13
|
export { allChecks } from './checks/index.js';
|
|
14
|
+
export { SUPPORTED_PROTOCOL_VERSIONS, LATEST_SUPPORTED_PROTOCOL_VERSION, KNOWN_UNSUPPORTED_PROTOCOL_VERSIONS, isSupportedProtocolVersion, resolveRequestedProtocolVersion, } from './protocol/versions.js';
|
|
15
|
+
export { isSecretKey, redactRecord, redactDeep, sanitizeServerConfig } from './redact.js';
|
|
13
16
|
export * from './types.js';
|
package/dist/orchestrator.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeServerConfig } from './redact.js';
|
|
1
2
|
async function connectStub(config) {
|
|
2
3
|
return {
|
|
3
4
|
server: config,
|
|
@@ -21,7 +22,12 @@ export async function runChecks(config, options = {}) {
|
|
|
21
22
|
const connections = [];
|
|
22
23
|
const diagnostics = [];
|
|
23
24
|
for (const server of config.servers) {
|
|
24
|
-
const
|
|
25
|
+
const rawConnection = await connectImpl(server, timeoutMs, options);
|
|
26
|
+
// Redact secret-looking header/env/token-body values before this ever
|
|
27
|
+
// reaches a report, JSON export, or JUnit output — checks only read
|
|
28
|
+
// `server.name`/`transport`/`url`, never header or env *values*, so
|
|
29
|
+
// this can't change diagnostic behavior.
|
|
30
|
+
const connection = { ...rawConnection, server: sanitizeServerConfig(server) };
|
|
25
31
|
connections.push(connection);
|
|
26
32
|
if (connection.status !== 'connected') {
|
|
27
33
|
continue; // checks require a live connection; connection failure is its own signal in the report
|
package/dist/protocol/connect.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { KNOWN_UNSUPPORTED_PROTOCOL_VERSIONS, SUPPORTED_PROTOCOL_VERSIONS, isSupportedProtocolVersion, resolveRequestedProtocolVersion, } from './versions.js';
|
|
5
|
+
function readOwnVersion() {
|
|
6
|
+
try {
|
|
7
|
+
const pkgPath = fileURLToPath(new URL('../../package.json', import.meta.url));
|
|
8
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
9
|
+
return pkg.version ?? '0.0.0';
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return '0.0.0';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const CLIENT_INFO = { name: 'mcp-medic', version: readOwnVersion() };
|
|
4
16
|
function messageOf(error) {
|
|
5
17
|
return error instanceof Error ? error.message : String(error);
|
|
6
18
|
}
|
|
@@ -12,8 +24,24 @@ function logVerbose(options, message) {
|
|
|
12
24
|
console.error(`[debug] ${message}`);
|
|
13
25
|
}
|
|
14
26
|
}
|
|
15
|
-
function failed(config, status, stage, message, raw) {
|
|
16
|
-
return {
|
|
27
|
+
function failed(config, status, stage, message, raw, extra) {
|
|
28
|
+
return {
|
|
29
|
+
server: config,
|
|
30
|
+
status,
|
|
31
|
+
error: { stage, message, ...(raw === undefined ? {} : { raw }) },
|
|
32
|
+
...extra,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function normalizeServerInfo(value) {
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
37
|
+
return undefined;
|
|
38
|
+
const record = value;
|
|
39
|
+
const info = {};
|
|
40
|
+
if (typeof record.name === 'string')
|
|
41
|
+
info.name = record.name;
|
|
42
|
+
if (typeof record.version === 'string')
|
|
43
|
+
info.version = record.version;
|
|
44
|
+
return info.name || info.version ? info : undefined;
|
|
17
45
|
}
|
|
18
46
|
function withTimeout(promise, timeoutMs, label) {
|
|
19
47
|
return new Promise((resolve, reject) => {
|
|
@@ -54,6 +82,37 @@ function normalizeTools(result) {
|
|
|
54
82
|
};
|
|
55
83
|
});
|
|
56
84
|
}
|
|
85
|
+
function normalizeResources(result) {
|
|
86
|
+
if (!Array.isArray(result.resources))
|
|
87
|
+
throw new Error('resources/list response has no resources array');
|
|
88
|
+
return result.resources.map((resource, index) => {
|
|
89
|
+
if (!resource || typeof resource !== 'object' || typeof resource.uri !== 'string') {
|
|
90
|
+
throw new Error(`resources/list returned an invalid resource at index ${index}`);
|
|
91
|
+
}
|
|
92
|
+
const value = resource;
|
|
93
|
+
return {
|
|
94
|
+
uri: value.uri,
|
|
95
|
+
...(typeof value.name === 'string' ? { name: value.name } : {}),
|
|
96
|
+
...(typeof value.description === 'string' ? { description: value.description } : {}),
|
|
97
|
+
...(typeof value.mimeType === 'string' ? { mimeType: value.mimeType } : {}),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function normalizePrompts(result) {
|
|
102
|
+
if (!Array.isArray(result.prompts))
|
|
103
|
+
throw new Error('prompts/list response has no prompts array');
|
|
104
|
+
return result.prompts.map((prompt, index) => {
|
|
105
|
+
if (!prompt || typeof prompt !== 'object' || typeof prompt.name !== 'string') {
|
|
106
|
+
throw new Error(`prompts/list returned an invalid prompt at index ${index}`);
|
|
107
|
+
}
|
|
108
|
+
const value = prompt;
|
|
109
|
+
return {
|
|
110
|
+
name: value.name,
|
|
111
|
+
...(typeof value.description === 'string' ? { description: value.description } : {}),
|
|
112
|
+
...(Array.isArray(value.arguments) ? { arguments: value.arguments } : {}),
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
}
|
|
57
116
|
async function refreshTokenIfNeeded(config, options) {
|
|
58
117
|
const headers = { ...(config.headers ?? {}) };
|
|
59
118
|
if (!config.tokenRefreshUrl) {
|
|
@@ -347,6 +406,16 @@ class SseTransport {
|
|
|
347
406
|
}
|
|
348
407
|
export async function connect(config, timeoutMs, options) {
|
|
349
408
|
const started = Date.now();
|
|
409
|
+
const requestedVersion = resolveRequestedProtocolVersion(options?.protocolVersion);
|
|
410
|
+
// Some real MCP protocol versions use a wire shape this client doesn't
|
|
411
|
+
// implement (e.g. 2026-07-28 drops the initialize handshake entirely).
|
|
412
|
+
// Fail fast on those, before spawning a process or opening a connection,
|
|
413
|
+
// rather than attempting a handshake that was never going to work.
|
|
414
|
+
const unsupportedReason = KNOWN_UNSUPPORTED_PROTOCOL_VERSIONS[requestedVersion];
|
|
415
|
+
if (unsupportedReason) {
|
|
416
|
+
return failed(config, 'failed', 'handshake', `cannot request protocol version ${requestedVersion}: ${unsupportedReason}. ` +
|
|
417
|
+
`This client supports: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')}.`);
|
|
418
|
+
}
|
|
350
419
|
let transport;
|
|
351
420
|
try {
|
|
352
421
|
if (config.transport === 'stdio') {
|
|
@@ -371,42 +440,97 @@ export async function connect(config, timeoutMs, options) {
|
|
|
371
440
|
else {
|
|
372
441
|
return failed(config, 'failed', 'spawn', `unsupported transport: ${String(config.transport)}`);
|
|
373
442
|
}
|
|
443
|
+
// Requests are numbered sequentially in the order they're actually sent
|
|
444
|
+
// (notifications don't consume an id) — tracked locally rather than
|
|
445
|
+
// hardcoded, since which optional capability calls happen below depends
|
|
446
|
+
// on what the server declares.
|
|
447
|
+
let nextExpectedId = 1;
|
|
374
448
|
let initialize;
|
|
375
449
|
try {
|
|
376
450
|
const response = await withTimeout(transport.request('initialize', {
|
|
377
|
-
protocolVersion:
|
|
451
|
+
protocolVersion: requestedVersion,
|
|
378
452
|
capabilities: {},
|
|
379
453
|
clientInfo: CLIENT_INFO,
|
|
380
454
|
}), timeoutMs, 'initialize handshake');
|
|
381
|
-
initialize = validateResponse(response,
|
|
455
|
+
initialize = validateResponse(response, nextExpectedId++);
|
|
382
456
|
if (!initialize.capabilities || typeof initialize.capabilities !== 'object') {
|
|
383
457
|
throw new Error('initialize response has no capabilities object');
|
|
384
458
|
}
|
|
459
|
+
if (typeof initialize.protocolVersion !== 'string' || !initialize.protocolVersion) {
|
|
460
|
+
throw new Error('initialize response is missing a protocolVersion string (protocol violation — ' +
|
|
461
|
+
'the server MUST report the version it negotiated)');
|
|
462
|
+
}
|
|
385
463
|
}
|
|
386
464
|
catch (error) {
|
|
387
465
|
const timedOut = messageOf(error).includes('timed out');
|
|
388
466
|
const message = messageOf(error);
|
|
389
467
|
return failed(config, timedOut ? 'timeout' : 'failed', message.startsWith('failed to start') ? 'spawn' : 'handshake', message, error);
|
|
390
468
|
}
|
|
469
|
+
const negotiatedVersion = initialize.protocolVersion;
|
|
470
|
+
const compatible = isSupportedProtocolVersion(negotiatedVersion);
|
|
471
|
+
const protocolVersion = { requested: requestedVersion, negotiated: negotiatedVersion, compatible };
|
|
472
|
+
const serverInfo = normalizeServerInfo(initialize.serverInfo);
|
|
473
|
+
const capabilities = initialize.capabilities;
|
|
474
|
+
if (!compatible) {
|
|
475
|
+
// Per spec: if the client doesn't support the version the server
|
|
476
|
+
// negotiated, it SHOULD disconnect rather than proceed — don't send
|
|
477
|
+
// notifications/initialized or tools/list against a protocol version
|
|
478
|
+
// this client can't actually speak.
|
|
479
|
+
return failed(config, 'failed', 'handshake', `protocol version mismatch: requested ${requestedVersion}, server negotiated ` +
|
|
480
|
+
`${negotiatedVersion}, which this client does not support. ` +
|
|
481
|
+
`This client supports: ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')}.`, undefined, { protocolVersion, serverInfo, latencyMs: Date.now() - started });
|
|
482
|
+
}
|
|
391
483
|
try {
|
|
392
484
|
await withTimeout(transport.notify('notifications/initialized'), timeoutMs, 'initialized notification');
|
|
393
485
|
}
|
|
394
486
|
catch (error) {
|
|
395
|
-
return failed(config, messageOf(error).includes('timed out') ? 'timeout' : 'failed', 'capability-negotiation', messageOf(error), error);
|
|
487
|
+
return failed(config, messageOf(error).includes('timed out') ? 'timeout' : 'failed', 'capability-negotiation', messageOf(error), error, { protocolVersion, serverInfo });
|
|
396
488
|
}
|
|
489
|
+
let tools;
|
|
397
490
|
try {
|
|
398
|
-
|
|
399
|
-
return {
|
|
400
|
-
server: config,
|
|
401
|
-
status: 'connected',
|
|
402
|
-
capabilities: initialize.capabilities,
|
|
403
|
-
tools,
|
|
404
|
-
latencyMs: Date.now() - started,
|
|
405
|
-
};
|
|
491
|
+
tools = normalizeTools(validateResponse(await withTimeout(transport.request('tools/list'), timeoutMs, 'tools/list'), nextExpectedId++));
|
|
406
492
|
}
|
|
407
493
|
catch (error) {
|
|
408
|
-
return failed(config, messageOf(error).includes('timed out') ? 'timeout' : 'failed', 'list-tools', messageOf(error), error);
|
|
494
|
+
return failed(config, messageOf(error).includes('timed out') ? 'timeout' : 'failed', 'list-tools', messageOf(error), error, { protocolVersion, serverInfo });
|
|
495
|
+
}
|
|
496
|
+
// Resources and prompts are optional MCP capabilities: only inspect them
|
|
497
|
+
// if the server actually declared support in its initialize response.
|
|
498
|
+
// Passive enumeration only (resources/list, prompts/list) — never
|
|
499
|
+
// resources/read or prompts/get, which would be real invocation.
|
|
500
|
+
// A failure here is never fatal to the connection: tools is the one
|
|
501
|
+
// capability mcp-medic requires, so a broken resources/prompts listing
|
|
502
|
+
// is reported alongside a still-successful connection.
|
|
503
|
+
let resources;
|
|
504
|
+
let prompts;
|
|
505
|
+
const capabilityErrors = {};
|
|
506
|
+
if (capabilities.resources && typeof capabilities.resources === 'object') {
|
|
507
|
+
try {
|
|
508
|
+
resources = normalizeResources(validateResponse(await withTimeout(transport.request('resources/list'), timeoutMs, 'resources/list'), nextExpectedId++));
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
capabilityErrors.resources = messageOf(error);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (capabilities.prompts && typeof capabilities.prompts === 'object') {
|
|
515
|
+
try {
|
|
516
|
+
prompts = normalizePrompts(validateResponse(await withTimeout(transport.request('prompts/list'), timeoutMs, 'prompts/list'), nextExpectedId++));
|
|
517
|
+
}
|
|
518
|
+
catch (error) {
|
|
519
|
+
capabilityErrors.prompts = messageOf(error);
|
|
520
|
+
}
|
|
409
521
|
}
|
|
522
|
+
return {
|
|
523
|
+
server: config,
|
|
524
|
+
status: 'connected',
|
|
525
|
+
capabilities,
|
|
526
|
+
tools,
|
|
527
|
+
...(resources !== undefined ? { resources } : {}),
|
|
528
|
+
...(prompts !== undefined ? { prompts } : {}),
|
|
529
|
+
...(Object.keys(capabilityErrors).length > 0 ? { capabilityErrors } : {}),
|
|
530
|
+
protocolVersion,
|
|
531
|
+
serverInfo,
|
|
532
|
+
latencyMs: Date.now() - started,
|
|
533
|
+
};
|
|
410
534
|
}
|
|
411
535
|
catch (error) {
|
|
412
536
|
return failed(config, 'failed', 'handshake', messageOf(error), error);
|
package/dist/protocol/index.d.ts
CHANGED
package/dist/protocol/index.js
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP protocol version support, centralized so it isn't scattered across
|
|
3
|
+
* the transport implementation.
|
|
4
|
+
*
|
|
5
|
+
* Source of truth: the official MCP specification's versioned schema
|
|
6
|
+
* directories, https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema
|
|
7
|
+
* (checked directly rather than assumed — do not add a version here
|
|
8
|
+
* without confirming it against that list).
|
|
9
|
+
*
|
|
10
|
+
* `SUPPORTED_PROTOCOL_VERSIONS` lists every version whose wire shape this
|
|
11
|
+
* client's transport (an `initialize` request, a `notifications/initialized`
|
|
12
|
+
* notification, then `tools/list`) can correctly speak. `2024-11-05`
|
|
13
|
+
* through `2025-11-25` are additive on top of that same handshake model —
|
|
14
|
+
* this client doesn't implement every feature each of them adds (OAuth
|
|
15
|
+
* flows, icons, elicitation, tasks, ...), but it doesn't need to: it only
|
|
16
|
+
* ever sends `initialize`/`notifications/initialized`/`tools/list`, and a
|
|
17
|
+
* spec-compliant server response to those three calls has the same shape
|
|
18
|
+
* across all four versions (any additive fields in between are safely
|
|
19
|
+
* ignored by a client that never reads them).
|
|
20
|
+
*/
|
|
21
|
+
export declare const SUPPORTED_PROTOCOL_VERSIONS: readonly ["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"];
|
|
22
|
+
export type SupportedProtocolVersion = (typeof SUPPORTED_PROTOCOL_VERSIONS)[number];
|
|
23
|
+
/** The version requested when the caller asks for "auto" (or specifies nothing). */
|
|
24
|
+
export declare const LATEST_SUPPORTED_PROTOCOL_VERSION: SupportedProtocolVersion;
|
|
25
|
+
/**
|
|
26
|
+
* Real MCP protocol versions that exist but use a wire shape this client
|
|
27
|
+
* does not implement, keyed by version string, valued by why. Requesting
|
|
28
|
+
* one of these fails fast with an explicit reason instead of attempting
|
|
29
|
+
* (and inevitably failing) an `initialize` call the server was never going
|
|
30
|
+
* to understand the way this client sends it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const KNOWN_UNSUPPORTED_PROTOCOL_VERSIONS: Readonly<Record<string, string>>;
|
|
33
|
+
export declare function isSupportedProtocolVersion(version: string): version is SupportedProtocolVersion;
|
|
34
|
+
/** Resolves a user-facing `--protocol-version` value ("auto" or a literal version) to the version to request. */
|
|
35
|
+
export declare function resolveRequestedProtocolVersion(preference: string | undefined): string;
|
|
36
|
+
export interface ProtocolVersionNegotiation {
|
|
37
|
+
/** The protocolVersion this client sent in `initialize`. */
|
|
38
|
+
requested: string;
|
|
39
|
+
/** The protocolVersion the server returned in its `initialize` response, if it sent a valid one. */
|
|
40
|
+
negotiated?: string;
|
|
41
|
+
/** Whether `negotiated` is a version this client's transport can actually speak. */
|
|
42
|
+
compatible: boolean;
|
|
43
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP protocol version support, centralized so it isn't scattered across
|
|
3
|
+
* the transport implementation.
|
|
4
|
+
*
|
|
5
|
+
* Source of truth: the official MCP specification's versioned schema
|
|
6
|
+
* directories, https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema
|
|
7
|
+
* (checked directly rather than assumed — do not add a version here
|
|
8
|
+
* without confirming it against that list).
|
|
9
|
+
*
|
|
10
|
+
* `SUPPORTED_PROTOCOL_VERSIONS` lists every version whose wire shape this
|
|
11
|
+
* client's transport (an `initialize` request, a `notifications/initialized`
|
|
12
|
+
* notification, then `tools/list`) can correctly speak. `2024-11-05`
|
|
13
|
+
* through `2025-11-25` are additive on top of that same handshake model —
|
|
14
|
+
* this client doesn't implement every feature each of them adds (OAuth
|
|
15
|
+
* flows, icons, elicitation, tasks, ...), but it doesn't need to: it only
|
|
16
|
+
* ever sends `initialize`/`notifications/initialized`/`tools/list`, and a
|
|
17
|
+
* spec-compliant server response to those three calls has the same shape
|
|
18
|
+
* across all four versions (any additive fields in between are safely
|
|
19
|
+
* ignored by a client that never reads them).
|
|
20
|
+
*/
|
|
21
|
+
export const SUPPORTED_PROTOCOL_VERSIONS = [
|
|
22
|
+
'2025-11-25',
|
|
23
|
+
'2025-06-18',
|
|
24
|
+
'2025-03-26',
|
|
25
|
+
'2024-11-05',
|
|
26
|
+
];
|
|
27
|
+
/** The version requested when the caller asks for "auto" (or specifies nothing). */
|
|
28
|
+
export const LATEST_SUPPORTED_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
|
|
29
|
+
/**
|
|
30
|
+
* Real MCP protocol versions that exist but use a wire shape this client
|
|
31
|
+
* does not implement, keyed by version string, valued by why. Requesting
|
|
32
|
+
* one of these fails fast with an explicit reason instead of attempting
|
|
33
|
+
* (and inevitably failing) an `initialize` call the server was never going
|
|
34
|
+
* to understand the way this client sends it.
|
|
35
|
+
*/
|
|
36
|
+
export const KNOWN_UNSUPPORTED_PROTOCOL_VERSIONS = {
|
|
37
|
+
'2026-07-28': "removes the initialize/notifications-initialized handshake entirely in favor of a stateless " +
|
|
38
|
+
"per-request model (a server/discover RPC, protocol version and capabilities carried per-request " +
|
|
39
|
+
"in _meta fields) — a different wire protocol this client doesn't implement yet",
|
|
40
|
+
};
|
|
41
|
+
export function isSupportedProtocolVersion(version) {
|
|
42
|
+
return SUPPORTED_PROTOCOL_VERSIONS.includes(version);
|
|
43
|
+
}
|
|
44
|
+
/** Resolves a user-facing `--protocol-version` value ("auto" or a literal version) to the version to request. */
|
|
45
|
+
export function resolveRequestedProtocolVersion(preference) {
|
|
46
|
+
const trimmed = preference?.trim();
|
|
47
|
+
if (!trimmed || trimmed === 'auto') {
|
|
48
|
+
return LATEST_SUPPORTED_PROTOCOL_VERSION;
|
|
49
|
+
}
|
|
50
|
+
return trimmed;
|
|
51
|
+
}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret redaction for anything that ends up in a report, diff, or log line.
|
|
3
|
+
*
|
|
4
|
+
* mcp-medic's `MCPServerConfig` can carry live credentials — HTTP headers
|
|
5
|
+
* (`Authorization: Bearer ...`), stdio process env vars (`API_KEY=...`),
|
|
6
|
+
* and OAuth `tokenRefreshBody` fields (`client_secret`, ...). None of that
|
|
7
|
+
* should ever reach `--json`/`--export-json` output, `diff` output, or
|
|
8
|
+
* `--verbose` logs verbatim — those are routinely pasted into CI logs,
|
|
9
|
+
* issue trackers, and Slack.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isSecretKey(key: string): boolean;
|
|
12
|
+
/** Redacts values of secret-looking keys in a flat string-keyed record.
|
|
13
|
+
* Non-secret keys (e.g. `Content-Type`, `NODE_ENV`) are left untouched so
|
|
14
|
+
* the output stays useful for debugging. */
|
|
15
|
+
export declare function redactRecord(record: Record<string, string> | undefined): Record<string, string> | undefined;
|
|
16
|
+
/** Redacts secret-looking keys anywhere in an arbitrary JSON-like value
|
|
17
|
+
* (objects/arrays nested to any depth). Used for loosely-typed structures
|
|
18
|
+
* such as `tokenRefreshBody`. */
|
|
19
|
+
export declare function redactDeep(value: unknown): unknown;
|
|
20
|
+
/** Shape-preserving subset of `MCPServerConfig`'s secret-carrying fields —
|
|
21
|
+
* kept local (rather than importing the real type) so this module has no
|
|
22
|
+
* dependency on `types.ts` and can't accidentally widen what it touches. */
|
|
23
|
+
interface SanitizableServerConfig {
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
env?: Record<string, string>;
|
|
26
|
+
tokenRefreshBody?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
/** Returns a shallow copy of a server config with `headers`, `env`, and
|
|
29
|
+
* `tokenRefreshBody` secret-looking values redacted. Safe to call before
|
|
30
|
+
* a connection result is stored in a report or diff — no built-in check
|
|
31
|
+
* reads these fields' values (only `name`/`transport`/`url`), so redacting
|
|
32
|
+
* them here doesn't change diagnostic behavior. */
|
|
33
|
+
export declare function sanitizeServerConfig<T extends SanitizableServerConfig>(server: T): T;
|
|
34
|
+
export {};
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret redaction for anything that ends up in a report, diff, or log line.
|
|
3
|
+
*
|
|
4
|
+
* mcp-medic's `MCPServerConfig` can carry live credentials — HTTP headers
|
|
5
|
+
* (`Authorization: Bearer ...`), stdio process env vars (`API_KEY=...`),
|
|
6
|
+
* and OAuth `tokenRefreshBody` fields (`client_secret`, ...). None of that
|
|
7
|
+
* should ever reach `--json`/`--export-json` output, `diff` output, or
|
|
8
|
+
* `--verbose` logs verbatim — those are routinely pasted into CI logs,
|
|
9
|
+
* issue trackers, and Slack.
|
|
10
|
+
*/
|
|
11
|
+
const REDACTED = '[REDACTED]';
|
|
12
|
+
/** Matches key names that conventionally carry secret values. Intentionally
|
|
13
|
+
* broad — false positives (redacting a harmless key) are safe; false
|
|
14
|
+
* negatives (leaking a secret) are not. */
|
|
15
|
+
const SECRET_KEY_PATTERN = /(authorization|auth|token|secret|password|passwd|pwd|api[-_]?key|apikey|cookie|credential|bearer|session|private[-_]?key|client[-_]?secret)/i;
|
|
16
|
+
export function isSecretKey(key) {
|
|
17
|
+
return SECRET_KEY_PATTERN.test(key);
|
|
18
|
+
}
|
|
19
|
+
/** Redacts values of secret-looking keys in a flat string-keyed record.
|
|
20
|
+
* Non-secret keys (e.g. `Content-Type`, `NODE_ENV`) are left untouched so
|
|
21
|
+
* the output stays useful for debugging. */
|
|
22
|
+
export function redactRecord(record) {
|
|
23
|
+
if (!record)
|
|
24
|
+
return record;
|
|
25
|
+
const result = {};
|
|
26
|
+
for (const [key, value] of Object.entries(record)) {
|
|
27
|
+
result[key] = isSecretKey(key) ? REDACTED : value;
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
/** Redacts secret-looking keys anywhere in an arbitrary JSON-like value
|
|
32
|
+
* (objects/arrays nested to any depth). Used for loosely-typed structures
|
|
33
|
+
* such as `tokenRefreshBody`. */
|
|
34
|
+
export function redactDeep(value) {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
return value.map(redactDeep);
|
|
37
|
+
}
|
|
38
|
+
if (value && typeof value === 'object') {
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const [key, v] of Object.entries(value)) {
|
|
41
|
+
result[key] = isSecretKey(key) ? REDACTED : redactDeep(v);
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
/** Returns a shallow copy of a server config with `headers`, `env`, and
|
|
48
|
+
* `tokenRefreshBody` secret-looking values redacted. Safe to call before
|
|
49
|
+
* a connection result is stored in a report or diff — no built-in check
|
|
50
|
+
* reads these fields' values (only `name`/`transport`/`url`), so redacting
|
|
51
|
+
* them here doesn't change diagnostic behavior. */
|
|
52
|
+
export function sanitizeServerConfig(server) {
|
|
53
|
+
return {
|
|
54
|
+
...server,
|
|
55
|
+
...(server.headers !== undefined ? { headers: redactRecord(server.headers) } : {}),
|
|
56
|
+
...(server.env !== undefined ? { env: redactRecord(server.env) } : {}),
|
|
57
|
+
...(server.tokenRefreshBody !== undefined
|
|
58
|
+
? { tokenRefreshBody: redactDeep(server.tokenRefreshBody) }
|
|
59
|
+
: {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/report.js
CHANGED
|
@@ -8,6 +8,25 @@ export function formatReportHuman(report, options = {}) {
|
|
|
8
8
|
for (const conn of report.connections) {
|
|
9
9
|
const status = conn.status === 'connected' ? 'OK' : conn.status.toUpperCase();
|
|
10
10
|
lines.push(`[${status}] ${conn.server.name} (${conn.server.transport})`);
|
|
11
|
+
if (conn.protocolVersion) {
|
|
12
|
+
const { requested, negotiated, compatible } = conn.protocolVersion;
|
|
13
|
+
const statusText = compatible ? '✓ compatible' : '✗ incompatible';
|
|
14
|
+
lines.push(` Protocol: requested ${requested}, server negotiated ${negotiated ?? '(none)'} — ${statusText}`);
|
|
15
|
+
}
|
|
16
|
+
if (conn.tools) {
|
|
17
|
+
const parts = [`${conn.tools.length} tool(s)`];
|
|
18
|
+
if (conn.resources)
|
|
19
|
+
parts.push(`${conn.resources.length} resource(s)`);
|
|
20
|
+
if (conn.prompts)
|
|
21
|
+
parts.push(`${conn.prompts.length} prompt(s)`);
|
|
22
|
+
lines.push(` Capabilities: ${parts.join(', ')}`);
|
|
23
|
+
}
|
|
24
|
+
if (conn.capabilityErrors?.resources) {
|
|
25
|
+
lines.push(` resources/list: ${conn.capabilityErrors.resources}`);
|
|
26
|
+
}
|
|
27
|
+
if (conn.capabilityErrors?.prompts) {
|
|
28
|
+
lines.push(` prompts/list: ${conn.capabilityErrors.prompts}`);
|
|
29
|
+
}
|
|
11
30
|
if (conn.error) {
|
|
12
31
|
lines.push(` ${conn.error.stage}: ${conn.error.message}`);
|
|
13
32
|
}
|
package/dist/sarif.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RunReport } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Formats a RunReport as SARIF 2.1.0 (https://sarifweb.azurewebsites.net/),
|
|
4
|
+
* consumable by GitHub Code Scanning (`upload-sarif`) and other SARIF
|
|
5
|
+
* viewers. mcp-medic's diagnostics have no source line/column — they're
|
|
6
|
+
* about a *running server's* declared tools/capabilities, not source code —
|
|
7
|
+
* so each result's physicalLocation points at the config file the server
|
|
8
|
+
* was declared in, with the server (and tool, if any) named as a logical
|
|
9
|
+
* location instead of a line range.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatReportSarif(report: RunReport): string;
|
package/dist/sarif.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
function readOwnVersion() {
|
|
4
|
+
try {
|
|
5
|
+
const pkgPath = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
6
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
7
|
+
return pkg.version ?? '0.0.0';
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return '0.0.0';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function sarifLevel(severity) {
|
|
14
|
+
if (severity === 'error')
|
|
15
|
+
return 'error';
|
|
16
|
+
if (severity === 'warning')
|
|
17
|
+
return 'warning';
|
|
18
|
+
return 'note';
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Formats a RunReport as SARIF 2.1.0 (https://sarifweb.azurewebsites.net/),
|
|
22
|
+
* consumable by GitHub Code Scanning (`upload-sarif`) and other SARIF
|
|
23
|
+
* viewers. mcp-medic's diagnostics have no source line/column — they're
|
|
24
|
+
* about a *running server's* declared tools/capabilities, not source code —
|
|
25
|
+
* so each result's physicalLocation points at the config file the server
|
|
26
|
+
* was declared in, with the server (and tool, if any) named as a logical
|
|
27
|
+
* location instead of a line range.
|
|
28
|
+
*/
|
|
29
|
+
export function formatReportSarif(report) {
|
|
30
|
+
const artifactUri = report.configSource || 'mcp-config.json';
|
|
31
|
+
const rules = new Map();
|
|
32
|
+
const results = [];
|
|
33
|
+
for (const d of report.diagnostics) {
|
|
34
|
+
if (!rules.has(d.checkId)) {
|
|
35
|
+
rules.set(d.checkId, {
|
|
36
|
+
id: d.checkId,
|
|
37
|
+
shortDescription: { text: d.checkId },
|
|
38
|
+
defaultConfiguration: { level: sarifLevel(d.severity) },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const logicalLocations = [{ name: d.serverName, kind: 'module' }];
|
|
42
|
+
if (d.toolName) {
|
|
43
|
+
logicalLocations.push({ name: d.toolName, kind: 'member' });
|
|
44
|
+
}
|
|
45
|
+
results.push({
|
|
46
|
+
ruleId: d.checkId,
|
|
47
|
+
level: sarifLevel(d.severity),
|
|
48
|
+
message: {
|
|
49
|
+
text: d.suggestedFix?.description ? `${d.message} Suggested fix: ${d.suggestedFix.description}` : d.message,
|
|
50
|
+
},
|
|
51
|
+
locations: [
|
|
52
|
+
{
|
|
53
|
+
physicalLocation: { artifactLocation: { uri: artifactUri } },
|
|
54
|
+
logicalLocations,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const sarif = {
|
|
60
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
61
|
+
version: '2.1.0',
|
|
62
|
+
runs: [
|
|
63
|
+
{
|
|
64
|
+
tool: {
|
|
65
|
+
driver: {
|
|
66
|
+
name: 'mcp-medic',
|
|
67
|
+
informationUri: 'https://github.com/shivam039/mcp-doctor',
|
|
68
|
+
version: readOwnVersion(),
|
|
69
|
+
rules: [...rules.values()],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
results,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
return JSON.stringify(sarif, null, 2);
|
|
77
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -19,11 +19,47 @@ export interface MCPToolDefinition {
|
|
|
19
19
|
description?: string;
|
|
20
20
|
inputSchema: unknown;
|
|
21
21
|
}
|
|
22
|
+
export interface MCPResourceDefinition {
|
|
23
|
+
uri: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
mimeType?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface MCPPromptDefinition {
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
arguments?: unknown[];
|
|
32
|
+
}
|
|
33
|
+
export interface ProtocolVersionInfo {
|
|
34
|
+
/** The protocolVersion this client sent in `initialize`. */
|
|
35
|
+
requested: string;
|
|
36
|
+
/** The protocolVersion the server returned in its `initialize` response, if valid. */
|
|
37
|
+
negotiated?: string;
|
|
38
|
+
/** Whether `negotiated` is a version this client's transport can actually speak. */
|
|
39
|
+
compatible: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface MCPServerInfo {
|
|
42
|
+
name?: string;
|
|
43
|
+
version?: string;
|
|
44
|
+
}
|
|
22
45
|
export interface MCPConnection {
|
|
23
46
|
server: MCPServerConfig;
|
|
24
47
|
status: 'connected' | 'failed' | 'timeout';
|
|
25
48
|
capabilities?: Record<string, unknown>;
|
|
26
49
|
tools?: MCPToolDefinition[];
|
|
50
|
+
/** Populated only if the server's `initialize` response declared a `resources` capability. */
|
|
51
|
+
resources?: MCPResourceDefinition[];
|
|
52
|
+
/** Populated only if the server's `initialize` response declared a `prompts` capability. */
|
|
53
|
+
prompts?: MCPPromptDefinition[];
|
|
54
|
+
/** Best-effort failures from optional capability inspection (resources/prompts) — these
|
|
55
|
+
* never fail the overall connection, since `tools` is the one capability mcp-medic requires. */
|
|
56
|
+
capabilityErrors?: {
|
|
57
|
+
resources?: string;
|
|
58
|
+
prompts?: string;
|
|
59
|
+
};
|
|
60
|
+
/** Set once an `initialize` response was received, even if negotiation was incompatible or a later stage failed. */
|
|
61
|
+
protocolVersion?: ProtocolVersionInfo;
|
|
62
|
+
serverInfo?: MCPServerInfo;
|
|
27
63
|
error?: {
|
|
28
64
|
stage: 'spawn' | 'handshake' | 'capability-negotiation' | 'list-tools';
|
|
29
65
|
message: string;
|
|
@@ -55,6 +91,8 @@ export interface RunOptions {
|
|
|
55
91
|
checks?: Check[];
|
|
56
92
|
verbose?: boolean;
|
|
57
93
|
onLog?: (message: string) => void;
|
|
94
|
+
/** "auto" (default) requests the newest protocol version this client supports; an explicit version string requests that version instead. */
|
|
95
|
+
protocolVersion?: string;
|
|
58
96
|
}
|
|
59
97
|
export interface RunReport {
|
|
60
98
|
configSource?: string;
|