From 69c1cf4f70d53aef668bca09d278e627c130fcde Mon Sep 17 00:00:00 2001 From: Yifei Liu Date: Sat, 5 Sep 2026 18:41:43 +0800 Subject: [PATCH] fix: use ASCII tmux format separator on Windows --- README.md | 12 +++++++ mcp_ssh_tmux/__init__.py | 10 ++++++ tests/test_platform_compat.py | 64 +++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 tests/test_platform_compat.py diff --git a/README.md b/README.md index d68cc44..5d1dbb6 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/mcp_ssh_tmux/__init__.py b/mcp_ssh_tmux/__init__.py index e69de29..000a5f0 100644 --- a/mcp_ssh_tmux/__init__.py +++ b/mcp_ssh_tmux/__init__.py @@ -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__|") diff --git a/tests/test_platform_compat.py b/tests/test_platform_compat.py new file mode 100644 index 0000000..4511776 --- /dev/null +++ b/tests/test_platform_compat.py @@ -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)