[#10] Server catalog / one-click add — design pass first #10

Open
opened 2026-07-02 00:51:12 -04:00 by the_og · 6 comments
Owner

Priority: P2 — design before building

Do not implement without first posting a design proposal as a comment on this issue. The scope here is deliberately open-ended; designing in a comment first keeps the supervising session in the loop before code is written.

Problem

There is a gap between "I read about a cool MCP server" and "it is running" — copy-pasting JSON from docs is the default flow. BCC already solves the pasting half well (broken-JSON repair, verbose feedback). The remaining gap is discovery: a small curated list of popular servers with pre-filled config the user can add with one click.

Two of the three leading competing tools list a server catalog as a differentiator.

Proposed solution (sketch — refine in a design comment)

A simple static/curated catalog (not a live registry integration) shown as a picker dialog:

  • Pre-filled entries for filesystem, GitHub, Postgres, Slack, etc.
  • Each entry feeds into the existing paste/import pipeline (so repair, env-var handling, etc. all apply automatically).
  • The catalog is a versioned JSON file in the repo — no live network calls required.

Before coding

Post a comment on this issue with:

  1. Proposed catalog format (JSON schema)
  2. UI sketch (picker dialog vs inline panel vs menu)
  3. How entries interact with the existing paste/import flow
  4. How to keep the catalog updated over time

Relevant code

  • bcc_core.py::parse_pasted_json_verbose — the natural entry point for catalog items
  • bcc.py paste dialog — likely reuse path
## Priority: P2 — design before building **Do not implement without first posting a design proposal as a comment on this issue.** The scope here is deliberately open-ended; designing in a comment first keeps the supervising session in the loop before code is written. ## Problem There is a gap between "I read about a cool MCP server" and "it is running" — copy-pasting JSON from docs is the default flow. BCC already solves the *pasting* half well (broken-JSON repair, verbose feedback). The remaining gap is discovery: a small curated list of popular servers with pre-filled config the user can add with one click. Two of the three leading competing tools list a server catalog as a differentiator. ## Proposed solution (sketch — refine in a design comment) A simple **static/curated** catalog (not a live registry integration) shown as a picker dialog: - Pre-filled entries for filesystem, GitHub, Postgres, Slack, etc. - Each entry feeds into the existing paste/import pipeline (so repair, env-var handling, etc. all apply automatically). - The catalog is a versioned JSON file in the repo — no live network calls required. ## Before coding Post a comment on this issue with: 1. Proposed catalog format (JSON schema) 2. UI sketch (picker dialog vs inline panel vs menu) 3. How entries interact with the existing paste/import flow 4. How to keep the catalog updated over time ## Relevant code - `bcc_core.py::parse_pasted_json_verbose` — the natural entry point for catalog items - `bcc.py` paste dialog — likely reuse path
the_og added the P2 label 2026-07-02 00:51:12 -04:00
Author
Owner

Scope expansion (AJ) — this is the "MCP server marketplace" idea; recording it as a roadmap feature. Extends the catalog sketch above with:

  • Search / filter over the catalog, not just a flat picker — as the list grows this is what makes it a "marketplace" rather than a short menu.
  • Tiered entries by setup difficulty:
    • Basic servers (filesystem, GitHub, fetch, etc.) → pre-filled config, one-click add through the existing paste/import pipeline (repair, env handling, secret masking all apply for free).
    • Complex servers (need OAuth apps, external accounts, non-trivial setup) → don't ship a half-config that will just break. Instead show a short description plus a link to the source/setup docs, so the catalog is honest about what needs manual work.
    • Catalog schema should carry a per-entry setup: "basic" | "link-only" (or similar) so the UI knows whether to offer "Add" or "Open setup guide".

Still design-first per the original note (post a design comment before coding), and still curated static JSON in-repo rather than a live registry to start. Framing as roadmap / later — not committed to the next release yet.

Scope expansion (AJ) — this is the "MCP server marketplace" idea; recording it as a **roadmap** feature. Extends the catalog sketch above with: - **Search / filter** over the catalog, not just a flat picker — as the list grows this is what makes it a "marketplace" rather than a short menu. - **Tiered entries by setup difficulty:** - *Basic servers* (filesystem, GitHub, fetch, etc.) → pre-filled config, one-click add through the existing paste/import pipeline (repair, env handling, secret masking all apply for free). - *Complex servers* (need OAuth apps, external accounts, non-trivial setup) → **don't ship a half-config that will just break**. Instead show a short description plus a **link to the source/setup docs**, so the catalog is honest about what needs manual work. - Catalog schema should carry a per-entry `setup: "basic" | "link-only"` (or similar) so the UI knows whether to offer "Add" or "Open setup guide". Still **design-first** per the original note (post a design comment before coding), and still **curated static JSON in-repo** rather than a live registry to start. Framing as roadmap / later — not committed to the next release yet.
Author
Owner

Design pass (Cowork supervisor)

Goal

A searchable, curated catalog of popular MCP servers so users can add one without hand-writing JSON — the natural home for the search box (#27) and the paste/import pipeline we already have.

Data source (start static)

Ship a curated catalog.json inside the app (bundled via the datas mechanism just added in #20). Start static; a later phase can fetch a remote manifest reusing the update-checker's fail-quiet HTTP path. Version the file so we can evolve the schema.

Entry schema (proposed):

{
  "name": "brave-search",
  "title": "Brave Search",
  "description": "Web search via the Brave API.",
  "category": "search",
  "homepage": "https://…",
  "setup": "basic" | "link-only",
  "config": {                     // present when setup == "basic"
    "command": "npx",
    "args": ["-y", "@modelcontextprotocol/server-brave-search"],
    "env": {"BRAVE_API_KEY": "<YOUR_BRAVE_API_KEY>"}
  },
  "docs_url": "https://…",        // present when setup == "link-only"
  "notes": "Requires a free Brave API key."
}

Tiers

  • basic — fully prefillable. One-click "Add" builds a ServerEntry with placeholder env values and routes it through the existing paste/import pipeline (parse_pasted_json_import_server) so we reuse the duplicate-name prompt (#8) and secret masking for free. After adding, open the editor focused on the env fields so the user drops in their key.
  • link-only — complex/manual setup. Show the description + an "Open setup docs" button (QDesktopServices.openUrl); do not auto-add.

Testable core (keep logic out of the GUI)

  • load_catalog(path) -> list[CatalogEntry] + a validator (required fields per tier).
  • catalog_entry_to_paste_json(entry) -> dict (pure) — the basic-tier → importable-config builder. Unit-test both.

UI

A "Browse catalog…" button near Add/Paste opens a dialog: search box (reuse the server_matches_filter idea), category chips, a list with per-row Add (basic) or Docs (link-only).

Phasing

  1. Static catalog.json + loader/validator/builder + the dialog (basic + link-only).
  2. Remote manifest with version/etag (reuse update-checker infra), still falling back to the bundled copy offline.
  3. Community submissions / PR-to-catalog flow.

Open decisions for you

  1. Initial curated list — which ~15–25 servers to seed (I can draft one from the official MCP servers repo + popular community ones).
  2. Category taxonomy — e.g. search / filesystem / dev-tools / data / productivity / cloud.
  3. Static-first vs remote-first — I recommend static-first (simpler, offline-safe), remote later.

If you're happy with this shape, the phase-1 core (loader + builder + tests) is a clean subagent task; I'd hold the curated list for your input on #1.

## Design pass (Cowork supervisor) ### Goal A searchable, curated catalog of popular MCP servers so users can add one without hand-writing JSON — the natural home for the search box (#27) and the paste/import pipeline we already have. ### Data source (start static) Ship a curated `catalog.json` inside the app (bundled via the `datas` mechanism just added in #20). Start static; a later phase can fetch a remote manifest reusing the update-checker's fail-quiet HTTP path. Version the file so we can evolve the schema. Entry schema (proposed): ``` { "name": "brave-search", "title": "Brave Search", "description": "Web search via the Brave API.", "category": "search", "homepage": "https://…", "setup": "basic" | "link-only", "config": { // present when setup == "basic" "command": "npx", "args": ["-y", "@modelcontextprotocol/server-brave-search"], "env": {"BRAVE_API_KEY": "<YOUR_BRAVE_API_KEY>"} }, "docs_url": "https://…", // present when setup == "link-only" "notes": "Requires a free Brave API key." } ``` ### Tiers - **basic** — fully prefillable. One-click "Add" builds a `ServerEntry` with placeholder env values and routes it through the **existing** paste/import pipeline (`parse_pasted_json` → `_import_server`) so we reuse the duplicate-name prompt (#8) and secret masking for free. After adding, open the editor focused on the env fields so the user drops in their key. - **link-only** — complex/manual setup. Show the description + an "Open setup docs" button (`QDesktopServices.openUrl`); do not auto-add. ### Testable core (keep logic out of the GUI) - `load_catalog(path) -> list[CatalogEntry]` + a validator (required fields per tier). - `catalog_entry_to_paste_json(entry) -> dict` (pure) — the basic-tier → importable-config builder. Unit-test both. ### UI A "Browse catalog…" button near Add/Paste opens a dialog: search box (reuse the `server_matches_filter` idea), category chips, a list with per-row **Add** (basic) or **Docs** (link-only). ### Phasing 1. Static `catalog.json` + loader/validator/builder + the dialog (basic + link-only). 2. Remote manifest with version/etag (reuse update-checker infra), still falling back to the bundled copy offline. 3. Community submissions / PR-to-catalog flow. ### Open decisions for you 1. **Initial curated list** — which ~15–25 servers to seed (I can draft one from the official MCP servers repo + popular community ones). 2. **Category taxonomy** — e.g. search / filesystem / dev-tools / data / productivity / cloud. 3. **Static-first vs remote-first** — I recommend static-first (simpler, offline-safe), remote later. If you're happy with this shape, the phase-1 core (loader + builder + tests) is a clean subagent task; I'd hold the curated list for your input on #1.
Author
Owner

Design pass (2026-07-12) — proposed for next week's release

Scope: curated static catalog, no live registry. A catalog.json shipped with the app (and bundled via the existing datas mechanism):

{
  "version": 1,
  "updated": "2026-07-12",
  "servers": [
    {
      "id": "filesystem",
      "display": "Filesystem",
      "description": "Read/write access to local directories you choose.",
      "category": "files",
      "homepage": "https://github.com/modelcontextprotocol/servers",
      "stars": 12345,
      "config": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "<PATH>"]},
      "placeholders": {"<PATH>": "Directory to expose"},
      "env_required": {}
    }
  ]
}
  • Popularity signal (the differentiator per the round-2 competitor audit): a stars snapshot refreshed at release time by a small scripts/refresh_catalog.py (hits the GitHub API at build time, never at app runtime — keeps the app offline-safe and fail-quiet).
  • UI: "Add from catalog…" button next to Paste JSON… → dialog with search box (reuse server_matches_filter style), category filter, detail pane (description + stars + homepage link), Add button. Add feeds the existing _import_server path (name-collision prompt included), then focuses the editor with placeholder fields highlighted — env secrets are never pre-filled.
  • Curation v1: ~15 entries (filesystem, github, git, postgres, sqlite, slack, brave-search, puppeteer/playwright, fetch, memory, sequential-thinking, google-maps, sentry, cloudflare, home-assistant).
  • Core/testable: catalog load/validate (load_catalog() -> list[CatalogEntry], schema-checked, fail-quiet), placeholder substitution check, "config parses through looks_like_server". GUI dialog itself stays thin.

Estimate: one PR for core+data+script, one for the dialog. Fits comfortably before next week's release alongside #5 Phase 1.

## Design pass (2026-07-12) — proposed for next week's release **Scope: curated static catalog, no live registry.** A `catalog.json` shipped with the app (and bundled via the existing `datas` mechanism): ```json { "version": 1, "updated": "2026-07-12", "servers": [ { "id": "filesystem", "display": "Filesystem", "description": "Read/write access to local directories you choose.", "category": "files", "homepage": "https://github.com/modelcontextprotocol/servers", "stars": 12345, "config": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "<PATH>"]}, "placeholders": {"<PATH>": "Directory to expose"}, "env_required": {} } ] } ``` - **Popularity signal** (the differentiator per the round-2 competitor audit): a `stars` snapshot refreshed at release time by a small `scripts/refresh_catalog.py` (hits the GitHub API at build time, never at app runtime — keeps the app offline-safe and fail-quiet). - **UI**: "Add from catalog…" button next to Paste JSON… → dialog with search box (reuse `server_matches_filter` style), category filter, detail pane (description + stars + homepage link), Add button. Add feeds the existing `_import_server` path (name-collision prompt included), then focuses the editor with placeholder fields highlighted — env secrets are never pre-filled. - **Curation v1**: ~15 entries (filesystem, github, git, postgres, sqlite, slack, brave-search, puppeteer/playwright, fetch, memory, sequential-thinking, google-maps, sentry, cloudflare, home-assistant). - **Core/testable**: catalog load/validate (`load_catalog() -> list[CatalogEntry]`, schema-checked, fail-quiet), placeholder substitution check, "config parses through looks_like_server". GUI dialog itself stays thin. Estimate: one PR for core+data+script, one for the dialog. Fits comfortably before next week's release alongside #5 Phase 1.
Author
Owner

Catalog research pass (2026-07-12) — 112 servers surveyed

Ran a 4-way parallel research fan-out (official/dev tooling · business SaaS · cloud/infra/data · personal/homelab/search), verifying every config against primary sources (server README, vendor docs, npm/PyPI). Merged + deduped + schema-validated against the entry schema from the design comment above.

Result: 112 servers — 84 basic / 28 link-only; 80 are official vendor servers. Draft artifacts live in the workspace (catalog-draft.json + MCP-CATALOG-RESEARCH-COWORK.md + per-shard raw output with source URLs); not committed pending curation decisions.

Findings that change the design

1. The link-only tier is load-bearing. The major shift since this issue was opened: vendors have moved from local stdio servers to remote HTTP/SSE + OAuth endpoints. Supabase, Neon, PlanetScale, Vercel, Sentry, Cloudflare, Datadog, Hugging Face, Slack, Linear, Atlassian, HubSpot, Figma, Canva, Zapier, Zoom, Google Workspace and Microsoft 365 all ship remote-only servers with no supported local API-key stdio config. A prefilled "basic" config is structurally impossible for a quarter of the catalog — including much of what users actually search for. The tiered setup: basic | link-only decision holds up; the link-only path must be first-class in the dialog, not hidden.

2. Several reference servers are archived. modelcontextprotocol/servers archived GitHub, GitLab, Postgres, Puppeteer, Redis, Sentry, SQLite, Slack and Brave Search into servers-archived. The catalog must point at live successors (brave/brave-search-mcp-server, crystaldba/postgres-mcp, Playwright/Chrome-DevTools over Puppeteer). Recommend excluding sqlite and puppeteer from v1 — SQLite now needs a git checkout to run, which is the opposite of one-click.

3. @modelcontextprotocol/server-github (npm) is discontinued — GitHub's official server is Docker-local or hosted-remote now. Worth catching, since github is the single most obvious catalog entry.

4. Star ranking needs a fix before it can drive sort order. The 7 reference servers each inherit the monorepo's ~86k stars, so a naive star sort puts them all on top. Needs a per-entry override or monorepo discount in scripts/refresh_catalog.py. Some counts are also rounded (GitHub API rate-limited during research) — treat as hints until the build-time refresher runs.

Proposed v1 seed (20) — up from the 15 sketched above

filesystem, fetch, memory, sequential-thinking, git, github, playwright, chrome-devtools, postgres, n8n, notion, obsidian, brave-search, tavily, firecrawl, home-assistant, kubernetes, aws-api-mcp-server, grafana, slack

Filter: broad appeal × verified basic-tier config × no OAuth-app registration needed (except slack, kept as the canonical link-only demo).

Category taxonomy (20, from the merged data)

files, dev, code-hosting, database, browser, search, ai, data, cloud, infra, observability, productivity, communication, crm, finance, design, media, smart-home, personal, other

Collapse to ~7 chips in the browse dialog: Files & Dev · Data · Search & AI · Cloud & Infra · Work · Home & Personal · Other.

Decisions needed

  1. Seed size — ship 20, or ship the full 112? Search/filter is already built (#27), and a searchable 112-entry list is what makes this a marketplace rather than a menu.
  2. Drop the 2 deprecated entries, or keep them with a warning badge?
  3. Confirm link-only entries are shown in the dialog (with a Docs button) rather than filtered out.

Phase-1 core (loader + validator + catalog_entry_to_paste_json builder + tests) is unblocked and ready to hand to a subagent once (1) is decided.

## Catalog research pass (2026-07-12) — 112 servers surveyed Ran a 4-way parallel research fan-out (official/dev tooling · business SaaS · cloud/infra/data · personal/homelab/search), verifying every config against primary sources (server README, vendor docs, npm/PyPI). Merged + deduped + schema-validated against the entry schema from the design comment above. **Result: 112 servers — 84 `basic` / 28 `link-only`; 80 are official vendor servers.** Draft artifacts live in the workspace (`catalog-draft.json` + `MCP-CATALOG-RESEARCH-COWORK.md` + per-shard raw output with source URLs); not committed pending curation decisions. ### Findings that change the design **1. The `link-only` tier is load-bearing.** The major shift since this issue was opened: vendors have moved from local stdio servers to **remote HTTP/SSE + OAuth endpoints**. Supabase, Neon, PlanetScale, Vercel, Sentry, Cloudflare, Datadog, Hugging Face, Slack, Linear, Atlassian, HubSpot, Figma, Canva, Zapier, Zoom, Google Workspace and Microsoft 365 all ship remote-only servers with **no supported local API-key stdio config**. A prefilled "basic" config is structurally impossible for a quarter of the catalog — including much of what users actually search for. The tiered `setup: basic | link-only` decision holds up; the link-only path must be first-class in the dialog, not hidden. **2. Several reference servers are archived.** `modelcontextprotocol/servers` archived GitHub, GitLab, Postgres, Puppeteer, Redis, Sentry, SQLite, Slack and Brave Search into `servers-archived`. The catalog must point at live successors (`brave/brave-search-mcp-server`, `crystaldba/postgres-mcp`, Playwright/Chrome-DevTools over Puppeteer). Recommend **excluding `sqlite` and `puppeteer` from v1** — SQLite now needs a git checkout to run, which is the opposite of one-click. **3. `@modelcontextprotocol/server-github` (npm) is discontinued** — GitHub's official server is Docker-local or hosted-remote now. Worth catching, since `github` is the single most obvious catalog entry. **4. Star ranking needs a fix before it can drive sort order.** The 7 reference servers each inherit the monorepo's ~86k stars, so a naive star sort puts them all on top. Needs a per-entry override or monorepo discount in `scripts/refresh_catalog.py`. Some counts are also rounded (GitHub API rate-limited during research) — treat as hints until the build-time refresher runs. ### Proposed v1 seed (20) — up from the 15 sketched above `filesystem`, `fetch`, `memory`, `sequential-thinking`, `git`, `github`, `playwright`, `chrome-devtools`, `postgres`, `n8n`, `notion`, `obsidian`, `brave-search`, `tavily`, `firecrawl`, `home-assistant`, `kubernetes`, `aws-api-mcp-server`, `grafana`, `slack` Filter: broad appeal × verified basic-tier config × no OAuth-app registration needed (except `slack`, kept as the canonical link-only demo). ### Category taxonomy (20, from the merged data) `files`, `dev`, `code-hosting`, `database`, `browser`, `search`, `ai`, `data`, `cloud`, `infra`, `observability`, `productivity`, `communication`, `crm`, `finance`, `design`, `media`, `smart-home`, `personal`, `other` Collapse to ~7 chips in the browse dialog: **Files & Dev** · **Data** · **Search & AI** · **Cloud & Infra** · **Work** · **Home & Personal** · **Other**. ### Decisions needed 1. **Seed size** — ship 20, or ship the full 112? Search/filter is already built (#27), and a searchable 112-entry list is what makes this a *marketplace* rather than a menu. 2. Drop the 2 deprecated entries, or keep them with a warning badge? 3. Confirm `link-only` entries are shown in the dialog (with a Docs button) rather than filtered out. Phase-1 core (loader + validator + `catalog_entry_to_paste_json` builder + tests) is unblocked and ready to hand to a subagent once (1) is decided.
Author
Owner

Decision + final design (2026-07-12) — signed remote catalog

Decisions taken with AJ after the research pass above.

1. Distribution: bundle and refresh (not either/or)

Pure-bundled rots — the research proved the churn rate empirically (9 reference servers archived, GitHub's npm package discontinued, ClickUp gone proprietary, most SaaS vendors migrated to remote OAuth). A list frozen into a release contains broken npx lines within a quarter, and the failure lands on the user: they click Add, Claude fails to spawn the server, BCC looks like the broken thing.

Pure-remote breaks the offline and first-run cases and makes a core feature depend on our uptime forever.

The real argument for remote is release decoupling. Today a catalog fix costs a tag push + 3-platform PyInstaller build + a release. That cost is why the list would rot. Remote turns "fix a broken entry" into a one-line commit.

Shape:

  • data/catalog.json bundled via the datas mechanism from #20. Always present, always works, zero network at startup.
  • On opening the Browse dialog (not at app startup — don't tax launch), if the cache is >24h old, background-fetch from a hardcoded HTTPS raw URL on this repo. Reuse the update checker's fail-quiet HTTP path from #19.
  • Cache under AppDataLocation. Resolve by version: max(bundled, cached) — a fresh install must never be pinned to a stale cache.
  • Any failure at any step → silent fallback to cache, then bundled. Never a dialog, never a blocking spinner.

2. Trust: the remote catalog is signed, and signing is the approval act

The threat here is not a stranger pushing to the repo. It is the maintainer merging a friendly-looking PR that changes one args entry — because a catalog entry is a command + args that BCC writes into a config file that Claude then executes. That is a code-execution supply-chain surface, and it is more serious than the update checker (which is notify-only precisely to avoid this class of risk).

Detached Ed25519 signature over the catalog bytes. Public key compiled into the binary; private key never in the repo, never in CI.

data/catalog.json        # payload — anyone can PR this
data/catalog.json.sig    # detached signature — only the maintainer can produce this
bcc_core.CATALOG_PUBKEYS # list (not a single constant, so keys can be rotated)

Load order on remote fetch: fetch → verify signature → anti-rollback version check → schema+allowlist validate → use. Any step fails, the whole remote file is discarded.

Why this gives the property we want: a contributor's PR can change catalog.json, but cannot produce a matching signature. If the maintainer blindly merges it, the signature no longer verifies the file — so every BCC in the world rejects the remote catalog and keeps the last good one. The bad entry reaches nobody. Publishing requires deliberately signing with the key. Signing is the approval act, and it cannot be performed by accident.

Defence in depth:

  • Anti-rollback: reject a remote catalog whose version is lower than the cached/bundled one. Stops replay of an older validly signed catalog to reintroduce a since-removed bad entry.
  • Command allowlist in the validator: npx / uvx / docker / node / python only; reject the file otherwise. (The 20-entry seed uses only npx ×11, uvx ×6, docker ×2 — the allowlist costs us nothing today.)
  • CI gate: verify catalog.json.sig against catalog.json on every push to main. A merged-but-unsigned PR turns main red in 60 seconds instead of never, and it catches a forgotten re-sign.
  • Existing guarantee still holds underneath: catalog entries route through the import + editor flow, so the user sees the command before it's written. Signing protects users who don't look; the editor protects those who do.

Dependency call: use the cryptography package for Ed25519 verify rather than hand-rolling RFC 8032 in stdlib. Bundle size is not a constraint at BCC's current footprint, and "we didn't hand-roll the crypto" is worth a few MB.

Tracked separately in #61 (in-app verification, remote refresh, anti-rollback) and #62 (Catalog Console — maintainer review + signing tool; signing is never a manual script).

3. Bundled seed: the curated 20

filesystem  fetch  memory  sequential-thinking  git  github  playwright  chrome-devtools
postgres  n8n  notion  obsidian  brave-search  tavily  firecrawl  home-assistant
kubernetes  aws-api-mcp-server  grafana  slack

19 basic / 1 link-only (slack, kept as the canonical link-only demo). 10 need a secret — env var names prefilled, values never. 3 carry <PLACEHOLDER> args.

The remaining 92 researched servers are the growth path for the signed remote catalog — they can ship without a binary release the moment the remote path lands. Staged in the workspace as catalog-seed-v1.json (bundled) and catalog-pool-112.json (pool, with source URLs for every verified config).

Phase 1 scope (this release)

  1. data/catalog.json (the 20) + load_catalog() / validate_catalog() (schema + command allowlist) / catalog_entry_to_paste_json() — all pure, all unit-tested, GUI-free.
  2. Browse-catalog dialog: search box (reuse server_matches_filter), category chips, detail pane, Add (basic) → existing _import_server path, Docs (link-only) → QDesktopServices.openUrl.
  3. Signature verify + remote refresh + cache (#61).

Category taxonomy collapses to 7 chips: Files & Dev · Data · Search & AI · Cloud & Infra · Work · Home & Personal · Other.

## Decision + final design (2026-07-12) — signed remote catalog Decisions taken with AJ after the research pass above. ### 1. Distribution: bundle **and** refresh (not either/or) Pure-bundled rots — the research proved the churn rate empirically (9 reference servers archived, GitHub's npm package discontinued, ClickUp gone proprietary, most SaaS vendors migrated to remote OAuth). A list frozen into a release contains broken `npx` lines within a quarter, and the failure lands on the *user*: they click Add, Claude fails to spawn the server, BCC looks like the broken thing. Pure-remote breaks the offline and first-run cases and makes a core feature depend on our uptime forever. **The real argument for remote is release decoupling.** Today a catalog fix costs a tag push + 3-platform PyInstaller build + a release. That cost is why the list would rot. Remote turns "fix a broken entry" into a one-line commit. **Shape:** - `data/catalog.json` bundled via the `datas` mechanism from #20. Always present, always works, zero network at startup. - On opening the Browse dialog (**not** at app startup — don't tax launch), if the cache is >24h old, background-fetch from a **hardcoded HTTPS raw URL** on this repo. Reuse the update checker's fail-quiet HTTP path from #19. - Cache under `AppDataLocation`. Resolve by `version`: `max(bundled, cached)` — a fresh install must never be pinned to a stale cache. - Any failure at any step → silent fallback to cache, then bundled. Never a dialog, never a blocking spinner. ### 2. Trust: the remote catalog is **signed**, and signing is the approval act The threat here is **not** a stranger pushing to the repo. It is the maintainer merging a friendly-looking PR that changes one `args` entry — because a catalog entry is a `command` + `args` that BCC writes into a config file that Claude then **executes**. That is a code-execution supply-chain surface, and it is more serious than the update checker (which is notify-only precisely to avoid this class of risk). **Detached Ed25519 signature over the catalog bytes.** Public key compiled into the binary; private key never in the repo, never in CI. ``` data/catalog.json # payload — anyone can PR this data/catalog.json.sig # detached signature — only the maintainer can produce this bcc_core.CATALOG_PUBKEYS # list (not a single constant, so keys can be rotated) ``` Load order on remote fetch: **fetch → verify signature → anti-rollback version check → schema+allowlist validate → use.** Any step fails, the whole remote file is discarded. **Why this gives the property we want:** a contributor's PR can change `catalog.json`, but cannot produce a matching signature. If the maintainer blindly merges it, the signature no longer verifies the file — so every BCC in the world *rejects the remote catalog and keeps the last good one*. The bad entry reaches nobody. Publishing requires deliberately signing with the key. **Signing is the approval act, and it cannot be performed by accident.** Defence in depth: - **Anti-rollback:** reject a remote catalog whose `version` is lower than the cached/bundled one. Stops replay of an older *validly signed* catalog to reintroduce a since-removed bad entry. - **Command allowlist** in the validator: `npx` / `uvx` / `docker` / `node` / `python` only; reject the file otherwise. (The 20-entry seed uses only npx ×11, uvx ×6, docker ×2 — the allowlist costs us nothing today.) - **CI gate:** verify `catalog.json.sig` against `catalog.json` on every push to main. A merged-but-unsigned PR turns main red in 60 seconds instead of never, and it catches a forgotten re-sign. - **Existing guarantee still holds underneath:** catalog entries route through the import + editor flow, so the user *sees* the command before it's written. Signing protects users who don't look; the editor protects those who do. Dependency call: use the `cryptography` package for Ed25519 verify rather than hand-rolling RFC 8032 in stdlib. Bundle size is not a constraint at BCC's current footprint, and "we didn't hand-roll the crypto" is worth a few MB. Tracked separately in **#61** (in-app verification, remote refresh, anti-rollback) and **#62** (Catalog Console — maintainer review + signing tool; signing is never a manual script). ### 3. Bundled seed: the curated 20 ``` filesystem fetch memory sequential-thinking git github playwright chrome-devtools postgres n8n notion obsidian brave-search tavily firecrawl home-assistant kubernetes aws-api-mcp-server grafana slack ``` 19 basic / 1 link-only (slack, kept as the canonical link-only demo). 10 need a secret — env var *names* prefilled, values never. 3 carry `<PLACEHOLDER>` args. The remaining 92 researched servers are the **growth path for the signed remote catalog** — they can ship without a binary release the moment the remote path lands. Staged in the workspace as `catalog-seed-v1.json` (bundled) and `catalog-pool-112.json` (pool, with `source` URLs for every verified config). ### Phase 1 scope (this release) 1. `data/catalog.json` (the 20) + `load_catalog()` / `validate_catalog()` (schema + command allowlist) / `catalog_entry_to_paste_json()` — all pure, all unit-tested, GUI-free. 2. Browse-catalog dialog: search box (reuse `server_matches_filter`), category chips, detail pane, **Add** (basic) → existing `_import_server` path, **Docs** (link-only) → `QDesktopServices.openUrl`. 3. Signature verify + remote refresh + cache (#61). Category taxonomy collapses to 7 chips: **Files & Dev · Data · Search & AI · Cloud & Infra · Work · Home & Personal · Other**.
Author
Owner

Phase 1 landed as PRs — and the supply-chain audit immediately paid for itself

PR #65 (catalog core + signed verification) · PR #64 (signed release checksums, #63). 239 tests green, ruff clean.

The pinning pass caught a live one

Before shipping, every basic-tier entry was resolved against its actual registry (npm / PyPI / GHCR / Docker Hub) to pin an exact version. That audit surfaced firecrawl-mcp: the npm package's repo and author field both point at Firecrawl, and we had it flagged official: true — but the sole account with publish rights is hello_sideguide (hello@sideguide.dev), a domain with no visible relationship to firecrawl.dev.

That is not proof of anything malicious. It is exactly the shape of the attack this whole design exists to stop: a package that looks official, is presented as official, and whose publish rights sit with an identity we cannot tie to the vendor — while we write its command into a config file that Claude then executes.

Dropped from the seed (now 19 entries). Retained in the research pool with the flag recorded; can return if the relationship checks out. Also flagged, milder: tavily-mcp publishes from yash.bhandare@nebius.com (other maintainers look Tavily-affiliated, so this reads as a partner account rather than a hijack — kept, pinned).

Positive signal worth recording: most of the official npm packages publish via GitHub Actions OIDC trusted publishing, which meaningfully reduces the stolen-laptop-token vector. That's a signal the Catalog Console (#62) should surface.

Also fixed in the same pass

  • Every entry pinned to an exact version (@scope/pkg@1.2.3, pkg@1.2.3 for uvx, immutable tags for docker — never latest). Unpinned npx -y <pkg> means a package compromised after we ship silently auto-upgrades into every user. A pin bounds the blast radius to versions we actually reviewed.
  • postgres now ships --access-mode=restricted, not unrestricted. The seed data as researched would have handed an LLM write access to the user's database by default. A curated catalog does not get to make that choice for someone.
  • last_release (real ISO date, pulled live from each registry) added to the schema so the UI can show freshness. postgres-mcp (2025-05-16) and mcp-obsidian (2025-04-01) are both ~14 months stale — kept, since they're the best available for their slot, but users should see that.

uvx pin syntax — verified, not assumed

The documented form is uvx pkg@1.2.3 (--from 'pkg==1.2.3' cmd is only for when the command and package names differ). Confirmed against the uv docs and a live invocation rather than guessed — a pin to a version that doesn't exist is worse than no pin, since it breaks the entry for every user.

Next

Browse-catalog dialog on top of the landed core API, then remote fetch + cache. The key for CATALOG_PUBKEYS (currently a placeholder) comes from the Console in #62 — nothing can actually be published until that exists.

## Phase 1 landed as PRs — and the supply-chain audit immediately paid for itself **PR #65** (catalog core + signed verification) · **PR #64** (signed release checksums, #63). 239 tests green, ruff clean. ### The pinning pass caught a live one Before shipping, every basic-tier entry was resolved against its actual registry (npm / PyPI / GHCR / Docker Hub) to pin an exact version. That audit surfaced **firecrawl-mcp**: the npm package's repo and `author` field both point at Firecrawl, and we had it flagged `official: true` — but the **sole account with publish rights is `hello_sideguide` (hello@sideguide.dev)**, a domain with no visible relationship to firecrawl.dev. That is not proof of anything malicious. It is *exactly* the shape of the attack this whole design exists to stop: a package that looks official, is presented as official, and whose publish rights sit with an identity we cannot tie to the vendor — while we write its `command` into a config file that Claude then executes. **Dropped from the seed (now 19 entries).** Retained in the research pool with the flag recorded; can return if the relationship checks out. Also flagged, milder: `tavily-mcp` publishes from `yash.bhandare@nebius.com` (other maintainers look Tavily-affiliated, so this reads as a partner account rather than a hijack — kept, pinned). Positive signal worth recording: most of the official npm packages publish via **GitHub Actions OIDC trusted publishing**, which meaningfully reduces the stolen-laptop-token vector. That's a signal the Catalog Console (#62) should surface. ### Also fixed in the same pass - **Every entry pinned to an exact version** (`@scope/pkg@1.2.3`, `pkg@1.2.3` for uvx, immutable tags for docker — never `latest`). Unpinned `npx -y <pkg>` means a package compromised *after* we ship silently auto-upgrades into every user. A pin bounds the blast radius to versions we actually reviewed. - **`postgres` now ships `--access-mode=restricted`**, not `unrestricted`. The seed data as researched would have handed an LLM write access to the user's database by default. A curated catalog does not get to make that choice for someone. - **`last_release`** (real ISO date, pulled live from each registry) added to the schema so the UI can show freshness. `postgres-mcp` (2025-05-16) and `mcp-obsidian` (2025-04-01) are both ~14 months stale — kept, since they're the best available for their slot, but users should see that. ### uvx pin syntax — verified, not assumed The documented form is `uvx pkg@1.2.3` (`--from 'pkg==1.2.3' cmd` is only for when the command and package names differ). Confirmed against the uv docs and a live invocation rather than guessed — a pin to a version that doesn't exist is worse than no pin, since it breaks the entry for every user. ### Next Browse-catalog dialog on top of the landed core API, then remote fetch + cache. The key for `CATALOG_PUBKEYS` (currently a placeholder) comes from the Console in #62 — nothing can actually be published until that exists.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: the_og/better-claude-config#10