mcp-doctor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/cli/commands/check.d.ts +5 -0
- package/dist/cli/commands/check.js +67 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +24 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/locations.d.ts +3 -0
- package/dist/config/locations.js +138 -0
- package/dist/config/locations.js.map +1 -0
- package/dist/config/types.d.ts +37 -0
- package/dist/config/types.js +2 -0
- package/dist/config/types.js.map +1 -0
- package/dist/utils/output.d.ts +6 -0
- package/dist/utils/output.js +102 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/validators/env-vars.d.ts +2 -0
- package/dist/validators/env-vars.js +70 -0
- package/dist/validators/env-vars.js.map +1 -0
- package/dist/validators/index.d.ts +4 -0
- package/dist/validators/index.js +5 -0
- package/dist/validators/index.js.map +1 -0
- package/dist/validators/json-syntax.d.ts +6 -0
- package/dist/validators/json-syntax.js +77 -0
- package/dist/validators/json-syntax.js.map +1 -0
- package/dist/validators/paths.d.ts +2 -0
- package/dist/validators/paths.js +125 -0
- package/dist/validators/paths.js.map +1 -0
- package/dist/validators/server-health.d.ts +2 -0
- package/dist/validators/server-health.js +135 -0
- package/dist/validators/server-health.js.map +1 -0
- package/package.json +45 -0
- package/src/cli/commands/check.ts +103 -0
- package/src/cli/index.ts +29 -0
- package/src/config/locations.ts +141 -0
- package/src/config/types.ts +45 -0
- package/src/utils/output.ts +128 -0
- package/src/validators/env-vars.ts +85 -0
- package/src/validators/index.ts +4 -0
- package/src/validators/json-syntax.ts +83 -0
- package/src/validators/paths.ts +140 -0
- package/src/validators/server-health.ts +165 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env-vars.js","sourceRoot":"","sources":["../../src/validators/env-vars.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/validators/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
export function validateJsonSyntax(filePath) {
|
|
3
|
+
const results = [];
|
|
4
|
+
let content;
|
|
5
|
+
try {
|
|
6
|
+
content = readFileSync(filePath, 'utf-8');
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
results.push({
|
|
10
|
+
level: 'error',
|
|
11
|
+
code: 'FILE_READ_ERROR',
|
|
12
|
+
message: `Cannot read file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
13
|
+
file: filePath,
|
|
14
|
+
});
|
|
15
|
+
return { valid: false, results };
|
|
16
|
+
}
|
|
17
|
+
// Check for empty file
|
|
18
|
+
if (!content.trim()) {
|
|
19
|
+
results.push({
|
|
20
|
+
level: 'warning',
|
|
21
|
+
code: 'EMPTY_FILE',
|
|
22
|
+
message: 'Config file is empty',
|
|
23
|
+
file: filePath,
|
|
24
|
+
});
|
|
25
|
+
return { valid: true, results, config: {} };
|
|
26
|
+
}
|
|
27
|
+
// Try to parse JSON
|
|
28
|
+
try {
|
|
29
|
+
const config = JSON.parse(content);
|
|
30
|
+
return { valid: true, results, config };
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof SyntaxError) {
|
|
34
|
+
// Try to find the line number
|
|
35
|
+
const match = error.message.match(/position\s+(\d+)/i);
|
|
36
|
+
let line;
|
|
37
|
+
let column;
|
|
38
|
+
if (match) {
|
|
39
|
+
const position = parseInt(match[1], 10);
|
|
40
|
+
const lines = content.substring(0, position).split('\n');
|
|
41
|
+
line = lines.length;
|
|
42
|
+
column = lines[lines.length - 1].length + 1;
|
|
43
|
+
}
|
|
44
|
+
// Check for common issues
|
|
45
|
+
const trailingCommaMatch = content.match(/,\s*([}\]])/);
|
|
46
|
+
if (trailingCommaMatch) {
|
|
47
|
+
// Find line of trailing comma
|
|
48
|
+
const beforeMatch = content.substring(0, content.indexOf(trailingCommaMatch[0]));
|
|
49
|
+
const trailingCommaLine = beforeMatch.split('\n').length;
|
|
50
|
+
results.push({
|
|
51
|
+
level: 'error',
|
|
52
|
+
code: 'JSON_TRAILING_COMMA',
|
|
53
|
+
message: 'Trailing comma detected (not allowed in JSON)',
|
|
54
|
+
file: filePath,
|
|
55
|
+
line: trailingCommaLine,
|
|
56
|
+
suggestion: 'Remove the comma before the closing bracket/brace',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
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
|
+
results.push({
|
|
66
|
+
level: 'error',
|
|
67
|
+
code: 'JSON_SYNTAX',
|
|
68
|
+
message,
|
|
69
|
+
file: filePath,
|
|
70
|
+
line,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { valid: false, results };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=json-syntax.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-syntax.js","sourceRoot":"","sources":["../../src/validators/json-syntax.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, accessSync, constants } from 'fs';
|
|
2
|
+
import { isAbsolute, resolve, dirname } from 'path';
|
|
3
|
+
import { getServersFromConfig } from '../config/locations.js';
|
|
4
|
+
export function validatePaths(config, configPath) {
|
|
5
|
+
const results = [];
|
|
6
|
+
const servers = getServersFromConfig(config);
|
|
7
|
+
if (!servers) {
|
|
8
|
+
return results;
|
|
9
|
+
}
|
|
10
|
+
const configDir = dirname(configPath);
|
|
11
|
+
for (const [name, serverRaw] of Object.entries(servers)) {
|
|
12
|
+
const server = serverRaw;
|
|
13
|
+
// Skip HTTP/URL-based servers
|
|
14
|
+
if (server.url)
|
|
15
|
+
continue;
|
|
16
|
+
// Skip if no command specified
|
|
17
|
+
if (!server.command) {
|
|
18
|
+
results.push({
|
|
19
|
+
level: 'error',
|
|
20
|
+
code: 'MISSING_COMMAND',
|
|
21
|
+
message: `Server "${name}": No command specified`,
|
|
22
|
+
file: configPath,
|
|
23
|
+
suggestion: 'Add a "command" field specifying how to start the server',
|
|
24
|
+
});
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const command = server.command;
|
|
28
|
+
// Check if command is a path (contains / or \)
|
|
29
|
+
if (command.includes('/') || command.includes('\\')) {
|
|
30
|
+
const resolvedPath = isAbsolute(command)
|
|
31
|
+
? command
|
|
32
|
+
: resolve(configDir, command);
|
|
33
|
+
// Check existence
|
|
34
|
+
if (!existsSync(resolvedPath)) {
|
|
35
|
+
results.push({
|
|
36
|
+
level: 'error',
|
|
37
|
+
code: 'PATH_NOT_FOUND',
|
|
38
|
+
message: `Server "${name}": Command path does not exist: ${command}`,
|
|
39
|
+
file: configPath,
|
|
40
|
+
suggestion: `Check the path is correct. Resolved to: ${resolvedPath}`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// Check executable permission (Unix only)
|
|
45
|
+
if (process.platform !== 'win32') {
|
|
46
|
+
try {
|
|
47
|
+
accessSync(resolvedPath, constants.X_OK);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
results.push({
|
|
51
|
+
level: 'error',
|
|
52
|
+
code: 'PATH_NOT_EXECUTABLE',
|
|
53
|
+
message: `Server "${name}": Command is not executable: ${command}`,
|
|
54
|
+
file: configPath,
|
|
55
|
+
suggestion: `Run: chmod +x "${resolvedPath}"`,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Warn about relative paths
|
|
61
|
+
if (!isAbsolute(command)) {
|
|
62
|
+
results.push({
|
|
63
|
+
level: 'warning',
|
|
64
|
+
code: 'PATH_RELATIVE',
|
|
65
|
+
message: `Server "${name}": Using relative path "${command}"`,
|
|
66
|
+
file: configPath,
|
|
67
|
+
suggestion: `Consider using absolute path: ${resolvedPath}`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Command is not a path (e.g., "npx", "node", "python")
|
|
73
|
+
// We can't easily validate these as they depend on PATH
|
|
74
|
+
// But we can warn if it looks like a typo
|
|
75
|
+
const commonCommands = ['npx', 'node', 'python', 'python3', 'uv', 'uvx', 'deno', 'bun'];
|
|
76
|
+
const looksLikePath = command.includes('.') && !commonCommands.includes(command);
|
|
77
|
+
if (looksLikePath) {
|
|
78
|
+
results.push({
|
|
79
|
+
level: 'warning',
|
|
80
|
+
code: 'POSSIBLE_PATH_TYPO',
|
|
81
|
+
message: `Server "${name}": "${command}" looks like a filename but isn't a path`,
|
|
82
|
+
file: configPath,
|
|
83
|
+
suggestion: 'If this is a file, use a full path like "./server.js" or "/path/to/server.js"',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Check cwd if specified
|
|
88
|
+
if (server.cwd) {
|
|
89
|
+
const cwdPath = isAbsolute(server.cwd)
|
|
90
|
+
? server.cwd
|
|
91
|
+
: resolve(configDir, server.cwd);
|
|
92
|
+
if (!existsSync(cwdPath)) {
|
|
93
|
+
results.push({
|
|
94
|
+
level: 'error',
|
|
95
|
+
code: 'CWD_NOT_FOUND',
|
|
96
|
+
message: `Server "${name}": Working directory does not exist: ${server.cwd}`,
|
|
97
|
+
file: configPath,
|
|
98
|
+
suggestion: `Create the directory or update the path. Resolved to: ${cwdPath}`,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Check args for paths
|
|
103
|
+
if (server.args && Array.isArray(server.args)) {
|
|
104
|
+
for (const arg of server.args) {
|
|
105
|
+
// If arg looks like an absolute path, check it exists
|
|
106
|
+
if (typeof arg === 'string' && isAbsolute(arg) && (arg.includes('/') || arg.includes('\\'))) {
|
|
107
|
+
// Only check if it looks like a file path (not a URL or flag)
|
|
108
|
+
if (!arg.startsWith('-') && !arg.startsWith('http')) {
|
|
109
|
+
if (!existsSync(arg)) {
|
|
110
|
+
results.push({
|
|
111
|
+
level: 'warning',
|
|
112
|
+
code: 'ARG_PATH_NOT_FOUND',
|
|
113
|
+
message: `Server "${name}": Argument path may not exist: ${arg}`,
|
|
114
|
+
file: configPath,
|
|
115
|
+
suggestion: 'Verify this path is correct',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/validators/paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { getServersFromConfig } from '../config/locations.js';
|
|
3
|
+
export async function testServerHealth(config, timeout = 10000) {
|
|
4
|
+
const results = [];
|
|
5
|
+
const servers = getServersFromConfig(config);
|
|
6
|
+
if (!servers) {
|
|
7
|
+
return results;
|
|
8
|
+
}
|
|
9
|
+
const testPromises = Object.entries(servers).map(async ([name, serverRaw]) => {
|
|
10
|
+
const server = serverRaw;
|
|
11
|
+
const startTime = Date.now();
|
|
12
|
+
// Skip URL-based servers for now
|
|
13
|
+
if (server.url) {
|
|
14
|
+
return {
|
|
15
|
+
name,
|
|
16
|
+
healthy: true, // Assume healthy for HTTP servers
|
|
17
|
+
responseTime: undefined,
|
|
18
|
+
error: 'HTTP servers not tested (coming soon)',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// Skip if no command
|
|
22
|
+
if (!server.command) {
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
healthy: false,
|
|
26
|
+
error: 'No command specified',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const healthy = await testStdioServer(server.command, server.args || [], server.env, timeout);
|
|
31
|
+
const responseTime = Date.now() - startTime;
|
|
32
|
+
return {
|
|
33
|
+
name,
|
|
34
|
+
healthy,
|
|
35
|
+
responseTime: healthy ? responseTime : undefined,
|
|
36
|
+
error: healthy ? undefined : `Timeout after ${timeout}ms`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
healthy: false,
|
|
43
|
+
error: error instanceof Error ? error.message : String(error),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return Promise.all(testPromises);
|
|
48
|
+
}
|
|
49
|
+
async function testStdioServer(command, args, env, timeout = 10000) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
let proc;
|
|
52
|
+
try {
|
|
53
|
+
proc = spawn(command, args, {
|
|
54
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
55
|
+
env: { ...process.env, ...env },
|
|
56
|
+
shell: process.platform === 'win32', // Use shell on Windows for npx etc
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
resolve(false);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
proc.kill();
|
|
65
|
+
resolve(false);
|
|
66
|
+
}, timeout);
|
|
67
|
+
let buffer = '';
|
|
68
|
+
let initialized = false;
|
|
69
|
+
proc.stdout?.on('data', (data) => {
|
|
70
|
+
buffer += data.toString();
|
|
71
|
+
// Try to parse complete JSON-RPC messages
|
|
72
|
+
const lines = buffer.split('\n');
|
|
73
|
+
buffer = lines.pop() || '';
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
if (!line.trim())
|
|
76
|
+
continue;
|
|
77
|
+
try {
|
|
78
|
+
const message = JSON.parse(line);
|
|
79
|
+
// Handle initialize response
|
|
80
|
+
if (message.id === 1 && message.result && !initialized) {
|
|
81
|
+
initialized = true;
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
proc.kill();
|
|
84
|
+
resolve(true);
|
|
85
|
+
}
|
|
86
|
+
// Handle error response
|
|
87
|
+
if (message.id === 1 && message.error) {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
proc.kill();
|
|
90
|
+
resolve(false);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Not valid JSON, might be partial or non-JSON output
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
proc.on('error', (error) => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
// Check for common issues
|
|
101
|
+
if (error.code === 'ENOENT') {
|
|
102
|
+
// Command not found - this is expected for some setups
|
|
103
|
+
}
|
|
104
|
+
resolve(false);
|
|
105
|
+
});
|
|
106
|
+
proc.on('exit', (code) => {
|
|
107
|
+
if (!initialized) {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
// Non-zero exit before responding is a failure
|
|
110
|
+
if (code !== 0 && code !== null) {
|
|
111
|
+
resolve(false);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
// Send initialize request
|
|
116
|
+
const initRequest = JSON.stringify({
|
|
117
|
+
jsonrpc: '2.0',
|
|
118
|
+
id: 1,
|
|
119
|
+
method: 'initialize',
|
|
120
|
+
params: {
|
|
121
|
+
protocolVersion: '2024-11-05',
|
|
122
|
+
capabilities: {},
|
|
123
|
+
clientInfo: { name: 'mcp-doctor', version: '1.0.0' },
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
try {
|
|
127
|
+
proc.stdin?.write(initRequest + '\n');
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
resolve(false);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=server-health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-health.js","sourceRoot":"","sources":["../../src/validators/server-health.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-doctor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Diagnose and fix MCP configuration issues across Claude Desktop, Cursor, VS Code, and more",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-doctor": "./dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/cli/index.ts",
|
|
12
|
+
"start": "node dist/cli/index.js",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"model-context-protocol",
|
|
18
|
+
"claude",
|
|
19
|
+
"cursor",
|
|
20
|
+
"vscode",
|
|
21
|
+
"debugging",
|
|
22
|
+
"cli",
|
|
23
|
+
"developer-tools",
|
|
24
|
+
"anthropic"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/Crooj026/mcp-doctor"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"chalk": "^5.3.0",
|
|
37
|
+
"commander": "^12.1.0",
|
|
38
|
+
"ora": "^8.0.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.10.0",
|
|
42
|
+
"tsx": "^4.7.0",
|
|
43
|
+
"typescript": "^5.3.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import { getConfigLocations } from '../../config/locations.js';
|
|
3
|
+
import {
|
|
4
|
+
validateJsonSyntax,
|
|
5
|
+
validatePaths,
|
|
6
|
+
validateEnvVars,
|
|
7
|
+
testServerHealth,
|
|
8
|
+
} from '../../validators/index.js';
|
|
9
|
+
import {
|
|
10
|
+
printHeader,
|
|
11
|
+
printConfigsFound,
|
|
12
|
+
printValidationResults,
|
|
13
|
+
printServerResults,
|
|
14
|
+
printSummary,
|
|
15
|
+
} from '../../utils/output.js';
|
|
16
|
+
import { ValidationResult, ServerTestResult } from '../../config/types.js';
|
|
17
|
+
|
|
18
|
+
interface CheckOptions {
|
|
19
|
+
skipHealth?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function checkCommand(options: CheckOptions = {}): Promise<void> {
|
|
23
|
+
printHeader('🩺 MCP Doctor');
|
|
24
|
+
|
|
25
|
+
const spinner = ora('Scanning for MCP configurations...').start();
|
|
26
|
+
|
|
27
|
+
// Find all config files
|
|
28
|
+
const locations = getConfigLocations();
|
|
29
|
+
const foundConfigs = locations.filter((l) => l.exists);
|
|
30
|
+
|
|
31
|
+
spinner.stop();
|
|
32
|
+
|
|
33
|
+
// Show what we found
|
|
34
|
+
printConfigsFound(locations);
|
|
35
|
+
|
|
36
|
+
if (foundConfigs.length === 0) {
|
|
37
|
+
process.exitCode = 0;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Run validators on each config
|
|
42
|
+
const allResults: ValidationResult[] = [];
|
|
43
|
+
const allServerResults: ServerTestResult[] = [];
|
|
44
|
+
const configsWithServers: Array<{ path: string; config: Record<string, unknown> }> = [];
|
|
45
|
+
|
|
46
|
+
for (const configLoc of foundConfigs) {
|
|
47
|
+
// JSON syntax check
|
|
48
|
+
const { valid, results: syntaxResults, config } = validateJsonSyntax(configLoc.path);
|
|
49
|
+
allResults.push(...syntaxResults);
|
|
50
|
+
|
|
51
|
+
// If JSON is invalid, skip other validators for this file
|
|
52
|
+
if (!valid || !config) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Store for server health testing
|
|
57
|
+
configsWithServers.push({ path: configLoc.path, config });
|
|
58
|
+
|
|
59
|
+
// Path validation
|
|
60
|
+
allResults.push(...validatePaths(config, configLoc.path));
|
|
61
|
+
|
|
62
|
+
// Environment variable validation
|
|
63
|
+
allResults.push(...validateEnvVars(config, configLoc.path));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Print validation results
|
|
67
|
+
printValidationResults(allResults);
|
|
68
|
+
|
|
69
|
+
// Server health tests (unless skipped)
|
|
70
|
+
if (!options.skipHealth && configsWithServers.length > 0) {
|
|
71
|
+
const healthSpinner = ora('Testing server connectivity...').start();
|
|
72
|
+
|
|
73
|
+
for (const { config } of configsWithServers) {
|
|
74
|
+
const results = await testServerHealth(config);
|
|
75
|
+
allServerResults.push(...results);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
healthSpinner.stop();
|
|
79
|
+
|
|
80
|
+
// Deduplicate servers by name (same server might be in multiple configs)
|
|
81
|
+
const uniqueServers = new Map<string, ServerTestResult>();
|
|
82
|
+
for (const result of allServerResults) {
|
|
83
|
+
// Keep the healthy result if we have conflicting results
|
|
84
|
+
if (!uniqueServers.has(result.name) || result.healthy) {
|
|
85
|
+
uniqueServers.set(result.name, result);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
printServerResults(Array.from(uniqueServers.values()));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Summary
|
|
93
|
+
const errors = allResults.filter((r) => r.level === 'error').length;
|
|
94
|
+
const warnings = allResults.filter((r) => r.level === 'warning').length;
|
|
95
|
+
const healthyServers = allServerResults.filter((s) => s.healthy).length;
|
|
96
|
+
|
|
97
|
+
printSummary(errors, warnings, healthyServers, allServerResults.length);
|
|
98
|
+
|
|
99
|
+
// Set exit code
|
|
100
|
+
if (errors > 0) {
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { checkCommand } from './commands/check.js';
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('mcp-doctor')
|
|
10
|
+
.description('Diagnose and fix MCP configuration issues')
|
|
11
|
+
.version('0.1.0');
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.command('check')
|
|
15
|
+
.description('Validate all MCP configuration files')
|
|
16
|
+
.option('--skip-health', 'Skip server health checks')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
await checkCommand({
|
|
19
|
+
skipHealth: options.skipHealth,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Default command (no subcommand) runs check
|
|
24
|
+
program
|
|
25
|
+
.action(async () => {
|
|
26
|
+
await checkCommand();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
program.parse();
|