From b7d1f1f66c870399b4802eda2a256860dfe64dbe Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sat, 4 Jul 2026 02:38:05 -0300 Subject: [PATCH 1/4] feat(install): register the Claude Code MCP server at user scope 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. --- .claude-plugin/plugin.json | 14 ++++++++++- hooks/check-server-version.sh | 29 ++++++++++++++++++++++ install.sh | 45 +++++++++++++++++++++++++++++------ 3 files changed, 80 insertions(+), 8 deletions(-) create mode 100755 hooks/check-server-version.sh diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 963ead4a..fe9bb9ac 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -6,5 +6,17 @@ "author": { "name": "Pipefy", "url": "https://github.com/pipefy" }, "homepage": "https://github.com/pipefy/ai-toolkit", "repository": "https://github.com/pipefy/ai-toolkit", - "license": "Apache-2.0" + "license": "Apache-2.0", + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "sh \"${CLAUDE_PLUGIN_ROOT}/hooks/check-server-version.sh\"" + } + ] + } + ] + } } diff --git a/hooks/check-server-version.sh b/hooks/check-server-version.sh new file mode 100755 index 00000000..e5024492 --- /dev/null +++ b/hooks/check-server-version.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# SessionStart nudge: when install.sh has registered a user-scope `pipefy` +# server (the persistent pipefy-mcp-server binary, which shadows the plugin's +# bundled uvx entry), plugin auto-updates no longer reach that running server. +# This compares the installed binary's version against the plugin's declared +# version and, on drift, asks the user to re-run /pipefy:install. +# +# No-op for users on the pure plugin/uvx path: they have no installed binary, +# so there is nothing shadowing the plugin and nothing to update. +set -eu + +# No installed binary means nothing shadows the plugin, so there is no +# user-scope override that could drift -> nothing to nudge about. +command -v pipefy-mcp-server >/dev/null 2>&1 || exit 0 + +# Cheap checks first; only spawn the binary (Python cold start) once we have a +# plugin version to compare against. +manifest="${CLAUDE_PLUGIN_ROOT:-}/.claude-plugin/plugin.json" +[ -f "$manifest" ] || exit 0 +plugin=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -n1) +[ -n "$plugin" ] || exit 0 + +installed=$(pipefy-mcp-server --version 2>/dev/null | tr -d '[:space:]') || exit 0 +[ -n "$installed" ] || exit 0 + +if [ "$installed" != "$plugin" ]; then + echo "Pipefy: the installed MCP server ($installed) differs from this plugin ($plugin); re-run /pipefy:install to sync." +fi +exit 0 diff --git a/install.sh b/install.sh index 7668f030..9723d991 100755 --- a/install.sh +++ b/install.sh @@ -4,7 +4,8 @@ # Resolves the latest GitHub Release tag (or a tag passed via --version), # discovers its wheel assets, installs the CLI and MCP server via # `uv tool install`, optionally installs skills via `npx skills add`, and -# writes the MCP server registration into the chosen client's config. +# registers the MCP server with the chosen client (a config-file write for +# most clients; `claude mcp add` at user scope for Claude Code). set -eu @@ -71,8 +72,11 @@ and register the MCP server with an MCP client. Options: --yes, -y Skip all confirmation prompts. --no-skills Skip the skills installation step. - --client Register MCP server in this client's config. + --client Register the MCP server for this client. One of: claude-code, claude-desktop, cursor, codex, none. + claude-code registers via 'claude mcp add' at user scope + (overrides the plugin's bundled server); the others write + the client's own config file. Defaults to 'none' (prints snippet to paste). --version Install a specific GitHub Release tag (e.g. v0.2.0-beta.2). Defaults to the most recent release (incl. prereleases). @@ -314,6 +318,28 @@ require_python3() { fi } +require_claude() { + if ! command -v claude >/dev/null 2>&1; then + err "the 'claude' CLI is required to register the MCP server in Claude Code, but was not found. Install Claude Code (https://claude.com/claude-code), or re-run with --client none and paste the snippet manually." + fi +} + +claude_code_register_pipefy() { + # Register at USER scope: Claude Code resolves same-named servers + # local > project > user > plugin, so a user-scope `pipefy` shadows the + # plugin's bundled entry and only the binary installed above spawns. The + # bare binary (not a uvx command) keeps the server on the system Python it + # was installed with, without baking an interpreter path into user config. + if [ "$DRY_RUN" -eq 0 ]; then + require_claude + # `claude mcp add` errors on a duplicate name; drop any prior user-scope + # entry first to keep re-runs idempotent. + claude mcp remove pipefy --scope user >/dev/null 2>&1 || true + fi + run claude mcp add pipefy --scope user -- pipefy-mcp-server + [ "$DRY_RUN" -eq 1 ] || say "Registered 'pipefy' at user scope (overrides the plugin's bundled server)." +} + json_merge_pipefy() { path="$1" if [ "$DRY_RUN" -eq 1 ]; then @@ -404,6 +430,8 @@ EOF } write_client_config() { + # cursor/claude-desktop/codex write the client's own config file; claude-code + # instead registers via `claude mcp add` (user scope) to override the plugin. case "$CLIENT" in cursor) json_merge_pipefy "$HOME/.cursor/mcp.json" @@ -415,11 +443,7 @@ write_client_config() { codex_append_pipefy "$HOME/.codex/config.toml" ;; claude-code) - cat < Pipefy MCP server registered at user scope (overrides the plugin's" + say " bundled server). Reload or restart Claude Code for it to take effect." + say " For skills and slash commands, also install the plugin:" + say " /plugin marketplace add $REPO && /plugin install pipefy@pipefy" + fi say "" say "Next: authenticate with Pipefy." say " Default (browser): pipefy auth login" From 62aa4a9d14533af8635286a259723c301bc2ea3d Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sat, 4 Jul 2026 02:44:13 -0300 Subject: [PATCH 2/4] refactor(install): drop a dead guard in the drift hook 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. --- hooks/check-server-version.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks/check-server-version.sh b/hooks/check-server-version.sh index e5024492..bd8c1a79 100755 --- a/hooks/check-server-version.sh +++ b/hooks/check-server-version.sh @@ -20,7 +20,7 @@ manifest="${CLAUDE_PLUGIN_ROOT:-}/.claude-plugin/plugin.json" plugin=$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -n1) [ -n "$plugin" ] || exit 0 -installed=$(pipefy-mcp-server --version 2>/dev/null | tr -d '[:space:]') || exit 0 +installed=$(pipefy-mcp-server --version 2>/dev/null | tr -d '[:space:]') [ -n "$installed" ] || exit 0 if [ "$installed" != "$plugin" ]; then From db3aadb5f1e00d389828831146413ecb90e99ada Mon Sep 17 00:00:00 2001 From: Gabriel Date: Mon, 6 Jul 2026 15:13:28 -0300 Subject: [PATCH 3/4] fix(pr-review): point drift nudge at install.sh, not /pipefy:install /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. --- hooks/check-server-version.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hooks/check-server-version.sh b/hooks/check-server-version.sh index bd8c1a79..a632ad51 100755 --- a/hooks/check-server-version.sh +++ b/hooks/check-server-version.sh @@ -3,7 +3,9 @@ # server (the persistent pipefy-mcp-server binary, which shadows the plugin's # bundled uvx entry), plugin auto-updates no longer reach that running server. # This compares the installed binary's version against the plugin's declared -# version and, on drift, asks the user to re-run /pipefy:install. +# version and, on drift, tells the user to re-run install.sh (the path that +# actually reinstalls the binary and re-registers the user-scope server; +# /pipefy:install only installs the CLI). # # No-op for users on the pure plugin/uvx path: they have no installed binary, # so there is nothing shadowing the plugin and nothing to update. @@ -24,6 +26,6 @@ installed=$(pipefy-mcp-server --version 2>/dev/null | tr -d '[:space:]') [ -n "$installed" ] || exit 0 if [ "$installed" != "$plugin" ]; then - echo "Pipefy: the installed MCP server ($installed) differs from this plugin ($plugin); re-run /pipefy:install to sync." + echo "Pipefy: the installed MCP server ($installed) differs from this plugin ($plugin). Re-run the installer to sync: curl -fsSL https://raw.githubusercontent.com/pipefy/ai-toolkit/main/install.sh | sh -s -- --client claude-code" fi exit 0 From 5ee83021f519085a7c2e01e44807548801840094 Mon Sep 17 00:00:00 2001 From: Gabriel Date: Mon, 6 Jul 2026 15:14:19 -0300 Subject: [PATCH 4/4] fix(pr-review): gate drift nudge on a registered user-scope server 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. --- hooks/check-server-version.sh | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/hooks/check-server-version.sh b/hooks/check-server-version.sh index a632ad51..100914b4 100755 --- a/hooks/check-server-version.sh +++ b/hooks/check-server-version.sh @@ -7,14 +7,34 @@ # actually reinstalls the binary and re-registers the user-scope server; # /pipefy:install only installs the CLI). # -# No-op for users on the pure plugin/uvx path: they have no installed binary, -# so there is nothing shadowing the plugin and nothing to update. +# No-op unless that user-scope override is registered: users on the pure +# plugin/uvx path have no installed binary, and users who installed the binary +# only for another client (e.g. --client cursor) still run the plugin's uvx +# server here, so there is nothing shadowing the plugin to nudge about. set -eu -# No installed binary means nothing shadows the plugin, so there is no -# user-scope override that could drift -> nothing to nudge about. +# Cheap pre-filter: no installed binary means nothing could shadow the plugin. command -v pipefy-mcp-server >/dev/null 2>&1 || exit 0 +# The precise trigger is a user-scope `pipefy` server whose command is that +# binary. `command -v` alone is not enough: --client cursor also puts the +# binary on PATH without registering a Claude Code override, so the plugin's +# uvx server still runs here and there is nothing to sync. Fall through to no +# nudge if python3 is missing or the config cannot be read. +python3 - <<'PY' || exit 0 +import json +import os +import sys + +try: + servers = json.load(open(os.path.expanduser("~/.claude.json"))).get("mcpServers", {}) +except (OSError, ValueError): + sys.exit(1) +entry = servers.get("pipefy") if isinstance(servers, dict) else None +command = entry.get("command") if isinstance(entry, dict) else None +sys.exit(0 if command == "pipefy-mcp-server" else 1) +PY + # Cheap checks first; only spawn the binary (Python cold start) once we have a # plugin version to compare against. manifest="${CLAUDE_PLUGIN_ROOT:-}/.claude-plugin/plugin.json"