Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/claude-code-plugin/hooks/session-start.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
60 changes: 60 additions & 0 deletions packages/claude-code-plugin/hooks/test_session_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."""

Expand Down
33 changes: 33 additions & 0 deletions packages/claude-code-plugin/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
console.log('╔════════════════════════════════════════════════════════════╗');
console.log('║ CodingBuddy Claude Code Plugin Builder ║');
Expand All @@ -155,6 +184,10 @@ async function main(): Promise<void> {
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');
Expand Down
Loading