mcp-doctor 0.1.0 → 0.1.1
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 +24 -0
- package/dist/cli/commands/check.d.ts +1 -0
- package/dist/cli/commands/check.js +48 -20
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/index.js +17 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/utils/output.d.ts +13 -2
- package/dist/utils/output.js +54 -27
- package/dist/utils/output.js.map +1 -1
- package/dist/validators/json-syntax.d.ts +13 -3
- package/dist/validators/json-syntax.js +66 -35
- package/dist/validators/json-syntax.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/check.ts +74 -22
- package/src/cli/index.ts +17 -10
- package/src/utils/output.ts +110 -29
- package/src/validators/json-syntax.ts +90 -39
package/README.md
CHANGED
|
@@ -37,6 +37,14 @@ Found 2 config files:
|
|
|
37
37
|
|
|
38
38
|
~/.cursor/mcp.json:12
|
|
39
39
|
Trailing comma detected (not allowed in JSON)
|
|
40
|
+
|
|
41
|
+
10 │ "github": {
|
|
42
|
+
11 │ "command": "npx",
|
|
43
|
+
> 12 │ "args": ["-y", "@modelcontextprotocol/server-github"],
|
|
44
|
+
^
|
|
45
|
+
13 │ },
|
|
46
|
+
14 │ }
|
|
47
|
+
|
|
40
48
|
💡 Remove the comma before the closing bracket/brace
|
|
41
49
|
|
|
42
50
|
⚠️ 2 Warnings
|
|
@@ -92,6 +100,10 @@ mcp-doctor check
|
|
|
92
100
|
|
|
93
101
|
# Skip server health tests (faster)
|
|
94
102
|
mcp-doctor check --skip-health
|
|
103
|
+
|
|
104
|
+
# Validate a specific config file
|
|
105
|
+
mcp-doctor --file ./my-config.json
|
|
106
|
+
mcp-doctor --file ~/custom/location/config.json
|
|
95
107
|
```
|
|
96
108
|
|
|
97
109
|
## Installation
|
|
@@ -106,6 +118,18 @@ npm install -g mcp-doctor
|
|
|
106
118
|
npx mcp-doctor
|
|
107
119
|
```
|
|
108
120
|
|
|
121
|
+
## Config File Locations
|
|
122
|
+
|
|
123
|
+
| Client | Location |
|
|
124
|
+
| ------------------------ | ----------------------------------------------------------------- |
|
|
125
|
+
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
|
|
126
|
+
| Claude Desktop (Windows) | `%APPDATA%/Claude/claude_desktop_config.json` |
|
|
127
|
+
| Claude Desktop (Linux) | `~/.config/claude/claude_desktop_config.json` |
|
|
128
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
129
|
+
| VS Code | `~/Library/Application Support/Code/User/settings.json` |
|
|
130
|
+
| Claude Code | `~/.claude.json` or `./.mcp.json` |
|
|
131
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
|
|
132
|
+
|
|
109
133
|
## FAQ
|
|
110
134
|
|
|
111
135
|
**Q: It says "No MCP configuration files found"**
|
|
@@ -1,28 +1,56 @@
|
|
|
1
|
-
import ora from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { getConfigLocations } from "../../config/locations.js";
|
|
5
|
+
import { validateJsonSyntax, validatePaths, validateEnvVars, testServerHealth, } from "../../validators/index.js";
|
|
6
|
+
import { printHeader, printConfigsFound, printValidationResults, printServerResults, printSummary, } from "../../utils/output.js";
|
|
5
7
|
export async function checkCommand(options = {}) {
|
|
6
|
-
printHeader(
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
printHeader("🩺 MCP Doctor");
|
|
9
|
+
let foundConfigs;
|
|
10
|
+
// If --file is specified, use that instead of scanning
|
|
11
|
+
if (options.filePath) {
|
|
12
|
+
const resolvedPath = resolve(options.filePath);
|
|
13
|
+
if (!existsSync(resolvedPath)) {
|
|
14
|
+
console.log(`\n❌ File not found: ${resolvedPath}\n`);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
foundConfigs = [
|
|
19
|
+
{
|
|
20
|
+
client: "Custom",
|
|
21
|
+
scope: "global",
|
|
22
|
+
path: resolvedPath,
|
|
23
|
+
exists: true,
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
console.log(`Validating: ${resolvedPath}\n`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const spinner = ora("Scanning for MCP configurations...").start();
|
|
30
|
+
// Find all config files
|
|
31
|
+
const locations = getConfigLocations();
|
|
32
|
+
foundConfigs = locations.filter((l) => l.exists);
|
|
33
|
+
spinner.stop();
|
|
34
|
+
// Show what we found
|
|
35
|
+
printConfigsFound(locations);
|
|
36
|
+
if (foundConfigs.length === 0) {
|
|
37
|
+
process.exitCode = 0;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
17
40
|
}
|
|
18
41
|
// Run validators on each config
|
|
19
42
|
const allResults = [];
|
|
20
43
|
const allServerResults = [];
|
|
21
44
|
const configsWithServers = [];
|
|
45
|
+
let firstErrorContext;
|
|
22
46
|
for (const configLoc of foundConfigs) {
|
|
23
47
|
// JSON syntax check
|
|
24
|
-
const { valid, results: syntaxResults, config } = validateJsonSyntax(configLoc.path);
|
|
48
|
+
const { valid, results: syntaxResults, config, errorContext, } = validateJsonSyntax(configLoc.path);
|
|
25
49
|
allResults.push(...syntaxResults);
|
|
50
|
+
// Capture first error context for display
|
|
51
|
+
if (errorContext && !firstErrorContext) {
|
|
52
|
+
firstErrorContext = errorContext;
|
|
53
|
+
}
|
|
26
54
|
// If JSON is invalid, skip other validators for this file
|
|
27
55
|
if (!valid || !config) {
|
|
28
56
|
continue;
|
|
@@ -35,10 +63,10 @@ export async function checkCommand(options = {}) {
|
|
|
35
63
|
allResults.push(...validateEnvVars(config, configLoc.path));
|
|
36
64
|
}
|
|
37
65
|
// Print validation results
|
|
38
|
-
printValidationResults(allResults);
|
|
66
|
+
printValidationResults(allResults, firstErrorContext);
|
|
39
67
|
// Server health tests (unless skipped)
|
|
40
68
|
if (!options.skipHealth && configsWithServers.length > 0) {
|
|
41
|
-
const healthSpinner = ora(
|
|
69
|
+
const healthSpinner = ora("Testing server connectivity...").start();
|
|
42
70
|
for (const { config } of configsWithServers) {
|
|
43
71
|
const results = await testServerHealth(config);
|
|
44
72
|
allServerResults.push(...results);
|
|
@@ -55,8 +83,8 @@ export async function checkCommand(options = {}) {
|
|
|
55
83
|
printServerResults(Array.from(uniqueServers.values()));
|
|
56
84
|
}
|
|
57
85
|
// Summary
|
|
58
|
-
const errors = allResults.filter((r) => r.level ===
|
|
59
|
-
const warnings = allResults.filter((r) => r.level ===
|
|
86
|
+
const errors = allResults.filter((r) => r.level === "error").length;
|
|
87
|
+
const warnings = allResults.filter((r) => r.level === "warning").length;
|
|
60
88
|
const healthyServers = allServerResults.filter((s) => s.healthy).length;
|
|
61
89
|
printSummary(errors, warnings, healthyServers, allServerResults.length);
|
|
62
90
|
// Set exit code
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../../src/cli/commands/check.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,KAAK,CAAC;AACtB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,eAAe,EACf,gBAAgB,GACjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,sBAAsB,EACtB,kBAAkB,EAClB,YAAY,GACb,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../../../src/cli/commands/check.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from
|
|
3
|
-
import { checkCommand } from
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { checkCommand } from "./commands/check.js";
|
|
4
4
|
const program = new Command();
|
|
5
5
|
program
|
|
6
|
-
.name(
|
|
7
|
-
.description(
|
|
8
|
-
.version(
|
|
6
|
+
.name("mcp-doctor")
|
|
7
|
+
.description("Diagnose and fix MCP configuration issues")
|
|
8
|
+
.version("0.1.1");
|
|
9
9
|
program
|
|
10
|
-
.command(
|
|
11
|
-
.description(
|
|
12
|
-
.option(
|
|
10
|
+
.command("check")
|
|
11
|
+
.description("Validate MCP configuration files")
|
|
12
|
+
.option("--skip-health", "Skip server health checks")
|
|
13
|
+
.option("--file <path>", "Validate a specific config file")
|
|
13
14
|
.action(async (options) => {
|
|
14
15
|
await checkCommand({
|
|
15
16
|
skipHealth: options.skipHealth,
|
|
17
|
+
filePath: options.file,
|
|
16
18
|
});
|
|
17
19
|
});
|
|
18
20
|
// Default command (no subcommand) runs check
|
|
19
21
|
program
|
|
20
|
-
.
|
|
21
|
-
|
|
22
|
+
.option("--skip-health", "Skip server health checks")
|
|
23
|
+
.option("--file <path>", "Validate a specific config file")
|
|
24
|
+
.action(async (options) => {
|
|
25
|
+
await checkCommand({
|
|
26
|
+
skipHealth: options.skipHealth,
|
|
27
|
+
filePath: options.file,
|
|
28
|
+
});
|
|
22
29
|
});
|
|
23
30
|
program.parse();
|
|
24
31
|
//# sourceMappingURL=index.js.map
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,YAAY,CAAC;KAClB,WAAW,CAAC,2CAA2C,CAAC;KACxD,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,YAAY,CAAC;KAClB,WAAW,CAAC,2CAA2C,CAAC;KACxD,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,kCAAkC,CAAC;KAC/C,MAAM,CAAC,eAAe,EAAE,2BAA2B,CAAC;KACpD,MAAM,CAAC,eAAe,EAAE,iCAAiC,CAAC;KAC1D,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,MAAM,YAAY,CAAC;QACjB,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,QAAQ,EAAE,OAAO,CAAC,IAAI;KACvB,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEL,6CAA6C;AAC7C,OAAO;KACJ,MAAM,CAAC,eAAe,EAAE,2BAA2B,CAAC;KACpD,MAAM,CAAC,eAAe,EAAE,iCAAiC,CAAC;KAC1D,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;IACxB,MAAM,YAAY,CAAC;QACjB,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,QAAQ,EAAE,OAAO,CAAC,IAAI;KACvB,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
package/dist/utils/output.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import { ValidationResult, ServerTestResult, ConfigLocation } from
|
|
1
|
+
import { ValidationResult, ServerTestResult, ConfigLocation } from "../config/types.js";
|
|
2
|
+
interface ErrorContext {
|
|
3
|
+
line: number;
|
|
4
|
+
column: number;
|
|
5
|
+
lineContent: string;
|
|
6
|
+
surroundingLines: Array<{
|
|
7
|
+
num: number;
|
|
8
|
+
content: string;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
2
11
|
export declare function printHeader(text: string): void;
|
|
3
12
|
export declare function printConfigsFound(configs: ConfigLocation[]): void;
|
|
4
|
-
export declare function
|
|
13
|
+
export declare function printCodeContext(errorContext: ErrorContext): void;
|
|
14
|
+
export declare function printValidationResults(results: ValidationResult[], errorContext?: ErrorContext): void;
|
|
5
15
|
export declare function printServerResults(results: ServerTestResult[]): void;
|
|
6
16
|
export declare function printSummary(errors: number, warnings: number, healthyServers: number, totalServers: number): void;
|
|
17
|
+
export {};
|
package/dist/utils/output.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import chalk from
|
|
1
|
+
import chalk from "chalk";
|
|
2
2
|
export function printHeader(text) {
|
|
3
3
|
console.log(chalk.bold(`\n${text}\n`));
|
|
4
4
|
}
|
|
@@ -6,8 +6,8 @@ export function printConfigsFound(configs) {
|
|
|
6
6
|
const found = configs.filter((c) => c.exists);
|
|
7
7
|
const notFound = configs.filter((c) => !c.exists);
|
|
8
8
|
if (found.length === 0) {
|
|
9
|
-
console.log(chalk.yellow(
|
|
10
|
-
console.log(chalk.gray(
|
|
9
|
+
console.log(chalk.yellow("No MCP configuration files found.\n"));
|
|
10
|
+
console.log(chalk.gray("Looked in:"));
|
|
11
11
|
notFound.slice(0, 5).forEach((c) => {
|
|
12
12
|
console.log(chalk.gray(` ${c.client}: ${c.path}`));
|
|
13
13
|
});
|
|
@@ -16,51 +16,78 @@ export function printConfigsFound(configs) {
|
|
|
16
16
|
}
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
console.log(chalk.green(`Found ${found.length} config file${found.length > 1 ?
|
|
19
|
+
console.log(chalk.green(`Found ${found.length} config file${found.length > 1 ? "s" : ""}:\n`));
|
|
20
20
|
found.forEach((c) => {
|
|
21
|
-
const scope = c.scope ===
|
|
21
|
+
const scope = c.scope === "project" ? chalk.gray(" (project)") : "";
|
|
22
22
|
console.log(` ${chalk.cyan(c.client)}${scope}`);
|
|
23
23
|
console.log(chalk.gray(` ${c.path}\n`));
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
|
-
export function
|
|
27
|
-
const
|
|
28
|
-
const
|
|
26
|
+
export function printCodeContext(errorContext) {
|
|
27
|
+
const { line: errorLine, column, surroundingLines } = errorContext;
|
|
28
|
+
const lineNumWidth = Math.max(...surroundingLines.map((l) => l.num.toString().length));
|
|
29
|
+
console.log("");
|
|
30
|
+
for (const { num, content } of surroundingLines) {
|
|
31
|
+
const lineNum = num.toString().padStart(lineNumWidth, " ");
|
|
32
|
+
const isErrorLine = num === errorLine;
|
|
33
|
+
if (isErrorLine) {
|
|
34
|
+
console.log(chalk.red(` > ${lineNum} │ ${content}`));
|
|
35
|
+
// Print the pointer
|
|
36
|
+
const pointerPadding = " ".repeat(4 + lineNumWidth + 3 + column - 1);
|
|
37
|
+
console.log(chalk.red(`${pointerPadding}^`));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.log(chalk.gray(` ${lineNum} │ ${content}`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
console.log("");
|
|
44
|
+
}
|
|
45
|
+
export function printValidationResults(results, errorContext) {
|
|
46
|
+
const errors = results.filter((r) => r.level === "error");
|
|
47
|
+
const warnings = results.filter((r) => r.level === "warning");
|
|
29
48
|
if (errors.length > 0) {
|
|
30
|
-
console.log(chalk.red(`❌ ${errors.length} Error${errors.length > 1 ?
|
|
31
|
-
errors.forEach((e) => {
|
|
32
|
-
const location = e.line ? `:${e.line}` :
|
|
49
|
+
console.log(chalk.red(`❌ ${errors.length} Error${errors.length > 1 ? "s" : ""}\n`));
|
|
50
|
+
errors.forEach((e, index) => {
|
|
51
|
+
const location = e.line ? `:${e.line}` : "";
|
|
33
52
|
console.log(chalk.red(` ${shortPath(e.file)}${location}`));
|
|
34
53
|
console.log(chalk.red(` ${e.message}`));
|
|
54
|
+
// Show code context for first JSON error
|
|
55
|
+
if (errorContext &&
|
|
56
|
+
index === 0 &&
|
|
57
|
+
(e.code === "JSON_TRAILING_COMMA" || e.code === "JSON_SYNTAX")) {
|
|
58
|
+
printCodeContext(errorContext);
|
|
59
|
+
}
|
|
35
60
|
if (e.suggestion) {
|
|
36
61
|
console.log(chalk.gray(` 💡 ${e.suggestion}`));
|
|
37
62
|
}
|
|
38
|
-
console.log(
|
|
63
|
+
console.log("");
|
|
39
64
|
});
|
|
40
65
|
}
|
|
41
66
|
if (warnings.length > 0) {
|
|
42
|
-
console.log(chalk.yellow(`⚠️ ${warnings.length} Warning${warnings.length > 1 ?
|
|
67
|
+
console.log(chalk.yellow(`⚠️ ${warnings.length} Warning${warnings.length > 1 ? "s" : ""}\n`));
|
|
43
68
|
warnings.forEach((w) => {
|
|
44
|
-
const location = w.line ? `:${w.line}` :
|
|
69
|
+
const location = w.line ? `:${w.line}` : "";
|
|
45
70
|
console.log(chalk.yellow(` ${shortPath(w.file)}${location}`));
|
|
46
71
|
console.log(chalk.yellow(` ${w.message}`));
|
|
47
72
|
if (w.suggestion) {
|
|
48
73
|
console.log(chalk.gray(` 💡 ${w.suggestion}`));
|
|
49
74
|
}
|
|
50
|
-
console.log(
|
|
75
|
+
console.log("");
|
|
51
76
|
});
|
|
52
77
|
}
|
|
53
78
|
}
|
|
54
79
|
export function printServerResults(results) {
|
|
55
80
|
if (results.length === 0) {
|
|
56
|
-
console.log(chalk.gray(
|
|
81
|
+
console.log(chalk.gray("No servers to test.\n"));
|
|
57
82
|
return;
|
|
58
83
|
}
|
|
59
84
|
const healthy = results.filter((s) => s.healthy);
|
|
60
85
|
const unhealthy = results.filter((s) => !s.healthy);
|
|
61
|
-
console.log(`🔌 Server Health: ${chalk.green(healthy.length)} healthy, ${unhealthy.length > 0
|
|
86
|
+
console.log(`🔌 Server Health: ${chalk.green(healthy.length)} healthy, ${unhealthy.length > 0
|
|
87
|
+
? chalk.red(unhealthy.length + " failed")
|
|
88
|
+
: chalk.gray("0 failed")}\n`);
|
|
62
89
|
healthy.forEach((s) => {
|
|
63
|
-
const time = s.responseTime ? chalk.gray(` (${s.responseTime}ms)`) :
|
|
90
|
+
const time = s.responseTime ? chalk.gray(` (${s.responseTime}ms)`) : "";
|
|
64
91
|
console.log(chalk.green(` ✓ ${s.name}${time}`));
|
|
65
92
|
});
|
|
66
93
|
unhealthy.forEach((s) => {
|
|
@@ -69,33 +96,33 @@ export function printServerResults(results) {
|
|
|
69
96
|
console.log(chalk.gray(` ${s.error}`));
|
|
70
97
|
}
|
|
71
98
|
});
|
|
72
|
-
console.log(
|
|
99
|
+
console.log("");
|
|
73
100
|
}
|
|
74
101
|
export function printSummary(errors, warnings, healthyServers, totalServers) {
|
|
75
|
-
console.log(chalk.gray(
|
|
102
|
+
console.log(chalk.gray("─".repeat(50)));
|
|
76
103
|
if (errors === 0 && warnings === 0) {
|
|
77
|
-
console.log(chalk.green(
|
|
104
|
+
console.log(chalk.green("\n✓ All configurations valid"));
|
|
78
105
|
}
|
|
79
106
|
else if (errors === 0) {
|
|
80
|
-
console.log(chalk.yellow(`\n⚠️ Configuration valid with ${warnings} warning${warnings > 1 ?
|
|
107
|
+
console.log(chalk.yellow(`\n⚠️ Configuration valid with ${warnings} warning${warnings > 1 ? "s" : ""}`));
|
|
81
108
|
}
|
|
82
109
|
else {
|
|
83
|
-
console.log(chalk.red(`\n❌ Found ${errors} error${errors > 1 ?
|
|
110
|
+
console.log(chalk.red(`\n❌ Found ${errors} error${errors > 1 ? "s" : ""} that need${errors === 1 ? "s" : ""} fixing`));
|
|
84
111
|
}
|
|
85
112
|
if (totalServers > 0) {
|
|
86
113
|
if (healthyServers === totalServers) {
|
|
87
|
-
console.log(chalk.green(`✓ All ${totalServers} server${totalServers > 1 ?
|
|
114
|
+
console.log(chalk.green(`✓ All ${totalServers} server${totalServers > 1 ? "s" : ""} responding`));
|
|
88
115
|
}
|
|
89
116
|
else {
|
|
90
117
|
console.log(chalk.yellow(`⚠️ ${healthyServers}/${totalServers} servers responding`));
|
|
91
118
|
}
|
|
92
119
|
}
|
|
93
|
-
console.log(
|
|
120
|
+
console.log("");
|
|
94
121
|
}
|
|
95
122
|
function shortPath(path) {
|
|
96
|
-
const home = process.env.HOME || process.env.USERPROFILE ||
|
|
123
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
97
124
|
if (home && path.startsWith(home)) {
|
|
98
|
-
return
|
|
125
|
+
return "~" + path.slice(home.length);
|
|
99
126
|
}
|
|
100
127
|
return path;
|
|
101
128
|
}
|
package/dist/utils/output.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/utils/output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/utils/output.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { ValidationResult } from
|
|
2
|
-
export
|
|
1
|
+
import { ValidationResult } from "../config/types.js";
|
|
2
|
+
export interface JsonSyntaxResult {
|
|
3
3
|
valid: boolean;
|
|
4
4
|
results: ValidationResult[];
|
|
5
5
|
config?: Record<string, unknown>;
|
|
6
|
-
|
|
6
|
+
errorContext?: {
|
|
7
|
+
line: number;
|
|
8
|
+
column: number;
|
|
9
|
+
lineContent: string;
|
|
10
|
+
surroundingLines: Array<{
|
|
11
|
+
num: number;
|
|
12
|
+
content: string;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export declare function validateJsonSyntax(filePath: string): JsonSyntaxResult;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { readFileSync } from
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
2
|
export function validateJsonSyntax(filePath) {
|
|
3
3
|
const results = [];
|
|
4
4
|
let content;
|
|
5
5
|
try {
|
|
6
|
-
content = readFileSync(filePath,
|
|
6
|
+
content = readFileSync(filePath, "utf-8");
|
|
7
7
|
}
|
|
8
8
|
catch (error) {
|
|
9
9
|
results.push({
|
|
10
|
-
level:
|
|
11
|
-
code:
|
|
12
|
-
message: `Cannot read file: ${error instanceof Error ? error.message :
|
|
10
|
+
level: "error",
|
|
11
|
+
code: "FILE_READ_ERROR",
|
|
12
|
+
message: `Cannot read file: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
13
13
|
file: filePath,
|
|
14
14
|
});
|
|
15
15
|
return { valid: false, results };
|
|
@@ -17,13 +17,14 @@ export function validateJsonSyntax(filePath) {
|
|
|
17
17
|
// Check for empty file
|
|
18
18
|
if (!content.trim()) {
|
|
19
19
|
results.push({
|
|
20
|
-
level:
|
|
21
|
-
code:
|
|
22
|
-
message:
|
|
20
|
+
level: "warning",
|
|
21
|
+
code: "EMPTY_FILE",
|
|
22
|
+
message: "Config file is empty",
|
|
23
23
|
file: filePath,
|
|
24
24
|
});
|
|
25
25
|
return { valid: true, results, config: {} };
|
|
26
26
|
}
|
|
27
|
+
const lines = content.split("\n");
|
|
27
28
|
// Try to parse JSON
|
|
28
29
|
try {
|
|
29
30
|
const config = JSON.parse(content);
|
|
@@ -31,45 +32,75 @@ export function validateJsonSyntax(filePath) {
|
|
|
31
32
|
}
|
|
32
33
|
catch (error) {
|
|
33
34
|
if (error instanceof SyntaxError) {
|
|
34
|
-
// Try to find the
|
|
35
|
-
const
|
|
36
|
-
let line;
|
|
37
|
-
let column;
|
|
38
|
-
if (
|
|
39
|
-
const position = parseInt(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
// Try to find the position from error message
|
|
36
|
+
const posMatch = error.message.match(/position\s+(\d+)/i);
|
|
37
|
+
let line = 1;
|
|
38
|
+
let column = 1;
|
|
39
|
+
if (posMatch) {
|
|
40
|
+
const position = parseInt(posMatch[1], 10);
|
|
41
|
+
// Convert position to line/column
|
|
42
|
+
let currentPos = 0;
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
if (currentPos + lines[i].length + 1 > position) {
|
|
45
|
+
line = i + 1;
|
|
46
|
+
column = position - currentPos + 1;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
currentPos += lines[i].length + 1; // +1 for newline
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Check for trailing comma specifically
|
|
53
|
+
const trailingCommaRegex = /,(\s*[\]\}])/g;
|
|
54
|
+
let trailingCommaMatch;
|
|
55
|
+
let trailingCommaLine = 0;
|
|
56
|
+
let trailingCommaColumn = 0;
|
|
57
|
+
let searchPos = 0;
|
|
58
|
+
while ((trailingCommaMatch = trailingCommaRegex.exec(content)) !== null) {
|
|
59
|
+
// Find line number of this match
|
|
60
|
+
const beforeMatch = content.substring(0, trailingCommaMatch.index);
|
|
61
|
+
const matchLines = beforeMatch.split("\n");
|
|
62
|
+
trailingCommaLine = matchLines.length;
|
|
63
|
+
trailingCommaColumn = matchLines[matchLines.length - 1].length + 1;
|
|
64
|
+
searchPos = trailingCommaMatch.index;
|
|
43
65
|
}
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
66
|
+
// Build surrounding lines context
|
|
67
|
+
const errorLine = trailingCommaLine > 0 ? trailingCommaLine : line;
|
|
68
|
+
const errorColumn = trailingCommaLine > 0 ? trailingCommaColumn : column;
|
|
69
|
+
const surroundingLines = [];
|
|
70
|
+
const startLine = Math.max(0, errorLine - 3);
|
|
71
|
+
const endLine = Math.min(lines.length - 1, errorLine + 1);
|
|
72
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
73
|
+
surroundingLines.push({
|
|
74
|
+
num: i + 1,
|
|
75
|
+
content: lines[i],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const errorContext = {
|
|
79
|
+
line: errorLine,
|
|
80
|
+
column: errorColumn,
|
|
81
|
+
lineContent: lines[errorLine - 1] || "",
|
|
82
|
+
surroundingLines,
|
|
83
|
+
};
|
|
84
|
+
if (trailingCommaLine > 0) {
|
|
50
85
|
results.push({
|
|
51
|
-
level:
|
|
52
|
-
code:
|
|
53
|
-
message:
|
|
86
|
+
level: "error",
|
|
87
|
+
code: "JSON_TRAILING_COMMA",
|
|
88
|
+
message: "Trailing comma detected (not allowed in JSON)",
|
|
54
89
|
file: filePath,
|
|
55
90
|
line: trailingCommaLine,
|
|
56
|
-
suggestion:
|
|
91
|
+
suggestion: "Remove the comma before the closing bracket/brace",
|
|
57
92
|
});
|
|
58
93
|
}
|
|
59
94
|
else {
|
|
60
|
-
// Generic JSON error
|
|
61
|
-
let message = error.message;
|
|
62
|
-
if (line && column) {
|
|
63
|
-
message = `Invalid JSON at line ${line}, column ${column}: ${error.message}`;
|
|
64
|
-
}
|
|
65
95
|
results.push({
|
|
66
|
-
level:
|
|
67
|
-
code:
|
|
68
|
-
message,
|
|
96
|
+
level: "error",
|
|
97
|
+
code: "JSON_SYNTAX",
|
|
98
|
+
message: error.message,
|
|
69
99
|
file: filePath,
|
|
70
100
|
line,
|
|
71
101
|
});
|
|
72
102
|
}
|
|
103
|
+
return { valid: false, results, errorContext };
|
|
73
104
|
}
|
|
74
105
|
return { valid: false, results };
|
|
75
106
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-syntax.js","sourceRoot":"","sources":["../../src/validators/json-syntax.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"json-syntax.js","sourceRoot":"","sources":["../../src/validators/json-syntax.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,53 +1,105 @@
|
|
|
1
|
-
import ora from
|
|
2
|
-
import {
|
|
1
|
+
import ora from "ora";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { getConfigLocations } from "../../config/locations.js";
|
|
3
5
|
import {
|
|
4
6
|
validateJsonSyntax,
|
|
5
7
|
validatePaths,
|
|
6
8
|
validateEnvVars,
|
|
7
9
|
testServerHealth,
|
|
8
|
-
} from
|
|
10
|
+
} from "../../validators/index.js";
|
|
9
11
|
import {
|
|
10
12
|
printHeader,
|
|
11
13
|
printConfigsFound,
|
|
12
14
|
printValidationResults,
|
|
13
15
|
printServerResults,
|
|
14
16
|
printSummary,
|
|
15
|
-
} from
|
|
16
|
-
import {
|
|
17
|
+
} from "../../utils/output.js";
|
|
18
|
+
import {
|
|
19
|
+
ValidationResult,
|
|
20
|
+
ServerTestResult,
|
|
21
|
+
ConfigLocation,
|
|
22
|
+
} from "../../config/types.js";
|
|
17
23
|
|
|
18
24
|
interface CheckOptions {
|
|
19
25
|
skipHealth?: boolean;
|
|
26
|
+
filePath?: string;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export async function checkCommand(options: CheckOptions = {}): Promise<void> {
|
|
23
|
-
printHeader(
|
|
30
|
+
printHeader("🩺 MCP Doctor");
|
|
31
|
+
|
|
32
|
+
let foundConfigs: ConfigLocation[];
|
|
33
|
+
|
|
34
|
+
// If --file is specified, use that instead of scanning
|
|
35
|
+
if (options.filePath) {
|
|
36
|
+
const resolvedPath = resolve(options.filePath);
|
|
37
|
+
|
|
38
|
+
if (!existsSync(resolvedPath)) {
|
|
39
|
+
console.log(`\n❌ File not found: ${resolvedPath}\n`);
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
foundConfigs = [
|
|
45
|
+
{
|
|
46
|
+
client: "Custom",
|
|
47
|
+
scope: "global",
|
|
48
|
+
path: resolvedPath,
|
|
49
|
+
exists: true,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
26
52
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
53
|
+
console.log(`Validating: ${resolvedPath}\n`);
|
|
54
|
+
} else {
|
|
55
|
+
const spinner = ora("Scanning for MCP configurations...").start();
|
|
30
56
|
|
|
31
|
-
|
|
57
|
+
// Find all config files
|
|
58
|
+
const locations = getConfigLocations();
|
|
59
|
+
foundConfigs = locations.filter((l) => l.exists);
|
|
32
60
|
|
|
33
|
-
|
|
34
|
-
printConfigsFound(locations);
|
|
61
|
+
spinner.stop();
|
|
35
62
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
// Show what we found
|
|
64
|
+
printConfigsFound(locations);
|
|
65
|
+
|
|
66
|
+
if (foundConfigs.length === 0) {
|
|
67
|
+
process.exitCode = 0;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
39
70
|
}
|
|
40
71
|
|
|
41
72
|
// Run validators on each config
|
|
42
73
|
const allResults: ValidationResult[] = [];
|
|
43
74
|
const allServerResults: ServerTestResult[] = [];
|
|
44
|
-
const configsWithServers: Array<{
|
|
75
|
+
const configsWithServers: Array<{
|
|
76
|
+
path: string;
|
|
77
|
+
config: Record<string, unknown>;
|
|
78
|
+
}> = [];
|
|
79
|
+
let firstErrorContext:
|
|
80
|
+
| {
|
|
81
|
+
line: number;
|
|
82
|
+
column: number;
|
|
83
|
+
lineContent: string;
|
|
84
|
+
surroundingLines: Array<{ num: number; content: string }>;
|
|
85
|
+
}
|
|
86
|
+
| undefined;
|
|
45
87
|
|
|
46
88
|
for (const configLoc of foundConfigs) {
|
|
47
89
|
// JSON syntax check
|
|
48
|
-
const {
|
|
90
|
+
const {
|
|
91
|
+
valid,
|
|
92
|
+
results: syntaxResults,
|
|
93
|
+
config,
|
|
94
|
+
errorContext,
|
|
95
|
+
} = validateJsonSyntax(configLoc.path);
|
|
49
96
|
allResults.push(...syntaxResults);
|
|
50
97
|
|
|
98
|
+
// Capture first error context for display
|
|
99
|
+
if (errorContext && !firstErrorContext) {
|
|
100
|
+
firstErrorContext = errorContext;
|
|
101
|
+
}
|
|
102
|
+
|
|
51
103
|
// If JSON is invalid, skip other validators for this file
|
|
52
104
|
if (!valid || !config) {
|
|
53
105
|
continue;
|
|
@@ -64,11 +116,11 @@ export async function checkCommand(options: CheckOptions = {}): Promise<void> {
|
|
|
64
116
|
}
|
|
65
117
|
|
|
66
118
|
// Print validation results
|
|
67
|
-
printValidationResults(allResults);
|
|
119
|
+
printValidationResults(allResults, firstErrorContext);
|
|
68
120
|
|
|
69
121
|
// Server health tests (unless skipped)
|
|
70
122
|
if (!options.skipHealth && configsWithServers.length > 0) {
|
|
71
|
-
const healthSpinner = ora(
|
|
123
|
+
const healthSpinner = ora("Testing server connectivity...").start();
|
|
72
124
|
|
|
73
125
|
for (const { config } of configsWithServers) {
|
|
74
126
|
const results = await testServerHealth(config);
|
|
@@ -90,8 +142,8 @@ export async function checkCommand(options: CheckOptions = {}): Promise<void> {
|
|
|
90
142
|
}
|
|
91
143
|
|
|
92
144
|
// Summary
|
|
93
|
-
const errors = allResults.filter((r) => r.level ===
|
|
94
|
-
const warnings = allResults.filter((r) => r.level ===
|
|
145
|
+
const errors = allResults.filter((r) => r.level === "error").length;
|
|
146
|
+
const warnings = allResults.filter((r) => r.level === "warning").length;
|
|
95
147
|
const healthyServers = allServerResults.filter((s) => s.healthy).length;
|
|
96
148
|
|
|
97
149
|
printSummary(errors, warnings, healthyServers, allServerResults.length);
|
package/src/cli/index.ts
CHANGED
|
@@ -1,29 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { Command } from
|
|
4
|
-
import { checkCommand } from
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { checkCommand } from "./commands/check.js";
|
|
5
5
|
|
|
6
6
|
const program = new Command();
|
|
7
7
|
|
|
8
8
|
program
|
|
9
|
-
.name(
|
|
10
|
-
.description(
|
|
11
|
-
.version(
|
|
9
|
+
.name("mcp-doctor")
|
|
10
|
+
.description("Diagnose and fix MCP configuration issues")
|
|
11
|
+
.version("0.1.1");
|
|
12
12
|
|
|
13
13
|
program
|
|
14
|
-
.command(
|
|
15
|
-
.description(
|
|
16
|
-
.option(
|
|
14
|
+
.command("check")
|
|
15
|
+
.description("Validate MCP configuration files")
|
|
16
|
+
.option("--skip-health", "Skip server health checks")
|
|
17
|
+
.option("--file <path>", "Validate a specific config file")
|
|
17
18
|
.action(async (options) => {
|
|
18
19
|
await checkCommand({
|
|
19
20
|
skipHealth: options.skipHealth,
|
|
21
|
+
filePath: options.file,
|
|
20
22
|
});
|
|
21
23
|
});
|
|
22
24
|
|
|
23
25
|
// Default command (no subcommand) runs check
|
|
24
26
|
program
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
+
.option("--skip-health", "Skip server health checks")
|
|
28
|
+
.option("--file <path>", "Validate a specific config file")
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
await checkCommand({
|
|
31
|
+
skipHealth: options.skipHealth,
|
|
32
|
+
filePath: options.file,
|
|
33
|
+
});
|
|
27
34
|
});
|
|
28
35
|
|
|
29
36
|
program.parse();
|
package/src/utils/output.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import chalk from
|
|
2
|
-
import {
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import {
|
|
3
|
+
ValidationResult,
|
|
4
|
+
ServerTestResult,
|
|
5
|
+
ConfigLocation,
|
|
6
|
+
} from "../config/types.js";
|
|
7
|
+
|
|
8
|
+
interface ErrorContext {
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
lineContent: string;
|
|
12
|
+
surroundingLines: Array<{ num: number; content: string }>;
|
|
13
|
+
}
|
|
3
14
|
|
|
4
15
|
export function printHeader(text: string): void {
|
|
5
16
|
console.log(chalk.bold(`\n${text}\n`));
|
|
@@ -10,59 +21,109 @@ export function printConfigsFound(configs: ConfigLocation[]): void {
|
|
|
10
21
|
const notFound = configs.filter((c) => !c.exists);
|
|
11
22
|
|
|
12
23
|
if (found.length === 0) {
|
|
13
|
-
console.log(chalk.yellow(
|
|
14
|
-
console.log(chalk.gray(
|
|
24
|
+
console.log(chalk.yellow("No MCP configuration files found.\n"));
|
|
25
|
+
console.log(chalk.gray("Looked in:"));
|
|
15
26
|
notFound.slice(0, 5).forEach((c) => {
|
|
16
27
|
console.log(chalk.gray(` ${c.client}: ${c.path}`));
|
|
17
28
|
});
|
|
18
29
|
if (notFound.length > 5) {
|
|
19
|
-
console.log(
|
|
30
|
+
console.log(
|
|
31
|
+
chalk.gray(` ... and ${notFound.length - 5} more locations`)
|
|
32
|
+
);
|
|
20
33
|
}
|
|
21
34
|
return;
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
console.log(
|
|
37
|
+
console.log(
|
|
38
|
+
chalk.green(
|
|
39
|
+
`Found ${found.length} config file${found.length > 1 ? "s" : ""}:\n`
|
|
40
|
+
)
|
|
41
|
+
);
|
|
25
42
|
found.forEach((c) => {
|
|
26
|
-
const scope = c.scope ===
|
|
43
|
+
const scope = c.scope === "project" ? chalk.gray(" (project)") : "";
|
|
27
44
|
console.log(` ${chalk.cyan(c.client)}${scope}`);
|
|
28
45
|
console.log(chalk.gray(` ${c.path}\n`));
|
|
29
46
|
});
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
export function
|
|
33
|
-
const
|
|
34
|
-
const
|
|
49
|
+
export function printCodeContext(errorContext: ErrorContext): void {
|
|
50
|
+
const { line: errorLine, column, surroundingLines } = errorContext;
|
|
51
|
+
const lineNumWidth = Math.max(
|
|
52
|
+
...surroundingLines.map((l) => l.num.toString().length)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
console.log("");
|
|
56
|
+
|
|
57
|
+
for (const { num, content } of surroundingLines) {
|
|
58
|
+
const lineNum = num.toString().padStart(lineNumWidth, " ");
|
|
59
|
+
const isErrorLine = num === errorLine;
|
|
60
|
+
|
|
61
|
+
if (isErrorLine) {
|
|
62
|
+
console.log(chalk.red(` > ${lineNum} │ ${content}`));
|
|
63
|
+
// Print the pointer
|
|
64
|
+
const pointerPadding = " ".repeat(4 + lineNumWidth + 3 + column - 1);
|
|
65
|
+
console.log(chalk.red(`${pointerPadding}^`));
|
|
66
|
+
} else {
|
|
67
|
+
console.log(chalk.gray(` ${lineNum} │ ${content}`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log("");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function printValidationResults(
|
|
75
|
+
results: ValidationResult[],
|
|
76
|
+
errorContext?: ErrorContext
|
|
77
|
+
): void {
|
|
78
|
+
const errors = results.filter((r) => r.level === "error");
|
|
79
|
+
const warnings = results.filter((r) => r.level === "warning");
|
|
35
80
|
|
|
36
81
|
if (errors.length > 0) {
|
|
37
|
-
console.log(
|
|
38
|
-
|
|
39
|
-
|
|
82
|
+
console.log(
|
|
83
|
+
chalk.red(`❌ ${errors.length} Error${errors.length > 1 ? "s" : ""}\n`)
|
|
84
|
+
);
|
|
85
|
+
errors.forEach((e, index) => {
|
|
86
|
+
const location = e.line ? `:${e.line}` : "";
|
|
40
87
|
console.log(chalk.red(` ${shortPath(e.file)}${location}`));
|
|
41
88
|
console.log(chalk.red(` ${e.message}`));
|
|
89
|
+
|
|
90
|
+
// Show code context for first JSON error
|
|
91
|
+
if (
|
|
92
|
+
errorContext &&
|
|
93
|
+
index === 0 &&
|
|
94
|
+
(e.code === "JSON_TRAILING_COMMA" || e.code === "JSON_SYNTAX")
|
|
95
|
+
) {
|
|
96
|
+
printCodeContext(errorContext);
|
|
97
|
+
}
|
|
98
|
+
|
|
42
99
|
if (e.suggestion) {
|
|
43
100
|
console.log(chalk.gray(` 💡 ${e.suggestion}`));
|
|
44
101
|
}
|
|
45
|
-
console.log(
|
|
102
|
+
console.log("");
|
|
46
103
|
});
|
|
47
104
|
}
|
|
48
105
|
|
|
49
106
|
if (warnings.length > 0) {
|
|
50
|
-
console.log(
|
|
107
|
+
console.log(
|
|
108
|
+
chalk.yellow(
|
|
109
|
+
`⚠️ ${warnings.length} Warning${warnings.length > 1 ? "s" : ""}\n`
|
|
110
|
+
)
|
|
111
|
+
);
|
|
51
112
|
warnings.forEach((w) => {
|
|
52
|
-
const location = w.line ? `:${w.line}` :
|
|
113
|
+
const location = w.line ? `:${w.line}` : "";
|
|
53
114
|
console.log(chalk.yellow(` ${shortPath(w.file)}${location}`));
|
|
54
115
|
console.log(chalk.yellow(` ${w.message}`));
|
|
55
116
|
if (w.suggestion) {
|
|
56
117
|
console.log(chalk.gray(` 💡 ${w.suggestion}`));
|
|
57
118
|
}
|
|
58
|
-
console.log(
|
|
119
|
+
console.log("");
|
|
59
120
|
});
|
|
60
121
|
}
|
|
61
122
|
}
|
|
62
123
|
|
|
63
124
|
export function printServerResults(results: ServerTestResult[]): void {
|
|
64
125
|
if (results.length === 0) {
|
|
65
|
-
console.log(chalk.gray(
|
|
126
|
+
console.log(chalk.gray("No servers to test.\n"));
|
|
66
127
|
return;
|
|
67
128
|
}
|
|
68
129
|
|
|
@@ -71,12 +132,14 @@ export function printServerResults(results: ServerTestResult[]): void {
|
|
|
71
132
|
|
|
72
133
|
console.log(
|
|
73
134
|
`🔌 Server Health: ${chalk.green(healthy.length)} healthy, ${
|
|
74
|
-
unhealthy.length > 0
|
|
135
|
+
unhealthy.length > 0
|
|
136
|
+
? chalk.red(unhealthy.length + " failed")
|
|
137
|
+
: chalk.gray("0 failed")
|
|
75
138
|
}\n`
|
|
76
139
|
);
|
|
77
140
|
|
|
78
141
|
healthy.forEach((s) => {
|
|
79
|
-
const time = s.responseTime ? chalk.gray(` (${s.responseTime}ms)`) :
|
|
142
|
+
const time = s.responseTime ? chalk.gray(` (${s.responseTime}ms)`) : "";
|
|
80
143
|
console.log(chalk.green(` ✓ ${s.name}${time}`));
|
|
81
144
|
});
|
|
82
145
|
|
|
@@ -87,7 +150,7 @@ export function printServerResults(results: ServerTestResult[]): void {
|
|
|
87
150
|
}
|
|
88
151
|
});
|
|
89
152
|
|
|
90
|
-
console.log(
|
|
153
|
+
console.log("");
|
|
91
154
|
}
|
|
92
155
|
|
|
93
156
|
export function printSummary(
|
|
@@ -96,19 +159,37 @@ export function printSummary(
|
|
|
96
159
|
healthyServers: number,
|
|
97
160
|
totalServers: number
|
|
98
161
|
): void {
|
|
99
|
-
console.log(chalk.gray(
|
|
162
|
+
console.log(chalk.gray("─".repeat(50)));
|
|
100
163
|
|
|
101
164
|
if (errors === 0 && warnings === 0) {
|
|
102
|
-
console.log(chalk.green(
|
|
165
|
+
console.log(chalk.green("\n✓ All configurations valid"));
|
|
103
166
|
} else if (errors === 0) {
|
|
104
|
-
console.log(
|
|
167
|
+
console.log(
|
|
168
|
+
chalk.yellow(
|
|
169
|
+
`\n⚠️ Configuration valid with ${warnings} warning${
|
|
170
|
+
warnings > 1 ? "s" : ""
|
|
171
|
+
}`
|
|
172
|
+
)
|
|
173
|
+
);
|
|
105
174
|
} else {
|
|
106
|
-
console.log(
|
|
175
|
+
console.log(
|
|
176
|
+
chalk.red(
|
|
177
|
+
`\n❌ Found ${errors} error${errors > 1 ? "s" : ""} that need${
|
|
178
|
+
errors === 1 ? "s" : ""
|
|
179
|
+
} fixing`
|
|
180
|
+
)
|
|
181
|
+
);
|
|
107
182
|
}
|
|
108
183
|
|
|
109
184
|
if (totalServers > 0) {
|
|
110
185
|
if (healthyServers === totalServers) {
|
|
111
|
-
console.log(
|
|
186
|
+
console.log(
|
|
187
|
+
chalk.green(
|
|
188
|
+
`✓ All ${totalServers} server${
|
|
189
|
+
totalServers > 1 ? "s" : ""
|
|
190
|
+
} responding`
|
|
191
|
+
)
|
|
192
|
+
);
|
|
112
193
|
} else {
|
|
113
194
|
console.log(
|
|
114
195
|
chalk.yellow(`⚠️ ${healthyServers}/${totalServers} servers responding`)
|
|
@@ -116,13 +197,13 @@ export function printSummary(
|
|
|
116
197
|
}
|
|
117
198
|
}
|
|
118
199
|
|
|
119
|
-
console.log(
|
|
200
|
+
console.log("");
|
|
120
201
|
}
|
|
121
202
|
|
|
122
203
|
function shortPath(path: string): string {
|
|
123
|
-
const home = process.env.HOME || process.env.USERPROFILE ||
|
|
204
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
124
205
|
if (home && path.startsWith(home)) {
|
|
125
|
-
return
|
|
206
|
+
return "~" + path.slice(home.length);
|
|
126
207
|
}
|
|
127
208
|
return path;
|
|
128
209
|
}
|
|
@@ -1,17 +1,31 @@
|
|
|
1
|
-
import { readFileSync } from
|
|
2
|
-
import { ValidationResult } from
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { ValidationResult } from "../config/types.js";
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export interface JsonSyntaxResult {
|
|
5
|
+
valid: boolean;
|
|
6
|
+
results: ValidationResult[];
|
|
7
|
+
config?: Record<string, unknown>;
|
|
8
|
+
errorContext?: {
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
lineContent: string;
|
|
12
|
+
surroundingLines: Array<{ num: number; content: string }>;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validateJsonSyntax(filePath: string): JsonSyntaxResult {
|
|
5
17
|
const results: ValidationResult[] = [];
|
|
6
18
|
|
|
7
19
|
let content: string;
|
|
8
20
|
try {
|
|
9
|
-
content = readFileSync(filePath,
|
|
21
|
+
content = readFileSync(filePath, "utf-8");
|
|
10
22
|
} catch (error) {
|
|
11
23
|
results.push({
|
|
12
|
-
level:
|
|
13
|
-
code:
|
|
14
|
-
message: `Cannot read file: ${
|
|
24
|
+
level: "error",
|
|
25
|
+
code: "FILE_READ_ERROR",
|
|
26
|
+
message: `Cannot read file: ${
|
|
27
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
28
|
+
}`,
|
|
15
29
|
file: filePath,
|
|
16
30
|
});
|
|
17
31
|
return { valid: false, results };
|
|
@@ -20,62 +34,99 @@ export function validateJsonSyntax(filePath: string): { valid: boolean; results:
|
|
|
20
34
|
// Check for empty file
|
|
21
35
|
if (!content.trim()) {
|
|
22
36
|
results.push({
|
|
23
|
-
level:
|
|
24
|
-
code:
|
|
25
|
-
message:
|
|
37
|
+
level: "warning",
|
|
38
|
+
code: "EMPTY_FILE",
|
|
39
|
+
message: "Config file is empty",
|
|
26
40
|
file: filePath,
|
|
27
41
|
});
|
|
28
42
|
return { valid: true, results, config: {} };
|
|
29
43
|
}
|
|
30
44
|
|
|
45
|
+
const lines = content.split("\n");
|
|
46
|
+
|
|
31
47
|
// Try to parse JSON
|
|
32
48
|
try {
|
|
33
49
|
const config = JSON.parse(content);
|
|
34
50
|
return { valid: true, results, config };
|
|
35
51
|
} catch (error) {
|
|
36
52
|
if (error instanceof SyntaxError) {
|
|
37
|
-
// Try to find the
|
|
38
|
-
const
|
|
39
|
-
let line
|
|
40
|
-
let column
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
const position = parseInt(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
// Try to find the position from error message
|
|
54
|
+
const posMatch = error.message.match(/position\s+(\d+)/i);
|
|
55
|
+
let line = 1;
|
|
56
|
+
let column = 1;
|
|
57
|
+
|
|
58
|
+
if (posMatch) {
|
|
59
|
+
const position = parseInt(posMatch[1], 10);
|
|
60
|
+
// Convert position to line/column
|
|
61
|
+
let currentPos = 0;
|
|
62
|
+
for (let i = 0; i < lines.length; i++) {
|
|
63
|
+
if (currentPos + lines[i].length + 1 > position) {
|
|
64
|
+
line = i + 1;
|
|
65
|
+
column = position - currentPos + 1;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
currentPos += lines[i].length + 1; // +1 for newline
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for trailing comma specifically
|
|
73
|
+
const trailingCommaRegex = /,(\s*[\]\}])/g;
|
|
74
|
+
let trailingCommaMatch;
|
|
75
|
+
let trailingCommaLine = 0;
|
|
76
|
+
let trailingCommaColumn = 0;
|
|
77
|
+
let searchPos = 0;
|
|
78
|
+
|
|
79
|
+
while ((trailingCommaMatch = trailingCommaRegex.exec(content)) !== null) {
|
|
80
|
+
// Find line number of this match
|
|
81
|
+
const beforeMatch = content.substring(0, trailingCommaMatch.index);
|
|
82
|
+
const matchLines = beforeMatch.split("\n");
|
|
83
|
+
trailingCommaLine = matchLines.length;
|
|
84
|
+
trailingCommaColumn = matchLines[matchLines.length - 1].length + 1;
|
|
85
|
+
searchPos = trailingCommaMatch.index;
|
|
47
86
|
}
|
|
48
87
|
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const beforeMatch = content.substring(0, content.indexOf(trailingCommaMatch[0]));
|
|
54
|
-
const trailingCommaLine = beforeMatch.split('\n').length;
|
|
88
|
+
// Build surrounding lines context
|
|
89
|
+
const errorLine = trailingCommaLine > 0 ? trailingCommaLine : line;
|
|
90
|
+
const errorColumn = trailingCommaLine > 0 ? trailingCommaColumn : column;
|
|
91
|
+
const surroundingLines: Array<{ num: number; content: string }> = [];
|
|
55
92
|
|
|
93
|
+
const startLine = Math.max(0, errorLine - 3);
|
|
94
|
+
const endLine = Math.min(lines.length - 1, errorLine + 1);
|
|
95
|
+
|
|
96
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
97
|
+
surroundingLines.push({
|
|
98
|
+
num: i + 1,
|
|
99
|
+
content: lines[i],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const errorContext = {
|
|
104
|
+
line: errorLine,
|
|
105
|
+
column: errorColumn,
|
|
106
|
+
lineContent: lines[errorLine - 1] || "",
|
|
107
|
+
surroundingLines,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
if (trailingCommaLine > 0) {
|
|
56
111
|
results.push({
|
|
57
|
-
level:
|
|
58
|
-
code:
|
|
59
|
-
message:
|
|
112
|
+
level: "error",
|
|
113
|
+
code: "JSON_TRAILING_COMMA",
|
|
114
|
+
message: "Trailing comma detected (not allowed in JSON)",
|
|
60
115
|
file: filePath,
|
|
61
116
|
line: trailingCommaLine,
|
|
62
|
-
suggestion:
|
|
117
|
+
suggestion: "Remove the comma before the closing bracket/brace",
|
|
63
118
|
});
|
|
64
119
|
} else {
|
|
65
|
-
// Generic JSON error
|
|
66
|
-
let message = error.message;
|
|
67
|
-
if (line && column) {
|
|
68
|
-
message = `Invalid JSON at line ${line}, column ${column}: ${error.message}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
120
|
results.push({
|
|
72
|
-
level:
|
|
73
|
-
code:
|
|
74
|
-
message,
|
|
121
|
+
level: "error",
|
|
122
|
+
code: "JSON_SYNTAX",
|
|
123
|
+
message: error.message,
|
|
75
124
|
file: filePath,
|
|
76
125
|
line,
|
|
77
126
|
});
|
|
78
127
|
}
|
|
128
|
+
|
|
129
|
+
return { valid: false, results, errorContext };
|
|
79
130
|
}
|
|
80
131
|
|
|
81
132
|
return { valid: false, results };
|