From d060e93eec11e8615c4a83a9f192ae9369aa63f3 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Wed, 8 Jul 2026 09:06:22 -0400 Subject: [PATCH] Narrow examples-gate exclusion to uv.lock, fix qr_server stale import A bare pyproject.toml in an examples/ subdir isn't evidence of an incompatible dependency graph -- most just declare extras (qrcode, phue2, atproto). Only a directory with its own uv.lock is genuinely independently resolved. Narrowing the exclusion brought 20 more files into scope and caught exactly the bug class the gate exists for: qr_server.py's `from mcp import types` / `mimeType=` are stale v1 idioms that would raise on the current SDK. --- examples/apps/qr_server/qr_server.py | 4 ++-- tests/test_examples_importable.py | 27 +++++++++++++++++---------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/examples/apps/qr_server/qr_server.py b/examples/apps/qr_server/qr_server.py index 04639fdb9..28ea8d4d1 100644 --- a/examples/apps/qr_server/qr_server.py +++ b/examples/apps/qr_server/qr_server.py @@ -23,7 +23,7 @@ import base64 import io import qrcode # type: ignore[import-untyped] -from mcp import types +from mcp_types import ImageContent from fastmcp import FastMCP from fastmcp.apps import AppConfig, ResourceCSP @@ -153,7 +153,7 @@ def generate_qr( img.save(buffer, format="PNG") b64 = base64.b64encode(buffer.getvalue()).decode() return ToolResult( - content=[types.ImageContent(type="image", data=b64, mimeType="image/png")] + content=[ImageContent(type="image", data=b64, mime_type="image/png")] ) diff --git a/tests/test_examples_importable.py b/tests/test_examples_importable.py index 02148ffec..14f3e915f 100644 --- a/tests/test_examples_importable.py +++ b/tests/test_examples_importable.py @@ -15,10 +15,13 @@ Execution is deliberately avoided: examples spin up servers, hit external services, and pull heavy optional dependencies. Static resolution catches the class of breakage we actually keep reintroducing (renamed imports) cheaply. -Standalone example sub-projects that pin their own ``fastmcp``/``mcp`` in a -local ``pyproject.toml`` (e.g. ``examples/testing_demo`` targets v1 on purpose) -are excluded — their imports are validated against a different package than the -one installed here. +Standalone example sub-projects with their own ``uv.lock`` (e.g. +``examples/testing_demo`` targets v1 on purpose) are excluded — their +dependency graph is independently resolved, so their imports are validated +against a different package than the one installed here. A bare +``pyproject.toml`` without its own lock (e.g. ``examples/apps/qr_server``) +still resolves against this tree's install and is checked like any other +example — it is not, on its own, evidence of an incompatible dependency. Run: uv run pytest tests/test_examples_importable.py -v -s @@ -41,13 +44,17 @@ _CHECKED_ROOTS = ("fastmcp", "mcp", "mcp_types") def _standalone_dirs() -> set[Path]: """Directories that are self-contained example sub-projects. - A ``pyproject.toml`` under ``examples/`` marks a project boundary: the - directory ships its own dependency pins (``examples/testing_demo`` targets - fastmcp v1 on purpose, ``examples/smart_home`` pins fastmcp from git), so - its imports must not be validated against the package installed for the - main test suite. Everything below such a directory is excluded. + A ``uv.lock`` under ``examples/`` marks a genuinely independent dependency + graph (``examples/testing_demo`` locks and targets fastmcp v1 on purpose), + so its imports must not be validated against the package installed for + the main test suite. A ``pyproject.toml`` alone is not sufficient — most + example sub-projects (``examples/apps/qr_server``, ``examples/smart_home``, + ``examples/atproto_mcp``) have one purely to declare extra dependencies + (qrcode, phue2, atproto) but share this tree's fastmcp/mcp install, so + their fastmcp/mcp imports are still checked. Everything below a directory + with its own lock is excluded. """ - return {pyproject.parent for pyproject in EXAMPLES_DIR.rglob("pyproject.toml")} + return {lockfile.parent for lockfile in EXAMPLES_DIR.rglob("uv.lock")} def _find_example_files() -> list[Path]: