Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ Add this to your `mcp.json` (e.g., in Claude Desktop, Cursor, or 1mcp):

*Note: If you installed via `uv tool install`, you can just use `mcp-ssh-tmux` as the command.*

### Native Windows tmux compatibility

On Windows, the server defaults `LIBTMUX_TMUX_FORMAT_SEPARATOR` to the ASCII
marker `|__LIBTMUX_SEP__|` before importing libtmux. Native Windows tmux builds
can replace libtmux's default Unicode separator (`␞`) with `?`, causing
`list_sessions` and other operations to fail with
`zip() argument 2 is shorter than argument 1`.

An explicitly configured `LIBTMUX_TMUX_FORMAT_SEPARATOR` is preserved; use an
ASCII marker that will not appear in tmux field values. Other platforms keep
libtmux's default. Restart the MCP server after changing the environment.

## Tools

- `open_session(host, username, port)`: Opens a new SSH connection in a unique tmux window.
Expand Down
10 changes: 10 additions & 0 deletions mcp_ssh_tmux/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Package initialization and platform compatibility defaults."""

import os
import sys

# Configure this before importing libtmux: its format separator is read once
# at import time. Native Windows tmux can replace the default Unicode U+241E
# separator with '?', causing libtmux's strict field parsing to fail.
if sys.platform == "win32":
os.environ.setdefault("LIBTMUX_TMUX_FORMAT_SEPARATOR", "|__LIBTMUX_SEP__|")
64 changes: 64 additions & 0 deletions tests/test_platform_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Test platform defaults in fresh processes without starting tmux or SSH."""

import os
from pathlib import Path
import subprocess
import sys

import pytest


ROOT = Path(__file__).resolve().parents[1]
ENV_KEY = "LIBTMUX_TMUX_FORMAT_SEPARATOR"


@pytest.mark.parametrize(
("platform", "configured", "expected"),
[
("win32", None, "|__LIBTMUX_SEP__|"),
("win32", "custom-separator", "custom-separator"),
("linux", None, None),
("darwin", None, None),
("linux", "custom-separator", "custom-separator"),
],
)
def test_platform_separator_default(platform, configured, expected):
env = os.environ.copy()
env.pop(ENV_KEY, None)
if configured is not None:
env[ENV_KEY] = configured
script = f"""
import os
import sys
sys.platform = {platform!r}
import mcp_ssh_tmux
assert os.environ.get({ENV_KEY!r}) == {expected!r}
"""
subprocess.run([sys.executable, "-c", script], cwd=ROOT, env=env, check=True)


def test_windows_separator_survives_lossy_encoding_and_parses():
env = os.environ.copy()
env.pop(ENV_KEY, None)
script = """
import sys
platform = sys.platform
sys.platform = 'win32'
import mcp_ssh_tmux
sys.platform = platform
from libtmux.formats import FORMAT_SEPARATOR
from libtmux.neo import get_output_format, parse_output
from inspect import signature
assert FORMAT_SEPARATOR == '|__LIBTMUX_SEP__|'
for command in ('list-sessions', 'list-windows', 'list-panes'):
# Older libtmux releases use one format for all listing commands.
kwargs = {'list_cmd': command, 'tmux_version': '3.6a'} if 'list_cmd' in signature(get_output_format).parameters else {}
fields, template = get_output_format(**kwargs)
assert template.encode('ascii', errors='replace').decode('ascii') == template
values = ['test' if field == 'session_name' else '' for field in fields]
output = FORMAT_SEPARATOR.join(values) + FORMAT_SEPARATOR
output = output.encode('ascii', errors='replace').decode('ascii')
parsed = parse_output(output, **kwargs)
assert parsed['session_name'] == 'test'
"""
subprocess.run([sys.executable, "-c", script], cwd=ROOT, env=env, check=True)