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 +4 -4
- data/README.md +24 -4
- data/assets/logo.png +0 -0
- data/assets/logo.svg +2 -0
- data/lib/libtmux/mcp/application.rb +6 -1
- data/lib/libtmux/mcp/catalog.rb +3 -3
- data/lib/libtmux/mcp/cli.rb +5 -1
- data/lib/libtmux/mcp/observation.rb +24 -10
- data/lib/libtmux/mcp/version.rb +1 -1
- metadata +8 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67e80d372c18bcd568c338b521a4511cd8ef7ab96e4d0cfcc7ddca3433a2cad5
|
|
4
|
+
data.tar.gz: c2a828bb53e812bf8c2de9ee3ddba7a09eb6064d31d075633bb57248a494bd9b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
|
80
|
-
|
|
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
|
-
|
|
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
|
data/lib/libtmux/mcp/catalog.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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"
|
data/lib/libtmux/mcp/cli.rb
CHANGED
|
@@ -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
|
-
|
|
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 &&
|
|
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 >=
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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?
|
data/lib/libtmux/mcp/version.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|