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:
@@ -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,
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
Reference in New Issue
Block a user