feat: add _meta field to CallToolRequestParams per MCP protocol

Support the protocol-reserved `_meta` parameter for tool invocations, enabling metadata (e.g., progressToken, identityToken) transmission as specified in MCP spec.

See: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/draft/basic/index.mdx#general-fields
This commit is contained in:
zino 2025-11-19 17:21:33 +08:00
parent 4819e324da
commit 72f635e4e3
5 changed files with 11 additions and 14 deletions

View File

@ -174,7 +174,7 @@ class MCPClientWithAuthRetry(MCPClient):
""" """
return self._execute_with_retry(super().list_tools) return self._execute_with_retry(super().list_tools)
def invoke_tool(self, tool_name: str, tool_args: dict[str, Any]) -> CallToolResult: def invoke_tool(self, tool_name: str, tool_args: dict[str, Any], _meta: dict[str, Any]) -> CallToolResult:
""" """
Invoke a tool on the MCP server with auth retry. Invoke a tool on the MCP server with auth retry.
@ -188,4 +188,4 @@ class MCPClientWithAuthRetry(MCPClient):
Raises: Raises:
MCPAuthError: If authentication fails after retries MCPAuthError: If authentication fails after retries
""" """
return self._execute_with_retry(super().invoke_tool, tool_name, tool_args) return self._execute_with_retry(super().invoke_tool, tool_name, tool_args, _meta)

View File

@ -96,11 +96,11 @@ class MCPClient:
response = self._session.list_tools() response = self._session.list_tools()
return response.tools return response.tools
def invoke_tool(self, tool_name: str, tool_args: dict[str, Any]) -> CallToolResult: def invoke_tool(self, tool_name: str, tool_args: dict[str, Any], _meta: dict[str, Any]) -> CallToolResult:
"""Call a tool""" """Call a tool"""
if not self._session: if not self._session:
raise ValueError("Session not initialized.") raise ValueError("Session not initialized.")
return self._session.call_tool(tool_name, tool_args) return self._session.call_tool(tool_name, tool_args, _meta=_meta)
def cleanup(self): def cleanup(self):
"""Clean up resources""" """Clean up resources"""

View File

@ -248,6 +248,7 @@ class ClientSession(
self, self,
name: str, name: str,
arguments: dict[str, Any] | None = None, arguments: dict[str, Any] | None = None,
_meta: dict[str, Any] | None = None,
read_timeout_seconds: timedelta | None = None, read_timeout_seconds: timedelta | None = None,
) -> types.CallToolResult: ) -> types.CallToolResult:
"""Send a tools/call request.""" """Send a tools/call request."""
@ -256,7 +257,7 @@ class ClientSession(
types.ClientRequest( types.ClientRequest(
types.CallToolRequest( types.CallToolRequest(
method="tools/call", method="tools/call",
params=types.CallToolRequestParams(name=name, arguments=arguments), params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=_meta),
) )
), ),
types.CallToolResult, types.CallToolResult,

View File

@ -855,6 +855,7 @@ class CallToolRequestParams(RequestParams):
name: str name: str
arguments: dict[str, Any] | None = None arguments: dict[str, Any] | None = None
_meta: dict[str, Any] | None = None
model_config = ConfigDict(extra="allow") model_config = ConfigDict(extra="allow")

View File

@ -49,13 +49,7 @@ class MCPTool(Tool):
message_id: str | None = None, message_id: str | None = None,
inputs: dict[str, Any] | None = None, inputs: dict[str, Any] | None = None,
) -> Generator[ToolInvokeMessage, None, None]: ) -> Generator[ToolInvokeMessage, None, None]:
# If inputs is provided, flatten its key/value pairs into the tool_parameters. result = self.invoke_remote_mcp_tool(tool_parameters, _meta=inputs)
# This is a conservative merge: only add a key from inputs if it does not already exist in tool_parameters.
if inputs:
for key, value in inputs.items():
if key not in tool_parameters:
tool_parameters[key] = value
result = self.invoke_remote_mcp_tool(tool_parameters)
# handle dify tool output # handle dify tool output
for content in result.content: for content in result.content:
if isinstance(content, TextContent): if isinstance(content, TextContent):
@ -139,7 +133,7 @@ class MCPTool(Tool):
if value is not None and not (isinstance(value, str) and value.strip() == "") if value is not None and not (isinstance(value, str) and value.strip() == "")
} }
def invoke_remote_mcp_tool(self, tool_parameters: dict[str, Any]) -> CallToolResult: def invoke_remote_mcp_tool(self, tool_parameters: dict[str, Any], _meta: dict[str, Any]) -> CallToolResult:
headers = self.headers.copy() if self.headers else {} headers = self.headers.copy() if self.headers else {}
tool_parameters = self._handle_none_parameter(tool_parameters) tool_parameters = self._handle_none_parameter(tool_parameters)
@ -174,7 +168,8 @@ class MCPTool(Tool):
sse_read_timeout=self.sse_read_timeout, sse_read_timeout=self.sse_read_timeout,
provider_entity=provider_entity, provider_entity=provider_entity,
) as mcp_client: ) as mcp_client:
return mcp_client.invoke_tool(tool_name=self.entity.identity.name, tool_args=tool_parameters) return mcp_client.invoke_tool(tool_name=self.entity.identity.name, tool_args=tool_parameters,
_meta=_meta)
except MCPConnectionError as e: except MCPConnectionError as e:
raise ToolInvokeError(f"Failed to connect to MCP server: {e}") from e raise ToolInvokeError(f"Failed to connect to MCP server: {e}") from e
except Exception as e: except Exception as e: