Skip to content

Commit 0e89cdb

Browse files
Vamil Gandhidbschmigelski
authored andcommitted
feat: add optional outputSchema support for tool specifications
- Add outputSchema as optional field in ToolSpec using NotRequired - Filter outputSchema unconditionally in streaming.py before passing to models - Add outputSchema support to MCP agent tool adapter - Add tests for outputSchema functionality - Addresses PR #818 review comments
1 parent 7226025 commit 0e89cdb

File tree

4 files changed

+36
-5
lines changed

4 files changed

+36
-5
lines changed

src/strands/event_loop/streaming.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import json
44
import logging
5-
from typing import Any, AsyncGenerator, AsyncIterable, Optional
5+
from typing import Any, AsyncGenerator, AsyncIterable, Optional, cast
66

77
from ..models.model import Model
88
from ..types._events import (
@@ -346,7 +346,17 @@ async def stream_messages(
346346
logger.debug("model=<%s> | streaming messages", model)
347347

348348
messages = remove_blank_messages_content_text(messages)
349-
chunks = model.stream(messages, tool_specs if tool_specs else None, system_prompt)
349+
350+
# TODO(#780): Remove outputSchema filtering once model-specific behavior handling is implemented.
351+
# For now, we filter out outputSchema from all tool specs to ensure compatibility with all model providers.
352+
# Some providers (e.g., Bedrock) will throw validation errors if they receive unknown fields.
353+
filtered_tool_specs = None
354+
if tool_specs:
355+
filtered_tool_specs = cast(
356+
list[ToolSpec], [{k: v for k, v in spec.items() if k != "outputSchema"} for spec in tool_specs]
357+
)
358+
359+
chunks = model.stream(messages, filtered_tool_specs, system_prompt)
350360

351361
async for event in process_stream(chunks):
352362
yield event

src/strands/tools/mcp/mcp_agent_tool.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,24 @@ def tool_spec(self) -> ToolSpec:
5454
"""Get the specification of the tool.
5555
5656
This method converts the MCP tool specification to the agent framework's
57-
ToolSpec format, including the input schema and description.
57+
ToolSpec format, including the input schema, description, and optional output schema.
5858
5959
Returns:
6060
ToolSpec: The tool specification in the agent framework format
6161
"""
6262
description: str = self.mcp_tool.description or f"Tool which performs {self.mcp_tool.name}"
63-
return {
63+
64+
spec: ToolSpec = {
6465
"inputSchema": {"json": self.mcp_tool.inputSchema},
6566
"name": self.mcp_tool.name,
6667
"description": description,
6768
}
6869

70+
if self.mcp_tool.outputSchema:
71+
spec["outputSchema"] = {"json": self.mcp_tool.outputSchema}
72+
73+
return spec
74+
6975
@property
7076
def tool_type(self) -> str:
7177
"""Get the type of the tool.

src/strands/types/tools.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from dataclasses import dataclass
1010
from typing import TYPE_CHECKING, Any, AsyncGenerator, Awaitable, Callable, Literal, Protocol, Union
1111

12-
from typing_extensions import TypedDict
12+
from typing_extensions import NotRequired, TypedDict
1313

1414
from .media import DocumentContent, ImageContent
1515

@@ -27,11 +27,15 @@ class ToolSpec(TypedDict):
2727
description: A human-readable description of what the tool does.
2828
inputSchema: JSON Schema defining the expected input parameters.
2929
name: The unique name of the tool.
30+
outputSchema: Optional JSON Schema defining the expected output format.
31+
Note: Not all model providers support this field. Providers that don't
32+
support it should filter it out before sending to their API.
3033
"""
3134

3235
description: str
3336
inputSchema: JSONSchema
3437
name: str
38+
outputSchema: NotRequired[JSONSchema]
3539

3640

3741
class Tool(TypedDict):

tests/strands/tools/mcp/test_mcp_agent_tool.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def mock_mcp_tool():
1313
mock_tool.name = "test_tool"
1414
mock_tool.description = "A test tool"
1515
mock_tool.inputSchema = {"type": "object", "properties": {}}
16+
mock_tool.outputSchema = None # MCP tools can have optional outputSchema
1617
return mock_tool
1718

1819

@@ -58,6 +59,16 @@ def test_tool_spec_without_description(mock_mcp_tool, mock_mcp_client):
5859
assert tool_spec["description"] == "Tool which performs test_tool"
5960

6061

62+
def test_tool_spec_with_output_schema(mock_mcp_tool, mock_mcp_client):
63+
mock_mcp_tool.outputSchema = {"type": "object", "properties": {"result": {"type": "string"}}}
64+
65+
agent_tool = MCPAgentTool(mock_mcp_tool, mock_mcp_client)
66+
tool_spec = agent_tool.tool_spec
67+
68+
assert "outputSchema" in tool_spec
69+
assert tool_spec["outputSchema"]["json"] == {"type": "object", "properties": {"result": {"type": "string"}}}
70+
71+
6172
@pytest.mark.asyncio
6273
async def test_stream(mcp_agent_tool, mock_mcp_client, alist):
6374
tool_use = {"toolUseId": "test-123", "name": "test_tool", "input": {"param": "value"}}

0 commit comments

Comments
 (0)