Skip to main content
Glama
README.md
# hyperpanes-mcp

[![CI](https://github.com/Eyalm321/hyperpanes-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Eyalm321/hyperpanes-mcp/actions/workflows/ci.yml)

An [MCP](https://modelcontextprotocol.io) server for **hyperpanes** — the Electron
terminal-workspace app (one window of tiled, labeled, color-framed terminal panes). It lets
an agent **compose and launch** workspace layouts and **drive a running instance** — read,
spawn, and arrange panes, stream their output, and (guarded) type into live shells — from
Claude, Cursor, or any MCP client.

It works at two levels, and you can use either without the other:

| Level | Needs a running app? | What it does |
|---|---|---|
| **Compose & launch** | No | Generate/validate a workspace config and shell out to `hyperpanes` to open it. |
| **Live control** | Yes | Talk to the app's loopback control API to inspect/drive panes and stream output. |

> **Live control is off by default.** The app's control API is a loopback (`127.0.0.1`),
> token-authenticated server that only listens once you enable Preferences → **"Allow agent
> control"** in hyperpanes. Typing into shells (`send_input`) is gated further still — see the
> [send_input safety model](#send_input-safety-model).

## Tools

### Compose & launch (no running app needed)

| Tool | Description |
|---|---|
| `list_layouts` | List the tab layouts (`auto`, `single`, `columns`, `rows`, `grid`, `main-stack`) with descriptions. |
| `validate_workspace` | Validate a workspace spec against the schema. Returns `{ valid, errors?, summary }` (window/tab/pane counts). |
| `build_workspace` | Validate + return canonical workspace JSON; optionally write it to a `.json` file; include the equivalent `hyperpanes` CLI command when losslessly expressible. |
| `launch_workspace` | Launch `hyperpanes` with a workspace, from a `.json` path or an inline spec. Defaults to a lossless temp-file launch; `mode:"cli"` compiles to flags. Needs `HYPERPANES_BIN`. |

### Inspect a running instance

| Tool | Description |
|---|---|
| `control_status` | Is the control API reachable? Reports app pid/version, whether the app allows input, the bridge-side `send_input` gate, and the `control.json` path. **Call this first.** |
| `list_panes` | All panes across windows/tabs, with status, activity (busy/idle/exited), org metadata, tab/window context, and each pane's output-resource URI. |
| `read_pane` | A pane's terminal output. `mode:"screen"` = the **rendered cell grid** (clean TUI transcript, no overdraw/spinner spam); `tail` = last N lines; `strip` = ANSI-stripped raw text; `waitForIdle` = **block until the pane goes output-quiet** (`settleMs`/`timeoutMs`) so you read a reply without polling; `since` = a byte cursor for **delta reads** (only new output). Every read returns the current `cursor`. |
| `read_messages` | Drain a pane's durable message inbox past a cursor (`after` = highest seq seen). |
| `whoami` | Identify the pane this bridge is running inside (`HYPERPANES_PANE_ID`) and its org metadata — so a manager-agent-in-a-pane can learn who it is before driving sub-workers. |

### Drive panes

| Tool | Description |
|---|---|
| `open_pane` | Open a new pane in a window's active tab (defaults to the first window). Returns the new `paneId`; accepts `meta` (org metadata) and `env` (e.g. a scoped token) at spawn. Pass `args` (a string array) to run `command` **directly** with that verbatim argv — no shell, no re-parse — the reliable way to pass arguments containing spaces/quotes (e.g. `command:"claude", args:["--append-system-prompt","…persona…"]`). |
| `set_layout` | Set a tab's tiling layout (defaults to the first window's active tab). |
| `focus_pane` | Focus a pane (and its tab/window). |
| `close_pane` | Close a pane, terminating its shell. |
| `restart_pane` | Kill and respawn a pane's shell. |
| `rename_pane` | Change a pane's label and (optionally) subtitle, live. |
| `recolor_pane` | Change a pane's frame color, live (any CSS color). |
| `set_meta` | Attach/update a pane's free-form metadata (merge; `null` deletes a key). How an orchestrator records the org chart as data. |
| `set_talk` | Turn per-pane talk on/off — while enabled, the pane speaks NEW Claude assistant replies aloud via local TTS. Off by default. |

### Speech (TTS)

Global controls over the app's text-to-speech output (talk is opted into per-pane via `set_talk` above).

| Tool | Description |
|---|---|
| `set_speech` | Set global speech settings: `muted` mutes/unmutes all speech, `focusedOnly` restricts speaking to the focused pane. Provide at least one. |
| `stop_speech` | Immediately kill any in-flight speech utterance and clear the backlog. |

### Send input

All three **type into a live shell** — they run whatever you send in a real terminal. Triple-gated
and never on by default. See the [safety model](#send_input-safety-model).

| Tool | Description |
|---|---|
| `send_input` ⚠️ | Type text. `submit:true` writes your text, then a **separate** Enter a beat later — the reliable way to submit a TUI line (a trailing `\n` in one write is read as a bracketed paste, not Enter). |
| `send_keys` ⚠️ | Send **named keys** as the right terminal bytes: `enter`, `escape`, `tab`, `shift+tab`, arrows, `home`/`end`, `pageup`/`pagedown`, `backspace`, `delete`, `space`, `ctrl+<letter>`. For menus, y/n & trust prompts, and cancelling. |
| `prompt_pane` ⚠️ | **One full turn in one call**: type → submit → wait for the pane to settle → return the rendered transcript + whether it's now `awaitingInput`. The way to converse with a TUI agent in a pane. |

### Driving an interactive TUI agent

Two supported patterns for talking to an agent running **inside** a pane (e.g. a live `claude`):

- **Structured bus (preferred when the agent is MCP-capable).** If the pane-agent also has the
  hyperpanes MCP, converse over its **inbox** (`send_message` / `send_to_parent` / `read_messages`)
  — a clean, structured channel, no screen-scraping. Run such workers with an inbox-poll loop
  ("listening agent") so they pick messages up unprompted.
- **TUI scrape (for any agent, incl. an interactive `claude` that won't poll its inbox).** Drive
  the terminal directly. The one-call path is **`prompt_pane`**; under the hood that's
  `send_input({ submit:true })` to type a line, `read_pane({ waitForIdle:true })` to block until the
  reply lands, and `read_pane({ mode:"screen" })` to read it back cleanly. Use `send_keys(["enter"])`
  to clear a first-run **trust dialog**, and watch `awaitingInput` on the result to know when the
  agent is blocked on a prompt rather than done.

### Agent orchestration

Turn the control plane into a substrate for an LLM **agent org** — one orchestrator driving
worker panes, or a recursive manager→worker tree. Hierarchy is **data** (`meta.parent`), the
message bus is hierarchy-agnostic, and tokens scope what a child can reach.

| Tool | Description |
|---|---|
| `send_message` | Enqueue a structured message to a pane's durable inbox (at-least-once delivery). |
| `send_to_parent` | Message this pane's org parent (resolved from `meta.parent`). |
| `broadcast_subtree` | Message every pane in an org subtree (all panes whose `meta.parent` chain leads back to a root). |
| `mint_token` | Mint a subtree-scoped control token (no escalation) to hand a child via `open_pane` env — the child controls only its subtree and never sees the master token. |
| `lock_pane` | Take an advisory write lock so only the holder can `send_input` until it expires. |
| `unlock_pane` | Release an advisory write lock you hold. |

### Work queue

A durable, SQLite-backed **job queue** behind the control server — fan work out to competing
workers with at-least-once delivery and leases. A `claim` returns each task's `fencingToken`;
every `ack`/`nack`/`extend` must carry it (lease fencing, so a reaped worker can't clobber a
task another worker now owns).

| Tool | Description |
|---|---|
| `list_queues` | Every queue and its per-state depth. |
| `enqueue_task` | Append a task (`payload` + optional `kind`/`title`/`priority`/`maxAttempts`/`visibilityTimeoutMs`/`delayMs`/`dedupeKey`). Returns the task `id` + `seq`. |
| `claim_task` | Claim the next task(s) for a `worker` (competing consumers; empty → `{tasks:[]}`). Each task carries a `fencingToken` + `visibilityDeadline`. |
| `ack_task` | Complete a claimed task (carry the `fencingToken`). |
| `nack_task` | Fail a task — re-queued until `maxAttempts`, or `requeue:false` to fail now; `delayMs` backs off. |
| `extend_task` | Heartbeat a long task: extend its lease so it isn't reaped. |
| `get_task` | Fetch one task (state, payload, attempts, lease, timestamps). |
| `list_tasks` | List/inspect a queue's tasks with a `state` filter + `after`/`limit` paging. |
| `purge_queue` | Drop terminal tasks (done/failed/dead) for cleanup. |

## Resources

Pane output and inboxes are exposed as **subscribable MCP resources** — read for a snapshot,
subscribe for a live stream (the bridge consumes the app's `/events` WebSocket and emits
`resources/updated` / `resources/list_changed` notifications):

| Resource URI | Content |
|---|---|
| `hyperpanes://pane/{paneId}/output` | Terminal output — scrollback on read, deltas on subscribe (`text/plain`). |
| `hyperpanes://pane/{paneId}/messages` | The pane's durable message inbox — JSON on read, live deliveries on subscribe. |

## Installation

The server runs over stdio and is launched by your MCP client.

### Claude Desktop / generic MCP config

```json
{
  "mcpServers": {
    "hyperpanes": {
      "command": "npx",
      "args": ["-y", "hyperpanes-mcp"],
      "env": {
        "HYPERPANES_BIN": "C:/path/to/hyperpanes.exe"
      }
    }
  }
}
```

`HYPERPANES_BIN` is only needed for `launch_workspace`; the live-control tools find the app
via its `control.json` (see [Configuration](#configuration)).

### Claude Code

```bash
claude mcp add hyperpanes -- npx -y hyperpanes-mcp
```

### Install globally

```bash
npm install -g hyperpanes-mcp
hyperpanes-mcp   # runs the stdio server
```

Also published to GitHub Packages as `@eyalm321/hyperpanes-mcp`.

## Configuration

All variables are optional. `launch_workspace` needs a launcher; the live-control tools need
the app running with **"Allow agent control"** enabled.

| Env var | Purpose |
|---|---|
| `HYPERPANES_BIN` | Path to the hyperpanes executable (for `launch_workspace`). No PATH fallback — it fails loudly rather than spawn the wrong process. |
| `HYPERPANES_LAUNCH_ARGS` | Whitespace-separated leading args for the launcher (e.g. a dev runner). |
| `HYPERPANES_CONTROL_FILE` | Override the path to the app's `control.json` (use if the app runs under a non-default data dir). |
| `HYPERPANES_USER_DATA` | Override just the userData dir; `<dir>/control.json` is used. |
| `HYPERPANES_CONTROL_TOKEN` / `HYPERPANES_CONTROL_PORT` | A scoped control token + port for a child pane (set automatically by `mint_token` / `open_pane` env). Used instead of reading `control.json`. |
| `HYPERPANES_PANE_ID` | The pane this bridge runs inside — enables `whoami` and the hierarchy helpers. |
| `HYPERPANES_ALLOW_INPUT` | `1`/`true` to permit `send_input` on this bridge (off by default). |
| `HYPERPANES_INPUT_ALLOWLIST` | Comma-separated pane ids or labels allowed to receive input. |

Default `control.json` locations:

- **Windows:** `%APPDATA%\hyperpanes\control.json`
- **macOS:** `~/Library/Application Support/hyperpanes/control.json`
- **Linux:** `$XDG_CONFIG_HOME/hyperpanes/control.json` (or `~/.config/hyperpanes/…`)

## send_input safety model

> `send_input` types into **live shells** — it runs whatever you send in a real terminal. It
> is the sharp edge of this server and is **never on by default**. Three independent gates,
> all required:

1. **App-side (enforced by hyperpanes):** the control server is loopback + token, **disabled
   by default**, and `send_input` returns **403 unless** "Allow agent control → input" is on.
   The bridge cannot bypass this.
2. **Bridge opt-in:** refused unless **`HYPERPANES_ALLOW_INPUT=1`** is set in this server's
   environment. Optionally **`HYPERPANES_INPUT_ALLOWLIST`** restricts which panes accept input.
3. **Per-call confirmation:** every call must pass **`confirm: true`**.

`control_status` surfaces all three (`appAllowsInput` + `inputGate`) so a refusal is always
explainable.

## Workspace schema

A faithful mirror of the app's `WorkspaceFile`. The canonical shape is nested; the legacy
single-window fields are kept for back-compat, and everything normalizes through one
`windowsOf` funnel (windows[] verbatim → groups[] as one window → panes[] as one window/tab).

```
WorkspaceFile { name?, layout?, panes?, groups?, active?, windows? }
WindowSpec    { title?, active?, bounds?, groups[] }
GroupSpec     { title?, layout?, panes[], sizes?, mainFraction?, focused?, zoomed? }   // a tab
PaneSpec      { label?, subtitle?, color?, command?, cwd?, shell?, fontSize? }
Layout        = auto | single | columns | rows | grid | main-stack
```

- **Launch modes.** `launch_workspace` defaults to writing a temp `.json` (**lossless**).
  `mode:"cli"` compiles to `--window`/`--tab`/`-c …` flags — convenient but **lossy**: window
  bounds, the active-tab index, pane subtitle, split sizes, and command-less panes are JSON-only
  and reported in `lossy`.
- **Relative `cwd`.** In a workspace *file*, relative `cwd` resolves against the file's dir.
  Inline specs are written to a temp file — prefer absolute `cwd` for inline specs.
- **Strict validation.** Unknown keys are rejected (typo guard), `layout` must be a known id,
  `fontSize` a positive integer, and a workspace must declare at least one pane.

See [`examples/dev.workspace.json`](examples/dev.workspace.json) for a full two-window spec.

## Development

```bash
npm install
npm run build      # tsc -> dist/
npm test           # vitest (pure units; no running app needed)
npm run test:watch
npm run dev        # tsx src/index.ts
node scripts/smoke.mjs   # end-to-end stdio check (no app needed)
```

The unit tests mirror the app's own `workspace.test.ts` / control read-model cases, so a
contract drift in the app surfaces here as a test failure.

## Architecture

```
src/
  index.ts          # stdio entrypoint
  server.ts         # creates the MCP server; registers compose/launch tools + wires control tools
  schema.ts         # workspace schema (zod) + windowsOf/summarize — mirrors the app's workspace.ts
  compile-cli.ts    # WorkspaceFile -> hyperpanes CLI argv (inverse of the app's parseCli)
  launch.ts         # launcher resolution + launch planning/execution
  control-tools.ts  # live-control + orchestration tools, and the subscribable pane resources
  control/
    discovery.ts    # locate + parse control.json (and scoped-token env)
    client.ts       # HTTP client for the control API (state/output/input/command/messages/tokens/locks)
    model.ts        # read-model types + pure helpers (flatten, resolve, URIs, whoami, subtree)
    subscriptions.ts# /events WebSocket -> MCP resource notifications
    input-gate.ts   # send_input gating (opt-in + confirm + allowlist)
scripts/smoke.mjs   # end-to-end stdio check
examples/           # sample workspace files
```

## Releasing

CI runs the build + tests on every push and PR to `main` (Node 20 & 22). Publishing is
triggered by creating a **GitHub Release**, which publishes to **both** registries:

- **npm** as the unscoped package `hyperpanes-mcp`
- **GitHub Packages** as `@eyalm321/hyperpanes-mcp`

### One-time repo setup

1. Add an `NPM_TOKEN` repository secret (an npm **automation** token). `GITHUB_TOKEN` is
   provided automatically for GitHub Packages.
2. To release: `npm version <patch|minor|major>`, push with `--follow-tags`, then create a
   GitHub Release for the tag (e.g. `v0.1.1`). The `publish` workflow builds, tests, and
   publishes to both registries.

## License

MIT © Eyalm321

TDQS

A3.6/5.0

Scored across 47 tools

Disambiguation4/5

Most tools are clearly distinct, but there is some potential overlap between messaging/input tools like send_message, queue_prompt, prompt_pane, and send_input, which all involve sending text to a pane. Descriptions are detailed enough to separate them, but the boundaries are not perfectly obvious at a glance.

Naming Consistency4/5

The naming convention is predominantly verb_noun in snake_case (e.g., launch_workspace, lock_pane, enqueue_task), which is consistent. Minor deviations like whoami and send_to_parent break the pattern slightly, but overall the convention is predictable and readable.

Tool Count2/5

With 47 tools, this server is far above the typical well-scoped range of 3-15. While the tool count reflects a broad feature set (panes, workspaces, queues, messaging, projects, speech), it exceeds the threshold where usability suffers, making it feel bloated and harder to navigate.

Completeness4/5

The tool set covers the domain comprehensively: pane lifecycle, workspace management, queue operations, project management, messaging, and app control. Minor gaps exist (e.g., no explicit window/tab listing tool, no move/resize pane), but agents can work around them using existing tools, so the surface is largely complete.

Maintenance

ActivitySlowing
ResponsivenessUnresponsive