Skip to content

Commit e696496

Browse files
grapharc[mcp] shipped broken: an unbounded pin, and a package that could not be imported without it
Two defects, one found by the other. `grapharc/mcp/server.py` builds on `mcp.server.fastmcp`, which exists in the mcp 1.x line and was removed in 2.0 — and the extra said `mcp>=1.2`. So every fresh `pip install 'grapharc[mcp]'` resolved to 2.0.0 and could not import the server at all, while `uv.lock` held development on 1.28 and kept every local test green. The ceiling is explicit now: raising it means porting off FastMCP, which should be a diff someone writes rather than a resolver accident. CI reported that as "in the source tree but not in the wheel: ['grapharc.mcp.driver', 'grapharc.mcp.server']", which was not true — both were in the wheel. `pkgutil.walk_packages` imports a package to walk into it, and `grapharc/mcp/__init__.py` eagerly imported the SDK-backed module, so any environment without a working extra made the whole subpackage unwalkable and it read as missing. `grapharc/slack/__init__.py` already documents the right posture; `driver.py` needs no SDK. The three SDK-backed names resolve on first access now, so `import grapharc.mcp` works without the extra and `from grapharc.mcp import build_server` still works with it. 130 modules import from a clean `[all]` wheel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent de313ef commit e696496

3 files changed

Lines changed: 35 additions & 4 deletions

File tree

grapharc/mcp/__init__.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,35 @@
77
88
`grapharc mcp` is the entry point; `build_server` is the library surface a
99
test drives directly.
10+
11+
**The extra is imported lazily**, the same posture `grapharc.slack` takes: only
12+
`server.py` needs the MCP SDK, `driver.py` needs nothing beyond grapharc's core
13+
dependencies, and importing this package must not require either. Eagerly
14+
importing `server` here made `grapharc.mcp` unimportable in any environment
15+
without the extra — which is every environment the packaging check builds, so a
16+
whole subpackage read as missing from the wheel it was actually in.
17+
18+
`build_server` and `serve_stdio` therefore resolve on first attribute access
19+
(PEP 562), and `from grapharc.mcp import build_server` still works.
1020
"""
1121

22+
from typing import Any
23+
1224
from grapharc.mcp.driver import DriverError
13-
from grapharc.mcp.server import FORBIDDEN_TOOL_WORDS, build_server, serve_stdio
1425

1526
__all__ = ["FORBIDDEN_TOOL_WORDS", "DriverError", "build_server", "serve_stdio"]
27+
28+
_DEFERRED = frozenset({"FORBIDDEN_TOOL_WORDS", "build_server", "serve_stdio"})
29+
30+
31+
def __getattr__(name: str) -> Any:
32+
"""Resolve the SDK-backed names on first use, not at import."""
33+
if name in _DEFERRED:
34+
from grapharc.mcp import server
35+
36+
return getattr(server, name)
37+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
38+
39+
40+
def __dir__() -> list[str]:
41+
return sorted(__all__)

pyproject.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,14 @@ server = [
8181
"fastapi>=0.115",
8282
"uvicorn>=0.32",
8383
]
84-
# MCP client (ROADMAP 3.5).
84+
# MCP client (ROADMAP 3.5). Upper-bounded on purpose: `grapharc/mcp/server.py`
85+
# builds on `mcp.server.fastmcp`, which exists in the 1.x line and was removed
86+
# in 2.0. `>=1.2` alone resolved to 2.0.0 for anyone installing fresh, so
87+
# `grapharc mcp` was broken on arrival while the lockfile kept development on
88+
# 1.28 and every local test green. Raising this ceiling means porting the
89+
# server off FastMCP, which is a change with a diff, not a resolver accident.
8590
mcp = [
86-
"mcp>=1.2",
91+
"mcp>=1.2,<2",
8792
]
8893
# LadybugDB-backed claim store — grapharc/memory/ladybug_store.py imports this
8994
# one, unlike the `memory` extra above. The distribution is `real-ladybug`, not

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)