359cb24d9a
- client.py: async httpx wrapper, FableAPIError, stream_get() SSE generator, singleton init_client()/get_client() and _reset_client() for tests - tools/: notes, tasks, projects, milestones, search, chat — thin async functions that accept FableClient and call Fable REST endpoints - server.py: FastMCP entry point with 20 tools registered via @mcp.tool(), each opening a fresh FableClient context per call; validates env vars at startup - tests: 34 tests covering client HTTP behaviour, error handling, singleton, SSE streaming, and all tool modules Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
76 lines
2.2 KiB
Python
76 lines
2.2 KiB
Python
"""MCP tools for Fable chat — create conversations and stream responses."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
from fable_mcp.client import FableClient
|
|
|
|
|
|
async def list_conversations(
|
|
client: FableClient,
|
|
*,
|
|
limit: int = 20,
|
|
offset: int = 0,
|
|
) -> dict[str, Any]:
|
|
"""List MCP chat conversations."""
|
|
params: dict[str, Any] = {"limit": limit, "offset": offset, "type": "mcp"}
|
|
return await client.get("/api/chat/conversations", params=params)
|
|
|
|
|
|
async def send_message(
|
|
client: FableClient,
|
|
*,
|
|
message: str,
|
|
conversation_id: str | None = None,
|
|
think: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Send a message to Fable and return the full assistant response.
|
|
|
|
Creates a new MCP conversation if conversation_id is None.
|
|
Streams the SSE response and accumulates token chunks into a single string.
|
|
|
|
Returns:
|
|
Dict with:
|
|
- "conversation_id": str
|
|
- "response": str (full assistant message)
|
|
- "tool_calls": list of any tool_call events observed
|
|
"""
|
|
if conversation_id is None:
|
|
conv = await client.post(
|
|
"/api/chat/conversations",
|
|
json={"conversation_type": "mcp"},
|
|
)
|
|
conversation_id = conv["id"]
|
|
|
|
# Build SSE stream URL with message as query param
|
|
params: dict[str, Any] = {"message": message}
|
|
if think:
|
|
params["think"] = "true"
|
|
|
|
tokens: list[str] = []
|
|
tool_calls: list[Any] = []
|
|
|
|
stream_path = f"/api/chat/conversations/{conversation_id}/stream"
|
|
async for raw_line in client.stream_get(stream_path, params=params):
|
|
if not raw_line.startswith("data: "):
|
|
continue
|
|
payload_str = raw_line[len("data: "):]
|
|
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
|
|
|
|
return {
|
|
"conversation_id": conversation_id,
|
|
"response": "".join(tokens),
|
|
"tool_calls": tool_calls,
|
|
}
|