From 8d8a2c3742c735432e831e0c32c09870b4dd8512 Mon Sep 17 00:00:00 2001 From: joshua Date: Fri, 18 Sep 2026 23:49:19 +0000 Subject: [PATCH] ci(mcp): add dependency-resolution workflow for the SDK 2 floor New matrix job across Python 3.10-3.14 verifies uv.lock against the declared floors, installs the locked mcp+proxy extras and runs the MCP unit suites, then resolves the same extras with uv's lowest-direct strategy into a clean venv and runs scripts/check_mcp_sdk_install.py to prove the floor still imports the SDK 2 API surface. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../test-mcp-dependency-resolution.yml | 100 ++++++++++++++++++ scripts/check_mcp_sdk_install.py | 72 +++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 .github/workflows/test-mcp-dependency-resolution.yml create mode 100644 scripts/check_mcp_sdk_install.py diff --git a/.github/workflows/test-mcp-dependency-resolution.yml b/.github/workflows/test-mcp-dependency-resolution.yml new file mode 100644 index 00000000000..ce6cb2c5b5d --- /dev/null +++ b/.github/workflows/test-mcp-dependency-resolution.yml @@ -0,0 +1,100 @@ +name: LiteLLM MCP Dependency Resolution + +on: + pull_request: + branches: + - main + - litellm_internal_staging + - litellm_oss_staging + - "litellm_**" + +permissions: + contents: read + pull-requests: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + resolve: + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Detect relevant changes + id: changes + uses: ./.github/actions/detect-changes + + - name: Set up Python + if: steps.changes.outputs.decision != 'skip' + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Set up uv + if: steps.changes.outputs.decision != 'skip' + uses: ./.github/actions/setup-uv-with-retries + with: + version: "0.10.9" + + - name: Cache the Rust build + if: steps.changes.outputs.decision != 'skip' + uses: ./.github/actions/cache-cargo-build + + - name: Verify lockfile + if: steps.changes.outputs.decision != 'skip' + run: | + uv lock --check + + - name: Install locked dependencies + if: steps.changes.outputs.decision != 'skip' + run: | + .github/scripts/uv_sync_with_retries.sh --frozen --python ${{ matrix.python-version }} --group proxy-dev --extra mcp --extra proxy --extra semantic-router + + - name: Check locked MCP SDK installation + if: steps.changes.outputs.decision != 'skip' + run: | + uv run --no-sync python scripts/check_mcp_sdk_install.py + + - name: Cache Prisma binaries + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 3 + uses: ./.github/actions/cache-prisma-binaries + + - name: Generate Prisma client + if: steps.changes.outputs.decision != 'skip' + timeout-minutes: 3 + run: | + uv run --no-sync prisma generate --schema litellm/proxy/schema.prisma + + - name: Run MCP unit tests + if: steps.changes.outputs.decision != 'skip' + env: + LITELLM_LOCAL_MODEL_COST_MAP: "True" + run: | + uv run --no-sync pytest -q -p no:cacheprovider -n 4 tests/test_litellm/proxy/_experimental/mcp_server tests/test_litellm/experimental_mcp_client + + - name: Resolve lowest direct dependencies + if: steps.changes.outputs.decision != 'skip' + run: | + uv pip compile pyproject.toml --python-version ${{ matrix.python-version }} --extra mcp --extra proxy --resolution lowest-direct -o lowest-direct.txt + + - name: Install lowest direct dependencies + if: steps.changes.outputs.decision != 'skip' + run: | + uv venv --python ${{ matrix.python-version }} .venv-lowest + uv pip install --python .venv-lowest -r lowest-direct.txt -e . + + - name: Check lowest-direct MCP SDK installation + if: steps.changes.outputs.decision != 'skip' + run: | + .venv-lowest/bin/python scripts/check_mcp_sdk_install.py diff --git a/scripts/check_mcp_sdk_install.py b/scripts/check_mcp_sdk_install.py new file mode 100644 index 00000000000..9b5106118e7 --- /dev/null +++ b/scripts/check_mcp_sdk_install.py @@ -0,0 +1,72 @@ +import importlib +import importlib.metadata +import sys +from typing import Final + +MINIMUM_MCP_VERSION: Final[tuple[int, int, int]] = (2, 2, 0) + +IMPORTED_MODULES: Final[tuple[str, ...]] = ( + "litellm", + "litellm.experimental_mcp_client", + "litellm.experimental_mcp_client.client", + "litellm.proxy._experimental.mcp_server.server", + "litellm.proxy._experimental.mcp_server.mcp_server_manager", + "litellm.proxy._experimental.mcp_server.rest_endpoints", +) + + +def _version_tuple(distribution: str) -> tuple[int, ...]: + return tuple(int(part) for part in importlib.metadata.version(distribution).split(".") if part.isdigit()) + + +def main() -> int: + for module_name in IMPORTED_MODULES: + try: + importlib.import_module(module_name) + except Exception as exc: + sys.stderr.write(f"failed to import {module_name}: {exc}\n") + return 1 + + mcp_version: Final = _version_tuple("mcp") + if mcp_version < MINIMUM_MCP_VERSION: + sys.stderr.write(f"mcp {importlib.metadata.version('mcp')} below floor 2.2.0\n") + return 1 + + from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS + + for required in ("2024-11-05", "2025-06-18"): + if required not in HANDSHAKE_PROTOCOL_VERSIONS: + sys.stderr.write(f"HANDSHAKE_PROTOCOL_VERSIONS missing {required}\n") + return 1 + + scope: Final = { + "type": "http", + "method": "POST", + "path": "/mcp", + "headers": [(b"mcp-protocol-version", b"2026-07-28")], + } + mcp_server: Final = sys.modules["litellm.proxy._experimental.mcp_server.server"] + if mcp_server.unsupported_protocol_version(scope) != "2026-07-28": + sys.stderr.write("unsupported_protocol_version accepted a modern-only version\n") + return 1 + if ( + mcp_server.unsupported_protocol_version(dict(scope, headers=[(b"mcp-protocol-version", b"2025-06-18")])) + is not None + ): + sys.stderr.write("unsupported_protocol_version rejected a handshake version\n") + return 1 + + sys.stdout.write( + "python {} mcp {} httpx2 {} pydantic {} litellm {}\n".format( + sys.version.split()[0], + importlib.metadata.version("mcp"), + importlib.metadata.version("httpx2"), + importlib.metadata.version("pydantic"), + importlib.metadata.version("litellm"), + ) + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())