docs: workbench v2 design spec — persistence, desktop layout, packaging
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
# 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`
|
||||
|
||||
```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:
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```html
|
||||
<!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
|
||||
|
||||
```toml
|
||||
[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
|
||||
```
|
||||
Reference in New Issue
Block a user