mcp-loader v2: streamable-HTTP transport for remote MCP servers (context7)

- New RemoteMcpClient implementing MCP streamable-HTTP per spec 2025-03-26:
  POST JSON-RPC, parse application/json or text/event-stream responses,
  round-trip optional Mcp-Session-Id, optional auth via 'headers' config.
- Refactor StdioMcpClient to share an IMcpClient interface with the remote
  client; extension entry dispatches on cfg.type. Drops the v1 'remote
  skipped with warning' code path.
- Bump MCP_PROTOCOL_VERSION to 2025-11-25 (single constant, both clients).
- 404 self-heal: when a remote returns 404 to a request carrying our
  Mcp-Session-Id, drop the id, re-initialize, retry the request once
  (per spec 2025-11-25 \u00a72.2). allowReinitOn404=false on the retry path
  prevents recursion. Verified via mock-server smoke test.
- Sanitize pi-facing tool names to ^[A-Za-z][A-Za-z0-9_]{0,63}$. Anthropic
  allows hyphens but Bedrock's Anthropic shim rejects them, causing entire
  turns to 4xx silently when context7's hyphenated tools (resolve-library-id,
  query-docs) were registered. Original MCP-side names are preserved in the
  tool-execute closure, so sanitization is purely pi-facing.
- /mcp slash command: drop 'remote (skipped)' status label.
- Docs: README and AGENTS updated for transports, headers config, 404
  self-heal, tool-name sanitization rationale, OAuth limitation.

End-to-end verified: context7 connects through pi, returns useful docs
(Bun streaming/SSE example fetched successfully).
This commit is contained in:
2026-05-09 15:26:36 +02:00
parent 7eec49b9b8
commit 37cc49e06f
3 changed files with 349 additions and 72 deletions
+11 -5
View File
@@ -205,7 +205,7 @@ This is a verbatim copy of the upstream `examples/extensions/todo.ts` shipped wi
### `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.
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, with non-`[A-Za-z0-9_]` characters replaced by `_` so the names pass the strictest provider tool-name regex (e.g. AWS Bedrock).
**Settings.json shape:**
@@ -239,13 +239,20 @@ Generic MCP server loader. Reads an `mcp` block from `~/.pi/agent/settings.json`
| `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. |
| `headers` | Optional object of HTTP headers (e.g. `Authorization`, `X-API-Key`) sent with every request. 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):**
**Transports:**
- **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`).
- `local` — stdio JSON-RPC subprocess (mcp-searxng, gitea-mcp, mcp-server-time…).
- `remote` — streamable-HTTP per MCP spec 2025-03-26: POST JSON-RPC, server replies either `application/json` or `text/event-stream`. Optional `Mcp-Session-Id` round-trip if the server issues one. No GET subscription stream (server-initiated notifications are not consumed).
**Limitations:**
- **No stdio reconnect** if a subprocess dies mid-session — those tools become unavailable until `/reload` (same as `mempalace.ts`).
- **Remote sessions self-heal on 404.** If a streamable-HTTP server forgets our session id (e.g. server restart), the client transparently re-initializes and retries the request once.
- **No OAuth flow.** Remote servers requiring OAuth must be accessed with a pre-issued bearer token via `headers`.
- **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.
@@ -255,7 +262,6 @@ Generic MCP server loader. Reads an `mcp` block from `~/.pi/agent/settings.json`
- `running · N tools` — connected, tools registered
- `failed: <message>` — start handshake threw
- `disabled in settings``enabled: false`
- `remote (skipped — v1 stdio only)` — type `remote`, awaiting v2
- `invalid: <message>` — malformed config (read-only row)
UX matches `/ext`: **space** stages a toggle, **enter** writes back to `settings.json` and reloads pi, **esc** cancels. Toggling re-enables a previously-disabled server by removing the explicit `enabled` key (the default is `true`).