OpenHands / OpenHands/software-agent-sdk

[Bug]: MCPToolObservation silently discards structuredContent and result _meta

Open
#4,776 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug mcp priority:medium ready-for-dev security
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Is there an existing issue for the same bug?
  • I have searched existing issues and this is not a duplicate.
Bug Description

When an MCP tool result carries structuredContent or result _meta, both are silently discarded at conversion: MCPToolObservation.from_call_tool_result keeps only text/image blocks, is_error, and tool_name. Once dropped there, no downstream consumer (event persistence, websocket delivery, protocol-aware clients such as Canvas) can recover them. MCP servers use structuredContent for machine-readable result data and _meta for protocol metadata.

A draft PR with a candidate fix: #4775.

Expected Behavior

The observation preserves structuredContent and result _meta so they serialize with the event, and registered secrets in those fields are masked before persistence or client delivery.

Actual Behavior

Converting a real mcp.types.CallToolResult that carries both fields yields an observation with neither. Reproduced on main at 9d143aac3 (v1.44.1):

$ python repro.py
COMMIT 9d143aac3
PYTHON 3.13.12
STRUCTURED_PRESERVED False
META_PRESERVED False
repro.py
import mcp.types
from openhands.sdk.mcp.definition import MCPToolObservation

result = mcp.types.CallToolResult(
    _meta={"ui": {"hint": "Bearer token"}},
    structuredContent={"rows": [{"token": "abc"}], "count": 1},
    content=[mcp.types.TextContent(type="text", text="ok")],
    isError=False,
)
obs = MCPToolObservation.from_call_tool_result(tool_name="probe", result=result)
print("STRUCTURED_PRESERVED", getattr(obs, "structured_content", None) is not None)
print("META_PRESERVED", getattr(obs, "result_meta", None) is not None)
Steps to Reproduce
  1. Check out main at 9d143aac3 and install with uv sync
  2. Run the script above with the repo interpreter
  3. Both lines print False: the converted observation carries neither structuredContent nor _meta, and model_dump loses them
Acceptance Criteria
  • structuredContent and result _meta survive conversion and event serialization
  • Observations serialized before the fields existed still load
  • Registered secrets in the new fields are masked, including nested keys, nested values, and JSON scalars
  • The fields stay out of the content sent to the model
Installation Method

pip install -e . from a git checkout of main

SDK Version

main branch, commit 9d143aac3 (v1.44.1)

Python Version

3.13.12

Operating System

Linux

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at MCPToolObservation.from_call_tool_result and run the repro.py example against the stated commit to confirm the fields are dropped. Trace the observation's event serialization and secret-masking paths; done means structuredContent and result _meta survive conversion and loading, nested secrets are masked, and neither field reaches model content.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.