Metadata-Version: 2.4
Name: cliptunnel-mcp
Version: 0.7.0
Summary: Operate a locked-down remote machine through its clipboard
Author-email: Jordi Murgó <jordi.murgo@gmail.com>
Maintainer-email: Jordi Murgó <jordi.murgo@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/jordi-murgo/cliptunnel-mcp
Project-URL: Repository, https://github.com/jordi-murgo/cliptunnel-mcp.git
Project-URL: Issues, https://github.com/jordi-murgo/cliptunnel-mcp/issues
Keywords: mcp,clipboard,tunnel,remote,agent,controller,citrix
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: MacOS
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: clipboard-event>=0.2.0
Provides-Extra: server
Requires-Dist: mcp<2,>=1.2; extra == "server"
Dynamic: license-file

# cliptunnel-mcp

Operate locked-down remote machines through their clipboard — now with multi-remote support, autonomous agents, clipboard preservation, and agent heartbeat.

## What it does

`cliptunnel-mcp` turns a shared clipboard into a reliable control channel between machines. When a remote machine sits behind a Citrix session, a locked-down VDI, or any environment that blocks SSH, file transfer, and networking but still exposes a clipboard, ClipTunnel tunnels commands through that single slot and exposes them as [Model Context Protocol](https://modelcontextprotocol.io) tools.

**v0.7.0** ships the CT3 wire protocol v3 with prefixed endpoint IDs (`C`/`R` + 7 hex), announce-based discovery, multi-controller awareness, an agent heartbeat that keeps the remote roster self-healing, and clipboard preservation that restores the user's clipboard after every exchange.

The package ships four layers:

- **Protocol** — CT3 wire format with prefixed endpoint IDs (`C`/`R` + 7 hex), broadcast routing, keepalive pings, announce-based discovery, and typed messages (command, response, error, ack, ping, announce).
- **Endpoints** — `Controller` (operator side) with a remote + controller registry, and multiple `Agent` instances (remote side), each with a unique prefixed ID. Both run background threads with ARQ retransmission, sequence-bound deduplication, and generation-safe lifecycle.
- **MCP server** — a FastMCP application with 26 tools including shell, filesystem, binary transfer, sysinfo, remote agent management, connection listing, and announce-based discovery.
- **Clipboard transport** — backed by [clipboard-event](https://github.com/jordi-murgo/clipboard-event) for cross-platform event-driven change detection, with user-clipboard preservation (non-protocol content is backed up and restored after each exchange).

## Architecture

![Mermaid diagram](https://mermaid.ink/img/Z3JhcGggTFIKICAgIHN1YmdyYXBoIE9wZXJhdG9yWyJPcGVyYXRvciBtYWNoaW5lIl0KICAgICAgICBDbGllbnRbIk1DUCBDbGllbnQ8YnIvPihDbGF1ZGUsIFBpLCBDdXJzb3IpIl0KICAgICAgICBTZXJ2ZXJbIk1DUCBTZXJ2ZXI8YnIvPihGYXN0TUNQKSJdCiAgICAgICAgQ29udHJvbGxlclsiQ29udHJvbGxlcjxici8-wrcgcmVtb3RlICsgY29udHJvbGxlciByZWdpc3RyeTxici8-wrcga2VlcGFsaXZlPGJyLz7CtyBhbm5vdW5jZTxici8-wrcgY2xpcGJvYXJkIHJlc3RvcmUiXQogICAgICAgIENsaWVudCAtLSAiTUNQIC8gc3RkaW8iIC0tPiBTZXJ2ZXIKICAgICAgICBTZXJ2ZXIgLS0-IENvbnRyb2xsZXIKICAgIGVuZAoKICAgIHN1YmdyYXBoIFJlbW90ZTFbIlJlbW90ZSBtYWNoaW5lIEEiXQogICAgICAgIEFnZW50MVsiQWdlbnQ8YnIvPnJlbW90ZV9pZDogUjFiMmMzZDQ8YnIvPsK3IGhlYXJ0YmVhdCJdCiAgICAgICAgT3BzMVsib3BlcmF0aW9uczxici8-ZGlzcGF0Y2giXQogICAgICAgIENvcGlsb3QxWyJjb3BpbG90X2NsaWVudDxici8-YWdlbnRfc2Vzc2lvbiJdCiAgICAgICAgQWdlbnQxIC0tPiBPcHMxCiAgICAgICAgT3BzMSAtLT4gQ29waWxvdDEKICAgIGVuZAoKICAgIHN1YmdyYXBoIFJlbW90ZTJbIlJlbW90ZSBtYWNoaW5lIEIiXQogICAgICAgIEFnZW50MlsiQWdlbnQ8YnIvPnJlbW90ZV9pZDogUjVmNmE3Yjg8YnIvPsK3IGhlYXJ0YmVhdCJdCiAgICAgICAgT3BzMlsib3BlcmF0aW9uczxici8-ZGlzcGF0Y2giXQogICAgICAgIEFnZW50MiAtLT4gT3BzMgogICAgZW5kCgogICAgQ29udHJvbGxlciAtLSAiQ1QzIHdpcmU8YnIvPihjbGlwYm9hcmQpIiAtLT4gQWdlbnQxCiAgICBDb250cm9sbGVyIC0tICJDVDMgd2lyZTxici8-KGNsaXBib2FyZCkiIC0tPiBBZ2VudDIK)


On startup the Controller broadcasts an ANNOUNCE. Each Agent generates a random prefixed ID (`R` + 7 hex), waits a random delay (0.1–4.0s), and sends back its sysinfo as a registration response. The Controller maintains a registry of all connected remotes and any other controllers it discovers. A keepalive thread pings remotes after 5 minutes of inactivity and marks them dead if no response is received within 30 seconds. Each Agent additionally runs a **heartbeat** thread that periodically re-sends its registration, so a lost announce response never leaves an agent invisible. After every exchange the Controller **restores the user's clipboard** content that was present before the protocol traffic.

### Wire format

```
CT3|<from>|<to>|<seq>|<type>|<payload>
```

| Field    | Value                                                                |
|----------|----------------------------------------------------------------------|
| `CT3`    | Protocol signature + version                                         |
| `from`   | `C` + 7 hex (Controller) or `R` + 7 hex (remote ID, e.g. `R1b2c3d4`) |
| `to`     | `C` + 7 hex (Controller), `*` (broadcast), or `R` + 7 hex (specific remote) |
| `seq`    | Positive integer, monotonic per session (`0` = registration/heartbeat) |
| `type`   | `C` (command), `R` (response), `E` (error), `A` (ack), `P` (ping), `N` (announce) |
| `payload`| Base64-encoded UTF-8                                                 |

### Registration and announce flow

![Mermaid diagram](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDIGFzIENvbnRyb2xsZXIKICAgIHBhcnRpY2lwYW50IEExIGFzIEFnZW50IEEKICAgIHBhcnRpY2lwYW50IEEyIGFzIEFnZW50IEIKCiAgICBDLT4-QTE6IENUM3xDMWEyYjNjNHwqfHNlcXxOfCAoYW5ub3VuY2UgYnJvYWRjYXN0KQogICAgQy0-PkEyOiBDVDN8QzFhMmIzYzR8KnxzZXF8TnwgKGFubm91bmNlIGJyb2FkY2FzdCkKCiAgICBOb3RlIG92ZXIgQTE6IHJhbmRvbSBkZWxheSAwLjHigJM0LjBzCiAgICBOb3RlIG92ZXIgQTI6IHJhbmRvbSBkZWxheSAwLjHigJM0LjBzCgogICAgQTEtLT4-QzogQ1QzfFIxYjJjM2Q0fEMxYTJiM2M0fDB8Unw8c3lzaW5mbz4gKHJlZ2lzdHJhdGlvbikKICAgIEMtLT4-QTE6IENUM3xDMWEyYjNjNHxSMWIyYzNkNHxzZXF8QXwgKEFDSykKICAgIEEyLS0-PkM6IENUM3xSNWY2YTdiOHxDMWEyYjNjNHwwfFJ8PHN5c2luZm8-IChyZWdpc3RyYXRpb24pCiAgICBDLS0-PkEyOiBDVDN8QzFhMmIzYzR8UjVmNmE3Yjh8c2VxfEF8IChBQ0spCgogICAgTm90ZSBvdmVyIEM6IHJlZ2lzdHJ5IHVwZGF0ZWQ6PGJyLz5SMWIyYzNkNCDihpIge3N5c2luZm8sIGFsaXZlfTxici8-UjVmNmE3Yjgg4oaSIHtzeXNpbmZvLCBhbGl2ZX0K)


Because the clipboard is a single last-writer-wins slot, simultaneous announce responses can collide and one agent's registration may be lost. The heartbeat below makes this self-healing: the missing agent re-registers on the next cycle.

### Heartbeat

![Mermaid diagram](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDIGFzIENvbnRyb2xsZXIKICAgIHBhcnRpY2lwYW50IEEgYXMgQWdlbnQKCiAgICBOb3RlIG92ZXIgQTogZXZlcnkgQ0xJUFRVTk5FTF9IRUFSVEJFQVRfU0VDUyAoZGVmYXVsdCAxMjBzKTxici8-KyByYW5kb20gaml0dGVyIDDigJMxNXMKICAgIEEtLT4-QzogQ1QzfFIuLi58Qy4uLnwwfFJ8PHN5c2luZm8-IChyZWdpc3RyYXRpb24gcmUtc2VuZCkKICAgIEMtLT4-QTogQ1QzfEMuLi58Ui4uLnxzZXF8QXwgKEFDSykKICAgIE5vdGUgb3ZlciBDOiByZWdpc3RyeSB1cHNlcnQgKyBsYXN0X3NlZW4gcmVmcmVzaGVkPGJyLz5jbGlwYm9hcmQgcmVzdG9yZWQK)


Each Agent runs a daemon thread that re-sends its registration (a `RESPONSE` with `seq=0` carrying `sysinfo`) to every known controller on a configurable interval plus jitter. The jitter prevents multiple agents sharing a clipboard from synchronizing their writes. A lost heartbeat is harmless — the next one arrives. The controller's existing registration upsert path consumes it with no protocol or controller changes.

| Setting | Default | Effect |
|---------|---------|--------|
| `CLIPTUNNEL_HEARTBEAT_SECS` env var | `120` | Interval in seconds. `<= 0` disables the heartbeat. |
| `Agent(heartbeat_secs=...)` | `None` (resolves env, then default) | Programmatic override of the env var. |

### Keepalive

![Mermaid diagram](https://mermaid.ink/img/c2VxdWVuY2VEaWFncmFtCiAgICBwYXJ0aWNpcGFudCBDIGFzIENvbnRyb2xsZXIKICAgIHBhcnRpY2lwYW50IEEgYXMgQWdlbnQKCiAgICBOb3RlIG92ZXIgQzogaWRsZSA-IDUgbWluIGRldGVjdGVkCiAgICBDLT4-QTogQ1QzfEMuLi58Ui4uLnxzZXF8UHwgKHBpbmcpCiAgICBBLS0-PkM6IENUM3xSLi4ufEMuLi58c2VxfEF8IChBQ0spCiAgICBOb3RlIG92ZXIgQzogbGFzdF9zZWVuIHVwZGF0ZWQKCiAgICBOb3RlIG92ZXIgQyxBOiBubyByZXNwb25zZSAzMHMgYWZ0ZXIgcGluZwogICAgTm90ZSBvdmVyIEM6IHN0YXR1cyDihpIgZGVhZAo)


With the heartbeat active, the keepalive loop stays mostly idle — it only pings remotes that have stopped heartbeating, and is what ultimately marks a silent agent `dead`.

### Clipboard preservation

The clipboard is the user's real pasteboard, so every protocol write would clobber whatever the user copied. ClipTunnel now preserves it:

- **Backup** — the transport observes every clipboard change. Any non-empty value that is not CT3 protocol traffic (`CT3|…`) is retained as the user-clipboard candidate. The backup is also seeded at construction from the initial value, so a startup announce never destroys pre-existing content.
- **Guarded restore** — after the Controller sends the final ACK of an exchange, it calls `transport.restore_user_clipboard()`. The restore happens **only if the OS clipboard still holds this process's last self-write**; if another process or the user wrote anything in between, the restore is a silent no-op (it would otherwise clobber that content). On success the backup is written back as a self-write.

This makes the heartbeat and the restore synergistic: a racy restore that clobbers an in-flight message is cured by the next heartbeat, and the user's clipboard survives the protocol traffic.

## Installation

```bash
pip install cliptunnel-mcp          # core + cliptunnel-agent binary
pip install cliptunnel-mcp[server]  # adds MCP server binary (mcp>=1.2,<2)
```

Dependencies: `clipboard-event>=0.2.0` (cross-platform clipboard change notifications).

| Binary              | Extra needed | Purpose                                      |
|---------------------|--------------|----------------------------------------------|
| `cliptunnel-agent`  | *(none)*     | Runs the Agent on the local OS clipboard.    |
| `cliptunnel-mcp`    | `[server]`   | Runs the MCP server over stdio.              |

## Quick start

### Agent (remote machine)

```bash
cliptunnel-agent
```

> **Antivirus / EDR workaround (Windows)**: unsigned `.exe` entry points may be quarantined. Use `python -m` instead:
>
> ```bash
> python -m cliptunnel_mcp.agent    # instead of cliptunnel-agent
> python -m cliptunnel_mcp.server   # instead of cliptunnel-mcp
> ```

The Agent generates a random prefixed ID, registers with the Controller by sending its sysinfo, then watches the clipboard for commands. It uses `clipboard-event` for change detection (event-driven on Windows and Wayland, polling on macOS and X11). A heartbeat thread re-registers every `CLIPTUNNEL_HEARTBEAT_SECS` (default 120s) so the Controller never loses it; set the variable to `0` or a negative value to disable it.

### Controller + MCP server (operator machine)

Configure your MCP client (Claude Desktop, Cursor, Pi, etc.):

```json
{
  "mcpServers": {
    "cliptunnel": {
      "command": "cliptunnel-mcp",
      "args": []
    }
  }
}
```

The server broadcasts an announce on startup, discovers connected remotes and any other controllers, and maintains a live registry with keepalive pings. After every exchange it restores the user's clipboard content.

### Controller only (no MCP)

```python
from cliptunnel_mcp.clipboard_transport import ClipboardTransport
from cliptunnel_mcp import Controller
import json

controller = Controller(transport=ClipboardTransport())

# Send to a specific remote
future = controller.send_command(json.dumps({"op": "shell", "cmd": "whoami"}), remote_id="R1b2c3d4")
result = future.result(timeout=30)

# List connected remotes
connections = controller.get_connections()
# {"remotes": {"R1b2c3d4": {"os": "Windows", "status": "alive", "last_seen": 1692634123.4, "last_seen_ago": 0.3, ...}}, "controllers": {...}}
```

### Programmatic Agent

```python
from cliptunnel_mcp.clipboard_transport import ClipboardTransport
from cliptunnel_mcp import Agent
from cliptunnel_mcp.operations import dispatch

agent = Agent(transport=ClipboardTransport(), handler=dispatch)
# Agent generates its own remote_id, registers automatically, and heartbeats every 120s.
# Disable the heartbeat with heartbeat_secs=0 (or CLIPTUNNEL_HEARTBEAT_SECS=0).
```

## MCP tools

The server exposes **26 tools** over stdio. All tools accept an optional `remote_id` parameter to target a specific remote. If omitted, the Controller picks the first alive remote.

### Shell & filesystem

| Tool | Description |
|------|-------------|
| `remote_shell` | Execute a shell command; auto-sync (10s) then async with `job_id` polling. |
| `remote_shell_result` | Poll for the result of an async shell command. |
| `remote_fs_read` | Read a file. |
| `remote_fs_write` | Create or overwrite a file (creates parent dirs). |
| `remote_fs_list` | List directory entries. |
| `remote_fs_delete` | Delete a file. |
| `remote_fs_replace` | Search-and-replace in a file (exact-once match). |
| `remote_fs_search` | Regex search in a file. |
| `remote_fs_find` | Glob-find files under a directory. |
| `remote_fs_bin_read` | Read a binary file as base64. |
| `remote_fs_bin_write` | Write base64 content to a binary file. |

### Binary transfer

| Tool | Description |
|------|-------------|
| `remote_upload` | Upload a local file to a remote machine. |
| `remote_download` | Download a remote file to the local machine. |

### System info

| Tool | Description |
|------|-------------|
| `remote_sysinfo` | Return system info: OS, Python, CPU, memory, disk, user, shell, agent auth, clipboard backend. |

### Remote agent (Copilot)

| Tool | Description |
|------|-------------|
| `remote_agent_login` | Start GitHub OAuth device flow for Copilot authentication. |
| `remote_agent_login_status` | Poll login state (idle/polling/done/error). |
| `remote_agent_models` | List available Copilot models on the remote. |
| `remote_agent_start` | Create an autonomous agent session (async). |
| `remote_agent_continue` | Send a message to an existing session. |
| `remote_agent_result` | Poll for the async result. |
| `remote_agent_status` | Query session status. |
| `remote_agent_list` | List active agent sessions. |
| `remote_agent_clear` | Clear session message history. |
| `remote_agent_end` | Destroy a session. |

### Connections & discovery

| Tool | Description |
|------|-------------|
| `remote_connections` | List all connected remotes and controllers with sysinfo, `last_seen` (epoch), `last_seen_ago` (seconds), and `status` (alive/dead). |
| `remote_discovery` | Broadcast an ANNOUNCE to discover remotes and other controllers on the shared clipboard. |

## Operations

The `dispatch` handler supports these operations:

| Operation | Parameters | Returns |
|-----------|------------|---------|
| `shell` | `cmd` | JSON: `{stdout, stderr, returncode}` |
| `fs.read` | `path` | JSON: `{content, lines}` |
| `fs.write` | `path`, `content` | `wrote N bytes to PATH` |
| `fs.list` | `path` | JSON: `[{name, size, is_dir}]` |
| `fs.delete` | `path` | `deleted PATH` |
| `fs.replace` | `path`, `old`, `new` | `replaced 1 occurrence` (exact-once) |
| `fs.search` | `path`, `pattern` | JSON: `[{line, content}]` (regex) |
| `fs.find` | `path`, `pattern` | JSON: `[PATH, ...]` (glob) |
| `fs.bin_read` | `path` | JSON: `{path, size, b64}` |
| `fs.bin_write` | `path`, `b64` | `wrote N bytes to PATH` |
| `sysinfo` | — | JSON: full system info |
| `register` | — | JSON: sysinfo (alias for agent registration) |
| `agent` | `action`, ... | JSON: session management (start, continue, result, status, clear, end, list, login, login_status) |

## Remote agent

The Agent can run autonomous Copilot-powered agents on the remote machine. Each agent session:

- Uses the GitHub Copilot API with function calling (shell, fs_read, fs_write, fs_replace, fs_search, fs_list, fs_find)
- Runs asynchronously in a background thread
- Supports multi-turn conversations with `remote_agent_continue`
- Default model: `mai-code-1.1-flash`

### Authentication

```python
# Via MCP tools:
remote_agent_login()          # Returns user_code + verification_uri
# Open https://github.com/login/device, enter the code
remote_agent_login_status()   # Returns {status: "done", token_saved: true}
```

Token stored in `.copilot_agent_token` on the remote machine. The token lookup is relative to the agent process working directory, so launch the agent from a directory that contains (or can access) the token file.

## API surface

### `Controller`

The operator-side endpoint with remote + controller registry, keepalive, and clipboard restore.

| Method | Description |
|--------|-------------|
| `send_command(command, remote_id=None) -> Future` | Queue a command to a specific remote (or first alive). |
| `send_command_sync(command, remote_id=None) -> str \| None` | Send and block until response or timeout. |
| `get_connections() -> dict` | Return `{"remotes": {...}, "controllers": {...}}` with sysinfo, last_seen, last_seen_ago, and status. |
| `close()` | Stop background threads. Idempotent. |

### `Agent`

The remote-side endpoint with auto-registration, heartbeat, and ping handling.

| Method | Description |
|--------|-------------|
| `close()` | Stop this agent (heartbeat, reader, dispatcher, pool). Idempotent. |
| `send_registration(controller_id=None)` | Send sysinfo to a controller (or all known controllers). Also used by the heartbeat. |

Constructor parameters: `transport` (required), `handler` (required), `poll_interval`, `max_workers`, `response_ack_timeout`, `heartbeat_secs` (default `None` → resolves `CLIPTUNNEL_HEARTBEAT_SECS`, then `120`; `<= 0` disables).

### `ClipboardTransport`

| Method | Description |
|--------|-------------|
| `read() -> str \| None` | Return the current clipboard value (cached). |
| `write(text: str)` | Write to the clipboard as a self-write. |
| `restore_user_clipboard() -> bool` | Guarded restore of the backed-up user content; `True` on success, `False` if the slot was touched by another writer or no backup exists. |

### Protocol primitives

| Symbol | Description |
|--------|-------------|
| `pack(msg) -> str` | Serialize a `Message` into wire format. |
| `unpack(raw) -> Message \| None` | Parse a wire string; `None` on malformed. |
| `validate(raw, my_id) -> bool` | True if addressed to `my_id` (`C`/`R` + 7 hex) or broadcast. |
| `generate_controller_id() -> str` | Generate `C` + 7 hex. |
| `generate_remote_id() -> str` | Generate `R` + 7 hex. |
| `Message` | Dataclass: `frm`, `to`, `seq`, `mtype`, `payload`. |
| `MsgType` | Enum: `COMMAND`, `RESPONSE`, `ERROR`, `ACK`, `PING`, `ANNOUNCE`. |
| `SeqTracker` | Per-seq dedupe state: new → processing → done. |

## Clipboard backend

ClipTunnel uses [clipboard-event](https://github.com/jordi-murgo/clipboard-event) for cross-platform clipboard access and change detection:

| Platform | Backend | Change detection | Latency |
|----------|---------|------------------|---------|
| macOS | NSPasteboard `changeCount` | Polling (50ms) | ~50ms |
| Windows | `WM_CLIPBOARD_UPDATE` | Event-driven | Sub-ms |
| Linux / Wayland | `wl-paste --watch` | Event-driven | Sub-ms |
| Linux / X11 | `xclip`/`xsel` | Polling (100ms) | ~100ms |

The `ClipboardTransport` adapts clipboard-event to the `Transport` and `RevisionMonitor` protocols, backs up non-protocol clipboard content, and guards restores against concurrent writers. For custom setups, implement the `Transport` protocol directly.

## Platform support

| Platform | Status | Clipboard | CI |
|----------|--------|-----------|-----|
| macOS | Tested | clipboard-event (changeCount) | macOS + Linux + Windows × Python 3.10–3.14 |
| Windows | Tested | clipboard-event (WM_CLIPBOARD_UPDATE) | Same |
| Linux / Wayland | Tested | clipboard-event (wl-paste --watch) | Same |
| Linux / X11 | Core works | clipboard-event (xclip polling) | Same |

## Development

```bash
# Create a virtual environment
uv venv && source .venv/bin/activate

# Install in development mode
uv pip install -e . pytest

# Run the test suite (275 tests)
python -m pytest -q
# or
python -m unittest discover -s tests -t .

# Bare mode — no install, just PYTHONPATH
PYTHONPATH=src:. python -m pytest -q
```

The test suite uses a deterministic `ClipboardSlot` test double. No clipboard hardware needed.

## Lifecycle and coalescing semantics

- **One command at a time**: the Controller dispatches commands serially per target remote.
- **Immediate ACK**: the Agent ACKs every command before processing.
- **One response at a time**: the Agent holds one pending response; retransmits until the Controller's ACK.
- **Announce discovery**: the Controller broadcasts an ANNOUNCE on startup and on `remote_discovery`; agents and other controllers reply. Replies can collide on the shared slot; the heartbeat makes this self-healing.
- **Heartbeat**: each Agent re-sends its registration every `CLIPTUNNEL_HEARTBEAT_SECS` (default 120s) + jitter (0–15s); `<= 0` disables. The controller upserts the roster on every heartbeat.
- **Keepalive**: Controller pings remotes after 5 min idle, marks dead if no response within 30s. With the heartbeat active, this only fires for agents that have stopped heartbeating.
- **Clipboard preservation**: the transport backs up non-CT3 clipboard content; the Controller restores it (guarded) after the final ACK of every exchange.
- **Broadcast routing**: `to=*` messages are processed by all remotes with random backoff; no ACK.
- **Targeted routing**: `to=<R+7hex>` messages are processed only by that remote; others ignore.
- **Stale message guard**: the Controller skips R/E with `seq <= min_seq`.
- **Generation-safe**: closing and restarting never strands threads.
- **Paced writes**: bounded inter-write gap prevents message loss.

## Limitations

- **Text-only clipboard**: the protocol carries UTF-8 strings, and the preservation backup is text-only. Binary files are base64-encoded; rich content (images, RTF) copied by the user is not preserved by the restore.
- **Shared slot**: multiple remotes and controllers share one clipboard; the protocol serializes all traffic, and announce responses can race (mitigated by the heartbeat).
- **No encryption**: the wire format is plain base64.
- **Multi-controller**: multiple controllers are discovered and tracked, but they share the single clipboard channel — the protocol is designed for one primary Controller and multiple Agents.
- **CT3-looking user content**: if the user copies a string starting with `CT3|`, it is treated as protocol traffic and not backed up.

## License

MIT — see [LICENSE](https://github.com/jordi-murgo/cliptunnel-mcp/blob/main/LICENSE).
