mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(coding-agent): codemode and MCP
This adds codemode and MCP support to pi. Codemode (packages/codemode) runs model-written JavaScript in a QuickJS wasm VM inside a worker. From there the script calls pi's tools as async functions, uses a per-session store, and can read the model catalog and run classifiers. The codemode tool in coding-agent is compatible with Codex's exec tool, and codemode.mode works like Codex's tool modes (on or only). Large results are written to a JSON temp file. MCP (packages/mcp) is a standalone client for stdio and streamable HTTP servers, with OAuth. The core only gets general mechanisms; codemode, tool_search and MCP are built-in extensions that use them. Other extensions can replace these built-ins. Tools declare an exposure: direct, model-only, codemode, deferred or hidden. prepareLoadout() lets an orchestrating tool change which tool declarations the model sees. ctx.executeTool() runs nested calls through validation and the tool_call/tool_result hooks. Those calls emit events with parentToolCallId and are recorded as bounded nestedCalls on the parent result, which compaction and HTML export use. Tools can also return structuredContent with an outputSchema, and can report errors with isError instead of throwing. MCP servers come from mcp.json (global, or per project after the project is trusted) or from pi.registerMcpServer(). They are managed with /mcp and pi mcp list|login|logout, and their runtimes load on first use. Closes #10040
This commit is contained in:
parent
b485fa3128
commit
8562bcf66a
143 changed files with 17620 additions and 225 deletions
|
|
@ -13,6 +13,7 @@ pi update [target] [options]
|
|||
pi list
|
||||
pi config [options]
|
||||
pi auth <check|print-api-key|print-bearer-token> [options]
|
||||
pi mcp <list|login|logout> [options]
|
||||
```
|
||||
|
||||
<a id="modes"></a>
|
||||
|
|
@ -137,6 +138,27 @@ Default enabled tools are `read`, `bash`, `edit`, and `write`, unless `defaultTo
|
|||
| `find` | Find paths using glob patterns |
|
||||
| `ls` | List directory contents |
|
||||
|
||||
Built-in extensions add two more tools. They are off by default; name them in `--tools` or `defaultTools` to enable them.
|
||||
|
||||
| Built-in extension | Purpose |
|
||||
|---|---|
|
||||
| `codemode` | Run JavaScript that calls the other tools, for example in parallel with `Promise.allSettled`; only the script's output reaches the model |
|
||||
| `tool_search` | Search tools that are not declared to the model (`codemode` and `deferred` exposure, such as MCP tools) and declare the matches for the next call |
|
||||
|
||||
Codemode scripts run in a QuickJS sandbox that can only reach the other tools, through `tools.<name>(args)`; `ALL_TOOLS` lists them. Output comes from `text(value)`, `image(dataUrlOrImageContent)`, `console.*`, and a top-level `return value`; `exit()` ends the script early. The result starts with `Script completed` or `Script failed`, the wall time, and the output; a failed script keeps its partial output, followed by `Script error:` and the error.
|
||||
|
||||
A script may start with an options line such as `// @options: {"max_output_tokens": 2000, "timeout_ms": 60000}`. `max_output_tokens` (default 10000) limits the output: longer output keeps its start and end, and the full text is written to a temp file whose path is included in the result. `timeout_ms` is a hard deadline, unset by default.
|
||||
|
||||
While `codemode` is active, `codemode.mode` in [settings](settings.md#tools) decides how the other tools are presented. With `on` (default) declared tools keep being declared and their descriptions show how to call them from scripts. With `only` they are hidden from the model and listed in the `codemode` description instead, so the model calls them through scripts.
|
||||
|
||||
The `codemode` description lists the callable tools with their TypeScript declarations, grouped by namespace (for example one MCP server). Declarations share a budget of 3000 estimated tokens (`codemode.inlineBudget` in [settings](settings.md#tools)); every namespace is still listed with its tool count, and the description says whether the list is complete. Scripts find the rest with `await searchTools(query, { limit, namespace })`, which ranks tools with BM25, and `await describeTool(name)`, or by filtering `ALL_TOOLS`.
|
||||
|
||||
`tool_search` is off by default; enable it with `--tools` or `defaultTools`. It uses the same ranking over tools that are not declared yet and declares the matches for the next model call. Loaded tools are recorded in the session like other tool changes, so they stay declared on that branch.
|
||||
|
||||
Tools with an output schema resolve to structured values: `bash` to `{ output, exit_code, wall_time_seconds }`, also for non-zero exit codes, and MCP tools to their `CallToolResult`. Other tools resolve to their text output.
|
||||
|
||||
`store(key, value)` and `load(key)` keep JSON values across `codemode` calls: each successful script that stores values appends a `codemode-store` custom entry to the session, so resumed sessions keep the values and each branch sees only the values written on its path. Scripts can also use `models`: `getModelsOfType`, `getAvailableOfType`, and `getModelOfType` list the model catalog, and `classify(model, context)` runs a classifier model with the session's credentials, at most four at a time per script.
|
||||
|
||||
<a id="resource-options"></a>
|
||||
|
||||
## Resources
|
||||
|
|
@ -266,3 +288,20 @@ Authentication commands require `--provider <provider>` or `--model <model>`. Se
|
|||
| `--min-expiry <duration>` | `print-bearer-token` | Require remaining token lifetime using `ms`, `s`, `m`, or `h`, such as `30m` |
|
||||
|
||||
Credential-printing commands write secrets to stdout.
|
||||
|
||||
## MCP commands
|
||||
|
||||
These commands work outside a session, so agents can run them through `bash`. See [MCP Servers](mcp.md).
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `pi mcp add <server> [options] -- <command> [args...]` | Add or replace a stdio server in `mcp.json`; `--env KEY=VALUE` (repeatable) and `--cwd <dir>` set its environment and working directory. Arguments after the command are passed to it |
|
||||
| `pi mcp add <server> [options] --url <url>` | Add or replace a streamable HTTP server; `--header KEY=VALUE` (repeatable), `--bearer-token-env-var <NAME>` (sends `Authorization: Bearer ${NAME}`), `--oauth-client-id`, `--oauth-client-secret`, and `--oauth-callback-port` configure authentication |
|
||||
| `pi mcp remove <server>` | Remove a server from `mcp.json`; stored OAuth credentials are kept |
|
||||
| `pi mcp list [--json]` | Connect to every enabled server and print its state, tools, and errors; exit with `1` when a config entry is invalid or an enabled server is not connected |
|
||||
| `pi mcp login <server> [--timeout <seconds>]` | Sign in to an OAuth server: open the authorization page and wait for the browser (default 300 seconds); a terminal also accepts the pasted redirect URL |
|
||||
| `pi mcp logout <server>` | Delete the stored OAuth credentials of a server |
|
||||
|
||||
`add` and `remove` change `~/.pi/agent/mcp.json`, or `.pi/mcp.json` in the current directory with `--local` (`-l`). `add` also takes `--exposure <mode>` (see [Exposure](mcp.md#exposure)) and does not connect; run `pi mcp list` to check the server.
|
||||
|
||||
Project `.pi/mcp.json` files are only read for projects that are already trusted.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ The agent directory is shown as `<agent-dir>` below. Set its location with the `
|
|||
|---|---|
|
||||
| `<agent-dir>/settings.json` | User-level [settings](settings.md), including preferences, defaults, resource paths, and Pi package declarations. |
|
||||
| `<agent-dir>/keybindings.json` | Custom terminal UI and application [keybindings](keybindings.md). |
|
||||
| `<agent-dir>/mcp.json` | [MCP servers](mcp.md) available in every project. |
|
||||
| `<agent-dir>/models.json` | [Compatible endpoints, models, and model overrides](models.md#configure-a-compatible-endpoint). |
|
||||
| `<agent-dir>/auth.json` | Saved API keys and OAuth credentials. |
|
||||
| `<agent-dir>/AGENTS.override.md`, `AGENTS.md`, `AGENTS.MD`, `CLAUDE.md`, or `CLAUDE.MD` | User instructions applied across working directories. |
|
||||
|
|
@ -27,6 +28,7 @@ The agent directory is shown as `<agent-dir>` below. Set its location with the `
|
|||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `.pi/settings.json` | Project-level [settings](settings.md), resource paths, and Pi package declarations. |
|
||||
| `.pi/mcp.json` | Project [MCP servers](mcp.md). |
|
||||
| `.pi/SYSTEM.md` | Replaces the system prompt for the project. |
|
||||
| `.pi/APPEND_SYSTEM.md` | Adds project-specific instructions to the system prompt. |
|
||||
| `.pi/extensions/` | Project extensions. |
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@
|
|||
{
|
||||
"title": "Use Pi Packages",
|
||||
"path": "packages.md"
|
||||
},
|
||||
{
|
||||
"title": "Connect MCP Servers",
|
||||
"path": "mcp.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ Automatic retries, recovery, compaction, or queued work can continue afterward.
|
|||
| Persist non-context session data | `pi.appendEntry()` |
|
||||
| Change active tools, model, or thinking level | Session control methods on `pi` |
|
||||
| Add a model provider | `pi.registerProvider()` |
|
||||
| Add an MCP server | `pi.registerMcpServer()` |
|
||||
| Route each request to a model | [`pi.registerVirtualModel()`](virtual-models.md) |
|
||||
| Add terminal rendering | Renderer registration and `ctx.ui` |
|
||||
| Communicate with another extension | `pi.events` |
|
||||
|
|
@ -142,14 +143,61 @@ Use sequential execution when tools share mutable in-memory state.
|
|||
File-mutating tools should wrap the complete read-modify-write operation with `withFileMutationQueue()`.
|
||||
Truncate large model-facing results and tell the model where to read the complete output.
|
||||
|
||||
Declare `outputSchema` and return a matching `structuredContent` when the result is data. The model still receives `content`; programmatic callers such as codemode scripts receive `structuredContent` instead of the text. Tools without `outputSchema` are passed to scripts as their text content. To report a failure that still carries data, return the result with `isError: true` instead of throwing: the model sees an error, and scripts still receive `structuredContent`.
|
||||
|
||||
A tool can run other tools with `ctx.executeTool(name, args, { signal, onUpdate })`. Nested calls go through argument validation and the `tool_call` and `tool_result` handlers like model-issued calls, and emit `tool_execution_start`, `tool_execution_update`, and `tool_execution_end`; all of these events carry `parentToolCallId`, and their `toolCallId` is assigned by pi as `<parent id>/<n>`. These ids do not appear as tool calls or tool results in the transcript. Nested calls do not add transcript entries: their results only reach the calling tool, which reports them itself, for example through `onUpdate` and `details`. The session keeps a bounded record of them (name, arguments, status, duration, error; never results) as `nestedCalls` on the calling tool's result message. It is used for compaction file lists and shown in HTML exports. Arguments over 8 KiB per call or 32 KiB per tool result are omitted, at most 256 calls are kept, and `complete: false` marks a record that lost anything. `ctx.tools` lists the tools `ctx.executeTool()` can call. `tool_result` handlers that redact `content` should also replace `structuredContent`; replacing only `content` drops it.
|
||||
|
||||
See [`hello.ts`](../examples/extensions/hello.ts), [`todo.ts`](../examples/extensions/todo.ts), [`dynamic-tools.ts`](../examples/extensions/dynamic-tools.ts), and [`truncated-tool.ts`](../examples/extensions/truncated-tool.ts).
|
||||
|
||||
### Tool exposure
|
||||
|
||||
`exposure` controls how the model reaches a tool. "Callable" means callable from other tools through `ctx.executeTool()` (`ctx.tools`), as the `codemode` tool's scripts do:
|
||||
|
||||
- `direct` (default): declared to the model while active, and callable while active.
|
||||
- `model-only`: declared to the model while active, never callable. Use it for tools that orchestrate other tools or ask the user.
|
||||
- `codemode`: callable whenever registered, and listed by the `codemode` tool. Not declared to the model unless activated explicitly.
|
||||
- `deferred`: like `codemode`, but codemode tools do not list it; `tool_search` can find and activate it.
|
||||
- `hidden`: registered but unreachable. Re-register a tool with `exposure: "hidden"` to withdraw it, since tools cannot be unregistered.
|
||||
|
||||
`namespace: { name, description }` groups related tools, as MCP servers do. Codemode tools list a namespace under one heading.
|
||||
|
||||
Registering a `direct` or `model-only` tool activates it; the other exposures are not activated on registration. The active set (`pi.getActiveTools()`, `pi.setActiveTools()`) is the set of tools declared to the model. `pi.getAllTools()` reports each tool's `exposure`, `namespace`, and `annotations`.
|
||||
|
||||
`annotations` are hints about what a tool does, with the meaning of MCP tool annotations: `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`. MCP tools carry the hints their server declares. Missing hints take the MCP defaults: a tool is not read-only, and may be destructive and reach an open world. The hints are not verified, but a permission extension can use them to decide which calls to confirm. This confirms the calls Codex asks approval for:
|
||||
|
||||
```typescript
|
||||
pi.on("tool_call", async (event, ctx) => {
|
||||
const hints = pi.getAllTools().find((tool) => tool.name === event.toolName)?.annotations;
|
||||
const needsApproval =
|
||||
hints?.destructiveHint === true ||
|
||||
(!hints?.readOnlyHint && ((hints?.destructiveHint ?? true) || (hints?.openWorldHint ?? true)));
|
||||
if (needsApproval && !(await ctx.ui.confirm("Allow tool call?", event.toolName))) {
|
||||
return { block: true, reason: `${event.toolName} was not approved` };
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
A tool that orchestrates other tools can adjust what the model sees while it is active with `prepareLoadout(loadout)`. It runs whenever the active tools change and receives the declared tools, the callable tools, and every registered tool with its exposure and namespace. It returns replacement `descriptions` for declared tools (including its own) and `hiddenDeclarations`: active tools whose declarations requests leave out while they stay active and callable. `codemode` and `tool_search` use only this hook, `exposure`, and `ctx.executeTool()`, so another tool can implement the same behavior under a different name.
|
||||
|
||||
### Activate tools dynamically
|
||||
|
||||
Register every tool first, keep optional tools inactive, and use `pi.setActiveTools()` from a loader tool to select the desired active tools. Names must already be registered; unknown names are ignored.
|
||||
|
||||
Pi records the initial prompt and tool set in the transcript's first system message, then appends tool and prompt changes before the next model request. Providers that cannot represent the transition receive a complete transcript checkpoint, which can invalidate the cached prefix.
|
||||
|
||||
### MCP servers
|
||||
|
||||
`pi.registerMcpServer(name, config)` adds an MCP server for the current session. `config` has the shape of an `mcpServers` entry in [`mcp.json`](mcp.md): `command`, `args`, `env`, and `cwd` for stdio servers, `url`, `headers`, and `oauth` for HTTP servers, plus `exposure`, `toolExposure`, `enabled`, and `timeout`.
|
||||
|
||||
```typescript
|
||||
pi.registerMcpServer("jira", { url: "https://mcp.example.com/jira", exposure: "codemode" });
|
||||
pi.unregisterMcpServer("jira");
|
||||
```
|
||||
|
||||
Servers registered while the extension loads connect when the session starts, together with the `mcp.json` servers; servers registered later connect right away, and `pi.unregisterMcpServer()` closes the connection and makes the server's tools unreachable. Registrations are not saved: register again on every load, for example based on the extension's own settings. A server in `mcp.json` with the same name takes precedence, and `/mcp` shows the override. Registering the same name again replaces the extension's earlier registration; names registered by another extension, invalid names, and invalid configs throw.
|
||||
|
||||
The built-in MCP support connects registered servers. When nothing does, because another extension replaced it (see [MCP](mcp.md#other-mcp-extensions)), each registration is reported as an extension error. Other MCP extensions can connect registered servers too: read them with `pi.getMcpServers()` on `session_start` and handle the `mcp_servers_change` event for later changes.
|
||||
|
||||
<a id="extensioncontext"></a>
|
||||
<a id="extensioncommandcontext"></a>
|
||||
<a id="use-extension-context"></a>
|
||||
|
|
|
|||
188
packages/coding-agent/docs/mcp.md
Normal file
188
packages/coding-agent/docs/mcp.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
# MCP Servers
|
||||
|
||||
Pi connects to [Model Context Protocol](https://modelcontextprotocol.io) servers over stdio or streamable HTTP and makes their tools available to the model.
|
||||
|
||||
## Configure servers
|
||||
|
||||
Add servers to `~/.pi/agent/mcp.json`, or to `.pi/mcp.json` in a project. The format matches other MCP clients, so existing `mcpServers` entries can be copied over:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
|
||||
},
|
||||
"docs": {
|
||||
"url": "https://example.com/mcp",
|
||||
"headers": { "Authorization": "Bearer ${DOCS_TOKEN}" },
|
||||
"exposure": "direct"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- stdio servers take `command`, `args`, `env`, and `cwd`. Relative `cwd` resolves against the session directory. A leading `~/` in `command`, an argument, or `cwd` names the home directory.
|
||||
- HTTP servers take `url`, `headers`, and `oauth` (see [Sign in with OAuth](#sign-in-with-oauth)). The legacy SSE transport is not supported.
|
||||
- `env` and `headers` values can reference environment variables (`${NAME}`) or commands (`!command`), like provider API keys.
|
||||
- `timeout` sets the per-request timeout in seconds (default 60). Progress notifications from the server reset it.
|
||||
- `enabled: false` keeps an entry without connecting to it.
|
||||
|
||||
Project entries replace global entries with the same name. A project `mcp.json` is only read after the project is trusted, because stdio servers run commands.
|
||||
|
||||
`pi mcp add` and `pi mcp remove` edit the file from a shell (see [MCP commands](cli.md#mcp-commands)):
|
||||
|
||||
```bash
|
||||
pi mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem .
|
||||
pi mcp add docs --url https://example.com/mcp --bearer-token-env-var DOCS_TOKEN --exposure direct
|
||||
pi mcp add -l tools --env API_KEY='${TOOLS_KEY}' -- uvx tools-mcp
|
||||
pi mcp remove docs
|
||||
```
|
||||
|
||||
Rules that are easy to get wrong:
|
||||
|
||||
- Server names may only contain letters, digits, `_`, and `-`. Tools are named `mcp__<server>__<tool>`.
|
||||
- `type` is optional: a `command` makes a stdio server and a `url` a streamable HTTP server. When present, it must be `stdio`, `http`, or `streamable-http`. `sse` is rejected; most servers that document an SSE endpoint also serve streamable HTTP, often at `/mcp` instead of `/sse`.
|
||||
- `command` is a single executable and `args` its arguments, not one shell string.
|
||||
- Keep secrets out of the file: use `${NAME}` for environment variables, as in `"Authorization": "Bearer ${GITHUB_TOKEN}"`, or `!command` to run a command. A command must make up the whole value, so it has to print the header value itself: `"Authorization": "!echo Bearer $(gh auth token)"`.
|
||||
- Invalid entries are skipped and reported; the other servers still connect.
|
||||
|
||||
## Set up servers
|
||||
|
||||
When asked to add an MCP server, the agent should:
|
||||
|
||||
1. Add simple servers with `pi mcp add` (add `-l` for the project file), or edit `mcp.json` directly for settings the command does not cover. Put personal servers and servers with credentials in `~/.pi/agent/mcp.json`. Use the project `.pi/mcp.json` only for servers the project itself needs, and only in trusted projects.
|
||||
2. Convert entries written for other clients:
|
||||
- Claude Desktop, Claude Code, and Cursor use the same `mcpServers` shape; copy the entry.
|
||||
- VS Code uses a top-level `servers` object and `inputs` prompts; move the entry under `mcpServers` and replace `${input:...}` with `${NAME}` environment variables.
|
||||
- Codex uses TOML (`[mcp_servers.<name>]` with `command`, `args`, `env`, or `url`); write the same fields as JSON.
|
||||
- opencode uses `"type": "local"` with `command` as an array (split it into `command` and `args`), `"type": "remote"` for URLs, `environment` for `env`, and `{env:NAME}` for `${NAME}`.
|
||||
3. Run `pi mcp list` to check the entry. It connects to every enabled server and prints the state, the tools, and errors such as the stderr of a stdio server that failed to start. It exits with 1 while anything is wrong.
|
||||
4. For a server that needs a sign-in, run `pi mcp login <server>`. It opens the authorization page in the user's browser and waits until the user approves access; tell the user to approve it. A running session uses the new credentials on its next turn.
|
||||
5. Tell the user to run `/reload` (or start a new session) so the running session connects to added or changed servers.
|
||||
|
||||
Pi connects when a session starts. The first prompt waits up to 10 seconds for startup connections; the tools of servers that take longer become available once they connect. HTTP connections that fail with a network error or a transient status (408, 429, 5xx) are retried twice. A server that drops its connection shows as disconnected and is reconnected on the next call. When a server announces that its tool list changed, new tools are added and withdrawn tools become unreachable until the server offers them again.
|
||||
|
||||
Config errors, servers that failed to connect, and servers that need a sign-in are reported once after startup.
|
||||
|
||||
Log messages servers send with MCP logging notifications are appended to `~/.pi/agent/mcp.log` as `<time> [<server>] <level> <logger>: <message>`. The file is moved to `mcp.log.1` when it grows past 5 MB.
|
||||
|
||||
## Manage servers
|
||||
|
||||
`/mcp` opens the server manager. It lists every configured server with its state, tool count, exposure, and whether it comes from the global or the project `mcp.json`; servers that need attention come first. Select a server to:
|
||||
|
||||
- sign in, for OAuth servers that need it (see [Sign in with OAuth](#sign-in-with-oauth))
|
||||
- see its tools, its command or URL, and the full connection error, including the tail of a stdio server's stderr
|
||||
- reconnect
|
||||
- sign out, which deletes the stored OAuth credentials
|
||||
- change its exposure (see [Exposure](#exposure))
|
||||
- disable or enable it
|
||||
|
||||
Exposure changes and enabling or disabling are saved to the `mcp.json` that defines the server; other content of the file is kept. Disabled servers stay listed so they can be enabled again.
|
||||
|
||||
Outside the interactive TUI, `/mcp` prints the server status. `/mcp login <server>`, `/mcp logout <server>`, and `/mcp reconnect <server>` run those actions directly.
|
||||
|
||||
From a shell, `pi mcp add`, `pi mcp remove`, `pi mcp list`, `pi mcp login <server>`, and `pi mcp logout <server>` manage servers without a session (see [MCP commands](cli.md#mcp-commands)).
|
||||
|
||||
Stopping a stdio server closes its stdin, then sends SIGTERM and finally SIGKILL to its whole process group, so servers started through wrappers such as `npx` or `uvx` do not linger.
|
||||
|
||||
## Sign in with OAuth
|
||||
|
||||
Remote servers that use OAuth, such as Sentry, need no credentials in `mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"sentry": { "url": "https://mcp.sentry.dev/mcp" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When such a server rejects the connection, `/mcp` shows it as needing sign-in. Select it and choose "Sign in" (or run `/mcp login sentry`, or `pi mcp login sentry` in a shell) to open the authorization page in your browser. After you approve access, the browser redirects to a temporary server on `127.0.0.1` and pi connects. If the browser runs on another machine, for example over SSH, paste the URL it was redirected to into the sign-in screen instead.
|
||||
|
||||
Pi registers itself with the authorization server (dynamic client registration), stores tokens in `~/.pi/agent/mcp-auth.json`, and refreshes access tokens automatically when they expire or the server rejects them. If the server later asks for more scope than was granted, it shows as needing sign-in again, and signing in requests the new scope. "Sign out" in `/mcp` (or `/mcp logout sentry`) deletes the stored credentials.
|
||||
|
||||
OAuth applies to HTTP servers without an `Authorization` header. For authorization servers that do not support dynamic client registration, configure a pre-registered client:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"example": {
|
||||
"url": "https://mcp.example.com/mcp",
|
||||
"oauth": { "clientId": "my-client", "clientSecret": "${EXAMPLE_SECRET}", "callbackPort": 8765 }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The redirect URI must match the one registered for the client. `callbackPort` fixes it to `http://127.0.0.1:<port>/callback`. For another redirect URI, set `callbackUrl`, for example `"callbackUrl": "http://localhost:8080/oauth/callback"`. It must be an `http` URI on `localhost`, `127.0.0.1`, or `[::1]`, and is sent exactly as written. Without a port in `callbackUrl`, pi listens on `callbackPort`, or on a free port, and adds it to the URI; authorization servers accept any port for loopback redirects (RFC 8252). `clientSecret` is optional and can reference environment variables or commands.
|
||||
|
||||
`scope` sets the scopes to request, separated by spaces, for servers that do not advertise the ones they need. Without it, pi requests the scopes the server advertises. When a server later asks for more scope, pi requests those on top of `scope`.
|
||||
|
||||
## Exposure
|
||||
|
||||
Each server's tools are registered as `mcp__<server>__<tool>`. The `exposure` setting controls how the model reaches them:
|
||||
|
||||
- `codemode` (default): the tools are callable from [`codemode`](cli.md#tools) scripts and listed in the `codemode` tool's description, but are not declared to the model. Large MCP tool lists stay out of the model's tool declarations, and scripts can call several MCP tools, in parallel if needed, while returning only the part of the result the model needs. Pi activates the `codemode` tool when such a server connects. Large servers do not fill the description: declarations share a token budget, and scripts find the remaining tools with `searchTools()` (see [`codemode`](cli.md#tools)).
|
||||
- `codemode-deferred`: like `codemode`, but the tools are not listed in the `codemode` tool's description either; it only names the server and its tool count. Scripts call them by name and find them with `searchTools()` or in `ALL_TOOLS`. Use it for large servers that codemode scripts use rarely.
|
||||
- `deferred`: the tools are not declared to the model until the [`tool_search`](cli.md#tools) tool loads them. The model searches, and the matches are declared from its next call on and called directly, without codemode. Pi activates the `tool_search` tool when such a server connects. Use it for large servers without codemode.
|
||||
- `direct`: the tools are declared to the model like built-in tools, and are also callable from codemode.
|
||||
- `hidden`: the tools are registered but cannot be called.
|
||||
|
||||
`toolExposure` sets the exposure of single tools and overrides `exposure` for them. Keys are tool names as the server offers them, or patterns where `*` matches any characters. An exact name wins over patterns; among patterns, the first match in the object wins. With `hidden` as the server's exposure, only the listed tools are reachable:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"url": "https://api.githubcopilot.com/mcp/",
|
||||
"exposure": "deferred",
|
||||
"toolExposure": {
|
||||
"search_code": "direct",
|
||||
"get_*": "codemode",
|
||||
"delete_*": "hidden"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`pi mcp list` marks tools whose exposure differs from the server's, and the Tools view in `/mcp` shows it too.
|
||||
|
||||
Tools that are not declared (`codemode`, `codemode-deferred`, and `deferred` exposure) are reachable through either tool: codemode scripts can call all of them, and `tool_search` can load any of them. For example, with `codemode` active, scripts can call the tools of a `deferred` server, and with `tool_search` active, the model can load the tools of a `codemode` server.
|
||||
|
||||
Tools called from codemode scripts do not depend on the active tool set, so they stay callable after `/tree`, resume, and fork. Tools loaded by `tool_search` are recorded in the transcript like any other tool change and stay declared on that branch. To keep pi from activating the `codemode` tool, set `"autoEnableCodemode": false` at the top level of `mcp.json`, next to `mcpServers`. A project `mcp.json` value overrides the global one. Pi warns once when neither `codemode` nor `tool_search` is active, since the tools then cannot be called.
|
||||
|
||||
Text results over 20KB reach the model with the middle cut out, in the format Codex uses: the start and end of the text around a `…N chars truncated…` marker. The full text is saved to a temp file whose path the result names. Codemode scripts always receive the whole result, so a script can filter a large result down to what the model needs.
|
||||
|
||||
Codemode scripts receive an MCP tool's whole `CallToolResult` (`content` blocks as sent by the server, `structuredContent`, and `isError`), and the `codemode` description declares it as `CallToolResult<T>`. 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 to the model. The server's `instructions` describe its tools in the `codemode` description.
|
||||
|
||||
## Resources
|
||||
|
||||
When a connected server offers [resources](https://modelcontextprotocol.io/specification/2025-11-25/server/resources), pi adds the resource tools Codex and opencode use:
|
||||
|
||||
- `list_mcp_resources` lists resources as JSON: `{ server?, resources: [{ server, uri, name, ... }], nextCursor? }`. With `server`, it lists one page of that server, and `cursor` continues with the next one. Without, it lists every resource of every server.
|
||||
- `list_mcp_resource_templates` lists URI templates for resources the servers do not list, in the same way.
|
||||
- `read_mcp_resource` reads a resource given `server` and `uri`. Text resources reach the model as text and images as images; other binary resources are saved to temp files, and the model sees the file path. Scripts receive `{ server, uri, contents }`.
|
||||
|
||||
The tools reach every enabled server with resources whose exposure is not `hidden`, and take the widest exposure among them: `direct` if one of the servers is direct, else `codemode`, else `codemode-deferred`, else `deferred`. Resource links in tool results name `read_mcp_resource` and the server.
|
||||
|
||||
Resources for MCP Apps (`ui://` URIs or `text/html;profile=mcp-app`) are left out of the listings, since pi does not render them, and so are resource icons.
|
||||
|
||||
Reading and listing resources is retried once after a transient HTTP error (408, 429, 5xx). Tool calls are not retried, since the server may have run them.
|
||||
|
||||
## Permissions
|
||||
|
||||
Every MCP call goes through pi's tool pipeline, so `tool_call` and `tool_result` extension handlers, including permission gates, apply to MCP tools. Calls made from codemode scripts carry the `codemode` call's id as `parentToolCallId`. `pi.getAllTools()` reports the tool annotations servers declare (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), so a permission extension can confirm only calls that change something (see [Extensions](extensions.md#tool-exposure)). The resource tools are marked read-only.
|
||||
|
||||
## Servers from extensions
|
||||
|
||||
Extensions can add servers for the current session with `pi.registerMcpServer(name, config)`, using the same config shape as `mcp.json` (see [Extensions](extensions.md#mcp-servers)). They connect like configured servers and appear in `/mcp` with the extension as their source. Enabling, disabling, and exposure changes for them apply to the current session only. A server in `mcp.json` with the same name takes precedence; `/mcp` lists the overridden registration. `pi mcp` shell commands do not load extensions and only see `mcp.json` servers.
|
||||
|
||||
## Other MCP extensions
|
||||
|
||||
An installed extension that registers the `/mcp` command, such as `pi-mcp-adapter`, replaces the built-in MCP support: pi then neither reads `mcp.json` in sessions nor connects servers, and `/mcp` belongs to that extension. Remove the extension to use the built-in support. Likewise, an extension that registers a tool named `codemode` or `tool_search` replaces the built-in tool of that name. `pi mcp` shell commands always use the built-in support.
|
||||
|
||||
## SDK
|
||||
|
||||
SDK sessions do not load the built-in extensions. Add the MCP extension, the codemode extension for `codemode` and `codemode-deferred` servers, and the tool search extension for `deferred` servers to the resource loader. See [SDK](sdk.md#codemode-mcp).
|
||||
|
|
@ -109,7 +109,11 @@ Use `DefaultResourceLoader` when you want standard discovery with selected overr
|
|||
|
||||
<a id="inlineextension"></a>
|
||||
|
||||
Inline extension factories can be supplied through `DefaultResourceLoader`. Give one an `InlineExtension` name only when it needs a stable name in diagnostics and startup output.
|
||||
Inline extension factories can be supplied through `DefaultResourceLoader`. Give one an `InlineExtension` name only when it needs a stable name in diagnostics and startup output. A named inline extension with `replaceable: true` is left out when another extension registers a tool, command, or flag with a name it registers during loading, instead of both loading with a conflict. The CLI's built-in codemode, tool search, and MCP extensions are replaceable.
|
||||
|
||||
<a id="codemode-mcp"></a>
|
||||
|
||||
The CLI loads `codemode`, `tool_search`, and MCP as built-in extensions. SDK sessions do not; add `createCodemodeExtension()`, `createToolSearchExtension()`, and `createMcpExtension()` to the `extensionFactories` of `DefaultResourceLoader`. `codemode` and `tool_search` are registered inactive: enable them through the `defaultTools` setting, or let the MCP extension activate them: `codemode` for servers with `codemode` or `codemode-deferred` exposure, `tool_search` for servers with `deferred` exposure. The MCP extension connects its servers on `session_start`, so call `session.bindExtensions()`. See [Codemode and MCP](../examples/sdk/14-codemode-mcp.ts).
|
||||
|
||||
See the focused examples for [models](../examples/sdk/02-custom-model.ts), [tools](../examples/sdk/05-tools.ts), [extensions](../examples/sdk/06-extensions.ts), and [full control](../examples/sdk/12-full-control.ts).
|
||||
|
||||
|
|
@ -130,6 +134,7 @@ See the focused examples for [models](../examples/sdk/02-custom-model.ts), [tool
|
|||
| [Sessions](../examples/sdk/11-sessions.ts) | Control session persistence and restoration |
|
||||
| [Full control](../examples/sdk/12-full-control.ts) | Replace default discovery and state services |
|
||||
| [Session runtime](../examples/sdk/13-session-runtime.ts) | Replace the active session safely |
|
||||
| [Codemode and MCP](../examples/sdk/14-codemode-mcp.ts) | Add the `codemode`, `tool_search`, and MCP extensions |
|
||||
|
||||
<a id="exports"></a>
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ Project trust does not limit what tool calls can access or affect. After Pi star
|
|||
Pi requires a project-trust decision when it finds any of these resources from the current working directory:
|
||||
|
||||
- `.pi/settings.json`
|
||||
- `.pi/mcp.json`
|
||||
- `.pi/extensions`, `.pi/skills`, `.pi/prompts`, or `.pi/themes`
|
||||
- `.pi/SYSTEM.md` or `.pi/APPEND_SYSTEM.md`
|
||||
- project `.agents/skills` in the current directory or an ancestor directory
|
||||
|
|
@ -46,6 +47,7 @@ A bare `.pi` directory does not require project trust.
|
|||
Granting project trust allows Pi to load:
|
||||
|
||||
- project settings
|
||||
- project MCP servers from `.pi/mcp.json`
|
||||
- extensions, skills, prompt templates, themes, and system-prompt files under `.pi`
|
||||
- missing packages configured through project settings
|
||||
- project-local and project-package extensions
|
||||
|
|
|
|||
|
|
@ -38,8 +38,10 @@ See [Choose a Model](models.md) for model selection and thinking controls.
|
|||
| Setting | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `defaultTools` | `string[]` | `read`, `bash`, `edit`, `write` | Built-in tools enabled at startup. An empty array disables all built-in tools but not extension or SDK tools. |
|
||||
| `codemode.mode` | `"on"` \| `"only"` | `"on"` | How the `codemode` tool presents tools while it is active. `on`: declared tools get their `codemode` declaration appended to their description, and `codemode` lists only tools that are not declared (MCP `codemode` exposure). `only`: `codemode` lists every tool scripts can call, and active built-in and extension tools are hidden from the model, so it reaches them through `codemode`. |
|
||||
| `codemode.inlineBudget` | number | `3000` | Estimated tokens (characters / 4) the `codemode` tool's description may spend on tool declarations. Tools that do not fit are left out and found with `searchTools()`. `0` lists only namespaces. |
|
||||
|
||||
Available built-in tools are `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and `ls`. CLI tool options override this setting for one invocation. See [Command Line](cli.md#tools).
|
||||
Available built-in tools are `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and `ls`. `defaultTools` can also name `codemode` and `tool_search`, which built-in extensions register inactive. CLI tool options override this setting for one invocation. See [Command Line](cli.md#tools).
|
||||
|
||||
## Sessions and context
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue