@mcp-shark/mcp-shark
Version:
Aggregate multiple Model Context Protocol (MCP) servers into a single unified interface with a powerful monitoring UI. Prov deep visibility into every request and response.
283 lines (271 loc) • 10.9 kB
JavaScript
import { execSync } from 'child_process';
import * as path from 'node:path';
import * as fs from 'node:fs';
import * as os from 'node:os';
/**
* Get system PATH from the host machine's shell environment
* This works in Electron by executing a shell command to get the actual PATH
* Includes both system PATH and user's custom PATH from shell config files
*/
function getSystemPath() {
try {
if (process.platform === 'win32') {
// Windows: use cmd to get PATH (includes user PATH)
const pathOutput = execSync('cmd /c echo %PATH%', {
encoding: 'utf8',
timeout: 2000,
stdio: ['ignore', 'pipe', 'ignore'],
});
return pathOutput.trim();
} else {
// Unix-like: use shell to get PATH from user's actual shell environment
// Try to detect the user's default shell first
const userShell = process.env.SHELL || '/bin/zsh';
const shells = [userShell, '/bin/zsh', '/bin/bash', '/bin/sh'];
for (const shell of shells) {
if (fs.existsSync(shell)) {
try {
// For zsh, we need to load both login and interactive configs
// zsh -l loads .zprofile/.zlogin, but .zshrc has interactive configs
// Try interactive mode first (loads .zshrc), then login mode
const shellName = path.basename(shell);
let pathOutput;
if (shellName === 'zsh') {
// For zsh, try interactive mode to get .zshrc PATH additions
try {
pathOutput = execSync(`${shell} -i -c 'echo $PATH'`, {
encoding: 'utf8',
timeout: 2000,
stdio: ['ignore', 'pipe', 'ignore'],
maxBuffer: 1024 * 1024,
env: {
...Object.fromEntries(
Object.entries(process.env).filter(([key]) => key !== 'PATH')
),
},
});
} catch (_e) {
// Fallback to login shell
pathOutput = execSync(`${shell} -l -c 'echo $PATH'`, {
encoding: 'utf8',
timeout: 2000,
stdio: ['ignore', 'pipe', 'ignore'],
maxBuffer: 1024 * 1024,
env: {
...Object.fromEntries(
Object.entries(process.env).filter(([key]) => key !== 'PATH')
),
},
});
}
} else {
// For bash/sh, use login shell
pathOutput = execSync(`${shell} -l -c 'echo $PATH'`, {
encoding: 'utf8',
timeout: 2000,
stdio: ['ignore', 'pipe', 'ignore'],
maxBuffer: 1024 * 1024,
env: {
...Object.fromEntries(
Object.entries(process.env).filter(([key]) => key !== 'PATH')
),
},
});
}
const systemPath = pathOutput.trim();
if (systemPath) {
console.log(`[Server Manager] Got PATH from ${shell} (${shellName})`);
return systemPath;
}
} catch (_e) {
// Try next shell
continue;
}
}
}
// Fallback: try to read from common shell config files
// For zsh, check .zshrc first (interactive), then .zprofile (login)
const homeDir = os.homedir();
const configFiles = [
{ file: path.join(homeDir, '.zshrc'), shell: 'zsh', interactive: true },
{ file: path.join(homeDir, '.zprofile'), shell: 'zsh', interactive: false },
{ file: path.join(homeDir, '.zlogin'), shell: 'zsh', interactive: false },
{ file: path.join(homeDir, '.bashrc'), shell: 'bash', interactive: true },
{ file: path.join(homeDir, '.bash_profile'), shell: 'bash', interactive: false },
{ file: path.join(homeDir, '.profile'), shell: 'sh', interactive: false },
];
for (const { file, shell: shellName, interactive } of configFiles) {
if (fs.existsSync(file)) {
try {
// For zsh interactive configs, use -i flag
const flag = shellName === 'zsh' && interactive ? '-i' : '';
const pathOutput = execSync(
`/bin/${shellName} ${flag} -c 'source ${file} 2>/dev/null; echo $PATH'`,
{
encoding: 'utf8',
timeout: 2000,
stdio: ['ignore', 'pipe', 'ignore'],
maxBuffer: 1024 * 1024,
env: {
...Object.fromEntries(
Object.entries(process.env).filter(([key]) => key !== 'PATH')
),
},
}
);
const systemPath = pathOutput.trim();
if (systemPath && systemPath.length > 10) {
// Only use if we got a meaningful PATH
console.log(`[Server Manager] Got PATH from ${file}`);
return systemPath;
}
} catch (_e) {
// Try next config file
continue;
}
}
}
}
} catch (error) {
console.warn('[Server Manager] Could not get system PATH:', error.message);
}
return null;
}
/**
* Enhance PATH environment variable to include system paths and user paths
* This is especially important in Electron where PATH might not include system executables
*/
export function enhancePath(originalPath) {
const homeDir = os.homedir();
const pathSeparator = process.platform === 'win32' ? ';' : ':';
// Try to get the actual system PATH from the host (includes user's custom PATH)
const systemPath = getSystemPath();
if (systemPath) {
console.log('[Server Manager] Using system PATH from host machine');
// Combine system PATH with original PATH, prioritizing system PATH
// Also add user-specific paths that might not be in system PATH
const userPaths = [
// Common user-specific binary locations
path.join(homeDir, '.local', 'bin'),
path.join(homeDir, '.npm-global', 'bin'),
path.join(homeDir, '.cargo', 'bin'),
path.join(homeDir, 'bin'),
// Node version managers
path.join(homeDir, '.nvm', 'current', 'bin'),
// Try to find actual nvm node version (check common versions)
...(function () {
try {
const nvmVersionsPath = path.join(homeDir, '.nvm', 'versions', 'node');
if (fs.existsSync(nvmVersionsPath)) {
return fs
.readdirSync(nvmVersionsPath, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => path.join(nvmVersionsPath, dirent.name, 'bin'));
}
} catch (_e) {
// Ignore errors reading nvm directory
}
return [];
})(),
path.join(homeDir, '.fnm', 'node-versions', 'v20.0.0', 'install', 'bin'), // fnm
// Python version managers
path.join(homeDir, '.pyenv', 'shims'),
path.join(homeDir, '.pyenv', 'bin'),
// Go version managers
path.join(homeDir, '.gvm', 'bin'),
path.join(homeDir, '.gvm', 'gos', 'current', 'bin'),
// Rust/Cargo
path.join(homeDir, '.cargo', 'bin'),
// Go
path.join(homeDir, 'go', 'bin'),
path.join(homeDir, '.go', 'bin'),
// iTerm utilities
'/Applications/iTerm.app/Contents/Resources/utilities',
// Windows user paths
...(process.platform === 'win32'
? [
path.join(homeDir, 'AppData', 'Local', 'Programs'),
path.join(homeDir, 'AppData', 'Roaming', 'npm'),
path.join(homeDir, 'AppData', 'Local', 'Microsoft', 'WindowsApps'),
]
: []),
].filter((p) => {
// Filter out paths that don't exist, but allow dynamic version paths
if (p.includes('v20.0.0') || p.includes('current')) {
// For version manager paths, check if parent directory exists
return fs.existsSync(path.dirname(p));
}
return fs.existsSync(p);
});
// Combine: system PATH (from shell) + user-specific paths + original PATH
return [systemPath, ...userPaths, originalPath || ''].filter((p) => p).join(pathSeparator);
}
// Fallback: add common system and user locations
console.log('[Server Manager] Could not get system PATH, adding common locations');
const pathsToAdd = [
// System binary locations
'/usr/local/bin',
'/usr/bin',
'/opt/homebrew/bin',
'/usr/local/opt/node/bin',
'/opt/local/bin',
'/sbin',
'/usr/sbin',
// macOS specific
...(process.platform === 'darwin'
? [
'/opt/homebrew/opt/python/bin',
'/usr/local/opt/python/bin',
'/Applications/Docker.app/Contents/Resources/bin',
]
: []),
// Linux specific
...(process.platform === 'linux' ? ['/snap/bin', path.join(homeDir, '.local', 'bin')] : []),
// Windows specific
...(process.platform === 'win32'
? [
path.join(process.env.ProgramFiles || '', 'nodejs'),
path.join(process.env['ProgramFiles(x86)'] || '', 'nodejs'),
path.join(homeDir, 'AppData', 'Roaming', 'npm'),
path.join(process.env.ProgramFiles || '', 'Docker', 'Docker', 'resources', 'bin'),
]
: []),
// User-specific paths (prioritize these)
path.join(homeDir, '.local', 'bin'),
path.join(homeDir, '.npm-global', 'bin'),
path.join(homeDir, '.cargo', 'bin'),
path.join(homeDir, 'bin'),
path.join(homeDir, '.nvm', 'current', 'bin'),
// Try to find actual nvm node version (check common versions)
...(function () {
try {
const nvmVersionsPath = path.join(homeDir, '.nvm', 'versions', 'node');
if (fs.existsSync(nvmVersionsPath)) {
return fs
.readdirSync(nvmVersionsPath, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => path.join(nvmVersionsPath, dirent.name, 'bin'));
}
} catch (_e) {
// Ignore errors reading nvm directory
}
return [];
})(),
path.join(homeDir, '.pyenv', 'shims'),
path.join(homeDir, '.pyenv', 'bin'),
path.join(homeDir, '.gvm', 'bin'),
path.join(homeDir, '.gvm', 'gos', 'current', 'bin'),
path.join(homeDir, 'go', 'bin'),
path.join(homeDir, '.go', 'bin'),
// iTerm utilities
'/Applications/iTerm.app/Contents/Resources/utilities',
// Windows user paths
...(process.platform === 'win32'
? [
path.join(homeDir, 'AppData', 'Local', 'Programs'),
path.join(homeDir, 'AppData', 'Local', 'Microsoft', 'WindowsApps'),
]
: []),
].filter((p) => p && fs.existsSync(p));
return [...pathsToAdd, originalPath || ''].filter((p) => p).join(pathSeparator);
}