From d5790a9bfef719f03d17f2d719f1882e55d13b3b Mon Sep 17 00:00:00 2001 From: Fahad Date: Tue, 21 Oct 2025 10:41:02 +0400 Subject: [PATCH] fix: handle claude's array style JSON https://github.com/BeehiveInnovations/zen-mcp-server/issues/295 --- clink/parsers/claude.py | 28 +++++++++++++++++++++++++++- tests/test_clink_claude_parser.py | 25 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/clink/parsers/claude.py b/clink/parsers/claude.py index 37b276a..929530e 100644 --- a/clink/parsers/claude.py +++ b/clink/parsers/claude.py @@ -18,11 +18,35 @@ class ClaudeJSONParser(BaseParser): raise ParserError("Claude CLI returned empty stdout while JSON output was expected") try: - payload: dict[str, Any] = json.loads(stdout) + loaded = json.loads(stdout) except json.JSONDecodeError as exc: # pragma: no cover - defensive logging raise ParserError(f"Failed to decode Claude CLI JSON output: {exc}") from exc + events: list[dict[str, Any]] | None = None + assistant_entry: dict[str, Any] | None = None + + if isinstance(loaded, dict): + payload: dict[str, Any] = loaded + elif isinstance(loaded, list): + events = [item for item in loaded if isinstance(item, dict)] + result_entry = next( + (item for item in events if item.get("type") == "result" or "result" in item), + None, + ) + assistant_entry = next( + (item for item in reversed(events) if item.get("type") == "assistant"), + None, + ) + payload = result_entry or assistant_entry or (events[-1] if events else {}) + if not payload: + raise ParserError("Claude CLI JSON array did not contain any parsable objects") + else: + raise ParserError("Claude CLI returned unexpected JSON payload") + metadata = self._build_metadata(payload, stderr) + if events is not None: + metadata["raw_events"] = events + metadata["raw"] = loaded result = payload.get("result") content: str = "" @@ -37,6 +61,8 @@ class ClaudeJSONParser(BaseParser): return ParsedCLIResponse(content=content, metadata=metadata) message = self._extract_message(payload) + if message is None and assistant_entry and assistant_entry is not payload: + message = self._extract_message(assistant_entry) if message: return ParsedCLIResponse(content=message, metadata=metadata) diff --git a/tests/test_clink_claude_parser.py b/tests/test_clink_claude_parser.py index ac3b196..637f036 100644 --- a/tests/test_clink_claude_parser.py +++ b/tests/test_clink_claude_parser.py @@ -1,5 +1,7 @@ """Tests for the Claude CLI JSON parser.""" +import json + import pytest from clink.parsers.base import ParserError @@ -43,3 +45,26 @@ def test_claude_parser_requires_output(): with pytest.raises(ParserError): parser.parse(stdout="", stderr="") + + +def test_claude_parser_handles_array_payload_with_result_event(): + parser = ClaudeJSONParser() + events = [ + {"type": "system", "session_id": "abc"}, + {"type": "assistant", "message": "intermediate"}, + { + "type": "result", + "subtype": "success", + "result": "42", + "duration_api_ms": 9876, + "usage": {"input_tokens": 12, "output_tokens": 3}, + }, + ] + stdout = json.dumps(events) + + parsed = parser.parse(stdout=stdout, stderr="warning") + + assert parsed.content == "42" + assert parsed.metadata["duration_api_ms"] == 9876 + assert parsed.metadata["raw_events"] == events + assert parsed.metadata["raw"] == events