Skip to content

ci: run compat tests daily at 2 AM UTC and on SDK-code PRs; pin anyio<4.16 for mcp<1.10 legs - #57

Merged
naji247 merged 2 commits into
mainfrom
ci/compat-triggers
Nov 3, 2026
Merged

naji247 merged 2 commits into
mainfrom
ci/compat-triggers

Conversation

@naji247

@naji247 naji247 commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Summary

mcp-compatibility.yml (job gating):

   schedule:
-    - cron: '0 0 * * *'
+    - cron: '0 2 * * *'
 jobs:
+  changes:        # git diff --name-only base...head -> outputs.sdk ('true' on non-PR events)
   discover-mcp-versions:
+    needs: changes
+    if: github.event_name != 'pull_request' || needs.changes.outputs.sdk == 'true'
   discover-fastmcp-versions:      # same needs/if
   test-dependency-groups:         # same needs/if

test-fastmcp-compatibility and test-without-fastmcp skip with their discover job. send-failure-notification (if: failure()) skips. report-compatibility (if: always()) still runs and passes.

mcp-prerelease-compatibility.yml (path filter):

 on:
+  pull_request:
+    branches: [ main ]
+    paths: [src/**, tests/**, pyproject.toml, uv.lock, .github/scripts/**, .github/workflows/mcp-prerelease-compatibility.yml]
   schedule:
     - cron: '0 2 * * *'   # unchanged

SDK-code paths: src/**, tests/**, pyproject.toml, uv.lock, .github/scripts/**, plus the workflow's own file.

Evidence

  • Before: compat: push main, every PR to main, cron 0 0 * * *, dispatch. prerelease: cron 0 2 * * *, dispatch.
    After: compat: push main, PR to main (compat jobs gated on SDK paths), cron 0 2 * * *, dispatch. prerelease: PR to main on SDK paths, cron 0 2 * * *, dispatch. yaml.safe_load passes for both files. actionlint was not available on the devbox, so it did not run.

Merge Danger

Door: two-way

Blast Radius: CI

report-compatibility is the only required check. It lives in mcp-compatibility.yml, so that workflow uses job gating and the check still runs on every PR. A docs-only PR now skips all compat matrices and the prerelease workflow.

Fix: test-without-fastmcp (1.9.4) red on main since 2026-11-02

anyio 4.16.0 (published 2026-11-01 11:20 UTC) now raises ClosedResourceError in a receive() that waits on a memory stream when the stream closes. mcp below 1.10 does not catch that in BaseSession._receive_loop. The first client disconnect kills the Streamable-HTTP session manager's task group, and every later request gets a 500. mcp 1.10.0 added the except anyio.ClosedResourceError guard. Only the 1.9.4 leg breaks, because tests/conftest.py runs e2e/official only on mcp 1.9.2 and later. The bug is upstream: a plain mcp 1.9.4 server without agentcat.track() shuts down the same way.

mcp-compatibility.yml, test-without-fastmcp install step:

         if [[ "${{ matrix.mcp-version }}" == 1.* ]]; then
           uv pip install "pydantic<2.12"
         fi
+
+        # anyio 4.16 ... mcp 1.10.0 added the guard. Keep those legs on anyio 4.15.
+        if [[ "${{ matrix.mcp-version }}" == 1.[0-9].* ]]; then
+          uv pip install "anyio<4.16"
+        fi
@@ Show installed MCP version
         uv pip show mcp | grep "Version:" || echo "MCP not found"
+        uv pip show anyio | grep "Version:" || echo "anyio not found"

Evidence

  • Before: RuntimeError: Task group is not initialized. Make sure to use run(). from mcp/server/streamable_http_manager.py:137; 24 e2e HTTP tests fail (PR run, main #582). Main #579 and #580 ran the same commit (8bb1a65); the only package changes between them were anyio 4.15.1 → 4.16.0, pytest-asyncio 1.4.1 → 1.4.1.post1, python-dateutil 3.0.0 → 4.0.0. Locally, with CI's exact package set, only swapping anyio flips tests/e2e/official (24 failed ↔ 32 passed).
  • After: locally, mcp 1.9.4 + anyio 4.15.1: 868 passed, 30 skipped. PR CI: see the checks on this PR.

Merge Danger

Door: two-way

Blast Radius: CI

The pin only applies to the mcp 1.0–1.9 legs of test-without-fastmcp; mcp 1.10+ and 2.x legs still test the newest anyio. The pin hides a real upstream problem: a Customer on mcp 1.9.x who installs fresh today gets anyio 4.16, and their Streamable-HTTP server stops serving after the first client disconnects. AgentCat does not cause or fix that. The package metadata and README stay unchanged; whether to tell Customers "use mcp>=1.10 or anyio<4.16 with Streamable HTTP" is a separate owner decision.

anyio 4.16.0 (2026-11-01) raises ClosedResourceError in a receive()
blocked on a memory stream that gets closed. mcp below 1.10 does not
catch it in BaseSession._receive_loop, so the first client disconnect
tears down the Streamable-HTTP session manager's task group and every
later request returns 500 "Task group is not initialized. Make sure to
use run()." mcp 1.10.0 added the guard.

Only the mcp 1.9.4 leg broke because e2e/official is gated on
mcp>=1.9.2. Pin anyio<4.16 for 1.0-1.9 legs, as with pydantic<2.12,
and print the installed anyio version.
@naji247 naji247 changed the title ci: run compat tests daily at 2 AM UTC and on SDK-code PRs ci: run compat tests daily at 2 AM UTC and on SDK-code PRs; pin anyio<4.16 for mcp<1.10 legs Nov 3, 2026
@naji247
naji247 merged commit c06ceef into main Nov 3, 2026
55 checks passed
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