@iflow-mcp/ejmockler-brutalist
Version:
Deploy Claude, Codex & Gemini CLI agents to demolish your work before users do. Real file analysis. Brutal honesty. Now with conversation continuation & intelligent pagination.
170 lines • 7.11 kB
JavaScript
/**
* MCP Server Registry — manages which MCP servers are available to spawned CLI agents.
*
* Brutalist generates per-CLI MCP configurations from this registry so that
* adversarial agents can use tools (e.g. Playwright) for evidence-backed
* analysis while remaining unable to modify the codebase.
*/
import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import { exec } from 'child_process';
import { promisify } from 'util';
import { logger } from './logger.js';
const execAsync = promisify(exec);
// ── Registry ───────────────────────────────────────────────────────────────
/** Built-in known MCP servers. Extend this as new integrations are added. */
const BUILTIN_SERVERS = {
playwright: {
command: 'npx',
args: ['@playwright/mcp@latest'],
},
};
/**
* Merge built-in servers with user-defined servers from the
* `BRUTALIST_MCP_SERVERS` env var (JSON object keyed by server name).
*/
function loadRegistry() {
const registry = { ...BUILTIN_SERVERS };
const envServers = process.env.BRUTALIST_MCP_SERVERS;
if (envServers) {
try {
const parsed = JSON.parse(envServers);
for (const [name, spec] of Object.entries(parsed)) {
if (spec?.command) {
registry[name] = spec;
}
}
}
catch (e) {
logger.warn('Failed to parse BRUTALIST_MCP_SERVERS env var:', e);
}
}
return registry;
}
/** Resolve server specs by name from the registry. Unknown names are skipped with a warning. */
export function resolveServers(names) {
const registry = loadRegistry();
const resolved = {};
for (const name of names) {
const spec = registry[name];
if (spec) {
resolved[name] = spec;
}
else {
logger.warn(`MCP server "${name}" not found in registry — skipping`);
}
}
return resolved;
}
/** Return all registered server names (for discovery / cli_agent_roster). */
export function listRegisteredServers() {
return Object.keys(loadRegistry());
}
// ── Playwright: auto-install browsers ───────────────────────────────────────
/**
* Ensure Playwright browsers are installed. Runs `npx playwright install chromium`
* once per process — subsequent calls return the same promise. The command is
* idempotent: if browsers are already present it completes in ~1s with no download.
* Without browsers, the Playwright MCP server launches but immediately fails.
*/
let playwrightInstallPromise = null;
export function ensurePlaywrightBrowsers() {
if (playwrightInstallPromise)
return playwrightInstallPromise;
playwrightInstallPromise = (async () => {
logger.info('🎭 Ensuring Playwright chromium browser is installed...');
try {
await execAsync('npx playwright install chromium', { timeout: 120_000 });
logger.info('✅ Playwright chromium browser ready');
}
catch (e) {
logger.warn('⚠️ Failed to install Playwright chromium browser:', e);
// Don't block the critique — Playwright MCP will fail gracefully
// and critics will fall back to source-only analysis
}
})();
return playwrightInstallPromise;
}
// ── Claude: temp JSON config file ──────────────────────────────────────────
/**
* Write a temporary Claude MCP config file and return its path.
* Caller is responsible for cleanup via `cleanupTempConfig`.
*/
export async function writeClaudeMCPConfig(servers, sessionId) {
const config = { mcpServers: {} };
for (const [name, spec] of Object.entries(servers)) {
config.mcpServers[name] = {
command: spec.command,
args: spec.args,
...(spec.env && { env: spec.env }),
};
}
const tmpDir = os.tmpdir();
const filename = `brutalist-mcp-${sessionId}-${Date.now()}.json`;
const filepath = path.join(tmpDir, filename);
await fs.writeFile(filepath, JSON.stringify(config, null, 2), 'utf-8');
logger.info(`Wrote Claude MCP config: ${filepath}`);
return filepath;
}
/** Remove a temp config file. Swallows errors (file may already be gone). */
export async function cleanupTempConfig(filepath) {
try {
await fs.unlink(filepath);
logger.info(`Cleaned up MCP config: ${filepath}`);
}
catch {
// Already removed or never created — fine
}
}
// ── Codex: -c config override string ───────────────────────────────────────
/**
* Build a TOML-compatible value string for Codex's `-c mcp_servers=...` flag.
* This **replaces** Codex's configured servers entirely (excluding brutalist).
*
* Codex parses the value as TOML, so we produce an inline-table representation:
* {playwright={command="npx", args=["@playwright/mcp@latest"]}}
*/
export function buildCodexMCPOverride(servers) {
const entries = Object.entries(servers).map(([name, spec]) => {
const args = spec.args.map(a => `"${a}"`).join(', ');
return `${name}={command="${spec.command}", args=[${args}]}`;
});
return `{${entries.join(', ')}}`;
}
// ── Gemini: ensure servers are pre-configured ──────────────────────────────
/**
* Ensure the requested MCP servers are configured in Gemini CLI.
* Runs `gemini mcp list` to check, then `gemini mcp add` for missing ones.
* Idempotent — safe to call on every invocation.
*/
export async function ensureGeminiMCPServers(servers) {
let existingNames;
try {
const { stdout } = await execAsync('gemini mcp list', { timeout: 10_000 });
// Parse the table output — server names are in the first column
existingNames = new Set(stdout.split('\n')
.filter(line => line.trim() && !line.startsWith('Name') && !line.startsWith('Loaded'))
.map(line => line.split(/\s+/)[0])
.filter(Boolean));
}
catch {
logger.warn('Could not list Gemini MCP servers — skipping pre-configuration');
return;
}
for (const [name, spec] of Object.entries(servers)) {
if (existingNames.has(name)) {
logger.info(`Gemini MCP server "${name}" already configured`);
continue;
}
try {
const args = spec.args.map(a => `"${a}"`).join(' ');
await execAsync(`gemini mcp add ${name} ${spec.command} ${args}`, { timeout: 15_000 });
logger.info(`Added Gemini MCP server: ${name}`);
}
catch (e) {
logger.warn(`Failed to add Gemini MCP server "${name}":`, e);
}
}
}
//# sourceMappingURL=mcp-registry.js.map