Skip to main content
Glama
README.md
# Easel

![license: MIT](https://img.shields.io/badge/license-MIT-blue) ![node >= 22](https://img.shields.io/badge/node-%3E%3D22-brightgreen) ![MCP](https://img.shields.io/badge/MCP-Streamable%20HTTP-7c5cff)

A **local, human-in-the-loop MCP server** that bridges an AI agent to Higgsfield's
web image generator. The agent asks for an image; a **human** generates it by hand
in the Higgsfield web UI and pastes the result back through a small dashboard; the
agent picks it up by polling. Nothing is generated automatically — this is a manual
bridge so you can use your Higgsfield **web** subscription and features.

```
  AI agent ──request_image_generation──▶ [ bridge daemon ] ──live──▶ 🧑 dashboard (localhost)
     │                                         │                         │  copy prompt
     │  (does other work, then polls)          │  SQLite queue           │  generate in Higgsfield
     ◀──────get_image_result (long-poll)───────┤                         │  paste result back
                       image ◀─────────────────┴─────────────────────────┘  Mark complete
```

## How it works (the agent's contract)

1. The agent calls **`request_image_generation`** with a prompt + context. It returns a
   `job_id` **immediately** — it never blocks, and never returns an image.
2. The agent goes off and does other work, then polls **`get_image_result(job_id)`**
   every 5–10s. That call **long-polls** (holds up to ~25s waiting for a change), so
   loose polling is cheap and a "call once" agent still tends to catch the result.
3. You (the human) see the request appear live on the dashboard, copy the prompt, open
   Higgsfield, generate the image, and **paste / drag / upload** it back, then
   **Mark complete**. You can also **Reject** with a reason (the agent sees it and can
   rephrase) or **Dismiss** (cancel).
4. The next poll returns the image as an absolute **file path** and, for images under
   ~1 MB, an inline **MCP image content block** the agent can see directly.

This process is written into every tool's description, so the calling agent knows to
stay non-blocking and poll.

## Requirements

- **Node.js ≥ 22** (uses the built-in `node:sqlite`; 24+ recommended). It will refuse
  to start on older Node with a clear message.
- macOS gets a native desktop notification on each new request; every platform also
  logs a clickable dashboard URL to the daemon's terminal.

## Install & build

```bash
cd ~/easel
npm install
npm run build
```

## Run the daemon

Start it once and leave it running (it must outlive individual agent sessions so the
queue and dashboard survive):

```bash
npm start
# Dashboard:   http://127.0.0.1:7391/
# MCP (http):  http://127.0.0.1:7391/mcp
```

Open the dashboard in a browser and keep the tab around (click **Enable alerts** for
desktop notifications). If you forget to start the daemon, the agent's tool calls fail
with a plain connection error — start it and retry.

### Register it with your MCP client

Add to your client's MCP config (e.g. `.mcp.json` for Claude Code):

```jsonc
{
  "mcpServers": {
    "easel": { "type": "http", "url": "http://127.0.0.1:7391/mcp" }
  }
}
```

## MCP tools

| Tool | What it does |
|------|--------------|
| `request_image_generation` | Enqueue a request; returns a `job_id` immediately (non-blocking). Accepts `prompt`, `negative_prompt`, `aspect_ratio`, `style_hints`, `num_variations`, `reference_images` (`url` / local `path` / inline `data_base64`), `notes`, `requested_by`, `supersedes_job_id`, `idempotency_key`. |
| `get_image_result` | Poll a `job_id`. Long-polls ~25s. Returns `queued` / `in_progress` / `completed` / `rejected` / `cancelled` / `not_found`. On `completed`, returns file paths + inline image content blocks. |
| `cancel_image_generation` | Cancel a queued/in-progress request (idempotent). |
| `list_pending_jobs` | List jobs (defaults to `queued` + `in_progress`); for reconciling after a restart or a lost `job_id`. |

## Configuration (env vars)

| Var | Default | Meaning |
|-----|---------|---------|
| `EASEL_PORT` | `7391` | Port for both the dashboard and the MCP endpoint. On conflict the daemon exits (0 if it's already this bridge, 1 if a foreign process). |
| `EASEL_DATA_DIR` | `~/.easel` | Where `bridge.db` and `files/` (reference + result images) live. |

## Design notes

- **One process, one port** serves the MCP endpoint (`/mcp`, stateful Streamable HTTP),
  the dashboard (`/`), its API (`/api/*`), a live SSE stream (`/events`), and stored
  images (`/files/*`, path-traversal-guarded). Bound to `127.0.0.1` with a Host-header
  check.
- **State lives only in SQLite**, keyed by job id (not MCP session) — multiple agent
  sessions share one queue. On restart, any `in_progress` job is reset to `queued`
  (no human is mid-work after a crash).
- **Images**: returned by absolute path always; inline base64 / MCP image blocks only
  under a ~1 MB cap so a huge PNG can't blow up the agent's context. The dashboard
  normalizes pasted/dropped images (canvas resize to ≤2048px) before upload.

## Security

This is a **single-user local tool**. It binds to `127.0.0.1` and checks the `Host`
header; there is no authentication, so **don't expose the port to a network**. It
guards against path traversal, secret exfiltration via reference images (only real
image files are accepted), SSRF via reference URLs (public `http(s)` only), and
`osascript` injection. See [SECURITY.md](SECURITY.md) for the full threat model and
how to report issues.

## Dev

- `npm run dev` — rebuild on change and restart the daemon.
- `npm run typecheck` — `tsc --noEmit` (strict).
- `npm start` — run the built daemon.

The `scripts/` folder has two helpers (start the daemon first; both honor `EASEL_PORT`):

- `node scripts/smoke.mjs` — end-to-end MCP round-trip + security-guard test.
- `node scripts/seed.mjs` — populate the dashboard with a few sample jobs to eyeball the UI.

See [CONTRIBUTING.md](CONTRIBUTING.md) to contribute.

## License

[MIT](LICENSE) © pradeexsu