98 lines
3.9 KiB
Markdown
98 lines
3.9 KiB
Markdown
|
|
# mcpctl × pi — native integration (no MCP, no Claude)
|
|||
|
|
|
|||
|
|
## Motivation
|
|||
|
|
|
|||
|
|
With Claude Code, `mcpctl config claude --project X` wires a project's MCP
|
|||
|
|
servers into the agent two ways:
|
|||
|
|
|
|||
|
|
1. a `.mcp.json` entry running `mcpctl mcp -p X` (an MCP **stdio bridge**), and
|
|||
|
|
2. a `SessionStart` hook + `mcpctl skills sync` that materialises server-side
|
|||
|
|
**skills** under `~/.claude/skills/`.
|
|||
|
|
|
|||
|
|
Claude Code supports MCP natively, so tools travel over MCP. **pi does not
|
|||
|
|
support MCP.** But pi has two native mechanisms that cover the same ground:
|
|||
|
|
|
|||
|
|
- **Extensions** — TypeScript modules that can register tools, commands, and
|
|||
|
|
custom UI, and can talk HTTP directly.
|
|||
|
|
- **Skills** — pi implements the [Agent Skills standard](https://agentskills.io/specification)
|
|||
|
|
and loads `SKILL.md` trees (the exact format mcpctl already syncs).
|
|||
|
|
|
|||
|
|
This addon exploits both so mcpctl works with **pi only**, with **no MCP** and
|
|||
|
|
**no dependency on `~/.claude/`** (so it keeps working if you drop Claude).
|
|||
|
|
|
|||
|
|
## How it works
|
|||
|
|
|
|||
|
|
The MCP layer is not magic. `mcpctl mcp` is just a JSON-RPC ↔ stdio bridge that
|
|||
|
|
forwards to mcplocal's Streamable-HTTP endpoint:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
POST {mcplocalUrl}/projects/<project>/mcp
|
|||
|
|
Accepts: application/json, text/event-stream
|
|||
|
|
Authorization: Bearer <token>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The protocol is JSON-RPC over HTTP:
|
|||
|
|
- `initialize` → returns an `mcp-session-id` header
|
|||
|
|
- `notifications/initialized`
|
|||
|
|
- `tools/list` → `{ tools: [{ name, description, inputSchema }] }`
|
|||
|
|
- `tools/call` → `{ name, arguments }` → `{ content, isError }`
|
|||
|
|
|
|||
|
|
The pi extension is a **direct JSON-RPC client** to that same endpoint. It
|
|||
|
|
replaces the MCP transport with a plain function call, so pi never needs an MCP
|
|||
|
|
client library.
|
|||
|
|
|
|||
|
|
### Gating
|
|||
|
|
|
|||
|
|
Some projects are *gated*: until `begin_session` is called, `tools/list` returns
|
|||
|
|
only `begin_session`. After it is called the full tool set appears.
|
|||
|
|
|
|||
|
|
**Critical subtlety:** mcplocal's gate state is bound to a `mcp-session-id`. A
|
|||
|
|
fresh session is gated again even after another session was ungated — so the
|
|||
|
|
extension keeps **one persistent `McpHttpSession` per active project** and
|
|||
|
|
routes every `tools/call` through it. After each call it re-runs `tools/list`
|
|||
|
|
and reconciles the active pi tool set, so calling `begin_session` automatically
|
|||
|
|
opens the gate and reveals the rest — no push listener required (mcplocal's HTTP
|
|||
|
|
responses are request/response, not a persistent push stream).
|
|||
|
|
|
|||
|
|
## Deliverables
|
|||
|
|
|
|||
|
|
| Artifact | Purpose |
|
|||
|
|
|----------|---------|
|
|||
|
|
| `src/pi-ext/mcpctl-pi.ts` | Self-contained pi extension (load via `pi -e` or settings `extensions`) |
|
|||
|
|
| `mcpctl config pi` | CLI wiring: registers the extension with pi + initial skill sync |
|
|||
|
|
| `docs/pi-extension.md` | This document |
|
|||
|
|
|
|||
|
|
## Project switching
|
|||
|
|
|
|||
|
|
The active project is persisted in `~/.mcpctl/pi-state.json` (settable, immune to
|
|||
|
|
your shell's cwd) and is also inferred from a `.mcpctl-project` marker walk-up.
|
|||
|
|
`/mcpctl` opens an interactive menu; **switch project** lists projects via
|
|||
|
|
`ctx.ui.select()` and reloads the tool set for the new project, namespaced so
|
|||
|
|
projects never collide.
|
|||
|
|
|
|||
|
|
### Tool naming
|
|||
|
|
|
|||
|
|
To allow multiple projects and avoid schema collisions, registered pi tools are
|
|||
|
|
namespaced: `mc_<project-slug>_<mcp-tool-name>`. Only the **current project's**
|
|||
|
|
tools are in the active set at any time.
|
|||
|
|
|
|||
|
|
## Skills
|
|||
|
|
|
|||
|
|
Skills already sync in the Agent Skills `SKILL.md` format. `mcpctl config pi`
|
|||
|
|
(when given a project) writes them under pi's own directory
|
|||
|
|
`~/.pi/agent/skills/` and registers that directory in `~/.pi/agent/settings.json`
|
|||
|
|
— no Claude involvement. `mcpctl pi sync-skills` (a thin wrapper) re-runs the
|
|||
|
|
existing `skills` code path against the pi target directory.
|
|||
|
|
|
|||
|
|
## Layout
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
src/pi-ext/
|
|||
|
|
mcpctl-pi.ts # the extension (self-contained)
|
|||
|
|
mcp-http.ts # vendored Streamable-HTTP JSON-RPC client (no deps)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The extension imports only from pi-bundled packages
|
|||
|
|
(`@earendil-works/pi-coding-agent`, `@earendil-works/pi-ai`, `typebox`), so it
|
|||
|
|
loads standalone.
|