Skip to content

grapharc[mcp] is broken on arrival: mcp>=1.2 resolves to 2.0, where mcp.server.fastmcp no longer exists #101

Description

@Shashankss1205

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:

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

Activity

  1. Shashankss1205 commented on Aug 13, 2026

    @Shashankss1205
    CollaboratorAuthor

    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:

    langchain-openai>=0.2     (openrouter, openai, novita, ollama)
    fastapi>=0.115            uvicorn>=0.32
    real-ladybug>=0.15.3      anthropic>=0.40      slack-bolt>=1.20
    opentelemetry-{api,sdk,exporter-otlp-proto-http}>=1.27
    

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions