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


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