Skip to main content
Glama

pi-mcp

MCP server for running and supervising pi coding agents over RPC. The design keeps a pi process alive between turns (hot resume); an explicit resurrect cold-resumes a ripped or exited agent from its session file.

Status: SQLite-backed agents, bwrap-caged pi --mode rpc processes, normalized mounts, idle keep-alive, logs, chained agents, callback settle notifications, per-child token/MCP config patching, per-agent context budgets that trigger pi autocompaction, rip/resurrect with lifecycle counters, child mounts scoped to their parent agent, background agent refresh, and focused runner, store, artifact, and server modules.

Quick start

uv sync --extra dev
PI_MCP_DB=/tmp/pi-mcp.sqlite3 \
  PI_MCP_AGENTS_DIR=/tmp/pi-mcp/agents \
  PI_MCP_ACCESS_TOKEN=alpha \
  uv run pi-mcp

run needs pi and bubblewrap (bwrap) on the host. pi-mcp-fake can still seed fake sessions. stdio is the default transport. For HTTP:

uv run pi-mcp --transport streamable-http --host 0.0.0.0 --port 8000

Current tools:

Tool

Arguments

Returns

list

none

Agents visible to the caller's token

get_info

agent_id

Metadata, public state, last assistant text

run

provider?, model?, context_size?, mounts, callbacks/tokens

Starts a caged pi RPC agent and waits for handshake

rip

agent_id

Stops the process but keeps session and properties

resurrect

agent_id, prompt?

Cold-resumes with stored properties and optional turn

send_message

agent_id, message, mode?

Prompts an idle agent or queues for a running one

remove

agent_id

Stops the process and deletes its artifact directory

grep

agent_id, args[], stream?

Proxy the real host grep against a log stream

list_tmp

agent_id, path?, recursive?, max_entries?

List forwarded /tmp files with type, size, mtime

grep_tmp

agent_id, args[], cwd?, path?

Proxy the real host grep in the agent's /tmp

read_tmp

agent_id, path, ranges/caps

Read a file by line or byte range

read

agent_id, stream, ranges/caps

Read the stdout or errors log

See Tool reference for full arguments and response examples.

The main transport process is bound to one PI_MCP_ACCESS_TOKEN; per-request HTTP auth for that transport is not implemented yet. The generated child MCP endpoint does use per-request bearer authentication.

Related MCP server: pi-agent-mcp

Chaining agents

Every caged agent receives AGENT_ID in its environment. To have a child report back, call run(..., callback_agent_id=AGENT_ID) from inside the parent. When the child settles, pi-mcp prompts the parent with the child's agent_id, last text, error/exit code, session, and artifact paths. The parent can then use list_tmp, read, grep, get_info, and read_tmp on that id.

Agents get a generated /tmp/pi-agent/mcp.json that points at a loopback pi-mcp endpoint, so a caged agent can call the same server. The config carries a bearer token bound to that agent. When an agent starts a child, the child's write/read/denied rules are narrowed to the parent's mounts: it cannot request paths outside the parent's grants, cannot enter a parent-denied path, and inherits parent read-only overrides and denials. Each child's stored rules are already scoped, so validation is transitive without walking the parent chain. A parent with no mount grants cannot give a child new mounts. Callback validation happens before the child starts; unknown ids abort without creating artifacts. Callback delivery may cross groups by id, but artifact tools remain group-scoped. Set PI_MCP_CHILD_MCP=0 to disable the child endpoint and generated config.

Token patching

Pass run(access_token="...") to put a child in another token group. A known token joins its group; an unknown token creates one. Only that child's private mcp.json is patched, with a new server-minted token bound to the child agent; the explicit group token is never copied into the config. Global and project pi MCP config are never rewritten, and the minted token is mode 0600 in the child config, is not returned by tools, and is not stored in SQLite.

The loopback endpoint accepts only tokens minted for an agent; arbitrary bearer values and raw group tokens are rejected. Tokens are revoked when the agent exits, is ripped, or is removed.

Without access_token, the child stays in the caller's group and receives an ephemeral capability token. This keeps chained parents able to inspect their children. With an explicit token, the parent cannot see the child through normal group-scoped tools; use callback_agent_id for the result.

Context limits

Pass run(context_size=50000) to give an agent a lower pi context trigger. context_size has a 20,000-token minimum. pi-mcp writes a private pi-agent/models.json with contextWindow = context_size + headroom, where headroom = max(context_size // 10, 10000). It writes reserveTokens equal to the same headroom, so autocompaction starts when the projected context exceeds context_size. The headroom is room for the response and summary call; without it pi can clamp the summary to one token. The summary keeps 10% of context_size as recent tokens; this is hardcoded and not configurable. The setting is shown as context_size by list and get_info.

The budget is an accounting override, not the provider's real window. The server only requires an integer of at least 20,000; it does not resolve provider catalogs. Keep context_size + headroom + model.maxTokens below the model's actual window. If the resulting window is at or above the real one, compaction turns into overflow recovery. Without context_size, pi's own model window and defaults apply.

Agent states

list, get_info, rip, and resurrect expose one public state:

  • active — the process is live and a turn is running.

  • idle — the process is live and settled.

  • ripped — the process was explicitly stopped; the session is resumable.

  • exited — the process ended on its own; the session is resumable.

A background thread refreshes each live agent with a configurable pause. The default is one second, so a full lap takes pause × live agents. Configure it at server start with --refresh-pause or PI_MCP_REFRESH_PAUSE; 0 disables the background thread. Natural exits are marked exited and their child tokens are revoked during this lap. rip is immediate and marks the agent ripped.

Rip and resurrect

rip(agent_id) closes the pi process but keeps the agent record, session file, logs, forwarded /tmp, and original spawn settings. Its state becomes ripped. It is idempotent; a second rip does not increment the counter.

resurrect(agent_id, prompt=...) starts a new process for the same agent id and session using the stored provider, model, context_size, mount rules, token group, and callback target. Those properties cannot be overridden. If prompt is supplied, the resurrected agent starts that turn immediately; otherwise it returns idle.

get_info and list expose lifecycle counters:

{
  "lifecycle": {
    "retries": 1,
    "rips": 1,
    "self_exits": 1,
    "ripped_at": "2026-11-01T12:00:00Z",
    "resurrected_at": "2026-11-01T12:05:00Z"
  }
}
  • rips counts successful explicit rip transitions.

  • retries counts successful resurrect calls.

  • self_exits counts processes that exited without being ripped, removed, or shut down with the server. This is the counter for “the agent exited on its own.”

send_message still requires a running process; automatic cold resume is not implemented. Call resurrect first; the state returns to idle or active. A second prompt after resurrection reuses the new process, just like a hot prompt.

Agent artifacts

PI_MCP_AGENTS_DIR (or --agents-dir) selects the artifact root. It defaults to <db-parent>/agents. Each agent gets one directory:

$PI_MCP_AGENTS_DIR/<agent_id>/
  stdout.log
  errors.log
  agent_session.jsonl
  tmp/                 # bind-mounted at /tmp inside the cage

The session file is mounted read-write at /agent_session.jsonl. tmp/ is mounted read-write at /tmp, so files the agent writes there are forwarded to the host. Use list_tmp to inspect the tree, grep_tmp to proxy the host grep, and read_tmp to read or download files. read selects the stdout or errors stream. Reads default to lines_from=0 with max_bytes=1024; line ranges are 0-based and end-exclusive. Use bytes_from/bytes_to instead for exact byte ranges. Long lines over 10k chars are truncated with ...<truncated N chars>, warnings, and byte offsets in truncated_regions. Binary data is returned as base64. grep proxies the host binary against the selected log path. Read/list paths are scoped to that agent's tmp/, and traversal or symlinks escaping it are rejected. The directory survives process exit; remove deletes it. get_info returns all paths under artifacts.

Tool reference

All successful tool results are returned in MCP structuredContent. IDs, paths, byte counts, and timestamps in the examples are illustrative. Tool failures are returned as MCP tool errors with isError: true.

list

List all sessions visible to the caller's token group.

Argument

Type

Required

Default

Notes

—

—

—

—

No arguments.

{
  "count": 1,
  "agents": [
    {
      "agent_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
      "name": "deepseek/deepseek-flash",
      "state": "idle",
      "provider": "deepseek",
      "model": "deepseek-flash",
      "context_size": 50000,
      "lifecycle": {
        "retries": 0,
        "rips": 0,
        "self_exits": 0,
        "ripped_at": null,
        "resurrected_at": null
      },
      "messages": 2,
      "tokens": {
        "input": 120,
        "output": 45,
        "total": 165
      },
      "logs": {
        "stdout_bytes": 1234,
        "stderr_bytes": 0
      },
      "updated_at": "2026-11-01T08:46:51Z"
    }
  ]
}

In list, messages is an integer total. In get_info, it is an object.

get_info

Get complete metadata for one agent.

Argument

Type

Required

Default

Notes

agent_id

string

yes

—

Agent UUID.

{
  "agent_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
  "name": "deepseek/deepseek-flash",
  "state": "idle",
  "provider": "deepseek",
  "model": "deepseek-flash",
  "context_size": 50000,
  "mounts": {
    "write": ["/home/user/project"],
    "read": ["/home/user/data"],
    "denied": []
  },
  "cwd": "/tmp",
  "session_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
  "pid": 4242,
  "created_at": "2026-11-01T07:12:51Z",
  "updated_at": "2026-11-01T08:46:51Z",
  "last_activity_at": "2026-11-01T08:46:51Z",
  "messages": {
    "total": 2,
    "user": 1,
    "assistant": 1,
    "tool_calls": 0
  },
  "tokens": {
    "input": 120,
    "output": 45,
    "total": 165
  },
  "session_bytes": 149799,
  "logs": {
    "stdout_bytes": 40922,
    "stderr_bytes": 0
  },
  "queue_length": 0,
  "last_error": null,
  "exit_code": null,
  "lifecycle": {
    "retries": 0,
    "rips": 0,
    "self_exits": 0,
    "ripped_at": null,
    "resurrected_at": null
  },
  "callback_agent_id": null,
  "callback": {
    "agent_id": null,
    "settle_count": 0,
    "notified_seq": 0,
    "notified_at": null,
    "error": null
  },
  "artifacts": {
    "dir": "/home/user/.local/share/pi-mcp/agents/01a1964b-2c01-70a5-b520-78f933cdafad",
    "stdout_log": "/home/user/.local/share/pi-mcp/agents/01a1964b-2c01-70a5-b520-78f933cdafad/stdout.log",
    "error_log": "/home/user/.local/share/pi-mcp/agents/01a1964b-2c01-70a5-b520-78f933cdafad/errors.log",
    "session": "/home/user/.local/share/pi-mcp/agents/01a1964b-2c01-70a5-b520-78f933cdafad/agent_session.jsonl",
    "tmp": "/home/user/.local/share/pi-mcp/agents/01a1964b-2c01-70a5-b520-78f933cdafad/tmp"
  },
  "last_assistant_text": "Done. I updated the parser."
}

state is one of active, idle, ripped, or exited. pid is null when no process is live.

run

Spawn a caged pi agent and return after its idle handshake. It waits for send_message.

Argument

Type

Required

Default

Notes

provider

string

no

$PI_MCP_PROVIDER or deepseek

Provider name.

model

string

no

$PI_MCP_MODEL or deepseek-flash

Model name.

write

string[]

no

[]

Absolute host paths mounted writable.

read

string[]

no

[]

Absolute host paths mounted read-only.

denied

string[]

no

[]

Absolute host paths explicitly denied.

callback_agent_id

string

no

null

Existing agent to prompt when this child settles.

access_token

string

no

null

Starts the child in another token group and patches its MCP config.

context_size

integer

no

null

Must be at least 20000. Lowers the child's context budget.

Response: same AgentInfo shape as get_info. A newly started agent has "state": "idle" and "last_assistant_text": null.

rip

Stop the process while keeping the record, session, logs, forwarded /tmp, and spawn settings. Idempotent.

Argument

Type

Required

Default

Notes

agent_id

string

yes

—

Agent UUID.

Returns full AgentInfo plus:

{
  "ripped": true,
  "already_ripped": false
}

state becomes "ripped", pid becomes null, and lifecycle.rips increments.

resurrect

Restart a ripped or exited agent using its stored settings.

Argument

Type

Required

Default

Notes

agent_id

string

yes

—

Agent UUID.

prompt

string

no

null

If present, immediately starts a turn. Must not be empty.

Returns full AgentInfo plus:

{
  "resurrected": true,
  "prompt_sent": true,
  "delivery": "resurrect_prompt",
  "disposition": "started"
}

Without prompt, prompt_sent is false, and delivery / disposition are null.

send_message

Send a message to a live agent.

Argument

Type

Required

Default

Notes

agent_id

string

yes

—

Agent UUID.

message

string

yes

—

Message text.

mode

"follow_up" or "steer"

no

"follow_up"

Used when the agent is already running.

{
  "agent_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
  "delivery": "hot_prompt",
  "disposition": "started",
  "state": "active"
}

Possible deliveries: hot_prompt, hot_follow_up, hot_steer.

remove

Stop the agent if running, then delete its record and artifacts.

Argument

Type

Required

Default

Notes

agent_id

string

yes

—

Agent UUID.

{
  "agent_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
  "removed": true
}

grep

Run the host's grep against an agent log stream. No shell is used.

Argument

Type

Required

Default

Notes

agent_id

string

yes

—

Agent UUID.

args

string[]

yes

—

Grep argv, without the log path.

stream

"stdout" or "errors"

no

"stdout"

Selects stdout.log or errors.log.

{
  "agent_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
  "stream": "stdout",
  "binary": "/usr/bin/grep",
  "cwd": "/home/user/.local/share/pi-mcp/agents/01a1964b-2c01-70a5-b520-78f933cdafad",
  "path": "stdout.log",
  "args": ["-n", "response"],
  "argv": ["-n", "response", "stdout.log"],
  "exit_code": 0,
  "stdout": "5:{\"type\":\"response\"}\n",
  "stderr": "",
  "stdout_truncated": false,
  "stderr_truncated": false
}

The log filename is appended as the final grep operand.

list_tmp

List files under an agent's forwarded /tmp.

Argument

Type

Required

Default

Notes

agent_id

string

yes

—

Agent UUID.

path

string

no

""

Relative directory under the agent tmp.

recursive

boolean

no

true

Recurse into subdirectories.

max_entries

integer

no

1000

Clamped to 1..5000.

{
  "agent_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
  "path": ".",
  "recursive": true,
  "count": 3,
  "truncated": false,
  "entries": [
    {
      "path": "notes.txt",
      "type": "file",
      "size": 21,
      "mtime": 1793522881.5025642
    },
    {
      "path": "sub",
      "type": "directory",
      "size": 4096,
      "mtime": 1793522881.5025642
    },
    {
      "path": "sub/data.bin",
      "type": "file",
      "size": 7,
      "mtime": 1793522881.5026317
    },
    {
      "path": "link",
      "type": "symlink",
      "size": 8,
      "mtime": 1793522881.5027,
      "target": "../outside.txt"
    }
  ]
}

mtime is a Unix timestamp in seconds. Symlinks include target.

grep_tmp

Run the host's grep inside an agent's forwarded /tmp.

Argument

Type

Required

Default

Notes

agent_id

string

yes

—

Agent UUID.

args

string[]

yes

—

Grep argv, with no shell.

cwd

string

no

""

Relative working directory under tmp.

path

string

no

null

Optional file relative to cwd; appended as the final operand.

{
  "agent_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
  "binary": "/usr/bin/grep",
  "cwd": "/home/user/.local/share/pi-mcp/agents/01a1964b-2c01-70a5-b520-78f933cdafad/tmp/sub",
  "path": "data.bin",
  "args": ["-a", "xyz"],
  "argv": ["-a", "xyz", "data.bin"],
  "exit_code": 0,
  "stdout": "abc\u0000xyz\n",
  "stderr": "",
  "stdout_truncated": false,
  "stderr_truncated": false
}

Traversal and symlinks escaping the agent tmp are rejected.

read_tmp

Read or download a file from an agent's forwarded /tmp.

Argument

Type

Required

Default

Notes

agent_id

string

yes

—

Agent UUID.

path

string

yes

—

File path relative to the agent tmp.

lines_from

integer

no

null

0-based start line.

lines_to

integer

no

null

End line, exclusive.

bytes_from

integer

no

null

Byte mode start offset.

bytes_to

integer

no

null

Byte mode end offset.

max_lines

integer

no

null

Maximum lines to return.

max_bytes

integer

no

1024

Maximum bytes; capped at 10 MiB.

Line mode example:

{
  "agent_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
  "path": "notes.txt",
  "mode": "lines",
  "size": 21,
  "lines_from": 1,
  "lines_to": 2,
  "line_count": 1,
  "next_line": 2,
  "next_offset": 21,
  "eof": true,
  "max_lines": null,
  "max_bytes": 1024,
  "encoding": "utf-8",
  "content": "beta line\n",
  "truncated_regions": [],
  "warnings": []
}

Byte mode replaces lines_from / lines_to / next_line with bytes_from / bytes_to:

{
  "agent_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
  "path": "sub/data.bin",
  "mode": "bytes",
  "size": 7,
  "bytes_from": 0,
  "bytes_to": 7,
  "line_count": 0,
  "next_offset": 7,
  "eof": true,
  "max_lines": null,
  "max_bytes": 1024,
  "encoding": "base64",
  "content": "YWJjAHh5eg==",
  "truncated_regions": [],
  "warnings": []
}

Binary content is base64-encoded. Long lines over 10,000 characters are truncated with warnings and byte regions.

read

Read an agent log stream.

Argument

Type

Required

Default

Notes

agent_id

string

yes

—

Agent UUID.

stream

"stdout" or "errors"

no

"stdout"

Selects stdout.log or errors.log.

lines_from

integer

no

null

0-based start line.

lines_to

integer

no

null

End line, exclusive.

bytes_from

integer

no

null

Byte mode start offset.

bytes_to

integer

no

null

Byte mode end offset.

max_lines

integer

no

null

Maximum lines to return.

max_bytes

integer

no

1024

Maximum bytes; capped at 10 MiB.

{
  "agent_id": "01a1964b-2c01-70a5-b520-78f933cdafad",
  "stream": "stdout",
  "path": "stdout.log",
  "mode": "lines",
  "size": 40922,
  "lines_from": 0,
  "lines_to": null,
  "line_count": 1,
  "next_line": 1,
  "next_offset": 145,
  "eof": false,
  "max_lines": 1,
  "max_bytes": 1024,
  "encoding": "utf-8",
  "content": "{\"type\":\"response\",\"id\":\"rpc-0\",\"success\":true}\n",
  "truncated_regions": [],
  "warnings": []
}

Line and byte ranges are mutually exclusive.

Tests

make lint                  # Ruff + Prettier checks
make test                  # unit + stdio MCP round-trip
make e2e                   # Docker + Testcontainers end-to-end
make real                  # Cheap real provider checks
make expensive             # Real compaction run; only when context/compaction changes

Documentation

  • Design — MCP surface, RPC lifecycle, persistence, supervision, configuration, security.

  • pi behavior — verified pi 1.4.0 process, RPC, bash, context, compaction, and record-type behavior.

  • Sandboxing — write/read/denied mount rules and bwrap behavior.

  • Development — setup, layout, container, manual tests.

License

MIT. See LICENSE.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    C
    quality
    B
    maintenance
    Enables MCP hosts to delegate coding tasks to Pi CLI as a programmable sub-agent with session tracking and process management.
    7
    4
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables MCP hosts like Claude Code and Codex to spawn, manage, and interact with persistent, reusable Pi coding-agent sessions, supporting task dispatch, status checks, and session lifecycle control.
    2 npm
    -
  • A
    license
    B
    quality
    B
    maintenance
    Enables launching and supervising local Codex and Claude Code agent sessions and one-shot tasks, exposing run, list, stop, and output operations over MCP with session-scoped permissions and sandbox controls.
    2
    836 npm
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Lets MCP clients create, resume, message, and stop Pi coding-agent sessions running on another machine, with conversations that survive client disconnects and server restarts. Sessions can be kept loopback-only or published privately to trusted callers over Tailscale HTTPS.
    1
    Apache 2.0