Skip to content

fix(mcp): register the marketplace before the Claude Code plugin install - #1294

Merged
gewenyu99 merged 5 commits into
mainfrom
fix/claude-code-marketplace
Sep 22, 2026
Merged

gewenyu99 merged 5 commits into
mainfrom
fix/claude-code-marketplace

Conversation

@gewenyu99

@gewenyu99 gewenyu99 commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

ClaudeCodeMCPClient.installPlugin runs claude plugin install posthog. Claude Code resolves a bare plugin name only against marketplaces the user has already registered, and it registers its catalog when it starts interactively — the wizard uses the non-interactive plugin subcommand. On a machine that never opened Claude Code the install dies with Plugin "posthog" not found in any configured marketplace.

258 events across 224 distinct users in the last 30 days (54 / 46 in the last 7), still occurring. The failure spans 29 error-tracking issue ids because $HOME and the resolved binary path sit inside the exception message.

Changes

  • Register the marketplace first. plugin marketplace add PostHog/ai-plugin, then plugin install posthog@posthog. On not found in, refresh the catalog and retry; then fall back to the bare name, so the path is never worse than before.
  • Run the CLI through execFile. These calls clone git repositories and take seconds. execSync blocked the event loop, which froze the TUI spinner on its first frame (1 painted frame vs 50 on an async path). The PluginCapable contract is unchanged — the methods already returned promises. findClaudeBinary stays synchronous.
  • Read --json listings and match plugin ids exactly. A plugin named posthog-something no longer reports ours as installed, and a copy installed from any marketplace does. Falls back to the old substring scan when --json is unsupported.
  • Uninstall every installed posthog@* id. Claude Code permits posthog@posthog and posthog@claude-plugins-official at the same time, and a bare plugin uninstall posthog removes only one — so mcp remove left a copy behind.
  • Show a spinner beside Installing MCP server... while the work runs.
  • Read enabled, not just id. plugin list --json returns it, and a plugin switched off in /plugin stays listed. Claude Code takes its MCP server from the plugin, so a disabled one serves nothing — and reporting it installed left the user with no server and a screen saying they were done. The install decision now asks whether anything is actually serving; the CLI re-enables on install, so falling through is the repair. isPluginInstalled keeps meaning present, because index.ts asks it whether there is anything to remove and a disabled plugin still has to be uninstalled. One method cannot answer both questions.
  • Read repo, not just name. marketplace list --json returns it. Matching on the name alone meant a marketplace merely called posthog satisfied the check, so registration was skipped and the install ran against someone else's catalog under our name.
  • Remove the marketplace too. installPlugin registers it and removal left it behind, while codex.ts clears both halves on the same call — the two clients disagreed about what removal means. Removal now acts when either half is present, guarded on the marketplace being ours by repo, and on it being registered at all (marketplace remove exits non-zero on one that is absent).
  • Report the marketplace-add failure with the install failure it caused. A failed add reached only the debug log, and the three install attempts that follow then fail with not found in any configured marketplace — the same string this PR's own acceptance query counts. The cause now travels with the report, so the new root cause stays distinguishable from the old bug. An add the fallback recovers from still reports nothing.

PostHog/ai-plugin rather than Anthropic's official marketplace: that one carries this plugin as a pinned sha of PostHog/ai-plugin.git, so installing through it would gate plugin releases on Anthropic bumping the pin. It also matches what the Codex client already registers.

Test plan

  • pnpm test — 3126 pass. pnpm typecheck, pnpm lint clean.
  • clients/__tests__/claude-code.test.ts: marketplace registered when absent and skipped when present, stale-catalog refresh and retry, bare-name fallback, marketplace-add failure covered by the fallback, exact id matching, multi-id uninstall, and the contract that the path issues no execSync call other than binary discovery.
  • Verified against the real CLI in a throwaway $HOME: reproduced the exact production error string, then install → re-install → remove → re-install all clean, with 44 event-loop ticks during a run that previously yielded none. marketplace add is idempotent and exits 0; an SSH clone failure auto-retries over HTTPS.
  • Disabled-plugin and marketplace-identity cases: a disabled copy counted as installed so removal still clears it; the install running over a disabled plugin rather than reporting it already installed; an enabled copy beside a disabled one reported already installed; our marketplace registered when a foreign one holds the name; removal clearing plugin and marketplace, clearing a marketplace left with no plugin, and leaving a foreign marketplace alone; the marketplace-add failure reported beside the install failure it caused.
  • Mutation checks, each failing at least one test: collapsing the install check back to presence; moving the enabled filter into isPluginInstalled (which hides a disabled plugin from removal); matching the marketplace on name without repo; dropping the marketplace-removal step; and dropping the marketplace cause from the report.
  • Test fixtures now emit enabled and repo as the CLI really does, rather than the trimmed shape that let the disabled-plugin defect through.
  • Against the real CLI, driving the client itself in a throwaway $HOME: disabled ⇒ present true, installPlugin runs and the plugin comes back enabled; absent ⇒ present false, installs; enabled ⇒ alreadyInstalled, no install call. Also confirmed marketplace remove cascades to the plugins installed from it, and fails on a marketplace that is absent — which is what the two guards are for.
  • Built binary: mcp remove against a sandbox holding both plugin copies removes both.

Acceptance

Measure by exception text, not issue id — the reported issue holds only 97 of the 258 occurrences:

SELECT count(), uniq(distinct_id) FROM events
WHERE event = '$exception' AND properties.team = 'docs-and-wizard'
  AND properties.build = 'prod'
  AND properties.$exception_values ILIKE '%not found in any configured marketplace%'
  AND timestamp >= '<release date>'

Baseline to beat: 54 events / 46 users per 7 days.

Grouping the 29 fingerprints into one (constant exception message, scrubbed properties) and classifying unactionable local-environment failures are follow-up work, not in this PR.

The disabled-plugin defect is not measurable from error tracking — it exits 0 and reports success. Verify by inspection:

claude plugin disable posthog@posthog   # then run `mcp add`
claude plugin list --json               # expect: enabled true

Report: https://us.posthog.com/project/2/inbox/reports/01a0c625-d3e6-724e-8809-8e4a14f10d8b

🤖 Generated with Claude Code

`claude plugin install <name>` resolves a bare name only against marketplaces
the user already registered, and the wizard never opens Claude Code
interactively. Register `PostHog/ai-plugin` first, then install `posthog@posthog`,
refreshing a stale catalog and falling back to the bare name.

Run the CLI through `execFile` rather than `execSync`. The calls clone git
repositories and take seconds; blocking the event loop froze the TUI spinner on
its first frame.

Read installed plugins and marketplaces from `--json`, matching plugin ids
exactly, so a plugin named `posthog-something` no longer counts as ours and a
copy installed from any marketplace does. Uninstall every installed `posthog@*`
id, since Claude Code permits more than one and a bare uninstall removes one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gewenyu99
gewenyu99 requested a review from a team as a code owner September 22, 2026 00:37
@github-actions

Copy link
Copy Markdown

🧙 Wizard CI

Run the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands:

Test all apps:

  • /wizard-ci all

Test all apps in a directory:

  • /wizard-ci ai-observability
  • /wizard-ci basic-integration
  • /wizard-ci mcp-analytics
  • /wizard-ci replay-vision
  • /wizard-ci revenue
  • /wizard-ci self-driving
  • /wizard-ci warehouse
  • /wizard-ci warehouse-seeded

Test an individual app:

  • /wizard-ci ai-observability/anthropic
  • /wizard-ci ai-observability/google-adk
  • /wizard-ci ai-observability/groq
Show more apps
  • /wizard-ci ai-observability/manual-capture
  • /wizard-ci ai-observability/openai
  • /wizard-ci ai-observability/openai-agents
  • /wizard-ci ai-observability/opentelemetry
  • /wizard-ci ai-observability/vercel-ai
  • /wizard-ci basic-integration/android
  • /wizard-ci basic-integration/angular
  • /wizard-ci basic-integration/astro
  • /wizard-ci basic-integration/django
  • /wizard-ci basic-integration/fastapi
  • /wizard-ci basic-integration/flask
  • /wizard-ci basic-integration/flutter
  • /wizard-ci basic-integration/javascript-node
  • /wizard-ci basic-integration/javascript-web
  • /wizard-ci basic-integration/laravel
  • /wizard-ci basic-integration/next-js
  • /wizard-ci basic-integration/nuxt
  • /wizard-ci basic-integration/python
  • /wizard-ci basic-integration/rails
  • /wizard-ci basic-integration/react-native
  • /wizard-ci basic-integration/react-router
  • /wizard-ci basic-integration/sveltekit
  • /wizard-ci basic-integration/swift
  • /wizard-ci basic-integration/tanstack-router
  • /wizard-ci basic-integration/tanstack-start
  • /wizard-ci basic-integration/vue
  • /wizard-ci mcp-analytics/custom-dispatcher
  • /wizard-ci mcp-analytics/typescript-sdk
  • /wizard-ci replay-vision/javascript-node
  • /wizard-ci replay-vision/next-js
  • /wizard-ci replay-vision/react-vite
  • /wizard-ci revenue/stripe
  • /wizard-ci self-driving/astro
  • /wizard-ci self-driving/fastapi
  • /wizard-ci self-driving/nuxt
  • /wizard-ci self-driving/react-router
  • /wizard-ci self-driving/sveltekit
  • /wizard-ci warehouse/monorepo-env
  • /wizard-ci warehouse/multi-source-next
  • /wizard-ci warehouse/stripe-node
  • /wizard-ci warehouse/zero-source
  • /wizard-ci warehouse-seeded/next-stripe
  • /wizard-ci warehouse-seeded/next-stripe-declined

Test against a Context Mill branch:

  • /wizard-ci all context-mill:my-branch

Add context-mill:<branch> to any command above to pin the Context Mill branch. It defaults to main.

Results will be posted here when complete.

gewenyu99 and others added 2 commits September 21, 2026 22:12
`ListedPlugin` modelled `id` alone and `ListedMarketplace` modelled `name`
alone, so two fields the CLI already returns went unread — and each one was
carrying an answer we were guessing at instead.

`plugin list --json` returns `enabled`. A plugin switched off in `/plugin`
stays listed, and Claude Code serves its MCP server *from* the plugin, so a
disabled plugin serves nothing. Reading `id` alone made `isPluginInstalled`
report true for it: the install was skipped, the screen said the user was
good to go, and no posthog server existed anywhere. Verified against the
real CLI — after `claude plugin disable`, the row is still listed with
`"enabled": false`.

`marketplace list --json` returns `repo`. Matching on the name alone meant a
marketplace merely *called* `posthog` satisfied the check, so registration
was skipped and the install ran against someone else's catalog under our
name. The field that distinguishes them was already in the payload.

A failed `marketplace add` reached only the debug log. The three install
attempts that follow then fail with `not found in any configured
marketplace` — the same string the bug this branch fixes produced — so the
new root cause was indistinguishable from the old one in error tracking,
and from this branch's own acceptance query. The cause now travels with the
install failure rather than being reported on its own, because an add that
the fallback recovers from is not worth an event: the comment above
`ensurePluginMarketplace` already said so, and a test already asserted it.

Fixtures now emit `enabled` and `repo` as the CLI does, rather than the
trimmed shape that let the first defect through.

Mutation-verified: reverting each of the three fixes fails a test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous commit taught `isPluginInstalled` to treat a disabled plugin as
not installed. That is the right answer for one caller and the wrong answer
for the other two, and the method has only one return value.

`index.ts` asks it to decide whether a client has anything to remove — its
comment says so — and `removePlugin` asks it again on the no-`--json` path.
For those, a disabled plugin is very much installed: it sits on disk and
`mcp remove` has to clear it. Answering false hid it from discovery, so the
plugin could never be removed at all.

Only the install decision cares whether the plugin actually serves. So
`isPluginInstalled` goes back to meaning present, and `installPlugin` asks
about `enabled` itself before reporting a no-op.

Verified against the real CLI, which settles what "install over a disabled
plugin" should do: it re-enables it and reports success, so falling through
to the install is the repair, not merely a retry.

  disabled  present=true   installPlugin -> {success:true}                  -> enabled
  absent    present=false  installPlugin -> {success:true}                  -> enabled
  enabled   present=true   installPlugin -> {success:true,alreadyInstalled} -> unchanged

Mutation-verified in both directions: collapsing the install check back to
presence fails the disabled-install test, and moving the `enabled` filter
back into `isPluginInstalled` fails the removal test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
// throws; the caller decides what a failure means.
private runClaude(binary: string, args: string[]): Promise<ClaudeRun> {
return new Promise((resolve) => {
execFile(binary, args, (error, stdout) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add timeout/buffer here to match #1295?

gewenyu99 and others added 2 commits September 22, 2026 10:48
`installPlugin` registers the PostHog marketplace, and `removePlugin` only
uninstalled the plugin. A machine that ran `mcp add` then `mcp remove` kept
our catalog forever, and `codex.ts` already clears both halves on the same
call, so the two clients disagreed about what removal means.

Removal now clears the marketplace too, and acts when either half is
present — a marketplace left behind by a failed plugin step is a state the
wizard created and should be able to undo.

Guarded on it being ours, by repo and not merely by name, since removing a
marketplace someone else registered under the name `posthog` is not ours to
do. Guarded on it being registered at all, because `marketplace remove`
exits non-zero on a marketplace that is absent and would report a failure
for a no-op.

Verified against the real CLI: `marketplace remove` cascades, taking the
plugins installed from that marketplace with it, which is what removal
wants and why the guard matters.

Mutation-verified: dropping the marketplace step fails 2 tests, and
matching on name without repo fails the foreign-marketplace test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gewenyu99
gewenyu99 merged commit 8c54c11 into main Sep 22, 2026
20 checks passed
@gewenyu99
gewenyu99 deleted the fix/claude-code-marketplace branch September 22, 2026 18:23
gewenyu99 added a commit that referenced this pull request Sep 23, 2026
Brings in the eight main fixes since beccc79 (#1275, #1280, #1284, #1294,
#1295, #1301, #1302, #1305). One conflict: agentic detection keeps main's
two-attempt retry loop from #1305 and adds A1's progress emitter to the
agent it initializes.

Generated-By: PostHog Desktop
Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants