# webtmux MCP Server

Model Context Protocol server for [webtmux](https://tmux.winds-os.com) — lets an external agent (Hermes, OpenFloor, Claude, etc.) drive a remote tmux workspace: list windows, observe terminal state, type and submit commands, and read chat history.

- **Transport:** Streamable HTTP (MCP spec)
- **Endpoint:** `https://tmux.winds-os.com/mcp`
- **Method:** `POST` for tool calls / requests · `GET` for SSE event stream
- **Protocol version:** MCP `2025-06-18` (any compliant SDK works)
- **Auth:** none at the MCP layer — gate it at your network/proxy if needed

---

## What the agent can do

### Terminal & chat
| Tool | Purpose | Mutates state |
|---|---|---|
| `list_windows` | Flattened list of all tmux windows across webtmux projects/sessions | No |
| `summarize_window` | LLM-summarized recent output of one window | No |
| `read_chat_history` | Paginated raw user+assistant chat records for a window | No |
| `read_chat_messages` | Parsed/cleaned chat view (snapshots → discrete messages, noise stripped) | No |
| `select_window` | Switch tmux focus to a window | Yes (requires `mode: "execute"`) |
| `send_to_window` | Type text into a window; optionally press Enter | Yes (requires `mode: "execute"`) |
| `submit_current_window` | Press Enter in a window without typing anything new | Yes (requires `mode: "execute"`) |

### Notes, tasks, kanban
| Tool | Purpose | Mutates state |
|---|---|---|
| `read_project_notes` | Read project-level notes markdown + parsed tasks + kanban grouping | No |
| `read_window_notes` | Same, scoped to a single tmux window | No |
| `write_project_notes` | Replace project notes markdown | Yes (`mode: "execute"`, refused when source=notion) |
| `write_window_notes` | Replace window notes markdown | Yes (`mode: "execute"`, refused when source=notion) |
| `list_tasks` | Parse notes into tasks grouped by kanban status; optional status filter | No |
| `update_task` | Add / set_status / set_text / remove / move a single task in place | Yes (`mode: "execute"`, refused when source=notion) |

### The `mode` guard

Every write tool takes a required `mode` parameter:

- `mode: "plan"` → **always rejected.** Use this when you are thinking out loud / drafting a plan; the server will refuse to mutate state.
- `mode: "execute"` → only mode that actually performs the action.

This is deliberate — it lets agents reason in two phases without accidentally changing the user's terminal.

---

## Window addressing

All tools that target a window accept the same set of identifiers (provide whichever you have):

| Field | Type | Notes |
|---|---|---|
| `session` | string | tmux session name |
| `windowIndex` | string \| number | tmux window index within the session |
| `windowName` | string | tmux window name |
| `current` | boolean | Resolve to the user's currently focused window |

If multiple are supplied, the server resolves to the most specific match. The result of every tool echoes the resolved window so the agent can confirm it acted on what it intended.

---

## Tool reference

### `list_windows`

**Input:** none.

**Output:**
```json
{
  "windows": [
    {
      "session": "main",
      "windowId": "@7",
      "windowIndex": 2,
      "windowName": "editor",
      "active": true,
      "project": "webtmux"
    }
  ]
}
```

---

### `summarize_window`

LLM-generated summary of the recent terminal output. Lossy — use `read_chat_history` for faithful records.

**Input:**
```json
{ "session": "main", "windowIndex": 2 }
```

**Output:**
```json
{
  "window": { "...": "..." },
  "summary": ["bullet 1", "bullet 2"]
}
```

---

### `read_chat_history`

Faithful chronological record of every user input line + every assistant snapshot for the window. Use this when the agent needs to reconstruct what happened.

**Input:**
```json
{
  "session": "main",
  "windowIndex": 2,
  "limit": 100,
  "before": 1737000000000,
  "since": 1736900000000
}
```

| Field | Required | Default | Meaning |
|---|---|---|---|
| `limit` | no | `50` | Max records (1–500) |
| `before` | no | — | ts cursor; only records older than this. Use the oldest `ts` from the previous page |
| `since` | no | — | ts floor; drop records older than this |

**Output:**
```json
{
  "window": { "...": "..." },
  "messages": [
    { "ts": 1737000000000, "role": "user", "session": "main", "window": "@7", "text": "ls -la" },
    { "ts": 1737000001000, "role": "assistant", "session": "main", "window": "@7", "text": "total 24\\n..." }
  ],
  "hasMore": true
}
```

**Pagination pattern:**
1. Call with `limit: 100`, no `before`.
2. Take the smallest `ts` in `messages`; pass as `before` on the next call.
3. Stop when `hasMore: false` or you have enough.

---

### `read_chat_messages`

Higher-level cleaned view — snapshots collapsed into discrete assistant turns, status footers and separators removed. Best for "what has the user been working on" summaries.

**Input:**
```json
{ "session": "main", "windowIndex": 2, "limit": 100 }
```

**Output:**
```json
{
  "window": { "...": "..." },
  "messages": [
    { "role": "user", "text": "deploy staging", "ts": 1737000000000 },
    { "role": "assistant", "text": "Deploying to staging...\\nDone.", "ts": 1737000005000 }
  ]
}
```

---

### `select_window`

Switch the user's focused window. Requires `mode: "execute"`.

**Input:**
```json
{ "mode": "execute", "session": "main", "windowIndex": 2 }
```

**Output:** `{ "selected": { ...window } }`

---

### `send_to_window`

Type text into a window. The two flags do very different things:

- `submit: false` (default) — types the text into the prompt but does **not** press Enter. The user still has to submit.
- `submit: true` — types the text and presses Enter. **Only use when the user explicitly said send / run / execute / submit / press enter.**

Built-in dedupe: identical (window, text, submit) within ~3s is dropped to prevent double-fires.

**Input:**
```json
{
  "mode": "execute",
  "session": "main",
  "windowName": "editor",
  "text": "git status",
  "submit": false
}
```

**Output:**
```json
{ "window": { "...": "..." }, "submitted": false, "status": "typed" }
```

Possible `status` values: `typed`, `sent`, `duplicate_ignored`.

---

### `submit_current_window`

Press Enter in a window without adding any new text. Use when the user said "send it" / "run that" / "press enter" referring to text already in the prompt.

**Input:**
```json
{ "mode": "execute", "session": "main", "windowIndex": 2 }
```

**Output:** `{ "window": { ... }, "submitted": true, "status": "entered" }`

---

## Notes, tasks, and kanban

webtmux stores **per-project** and **per-window** notes in `data/window-notes.json`. The notes blob is plain Markdown. Tasks are encoded as **one task per line** with an optional status prefix:

| Prefix | Status | Kanban column |
|---|---|---|
| (none) | `NS` | Not started |
| `[WIP]` | `WIP` | In progress |
| `[BL]` | `BL` | Blocked |
| `[CM]` | `CM` | Completed |

Example notes blob:

```
[WIP] Wire up SSE for live updates
Refactor session resolver
[BL] Waiting on auth team
[CM] Initial scaffold
```

The kanban view in the web UI is rendered directly from these lines — moving a card just rewrites the prefix. The MCP tools below follow the same convention, so any change made via MCP shows up in the kanban view immediately.

Each record has a `source` field: `"local"` or `"notion"`. When source is `"notion"`, the notes come from a linked Notion page and **all MCP write tools refuse to run** to avoid silent overwrites. Switch source to `"local"` in the web UI (or via `PATCH /api/local/projects/:project/source`) before writing.

### Scope: project vs. window

| Scope | Address with |
|---|---|
| `project` | `session` (the session name) |
| `window` | `session` + one of `windowIndex` / `windowName` / `windowId`, **or** `current: true` |

---

### `read_project_notes`

**Input:** `{ "session": "main" }`

**Output:**
```json
{
  "scope": "project",
  "session": "main",
  "source": "local",
  "notionPageId": null,
  "notes": "[WIP] Wire up SSE\nRefactor session resolver",
  "tasks": [
    { "status": "WIP", "text": "Wire up SSE" },
    { "status": "NS",  "text": "Refactor session resolver" }
  ],
  "kanban": {
    "NS":  [{ "index": 1, "text": "Refactor session resolver" }],
    "WIP": [{ "index": 0, "text": "Wire up SSE" }],
    "BL":  [],
    "CM":  []
  },
  "updatedAt": "2026-05-13T10:14:22.000Z"
}
```

---

### `read_window_notes`

**Input:** same window addressing as the terminal tools.
```json
{ "session": "main", "windowIndex": 2 }
```

**Output:** same shape as `read_project_notes`, but with `"scope": "window"` and a `window` object identifying the resolved tmux window.

---

### `write_project_notes`

Replace the full notes markdown for a project. Refused when source is `"notion"`.

**Input:**
```json
{
  "mode": "execute",
  "session": "main",
  "notes": "[WIP] New plan line one\n[NS] line two"
}
```

**Output:** `{ "scope": "project", "session": "main", "record": { ...persisted record } }`

---

### `write_window_notes`

Same as above, scoped to one window.

**Input:**
```json
{ "mode": "execute", "session": "main", "windowIndex": 2, "notes": "..." }
```

---

### `list_tasks`

Parse the notes for a scope and return tasks grouped by kanban status. Read-only.

**Input:**
```json
{ "scope": "project", "session": "main", "status": "WIP" }
```

| Field | Required | Meaning |
|---|---|---|
| `scope` | yes | `"project"` or `"window"` |
| `session` + window identifiers | depends on scope | as above |
| `status` | no | Filter to one column: `NS` / `WIP` / `BL` / `CM` |

**Output:**
```json
{
  "scope": "project",
  "session": "main",
  "window": null,
  "statusLabels": { "NS": "Not started", "WIP": "In progress", "BL": "Blocked", "CM": "Completed" },
  "tasks": [{ "status": "WIP", "text": "Wire up SSE" }],
  "kanban": { "NS": [...], "WIP": [...], "BL": [...], "CM": [...] }
}
```

---

### `update_task`

Mutate a single task without rewriting the whole notes blob. The server reparses → mutates → re-serializes. Refused when source is `"notion"`. Requires `mode: "execute"`.

**Common input:**
```json
{ "mode": "execute", "scope": "project", "session": "main", "action": "...", ... }
```

**Per-action arguments:**

| `action` | Required fields | Effect |
|---|---|---|
| `add` | `text`, optional `status` (default `NS`) | Appends a new task |
| `set_status` | `index`, `status` | Moves a task across kanban columns |
| `set_text` | `index`, `text` | Renames a task |
| `remove` | `index` | Deletes a task |
| `move` | `index`, `toIndex` | Reorders a task within the list |

`index` is the 0-based position in the parsed task list (the same number returned in `tasks[].index` or in the kanban groupings). Indexes shift after `remove`/`move` — always re-read with `list_tasks` between mutations if you're doing multiple in a row.

**Example — move a card to "In progress":**
```json
{ "mode": "execute", "scope": "project", "session": "main", "action": "set_status", "index": 1, "status": "WIP" }
```

**Example — add a new task:**
```json
{ "mode": "execute", "scope": "window", "session": "main", "windowIndex": 2, "action": "add", "text": "Smoke-test deploy", "status": "NS" }
```

**Output:**
```json
{
  "scope": "project",
  "session": "main",
  "window": null,
  "action": "set_status",
  "tasks": [ ...full updated list... ],
  "kanban": { "NS": [...], "WIP": [...], "BL": [...], "CM": [...] },
  "record": { ...persisted record... }
}
```

---

## Wiring it into an agent

### Hermes / OpenFloor (or any MCP-compliant runtime)

Add to your agent's MCP server config:

```json
{
  "mcpServers": {
    "webtmux": {
      "transport": "streamable-http",
      "url": "https://tmux.winds-os.com/mcp"
    }
  }
}
```

Some runtimes use `type` instead of `transport`, and some require a `headers` block. The two pieces that always matter are the URL and the HTTP transport.

### Claude Desktop (`claude_desktop_config.json`)

```json
{
  "mcpServers": {
    "webtmux": {
      "type": "streamable-http",
      "url": "https://tmux.winds-os.com/mcp"
    }
  }
}
```

### Programmatic test (Node, `@modelcontextprotocol/sdk`)

```js
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(new URL("https://tmux.winds-os.com/mcp"));
const client = new Client({ name: "smoke-test", version: "0.0.1" }, { capabilities: {} });
await client.connect(transport);

console.log(await client.listTools());
console.log(await client.callTool({ name: "list_windows", arguments: {} }));
```

### Quick curl smoke test

```bash
curl -X POST https://tmux.winds-os.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

---

## Suggested system-prompt guidance for the agent

> You have access to a webtmux MCP server that controls a remote tmux workspace.
>
> - To explore, prefer `list_windows`, `read_chat_history`, and `read_chat_messages` — these are read-only.
> - To act, you must pass `mode: "execute"` to `select_window`, `send_to_window`, and `submit_current_window`. When you're still planning, use `mode: "plan"` — the server will refuse the action, which is the intended safety check.
> - `send_to_window` with `submit: false` only types — the user still has to press Enter. Only set `submit: true` when the user explicitly said send / run / execute / submit / press enter.
> - When the user refers to "this window" or "here," call `list_windows` (or pass `current: true`) to resolve before acting.
> - For "what was I just doing?" questions, call `read_chat_messages` (cleaned view). For verbatim transcripts, call `read_chat_history`.

---

## Operational notes

- **Idempotency:** writes are deduped (~3s window) on identical `(window, text, submit)` tuples. The agent can safely retry on network errors.
- **Logging:** every tool call is logged server-side with toolName, target, duration, and (for `send_to_window`) a hash of the text — not the text itself.
- **Capture freshness:** `summarize_window` and `read_chat_*` read the latest persisted state. There may be a sub-second lag versus what's on the user's screen.
- **No write tools exist** for: creating/deleting sessions or windows, retitling, recording control, uploads, voice, or Notion page creation. Those are web-UI-only today. If the agent needs them, they have to be added as new `registerTool` calls in `server.js` → `createWebtmuxMcpServer()`.
- **Notes write guard:** the four notes/tasks write tools refuse when `source === "notion"`. This is intentional — Notion is the source of truth in that case, and the local notes blob would be overwritten on the next sync. Flip the source to `local` first.

---

## Changelog

- **2026-05-13** — Added notes/tasks/kanban tools: `read_project_notes`, `read_window_notes`, `write_project_notes`, `write_window_notes`, `list_tasks`, `update_task`. Writes refused when notes source is `notion`.
- **2026-05-13** — Added `read_chat_history` and `read_chat_messages` for agent-readable chat transcripts.
- Initial — `list_windows`, `summarize_window`, `select_window`, `send_to_window`, `submit_current_window`.
