Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/mcp/client/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,17 +271,22 @@ async def call_tool(
self,
name: str,
arguments: dict[str, Any] | None = None,
meta: dict[str, Any] | None = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to the end for backwards-compatibility — we should have probably added a , *, before, but I'll leave that up to a maintainer to decide if we should start doing that or not (it would also be a breaking change if we started doing that now).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, except the second thing would wait on that for the same

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(
types.CallToolRequest(
params=types.CallToolRequestParams(
name=name,
arguments=arguments,
**({"_meta": request_meta} if request_meta else {}),
),
)
),
Expand Down
12 changes: 11 additions & 1 deletion src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
Expand Down Expand Up @@ -1173,6 +1174,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]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add unit tests for this

"""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."""
Expand Down
73 changes: 73 additions & 0 deletions tests/client/test_session.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import inspect
from typing import Any

import anyio
Expand Down Expand Up @@ -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"
Comment on lines +503 to +511
Copy link
Contributor

@maxisbey maxisbey Oct 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this test, it shouldn't be needed :)

This is a fragile test which relies entirely on implementation details rather than functionality.

This should be tested via other unit tests, such as one that passes a "meta" parameter. If "meta" didn't exist then that test would fail. Similarly, there should be a unit tests which test the default case instead of doing inspection on the signature.



def test_call_tool_request_params_construction():
"""Test that CallToolRequestParams can be constructed with metadata."""
from mcp.types import CallToolRequestParams, RequestParams
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move imports to top of file


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move imports to top of file


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"]
Loading