Skip to content

Commit 34af705

Browse files
committed
mcp(feat[window_tools]): add get_window_info for single-window metadata
why: get_pane_info and get_server_info exist, but the window and session peers do not. Agents given a window_id and asked "what are this window's dimensions?" have to call list_panes or list_windows and filter — wasteful. Adding get_window_info closes one half of the core-tmux-hierarchy symmetry (get_session_info follows next). The symmetry argument is deliberately bounded to the four-level core hierarchy (Server > Session > Window > Pane). This is NOT a license to add get_buffer_info / get_hook_info / get_option_info — those scopes are outside the hierarchy and the existing show_*/load_* tools already cover their reads. An inline comment on the function memorializes that boundary so future contributors don't re-relitigate it. what: - Add get_window_info(window_id, window_index, session_name, session_id, socket_name) returning WindowInfo. Reuses _resolve_window (accepts window_id OR window_index+session) and _serialize_window — no new helpers. - Register with ANNOTATIONS_RO + TAG_READONLY, placed next to list_panes in the Window tool group. - Add test_get_window_info (resolves by window_id) and test_get_window_info_by_index (resolves by index+session) mirroring the minimal-assertion style used by test_list_panes. - Add docs/tools/window/get-window-info.md modeled after get-pane-info.md; insert the new page into the Window tools index grid and toctree. - Append get_window_info to the README tool catalog Window row.
1 parent d2901f9 commit 34af705

5 files changed

Lines changed: 143 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Give your AI agent hands inside the terminal — create sessions, run commands,
1717
|--------|-------|
1818
| **Server** | `list_sessions`, `create_session`, `kill_server`, `get_server_info` |
1919
| **Session** | `list_windows`, `create_window`, `rename_session`, `select_window`, `kill_session` |
20-
| **Window** | `list_panes`, `split_window`, `rename_window`, `select_layout`, `resize_window`, `move_window`, `kill_window` |
20+
| **Window** | `list_panes`, `get_window_info`, `split_window`, `rename_window`, `select_layout`, `resize_window`, `move_window`, `kill_window` |
2121
| **Pane** | `send_keys`, `paste_text`, `capture_pane`, `snapshot_pane`, `search_panes`, `get_pane_info`, `wait_for_text`, `wait_for_content_change`, `display_message`, `select_pane`, `swap_pane`, `resize_pane`, `set_pane_title`, `clear_pane`, `pipe_pane`, `enter_copy_mode`, `exit_copy_mode`, `kill_pane` |
2222
| **Options** | `show_option`, `set_option` |
2323
| **Environment** | `show_environment`, `set_environment` |
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Get window info
2+
3+
```{fastmcp-tool} window_tools.get_window_info
4+
```
5+
6+
**Use when** you need metadata for a single window (name, index, layout,
7+
dimensions, pane count) and you already know the `window_id` or
8+
`window_index`. Avoids the `list_windows` + filter dance.
9+
10+
**Avoid when** you need every window in a session — call `list_panes` with
11+
`session_id` or iterate through the session's windows via the
12+
`tmux://sessions/{name}/windows` resource.
13+
14+
**Side effects:** None. Readonly.
15+
16+
**Example:**
17+
18+
```json
19+
{
20+
"tool": "get_window_info",
21+
"arguments": {
22+
"window_id": "@1"
23+
}
24+
}
25+
```
26+
27+
Response:
28+
29+
```json
30+
{
31+
"window_id": "@1",
32+
"window_name": "editor",
33+
"window_index": "1",
34+
"session_id": "$0",
35+
"session_name": "dev",
36+
"pane_count": 2,
37+
"window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]",
38+
"window_active": "1",
39+
"window_width": "80",
40+
"window_height": "24"
41+
}
42+
```
43+
44+
Resolve by `window_index` when only the index is known — requires
45+
`session_name` or `session_id` to disambiguate:
46+
47+
```json
48+
{
49+
"tool": "get_window_info",
50+
"arguments": {
51+
"window_index": "1",
52+
"session_name": "dev"
53+
}
54+
}
55+
```
56+
57+
```{fastmcp-tool-input} window_tools.get_window_info
58+
```

docs/tools/window/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Window-scoped tools — enumerate panes, split / rename / relayout / resize / mo
99
Enumerate panes inside a window.
1010
:::
1111

12+
:::{grid-item-card} {tooliconl}`get-window-info`
13+
Read metadata for one window.
14+
:::
15+
1216
:::{grid-item-card} {tooliconl}`split-window`
1317
Split a window into a new pane.
1418
:::
@@ -40,6 +44,7 @@ Terminate a window. Destructive.
4044
:maxdepth: 1
4145
4246
list-panes
47+
get-window-info
4348
split-window
4449
rename-window
4550
select-layout

src/libtmux_mcp/tools/window_tools.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,56 @@ def list_panes(
9898
return _apply_filters(panes, filters, _serialize_pane)
9999

100100

101+
# get_window_info completes the core-tmux-hierarchy symmetry of get_*_info
102+
# tools: the four hierarchy levels (server, session, window, pane) now each
103+
# have a targeted single-object read. This is deliberately NOT a license to
104+
# add get_buffer_info / get_hook_info / get_option_info — those scopes are
105+
# not part of the hierarchy and the existing show_*/load_* tools already
106+
# cover their reads. See the brainstorm-and-refine audit §7.1.
107+
@handle_tool_errors
108+
def get_window_info(
109+
window_id: str | None = None,
110+
window_index: str | None = None,
111+
session_name: str | None = None,
112+
session_id: str | None = None,
113+
socket_name: str | None = None,
114+
) -> WindowInfo:
115+
"""Return metadata for a single tmux window (ID, name, layout, dimensions).
116+
117+
Use this instead of list_windows + filter when you only need one
118+
window's info. Resolves the window by window_id first; falls back
119+
to window_index within a session if window_id is not given.
120+
121+
Parameters
122+
----------
123+
window_id : str, optional
124+
Window ID (e.g. '@1').
125+
window_index : str, optional
126+
Window index within the session. Requires session_name or
127+
session_id to disambiguate.
128+
session_name : str, optional
129+
Session name for window_index lookup.
130+
session_id : str, optional
131+
Session ID for window_index lookup.
132+
socket_name : str, optional
133+
tmux socket name.
134+
135+
Returns
136+
-------
137+
WindowInfo
138+
Serialized window metadata.
139+
"""
140+
server = _get_server(socket_name=socket_name)
141+
window = _resolve_window(
142+
server,
143+
window_id=window_id,
144+
window_index=window_index,
145+
session_name=session_name,
146+
session_id=session_id,
147+
)
148+
return _serialize_window(window)
149+
150+
101151
@handle_tool_errors
102152
def split_window(
103153
pane_id: str | None = None,
@@ -425,6 +475,9 @@ def register(mcp: FastMCP) -> None:
425475
mcp.tool(title="List Panes", annotations=ANNOTATIONS_RO, tags={TAG_READONLY})(
426476
list_panes
427477
)
478+
mcp.tool(title="Get Window Info", annotations=ANNOTATIONS_RO, tags={TAG_READONLY})(
479+
get_window_info
480+
)
428481
mcp.tool(title="Split Window", annotations=ANNOTATIONS_CREATE, tags={TAG_MUTATING})(
429482
split_window
430483
)

tests/test_window_tools.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from fastmcp.exceptions import ToolError
99

1010
from libtmux_mcp.tools.window_tools import (
11+
get_window_info,
1112
kill_window,
1213
list_panes,
1314
move_window,
@@ -34,6 +35,31 @@ def test_list_panes(mcp_server: Server, mcp_session: Session) -> None:
3435
assert result[0].pane_id is not None
3536

3637

38+
def test_get_window_info(mcp_server: Server, mcp_session: Session) -> None:
39+
"""get_window_info returns a WindowInfo for a single window."""
40+
window = mcp_session.active_window
41+
result = get_window_info(
42+
window_id=window.window_id,
43+
socket_name=mcp_server.socket_name,
44+
)
45+
assert result.window_id == window.window_id
46+
assert result.window_name is not None
47+
assert result.pane_count >= 1
48+
assert result.session_id == mcp_session.session_id
49+
50+
51+
def test_get_window_info_by_index(mcp_server: Server, mcp_session: Session) -> None:
52+
"""get_window_info resolves by window_index when session is named."""
53+
window = mcp_session.active_window
54+
assert window.window_index is not None
55+
result = get_window_info(
56+
window_index=window.window_index,
57+
session_name=mcp_session.session_name,
58+
socket_name=mcp_server.socket_name,
59+
)
60+
assert result.window_id == window.window_id
61+
62+
3763
def test_split_window(mcp_server: Server, mcp_session: Session) -> None:
3864
"""split_window creates a new pane."""
3965
window = mcp_session.active_window

0 commit comments

Comments
 (0)