MCP and codemode
An agent becomes more useful when it can reach the systems where work happens: documentation, issue trackers, a database browser, or a local repository service. But “more tools” also means more descriptions in context, more credentials to protect, and more opportunities to make a consequential call. Pi addresses those concerns with two complementary mechanisms: MCP connects external servers, and codemode composes tool calls while returning only the useful result to the language model.
Use this lesson after Extensions and packages. An extension can register an MCP server or tool, but MCP is a standardized connection and tool surface. Do not confuse an MCP server's declaration with permission to use every capability it exposes.
MCP: connect a server, then inspect it
Pi connects to Model Context Protocol servers over stdio or streamable HTTP. A local server has a command and argument array; it is not a shell command string. A remote server has a URL. The legacy SSE transport is rejected.
For example, the CLI can create a user-level stdio entry, inspect it, and start Pi:
pi mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem .
pi mcp list
pi
A remote entry can be added with a URL and a bearer-token environment-variable reference:
pi mcp add docs --url https://example.com/mcp --bearer-token-env-var DOCS_TOKEN
Those are configuration operations, not tool invocations. User-level servers
live in ~/.pi/agent/mcp.json; project servers live in .pi/mcp.json and load
only after project trust. A project entry with the same name replaces the
user-level entry. A project entry that contains only enabled, exposure, or
toolExposure instead overrides only those properties while keeping the
user-level command, URL, environment, headers, and authentication.
That layering lets a trusted project disable a personal server without copying its secret configuration. Keep personal servers and servers with credentials in the user-level file. Review a project MCP configuration before trusting it: a stdio server is an executable program, and a remote server can expose actions to the model.
Inside Pi, /mcp lists server state, source, exposure, and tool count. It can
reconnect, enable or disable, and manage supported sign-in flows. Outside a
session, pi mcp list connects enabled servers and reports errors; it exits
nonzero if an entry is invalid or an enabled server does not connect. Run
/reload after changing a server outside an active session.
Exposure is context and safety design
MCP tools are named mcp__<server>__<tool>, but names alone do not decide how
much of the tool set appears in the prompt. A server's exposure determines the
route:
codemodeis the default: scripts can call the tools, while the model does not see every declaration.deferredmakes tools discoverable throughtool_searchfor direct use.directdeclares tools to the model and also permits codemode calls.hiddenleaves them registered but unreachable.
toolExposure can override that per server tool with exact names or patterns.
This keeps a small, frequent read-only tool direct while keeping a large or
consequential set discoverable on demand. Every MCP call still passes through
Pi's tool pipeline, including extension permission gates. Exposure makes a tool
reachable; it does not waive approval.
At session start, enabled servers connect in the background. Direct tools may hold the first prompt for up to ten seconds because their declarations must be ready. Other servers wait when a script or discovery mechanism needs them. A failed server is not a reason to keep retrying manually without diagnosis: inspect its status and the reported connection error.
Codemode: orchestrate tools, not an unrestricted shell
The codemode tool accepts raw JavaScript source. Pi runs it as an async
function body in a QuickJS sandbox, so top-level await and return work. The
sandbox has no Node APIs, filesystem, network, or timers. It reaches outward
only through Pi tools and the non-LLM models catalog.
That narrow environment is the point. The script can call several tools in
parallel, reduce a large response to a concise answer, and return only that
answer to the model. Tool output does not automatically flood the next model
turn. A script might search for callable tools, call independent read-only
queries with Promise.allSettled(), then return a short comparison. If a call
fails, Promise.allSettled() retains successful results instead of abandoning
them with the first rejection.
The key globals are tools, text(), image(), console, searchTools(),
describeTool(), describeNamespace(), ALL_TOOLS, and models. A callable
MCP tool name becomes a JavaScript identifier by replacing non-identifier
characters with underscores. For example, an MCP tool can be reached as
tools.mcp__dev_radius__search(...) when its name is callable in the current
session.
A script can begin with one options line to bound its output tokens or impose a whole-script deadline:
// @options: {"max_output_tokens": 800, "timeout_ms": 60000}
const matches = await Promise.allSettled([
tools.grep({ pattern: "TODO", path: "src" }),
tools.grep({ pattern: "FIXME", path: "src" }),
]);
return matches.map((result) => result.status);
The example teaches composition, not a claim that every session has the same
grep arguments. Read describeTool() for the installed declaration before
writing a real script.
Tool effects are real. A script failure does not undo tool calls that already
ran; unfinished calls are cancelled when the script ends. Use scripts first for
read and aggregation work, and keep writes narrow and deliberately authorized.
The optional store() and load() preserve small JSON values across successful
codemode calls on a session branch; they are for IDs, cursors, and summaries,
not image data or a substitute for durable project records.
Codemode can also run available classifier and image models, but not chat models. Their usage contributes to the codemode result and session cost. Query the available catalog rather than assuming a provider/model ID works with the current credentials.
Common mistakes
- Pasting secrets into a project
mcp.jsoninstead of keeping them personal. - Treating an MCP command field as a shell string.
- Making every large server direct and paying for tool descriptions on each turn.
- Assuming a sandboxed script rolls back completed tool calls after an error.
- Printing a huge tool result instead of returning a useful filtered summary.
Try it
Run pi mcp list and inspect server state without changing configuration. In an
interactive session, open /mcp and compare a server's exposure with its tool
count. Then describe a read-only investigation that would benefit from one
codemode script: the independent calls it would make, the small result it would
return, and the approval boundary it would preserve.