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
+47
View File
@@ -203,6 +203,53 @@ State lives in the session's tool result `details`, not an external file. So:
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:**
```jsonc
{
// … 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](https://github.com/unjs/jiti) — no compilation step. See the [pi extensions docs](https://github.com/mariozechner/pi-coding-agent/blob/main/docs/extensions.md) and the [built-in examples](https://github.com/mariozechner/pi-coding-agent/tree/main/examples/extensions) for the API surface.
---