Files
workbench-server/docs/superpowers/specs/2026-03-30-workbench-v2-design.md

12 KiB

Workbench Server v2 Design Spec

Date: 2026-03-30 Status: Draft Repo: https://git.sethpc.xyz/Seth/workbench-server

Summary

Workbench is an MCP server that lets any AI CLI create and control interactive web pages served over LAN. The AI pushes arbitrary HTML/CSS/JS to a browser page via WebSocket. Use cases: hardware diagnostics, guided procedures, dashboards, data collection — anything that benefits from a visual display surface the AI can control.

v2 adds: server persistence (survives AI CLI restarts), pip-installable packaging, AI-guided setup (INSTALL.md/START.md), and a simplified desktop-focused layout (no embedded terminal panel).

What Changes From v1

Aspect v1 v2
Server persistence In-memory only — lost on restart .server.json on disk, reconnect on startup
Layout Split: diagnostic panel + sethmux iframe Full-width display surface, no terminal
Mobile support Responsive stacking Desktop only
Dependencies mcp, aiohttp mcp, aiohttp (unchanged)
Packaging Raw script pip-installable package
Setup Manual AI-guided (INSTALL.md → START.md)
tmux/sethmux Embedded iframe Removed entirely

What Stays The Same

  • MCP server on stdio transport
  • 6 MCP tools: workbench_scaffold, workbench_state, workbench_log, workbench_read_log, workbench_list, workbench_stop
  • HTTP + WebSocket per project (aiohttp)
  • AI pushes arbitrary HTML/CSS/JS — no widget system, full creative freedom
  • Dual-format session logging (session.md + session.jsonl)
  • Project directories at ~/workbench/<name>/
  • LAN-native, no cloud, no auth
  • Cost tracking (cost-log.jsonl)

Architecture

┌─────────────────────┐         ┌──────────────────────────────┐
│  AI CLI terminal    │         │  Browser (desktop)           │
│                     │         │                              │
│  AI calls MCP tools │  stdio  │  ┌────────────────────────┐  │
│  workbench_state  ──┼────┐    │  │  AI-generated content  │  │
│  workbench_log      │    │    │  │  HTML / CSS / JS       │  │
│  etc.               │    │    │  │  updated live via WS   │  │
│                     │    ▼    │  │                        │  │
│                     │  MCP    │  │  Schematics, tables,   │  │
│                     │  server │  │  checklists, dashboards│  │
│                     │    │    │  │  — anything the AI     │  │
│                     │    │    │  │    decides to build     │  │
│                     │    ▼    │  ├────────────────────────┤  │
│                     │  HTTP + │  │  Log feed              │  │
│                     │  WS on  │  └────────────────────────┘  │
│                     │  LAN    │  ● Connected │ project-name  │
└─────────────────────┘         └──────────────────────────────┘

Persistence Mechanism

Problem

The MCP server runs as a subprocess of the AI CLI. When the AI restarts, the MCP server restarts, and all in-memory state (running HTTP servers, WebSocket clients) is lost. The AI then calls workbench_scaffold again and starts a duplicate server.

Solution

Persist server state to disk. On startup and on each workbench_scaffold call, check for an existing running server before starting a new one.

Per-project server file: ~/workbench/<name>/.server.json

{
  "pid": 12345,
  "port": 8070,
  "started": "2026-03-30T10:00:00-04:00"
}

workbench_scaffold flow

workbench_scaffold(name, title)
  │
  ├─ Project dir exists?
  │   ├─ No → create dir, write scaffold HTML, init logs
  │   └─ Yes → keep existing files
  │
  ├─ .server.json exists?
  │   ├─ No → start new HTTP server, write .server.json
  │   └─ Yes → read port from file
  │           ├─ HTTP GET localhost:<port> returns 200?
  │           │   ├─ Yes → server is alive, reattach (add to active_projects)
  │           │   └─ No → server is dead, clean up .server.json, start new server
  │           └─ PID still running? (fallback check)
  │
  └─ Return {"path": "...", "url": "http://<lan-ip>:<port>"}

MCP server startup (reconnect)

When the MCP server process starts (before any tool calls), scan for running servers:

for project_dir in ~/workbench/*/:
    server_file = project_dir / ".server.json"
    if server_file.exists():
        info = json.loads(server_file.read_text())
        if is_server_alive(info["port"]):
            active_projects[name] = {"port": info["port"], ...}

This means: AI CLI restarts → MCP server restarts → immediately knows about all running project servers → workbench_list and workbench_state work without calling workbench_scaffold again.

workbench_stop flow

workbench_stop(project)
  │
  ├─ Log session end to cost-log.jsonl
  ├─ Stop HTTP server (runner.cleanup())
  ├─ Delete .server.json
  └─ Remove from active_projects

Scaffold HTML (v2)

Full-width desktop layout. No split pane, no iframe, no terminal embed.

<!DOCTYPE html>
<html>
<head>
  <title>{{TITLE}}</title>
  <style>
    :root {
      --bg: #0a0f0c;
      --panel-bg: #111a15;
      --border: #2a3a2e;
      --text: #aaccaa;
      --text-dim: #557755;
      --accent: #33ff66;
    }
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      background: var(--bg);
      color: var(--text);
      font-family: monospace;
      font-size: 14px;
      height: 100vh;
      display: flex;
      flex-direction: column;
    }
    #content {
      flex: 1;
      overflow-y: auto;
      padding: 16px;
    }
    #log-feed {
      border-top: 1px solid var(--border);
      padding: 8px 12px;
      max-height: 200px;
      overflow-y: auto;
    }
    #log-feed h3 {
      color: var(--text-dim);
      font-size: 11px;
      letter-spacing: 2px;
      text-transform: uppercase;
      margin-bottom: 4px;
    }
    .log-entry {
      font-size: 12px;
      color: var(--text-dim);
      padding: 2px 0;
    }
    .log-entry .log-time {
      color: var(--accent);
      margin-right: 8px;
    }
    #status {
      background: var(--panel-bg);
      border-top: 1px solid var(--border);
      padding: 4px 12px;
      font-size: 11px;
      color: var(--text-dim);
    }
    #status.connected { color: var(--accent); }
  </style>
</head>
<body>
  <div id="content">
    <h1 style="color: var(--accent); font-size: 18px;">{{TITLE}}</h1>
    <p style="color: var(--text-dim); margin-top: 8px;">{{DESCRIPTION}}</p>
    <p style="color: var(--text-dim); margin-top: 16px;">Waiting for content...</p>
  </div>
  <div id="log-feed">
    <h3>Session Log</h3>
    <div id="log-entries"></div>
  </div>
  <div id="status">Connecting...</div>

  <script>
    const WS_URL = `ws://${location.host}/ws`;
    let ws;

    function connect() {
      ws = new WebSocket(WS_URL);
      ws.onopen = () => {
        document.getElementById('status').textContent = 'Connected';
        document.getElementById('status').className = 'connected';
      };
      ws.onclose = () => {
        document.getElementById('status').textContent = 'Disconnected — reconnecting...';
        document.getElementById('status').className = '';
        setTimeout(connect, 2000);
      };
      ws.onmessage = (e) => {
        const msg = JSON.parse(e.data);
        if (msg.type === 'state') handleState(msg.state);
        if (msg.type === 'log') handleLog(msg.entry);
      };
    }

    function handleState(state) {
      if (state.template) {
        document.getElementById('content').innerHTML = state.template;
      }
      if (state.styles) {
        let el = document.getElementById('dynamic-styles');
        if (!el) { el = document.createElement('style'); el.id = 'dynamic-styles'; document.head.appendChild(el); }
        el.textContent = state.styles;
      }
      if (state.script) {
        try { new Function(state.script)(); } catch(e) { console.error('Script error:', e); }
      }
      window.__workbench_state = state;
      try { localStorage.setItem('workbench-state', JSON.stringify(state)); } catch(e) {}
    }

    function handleLog(entry) {
      const div = document.createElement('div');
      div.className = 'log-entry';
      const time = new Date().toLocaleTimeString();
      div.innerHTML = `<span class="log-time">${time}</span>${entry}`;
      const feed = document.getElementById('log-entries');
      feed.appendChild(div);
      feed.scrollTop = feed.scrollHeight;
    }

    // Restore state from localStorage on load
    try {
      const saved = JSON.parse(localStorage.getItem('workbench-state'));
      if (saved) handleState(saved);
    } catch(e) {}

    connect();
  </script>
</body>
</html>

Key changes from v1:

  • No split layout, no divider, no term-panel, no iframe
  • Full-width #content area
  • Flexbox column layout (content grows, log and status at bottom)
  • No mobile media queries
  • Same WebSocket reconnect logic
  • Same localStorage state persistence

MCP Tools (unchanged API)

The 6 tools keep the same interface. Only internal behavior changes for persistence.

workbench_scaffold

Same parameters: name, title, description. New behavior: checks .server.json before starting a new server (see Persistence Mechanism above). Returns: {"path": "...", "url": "http://<lan-ip>:<port>"} (unchanged).

workbench_state

Unchanged. Pushes JSON to browser via WebSocket. template, styles, script fields.

workbench_log

Unchanged. Appends to session.md and session.jsonl, pushes to browser log feed.

workbench_read_log

Unchanged. Returns recent log entries from session.jsonl.

workbench_list

Same return format, but now also checks .server.json files to detect servers that survived an MCP restart.

workbench_stop

Same behavior, plus deletes .server.json on stop.

Source Structure

workbench-server/
  pyproject.toml
  README.md
  INSTALL.md
  LICENSE
  src/
    workbench/
      __init__.py
      __main__.py         # python -m workbench
      cli.py              # CLI: mcp, serve, list, help
      server.py           # MCP server + HTTP/WS management + persistence
      project.py          # Project dir management, logging
      scaffold.html       # HTML template
  tests/
    conftest.py
    test_project.py
    test_persistence.py
    test_server.py

Packaging

[build-system]
requires = ["setuptools>=68.0"]
build-backend = "setuptools.build_meta"

[project]
name = "workbench-server"
version = "0.1.0"
description = "MCP server that lets AI CLIs build interactive web pages served over LAN"
requires-python = ">=3.10"
dependencies = [
    "mcp>=1.26.0",
    "aiohttp>=3.9.0",
]

[project.scripts]
workbench = "workbench.cli:main"

INSTALL.md

Same AI-guided pattern as kitty-workbench, but simpler (no terminal detection needed):

  1. Clone + pip install
  2. Detect platform and browser availability
  3. Configure MCP for user's AI CLI
  4. SSH ControlMaster setup (if applicable)
  5. Smoke test
  6. Write ~/workbench/START.md

README.md

Public-facing. Setup is "paste the URL" or "read INSTALL.md". Desktop browser as the display surface. No mention of kitty or terminal splitting. Examples: hardware diagnostics, guided procedures, data collection.

CLI Interface

workbench mcp              # start MCP server (stdio transport)
workbench serve <name>     # serve a project without MCP (standalone)
workbench list             # list projects
workbench help             # usage