Skip to content

Commit 46937d2

Browse files
fix: forward meta to MCP task-augmented tool calls (#2081)
Co-authored-by: agent-of-mkmeral <agent-of-mkmeral@users.noreply.github.com>
1 parent 65b06d9 commit 46937d2

File tree

2 files changed

+80
-1
lines changed

2 files changed

+80
-1
lines changed

src/strands/tools/mcp/mcp_client.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,9 @@ def _create_call_tool_coroutine(
592592
async def _call_as_task() -> MCPCallToolResult:
593593
# When task-augmented execution is used, use the read_timeout_seconds parameter
594594
# (which is a timedelta) for the polling timeout.
595-
return await self._call_tool_as_task_and_poll_async(name, arguments, poll_timeout=read_timeout_seconds)
595+
return await self._call_tool_as_task_and_poll_async(
596+
name, arguments, poll_timeout=read_timeout_seconds, meta=meta
597+
)
596598

597599
return _call_as_task()
598600
else:
@@ -1100,6 +1102,7 @@ async def _call_tool_as_task_and_poll_async(
11001102
arguments: dict[str, Any] | None = None,
11011103
ttl: timedelta | None = None,
11021104
poll_timeout: timedelta | None = None,
1105+
meta: dict[str, Any] | None = None,
11031106
) -> MCPCallToolResult:
11041107
"""Call a tool using task-augmented execution and poll until completion.
11051108
@@ -1113,6 +1116,7 @@ async def _call_tool_as_task_and_poll_async(
11131116
arguments: Optional arguments to pass to the tool.
11141117
ttl: Task time-to-live. Uses configured value if not specified.
11151118
poll_timeout: Timeout for polling. Uses configured value if not specified.
1119+
meta: Optional metadata to pass to the tool call per MCP spec (_meta).
11161120
11171121
Returns:
11181122
MCPCallToolResult: The final tool result after task completion.
@@ -1133,6 +1137,7 @@ async def _call_tool_as_task_and_poll_async(
11331137
name=name,
11341138
arguments=arguments,
11351139
ttl=ttl_ms,
1140+
meta=meta,
11361141
)
11371142
task_id = create_result.task.taskId
11381143
self._log_debug_with_thread("tool=<%s>, task_id=<%s> | task created", name, task_id)

tests/strands/tools/mcp/test_mcp_client_tasks.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,77 @@ async def poll(task_id):
214214
result = await client.call_tool_async(tool_use_id="t", name="success_tool", arguments={})
215215
assert result["status"] == "success"
216216
assert "Done" in result["content"][0].get("text", "")
217+
218+
219+
class TestTaskMetaForwarding:
220+
"""Tests for meta parameter forwarding in task-augmented execution."""
221+
222+
def _setup_task_tool_with_meta(self, mock_session, tool_name: str) -> MagicMock:
223+
"""Helper to set up a mock task-enabled tool and return the experimental mock."""
224+
mock_session.get_server_capabilities = MagicMock(return_value=create_server_capabilities(True))
225+
mock_tool = MCPTool(
226+
name=tool_name,
227+
description="A test tool",
228+
inputSchema={"type": "object"},
229+
execution=ToolExecution(taskSupport="optional"),
230+
)
231+
mock_session.list_tools = AsyncMock(return_value=ListToolsResult(tools=[mock_tool], nextCursor=None))
232+
mock_create_result = MagicMock()
233+
mock_create_result.task.taskId = "test-task-id"
234+
mock_session.experimental = MagicMock()
235+
mock_session.experimental.call_tool_as_task = AsyncMock(return_value=mock_create_result)
236+
237+
async def successful_poll(task_id):
238+
yield MagicMock(status="completed", statusMessage=None)
239+
240+
mock_session.experimental.poll_task = successful_poll
241+
mock_session.experimental.get_task_result = AsyncMock(
242+
return_value=MCPCallToolResult(content=[MCPTextContent(type="text", text="Done")], isError=False)
243+
)
244+
245+
return mock_session.experimental
246+
247+
def test_call_tool_sync_forwards_meta_to_task(self, mock_transport, mock_session):
248+
"""Test that call_tool_sync forwards meta to call_tool_as_task."""
249+
experimental = self._setup_task_tool_with_meta(mock_session, "meta_tool")
250+
meta = {"com.example/request_id": "abc-123"}
251+
252+
with MCPClient(mock_transport["transport_callable"], tasks_config=TasksConfig()) as client:
253+
client.list_tools_sync()
254+
client.call_tool_sync(
255+
tool_use_id="test-id", name="meta_tool", arguments={"param": "value"}, meta=meta
256+
)
257+
258+
experimental.call_tool_as_task.assert_called_once()
259+
call_kwargs = experimental.call_tool_as_task.call_args
260+
assert call_kwargs.kwargs.get("meta") == meta
261+
262+
@pytest.mark.asyncio
263+
async def test_call_tool_async_forwards_meta_to_task(self, mock_transport, mock_session):
264+
"""Test that call_tool_async forwards meta to call_tool_as_task."""
265+
experimental = self._setup_task_tool_with_meta(mock_session, "meta_tool")
266+
meta = {"com.example/trace_id": "xyz-456"}
267+
268+
with MCPClient(mock_transport["transport_callable"], tasks_config=TasksConfig()) as client:
269+
client.list_tools_sync()
270+
await client.call_tool_async(
271+
tool_use_id="test-id", name="meta_tool", arguments={"param": "value"}, meta=meta
272+
)
273+
274+
experimental.call_tool_as_task.assert_called_once()
275+
call_kwargs = experimental.call_tool_as_task.call_args
276+
assert call_kwargs.kwargs.get("meta") == meta
277+
278+
def test_call_tool_sync_forwards_none_meta_to_task(self, mock_transport, mock_session):
279+
"""Test that call_tool_sync forwards None meta to call_tool_as_task when not provided."""
280+
experimental = self._setup_task_tool_with_meta(mock_session, "no_meta_tool")
281+
282+
with MCPClient(mock_transport["transport_callable"], tasks_config=TasksConfig()) as client:
283+
client.list_tools_sync()
284+
client.call_tool_sync(
285+
tool_use_id="test-id", name="no_meta_tool", arguments={"param": "value"}
286+
)
287+
288+
experimental.call_tool_as_task.assert_called_once()
289+
call_kwargs = experimental.call_tool_as_task.call_args
290+
assert call_kwargs.kwargs.get("meta") is None

0 commit comments

Comments
 (0)