Skip to content

refactor: remove request_ctx ContextVar, thread Context explicitly#2203

Merged
maxisbey merged 6 commits intomainfrom
remove-request-ctx-contextvar
Mar 4, 2026
Merged

refactor: remove request_ctx ContextVar, thread Context explicitly#2203
maxisbey merged 6 commits intomainfrom
remove-request-ctx-contextvar

Conversation

@maxisbey
Copy link
Contributor

@maxisbey maxisbey commented Mar 2, 2026

Removes the request_ctx ContextVar and threads Context explicitly through the MCPServer request-handling chain.

Motivation and Context

The request_ctx ContextVar in mcp.server.lowlevel.server was redundant: the lowlevel server already passes ServerRequestContext as the first argument to every _handle_* method. The ContextVar was a second mechanism carrying the same value, used only by MCPServer.get_context().

This PR removes the ContextVar entirely, making the data flow explicit. _handle_* methods construct the high-level Context at the lowlevel→MCPServer boundary and pass it through.

Part of #2112 (context refactor) / #1684 (contextvars cleanup).

How Has This Been Tested?

  • All existing tests pass (1123 passed, 98 skipped, 1 xfailed)
  • New tests for the context-required guards in Tool.run(), Prompt.render(), ResourceTemplate.create_resource()
  • uv run --frozen pyright src/ tests/ — 0 errors
  • uv run --frozen ruff check — clean

Breaking Changes

Yes — documented in docs/migration.md:

  • MCPServer.get_context() removed — use ctx: Context parameter injection in tool/resource/prompt functions instead (the existing recommended pattern)
  • request_ctx ContextVar removed from mcp.server.lowlevel.server — code importing it directly will need to use parameter injection
  • MCPServer.call_tool(), read_resource(), get_prompt() now accept an optional context: Context | None = None parameter — backward-compatible for callers that don't pass it
  • New behavior: if a tool/resource/prompt declares a ctx: Context parameter but is called with context=None, a clear error is raised (ToolError for tools, ValueError for prompts/resource templates). Previously None was silently injected.

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Context class moved from mcp.server.mcpserver.server to mcp.server.mcpserver.context. The public import path (from mcp.server.mcpserver import Context) is unchanged via __init__.py re-export. The old direct-module path (from mcp.server.mcpserver.server import Context) also still works because server.py imports Context at module level.

Exception hierarchy cleanup (making prompts/resources raise MCPServerError subclasses instead of ValueError) 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

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
@maxisbey maxisbey requested review from Kludex and felixweinberger and removed request for Kludex March 2, 2026 14:17
Comment on lines +147 to +148
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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the Context be None here? 🤔

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, probably it's RuntimeError, if anything.

Copy link
Contributor Author

@maxisbey maxisbey Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed

"""
progress_token = self.request_context.meta.get("progress_token") if self.request_context.meta else None

if progress_token is None: # pragma: no cover
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if progress_token is None: # pragma: no cover
if progress_token is None: # pragma: no branch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

didn't work, this causes it to fail coverage

maxisbey and others added 5 commits March 3, 2026 11:57
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.
@maxisbey maxisbey force-pushed the remove-request-ctx-contextvar branch from 83e1a6c to 56d237a Compare March 4, 2026 11:35
Comment on lines +106 to +107
Raises:
ValueError: If creating the resource fails.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, few lines down

@maxisbey maxisbey merged commit cc22bf5 into main Mar 4, 2026
36 checks passed
@maxisbey maxisbey deleted the remove-request-ctx-contextvar branch March 4, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants