You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
grapharc[mcp] is broken on arrival: mcp>=1.2 resolves to 2.0, where mcp.server.fastmcp no longer exists #101
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:
mcp = ["mcp>=1.2"]
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:
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.
Closing this: both halves are on main and verified against the tree as it stands today.
pyproject.toml:91 mcp = ["mcp>=1.2,<2"]
grapharc/mcp/__init__.py __getattr__ resolves build_server / serve_stdio /
FORBIDDEN_TOOL_WORDS on first access (PEP 562),
so `import grapharc.mcp` needs no SDK
The comment above the pin carries the reason, so a future >=1.2 widening has to argue with it rather than happen by accident, and the package docstring records the posture the slack extra already took — only the module that needs the extra imports it, and it imports it lazily. That is also what fixed the false CI report of grapharc.mcp.driver / grapharc.mcp.server "missing from the wheel": pkgutil.walk_packages imports a package to walk into it, so an eagerly-importing __init__ made a present subpackage read as absent.
The follow-up in "Worth considering separately" is still open and still true. Every other extra remains unbounded:
Each is one major release away from exactly this failure, and uv.lock will hide each one the same way — the only job that can catch it is a resolve that starts from pyproject.toml rather than the lockfile. That is a different change from this one (a scheduled CI job, not a pin), so it belongs in its own issue rather than keeping this one open; filing it separately so it does not get lost.
Activity
Shashankss1205 commentedon Aug 13, 2026
Closing this: both halves are on
mainand verified against the tree as it stands today.The comment above the pin carries the reason, so a future
>=1.2widening has to argue with it rather than happen by accident, and the package docstring records the posture theslackextra already took — only the module that needs the extra imports it, and it imports it lazily. That is also what fixed the false CI report ofgrapharc.mcp.driver/grapharc.mcp.server"missing from the wheel":pkgutil.walk_packagesimports a package to walk into it, so an eagerly-importing__init__made a present subpackage read as absent.The follow-up in "Worth considering separately" is still open and still true. Every other extra remains unbounded:
Each is one major release away from exactly this failure, and
uv.lockwill hide each one the same way — the only job that can catch it is a resolve that starts frompyproject.tomlrather than the lockfile. That is a different change from this one (a scheduled CI job, not a pin), so it belongs in its own issue rather than keeping this one open; filing it separately so it does not get lost.ci: the drift job checks an installed wheel, not just the source tree (…