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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/dist/cli/commands/check.d.ts +5 -0
  4. package/dist/cli/commands/check.js +67 -0
  5. package/dist/cli/commands/check.js.map +1 -0
  6. package/dist/cli/index.d.ts +2 -0
  7. package/dist/cli/index.js +24 -0
  8. package/dist/cli/index.js.map +1 -0
  9. package/dist/config/locations.d.ts +3 -0
  10. package/dist/config/locations.js +138 -0
  11. package/dist/config/locations.js.map +1 -0
  12. package/dist/config/types.d.ts +37 -0
  13. package/dist/config/types.js +2 -0
  14. package/dist/config/types.js.map +1 -0
  15. package/dist/utils/output.d.ts +6 -0
  16. package/dist/utils/output.js +102 -0
  17. package/dist/utils/output.js.map +1 -0
  18. package/dist/validators/env-vars.d.ts +2 -0
  19. package/dist/validators/env-vars.js +70 -0
  20. package/dist/validators/env-vars.js.map +1 -0
  21. package/dist/validators/index.d.ts +4 -0
  22. package/dist/validators/index.js +5 -0
  23. package/dist/validators/index.js.map +1 -0
  24. package/dist/validators/json-syntax.d.ts +6 -0
  25. package/dist/validators/json-syntax.js +77 -0
  26. package/dist/validators/json-syntax.js.map +1 -0
  27. package/dist/validators/paths.d.ts +2 -0
  28. package/dist/validators/paths.js +125 -0
  29. package/dist/validators/paths.js.map +1 -0
  30. package/dist/validators/server-health.d.ts +2 -0
  31. package/dist/validators/server-health.js +135 -0
  32. package/dist/validators/server-health.js.map +1 -0
  33. package/package.json +45 -0
  34. package/src/cli/commands/check.ts +103 -0
  35. package/src/cli/index.ts +29 -0
  36. package/src/config/locations.ts +141 -0
  37. package/src/config/types.ts +45 -0
  38. package/src/utils/output.ts +128 -0
  39. package/src/validators/env-vars.ts +85 -0
  40. package/src/validators/index.ts +4 -0
  41. package/src/validators/json-syntax.ts +83 -0
  42. package/src/validators/paths.ts +140 -0
  43. package/src/validators/server-health.ts +165 -0
  44. package/tsconfig.json +18 -0
@@ -0,0 +1,141 @@
1
+ import { homedir, platform } from 'os';
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { ConfigLocation } from './types.js';
5
+
6
+ export function getConfigLocations(projectDir?: string): ConfigLocation[] {
7
+ const home = homedir();
8
+ const os = platform();
9
+ const cwd = projectDir || process.cwd();
10
+
11
+ const locations: Array<Omit<ConfigLocation, 'exists'>> = [];
12
+
13
+ // ============================================
14
+ // Claude Desktop
15
+ // ============================================
16
+ if (os === 'darwin') {
17
+ locations.push({
18
+ client: 'Claude Desktop',
19
+ scope: 'global',
20
+ path: join(home, 'Library/Application Support/Claude/claude_desktop_config.json'),
21
+ });
22
+ } else if (os === 'win32') {
23
+ locations.push({
24
+ client: 'Claude Desktop',
25
+ scope: 'global',
26
+ path: join(process.env.APPDATA || join(home, 'AppData/Roaming'), 'Claude/claude_desktop_config.json'),
27
+ });
28
+ } else {
29
+ // Linux
30
+ locations.push({
31
+ client: 'Claude Desktop',
32
+ scope: 'global',
33
+ path: join(home, '.config/claude/claude_desktop_config.json'),
34
+ });
35
+ }
36
+
37
+ // ============================================
38
+ // Claude Code
39
+ // ============================================
40
+ locations.push({
41
+ client: 'Claude Code',
42
+ scope: 'global',
43
+ path: join(home, '.claude.json'),
44
+ });
45
+ locations.push({
46
+ client: 'Claude Code',
47
+ scope: 'project',
48
+ path: join(cwd, '.mcp.json'),
49
+ });
50
+
51
+ // ============================================
52
+ // Cursor
53
+ // ============================================
54
+ if (os === 'darwin') {
55
+ locations.push({
56
+ client: 'Cursor',
57
+ scope: 'global',
58
+ path: join(home, 'Library/Application Support/Cursor/User/globalStorage/cursor.mcp/mcp.json'),
59
+ });
60
+ } else if (os === 'win32') {
61
+ locations.push({
62
+ client: 'Cursor',
63
+ scope: 'global',
64
+ path: join(process.env.APPDATA || join(home, 'AppData/Roaming'), 'Cursor/User/globalStorage/cursor.mcp/mcp.json'),
65
+ });
66
+ } else {
67
+ locations.push({
68
+ client: 'Cursor',
69
+ scope: 'global',
70
+ path: join(home, '.config/Cursor/User/globalStorage/cursor.mcp/mcp.json'),
71
+ });
72
+ }
73
+ // Also check older Cursor location
74
+ locations.push({
75
+ client: 'Cursor',
76
+ scope: 'global',
77
+ path: join(home, '.cursor/mcp.json'),
78
+ });
79
+ locations.push({
80
+ client: 'Cursor',
81
+ scope: 'project',
82
+ path: join(cwd, '.cursor/mcp.json'),
83
+ });
84
+
85
+ // ============================================
86
+ // VS Code (MCP extension)
87
+ // ============================================
88
+ if (os === 'darwin') {
89
+ locations.push({
90
+ client: 'VS Code',
91
+ scope: 'global',
92
+ path: join(home, 'Library/Application Support/Code/User/settings.json'),
93
+ });
94
+ } else if (os === 'win32') {
95
+ locations.push({
96
+ client: 'VS Code',
97
+ scope: 'global',
98
+ path: join(process.env.APPDATA || join(home, 'AppData/Roaming'), 'Code/User/settings.json'),
99
+ });
100
+ } else {
101
+ locations.push({
102
+ client: 'VS Code',
103
+ scope: 'global',
104
+ path: join(home, '.config/Code/User/settings.json'),
105
+ });
106
+ }
107
+ locations.push({
108
+ client: 'VS Code',
109
+ scope: 'project',
110
+ path: join(cwd, '.vscode/settings.json'),
111
+ });
112
+
113
+ // ============================================
114
+ // Windsurf
115
+ // ============================================
116
+ locations.push({
117
+ client: 'Windsurf',
118
+ scope: 'global',
119
+ path: join(home, '.codeium/windsurf/mcp_config.json'),
120
+ });
121
+
122
+ // Check existence and return
123
+ return locations.map((loc) => ({
124
+ ...loc,
125
+ exists: existsSync(loc.path),
126
+ }));
127
+ }
128
+
129
+ export function getServersFromConfig(config: Record<string, unknown>): Record<string, unknown> | null {
130
+ // Try different config formats
131
+ if (config.mcpServers && typeof config.mcpServers === 'object') {
132
+ return config.mcpServers as Record<string, unknown>;
133
+ }
134
+ if (config['mcp.servers'] && typeof config['mcp.servers'] === 'object') {
135
+ return config['mcp.servers'] as Record<string, unknown>;
136
+ }
137
+ if (config.servers && typeof config.servers === 'object') {
138
+ return config.servers as Record<string, unknown>;
139
+ }
140
+ return null;
141
+ }
@@ -0,0 +1,45 @@
1
+ export interface MCPServerConfig {
2
+ command?: string;
3
+ args?: string[];
4
+ env?: Record<string, string>;
5
+ cwd?: string;
6
+ // HTTP transport
7
+ url?: string;
8
+ }
9
+
10
+ export interface MCPConfig {
11
+ mcpServers?: Record<string, MCPServerConfig>;
12
+ // VS Code style
13
+ 'mcp.servers'?: Record<string, MCPServerConfig>;
14
+ // Claude Code style (servers at root)
15
+ servers?: Record<string, MCPServerConfig>;
16
+ }
17
+
18
+ export interface ConfigLocation {
19
+ client: string;
20
+ scope: 'global' | 'project';
21
+ path: string;
22
+ exists: boolean;
23
+ }
24
+
25
+ export interface ValidationResult {
26
+ level: 'error' | 'warning' | 'info';
27
+ code: string;
28
+ message: string;
29
+ file: string;
30
+ line?: number;
31
+ suggestion?: string;
32
+ }
33
+
34
+ export interface ServerTestResult {
35
+ name: string;
36
+ healthy: boolean;
37
+ responseTime?: number;
38
+ error?: string;
39
+ }
40
+
41
+ export interface CheckResults {
42
+ configsFound: ConfigLocation[];
43
+ validationResults: ValidationResult[];
44
+ serverResults: ServerTestResult[];
45
+ }
@@ -0,0 +1,128 @@
1
+ import chalk from 'chalk';
2
+ import { ValidationResult, ServerTestResult, ConfigLocation } from '../config/types.js';
3
+
4
+ export function printHeader(text: string): void {
5
+ console.log(chalk.bold(`\n${text}\n`));
6
+ }
7
+
8
+ export function printConfigsFound(configs: ConfigLocation[]): void {
9
+ const found = configs.filter((c) => c.exists);
10
+ const notFound = configs.filter((c) => !c.exists);
11
+
12
+ if (found.length === 0) {
13
+ console.log(chalk.yellow('No MCP configuration files found.\n'));
14
+ console.log(chalk.gray('Looked in:'));
15
+ notFound.slice(0, 5).forEach((c) => {
16
+ console.log(chalk.gray(` ${c.client}: ${c.path}`));
17
+ });
18
+ if (notFound.length > 5) {
19
+ console.log(chalk.gray(` ... and ${notFound.length - 5} more locations`));
20
+ }
21
+ return;
22
+ }
23
+
24
+ console.log(chalk.green(`Found ${found.length} config file${found.length > 1 ? 's' : ''}:\n`));
25
+ found.forEach((c) => {
26
+ const scope = c.scope === 'project' ? chalk.gray(' (project)') : '';
27
+ console.log(` ${chalk.cyan(c.client)}${scope}`);
28
+ console.log(chalk.gray(` ${c.path}\n`));
29
+ });
30
+ }
31
+
32
+ export function printValidationResults(results: ValidationResult[]): void {
33
+ const errors = results.filter((r) => r.level === 'error');
34
+ const warnings = results.filter((r) => r.level === 'warning');
35
+
36
+ if (errors.length > 0) {
37
+ console.log(chalk.red(`❌ ${errors.length} Error${errors.length > 1 ? 's' : ''}\n`));
38
+ errors.forEach((e) => {
39
+ const location = e.line ? `:${e.line}` : '';
40
+ console.log(chalk.red(` ${shortPath(e.file)}${location}`));
41
+ console.log(chalk.red(` ${e.message}`));
42
+ if (e.suggestion) {
43
+ console.log(chalk.gray(` 💡 ${e.suggestion}`));
44
+ }
45
+ console.log('');
46
+ });
47
+ }
48
+
49
+ if (warnings.length > 0) {
50
+ console.log(chalk.yellow(`⚠️ ${warnings.length} Warning${warnings.length > 1 ? 's' : ''}\n`));
51
+ warnings.forEach((w) => {
52
+ const location = w.line ? `:${w.line}` : '';
53
+ console.log(chalk.yellow(` ${shortPath(w.file)}${location}`));
54
+ console.log(chalk.yellow(` ${w.message}`));
55
+ if (w.suggestion) {
56
+ console.log(chalk.gray(` 💡 ${w.suggestion}`));
57
+ }
58
+ console.log('');
59
+ });
60
+ }
61
+ }
62
+
63
+ export function printServerResults(results: ServerTestResult[]): void {
64
+ if (results.length === 0) {
65
+ console.log(chalk.gray('No servers to test.\n'));
66
+ return;
67
+ }
68
+
69
+ const healthy = results.filter((s) => s.healthy);
70
+ const unhealthy = results.filter((s) => !s.healthy);
71
+
72
+ console.log(
73
+ `🔌 Server Health: ${chalk.green(healthy.length)} healthy, ${
74
+ unhealthy.length > 0 ? chalk.red(unhealthy.length + ' failed') : chalk.gray('0 failed')
75
+ }\n`
76
+ );
77
+
78
+ healthy.forEach((s) => {
79
+ const time = s.responseTime ? chalk.gray(` (${s.responseTime}ms)`) : '';
80
+ console.log(chalk.green(` ✓ ${s.name}${time}`));
81
+ });
82
+
83
+ unhealthy.forEach((s) => {
84
+ console.log(chalk.red(` ✗ ${s.name}`));
85
+ if (s.error) {
86
+ console.log(chalk.gray(` ${s.error}`));
87
+ }
88
+ });
89
+
90
+ console.log('');
91
+ }
92
+
93
+ export function printSummary(
94
+ errors: number,
95
+ warnings: number,
96
+ healthyServers: number,
97
+ totalServers: number
98
+ ): void {
99
+ console.log(chalk.gray('─'.repeat(50)));
100
+
101
+ if (errors === 0 && warnings === 0) {
102
+ console.log(chalk.green('\n✓ All configurations valid'));
103
+ } else if (errors === 0) {
104
+ console.log(chalk.yellow(`\n⚠️ Configuration valid with ${warnings} warning${warnings > 1 ? 's' : ''}`));
105
+ } else {
106
+ console.log(chalk.red(`\n❌ Found ${errors} error${errors > 1 ? 's' : ''} that need${errors === 1 ? 's' : ''} fixing`));
107
+ }
108
+
109
+ if (totalServers > 0) {
110
+ if (healthyServers === totalServers) {
111
+ console.log(chalk.green(`✓ All ${totalServers} server${totalServers > 1 ? 's' : ''} responding`));
112
+ } else {
113
+ console.log(
114
+ chalk.yellow(`⚠️ ${healthyServers}/${totalServers} servers responding`)
115
+ );
116
+ }
117
+ }
118
+
119
+ console.log('');
120
+ }
121
+
122
+ function shortPath(path: string): string {
123
+ const home = process.env.HOME || process.env.USERPROFILE || '';
124
+ if (home && path.startsWith(home)) {
125
+ return '~' + path.slice(home.length);
126
+ }
127
+ return path;
128
+ }
@@ -0,0 +1,85 @@
1
+ import { ValidationResult, MCPServerConfig } from '../config/types.js';
2
+ import { getServersFromConfig } from '../config/locations.js';
3
+
4
+ export function validateEnvVars(
5
+ config: Record<string, unknown>,
6
+ configPath: string
7
+ ): ValidationResult[] {
8
+ const results: ValidationResult[] = [];
9
+ const servers = getServersFromConfig(config);
10
+
11
+ if (!servers) {
12
+ return results;
13
+ }
14
+
15
+ for (const [name, serverRaw] of Object.entries(servers)) {
16
+ const server = serverRaw as MCPServerConfig;
17
+
18
+ if (!server.env) continue;
19
+
20
+ for (const [key, value] of Object.entries(server.env)) {
21
+ if (typeof value !== 'string') continue;
22
+
23
+ // Check for ${VAR} or $VAR references
24
+ const varRefs = value.match(/\$\{([A-Z_][A-Z0-9_]*)\}|\$([A-Z_][A-Z0-9_]*)/g) || [];
25
+
26
+ for (const ref of varRefs) {
27
+ const varName = ref.replace(/^\$\{?|\}?$/g, '');
28
+
29
+ if (!process.env[varName]) {
30
+ results.push({
31
+ level: 'warning',
32
+ code: 'ENV_VAR_MISSING',
33
+ message: `Server "${name}": Environment variable ${varName} is not set`,
34
+ file: configPath,
35
+ suggestion: `Set it with: export ${varName}="your-value"`,
36
+ });
37
+ }
38
+ }
39
+
40
+ // Check for empty values that look like they should be set
41
+ if (value === '' || value === '""' || value === "''") {
42
+ results.push({
43
+ level: 'warning',
44
+ code: 'ENV_VAR_EMPTY',
45
+ message: `Server "${name}": Environment variable ${key} is empty`,
46
+ file: configPath,
47
+ suggestion: 'This may cause authentication or configuration issues',
48
+ });
49
+ }
50
+
51
+ // Warn about hardcoded secrets
52
+ const secretKeyPatterns = [
53
+ /^(api[_-]?key|secret|token|password|pwd|auth|credential)/i,
54
+ /_(api[_-]?key|secret|token|password|pwd|auth|credential)$/i,
55
+ ];
56
+
57
+ const secretValuePatterns = [
58
+ /^sk[-_][a-zA-Z0-9]{20,}$/, // OpenAI-style keys
59
+ /^[a-f0-9]{32,}$/i, // Hex strings (32+ chars)
60
+ /^eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/, // JWT tokens
61
+ /^ghp_[a-zA-Z0-9]{36}$/, // GitHub tokens
62
+ /^github_pat_[a-zA-Z0-9_]{22,}/, // GitHub PAT
63
+ /^xoxb-[0-9]{10,}/, // Slack bot tokens
64
+ /^xoxp-[0-9]{10,}/, // Slack user tokens
65
+ /^AKIA[A-Z0-9]{16}$/, // AWS access keys
66
+ ];
67
+
68
+ const isSecretKey = secretKeyPatterns.some((p) => p.test(key));
69
+ const looksLikeSecret = secretValuePatterns.some((p) => p.test(value));
70
+
71
+ // Only warn if it's not a variable reference
72
+ if ((isSecretKey || looksLikeSecret) && !value.includes('$')) {
73
+ results.push({
74
+ level: 'warning',
75
+ code: 'HARDCODED_SECRET',
76
+ message: `Server "${name}": Possible hardcoded secret in ${key}`,
77
+ file: configPath,
78
+ suggestion: 'Use an environment variable reference like ${' + key.toUpperCase() + '} instead',
79
+ });
80
+ }
81
+ }
82
+ }
83
+
84
+ return results;
85
+ }
@@ -0,0 +1,4 @@
1
+ export { validateJsonSyntax } from './json-syntax.js';
2
+ export { validatePaths } from './paths.js';
3
+ export { validateEnvVars } from './env-vars.js';
4
+ export { testServerHealth } from './server-health.js';
@@ -0,0 +1,83 @@
1
+ import { readFileSync } from 'fs';
2
+ import { ValidationResult } from '../config/types.js';
3
+
4
+ export function validateJsonSyntax(filePath: string): { valid: boolean; results: ValidationResult[]; config?: Record<string, unknown> } {
5
+ const results: ValidationResult[] = [];
6
+
7
+ let content: string;
8
+ try {
9
+ content = readFileSync(filePath, 'utf-8');
10
+ } catch (error) {
11
+ results.push({
12
+ level: 'error',
13
+ code: 'FILE_READ_ERROR',
14
+ message: `Cannot read file: ${error instanceof Error ? error.message : 'Unknown error'}`,
15
+ file: filePath,
16
+ });
17
+ return { valid: false, results };
18
+ }
19
+
20
+ // Check for empty file
21
+ if (!content.trim()) {
22
+ results.push({
23
+ level: 'warning',
24
+ code: 'EMPTY_FILE',
25
+ message: 'Config file is empty',
26
+ file: filePath,
27
+ });
28
+ return { valid: true, results, config: {} };
29
+ }
30
+
31
+ // Try to parse JSON
32
+ try {
33
+ const config = JSON.parse(content);
34
+ return { valid: true, results, config };
35
+ } catch (error) {
36
+ if (error instanceof SyntaxError) {
37
+ // Try to find the line number
38
+ const match = error.message.match(/position\s+(\d+)/i);
39
+ let line: number | undefined;
40
+ let column: number | undefined;
41
+
42
+ if (match) {
43
+ const position = parseInt(match[1], 10);
44
+ const lines = content.substring(0, position).split('\n');
45
+ line = lines.length;
46
+ column = lines[lines.length - 1].length + 1;
47
+ }
48
+
49
+ // Check for common issues
50
+ const trailingCommaMatch = content.match(/,\s*([}\]])/);
51
+ if (trailingCommaMatch) {
52
+ // Find line of trailing comma
53
+ const beforeMatch = content.substring(0, content.indexOf(trailingCommaMatch[0]));
54
+ const trailingCommaLine = beforeMatch.split('\n').length;
55
+
56
+ results.push({
57
+ level: 'error',
58
+ code: 'JSON_TRAILING_COMMA',
59
+ message: 'Trailing comma detected (not allowed in JSON)',
60
+ file: filePath,
61
+ line: trailingCommaLine,
62
+ suggestion: 'Remove the comma before the closing bracket/brace',
63
+ });
64
+ } 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
+ results.push({
72
+ level: 'error',
73
+ code: 'JSON_SYNTAX',
74
+ message,
75
+ file: filePath,
76
+ line,
77
+ });
78
+ }
79
+ }
80
+
81
+ return { valid: false, results };
82
+ }
83
+ }
@@ -0,0 +1,140 @@
1
+ import { existsSync, accessSync, constants } from 'fs';
2
+ import { isAbsolute, resolve, dirname } from 'path';
3
+ import { ValidationResult, MCPServerConfig } from '../config/types.js';
4
+ import { getServersFromConfig } from '../config/locations.js';
5
+
6
+ export function validatePaths(
7
+ config: Record<string, unknown>,
8
+ configPath: string
9
+ ): ValidationResult[] {
10
+ const results: ValidationResult[] = [];
11
+ const servers = getServersFromConfig(config);
12
+
13
+ if (!servers) {
14
+ return results;
15
+ }
16
+
17
+ const configDir = dirname(configPath);
18
+
19
+ for (const [name, serverRaw] of Object.entries(servers)) {
20
+ const server = serverRaw as MCPServerConfig;
21
+
22
+ // Skip HTTP/URL-based servers
23
+ if (server.url) continue;
24
+
25
+ // Skip if no command specified
26
+ if (!server.command) {
27
+ results.push({
28
+ level: 'error',
29
+ code: 'MISSING_COMMAND',
30
+ message: `Server "${name}": No command specified`,
31
+ file: configPath,
32
+ suggestion: 'Add a "command" field specifying how to start the server',
33
+ });
34
+ continue;
35
+ }
36
+
37
+ const command = server.command;
38
+
39
+ // Check if command is a path (contains / or \)
40
+ if (command.includes('/') || command.includes('\\')) {
41
+ const resolvedPath = isAbsolute(command)
42
+ ? command
43
+ : resolve(configDir, command);
44
+
45
+ // Check existence
46
+ if (!existsSync(resolvedPath)) {
47
+ results.push({
48
+ level: 'error',
49
+ code: 'PATH_NOT_FOUND',
50
+ message: `Server "${name}": Command path does not exist: ${command}`,
51
+ file: configPath,
52
+ suggestion: `Check the path is correct. Resolved to: ${resolvedPath}`,
53
+ });
54
+ } else {
55
+ // Check executable permission (Unix only)
56
+ if (process.platform !== 'win32') {
57
+ try {
58
+ accessSync(resolvedPath, constants.X_OK);
59
+ } catch {
60
+ results.push({
61
+ level: 'error',
62
+ code: 'PATH_NOT_EXECUTABLE',
63
+ message: `Server "${name}": Command is not executable: ${command}`,
64
+ file: configPath,
65
+ suggestion: `Run: chmod +x "${resolvedPath}"`,
66
+ });
67
+ }
68
+ }
69
+ }
70
+
71
+ // Warn about relative paths
72
+ if (!isAbsolute(command)) {
73
+ results.push({
74
+ level: 'warning',
75
+ code: 'PATH_RELATIVE',
76
+ message: `Server "${name}": Using relative path "${command}"`,
77
+ file: configPath,
78
+ suggestion: `Consider using absolute path: ${resolvedPath}`,
79
+ });
80
+ }
81
+ } else {
82
+ // Command is not a path (e.g., "npx", "node", "python")
83
+ // We can't easily validate these as they depend on PATH
84
+ // But we can warn if it looks like a typo
85
+
86
+ const commonCommands = ['npx', 'node', 'python', 'python3', 'uv', 'uvx', 'deno', 'bun'];
87
+ const looksLikePath = command.includes('.') && !commonCommands.includes(command);
88
+
89
+ if (looksLikePath) {
90
+ results.push({
91
+ level: 'warning',
92
+ code: 'POSSIBLE_PATH_TYPO',
93
+ message: `Server "${name}": "${command}" looks like a filename but isn't a path`,
94
+ file: configPath,
95
+ suggestion: 'If this is a file, use a full path like "./server.js" or "/path/to/server.js"',
96
+ });
97
+ }
98
+ }
99
+
100
+ // Check cwd if specified
101
+ if (server.cwd) {
102
+ const cwdPath = isAbsolute(server.cwd)
103
+ ? server.cwd
104
+ : resolve(configDir, server.cwd);
105
+
106
+ if (!existsSync(cwdPath)) {
107
+ results.push({
108
+ level: 'error',
109
+ code: 'CWD_NOT_FOUND',
110
+ message: `Server "${name}": Working directory does not exist: ${server.cwd}`,
111
+ file: configPath,
112
+ suggestion: `Create the directory or update the path. Resolved to: ${cwdPath}`,
113
+ });
114
+ }
115
+ }
116
+
117
+ // Check args for paths
118
+ if (server.args && Array.isArray(server.args)) {
119
+ for (const arg of server.args) {
120
+ // If arg looks like an absolute path, check it exists
121
+ if (typeof arg === 'string' && isAbsolute(arg) && (arg.includes('/') || arg.includes('\\'))) {
122
+ // Only check if it looks like a file path (not a URL or flag)
123
+ if (!arg.startsWith('-') && !arg.startsWith('http')) {
124
+ if (!existsSync(arg)) {
125
+ results.push({
126
+ level: 'warning',
127
+ code: 'ARG_PATH_NOT_FOUND',
128
+ message: `Server "${name}": Argument path may not exist: ${arg}`,
129
+ file: configPath,
130
+ suggestion: 'Verify this path is correct',
131
+ });
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ return results;
140
+ }