Skip to content

MCP: add support for tool_result.structuredContent #1150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dependencies = [
"typing-extensions>=4.12.2, <5",
"requests>=2.0, <3",
"types-requests>=2.0, <3",
"mcp>=1.9.4, <2; python_version >= '3.10'",
"mcp>=1.11.0, <2; python_version >= '3.10'",
]
classifiers = [
"Typing :: Typed",
Expand Down
39 changes: 39 additions & 0 deletions src/agents/mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@
class MCPServer(abc.ABC):
"""Base class for Model Context Protocol servers."""

def __init__(self, use_structured_content: bool = False):
"""
Args:
use_structured_content: Whether to use `tool_result.structured_content` when calling an
MCP tool.Defaults to False for backwards compatibility - most MCP servers still
include the structured content in the `tool_result.content`, and using it by
default will cause duplicate content. You can set this to True if you know the
server will not duplicate the structured content in the `tool_result.content`.
"""
self.use_structured_content = use_structured_content

@abc.abstractmethod
async def connect(self):
"""Connect to the server. For example, this might mean spawning a subprocess or
Expand Down Expand Up @@ -86,6 +97,7 @@ def __init__(
cache_tools_list: bool,
client_session_timeout_seconds: float | None,
tool_filter: ToolFilter = None,
use_structured_content: bool = False,
):
"""
Args:
Expand All @@ -98,7 +110,13 @@ def __init__(

client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
tool_filter: The tool filter to use for filtering tools.
use_structured_content: Whether to use `tool_result.structured_content` when calling an
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
include the structured content in the `tool_result.content`, and using it by
default will cause duplicate content. You can set this to True if you know the
server will not duplicate the structured content in the `tool_result.content`.
"""
super().__init__(use_structured_content=use_structured_content)
self.session: ClientSession | None = None
self.exit_stack: AsyncExitStack = AsyncExitStack()
self._cleanup_lock: asyncio.Lock = asyncio.Lock()
Expand Down Expand Up @@ -346,6 +364,7 @@ def __init__(
name: str | None = None,
client_session_timeout_seconds: float | None = 5,
tool_filter: ToolFilter = None,
use_structured_content: bool = False,
):
"""Create a new MCP server based on the stdio transport.

Expand All @@ -364,11 +383,17 @@ def __init__(
command.
client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
tool_filter: The tool filter to use for filtering tools.
use_structured_content: Whether to use `tool_result.structured_content` when calling an
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
include the structured content in the `tool_result.content`, and using it by
default will cause duplicate content. You can set this to True if you know the
server will not duplicate the structured content in the `tool_result.content`.
"""
super().__init__(
cache_tools_list,
client_session_timeout_seconds,
tool_filter,
use_structured_content,
)

self.params = StdioServerParameters(
Expand Down Expand Up @@ -429,6 +454,7 @@ def __init__(
name: str | None = None,
client_session_timeout_seconds: float | None = 5,
tool_filter: ToolFilter = None,
use_structured_content: bool = False,
):
"""Create a new MCP server based on the HTTP with SSE transport.

Expand All @@ -449,11 +475,17 @@ def __init__(

client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
tool_filter: The tool filter to use for filtering tools.
use_structured_content: Whether to use `tool_result.structured_content` when calling an
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
include the structured content in the `tool_result.content`, and using it by
default will cause duplicate content. You can set this to True if you know the
server will not duplicate the structured content in the `tool_result.content`.
"""
super().__init__(
cache_tools_list,
client_session_timeout_seconds,
tool_filter,
use_structured_content,
)

self.params = params
Expand Down Expand Up @@ -514,6 +546,7 @@ def __init__(
name: str | None = None,
client_session_timeout_seconds: float | None = 5,
tool_filter: ToolFilter = None,
use_structured_content: bool = False,
):
"""Create a new MCP server based on the Streamable HTTP transport.

Expand All @@ -535,11 +568,17 @@ def __init__(

client_session_timeout_seconds: the read timeout passed to the MCP ClientSession.
tool_filter: The tool filter to use for filtering tools.
use_structured_content: Whether to use `tool_result.structured_content` when calling an
MCP tool. Defaults to False for backwards compatibility - most MCP servers still
include the structured content in the `tool_result.content`, and using it by
default will cause duplicate content. You can set this to True if you know the
server will not duplicate the structured content in the `tool_result.content`.
"""
super().__init__(
cache_tools_list,
client_session_timeout_seconds,
tool_filter,
use_structured_content,
)

self.params = params
Expand Down
10 changes: 9 additions & 1 deletion src/agents/mcp/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,16 @@ async def invoke_mcp_tool(
# string. We'll try to convert.
if len(result.content) == 1:
tool_output = result.content[0].model_dump_json()
# Append structured content if it exists and we're using it.
if server.use_structured_content and result.structuredContent:
tool_output = f"{tool_output}\n{json.dumps(result.structuredContent)}"
elif len(result.content) > 1:
tool_output = json.dumps([item.model_dump(mode="json") for item in result.content])
tool_results = [item.model_dump(mode="json") for item in result.content]
if server.use_structured_content and result.structuredContent:
tool_results.append(result.structuredContent)
tool_output = json.dumps(tool_results)
elif server.use_structured_content and result.structuredContent:
tool_output = json.dumps(result.structuredContent)
else:
logger.error(f"Errored MCP tool result: {result}")
tool_output = "Error running tool."
Expand Down
1 change: 1 addition & 0 deletions tests/mcp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ def __init__(
tool_filter: ToolFilter = None,
server_name: str = "fake_mcp_server",
):
super().__init__(use_structured_content=False)
self.tools: list[MCPTool] = tools or []
self.tool_calls: list[str] = []
self.tool_results: list[str] = []
Expand Down
6 changes: 3 additions & 3 deletions tests/mcp/test_mcp_tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ async def test_mcp_tracing():
"data": {
"name": "test_tool_1",
"input": "",
"output": '{"type":"text","text":"result_test_tool_1_{}","annotations":null}', # noqa: E501
"output": '{"type":"text","text":"result_test_tool_1_{}","annotations":null,"meta":null}', # noqa: E501
"mcp_data": {"server": "fake_mcp_server"},
},
},
Expand Down Expand Up @@ -133,7 +133,7 @@ async def test_mcp_tracing():
"data": {
"name": "test_tool_2",
"input": "",
"output": '{"type":"text","text":"result_test_tool_2_{}","annotations":null}', # noqa: E501
"output": '{"type":"text","text":"result_test_tool_2_{}","annotations":null,"meta":null}', # noqa: E501
"mcp_data": {"server": "fake_mcp_server"},
},
},
Expand Down Expand Up @@ -197,7 +197,7 @@ async def test_mcp_tracing():
"data": {
"name": "test_tool_3",
"input": "",
"output": '{"type":"text","text":"result_test_tool_3_{}","annotations":null}', # noqa: E501
"output": '{"type":"text","text":"result_test_tool_3_{}","annotations":null,"meta":null}', # noqa: E501
"mcp_data": {"server": "fake_mcp_server"},
},
},
Expand Down
35 changes: 31 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.