fix: stabilize cli and mcp dependency compatibility

This commit is contained in:
lda
2026-06-29 16:33:41 +07:00 Verified
parent 746e4f7a2a
commit 99f733c5ea
8 changed files with 208 additions and 7 deletions
+2
View File
@@ -13,6 +13,8 @@ For a presentation-oriented summary of the current product path and demo flow,
see [`workflow platform presentation`](add/2026-06-workflow-platform-presentation.md).
For running and auditing external-agent workflow challenges, see
[`agent challenge evaluation`](runbooks/agent-challenge-evaluation.md).
For verified Python 3.14 dependency constraints and their removal criteria, see
[`dependency compatibility`](runbooks/dependency-compatibility.md).
## Packages
+94
View File
@@ -0,0 +1,94 @@
# Dependency Compatibility
This project targets Python 3.14. The constraints below document verified
dependency regressions rather than general version preferences.
## AnyIO 4.14.x And FastMCP Teardown
The project currently pins `anyio<4.14`. With Python 3.14 on Windows,
FastMCP 3.4.2 client teardown fails under AnyIO 4.14.0 and 4.14.1 with:
```text
RuntimeError: Attempted to exit cancel scope in a different task than it was entered in
```
The same focused tests pass with AnyIO 4.12.0 and 4.13.0. The full project
suite passes with AnyIO 4.13.0.
Reproduce the failing version with:
```powershell
uv run --with anyio==4.14.1 pytest tests/wf_mcp/server/test_tools.py::test_server_search_mode_pins_stable_control_and_workflow_tools -q -n0
```
AnyIO issue [#1179](https://github.com/agronholm/anyio/issues/1179) describes a
related 4.14.0 pytest-runner regression and was closed by
[#1180](https://github.com/agronholm/anyio/pull/1180). The FastMCP 3.4.2
teardown case above still reproduces with AnyIO 4.14.1, so that AnyIO release
alone is not sufficient for this project.
FastMCP PR [#4363](https://github.com/PrefectHQ/fastmcp/pull/4363) shields
stateful proxy disconnect during session teardown. The focused test fails at
the parent commit `5fa4f32c` and passes at the merged fix commit `ddcdf648` with
AnyIO 4.14.1. Current FastMCP `main` at `de521e65` also passes.
Remove the pin after a FastMCP release includes PR #4363, the focused test
passes with AnyIO 4.14.1 or newer, and the full suite remains clean.
### Verified Matrix
| FastMCP | AnyIO | Result |
| --- | --- | --- |
| 3.4.2 | 4.13.0 | Pass |
| 3.4.2 | 4.14.0 | Fail |
| 3.4.2 | 4.14.1 | Fail |
| `5fa4f32c` before PR #4363 | 4.14.1 | Fail |
| `ddcdf648` from PR #4363 | 4.14.1 | Pass |
| `main` at `de521e65` | 4.14.1 | Pass |
## Typer Command Callback Errors
With Typer 0.26.8 and Click 8.4.2, a nested Typer command that raises
`click.ClickException` exits with code 1 under `typer.testing.CliRunner`, but
both captured output streams are empty. The unformatted `ClickException`
remains on `result.exception`. Single-command applications behave the same way,
and direct invocation renders a traceback instead of a concise error.
The CLI therefore writes its concise remote-operation error explicitly to
stderr and raises `typer.Exit(1)` instead of relying on standalone Click
exception formatting. This keeps real CLI and test-runner behavior aligned.
Minimal reproducer:
```python
import click
import typer
from typer.testing import CliRunner
app = typer.Typer()
@app.command()
def fail() -> None:
raise click.ClickException("broken")
result = CliRunner().invoke(app)
assert result.exit_code == 1
assert result.stderr == "" # Expected to contain "Error: broken".
assert isinstance(result.exception, click.ClickException)
```
This regression is tracked as Typer issue
[#1867](https://github.com/fastapi/typer/issues/1867). The repository copy of
the verified issue report is
[`2026-06-29-typer-click-exception-regression.md`](../superpowers/research/2026-06-29-typer-click-exception-regression.md).
Typer 0.24.2 and 0.25.0 format the exception normally. Vendoring Click in Typer
0.26.0 introduced the exception-class identity mismatch; Typer 0.26.8 and
current `master` at `b210c0e2` reproduce the failure.
## Separate Warning Backlog
The full suite currently emits Python 3.14 deprecation warnings from
`fastapi-jsonrpc` use of `asyncio.iscoroutinefunction`. Those warnings are
separate from both regressions above and are not addressed by these workarounds.
@@ -0,0 +1,97 @@
### Description
After Typer vendored Click in 0.26.0, exceptions imported from the external
`click` package are no longer handled as normal Typer CLI errors.
A real invocation renders a Rich traceback ending in
`ClickException: broken`. Under `typer.testing.CliRunner`, both stdout and
stderr are empty and the raw `click.ClickException` remains in
`result.exception`.
Typer 0.25.0 renders the expected concise error panel and returns `SystemExit`.
### Root Cause
Typer commit `1829d73` vendored Click as `typer._click`. Typer's `_main()`
catches `typer._click.exceptions.ClickException`, but an application that
raises `click.exceptions.ClickException` raises a different class:
```python
import click
import typer._click
assert click.ClickException is not typer._click.ClickException
```
The exception therefore bypasses Typer's Click-exception handler. This affects
the external Click variants of `ClickException`, `UsageError`, `BadParameter`,
and other subclasses.
Typer publicly re-exports some vendored exception types, including
`typer.BadParameter` and `typer.Exit`, but it does not currently expose a public
`typer.ClickException` or `typer.UsageError` equivalent.
### Minimal Reproduction
```python
import click
import typer
from typer.testing import CliRunner
app = typer.Typer()
@app.command()
def fail() -> None:
raise click.ClickException("broken")
result = CliRunner().invoke(app)
print("exit:", result.exit_code)
print("stdout:", repr(result.stdout))
print("stderr:", repr(result.stderr))
print("exception:", type(result.exception).__name__)
```
The same behavior occurs in nested Typer command groups.
### Version Matrix
Tested with Click 8.4.2 unless noted otherwise:
| Typer | Result |
| --- | --- |
| 0.24.2 | Concise error output; `SystemExit` |
| 0.25.0 | Concise error output; `SystemExit` |
| 0.26.0 | Empty runner output; raw `ClickException` |
| 0.26.8 with Click 8.1.7 | Empty runner output; raw `ClickException` |
| 0.26.8 with Click 8.4.2 | Empty runner output; raw `ClickException` |
| `master` at `b210c0e2` | Empty runner output; raw `ClickException` |
Test environment:
- Python 3.14.3
- Windows 11
- Click 8.1.7 and 8.4.2
### Expected Behavior
Typer should provide a public way to raise its concise general-purpose CLI
exception after vendoring Click. Possible resolutions include:
- re-exporting the vendored `ClickException` and `UsageError` classes from the
`typer` namespace;
- preserving compatibility with exceptions imported from external Click; or
- documenting that external Click exceptions are no longer compatible and
identifying their supported Typer replacements.
### Workaround
Applications can write the message explicitly and raise `typer.Exit(1)`:
```python
typer.echo("Error: something went wrong", err=True)
raise typer.Exit(code=1)
```
Importing from `typer._click` also works, but relies on a private API.