|
| 1 | +# Generics Audit |
| 2 | + |
| 3 | +## 1. Dangling TypeVars in manager/base methods |
| 4 | + |
| 5 | +**Where:** `tool_manager.py:84`, `tools/base.py:95`, `prompts/manager.py:51`, |
| 6 | +`prompts/base.py:138`, `resource_manager.py:84`, `templates.py:102` |
| 7 | + |
| 8 | +`Context[LifespanContextT, RequestT]` on a method of a non-generic class. |
| 9 | +TypeVars appear once, return types don't use them. Pyright treats them as |
| 10 | +method-scoped and discards per-call. Behaviorally identical to |
| 11 | +`Context[Any, Any]` but reads as if it means something. |
| 12 | + |
| 13 | +**Fix:** `Context[Any, Any]`. The handler boundary is `Callable[..., Any]` -- |
| 14 | +T is already dead one layer down, threading it through managers is ceremony. |
| 15 | + |
| 16 | +## 2. `@server.tool()` doesn't check `Context[T]` against `MCPServer[T]` |
| 17 | + |
| 18 | +Only user-visible gap. `MCPServer[AppState]` + `def tool(ctx: Context[int])` |
| 19 | +passes pyright. Decorator sig is `Callable[[_CallableT], _CallableT]` -- no |
| 20 | +relationship to the server's T. |
| 21 | + |
| 22 | +**Fix:** Won't-fix. Introspection-based injection can't be pattern-matched at |
| 23 | +the type level. Document the `ServerContext: TypeAlias = Context[AppState, Any]` |
| 24 | +workaround. |
| 25 | + |
| 26 | +## 3. `Context._mcp_server: MCPServer` (bare) |
| 27 | + |
| 28 | +**Where:** `mcpserver/context.py:59,66,75` |
| 29 | + |
| 30 | +`Context[AppState, Any].mcp_server` returns `MCPServer[Any]`. Context knows |
| 31 | +AppState, property forgot. |
| 32 | + |
| 33 | +**Fix:** `MCPServer[LifespanContextT]`. Sound -- every construction site |
| 34 | +(`server.py:301,334,371`) passes `mcp_server=self` where self's T matches ctx's T. |
| 35 | + |
| 36 | +## 4. Two TypeVars, one concept, two defaults |
| 37 | + |
| 38 | +| | `LifespanResultT` | `LifespanContextT` | |
| 39 | +|---------|-------------------------|------------------------| |
| 40 | +| Defined | `lowlevel/server.py:74` | `server/context.py:13` | |
| 41 | +| Default | `Any` | `dict[str, Any]` | |
| 42 | +| Used by | Server, MCPServer, Settings, ExperimentalHandlers | ServerRequestContext, Context | |
| 43 | + |
| 44 | +Same concept ("what lifespan yields"). Works because TypeVar identity doesn't |
| 45 | +matter for binding, only position. But `Context()` gives |
| 46 | +`Context[dict[str, Any], Any]` while `MCPServer()` gives `MCPServer[Any]`. |
| 47 | + |
| 48 | +**Fix:** Import `LifespanResultT` into `context.py`, delete `LifespanContextT`. |
| 49 | + |
| 50 | +## 5. `ServerSessionT` orphan |
| 51 | + |
| 52 | +**Where:** `server/session.py:60` |
| 53 | + |
| 54 | +Defined, zero references. |
| 55 | + |
| 56 | +**Fix:** Delete. |
| 57 | + |
| 58 | +## 6. `RequestResponder.__init__` has 3 dangling TypeVars |
| 59 | + |
| 60 | +**Where:** `shared/session.py:79` |
| 61 | + |
| 62 | +Class is `Generic[ReceiveRequestT, SendResultT]` but `__init__`'s `session` |
| 63 | +param uses 5. The extra 3 are method-scoped on `__init__`. |
| 64 | + |
| 65 | +**Fix:** `session: BaseSession[Any, Any, SendResultT, ReceiveRequestT, Any]` |
| 66 | + |
| 67 | +## 7. Arity drift on `ServerRequestContext` |
| 68 | + |
| 69 | +Lowlevel writes `ServerRequestContext[T]` (1 arg), mcpserver writes |
| 70 | +`ServerRequestContext[T, R]` (2 args). Both valid because `RequestT` has |
| 71 | +`default=Any`. Not a bug, inconsistent. |
| 72 | + |
| 73 | +## 8. `Context` is invariant |
| 74 | + |
| 75 | +`Context[AppState]` not assignable to `Context[object]`. Forced through `Any`. |
| 76 | +Doesn't matter once #1 is fixed. |
0 commit comments