feat(fable-mcp): briefing introspection and manual trigger tools
Adds the MCP tools needed to debug the agentic briefing pipeline without waiting for scheduled slots to fire: - fable_list_briefings — list briefing conversations newest first - fable_get_today_briefing — fetch today's briefing with all messages - fable_get_briefing_messages(conv_id) — message list for a specific briefing conversation, including tool_calls with embedded results - fable_trigger_briefing(slot) — manually run a slot via POST /api/briefing/trigger (fires RSS/weather refresh, same as the scheduler) - fable_get_conversation(conv_id) — generic conversation read for chat or briefing threads, full messages + tool_calls + metadata All of these hit existing REST endpoints (/api/briefing/conversations, /api/briefing/conversations/<id>/messages, /api/briefing/trigger, /api/chat/conversations/<id>) so no API surface changed — the gap was purely on the MCP side. Bearer token auth works across both blueprints because login_required already accepts API keys. Enables a full "trigger → inspect → tune prompt → repeat" loop from Claude without touching the UI, which is necessary for iterating on agentic briefing prompts when the scheduled slot is hours away. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -628,6 +628,100 @@ async def fable_remove_rss_feed(feed_id: int) -> dict:
|
|||||||
return await briefing.remove_rss_feed(client, feed_id=feed_id)
|
return await briefing.remove_rss_feed(client, feed_id=feed_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Briefing introspection & control
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def fable_list_briefings() -> dict:
|
||||||
|
"""List the user's briefing conversations, newest first.
|
||||||
|
|
||||||
|
Each briefing has an associated date and conversation id. Use
|
||||||
|
``fable_get_briefing_messages`` or ``fable_get_conversation`` with
|
||||||
|
the id to pull the actual briefing text and tool-call receipts.
|
||||||
|
|
||||||
|
Returns a dict with a ``conversations`` list.
|
||||||
|
"""
|
||||||
|
async with FableClient() as client:
|
||||||
|
return await briefing.list_briefing_conversations(client)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def fable_get_today_briefing() -> dict:
|
||||||
|
"""Fetch today's briefing conversation, creating it if needed.
|
||||||
|
|
||||||
|
Returns the full conversation object including all messages — the
|
||||||
|
scheduled briefing assistant turns, any tool calls the agentic path
|
||||||
|
made, and any chat replies the user has sent in the briefing thread.
|
||||||
|
|
||||||
|
Use this to inspect what a briefing actually said (and what tool
|
||||||
|
results grounded it) without having to query by id.
|
||||||
|
"""
|
||||||
|
async with FableClient() as client:
|
||||||
|
return await briefing.get_today_briefing(client)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def fable_get_briefing_messages(conversation_id: int) -> dict:
|
||||||
|
"""Fetch all messages for a specific briefing conversation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
conversation_id: The briefing conversation id from fable_list_briefings.
|
||||||
|
|
||||||
|
Returns a dict with a ``messages`` list. Each message includes
|
||||||
|
role, content, tool_calls (with results), and metadata — the
|
||||||
|
metadata carries ``briefing_slot`` tags on agentic briefing turns.
|
||||||
|
"""
|
||||||
|
async with FableClient() as client:
|
||||||
|
return await briefing.get_briefing_messages(
|
||||||
|
client, conversation_id=conversation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def fable_trigger_briefing(slot: str = "compilation") -> dict:
|
||||||
|
"""Manually run a briefing slot for the current user.
|
||||||
|
|
||||||
|
Fires the same data refresh the scheduler does (RSS, weather),
|
||||||
|
runs the briefing pipeline (legacy or agentic based on the user's
|
||||||
|
``briefing_mode`` setting), and writes the result into today's
|
||||||
|
briefing conversation. Use this to test prompt changes without
|
||||||
|
waiting for the next scheduled slot.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
slot: One of ``compilation`` (full morning, default), ``morning``,
|
||||||
|
``midday``, or ``afternoon``.
|
||||||
|
|
||||||
|
Returns a dict with ``conversation_id``, ``message_id``, and ``slot``.
|
||||||
|
"""
|
||||||
|
async with FableClient() as client:
|
||||||
|
return await briefing.trigger_briefing(client, slot=slot)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Generic conversation access
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def fable_get_conversation(conversation_id: int) -> dict:
|
||||||
|
"""Fetch any conversation (chat or briefing) with its full message list.
|
||||||
|
|
||||||
|
Returns conversation metadata plus an ordered ``messages`` array.
|
||||||
|
Each message includes role, content, tool_calls (with results),
|
||||||
|
context_note_id, and msg_metadata. Tool calls are in the stored
|
||||||
|
flat format: ``[{"function": name, "arguments": {...}, "result": {...}}]``.
|
||||||
|
|
||||||
|
Useful for debugging agentic briefings, inspecting chat history,
|
||||||
|
or verifying that a tool actually ran with the expected arguments.
|
||||||
|
"""
|
||||||
|
async with FableClient() as client:
|
||||||
|
return await briefing.get_conversation(
|
||||||
|
client, conversation_id=conversation_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Entry point
|
# Entry point
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""MCP tools for Fable RSS feed management."""
|
"""MCP tools for Fable briefings and RSS feed management."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -6,6 +6,8 @@ from typing import Any
|
|||||||
from fable_mcp.client import FableClient
|
from fable_mcp.client import FableClient
|
||||||
|
|
||||||
|
|
||||||
|
# ── RSS feeds ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async def list_rss_feeds(client: FableClient) -> dict[str, Any]:
|
async def list_rss_feeds(client: FableClient) -> dict[str, Any]:
|
||||||
"""List the user's RSS feeds."""
|
"""List the user's RSS feeds."""
|
||||||
return await client.get("/api/briefing/feeds")
|
return await client.get("/api/briefing/feeds")
|
||||||
@@ -30,3 +32,47 @@ async def add_rss_feed(
|
|||||||
async def remove_rss_feed(client: FableClient, *, feed_id: int) -> dict[str, Any]:
|
async def remove_rss_feed(client: FableClient, *, feed_id: int) -> dict[str, Any]:
|
||||||
"""Remove an RSS feed by ID."""
|
"""Remove an RSS feed by ID."""
|
||||||
return await client.delete(f"/api/briefing/feeds/{feed_id}")
|
return await client.delete(f"/api/briefing/feeds/{feed_id}")
|
||||||
|
|
||||||
|
|
||||||
|
# ── Briefings ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def list_briefing_conversations(client: FableClient) -> dict[str, Any]:
|
||||||
|
"""List the user's briefing conversations, newest first."""
|
||||||
|
return await client.get("/api/briefing/conversations")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_today_briefing(client: FableClient) -> dict[str, Any]:
|
||||||
|
"""Fetch today's briefing conversation with all messages."""
|
||||||
|
return await client.get("/api/briefing/conversations/today")
|
||||||
|
|
||||||
|
|
||||||
|
async def get_briefing_messages(
|
||||||
|
client: FableClient,
|
||||||
|
*,
|
||||||
|
conversation_id: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Fetch messages for a specific briefing conversation."""
|
||||||
|
return await client.get(f"/api/briefing/conversations/{conversation_id}/messages")
|
||||||
|
|
||||||
|
|
||||||
|
async def trigger_briefing(
|
||||||
|
client: FableClient,
|
||||||
|
*,
|
||||||
|
slot: str = "compilation",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Manually trigger a briefing slot — fires data refresh and runs the pipeline.
|
||||||
|
|
||||||
|
Slot is one of: compilation (full morning), morning, midday, afternoon.
|
||||||
|
"""
|
||||||
|
return await client.post("/api/briefing/trigger", json={"slot": slot})
|
||||||
|
|
||||||
|
|
||||||
|
# ── Generic conversations ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def get_conversation(
|
||||||
|
client: FableClient,
|
||||||
|
*,
|
||||||
|
conversation_id: int,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Fetch any conversation (chat or briefing) with all messages and tool calls."""
|
||||||
|
return await client.get(f"/api/chat/conversations/{conversation_id}")
|
||||||
|
|||||||
Reference in New Issue
Block a user