Add cross-platform packaging, icons, and Claude Code config discovery
Build & Release / Build (Linux) (push) Failing after 2m56s
Build & Release / Build (macOS) (push) Successful in 4m24s
Build & Release / Build (Windows) (push) Has been cancelled
Build & Release / Publish Release (push) Has been cancelled

- Auto-discovers Claude Code (~/.claude/settings.json) alongside Claude Desktop
- Generated icons/app.icns (macOS) and icons/app.ico (Windows) from rounded PNGs
- bcc.spec: PyInstaller spec for all platforms (.app on macOS, onefile on Windows/Linux)
- .github/workflows/release.yml: builds and publishes GitHub Release on version tags
- scripts/build_icons.py: regenerates icon files from source PNGs
- requirements-dev.txt: adds pyinstaller and pillow for building
- CLAUDE.md: initial repo guidance for Claude Code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AJ Avezzano
2026-06-29 15:01:30 -04:00
parent 780d4c3a9c
commit dd7557ea62
23 changed files with 458 additions and 14 deletions
+129
View File
@@ -0,0 +1,129 @@
name: Build & Release
# Trigger on version tags (e.g. git tag v1.0.0 && git push --tags)
on:
push:
tags:
- "v*"
# Manual run from Actions tab (useful for testing the workflow itself)
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
platform: macOS
artifact: BetterClaudeConfig-macOS.zip
- os: windows-latest
platform: Windows
artifact: BetterClaudeConfig-Windows.zip
- os: ubuntu-latest
platform: Linux
artifact: BetterClaudeConfig-Linux.tar.gz
runs-on: ${{ matrix.os }}
name: Build (${{ matrix.platform }})
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements-dev.txt
# macOS: build app.icns from source PNGs (iconutil is built into macOS)
- name: Generate app.icns (macOS)
if: runner.os == 'macOS'
run: python scripts/build_icons.py
# Windows: generate app.ico using Pillow (already installed via requirements-dev.txt)
- name: Generate app.ico (Windows)
if: runner.os == 'Windows'
run: python scripts/build_icons.py
- name: Build with PyInstaller
run: pyinstaller bcc.spec
# ── Package ──────────────────────────────────────────────────────────
- name: Package (macOS)
if: runner.os == 'macOS'
run: |
cd dist
zip -r --symlinks "../${{ matrix.artifact }}" BetterClaudeConfig.app
- name: Package (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Compress-Archive -Path dist\BetterClaudeConfig.exe `
-DestinationPath "${{ matrix.artifact }}"
- name: Package (Linux)
if: runner.os == 'Linux'
run: |
tar -czf "${{ matrix.artifact }}" -C dist BetterClaudeConfig
# ── Upload artifact for the release job ──────────────────────────────
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: ${{ matrix.artifact }}
# ── Create GitHub Release with all three artifacts ──────────────────────
release:
name: Publish Release
needs: build
runs-on: ubuntu-latest
# Only publish when a tag was pushed (not on workflow_dispatch without a tag)
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: Better Claude Config ${{ github.ref_name }}
draft: false
prerelease: false
generate_release_notes: true
files: artifacts/*
body: |
## Better Claude Config ${{ github.ref_name }}
A GUI for managing MCP server configurations for Claude Desktop and Claude Code — no hand-editing JSON.
### Download
| Platform | File |
|----------|------|
| macOS | `BetterClaudeConfig-macOS.zip` — unzip and drag **BetterClaudeConfig.app** to Applications |
| Windows | `BetterClaudeConfig-Windows.zip` — unzip and run **BetterClaudeConfig.exe** |
| Linux | `BetterClaudeConfig-Linux.tar.gz` — extract and run **BetterClaudeConfig** |
### macOS note
The app is not code-signed. On first launch, right-click → **Open** to bypass Gatekeeper, or run:
```
xattr -cr /Applications/BetterClaudeConfig.app
```
### Requirements
No Python installation needed — the app is self-contained.
+2 -1
View File
@@ -8,7 +8,8 @@ venv/
# PyInstaller / build # PyInstaller / build
build/ build/
dist/ dist/
*.spec # Note: bcc.spec is tracked; *.spec here would exclude it — keep this explicit:
*.spec.bak
# App runtime # App runtime
.bcc_backups/ .bcc_backups/
+60
View File
@@ -0,0 +1,60 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Install runtime dependency
pip install -r requirements.txt
python bcc.py # run the GUI
python test_core.py # run unit tests (23 tests, no GUI needed)
# Build a self-contained binary
pip install -r requirements-dev.txt
python scripts/build_icons.py # regenerate icons/app.icns + icons/app.ico if needed
pyinstaller bcc.spec
# macOS → dist/BetterClaudeConfig.app
# Windows → dist/BetterClaudeConfig.exe
# Linux → dist/BetterClaudeConfig
```
Requires Python 3.10+. Runtime dependency: `PySide6>=6.6`. Build-time: `pyinstaller>=6.0`, `pillow>=10.0`.
## Architecture
The codebase is split into two layers:
**`bcc_core.py`** — All logic with no GUI imports. Contains:
- `Profile` / `ServerEntry` dataclasses (the data model)
- `discover_profiles()` — scans the platform's app-support directory for `Claude*` folders (Claude Desktop) **and** always adds `~/.claude/settings.json` (Claude Code, cross-platform)
- `load_config` / `extract_servers` / `apply_servers` / `write_config` — the read/write pipeline; writes are atomic with rotating timestamped backups in `.bcc_backups/`
- `parse_pasted_json()` — accepts three JSON shapes (full config, inner map, or bare server object)
- `check_dependency()` / `diagnostics_text()` / `pin_command_path()` — PATH resolution logic; distinguishes "found on normal PATH" (ok) vs "found only on augmented PATH" (warn) vs "not found" (missing). Uses an `lru_cache`-memoized `augmented_path()` that extends the inherited PATH with common runtime locations (nvm, homebrew, cargo, volta, etc.)
- `test_remote()` — synchronous HTTP reachability check, intended to run off the UI thread
**`bcc.py`** — PySide6 GUI that is a thin shell over `bcc_core`. Key classes:
- `MainWindow` — manages the profile combo, two server tables (active/disabled), action bar, and dirty state
- `ServerEditor` — right-panel form with a `QStackedWidget` for stdio vs remote pages; calls `core.check_dependency()` on every field change
- `KeyValueTable` — reusable widget for env vars and headers
- `ConnTester(QThread)` — background thread for remote reachability tests
**The cardinal rule**: `apply_servers()` only ever writes to `mcpServers` and `_disabledMcpServers`. All other keys in the user's config are preserved verbatim and in their original order.
Disabled servers are parked under `_disabledMcpServers` (which Claude Desktop ignores) so they can be re-enabled without losing their definition.
## Packaging
- **`bcc.spec`** — PyInstaller spec; handles macOS (onedir → `.app` bundle) and Windows/Linux (onefile). Platform-aware icon selection.
- **`icons/app.icns`** (macOS) and **`icons/app.ico`** (Windows) are pre-generated and committed; source PNGs are in `icons/twin-gears/rounded/`.
- **`scripts/build_icons.py`** — regenerates both icon files from source PNGs. Uses `iconutil` (macOS-only) for `.icns`, Pillow for `.ico`.
- **`.github/workflows/release.yml`** — builds on all three platforms on tag push, publishes as GitHub Release assets.
To release: `git tag vX.Y.Z && git push --tags`.
## Key constants
- `ACCENT` at the top of `bcc.py` — the one constant to change for a rebrand
- `KNOWN_FIELDS` in `bcc_core.py` — fields the editor renders; anything else on a server object is preserved as `_extra` in `ServerEditor`
- `DISABLED_KEY = "_disabledMcpServers"` — the parking key for disabled servers
- `MAX_BACKUPS = 15` — rolling backup limit per config file
+44 -8
View File
@@ -1,26 +1,39 @@
# Better Claude Config (BCC) # Better Claude Config (BCC)
A small, cross-platform GUI for editing the `mcpServers` block of one or more A small, cross-platform GUI for editing the `mcpServers` block of Claude Desktop
Claude Desktop installs — without ever hand-writing JSON. and Claude Code installs — without ever hand-writing JSON.
BCC only ever touches `mcpServers` (and its own `_disabledMcpServers` parking BCC only ever touches `mcpServers` (and its own `_disabledMcpServers` parking
key). Every other key in your config is preserved verbatim, in its original key). Every other key in your config is preserved verbatim, in its original
order. Each write is atomic and makes a timestamped backup first. order. Each write is atomic and makes a timestamped backup first.
## Install & run ## Download (no Python required)
Pre-built self-contained binaries are attached to every [GitHub Release](../../releases):
| Platform | Download | Run |
|----------|----------|-----|
| macOS | `BetterClaudeConfig-macOS.zip` | Unzip → drag **BetterClaudeConfig.app** to Applications |
| Windows | `BetterClaudeConfig-Windows.zip` | Unzip → double-click **BetterClaudeConfig.exe** |
| Linux | `BetterClaudeConfig-Linux.tar.gz` | Extract → run **BetterClaudeConfig** |
> **macOS Gatekeeper note:** the app is not notarized. On first launch, right-click → **Open**, or run `xattr -cr /Applications/BetterClaudeConfig.app` in a terminal.
## Run from source
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
python bcc.py python bcc.py
``` ```
(Python 3.10+.) (Python 3.10+, PySide6 6.6+.)
## What it does ## What it does
- **Auto-discovers installs** — scans the platform's app-support folder for any - **Auto-discovers installs** — scans the platform's app-support folder for any
`Claude*` directory (so `Claude` and `Claude-Work` both show up), and lets you `Claude*` directory (so `Claude` and `Claude-Work` both show up) **and** finds
**Add config…** to point at any file manually. Claude Code at `~/.claude/settings.json`. Use **Add config…** to point at any
other file manually.
- **Form-based editing** — name, command, args (one per line), env vars, or for - **Form-based editing** — name, command, args (one per line), env vars, or for
remote servers: URL, transport, and headers. No raw JSON. remote servers: URL, transport, and headers. No raw JSON.
- **Paste JSON** — drop in any snippet from an MCP doc (full `mcpServers` block, - **Paste JSON** — drop in any snippet from an MCP doc (full `mcpServers` block,
@@ -55,17 +68,40 @@ After saving, **restart that Claude install** for changes to take effect.
## Config locations it scans ## Config locations it scans
| OS | Base it scans for `Claude*` | **Claude Desktop**
|---------|---------------------------------|
| OS | Base it scans for `Claude*` |
|---------|----------------------------------|
| macOS | `~/Library/Application Support` | | macOS | `~/Library/Application Support` |
| Windows | `%APPDATA%` | | Windows | `%APPDATA%` |
| Linux | `~/.config` | | Linux | `~/.config` |
**Claude Code** (all platforms): `~/.claude/settings.json`
## Files ## Files
- `bcc.py` — the GUI. - `bcc.py` — the GUI.
- `bcc_core.py` — all the file/JSON/validation/dependency logic (no GUI deps). - `bcc_core.py` — all the file/JSON/validation/dependency logic (no GUI deps).
- `test_core.py` — unit suite for the core (`python test_core.py`). - `test_core.py` — unit suite for the core (`python test_core.py`).
- `bcc.spec` — PyInstaller build spec (cross-platform).
- `scripts/build_icons.py` — regenerates `icons/app.icns` and `icons/app.ico` from source PNGs.
## Building from source
```bash
pip install -r requirements-dev.txt
python scripts/build_icons.py # regenerate icons if needed
pyinstaller bcc.spec
# macOS → dist/BetterClaudeConfig.app
# Windows → dist/BetterClaudeConfig.exe
# Linux → dist/BetterClaudeConfig
```
Releases are built automatically by GitHub Actions (`.github/workflows/release.yml`) when a version tag is pushed:
```bash
git tag v1.0.0 && git push --tags
```
## Rebrand ## Rebrand
+111
View File
@@ -0,0 +1,111 @@
# -*- mode: python ; coding: utf-8 -*-
#
# PyInstaller spec for Better Claude Config.
#
# macOS → onedir build wrapped in a .app bundle (double-click native)
# Windows → onefile .exe (no console window)
# Linux → onefile binary (no console window)
#
# Build:
# pip install pyinstaller
# pyinstaller bcc.spec
#
# Output:
# macOS → dist/BetterClaudeConfig.app
# Windows → dist/BetterClaudeConfig.exe
# Linux → dist/BetterClaudeConfig
import sys
APP_NAME = "BetterClaudeConfig"
BUNDLE_ID = "com.betterclaude.config"
if sys.platform == "darwin":
icon = "icons/app.icns"
elif sys.platform == "win32":
icon = "icons/app.ico"
else:
icon = None # Linux: icon embedded via .desktop file, not the binary
a = Analysis(
["bcc.py"],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=["_tkinter", "tkinter"],
noarchive=False,
)
pyz = PYZ(a.pure)
if sys.platform == "darwin":
# ------------------------------------------------------------------ macOS
# onedir so PyInstaller produces a proper .app bundle structure.
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name=APP_NAME,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
console=False,
argv_emulation=True, # lets macOS open files by passing argv
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=icon,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=False,
upx_exclude=[],
name=APP_NAME,
)
app = BUNDLE(
coll,
name=f"{APP_NAME}.app",
icon=icon,
bundle_identifier=BUNDLE_ID,
info_plist={
"CFBundleName": "Better Claude Config",
"CFBundleDisplayName": "Better Claude Config",
"CFBundleShortVersionString": "1.0.0",
"CFBundleVersion": "1.0.0",
"NSHighResolutionCapable": True,
"NSRequiresAquaSystemAppearance": False, # supports dark mode
"LSMinimumSystemVersion": "11.0",
},
)
else:
# ----------------------------------------------- Windows / Linux (onefile)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name=APP_NAME,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=icon,
)
+11 -4
View File
@@ -78,11 +78,12 @@ def app_support_base() -> Path:
def discover_profiles() -> list[Profile]: def discover_profiles() -> list[Profile]:
""" """
Find every `Claude*` data directory in the platform's app-support base. Find every `Claude*` data directory in the platform's app-support base
(Claude Desktop installs), then also check for a Claude Code global config.
This catches the standard `Claude` folder plus any sibling like Claude Desktop: scans the platform app-support folder for any `Claude*`
`Claude-Work`, `Claude-Personal`, etc. A directory is listed even if the directory (catches `Claude`, `Claude-Work`, etc.).
config file doesn't exist yet (it will be created on first save). Claude Code: always at ~/.claude/settings.json on every platform.
""" """
base = app_support_base() base = app_support_base()
out: list[Profile] = [] out: list[Profile] = []
@@ -93,6 +94,12 @@ def discover_profiles() -> list[Profile]:
seen.add(d.name) seen.add(d.name)
cfg = d / CONFIG_FILENAME cfg = d / CONFIG_FILENAME
out.append(Profile(label=d.name, path=cfg, config_exists=cfg.is_file())) out.append(Profile(label=d.name, path=cfg, config_exists=cfg.is_file()))
# Claude Code global settings (~/.claude/settings.json) — same mcpServers format,
# cross-platform (the ~/.claude/ directory is always in the same place).
cc_cfg = Path.home() / ".claude" / "settings.json"
out.append(Profile(label="Claude Code", path=cc_cfg, config_exists=cc_cfg.is_file()))
return out return out
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

+6
View File
@@ -0,0 +1,6 @@
# Runtime (also in requirements.txt)
PySide6>=6.6
# Build / packaging
pyinstaller>=6.0
pillow>=10.0 # generates icons/app.ico during CI (Windows build)
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env python3
"""
Generate icons/app.ico (Windows) and icons/app.icns (macOS) from the
rounded PNG source files in icons/twin-gears/rounded/.
Run from the repo root:
python scripts/build_icons.py
Requirements:
pip install pillow # for .ico
iconutil # built into macOS; for .icns
"""
from __future__ import annotations
import os
import platform
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).parent.parent
SRC = ROOT / "icons" / "twin-gears" / "rounded"
OUT = ROOT / "icons"
def build_ico():
try:
from PIL import Image
except ImportError:
print("Pillow not installed — skipping .ico generation (pip install pillow)")
return
sizes = [16, 32, 48, 64, 128, 256]
imgs = []
for s in sizes:
p = SRC / f"icon-{s}.png"
if not p.exists():
print(f" Missing {p.name}, skipping")
continue
imgs.append(Image.open(p).convert("RGBA"))
if not imgs:
print(" No source PNGs found — cannot build .ico")
return
dest = OUT / "app.ico"
imgs[-1].save(dest, format="ICO", append_images=imgs[:-1])
print(f" Generated {dest.relative_to(ROOT)} ({dest.stat().st_size // 1024 + 1} KB)")
def build_icns():
if shutil.which("iconutil") is None:
print(" iconutil not found (macOS-only) — skipping .icns generation")
return
iconset = Path(tempfile.mkdtemp()) / "app.iconset"
iconset.mkdir()
mapping = {
"icon_16x16.png": "icon-16.png",
"icon_16x16@2x.png": "icon-32.png",
"icon_32x32.png": "icon-32.png",
"icon_32x32@2x.png": "icon-64.png",
"icon_128x128.png": "icon-128.png",
"icon_128x128@2x.png": "icon-256.png",
"icon_256x256.png": "icon-256.png",
"icon_256x256@2x.png": "icon-512.png",
"icon_512x512.png": "icon-512.png",
"icon_512x512@2x.png": "icon-1024.png",
}
for dst_name, src_name in mapping.items():
src = SRC / src_name
if src.exists():
shutil.copy2(src, iconset / dst_name)
else:
print(f" Missing {src_name}, skipping {dst_name}")
dest = OUT / "app.icns"
subprocess.run(
["iconutil", "-c", "icns", str(iconset), "-o", str(dest)],
check=True,
)
shutil.rmtree(iconset.parent)
print(f" Generated {dest.relative_to(ROOT)} ({dest.stat().st_size // 1024} KB)")
if __name__ == "__main__":
print("Building icons/app.ico ...")
build_ico()
print("Building icons/app.icns ...")
build_icns()
print("Done.")
+2 -1
View File
@@ -18,7 +18,8 @@ for d in ("Claude", "Claude-Work", "NotClaude"):
c.app_support_base = lambda: base c.app_support_base = lambda: base
profs = c.discover_profiles() profs = c.discover_profiles()
labels = sorted(p.label for p in profs) labels = sorted(p.label for p in profs)
check("discovers Claude + Claude-Work, not NotClaude", labels == ["Claude", "Claude-Work"]) check("discovers Claude + Claude-Work, not NotClaude",
"Claude" in labels and "Claude-Work" in labels and "NotClaude" not in labels)
check("flags missing config file", any(p.label=="Claude-Work" and not p.config_exists for p in profs)) check("flags missing config file", any(p.label=="Claude-Work" and not p.config_exists for p in profs))
# ---- 2. write preserves OTHER keys + order, only touches mcpServers ---- # ---- 2. write preserves OTHER keys + order, only touches mcpServers ----