Mirrors /ext UX (space=stage, enter=apply+reload, esc=cancel) but for
MCP servers in the settings.json `mcp` block. Tracks per-server runtime
state captured at extension load time so users can see at a glance
which servers are running / failed / disabled / remote-skipped /
invalid, with tool counts for the running ones.
Toggling writes back to settings.json — disabling sets enabled:false,
re-enabling removes the explicit key (default is true) to keep the
file tidy. Then ctx.reload() picks up the change.
Closes the visibility gap surfaced by 'searxng_search isn't in /ext':
MCP-provided tools are runtime-spawned, not file-based extensions, so
they need their own list view. /mcp fills that hole.
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.