libtmux-mcp 0.1.0.alpha.1 → 0.1.0.alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8716cbbd29f60027cd48806d7ec292c1e431cad8e00721a31f36ca1700832480
4
- data.tar.gz: 5a84c18e207a57bfe2bc2c2b29d9a06eec1cc4ca58db90472e9274fe6ef80eda
3
+ metadata.gz: 67e80d372c18bcd568c338b521a4511cd8ef7ab96e4d0cfcc7ddca3433a2cad5
4
+ data.tar.gz: c2a828bb53e812bf8c2de9ee3ddba7a09eb6064d31d075633bb57248a494bd9b
5
5
  SHA512:
6
- metadata.gz: 1cc0536edcdb10af7fa48c860e283c1bbf9bdd23512a580cce7f0fcff1836317d4966a86f2ab7b54429d313e5f777b9bed8236e8dd6f71ee243fcca8922c69d2
7
- data.tar.gz: 8652d60ba20578e39b3de4d9da8aa17724e95bfbac8f50767099ac88f58c5a2be6bb7b66a7f6999e041afaee6f75bf5d944023938e272e0e5ac7d5b05a1b91f4
6
+ metadata.gz: fe3c16a4ad48bb285b4081e4be8221b1f254ed7dfb77708d90c00bbdce2551ecf2ee6ce4f0e833f9ee0dac84e3042e9bc0ac472098917d8e8b42abe986deba2a
7
+ data.tar.gz: 3274acee7920109154be895df17286b50fc7f34fc5939f85a20891c63253e266c2a841b5239fe4b8466aa48c49cf2e2fbed0be649e5fe9066691362aa99e7ef9
data/README.md CHANGED
@@ -1,9 +1,28 @@
1
+ <!-- libtmux-logo -->
2
+ <div align="center">
3
+ <picture>
4
+ <source srcset="assets/logo.svg" type="image/svg+xml">
5
+ <img src="assets/logo.png" width="128" height="128" alt="libtmux for Ruby MCP">
6
+ </picture>
7
+ </div>
8
+ <!-- /libtmux-logo -->
9
+
10
+ <div align="center">
11
+
1
12
  # libtmux-mcp
2
13
 
3
14
  Expose an existing tmux server over MCP stdio. Read snapshots, capture pane
4
15
  output, wait for events, or explicitly enable creation, input and shell commands.
5
16
  The official MCP SDK handles the protocol; bounded Async tasks handle transport.
6
17
 
18
+ </div>
19
+
20
+ Requires tmux 3.3a or newer. The server refuses an older daemon when it
21
+ starts, before serving any tool, and prints the reason and the remedy on
22
+ stderr. If the daemon changes while the server runs, a tool call returns an
23
+ `unsupported` result with `delivery: not_sent` and a fixed line that names the
24
+ floor and says to upgrade tmux and restart the server.
25
+
7
26
  Install the alpha and its `libtmux-mcp` executable:
8
27
 
9
28
  ```console
@@ -62,7 +81,7 @@ subsequent calls return a splice against that exact captured state. A screen
62
81
  delta does not establish that every intervening output byte was observed.
63
82
 
64
83
  Capture refuses nonempty effective `after-capture-pane` hooks, including
65
- inherited sparse entries. On tmux 3.2a–3.4, callers must keep capture-hook
84
+ inherited sparse entries. On tmux 3.3a–3.4, callers must keep capture-hook
66
85
  configuration stable throughout observation: those versions require a separate
67
86
  hook preflight. tmux 3.5+ checks the hook in the capture command queue. Both paths
68
87
  retain an explicit session/pane context and refuse its removal instead of
@@ -71,13 +90,14 @@ identity checks on every version that supports it.
71
90
 
72
91
  `tmux_wait` observes screen text or process exit through events. Canceling it
73
92
  retires its observation resources without signaling the pane program. Strong
74
- process tracking requires tmux 3.3 or later and a native identity backend:
93
+ process tracking requires tmux 3.3a or later and a native identity backend:
75
94
  Linux peer pidfds with matching process namespaces, or Darwin kqueue process
76
95
  observation. Acquisition verifies the live daemon and pane before retaining
77
96
  a cursor; unavailable evidence produces an explicit refusal. The
78
97
  [compatibility workflow](https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml)
79
- records each exact platform/version result, including the required tmux 3.2a
80
- refusal and positive identity cases on later versions.
98
+ records each exact platform/version result: positive identity cases on
99
+ supported releases, and refusal jobs for tmux 3.0a, 3.1c, 3.2a and unpatched
100
+ 3.3.
81
101
 
82
102
  Resource templates expose metadata pages and pane screens under encoded
83
103
  endpoint/generation URIs. They enforce the same policy and response limits as
data/assets/logo.png ADDED
Binary file
data/assets/logo.svg ADDED
@@ -0,0 +1,2 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 135.46667 135.46666" role="img" aria-label="libtmux for Ruby — MCP" class="libtmux-logo" data-language="ruby" data-variant="mcp" focusable="false"><title>libtmux for Ruby — MCP</title><g transform="translate(-210.28959,-409.98108)" data-part="artwork"><path d="m 334.66493,533.88598 v 11.56176 h 3.76501 c 4.04619,2.1e-4 7.32623,-3.28012 7.32632,-7.32631 v -4.23545 z" fill="#333333" data-part="base-edge"></path><g transform="matrix(1.024,0,0,1.024,-5.0469422,-9.8395302)" data-part="primary-tile"><path d="M 227.78906,0 213.98242,3.6992188 V 460.62109 H 241.4082 V 15.564453 Z" transform="matrix(0.26458333,0,0,0.26458333,210.28959,409.98108)" fill="#2D0002" data-part="primary-edge"></path><path d="m 210.28981,417.13553 v 113.86425 l 60.26885,2.60089 V 409.98109 h -53.11441 c -3.95129,0 -7.15444,3.20315 -7.15444,7.15444 z" fill="#701516" data-part="primary"></path></g><g transform="matrix(1.024,0,0,1.024,-5.0469422,-9.8395302)" data-part="lower-tile"><path d="m 486.38086,246.03906 -5.24805,4.99805 v 207.2207 l 5.24805,-0.93164 L 500,465.18945 V 261.60352 Z" transform="matrix(0.26458333,0,0,0.26458333,210.28959,409.98108)" fill="#67221F" data-part="lower-edge"></path><path d="m 278.70874,475.07917 v 64.8875 l 60.26883,-8.98455 v -55.90295 z" fill="#984640" data-part="lower"></path></g><path d="m 210.28959,533.88598 v 4.23545 c 9e-5,4.04619 3.28012,7.32651 7.32631,7.32631 h 117.12417 c 4.04619,2e-4 7.32621,-3.28012 7.32631,-7.32631 v -4.23545 z" fill="#231F20" data-part="base"></path><g transform="matrix(1.024,0,0,1.024,-5.0469422,-9.8395302)" data-part="upper-tile"><path d="m 459.33984,0 -25.33593,2.1074219 h 21.38281 c 14.93397,0 27.04101,12.1070061 27.04101,27.0410161 l 1.51727,183.649512 2.43542,2.38046 -1.84468,1.91057 15.46424,13.65284 L 500,42.603516 C 499.99913,8.8176763 474.27408,-6.8031496e-4 459.33984,0 Z" transform="matrix(0.26458333,0,0,0.26458333,210.28959,409.98108)" fill="#854A46" data-part="upper-side"></path><path d="m 331.82331,409.98109 h -53.11442 v 56.93261 h 60.26885 v -49.77817 c 0,-3.95129 -3.20315,-7.15444 -7.15443,-7.15444 z" fill="#B6706A" data-part="upper"></path><path d="m 278.7089,466.9137 h 60.26885 l 3.6035,4.11782 h -60.26932 z" fill="#733A36" data-part="upper-bottom"></path></g></g><g transform="scale(0.264583339844)" data-part="mcp-emblem"><g transform="translate(102,82) scale(2.133333333333)"><g fill="none" stroke="#999999" stroke-width="12" stroke-linecap="round" data-part="mcp-shadow"><path d="M25 97.8528L92.8823 29.9706C102.255 20.598 117.451 20.598 126.823 29.9706V29.9706C136.196 39.3431 136.196 54.5391 126.823 63.9117L75.5581 115.177" data-part="mcp-shadow-1"></path><path d="M76.2653 114.47L126.823 63.9117C136.196 54.5391 151.392 54.5391 160.765 63.9117L161.118 64.2652C170.491 73.6378 170.491 88.8338 161.118 98.2063L99.7248 159.6C96.6006 162.724 96.6006 167.789 99.7248 170.913L112.331 183.52" data-part="mcp-shadow-2"></path><path d="M109.853 46.9411L59.6482 97.1457C50.2757 106.518 50.2757 121.714 59.6482 131.087V131.087C69.0208 140.459 84.2168 140.459 93.5894 131.087L143.794 80.8822" data-part="mcp-shadow-3"></path></g></g><g transform="translate(102,72) scale(2.133333333333)"><g fill="none" stroke="#E6E6E6" stroke-width="12" stroke-linecap="round" data-part="mcp-symbol"><path d="M25 97.8528L92.8823 29.9706C102.255 20.598 117.451 20.598 126.823 29.9706V29.9706C136.196 39.3431 136.196 54.5391 126.823 63.9117L75.5581 115.177" data-part="mcp-symbol-1"></path><path d="M76.2653 114.47L126.823 63.9117C136.196 54.5391 151.392 54.5391 160.765 63.9117L161.118 64.2652C170.491 73.6378 170.491 88.8338 161.118 98.2063L99.7248 159.6C96.6006 162.724 96.6006 167.789 99.7248 170.913L112.331 183.52" data-part="mcp-symbol-2"></path><path d="M109.853 46.9411L59.6482 97.1457C50.2757 106.518 50.2757 121.714 59.6482 131.087V131.087C69.0208 140.459 84.2168 140.459 93.5894 131.087L143.794 80.8822" data-part="mcp-symbol-3"></path></g></g></g></svg>
@@ -175,7 +175,12 @@ module LibTmux
175
175
  created = result && result["created"]
176
176
  {"state" => created ? "known" : delivery == :not_sent ? "none" : "unknown", "created" => created || []}
177
177
  end
178
- failure_response(code, "The operation could not establish its requested result.", delivery.to_s, effects: effects)
178
+ message = if delivery == :not_sent && LibTmux::Internal::TmuxVersion.refusal?(error)
179
+ LibTmux::Internal::TmuxVersion.hint
180
+ else
181
+ "The operation could not establish its requested result."
182
+ end
183
+ failure_response(code, message, delivery.to_s, effects: effects)
179
184
  end
180
185
 
181
186
  def inspect
@@ -183,7 +183,7 @@ module LibTmux
183
183
  "truncated" => {"type" => "boolean"}, "next_cursor" => text},
184
184
  %w[capture_id server_identity entity items coverage interval truncated])
185
185
  end
186
- {"oneOf" => [object({"ok" => {"const" => true}, "data" => payload}),
186
+ {"type" => "object", "oneOf" => [object({"ok" => {"const" => true}, "data" => payload}),
187
187
  object({"ok" => {"const" => false}, "error" => object({"code" => text,
188
188
  "message" => text, "delivery" => {"enum" => %w[not_sent possibly_sent observed]},
189
189
  "effects" => name == "tmux_run" ? run_effects : effects}, %w[code message delivery])})]}
@@ -237,9 +237,9 @@ module LibTmux
237
237
  if name == "tmux_run"
238
238
  "Run an authored POSIX script in an explicitly enrolled idle, empty zsh 5.9 editor. Opt-in; no terminal injection. Exact queue authorization binds the existing shell generation; execution may follow a later pane replacement without retargeting it. Inherits cwd/exported environment, closed stdin, separate bounded UTF-8/base64 outputs. Defaults: 65536 bytes per output, 65536 script bytes; overflow refuses completion. Reports native exit/signal only from the helper; cancellation never proves descendants stopped."
239
239
  elsif name == "tmux_capture"
240
- "Read exact-pane screen/history rows preserving LF. Defaults: 200 lines, 65536 bytes, zero history lines, track=false. Limits: 1000 lines/history, 262144 bytes. UTF-8 or base64; truncation is separate from unknown history continuity. Tracked process cursors require tmux>=3.3 and a native process identity backend: Linux peer pidfds with matching PID namespaces, or Darwin kqueue process observation. Continuation returns an exact state row splice, not a live-output journal. Refuses capture after-hooks."
240
+ "Read exact-pane screen/history rows preserving LF. Defaults: 200 lines, 65536 bytes, zero history lines, track=false. Limits: 1000 lines/history, 262144 bytes. UTF-8 or base64; truncation is separate from unknown history continuity. Tracked process cursors require tmux>=3.3a and a native process identity backend: Linux peer pidfds with matching PID namespaces, or Darwin kqueue process observation. Continuation returns an exact state row splice, not a live-output journal. Refuses capture after-hooks."
241
241
  elsif name == "tmux_wait"
242
- "Wait for observed literal screen text or the initial pane process exit. Deadline in seconds is capped by the application limit. Uses control events/process descriptors; no polling. Requires tmux>=3.3 and Linux peer pidfds with matching PID namespaces or Darwin kqueue process observation. Reports observed evidence, never remote termination caused by cancellation or an inferred process exit status."
242
+ "Wait for observed literal screen text or the initial pane process exit. Deadline in seconds is capped by the application limit. Uses control events/process descriptors; no polling. Requires tmux>=3.3a and Linux peer pidfds with matching PID namespaces or Darwin kqueue process observation. Reports observed evidence, never remote termination caused by cancellation or an inferred process exit status."
243
243
  elsif name == "tmux_send"
244
244
  "Send literal UTF-8 text or named keys to one exact pane. Opt-in mutation; 64 KiB total text bytes, at most 256 keys. Reports tmux client completion and input dispatch only; never shell completion."
245
245
  elsif name == "tmux_create"
@@ -36,7 +36,11 @@ module LibTmux
36
36
  rescue Interrupt, LibTmux::Cancelled => error
37
37
  report("MCP interrupted; dispatched effects may remain.", 130, failure: error)
38
38
  rescue StandardError => error
39
- report("MCP execution failed (#{error.class}); dispatched effects may remain.", 1, failure: error)
39
+ if error.is_a?(LibTmux::UnsupportedFeatureError) && error.phase == :admission && error.delivery == :not_sent
40
+ report("MCP refused to start: #{error.message}.", 1, failure: error)
41
+ else
42
+ report("MCP execution failed (#{error.class}); dispatched effects may remain.", 1, failure: error)
43
+ end
40
44
  end
41
45
  end.wait
42
46
  rescue OptionParser::ParseError, ArgumentError => error
@@ -30,18 +30,30 @@ module LibTmux
30
30
  end
31
31
  end
32
32
 
33
+ def self.supported_release?(version)
34
+ LibTmux::Internal::TmuxVersion.require_supported(version)
35
+ true
36
+ rescue UnsupportedFeatureError
37
+ false
38
+ end
39
+
40
+ # Before tmux 3.5, whole-array formats are empty even for sparse hooks,
41
+ # so the capture hook is read with show-options instead.
42
+ def self.preflight_hooks?(release)
43
+ release < LibTmux::Internal::TmuxVersion.parse("3.5")
44
+ end
45
+
33
46
  def self.capabilities(version)
34
- parsed = /\A(\d+)\.(\d+)/.match(version)
35
47
  native = case RUBY_PLATFORM
36
48
  when /\A(?:x86_64|aarch64)-linux/
37
49
  ["64-bit Linux with peer pidfds (kernel >= 6.6)", "same PID namespace and matching procfs"]
38
50
  when /\A(?:x86_64|arm64)-darwin/
39
51
  ["64-bit Darwin with kqueue NOTE_EXIT/NOTE_REAP", "fresh pinned-route daemon identity"]
40
52
  end
41
- conditional = native && Fiddle::SIZEOF_LONG == 8 && parsed && ([parsed[1].to_i, parsed[2].to_i] <=> [3, 3]) >= 0
53
+ conditional = native && Fiddle::SIZEOF_LONG == 8 && supported_release?(version)
42
54
  {"screen" => "bounded_rows", "history_continuity" => "unknown",
43
55
  "process_cursor" => conditional ? "conditional" : "unsupported",
44
- "requirements" => ["tmux >= 3.3", *(native || ["supported 64-bit Linux or Darwin process identity backend"]),
56
+ "requirements" => ["tmux >= #{LibTmux::Internal::TmuxVersion::MINIMUM}", *(native || ["supported 64-bit Linux or Darwin process identity backend"]),
45
57
  "live pane process", "empty effective capture hook", "stable capture-hook configuration on tmux < 3.5"],
46
58
  "wait_conditions" => %w[screen_contains process_exit]}
47
59
  end
@@ -260,6 +272,9 @@ module LibTmux
260
272
  private
261
273
 
262
274
  def acquire_identity(snapshot, pane)
275
+ # tmux 3.8 reports no PID for a dead pane; there is no process to bind.
276
+ raise TargetNotFoundError.new("pane process has exited", phase: :admission) if pane.pid.nil?
277
+
263
278
  identity = ProcessIdentity.acquire(@server, server_pid: snapshot.server_info.fetch(:pid), pane_pid: pane.pid,
264
279
  budget: @budget, on_retire: ->(resource) { @owned << resource })
265
280
  @owned << identity
@@ -285,10 +300,10 @@ module LibTmux
285
300
  end
286
301
 
287
302
  def require_tracking(snapshot)
288
- version = /\A(\d+)\.(\d+)/.match(snapshot.server_info.fetch(:version))
289
- unless version && ([version[1].to_i, version[2].to_i] <=> [3, 3]) >= 0
290
- raise UnsupportedFeatureError.new("process cursors require tmux 3.3 death-status formats", phase: :admission)
291
- end
303
+ return if self.class.supported_release?(snapshot.server_info.fetch(:version))
304
+
305
+ raise UnsupportedFeatureError.new("process cursors require tmux #{LibTmux::Internal::TmuxVersion::MINIMUM} or newer",
306
+ phase: :admission)
292
307
  end
293
308
 
294
309
  def read_rows(snapshot, pane, limits, identity)
@@ -308,10 +323,9 @@ module LibTmux
308
323
  hook_error = "libtmux-hook-refused-#{SecureRandom.hex(16)}"
309
324
  hook_failure = @server.__send__(:tmux_command, [hook_error])
310
325
  begin
311
- # Before tmux 3.5, whole-array formats are empty even for sparse hooks.
312
326
  # Configuration must stay stable between this preflight and capture.
313
- version = /\A(\d+)\.(\d+)/.match(snapshot.server_info.fetch(:version))
314
- unless version && ([version[1].to_i, version[2].to_i] <=> [3, 5]) >= 0
327
+ release = LibTmux::Internal::TmuxVersion.parse(snapshot.server_info.fetch(:version))
328
+ if self.class.preflight_hooks?(release)
315
329
  hooks = @server.__send__(:execute_typed,
316
330
  [names.fetch("show-options"), "-A", "-v", "-t", target, "after-capture-pane"], **@budget.options)
317
331
  unless hooks.stdout.empty?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module LibTmux
4
4
  module MCP
5
- VERSION = "0.1.0.alpha.1"
5
+ VERSION = "0.1.0.alpha.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: libtmux-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.alpha.1
4
+ version: 0.1.0.alpha.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - libtmux contributors
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.1.0.alpha.1
18
+ version: 0.1.0.alpha.2
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.1.0.alpha.1
25
+ version: 0.1.0.alpha.2
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: libtmux-async
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.1.0.alpha.1
32
+ version: 0.1.0.alpha.2
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.1.0.alpha.1
39
+ version: 0.1.0.alpha.2
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: mcp
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +94,8 @@ extra_rdoc_files: []
94
94
  files:
95
95
  - LICENSE
96
96
  - README.md
97
+ - assets/logo.png
98
+ - assets/logo.svg
97
99
  - exe/libtmux-mcp
98
100
  - lib/libtmux/mcp.rb
99
101
  - lib/libtmux/mcp/application.rb
@@ -116,6 +118,7 @@ licenses:
116
118
  - MIT
117
119
  metadata:
118
120
  source_code_uri: https://github.com/libtmux/libtmux-ruby
121
+ documentation_uri: https://libtmux.org/en/ruby/v0.1.0.alpha.2/mcp/
119
122
  rdoc_options: []
120
123
  require_paths:
121
124
  - lib