Skip to content

prototype: fastmcp 4.x tasks extension (SEP-2663) against wait_for_text - #108

Draft
tony wants to merge 3 commits into
mainfrom
tasks
Draft

prototype: fastmcp 4.x tasks extension (SEP-2663) against wait_for_text#108
tony wants to merge 3 commits into
mainfrom
tasks

Conversation

@tony

@tony tony commented Jul 25, 2026

Copy link
Copy Markdown
Member

What this is

A prototype. It wires libtmux-mcp up to the fastmcp-tasks 4.x alpha so we can find out what the MCP tasks extension (SEP-2663) actually does to a tool like wait_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

Area Change
pyproject.toml Pins fastmcp==4.0.0a2 and fastmcp-tasks==4.0.0a2; adds [tool.uv] prerelease = "allow" and exclude-newer-package exemptions for the alpha chain
src/libtmux_mcp/middleware.py from mcp.types import ...from mcp_types import ...
tests/test_middleware.py The same import, plus mcp_types.Implementation
uv.lock Resolved against the alpha: mcp 1.28.1 → 2.0.0b2, adds mcp-types 2.0.0b2 and pydocket, swaps httpx/httpcore for httpx2/httpcore2

The port itself is one line in src/. That was the surprise — mcp 2.0.0b2 deletes the mcp.types module, 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 --check clean — the third commit exists because uv records the pre-release mode inside uv.lock, so a lockfile built with UV_PRERELEASE=allow exported failed for anyone who did not also export it.
  • Server starts and serves all 51 tools over real stdio.
  • ruff, ruff format, mypy (65 files), 791 tests — parity with main.
  • Tasks execute on 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-redis is an embedded Rust engine, not a client for an external server. A task on memory:// completed in 321 ms inside an isolated network namespace (unshare -rn, loopback only, down) with TCP ActiveOpens 0→0, while redis://127.0.0.1:6379/0 failed 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_task genuinely 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_tool does not. The client transparently polls the task to completion inside the call: a 4.000 s body took 5.1276 s wall, with 1 tools/call plus 9 tasks/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 a Context(session=None), so report_progress finds no progress token; GetTaskResult has no numeric field, and TaskStatusNotification is constructed zero times in the install. On the real wait_for_text: 0 progress callbacks under a task versus 36 in the foreground, same server and client and handler. wait_for_text reports 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

  • Reachability. The extension only engages for a client that negotiated protocol 2026-07-28 and declared io.modelcontextprotocol/tasks. Of the CLIs installed here, only agy reaches that protocol era, and its capabilities carry elicitation and roots but not tasks. Replaying agy's handshake bytes with only "extensions":{"io.modelcontextprotocol/tasks":{}} added flips a required tool from -32003 to a real task id — so the gate is one capability wide. Whether and when hosts ship it is unknown.
  • Auto-promotion. 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.
  • Queueing and the entry baseline. wait_for_text snapshots 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.
  • Logging deprecation. Protocol 2026-07-28 also deprecates the logging capability (SEP-2577, observed live), and wait.py logs at three sites. Both of the tool's agent-facing channels would be affected at once.
  • Alpha churn. Every version in the chain is alpha or beta, and MODERN_PROTOCOL_VERSIONS is currently ('2026-07-28',) against a published current of 2025-11-25. Findings here can change under us.

Cost, if it ever were adopted

mcp 2.0.0b2 deletes mcp.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:

uv lock --check
uv run --frozen pytest tests/ -q --reruns 0

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 real wait_for_text made task-capable — rebased onto current main, and the place to re-run the progress and queueing questions).

tony added 3 commits July 25, 2026 13:38
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-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.13%. Comparing base (ab82ad9) to head (57b9161).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@tony tony changed the title spike: fastmcp 4.x tasks extension (SEP-2663) — evaluated, rejected, kept runnable prototype: fastmcp 4.x tasks extension (SEP-2663) against wait_for_text Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants