Skip to content

Commit 2ca849f

Browse files
authored
py(deps[fastmcp]): Migrate to the FastMCP 4 release line (#131)
Move the FastMCP requirement to the 4.x release line, which brings MCP Python SDK v2 with it, and repair what its stricter parameter screening and error classification change for a caller. No tool, resource, or prompt changes shape, and one server serves both the session-based and the sessionless 2026-07-28 protocol eras, negotiated per connection. - Keep `tmux://` reads accepting the socket paths `list_servers` reports and the `<letter>:<rest>` session names tmux allows, screening per template so traversal and null-byte checks survive. - Answer argument completion for the `tmux://` templates from the live server, and stop a wait tool failing when its context carries no session. - Retire the camelCase SDK shims in the suite, bind the documented dependency floors to the manifest, skip path-like session-name tests on tmux below 3.7, and fail the docs build on a Sphinx warning.
2 parents 9d90d11 + 55a2395 commit 2ca849f

21 files changed

Lines changed: 748 additions & 131 deletions

‎.github/workflows/docs.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ jobs:
7474
- name: Build documentation
7575
if: env.PUBLISH == 'true'
7676
run: |
77-
cd docs && just html
77+
cd docs && just sphinxopts="-W --keep-going" html
7878
7979
- name: Configure AWS Credentials
8080
if: env.PUBLISH == 'true'

‎CHANGES‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,81 @@
66
_Notes on upcoming releases will be added here_
77
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->
88

9+
### Dependencies
10+
11+
**Minimum `fastmcp>=4.0.2`** (was `>=3.4.2,<4.0.0`)
12+
13+
libtmux-mcp tracks the FastMCP 4 line. FastMCP 4 builds on MCP Python SDK
14+
v2, which brings `mcp>=2.0.0`, `mcp-types` and pydantic 2.12 with it, and
15+
replaces httpx with httpx2 across FastMCP's HTTP stack. No tool, resource or
16+
prompt changes shape, and one server serves both the session-based and the
17+
sessionless `2026-07-28` protocol eras, negotiated per connection.
18+
19+
**FastMCP releases are adopted without waiting out the cooldown**
20+
21+
The dependency cooldown held every FastMCP release back, so a refreshed lock
22+
resolved to a patch behind the one being tested against. `fastmcp` and its
23+
`fastmcp-slim` companion are exempt from it.
24+
25+
### What's new
26+
27+
**`tmux://` templates answer argument completion**
28+
29+
The templates take live tmux identifiers, and MCP publishes a template's URI
30+
but never its parameter domain, so a reader had no way to discover a valid
31+
session name or pane id from the listing. A completion handler offers them
32+
from the live server, filtered by what has been typed and scoped by an
33+
already-supplied `socket_name`. A server that is gone yields no suggestions
34+
rather than an error.
35+
36+
Whether suggestions appear depends on the client. The MCP Inspector's browser
37+
UI and VS Code send `completion/complete`; measured against five agent CLIs,
38+
none do.
39+
40+
### Fixes
41+
42+
**`tmux://` reads keep accepting tmux names that look like paths**
43+
44+
FastMCP 4 screens templated resource parameters before a handler runs. Its
45+
absolute-path check does not fit tmux identifiers: it rejects the socket paths
46+
`list_servers` reports, and reads any `<letter>:<rest>` session name — which
47+
tmux accepts — as a Windows drive-relative path. Both stay readable, so
48+
`tmux://` and the equivalent tools continue to agree on which sessions and
49+
servers exist.
50+
51+
Traversal and null-byte screening remain on, and are set per template rather
52+
than server-wide. `socket_name` reaches `tmux -L`, which appends it to the
53+
socket directory without normalising, so `../..` really does place the socket
54+
elsewhere; exempting the parameter outright would drop that guard along with
55+
the absolute-path check.
56+
57+
**A `tmux://` read that fails now says so as `-32603`**
58+
59+
FastMCP's `ErrorHandlingMiddleware` matches not-found exceptions by exact
60+
class, so only an unknown resource URI still maps to `-32002`. A missing
61+
session raises `ResourceError`, and a rejected path raises
62+
`ResourceSecurityError` — a `NotFoundError` subclass, and so not an exact
63+
match — and both reach the client as `-32603` "Internal error". The message is
64+
unchanged; only clients matching on the numeric code are affected.
65+
66+
**A wait tool survives a context that carries no session**
67+
68+
`Context.session` raises rather than degrading when there is no request
69+
context, so `ctx.warning` and `ctx.report_progress` took the whole tool call
70+
down in any execution that lacks one. The wait helpers check before
71+
delivering: the call returns its normal result, without notifications, instead
72+
of failing. Notification delivery on an ordinary request is unchanged.
73+
974
### Documentation
1075

76+
**The completion topic describes the handlers the server registers**
77+
78+
The page said libtmux-mcp registered no completion handlers and offered
79+
workarounds for the gap. It describes what the `tmux://` templates complete,
80+
and states the caveat where it now belongs: whether suggestions appear depends
81+
on the client. The retired claims are pinned as forbidden text, so the page
82+
cannot drift back to underclaiming a shipped feature.
83+
1184
**The toolset migration notes reach the migration page**
1285

1386
The 0.1.0a21 notes mapping the retired tiers onto `inspect`, `manage`,
@@ -17,6 +90,38 @@ of `MIGRATION`, and they state that `LIBTMUX_SAFETY` raises while the server
1790
module loads — every value, the former default `mutating` included — and that
1891
the former default is reached by removing the variable and setting nothing.
1992

93+
### Development
94+
95+
**A Sphinx warning fails the documentation build**
96+
97+
The docs workflow passes `-W --keep-going`, so a warning stops the build that
98+
publishes the site instead of scrolling past in a green run.
99+
100+
**Path-like session-name tests skip on tmux below 3.7**
101+
102+
tmux before 3.7 rewrites `:` and `.` in a session name to `_`, turning `a:1`
103+
into `a_1`, and exits 0 with no stderr — so a caller cannot tell from the
104+
result. The resource tests that need such a name check what tmux stored and
105+
skip when it differs, naming the rewrite. They ran only against a newer tmux
106+
before, and reported the resulting absent session as a failed read on every
107+
supported tmux in CI.
108+
109+
**The test suite runs without FastMCP's camelCase compatibility shims**
110+
111+
FastMCP 4 answers MCP SDK v1 attribute spellings such as `tool.inputSchema`
112+
through warn-once shims it plans to remove. Neither gate catches a test that
113+
depends on one: the type checker sees `Any`, and the warning fires once per
114+
attribute for the whole run. The suite sets `FASTMCP_MCP_CAMELCASE_COMPAT=false`,
115+
so a surviving camelCase read raises `AttributeError` and fails the test that
116+
made it.
117+
118+
**The compatibility reference is bound to the manifest**
119+
120+
The dependency table restated floors by hand and had drifted: it named libtmux
121+
0.55 against a manifest requiring 0.62, and omitted `regex` entirely. A test
122+
compares the table against `[project].dependencies` as version ranges, so the
123+
next floor that moves without its documentation fails the suite.
124+
20125
## libtmux-mcp 0.1.0a21 (2026-08-30)
21126

22127
libtmux-mcp 0.1.0a21 replaces cumulative safety tiers with independently

‎conftest.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from __future__ import annotations
1212

13+
import os
1314
import shutil
1415
import typing as t
1516

@@ -24,6 +25,18 @@
2425
if t.TYPE_CHECKING:
2526
import pathlib
2627

28+
# Run the suite without fastmcp's camelCase compatibility bridge.
29+
# fastmcp 4 still answers SDK v1 spellings like `tool.inputSchema`
30+
# through warn-once shims scheduled for removal, and neither gate
31+
# catches a read that relies on one: mypy sees `Any`, and the warning
32+
# fires once per attribute for the whole run. With the bridge off a
33+
# surviving read raises AttributeError and fails its own test.
34+
#
35+
# Set through the environment, before anything imports fastmcp (its
36+
# settings singleton is built at import), so subprocess tests inherit
37+
# it via os.environ.copy().
38+
os.environ.setdefault("FASTMCP_MCP_CAMELCASE_COMPAT", "false")
39+
2740
pytest_plugins = ["pytester", "sphinx.testing.fixtures"]
2841

2942

‎docs/reference/compatibility.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424

2525
| Package | Required version |
2626
|---------|-----------------|
27-
| [libtmux](https://libtmux.git-pull.com/) | >= 0.55.0, < 1.0 |
28-
| [FastMCP](https://github.com/jlowin/fastmcp) | >= 3.1.0, < 4.0.0 |
27+
| [libtmux](https://libtmux.git-pull.com/) | >= 0.62.0, < 1.0 |
28+
| [FastMCP](https://github.com/jlowin/fastmcp) | >= 4.0.2, < 5.0.0 |
29+
| [regex](https://github.com/mrabarnett/mrab-regex) | >= 2024.11.6 |
2930

3031
## MCP clients
3132

‎docs/topics/completion.md‎

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
The
66
[MCP completion](https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion)
77
protocol lets clients ask a server for argument suggestions. libtmux-mcp
8-
does not currently register custom completion handlers.
8+
answers it for the ``tmux://`` resource templates, offering the session
9+
names, window indexes and pane ids the live server currently has.
910

1011
## What the spec does
1112

@@ -17,20 +18,29 @@ session picker popup when filling ``session_name=`` on
1718

1819
## What libtmux-mcp currently exposes
1920

20-
- **Prompt arguments** — the four recipes ({doc}`/prompts`)
21-
advertise their argument names and types through their schemas.
22-
- **Resource template parameters** —
23-
{doc}`/resources` URIs carry ``{session_name}``,
24-
``{window_index}``, ``{pane_id}``, and ``{?socket_name}``
25-
placeholders.
21+
- **Resource template parameters** — {doc}`/resources` URIs carry
22+
``{session_name}``, ``{window_index}``, ``{pane_id}`` and
23+
``{?socket_name}``. The first three complete from the live server,
24+
filtered by what has been typed. Supplying ``socket_name`` first
25+
scopes the suggestions to that server; a server that is gone yields
26+
no suggestions rather than an error.
27+
- **Prompt arguments** — the four recipes ({doc}`/prompts`) advertise
28+
their argument names and types through their schemas. They take free
29+
text rather than tmux identifiers, so they have no domain to
30+
enumerate.
31+
32+
MCP publishes a template's URI but never its parameter domain, so
33+
without this a reader has no way to discover a valid session name or
34+
pane id from the template listing alone.
2635

2736
```{warning}
28-
Clients should not rely on ``completion/complete`` returning live tmux
29-
suggestions, schema-derived examples, or enum-like values today.
30-
Adding live suggestions requires dedicated completion handlers.
37+
Whether you see suggestions depends on the client, not the server.
38+
Completion serves human-facing hosts — the MCP Inspector's browser UI
39+
and VS Code send ``completion/complete``. Measured against five agent
40+
CLIs, none send it, so an agent sees no difference.
3141
```
3242

33-
## Workarounds for clients that need live enumeration
43+
## Enumerating from a client that does not complete
3444

3545
Agents that need to pick a real session / window / pane can call
3646
{tool}`list-sessions`, {tool}`list-windows`, or {tool}`list-panes`

‎pyproject.toml‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ include = [
4141

4242
dependencies = [
4343
"libtmux>=0.62.0,<1.0",
44-
"fastmcp>=3.4.2,<4.0.0",
44+
"fastmcp>=4.0.2,<5.0.0",
4545
"regex>=2024.11.6",
4646
]
4747

@@ -57,9 +57,9 @@ libtmux-mcp = "libtmux_mcp:main"
5757
[dependency-groups]
5858
dev = [
5959
# Docs
60-
"gp-sphinx==0.1.0a38",
61-
"sphinx-autodoc-api-style==0.1.0a38",
62-
"sphinx-autodoc-fastmcp==0.1.0a38",
60+
"gp-sphinx==0.1.0a39",
61+
"sphinx-autodoc-api-style==0.1.0a39",
62+
"sphinx-autodoc-fastmcp==0.1.0a39",
6363
"gp-libs>=0.0.19",
6464
"sphinx-autobuild",
6565
# Testing
@@ -73,6 +73,8 @@ dev = [
7373
"syrupy>=5.1.0",
7474
# scripts/mcp_swap.py (PEP 723 script; dep listed here so tests can import it)
7575
"tomlkit>=0.13",
76+
# tests/docs/test_compatibility.py compares version ranges, not strings.
77+
"packaging",
7678
# Coverage
7779
"codecov",
7880
"coverage",
@@ -84,9 +86,9 @@ dev = [
8486
]
8587

8688
docs = [
87-
"gp-sphinx==0.1.0a38",
88-
"sphinx-autodoc-api-style==0.1.0a38",
89-
"sphinx-autodoc-fastmcp==0.1.0a38",
89+
"gp-sphinx==0.1.0a39",
90+
"sphinx-autodoc-api-style==0.1.0a39",
91+
"sphinx-autodoc-fastmcp==0.1.0a39",
9092
"gp-libs>=0.0.19",
9193
"sphinx-autobuild",
9294
]
@@ -138,6 +140,8 @@ sphinx-gp-sitemap = false
138140
sphinx-gp-theme = false
139141
sphinx-ux-autodoc-layout = false
140142
sphinx-ux-badges = false
143+
fastmcp = false
144+
fastmcp-slim = false
141145

142146
[tool.mypy]
143147
strict = true

‎src/libtmux_mcp/middleware.py‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,15 @@
3535
from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
3636
from fastmcp.server.middleware.response_limiting import ResponseLimitingMiddleware
3737
from fastmcp.server.transforms import Transform, Visibility
38-
from fastmcp.tools.base import ToolResult
38+
from fastmcp.tools import ToolResult
3939
from mcp.types import CallToolRequestParams, TextContent
4040
from pydantic import ValidationError as PydanticValidationError
4141

4242
from libtmux_mcp._utils import TOOLSET_INSPECT, VALID_TOOLSETS, ExpectedToolError
4343

4444
if t.TYPE_CHECKING:
4545
from fastmcp.server.transforms import GetToolNext
46-
from fastmcp.tools.base import Tool
46+
from fastmcp.tools import Tool
4747
from fastmcp.utilities.versions import VersionSpec
4848

4949

@@ -406,9 +406,18 @@ class ToolErrorResultMiddleware(ErrorHandlingMiddleware):
406406
intercepts tool-call exceptions first (``on_call_tool`` is the
407407
innermost hook of a middleware's chain) and returns
408408
:func:`_error_tool_result` instead; non-tool messages fall through
409-
to the inherited ``on_message``, preserving the MCP ``-32002``
410-
resource-not-found transform this middleware was originally
411-
adopted for.
409+
to the inherited ``on_message``.
410+
411+
That inherited transform matches the not-found exception types by
412+
exact class -- ``error_type in (FileNotFoundError, KeyError,
413+
NotFoundError)`` -- so only an unknown resource URI, which fastmcp
414+
raises as ``NotFoundError`` itself, reaches a client as ``-32002``.
415+
The ``tmux://`` handlers raise :exc:`~fastmcp.exceptions.ResourceError`
416+
for a missing session, and path screening raises
417+
``ResourceSecurityError``, a ``NotFoundError`` *subclass*; neither is
418+
an exact match, so both arrive as ``-32603`` "Internal error" -- the
419+
code that tells a client the server is broken, for what is really an
420+
absent or disallowed target.
412421
413422
Logging honors ``FastMCPError.log_level`` (fastmcp >= 3.3): the
414423
expected failures demoted to WARNING by

‎src/libtmux_mcp/prompts/__init__.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
if t.TYPE_CHECKING:
3232
from fastmcp import FastMCP
33-
from fastmcp.tools.base import Tool
33+
from fastmcp.tools import Tool
3434

3535
#: Env-var gate that enables exposing prompts as tools for clients that
3636
#: do not speak the MCP prompts protocol. Off by default — a sprawling
@@ -39,10 +39,10 @@
3939

4040
_PROMPT_TOOL_NAMES = frozenset({"list_prompts", "get_prompt"})
4141
_PROMPT_TOOL_ANNOTATIONS = ToolAnnotations(
42-
readOnlyHint=True,
43-
destructiveHint=False,
44-
idempotentHint=True,
45-
openWorldHint=False,
42+
read_only_hint=True,
43+
destructive_hint=False,
44+
idempotent_hint=True,
45+
open_world_hint=False,
4646
)
4747

4848
__all__ = ["ENV_PROMPTS_AS_TOOLS", "register_prompts"]

‎src/libtmux_mcp/resources/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ def register_resources(mcp: FastMCP) -> None:
1313
from libtmux_mcp.resources import hierarchy
1414

1515
hierarchy.register(mcp)
16+
hierarchy.register_completions(mcp)

0 commit comments

Comments
 (0)