Skip to main content
Glama
README.md
# ComfyUI-AIAssistant

Let a local AI assistant see what is selected on your ComfyUI canvas — and,
on explicit request, insert text into it.

Every browser tab publishes a bounded snapshot of the current node selection
to the ComfyUI process over a same-origin WebSocket. The snapshot lives in
RAM and is served to any local consumer over HTTP or MCP. One narrow write
tool, `set_widget_text`, can insert text into a text-like widget of the
selected node, guarded by concurrency checks.

No chat UI, no AI service calls, no workflow nodes, no runtime dependencies.

## Highlights

- **Selection context for agents** — ask *"how should I configure the
  selected node?"* and get an answer grounded in the live widget values,
  input/output slots and connections.
- **MCP server built in** — one URL, no extra process: `POST /mcp` speaks a
  stateless subset of MCP Streamable HTTP (verified live with Claude Code
  and OpenCode).
- **Multi-tab aware** — every tab is a page in a bounded registry; the
  active page follows window focus and carries a label (`AI-XXXX`) shown at
  the top of the tab, so you and the agent always name the same tab.
- **Read-only by design, one narrow carve-out** — the only mutation the
  plugin can perform is writing text into one text-like widget of the
  selected node on the active page, on an explicit MCP request, guarded by
  revision and page-identity checks. On the canvas the edit behaves exactly
  like a manual widget edit: undo works, the workflow becomes dirty.
- **Private by construction** — RAM-only storage, no persistence, no
  telemetry, no outbound network requests, no payload logging; secret-like
  widget names are redacted before the snapshot leaves the page.

## Requirements

- ComfyUI with the current frontend (developed and verified against
  ComfyUI 0.33.3, frontend 1.50.6, aiohttp 3.14, Python 3.13).
- Python ≥ 3.10.
- No additional Python or Node packages.

## Install

```bash
cd <ComfyUI>/custom_nodes
git clone https://github.com/alksef/ComfyUI-AIAssistant.git
```

Restart ComfyUI. Once the frontend loads, each tab shows its page label
(for example `AI-6DEF`) at the top center of the window — that label
identifies the tab in the context envelope below.

## Connect an MCP client

The endpoint is `http://127.0.0.1:8188/mcp` — the ComfyUI port, Streamable
HTTP transport, stateless. Adjust the host/port if your ComfyUI listens
elsewhere.

### Claude Code

```bash
claude mcp add --transport http comfyui http://127.0.0.1:8188/mcp
```

Then ask: *"what is selected in ComfyUI?"* — Claude reads it through
`get_selection`.

### OpenCode

In `opencode.json`:

```json
{
  "mcp": {
    "comfyui": {
      "type": "remote",
      "url": "http://127.0.0.1:8188/mcp"
    }
  }
}
```

### Any MCP client (or plain curl)

```bash
curl -s http://127.0.0.1:8188/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"get_selection","arguments":{}}}'
```

Supported methods: `initialize`, `tools/list`, `tools/call`, `ping`.
Notifications get `202 Accepted`; `GET`/`DELETE` get `405 Allow: POST`;
bodies are capped at 64 KiB. No sessions, no SSE — one JSON-RPC message per
POST.

## Tools

### `get_selection`

Read-only, no arguments. Returns the context envelope (below) as compact
JSON in `content[0].text`. An absent snapshot (`available: false`) is a
normal result: no tab has published yet.

### `set_widget_text`

Inserts `text` into `widget` of the **currently selected node** on the
**active page**. All four parameters are required:

| Parameter          | Type    | Meaning                                              |
| ------------------ | ------- | ---------------------------------------------------- |
| `widget`           | string  | Exact widget name as `get_selection` reports it      |
| `text`             | string  | Text to insert (≤ 8192 chars)                        |
| `expected_revision`| integer | Revision you last saw — forces a read before a write |
| `expected_page`    | string  | `active_page.page_id` you last saw                   |

The revision and page checks make writes safe against a stale plan: if the
user edited something or switched tabs between your read and your write,
the call is refused with the current revision so you can re-read and retry.

Results (`content[0].text`, compact JSON):

- success — `{"status":"applied","revision":<new>,"widget":<name>}` when the
  page confirmed the edit within the bounded wait (≈3 s), or
  `{"status":"queued",...}` when the command was dispatched but no
  confirmation arrived in time;
- refusal (`isError: true`) — `invalid params …`, `no selection available`,
  `no node selected`, `multiple nodes selected`, `unknown widget`,
  `widget is not text-like`, `no active page`, `active page changed`,
  `revision mismatch` (the last two carry `current_revision` for a cheap
  retry).

Only text-like widgets (type `text`, `customtext` or `string`) of a singly
selected node can be written. Nothing else mutates: no queue, no graph
structure, no settings, no files.

## HTTP API (for scripts and debugging)

All responses carry `Cache-Control: no-store`. Bodies are capped at 128 KiB.

- `GET /ai-assistant/context` — the context envelope. Poll this per
  question if your consumer is not an MCP client.
- `POST /ai-assistant/context` — anonymous snapshot channel: submit a
  normalized snapshot object (`schema_version`, `captured_at`, `revision`,
  `workflow`, `selection`) exactly as the frontend builds it; useful for
  testing consumers without a browser. Does not create registry pages.
- `WS /ai-assistant/ws` — the frontend channel (below).

### Context envelope

```json
{
  "schema_version": "comfyui.ai-assistant.context/1",
  "available": true,
  "received_at": "2026-08-29T12:00:00.123456+00:00",
  "snapshot": {
    "schema_version": "comfyui.ai-assistant.context/1",
    "captured_at": "2026-08-29T12:00:00.120000+00:00",
    "revision": 7,
    "workflow": {
      "id": "9",
      "title": "ace_step remix",
      "selected_count": 1,
      "selection_detail": "full",
      "selected_limit": 1,
      "selection_truncated": false
    },
    "selection": [
      {
        "id": "107",
        "comfyClass": "CLIPTextEncode",
        "title": "Song Tags",
        "type": "CLIPTextEncode",
        "mode": 0,
        "widgets": [
          { "name": "text", "type": "text", "value": "K-Pop Girl Group, …",
            "truncated": false, "redacted": false, "unsupported": false }
        ],
        "inputs": [
          { "name": "clip", "type": "CLIP",
            "link": { "link_id": "42", "origin_id": "11", "origin_slot": 0,
                      "target_id": "107", "target_slot": 0, "type": "CLIP" } }
        ],
        "outputs": [
          { "name": "CONDITIONING", "type": "CONDITIONING", "links": [] }
        ]
      }
    ]
  },
  "pages": [
    { "page_id": "6def0a1b-2c3d-4e5f-8a9b-0c1d2e3f4a5b",
      "page_label": "AI-6DEF", "workflow_name": "ace_step remix",
      "connected": true }
  ],
  "active_page": { "page_id": "6def0a1b-…", "page_label": "AI-6DEF", "connected": true }
}
```

Notes:

- `selection_detail` is `full` for a single selection, `names_only` for a
  multiple selection (bounded node names, no ids/params/widgets/links),
  `none` for an empty selection — an empty selection is a *valid* snapshot.
- `revision` grows monotonically per page on every published change; use it
  with `page_id` for the `set_widget_text` checks.
- Values are bounded and normalized: strings over 512 chars truncate to
  `<truncated>`, non-JSON shapes become `<unsupported>`, and widget names
  containing terms like `password`, `token`, `api_key` are reported with
  `"<redacted>"` values.
- `active_page` is the most recently focused connected tab; when it
  disconnects, the next remaining tab takes over immediately (socket
  liveness — no timeouts).

### WebSocket channel (frontend ↔ server)

JSON text frames, one object per frame. The frontend sends `register`
(`{type, page_id}`), `snapshot` (`{type, page_id, snapshot}` — the
normalized snapshot object), `activate` (`{type, page_id}`, on focus and
visibility change), and receives `registered` / `accepted` (with the
revision) / `activated` acks, `error` frames that never close the socket,
and command frames `{type: "command", command_id, op: "set_widget_text",
widget, text}` carrying the write. The registry holds at most 8 pages;
disconnected pages are evicted first.

## Security and privacy boundary

- The plugin **stores snapshots only in process memory** and never writes
  them to disk, never logs them, never echoes them anywhere except to the
  asking consumer.
- The plugin **makes no outbound network requests**. The WebSocket is
  same-origin inbound; consumers pull context.
- Secret-like widget names are **redacted in the frontend** before the
  snapshot leaves the page; values never travel for them.
- The endpoints ride the ComfyUI port and are reachable wherever ComfyUI is
  reachable — including LAN exposure if you run ComfyUI that way. They
  expose exactly the bounded selection snapshot and the one guarded write;
  there is no authentication, so treat the port as trusted (default:
  localhost).
- The only mutation surface is the `set_widget_text` carve-out described
  above. `AGENTS.md` records this boundary for contributors.

## Limitations

- Writes target text-like widgets only, a single selection only, the active
  page only.
- `"queued"` means dispatched without confirmation in the bounded wait —
  check the revision or re-read before retrying; a concurrent manual edit
  can also advance the revision and be reported as `"applied"`.
- The MCP subset is stateless by design: no sessions, no SSE streams, no
  server-initiated notifications, no batching.

## Development

```bash
python -m unittest discover -s tests -v   # backend (needs aiohttp)
ruff check ai_assistant tests             # lint
ruff format --check ai_assistant tests    # format
npm test                                  # frontend (node:test)
```

The backend core (`pages`, `commands`, `mcp_protocol`) is stdlib-only and
fully testable offline; `server.py` is covered through aiohttp test
utilities. Design decisions and their history live in `docs/roadmaps/`.

## License

MIT — see [LICENSE](LICENSE).