Conversation
SPIKE ONLY — this branch must never merge. Records the exact dependency state the trial ran against so the measurements are reproducible. fastmcp-tasks 4.0.0a2 hard-pins fastmcp-slim==4.0.0a2, which drags mcp 1.28.1 -> 2.0.0b2, adds mcp-types 2.0.0b2, and swaps httpx/httpcore for httpx2/httpcore2. Every version in the chain is alpha or beta. The cooldown exemption is deliberate and scoped: the user's global uv config sets exclude-newer = "3 days" as a supply-chain guard, and 4.0.0a2 was published one day before this trial. Exempting it here in a throwaway worktree evaluates the alpha without weakening that policy anywhere else. Trial outcome: REJECT. memory:// does work with zero Redis network I/O (proved in an isolated network namespace: 321ms, TCP ActiveOpens 0->0), and the alpha genuinely fixes 3.4.4's hardcoded poll interval. But numeric progress is still dropped under task execution — 0 callbacks vs 36 in the foreground on the real wait_for_text — and no shipping client declares io.modelcontextprotocol/tasks. See docs/topics/waiting.md on the shipping branch.
SPIKE BRANCH — the tasks verdict is still REJECT; this only makes the
arm runnable so the rejection rests on measurement rather than on a
server that would not boot.
mcp 2.0.0b2 deletes the `mcp.types` module; `CallToolRequestParams` and
`TextContent` now live in the separate `mcp-types` distribution, which
is where fastmcp 4.x imports them from too. That is the entire reason
the server would not start:
ModuleNotFoundError: No module named 'mcp.types'
ONE line in `src/` (`middleware.py`), plus the same import in
`test_middleware.py`. Everything else that was failing turned out to be
latent fragility in the tests and one real silent-degradation bug, all
of which are fixed on the shipping branch and arrive here by rebase:
the `clientInfo`/`client_info` accessor, and annotation assertions that
pinned the SDK's Python spelling instead of the wire names.
Full chain green here: ruff, ruff format, mypy (65 files), 790 tests —
identical to the shipping branch.
`uv` records the pre-release mode it resolved under in `uv.lock`, so a lockfile built with `UV_PRERELEASE=allow` fails `uv lock --check` for anyone who does not also export it — and `uv sync` then wants to re-resolve. Checking the branch out and running the project's own commands did not work. Declare `prerelease = "allow"` under `[tool.uv]` instead. `uv lock`, `uv lock --check` and `uv sync` now all succeed with no environment override, which is what makes this branch runnable rather than merely committed. Still SPIKE ONLY: this and the `exclude-newer-package` exemptions above it exist so the fastmcp 4.x alpha can be evaluated, and must not reach a shipping branch.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #108 +/- ##
=======================================
Coverage 86.13% 86.13%
=======================================
Files 46 46
Lines 3593 3593
Branches 516 516
=======================================
Hits 3095 3095
Misses 354 354
Partials 144 144 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
A prototype. It wires libtmux-mcp up to the
fastmcp-tasks4.x alpha so we can find out what the MCP tasks extension (SEP-2663) actually does to a tool likewait_for_text— a tool that blocks, reports progress, and owns a child process.Not proposed for merge, and there is no conclusion here to sign off on. It pins an alpha dependency chain and carries supply-chain exemptions to do it, so it should stay a branch. What it is for is making the questions cheap to answer: the branch runs, so anything below can be re-measured in a checkout rather than re-derived from scratch.
What it changes
pyproject.tomlfastmcp==4.0.0a2andfastmcp-tasks==4.0.0a2; adds[tool.uv] prerelease = "allow"andexclude-newer-packageexemptions for the alpha chainsrc/libtmux_mcp/middleware.pyfrom mcp.types import ...→from mcp_types import ...tests/test_middleware.pymcp_types.Implementationuv.lockmcp1.28.1 → 2.0.0b2, addsmcp-types2.0.0b2 andpydocket, swaps httpx/httpcore for httpx2/httpcore2The port itself is one line in
src/. That was the surprise —mcp2.0.0b2 deletes themcp.typesmodule, which looks alarming until you find we import from it exactly once.Status: it runs
Everything below is green on the alpha stack, with no environment overrides:
uv lock --checkclean — the third commit exists becauseuvrecords the pre-release mode insideuv.lock, so a lockfile built withUV_PRERELEASE=allowexported failed for anyone who did not also export it.main.memory://with zero TCP connections to a Redis port.What we have learned so far
Observations, each with how it was measured. Some of these are firm, some are snapshots of a moving alpha.
Redis is not required. This is the one I had wrong initially and want to correct loudly:
burner-redisis an embedded Rust engine, not a client for an external server. A task onmemory://completed in 321 ms inside an isolated network namespace (unshare -rn, loopback only, down) with TCPActiveOpens0→0, whileredis://127.0.0.1:6379/0failed in the same namespace. So the "tasks mean running Redis" objection is simply false.The 3.4.4 poll problem is fixed. The old client hardcoded a 500 ms poll and ignored the server's advertised interval. 4.x ramps from 20 ms, doubling to the server's value as a ceiling — shown by moving the setting and watching the plateau move with it.
call_tool_taskgenuinely frees the connection. Handle back in ~2 ms, and interleaved calls answered at 2.38 ms while a 4 s task ran.But plain
call_tooldoes not. The client transparently polls the task to completion inside the call: a 4.000 s body took 5.1276 s wall, with 1tools/callplus 9tasks/get. So which API the caller reaches for decides whether tasks buy anything at all.Numeric progress does not survive task execution.
make_task_context()builds aContext(session=None), soreport_progressfinds no progress token;GetTaskResulthas no numeric field, andTaskStatusNotificationis constructed zero times in the install. On the realwait_for_text: 0 progress callbacks under a task versus 36 in the foreground, same server and client and handler.wait_for_textreports progress on every tick today, so this is the sharpest open question for our use.The premise I started from was wrong. I assumed a long wait holds the MCP connection and tasks would free it. It does not: the wait tools await throughout and FastMCP already serves the connection concurrently — a tool awaiting 6 s served 58 interleaved calls at a 3.4 ms median, against exactly one at 6014 ms for a control that blocked the event loop. Worth stating plainly because it is the reason the whole idea looked more valuable than it is.
Open questions / edge cases being looked into
2026-07-28and declaredio.modelcontextprotocol/tasks. Of the CLIs installed here, only agy reaches that protocol era, and its capabilities carryelicitationandrootsbut not tasks. Replaying agy's handshake bytes with only"extensions":{"io.modelcontextprotocol/tasks":{}}added flips arequiredtool from-32003to a real task id — so the gate is one capability wide. Whether and when hosts ship it is unknown.mode="optional"becomes a task the moment a client declares the capability, with no change on our side. If the progress behaviour above still holds then, it would change our tool's behaviour without a deploy. Needs a decision about which mode is safe to ship, if any.wait_for_textsnapshots its entry cursor row inside the tool body, i.e. at worker pickup rather than at request time. Under a busy Docket worker pool that delay sits in front of the snapshot. Not yet measured on 4.x — this is the risk I would want closed first, because it is the same failure class Fix wait_for_text's blind spot and bound every wait #100 just fixed.2026-07-28also deprecates the logging capability (SEP-2577, observed live), andwait.pylogs at three sites. Both of the tool's agent-facing channels would be affected at once.MODERN_PROTOCOL_VERSIONSis currently('2026-07-28',)against a published current of2025-11-25. Findings here can change under us.Cost, if it ever were adopted
mcp2.0.0b2 deletesmcp.types, plus ~10.85 MiB of venv, ~25% slower cold start, and a 20-thread Rust Tokio runtime inside a stdio server whose job is watching a tmux pane.Re-measuring
The branch is checkout-and-run:
Companion prototype branches, kept as working notes rather than proposals:
tasks-minimal(minimal SEP-1686 proof),tasks-clients(the wire-capture harness behind the reachability observations),tasks-wait(the realwait_for_textmade task-capable — rebased onto currentmain, and the place to re-run the progress and queueing questions).