pi-delegate-mcp

pi-delegate-mcp

Enables delegating coding tasks to a pi agent as a steerable background worker, allowing mid-run redirection, follow-ups, and keeping the delegate's context isolated from your main conversation.

Category
Visit Server

README

██████╗ ██╗    ██████╗ ███████╗██╗     ███████╗ ██████╗  █████╗ ████████╗███████╗
██╔══██╗██║    ██╔══██╗██╔════╝██║     ██╔════╝██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝
██████╔╝██║    ██║  ██║█████╗  ██║     █████╗  ██║  ███╗███████║   ██║   █████╗
██╔═══╝ ██║    ██║  ██║██╔══╝  ██║     ██╔══╝  ██║   ██║██╔══██║   ██║   ██╔══╝
██║     ██║    ██████╔╝███████╗███████╗███████╗╚██████╔╝██║  ██║   ██║   ███████╗
╚═╝     ╚═╝    ╚═════╝ ╚══════╝╚══════╝╚══════╝ ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ╚══════╝
                            ███╗   ███╗ ██████╗██████╗
                            ████╗ ████║██╔════╝██╔══██╗
                            ██╔████╔██║██║     ██████╔╝
                            ██║╚██╔╝██║██║     ██╔═══╝
                            ██║ ╚═╝ ██║╚██████╗██║
                            ╚═╝     ╚═╝ ╚═════╝╚═╝

npm node license

MCP server that exposes the pi coding agent as a delegable, steerable worker.

Point Claude Code (or any MCP host) at it and delegate work to any of pi's ~38 providers (DeepSeek, Grok, GLM, Kimi, Qwen, Codex, OpenRouter, local llama.cpp) with the sub-agent's context staying out of your main conversation.

What it's for

Your main harness runs on an expensive model, with a context window you care about. A lot of what it does doesn't need that model, and actively damages that context: grepping a repo for every call site, reading a 2000-line file to answer one question, auditing what a refactor left behind.

Hand that work to a delegate instead:

  • Cost. The grunt work runs on DeepSeek, GLM, Kimi, Qwen, or a local llama.cpp. You pay frontier prices only for the reasoning that actually needs them.
  • Context. The delegate reads the files on its own budget and returns a result. The 200 KB it read never enters your conversation.
  • Blast radius. Delegates are read-only by default (read, grep, find, ls), enforced at session construction. A cheap model doing exploratory work cannot touch your tree unless you opt it in.

The delegate is always the pi agent. Codex, Grok, DeepSeek and the rest supply the model behind it; this is not a wrapper around their CLIs.

Why pi, and not opencode or a CLI wrapper?

A delegate is only steerable if two channels stay open: you must be able to redirect it mid-task, and it must be able to ask you something and block until you answer. Most ways of driving a coding agent from another program close both.

pi -p / CLI wrappers opencode SDK this server
Runs in-process no (subprocess) no (HTTP client to opencode serve) yes (createAgentSession)
Redirect a running turn no abort only steer
Agent can ask you something no (ctx.hasUI false) not in the session API statusanswer *
Model per call no yes model argument

pi -p and --mode json set ctx.hasUI = false. A delegate started that way is fire-and-forget by construction: it cannot raise a question, and you cannot redirect it.

opencode's SDK is a typed client for a separate server process: createOpencode() boots opencode serve and talks HTTP to it. Clean design, but it means a second process to supervise, and the session surface it exposes (prompt, abort, revert, messages) has no mid-turn steering and no path for the agent to ask the caller anything.

pi ships createAgentSession as an embeddable library. This server holds the session object in-process, so session.steer() can land a message after the current tool call and before the next model call, and a synthetic uiContext catches the agent's questions and parks them for answer. Nothing is shelled out; nothing has to be supervised.

* Questions come from pi extensions, so that channel is open only for delegates spawned with extensions: true. See Web search and other extension tools.

(The table compares the delegation channel, not sandboxing; opencode has its own permission config. See Read-only by default for what this server does and does not enforce.)

Tools

Tool Purpose
init Call first. Reports reachable models, permitted tools, and how to drive a delegate. Every other tool refuses until it has run once.
spawn Delegate in the background. Returns sessionId immediately. Use this by default.
spawn_batch Fan out up to 10 delegates in one call. Validated as a batch, so nothing starts if one task is bad.
run Delegate and block until done. For quick questions only.
status State, turns, tools used, latest text, and pending questions.
steer Redirect a running agent. Lands after its current tool call.
follow_up Give a finished delegate another turn. It keeps everything it read, so you do not re-explain the task.
answer Answer a question surfaced by status. Only reachable with extensions: true, since only extensions can ask.
abort Stop a session; partial output stays readable.
models List models this delegate may use.
sessions List sessions, running and finished. Filter by state, expand with verbose.
forget Drop a finished session from history, freeing its id.

Install

Requires Node.js 22.19+ and a working pi install that has been logged in once (pi, then /login).

Claude Code

claude mcp add pi -e PI_DELEGATE_MODEL=openrouter/stealth/ox-alpha -- npx -y pi-delegate-mcp

Any MCP host, via .mcp.json

{
  "mcpServers": {
    "pi": {
      "command": "npx",
      "args": ["-y", "pi-delegate-mcp"],
      "env": { "PI_DELEGATE_MODEL": "openrouter/stealth/ox-alpha" },
      "timeout": 1800000
    }
  }
}

npx resolves the package on every launch. To pin it, install globally and call the binary directly:

npm install -g pi-delegate-mcp
{ "mcpServers": { "pi": { "command": "pi-delegate-mcp", "timeout": 1800000 } } }

Keep the server key short, since it prefixes every tool name (mcp__pi__spawn).

From source

git clone https://github.com/howznguyen/pi-delegate-mcp && cd pi-delegate-mcp
npm install && npm run build && npm link

First run

Ask your agent to delegate something. It calls init once to learn what this server can reach, then spawn:

{ "id": "audit-01", "label": "who still imports onnxruntime",
  "prompt": "Search this repo for anything still importing onnxruntime and list the files.",
  "cwd": "/path/to/repo" }
{ "sessionId": "audit-01", "state": "running", "model": "opencode-go/deepseek-v4-flash",
  "activeTools": ["read", "grep", "find", "ls"] }

spawn returns immediately. Poll with status for the ordered tool trace and the answer, or sessions when several are in flight. If init fails, it says exactly what is missing: pi not installed, no provider logged in, or a model scope that matches nothing.

Model names in the examples below are illustrative. Run models to see what your own pi install can actually reach.

Traceability

spawn and run both accept your own id and a free-text label:

{
  "id": "search-audit-01",
  "label": "what ONNX removal left behind",
  "prompt": "...",
  "model": "opencode-go/deepseek-v4-flash"
}

Ids are [A-Za-z0-9._:-], 1-64 chars, must start alphanumeric, and must be unique among live sessions. Omit for a UUID.

Finished sessions stay readable via status and sessions instead of vanishing, so you can go back and check what a delegate actually did. The newest PI_DELEGATE_HISTORY (default 50) are kept; forget drops one early.

status returns an ordered toolCalls trace: every tool the delegate ran, with arguments and timing. Add verbose: true for call ids and results:

{
  "seq": 1,
  "id": "call_467b4bb4…",
  "name": "bash",
  "state": "ok",
  "ms": 10,
  "args": "{\"command\":\"echo hello-trace\"}",
  "result": "hello-trace\n"
}

Arguments and results are clipped (PI_DELEGATE_TRACE_ARGS, PI_DELEGATE_TRACE_RESULT) with the dropped length recorded, so one read of a large file cannot flood your context.

Giving a delegate another turn

A finished delegate is not spent. pi keeps its session in memory, so follow_up re-prompts the same agent with everything it already read still in context:

{ "sessionId": "search-audit-01", "prompt": "Now check whether the build files reference it too" }
{ "sessionId": "search-audit-01", "state": "running", "turnsSoFar": 1 }

The delegate picks up where it left off. It still holds the files it read on the first turn, so the second question costs one model call rather than a fresh session re-reading the repository.

This is the cheap way to have a conversation with a delegate. Spawning a fresh one means re-explaining the task and paying for it to re-read the same files, and its answer arrives with none of the reasoning that led there.

follow_up refuses a delegate that is still working, because redirecting one mid-task is what steer is for. The two are not interchangeable: steer lands between tool calls on a running agent, follow_up starts a new turn on a finished one.

Fanning out

spawn_batch starts a whole batch in one call. Tasks inherit the batch-level model, cwd, tools and extensions, and override them individually where they need to:

{
  "idPrefix": "audit",
  "model": "opencode-go/deepseek-v4-flash",
  "cwd": "/repo",
  "tools": ["ls"],
  "tasks": [
    { "prompt": "What still imports onnxruntime?", "label": "imports" },
    { "prompt": "Which build files still reference ONNX?", "label": "build" },
    {
      "prompt": "Any ONNX model files left on disk?",
      "label": "artifacts",
      "model": "opencode-go/ox-alpha-free"
    }
  ]
}

That names them audit-01, audit-02, audit-03 and returns in a few milliseconds, since launching a delegate does not wait for it to think.

The batch is validated before anything starts: id format, ids duplicated inside the batch, ids already live, blocked tools, and every model name. One bad task fails the call and launches nothing. Half a fan-out is the worst outcome, because you pay for the delegates that did start and still have to work out which ones did not.

Poll the whole batch with one sessions call rather than one status per delegate. Drop to status only for the delegate you actually want to read. steer and abort stay per session.

Picking a model per call

model on any call overrides PI_DELEGATE_MODEL. An unresolvable name is a hard error, never a silent fallback to the default model, because a silent fallback is how you end up billing a model you never asked for.

Which names resolve is decided by pi's own enabledModels scope, which this server enforces rather than merely displays:

opencode-go/deepseek-v4-flash  -> ok      (listed in enabledModels)
opencode-go/glm-5.3            -> refused (out of scope)
knowns-hub/claude-opus         -> ok      (custom provider, see below)

Custom providers bypass the scope. Any model served by a provider declared in ~/.pi/agent/models.json is offered even when enabledModels does not name it, on the grounds that declaring a provider by hand is already an intent to use it. This is why the list can be much longer than enabledModels: three entries in the scope plus two custom providers can easily mean fifteen offered models. init says so explicitly in models.scopeNote when it applies.

Two switches change that:

Effect
PI_DELEGATE_STRICT_SCOPE=1 Honour enabledModels exactly. The custom-provider bypass is dropped.
PI_DELEGATE_IGNORE_SCOPE=1 Drop scoping altogether. Every authenticated model is usable.

Call models to see what is actually reachable under whichever setting is in force.

Status line

Claude Code allows exactly one statusLine command, so pi-delegate-statusline wraps whatever you already run and appends a segment showing this workspace's delegates:

{
  "statusLine": {
    "type": "command",
    "command": "PI_DELEGATE_STATUSLINE_WRAP=ccstatusline pi-delegate-statusline",
    "refreshInterval": 10
  }
}

Drop PI_DELEGATE_STATUSLINE_WRAP to print the pi segment alone.

π ▸ audit engine·t1·12s audit index·t2·8s   running, with turn counts and elapsed time
π ▸ migrate·t7·3m04s ?1 waiting             one delegate is blocked on a question
π ✓2                                        finished, nothing running

Which delegates belong to which session

Filtering by directory is not enough: two Claude Code sessions open on the same repository would show each other's delegates. Attribution uses process lineage instead.

The MCP host spawns one server per session, so the server records process.ppid, the host's pid. The status line, spawned by that same host, walks its own ancestry and keeps only the state files whose hostPid it finds there. Same repo, two sessions, no crosstalk. The directory filter remains as a fallback for state files written before this existed.

State lives in $XDG_STATE_HOME/pi-delegate-mcp/<pid>.json (PI_DELEGATE_STATE_DIR to relocate). Files are pruned when their process is gone, ESRCH only, since EPERM means the process is alive under another user. Servers also exit on their own when stdin closes or the host pid disappears, so a host that dies without closing the transport leaves nothing behind.

Read-only by default

Tools are locked to read, grep, find, ls at session construction. Anything else is refused before a session is even created.

To widen that, name the extra tools on the server:

"env": { "PI_DELEGATE_ALLOW_TOOLS": "bash" }

or PI_DELEGATE_ALLOW_WRITE=1 to permit everything.

bash is not a middle ground. pi ships no permission system, so a delegate holding bash can write files, delete them, and reach the network regardless of whether write and edit are on its list. Refusing those two while allowing bash records your intent; it does not enforce anything. Claude Code's permission prompts and hooks never see what pi does. If you need a real boundary, run this server inside a container.

Web search and other extension tools

pi's own tools are read, grep, find, ls, bash, powershell, write, edit. There is no search and no fetch among them. Those come from pi extensions, which register their own tools, and a delegate can use them.

Set extensions: true on the call and permit the tool names on the server:

"env": { "PI_DELEGATE_ALLOW_TOOLS": "web_search,fetch_content" }
{ "prompt": "Find the current Node LTS version and tell me just the number",
  "extensions": true, "tools": ["read", "grep", "find", "ls", "web_search"] }
{ "seq": 1, "name": "web_search", "state": "ok", "ms": 2568,
  "args": "{\"query\":\"latest stable Node.js LTS version\",\"numResults\":5}" }

This is how you give a delegate network reach without handing it bash. web_search can search and nothing else, and it passes through the same allowlist as every other tool, so the read-only default is unchanged for calls that do not ask for it.

Which tools exist depends on what the user running the server has installed. pi-web-access provides web_search, fetch_content, source_check and get_search_content. pi-mcp-adapter bridges the MCP servers in ~/.pi/agent/mcp.json and exposes them as mcp. pi has no MCP client of its own, so that extension is the only route to one.

extensions: true trusts every installed extension, not just the one you wanted. They load as a set, they run with the full privileges of this server's process, and some open sockets and timers that outlive the session. Turn it on per call, for the delegates that need it, rather than leaving it on by default. It also costs real startup time, which is why it is off unless asked for.

Configuration

Env var Default Meaning
PI_DELEGATE_MODEL pi's own default Model used when a call omits model
PI_DELEGATE_ALLOW_TOOLS unset Comma list of extra tools to permit, e.g. bash
PI_DELEGATE_ALLOW_WRITE unset 1 permits every tool
PI_DELEGATE_HISTORY 50 Finished sessions kept for review
PI_DELEGATE_TRACE_ARGS 400 Max chars of tool arguments kept in the trace
PI_DELEGATE_TRACE_RESULT 600 Max chars of tool results kept in the trace
PI_DELEGATE_BATCH_MAX 10 Ceiling on tasks per spawn_batch call
PI_DELEGATE_LIST_CAP 60 Above this, init summarises models by provider instead of listing them
PI_DELEGATE_STATE_DIR XDG state dir Where status-line state is published
PI_DELEGATE_STATUSLINE_WRAP unset Status line command to wrap and append to
PI_DELEGATE_STATUSLINE_LOG unset File to append a timestamp to on every status line render, for debugging
PI_DELEGATE_PROGRESS_MS 15000 Progress notification interval during run
PI_DELEGATE_IGNORE_SCOPE unset 1 ignores pi's enabledModels scope, allowing any configured model
PI_DELEGATE_STRICT_SCOPE unset 1 honours enabledModels exactly, dropping the custom-provider bypass
PI_CODING_AGENT_DIR ~/.pi/agent Where pi's auth.json and config are read from

Long-running work

The MCP TypeScript SDK defaults to a 60 second request timeout, which a real task will blow through. Three defences, in order of preference:

  1. Use spawn + status. Nothing blocks, so no timeout applies.
  2. run emits periodic progress notifications, which reset the host's timeout.
  3. Raise the ceiling with "timeout" in .mcp.json or MCP_TOOL_TIMEOUT in the environment.

CLAUDE_AUTO_BACKGROUND_TASKS=1 makes Claude Code background long MCP calls after ~2 minutes. Note that progress notifications are discarded once a call is backgrounded, so pick (1) or (3), not both.

Auth

The server does not handle credentials. pi authenticates itself from ~/.pi/agent/auth.json, then environment variables. MCP hosts often launch servers with a stripped environment, so prefer auth.json (run pi once and /login) over exporting keys in a shell profile.

Development

npm install
npm run build       # tsc, src/*.ts -> dist/
npm run typecheck   # tsc --noEmit, strict
npm run test:ci     # offline: boots the server over stdio and lists its tools
npm test            # full suite: needs a logged-in pi, makes real model calls

test:ci is what CI runs and what prepublishOnly gates on, because it needs no credentials and no network. npm test drives real delegates against real providers, so it costs money and only works where pi has been logged in.

Path What lives there
src/config.ts Every environment variable, read in one place
src/permissions.ts The tool allowlist and the gate that enforces it
src/registry.ts Session map, id claiming, history eviction
src/tools/ One module per group of MCP tools
src/pi/ Everything that touches the pi SDK
src/statusline/ State file publishing and the status line binary

Releases are tag-driven. npm version patch && git push --follow-tags runs the build and tests, then publishes over OIDC trusted publishing, so no npm token is stored anywhere in the repository.

Issues and pull requests are welcome. If you are reporting a delegate that misbehaved, the toolCalls trace from status with verbose: true is the useful thing to attach.

Prior art

abatilo/pi-mcp-bridge takes the simpler route: spawn pi --mode json -p --session-id <uuid> and let pi persist sessions on disk, so the bridge holds no state at all. Elegant, and worth reading. It trades away steering, questions, and tool control to get there.

License

MIT

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured