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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions