Found by reproducing the build and check the distribution CI job locally. This ships broken today — the lockfile hides it from every local test.
What happens
grapharc/mcp/server.py builds on from mcp.server.fastmcp import FastMCP. That module exists in the mcp 1.x line and was removed in 2.0. The extra was declared unbounded:
So a fresh install resolves to mcp 2.0.0 and the server cannot import:
$ pip install 'grapharc[mcp]' # gets mcp 2.0.0
$ python -c "import grapharc.mcp"
File ".../grapharc/mcp/server.py", line 33, in <module>
from mcp.server.fastmcp import FastMCP
ModuleNotFoundError: No module named 'mcp.server.fastmcp'
mcp.server in 2.0 exposes lowlevel, mcpserver, session, stdio, … and no fastmcp.
Why it was invisible
uv.lock pins mcp==1.28.1, so the whole dev environment and every local pytest run uses 1.x and passes. Only a resolver starting from pyproject.toml — that is, a real user — gets 2.0. The one job that catches it is CI's clean-environment wheel check.
The second defect it exposed
CI reported this as "in the source tree but not in the wheel: ['grapharc.mcp.driver', 'grapharc.mcp.server']", which is untrue — both files are in the wheel. pkgutil.walk_packages imports a package to walk into it, and grapharc/mcp/__init__.py eagerly did:
from grapharc.mcp.server import FORBIDDEN_TOOL_WORDS, build_server, serve_stdio
so any environment without a working mcp SDK made the whole subpackage unwalkable and it read as missing. grapharc/slack/__init__.py already documents the right posture for this — "Only bot.py needs the slack extra, and it imports it lazily … so a wheel without the extra still imports" — and grapharc/mcp/driver.py needs no SDK at all.
Fixed in the same change
mcp = ["mcp>=1.2,<2"]. Raising that ceiling means porting off FastMCP, which should be a diff someone writes, not a resolver accident.
grapharc/mcp/__init__.py resolves build_server / serve_stdio / FORBIDDEN_TOOL_WORDS lazily via PEP 562, so import grapharc.mcp works without the extra and from grapharc.mcp import build_server still works with it. 130 modules now import from a clean [all] wheel.
Worth considering separately
Every other extra is unbounded too (langchain-openai>=0.2, fastapi>=0.115, slack-bolt>=1.20, real-ladybug>=0.15.3, anthropic>=0.40, the three OTel packages). Each is one major release away from the same failure, and the lockfile will hide each of them the same way. A periodic CI job that resolves from pyproject.toml rather than uv.lock would catch the next one before a user does.
🤖 Generated with Claude Code
Found by reproducing the
build and check the distributionCI job locally. This ships broken today — the lockfile hides it from every local test.What happens
grapharc/mcp/server.pybuilds onfrom mcp.server.fastmcp import FastMCP. That module exists in themcp1.x line and was removed in 2.0. The extra was declared unbounded:So a fresh install resolves to
mcp2.0.0 and the server cannot import:mcp.serverin 2.0 exposeslowlevel,mcpserver,session,stdio, … and nofastmcp.Why it was invisible
uv.lockpinsmcp==1.28.1, so the whole dev environment and every localpytestrun uses 1.x and passes. Only a resolver starting frompyproject.toml— that is, a real user — gets 2.0. The one job that catches it is CI's clean-environment wheel check.The second defect it exposed
CI reported this as "in the source tree but not in the wheel: ['grapharc.mcp.driver', 'grapharc.mcp.server']", which is untrue — both files are in the wheel.
pkgutil.walk_packagesimports a package to walk into it, andgrapharc/mcp/__init__.pyeagerly did:so any environment without a working
mcpSDK made the whole subpackage unwalkable and it read as missing.grapharc/slack/__init__.pyalready documents the right posture for this — "Onlybot.pyneeds theslackextra, and it imports it lazily … so a wheel without the extra still imports" — andgrapharc/mcp/driver.pyneeds no SDK at all.Fixed in the same change
mcp = ["mcp>=1.2,<2"]. Raising that ceiling means porting off FastMCP, which should be a diff someone writes, not a resolver accident.grapharc/mcp/__init__.pyresolvesbuild_server/serve_stdio/FORBIDDEN_TOOL_WORDSlazily via PEP 562, soimport grapharc.mcpworks without the extra andfrom grapharc.mcp import build_serverstill works with it. 130 modules now import from a clean[all]wheel.Worth considering separately
Every other extra is unbounded too (
langchain-openai>=0.2,fastapi>=0.115,slack-bolt>=1.20,real-ladybug>=0.15.3,anthropic>=0.40, the three OTel packages). Each is one major release away from the same failure, and the lockfile will hide each of them the same way. A periodic CI job that resolves frompyproject.tomlrather thanuv.lockwould catch the next one before a user does.🤖 Generated with Claude Code