Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 @@ -273,15 +273,20 @@ 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(
types.CallToolRequest(
params=types.CallToolRequestParams(
name=name,
arguments=arguments,
_meta=request_meta,
),
)
),
Expand Down
20 changes: 19 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,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]:
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).

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."""
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