From 98c22a6d76ed15a20e900dce9a9288b1ffc1413a Mon Sep 17 00:00:00 2001 From: Naseem Alnaji Date: Tue, 3 Nov 2026 19:08:50 +0000 Subject: [PATCH 1/2] ci: run compat tests daily at 2 AM UTC and on SDK-code PRs --- .github/workflows/mcp-compatibility.yml | 49 ++++++++++++++++++- .../mcp-prerelease-compatibility.yml | 11 +++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mcp-compatibility.yml b/.github/workflows/mcp-compatibility.yml index 33aafd9..35f95a2 100644 --- a/.github/workflows/mcp-compatibility.yml +++ b/.github/workflows/mcp-compatibility.yml @@ -6,8 +6,8 @@ on: pull_request: branches: [ main ] schedule: - # Run daily at midnight UTC to catch new MCP versions - - cron: '0 0 * * *' + # Run daily at 2 AM UTC to catch new MCP versions + - cron: '0 2 * * *' workflow_dispatch: # The two generations agentcat 2.x supports, as PEP 440 ranges. `mcp` spans @@ -36,8 +36,49 @@ env: FASTMCP_RANGE: ">=3.0,<5" jobs: + # Decides whether a pull request touches SDK code. The compat jobs below are + # gated on its output instead of filtering the whole workflow with + # `on.pull_request.paths`, because `report-compatibility` is a required + # status check: a workflow that never starts leaves it pending and blocks + # the merge, while skipped jobs let it run and pass. On push, schedule and + # workflow_dispatch the output is always 'true'. + changes: + runs-on: ubuntu-latest + outputs: + sdk: ${{ steps.diff.outputs.sdk }} + + steps: + - uses: actions/checkout@v4 + if: github.event_name == 'pull_request' + with: + fetch-depth: 0 + + - name: Detect SDK-code changes + id: diff + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + if [ "$EVENT_NAME" != "pull_request" ]; then + echo "sdk=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + # SDK code: library source, package manifest and lockfile, the tests + # the compat jobs run, the scripts they call, and this workflow. + changed=$(git diff --name-only "$BASE_SHA...$HEAD_SHA") + echo "Changed files:" + echo "$changed" + if echo "$changed" | grep -qE '^(src/|tests/|pyproject\.toml$|uv\.lock$|\.github/scripts/|\.github/workflows/mcp-compatibility\.yml$)'; then + echo "sdk=true" >> "$GITHUB_OUTPUT" + else + echo "sdk=false" >> "$GITHUB_OUTPUT" + fi + discover-mcp-versions: runs-on: ubuntu-latest + needs: changes + if: github.event_name != 'pull_request' || needs.changes.outputs.sdk == 'true' outputs: mcp-versions: ${{ steps.get-mcp-versions.outputs.versions }} @@ -57,6 +98,8 @@ jobs: discover-fastmcp-versions: runs-on: ubuntu-latest + needs: changes + if: github.event_name != 'pull_request' || needs.changes.outputs.sdk == 'true' outputs: fastmcp-versions: ${{ steps.get-fastmcp-versions.outputs.versions }} @@ -85,6 +128,8 @@ jobs: # per-leg test path to keep in sync here. test-dependency-groups: runs-on: ubuntu-latest + needs: changes + if: github.event_name != 'pull_request' || needs.changes.outputs.sdk == 'true' strategy: fail-fast: false matrix: diff --git a/.github/workflows/mcp-prerelease-compatibility.yml b/.github/workflows/mcp-prerelease-compatibility.yml index d55bb60..533d0b5 100644 --- a/.github/workflows/mcp-prerelease-compatibility.yml +++ b/.github/workflows/mcp-prerelease-compatibility.yml @@ -1,6 +1,17 @@ name: MCP Prerelease Compatibility Testing on: + # Only on PRs that change SDK code. Path filtering is safe here: this + # workflow holds only compat jobs and none of its checks is required. + pull_request: + branches: [ main ] + paths: + - 'src/**' + - 'tests/**' + - 'pyproject.toml' + - 'uv.lock' + - '.github/scripts/**' + - '.github/workflows/mcp-prerelease-compatibility.yml' schedule: # Run daily at 2 AM UTC to catch new prereleases - cron: '0 2 * * *' From 5ef992951c632c3fdb4c16ba1aa0b0150e648580 Mon Sep 17 00:00:00 2001 From: Naseem Alnaji Date: Tue, 3 Nov 2026 20:18:33 +0000 Subject: [PATCH 2/2] fix(compat): keep mcp<1.10 legs on anyio<4.16 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. --- .github/workflows/mcp-compatibility.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/mcp-compatibility.yml b/.github/workflows/mcp-compatibility.yml index 35f95a2..2d83f91 100644 --- a/.github/workflows/mcp-compatibility.yml +++ b/.github/workflows/mcp-compatibility.yml @@ -324,6 +324,16 @@ jobs: uv pip install "pydantic<2.12" fi + # anyio 4.16 wakes a receive() blocked on a closed memory stream with + # ClosedResourceError. MCP below 1.10 does not catch that in + # BaseSession._receive_loop, so the first client disconnect kills the + # Streamable-HTTP session manager's task group and every later request + # gets a 500 ("Task group is not initialized"). Upstream bug, AgentCat + # or not; 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 + # Belt and braces: nothing above should pull fastmcp in. uv pip uninstall fastmcp -y 2>/dev/null || true @@ -331,6 +341,7 @@ jobs: run: | echo "Installed MCP version:" uv pip show mcp | grep "Version:" || echo "MCP not found" + uv pip show anyio | grep "Version:" || echo "anyio not found" - name: Verify FastMCP is not installed run: |