From ea1328660e70182e20d9084252750d8010ed9f57 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Fri, 20 Feb 2026 09:22:10 -0500
Subject: [PATCH] Fix NameError with future annotations and Context/Depends
parameters (#3243)
* Fix NameError with future annotations and Context/Depends parameters
Closes #3238, closes #905
* chore: Update SDK documentation
* Drop unnecessary pre-resolution of annotations
Pydantic (even 2.11.7) uses __module__ not __globals__ to resolve
annotations, so setting __module__ alone is sufficient.
* chore: Update SDK documentation
* Restore annotation pre-resolution for Pydantic compat
The wrapper's __globals__ is read-only and points to dependencies.py,
so some Pydantic versions use it instead of __module__ when resolving
string annotations. Pre-resolving via get_type_hints on the original
function ensures annotations are type objects before Pydantic sees them.
* chore: Update SDK documentation
---------
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
---
.../fastmcp-server-dependencies.mdx | 48 +++++++--------
src/fastmcp/server/dependencies.py | 16 +++--
tests/tools/test_tool_future_annotations.py | 60 ++++++++++++-------
3 files changed, 73 insertions(+), 51 deletions(-)
diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx
index eb1cbaa8e..88a756ebc 100644
--- a/docs/python-sdk/fastmcp-server-dependencies.mdx
+++ b/docs/python-sdk/fastmcp-server-dependencies.mdx
@@ -213,7 +213,7 @@ Handles:
- Async wrapper function without injected parameters
-### `resolve_dependencies`
+### `resolve_dependencies`
```python
resolve_dependencies(fn: Callable[..., Any], arguments: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None]
@@ -239,7 +239,7 @@ time, so all injection goes through the unified DI system.
which will be filtered out)
-### `CurrentContext`
+### `CurrentContext`
```python
CurrentContext() -> Context
@@ -258,7 +258,7 @@ current MCP operation (tool/resource/prompt call).
- `RuntimeError`: If no active context found (during resolution)
-### `CurrentDocket`
+### `CurrentDocket`
```python
CurrentDocket() -> Docket
@@ -278,7 +278,7 @@ automatically creates for background task scheduling.
- `ImportError`: If fastmcp[tasks] not installed
-### `CurrentWorker`
+### `CurrentWorker`
```python
CurrentWorker() -> Worker
@@ -298,7 +298,7 @@ automatically creates for background task processing.
- `ImportError`: If fastmcp[tasks] not installed
-### `CurrentFastMCP`
+### `CurrentFastMCP`
```python
CurrentFastMCP() -> FastMCP
@@ -316,7 +316,7 @@ This dependency provides access to the active FastMCP server.
- `RuntimeError`: If no server in context (during resolution)
-### `CurrentRequest`
+### `CurrentRequest`
```python
CurrentRequest() -> Request
@@ -336,7 +336,7 @@ current HTTP request. Only available when running over HTTP transports
- `RuntimeError`: If no HTTP request in context (e.g., STDIO transport)
-### `CurrentHeaders`
+### `CurrentHeaders`
```python
CurrentHeaders() -> dict[str, str]
@@ -353,7 +353,7 @@ safe to use in code that might run over any transport.
- A dependency that resolves to a dictionary of header name -> value
-### `CurrentAccessToken`
+### `CurrentAccessToken`
```python
CurrentAccessToken() -> AccessToken
@@ -372,7 +372,7 @@ authenticated request. Raises an error if no authentication is present.
- `RuntimeError`: If no authenticated user (use get_access_token() for optional)
-### `TokenClaim`
+### `TokenClaim`
```python
TokenClaim(name: str) -> str
@@ -406,7 +406,7 @@ Returned by ``get_task_context()`` when running inside a Docket worker.
Contains identifiers needed to communicate with the MCP session.
-### `ProgressLike`
+### `ProgressLike`
Protocol for progress tracking interface.
@@ -417,7 +417,7 @@ and Docket's Progress (worker context).
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
@@ -426,7 +426,7 @@ current(self) -> int | None
Current progress value.
-#### `total`
+#### `total`
```python
total(self) -> int
@@ -435,7 +435,7 @@ total(self) -> int
Total/target progress value.
-#### `message`
+#### `message`
```python
message(self) -> str | None
@@ -444,7 +444,7 @@ message(self) -> str | None
Current progress message.
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -453,7 +453,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
-#### `increment`
+#### `increment`
```python
increment(self, amount: int = 1) -> None
@@ -462,7 +462,7 @@ increment(self, amount: int = 1) -> None
Atomically increment the current progress value.
-#### `set_message`
+#### `set_message`
```python
set_message(self, message: str | None) -> None
@@ -471,7 +471,7 @@ set_message(self, message: str | None) -> None
Update the progress status message.
-### `InMemoryProgress`
+### `InMemoryProgress`
In-memory progress tracker for immediate tool execution.
@@ -483,25 +483,25 @@ progress doesn't need to be observable across processes.
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
```
-#### `total`
+#### `total`
```python
total(self) -> int
```
-#### `message`
+#### `message`
```python
message(self) -> str | None
```
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -510,7 +510,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
-#### `increment`
+#### `increment`
```python
increment(self, amount: int = 1) -> None
@@ -519,7 +519,7 @@ increment(self, amount: int = 1) -> None
Atomically increment the current progress value.
-#### `set_message`
+#### `set_message`
```python
set_message(self, message: str | None) -> None
@@ -528,7 +528,7 @@ set_message(self, message: str | None) -> None
Update the progress status message.
-### `Progress`
+### `Progress`
FastMCP Progress dependency that works in both server and worker contexts.
diff --git a/src/fastmcp/server/dependencies.py b/src/fastmcp/server/dependencies.py
index acd7fea19..15c8c3124 100644
--- a/src/fastmcp/server/dependencies.py
+++ b/src/fastmcp/server/dependencies.py
@@ -607,15 +607,23 @@ def without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]:
result = await result
return result
- # Set wrapper metadata (only parameter annotations, not return type)
+ # Resolve string annotations (from `from __future__ import annotations`) using
+ # the original function's module context. The wrapper's __globals__ points to
+ # this module (dependencies.py) and is read-only, so some Pydantic versions
+ # can't resolve names like Annotated or Literal from string annotations.
+ try:
+ resolved_hints = get_type_hints(fn, include_extras=True)
+ except Exception:
+ resolved_hints = getattr(fn, "__annotations__", {})
+
wrapper.__signature__ = new_sig # type: ignore[attr-defined]
wrapper.__annotations__ = {
- k: v
- for k, v in getattr(fn, "__annotations__", {}).items()
- if k not in exclude and k != "return"
+ k: v for k, v in resolved_hints.items() if k not in exclude and k != "return"
}
wrapper.__name__ = getattr(fn, "__name__", "wrapper")
wrapper.__doc__ = getattr(fn, "__doc__", None)
+ wrapper.__module__ = fn.__module__
+ wrapper.__qualname__ = getattr(fn, "__qualname__", wrapper.__qualname__)
return wrapper
diff --git a/tests/tools/test_tool_future_annotations.py b/tests/tools/test_tool_future_annotations.py
index a5d9d8c0d..10ca9c57a 100644
--- a/tests/tools/test_tool_future_annotations.py
+++ b/tests/tools/test_tool_future_annotations.py
@@ -1,9 +1,9 @@
from __future__ import annotations
-from typing import Any, cast
+from typing import Annotated, Any, Literal, cast
import mcp.types
-import pytest
+from pydantic import Field
from fastmcp import Context, FastMCP
from fastmcp.client import Client
@@ -55,6 +55,20 @@ async def async_with_context(ctx: Context) -> str:
return f"Async request: {ctx.request_id}"
+@fastmcp_server.tool
+def annotated_with_context(
+ query: Annotated[str, Field(description="Search query")], ctx: Context
+) -> str:
+ """Tool using Annotated + Field with context."""
+ return f"Result for: {query}"
+
+
+@fastmcp_server.tool
+def literal_with_context(mode: Literal["fast", "slow"], ctx: Context) -> str:
+ """Tool using Literal with context."""
+ return f"Mode: {mode}"
+
+
class TestFutureAnnotations:
async def test_simple_with_context(self):
async with Client(fastmcp_server) as client:
@@ -102,6 +116,23 @@ class TestFutureAnnotations:
"Async request:" in cast(mcp.types.TextContent, result.content[0]).text
)
+ async def test_annotated_with_context(self):
+ """Test Annotated[str, Field(...)] works with Context and future annotations."""
+ async with Client(fastmcp_server) as client:
+ result = await client.call_tool(
+ "annotated_with_context", {"query": "hello"}
+ )
+ assert (
+ "Result for: hello"
+ in cast(mcp.types.TextContent, result.content[0]).text
+ )
+
+ async def test_literal_with_context(self):
+ """Test Literal types work with Context and future annotations."""
+ async with Client(fastmcp_server) as client:
+ result = await client.call_tool("literal_with_context", {"mode": "fast"})
+ assert "Mode: fast" in cast(mcp.types.TextContent, result.content[0]).text
+
async def test_modern_union_syntax_works(self):
"""Test that modern | union syntax works with future annotations."""
# This demonstrates that our solution works with | syntax when types
@@ -138,33 +169,16 @@ class TestFutureAnnotations:
)
-@pytest.mark.xfail(
- reason="Closure-scoped types cannot be resolved with 'from __future__ import annotations'. "
- "When using future annotations, all type annotations become strings that need to be evaluated "
- "using eval() in the function's global namespace. Types defined only in closure scope "
- "(like local imports or type aliases) are not available in the function's __globals__ "
- "and therefore cannot be resolved by get_type_hints()."
-)
-def test_closure_scoped_types_limitation():
- """
- This test demonstrates that closure-scoped types don't work with future annotations.
-
- The fundamental issue is that 'from __future__ import annotations' converts all
- annotations to strings, and those strings can only be resolved using the function's
- global namespace, not local variables from closures.
- """
-
- def create_failing_closure():
- # This import is only available in the closure scope
+def test_closure_scoped_types_with_builtins():
+ """Closure-scoped tools work when annotations only reference builtins."""
+ def create_closure():
mcp = FastMCP()
@mcp.tool
def closure_tool(value: str | None) -> str:
- """This will fail because Optional can't be resolved from closure import."""
return str(value)
return mcp
- # This should raise an error during tool registration
- create_failing_closure()
+ create_closure()