Skip to content

Tool context kwarg detection not compatible with string-valued/__future__ annotations #1129

@dmontagu

Description

@dmontagu

Initial Checks

Description

When inferring the context kwarg for tools, the relevant code —

if context_kwarg is None:
sig = inspect.signature(fn)
for param_name, param in sig.parameters.items():
if get_origin(param.annotation) is not None:
continue
if issubclass(param.annotation, Context):
context_kwarg = param_name
break
— has the check if issubclass(param.annotation, Context):.

However, param.annotation is the raw value of the annotation. In particular, if using from __future__ import annotations, this will be a string, and so the check will incorrectly result in the context argument not being detected.

Not only is it unfortunate that this currently misbehaves at all, but it is unfortunate that you don't get an error until the model actually tries to call the tool with "mis-annotated" context, and even worse, when the model does try to call the tool with context you get a cryptic (and as far as I can tell inaccurate) error message saying that you can't access context outside of a request, even though the tool really is being called in a request. (This was a painful one to debug.)

In Pydantic AI we work around stuff like this by relying on a combination of inpect.Signature.parameters and typing.get_type_hints — see https://github.com/pydantic/pydantic-ai/blob/a08a20ea1f87a4d093fdb08cb5dd8691e3589656/pydantic_ai_slim/pydantic_ai/_function_schema.py#L102-L133 for more details. (Actually we use pydantic._internal._typing_extra.get_function_type_hints, which is very similar to get_type_hints but has tweaks to better handle some edge cases, but typing.get_type_hints might be enough here.)

Example Code

# https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/tool_progress.py
# with the future import added
from __future__ import annotations

from mcp.server.fastmcp import Context, FastMCP

mcp = FastMCP(name="Progress Example")


@mcp.tool()
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
    """Execute a task with progress updates."""
    await ctx.info(f"Starting: {task_name}")

    for i in range(steps):
        progress = (i + 1) / steps
        await ctx.report_progress(
            progress=progress,
            total=1.0,
            message=f"Step {i + 1}/{steps}",
        )
        await ctx.debug(f"Completed step {i + 1}")

    return f"Task '{task_name}' completed"

Python & MCP Python SDK

Confirmed explicitly with python 3.13 and 3.11, and MCP SDK 1.7.1 and 1.11.0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions