Files
pi-extensions/README.md
joakimp 141bf64d81 Add mcp-loader extension: generic MCP server registration via settings.json
Reads an `mcp` block from ~/.pi/agent/settings.json (same shape as
opencode and Claude Desktop) and connects to each declared MCP server,
exposing all of their tools to pi as native tools namespaced as
<server-name>_<tool-name>.

Why: pi has no built-in MCP loader. Adding each new MCP server as a
hand-rolled extension (the way mempalace.ts does it) doesn't scale.
This is the config-driven generalization — one extension, any number
of servers, no per-server boilerplate.

Settings.json schema matches opencode and Claude Desktop verbatim:

  {
    "mcp": {
      "searxng": {
        "type": "local",
        "command": ["uvx", "mcp-searxng"],
        "env": { "SEARXNG_URL": "https://searxng.your-host.lan" }
      },
      "context7": {
        "type": "remote",
        "url": "https://mcp.context7.com/mcp"
      }
    }
  }

Per-server keys: type (local/remote), command, url, enabled, env.

Implementation:
  • StdioMcpClient class spawns subprocess, performs MCP initialize
    handshake (protocol 2024-11-05), lists tools, exposes a callTool()
    method. Newline-delimited JSON-RPC over stdio.
  • Each MCP tool registered via pi.registerTool with the server-
    namespaced name, the upstream MCP inputSchema passed through
    via Type.Unsafe (TypeBox is JSON-Schema-compatible at runtime).
  • Per-server fail-soft: a server that won't start logs one stderr
    line and is skipped; others continue.
  • SIGTERM all subprocesses on session_shutdown so /reload doesn't
    leak processes.

Tool naming: prefix with <serverName>_ except when the upstream tool
name already starts with that prefix (mempalace's tools are already
mempalace_search, mempalace_kg_query, etc — avoids double-prefixing).

Coexists with mempalace.ts but does not replace it. The mempalace
bridge has bespoke agent-identity injection that's worth preserving.

v1 limitations:
  • Stdio transport only. Remote (streamable-HTTP) servers are
    detected and skipped with a warning. v2 will add streamable-HTTP.
  • No reconnect on subprocess death — same limitation as mempalace.ts.

Verification:
  • node --check syntax clean
  • Standalone smoke test against `uvx mcp-server-time`: handshake +
    tools/list (2 tools) + tools/call (get_current_time) all green
    on the same JSON-RPC code that lives inside the loader.

Debug: set PI_MCP_LOADER_DEBUG=1 to surface per-server stderr.
2026-05-08 20:02:21 +02:00

12 KiB

pi-extensions

Custom and modified extensions for the pi coding-agent.

This repo is the single source of truth for extensions that aren't suitable for general publishing — personal workflow tweaks, modified versions of built-in examples, and extensions written for specific infrastructure. Symlinked into ~/.pi/agent/extensions/ so pi loads them automatically.

Part of the same family as pi-toolkit (bring-up) and skillset (agent skills).


Install

git clone ssh://git@gitea.jordbo.se:2222/joakimp/pi-extensions.git ~/src/src_local/pi-extensions
cd ~/src/src_local/pi-extensions
chmod +x install.sh
./install.sh

Each .ts file in extensions/ is symlinked into ~/.pi/agent/extensions/. Existing real files are backed up with a timestamp. Re-runs are idempotent.

Install a subset:

./install.sh --only ssh-controlmaster          # just this one
./install.sh --only "ssh-controlmaster,other"  # explicit list
./install.sh --skip "git-checkpoint"           # all except these

--only and --skip accept comma-separated names without the .ts suffix. --only takes precedence if both are given.

Alternative: pi install (local path)

Because package.json declares a pi manifest, you can also register this repo as a pi package:

pi install ~/src/src_local/pi-extensions

This makes pi manage the extension loading directly. The install.sh approach (symlinks) and pi install are mutually exclusive for the same extension — pick one per machine.

Uninstall

./install.sh --uninstall

Removes symlinks that point into this repo. Your own files in ~/.pi/agent/extensions/ are never touched.


Extensions

ssh-controlmaster.ts

Transparent SSH remote execution via a persistent ControlMaster socket.

When launched with --ssh user@host, all of pi's native file and shell tools (read, write, edit, bash) are transparently redirected to execute on the remote machine. One SSH connection is established at session start; all subsequent tool calls multiplex over it via a Unix socket. Much faster than the plain ssh.ts example which opens a new connection per tool call.

Use cases:

  • Diagnose and fix issues on a remote server without installing pi there
  • Work on Proxmox hosts, LXC containers, or ephemeral VMs
  • Any machine you have SSH key access to but don't own

Usage:

# Key-based auth (normal)
pi --ssh user@192.168.1.10

# Explicit remote path (skips the initial pwd call)
pi --ssh root@proxmox-node:/etc/pve

# Password auth — prompts before connecting
pi --ssh user@host --ssh-ask-pass

# Try without modifying your global install
pi -e ~/src/src_local/pi-extensions/extensions/ssh-controlmaster.ts --ssh user@host

Requirements:

  • SSH key-based auth (preferred), or password auth via --ssh-ask-pass (see below)
  • bash available on the remote

Note on --ssh-ask-pass: The password is prompted via pi's input dialog before the SSH connection is opened. Input is not masked — the password is visible while typing. It is passed to SSH via a temporary SSH_ASKPASS script (/tmp/pi-askpass-<pid>.sh, chmod 700) which is deleted immediately after the master is established.

How it works:

  1. On session_start, runs ssh -G <host> to read the effective config for that host
  2. If ~/.ssh/config already configures ControlMaster auto or yes for the host, the existing system socket is reused — no second connection is opened and pi does not tear down the master on exit (it was the system's to manage)
  3. Otherwise pi establishes its own master: ssh -fN -o ControlMaster=yes -o ControlPersist=yes -o ControlPath=/tmp/pi-cm-<pid>.sock <remote> and shuts it down cleanly on exit
  4. All tool calls multiplex over the socket with -o ControlMaster=no -o ControlPath=<socket> — near-zero per-call overhead
  5. The system prompt is patched to tell the LLM it's operating on <remoteCwd> (via SSH ControlMaster: <remote>)
  6. User ! shell commands are also routed over SSH

The status bar shows ⚡ own master or ⚡ system master so you can see which path was taken.

Status bar: Shows SSH ⚡ user@host:/path when the master is ready, ⟳ connecting… during setup, and an error state if the master fails to start.

Path mapping: Paths are rewritten by replacing the local cwd with the remote cwd. This means pi should be started from a directory that maps cleanly to a path on the remote. Use the user@host:/explicit/path form when the remote path differs significantly from your local working directory.


confirm-destructive.ts

Confirmation gates for dangerous bash commands and destructive session actions. Always-on — no flag needed.

Bash commands intercepted:

  • Recursive removes (rm -rf, rm -r, etc.)
  • Any sudo command
  • chmod/chown 777
  • dd if= (disk operations)
  • mkfs (format filesystem)
  • git push --force / git push -f
  • Writes to /dev/*
  • truncate --size 0

In non-interactive mode (e.g. pi -p) dangerous commands are blocked outright rather than prompted.

Session actions gated:

  • /new — confirms before clearing the session
  • /resume — confirms before switching away if the current session has messages
  • /fork — always confirms

git-checkpoint.ts

Creates a git stash checkpoint at the start of each turn, keyed to the session entry ID. If you /fork from a past entry, you're offered the option to restore the code to that point.

Silently skips when the working directory isn't inside a git repo, or when there are no changes to stash. Status bar shows ⎇ N checkpoints during active sessions.

Notes:

  • Uses git stash create — non-destructive, doesn't touch your working tree
  • Stash objects persist in the git repo even after pi exits, so you can apply them manually with git stash apply <ref> if needed
  • Checkpoints are in-memory per session — the entry→ref mapping is lost on restart, but the underlying stash objects remain

notify.ts

Sends a native terminal notification when the agent finishes and is waiting for input. Only fires when the agent ran for longer than the threshold (default 8 seconds) — quick responses are silently skipped.

Terminal support:

  • Kitty (KITTY_WINDOW_ID) → OSC 99
  • Windows Terminal / WSL (WT_SESSION) → Windows toast
  • Everything else (iTerm2, WezTerm, Ghostty) → OSC 777

Flag:

pi --notify-min-secs 15   # only notify for tasks over 15 seconds
pi --notify-min-secs 0    # notify on every agent completion

ext-toggle.ts

Registers /ext — a slash command that lists extensions in ~/.pi/agent/extensions/ and toggles individual ones on/off without leaving the TUI.

How it works: pi auto-discovers *.ts only. Toggling renames a file (or symlink) between name.ts and name.ts.off, so a disabled extension is invisible to the loader. After a toggle, the extension calls ctx.reload() so the change takes effect immediately — no restart needed.

Usage:

/ext                # opens the multi-toggle overlay
  • / — navigate
  • space — stage a toggle (visual / flip; not yet applied)
  • enter — commit all staged changes and reload pi
  • esc — cancel, no changes

A footer line shows pending changes (e.g. pending: notify→off, foo→on) so you can see exactly what enter will apply. Guard rejections appear there too (⊘ ssh-controlmaster: …).

Notes:

  • Subdirectory-style extensions (name/index.ts) are listed read-only — v1 doesn't toggle them. Move the directory aside manually if needed.
  • install.sh --uninstall cleans up both .ts and .ts.off symlinks pointing into this repo, so a disabled extension won't be left behind.
  • Re-running ./install.sh respects a prior /ext disable: if <name>.ts.off already exists, the installer leaves it alone instead of silently re-enabling.
  • ssh-controlmaster cannot be disabled via /ext while pi was launched with --ssh — disabling mid-session would silently revert tool calls to the local filesystem. Exit pi and relaunch without --ssh instead.

Adding a new extension

  1. Drop a .ts file into extensions/
  2. Re-run ./install.sh — it picks up the new file and symlinks it
  3. In a running pi session, /reload is enough; no restart needed
  4. (or, with ext-toggle installed: /ext to disable noisy ones at runtime)

todo.ts

Gives the agent a todo tool (actions: list / add / toggle / clear) so it can externalize a multi-step plan and tick items off as it works. Also registers /todos so you can inspect the current list at any time.

State lives in the session's tool result details, not an external file. So:

  • pi --continue / --resume brings the todos back with the conversation.
  • /fork forks the todo list along with the branch — each branch has its own state.

This is a verbatim copy of the upstream examples/extensions/todo.ts shipped with pi-coding-agent. Refresh from upstream when desired (see AGENTS.md).

mcp-loader.ts

Generic MCP server loader. Reads an mcp block from ~/.pi/agent/settings.json (same shape as opencode and Claude Desktop) and connects to each declared server, exposing all of their tools to pi as native tools — namespaced as <server-name>_<tool-name> to avoid collisions.

Settings.json shape:

{
  // … existing pi settings …
  "mcp": {
    "searxng": {
      "type": "local",
      "command": ["uvx", "mcp-searxng"],
      "env": { "SEARXNG_URL": "https://searxng.your-host.lan" }
    },
    "gitea": {
      "type": "local",
      "command": ["gitea-mcp", "-t", "stdio"],
      "enabled": false,
      "env": { "GITEA_ACCESS_TOKEN": "...", "GITEA_HOST": "https://gitea.example.com" }
    },
    "context7": {
      "type": "remote",
      "url": "https://mcp.context7.com/mcp"
    }
  }
}

Per-server keys:

Key Description
type "local" (stdio subprocess) or "remote" (streamable-http). Default "local".
command Argv array. First element is the executable, rest are args. Local servers only.
url Remote MCP endpoint URL. Remote servers only.
enabled Default true. Set false to disable a server without removing the entry.
env Optional object of env vars injected into the subprocess. Inherits parent env first, then overlays these keys. Local servers only.

Limitations (v1):

  • Stdio only. Remote/streamable-HTTP transport is detected and skipped with a warning. Server like context7 configured as "type": "remote" will not load until v2.
  • No reconnect if a subprocess dies mid-session — those tools become unavailable until /reload (same as mempalace.ts).
  • Coexists with mempalace.ts but does not replace it. The mempalace bridge has bespoke handling (agent identity injection) that's worth keeping. Don't list mempalace in the mcp block too — you'd get duplicate tool registrations.

Debug: set PI_MCP_LOADER_DEBUG=1 in the environment to surface per-server stderr and connection logs.

Each extension is a TypeScript module loaded by jiti — no compilation step. See the pi extensions docs and the built-in examples for the API surface.


Deploying on a new machine

# 1. Prerequisites: pi installed, SSH key auth working
pi --help           # creates ~/.pi/agent/ on first run

# 2. Clone and install
git clone ssh://git@gitea.jordbo.se:2222/joakimp/pi-extensions.git ~/src/src_local/pi-extensions
cd ~/src/src_local/pi-extensions && ./install.sh

# 3. Verify
ls -la ~/.pi/agent/extensions/   # should show symlinks into this repo

  • pi-toolkit — pi bring-up: settings template, keybindings, shell env loader
  • mempalace-toolkit — persistent memory layer for pi via MemPalace MCP
  • skillset — agent skills for pi, opencode, and Claude

License

MIT — see LICENSE.