Files
Giles Odigwe 93cbf6b3f0 Python: Parse MCP CallToolResult.structuredContent field to prevent tool results returning None (#6421)
* Parse structuredContent from MCP CallToolResult (#3313)

The _parse_tool_result_from_mcp method only iterated over the content
field from CallToolResult, ignoring the structuredContent field entirely.
MCP servers that return JSON data via structuredContent (e.g., Power BI
MCP) appeared to return None.

Add handling for structuredContent: when present, serialize it as JSON
text and append it to the result list. This preserves the data for the
LLM while maintaining backward compatibility with existing behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Python: Parse MCP CallToolResult.structuredContent field to prevent tool results returning None

Fixes #3313

* Address review feedback: add default=str to json.dumps and remove .checkpoints/

- Add default=str to json.dumps for structuredContent serialization so
  non-JSON-serializable values (e.g. bytes) degrade gracefully instead
  of raising TypeError
- Remove all .checkpoints/ runtime artifacts from the repository
- Add **/.checkpoints/ to .gitignore to prevent future accidental commits
- Add test for non-serializable structuredContent values

Fixes #3313

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address review feedback for #3313: Python: MCP CallToolResult.structuredContent field is not parsed, causing tool results to return None

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-10 12:51:09 +00:00

128 lines
4.2 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
"""Scrolling panel for conversation history display."""
from __future__ import annotations
from typing import TYPE_CHECKING
from textual.widgets import RichLog
if TYPE_CHECKING:
from ..app_state import OutputEntry
class HarnessScrollPanel(RichLog):
"""Scrolling panel for displaying conversation history.
Uses Textual's RichLog widget for efficient append-only rendering with
Rich text formatting support. Automatically scrolls to the bottom when
new entries are added.
For streaming text, the panel uses a truncate-and-rewrite strategy: it
tracks where streaming began in the RichLog lines list, and on each update
truncates back to that point and rewrites the full accumulated text as a
single write. This ensures consistent rendering without line-break artifacts
between streamed chunks.
"""
def __init__(self, **kwargs) -> None:
"""Initialize the scroll panel.
Args:
**kwargs: Additional arguments passed to RichLog.
"""
super().__init__(
**kwargs,
auto_scroll=True, # Automatically scroll to bottom
wrap=True, # Wrap long lines instead of horizontal scroll
markup=True, # Enable Rich markup
highlight=True, # Enable syntax highlighting
)
self._entries: list[OutputEntry] = []
self._is_streaming = False
self._streaming_line_start: int = 0
def append_entry(self, entry: OutputEntry) -> None:
"""Append a new output entry to the conversation history.
Args:
entry: The output entry to append.
"""
self._entries.append(entry)
text = self._format_entry(entry)
self.write(text)
def set_streaming_entry(self, entry: OutputEntry) -> None:
"""Set or update the current streaming entry.
On each update, truncates the RichLog back to where streaming
started, then rewrites the full streaming text as a single block.
This ensures no spurious line breaks between chunks while avoiding
a full rewrite of all entries.
Args:
entry: The streaming entry (will be mutated externally).
"""
if not self._is_streaming:
# First streaming chunk — record where streaming lines begin
self._is_streaming = True
self._entries.append(entry)
self._streaming_line_start = len(self.lines)
# Truncate lines back to where streaming started
if len(self.lines) > self._streaming_line_start:
del self.lines[self._streaming_line_start :]
from textual.geometry import Size
self.virtual_size = Size(self._widest_line_width, len(self.lines))
# Write full streaming text as a single renderable
formatted = self._format_text(entry.text, entry.color)
self.write(formatted)
def end_streaming(self) -> None:
"""End the current streaming mode."""
if self._is_streaming:
self._is_streaming = False
self._streaming_line_start = 0
def _rewrite_all(self) -> None:
"""Clear and rewrite all entries from scratch."""
self.clear()
for entry in self._entries:
self.write(self._format_entry(entry))
def _format_entry(self, entry: OutputEntry) -> str:
"""Format an output entry with Rich markup.
Args:
entry: The entry to format.
Returns:
Formatted string with Rich markup for color and styling.
"""
return self._format_text(entry.text, entry.color)
@staticmethod
def _format_text(text: str, color: str | None) -> str:
"""Format text with optional Rich color markup.
Args:
text: The text to format.
color: Optional Rich color name.
Returns:
Formatted string.
"""
if color:
return f"[{color}]{text}[/{color}]"
return text
def clear_history(self) -> None:
"""Clear all conversation history from the panel."""
self._entries.clear()
self._is_streaming = False
self._streaming_line_start = 0
self.clear()