Files
FabledScribe/fable-mcp/fable_mcp/tools/briefing.py
T
Bryan Van Deusen 2fd914916f 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>
2026-04-10 15:06:55 -04:00

79 lines
2.8 KiB
Python

"""MCP tools for Fable briefings and RSS feed management."""
from __future__ import annotations
from typing import Any
from fable_mcp.client import FableClient
# ── RSS feeds ────────────────────────────────────────────────────────────────
async def list_rss_feeds(client: FableClient) -> dict[str, Any]:
"""List the user's RSS feeds."""
return await client.get("/api/briefing/feeds")
async def add_rss_feed(
client: FableClient,
*,
url: str,
title: str | None = None,
category: str | None = None,
) -> dict[str, Any]:
"""Add a new RSS feed. Title is optional — auto-populated from feed metadata."""
payload: dict[str, Any] = {"url": url}
if title:
payload["title"] = title
if category:
payload["category"] = category
return await client.post("/api/briefing/feeds", json=payload)
async def remove_rss_feed(client: FableClient, *, feed_id: int) -> dict[str, Any]:
"""Remove an RSS feed by 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}")