pi has had MCP as a built-in extension (builtin:mcp) since 0.99.0. It reads
~/.pi/agent/mcp.json and adds `pi mcp add|list|login`, a /mcp manager,
OAuth, per-tool exposure and resources. An extension that registers /mcp
REPLACES the built-in, so with mcp-loader installed pi never read mcp.json
and printed "registers command /mcp, so built-in extension mcp was not
loaded" on every start.
Measured on pi 1.1.0 (pi-devbox v1.11.0, tor-ms22) before removing it:
- time to the (auto) footer in a real pty, 7 interleaved runs each:
-ne 0.276 s | -ne -e mcp-loader.ts 0.283 s | -ne -e builtin:mcp 0.272 s
- with no servers, the same prompt costs 79 input + 7680 cached tokens with
the loader, the built-in, or neither
- a stdio server (server-everything) connected via `pi mcp list` in 3 s,
and its echo tool ran end to end from `pi -p` with direct exposure
- no device in the fleet had an `mcp` block, so nothing needs migrating
install.sh now prunes links into this repo whose target is gone (.ts and
.ts.off), so the stale mcp-loader.ts link on a preserved pi-devbox volume is
removed on the first boot of an image without the file. It also warns once
if settings.json still carries an `mcp` block. Tested two-sided in a temp
HOME: the new script removes the repo link and a stale .ts.off, and keeps a
foreign dangling link and a real file. The old script leaves the dangling
link. Re-runs are idempotent, and there is no warning without an mcp block.
mempalace.ts (mempalace-toolkit) is unaffected: it registers mempalace_*
tools itself. Its vendored RemoteMcpClient loses its sync partner; that is
handled in mempalace-toolkit.
activate_hooks assumed SCRIPT_DIR is a clone the caller owns. pi-devbox bakes
this repo root-owned under /opt/pi-extensions and runs install.sh as
`developer` on every boot: git refuses the repo ("dubious ownership"),
`config --local` fails (silenced), `cur` is empty, and the unguarded
`git config core.hooksPath hooks` exits 128 -- under set -euo pipefail that
aborted the installer and the entrypoint printed
"WARN: pi-extensions install.sh failed (continuing)" on every boot of every
device. Damage was nil (activate_hooks is the last real step; all symlinks
were already created) but a WARN that always fires is a WARN nobody reads.
Now: resolve the git dir and require .git/config to be writable; otherwise
print one note and return 0. Hooks are for clones you commit from, and a
vendored read-only copy is not one.
Measured (function extracted, run under set -euo pipefail, 8 cases):
old fn /opt/pi-extensions as developer rc=128 "not in a git directory"
old fn root-owned throwaway clone rc=128
new fn /opt/pi-extensions, root-owned throwaway, no .git, .git/config 444
rc=0 NOTE "Repo hooks skipped"
new fn writable clone, hooksPath unset rc=0 activated, config reads "hooks"
new fn writable clone, hooksPath=hooks rc=0 "already active"
neither root-owned config was modified. shellcheck: only the pre-existing
SC2155 at line 170.
Editing skill/ here is only half the job: skillset mirrors it at
skills/pi-extensions/, and that copy is what most consumers actually read --
each Mac's host-side ~/.agents/skills/pi-extensions symlinks into it, and a
workstation with only skillset cloned has no other copy. The refresh is a manual
cp in another repo and it has now been forgotten on two consecutive edits, so
the mirror drifted 4890 B behind, then 9579 B.
hooks/pre-push compares the PUSHED content (git show <sha>:skill/...) against the
mirror on disk and prints the exact cp/sed/commit sequence when they differ.
Pushed content rather than the worktree: uncommitted local edits are not what
this push publishes.
It warns and exits 0 rather than blocking, for two reasons that are not
squeamishness: the direction rule is "edit upstream, THEN refresh", so the
refresh legitimately comes after this push, and it is a commit in a different
repo that cannot be made from here. Enforcement belongs downstream and already
exists -- skillset's pre-commit gate refuses a commit that leaves the mirror
stale. This hook only shortens time-to-detection from "next skillset commit" to
"seconds, to the person who caused it".
Silent when the mirror already matches, when the push does not touch skill/, on
branch deletions, and when no skillset clone is on disk -- a reminder that cannot
be acted on is noise that trains people to skim hook output.
install.sh activates it (core.hooksPath=hooks, per-clone config that cannot be
tracked), preserves a foreign hooksPath rather than clobbering it, and does NOT
undo the activation on --uninstall: removing a safety gate as a side effect of
uninstalling extensions would be a surprise in the wrong direction.
Verified: all three activate_hooks branches in a throwaway repo, and five
pre-push scenarios driven through the real stdin protocol (stale -> warns,
in-sync -> silent, non-skill push -> silent, branch deletion -> silent, no
skillset clone -> silent).
Pi moved to its new home at earendil-works on 2026-05-07
(https://pi.dev/news/2026/5/7/pi-has-a-new-home). Affected packages:
@mariozechner/pi-coding-agent -> @earendil-works/pi-coding-agent
@mariozechner/pi-tui -> @earendil-works/pi-tui
@mariozechner/pi-ai -> @earendil-works/pi-ai
@mariozechner/pi-agent-core -> @earendil-works/pi-agent-core
The old @mariozechner/* packages are deprecated on npm with the
explicit message 'please use @earendil-works/pi-coding-agent instead
going forward', and the version stream has moved on (old top-out
0.73.1; new currently 0.74.0). Anyone npm-installing the old names
gets a deprecation warning + a stale binary.
Sweep:
- All 7 extension TypeScript files: import statements updated.
- README, AGENTS, install.sh: textual references and the github.com/
mariozechner/pi-coding-agent URL pointed at github.com/earendil-works/
pi (the new monorepo root; coding-agent now lives at
packages/coding-agent inside it).
- Bun build of mcp-loader, ext-toggle, ssh-controlmaster verified clean.
Brew install references (`brew install pi-coding-agent`) left as-is:
the homebrew formula still works at 0.73.1 and a tap update is
tracked upstream at earendil-works/pi#2755. Historical CHANGELOG
entries are untouched.
When linking, check for <name>.ts.off pointing into this repo and skip
relinking if found. Means a previously /ext-disabled extension stays
disabled across install.sh re-runs (e.g. when adding a new extension).
README + AGENTS updated with the new behavior.
extensions/ext-toggle.ts:
/ext lists ~/.pi/agent/extensions/ with active/disabled markers
and toggles individual extensions by renaming between name.ts and
name.ts.off (pi only auto-discovers *.ts). Calls ctx.reload() so the
change takes effect without restarting pi.
Subdirectory-style extensions (name/index.ts) are listed read-only
in v1 — toggling a directory cleanly is more work than the rename
trick is worth.
install.sh:
--uninstall now matches both *.ts and *.ts.off symlinks pointing
into this repo, so a disabled extension is still cleaned up.
README.md / AGENTS.md:
Document ext-toggle alongside the others; AGENTS notes the API
surface used (registerCommand, ui.select/confirm/notify, reload)
and the rename-not-delete design decision.
macOS ships bash 3.2 which does not support associative arrays (declare -A,
bash 4+ only). Replace INSTALL_SET associative array with a space-delimited
string and an in_install_set() helper function. All operations preserved:
add, skip/remove, empty-check, iteration, membership test.