Parameterized Context loses request state in resource and prompt handlers
Dieses Issue hat noch niemand übernommen.
Bewertung
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Anfängerfreundlichkeit
- 55/100
- Issue-Typ
- Bug
- Klarheit
- Klar beschrieben
- Aktivitätsstatus
- Ruhig
- Tech-Stack
- python
- Bereich
- backend-api-design
Rechercherichtung
Beginne bei ResourceTemplate.from_function und Prompt.from_function und vergleiche ihren Validierungspfad mit FuncMetadata.call_fn_with_arg_validation, das von tools verwendet wird. Rufe die Resource- und Prompt-Aufrufe aus dem minimalen Beispiel erneut auf und untersuche, ob der injizierte Context seine Identität und seinen Request-Zustand beibehält. Die Aufgabe ist erfüllt, wenn die Client-Argumente weiterhin validiert werden, während der vom Server erstellte Context unverändert beide Handler erreicht.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Description
A resource-template or prompt handler annotated with a parameterized Context[LifespanContextT] receives a different Context object from the one the server injected. The reconstructed object has none of the private request state, so accessing ctx.request_context, ctx.session, ctx.request_id, or lifespan state fails with:
ValueError: Context is not available outside of a request
The equivalent tool handler works, and handlers annotated with unparameterized Context happen to work. This makes the documented typed lifespan-context pattern unusable specifically in dynamic resources and prompts.
Minimal reproduction
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
import anyio
from mcp.client import Client
from mcp.server.mcpserver import Context, MCPServer
@asynccontextmanager
async def lifespan(_: MCPServer[str]) -> AsyncIterator[str]:
yield "live-state"
async def main() -> None:
server = MCPServer("probe", lifespan=lifespan)
@server.resource("probe://{name}")
async def resource(name: str, ctx: Context[str]) -> str:
return f"{name}:{ctx.request_context.lifespan_context}"
@server.prompt("probe")
async def prompt(name: str, ctx: Context[str]) -> str:
return f"{name}:{ctx.request_context.lifespan_context}"
async with Client(server) as client:
await client.read_resource("probe://value")
await client.get_prompt("probe", {"name": "value"})
anyio.run(main)
Both calls fail. Each should return content containing value:live-state.
Root cause
ResourceTemplate.from_function and Prompt.from_function correctly omit the detected context parameter from their public argument schemas, but then wrap the original handler with pydantic.validate_call. At invocation time they inject the live Context into the wrapped handler, so Pydantic validates it again.
For a generic annotation, Pydantic converts the unparameterized runtime instance into Context[str, Any]. That creates a new model instance and does not carry over Context's private _request_context, _mcp_server, and related attributes:
server-created Context
|
v
validate_call parameter: Context[str]
|
v
new Context[str, Any] instance
|
v
private request state absent
A direct identity probe on current main shows:
Context -> same object, request state present
Context[str] -> new object, request state absent
Tools avoid this because FuncMetadata.call_fn_with_arg_validation validates only client-supplied arguments and passes injected values directly to the raw handler.
Impact
This affects resource templates and prompts that use typed lifespan context, including otherwise valid code following the SDK's generic Context[LifespanContextT] typing. It can also hide in tests that only assert that ctx is non-null rather than reading request-scoped state.
A compatible fix should preserve the existing validation/coercion of client-supplied resource or prompt arguments while passing the server-created context object directly, with its identity and private state intact.
Environment
- MCP Python SDK: 2.0.0 and current
mainata4f4ccd091138771535e17191123f20b30fda68e - Python: 3.12.13
- Pydantic: 2.12.5
AI assistance was used to investigate and draft this report.
- Vorherrschende Sprache
- Python
- Sterne
- 24.3k
- Forks
- 4k
- Ø Merge
- 1 T. 11 Std.
- Gemergte PRs (30 T.)
- 30
Beitragsleitfaden
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus modelcontextprotocol/python-sdk
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
modelcontextprotocol/python-sdk#3566 ·
-
v1 v2
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 85/100
modelcontextprotocol/python-sdk#3546 · 5 Kommentare ·
-
v1 v2
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 76/100
modelcontextprotocol/python-sdk#3545 · 1 Kommentar ·
-
v1 v2
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 91/100
modelcontextprotocol/python-sdk#3508 · 2 Kommentare ·
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 64/100
modelcontextprotocol/python-sdk#3504 ·
Alle Issues in modelcontextprotocol/python-sdk
Ähnliche Issues
-
essnmx good first issue
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 95/100
-
[Feature] 奇物选择添加优先级 Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 65/100
syfoud/Simulated_Scepter#174 ·
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
Giskard-AI/giskard-oss#2840 · 1 Kommentar ·
-
A claim comment carrying the issue number is silently declined while the workflow reports success Offenarea: repo bug perceived difficulty: 2
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 70/100
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 75/100
yeti-platform/yeti#1380 ·