@briefhq/mcp-server
Version:
Brief MCP server and CLI – connect Cursor/Claude MCP to your Brief organization
836 lines (835 loc) • 42.3 kB
JavaScript
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);
});