# puredata-mcp

A reliable [Model Context Protocol](https://modelcontextprotocol.io) server for
[Pure Data](https://puredata.info). It lets an MCP client (Claude Desktop, etc.)
build and drive Pd patches in natural language: create objects, wire them
together, toggle audio, and send live messages into a running patch. It also
keeps an authoritative in-memory model of the patch, so it can **version** your
work — git-backed checkpoints, standalone `.pd` export, and a musical
graph-level diff between versions.

## Why this design is reliable

It speaks Pd's **native FUDI protocol** over a plain TCP socket and uses Pd
vanilla's built-in **dynamic patching** messages (`obj`, `msg`, `connect`,
`disconnect`, `clear`). That means:

- **No externals** to install (no mrpeach/OSC, no zexy, no `namecanvas`).
- **No intermediate daemon** — the MCP server talks straight to `[netreceive]`.
- **Object ids stay in sync** with Pd because the server mirrors Pd's exact
  creation-index ordering.

Earlier OSC-based Pd MCP attempts were fragile precisely because they stacked an
OSC layer plus a relay daemon on top of Pd. This removes both.

## Architecture

```
 Claude / MCP client
        │  (stdio, JSON-RPC)
        ▼
 puredata_mcp.server  ──FUDI over TCP──▶  [netreceive 3000]   (mcp_host.pd)
                                               │
                                          [route __dsp __send]
                                          │     │          └─▶ [send pd-canvas] ─▶ [pd canvas]
                                          │     └─▶ relay ─▶ [send] (to a named [receive])
                                          └─▶ [; pd dsp $1(   (global audio on/off)
```

Objects you create land inside the `[pd canvas]` subpatch, addressed by Pd's
dynamic-patching receiver `pd-canvas`.

## Install

Pick the channel that matches your client. The repo ships in three
formats so each lands cleanly:

| Client | How to install | What it activates |
|---|---|---|
| **Claude Code** (CLI REPL) | inside a `claude` session: `/plugin marketplace add jfboisvenue/pd-mcp-server` then `/plugin install puredata@puredata-marketplace` | Skill auto-load + 30 MCP tools |
| **Claude Desktop — Cowork sessions** | Plugins panel → **"Add from repository"** → paste `https://github.com/jfboisvenue/pd-mcp-server` | Skill auto-load + 30 MCP tools (Cowork chat only) |
| **Claude Desktop — regular chat** | Download `puredata-mcp.mcpb` from [Releases](https://github.com/jfboisvenue/pd-mcp-server/releases) → double-click → Install via **Settings → Extensions** | 30 MCP tools (no skill — the plugin/skill format isn't loaded by this channel) |

You can install both — the plugin (Code + Cowork) and the `.mcpb`
(regular Desktop chat) coexist, share the same source code, and don't
conflict.

After install, the skill (where applicable) auto-loads when relevant
and the MCP tools become available. **You still need to open
`pd/mcp_host.pd` in Pd** for the server to have something to talk to.

### Why three channels?

Anthropic's plugin marketplace targets Claude Code and Desktop's Cowork
sessions. Plugins installed this way load their skill in regular Desktop
chat too, but **not** the MCP server — that's by design across all
platforms (macOS, Windows, Linux), not a Linux-specific bug. For the MCP
to appear in regular Desktop chat, you need either a `.mcpb` Desktop
Extension or a manual entry in `~/.config/Claude/claude_desktop_config.json`
(see *Manual MCP install* below).

### Local dev / testing

For iteration on the code itself:

```bash
claude --plugin-dir /absolute/path/to/pd-mcp-server   # per-session
```

Or symlink once, persistent across sessions and surfaces:

```bash
mkdir -p ~/.claude/skills && \
ln -s /absolute/path/to/pd-mcp-server ~/.claude/skills/puredata
```

## Requirements

**For the MCP core (object/wire/DSP tools):**
- Pure Data **vanilla 0.51+**
- Python **3.10+** to run the MCP server
- [`uv`](https://github.com/astral-sh/uv) (recommended) or `pip`

**For the Python tools** (`pd_create_python_object`, `pd_update_python_script`)
the user's Pd host machine additionally needs:

- Pure Data **vanilla ≥ 0.56** — py4pd 1.2.3 uses `pd_snprintf`, missing in
  0.54.x and earlier (symptom: `undefined symbol: pd_snprintf` /
  `couldn't create`).
- [**py4pd external**](https://github.com/charlesneimog/py4pd) **≥ 1.2.3**
  installed in Pd (Help → Find externals → "py4pd").
- **Python 3.14** installed system-wide — py4pd's Deken build links against
  `libpython3.14.so.1.0`. On Ubuntu 24.04 (which ships 3.12 by default) you
  may need to install 3.14 separately (e.g. deadsnakes PPA).
- The host patch must contain `[declare -path <scripts dir> -lib py4pd]`.
  The bundled `pd/mcp_host.pd` already does this; custom host patches need
  it added.

Verified working stack: Pd 0.56-3 + py4pd 1.2.3 + Python 3.14.5 +
`.pd_py` classes inheriting `pd.NewObject`.

## Manual MCP install (fallback)

If the `.mcpb` install isn't an option (corporate restriction, edge
case, you want to point Desktop at your dev checkout), register the
server directly in `~/.config/Claude/claude_desktop_config.json` — the
same file Desktop reads other classic MCPs from:

```json
{
  "mcpServers": {
    "puredata": {
      "command": "/home/YOU/.local/bin/uv",
      "args": [
        "--directory",
        "/absolute/path/to/pd-mcp-server",
        "run",
        "python",
        "-m",
        "puredata_mcp.server"
      ],
      "env": {
        "PD_HOST": "127.0.0.1",
        "PD_PORT": "3000"
      }
    }
  }
}
```

Use the **absolute** path to `uv` (`which uv` → that path). GUI launchers
don't reliably inherit the shell's `PATH`, so a bare `"uv"` sometimes
fails to resolve even when uv is installed.

Then fully quit Claude Desktop (right-click tray icon → Quit, or
`pkill claude-desktop` on Linux) and relaunch. Verify the server started:

```bash
grep puredata ~/.config/Claude/logs/mcp.log | tail -5
# expect: [puredata] Server started and connected successfully
```

This route bypasses the plugin/`.mcpb` mechanisms entirely. It's the
most reliable path when debugging or pinning to a local checkout, at
the cost of hardcoded paths you maintain manually. The skill won't
auto-load this way — the manual config only registers the MCP.

## Releasing a new `.mcpb` (maintainer only)

Tag a version matching `pyproject.toml` and push:

```bash
git tag v0.1.0
git push --tags
```

GitHub Actions (`.github/workflows/release-mcpb.yml`) validates the
manifest, packs the bundle via `mcpb pack`, and publishes a GitHub
release with `puredata-mcp.mcpb` attached. End users install it via
Settings → Extensions.

The `.mcpbignore` file controls what ships in the bundle (currently
10 files / ~23KB). Dev artifacts (`.venv/`, tests, plugin manifests,
CI config, etc.) are excluded. To preview locally:

```bash
npx --yes @anthropic-ai/mcpb pack . /tmp/puredata-mcp.mcpb
unzip -l /tmp/puredata-mcp.mcpb
```

## Tools

The agent must call **`pd_init` first** — every other tool refuses to run
until it has. `pd_init` returns the orientation guide (wire model, id
contract, common-object cheat sheet, cookbook, safety notes), so the LLM
has a single, server-authored source of truth instead of having to infer
conventions from individual tool docstrings.

| Tool | What it does |
|------|--------------|
| `pd_init` | **Mandatory first call.** Returns the orientation guide and unlocks the rest; pass `project_dir` to bind the session so this patch gets its own checkpoints/scripts |
| `pd_create_object` | Create `[type args…]` at (x,y); returns its id |
| `pd_create_message` | Create a message box `[atoms…(` |
| `pd_create_comment` | Create a text comment (spaces preserved) |
| `pd_create_floatatom` | Create a typeable number atom |
| `pd_create_bang` | Create a bang button `[bng]` |
| `pd_create_toggle` | Create a toggle `[tgl]` |
| `pd_create_number_box` | Create a bounded number box `[nbx]` |
| `pd_create_slider` | Create a slider (horizontal `[hsl]` or vertical `[vsl]`) |
| `pd_create_python_object` | Write `<name>.pd_py` defining a `pd.NewObject` class and create `[<name>]` on the canvas (atomic, py4pd 1.2.3+) |
| `pd_update_python_script` | Rewrite an existing `<name>.pd_py` (re-create the object on the canvas to pick up changes) |
| `pd_connect` | Wire `source_id:outlet → target_id:inlet` |
| `pd_disconnect` | Remove a connection |
| `pd_set_dsp` | Start/stop global audio DSP |
| `pd_send_message` | Send atoms to a named `[receive]` in the patch |
| `pd_clear_canvas` | Delete everything; reset ids to 0 |
| `pd_resync_index` | Realign the id counter after a manual canvas edit |
| `pd_get_state` | List objects the server has created |
| `pd_snapshot` | Commit the current patch as a git-backed checkpoint (use `branch` to A/B sound variants) |
| `pd_restore` | Re-render the canvas from a saved checkpoint (clears first), addressed by label/hash/branch |
| `pd_list_checkpoints` | List all checkpoints across branches in the checkpoints repo |
| `pd_export_pd` | Write a standalone, openable `.pd` file from the current patch or a checkpoint |
| `pd_diff` | Musical graph-level diff between two checkpoints, or a checkpoint vs. the live patch |
| `pd_recover` | Re-render the autosaved working IR after a restart (needs a bound `project_dir`) |
| `pd_save_preset` | Save a named bag of parameter values (`{receiver: atoms}`) into the IR |
| `pd_apply_preset` | Re-send a saved preset's values into the live patch (non-destructive — no re-render) |
| `pd_list_presets` | List saved presets and their values |
| `pd_save_template` | Capture the patch (or a subset by id) as a reusable sub-graph; parameterize with `${token}` holes |
| `pd_apply_template` | Stamp a template into the current patch (append), substituting `${tokens}`; returns the id-map for boundary wiring |
| `pd_list_templates` | List saved templates with their object/connection counts and required `${params}` |

`pd_snapshot` … `pd_recover` are the **versioning layer**: the server holds an
authoritative in-memory model of the patch (the IR) and serializes *to* `.pd`,
never parses *from* it. Each patch gets **its own checkpoints repo** — bind it
once with `pd_init(project_dir="/path/to/patch")` and checkpoints land in
`<project_dir>/checkpoints/` (and `.pd_py` scripts in `<project_dir>/scripts/`).
Without a binding, the server falls back to `PD_CHECKPOINTS_DIR` then a single
bundled `checkpoints/` shared across patches. Each commit pairs `patch.json`
(the IR) with `patch.pd` (the openable file); branches double as A/B variants.

Binding a `project_dir` also turns on **autosave**: the live IR is written to
`<project_dir>/.pd_session.json` after every change (a single rolling file, not
versioned history). If the server or Pd restarts, the in-memory model is gone
but that file survives — `pd_recover` reloads and re-renders it so the canvas
matches again. Autosave is unsaved-work recovery; `pd_snapshot`/`pd_restore`
remain the way to keep named, versioned states.

> If your `project_dir` is itself a git repo, add `.pd_session.json` to its
> `.gitignore` — it's a transient working file, not something to commit. (The
> `checkpoints/` repo is separate and already its own git history.)

`pd_save_preset` … `pd_list_presets` are the **presets layer**: build your patch
so every tweakable parameter is fed by an `[r <name>]`, and parameter values
become data you can name, recall, and version. Recalling a preset is a pure
message send, never a re-render. When a `project_dir` is bound, presets form a
**durable per-project library** at `<project_dir>/presets.json` — a plain,
human-readable `{name: {receiver: atoms}}` file you can open, written on every
save and reloaded automatically at `pd_init` (presets imply no canvas objects,
so hydrating them is safe without a re-render). They also ride in the IR, so
snapshots version them and `pd_clear_canvas` keeps them. Unlike the transient
`.pd_session.json`, `presets.json` is meaningful data — fine to commit to your
project repo. See `pd_init`'s guide for the full workflow.

`pd_save_template` … `pd_list_templates` are the **templates layer**: where a
preset recalls *values*, a template stamps *structure*. Build a unit once — a
synth voice, a filter+envelope, a meter — capture it (whole canvas, or a subset
by `ids`, keeping only the wiring internal to that selection), then
`pd_apply_template` **appends** copies into any patch. Because Pd's creation
index is append-only, a stamped instance just takes the next free ids, and the
tool returns an `id_map` (template-local id → new canvas id) so you can wire the
copy's boundary into the rest of the patch. Parameterize with `${token}`
substitution — not a new language, just named holes in object args / message
atoms / comment text (e.g. `[r freq_${v}]`), filled at apply so multiple
instances get unique, non-colliding receiver and buffer names. Because a
template is reusable *tooling* — a synth voice you want in **every** patch, not
one project's content — the library is **global**, not per-project: each
template is saved as one `<name>.json` file in your **Pd user folder**, which
the server **auto-locates** (it reads Pd's own search paths from `~/.pdsettings`
/ the macOS plist, landing on e.g. `~/Documents/Pd/templates`; override with
`PD_TEMPLATES_DIR`). No project binding is needed — they persist and are
reusable everywhere, the save response reports the exact path, and `pd_init`
announces what it loaded. Unlike presets they do **not** ride in the IR. See
`pd_init`'s guide for the two-voice cookbook.

Python class files (`.pd_py`) written by `pd_create_python_object` live in
`pd/scripts/` by default, which the host patch declares via
`[declare -path scripts -lib py4pd]`. The location is overridable per call
(`scripts_dir` parameter) or globally (`PD_SCRIPTS_DIR` env var). The Python
tools require **py4pd ≥ 1.2.3** by Charles Neimog (Pd → Help → Find externals
→ "py4pd"), which itself requires **Pd ≥ 0.55** and **Python 3.14** on the
host system (the Deken build links against `libpython3.14.so.1.0`). Mental
model: one `.pd_py` file defines one `pd.NewObject` subclass and becomes one
custom Pd object; multiple objects = multiple files.

Object ids are Pd creation indices (0, 1, 2, …). `pd_connect` uses them.

### Example: 440 Hz sine to the DAC

```
pd_create_object  type="osc~"  args=["440"]   x=40 y=40   -> id 0
pd_create_object  type="*~"    args=["0.2"]   x=40 y=90   -> id 1
pd_create_object  type="dac~"                 x=40 y=140  -> id 2
pd_connect        0:0 -> 1:0
pd_connect        1:0 -> 2:0      (left channel)
pd_connect        1:0 -> 2:1      (right channel)
pd_set_dsp        on=true
```

## Testing

The wire format, FUDI escaping, and id bookkeeping are covered by tests that run
**without Pd**, using a mock `[netreceive]`:

```bash
pip install pytest
python -m pytest tests/ -v
```

## Known limitations (Pd vanilla)

- **No single-object delete.** Vanilla has no "delete object N" editing message;
  use `pd_clear_canvas` and rebuild, or restructure. (This is a Pd constraint,
  not a server bug.)
- **The server mirrors creation order, it does not read Pd back.** If you edit
  the `[pd canvas]` subpatch by hand while the server runs, ids can drift —
  call `pd_resync_index(next_index=N)` to realign the counter.
- **`pd_send_message` requires a matching `[receive <name>]`** already present in
  your patch.
- Default transport is **TCP**. For UDP, change the patch object to
  `netreceive -u 3000` and adjust the client accordingly.

## Layout

```
pd-mcp-server/
├── manifest.json               # .mcpb manifest (Desktop Extensions channel)
├── .mcpbignore                 # controls what ships in the .mcpb bundle
├── .github/workflows/
│   └── release-mcpb.yml        # CI: pack + publish .mcpb on tag push
├── .claude-plugin/
│   ├── plugin.json             # plugin manifest (Code + Cowork channel)
│   └── marketplace.json        # this repo is its own marketplace
├── .mcp.json                   # plugin's MCP server declaration (stdio)
├── skills/
│   └── puredata/
│       └── SKILL.md            # auto-triggers on Pd/synth/audio intents
├── bin/
│   └── puredata-mcp            # plugin launcher (finds uv across locations)
├── puredata_mcp/
│   ├── server.py               # FastMCP server + tools + init gate
│   ├── guide.py                # orientation guide returned by pd_init
│   ├── fudi.py                 # FUDI TCP client + escaping
│   ├── patch_state.py          # authoritative IR (nodes/edges) + init flag
│   ├── builders.py             # single source of truth for the wire/file format
│   ├── versioning.py           # git-backed snapshot/restore (dedicated repo)
│   ├── pd_serialize.py         # IR -> standalone .pd text
│   └── pd_diff.py              # graph-level diff + musical formatting
├── pd/mcp_host.pd              # the Pd host patch (open this in Pd)
├── checkpoints/                # versioning git repo (gitignored, auto-created)
├── pd/scripts/                 # .pd_py files dropped by pd_create_python_object
├── tests/                      # mock-Pd round-trip + unit tests
├── pyproject.toml
└── claude_desktop_config.example.json  # legacy example -- see "Manual MCP install"
```

## License

MIT
