diff --git a/src/fastmcp/client/client.py b/src/fastmcp/client/client.py index 9c7840d52..144c08ccd 100644 --- a/src/fastmcp/client/client.py +++ b/src/fastmcp/client/client.py @@ -880,6 +880,7 @@ async def call_tool_mcp( arguments: dict[str, Any], progress_handler: ProgressHandler | None = None, timeout: datetime.timedelta | float | int | None = None, + meta: dict[str, Any] | None = None, ) -> mcp.types.CallToolResult: """Send a tools/call request and return the complete MCP protocol result. @@ -891,6 +892,7 @@ async def call_tool_mcp( arguments (dict[str, Any]): Arguments to pass to the tool. timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None. progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None. + meta (dict[str, Any] | None, optional): Additional metadata to send with the tool call. Returns: mcp.types.CallToolResult: The complete response object from the protocol, @@ -904,11 +906,13 @@ async def call_tool_mcp( # Convert timeout to timedelta if needed if isinstance(timeout, int | float): timeout = datetime.timedelta(seconds=float(timeout)) + result = await self.session.call_tool( name=name, arguments=arguments, read_timeout_seconds=timeout, # ty: ignore[invalid-argument-type] progress_callback=progress_handler or self._progress_handler, + meta=meta, ) return result @@ -919,6 +923,7 @@ async def call_tool( timeout: datetime.timedelta | float | int | None = None, progress_handler: ProgressHandler | None = None, raise_on_error: bool = True, + meta: dict[str, Any] | None = None, ) -> CallToolResult: """Call a tool on the server. @@ -929,6 +934,8 @@ async def call_tool( arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None. timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None. progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None. + raise_on_error (bool, optional): Whether to raise a ToolError if the tool call results in an error. Defaults to True. + meta (dict[str, Any] | None, optional): Additional metadata to send with the tool call. Returns: CallToolResult: @@ -948,6 +955,7 @@ async def call_tool( arguments=arguments or {}, timeout=timeout, progress_handler=progress_handler, + meta=meta, ) data = None if result.isError and raise_on_error: diff --git a/tests/client/test_client.py b/tests/client/test_client.py index 8097b2965..9dc4bfc90 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -1,6 +1,6 @@ import asyncio import sys -from typing import cast +from typing import Any, cast from unittest.mock import AsyncMock import mcp @@ -148,6 +148,46 @@ async def test_call_tool_mcp(fastmcp_server): assert "Hello, World!" in content_str +async def test_call_tool_with_meta(): + """Test that meta parameter is properly passed from client to server.""" + server = FastMCP("MetaTestServer") + + # Create a tool that accesses the meta from the request context + @server.tool + def check_meta() -> dict[str, Any]: + """A tool that returns the meta from the request context.""" + from fastmcp.server.dependencies import get_context + + context = get_context() + meta = context.request_context.meta + + # Return the meta data as a dict + if meta is not None: + return { + "has_meta": True, + "user_id": getattr(meta, "user_id", None), + "trace_id": getattr(meta, "trace_id", None), + } + return {"has_meta": False} + + client = Client(transport=FastMCPTransport(server)) + + async with client: + # Test with meta parameter - verify the server receives it + test_meta = {"user_id": "test-123", "trace_id": "abc-def"} + result = await client.call_tool("check_meta", {}, meta=test_meta) + + assert result.data["has_meta"] is True + assert result.data["user_id"] == "test-123" + assert result.data["trace_id"] == "abc-def" + + # Test without meta parameter - verify fields are not present + result_no_meta = await client.call_tool("check_meta", {}) + # When meta is not provided, custom fields should not be present + assert result_no_meta.data.get("user_id") is None + assert result_no_meta.data.get("trace_id") is None + + async def test_list_resources(fastmcp_server): """Test listing resources with InMemoryClient.""" client = Client(transport=FastMCPTransport(fastmcp_server)) diff --git a/tests/server/test_context.py b/tests/server/test_context.py index 2a2e38e74..d87a30666 100644 --- a/tests/server/test_context.py +++ b/tests/server/test_context.py @@ -178,3 +178,59 @@ async def test_context_state_inheritance(self): assert context1.get_state("key1") == "key1-context1" assert context1.get_state("key-context3-only") is None + + +class TestContextMeta: + """Test suite for Context meta functionality.""" + + def test_request_context_meta_access(self, context): + """Test that meta can be accessed from request context.""" + from mcp.server.lowlevel.server import request_ctx + from mcp.shared.context import RequestContext + + # Create a mock meta object with attributes + class MockMeta: + def __init__(self): + self.user_id = "user-123" + self.trace_id = "trace-456" + self.custom_field = "custom-value" + + mock_meta = MockMeta() + + token = request_ctx.set( + RequestContext( # type: ignore[arg-type] + request_id=0, + meta=mock_meta, # type: ignore[arg-type] + session=MagicMock(wraps={}), + lifespan_context=MagicMock(), + ) + ) + + # Access meta through context + retrieved_meta = context.request_context.meta + assert retrieved_meta is not None + assert retrieved_meta.user_id == "user-123" + assert retrieved_meta.trace_id == "trace-456" + assert retrieved_meta.custom_field == "custom-value" + + request_ctx.reset(token) + + def test_request_context_meta_none(self, context): + """Test that context handles None meta gracefully.""" + from mcp.server.lowlevel.server import request_ctx + from mcp.shared.context import RequestContext + + token = request_ctx.set( + RequestContext( # type: ignore[arg-type] + request_id=0, + meta=None, + session=MagicMock(wraps={}), + lifespan_context=MagicMock(), + ) + ) + + # Access meta through context + retrieved_meta = context.request_context.meta + assert retrieved_meta is None + + request_ctx.reset(token)