Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
{
"name": "pipefy",
"displayName": "Pipefy",
"version": "0.2.0-beta.1",
"version": "0.2.0-beta.2",
"description": "Pipefy workflow integration for Claude Code.",
"author": { "name": "Gabriel Custodio" },
"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\""
}
]
}
]
}
}
6 changes: 3 additions & 3 deletions commands/install.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: install
description: Install the Pipefy CLI (pipefy-auth + pipefy-cli + pipefy-infra + pipefy-sdk) as a persistent uv tool.
description: Install the Pipefy CLI and MCP server as persistent uv tools, and register the server in Claude Code at user scope.
disable-model-invocation: true
---

Expand All @@ -10,9 +10,9 @@ Otherwise prompt the user to confirm running:

```
curl -fsSL https://raw.githubusercontent.com/pipefy/ai-toolkit/main/install.sh \
| sh -s -- --yes --no-skills --client none
| sh -s -- --yes --no-skills --client claude-code
```

The installer resolves the latest GitHub Release at runtime and runs `uv tool install` with the discovered wheel URLs. `--client none` skips MCP-client config writes; the Claude Code plugin's `.mcp.json` already wires the MCP server.
The installer resolves the latest GitHub Release at runtime and runs `uv tool install` with the discovered wheel URLs. `--client claude-code` registers the MCP server via `claude mcp add` at user scope, which takes precedence over the plugin's bundled `.mcp.json` entry so the system-Python binary is the one that runs. Reload or restart Claude Code afterward for the registration to take effect.

Verify with `pipefy --version`.
29 changes: 29 additions & 0 deletions hooks/check-server-version.sh
Original file line number Diff line number Diff line change
@@ -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."
Comment thread
gbrlcustodio marked this conversation as resolved.
fi
exit 0
45 changes: 38 additions & 7 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 <id> Register MCP server in this client's config.
--client <id> 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 <tag> Install a specific GitHub Release tag (e.g. v0.2.0-beta.2).
Defaults to the most recent release (incl. prereleases).
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -415,11 +443,7 @@ write_client_config() {
codex_append_pipefy "$HOME/.codex/config.toml"
;;
claude-code)
cat <<EOF
To install in Claude Code, run these slash commands:
/plugin marketplace add $REPO
/plugin install pipefy@pipefy
EOF
claude_code_register_pipefy
;;
""|none)
print_manual_snippet
Expand Down Expand Up @@ -450,6 +474,13 @@ print_next_steps() {
say " ~/.zshrc, ~/.config/fish/conf.d/uv.fish). If a new terminal still"
say " can't find 'pipefy', add \$HOME/.local/bin to PATH manually."
fi
if [ "$CLIENT" = "claude-code" ]; then
say ""
say "==> 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"
Expand Down
37 changes: 36 additions & 1 deletion scripts/bump_version.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Bump the lockstep workspace version across SDK, MCP, CLI, Auth, Infra, and root workspace meta.
"""Bump the lockstep workspace version across SDK, MCP, CLI, Auth, Infra, the Claude plugin manifest, and root workspace meta.

After rewriting the version strings, runs ``uv lock`` so the workspace
lockfile's ``pipefy-workspace`` entry tracks the new version.
Expand All @@ -20,6 +20,7 @@

REPO_ROOT = Path(__file__).resolve().parents[1]
ROOT_PYPROJECT = REPO_ROOT / "pyproject.toml"
PLUGIN_MANIFEST = REPO_ROOT / ".claude-plugin/plugin.json"
INIT_PATHS = (
REPO_ROOT / "packages/sdk/src/pipefy_sdk/__init__.py",
REPO_ROOT / "packages/mcp/src/pipefy_mcp/__init__.py",
Expand All @@ -45,6 +46,15 @@
re.MULTILINE,
)

# The Claude plugin manifest carries the release version too. The SessionStart
# hook (hooks/check-server-version.sh) does a raw-string compare of the
# installed server's --version against this field, so it must move with every
# bump or the hook nudges users who are already on the installed release.
# plugin.json has a single top-level "version" key.
PLUGIN_MANIFEST_VERSION_RE = re.compile(
r'("version"\s*:\s*)"([^"]+)"',
)

CORE_VER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)")
PRERELEASE_SUFFIX_RE = re.compile(
r"^(a|alpha|b|beta|rc)\.?(\d+)$",
Expand Down Expand Up @@ -90,6 +100,20 @@ def write_version_to_all_files(new_version: str) -> None:
raise ValueError(msg)
ROOT_PYPROJECT.write_text(new_root, encoding="utf-8")

manifest_text = PLUGIN_MANIFEST.read_text(encoding="utf-8")
new_manifest, manifest_count = PLUGIN_MANIFEST_VERSION_RE.subn(
rf'\1"{new_version}"',
manifest_text,
count=1,
)
if manifest_count != 1:
msg = (
f'Expected one "version" key in {PLUGIN_MANIFEST}, '
f"replaced {manifest_count}"
)
raise ValueError(msg)
PLUGIN_MANIFEST.write_text(new_manifest, encoding="utf-8")


def parse_core(version: str) -> tuple[int, int, int]:
"""Parse leading ``X.Y.Z`` from a version string."""
Expand Down Expand Up @@ -244,6 +268,17 @@ def verify_lockstep() -> int:
)
return 1

try:
manifest_text = PLUGIN_MANIFEST.read_text(encoding="utf-8")
except OSError as exc:
print(f"could not read {PLUGIN_MANIFEST}: {exc}", file=sys.stderr)
return 1
m = PLUGIN_MANIFEST_VERSION_RE.search(manifest_text)
if not m:
print(f'missing "version" in {PLUGIN_MANIFEST}', file=sys.stderr)
return 1
raw[str(PLUGIN_MANIFEST.relative_to(REPO_ROOT))] = m.group(2)

canonical = {label: str(Version(v)) for label, v in raw.items()}
if len(set(canonical.values())) != 1:
print(f"version mismatch across packages: {raw}", file=sys.stderr)
Expand Down
17 changes: 17 additions & 0 deletions tests/test_bump_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,20 @@ def test_root_project_version_re_rejects_missing_version(pyproject: str) -> None
r'\1"REPLACED"', pyproject, count=1
)
assert count == 0, f"expected no match in {pyproject!r}"


def test_plugin_manifest_version_re_replaces_only_the_version() -> None:
manifest = (
"{\n"
' "name": "pipefy",\n'
' "version": "0.2.0-beta.1",\n'
' "homepage": "https://github.com/pipefy/ai-toolkit"\n'
"}\n"
)
new_text, count = _bump.PLUGIN_MANIFEST_VERSION_RE.subn(
r'\1"0.2.0-beta.2"', manifest, count=1
)
assert count == 1
assert '"version": "0.2.0-beta.2"' in new_text
# group(2) is the current version that verify mode reads back.
assert _bump.PLUGIN_MANIFEST_VERSION_RE.search(manifest).group(2) == "0.2.0-beta.1"