@build000r/mcp-mcp
Version:
Fast CLI tool to scan, view, park (JIT), and interactively manage MCP server configurations across all Claude projects
152 lines (127 loc) • 4.83 kB
JavaScript
const fs = require('fs');
const path = require('path');
const os = require('os');
const { promisify } = require('util');
const readdir = promisify(fs.readdir);
const ConfigParser = require('./ConfigParser');
const PathUtils = require('../utils/PathUtils');
class ConfigScanner {
constructor() {
const patterns = PathUtils.getConfigPatterns();
this.skipDirPattern = patterns.skipDirPattern;
this.configFilePattern = patterns.configFilePattern;
}
async findConfigs(mode = 'upstream') {
const configs = [];
const homeDir = os.homedir();
// Always check main Claude config first
const homeConfigPath = path.join(homeDir, '.claude.json');
if (fs.existsSync(homeConfigPath)) {
const homeConfigs = await ConfigParser.parseClaudeConfig(homeConfigPath);
configs.push(...homeConfigs);
}
if (mode === 'upstream') {
// Scan from current directory up to home
await this.scanUpstream(process.cwd(), homeDir, configs);
} else {
// Scan entire computer (common directories)
await this.scanAll(homeDir, configs);
}
// Remove duplicates and sort
const uniqueConfigs = configs.filter((config, index, self) =>
index === self.findIndex(c => c.path === config.path && c.configType === config.configType)
);
return uniqueConfigs.sort((a, b) => a.path.length - b.path.length);
}
async scanUpstream(startDir, homeDir, configs) {
const visited = new Set();
let currentDir = startDir;
// Walk up the directory tree from current to home
while (currentDir && currentDir.startsWith(homeDir)) {
if (!visited.has(currentDir)) {
visited.add(currentDir);
await this.scanSingleDirectory(currentDir, configs);
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) break; // Reached root
currentDir = parentDir;
}
// Also scan the current directory's immediate subdirectories
await this.scanDirectoryRecursive(startDir, configs, visited, 2);
}
async scanAll(homeDir, configs) {
const visited = new Set();
const searchDirs = PathUtils.getSearchDirectories(homeDir);
// Scan all directories in parallel for speed
const scanPromises = [];
for (const dir of searchDirs) {
if (fs.existsSync(dir) && !visited.has(dir)) {
scanPromises.push(this.scanDirectoryRecursive(dir, configs, visited, 5));
}
}
await Promise.all(scanPromises);
}
async scanSingleDirectory(dir, configs) {
try {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && this.configFilePattern.test(entry.name)) {
const filePath = path.join(dir, entry.name);
const config = await ConfigParser.parseMCPConfigFile(filePath, entry.name);
if (config) {
configs.push({
path: dir,
configPath: filePath,
config: config,
scope: PathUtils.determineScope(dir),
configType: entry.name
});
}
}
}
} catch (error) {
// Skip directories we can't read
}
}
async scanDirectoryRecursive(dir, configs, visited, maxDepth, currentDepth = 0) {
if (currentDepth > maxDepth || visited.has(dir)) return;
visited.add(dir);
try {
const entries = await readdir(dir, { withFileTypes: true });
const subDirPromises = [];
// Process files first
for (const entry of entries) {
if (entry.isFile() && this.configFilePattern.test(entry.name)) {
const filePath = path.join(dir, entry.name);
const config = await ConfigParser.parseMCPConfigFile(filePath, entry.name);
if (config) {
configs.push({
path: dir,
configPath: filePath,
config: config,
scope: PathUtils.determineScope(dir),
configType: entry.name
});
}
}
}
// Then process subdirectories in parallel
for (const entry of entries) {
if (entry.isDirectory() && !this.skipDirPattern.test(entry.name) && !entry.name.startsWith('.')) {
const subDir = path.join(dir, entry.name);
subDirPromises.push(
this.scanDirectoryRecursive(subDir, configs, visited, maxDepth, currentDepth + 1)
);
}
}
// Limit parallelism
const batchSize = 5;
for (let i = 0; i < subDirPromises.length; i += batchSize) {
await Promise.all(subDirPromises.slice(i, i + batchSize));
}
} catch (error) {
// Skip directories we can't read
}
}
}
module.exports = ConfigScanner;