diff --git a/packages/claude-code-plugin/hooks/session-start.py b/packages/claude-code-plugin/hooks/session-start.py index f6918044..bd8dbb4d 100644 --- a/packages/claude-code-plugin/hooks/session-start.py +++ b/packages/claude-code-plugin/hooks/session-start.py @@ -373,6 +373,43 @@ def _install_hook_with_lib( ) +CODINGBUDDY_MCP_ENTRY = { + "command": "codingbuddy", + "args": ["mcp"], +} + + +def _ensure_mcp_json(mcp_json_path: Path) -> None: + """Ensure ~/.claude/mcp.json contains the codingbuddy MCP server entry (#1100). + + Creates the file if missing, or merges the codingbuddy entry into an + existing file while preserving other MCP server configurations. + """ + mcp_json_path.parent.mkdir(parents=True, exist_ok=True) + + if mcp_json_path.exists(): + try: + with open(mcp_json_path, "r", encoding="utf-8") as f: + if HAS_FCNTL: + fcntl.flock(f.fileno(), fcntl.LOCK_SH) + existing = json.load(f) + except (json.JSONDecodeError, OSError): + existing = {} + else: + existing = {} + + servers = existing.setdefault("mcpServers", {}) + if "codingbuddy" in servers: + return # Already configured — don't overwrite user customizations + + servers["codingbuddy"] = CODINGBUDDY_MCP_ENTRY + + with open(mcp_json_path, "w", encoding="utf-8") as f: + if HAS_FCNTL: + fcntl.flock(f.fileno(), fcntl.LOCK_EX) + json.dump(existing, f, indent=2, ensure_ascii=False) + + HUD_FILENAME = "codingbuddy-hud.py" # tmux suggestion messages (i18n) @@ -698,6 +735,12 @@ def main(): except Exception: pass # Never block session start + # Step 2.6: Ensure ~/.claude/mcp.json has codingbuddy entry (#1100) + try: + _ensure_mcp_json(home / ".claude" / "mcp.json") + except Exception: + pass # Never block session start + # Step 3: System prompt injection (#828) # SessionStart uses plain stdout for context injection (NOT JSON) try: diff --git a/packages/claude-code-plugin/hooks/test_session_start.py b/packages/claude-code-plugin/hooks/test_session_start.py index 7aff866c..0ea93f1b 100644 --- a/packages/claude-code-plugin/hooks/test_session_start.py +++ b/packages/claude-code-plugin/hooks/test_session_start.py @@ -309,6 +309,66 @@ def test_finds_agents_from_relative_path(self): assert len(json_files) > 0 +class TestEnsureMcpJson: + """Tests for _ensure_mcp_json function (#1100).""" + + def test_creates_mcp_json_when_missing(self): + """Test creates mcp.json with codingbuddy entry when file doesn't exist.""" + with tempfile.TemporaryDirectory() as tmpdir: + mcp_path = Path(tmpdir) / ".claude" / "mcp.json" + + session_hook._ensure_mcp_json(mcp_path) + + assert mcp_path.exists() + data = json.loads(mcp_path.read_text()) + assert "codingbuddy" in data["mcpServers"] + assert data["mcpServers"]["codingbuddy"]["command"] == "codingbuddy" + assert data["mcpServers"]["codingbuddy"]["args"] == ["mcp"] + + def test_merges_into_existing_mcp_json(self): + """Test adds codingbuddy entry while preserving existing servers.""" + with tempfile.TemporaryDirectory() as tmpdir: + mcp_path = Path(tmpdir) / "mcp.json" + mcp_path.write_text(json.dumps({ + "mcpServers": { + "other-server": {"command": "other", "args": ["--flag"]} + } + })) + + session_hook._ensure_mcp_json(mcp_path) + + data = json.loads(mcp_path.read_text()) + assert "codingbuddy" in data["mcpServers"] + assert "other-server" in data["mcpServers"] + + def test_does_not_overwrite_existing_codingbuddy(self): + """Test does not overwrite user's custom codingbuddy configuration.""" + with tempfile.TemporaryDirectory() as tmpdir: + mcp_path = Path(tmpdir) / "mcp.json" + custom_config = { + "mcpServers": { + "codingbuddy": {"command": "custom-path", "args": ["--custom"]} + } + } + mcp_path.write_text(json.dumps(custom_config)) + + session_hook._ensure_mcp_json(mcp_path) + + data = json.loads(mcp_path.read_text()) + assert data["mcpServers"]["codingbuddy"]["command"] == "custom-path" + + def test_handles_corrupted_mcp_json(self): + """Test handles corrupted JSON gracefully.""" + with tempfile.TemporaryDirectory() as tmpdir: + mcp_path = Path(tmpdir) / "mcp.json" + mcp_path.write_text("not valid json{{{") + + session_hook._ensure_mcp_json(mcp_path) + + data = json.loads(mcp_path.read_text()) + assert "codingbuddy" in data["mcpServers"] + + class TestHookLibCopy: """Tests for lib/ directory copying alongside hook file (#1102).""" diff --git a/packages/claude-code-plugin/scripts/build.ts b/packages/claude-code-plugin/scripts/build.ts index a9bb75d8..69212858 100644 --- a/packages/claude-code-plugin/scripts/build.ts +++ b/packages/claude-code-plugin/scripts/build.ts @@ -140,6 +140,35 @@ MIT return result; } +function createMcpJson(): BuildResult { + const result: BuildResult = { + step: 'MCP Configuration', + success: true, + details: [], + errors: [], + }; + + try { + const mcpConfig = { + mcpServers: { + codingbuddy: { + command: 'codingbuddy', + args: ['mcp'], + }, + }, + }; + + const mcpJsonPath = path.join(ROOT_DIR, '.mcp.json'); + fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n'); + result.details.push(`Generated .mcp.json`); + } catch (err: unknown) { + result.success = false; + result.errors.push(`Failed to create .mcp.json: ${getErrorMessage(err)}`); + } + + return result; +} + async function main(): Promise { console.log('╔════════════════════════════════════════════════════════════╗'); console.log('║ CodingBuddy Claude Code Plugin Builder ║'); @@ -155,6 +184,10 @@ async function main(): Promise { console.log('📖 Step 1: Generating README...'); results.push(createReadme()); + // Step 2: Generate .mcp.json + console.log('🔧 Step 2: Generating .mcp.json...'); + results.push(createMcpJson()); + // Summary console.log('\n════════════════════════════════════════════════════════════'); console.log('Build Summary');