From c37fd3c9e2ee5d022f17f63b41efc52cfa1c7855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Mon, 11 May 2026 00:52:05 +0300 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20merge-mcp.sh=20=E2=80=94=20pre?= =?UTF-8?q?serves=20laravel-boost,=20replaces=20brain-managed=20MCP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses jq --slurpfile pattern (NOT broken `.[1].mcpServers` form from plan): jq --slurpfile brain "$brain_mcp" \ '.mcpServers = ((.mcpServers // {}) + $brain[0].mcpServers)' "$target" Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/fixtures/sample-claude-json.json | 14 +++++ scripts/fixtures/sample-mcp.json | 9 +++ scripts/lib/merge-mcp.sh | 53 +++++++++++++++++ scripts/tests/merge-mcp-test.sh | 73 ++++++++++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 scripts/fixtures/sample-claude-json.json create mode 100644 scripts/fixtures/sample-mcp.json create mode 100644 scripts/lib/merge-mcp.sh create mode 100644 scripts/tests/merge-mcp-test.sh diff --git a/scripts/fixtures/sample-claude-json.json b/scripts/fixtures/sample-claude-json.json new file mode 100644 index 0000000..83fdf03 --- /dev/null +++ b/scripts/fixtures/sample-claude-json.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "php", + "args": ["app/artisan", "boost:mcp"] + }, + "magic": { + "type": "stdio", + "command": "cmd", + "args": ["/c", "npx", "-y", "@21st-dev/magic@latest", "API_KEY=OLD_VALUE"] + } + }, + "preserveMe": "yes" +} diff --git a/scripts/fixtures/sample-mcp.json b/scripts/fixtures/sample-mcp.json new file mode 100644 index 0000000..1cf549d --- /dev/null +++ b/scripts/fixtures/sample-mcp.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/anthropics/claude-code/main/schemas/mcp.json", + "mcpServers": { + "laravel-boost": { + "command": "php", + "args": ["app/artisan", "boost:mcp"] + } + } +} diff --git a/scripts/lib/merge-mcp.sh b/scripts/lib/merge-mcp.sh new file mode 100644 index 0000000..c3f70ea --- /dev/null +++ b/scripts/lib/merge-mcp.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Merge brain MCP template into consumer's claude.json or project .mcp.json +# Mode: user (merge into ~/.claude.json:mcpServers) or project (merge into /.mcp.json:mcpServers) +# Preserves consumer-specific servers; brain-managed servers override on key collision. +# +# Usage: merge-mcp.sh --mode=user|project +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +mode="" +case "${1:-}" in + --mode=user) mode="user"; shift ;; + --mode=project) mode="project"; shift ;; + *) log_error "First arg must be --mode=user or --mode=project"; exit 1 ;; +esac + +target="$1" +brain_mcp="$2" + +require_cmd jq || exit 1 +[ -f "$brain_mcp" ] || { log_error "Brain MCP file not found: $brain_mcp"; exit 1; } + +# If target missing, copy brain template +if [ ! -f "$target" ]; then + log_info "Target $target missing, creating from brain template" + cp "$brain_mcp" "$target" + exit 0 +fi + +# Backup +backup="${target}.bak.$(date +%s)" +cp "$target" "$backup" + +# CORRECTION A: use --slurpfile to bind brain file to $brain (array of docs); +# $brain[0] accesses the single document. This avoids the broken +# "jq -s '.[0] | ... .[1].mcpServers'" form (which inside the inner pipe makes +# .[1] index the object — runtime error "Cannot index object with number"). +# +# Merge semantics: target.mcpServers (or {} if missing) UNION brain.mcpServers, +# with brain entries overriding on key collision (laravel-boost stays — brain +# template doesn't define it; magic gets replaced if brain defines it). +tmp="${target}.tmp" +jq --slurpfile brain "$brain_mcp" \ + '.mcpServers = ((.mcpServers // {}) + $brain[0].mcpServers)' \ + "$target" > "$tmp" + +# Validate +jq empty "$tmp" || { log_error "Merge produced invalid JSON"; rm "$tmp"; exit 1; } + +mv "$tmp" "$target" +log_info "MCP ($mode mode) merged (backup: $backup)" diff --git a/scripts/tests/merge-mcp-test.sh b/scripts/tests/merge-mcp-test.sh new file mode 100644 index 0000000..a17e83f --- /dev/null +++ b/scripts/tests/merge-mcp-test.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +set -u +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +FAILURES=0 +assert_eq() { + if [ "$1" = "$2" ]; then echo "PASS: $3"; else echo "FAIL: $3 (expected '$1', got '$2')"; FAILURES=$((FAILURES + 1)); fi +} + +# Test 1: user-mode merge — magic replaced, laravel-boost preserved +tmpdir=$(mktemp -d) +cp "$SCRIPT_DIR/fixtures/sample-claude-json.json" "$tmpdir/.claude.json" +cat > "$tmpdir/brain-mcp.json" <>"] + } + } +} +EOF2 + +bash "$SCRIPT_DIR/lib/merge-mcp.sh" --mode=user "$tmpdir/.claude.json" "$tmpdir/brain-mcp.json" + +# magic args should now contain placeholder +magic_api_key=$(jq -r '.mcpServers.magic.args[-1]' "$tmpdir/.claude.json") +assert_eq "API_KEY=<>" "$magic_api_key" "user-mode: magic API_KEY replaced with placeholder" + +# laravel-boost preserved +lb_cmd=$(jq -r '.mcpServers["laravel-boost"].command' "$tmpdir/.claude.json") +assert_eq "php" "$lb_cmd" "user-mode: laravel-boost preserved" + +# preserveMe key still present (other keys unchanged) +preserve=$(jq -r '.preserveMe' "$tmpdir/.claude.json") +assert_eq "yes" "$preserve" "user-mode: other top-level keys preserved" + +rm -rf "$tmpdir" + +# Test 2: project-mode merge — playwright/github/semgrep added, laravel-boost preserved +tmpdir=$(mktemp -d) +cp "$SCRIPT_DIR/fixtures/sample-mcp.json" "$tmpdir/.mcp.json" +cat > "$tmpdir/brain-mcp.json" <