MCP server for hyperpanes — compose/validate/launch terminal-workspace layouts and drive a running instance (panes, layout, streaming output, guarded input)
hyperpanes-mcp
An MCP 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.
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. |
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.
| 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
claudethat won't poll its inbox). Drive the terminal directly. The one-call path isprompt_pane; under the hood that'ssend_input({ submit:true })to type a line,read_pane({ waitForIdle:true })to block until the reply lands, andread_pane({ mode:"screen" })to read it back cleanly. Usesend_keys(["enter"])to clear a first-run trust dialog, and watchawaitingInputon 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. |
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
{
"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).
Claude Code
claude mcp add hyperpanes -- npx -y hyperpanes-mcp
Install globally
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_inputtypes 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:
- App-side (enforced by hyperpanes): the control server is loopback + token, disabled
by default, and
send_inputreturns 403 unless "Allow agent control → input" is on. The bridge cannot bypass this. - Bridge opt-in: refused unless
HYPERPANES_ALLOW_INPUT=1is set in this server's environment. OptionallyHYPERPANES_INPUT_ALLOWLISTrestricts which panes accept input. - 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_workspacedefaults 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 inlossy. - Relative
cwd. In a workspace file, relativecwdresolves against the file's dir. Inline specs are written to a temp file — prefer absolutecwdfor inline specs. - Strict validation. Unknown keys are rejected (typo guard),
layoutmust be a known id,fontSizea positive integer, and a workspace must declare at least one pane.
See examples/dev.workspace.json for a full two-window spec.
Development
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
- Add an
NPM_TOKENrepository secret (an npm automation token).GITHUB_TOKENis provided automatically for GitHub Packages. - To release:
npm version <patch|minor|major>, push with--follow-tags, then create a GitHub Release for the tag (e.g.v0.1.1). Thepublishworkflow builds, tests, and publishes to both registries.
License
MIT © Eyalm321