@counterposition/skill-pi 0.84.2 → 0.99.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/package.json +15 -4
- package/skills/pi/README.md +8 -7
- package/skills/pi/SKILL.md +13 -7
- package/skills/pi/evals/evals.json +81 -0
- package/skills/pi/examples/classifier-gate.ts +46 -0
- package/skills/pi/examples/nested-tools.ts +52 -0
- package/skills/pi/examples/structured-tool.ts +61 -0
- package/skills/pi/references/codemode.md +88 -0
- package/skills/pi/references/extensions.md +59 -13
- package/skills/pi/references/mcp.md +122 -0
- package/skills/pi/references/packages.md +11 -5
- package/skills/pi/references/patterns.md +37 -2
- package/skills/pi/references/providers.md +113 -14
- package/skills/pi/references/sdk.md +84 -16
- package/skills/pi/references/settings.md +49 -16
|
@@ -81,28 +81,38 @@ Useful session events:
|
|
|
81
81
|
- `resources_discover`
|
|
82
82
|
- `project_trust` — user/global and CLI extensions only, fired before project resources load; return `{ trusted: "yes" | "no" | "undecided" }` (optionally `remember: true` to persist). The first yes/no decision wins and suppresses the built-in trust prompt.
|
|
83
83
|
|
|
84
|
-
`session_before_compact` and `session_compact` carry `reason` and `willRetry` (Pi 0.79.10) so handlers can distinguish manual `/compact`, threshold auto-compaction, and overflow-retry compaction.
|
|
84
|
+
`session_before_compact` and `session_compact` carry `reason` and `willRetry` (Pi 0.79.10) so handlers can distinguish manual `/compact`, threshold auto-compaction, and overflow-retry compaction. `session_compact_failed` (Pi 0.84.3) reports failed or aborted compactions with `reason`, `aborted`, `willRetry`, `fromExtension`, and `errorMessage`.
|
|
85
85
|
|
|
86
86
|
`session_start` carries `event.reason` — `"startup" | "reload" | "new" | "resume" | "fork"` — and `event.previousSessionFile` for `"new"`/`"resume"`/`"fork"`. The old post-transition `session_switch` / `session_fork` events were removed; route all of those flows through `session_start`.
|
|
87
87
|
|
|
88
88
|
Useful agent events:
|
|
89
89
|
|
|
90
90
|
- `input` — carries `event.streamingBehavior` (`"steer" | "followUp" | undefined`) so handlers can distinguish idle prompts, mid-stream steers, and queued follow-ups; return `{ action: "transform" | "handled" | "continue" }`
|
|
91
|
-
- `before_agent_start` —
|
|
91
|
+
- `before_agent_start` — `event.systemPrompt` is read-only; mutate `event.systemPromptOptions` (sections, selected tools, guidelines) so Pi appends a transcript delta that survives resume/branching and keeps the cached prefix (Pi 0.86.0). Returning `{ systemPrompt }` replaces the whole prompt for that run and is sent as the provider's leading system prompt
|
|
92
92
|
- `agent_start`
|
|
93
93
|
- `agent_end` — one low-level run; Pi may still auto-retry, auto-compact and retry, or continue with queued follow-ups
|
|
94
|
-
- `
|
|
94
|
+
- `agent_before_settle` (Pi 0.87.0) — the final actionable boundary before settlement (see below)
|
|
95
|
+
- `agent_settled` (Pi 0.80.4) — notification-only; fired when no retry/compaction/follow-up remains, for status integrations that need to know Pi will not continue automatically. Since Pi 0.87.0, runs requested from `agent_settled` handlers are deferred until every settled handler finishes (`ctx.isIdle()` stays true, no reentrant `agent_start`)
|
|
95
96
|
- `turn_start`
|
|
96
|
-
- `turn_end`
|
|
97
|
+
- `turn_end` — actionable since Pi 0.87.0; carries `turnIndex`, `message`, `toolResults`, and their session entry IDs
|
|
97
98
|
- `message_start`
|
|
98
99
|
- `message_update`
|
|
99
100
|
- `message_end` — return a replacement message to override usage/cost or rewrite the finalized assistant message
|
|
100
|
-
- `context`
|
|
101
|
+
- `context` — transforms conversation messages; since Pi 0.87.0 handlers no longer see prompt/tool system messages, and Pi restores that state afterward (filtering or slicing messages is safe)
|
|
102
|
+
- `context_with_system` (Pi 0.87.0) — runs after `context` on the full transcript including system messages and is sent verbatim; use only when a request-local transform must own the whole transcript, and keep a system message at index 0
|
|
101
103
|
- `before_provider_headers` (Pi 0.80.4) — mutate `event.headers` in place after outgoing HTTP headers are assembled: set a key to a string to add/override, `null` to delete. Runs once per provider request; retries reuse the same headers
|
|
102
104
|
- `before_provider_request`
|
|
103
105
|
- `after_provider_response` — inspect the provider HTTP status and headers before stream consumption
|
|
106
|
+
- `provider_stream_event` (Pi 0.99.0) — notification-only view of each parsed provider stream event before Pi normalizes it (`provider`, `api`, `model`, `data`). Treat `data` as read-only; handlers are awaited in stream order, so keep them fast. Not persisted
|
|
104
107
|
- `model_select`
|
|
105
108
|
- `thinking_level_select` — observe interactive thinking-level changes
|
|
109
|
+
- `ui_prompt_start` / `ui_prompt_end` (Pi 0.84.4) — distinguish active agent work from time spent waiting on blocking `ctx.ui` prompts (`kind`: `select`/`confirm`/`input`/`editor`/`custom`)
|
|
110
|
+
- `cache_warming_decision` (Pi 0.86.0) — override an idle prompt-cache refresh with `{ action: "warm" }` or `{ action: "stop" }`; the last handler returning an action wins
|
|
111
|
+
- `user_bash` — user `!`/`!!` commands. Fails closed since Pi 0.86.0: return `undefined` to pass to the next handler (then local execution), `{ operations }` or `{ result }` to handle it; a throwing handler or invalid result aborts the command
|
|
112
|
+
|
|
113
|
+
`turn_end` and `agent_before_settle` are actionable boundaries (Pi 0.87.0). Handlers receive the proposed `event.entries`, `event.continue`, a `context` preview, and the run `outcome`, and may return `{ entries: [...event.entries, draft], continue: true }` to persist `custom`, `custom_message`, `context_edit`, or `compaction` entries in order and ensure exactly one next provider request without disturbing steering/follow-up queues. Guard the condition — an unconditional `continue: true` loops.
|
|
114
|
+
|
|
115
|
+
`pi.on()` returns an unsubscribe function (Pi 0.86.0); handlers added or removed during a dispatch apply from the next dispatch.
|
|
106
116
|
|
|
107
117
|
Useful tool events:
|
|
108
118
|
|
|
@@ -112,6 +122,10 @@ Useful tool events:
|
|
|
112
122
|
- `tool_result`
|
|
113
123
|
- `tool_execution_end`
|
|
114
124
|
|
|
125
|
+
Calls another tool makes through `ctx.executeTool()` (codemode scripts, orchestrating tools) fire the same tool events, so permission gates see them; see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions).
|
|
126
|
+
|
|
127
|
+
Since Pi 0.86.0, tool-call `arguments` and result `details` must be JSON-compatible values (no `Date`, `Map`, class instances, `undefined` array slots); use `details: undefined` when there is nothing structured to keep. Tools registered without a parameter schema are rejected at registration.
|
|
128
|
+
|
|
115
129
|
Tool results may include `terminate: true` to end the current tool batch without an automatic follow-up LLM turn — useful for tools that produce a structured final answer (see `examples/extensions/structured-output.ts` in the Pi repo). Since Pi 0.84.1, blocked `tool_call` results may also return `terminate: true`; the agent stops early only when every finalized result in the batch is terminating. Tools that make nested LLM calls can return their combined `Usage` as `usage` on the tool result — Pi persists it and includes it in footer, `/session`, and RPC session totals (Pi 0.81.0), and `tool_result` handlers may inspect or replace that value.
|
|
116
130
|
|
|
117
131
|
`session_shutdown` events carry `event.reason` (`"quit" | "reload" | "new" | "resume" | "fork"`) and, where applicable, `event.targetSessionFile` so cleanup logic can distinguish teardown paths.
|
|
@@ -125,6 +139,8 @@ Tool results may include `terminate: true` to end the current tool batch without
|
|
|
125
139
|
- `ctx.cwd`
|
|
126
140
|
- `ctx.sessionManager` (read-only; `buildContextEntries()` returns active-branch entries with compaction applied, Pi 0.80.4)
|
|
127
141
|
- `ctx.modelRegistry` / `ctx.model` / `ctx.thinkingLevel` / `ctx.scopedModels` (Pi 0.83.0) — `ctx.modelRegistry` is the synchronous extension-facing facade; its `refresh()` became `Promise<void>` in Pi 0.80.8 (await it before synchronous registry reads). Since Pi 0.84.x it also exposes `getProvider(id)` (effective pi-ai provider) and `getProviderAuth(id)` (API key, headers, base URL, provider-scoped env — no loaded model required). `ctx.scopedModels` is the read-only session model scope resolved from `--models`/`enabledModels` (`{ model, thinkingLevel? }[]`, empty when unscoped); prefer it over enumerating `getAvailable()` for model pickers
|
|
142
|
+
- `ctx.modelRegistry.stream()` / `streamSimple()` / `complete()` (Pi 0.86.0) — nested model calls through configured providers with request-time auth; prefer `streamSimple()` for provider-neutral calls, pass `ctx.signal`, and report the combined `usage` on the tool result
|
|
143
|
+
- `ctx.modelRegistry.findOfType(type, provider, id)`, `getModelsOfType()`, `getAvailableOfType()`, and `classify(model, { state, questions }, { signal })` (Pi 0.99.0) — classifier and image models live beside chat models; `classify()` never rejects, so check `result.stopReason` (see `references/providers.md`)
|
|
128
144
|
- `ctx.hasUI` — `true` in TUI and RPC modes
|
|
129
145
|
- `ctx.signal` — the active agent abort signal (or `undefined` when idle); pass it to `fetch`/model calls for abort-aware nested work
|
|
130
146
|
- `ctx.isIdle()` / `ctx.hasPendingMessages()`
|
|
@@ -134,6 +150,8 @@ Tool results may include `terminate: true` to end the current tool batch without
|
|
|
134
150
|
- `ctx.getSystemPrompt()`
|
|
135
151
|
- `ctx.isProjectTrusted()` — the effective project trust decision, including temporary `--approve`/`--no-approve` decisions
|
|
136
152
|
|
|
153
|
+
Tool `execute()` receives `ExtensionToolContext`, which adds `ctx.tools` (the tools this call can reach) and `ctx.executeTool(name, args, { signal?, onUpdate? })` (Pi 0.99.0). `executeTool()` runs validation and the `tool_call`/`tool_result` hooks like a model call and never rejects: unknown tools, validation errors, blocks, and thrown errors come back as `{ isError: true }`, including after an abort. The signal defaults to the calling tool's. Nested usage is added to the calling tool's result, so report only the tool's own `usage`.
|
|
154
|
+
|
|
137
155
|
Use `CONFIG_DIR_NAME` (exported from `@earendil-works/pi-coding-agent`) instead of hardcoding `.pi` when building project config paths, e.g. `join(ctx.cwd, CONFIG_DIR_NAME, "my-extension.json")`.
|
|
138
156
|
|
|
139
157
|
Command handlers receive `ExtensionCommandContext`, which extends the above with session-control methods that would deadlock from event handlers: `ctx.getSystemPromptOptions()` (inspect the base system-prompt inputs), `ctx.waitForIdle()`, `ctx.newSession()`, `ctx.fork()`, `ctx.switchSession()`, `ctx.navigateTree()`, and `ctx.reload()`.
|
|
@@ -169,20 +187,35 @@ Important rules:
|
|
|
169
187
|
|
|
170
188
|
1. Truncate large output.
|
|
171
189
|
2. Respect `signal.aborted`.
|
|
172
|
-
3. Throw on failure instead of returning fake success.
|
|
173
|
-
4. Use the file-mutation queue for write/edit style tools.
|
|
190
|
+
3. Throw on failure instead of returning fake success, or return `isError: true` when the failure still carries data. A returned object without `isError` counts as success.
|
|
191
|
+
4. Use the file-mutation queue (`withFileMutationQueue()`) for write/edit style tools.
|
|
174
192
|
5. Put reconstructable state in `details`; it persists in session history.
|
|
175
193
|
|
|
194
|
+
## Tool Exposure & Structured Results
|
|
195
|
+
|
|
196
|
+
Pi 0.99.0 added fields for tools that other tools, codemode scripts, and permission extensions consume ([docs](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/extensions.md#tool-exposure), [types](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/src/core/extensions/types.ts)):
|
|
197
|
+
|
|
198
|
+
- `outputSchema` (TypeBox) plus `structuredContent` on every result: the model still reads `content`; codemode scripts receive `structuredContent` instead of text. Without `outputSchema`, scripts get the text. `structuredContent` must be JSON-compatible. A `tool_result` handler that replaces `content` must also return `structuredContent`, or it is dropped.
|
|
199
|
+
- `isError: true` on a returned result: the model sees an error, while `details` and `structuredContent` are kept for the UI and scripts.
|
|
200
|
+
- `exposure`: `direct` (default), `model-only`, `codemode`, `deferred`, or `hidden`. `direct` and `model-only` tools activate on registration; the others do not (`defaultActive: false` also keeps a `direct` tool inactive until `--tools`/`defaultTools`/`setActiveTools()` names it). Tools cannot be unregistered: re-register with `exposure: "hidden"` to withdraw one. The table of what each value declares and allows is in `references/codemode.md`.
|
|
201
|
+
- `namespace: { name, description? }` groups related tools under one heading in the `codemode` description, as MCP servers do.
|
|
202
|
+
- `annotations`: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, with MCP meanings. Missing hints mean "not read-only, may be destructive, open world". They are unverified; mark writes neither read-only nor casually idempotent.
|
|
203
|
+
- `prepareLoadout(loadout)`: for tools that orchestrate others. Runs whenever the active tools change with `declared`, `callable`, and `registered` tools, and returns replacement `descriptions` and `hiddenDeclarations` (active tools left out of requests but still callable). The built-in `codemode` and `tool_search` use only this, `exposure`, and `ctx.executeTool()`.
|
|
204
|
+
|
|
205
|
+
`pi.getAllTools()` reports each tool's `exposure`, `namespace`, and `annotations`. `pi.setActiveTools()` ignores unknown and `hidden` names. Compilable examples: `examples/structured-tool.ts` and `examples/nested-tools.ts`.
|
|
206
|
+
|
|
176
207
|
## Dynamic Tool Loading
|
|
177
208
|
|
|
209
|
+
For tools the model should find on demand, prefer `exposure: "deferred"` with the built-in `tool_search` (Pi 0.99.0), or `exposure: "codemode"` for tools scripts call. `tool_search` is inactive by default: enable it with `"defaultTools": ["+tool_search"]` or `--tools`, or let a connected MCP server with `deferred` exposure turn it on. Without it, the model can reach `deferred` tools only through codemode. The manual loader pattern below still works.
|
|
210
|
+
|
|
178
211
|
Pi 0.80.7 lets an extension register many tools while keeping only a small initial set active, then add more during execution — cache-friendly on models with native deferred loading. Lifecycle:
|
|
179
212
|
|
|
180
213
|
1. Register every tool with `pi.registerTool()` (all appear in `pi.getAllTools()`).
|
|
181
214
|
2. Keep a loader tool (e.g. `search_tools`) active; leave searchable tools inactive — e.g. on `session_start`, `pi.setActiveTools()` to the filtered set.
|
|
182
215
|
3. During loader execution, call `pi.setActiveTools([...pi.getActiveTools(), ...matches])`. The change must be purely additive; unknown names are ignored.
|
|
183
|
-
4. Pi records the added tools on that tool result and exposes their definitions before the next model response.
|
|
216
|
+
4. Pi records the added tools on that tool result and exposes their definitions before the next model response. Since Pi 0.86.0 prompt and tool changes are transcript-backed system messages, so they survive resume and branch navigation.
|
|
184
217
|
|
|
185
|
-
Native deferred loading preserves the cached prompt prefix on Anthropic Sonnet/Opus/Fable ≥ 4.5 (not Haiku)
|
|
218
|
+
Native deferred loading preserves the cached prompt prefix on Anthropic Sonnet/Opus/Fable ≥ 4.5 (not Haiku), OpenAI `gpt-5.4`+, and Fireworks Messages models (Pi 0.86.0; name the loader `ToolSearch` or `tool_search`); Kimi K3 works via `compat.deferredToolsMode: "kimi"` (Pi 0.80.9). For verified custom models/proxies, enable `compat.supportsToolReferences: true` (`anthropic-messages`) or `compat.supportsToolSearch: true` (`openai-responses`/`openai-codex-responses`). All other models fall back to sending the full active tool list on the next request — activation still works, but may invalidate the provider's cached prefix. Non-additive changes (removals/replacements) always use the fallback.
|
|
186
219
|
|
|
187
220
|
Cache tips: keep the loader active for the whole session; add rather than replace. Activating a tool that has `promptSnippet`/`promptGuidelines` rebuilds the system prompt and can invalidate the prefix even with native support — lazily loaded tools should rely on their `description` alone.
|
|
188
221
|
|
|
@@ -194,7 +227,7 @@ Cache tips: keep the loader active for the whole session; add rather than replac
|
|
|
194
227
|
- `{ type: "grammar", variants: { openai_lark?: string, openai_regex?: string } }` — OpenAI Lark/regex grammar encodings
|
|
195
228
|
- Set `false` to explicitly disable for that tool
|
|
196
229
|
|
|
197
|
-
Model capability metadata gates what is actually sent: `compat.supportsStrictTools` / Anthropic built-ins enable strict JSON-schema tools, and `compat.supportsOpenAIGrammarTools` marks endpoints that accept Lark/regex grammar tools (enabled in generated metadata for GPT-5+ across OpenAI, Codex, Azure, GitHub Copilot, opencode, Cloudflare AI Gateway). Unsupported combinations fall back to normal function tools rather than failing.
|
|
230
|
+
Model capability metadata gates what is actually sent: `compat.supportsStrictTools` / Anthropic built-ins enable strict JSON-schema tools, and `compat.supportsOpenAIGrammarTools` marks endpoints that accept Lark/regex grammar tools (enabled in generated metadata for GPT-5+ across OpenAI, Codex, Azure, GitHub Copilot, opencode, Cloudflare AI Gateway). Unsupported combinations fall back to normal function tools rather than failing. Since Pi 0.86.0, strict-prefer JSON-schema sampling is on by default for the built-in `read`, `bash`, `powershell`, `edit`, and `write` tools (no `PI_EXPERIMENTAL` needed); re-register a definition with `constrainedSampling: false` to opt out.
|
|
198
231
|
|
|
199
232
|
## Display Transformers
|
|
200
233
|
|
|
@@ -299,6 +332,14 @@ pi.on("session_start", async (_event, ctx) => {
|
|
|
299
332
|
});
|
|
300
333
|
```
|
|
301
334
|
|
|
335
|
+
## MCP Servers
|
|
336
|
+
|
|
337
|
+
`pi.registerMcpServer(name, config)` adds a server for the current session with the `mcp.json` entry shape; `pi.unregisterMcpServer(name)` closes it. Registrations are not saved, and an `mcp.json` server with the same name wins. Extensions that implement their own MCP client read `pi.getMcpServers()` on `session_start` and handle `mcp_servers_change`. See `references/mcp.md`.
|
|
338
|
+
|
|
339
|
+
## Virtual Models
|
|
340
|
+
|
|
341
|
+
`pi.registerVirtualModel({ provider, id, name, thinkingLevels?, route(request, ctx) })` (experimental, Pi 0.99.0) adds a selectable model that picks a physical model and thinking level for every request. See `references/providers.md#virtual-models`.
|
|
342
|
+
|
|
302
343
|
## Provider Integration
|
|
303
344
|
|
|
304
345
|
Extensions can register or override providers:
|
|
@@ -329,6 +370,8 @@ pi.unregisterProvider("my-provider");
|
|
|
329
370
|
|
|
330
371
|
Dynamic providers can implement `refreshModels(context)` (Pi 0.80.8) for model discovery: Pi calls it during catalog refresh (`/model`, `pi update --models`) and publishes the returned list; its models replace extension-provided `models`. Since Pi 0.84.0 the context is read-snapshot + generation-checked publication: read `context.stored` (the persisted provider snapshot) instead of `context.store`, and persist through `context.publish({ update?, persist? })` — `persist` omitted leaves storage unchanged, a `ModelsStoreEntry` writes it, `persist: null` deletes it. Config-form callbacks that only return models remain unchanged.
|
|
331
372
|
|
|
373
|
+
A config-form `models` list replaces the provider's models across chat, image, and classifier operations (Pi 0.99.0). Omitted `type` means `"chat"`; image and classifier entries need an explicit `type` plus implementations keyed by `api` through `images`/`classifiers` (see `references/providers.md`).
|
|
374
|
+
|
|
332
375
|
Since Pi 0.81.0, extensions can also register a complete pi-ai `Provider` via `pi.registerProvider(createProvider({...}))` — native auth (`login`/`resolve`), `getModels`, `refreshModels`, `filterModels`, and custom streaming. The registered provider becomes the composition base; `models.json` overrides still apply above it. Prefer this when you need real authentication or stream behavior rather than just a model list. The legacy config form (`name`, `baseUrl`, `apiKey`, `models`, `oauth`, `streamSimple`) remains supported. OAuth `refreshToken(credentials, signal)` callbacks must accept and honor the abort signal since Pi 0.84.0.
|
|
333
376
|
|
|
334
377
|
If you need auth for a specific model request, use:
|
|
@@ -339,12 +382,15 @@ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
|
339
382
|
|
|
340
383
|
(Still supported for extensions in Pi 0.80.8+; SDK code should use `ModelRuntime.getAuth()` — see `references/sdk.md`. `ProviderHeaders` values are `string | null` since Pi 0.84.0, where `null` is a delete marker: handle `null` when inspecting, pass through unchanged when forwarding to pi-ai streams.)
|
|
341
384
|
|
|
342
|
-
|
|
385
|
+
Custom `streamSimple` implementations receive a normalized `TranscriptContext` since Pi 0.86.0 — see `references/providers.md`. See `references/sdk.md` and `references/providers.md` for the full provider/model schema.
|
|
343
386
|
|
|
344
387
|
## Handy Patterns
|
|
345
388
|
|
|
346
|
-
- Permission gates via `tool_call`
|
|
347
|
-
-
|
|
389
|
+
- Permission gates via `tool_call` (they also see nested calls from codemode; see `references/codemode.md`)
|
|
390
|
+
- Data tools for codemode via `outputSchema` + `structuredContent`
|
|
391
|
+
- Orchestrating tools via `ctx.executeTool()` with `exposure: "model-only"`
|
|
392
|
+
- Prompt injection via `before_agent_start` (`systemPromptOptions.sections`)
|
|
393
|
+
- Model-context pruning via `context`, or durable omission via `context_edit` entries from `turn_end`/`agent_before_settle`
|
|
348
394
|
- Branch or compaction customization via `session_before_tree` / `session_before_compact`
|
|
349
395
|
- Provider payload inspection via `before_provider_request`
|
|
350
396
|
- Stateful tools via `details` plus `session_start` reconstruction
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# MCP Servers
|
|
2
|
+
|
|
3
|
+
Pi 0.99 connects to MCP servers through a built-in extension. There is no package to install. Checked against Pi v0.99.1: [mcp.md](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/mcp.md), [cli.md#mcp-commands](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/cli.md#mcp-commands), [extensions.md#mcp-servers](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/extensions.md#mcp-servers).
|
|
4
|
+
|
|
5
|
+
## Configure
|
|
6
|
+
|
|
7
|
+
| File | Scope |
|
|
8
|
+
|------|-------|
|
|
9
|
+
| `~/.pi/agent/mcp.json` | Global. Put personal servers and servers with credentials here |
|
|
10
|
+
| `.pi/mcp.json` | Project. Read only after the project is trusted, because stdio servers run commands |
|
|
11
|
+
|
|
12
|
+
A project entry replaces a global entry with the same name. The format is the usual `mcpServers` object, so Claude Desktop, Claude Code, and Cursor entries copy over unchanged:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"filesystem": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
|
|
20
|
+
},
|
|
21
|
+
"docs": {
|
|
22
|
+
"url": "https://example.com/mcp",
|
|
23
|
+
"headers": { "Authorization": "Bearer ${DOCS_TOKEN}" },
|
|
24
|
+
"exposure": "direct"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- stdio: `command` (one executable, not a shell string), `args`, `env`, `cwd`. Relative `cwd` resolves against the session directory; a leading `~/` works in `command`, args, and `cwd`.
|
|
31
|
+
- HTTP: `url`, `headers`, `oauth`. Only streamable HTTP; `"type": "sse"` is rejected (try the server's `/mcp` endpoint instead of `/sse`).
|
|
32
|
+
- `type` is optional (`stdio`, `http`, or `streamable-http`); `command` implies stdio and `url` implies HTTP.
|
|
33
|
+
- `env` and `headers` values accept `${NAME}` and `!command`. A command must produce the whole value: `"Authorization": "!echo Bearer $(gh auth token)"`.
|
|
34
|
+
- `timeout` is seconds per request (default 60); progress notifications reset it. `enabled: false` keeps the entry without connecting.
|
|
35
|
+
- Server names use letters, digits, `_`, `-`. Tools are named `mcp__<server>__<tool>`.
|
|
36
|
+
- Invalid entries are skipped and reported; other servers still connect.
|
|
37
|
+
|
|
38
|
+
Converting other clients: VS Code uses `servers` + `inputs` (move under `mcpServers`, replace `${input:...}` with `${NAME}`); Codex uses TOML `[mcp_servers.<name>]` (same fields as JSON); opencode uses `"type": "local"` with an array `command` (split into `command`/`args`), `environment` for `env`, and `{env:NAME}` for `${NAME}`.
|
|
39
|
+
|
|
40
|
+
## Set Up From a Shell
|
|
41
|
+
|
|
42
|
+
These commands work outside a session, so an agent can run them through `bash`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pi mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem .
|
|
46
|
+
pi mcp add docs --url https://example.com/mcp --bearer-token-env-var DOCS_TOKEN --exposure direct
|
|
47
|
+
pi mcp add -l tools --env API_KEY='${TOOLS_KEY}' -- uvx tools-mcp # -l writes .pi/mcp.json
|
|
48
|
+
pi mcp list # connects to every enabled server; exits 1 while anything is wrong
|
|
49
|
+
pi mcp login sentry # OAuth: opens the browser and waits for the user to approve
|
|
50
|
+
pi mcp remove docs # keeps stored OAuth credentials
|
|
51
|
+
pi mcp logout sentry
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`pi mcp add` does not connect; verify with `pi mcp list`. A running session picks up added or changed servers after `/reload` or a new session.
|
|
55
|
+
|
|
56
|
+
In a session, `/mcp` opens the server manager (state, tools, errors, sign in/out, reconnect, exposure, enable/disable). Changes are written back to the `mcp.json` that defines the server. Outside the TUI, `/mcp` prints status and `/mcp login|logout|reconnect <server>` run directly.
|
|
57
|
+
|
|
58
|
+
Pi connects on session start; the first prompt waits up to 10 seconds for startup connections. OAuth tokens live in `~/.pi/agent/mcp-auth.json`; server log notifications go to `~/.pi/agent/mcp.log`. OAuth is used only for HTTP servers whose config has no `Authorization` header. The optional `oauth: { clientId?, clientSecret?, callbackPort?, callbackUrl?, scope? }` sets a pre-registered client for servers without dynamic client registration, the loopback redirect (`callbackPort` alone gives `http://127.0.0.1:<port>/callback`; a `callbackUrl` without a port gets `callbackPort` or a free one), and `scope`, space-separated scopes to request when the server does not advertise the ones it needs.
|
|
59
|
+
|
|
60
|
+
## Exposure
|
|
61
|
+
|
|
62
|
+
`exposure` decides how the model reaches a server's tools:
|
|
63
|
+
|
|
64
|
+
| Value | Declared to the model | Callable from codemode | Notes |
|
|
65
|
+
|-------|----------------------|------------------------|-------|
|
|
66
|
+
| `codemode` (default) | No | Yes, listed in the `codemode` description | Pi activates `codemode` when such a server connects |
|
|
67
|
+
| `codemode-deferred` | No | Yes, found with `searchTools()` / `ALL_TOOLS` | Description names only the server and tool count; for large, rarely used servers |
|
|
68
|
+
| `deferred` | After `tool_search` loads them | Yes | Pi activates `tool_search`; for large servers without codemode |
|
|
69
|
+
| `direct` | Yes, like built-in tools | Yes | |
|
|
70
|
+
| `hidden` | No | No | Registered but unreachable |
|
|
71
|
+
|
|
72
|
+
`toolExposure` overrides single tools by name or `*` pattern (exact name wins, then first matching pattern):
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"github": {
|
|
78
|
+
"url": "https://api.githubcopilot.com/mcp/",
|
|
79
|
+
"exposure": "deferred",
|
|
80
|
+
"toolExposure": { "search_code": "direct", "get_*": "codemode", "delete_*": "hidden" }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Undeclared tools (`codemode`, `codemode-deferred`, `deferred`) are reachable through either tool: codemode scripts can call a `deferred` server's tools, and `tool_search` can load a `codemode` server's tools. Set top-level `"autoEnableCodemode": false` in `mcp.json` to stop Pi from activating `codemode`. Pi warns once when neither `codemode` nor `tool_search` is active, because the tools are then unreachable.
|
|
87
|
+
|
|
88
|
+
## Results
|
|
89
|
+
|
|
90
|
+
- Text results over 20KB reach the model with the middle cut out; the full text goes to a temp file named in the result.
|
|
91
|
+
- Codemode scripts receive the whole `CallToolResult` (`content`, `structuredContent`, `isError`). A result with `isError` resolves in scripts and is reported to the model as an error for direct calls. `image(result.content[0])` forwards an image block.
|
|
92
|
+
- Servers with resources add `list_mcp_resources`, `list_mcp_resource_templates`, and `read_mcp_resource` (marked read-only).
|
|
93
|
+
- Tool calls are never retried (the server may have run them); resource reads retry once on 408/429/5xx.
|
|
94
|
+
|
|
95
|
+
## Permissions
|
|
96
|
+
|
|
97
|
+
Every MCP call goes through Pi's tool pipeline, so `tool_call` and `tool_result` handlers (permission gates included) see MCP tools. Calls from codemode scripts are nested calls; see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions). `pi.getAllTools()` reports the server's `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` annotations. They are unverified hints; see [Tool Exposure](extensions.md#tool-exposure--structured-results).
|
|
98
|
+
|
|
99
|
+
## Servers From Extensions
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
pi.registerMcpServer("jira", { url: "https://mcp.example.com/jira", exposure: "codemode" });
|
|
103
|
+
pi.unregisterMcpServer("jira");
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- Same config shape as an `mcpServers` entry. Registrations are not saved: register on every load.
|
|
107
|
+
- Registered during load: connects on `session_start` with the `mcp.json` servers. Registered later: connects right away.
|
|
108
|
+
- An `mcp.json` server with the same name wins. Another extension's name, an invalid name, or an invalid config throws.
|
|
109
|
+
- `pi mcp` shell commands do not load extensions and only see `mcp.json` servers.
|
|
110
|
+
- Alternative MCP extensions read registrations with `pi.getMcpServers()` on `session_start` and handle `mcp_servers_change`.
|
|
111
|
+
|
|
112
|
+
## Turning It Off or Replacing It
|
|
113
|
+
|
|
114
|
+
- `"extensions": ["-builtin:mcp"]` in settings, or `pi config` → Built-in, disables the built-in support. `pi mcp` shell commands still work.
|
|
115
|
+
- `--no-extensions` disables built-ins too; `pi -ne -e builtin:mcp` keeps only MCP.
|
|
116
|
+
- An extension that registers the `/mcp` command (for example `pi-mcp-adapter`) replaces the built-in support: `mcp.json` is then not read in sessions.
|
|
117
|
+
|
|
118
|
+
## SDK
|
|
119
|
+
|
|
120
|
+
SDK sessions do not load built-in extensions. Add `createMcpExtension()`, plus `createCodemodeExtension()` for `codemode`/`codemode-deferred` servers and `createToolSearchExtension()` for `deferred` servers, then call `session.bindExtensions()` so `session_start` connects the servers. See `references/sdk.md`.
|
|
121
|
+
|
|
122
|
+
The standalone client is published as `@earendil-works/pi-mcp` (stdio and streamable HTTP transports, OAuth, an in-memory testing transport). It has no Pi session dependency.
|
|
@@ -94,16 +94,21 @@ If there is no `pi` manifest, Pi auto-discovers:
|
|
|
94
94
|
|
|
95
95
|
## Dependencies
|
|
96
96
|
|
|
97
|
+
Checked against Pi v0.99.1 [packages.md](https://github.com/earendil-works/pi/blob/v0.99.1/packages/coding-agent/docs/packages.md#declare-dependencies).
|
|
98
|
+
|
|
97
99
|
- Normal runtime dependencies belong in `dependencies`
|
|
98
|
-
- Pi
|
|
100
|
+
- Pi supplies these to extensions; declare them as peer deps with `"*"` ranges and never bundle them:
|
|
99
101
|
- `@earendil-works/pi-ai`
|
|
100
102
|
- `@earendil-works/pi-agent-core`
|
|
101
103
|
- `@earendil-works/pi-coding-agent`
|
|
102
104
|
- `@earendil-works/pi-tui`
|
|
103
105
|
- `typebox`
|
|
104
|
-
-
|
|
105
|
-
-
|
|
106
|
-
- Git package installs
|
|
106
|
+
- Do not list those host-provided packages in `dependencies`. A physical copy can bypass Pi's extension module mapping and create duplicate classes and registries; since Pi 0.99.0 Pi reports an extension warning for this manifest.
|
|
107
|
+
- Pi suppresses automatic peer installation for managed npm packages and for git packages installed with npm (`--omit=dev --legacy-peer-deps`), pnpm (`--prod`, no auto-installed peers), or Bun (`--omit=dev --omit=peer`). Local path packages are not installed or modified; their dependency tree is the author's responsibility.
|
|
108
|
+
- Git package installs omit dev dependencies — runtime needs go in `dependencies`, not `devDependencies`
|
|
109
|
+
- Installed packages load with separate module roots. Do not rely on two packages sharing one dependency instance, or on one package resolving another package's undeclared dependency.
|
|
110
|
+
- Other Pi packages used as dependencies must be included in the published tarball and referenced through their `node_modules` resource paths.
|
|
111
|
+
- Pin dev dependencies to the Pi line you test against (e.g. `^0.99.1`) so typecheck/tests validate against what users run; keep peers `"*"` so installs stay compatible across Pi versions. `"*"` does not prove compatibility: document the minimum supported Pi version and feature-check newer APIs (for example `ctx.executeTool`, `pi.registerMcpServer`) if you support older hosts.
|
|
107
112
|
|
|
108
113
|
## Package Filtering
|
|
109
114
|
|
|
@@ -134,4 +139,5 @@ Rules:
|
|
|
134
139
|
|
|
135
140
|
- Project settings override global settings for the same package identity — unless the project entry has `autoload: false`, in which case it is applied as a delta over the global entry (Pi 0.80.4)
|
|
136
141
|
- Identity is package name for npm, repo URL for git, and resolved absolute path for local sources
|
|
137
|
-
- `pi config` can enable or disable package resources after installation; since Pi 0.80.4 it manages global vs project-local scopes (Tab to switch, `pi config -l` to start in project mode with inherited global resources dimmed)
|
|
142
|
+
- `pi config` can enable or disable package resources after installation; since Pi 0.80.4 it manages global vs project-local scopes (Tab to switch, `pi config -l` to start in project mode with inherited global resources dimmed). Since Pi 0.99.0 its Built-in section also disables the built-in `mcp`, `llama.cpp`, `codemode`, and `tool-search` extensions, stored as `-builtin:<name>` in `extensions`
|
|
143
|
+
- The `codemode`, `tool-search`, and `mcp` built-ins are replaceable: an extension that registers one of their tools, commands, or flags (for example `/mcp`, `codemode`, or `tool_search`) replaces that built-in, and Pi warns
|
|
@@ -19,6 +19,11 @@ Practical recipes for extending Pi. Each pattern is a self-contained example.
|
|
|
19
19
|
13. [File Watcher / Trigger](#file-watcher--trigger)
|
|
20
20
|
14. [Input Transform](#input-transform)
|
|
21
21
|
15. [Tool Override](#tool-override)
|
|
22
|
+
16. [Structured Tool](#structured-tool)
|
|
23
|
+
17. [Nested Tool Calls](#nested-tool-calls)
|
|
24
|
+
18. [Classifier Gate](#classifier-gate)
|
|
25
|
+
|
|
26
|
+
Patterns 16–18 are full files under `examples/` that type-check against Pi 0.99.1 in this skill's package; read them rather than retyping.
|
|
22
27
|
|
|
23
28
|
---
|
|
24
29
|
|
|
@@ -43,6 +48,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
43
48
|
}
|
|
44
49
|
```
|
|
45
50
|
|
|
51
|
+
The handler also runs for `bash` calls made by codemode scripts; see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions).
|
|
52
|
+
|
|
46
53
|
## Protected Paths
|
|
47
54
|
|
|
48
55
|
Block writes to sensitive files:
|
|
@@ -213,7 +220,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
213
220
|
|
|
214
221
|
## Claude Rules Integration
|
|
215
222
|
|
|
216
|
-
Scan `.claude/rules/` and
|
|
223
|
+
Scan `.claude/rules/` and add a system-prompt section. `event.systemPrompt` is read-only; mutating `event.systemPromptOptions` lets Pi append a transcript delta that keeps the cached prefix (returning `{ systemPrompt }` instead replaces the whole prompt for that run):
|
|
217
224
|
|
|
218
225
|
```typescript
|
|
219
226
|
export default function (pi: ExtensionAPI) {
|
|
@@ -224,7 +231,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
224
231
|
.filter(f => f.toString().endsWith(".md"));
|
|
225
232
|
if (files.length === 0) return;
|
|
226
233
|
const listing = files.map(f => `- .claude/rules/${f}`).join("\n");
|
|
227
|
-
event.
|
|
234
|
+
event.systemPromptOptions.sections.project_rules =
|
|
235
|
+
`Project rules (${files.length} files):\n${listing}\nUse the read tool to load relevant rules.`;
|
|
228
236
|
});
|
|
229
237
|
}
|
|
230
238
|
```
|
|
@@ -312,3 +320,30 @@ pi.on("tool_call", async (event, ctx) => {
|
|
|
312
320
|
}
|
|
313
321
|
});
|
|
314
322
|
```
|
|
323
|
+
|
|
324
|
+
## Structured Tool
|
|
325
|
+
|
|
326
|
+
`examples/structured-tool.ts` registers a read-only data tool:
|
|
327
|
+
|
|
328
|
+
- `outputSchema` plus `structuredContent` on every result, so codemode scripts get `{ files, missing }` as an object while the model reads a text summary.
|
|
329
|
+
- `annotations: { readOnlyHint: true, openWorldHint: false }` for permission extensions, and a `namespace` for grouping in the `codemode` description.
|
|
330
|
+
- `isError: true` when no file was found: the model sees an error, scripts still get the data.
|
|
331
|
+
- Only `ENOENT` is treated as "missing"; other errors are rethrown.
|
|
332
|
+
|
|
333
|
+
## Nested Tool Calls
|
|
334
|
+
|
|
335
|
+
`examples/nested-tools.ts` registers `read_many`, which reads several files in parallel through `ctx.executeTool("read", ...)`:
|
|
336
|
+
|
|
337
|
+
- `exposure: "model-only"`: the model can call it, other tools and codemode cannot, so it cannot recurse.
|
|
338
|
+
- Checks `ctx.tools` first: an inactive `direct` tool is not callable.
|
|
339
|
+
- Each nested call runs every `tool_call` gate (see [Nested Calls and Permissions](codemode.md#nested-calls-and-permissions)). `executeTool()` never rejects, and aborted calls come back as `isError`, so the tool calls `signal?.throwIfAborted()` after the batch to report the abort as an abort rather than as failed reads.
|
|
340
|
+
- Streams progress through `onUpdate`.
|
|
341
|
+
|
|
342
|
+
## Classifier Gate
|
|
343
|
+
|
|
344
|
+
`examples/classifier-gate.ts` asks a classifier model whether a `bash` command is destructive before it runs:
|
|
345
|
+
|
|
346
|
+
- Finds Jev with `ctx.modelRegistry.findOfType("classifier", "typesafe", "jev-latest")` and calls `ctx.modelRegistry.classify(..., { signal: ctx.signal })`.
|
|
347
|
+
- `classify()` never rejects: it checks `stopReason` (`"aborted"` blocks, `"error"` falls through to confirmation) and validates the `bool` answer's probability.
|
|
348
|
+
- Fails closed: a missing model, an error, an out-of-range answer, or a probability above the strict `MAX_ALLOWED_PROBABILITY` (0.2; tune it) asks the user, and blocks when `ctx.hasUI` is false.
|
|
349
|
+
- Uses `satisfies ClassifierContext["questions"]` so the question literal keeps its narrow types.
|