Skip to content

Commit 5a730a9

Browse files
committed
test: cover get_prompt context=None fallback; revert no branch pragma
- pragma: no branch only exempts branch coverage, not line coverage. The return on line 97 is never executed, so no cover is correct. - get_prompt fallback at server.py:1089 was never hit since all tests use Client (E2E). Added a direct-call test.
1 parent 95d5b32 commit 5a730a9

File tree

3 files changed

+90
-1
lines changed

3 files changed

+90
-1
lines changed

GENERICS_AUDIT.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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.

src/mcp/server/mcpserver/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ async def report_progress(self, progress: float, total: float | None = None, mes
9393
"""
9494
progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None
9595

96-
if progress_token is None: # pragma: no branch
96+
if progress_token is None: # pragma: no cover
9797
return
9898

9999
await self.request_context.session.send_progress_notification(

tests/server/mcpserver/test_server.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,19 @@ def prompt_no_context(text: str) -> str:
12311231
class TestServerPrompts:
12321232
"""Test prompt functionality in MCPServer server."""
12331233

1234+
async def test_get_prompt_direct_call_without_context(self):
1235+
"""Test calling mcp.get_prompt() directly without passing context."""
1236+
mcp = MCPServer()
1237+
1238+
@mcp.prompt()
1239+
def fn() -> str:
1240+
return "Hello, world!"
1241+
1242+
result = await mcp.get_prompt("fn")
1243+
content = result.messages[0].content
1244+
assert isinstance(content, TextContent)
1245+
assert content.text == "Hello, world!"
1246+
12341247
async def test_prompt_decorator(self):
12351248
"""Test that the prompt decorator registers prompts correctly."""
12361249
mcp = MCPServer()

0 commit comments

Comments
 (0)