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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user