From e16ffad42087da5bd16c2afe8927a439a8ece3ed Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:58:51 -0400 Subject: [PATCH] Promote mcp-types to a core fastmcp-slim dependency Bare `import fastmcp` loads mcp_types via _sdk_patches and _compat, so a bare slim install (no [mcp] extra) hit ModuleNotFoundError. mcp-types only pulls pydantic + typing-extensions (already core), so graduating it to core fixes the import while the full mcp SDK stays in the [mcp] extra. --- fastmcp_slim/pyproject.toml | 2 +- tests/client/test_slim_package_boundaries.py | 55 ++++++++++++++++++++ uv.lock | 10 ++-- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/fastmcp_slim/pyproject.toml b/fastmcp_slim/pyproject.toml index 1d95fc219..7555ae5f1 100644 --- a/fastmcp_slim/pyproject.toml +++ b/fastmcp_slim/pyproject.toml @@ -4,6 +4,7 @@ dynamic = ["version", "optional-dependencies"] description = "The dependency-slim FastMCP package." authors = [{ name = "Jeremiah Lowin" }] dependencies = [ + "mcp-types==2.0.0b1", "platformdirs>=4.0.0", "pydantic[email]>=2.12.0", "pydantic-settings>=2.0.0", @@ -79,7 +80,6 @@ mcp = [ "exceptiongroup>=1.2.2", "httpx>=0.28.1,<1.0", "mcp==2.0.0b1", - "mcp-types==2.0.0b1", "opentelemetry-api>=1.28.0", # starlette floor: transitive via mcp (which only requires >=0.27). # Pin past CVE-2026-48710, which was patched in 1.0.1. diff --git a/tests/client/test_slim_package_boundaries.py b/tests/client/test_slim_package_boundaries.py index 5c6a4b073..5f59b6b3c 100644 --- a/tests/client/test_slim_package_boundaries.py +++ b/tests/client/test_slim_package_boundaries.py @@ -2,6 +2,9 @@ from __future__ import annotations import builtins import contextlib +import subprocess +import sys +import textwrap import types from collections.abc import Mapping, Sequence from typing import Any, cast @@ -59,3 +62,55 @@ async def test_multiserver_config_requires_server_for_now() -> None: ): async with transport.connect_session(): pass + + +def test_bare_slim_import_needs_only_mcp_types() -> None: + """A bare `fastmcp-slim` install ships `mcp-types` but not the full `mcp` SDK. + + `mcp-types` is a core dependency (it only pulls pydantic + typing-extensions), + while the full `mcp` package lives in the `[mcp]` extra pulled by + `[client]`/`[server]`. With `mcp` absent but `mcp-types` present, `import + fastmcp`, `import fastmcp.settings`, and `import fastmcp.types` must all + succeed, while `fastmcp.FastMCP` and `fastmcp.Client` raise the friendly + install-hint ImportError (they need `mcp.*` from the server/client extras). + """ + script = textwrap.dedent( + """ + import sys + import importlib.abc + + class BlockFullMcp(importlib.abc.MetaPathFinder): + # Block the full `mcp` package but leave `mcp_types` importable, + # exactly as a bare `fastmcp-slim` install would present. + def find_spec(self, name, path, target=None): + if name == "mcp" or name.startswith("mcp."): + raise ImportError(f"blocked: {name}") + return None + + sys.meta_path.insert(0, BlockFullMcp()) + + import fastmcp + import fastmcp.settings + import fastmcp.types + + assert "mcp_types" in sys.modules, "mcp_types should load on a bare install" + assert "mcp" not in sys.modules, "full mcp must not load on a bare install" + + for attr in ("FastMCP", "Client"): + try: + getattr(fastmcp, attr) + except ImportError: + pass + else: + raise AssertionError(f"fastmcp.{attr} should have raised ImportError") + + print("OK") + """ + ) + result = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip().endswith("OK") diff --git a/uv.lock b/uv.lock index f10298106..d78c55152 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-06-28T22:01:54.610739Z" +exclude-newer = "2026-06-29T14:57:29.757967Z" exclude-newer-span = "P1W" [options.exclude-newer-package] @@ -955,6 +955,7 @@ requires-dist = [{ name = "fastmcp-slim", extras = ["client", "server"], editabl name = "fastmcp-slim" source = { editable = "fastmcp_slim" } dependencies = [ + { name = "mcp-types" }, { name = "platformdirs" }, { name = "pydantic", extra = ["email"] }, { name = "pydantic-settings" }, @@ -979,7 +980,6 @@ client = [ { name = "exceptiongroup" }, { name = "httpx" }, { name = "mcp" }, - { name = "mcp-types" }, { name = "opentelemetry-api" }, { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, { name = "starlette" }, @@ -995,7 +995,6 @@ mcp = [ { name = "exceptiongroup" }, { name = "httpx" }, { name = "mcp" }, - { name = "mcp-types" }, { name = "opentelemetry-api" }, { name = "starlette" }, ] @@ -1012,7 +1011,6 @@ server = [ { name = "jsonref" }, { name = "jsonschema-path" }, { name = "mcp" }, - { name = "mcp-types" }, { name = "openapi-pydantic" }, { name = "opentelemetry-api" }, { name = "packaging" }, @@ -1052,9 +1050,7 @@ requires-dist = [ { name = "mcp", marker = "extra == 'client'", specifier = "==2.0.0b1" }, { name = "mcp", marker = "extra == 'mcp'", specifier = "==2.0.0b1" }, { name = "mcp", marker = "extra == 'server'", specifier = "==2.0.0b1" }, - { name = "mcp-types", marker = "extra == 'client'", specifier = "==2.0.0b1" }, - { name = "mcp-types", marker = "extra == 'mcp'", specifier = "==2.0.0b1" }, - { name = "mcp-types", marker = "extra == 'server'", specifier = "==2.0.0b1" }, + { name = "mcp-types", specifier = "==2.0.0b1" }, { name = "openai", marker = "extra == 'openai'", specifier = ">=1.102.0" }, { name = "openapi-pydantic", marker = "extra == 'server'", specifier = ">=0.5.1" }, { name = "opentelemetry-api", marker = "extra == 'client'", specifier = ">=1.28.0" },