fix(fable-mcp): fix SSE parser and add stream retry for race condition

- Parse multi-line SSE format correctly (event: on separate line, chunk in data.chunk)
- Retry stream GET up to 10x with 300ms backoff when 404 (buffer not ready yet)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 16:10:12 -04:00
parent 746b21fa4c
commit a7160772bf
2 changed files with 36 additions and 18 deletions
+35 -17
View File
@@ -1,10 +1,11 @@
"""MCP tools for Fable chat — create conversations and stream responses.""" """MCP tools for Fable chat — create conversations and stream responses."""
from __future__ import annotations from __future__ import annotations
import asyncio
import json import json
from typing import Any from typing import Any
from fable_mcp.client import FableClient from fable_mcp.client import FableClient, FableAPIError
async def list_conversations( async def list_conversations(
@@ -28,8 +29,12 @@ async def send_message(
"""Send a message to Fable and return the full assistant response. """Send a message to Fable and return the full assistant response.
Creates a new MCP conversation if conversation_id is None. Creates a new MCP conversation if conversation_id is None.
Posts the user message to start generation, then streams the SSE buffer Posts the user message to start generation, then streams the SSE buffer.
and accumulates token chunks into a single string.
SSE event format:
id: <N>
event: <type> # chunk | done | tool_call | status | ...
data: <json>
Returns: Returns:
Dict with: Dict with:
@@ -54,21 +59,34 @@ async def send_message(
tool_calls: list[Any] = [] tool_calls: list[Any] = []
stream_path = f"/api/chat/conversations/{conversation_id}/generation/stream" stream_path = f"/api/chat/conversations/{conversation_id}/generation/stream"
async for raw_line in client.stream_get(stream_path):
if not raw_line.startswith("data: "): # Retry connecting to the stream briefly — the background task may not have
continue # created the generation buffer by the time we issue the GET.
payload_str = raw_line[len("data: "):] for attempt in range(10):
try: try:
event = json.loads(payload_str) event_type: str | None = None
except json.JSONDecodeError: async for raw_line in client.stream_get(stream_path):
continue if raw_line.startswith("event: "):
event_type = event.get("type") event_type = raw_line[len("event: "):].strip()
if event_type == "token": elif raw_line.startswith("data: "):
tokens.append(event.get("content", "")) payload_str = raw_line[len("data: "):]
elif event_type == "tool_call": try:
tool_calls.append(event) data = json.loads(payload_str)
elif event_type == "done": except json.JSONDecodeError:
break continue
if event_type == "chunk":
tokens.append(data.get("chunk", ""))
elif event_type == "tool_call":
tool_calls.append(data.get("tool_call", data))
elif event_type == "done":
break
event_type = None # reset after consuming data line
break # stream completed successfully
except FableAPIError as exc:
if exc.status_code == 404 and attempt < 9:
await asyncio.sleep(0.3)
continue
raise
return { return {
"conversation_id": conversation_id, "conversation_id": conversation_id,
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "fable-mcp" name = "fable-mcp"
version = "0.2.2" version = "0.2.3"
description = "MCP server for Fabled Assistant" description = "MCP server for Fabled Assistant"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [