Skip to content

Commit d66d391

Browse files
authored
feat: #2367 add MCP tool meta resolver support (#2375)
1 parent e0bde88 commit d66d391

10 files changed

Lines changed: 300 additions & 42 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ dependencies = [
1313
"typing-extensions>=4.12.2, <5",
1414
"requests>=2.0, <3",
1515
"types-requests>=2.0, <3",
16-
"mcp>=1.11.0, <2; python_version >= '3.10'",
16+
"mcp>=1.19.0, <2; python_version >= '3.10'",
1717
]
1818
classifiers = [
1919
"Typing :: Typed",

src/agents/mcp/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
pass
1414

1515
from .util import (
16+
MCPToolMetaContext,
17+
MCPToolMetaResolver,
1618
MCPUtil,
1719
ToolFilter,
1820
ToolFilterCallable,
@@ -31,6 +33,8 @@
3133
"MCPServerStreamableHttpParams",
3234
"MCPServerManager",
3335
"MCPUtil",
36+
"MCPToolMetaContext",
37+
"MCPToolMetaResolver",
3438
"ToolFilter",
3539
"ToolFilterCallable",
3640
"ToolFilterContext",

src/agents/mcp/server.py

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@
2828
from ..run_context import RunContextWrapper
2929
from ..tool import ToolErrorFunction
3030
from ..util._types import MaybeAwaitable
31-
from .util import HttpClientFactory, ToolFilter, ToolFilterContext, ToolFilterStatic
31+
from .util import (
32+
HttpClientFactory,
33+
MCPToolMetaResolver,
34+
ToolFilter,
35+
ToolFilterContext,
36+
ToolFilterStatic,
37+
)
3238

3339

3440
class RequireApprovalToolList(TypedDict, total=False):
@@ -68,6 +74,7 @@ def __init__(
6874
use_structured_content: bool = False,
6975
require_approval: RequireApprovalSetting = None,
7076
failure_error_function: ToolErrorFunction | None | _UnsetType = _UNSET,
77+
tool_meta_resolver: MCPToolMetaResolver | None = None,
7178
):
7279
"""
7380
Args:
@@ -83,12 +90,15 @@ def __init__(
8390
a model-visible error message. If explicitly set to None, tool errors will be
8491
raised instead of converted. If left unset, the agent-level configuration (or
8592
SDK default) will be used.
93+
tool_meta_resolver: Optional callable that produces MCP request metadata (`_meta`) for
94+
tool calls. It is invoked by the Agents SDK before calling `call_tool`.
8695
"""
8796
self.use_structured_content = use_structured_content
8897
self._needs_approval_policy = self._normalize_needs_approval(
8998
require_approval=require_approval
9099
)
91100
self._failure_error_function = failure_error_function
101+
self.tool_meta_resolver = tool_meta_resolver
92102

93103
@abc.abstractmethod
94104
async def connect(self):
@@ -121,7 +131,12 @@ async def list_tools(
121131
pass
122132

123133
@abc.abstractmethod
124-
async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult:
134+
async def call_tool(
135+
self,
136+
tool_name: str,
137+
arguments: dict[str, Any] | None,
138+
meta: dict[str, Any] | None = None,
139+
) -> CallToolResult:
125140
"""Invoke a tool on the server."""
126141
pass
127142

@@ -258,6 +273,7 @@ def __init__(
258273
message_handler: MessageHandlerFnT | None = None,
259274
require_approval: RequireApprovalSetting = None,
260275
failure_error_function: ToolErrorFunction | None | _UnsetType = _UNSET,
276+
tool_meta_resolver: MCPToolMetaResolver | None = None,
261277
):
262278
"""
263279
Args:
@@ -288,11 +304,14 @@ def __init__(
288304
a model-visible error message. If explicitly set to None, tool errors will be
289305
raised instead of converted. If left unset, the agent-level configuration (or
290306
SDK default) will be used.
307+
tool_meta_resolver: Optional callable that produces MCP request metadata (`_meta`) for
308+
tool calls. It is invoked by the Agents SDK before calling `call_tool`.
291309
"""
292310
super().__init__(
293311
use_structured_content=use_structured_content,
294312
require_approval=require_approval,
295313
failure_error_function=failure_error_function,
314+
tool_meta_resolver=tool_meta_resolver,
296315
)
297316
self.session: ClientSession | None = None
298317
self.exit_stack: AsyncExitStack = AsyncExitStack()
@@ -561,15 +580,24 @@ async def list_tools(
561580
f"The server may have disconnected."
562581
) from e
563582

564-
async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None) -> CallToolResult:
583+
async def call_tool(
584+
self,
585+
tool_name: str,
586+
arguments: dict[str, Any] | None,
587+
meta: dict[str, Any] | None = None,
588+
) -> CallToolResult:
565589
"""Invoke a tool on the server."""
566590
if not self.session:
567591
raise UserError("Server not initialized. Make sure you call `connect()` first.")
568592
session = self.session
569593
assert session is not None
570594

571595
try:
572-
return await self._run_with_retries(lambda: session.call_tool(tool_name, arguments))
596+
if meta is None:
597+
return await self._run_with_retries(lambda: session.call_tool(tool_name, arguments))
598+
return await self._run_with_retries(
599+
lambda: session.call_tool(tool_name, arguments, meta=meta)
600+
)
573601
except httpx.HTTPStatusError as e:
574602
status_code = e.response.status_code
575603
raise UserError(
@@ -725,6 +753,7 @@ def __init__(
725753
message_handler: MessageHandlerFnT | None = None,
726754
require_approval: RequireApprovalSetting = None,
727755
failure_error_function: ToolErrorFunction | None | _UnsetType = _UNSET,
756+
tool_meta_resolver: MCPToolMetaResolver | None = None,
728757
):
729758
"""Create a new MCP server based on the stdio transport.
730759
@@ -760,17 +789,20 @@ def __init__(
760789
a model-visible error message. If explicitly set to None, tool errors will be
761790
raised instead of converted. If left unset, the agent-level configuration (or
762791
SDK default) will be used.
792+
tool_meta_resolver: Optional callable that produces MCP request metadata (`_meta`) for
793+
tool calls. It is invoked by the Agents SDK before calling `call_tool`.
763794
"""
764795
super().__init__(
765-
cache_tools_list,
766-
client_session_timeout_seconds,
767-
tool_filter,
768-
use_structured_content,
769-
max_retry_attempts,
770-
retry_backoff_seconds_base,
796+
cache_tools_list=cache_tools_list,
797+
client_session_timeout_seconds=client_session_timeout_seconds,
798+
tool_filter=tool_filter,
799+
use_structured_content=use_structured_content,
800+
max_retry_attempts=max_retry_attempts,
801+
retry_backoff_seconds_base=retry_backoff_seconds_base,
771802
message_handler=message_handler,
772803
require_approval=require_approval,
773804
failure_error_function=failure_error_function,
805+
tool_meta_resolver=tool_meta_resolver,
774806
)
775807

776808
self.params = StdioServerParameters(
@@ -837,6 +869,7 @@ def __init__(
837869
message_handler: MessageHandlerFnT | None = None,
838870
require_approval: RequireApprovalSetting = None,
839871
failure_error_function: ToolErrorFunction | None | _UnsetType = _UNSET,
872+
tool_meta_resolver: MCPToolMetaResolver | None = None,
840873
):
841874
"""Create a new MCP server based on the HTTP with SSE transport.
842875
@@ -874,17 +907,20 @@ def __init__(
874907
a model-visible error message. If explicitly set to None, tool errors will be
875908
raised instead of converted. If left unset, the agent-level configuration (or
876909
SDK default) will be used.
910+
tool_meta_resolver: Optional callable that produces MCP request metadata (`_meta`) for
911+
tool calls. It is invoked by the Agents SDK before calling `call_tool`.
877912
"""
878913
super().__init__(
879-
cache_tools_list,
880-
client_session_timeout_seconds,
881-
tool_filter,
882-
use_structured_content,
883-
max_retry_attempts,
884-
retry_backoff_seconds_base,
914+
cache_tools_list=cache_tools_list,
915+
client_session_timeout_seconds=client_session_timeout_seconds,
916+
tool_filter=tool_filter,
917+
use_structured_content=use_structured_content,
918+
max_retry_attempts=max_retry_attempts,
919+
retry_backoff_seconds_base=retry_backoff_seconds_base,
885920
message_handler=message_handler,
886921
require_approval=require_approval,
887922
failure_error_function=failure_error_function,
923+
tool_meta_resolver=tool_meta_resolver,
888924
)
889925

890926
self.params = params
@@ -954,6 +990,7 @@ def __init__(
954990
message_handler: MessageHandlerFnT | None = None,
955991
require_approval: RequireApprovalSetting = None,
956992
failure_error_function: ToolErrorFunction | None | _UnsetType = _UNSET,
993+
tool_meta_resolver: MCPToolMetaResolver | None = None,
957994
):
958995
"""Create a new MCP server based on the Streamable HTTP transport.
959996
@@ -992,17 +1029,20 @@ def __init__(
9921029
a model-visible error message. If explicitly set to None, tool errors will be
9931030
raised instead of converted. If left unset, the agent-level configuration (or
9941031
SDK default) will be used.
1032+
tool_meta_resolver: Optional callable that produces MCP request metadata (`_meta`) for
1033+
tool calls. It is invoked by the Agents SDK before calling `call_tool`.
9951034
"""
9961035
super().__init__(
997-
cache_tools_list,
998-
client_session_timeout_seconds,
999-
tool_filter,
1000-
use_structured_content,
1001-
max_retry_attempts,
1002-
retry_backoff_seconds_base,
1036+
cache_tools_list=cache_tools_list,
1037+
client_session_timeout_seconds=client_session_timeout_seconds,
1038+
tool_filter=tool_filter,
1039+
use_structured_content=use_structured_content,
1040+
max_retry_attempts=max_retry_attempts,
1041+
retry_backoff_seconds_base=retry_backoff_seconds_base,
10031042
message_handler=message_handler,
10041043
require_approval=require_approval,
10051044
failure_error_function=failure_error_function,
1045+
tool_meta_resolver=tool_meta_resolver,
10061046
)
10071047

10081048
self.params = params

src/agents/mcp/util.py

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import copy
34
import functools
45
import inspect
56
import json
@@ -104,6 +105,40 @@ class ToolFilterStatic(TypedDict):
104105
"""A tool filter that can be either a function, static configuration, or None (no filtering)."""
105106

106107

108+
@dataclass
109+
class MCPToolMetaContext:
110+
"""Context information available to MCP tool meta resolver functions."""
111+
112+
run_context: RunContextWrapper[Any]
113+
"""The current run context."""
114+
115+
server_name: str
116+
"""The name of the MCP server."""
117+
118+
tool_name: str
119+
"""The name of the tool being invoked."""
120+
121+
arguments: dict[str, Any] | None
122+
"""The parsed tool arguments."""
123+
124+
125+
if TYPE_CHECKING:
126+
MCPToolMetaResolver = Callable[
127+
[MCPToolMetaContext],
128+
MaybeAwaitable[dict[str, Any] | None],
129+
]
130+
else:
131+
MCPToolMetaResolver = Callable[..., Any]
132+
"""A function that produces MCP request metadata for tool calls.
133+
134+
Args:
135+
context: Context information about the tool invocation.
136+
137+
Returns:
138+
A dict to send as MCP `_meta`, or None to omit metadata.
139+
"""
140+
141+
107142
def create_static_tool_filter(
108143
allowed_tool_names: list[str] | None = None,
109144
blocked_tool_names: list[str] | None = None,
@@ -264,9 +299,57 @@ async def invoke_func(ctx: ToolContext[Any], input_json: str) -> ToolOutput:
264299
needs_approval=server._get_needs_approval_for_tool(tool, agent),
265300
)
266301

302+
@staticmethod
303+
def _merge_mcp_meta(
304+
resolved_meta: dict[str, Any] | None,
305+
explicit_meta: dict[str, Any] | None,
306+
) -> dict[str, Any] | None:
307+
if resolved_meta is None and explicit_meta is None:
308+
return None
309+
merged: dict[str, Any] = {}
310+
if resolved_meta is not None:
311+
merged.update(resolved_meta)
312+
if explicit_meta is not None:
313+
merged.update(explicit_meta)
314+
return merged
315+
316+
@classmethod
317+
async def _resolve_meta(
318+
cls,
319+
server: MCPServer,
320+
context: RunContextWrapper[Any],
321+
tool_name: str,
322+
arguments: dict[str, Any] | None,
323+
) -> dict[str, Any] | None:
324+
meta_resolver = getattr(server, "tool_meta_resolver", None)
325+
if meta_resolver is None:
326+
return None
327+
328+
arguments_copy = copy.deepcopy(arguments) if arguments is not None else None
329+
resolver_context = MCPToolMetaContext(
330+
run_context=context,
331+
server_name=server.name,
332+
tool_name=tool_name,
333+
arguments=arguments_copy,
334+
)
335+
result = meta_resolver(resolver_context)
336+
if inspect.isawaitable(result):
337+
result = await result
338+
if result is None:
339+
return None
340+
if not isinstance(result, dict):
341+
raise TypeError("MCP meta resolver must return a dict or None.")
342+
return result
343+
267344
@classmethod
268345
async def invoke_mcp_tool(
269-
cls, server: MCPServer, tool: MCPTool, context: RunContextWrapper[Any], input_json: str
346+
cls,
347+
server: MCPServer,
348+
tool: MCPTool,
349+
context: RunContextWrapper[Any],
350+
input_json: str,
351+
*,
352+
meta: dict[str, Any] | None = None,
270353
) -> ToolOutput:
271354
"""Invoke an MCP tool and return the result as a string."""
272355
try:
@@ -286,7 +369,12 @@ async def invoke_mcp_tool(
286369
logger.debug(f"Invoking MCP tool {tool.name} with input {input_json}")
287370

288371
try:
289-
result = await server.call_tool(tool.name, json_data)
372+
resolved_meta = await cls._resolve_meta(server, context, tool.name, json_data)
373+
merged_meta = cls._merge_mcp_meta(resolved_meta, meta)
374+
if merged_meta is None:
375+
result = await server.call_tool(tool.name, json_data)
376+
else:
377+
result = await server.call_tool(tool.name, json_data, meta=merged_meta)
290378
except UserError:
291379
# Re-raise UserError as-is (it already has a good message)
292380
raise

0 commit comments

Comments
 (0)