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.
This commit is contained in:
2026-05-08 20:02:21 +02:00
parent ba994014a7
commit 141bf64d81
3 changed files with 470 additions and 0 deletions
+61
View File
@@ -23,6 +23,7 @@ extensions/
notify.ts # Native terminal notification when agent finishes
ext-toggle.ts # /ext slash command — list & toggle extensions at runtime
todo.ts # `todo` tool for the agent + /todos for the user (copy of upstream example)
mcp-loader.ts # Generic MCP server loader — reads `mcp` block from settings.json
install.sh # Idempotent installer — symlinks extensions/ into ~/.pi/agent/extensions/
package.json # pi package manifest — enables `pi install /path` as an alternative
README.md # User-facing docs.
@@ -246,6 +247,66 @@ cp /opt/homebrew/Cellar/pi-coding-agent/*/libexec/lib/node_modules/@mariozechner
# review diff, then commit
```
### `mcp-loader.ts`
Generic MCP stdio client — reads an `mcp` block from `~/.pi/agent/settings.json`
and connects to each declared server, exposing the tools as namespaced pi tools.
**Why this exists:** pi has no built-in MCP loader (unlike opencode and Claude
Desktop). Adding each new MCP server as a hand-rolled extension (the way
`mempalace.ts` does it) doesn't scale past 2-3 servers. This loader is the
config-driven generalization — one extension, any number of servers.
**Key design decisions:**
- **Settings.json shape matches opencode / Claude Desktop verbatim.** A user
can copy-paste their `mcp` block from one harness's config to the other.
- **Tool names are prefixed with the server name** to avoid collisions when
multiple servers expose tools with the same short name. Tools that already
start with `<servername>_` (the convention some servers like mempalace
follow internally) skip the prefix to avoid double-prefixing.
- **Fail-soft per server.** A server that won't start (binary missing, init
handshake fails) logs one stderr line and is skipped. Other servers
continue. Pi keeps working.
- **Stdio transport only in v1.** Remote/streamable-HTTP servers are
detected and skipped with a warning. Adding remote support is the
obvious v2 (context7 is the prime motivator). Avoided in v1 because
streamable-HTTP needs SSE parsing, session management, and reconnect
logic that triples the implementation size.
- **Does not replace `mempalace.ts`.** The mempalace bridge has bespoke
agent-identity injection from `$MEMPALACE_AGENT_NAME` that's worth
preserving. Loader and bridge coexist. Listing `mempalace` in the `mcp`
block would produce duplicate tool registrations — don't.
- **No reconnect on subprocess death.** If a server's subprocess crashes
mid-session, its tools become permanently unavailable until pi `/reload`s.
Same limitation as the mempalace bridge today; not worth complicating v1.
**API used:**
- `pi.registerTool({ name, label, description, parameters, execute })` for
each MCP tool exposed by each connected server.
- `pi.on("session_shutdown", ...)` to SIGTERM all subprocesses cleanly.
- `Type.Unsafe<...>(inputSchema)` from typebox to pass the MCP server's
JSON schema through to pi without conversion (TypeBox schemas are
plain JSON Schema at runtime).
**Internal MCP client:**
- `class StdioMcpClient` — spawn subprocess, write newline-delimited
JSON-RPC, parse newline-delimited responses, match by request `id`.
- Sends `initialize` handshake with `protocolVersion: 2024-11-05`, then
`notifications/initialized`, then `tools/list` to discover tools.
- Stderr drained silently unless `PI_MCP_LOADER_DEBUG=1`.
**Future v2 extensions:**
- Streamable-HTTP transport for remote servers (context7).
- Per-tool enable/disable in settings.json (e.g.
`"mcp.searxng.tools": ["web_search"]` to expose only a subset).
- Reconnect-on-crash with exponential backoff.
- Schema sanitization for MCP servers that emit malformed `inputSchema`
(some return `{type: "object", properties: {...}}` without `required`).
No framework. Manual:
```bash