mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: forward MCP tool call metadata (#5815)
* Python: forward MCP tool call metadata * fix: preserve MCP tool meta after prompt reload
This commit is contained in:
committed by
GitHub
Unverified
parent
67f3db6280
commit
410268b624
@@ -261,6 +261,7 @@ class MCPTool:
|
||||
self.request_timeout = request_timeout
|
||||
self.client = client
|
||||
self._functions: list[FunctionTool] = []
|
||||
self._tool_call_meta_by_name: dict[str, dict[str, Any]] = {}
|
||||
self.is_connected: bool = False
|
||||
self._tools_loaded: bool = False
|
||||
self._prompts_loaded: bool = False
|
||||
@@ -1026,6 +1027,7 @@ class MCPTool:
|
||||
|
||||
# Track existing function names to prevent duplicates
|
||||
existing_names = {func.name for func in self._functions}
|
||||
self._tool_call_meta_by_name.clear()
|
||||
|
||||
params: types.PaginatedRequestParams | None = None
|
||||
while True:
|
||||
@@ -1035,6 +1037,9 @@ class MCPTool:
|
||||
tool_list = await self.session.list_tools(params=params) # type: ignore[union-attr]
|
||||
|
||||
for tool in tool_list.tools:
|
||||
if tool.meta is not None:
|
||||
self._tool_call_meta_by_name[tool.name] = dict(tool.meta)
|
||||
|
||||
normalized_name = _normalize_mcp_name(tool.name)
|
||||
local_name = _build_prefixed_mcp_name(normalized_name, self.tool_name_prefix)
|
||||
|
||||
@@ -1185,14 +1190,15 @@ class MCPTool:
|
||||
}
|
||||
}
|
||||
|
||||
# Inject OpenTelemetry trace context into MCP _meta for distributed tracing.
|
||||
otel_meta = _inject_otel_into_mcp_meta()
|
||||
# Some MCP proxies require their tools/list metadata to be echoed on tools/call.
|
||||
tool_meta = self._tool_call_meta_by_name.get(tool_name)
|
||||
meta = _inject_otel_into_mcp_meta(dict(tool_meta) if tool_meta is not None else None)
|
||||
|
||||
parser = self.parse_tool_results or self._parse_tool_result_from_mcp
|
||||
# Try the operation, reconnecting once if the connection is closed
|
||||
for attempt in range(2):
|
||||
try:
|
||||
result = await self.session.call_tool(tool_name, arguments=filtered_kwargs, meta=otel_meta) # type: ignore
|
||||
result = await self.session.call_tool(tool_name, arguments=filtered_kwargs, meta=meta) # type: ignore
|
||||
if result.isError:
|
||||
parsed = parser(result)
|
||||
text = (
|
||||
|
||||
@@ -4194,6 +4194,57 @@ async def test_mcp_tool_call_tool_otel_meta(use_span, expect_traceparent, span_e
|
||||
assert meta is None
|
||||
|
||||
|
||||
async def test_mcp_tool_call_tool_forwards_tool_list_meta():
|
||||
"""call_tool echoes per-tool metadata returned by tools/list."""
|
||||
from opentelemetry import trace
|
||||
|
||||
tool_meta = {
|
||||
"tool_configuration": {
|
||||
"name": "WorkIQSharePoint.readSmallBinaryFile",
|
||||
"type": "foundry_toolbox",
|
||||
}
|
||||
}
|
||||
|
||||
class TestServer(MCPTool):
|
||||
async def connect(self):
|
||||
self.session = Mock(spec=ClientSession)
|
||||
self.session.list_tools = AsyncMock(
|
||||
return_value=types.ListToolsResult(
|
||||
tools=[
|
||||
types.Tool(
|
||||
name="WorkIQSharePoint.readSmallBinaryFile",
|
||||
description="Read a binary file",
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {"fileId": {"type": "string"}},
|
||||
"required": ["fileId"],
|
||||
},
|
||||
_meta=tool_meta,
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
self.session.call_tool = AsyncMock(
|
||||
return_value=types.CallToolResult(content=[types.TextContent(type="text", text="result")])
|
||||
)
|
||||
self.session.list_prompts = AsyncMock(
|
||||
return_value=types.ListPromptsResult(prompts=[])
|
||||
)
|
||||
|
||||
def get_mcp_client(self) -> _AsyncGeneratorContextManager[Any, None]:
|
||||
return None
|
||||
|
||||
server = TestServer(name="test_server")
|
||||
async with server:
|
||||
await server.load_tools()
|
||||
await server.load_prompts()
|
||||
|
||||
with trace.use_span(trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT)):
|
||||
await server.call_tool("WorkIQSharePoint.readSmallBinaryFile", fileId="file-1")
|
||||
|
||||
assert server.session.call_tool.call_args.kwargs["meta"] == tool_meta
|
||||
|
||||
|
||||
async def test_mcp_streamable_http_tool_hook_not_duplicated_on_repeated_get_mcp_client():
|
||||
"""Test that calling get_mcp_client multiple times does not accumulate duplicate hooks."""
|
||||
tool = MCPStreamableHTTPTool(
|
||||
|
||||
Reference in New Issue
Block a user