/** * McpIntegrations - MCP-based IDE integrations for claude-mem * * Handles MCP config writing and context injection for IDEs that support * the Model Context Protocol. These are "MCP-only" integrations: they provide * search tools and context injection but do NOT capture transcripts. * * Supported IDEs: * - Copilot CLI * - Antigravity (Gemini) * - Goose * - Crush * - Roo Code * - Warp * * All IDEs point to the same MCP server: plugin/scripts/mcp-server.cjs */ import path from 'path'; import { homedir } from 'os'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; import { logger } from '../../utils/logger.js'; import { findMcpServerPath } from './CursorHooksInstaller.js'; import { readJsonSafe } from '../../utils/json-utils.js'; import { injectContextIntoMarkdownFile } from '../../utils/context-injection.js'; // ============================================================================ // Shared Constants // ============================================================================ const PLACEHOLDER_CONTEXT = `# claude-mem: Cross-Session Memory *No context yet. Complete your first session and context will appear here.* Use claude-mem's MCP search tools for manual memory queries.`; // ============================================================================ // Shared Utilities // ============================================================================ /** * Build the standard MCP server entry that all IDEs use. * Points to the same mcp-server.cjs script. */ function buildMcpServerEntry(mcpServerPath: string): { command: string; args: string[] } { return { command: process.execPath, args: [mcpServerPath], }; } /** * Write a standard MCP JSON config file, merging with existing config. * Supports both { "mcpServers": { ... } } and { "servers": { ... } } formats. */ function writeMcpJsonConfig( configFilePath: string, mcpServerPath: string, serversKeyName: string = 'mcpServers', ): void { const parentDirectory = path.dirname(configFilePath); mkdirSync(parentDirectory, { recursive: true }); const existingConfig = readJsonSafe>(configFilePath, {}); if (!existingConfig[serversKeyName]) { existingConfig[serversKeyName] = {}; } existingConfig[serversKeyName]['claude-mem'] = buildMcpServerEntry(mcpServerPath); writeFileSync(configFilePath, JSON.stringify(existingConfig, null, 2) + '\n'); } // ============================================================================ // MCP Installer Factory (Phase 1D) // ============================================================================ /** * Configuration for a JSON-based MCP IDE integration. */ interface McpInstallerConfig { ideId: string; ideLabel: string; configPath: string; configKey: 'servers' | 'mcpServers'; contextFile?: { path: string; isWorkspaceRelative: boolean; }; } /** * Factory function that creates an MCP installer for any JSON-config-based IDE. * Handles MCP config writing and optional context injection. */ function installMcpIntegration(config: McpInstallerConfig): () => Promise { return async (): Promise => { console.log(`\nInstalling Claude-Mem MCP integration for ${config.ideLabel}...\n`); const mcpServerPath = findMcpServerPath(); if (!mcpServerPath) { console.error('Could not find MCP server script'); console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs'); return 1; } try { // Write MCP config const configPath = config.configPath; // Warp special case: skip config write if ~/.warp/ doesn't exist if (config.ideId === 'warp' && !existsSync(path.dirname(configPath))) { console.log(` Note: ~/.warp/ not found. MCP may need to be configured via Warp Drive UI.`); } else { writeMcpJsonConfig(configPath, mcpServerPath, config.configKey); console.log(` MCP config written to: ${configPath}`); } // Inject context if configured let contextPath: string | undefined; if (config.contextFile) { contextPath = config.contextFile.path; injectContextIntoMarkdownFile(contextPath, PLACEHOLDER_CONTEXT); console.log(` Context placeholder written to: ${contextPath}`); } // Print summary const summaryLines = [`\nInstallation complete!\n`]; summaryLines.push(`MCP config: ${configPath}`); if (contextPath) { summaryLines.push(`Context: ${contextPath}`); } summaryLines.push(''); summaryLines.push(`Note: This is an MCP-only integration providing search tools and context.`); summaryLines.push(`Transcript capture is not available for ${config.ideLabel}.`); if (config.ideId === 'warp') { summaryLines.push('If MCP config via file is not supported, configure MCP through Warp Drive UI.'); } summaryLines.push(''); summaryLines.push('Next steps:'); summaryLines.push(' 1. Start claude-mem worker: npx claude-mem start'); summaryLines.push(` 2. Restart ${config.ideLabel} to pick up the MCP server`); summaryLines.push(''); console.log(summaryLines.join('\n')); return 0; } catch (error) { console.error(`\nInstallation failed: ${(error as Error).message}`); return 1; } }; } // ============================================================================ // Factory Configs for JSON-based IDEs // ============================================================================ const COPILOT_CLI_CONFIG: McpInstallerConfig = { ideId: 'copilot-cli', ideLabel: 'Copilot CLI', configPath: path.join(homedir(), '.github', 'copilot', 'mcp.json'), configKey: 'servers', contextFile: { path: path.join(process.cwd(), '.github', 'copilot-instructions.md'), isWorkspaceRelative: true, }, }; const ANTIGRAVITY_CONFIG: McpInstallerConfig = { ideId: 'antigravity', ideLabel: 'Antigravity', configPath: path.join(homedir(), '.gemini', 'antigravity', 'mcp_config.json'), configKey: 'mcpServers', contextFile: { path: path.join(process.cwd(), '.agent', 'rules', 'claude-mem-context.md'), isWorkspaceRelative: true, }, }; const CRUSH_CONFIG: McpInstallerConfig = { ideId: 'crush', ideLabel: 'Crush', configPath: path.join(homedir(), '.config', 'crush', 'mcp.json'), configKey: 'mcpServers', }; const ROO_CODE_CONFIG: McpInstallerConfig = { ideId: 'roo-code', ideLabel: 'Roo Code', configPath: path.join(process.cwd(), '.roo', 'mcp.json'), configKey: 'mcpServers', contextFile: { path: path.join(process.cwd(), '.roo', 'rules', 'claude-mem-context.md'), isWorkspaceRelative: true, }, }; const WARP_CONFIG: McpInstallerConfig = { ideId: 'warp', ideLabel: 'Warp', configPath: path.join(homedir(), '.warp', 'mcp.json'), configKey: 'mcpServers', contextFile: { path: path.join(process.cwd(), 'WARP.md'), isWorkspaceRelative: true, }, }; // ============================================================================ // Goose (YAML-based — separate handler) // ============================================================================ /** * Get the Goose config path. * Goose stores its config at ~/.config/goose/config.yaml. */ function getGooseConfigPath(): string { return path.join(homedir(), '.config', 'goose', 'config.yaml'); } /** * Check if a YAML string already has a claude-mem entry under mcpServers. * Uses string matching to avoid needing a YAML parser. */ function gooseConfigHasClaudeMemEntry(yamlContent: string): boolean { // Look for "claude-mem:" indented under mcpServers return yamlContent.includes('claude-mem:') && yamlContent.includes('mcpServers:'); } /** * Build the Goose YAML MCP server block as a string. * Produces properly indented YAML without needing a parser. */ function buildGooseMcpYamlBlock(mcpServerPath: string): string { // Goose expects the mcpServers section at the top level return [ 'mcpServers:', ' claude-mem:', ` command: ${process.execPath}`, ' args:', ` - ${mcpServerPath}`, ].join('\n'); } /** * Build just the claude-mem server entry (for appending under existing mcpServers). */ function buildGooseClaudeMemEntryYaml(mcpServerPath: string): string { return [ ' claude-mem:', ` command: ${process.execPath}`, ' args:', ` - ${mcpServerPath}`, ].join('\n'); } /** * Install claude-mem MCP integration for Goose. * * - Writes/merges MCP config into ~/.config/goose/config.yaml * - Uses string manipulation for YAML (no parser dependency) * * @returns 0 on success, 1 on failure */ export async function installGooseMcpIntegration(): Promise { console.log('\nInstalling Claude-Mem MCP integration for Goose...\n'); const mcpServerPath = findMcpServerPath(); if (!mcpServerPath) { console.error('Could not find MCP server script'); console.error(' Expected at: ~/.claude/plugins/marketplaces/thedotmack/plugin/scripts/mcp-server.cjs'); return 1; } try { const configPath = getGooseConfigPath(); const configDirectory = path.dirname(configPath); mkdirSync(configDirectory, { recursive: true }); if (existsSync(configPath)) { let yamlContent = readFileSync(configPath, 'utf-8'); if (gooseConfigHasClaudeMemEntry(yamlContent)) { // Already configured — replace the claude-mem block // Find the claude-mem entry and replace it const claudeMemPattern = /( {2}claude-mem:\n(?:.*\n)*?(?= {2}\S|\n\n|$))/; const newEntry = buildGooseClaudeMemEntryYaml(mcpServerPath) + '\n'; if (claudeMemPattern.test(yamlContent)) { yamlContent = yamlContent.replace(claudeMemPattern, newEntry); } writeFileSync(configPath, yamlContent); console.log(` Updated existing claude-mem entry in: ${configPath}`); } else if (yamlContent.includes('mcpServers:')) { // mcpServers section exists but no claude-mem entry — append under it const mcpServersIndex = yamlContent.indexOf('mcpServers:'); const insertionPoint = mcpServersIndex + 'mcpServers:'.length; const newEntry = '\n' + buildGooseClaudeMemEntryYaml(mcpServerPath); yamlContent = yamlContent.slice(0, insertionPoint) + newEntry + yamlContent.slice(insertionPoint); writeFileSync(configPath, yamlContent); console.log(` Added claude-mem to existing mcpServers in: ${configPath}`); } else { // No mcpServers section — append the entire block const mcpBlock = '\n' + buildGooseMcpYamlBlock(mcpServerPath) + '\n'; yamlContent = yamlContent.trimEnd() + '\n' + mcpBlock; writeFileSync(configPath, yamlContent); console.log(` Appended mcpServers section to: ${configPath}`); } } else { // File doesn't exist — create from template const templateContent = buildGooseMcpYamlBlock(mcpServerPath) + '\n'; writeFileSync(configPath, templateContent); console.log(` Created config with MCP server: ${configPath}`); } console.log(` Installation complete! MCP config: ${configPath} Note: This is an MCP-only integration providing search tools and context. Transcript capture is not available for Goose. Next steps: 1. Start claude-mem worker: npx claude-mem start 2. Restart Goose to pick up the MCP server `); return 0; } catch (error) { console.error(`\nInstallation failed: ${(error as Error).message}`); return 1; } } // ============================================================================ // Unified Installer (used by npx install command) // ============================================================================ /** * Map of IDE identifiers to their install functions. * Used by the install command to dispatch to the correct integration. */ export const MCP_IDE_INSTALLERS: Record Promise> = { 'copilot-cli': installMcpIntegration(COPILOT_CLI_CONFIG), 'antigravity': installMcpIntegration(ANTIGRAVITY_CONFIG), 'goose': installGooseMcpIntegration, 'crush': installMcpIntegration(CRUSH_CONFIG), 'roo-code': installMcpIntegration(ROO_CODE_CONFIG), 'warp': installMcpIntegration(WARP_CONFIG), };