refactor: remove request_ctx ContextVar, thread Context explicitly#2203
refactor: remove request_ctx ContextVar, thread Context explicitly#2203
Conversation
The request_ctx ContextVar in mcp.server.lowlevel.server was redundant with the ServerRequestContext already passed as the first argument to every _handle_* method. This removes the ContextVar entirely and threads Context explicitly. Changes: - MCPServer.get_context() removed — use ctx: Context parameter injection in tool/resource/prompt functions instead - MCPServer.call_tool/read_resource/get_prompt now accept an optional context: Context | None = None parameter; _handle_* methods construct the Context at the lowlevel boundary and pass it through - Context class moved from server.py to its own context.py module (still re-exported from mcp.server.mcpserver) - Tool.run/Prompt.render/ResourceTemplate.create_resource now raise a clear error if the registered function requires a Context but none was provided, instead of silently injecting None Github-Issue: #2112 Github-Issue: #1684
| if self.context_kwarg is not None and context is None: | ||
| raise ValueError(f"Prompt {self.name!r} requires a Context, but none was provided") |
There was a problem hiding this comment.
How would the Context be None here? 🤔
There was a problem hiding this comment.
Also, probably it's RuntimeError, if anything.
There was a problem hiding this comment.
Honestly the way Context is optional is rather weird through the whole call chain. If it's None it's sometimes just not injected which also breaks stuff.
After taking a bit of time to walk through it, I think it'd be better to leave it as optional on the MCPServer methods itself, but then make it required on the rest of the call stack, which is essentially the same functionality that existed before. If you called the previous MCPServer.get_context method it would just construct a new one if none existed.
So will do that to restore what was here before and remove some of the weird optional handling through the rest of the call chain.
| """ | ||
| progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None | ||
|
|
||
| if progress_token is None: # pragma: no cover |
There was a problem hiding this comment.
| if progress_token is None: # pragma: no cover | |
| if progress_token is None: # pragma: no branch |
There was a problem hiding this comment.
didn't work, this causes it to fail coverage
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
…rver Addresses review feedback asking when Context could be None. The answer: only via direct calls (tests, programmatic use). Rather than guard at the leaf layers, make the internal layers type-honest. - MCPServer.call_tool/read_resource/get_prompt: keep context optional, auto-construct Context(mcp_server=self) when None (restores the behavior get_context() used to provide) - ToolManager/Tool, PromptManager/Prompt, ResourceManager/ResourceTemplate: context is now required — type signature matches production reality where _handle_* always provides it - Guards removed from Tool.run/Prompt.render/ResourceTemplate.create_resource; no longer reachable since context is required - Prompt.render and PromptManager.render_prompt: arguments parameter no longer has a default (both arguments and context are now required positional) - Added TODO noting Context constructor nullability is vestigial (follow-up)
Removes the _elicit_url alias — bare elicit_url in the method body resolves to the module-level import via Python's LEGB scoping, not the same-named method on the class.
The line is executed in tests (when progress_token is set), just the early-return branch isn't. no branch is semantically more accurate.
- 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.
83e1a6c to
56d237a
Compare
| Raises: | ||
| ValueError: If creating the resource fails. |
There was a problem hiding this comment.
Yes, few lines down
Removes the
request_ctxContextVar and threadsContextexplicitly through the MCPServer request-handling chain.Motivation and Context
The
request_ctxContextVar inmcp.server.lowlevel.serverwas redundant: the lowlevel server already passesServerRequestContextas the first argument to every_handle_*method. The ContextVar was a second mechanism carrying the same value, used only byMCPServer.get_context().This PR removes the ContextVar entirely, making the data flow explicit.
_handle_*methods construct the high-levelContextat the lowlevel→MCPServer boundary and pass it through.Part of #2112 (context refactor) / #1684 (contextvars cleanup).
How Has This Been Tested?
Tool.run(),Prompt.render(),ResourceTemplate.create_resource()uv run --frozen pyright src/ tests/— 0 errorsuv run --frozen ruff check— cleanBreaking Changes
Yes — documented in
docs/migration.md:MCPServer.get_context()removed — usectx: Contextparameter injection in tool/resource/prompt functions instead (the existing recommended pattern)request_ctxContextVar removed frommcp.server.lowlevel.server— code importing it directly will need to use parameter injectionMCPServer.call_tool(),read_resource(),get_prompt()now accept an optionalcontext: Context | None = Noneparameter — backward-compatible for callers that don't pass itctx: Contextparameter but is called withcontext=None, a clear error is raised (ToolErrorfor tools,ValueErrorfor prompts/resource templates). PreviouslyNonewas silently injected.Types of changes
Checklist
Additional context
Contextclass moved frommcp.server.mcpserver.servertomcp.server.mcpserver.context. The public import path (from mcp.server.mcpserver import Context) is unchanged via__init__.pyre-export. The old direct-module path (from mcp.server.mcpserver.server import Context) also still works becauseserver.pyimportsContextat module level.Exception hierarchy cleanup (making prompts/resources raise
MCPServerErrorsubclasses instead ofValueError) was considered but deferred to #1742 (error taxonomy) to avoid scope creep — see that issue for the full analysis of the existing inconsistencies.AI Disclaimer