From 6fbbaa64e51322e1c7720046ae111633df9572e9 Mon Sep 17 00:00:00 2001 From: iupadhyay Date: Thu, 18 Sep 2025 01:27:51 +0530 Subject: [PATCH 1/4] Adds meta parameter and method for allowing information to be passed without intercepted by LLM --- src/mcp/client/session.py | 5 +++++ src/mcp/server/fastmcp/server.py | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index bcf80d62a..6af7f6e2e 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -271,10 +271,14 @@ async def call_tool( self, name: str, arguments: dict[str, Any] | None = None, + meta: dict[str, Any] | None = None, read_timeout_seconds: timedelta | None = None, progress_callback: ProgressFnT | 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} if request_meta else {}), ), ) ), diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index adc386817..084335025 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -1036,9 +1036,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) ``` @@ -1172,6 +1173,15 @@ 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).""" + if not self.request_context.meta: + return {} + + meta_dict = self.request_context.meta.model_dump(exclude={'progressToken'}) + return meta_dict + @property def session(self): """Access to the underlying session for advanced usage.""" From 5c23510b04c43fc1e4c63b0a259dbb03da363042 Mon Sep 17 00:00:00 2001 From: iupadhyay Date: Thu, 18 Sep 2025 01:48:54 +0530 Subject: [PATCH 2/4] Add unit test case for the change --- tests/client/test_session.py | 85 ++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/client/test_session.py b/tests/client/test_session.py index 53b60fce6..a6fd32cbb 100644 --- a/tests/client/test_session.py +++ b/tests/client/test_session.py @@ -24,6 +24,8 @@ ServerCapabilities, ServerResult, ) +import inspect +from mcp.client.session import ClientSession @pytest.mark.anyio @@ -497,3 +499,86 @@ 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"] From 0a471e56795059f4e1c10e06ff5c8be70e36c283 Mon Sep 17 00:00:00 2001 From: iupadhyay Date: Thu, 18 Sep 2025 01:57:01 +0530 Subject: [PATCH 3/4] type checks and redundant imports --- src/mcp/client/session.py | 2 +- src/mcp/server/fastmcp/server.py | 2 +- tests/client/test_session.py | 30 +++++++++--------------------- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index 6af7f6e2e..49153ed4e 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -286,7 +286,7 @@ async def call_tool( params=types.CallToolRequestParams( name=name, arguments=arguments, - **({'_meta': request_meta} if request_meta else {}), + **({"_meta": request_meta} if request_meta else {}), ), ) ), diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 084335025..27a6969bc 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -1179,7 +1179,7 @@ def request_meta(self) -> dict[str, Any]: if not self.request_context.meta: return {} - meta_dict = self.request_context.meta.model_dump(exclude={'progressToken'}) + meta_dict = self.request_context.meta.model_dump(exclude={"progressToken"}) return meta_dict @property diff --git a/tests/client/test_session.py b/tests/client/test_session.py index a6fd32cbb..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 @@ -24,8 +25,6 @@ ServerCapabilities, ServerResult, ) -import inspect -from mcp.client.session import ClientSession @pytest.mark.anyio @@ -500,14 +499,15 @@ async def mock_server(): 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" + assert "meta" in signature.parameters, "call_tool method should have 'meta' parameter" - meta_param = signature.parameters['meta'] + meta_param = signature.parameters["meta"] assert meta_param.default is None, "meta parameter should default to None" @@ -515,10 +515,7 @@ 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"} - ) + 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 @@ -527,14 +524,14 @@ def test_call_tool_request_params_construction(): "progressToken": None, "user_id": "test_user", "session_id": "test_session", - "custom_field": "custom_value" + "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 + **{"_meta": test_meta}, # Using alias ) assert params_with_meta.name == "test_tool" @@ -551,21 +548,12 @@ 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"] - } + 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} - ) + params=CallToolRequestParams(name="secure_tool", arguments={"query": "sensitive_data"}, **{"_meta": test_meta}), ) serialized = request.model_dump(by_alias=True) From 2625a056a4843cfdcddd30e2368617582ace7875 Mon Sep 17 00:00:00 2001 From: iupadhyay Date: Thu, 18 Sep 2025 02:31:11 +0530 Subject: [PATCH 4/4] addressing review comments --- src/mcp/client/session.py | 4 ++-- src/mcp/server/fastmcp/server.py | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index 49153ed4e..a663808ae 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -271,9 +271,9 @@ async def call_tool( self, name: str, arguments: dict[str, Any] | None = None, - meta: 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 @@ -286,7 +286,7 @@ async def call_tool( params=types.CallToolRequestParams( name=name, arguments=arguments, - **({"_meta": request_meta} if request_meta else {}), + _meta=request_meta, ), ) ), diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 27a6969bc..c36c86f00 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -1175,12 +1175,20 @@ def request_id(self) -> str: @property def request_meta(self) -> dict[str, Any]: - """Get the request metadata (hidden data passed from client).""" + """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 {} - meta_dict = self.request_context.meta.model_dump(exclude={"progressToken"}) - return meta_dict + # Return all metadata fields, including progressToken + return self.request_context.meta.model_dump() @property def session(self):