# PlutoMCPFriend.sh — MCP Adapter Guide (Claude Code)

PlutoMCPFriend exposes the Pluto coordination server as a native **MCP**
(Model Context Protocol) server for **Claude Code**. Pluto operations
become ordinary tool calls inside Claude — `pluto_send`,
`pluto_lock_acquire`, `pluto_task_update`, and friends. No PTY, no
`curl`, no copy-pasted session tokens, no JSON heredoc gymnastics.

Only Claude Code is supported. The launcher relies on Claude's
`--mcp-config` and `--append-system-prompt` flags for the role
auto-injection path; Cursor, Aider, and Copilot don't have stable
equivalents. **For non-Claude agents, use `PlutoAgentFriend.sh` instead** —
the PTY-based wrapper works with any TUI agent.

---

## Why MCP?

| Concern | PlutoAgentFriend (PTY) | PlutoMCPFriend (MCP) |
|---------|------------------------|----------------------|
| How the agent sends messages | curl + JSON heredoc | `pluto_send(...)` tool call |
| Session token handling | inlined into prompt; agent must paste it back | adapter holds it; agent never sees it |
| Inbound delivery | wrapper types text into stdin when idle | tool result includes `_pluto_inbox`; or call `pluto_recv` |
| Lock TTL renewal | agent must remember | adapter auto-renews at TTL/2 |
| Role on first turn | injected as natural-language prompt | MCP prompt + Claude `--append-system-prompt` |
| Works with non-Claude | yes (any TUI) | yes if framework supports MCP |

---

## Quick Start

```bash
# Prerequisite: Pluto server is running
./PlutoServer.sh --daemon

# Interactive setup wizard (recommended for first run)
./PlutoMCPFriend.sh

# Expert mode — one-shot launch with role applied on turn 1
./PlutoMCPFriend.sh --agent-id coder-1 --role specialist

# Tune the watcher per-call duration (default 60s, keep ≤120s)
./PlutoMCPFriend.sh --agent-id coder-1 --role specialist --wait-timeout-s 90

# Generate the .mcp.json without launching Claude
./PlutoMCPFriend.sh --agent-id reviewer-1 --no-launch
```

The first run creates a Python virtual environment under `/tmp/pluto/.venv`
and installs the MCP SDK (`pip install -r requirements.txt`). Subsequent
launches reuse the venv.

**Inside Claude Code, go to the `/` menu and select the appropriate role
(e.g., `/pluto-role-specialist`) to register with Pluto.** If you passed
`--role` at launch, this is pre-applied on turn 1, but you can change roles
mid-session via the slash menu.

---

## Agent name & role — how the launcher arguments map into Claude and Pluto

Two flags on `PlutoMCPFriend.sh` control how Claude sees the role and how
Pluto sees the agent. Get these right and the rest "just works".

### `--agent-id <name>` is your identity in Pluto's distributed system

The `--agent-id` you pass to the launcher is the **name the agent is
recognized as inside the Pluto coordination server** — it's what other
agents, the Orchestrator, and tools like `pluto_send`, `pluto_list_agents`,
`/locks/*` all use to address this session. Choose something role-shaped
and unique, e.g. `specialist-1`, `orchestrator`, `reviewer-frontend`.

```bash
./PlutoMCPFriend.sh --agent-id specialist-1 --role specialist
```

**Name conflicts → Pluto auto-suffixes.** If the name you requested is
already taken by an active session, the Pluto server registers you under
an auto-generated suffixed variant (e.g. `specialist-1` → `specialist-1-2`)
rather than refusing the registration. The launcher logs the actual
registered ID, and Pluto returns it in the registration response. **Always
use the returned `agent_id`, not the one you requested**, for subsequent
operations — `pluto_session` will show you the canonical name. This means
two `./PlutoMCPFriend.sh --agent-id specialist-1 ...` invocations against
the same Pluto server will both succeed; the second one ends up under a
different ID.

### Activating a role inside Claude — invoke the MCP slash command

`--role <name>` at launch time pre-applies the role on turn 1 via Claude's
`--append-system-prompt`. **But mid-session, the user activates a role by
running its MCP slash command** in Claude Code:

```
/pluto-role-specialist
/pluto-role-orchestrator
/pluto-role-reviewer
... (one per file in library/roles/)
```

Each of these is an MCP **prompt** registered by PlutoMCPFriend (see the
"Prompts" table below). Typing the slash command in Claude inlines the
matching `library/roles/<name>.md` plus the shared `protocol.md` plus the
live connection block (host/port + your registered `agent_id`) into the
conversation. That is the canonical way to switch or re-apply a role
without restarting the wrapper.

So: **the script's `--agent-id` is your name on the Pluto network; the
`/pluto-role-<name>` slash command in Claude is what tells the agent
which role to play.** Both can be changed independently — different
sessions can rotate through roles without changing their `agent_id`,
and a fresh `agent_id` can pick any role on its first turn.

---

## What ships out of the box

### 20 Pluto tools

Each is a thin wrapper around the existing `PlutoHttpClient` HTTP method;
the adapter injects the session token transparently.

| Tool | Wraps | Purpose |
|------|-------|---------|
| `pluto_send` | `POST /agents/send` | direct message to one agent |
| `pluto_broadcast` | `POST /agents/broadcast` | message to every agent |
| `pluto_recv` | drain in-memory inbox | explicit drain (call at start of turn). Returns ALL pending messages — the batch-mode default |
| `pluto_pop` | pop ONE message + ack | pipeline / event-driven counterpart to `pluto_recv`: one message per call plus a `remaining` counter. Pair with `pluto_set_delivery_mode("single")` so unrelated tool calls do not bulk-drain via piggyback |
| `pluto_set_delivery_mode` | switch `"batch"` ↔ `"single"` | toggles how `piggyback` / `pluto_recv` surface messages. `"batch"` (default) returns the whole buffer; `"single"` attaches at most one message + `_pluto_inbox_remaining` |
| `pluto_wait_for_messages` | event-driven block on inbox | long-poll until a message arrives (or timeout) — pair with a background sub-agent for chat-speed responsiveness |
| `pluto_publish` | `POST /agents/publish` | publish to a topic |
| `pluto_subscribe` | `POST /agents/subscribe` | subscribe to a topic |
| `pluto_list_agents` | `GET /agents?detailed=true` | discover peers |
| `pluto_find_agents` | `POST /agents/find` | filter peers by attribute |
| `pluto_lock_acquire` | `POST /locks/acquire` | grab a lock; auto-renews at TTL/2 |
| `pluto_lock_release` | `POST /locks/release` | release + cancel auto-renew |
| `pluto_lock_renew` | `POST /locks/renew` | manual renew (rarely needed) |
| `pluto_lock_info` | `GET /locks/resource` | who holds it / queue depth |
| `pluto_list_locks` | `GET /locks` | every active lock |
| `pluto_task_assign` | `POST /agents/task_assign` | assign a task |
| `pluto_task_update` | `POST /agents/task_update` | report task progress |
| `pluto_task_list` | `POST /agents/task_list` | enumerate tasks |
| `pluto_set_status` | `POST /agents/set_status` | custom status string |
| `pluto_snapshot_self` | `POST /agents/snapshot_self` | capture `.plut` + recovery prompt; pass `output_dir` to also write `<agent_id>.plut` and `<agent_id>-recovery.md` to disk |
| `pluto_restore_from_snapshot` | `POST /agents/restore_from_snapshot` | re-apply a saved `.plut` to current session — status flips to `recovered_from_file`, returns `reclaimed_locks`/`lost_locks` |
| `pluto_session` | (read-only, no network) | self-diagnostic: returns adapter↔Pluto registration state. The agent's "is MCP alive?" probe — if this errors, the user should run `/mcp` to refresh the Claude Code transport. |
| `pluto_health` | `GET /health` (live ping) | end-to-end probe: adapter state + server reachability + last auto-snapshot. Use when coordination feels wedged: `pluto_server != "ok"` means the Pluto server is unreachable; otherwise the adapter is healthy. |

### Prompts (slash commands)

In Claude Code these surface as slash commands. Two groups:

**Quick actions** — one-keystroke shortcuts; no need to type a request to the agent.

| Prompt | What it does |
|--------|-------------|
| `/pluto-check` | Calls `pluto_recv` and summarizes whatever arrived (one-shot, no blocking). Also re-arms the watcher unless `watcher_stop` is in effect. |
| `/pluto-watch` | Starts a chat-speed inbox watcher by spawning a background Task that calls `pluto_wait_for_messages`. Implicitly clears any `watcher_stop` kill switch. |
| `/pluto-watch-stop` | Engages the `watcher_stop` kill switch: stops auto-respawning the inbox watcher on drain for the rest of the session. Resume with `/pluto-watch`. |
| `/pluto-status` | Reports my agent_id, connected peers, inbox depth, and locks held |

**Roles & reference** — inlined documents.

| Prompt | What it returns |
|--------|-----------------|
| `/pluto-protocol` | inlined `library/protocol.md` + live connection block |
| `/pluto-guide` | inlined agent guide + live connection block |
| `/pluto-role-<name>` | one prompt per role file in `library/roles/` (specialist, orchestrator, reviewer, qa, deployer, evaluator, experiment-runner, data-steward, ssh-bridge) |

### 4 resources

| URI | Content |
|-----|---------|
| `pluto://inbox` | pending messages (read-only; does not ack — use `pluto_recv` for that) |
| `pluto://locks` | locks currently held by this agent |
| `pluto://agents` | every agent connected to the Pluto server |
| `pluto://server` | server health / version |

---

## How inbox messages reach the agent

The MCP adapter runs a 1 s background loop calling `/agents/peek` against
the Pluto server. New actionable messages are buffered in memory and
acked the moment they're handed to the agent. There are **three** delivery
mechanisms; the role prompt teaches the agent which to use.

The adapter has two **delivery modes** that change the shape of the
piggyback and `pluto_recv` paths:

* **`"batch"` (default)** — every drain empties the buffer at once. Right
  for turn-driven / interactive agents that process whatever queued up
  since the last reply.
* **`"single"`** — `piggyback` attaches at most one message (plus a
  `_pluto_inbox_remaining` counter) and `pluto_pop` is the canonical
  consumer. Right for pipeline / event-driven agents where each message
  is a discrete unit of work and you want one-message-per-turn reactivity.
  Switch with `pluto_set_delivery_mode("single")` once near the top of
  the session.

Without single-mode, calling `pluto_pop` alongside other Pluto tools
would still work, but every unrelated `pluto_send` / `pluto_lock_*` etc.
would bulk-drain the buffer via piggyback behind the pop loop's back.

### 1. Tool-result piggyback (free)

Every Pluto tool result includes a `_pluto_inbox` field if there are
pending messages. So just by doing any Pluto-related work the agent
automatically picks up its inbox — no separate poll required.

### 2. Explicit drain — `pluto_recv`

Returns and acks every pending message. The role prompt instructs the
agent to call this at the start of any turn where it hasn't already
invoked another Pluto tool. Returns immediately even if the buffer is
empty.

### 3. Event-driven long-poll — `pluto_wait_for_messages`

Blocks until a message lands in the buffer (or until the timeout
elapses). Implemented with an `asyncio.Event` set by the peek loop, so
fanout is sub-second — it does **not** poll.

Recommended usage: spawn a background Task from inside Claude where the
**subagent itself loops** short polls. A single 300+ s blocking call in
a subagent gets killed at the 600 s silence cutoff by Claude Code's
stream watchdog; looping short polls produces regular tool-call output
that keeps the subagent visible.

```
Task({
  description: "Pluto inbox watcher",
  subagent_type: "general-purpose",
  run_in_background: true,
  prompt: """
    You are a Pluto inbox watcher. Repeat this loop:
      1. Call pluto_wait_for_messages(60).
      2. If response.count > 0, return the response JSON and stop.
      3. If count == 0, go back to step 1.
    Stop after 5 iterations regardless. Each tool-call output keeps
    you visible to the stream watchdog.
  """
})
```

Claude Code notifies the parent agent when the Task completes (on the
parent's next turn, the result appears in context). Process any
messages, then spawn another watcher Task to keep listening. The user
never sees the agent stuck in a tool call.

The per-call duration is configurable via `--wait-timeout-s` on the
launcher (default 60 seconds — safe under the watchdog cutoff). Total
subagent lifetime is bounded by `5 × --wait-timeout-s`. The value
propagates into the role connection block, the `/pluto-watch` slash
prompt, and the tool's argument default.

### 4. Single-pop — `pluto_pop` (pipeline / event-driven)

For pipeline-style agents where each message is a discrete unit of work
(a file produced, a task to dispatch, a graph stage to advance), prefer
single-pop:

```
1. Once near session start:
     pluto_set_delivery_mode("single")

2. Per watcher wake / inbox notification:
     msg, remaining = pluto_pop(wait_s=<short>)
     # process msg
     while remaining > 0:
         msg, remaining = pluto_pop()
         # process msg
     # buffer drained — yield and wait for next wake
```

`pluto_pop` blocks up to `wait_s` seconds for an arrival when the buffer
is empty (0 = return immediately). Each pop returns the head of the
FIFO buffer plus a `remaining` counter so the agent knows whether to
loop or yield. Single-mode also rewires `piggyback`: any Pluto tool
result attaches at most one message under `_pluto_inbox` plus
`_pluto_inbox_remaining`, preserving the one-at-a-time invariant across
unrelated tool calls.

Trade-off: one extra round-trip per message. Negligible for agent-paced
traffic; a real cost at firehose rates — stay in batch mode unless
per-message reactivity is the point.

The active mode is visible in `pluto_session` under `delivery_mode`.

---

## Lock auto-renewal

`pluto_lock_acquire(resource, mode, ttl_ms, auto_renew=True)` registers
the granted lock with a background renewer that calls `/locks/renew` at
TTL/2 until the agent calls `pluto_lock_release` or the session ends. To
opt out, pass `auto_renew=False` and renew manually with
`pluto_lock_renew`.

If a renewal call fails (connectivity blip, lock revoked) the manager
stops retrying and logs a warning to stderr — the agent is responsible
for treating subsequent writes as unsafe.

---

## Self-snapshot & recover-from-file (v0.2.9 / v0.3.0)

> **v0.3.0** added *auto-snapshot* (default: every 2 h), a no-path
> `--resume` flag, `--clean-snapshots`, and the `pluto_health` MCP tool
> for end-to-end probing. The legacy `--restore <path>` flag still works
> as before — `--resume` is just a shorthand for the common case.

### TL;DR — recovering when `/mcp` says "Failed to reconnect to pluto"

```bash
# Easiest path: identity + locks come back automatically.
./PlutoMCPFriend.sh --agent-id <id> --resume --role <same-role>
```

`--resume` looks up `/tmp/pluto/snapshots/<agent_id>.plut`, restores
identity + locks + attributes, and warns-and-continues with a fresh
register if no snapshot is found. **You must re-pass `--role` (and/or
pick `/pluto-role-<name>` from the slash menu once Claude opens)** —
snapshots restore Pluto-side coordination state, not the in-prompt
role definition the MCP friend injects on launch.

### Auto-snapshot loop

Every registered MCP friend takes a snapshot every
`--auto-snapshot-interval` seconds (default **7200 = 2 h**) and one
final snapshot on graceful shutdown. Output goes to
`--snapshot-dir` (default `/tmp/pluto/snapshots`), overwriting
`<agent_id>.plut` each time. Disable with `--no-auto-snapshot`.

| Flag | Default | Notes |
|------|---------|-------|
| `--no-auto-snapshot` | off | disables the timer entirely |
| `--auto-snapshot-interval <s>` | 7200 | minimum 60 s |
| `--snapshot-dir <dir>` | `/tmp/pluto/snapshots` | shared with `--resume` |
| `--clean-snapshots` | — | one-shot; deletes `*.plut` + `*-recovery.md` in `--snapshot-dir`. Combine with `--agent-id` to scope to one agent |

### `pluto_health` MCP tool

The agent can probe end-to-end coordination health at any time:

```
pluto_health()
→ {
    "mcp_adapter":   "ok",
    "pluto_server":  "ok",            # "unreachable: ..." or "error: ..." otherwise
    "agent_registered": true,
    "agent_id":      "cells-orch",
    "host":          "127.0.0.1",
    "http_port":     9201,
    "server_version": "0.3.0",
    "server_epoch":  "8c4f1e2a-...",  # current live server epoch
    "session_epoch": "8c4f1e2a-...",  # epoch at register time
    "auto_snapshot": { ... }
  }
```

If the server has been restarted (or `--clean`-ed) since this adapter
registered, the live `server_epoch` will differ from the cached
`session_epoch` and `pluto_health` returns:

```
{
  "agent_registered":  false,
  "server_restarted":  true,
  "recovery_hint":     "Server epoch changed — Pluto was restarted/cleaned. ..."
}
```

This lets agents diagnose "session lost" *up-front* without first
hitting a 404 on a token-bearing call. Recovery: relaunch the MCP
friend (typically `./PlutoMCPFriend.sh --agent-id <id> --resume`).

> **Why doesn't the MCP friend just re-register itself?** It runs as a
> stdio child of Claude Code: when the transport dies (or the server
> changes epoch under it), the adapter can detect the situation via
> `pluto_health` but cannot respawn itself — only Claude can. So the
> contract is: the agent (via `pluto_health`) flags the problem; the
> user runs `/mcp` or relaunches `PlutoMCPFriend.sh`. By contrast
> `PlutoAgentFriend` owns its own process and auto-reregisters on
> `server_epoch` mismatch (see its guide).

`pluto_health` is complementary to `pluto_session`:
`pluto_session` is a cheap adapter-only probe (no network call) — if
*that* errors the MCP transport itself is dead and only `/mcp` in
Claude Code can recover it. `pluto_health` adds the live HTTP ping to
the Pluto server, so `pluto_server != "ok"` means the server is the
problem, not the adapter; `server_restarted == true` means the
adapter outlived its server session.

### Legacy section

When an agent is about to die for any reason — host restart, OS update,
process crash, manual `/clear` — Pluto can hand the agent a self-contained
state bundle so the *next* incarnation rejoins as the same identity.

A snapshot is two files written next to each other:

| File | Format | Purpose |
|------|--------|---------|
| `<agent_id>.plut`           | JSON     | coordination state — agent_id, session_id, attributes, custom_status, subscriptions, currently held locks (with fencing tokens) |
| `<agent_id>-recovery.md`    | Markdown | step-by-step recovery prompt the new instance should read first turn |

After restore, the agent's status becomes `recovered_from_file` (visible
to other agents via `pluto_list_agents`).

### Taking a snapshot from inside the agent

The agent calls the MCP tool with an `output_dir`:

```
pluto_snapshot_self(output_dir="/tmp/pluto/snapshots")
→ {
    "status": "ok",
    "plut_path":   "/tmp/pluto/snapshots/cells-orch.plut",
    "prompt_path": "/tmp/pluto/snapshots/cells-orch-recovery.md",
    "message":     "Snapshot saved. To restore later, run
                    PlutoMCPFriend with --restore /tmp/pluto/snapshots/cells-orch.plut"
  }
```

Omit `output_dir` to receive the raw payload inline (`{plut: {...}, prompt: "..."}`)
without writing to disk — useful for sending the snapshot to another agent.

### Restoring from a `.plut` file

Two paths, depending on whether the agent is *already running* or being
*launched fresh*:

**Mid-session (already registered):**
```
pluto_restore_from_snapshot(plut_path="/tmp/pluto/snapshots/cells-orch.plut")
→ {
    "status":          "ok",
    "agent_id":        "cells-orch",
    "session_id":      "sess-...",
    "reclaimed_locks": [{...}],   # still held — Pluto kept them through reconnect
    "lost_locks":      [{...}],   # released since snapshot — re-acquire if needed
    "subscriptions":   ["control", "alerts"],
    "attributes":      {...}
  }
```

**On launch (fresh process — recommended after a crash):** pass the
file path to the launcher so re-registration happens before turn 1:

```
./PlutoMCPFriend.sh --restore /tmp/pluto/snapshots/cells-orch.plut
```

`--agent-id` is read from the `.plut` automatically, so you don't need
to type it again — pass it explicitly only if you want to claim a
*different* identity, in which case the launcher refuses (locks would
silently land in `lost_locks`). Sequence on launch:

1. Validate the `.plut` and derive `agent_id` from its `agent_id` field.
2. Standard MCP register against Pluto.
3. Call `restore_from_snapshot` — overlay attributes, custom_status,
   subscriptions; split locks into `reclaimed_locks` / `lost_locks`;
   set status to `recovered_from_file`.
4. Log a one-line summary (`reclaimed=N lost=M`) to stderr.
5. Hand control to Claude.

What's *not* automated yet: the launcher does not auto-inject the
markdown recovery prompt (`<agent_id>-recovery.md`) into Claude's
system prompt. Read it yourself first turn, or paste it into the
agent's first user message — every step it lists is also embedded in
the `pluto-protocol` MCP prompt, so an agent that knows the protocol
will reconstruct most of the checklist on its own.

### What the recovery prompt contains

A typical generated `<agent_id>-recovery.md` walks the agent through:

1. Re-register on Pluto (the launcher already did this — for reference).
2. Apply the `.plut` via `restore_from_snapshot` (also automated).
3. Rebuild domain context — read `CLAUDE.md`, check `git log`, drain inbox.
4. Audit each entry in `reclaimed_locks` — verify the resource is still
   safe to keep holding given any drift while offline.
5. Re-confirm subscriptions and check the orchestrator for new tasks.

Pluto only restores *coordination* state, not application context — the
agent must reload its own working memory from the project.

### Lock semantics during snapshot/restore

A snapshot records lock refs and fencing tokens at the moment it was
taken. On restore Pluto checks each ref against current ownership:

- Still held by you → `reclaimed_locks` (no re-acquire needed; fencing
  token is unchanged).
- Released since snapshot (TTL or grace expired, you released it,
  forced release) → `lost_locks`. Treat as not-held; re-acquire only
  if the resource is still safe and free.

Snapshots **do not** preserve the inbox. Messages sent while you were
offline live on Pluto via the normal grace-period delivery if you
reconnect within `reconnect_grace_ms` (default 30 s); after that they
are lost. For longer outages, durable persistence is the orchestrator's
responsibility, not the snapshot's.

### When *not* to use it

Snapshots are best for known-imminent restarts or graceful shutdowns.
For sub-second blips Pluto's grace-period reconnect (`recovered`
status, automatic lock reclaim, queued inbox delivery) is already the
right tool — don't snapshot every minute "just in case."

---

## Role injection on startup

`PlutoMCPFriend.sh --role <name>` resolves the role file from
`library/roles/<name>.md` and launches Claude with
`claude --mcp-config .mcp.json --append-system-prompt "<role + protocol + connection block>"`,
so turn 1 already has the role applied.

The role is also reachable mid-session via the slash menu
(`/pluto-role-specialist`, etc.). Switching roles is a single command.

---

## CLI options

| Flag | Default | Description |
|------|---------|-------------|
| `--agent-id <name>` | *required* (or wizard prompts) | Pluto agent identity |
| `--role <name\|path>` | none | role to auto-apply via Claude `--append-system-prompt` |
| `--host <ip>` | from config / `127.0.0.1` | Pluto server host |
| `--http-port <port>` | from config / `9201` | Pluto HTTP port |
| `--ttl-ms <ms>` | `600000` | session TTL |
| `--wait-timeout-s <sec>` | `60` | `pluto_wait_for_messages` per-call block duration (the watcher subagent loops short calls of this length to dodge Claude Code's 600 s stream watchdog). Keep ≤ 120. Embedded into the role connection block, the `/pluto-watch` slash prompt, and the tool's argument default. |
| `--log-level <lvl>` | `WARNING` | adapter stderr verbosity |
| `--no-launch` | off | write `.mcp.json`, don't start Claude |
| `--no-wizard` | off | refuse the interactive wizard; require all args |
| `--version` |  | print version |
| `--help` |  | show this help |
| `-- <cmd...>` |  | extra args forwarded verbatim to `claude` |

---

## .mcp.json layout

`PlutoMCPFriend.sh` writes (or merges into) `<repo>/.mcp.json`:

```json
{
  "mcpServers": {
    "pluto": {
      "command": "/tmp/pluto/.venv/bin/python",
      "args": [
        "/path/to/src_py/agent_mcp_friend/pluto_mcp_friend.py",
        "--agent-id", "coder-1",
        "--host", "127.0.0.1",
        "--http-port", "9201",
        "--ttl-ms", "600000",
        "--log-level", "WARNING"
      ]
    }
  }
}
```

Other `mcpServers` entries you have configured are preserved — only the
`pluto` key is overwritten. The file is gitignored (per-user).

To run the adapter from a custom MCP client:

```bash
claude --mcp-config /path/to/.mcp.json
```

---

## Architecture

```
              Claude Code                          Pluto Erlang server
                  │                                        │
                  │  JSON-RPC stdio                        │  HTTP :9201
                  │  (tools, prompts, resources,           │  (existing API,
                  │   notifications/resources/updated)     │   unchanged)
                  │                                        │
                  └──── PlutoMCPFriend (Python) ───────────┘
                              │
                              ├── FastMCP server (mcp SDK)
                              ├── PlutoHttpClient (existing)
                              ├── InboxManager
                              │     ├── 1 s peek loop
                              │     ├── seq dedup, noise filter
                              │     └── piggyback helper
                              └── LockManager
                                    └── per-lock TTL/2 renewal
```

The adapter is ~700 LOC of Python wrapping the existing HTTP client.
The Erlang server is unchanged.

---

## Comparison vs PlutoAgentFriend

Use **PlutoAgentFriend** when:

- You're running anything other than Claude Code (Cursor, Aider, GitHub
  Copilot CLI, custom agent loops).
- You want to keep the agent completely unmodified, including its prompt
  format — PTY injection works without any MCP support.

Use **PlutoMCPFriend** when:

- You're running **Claude Code**.
- You want structured tool calls instead of curl-from-shell.
- You want the wrapper to manage tokens, acks, and lock renewals
  automatically.

The two are not mutually exclusive — different team members can run
different wrappers against the same Pluto server.

---

## Known limitations

### Background watcher subagents may not have MCP access

The recommended chat-speed pattern — spawning a Claude Code background
Task that calls `pluto_wait_for_messages` — relies on the subagent
inheriting the parent session's MCP server config. **Some Claude Code
versions don't propagate MCP to subagents**: the subagent runs in an
isolated tool-set that includes built-ins (Read/Edit/Bash/...) but
not third-party MCP tools. The watcher Task then completes well
before the timeout (typically 60–120 s, the subagent's idle cleanup
interval) with an empty result.

**The role connection block detects this and tells the agent to
fall back to turn-driven pulling**: `pluto_recv` at the start of
every reply, plus `/pluto-check` whenever the user wants an explicit
flush. Multi-agent fan-out still works, but messages surface on the
recipient's next turn rather than at sub-second chat speed.

If chat-speed delivery is critical for your workflow, consider
running the agents under `PlutoAgentFriend.sh` instead — the PTY
wrapper actively types injected messages into the agent's terminal,
so messages reach the agent's reasoning context the moment they
arrive (no subagent gymnastics required).

## Troubleshooting

**`pip install` fails with externally-managed-environment.**
The script creates a project venv at `/tmp/pluto/.venv`. If your system
Python rejects pip via PEP 668, the venv side-steps it. If you've
deleted the venv, the next launch recreates it.

**`pluto_*` tools don't show up in Claude Code.**
Verify Claude was launched with `--mcp-config <path>/.mcp.json` (the
launcher does this automatically). Run `claude --mcp-config <path>` and
look at Claude's MCP startup log for errors from the `pluto` server.

**Inbox never delivers.**
Check `pluto://inbox` resource by reading it directly from Claude Code.
If it's empty, the adapter's peek loop is healthy but no actionable
messages have arrived. Set `--log-level DEBUG` for adapter chatter.

**Pluto server unreachable.**
Run `./PlutoServer.sh --status`. If down, `./PlutoServer.sh --daemon`.
The adapter starts even when the server is down so the agent doesn't
fail outright; tools will return errors until the server comes back.
