mcp-medic 1.2.5 → 1.2.7

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 CHANGED
@@ -13,6 +13,7 @@ export interface ParsedArgs {
13
13
  updateSnapshotPath?: string;
14
14
  json: boolean;
15
15
  timeoutMs?: number;
16
+ jobs?: number;
16
17
  showFixes: boolean;
17
18
  verbose: boolean;
18
19
  failOn: 'error' | 'warning';
package/dist/cli.js CHANGED
@@ -113,6 +113,14 @@ export function parseArgs(argv) {
113
113
  }
114
114
  args.protocolVersion = val;
115
115
  }
116
+ else if (arg === '--jobs') {
117
+ const value = argv[++i];
118
+ const parsed = value ? Number(value) : NaN;
119
+ if (!Number.isSafeInteger(parsed) || parsed < 1) {
120
+ throw new Error(`--jobs requires a positive safe integer, got: ${value ?? '(none)'}`);
121
+ }
122
+ args.jobs = parsed;
123
+ }
116
124
  else {
117
125
  positional.push(arg);
118
126
  }
@@ -213,6 +221,7 @@ OPTIONS
213
221
  --timeout <ms> Per-server handshake timeout in milliseconds (default: 5000)
214
222
  --protocol-version <v> MCP protocolVersion to request: "auto" (default, latest supported)
215
223
  or an explicit version, e.g. ${SUPPORTED_PROTOCOL_VERSIONS[SUPPORTED_PROTOCOL_VERSIONS.length - 1]}
224
+ --jobs <n> Maximum config files processed at once in check-all (default: 1)
216
225
  --help, -h Show help
217
226
  --version, -V Print the installed mcp-medic version
218
227
 
@@ -568,6 +577,7 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
568
577
  const fleetReport = await runFleetChecks(glob, {
569
578
  checks,
570
579
  timeoutMs: args.timeoutMs,
580
+ jobs: args.jobs,
571
581
  verbose: args.verbose,
572
582
  protocolVersion: args.protocolVersion,
573
583
  });
package/dist/fleet.d.ts CHANGED
@@ -37,6 +37,7 @@ export declare function findConfigFiles(globOrPattern: string, rootDir?: string)
37
37
  */
38
38
  export declare function runFleetChecks(globPattern: string, options?: RunOptions & {
39
39
  cwd?: string;
40
+ jobs?: number;
40
41
  }): Promise<FleetReport>;
41
42
  /**
42
43
  * Compares two MCP configurations to identify server and attribute drift.
package/dist/fleet.js CHANGED
@@ -1,23 +1,46 @@
1
- import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { readdirSync, lstatSync, readFileSync, existsSync } from 'node:fs';
2
+ import { resolve, 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
6
  import { redactRecord } from './redact.js';
7
- function findFilesMatching(dir, pattern, results = []) {
7
+ async function mapWithConcurrency(items, concurrency, worker) {
8
+ const results = new Array(items.length);
9
+ let nextIndex = 0;
10
+ const workerCount = Math.min(concurrency, items.length);
11
+ await Promise.all(Array.from({ length: workerCount }, async () => {
12
+ while (true) {
13
+ const index = nextIndex++;
14
+ if (index >= items.length)
15
+ return;
16
+ results[index] = await worker(items[index]);
17
+ }
18
+ }));
19
+ return results;
20
+ }
21
+ function findFilesMatching(dir, rootDir, pattern, results = []) {
8
22
  if (!existsSync(dir))
9
23
  return results;
10
- const entries = readdirSync(dir);
24
+ let entries;
25
+ try {
26
+ entries = readdirSync(dir).sort();
27
+ }
28
+ catch {
29
+ return results;
30
+ }
11
31
  for (const entry of entries) {
12
32
  if (entry === 'node_modules' || entry === '.git' || entry === 'dist')
13
33
  continue;
14
34
  const fullPath = join(dir, entry);
15
35
  try {
16
- const stat = statSync(fullPath);
36
+ const stat = lstatSync(fullPath);
17
37
  if (stat.isDirectory()) {
18
- findFilesMatching(fullPath, pattern, results);
38
+ findFilesMatching(fullPath, rootDir, pattern, results);
19
39
  }
20
- else if (stat.isFile() && pattern.test(fullPath)) {
40
+ else if (stat.isFile()) {
41
+ const relativePath = fullPath.slice(rootDir.length + 1).split('\\').join('/');
42
+ if (!pattern.test(relativePath))
43
+ continue;
21
44
  results.push(fullPath);
22
45
  }
23
46
  }
@@ -31,13 +54,31 @@ function findFilesMatching(dir, pattern, results = []) {
31
54
  * Discovers config files matching a glob or pattern.
32
55
  */
33
56
  export function findConfigFiles(globOrPattern, rootDir = process.cwd()) {
34
- // Simple glob converter
35
- const regexStr = globOrPattern
36
- .replace(/\./g, '\\.')
37
- .replace(/\*\*/g, '.*')
38
- .replace(/\*/g, '[^/]*');
39
- const regex = new RegExp(`${regexStr}$`);
40
- return findFilesMatching(rootDir, regex);
57
+ const root = resolve(rootDir);
58
+ const glob = globOrPattern.split('\\').join('/').replace(/^\.\//, '');
59
+ let regexSource = '^';
60
+ for (let i = 0; i < glob.length; i += 1) {
61
+ const char = glob[i];
62
+ if (char === '*' && glob[i + 1] === '*') {
63
+ i += 1;
64
+ if (glob[i + 1] === '/') {
65
+ // A leading or interior **/ can match zero or more path segments.
66
+ regexSource += '(?:.*/)?';
67
+ i += 1;
68
+ }
69
+ else {
70
+ regexSource += '.*';
71
+ }
72
+ }
73
+ else if (char === '*') {
74
+ regexSource += '[^/]*';
75
+ }
76
+ else {
77
+ regexSource += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
78
+ }
79
+ }
80
+ const regex = new RegExp(`${regexSource}$`);
81
+ return findFilesMatching(root, root, regex).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
41
82
  }
42
83
  /**
43
84
  * Runs validation checks across all matching configuration files.
@@ -45,38 +86,36 @@ export function findConfigFiles(globOrPattern, rootDir = process.cwd()) {
45
86
  export async function runFleetChecks(globPattern, options = {}) {
46
87
  const root = options.cwd ?? process.cwd();
47
88
  const filePaths = findConfigFiles(globPattern, root);
48
- const fileResults = [];
49
- let totalServers = 0;
50
- let totalErrors = 0;
51
- let totalWarnings = 0;
52
- for (const filePath of filePaths) {
89
+ const jobs = options.jobs ?? 1;
90
+ if (!Number.isSafeInteger(jobs) || jobs < 1) {
91
+ throw new TypeError(`jobs must be a positive safe integer, got: ${jobs}`);
92
+ }
93
+ const processed = await mapWithConcurrency(filePaths, jobs, async (filePath) => {
53
94
  try {
54
95
  const rawText = readFileSync(filePath, 'utf-8');
55
96
  const rawJson = JSON.parse(rawText);
56
97
  const { config, errors } = loadConfig(rawJson, filePath);
57
98
  if (errors.length > 0 || !config) {
58
- fileResults.push({
59
- filePath,
60
- error: `Config validation failed: ${errors.join(', ')}`,
61
- });
62
- totalErrors += errors.length;
63
- continue;
99
+ return {
100
+ fileResult: { filePath, error: `Config validation failed: ${errors.join(', ')}` },
101
+ errorCount: errors.length,
102
+ };
64
103
  }
65
104
  const checks = options.checks ?? allChecks;
66
105
  const report = await runChecks(config, { ...options, checks });
67
- fileResults.push({ filePath, report });
68
- totalServers += report.summary.servers;
69
- totalErrors += report.summary.errors;
70
- totalWarnings += report.summary.warnings;
106
+ return { fileResult: { filePath, report }, errorCount: report.summary.errors };
71
107
  }
72
108
  catch (err) {
73
- fileResults.push({
74
- filePath,
75
- error: `Could not process file: ${err instanceof Error ? err.message : String(err)}`,
76
- });
77
- totalErrors += 1;
109
+ return {
110
+ fileResult: { filePath, error: `Could not process file: ${err instanceof Error ? err.message : String(err)}` },
111
+ errorCount: 1,
112
+ };
78
113
  }
79
- }
114
+ });
115
+ const fileResults = processed.map((item) => item.fileResult);
116
+ const totalServers = fileResults.reduce((sum, result) => sum + (result.report?.summary.servers ?? 0), 0);
117
+ const totalErrors = processed.reduce((sum, item) => sum + item.errorCount, 0);
118
+ const totalWarnings = fileResults.reduce((sum, result) => sum + (result.report?.summary.warnings ?? 0), 0);
80
119
  return {
81
120
  totalFiles: filePaths.length,
82
121
  successfulFiles: fileResults.filter((f) => !f.error).length,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-medic",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Diagnose broken MCP (Model Context Protocol) server configs before they break your agent silently.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",