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."""
from __future__ import annotations
import asyncio
import json
from typing import Any
from fable_mcp.client import FableClient
from fable_mcp.client import FableClient, FableAPIError
async def list_conversations(
@@ -28,8 +29,12 @@ async def send_message(
"""Send a message to Fable and return the full assistant response.
Creates a new MCP conversation if conversation_id is None.
Posts the user message to start generation, then streams the SSE buffer
and accumulates token chunks into a single string.
Posts the user message to start generation, then streams the SSE buffer.
SSE event format:
id: <N>
event: <type> # chunk | done | tool_call | status | ...
data: <json>
Returns:
Dict with:
@@ -54,21 +59,34 @@ async def send_message(
tool_calls: list[Any] = []
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: "):
continue
payload_str = raw_line[len("data: "):]
# Retry connecting to the stream briefly — the background task may not have
# created the generation buffer by the time we issue the GET.
for attempt in range(10):
try:
event = json.loads(payload_str)
except json.JSONDecodeError:
continue
event_type = event.get("type")
if event_type == "token":
tokens.append(event.get("content", ""))
elif event_type == "tool_call":
tool_calls.append(event)
elif event_type == "done":
break
event_type: str | None = None
async for raw_line in client.stream_get(stream_path):
if raw_line.startswith("event: "):
event_type = raw_line[len("event: "):].strip()
elif raw_line.startswith("data: "):
payload_str = raw_line[len("data: "):]
try:
data = json.loads(payload_str)
except json.JSONDecodeError:
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 {
"conversation_id": conversation_id,
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "fable-mcp"
version = "0.2.2"
version = "0.2.3"
description = "MCP server for Fabled Assistant"
requires-python = ">=3.12"
dependencies = [