diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index bcf80d62a..a663808ae 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -273,8 +273,12 @@ async def call_tool( arguments: dict[str, Any] | None = None, read_timeout_seconds: timedelta | None = None, progress_callback: ProgressFnT | None = None, + meta: dict[str, Any] | None = None, ) -> types.CallToolResult: """Send a tools/call request with optional progress callback support.""" + request_meta = None + if meta: + request_meta = types.RequestParams.Meta(**meta) result = await self.send_request( types.ClientRequest( @@ -282,6 +286,7 @@ async def call_tool( params=types.CallToolRequestParams( name=name, arguments=arguments, + _meta=request_meta, ), ) ), diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index d86fa85e3..c29b3ae0a 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -1037,9 +1037,10 @@ def my_tool(x: int, ctx: Context) -> str: # Access resources data = ctx.read_resource("resource://data") - # Get request info + # Get request info and metadata request_id = ctx.request_id client_id = ctx.client_id + user_meta = ctx.request_meta return str(x) ``` @@ -1173,6 +1174,23 @@ def request_id(self) -> str: """Get the unique ID for this request.""" return str(self.request_context.request_id) + @property + def request_meta(self) -> dict[str, Any]: + """Get the request metadata (hidden data passed from client). + + This contains metadata that was sent with the request but is not visible + to the LLM. Includes all metadata fields including progressToken. + Useful for authentication tokens, user context, session data, etc. + + Returns: + Dictionary containing the complete request metadata, or empty dict if none provided. + """ + if not self.request_context.meta: + return {} + + # Return all metadata fields, including progressToken + return self.request_context.meta.model_dump() + @property def session(self): """Access to the underlying session for advanced usage.""" diff --git a/tests/client/test_session.py b/tests/client/test_session.py index 53b60fce6..b651ff2a2 100644 --- a/tests/client/test_session.py +++ b/tests/client/test_session.py @@ -1,3 +1,4 @@ +import inspect from typing import Any import anyio @@ -497,3 +498,75 @@ async def mock_server(): assert received_capabilities.roots is not None # Custom list_roots callback provided assert isinstance(received_capabilities.roots, types.RootsCapability) assert received_capabilities.roots.listChanged is True # Should be True for custom callback + + +def test_call_tool_method_signature(): + """Test that call_tool method accepts meta parameter in its signature.""" + + signature = inspect.signature(ClientSession.call_tool) + + assert "meta" in signature.parameters, "call_tool method should have 'meta' parameter" + + meta_param = signature.parameters["meta"] + assert meta_param.default is None, "meta parameter should default to None" + + +def test_call_tool_request_params_construction(): + """Test that CallToolRequestParams can be constructed with metadata.""" + from mcp.types import CallToolRequestParams, RequestParams + + params_no_meta = CallToolRequestParams(name="test_tool", arguments={"param": "value"}) + assert params_no_meta.name == "test_tool" + assert params_no_meta.arguments == {"param": "value"} + assert params_no_meta.meta is None + + meta_data = { + "progressToken": None, + "user_id": "test_user", + "session_id": "test_session", + "custom_field": "custom_value", + } + test_meta = RequestParams.Meta.model_validate(meta_data) + + params_with_meta = CallToolRequestParams( + name="test_tool", + arguments={"param": "value"}, + **{"_meta": test_meta}, # Using alias + ) + + assert params_with_meta.name == "test_tool" + assert params_with_meta.arguments == {"param": "value"} + assert params_with_meta.meta is not None + + dumped = params_with_meta.meta.model_dump() + assert dumped["user_id"] == "test_user" + assert dumped["session_id"] == "test_session" + assert dumped["custom_field"] == "custom_value" + + +def test_metadata_serialization(): + """Test that metadata is properly serialized with _meta alias.""" + from mcp.types import CallToolRequest, CallToolRequestParams, RequestParams + + meta_data = {"progressToken": None, "user_id": "alice", "api_key": "secret_123", "permissions": ["read", "write"]} + test_meta = RequestParams.Meta.model_validate(meta_data) + + request = CallToolRequest( + method="tools/call", + params=CallToolRequestParams(name="secure_tool", arguments={"query": "sensitive_data"}, **{"_meta": test_meta}), + ) + + serialized = request.model_dump(by_alias=True) + + assert serialized["method"] == "tools/call" + assert serialized["params"]["name"] == "secure_tool" + assert serialized["params"]["arguments"]["query"] == "sensitive_data" + + assert "_meta" in serialized["params"] + meta_data_serialized = serialized["params"]["_meta"] + assert meta_data_serialized["user_id"] == "alice" + assert meta_data_serialized["api_key"] == "secret_123" + assert meta_data_serialized["permissions"] == ["read", "write"] + assert meta_data["user_id"] == "alice" + assert meta_data["api_key"] == "secret_123" + assert meta_data["permissions"] == ["read", "write"]