Skip to content

feat(install): register the Claude Code MCP server at user scope#369

Draft
gbrlcustodio wants to merge 4 commits into
devfrom
feat/claude-code-user-scope-install
Draft

feat(install): register the Claude Code MCP server at user scope#369
gbrlcustodio wants to merge 4 commits into
devfrom
feat/claude-code-user-scope-install

Conversation

@gbrlcustodio

@gbrlcustodio gbrlcustodio commented Jul 4, 2026

Copy link
Copy Markdown
Member

Draft. This is the deferred design piece from #270, ported onto current dev and stacked on #368. It is not ready to merge as-is: see the open questions below.

What

On the Claude Code path, install.sh currently leaves the plugin's bundled uvx .mcp.json entry as the running server while also uv tool installing pipefy-mcp-server, so a plugin user who runs /pipefy:install ends up with two materializations (the uvx-cached env plus an unused tool venv).

This makes the installer the single owner of the Claude Code registration:

  • install.sh: --client claude-code registers via claude mcp add pipefy --scope user -- pipefy-mcp-server (idempotent: removes any prior user-scope entry first; require_claude errors cleanly if the claude CLI is absent). By Claude Code's name precedence (local > project > user > plugin) the user-scope entry shadows the plugin's bundled server, so only the installed binary spawns, on the system Python the CLI is pinned to.
  • .claude-plugin/plugin.json + hooks/check-server-version.sh: a SessionStart hook that nudges the user to re-run /pipefy:install when the installed server's --version drifts from the plugin manifest version. It no-ops for users on the pure plugin/uvx path (no installed binary).

Why user scope, not editing the plugin

There is no supported way to disable a single plugin's bundled MCP server. Name-shadowing at a higher scope is the only lever, and it is deterministic per the documented precedence. The committed .mcp.json stays the zero-config default (/plugin install pipefy@pipefy still works with no prior step); the override only takes effect once a user runs the installer with --client claude-code.

Why the drift hook

Shadowing means plugin auto-updates no longer reach the running (installed) server. The hook is the mitigation for that staleness. It reads the plugin version (cheap sed on plugin.json) before spawning pipefy-mcp-server --version, so the Python cold start only happens for users who actually have the override installed.

The compare is a raw string, and it holds because --version is argparse action="version" echoing __version__ verbatim (the dashed form, e.g. 0.3.0-alpha.1), and the manifest now tracks that same value in lockstep (via the plugin.json lockstep added in #367). So a user on the current release is not nudged. This depends on #367 landing (before it, the manifest was stuck at 0.2.0-beta.1 and every session would nudge).

Stacking

Stacked on #368 (chore/mcp-json-pypi-install); GitHub retargets the base to dev when #368 merges. The two do not overlap in files, but #368 settles the .mcp.json shape this feature coexists with, and #367 provides the manifest-lockstep the hook relies on, so this should land after both.

Open questions (why this is a draft)

  1. Slash-command wiring is not included. feat(install): register Claude Code MCP server at user scope #270 also switched /pipefy:install (commands/install.md) from --client none to --client claude-code. On current dev that command no longer uses the curl installer at all: it runs a direct git-based uv tool install and never calls install.sh. Wiring the slash command to trigger user-scope registration is a separate decision, tangled with the pending CLI-install PyPI migration, so I left commands/install.md out. Without it, user-scope registration is reachable only by running install.sh --client claude-code directly.
  2. Is shadowing the desired UX? It is deterministic, but it means a plugin user who runs the installer silently stops receiving plugin auto-updates for the server (the hook is the only feedback). Worth confirming this is the intended model before shipping.
  3. --version format coupling. The hook's raw-string compare is correct only as long as --version keeps emitting the dashed __version__ and the manifest stays in lockstep with it. If either changes to the PEP 440-normalized form (0.3.0a1), the compare needs canonicalization on both sides.
  4. Is the manifest version the right anchor for the hook? (Surfaced by a /simplify altitude pass.) The hook diffs the installed binary's --version against plugin.json's manifest version, but that manifest version is not what the plugin actually runs: .mcp.json is an unpinned uvx --prerelease allow pipefy-mcp-server, i.e. latest PyPI pre-release. The truthful "is my shadowing binary stale?" comparison is installed-vs-what-the-plugin-would-run, so the options are: pin the bundled server in .mcp.json and read that pin, compare installed-vs-latest-PyPI (adds a network call to a session-start hook), or keep the manifest anchor and accept it as a proxy that is exact only at release time. Note build(release): pin workspace siblings to the lockstep version #367 makes the manifest track the package version in lockstep, which removes today's 0.2.0-beta.1 vs 0.3.0-alpha.1 skew but does not by itself make the manifest equal the running server's resolved version.
  5. Session-start cost. (Surfaced by a /simplify efficiency pass.) The hook spawns pipefy-mcp-server --version on every SessionStart for anyone who ran install.sh --client claude-code; measured at ~0.58s wall (Python cold start). Guards correctly defer the spawn (no binary -> instant exit), but it is the steady-state cost for the target population. If kept, the cheap mitigation is an mtime-keyed cache (stat the resolved binary, cache <mtime> <version>, re-spawn only when it changes) so the cost collapses to roughly once per install. Deferred as a design call rather than optimized in place, since question 2 may drop the hook entirely.

Test

  • A /simplify pass ran over this diff: applied one cleanup (removed a dead || exit 0 guard in the hook), and surfaced open questions 4 and 5 above; reuse/altitude otherwise judged the shell + hook at the right altitude.
  • sh -n install.sh and sh -n hooks/check-server-version.sh pass.
  • .claude-plugin/plugin.json parses as valid JSON.
  • install.sh --dry-run --client claude-code previews claude mcp add pipefy --scope user -- pipefy-mcp-server and the user-scope next-steps message.
  • Not yet exercised on a real Claude Code machine (the claude mcp add + reload + claude mcp list shadowing check).

On the Claude Code path, the installer left the plugin's bundled uvx
`.mcp.json` entry as the running server while also `uv tool install`ing
pipefy-mcp-server, so a plugin user who ran /pipefy:install ended up with
two materializations (the uvx-cached env plus an unused tool venv).

--client claude-code now registers via `claude mcp add pipefy --scope
user -- pipefy-mcp-server` (idempotent: it removes any prior user-scope
entry first, and require_claude errors cleanly if the CLI is absent). By
Claude Code's name precedence (local > project > user > plugin) the
user-scope entry shadows the plugin's bundled server, so only the
installed binary spawns, on the system Python the CLI is pinned to.

Because shadowing means plugin auto-updates no longer reach the running
server, a SessionStart hook (hooks/check-server-version.sh) nudges the
user to re-run /pipefy:install when the installed server's --version
drifts from the plugin manifest's version. It no-ops for users on the
pure plugin/uvx path (no installed binary). The compare is a raw string:
--version echoes __version__ (dashed, e.g. 0.3.0-alpha.1) and the
manifest tracks that same value in lockstep, so matched versions do not
nudge.
@gbrlcustodio gbrlcustodio self-assigned this Jul 4, 2026
@gbrlcustodio gbrlcustodio requested a review from adriannoes July 4, 2026 05:40
The version pipeline ends in tr, so its exit status is tr's, not
pipefy-mcp-server's; the || exit 0 could never observe the binary
failing. The empty-output case it looked like it guarded is already
handled by the [ -n "$installed" ] check on the next line.

@adriannoes adriannoes left a comment

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.

Summary

Solid draft — the installer path is the right shape for this problem. claude_code_register_pipefy() is clear about precedence, idempotent (remove-then-add), and require_claude fails with an actionable message. The hook's cheap-before-expensive guard ordering is also good, and the PR body is unusually honest about stacking (#367/#368) and open design questions.

I'm commenting rather than requesting changes because this is still a draft with known design calls outstanding. Before marking ready, two hook issues are worth fixing (see inline threads).

What worked well

  • User-scope registration via claude mcp add with documented name-shadowing (local > project > user > plugin)
  • Idempotent re-runs (mcp remove before mcp add)
  • require_claude with a clear fallback (--client none)
  • Hook defers the expensive pipefy-mcp-server --version spawn until manifest version is parsed

Before marking ready

  • Drift nudge points to /pipefy:install, which cannot resync the user-scope server for users who already have pipefy on PATH — remediation should reference install.sh --client claude-code or wire commands/install.md in the same stack.
  • command -v pipefy-mcp-server does not prove a Claude user-scope override exists (Cursor/Codex installs also place the binary on PATH).

Also noted: merge after #367 (manifest lockstep) and #368; live claude mcp list shadowing check still open per your test plan.

Comment thread hooks/check-server-version.sh Outdated
Comment thread hooks/check-server-version.sh
/pipefy:install only runs 'uv tool install' for the CLI and stops once pipefy is on PATH; it never reinstalls pipefy-mcp-server or re-registers the user-scope server. Re-running install.sh --client claude-code is what clears the drift.
command -v pipefy-mcp-server also matches --client cursor installs, where no Claude Code user-scope override exists and the plugin's uvx server still runs. Check ~/.claude.json for a top-level mcpServers.pipefy whose command is the binary, so the nudge only fires when the override actually shadows the plugin. Falls through to no nudge if python3 is missing or the config is unreadable.
Base automatically changed from chore/mcp-json-pypi-install to dev July 6, 2026 18:45
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