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