mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 22:14:18 +02:00
Add meta to call tool (#2206)
* feat: Add optional meta parameter to Client tool call methods * fix: Add support for mcp<1.19 * chore: cleaner solution * Refactor call_tool to directly accept meta parameter and add tests for meta functionality --------- Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
This commit is contained in:
parent
7d0c3700ed
commit
7e6610b7b6
3 changed files with 105 additions and 1 deletions
|
|
@ -880,6 +880,7 @@ class Client(Generic[ClientTransportT]):
|
|||
arguments: dict[str, Any],
|
||||
progress_handler: ProgressHandler | None = None,
|
||||
timeout: datetime.timedelta | float | int | None = None,
|
||||
meta: dict[str, Any] | None = None,
|
||||
) -> mcp.types.CallToolResult:
|
||||
"""Send a tools/call request and return the complete MCP protocol result.
|
||||
|
||||
|
|
@ -891,6 +892,7 @@ class Client(Generic[ClientTransportT]):
|
|||
arguments (dict[str, Any]): Arguments to pass to the tool.
|
||||
timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
|
||||
progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
|
||||
meta (dict[str, Any] | None, optional): Additional metadata to send with the tool call.
|
||||
|
||||
Returns:
|
||||
mcp.types.CallToolResult: The complete response object from the protocol,
|
||||
|
|
@ -904,11 +906,13 @@ class Client(Generic[ClientTransportT]):
|
|||
# Convert timeout to timedelta if needed
|
||||
if isinstance(timeout, int | float):
|
||||
timeout = datetime.timedelta(seconds=float(timeout))
|
||||
|
||||
result = await self.session.call_tool(
|
||||
name=name,
|
||||
arguments=arguments,
|
||||
read_timeout_seconds=timeout, # ty: ignore[invalid-argument-type]
|
||||
progress_callback=progress_handler or self._progress_handler,
|
||||
meta=meta,
|
||||
)
|
||||
return result
|
||||
|
||||
|
|
@ -919,6 +923,7 @@ class Client(Generic[ClientTransportT]):
|
|||
timeout: datetime.timedelta | float | int | None = None,
|
||||
progress_handler: ProgressHandler | None = None,
|
||||
raise_on_error: bool = True,
|
||||
meta: dict[str, Any] | None = None,
|
||||
) -> CallToolResult:
|
||||
"""Call a tool on the server.
|
||||
|
||||
|
|
@ -929,6 +934,8 @@ class Client(Generic[ClientTransportT]):
|
|||
arguments (dict[str, Any] | None, optional): Arguments to pass to the tool. Defaults to None.
|
||||
timeout (datetime.timedelta | float | int | None, optional): The timeout for the tool call. Defaults to None.
|
||||
progress_handler (ProgressHandler | None, optional): The progress handler to use for the tool call. Defaults to None.
|
||||
raise_on_error (bool, optional): Whether to raise a ToolError if the tool call results in an error. Defaults to True.
|
||||
meta (dict[str, Any] | None, optional): Additional metadata to send with the tool call.
|
||||
|
||||
Returns:
|
||||
CallToolResult:
|
||||
|
|
@ -948,6 +955,7 @@ class Client(Generic[ClientTransportT]):
|
|||
arguments=arguments or {},
|
||||
timeout=timeout,
|
||||
progress_handler=progress_handler,
|
||||
meta=meta,
|
||||
)
|
||||
data = None
|
||||
if result.isError and raise_on_error:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import asyncio
|
||||
import sys
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import mcp
|
||||
|
|
@ -148,6 +148,46 @@ async def test_call_tool_mcp(fastmcp_server):
|
|||
assert "Hello, World!" in content_str
|
||||
|
||||
|
||||
async def test_call_tool_with_meta():
|
||||
"""Test that meta parameter is properly passed from client to server."""
|
||||
server = FastMCP("MetaTestServer")
|
||||
|
||||
# Create a tool that accesses the meta from the request context
|
||||
@server.tool
|
||||
def check_meta() -> dict[str, Any]:
|
||||
"""A tool that returns the meta from the request context."""
|
||||
from fastmcp.server.dependencies import get_context
|
||||
|
||||
context = get_context()
|
||||
meta = context.request_context.meta
|
||||
|
||||
# Return the meta data as a dict
|
||||
if meta is not None:
|
||||
return {
|
||||
"has_meta": True,
|
||||
"user_id": getattr(meta, "user_id", None),
|
||||
"trace_id": getattr(meta, "trace_id", None),
|
||||
}
|
||||
return {"has_meta": False}
|
||||
|
||||
client = Client(transport=FastMCPTransport(server))
|
||||
|
||||
async with client:
|
||||
# Test with meta parameter - verify the server receives it
|
||||
test_meta = {"user_id": "test-123", "trace_id": "abc-def"}
|
||||
result = await client.call_tool("check_meta", {}, meta=test_meta)
|
||||
|
||||
assert result.data["has_meta"] is True
|
||||
assert result.data["user_id"] == "test-123"
|
||||
assert result.data["trace_id"] == "abc-def"
|
||||
|
||||
# Test without meta parameter - verify fields are not present
|
||||
result_no_meta = await client.call_tool("check_meta", {})
|
||||
# When meta is not provided, custom fields should not be present
|
||||
assert result_no_meta.data.get("user_id") is None
|
||||
assert result_no_meta.data.get("trace_id") is None
|
||||
|
||||
|
||||
async def test_list_resources(fastmcp_server):
|
||||
"""Test listing resources with InMemoryClient."""
|
||||
client = Client(transport=FastMCPTransport(fastmcp_server))
|
||||
|
|
|
|||
|
|
@ -178,3 +178,59 @@ class TestContextState:
|
|||
|
||||
assert context1.get_state("key1") == "key1-context1"
|
||||
assert context1.get_state("key-context3-only") is None
|
||||
|
||||
|
||||
class TestContextMeta:
|
||||
"""Test suite for Context meta functionality."""
|
||||
|
||||
def test_request_context_meta_access(self, context):
|
||||
"""Test that meta can be accessed from request context."""
|
||||
from mcp.server.lowlevel.server import request_ctx
|
||||
from mcp.shared.context import RequestContext
|
||||
|
||||
# Create a mock meta object with attributes
|
||||
class MockMeta:
|
||||
def __init__(self):
|
||||
self.user_id = "user-123"
|
||||
self.trace_id = "trace-456"
|
||||
self.custom_field = "custom-value"
|
||||
|
||||
mock_meta = MockMeta()
|
||||
|
||||
token = request_ctx.set(
|
||||
RequestContext( # type: ignore[arg-type]
|
||||
request_id=0,
|
||||
meta=mock_meta, # type: ignore[arg-type]
|
||||
session=MagicMock(wraps={}),
|
||||
lifespan_context=MagicMock(),
|
||||
)
|
||||
)
|
||||
|
||||
# Access meta through context
|
||||
retrieved_meta = context.request_context.meta
|
||||
assert retrieved_meta is not None
|
||||
assert retrieved_meta.user_id == "user-123"
|
||||
assert retrieved_meta.trace_id == "trace-456"
|
||||
assert retrieved_meta.custom_field == "custom-value"
|
||||
|
||||
request_ctx.reset(token)
|
||||
|
||||
def test_request_context_meta_none(self, context):
|
||||
"""Test that context handles None meta gracefully."""
|
||||
from mcp.server.lowlevel.server import request_ctx
|
||||
from mcp.shared.context import RequestContext
|
||||
|
||||
token = request_ctx.set(
|
||||
RequestContext( # type: ignore[arg-type]
|
||||
request_id=0,
|
||||
meta=None,
|
||||
session=MagicMock(wraps={}),
|
||||
lifespan_context=MagicMock(),
|
||||
)
|
||||
)
|
||||
|
||||
# Access meta through context
|
||||
retrieved_meta = context.request_context.meta
|
||||
assert retrieved_meta is None
|
||||
|
||||
request_ctx.reset(token)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue