mirror of
https://github.com/jarrodwatts/claude-hud.git
synced 2026-05-09 15:12:44 +00:00
* fix: exclude disabled MCP servers from count Fixes #3 - MCP count was showing all servers regardless of enabled/disabled state. ## Root Cause The HUD was counting all MCP server keys from config files without checking if they were in the disabled lists: - `disabledMcpServers` in `~/.claude.json` (user-scope) - `disabledMcpjsonServers` in `settings.local.json` (project .mcp.json) ## Changes - Add `getDisabledMcpServers()` function to read disabled arrays - Add `DisabledMcpKey` union type for compile-time safety - Filter disabled servers when counting MCPs - Add debug logging (enable via `DEBUG=claude-hud`) - Add 8 new tests including Issue #3 regression test ## Test Coverage - User-scope disabled filtering - Project .mcp.json disabled filtering - All MCPs disabled → 0 - Non-string values in disabled arrays (ignored) - Cross-scope duplicate counting behavior - Case-sensitive server name matching - Issue #3 exact scenario (6 MCPs, disable progressively) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract debug logging into shared helper module Per PR feedback, consolidate duplicate debug logging code from config-reader.ts and usage-api.ts into a shared debug.ts helper. - Add src/debug.ts with createDebug() factory function - Update config-reader.ts to use createDebug('config') - Update usage-api.ts to use createDebug('usage') - Same functionality, better DRY compliance 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: melon-hub <melon-hub@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
198 lines
6.1 KiB
TypeScript
198 lines
6.1 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import { createDebug } from './debug.js';
|
|
|
|
const debug = createDebug('config');
|
|
|
|
export interface ConfigCounts {
|
|
claudeMdCount: number;
|
|
rulesCount: number;
|
|
mcpCount: number;
|
|
hooksCount: number;
|
|
}
|
|
|
|
// Valid keys for disabled MCP arrays in config files
|
|
type DisabledMcpKey = 'disabledMcpServers' | 'disabledMcpjsonServers';
|
|
|
|
function getMcpServerNames(filePath: string): Set<string> {
|
|
if (!fs.existsSync(filePath)) return new Set();
|
|
try {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const config = JSON.parse(content);
|
|
if (config.mcpServers && typeof config.mcpServers === 'object') {
|
|
return new Set(Object.keys(config.mcpServers));
|
|
}
|
|
} catch (error) {
|
|
debug(`Failed to read MCP servers from ${filePath}:`, error);
|
|
}
|
|
return new Set();
|
|
}
|
|
|
|
function getDisabledMcpServers(filePath: string, key: DisabledMcpKey): Set<string> {
|
|
if (!fs.existsSync(filePath)) return new Set();
|
|
try {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const config = JSON.parse(content);
|
|
if (Array.isArray(config[key])) {
|
|
const validNames = config[key].filter((s: unknown) => typeof s === 'string');
|
|
if (validNames.length !== config[key].length) {
|
|
debug(`${key} in ${filePath} contains non-string values, ignoring them`);
|
|
}
|
|
return new Set(validNames);
|
|
}
|
|
} catch (error) {
|
|
debug(`Failed to read ${key} from ${filePath}:`, error);
|
|
}
|
|
return new Set();
|
|
}
|
|
|
|
function countMcpServersInFile(filePath: string, excludeFrom?: string): number {
|
|
const servers = getMcpServerNames(filePath);
|
|
if (excludeFrom) {
|
|
const exclude = getMcpServerNames(excludeFrom);
|
|
for (const name of exclude) {
|
|
servers.delete(name);
|
|
}
|
|
}
|
|
return servers.size;
|
|
}
|
|
|
|
function countHooksInFile(filePath: string): number {
|
|
if (!fs.existsSync(filePath)) return 0;
|
|
try {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const config = JSON.parse(content);
|
|
if (config.hooks && typeof config.hooks === 'object') {
|
|
return Object.keys(config.hooks).length;
|
|
}
|
|
} catch (error) {
|
|
debug(`Failed to read hooks from ${filePath}:`, error);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function countRulesInDir(rulesDir: string): number {
|
|
if (!fs.existsSync(rulesDir)) return 0;
|
|
let count = 0;
|
|
try {
|
|
const entries = fs.readdirSync(rulesDir, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(rulesDir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
count += countRulesInDir(fullPath);
|
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
count++;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
debug(`Failed to read rules from ${rulesDir}:`, error);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
export async function countConfigs(cwd?: string): Promise<ConfigCounts> {
|
|
let claudeMdCount = 0;
|
|
let rulesCount = 0;
|
|
let hooksCount = 0;
|
|
|
|
const homeDir = os.homedir();
|
|
const claudeDir = path.join(homeDir, '.claude');
|
|
|
|
// Collect all MCP servers across scopes, then subtract disabled ones
|
|
const userMcpServers = new Set<string>();
|
|
const projectMcpServers = new Set<string>();
|
|
|
|
// === USER SCOPE ===
|
|
|
|
// ~/.claude/CLAUDE.md
|
|
if (fs.existsSync(path.join(claudeDir, 'CLAUDE.md'))) {
|
|
claudeMdCount++;
|
|
}
|
|
|
|
// ~/.claude/rules/*.md
|
|
rulesCount += countRulesInDir(path.join(claudeDir, 'rules'));
|
|
|
|
// ~/.claude/settings.json (MCPs and hooks)
|
|
const userSettings = path.join(claudeDir, 'settings.json');
|
|
for (const name of getMcpServerNames(userSettings)) {
|
|
userMcpServers.add(name);
|
|
}
|
|
hooksCount += countHooksInFile(userSettings);
|
|
|
|
// ~/.claude.json (additional user-scope MCPs)
|
|
const userClaudeJson = path.join(homeDir, '.claude.json');
|
|
for (const name of getMcpServerNames(userClaudeJson)) {
|
|
userMcpServers.add(name);
|
|
}
|
|
|
|
// Get disabled user-scope MCPs from ~/.claude.json
|
|
const disabledUserMcps = getDisabledMcpServers(userClaudeJson, 'disabledMcpServers');
|
|
for (const name of disabledUserMcps) {
|
|
userMcpServers.delete(name);
|
|
}
|
|
|
|
// === PROJECT SCOPE ===
|
|
|
|
if (cwd) {
|
|
// {cwd}/CLAUDE.md
|
|
if (fs.existsSync(path.join(cwd, 'CLAUDE.md'))) {
|
|
claudeMdCount++;
|
|
}
|
|
|
|
// {cwd}/CLAUDE.local.md
|
|
if (fs.existsSync(path.join(cwd, 'CLAUDE.local.md'))) {
|
|
claudeMdCount++;
|
|
}
|
|
|
|
// {cwd}/.claude/CLAUDE.md (alternative location)
|
|
if (fs.existsSync(path.join(cwd, '.claude', 'CLAUDE.md'))) {
|
|
claudeMdCount++;
|
|
}
|
|
|
|
// {cwd}/.claude/CLAUDE.local.md
|
|
if (fs.existsSync(path.join(cwd, '.claude', 'CLAUDE.local.md'))) {
|
|
claudeMdCount++;
|
|
}
|
|
|
|
// {cwd}/.claude/rules/*.md (recursive)
|
|
rulesCount += countRulesInDir(path.join(cwd, '.claude', 'rules'));
|
|
|
|
// {cwd}/.mcp.json (project MCP config) - tracked separately for disabled filtering
|
|
const mcpJsonServers = getMcpServerNames(path.join(cwd, '.mcp.json'));
|
|
|
|
// {cwd}/.claude/settings.json (project settings)
|
|
const projectSettings = path.join(cwd, '.claude', 'settings.json');
|
|
for (const name of getMcpServerNames(projectSettings)) {
|
|
projectMcpServers.add(name);
|
|
}
|
|
hooksCount += countHooksInFile(projectSettings);
|
|
|
|
// {cwd}/.claude/settings.local.json (local project settings)
|
|
const localSettings = path.join(cwd, '.claude', 'settings.local.json');
|
|
for (const name of getMcpServerNames(localSettings)) {
|
|
projectMcpServers.add(name);
|
|
}
|
|
hooksCount += countHooksInFile(localSettings);
|
|
|
|
// Get disabled .mcp.json servers from settings.local.json
|
|
const disabledMcpJsonServers = getDisabledMcpServers(localSettings, 'disabledMcpjsonServers');
|
|
for (const name of disabledMcpJsonServers) {
|
|
mcpJsonServers.delete(name);
|
|
}
|
|
|
|
// Add remaining .mcp.json servers to project set
|
|
for (const name of mcpJsonServers) {
|
|
projectMcpServers.add(name);
|
|
}
|
|
}
|
|
|
|
// Total MCP count = user servers + project servers
|
|
// Note: Deduplication only occurs within each scope, not across scopes.
|
|
// A server with the same name in both user and project scope counts as 2 (separate configs).
|
|
const mcpCount = userMcpServers.size + projectMcpServers.size;
|
|
|
|
return { claudeMdCount, rulesCount, mcpCount, hooksCount };
|
|
}
|
|
|