"""Tests for tool context injection.""" import functools from dataclasses import dataclass from pydantic import BaseModel from typing_extensions import TypedDict from fastmcp import Context, FastMCP from fastmcp.tools.base import Tool def _normalize_anyof_order(schema): """Normalize the order of items in anyOf arrays for consistent comparison.""" if isinstance(schema, dict): if "anyOf" in schema: schema = schema.copy() schema["anyOf"] = sorted(schema["anyOf"], key=str) return {k: _normalize_anyof_order(v) for k, v in schema.items()} elif isinstance(schema, list): return [_normalize_anyof_order(item) for item in schema] return schema class PersonTypedDict(TypedDict): name: str age: int class PersonModel(BaseModel): name: str age: int @dataclass class PersonDataclass: name: str age: int class TestToolContextInjection: """Test context injection in tools.""" async def test_context_detection(self): """Test that context parameters are properly detected and excluded from schema.""" mcp = FastMCP() @mcp.tool def tool_with_context(x: int, ctx: Context) -> str: return f"Request: {x}" tools = await mcp.list_tools() assert len(tools) == 1 assert tools[0].name == "tool_with_context" # Context param should not appear in schema assert "ctx" not in tools[0].parameters.get("properties", {}) async def test_context_injection_basic(self): """Test that context is properly injected into tool calls.""" mcp = FastMCP() @mcp.tool def tool_with_context(x: int, ctx: Context) -> str: assert isinstance(ctx, Context) return f"Got context with x={x}" result = await mcp.call_tool("tool_with_context", {"x": 42}) assert result.structured_content == {"result": "Got context with x=42"} async def test_async_context(self): """Test that context works in async functions.""" mcp = FastMCP() @mcp.tool async def async_tool(x: int, ctx: Context) -> str: assert isinstance(ctx, Context) return f"Async with x={x}" result = await mcp.call_tool("async_tool", {"x": 42}) assert result.structured_content == {"result": "Async with x=42"} async def test_optional_context(self): """Test that context is optional.""" mcp = FastMCP() @mcp.tool def no_context(x: int) -> int: return x * 2 result = await mcp.call_tool("no_context", {"x": 21}) assert result.structured_content == {"result": 42} async def test_context_resource_access(self): """Test that context can access resources.""" mcp = FastMCP() @mcp.resource("test://data") def test_resource() -> str: return "resource data" @mcp.tool async def tool_with_resource(ctx: Context) -> str: result = await ctx.read_resource("test://data") assert len(result.contents) == 1 r = result.contents[0] return f"Read resource: {r.content} with mime type {r.mime_type}" result = await mcp.call_tool("tool_with_resource", {}) assert result.structured_content == { "result": "Read resource: resource data with mime type text/plain" } async def test_tool_decorator_with_tags(self): """Test that the tool decorator properly sets tags.""" mcp = FastMCP() @mcp.tool(tags={"example", "test-tag"}) def sample_tool(x: int) -> int: return x * 2 tools = await mcp.list_tools() assert len(tools) == 1 assert tools[0].tags == {"example", "test-tag"} async def test_callable_object_with_context(self): """Test that a callable object can be used as a tool with context.""" mcp = FastMCP() class MyTool: async def __call__(self, x: int, ctx: Context) -> int: assert isinstance(ctx, Context) return x + 1 mcp.add_tool(Tool.from_function(MyTool(), name="MyTool")) result = await mcp.call_tool("MyTool", {"x": 2}) assert result.structured_content == {"result": 3} async def test_decorated_tool_with_functools_wraps(self): """Regression test for #2524: @mcp.tool with functools.wraps decorator.""" def custom_decorator(func): @functools.wraps(func) async def wrapper(*args, **kwargs): return await func(*args, **kwargs) return wrapper mcp = FastMCP() @mcp.tool @custom_decorator async def decorated_tool(ctx: Context, query: str) -> str: assert isinstance(ctx, Context) return f"query: {query}" tools = await mcp.list_tools() tool = next(t for t in tools if t.name == "decorated_tool") assert "ctx" not in tool.parameters.get("properties", {}) result = await mcp.call_tool("decorated_tool", {"query": "test"}) assert result.structured_content == {"result": "query: test"}