Skip to content

Commit 66e5be3

Browse files
authored
MCP: add support for tool_result.structuredContent (#1150)
MCP recently added `structuredContent` as an additional field in tool results. This lets us use that field when sending the response to the model.
1 parent dff0548 commit 66e5be3

File tree

6 files changed

+84
-9
lines changed

6 files changed

+84
-9
lines changed

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.9.4, <2; python_version >= '3.10'",
16+
"mcp>=1.11.0, <2; python_version >= '3.10'",
1717
]
1818
classifiers = [
1919
"Typing :: Typed",

src/agents/mcp/server.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@
2828
class MCPServer(abc.ABC):
2929
"""Base class for Model Context Protocol servers."""
3030

31+
def __init__(self, use_structured_content: bool = False):
32+
"""
33+
Args:
34+
use_structured_content: Whether to use `tool_result.structured_content` when calling an
35+
MCP tool.Defaults to False for backwards compatibility - most MCP servers still
36+
include the structured content in the `tool_result.content`, and using it by
37+
default will cause duplicate content. You can set this to True if you know the
38+
server will not duplicate the structured content in the `tool_result.content`.
39+
"""
40+
self.use_structured_content = use_structured_content
41+
3142
@abc.abstractmethod
3243
async def connect(self):
3344
"""Connect to the server. For example, this might mean spawning a subprocess or
@@ -86,6 +97,7 @@ def __init__(
8697
cache_tools_list: bool,
8798
client_session_timeout_seconds: float | None,
8899
tool_filter: ToolFilter = None,
100+
use_structured_content: bool = False,
89101
):
90102
"""
91103
Args:
@@ -98,7 +110,13 @@ def __init__(
98110
99111
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
100112
tool_filter: The tool filter to use for filtering tools.
113+
use_structured_content: Whether to use `tool_result.structured_content` when calling an
114+
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
115+
include the structured content in the `tool_result.content`, and using it by
116+
default will cause duplicate content. You can set this to True if you know the
117+
server will not duplicate the structured content in the `tool_result.content`.
101118
"""
119+
super().__init__(use_structured_content=use_structured_content)
102120
self.session: ClientSession | None = None
103121
self.exit_stack: AsyncExitStack = AsyncExitStack()
104122
self._cleanup_lock: asyncio.Lock = asyncio.Lock()
@@ -346,6 +364,7 @@ def __init__(
346364
name: str | None = None,
347365
client_session_timeout_seconds: float | None = 5,
348366
tool_filter: ToolFilter = None,
367+
use_structured_content: bool = False,
349368
):
350369
"""Create a new MCP server based on the stdio transport.
351370
@@ -364,11 +383,17 @@ def __init__(
364383
command.
365384
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
366385
tool_filter: The tool filter to use for filtering tools.
386+
use_structured_content: Whether to use `tool_result.structured_content` when calling an
387+
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
388+
include the structured content in the `tool_result.content`, and using it by
389+
default will cause duplicate content. You can set this to True if you know the
390+
server will not duplicate the structured content in the `tool_result.content`.
367391
"""
368392
super().__init__(
369393
cache_tools_list,
370394
client_session_timeout_seconds,
371395
tool_filter,
396+
use_structured_content,
372397
)
373398

374399
self.params = StdioServerParameters(
@@ -429,6 +454,7 @@ def __init__(
429454
name: str | None = None,
430455
client_session_timeout_seconds: float | None = 5,
431456
tool_filter: ToolFilter = None,
457+
use_structured_content: bool = False,
432458
):
433459
"""Create a new MCP server based on the HTTP with SSE transport.
434460
@@ -449,11 +475,17 @@ def __init__(
449475
450476
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
451477
tool_filter: The tool filter to use for filtering tools.
478+
use_structured_content: Whether to use `tool_result.structured_content` when calling an
479+
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
480+
include the structured content in the `tool_result.content`, and using it by
481+
default will cause duplicate content. You can set this to True if you know the
482+
server will not duplicate the structured content in the `tool_result.content`.
452483
"""
453484
super().__init__(
454485
cache_tools_list,
455486
client_session_timeout_seconds,
456487
tool_filter,
488+
use_structured_content,
457489
)
458490

459491
self.params = params
@@ -514,6 +546,7 @@ def __init__(
514546
name: str | None = None,
515547
client_session_timeout_seconds: float | None = 5,
516548
tool_filter: ToolFilter = None,
549+
use_structured_content: bool = False,
517550
):
518551
"""Create a new MCP server based on the Streamable HTTP transport.
519552
@@ -535,11 +568,17 @@ def __init__(
535568
536569
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
537570
tool_filter: The tool filter to use for filtering tools.
571+
use_structured_content: Whether to use `tool_result.structured_content` when calling an
572+
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
573+
include the structured content in the `tool_result.content`, and using it by
574+
default will cause duplicate content. You can set this to True if you know the
575+
server will not duplicate the structured content in the `tool_result.content`.
538576
"""
539577
super().__init__(
540578
cache_tools_list,
541579
client_session_timeout_seconds,
542580
tool_filter,
581+
use_structured_content,
543582
)
544583

545584
self.params = params

src/agents/mcp/util.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,16 @@ async def invoke_mcp_tool(
198198
# string. We'll try to convert.
199199
if len(result.content) == 1:
200200
tool_output = result.content[0].model_dump_json()
201+
# Append structured content if it exists and we're using it.
202+
if server.use_structured_content and result.structuredContent:
203+
tool_output = f"{tool_output}\n{json.dumps(result.structuredContent)}"
201204
elif len(result.content) > 1:
202-
tool_output = json.dumps([item.model_dump(mode="json") for item in result.content])
205+
tool_results = [item.model_dump(mode="json") for item in result.content]
206+
if server.use_structured_content and result.structuredContent:
207+
tool_results.append(result.structuredContent)
208+
tool_output = json.dumps(tool_results)
209+
elif server.use_structured_content and result.structuredContent:
210+
tool_output = json.dumps(result.structuredContent)
203211
else:
204212
logger.error(f"Errored MCP tool result: {result}")
205213
tool_output = "Error running tool."

tests/mcp/helpers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def __init__(
6161
tool_filter: ToolFilter = None,
6262
server_name: str = "fake_mcp_server",
6363
):
64+
super().__init__(use_structured_content=False)
6465
self.tools: list[MCPTool] = tools or []
6566
self.tool_calls: list[str] = []
6667
self.tool_results: list[str] = []

tests/mcp/test_mcp_tracing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ async def test_mcp_tracing():
6262
"data": {
6363
"name": "test_tool_1",
6464
"input": "",
65-
"output": '{"type":"text","text":"result_test_tool_1_{}","annotations":null}', # noqa: E501
65+
"output": '{"type":"text","text":"result_test_tool_1_{}","annotations":null,"meta":null}', # noqa: E501
6666
"mcp_data": {"server": "fake_mcp_server"},
6767
},
6868
},
@@ -133,7 +133,7 @@ async def test_mcp_tracing():
133133
"data": {
134134
"name": "test_tool_2",
135135
"input": "",
136-
"output": '{"type":"text","text":"result_test_tool_2_{}","annotations":null}', # noqa: E501
136+
"output": '{"type":"text","text":"result_test_tool_2_{}","annotations":null,"meta":null}', # noqa: E501
137137
"mcp_data": {"server": "fake_mcp_server"},
138138
},
139139
},
@@ -197,7 +197,7 @@ async def test_mcp_tracing():
197197
"data": {
198198
"name": "test_tool_3",
199199
"input": "",
200-
"output": '{"type":"text","text":"result_test_tool_3_{}","annotations":null}', # noqa: E501
200+
"output": '{"type":"text","text":"result_test_tool_3_{}","annotations":null,"meta":null}', # noqa: E501
201201
"mcp_data": {"server": "fake_mcp_server"},
202202
},
203203
},

uv.lock

Lines changed: 31 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)