Skip to content

agentgateway.call_mcp_tool flattens CallToolResult to str, dropping structuredContent and other MCP fields #214

Description

@patrickmlr

Describe the Bug

sap_cloud_sdk.agentgateway.AgentGatewayClient.call_mcp_tool is typed -> str and both internal flow paths (_customer.py::call_mcp_tool_customer and _lob.py::call_mcp_tool_lob) discard almost everything on the returned MCP CallToolResult before crossing the SDK boundary:

# src/sap_cloud_sdk/agentgateway/_customer.py (main branch, lines 571-576)
# Same shape in _lob.py at lines 480-484.
result = await session.call_tool(tool.name, kwargs)
if not result.content:
    logger.warning("Tool '%s' returned empty content", tool.name)
    return ""
first = result.content[0]
return str(getattr(first, "text", ""))

Dropped fields:

  • structuredContent — the MCP-native channel for schema-typed structured tool output. Servers set this alongside content so consumers can distinguish human-readable text from typed data.
  • content[1:] — every content block after the first (multi-block responses lose everything but block 0).
  • isError — success and error results are both returned as str, indistinguishable at the type level.
  • _meta — arbitrary MCP metadata.

The reference converter converters.py::mcp_tool_to_langchain builds a LangChain StructuredTool whose coroutine returns this flattened str directly to LangChain, so downstream consumers using the reference converter also lose these fields.

Why it matters: langchain_mcp_adapters (a sibling library in the LangChain ecosystem) preserves structuredContent on the LangChain side by wrapping it into MCPToolArtifact on ToolMessage.artifact via response_format="content_and_artifact". Consumers that go through sap_cloud_sdk.agentgateway for its auth / mTLS / destination-resolution / tenant-routing plumbing pay for it by losing the structured channel. Concrete downstream impacts:

  • Agents emitting structured tool payloads as A2A Part(root=DataPart(...)) cannot recover structuredContent from a str return. The workaround is to json.loads the returned string, which only succeeds when the MCP server happens to duplicate its structured payload into content[0].text.
  • Any MCP tool that returns multiple content blocks (text + image, or multiple text blocks) is truncated to block 0.
  • Callers cannot check isError at the type level — success and error results have the same return type.

Steps to Reproduce

  1. Configure an MCP server that returns a CallToolResult with both content (text) and structuredContent (dict) populated. This is the standard MCP shape for schema-typed tool output.
  2. Invoke via agent_gateway_client.call_mcp_tool(tool, ...).
  3. Observe that the return value is str — the first content block's text. structuredContent, content[1:], isError, and _meta are unrecoverable from the return.

Expected Behavior

call_mcp_tool (or an equivalent method exposed by the SDK) allows consumers to access the full CallToolResult, preserving content, structuredContent, isError, and _meta. This lets consumers implement the same content_and_artifact split that langchain_mcp_adapters provides for the direct-MCP path.

Two possible shapes for the fix (maintainers know the compatibility surface best):

Option A — additive, non-breaking. Add a new method returning the raw CallToolResult, keep the existing call_mcp_tool unchanged:

async def call_mcp_tool_raw(
    self,
    tool: MCPTool,
    user_token: str | Callable[[], str] | None = None,
    app_tid: str | None = None,
    **kwargs,
) -> mcp.types.CallToolResult:
    ...

Update converters.py::mcp_tool_to_langchain (or ship a second reference converter) to call call_mcp_tool_raw and build a StructuredTool with response_format="content_and_artifact", matching the langchain_mcp_adapters shape.

Option B — breaking, cleaner long-term. Change call_mcp_tool to return CallToolResult; update the reference converter accordingly. Requires a major version bump and a migration note.

Option A seems preferable given call_mcp_tool is a documented public API with a stable signature and existing consumers would need to migrate.

Used Versions

  • Python version: 3.14.3 (bug is Python-version-independent — logic is in the SDK)
  • SAP Cloud SDK for Python version: 0.29.1 observed. Verified the same flattening logic is still on main at time of filing: agw_client.py:485 declares -> str, and _customer.py:571-576 / _lob.py:480-484 contain the flatten-to-content[0].text code.
  • Framework version: langchain-mcp-adapters==0.2.2 (for cross-reference with the sibling library's _convert_call_tool_result behavior)

Code Examples

# Consumer-side workaround currently in use — reconstructs a partial
# CallToolResult by parsing the flattened string. Works when the MCP
# server duplicates its payload into content[0].text; loses information
# when it doesn't.
raw_string = await agw_client.call_mcp_tool(tool, ...)
try:
    parsed = json.loads(raw_string)
    structured = parsed if isinstance(parsed, dict) else None
except json.JSONDecodeError:
    structured = None

result = CallToolResult(
    content=[TextContent(type="text", text=raw_string)],
    structuredContent=structured,
    isError=False,   # Cannot actually determine — SDK dropped it
)

Affected Development Phase

Development

Impact

Impaired

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions