UNPKG

@briefhq/mcp-server

Version:

Brief MCP server and CLI – connect Cursor/Claude MCP to your Brief organization

836 lines (835 loc) 42.3 kB
#!/usr/bin/env node import { Command, Option } from "commander"; import { createRequire } from "node:module"; import { resolve } from "node:path"; import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; import { execSync } from "node:child_process"; import { saveCredentials, deleteCredentials, getCredentials } from "./lib/auth.js"; import { testConnection } from "./lib/supabase.js"; import { detectRepo } from "./lib/init/repo.js"; import { hasClaudeCLI, composeClaudeAddCommand } from "./lib/init/claude.js"; import { getCodexConfigPath, readCodexConfig, mergeBriefIntoCodexConfig, writeCodexConfig, countBriefBlocks } from "./lib/init/codex.js"; import { getCursorDeeplink, getCursorExampleConfig } from "./lib/init/cursor.js"; import { getClaudeBriefPath, ensureParentDir } from "./lib/init/claudeDesktop.js"; const FALLBACK_GUIDELINES_CONTENT = [ "# Brief Guidelines for AI Coding Agents", "", "This repository uses Brief to provide comprehensive document management, decision tracking, and business context via the Model Context Protocol (MCP).", "", "## CRITICAL: All Brief MCP tools reference @brief-guidelines.md", "The tool descriptions contain references to @brief-guidelines.md - this file provides the complete workflow, especially for destructive operations requiring user confirmation.", "", "## Core Principles", "1. Full API Coverage: Access all Brief v1 API operations through MCP tools", "2. Permission Required: ALWAYS ask for explicit permission before destructive operations", "3. Clear Communication: Be concise but complete - explain what will happen", "", "## Layered MCP Tools", "- brief_discover_capabilities: Discovery layer (use sparingly) - see @brief-guidelines.md", "- brief_plan_operation: Planning layer (skip for direct commands)", "- brief_prepare_context: For content retrieval, NOT destructive previews", "- brief_execute_operation: Execution layer - MUST follow @brief-guidelines.md confirmation workflow", "", "## Critical Rules", "- Destructive operations (delete, bulk operations) REQUIRE confirmation token", "- Show clear preview of what will be affected before destructive operations", "- Documents default to My Documents folder if not specified", "- IMPORTANT: confirmation_token is a SEPARATE parameter, NOT parameters.token", "- Wait 5+ seconds before using confirmation tokens (security requirement)", "", ].join("\n"); const FALLBACK_CLAUDE_BRIEF_CONTENT = [ "# /brief Onboarding", "", "Use /brief to set up or invoke Brief tools in Claude Code.", "", "If MCP is not installed, run: npx -y @briefhq/mcp-server@latest init --app claude --scope user --yes --write", "", ].join("\n"); function loadTemplate(relativePath, fallbackContent) { try { const url = new URL(relativePath, import.meta.url); return { content: readFileSync(url, { encoding: "utf8" }), fromBundle: true }; } catch { return { content: fallbackContent, fromBundle: false }; } } function logCursorGuidance(printOnly) { const heading = printOnly ? "(print-only) Cursor deeplink:\n" : "\nCursor deeplink (click or open in a browser):\n"; console.log(heading); const deeplink = getCursorDeeplink(); console.log(`${deeplink}\n`); console.log("Cursor manual configuration (if needed):\n"); console.log(JSON.stringify(getCursorExampleConfig(), null, 2)); } function logGenericMcpConfig(printOnly) { const cfg = { mcpServers: { brief: { command: "npx", args: ["-y", "@briefhq/mcp-server@latest", "serve"], }, }, }; console.log(printOnly ? "(print-only) Generic MCP configuration to add:\n" : "\nGeneric MCP configuration (add to your tool's config):\n"); console.log(JSON.stringify(cfg, null, 2)); if (!printOnly) console.log(""); } function logClaudeDesktopInstructions(printOnly) { console.log(printOnly ? "(print-only) Claude Desktop installation steps:\n" : "\nClaude Desktop installation steps:\n"); console.log("1. Download the Brief Claude Desktop Extension (.DXT) from your Brief organization's Integrations page."); console.log("2. Open Claude Desktop → Settings → Extensions."); console.log("3. Drag and drop the .DXT file to install."); console.log("4. When prompted, configure with your organization and API key."); console.log("5. Restart Claude Desktop to complete installation."); console.log("\nTip: The Integrations page in the Brief web app includes the Claude Desktop install button/section; use that for one-click download.\n"); } /** * Automatically update .brief/brief-guidelines.md if it exists and is outdated. * This runs silently on server start to keep guidelines up-to-date. */ function autoUpdateGuidelinesIfNeeded() { try { const cwd = process.cwd(); const briefDir = resolve(cwd, ".brief"); const guidelinesPath = resolve(briefDir, "brief-guidelines.md"); // Only update if both .brief directory and guidelines file exist if (!existsSync(briefDir) || !existsSync(guidelinesPath)) { return; // No guidelines to update } const { content: template, fromBundle } = loadTemplate("./templates/brief-guidelines.md", FALLBACK_GUIDELINES_CONTENT); const current = readFileSync(guidelinesPath, { encoding: "utf8" }); // Check if file is already up-to-date (exact match) if (current === template) { return; // Already up-to-date } // Check if this looks like a Brief guidelines file const hasBriefHeader = /^#\s*Brief Guidelines for AI Coding Agents\b/im.test(current); if (!hasBriefHeader) { return; // Not a Brief guidelines file, don't touch it } // Create a backup with timestamp const backupPath = `${guidelinesPath}.bak.${Date.now()}`; writeFileSync(backupPath, current, { encoding: "utf8" }); // Update to latest version writeFileSync(guidelinesPath, template, { encoding: "utf8" }); // Log to stderr (safe for MCP stdio communication) only if verbose mode if (process.env.BRIEF_VERBOSE || process.env.DEBUG) { console.error(`ℹ️ Updated .brief/brief-guidelines.md to latest version (backup: ${backupPath})`); } } catch (err) { // Silently fail - don't block server startup if guidelines update fails // Only log if there's an unexpected error if (process.env.DEBUG) { console.error(`Debug: Failed to auto-update guidelines: ${err instanceof Error ? err.message : String(err)}`); } } } const program = new Command(); // Read version from package.json const require = createRequire(import.meta.url); let version = "0.0.0"; // fallback default try { const pkg = require('../package.json'); version = String(pkg?.version || version); } catch { // Keep fallback version if package.json read fails } program .name("brief-mcp") .description("Brief MCP Server") .version(version); // Skeleton: init command (implementation added in subsequent tasks) program .command("init") .description("Initialize Brief MCP across selected targets (one-liner setup)") // Credentials and behavior flags .option("--api-url <url>", "Brief API base URL (e.g., https://app.briefhq.ai)") .option("--api-key <key>", "Brief API key (prefix_secret)") // Target selection (repeatable) .addOption(new Option("--app <target>", "Installation target") .choices(["claude", "codex", "generic", "cursor", "claude-desktop"]) // do not write files for cursor/claude-desktop .argParser((val, prev = []) => { return Array.isArray(prev) ? [...prev, val] : [val]; }) .default([])) // Scope .addOption(new Option("--scope <scope>", "Scope for Claude Code installation") .choices(["user", "project", "local"]) // default resolved to user ) // Repo and write behavior .option("--project-dir <path>", "Project directory to use for repo-scoped actions") .option("--yes", "Assume yes for interactive prompts", false) .option("--no", "Assume no for interactive prompts (print-only unless --write)", false) .option("--write", "Apply changes to files when non-interactive", false) .option("--verbose", "Show detailed preview JSON and plan output", false) .option("--force", "Override scope repo validation (advanced)", false) .option("--print", "Print actions without applying changes (forces no-writes)", false) // Rules generation .option("--no-rules", "Skip generating repo/Claude onboarding files") .addOption(new Option("--rules-target <target>", "Where to write onboarding content") .choices(["repo", "claude", "both"]) // defaults to auto ) .addHelpText("after", `\nExamples:\n $ brief-mcp init\n $ npx -y @briefhq/mcp-server@latest init\n $ npx -y @briefhq/mcp-server@latest init --app claude --scope user --yes --write\n $ npx -y @briefhq/mcp-server@latest init --app claude --app codex --api-url https://app.briefhq.ai\n\nNotes:\n - This command guides you through configuring API credentials and adding Brief MCP\n to supported tools. Some flags are parsed now; full behavior lands in subsequent steps.`) .action(async (opts) => { try { // Resolve prompt default behavior const assumeYes = !!opts.yes; const assumeNo = !!opts.no; const behavior = assumeYes ? "yes" : assumeNo ? "no" : "interactive"; // Determine repo context (used for scope prompting later) const projectBase = opts.projectDir ? resolve(process.cwd(), opts.projectDir) : process.cwd(); const repoInfo = detectRepo(projectBase); // Gather inputs via prompts if interactive or values missing let apiUrl = String(opts.apiUrl ?? "").trim(); let apiKey = String(opts.apiKey ?? "").trim(); let apps = Array.isArray(opts.app) ? [...opts.app] : []; let scope = opts.scope; if (behavior === "interactive") { const prompts = (await import("prompts")).default; const answers = await prompts([ { type: () => (apiUrl ? null : "text"), name: "apiUrl", message: "API URL:", initial: apiUrl || process.env.BRIEF_API_BASE || "https://app.briefhq.ai", validate: (v) => { try { const u = new URL(v); if (!/^https?:$/.test(u.protocol)) return "Use http:// or https://"; return true; } catch { return "Enter a valid URL"; } }, }, { type: () => (apiKey ? null : "password"), name: "apiKey", message: "API key (prefix_secret):", validate: (v) => (!!v && v.includes("_")) || "Enter key as prefix_secret", }, { type: () => (apps.length ? null : "multiselect"), name: "apps", message: "Select installation targets:", hint: "Space to select, enter to confirm", instructions: false, choices: [ { title: "Claude Code", value: "claude", selected: true }, { title: "Codex CLI", value: "codex", selected: false }, { title: "Generic MCP (print JSON)", value: "generic", selected: false }, { title: "Cursor (deeplink only)", value: "cursor", selected: false }, { title: "Claude Desktop (DXT guidance)", value: "claude-desktop", selected: false }, ], min: 1, }, { type: () => (scope ? null : "select"), name: "scope", message: "Scope for Claude Code:", hint: repoInfo.inRepo ? "Default is user; project/local available (repo detected)" : "Only user scope available (no git repo detected)", choices: repoInfo.inRepo ? [ { title: "user (default)", value: "user" }, { title: "project (requires git repo)", value: "project" }, { title: "local (project-specific, not shared)", value: "local" }, ] : [ { title: "user (default)", value: "user" }, ], initial: 0, }, ], { onCancel: () => { console.log("Aborted."); process.exit(1); } }); apiUrl = String(answers.apiUrl ?? apiUrl ?? "").trim(); apiKey = String(answers.apiKey ?? apiKey ?? "").trim(); if (!apps.length && Array.isArray(answers.apps)) apps = answers.apps; scope = (scope ?? answers.scope ?? "user"); } // 2.5 Env var fallback if flags not provided if (!apiUrl && process.env.BRIEF_API_BASE) { apiUrl = String(process.env.BRIEF_API_BASE).trim(); } if (!apiKey && process.env.BRIEF_API_TOKEN) { apiKey = String(process.env.BRIEF_API_TOKEN).trim(); } // Normalize and de-duplicate selected targets in a deterministic order const allowedOrder = ["claude", "codex", "generic", "cursor", "claude-desktop"]; const provided = new Set(apps); apps = allowedOrder.filter((t) => provided.has(t)); // 4.4 Enforce scope validation: project/local requires a git repo unless --force const isNonUserScope = (scope || "user") !== "user"; if (isNonUserScope && !repoInfo.inRepo && !opts.force) { console.error(`❌ Scope '${scope}' requires a git repository.`); console.error(`Options:\n• Run from a git repository root\n• Use --project-dir /path/to/repo\n• Use --scope user for system-wide installation\n\nExample: npx -y @briefhq/mcp-server@latest init --scope user`); process.exit(1); } const preview = { apiUrl: apiUrl || null, apiKey: apiKey ? "[provided]" : null, apps, scope: scope || "user", projectDir: opts.projectDir || null, prompts: behavior, write: !!opts.write, rules: opts.rules === false ? "skip" : opts.rulesTarget || "auto", }; // 1.4 Orchestration skeleton: version check → save creds → test → writers → summary function nodeVersionSatisfies(minMajor, minMinor) { const [majorStr, minorStr] = process.versions.node.split("."); const major = Number(majorStr || 0); const minor = Number(minorStr || 0); if (Number.isNaN(major) || Number.isNaN(minor)) return false; if (major > minMajor) return true; if (major < minMajor) return false; return minor >= minMinor; } const nodeOk = nodeVersionSatisfies(18, 18); if (!nodeOk) { console.error(`Node ${process.versions.node} detected. Please install Node >= 18.18 to continue.`); process.exit(1); } if (!apiUrl) { console.error("Missing API URL. Provide --api-url or run interactively."); process.exit(1); } try { const u = new URL(apiUrl); if (!/^https?:$/.test(u.protocol)) throw new Error("bad scheme"); } catch { console.error("Invalid --api-url. Must be an absolute http(s) URL."); process.exit(1); } if (!apps.length) { console.error("No targets selected. Provide at least one --app or run interactively."); process.exit(1); } // Determine whether to apply changes (writes/network) or just preview const applyChanges = (behavior === "interactive" || !!opts.write) && !opts.print; // 2.2 API key presence/format validation (non-interactive and applying changes) if (behavior !== "interactive" && applyChanges) { if (!apiKey) { console.error("Missing API key. Provide --api-key or run interactively to enter it securely."); process.exit(1); } if (!apiKey.includes("_")) { console.error("Invalid --api-key format. Expected prefix_secret (contains underscore)."); process.exit(1); } } if (opts.verbose) { console.log("brief-mcp init (orchestration preview)\n"); console.log("Inputs:"); console.log(JSON.stringify(preview, null, 2)); if ((scope || "user") !== "user") { console.log("\nNote: project/local scope will be validated during repo detection (task 4.x)."); } console.log("\nPlan:"); console.log(`- Node version check (>= 18.18): ok`); console.log(`- ${applyChanges ? "Save" : "Would save"} credentials via keytar and ${applyChanges ? "validate" : "would validate"} connection (test)`); console.log("- Detect repo (git worktrees/submodules) and resolve scope defaults"); if (apps.includes("claude")) { console.log(`- Claude CLI detection: ${hasClaudeCLI() ? "found" : "not found (will print command)"}`); console.log(`- Claude add command: ${composeClaudeAddCommand((scope || "user"))}`); } console.log("- Apply targets in order with safe, idempotent writes:"); for (const a of apps) console.log(` • ${a}`); console.log("- Generate onboarding assets based on rules settings"); console.log("- Print summary with next steps\n"); console.log("Note: Skeleton with basic validation and error handling added (1.5). Next tasks implement save/test and writers."); } else { console.log("brief-mcp init: inputs parsed."); console.log(`- node: ${process.versions.node} (ok)`); console.log(`- targets: ${apps.join(", ")}`); console.log(`- scope: ${scope || "user"}${(scope || "user") !== "user" ? " (repo validation pending)" : ""}`); console.log(`- rules: ${opts.rules === false ? "skip" : preview.rules}`); console.log(`- mode: ${applyChanges ? "apply" : "print-only (no writes)"}`); console.log("Run with --verbose to see full preview and plan."); } // 2.3 Save credentials and 2.4 test connection (cleanup on failure) if (applyChanges) { try { await saveCredentials({ apiUrl, apiKey }); } catch (e) { console.error(`Failed to save credentials: ${e?.message || String(e)}`); process.exit(1); } const ok = await testConnection(); if (!ok) { console.error("❌ Connection test failed."); console.error("Troubleshooting:"); console.error("• Verify the API URL is correct (e.g., https://app.briefhq.ai)"); console.error("• Confirm your API key is valid and not expired/revoked"); console.error("• Check your network/VPN/proxy allows access to app.briefhq.ai"); console.error("• Re-run with --verbose to review parsed inputs"); try { await deleteCredentials(); } catch { /* noop */ } process.exit(1); } console.log("✅ Connected to Brief"); // 5.3 Apply Claude target: execute add command when CLI present; otherwise print if (apps.includes("claude")) { const claudeCmd = composeClaudeAddCommand((scope || "user")); if (hasClaudeCLI()) { console.log("\nInstalling Brief MCP into Claude Code..."); try { execSync(claudeCmd, { stdio: "inherit", timeout: 30_000 }); console.log("✅ Claude Code: added Brief MCP server"); // 5.4 Optional verification try { const listOut = execSync("claude mcp list", { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 10_000 }); const found = /\bbrief\b/i.test(listOut); if (found) { console.log("🔎 Verified: 'brief' appears in 'claude mcp list'."); } else { console.log("ℹ️ 'brief' not detected in 'claude mcp list'. If not visible in Claude, try restarting the app or re-running the add command."); } } catch { // Non-fatal; just proceed } } catch (e) { console.error("❌ Failed to add Brief MCP to Claude Code. You can run this manually:"); console.error(claudeCmd); } } else { console.log("\nClaude CLI not found. Run this to add Brief MCP to Claude Code:"); console.log(claudeCmd); } } // 6.x Codex CLI config writer if (apps.includes("codex")) { const cfgPath = getCodexConfigPath(); const existing = readCodexConfig(cfgPath); const dupCount = countBriefBlocks(existing); if (dupCount > 1) { console.warn(`⚠️ Detected ${dupCount} [mcp_servers.brief] blocks in ${cfgPath}. Keeping the first and leaving others untouched. Consider manually removing duplicates.`); } const { content, changed } = mergeBriefIntoCodexConfig(existing); if (changed) { const { backupPath } = writeCodexConfig(cfgPath, content); console.log(`✅ Codex CLI: ${existing ? "updated" : "created"} ${cfgPath}`); if (backupPath) console.log(`Backup saved at ${backupPath}`); // 6.4 Re-read to validate idempotency; if merge would change again, restore backup const after = readCodexConfig(cfgPath); const validate = mergeBriefIntoCodexConfig(after); if (validate.changed) { console.error("❌ Codex config validation failed (non-idempotent). Restoring backup."); if (backupPath) { try { const backup = readFileSync(backupPath, { encoding: "utf8" }); writeFileSync(cfgPath, backup, { encoding: "utf8" }); console.log("✅ Backup restored successfully."); } catch (e) { console.error(`❌ Failed to restore backup: ${e?.message || String(e)}`); console.error(`Manual restoration needed from: ${backupPath}`); } } process.exit(1); } else { console.log("🔎 Verified Codex config contains Brief block (idempotent)."); const afterDup = countBriefBlocks(after); if (afterDup > 1) { console.warn(`⚠️ Multiple [mcp_servers.brief] blocks still present (${afterDup}). Please clean up duplicates in ${cfgPath}.`); } } } else { console.log("ℹ️ Codex CLI already configured for Brief (no changes)."); } } // 7.2/7.3 Cursor outputs (deeplink + example JSON); print-only if (apps.includes("cursor")) { logCursorGuidance(false); } // 8.1 Claude Desktop guidance (no writes) if (apps.includes("claude-desktop")) { logClaudeDesktopInstructions(false); } // 7.1 Generic MCP config (print-only JSON block for consumers) if (apps.includes("generic")) { logGenericMcpConfig(false); } // 9.1 Ensure .brief directory exists in repo (no file writes yet) if (opts.rules !== false) { const target = opts.rulesTarget || "auto"; const allowRepo = target === "auto" || target === "repo" || target === "both"; if (allowRepo) { if (repoInfo.inRepo) { const briefDir = resolve((repoInfo.root || projectBase), ".brief"); if (!existsSync(briefDir)) { try { mkdirSync(briefDir, { recursive: true }); console.log(`✅ Created ${briefDir}`); } catch (e) { console.error(`❌ Failed to create ${briefDir}: ${e?.message || String(e)}`); } } else { console.log("ℹ️ .brief directory already exists (no changes)."); } // 9.2 Generate basic guidelines file if missing const guidelinesPath = resolve(briefDir, "brief-guidelines.md"); if (!existsSync(guidelinesPath)) { const { content: template } = loadTemplate("./templates/brief-guidelines.md", FALLBACK_GUIDELINES_CONTENT); try { writeFileSync(guidelinesPath, template, { encoding: "utf8" }); console.log(`✅ Wrote ${guidelinesPath}`); } catch (e) { console.error(`❌ Failed to write ${guidelinesPath}: ${e?.message || String(e)}`); } } else { // 9.3 Backup and idempotent merge/update: append template if key sections missing try { const current = readFileSync(guidelinesPath, { encoding: "utf8" }); const hasCore = /##\s*Core MCP Tools/i.test(current); const hasDiscover = /brief_discover_context/.test(current); const hasGuard = /brief_guard_approach/.test(current); const hasFallbackHeader = /^#\s*Brief Guidelines for AI Coding Agents\b/im.test(current); if (hasCore && hasDiscover && hasGuard) { console.log("ℹ️ .brief/brief-guidelines.md already contains core sections (no changes)."); } else if (hasFallbackHeader && (hasCore || hasDiscover || hasGuard)) { console.warn("⚠️ .brief/brief-guidelines.md contains partial Brief content. Review manually before re-running."); } else { const backupPath = `${guidelinesPath}.bak.${Date.now()}`; writeFileSync(backupPath, current, { encoding: "utf8" }); const { content: template, fromBundle } = loadTemplate("./templates/brief-guidelines.md", FALLBACK_GUIDELINES_CONTENT); if (hasFallbackHeader) { writeFileSync(guidelinesPath, template, { encoding: "utf8" }); console.log(`♻️ Replaced ${guidelinesPath} with ${fromBundle ? "bundled" : "fallback"} Brief guidelines.`); console.log(`Backup saved at ${backupPath}`); } else { const sep = current.trimEnd().length === 0 ? "\n" : "\n\n"; const merged = `${current.trimEnd()}${sep}${template.trimStart()}`; writeFileSync(guidelinesPath, merged, { encoding: "utf8" }); console.log(`✅ Updated ${guidelinesPath}`); console.log(`Backup saved at ${backupPath}`); } } } catch (e) { console.error(`❌ Failed to update ${guidelinesPath}: ${e?.message || String(e)}`); } } } else { console.log("ℹ️ Skipping .brief directory creation (no git repo detected)."); } } } // 10.2 Write/merge Claude command file (~/.claude/brief.md) if (opts.rules !== false) { const target = opts.rulesTarget || "auto"; const allowClaude = target === "auto" || target === "claude" || target === "both"; if (allowClaude) { const claudePath = getClaudeBriefPath(); try { ensureParentDir(claudePath); let existing = ""; try { existing = readFileSync(claudePath, { encoding: "utf8" }); } catch { } if (!existing) { const { content: template } = loadTemplate("./templates/claude-brief.md", FALLBACK_CLAUDE_BRIEF_CONTENT); writeFileSync(claudePath, template, { encoding: "utf8" }); console.log(`✅ Wrote ${claudePath}`); } else { // 10.3 Preserve user customizations: append template if key sections are missing const hasOnboarding = /#\s*\/brief\s*Onboarding/i.test(existing); const hasColdStart = /Cold\s*Start\s*Suggestions/i.test(existing); const hasDiscover = /brief_discover_context/.test(existing); if (hasOnboarding && hasColdStart && hasDiscover) { console.log("ℹ️ ~/.claude/brief.md already contains onboarding and cold-start sections (no changes)."); } else if (hasOnboarding && (hasColdStart || hasDiscover)) { console.warn("⚠️ ~/.claude/brief.md contains partial Brief content. Review manually before re-running."); } else { const { content: template, fromBundle } = loadTemplate("./templates/claude-brief.md", FALLBACK_CLAUDE_BRIEF_CONTENT); const backupPath = `${claudePath}.bak.${Date.now()}`; writeFileSync(backupPath, existing, { encoding: "utf8" }); if (hasOnboarding) { writeFileSync(claudePath, template, { encoding: "utf8" }); console.log(`♻️ Replaced ${claudePath} with ${fromBundle ? "bundled" : "fallback"} Brief onboarding.`); console.log(`Backup saved at ${backupPath}`); } else { const sep = existing.trimEnd().length === 0 ? "\n" : "\n\n"; const merged = `${existing.trimEnd()}${sep}${template.trimStart()}`; writeFileSync(claudePath, merged, { encoding: "utf8" }); console.log(`✅ Updated ${claudePath}`); console.log(`Backup saved at ${backupPath}`); } } } } catch (e) { console.error(`❌ Failed to write Claude command file: ${e?.message || String(e)}`); } } } } else { if (!apiKey) { console.log("(print-only) No API key provided; real run will require --api-key or interactive entry."); } if (apps.includes("codex")) { const cfgPath = getCodexConfigPath(); console.log("(print-only) Would ensure this file contains the Brief configuration:"); console.log(` ${cfgPath}`); console.log("Add or update the following block if missing or outdated:\n"); console.log("[mcp_servers.brief]\ncommand = \"npx\"\nargs = [\"-y\", \"@briefhq/mcp-server@latest\", \"serve\"]\n"); } if (apps.includes("generic")) { logGenericMcpConfig(true); } if (apps.includes("cursor")) { logCursorGuidance(true); } if (apps.includes("claude-desktop")) { logClaudeDesktopInstructions(true); } // 10.1 Claude command file path setup (no writes yet) if (opts.rules !== false) { const target = opts.rulesTarget || "auto"; const allowClaude = target === "auto" || target === "claude" || target === "both"; if (allowClaude) { const claudePath = getClaudeBriefPath(); try { const { dir, created } = ensureParentDir(claudePath); console.log(created ? `✅ Created ${dir}` : `ℹ️ ${dir} exists`); console.log(`Claude command file path: ${claudePath}`); } catch (e) { console.error(`❌ Failed to ensure Claude directory: ${e?.message || String(e)}`); } } } } } catch (err) { console.error(`init failed: ${err?.message || String(err)}`); process.exit(1); } }); program .command("update-guidelines") .description("Update .brief/brief-guidelines.md to the latest version") .option("--force", "Overwrite existing file without backup") .action(async (opts) => { const cwd = process.cwd(); const briefDir = resolve(cwd, ".brief"); const guidelinesPath = resolve(briefDir, "brief-guidelines.md"); // Check if .brief directory exists if (!existsSync(briefDir)) { console.error("❌ .brief directory not found. Run 'npx @briefhq/mcp-server@latest init' first."); process.exit(1); } const { content: template, fromBundle } = loadTemplate("./templates/brief-guidelines.md", FALLBACK_GUIDELINES_CONTENT); try { // Create backup of existing file (unless --force is used) if (existsSync(guidelinesPath) && !opts.force) { const backupPath = `${guidelinesPath}.bak.${Date.now()}`; const current = readFileSync(guidelinesPath, { encoding: "utf8" }); writeFileSync(backupPath, current, { encoding: "utf8" }); console.log(`📦 Backup saved to ${backupPath}`); } // Write the latest template writeFileSync(guidelinesPath, template, { encoding: "utf8" }); console.log(`✅ Updated ${guidelinesPath} with ${fromBundle ? "bundled" : "fallback"} Brief guidelines`); console.log(" AI agents will now use the latest Brief MCP workflow and confirmation patterns."); } catch (e) { console.error(`❌ Failed to update guidelines: ${e?.message || String(e)}`); process.exit(1); } }); program .command("configure") .description("Configure Brief MCP: API URL and API key") .option("--api-url <url>") .option("--api-key <key>") .action(async (opts) => { let apiUrl = String(opts.apiUrl ?? "").trim(); let apiKey = String(opts.apiKey ?? "").trim(); if (!apiUrl || !apiKey) { const prompts = (await import("prompts")).default; const answers = await prompts([ { type: () => (apiUrl ? null : "text"), name: "apiUrl", message: "API URL (e.g., http://localhost:3001):", initial: apiUrl || process.env.BRIEF_API_BASE || "", validate: (v) => { try { const u = new URL(v); if (!/^https?:$/.test(u.protocol)) return "Use http:// or https://"; return true; } catch { return "Enter a valid URL"; } }, }, { type: () => (apiKey ? null : "password"), name: "apiKey", message: "API key (prefix_secret):", initial: "", validate: (v) => (!!v && v.includes("_")) || "Enter key as prefix_secret", }, ]); apiUrl = String((answers.apiUrl ?? apiUrl ?? "")).trim(); apiKey = String((answers.apiKey ?? apiKey ?? "")).trim(); } try { const u = new URL(apiUrl); if (!/^https?:$/.test(u.protocol)) throw new Error("bad scheme"); } catch { console.error("Invalid value for --api-url (must be an absolute http(s) URL)"); process.exit(1); } try { await saveCredentials({ apiUrl, apiKey }); const ok = await testConnection(); if (!ok) throw new Error("Connection test failed"); console.log("✅ Connected to Brief"); } catch (err) { console.error(`❌ ${err instanceof Error ? err.message : "Connection failed"}`); console.error("Troubleshooting:\n• Verify API URL and key\n• Check network/VPN/proxy\n• Try again later in case of transient issues"); await deleteCredentials().catch(() => { }); process.exit(1); } }); program .command("serve") .description("Start the MCP server") .action(async () => { try { // Ensure configured before starting server await getCredentials(); // Auto-update guidelines if needed (runs silently, won't block server) autoUpdateGuidelinesIfNeeded(); const { runServer } = await import("./server.js"); await runServer(); } catch (err) { console.error(`Failed to start server: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } }); program .command("install") .description("Print MCP client config for Cursor or Claude") .argument("<app>", "cursor or claude") .action((app) => { console.warn("Note: 'brief-mcp install' is deprecated. Prefer using 'init --app <app> --print'."); console.warn("Example: npx -y @briefhq/mcp-server@latest init --app cursor --print\n"); app = (app || "").toLowerCase().trim(); const cfg = { mcpServers: { brief: { command: "npx", args: ["-y", "@briefhq/mcp-server@latest", "serve"], }, }, }; let target = null; if (app === "cursor") target = "~/.cursor/mcp.json"; if (app === "claude") target = "~/Library/Application Support/Claude/claude_desktop_config.json"; if (!target) { console.error('Unknown app. Use "cursor" or "claude".'); process.exit(1); } console.log(`Add to ${target}:\n`); console.log(JSON.stringify(cfg, null, 2)); }); program .command("test") .description("Sanity check configuration and list available tools") .action(async () => { try { await getCredentials(); } catch { console.error("Not configured. Run: brief-mcp configure"); process.exit(1); } const ok = await testConnection(); if (!ok) { console.error("❌ Connection test failed"); process.exit(1); } console.log("✅ Connection OK"); try { const { tools } = await import("./tools/index.js"); const names = tools && typeof tools === "object" ? Object.keys(tools).sort() : []; if (names.length === 0) { console.log("No tools exported."); } else { console.log(`Available tools (${names.length}):`, names.join(", ")); } } catch { // Non-fatal } }); // Ensure async command handlers are awaited and errors handled program .parseAsync() .catch((err) => { console.error(err); process.exit(1); });