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.
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) bashavailable 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 temporarySSH_ASKPASSscript (/tmp/pi-askpass-<pid>.sh,chmod 700) which is deleted immediately after the master is established.
How it works:
- On
session_start, runsssh -G <host>to read the effective config for that host - If
~/.ssh/configalready configuresControlMaster autooryesfor 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) - 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 - All tool calls multiplex over the socket with
-o ControlMaster=no -o ControlPath=<socket>— near-zero per-call overhead - The system prompt is patched to tell the LLM it's operating on
<remoteCwd> (via SSH ControlMaster: <remote>) - 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
sudocommand chmod/chown 777dd 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
↑/↓— navigatespace— stage a toggle (visual●/○flip; not yet applied)enter— commit all staged changes and reload piesc— 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 --uninstallcleans up both.tsand.ts.offsymlinks pointing into this repo, so a disabled extension won't be left behind.- Re-running
./install.shrespects a prior/extdisable: if<name>.ts.offalready exists, the installer leaves it alone instead of silently re-enabling. ssh-controlmastercannot be disabled via/extwhile pi was launched with--ssh— disabling mid-session would silently revert tool calls to the local filesystem. Exit pi and relaunch without--sshinstead.
Adding a new extension
- Drop a
.tsfile intoextensions/ - Re-run
./install.sh— it picks up the new file and symlinks it - In a running pi session,
/reloadis enough; no restart needed - (or, with
ext-toggleinstalled:/extto 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/--resumebrings the todos back with the conversation./forkforks 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
context7configured as"type": "remote"will not load until v2. - No reconnect if a subprocess dies mid-session — those tools become unavailable until
/reload(same asmempalace.ts). - Coexists with
mempalace.tsbut does not replace it. The mempalace bridge has bespoke handling (agent identity injection) that's worth keeping. Don't listmempalacein themcpblock 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
Related repos
pi-toolkit— pi bring-up: settings template, keybindings, shell env loadermempalace-toolkit— persistent memory layer for pi via MemPalace MCPskillset— agent skills for pi, opencode, and Claude
License
MIT — see LICENSE.