[#10] Server catalog / one-click add — design pass first #10
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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:
Before coding
Post a comment on this issue with:
Relevant code
bcc_core.py::parse_pasted_json_verbose— the natural entry point for catalog itemsbcc.pypaste dialog — likely reuse pathScope expansion (AJ) — this is the "MCP server marketplace" idea; recording it as a roadmap feature. Extends the catalog sketch above with:
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.
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.jsoninside the app (bundled via thedatasmechanism 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):
Tiers
ServerEntrywith 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.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_filteridea), category chips, a list with per-row Add (basic) or Docs (link-only).Phasing
catalog.json+ loader/validator/builder + the dialog (basic + link-only).Open decisions for you
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 (2026-07-12) — proposed for next week's release
Scope: curated static catalog, no live registry. A
catalog.jsonshipped with the app (and bundled via the existingdatasmechanism):starssnapshot refreshed at release time by a smallscripts/refresh_catalog.py(hits the GitHub API at build time, never at app runtime — keeps the app offline-safe and fail-quiet).server_matches_filterstyle), category filter, detail pane (description + stars + homepage link), Add button. Add feeds the existing_import_serverpath (name-collision prompt included), then focuses the editor with placeholder fields highlighted — env secrets are never pre-filled.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.
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/ 28link-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-onlytier 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 tieredsetup: basic | link-onlydecision holds up; the link-only path must be first-class in the dialog, not hidden.2. Several reference servers are archived.
modelcontextprotocol/serversarchived GitHub, GitLab, Postgres, Puppeteer, Redis, Sentry, SQLite, Slack and Brave Search intoservers-archived. The catalog must point at live successors (brave/brave-search-mcp-server,crystaldba/postgres-mcp, Playwright/Chrome-DevTools over Puppeteer). Recommend excludingsqliteandpuppeteerfrom 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, sincegithubis 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,slackFilter: 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,otherCollapse to ~7 chips in the browse dialog: Files & Dev · Data · Search & AI · Cloud & Infra · Work · Home & Personal · Other.
Decisions needed
link-onlyentries are shown in the dialog (with a Docs button) rather than filtered out.Phase-1 core (loader + validator +
catalog_entry_to_paste_jsonbuilder + tests) is unblocked and ready to hand to a subagent once (1) is decided.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
npxlines 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.jsonbundled via thedatasmechanism from #20. Always present, always works, zero network at startup.AppDataLocation. Resolve byversion:max(bundled, cached)— a fresh install must never be pinned to a stale cache.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
argsentry — because a catalog entry is acommand+argsthat 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.
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:
versionis lower than the cached/bundled one. Stops replay of an older validly signed catalog to reintroduce a since-removed bad entry.npx/uvx/docker/node/pythononly; reject the file otherwise. (The 20-entry seed uses only npx ×11, uvx ×6, docker ×2 — the allowlist costs us nothing today.)catalog.json.sigagainstcatalog.jsonon 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.Dependency call: use the
cryptographypackage 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
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) andcatalog-pool-112.json(pool, withsourceURLs for every verified config).Phase 1 scope (this release)
data/catalog.json(the 20) +load_catalog()/validate_catalog()(schema + command allowlist) /catalog_entry_to_paste_json()— all pure, all unit-tested, GUI-free.server_matches_filter), category chips, detail pane, Add (basic) → existing_import_serverpath, Docs (link-only) →QDesktopServices.openUrl.Category taxonomy collapses to 7 chips: Files & Dev · Data · Search & AI · Cloud & Infra · Work · Home & Personal · Other.
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
authorfield both point at Firecrawl, and we had it flaggedofficial: true— but the sole account with publish rights ishello_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
commandinto 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-mcppublishes fromyash.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
@scope/pkg@1.2.3,pkg@1.2.3for uvx, immutable tags for docker — neverlatest). Unpinnednpx -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.postgresnow ships--access-mode=restricted, notunrestricted. 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) andmcp-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' cmdis 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.