|
42 | 42 | #: :func:`libtmux_mcp._utils._get_server`. |
43 | 43 | _ServerCacheKey: t.TypeAlias = tuple[str | None, str | None, str | None] |
44 | 44 |
|
45 | | -_BASE_INSTRUCTIONS = ( |
| 45 | +# --------------------------------------------------------------------------- |
| 46 | +# _BASE_INSTRUCTIONS — composed from named segments. |
| 47 | +# |
| 48 | +# The string handed to FastMCP grew organically from "what does this server |
| 49 | +# do?" toward a hybrid of positive guidance (HIERARCHY, READ_TOOLS, |
| 50 | +# WAIT_NOT_POLL) and *gap-explainers* (HOOKS_GAP, BUFFERS_GAP) that document |
| 51 | +# why a tool the agent might expect is absent. Splitting into named |
| 52 | +# constants keeps additions deliberate: when a new ``_GAP`` segment feels |
| 53 | +# tempting, prefer first to push the explanation into the relevant tool's |
| 54 | +# docstring/description (where the agent encounters it at call time) and |
| 55 | +# only fall back to a server-level segment when the gap is *server-shaped* |
| 56 | +# (e.g. an entire tool family is intentionally missing). |
| 57 | +# |
| 58 | +# Output text is byte-identical to the previous monolith; tests assert on |
| 59 | +# substrings of ``_BASE_INSTRUCTIONS``, so keeping the join shape stable |
| 60 | +# matters. |
| 61 | +# --------------------------------------------------------------------------- |
| 62 | + |
| 63 | +_INSTR_HIERARCHY = ( |
46 | 64 | "libtmux MCP server for programmatic tmux control. " |
47 | 65 | "tmux hierarchy: Server > Session > Window > Pane. " |
48 | 66 | "Use pane_id (e.g. '%1') as the preferred targeting method - " |
49 | 67 | "it is globally unique within a tmux server. " |
50 | 68 | "Use send_keys to execute commands and capture_pane to read output. " |
51 | 69 | "Targeted tmux tools accept an optional socket_name parameter " |
52 | 70 | "(defaults to LIBTMUX_SOCKET env var); list_servers discovers " |
53 | | - "sockets via TMUX_TMPDIR plus optional extra_socket_paths instead.\n\n" |
| 71 | + "sockets via TMUX_TMPDIR plus optional extra_socket_paths instead." |
| 72 | +) |
| 73 | + |
| 74 | +_INSTR_METADATA_VS_CONTENT = ( |
54 | 75 | "IMPORTANT — metadata vs content: list_windows, list_panes, and " |
55 | 76 | "list_sessions only search metadata (names, IDs, current command). " |
56 | 77 | "To find text that is actually visible in terminals — when users ask " |
57 | 78 | "what panes 'contain', 'mention', 'show', or 'have' — use " |
58 | 79 | "search_panes to search across all pane contents, or list_panes + " |
59 | | - "capture_pane on each pane for manual inspection.\n\n" |
| 80 | + "capture_pane on each pane for manual inspection." |
| 81 | +) |
| 82 | + |
| 83 | +_INSTR_READ_TOOLS = ( |
60 | 84 | "READ TOOLS TO PREFER: snapshot_pane returns pane content plus " |
61 | 85 | "cursor position, mode, and scroll state in one call — use it " |
62 | 86 | "instead of capture_pane + get_pane_info when you need context. " |
63 | 87 | "display_message evaluates a tmux format string (e.g. " |
64 | 88 | "'#{pane_current_command}', '#{session_name}') against a target " |
65 | 89 | "and returns the expanded value — cheaper than parsing captured " |
66 | 90 | "output. (The tool is named after the tmux 'display-message -p' " |
67 | | - "verb it wraps; its MCP title is 'Evaluate tmux Format String'.)\n\n" |
| 91 | + "verb it wraps; its MCP title is 'Evaluate tmux Format String'.)" |
| 92 | +) |
| 93 | + |
| 94 | +_INSTR_WAIT_NOT_POLL = ( |
68 | 95 | "WAIT, DON'T POLL: for 'run command, wait for output' workflows " |
69 | 96 | "use wait_for_text (matches text/regex on a pane) or " |
70 | 97 | "wait_for_content_change (waits for any change). These block " |
71 | 98 | "server-side until the condition is met or the timeout expires, " |
72 | 99 | "which is dramatically cheaper in agent turns than capture_pane " |
73 | | - "in a retry loop.\n\n" |
| 100 | + "in a retry loop." |
| 101 | +) |
| 102 | + |
| 103 | +#: Gap-explainer: write-hook tools are intentionally absent. See module |
| 104 | +#: comment above for when to add another ``_GAP`` segment vs. push the |
| 105 | +#: explanation into a tool description. |
| 106 | +_INSTR_HOOKS_GAP = ( |
74 | 107 | "HOOKS ARE READ-ONLY: inspect via show_hooks / show_hook. Write-hook " |
75 | 108 | "tools are intentionally not exposed — tmux hooks survive process " |
76 | 109 | "death, so they belong in your tmux config file, not a transient " |
77 | | - "MCP session.\n\n" |
| 110 | + "MCP session." |
| 111 | +) |
| 112 | + |
| 113 | +#: Gap-explainer: ``list_buffers`` is intentionally absent because tmux |
| 114 | +#: buffers can include OS clipboard history. See module comment above. |
| 115 | +_INSTR_BUFFERS_GAP = ( |
78 | 116 | "BUFFERS: load_buffer stages content, paste_buffer delivers it into " |
79 | 117 | "a pane, delete_buffer removes the staged buffer. Track owned " |
80 | 118 | "buffers via the BufferRef returned from load_buffer — there is no " |
81 | 119 | "list_buffers tool because tmux buffers may include OS clipboard " |
82 | 120 | "history (passwords, private snippets)." |
83 | 121 | ) |
84 | 122 |
|
| 123 | +_BASE_INSTRUCTIONS = "\n\n".join( |
| 124 | + ( |
| 125 | + _INSTR_HIERARCHY, |
| 126 | + _INSTR_METADATA_VS_CONTENT, |
| 127 | + _INSTR_READ_TOOLS, |
| 128 | + _INSTR_WAIT_NOT_POLL, |
| 129 | + _INSTR_HOOKS_GAP, |
| 130 | + _INSTR_BUFFERS_GAP, |
| 131 | + ) |
| 132 | +) |
| 133 | + |
85 | 134 |
|
86 | 135 | def _build_instructions(safety_level: str = TAG_MUTATING) -> str: |
87 | 136 | """Build server instructions with agent context and safety level. |
|
0 commit comments