Skip to content

Commit 7fc489e

Browse files
authored
feat: add SDK-only custom data for tool outputs (#3486)
1 parent a4ba63f commit 7fc489e

14 files changed

Lines changed: 748 additions & 32 deletions

src/agents/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,20 @@
133133
)
134134
from .tool import (
135135
ApplyPatchTool,
136+
ApplyPatchToolCustomDataContext,
137+
ApplyPatchToolCustomDataExtractor,
136138
CodeInterpreterTool,
137139
ComputerProvider,
138140
ComputerTool,
141+
ComputerToolCustomDataContext,
142+
ComputerToolCustomDataExtractor,
139143
CustomTool,
144+
CustomToolCustomDataContext,
145+
CustomToolCustomDataExtractor,
140146
FileSearchTool,
141147
FunctionTool,
148+
FunctionToolCustomDataContext,
149+
FunctionToolCustomDataExtractor,
142150
FunctionToolResult,
143151
HostedMCPTool,
144152
ImageGenerationTool,
@@ -458,10 +466,16 @@ def enable_verbose_stdout_logging():
458466
"AgentUpdatedStreamEvent",
459467
"StreamEvent",
460468
"FunctionTool",
469+
"FunctionToolCustomDataContext",
470+
"FunctionToolCustomDataExtractor",
461471
"FunctionToolResult",
462472
"ComputerTool",
473+
"ComputerToolCustomDataContext",
474+
"ComputerToolCustomDataExtractor",
463475
"ComputerProvider",
464476
"CustomTool",
477+
"CustomToolCustomDataContext",
478+
"CustomToolCustomDataExtractor",
465479
"FileSearchTool",
466480
"CodeInterpreterTool",
467481
"ImageGenerationTool",
@@ -494,6 +508,8 @@ def enable_verbose_stdout_logging():
494508
"ApplyPatchOperation",
495509
"ApplyPatchResult",
496510
"ApplyPatchTool",
511+
"ApplyPatchToolCustomDataContext",
512+
"ApplyPatchToolCustomDataExtractor",
497513
"Tool",
498514
"WebSearchTool",
499515
"HostedMCPTool",

src/agents/items.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,13 @@ class ToolCallOutputItem(RunItemBase[Any]):
403403
tool_origin: ToolOrigin | None = None
404404
"""Optional metadata describing the source of a function-tool-backed item."""
405405

406+
custom_data: dict[str, Any] | None = None
407+
"""SDK-only custom data attached to this tool output.
408+
409+
This data is not part of ``raw_item`` and is not sent back to the model when the output item is
410+
replayed as input.
411+
"""
412+
406413
@property
407414
def call_id(self) -> str | None:
408415
"""Return the call identifier from the raw item, if available."""

src/agents/mcp/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
)
1818

1919
from .util import (
20+
MCPToolCustomDataContext,
21+
MCPToolCustomDataExtractor,
2022
MCPToolMetaContext,
2123
MCPToolMetaResolver,
2224
MCPUtil,
@@ -50,6 +52,8 @@
5052
"MCPServerManager",
5153
"LocalMCPApprovalCallable",
5254
"MCPUtil",
55+
"MCPToolCustomDataContext",
56+
"MCPToolCustomDataExtractor",
5357
"MCPToolMetaContext",
5458
"MCPToolMetaResolver",
5559
"ToolFilter",

src/agents/mcp/server.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from ..util._types import MaybeAwaitable
4646
from .util import (
4747
HttpClientFactory,
48+
MCPToolCustomDataExtractor,
4849
MCPToolMetaResolver,
4950
ToolFilter,
5051
ToolFilterContext,
@@ -229,6 +230,7 @@ def __init__(
229230
require_approval: RequireApprovalSetting = None,
230231
failure_error_function: ToolErrorFunction | None | _UnsetType = _UNSET,
231232
tool_meta_resolver: MCPToolMetaResolver | None = None,
233+
custom_data_extractor: MCPToolCustomDataExtractor | None = None,
232234
):
233235
"""
234236
Args:
@@ -248,13 +250,16 @@ def __init__(
248250
SDK default) will be used.
249251
tool_meta_resolver: Optional callable that produces MCP request metadata (`_meta`) for
250252
tool calls. It is invoked by the Agents SDK before calling `call_tool`.
253+
custom_data_extractor: Optional callable that produces SDK-only custom data for
254+
emitted MCP tool output items.
251255
"""
252256
self.use_structured_content = use_structured_content
253257
self._needs_approval_policy = self._normalize_needs_approval(
254258
require_approval=require_approval
255259
)
256260
self._failure_error_function = failure_error_function
257261
self.tool_meta_resolver = tool_meta_resolver
262+
self.custom_data_extractor = custom_data_extractor
258263

259264
@abc.abstractmethod
260265
async def connect(self):
@@ -544,6 +549,7 @@ def __init__(
544549
require_approval: RequireApprovalSetting = None,
545550
failure_error_function: ToolErrorFunction | None | _UnsetType = _UNSET,
546551
tool_meta_resolver: MCPToolMetaResolver | None = None,
552+
custom_data_extractor: MCPToolCustomDataExtractor | None = None,
547553
):
548554
"""
549555
Args:
@@ -576,12 +582,15 @@ def __init__(
576582
SDK default) will be used.
577583
tool_meta_resolver: Optional callable that produces MCP request metadata (`_meta`) for
578584
tool calls. It is invoked by the Agents SDK before calling `call_tool`.
585+
custom_data_extractor: Optional callable that produces SDK-only custom data for
586+
emitted MCP tool output items.
579587
"""
580588
super().__init__(
581589
use_structured_content=use_structured_content,
582590
require_approval=require_approval,
583591
failure_error_function=failure_error_function,
584592
tool_meta_resolver=tool_meta_resolver,
593+
custom_data_extractor=custom_data_extractor,
585594
)
586595
self.session: ClientSession | None = None
587596
self.exit_stack: AsyncExitStack = AsyncExitStack()
@@ -1108,6 +1117,7 @@ def __init__(
11081117
require_approval: RequireApprovalSetting = None,
11091118
failure_error_function: ToolErrorFunction | None | _UnsetType = _UNSET,
11101119
tool_meta_resolver: MCPToolMetaResolver | None = None,
1120+
custom_data_extractor: MCPToolCustomDataExtractor | None = None,
11111121
):
11121122
"""Create a new MCP server based on the stdio transport.
11131123
@@ -1145,6 +1155,8 @@ def __init__(
11451155
SDK default) will be used.
11461156
tool_meta_resolver: Optional callable that produces MCP request metadata (`_meta`) for
11471157
tool calls. It is invoked by the Agents SDK before calling `call_tool`.
1158+
custom_data_extractor: Optional callable that produces SDK-only custom data for
1159+
emitted MCP tool output items.
11481160
"""
11491161
super().__init__(
11501162
cache_tools_list=cache_tools_list,
@@ -1157,6 +1169,7 @@ def __init__(
11571169
require_approval=require_approval,
11581170
failure_error_function=failure_error_function,
11591171
tool_meta_resolver=tool_meta_resolver,
1172+
custom_data_extractor=custom_data_extractor,
11601173
)
11611174

11621175
self.params = StdioServerParameters(
@@ -1229,6 +1242,7 @@ def __init__(
12291242
require_approval: RequireApprovalSetting = None,
12301243
failure_error_function: ToolErrorFunction | None | _UnsetType = _UNSET,
12311244
tool_meta_resolver: MCPToolMetaResolver | None = None,
1245+
custom_data_extractor: MCPToolCustomDataExtractor | None = None,
12321246
):
12331247
"""Create a new MCP server based on the HTTP with SSE transport.
12341248
@@ -1268,6 +1282,8 @@ def __init__(
12681282
SDK default) will be used.
12691283
tool_meta_resolver: Optional callable that produces MCP request metadata (`_meta`) for
12701284
tool calls. It is invoked by the Agents SDK before calling `call_tool`.
1285+
custom_data_extractor: Optional callable that produces SDK-only custom data for
1286+
emitted MCP tool output items.
12711287
"""
12721288
super().__init__(
12731289
cache_tools_list=cache_tools_list,
@@ -1280,6 +1296,7 @@ def __init__(
12801296
require_approval=require_approval,
12811297
failure_error_function=failure_error_function,
12821298
tool_meta_resolver=tool_meta_resolver,
1299+
custom_data_extractor=custom_data_extractor,
12831300
)
12841301

12851302
self.params = params
@@ -1365,6 +1382,7 @@ def __init__(
13651382
require_approval: RequireApprovalSetting = None,
13661383
failure_error_function: ToolErrorFunction | None | _UnsetType = _UNSET,
13671384
tool_meta_resolver: MCPToolMetaResolver | None = None,
1385+
custom_data_extractor: MCPToolCustomDataExtractor | None = None,
13681386
):
13691387
"""Create a new MCP server based on the Streamable HTTP transport.
13701388
@@ -1405,6 +1423,8 @@ def __init__(
14051423
SDK default) will be used.
14061424
tool_meta_resolver: Optional callable that produces MCP request metadata (`_meta`) for
14071425
tool calls. It is invoked by the Agents SDK before calling `call_tool`.
1426+
custom_data_extractor: Optional callable that produces SDK-only custom data for
1427+
emitted MCP tool output items.
14081428
"""
14091429
super().__init__(
14101430
cache_tools_list=cache_tools_list,
@@ -1417,6 +1437,7 @@ def __init__(
14171437
require_approval=require_approval,
14181438
failure_error_function=failure_error_function,
14191439
tool_meta_resolver=tool_meta_resolver,
1440+
custom_data_extractor=custom_data_extractor,
14201441
)
14211442

14221443
self.params = params

src/agents/mcp/util.py

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import inspect
88
import json
99
from collections import Counter
10-
from collections.abc import Awaitable, Callable
10+
from collections.abc import Awaitable, Callable, Mapping
1111
from dataclasses import dataclass
12+
from types import MappingProxyType
1213
from typing import TYPE_CHECKING, Any, Protocol, Union
1314

1415
import httpx
@@ -39,6 +40,7 @@
3940
)
4041
from ..tool_context import ToolContext
4142
from ..tracing import FunctionSpanData, get_current_span, mcp_tools_span
43+
from ..util._custom_data import maybe_extract_custom_data
4244
from ..util._types import MaybeAwaitable
4345

4446
if TYPE_CHECKING:
@@ -149,13 +151,50 @@ class MCPToolMetaContext:
149151
"""The parsed tool arguments."""
150152

151153

154+
@dataclass(frozen=True)
155+
class MCPToolCustomDataContext:
156+
"""Context passed to MCP tool custom data extractors."""
157+
158+
run_context: RunContextWrapper[Any]
159+
"""The current run context."""
160+
161+
server_name: str
162+
"""The name of the MCP server."""
163+
164+
tool_name: str
165+
"""The original MCP tool name invoked on the server."""
166+
167+
tool_display_name: str
168+
"""The public tool name exposed through the Agents SDK."""
169+
170+
arguments: Mapping[str, Any]
171+
"""The parsed tool arguments."""
172+
173+
result_meta: Mapping[str, Any] | None
174+
"""The MCP tool result ``_meta`` payload, if present."""
175+
176+
structured_content: Mapping[str, Any] | None
177+
"""The MCP tool result ``structuredContent`` payload, if present."""
178+
179+
is_error: bool | None
180+
"""The MCP tool result ``isError`` flag, if present."""
181+
182+
tool_output: ToolOutput
183+
"""The model-visible tool output produced by the Agents SDK."""
184+
185+
152186
if TYPE_CHECKING:
153187
MCPToolMetaResolver = Callable[
154188
[MCPToolMetaContext],
155189
MaybeAwaitable[dict[str, Any] | None],
156190
]
191+
MCPToolCustomDataExtractor = Callable[
192+
[MCPToolCustomDataContext],
193+
MaybeAwaitable[Mapping[str, Any] | None],
194+
]
157195
else:
158196
MCPToolMetaResolver = Callable[..., Any]
197+
MCPToolCustomDataExtractor = Callable[..., Any]
159198
"""A function that produces MCP request metadata for tool calls.
160199
161200
Args:
@@ -164,6 +203,7 @@ class MCPToolMetaContext:
164203
Returns:
165204
A dict to send as MCP `_meta`, or None to omit metadata.
166205
"""
206+
"""A function that produces SDK-only custom data for MCP tool output items."""
167207

168208

169209
def create_static_tool_filter(
@@ -541,6 +581,41 @@ def _merge_mcp_meta(
541581
merged.update(copy.deepcopy(explicit_meta))
542582
return merged
543583

584+
@staticmethod
585+
def _copy_mapping_proxy(value: Any) -> Mapping[str, Any] | None:
586+
if not isinstance(value, dict):
587+
return None
588+
return MappingProxyType(copy.deepcopy(value))
589+
590+
@classmethod
591+
async def _extract_custom_data(
592+
cls,
593+
*,
594+
server: MCPServer,
595+
context: RunContextWrapper[Any],
596+
tool_name: str,
597+
tool_display_name: str,
598+
arguments: dict[str, Any],
599+
result: Any,
600+
tool_output: ToolOutput,
601+
) -> dict[str, Any] | None:
602+
extractor = getattr(server, "custom_data_extractor", None)
603+
if extractor is None:
604+
return None
605+
606+
extractor_context = MCPToolCustomDataContext(
607+
run_context=context,
608+
server_name=server.name,
609+
tool_name=tool_name,
610+
tool_display_name=tool_display_name,
611+
arguments=MappingProxyType(copy.deepcopy(arguments)),
612+
result_meta=cls._copy_mapping_proxy(getattr(result, "meta", None)),
613+
structured_content=cls._copy_mapping_proxy(getattr(result, "structuredContent", None)),
614+
is_error=getattr(result, "isError", None),
615+
tool_output=copy.deepcopy(tool_output),
616+
)
617+
return await maybe_extract_custom_data(extractor, extractor_context)
618+
544619
@classmethod
545620
async def _resolve_meta(
546621
cls,
@@ -688,6 +763,18 @@ async def invoke_mcp_tool(
688763
else:
689764
tool_output = tool_output_list
690765

766+
custom_data = await cls._extract_custom_data(
767+
server=server,
768+
context=context,
769+
tool_name=tool.name,
770+
tool_display_name=tool_name_for_display,
771+
arguments=json_data,
772+
result=result,
773+
tool_output=tool_output,
774+
)
775+
if custom_data and isinstance(context, ToolContext):
776+
context._custom_data = custom_data
777+
691778
current_span = get_current_span()
692779
if current_span:
693780
if isinstance(current_span.span_data, FunctionSpanData):

0 commit comments

Comments
 (0)