Skip to content
Closed
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
35 changes: 35 additions & 0 deletions tests/tools/test_mcp_structured_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ def __init__(self, text: str, block_type: str = "text"):
self.type = block_type


class _FakeEmbeddedResourceBlock:
"""Minimal EmbeddedResource-like block with text under .resource.text."""

def __init__(self, text: str):
self.type = "resource"
self.resource = SimpleNamespace(text=text, mimeType="text/markdown")


class _FakeCallToolResult:
"""Minimal CallToolResult stand-in.

Expand Down Expand Up @@ -78,6 +86,33 @@ def test_text_only_result(self, _patch_mcp_server):
data = json.loads(raw)
assert data == {"result": "hello"}

def test_embedded_resource_text_result(self, _patch_mcp_server):
"""EmbeddedResource.resource.text is forwarded as normal result text."""
session = _patch_mcp_server
session.call_tool = AsyncMock(
return_value=_FakeCallToolResult(
content=[_FakeEmbeddedResourceBlock("# Title\n")],
)
)
handler = mcp_tool._make_tool_handler("test-server", "my-tool", 30.0)
raw = handler({})
data = json.loads(raw)
assert data == {"result": "# Title\n"}

def test_embedded_resource_error_text_result(self, _patch_mcp_server):
"""EmbeddedResource.resource.text is preserved on MCP error results."""
session = _patch_mcp_server
session.call_tool = AsyncMock(
return_value=_FakeCallToolResult(
content=[_FakeEmbeddedResourceBlock("resource failed")],
is_error=True,
)
)
handler = mcp_tool._make_tool_handler("test-server", "my-tool", 30.0)
raw = handler({})
data = json.loads(raw)
assert data == {"error": "resource failed"}

def test_both_content_and_structured(self, _patch_mcp_server):
"""When both content and structuredContent are present, combine them."""
session = _patch_mcp_server
Expand Down
27 changes: 23 additions & 4 deletions tools/mcp_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2287,6 +2287,25 @@ async def _connect_server(name: str, config: dict) -> MCPServerTask:
# Handler / check-fn factories
# ---------------------------------------------------------------------------

def _extract_mcp_text_block(block) -> str:
"""Extract model-visible text from an MCP content block.

Most MCP tools return TextContent blocks exposing ``.text`` directly. Some
servers, including QMD, return EmbeddedResource blocks whose payload lives
under ``.resource.text``. Treat both shapes as equivalent text sources so
wrapper formatting does not silently drop resource-style responses.
"""
text = getattr(block, "text", None)
if isinstance(text, str) and text:
return text
resource = getattr(block, "resource", None)
if resource is not None:
resource_text = getattr(resource, "text", None)
if isinstance(resource_text, str) and resource_text:
return resource_text
return ""


def _make_tool_handler(server_name: str, tool_name: str, tool_timeout: float):
"""Return a sync handler that calls an MCP tool via the background loop.

Expand Down Expand Up @@ -2336,8 +2355,7 @@ async def _call():
if result.isError:
error_text = ""
for block in (result.content or []):
if hasattr(block, "text"):
error_text += block.text
error_text += _extract_mcp_text_block(block)
return json.dumps({
"error": _sanitize_error(
error_text or "MCP tool returned an error"
Expand All @@ -2357,8 +2375,9 @@ async def _call():
# the two — plugs into existing infrastructure.
parts: List[str] = []
for block in (result.content or []):
if hasattr(block, "text") and block.text:
parts.append(block.text)
text = _extract_mcp_text_block(block)
if text:
parts.append(text)
continue
image_tag = _cache_mcp_image_block(block)
if image_tag:
Expand Down