7e0938fe7d
Wipes today's briefing messages (keeps the conversation row) and optionally re-fires the compilation slot to regenerate. Pairs with the new POST /api/briefing/reset-today backend route. Also drops the stale briefing_mode reference from trigger_briefing's docstring now that legacy mode is gone. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
766 lines
26 KiB
Python
766 lines
26 KiB
Python
"""Fable MCP server — exposes Fable Assistant as MCP tools via stdio transport."""
|
|
from __future__ import annotations
|
|
|
|
from mcp.server.fastmcp import FastMCP
|
|
from dotenv import load_dotenv
|
|
|
|
from fable_mcp.client import FableClient
|
|
from fable_mcp.tools import notes, tasks, projects, milestones, search, chat, admin, briefing
|
|
|
|
load_dotenv()
|
|
|
|
_INSTRUCTIONS = """
|
|
Fable Assistant is a self-hosted second-brain and project management system with LLM integration.
|
|
|
|
## Data model
|
|
|
|
The hierarchy is: Project → Milestone → Task/Note.
|
|
|
|
- **Notes** and **Tasks** share the same underlying model. Tasks are notes with `is_task=True`.
|
|
The note tools (fable_*_note) operate on notes; the task tools (fable_*_task) operate on tasks.
|
|
Do not use note tools to manipulate tasks or vice versa.
|
|
|
|
- **Projects** group related work. A project has a title, description, goal, status, and an
|
|
auto-generated summary used for semantic search. Status values: `active`, `archived`.
|
|
|
|
- **Milestones** belong to a project and group tasks within it. Status values: `active`, `done`.
|
|
|
|
- **Tasks** belong to a project and optionally a milestone. They support sub-tasks via `parent_id`.
|
|
- Status values: `todo`, `in_progress`, `done`, `cancelled`
|
|
- Priority values: `none` (default), `low`, `medium`, `high`
|
|
|
|
- **Notes** are free-form markdown documents. They can belong to a project or be standalone
|
|
(orphan notes). Orphan notes are included in the default RAG scope for chat conversations.
|
|
|
|
## Tags
|
|
|
|
Tags are plain strings — do NOT include a `#` prefix. Example: `["python", "architecture"]`.
|
|
Tags are stored as an array on the note/task. Passing `tags=[]` clears all tags; omitting `tags`
|
|
leaves existing tags unchanged on updates.
|
|
|
|
## Integer-or-none fields
|
|
|
|
Due to MCP type constraints, optional integer fields (project_id, milestone_id, parent_id)
|
|
use `0` to mean "not set / no association". Pass `0` to leave the field unset.
|
|
|
|
## Search
|
|
|
|
`fable_search` performs semantic (embedding-based) search over notes and tasks. Use it to find
|
|
relevant content by meaning rather than exact keywords. Returns results ranked by cosine
|
|
similarity with id, title, a body snippet, and tags.
|
|
|
|
## Chat / LLM delegation
|
|
|
|
`fable_send_message` sends a natural-language message to Fable's built-in LLM (Ollama). Fable
|
|
handles its own tool use, RAG context injection, and conversation history internally.
|
|
|
|
Use `fable_send_message` when:
|
|
- The request is conversational or requires Fable's internal reasoning across many records
|
|
- You want Fable's RAG to surface relevant notes automatically
|
|
|
|
Use the direct CRUD tools when:
|
|
- You know exactly what to create/read/update/delete
|
|
- You need structured data back (IDs, field values) for further processing
|
|
- You are populating Fable programmatically from another system
|
|
|
|
## Task logs
|
|
|
|
Use `fable_add_task_log` to append time-stamped progress notes to a task without overwriting
|
|
its main body. Suitable for recording work sessions, decisions, or status updates over time.
|
|
|
|
## RSS / Briefing
|
|
|
|
Fable runs a daily briefing that summarises tasks, calendar events, and RSS feed items.
|
|
Use `fable_add_rss_feed` / `fable_remove_rss_feed` to manage the feeds included in that briefing.
|
|
|
|
## Admin logs
|
|
|
|
`fable_get_app_logs` requires an admin-scoped API key. Regular user keys will be rejected.
|
|
"""
|
|
|
|
mcp = FastMCP("fable", instructions=_INSTRUCTIONS)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Notes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_list_notes(
|
|
limit: int = 20,
|
|
offset: int = 0,
|
|
tag: str = "",
|
|
search_text: str = "",
|
|
) -> dict:
|
|
"""List notes (non-task documents) stored in Fable.
|
|
|
|
Optionally filter by a single tag (plain string, no # prefix) or a keyword search
|
|
against title and body. Results are ordered by last-updated descending.
|
|
|
|
Use fable_search for semantic/meaning-based lookup instead of exact keyword search.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await notes.list_notes(
|
|
client,
|
|
limit=limit,
|
|
offset=offset,
|
|
tag=tag or None,
|
|
search=search_text or None,
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_get_note(note_id: int) -> dict:
|
|
"""Fetch the full content of a single Fable note by its ID.
|
|
|
|
Returns id, title, body (markdown), tags, project_id, created_at, updated_at.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await notes.get_note(client, note_id=note_id)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_create_note(
|
|
title: str,
|
|
body: str = "",
|
|
tags: list[str] | None = None,
|
|
project_id: int = 0,
|
|
) -> dict:
|
|
"""Create a new note in Fable.
|
|
|
|
Args:
|
|
title: Note title (required).
|
|
body: Markdown content. Supports [[wikilinks]] to other notes by title.
|
|
tags: List of plain-string tags without # prefix, e.g. ["python", "ideas"].
|
|
project_id: Associate with a project (use 0 for no project / orphan note).
|
|
|
|
Returns the created note object including its assigned id.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await notes.create_note(
|
|
client,
|
|
title=title,
|
|
body=body,
|
|
tags=tags,
|
|
project_id=project_id or None,
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_update_note(
|
|
note_id: int,
|
|
title: str = "",
|
|
body: str = "",
|
|
tags: list[str] | None = None,
|
|
project_id: int = 0,
|
|
) -> dict:
|
|
"""Update an existing Fable note. Only explicitly provided fields are changed.
|
|
|
|
Args:
|
|
note_id: ID of the note to update.
|
|
title: New title, or omit to leave unchanged.
|
|
body: New markdown body, or omit to leave unchanged.
|
|
tags: Replaces the full tag list. Pass [] to clear all tags. Omit to leave unchanged.
|
|
project_id: New project association (0 = remove from project). Omit to leave unchanged.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await notes.update_note(
|
|
client,
|
|
note_id=note_id,
|
|
title=title or None,
|
|
body=body or None,
|
|
tags=tags,
|
|
project_id=project_id or None,
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_delete_note(note_id: int) -> str:
|
|
"""Permanently delete a Fable note by ID. This cannot be undone."""
|
|
async with FableClient() as client:
|
|
await notes.delete_note(client, note_id=note_id)
|
|
return f"Note {note_id} deleted."
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Tasks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_list_tasks(
|
|
limit: int = 20,
|
|
offset: int = 0,
|
|
status: str = "",
|
|
project_id: int = 0,
|
|
) -> dict:
|
|
"""List tasks in Fable.
|
|
|
|
Args:
|
|
status: Filter by status — one of: todo, in_progress, done, cancelled. Omit for all.
|
|
project_id: Filter to a specific project. Use 0 for no filter.
|
|
|
|
Results are ordered by last-updated descending.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await tasks.list_tasks(
|
|
client,
|
|
limit=limit,
|
|
offset=offset,
|
|
status=status or None,
|
|
project_id=project_id or None,
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_get_task(task_id: int) -> dict:
|
|
"""Fetch a single Fable task by ID.
|
|
|
|
Returns id, title, body, status, priority, tags, project_id, milestone_id,
|
|
parent_id, parent_title, due_date, created_at, updated_at.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await tasks.get_task(client, task_id=task_id)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_create_task(
|
|
title: str,
|
|
body: str = "",
|
|
status: str = "todo",
|
|
priority: str = "",
|
|
project_id: int = 0,
|
|
milestone_id: int = 0,
|
|
parent_id: int = 0,
|
|
tags: list[str] | None = None,
|
|
) -> dict:
|
|
"""Create a new task in Fable.
|
|
|
|
Args:
|
|
title: Task title (required).
|
|
body: Markdown description / notes for the task.
|
|
status: Initial status — one of: todo (default), in_progress, done, cancelled.
|
|
priority: One of: low, medium, high. Omit for no priority (defaults to "none").
|
|
project_id: Associate with a project (0 = no project).
|
|
milestone_id: Place within a project milestone (0 = no milestone).
|
|
parent_id: Make this a sub-task of another task (0 = top-level).
|
|
tags: List of plain-string tags without # prefix.
|
|
|
|
Returns the created task object including its assigned id.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await tasks.create_task(
|
|
client,
|
|
title=title,
|
|
body=body,
|
|
status=status,
|
|
priority=priority or None,
|
|
project_id=project_id or None,
|
|
milestone_id=milestone_id or None,
|
|
parent_id=parent_id or None,
|
|
tags=tags,
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_update_task(
|
|
task_id: int,
|
|
title: str = "",
|
|
body: str = "",
|
|
status: str = "",
|
|
priority: str = "",
|
|
project_id: int = 0,
|
|
milestone_id: int = 0,
|
|
) -> dict:
|
|
"""Update an existing Fable task. Only explicitly provided fields are changed.
|
|
|
|
Args:
|
|
task_id: ID of the task to update.
|
|
title: New title, or omit to leave unchanged.
|
|
body: New markdown body, or omit to leave unchanged.
|
|
status: New status — one of: todo, in_progress, done, cancelled.
|
|
priority: New priority — one of: none, low, medium, high.
|
|
project_id: New project (0 = remove from project). Omit to leave unchanged.
|
|
milestone_id: New milestone (0 = remove from milestone). Omit to leave unchanged.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await tasks.update_task(
|
|
client,
|
|
task_id=task_id,
|
|
title=title or None,
|
|
body=body or None,
|
|
status=status or None,
|
|
priority=priority or None,
|
|
project_id=project_id or None,
|
|
milestone_id=milestone_id or None,
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_add_task_log(task_id: int, content: str) -> dict:
|
|
"""Append a timestamped progress log entry to a Fable task.
|
|
|
|
Use this to record work sessions, decisions, or status updates over time without
|
|
overwriting the task's main body. Each entry is stored separately and shown
|
|
chronologically in the task view.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await tasks.add_task_log(client, task_id=task_id, content=content)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Projects
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_list_projects() -> dict:
|
|
"""List all Fable projects for the current user.
|
|
|
|
Returns id, title, description, goal, status (active/archived), color,
|
|
and a short auto-generated summary for each project.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await projects.list_projects(client)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_get_project(project_id: int) -> dict:
|
|
"""Fetch a Fable project by ID, including its milestone summary.
|
|
|
|
Returns full project fields plus a milestone_summary list with each milestone's
|
|
id, title, status, and task counts.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await projects.get_project(client, project_id=project_id)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_create_project(
|
|
title: str,
|
|
description: str = "",
|
|
goal: str = "",
|
|
status: str = "active",
|
|
color: str = "",
|
|
) -> dict:
|
|
"""Create a new project in Fable.
|
|
|
|
Args:
|
|
title: Project name (required).
|
|
description: Short summary of what the project is.
|
|
goal: The desired outcome or definition of done for the project.
|
|
status: active (default) or archived.
|
|
color: Optional hex colour for the project card (e.g. "#6366f1").
|
|
|
|
Returns the created project object including its assigned id.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await projects.create_project(
|
|
client,
|
|
title=title,
|
|
description=description,
|
|
goal=goal or None,
|
|
status=status,
|
|
color=color or None,
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_update_project(
|
|
project_id: int,
|
|
title: str = "",
|
|
description: str = "",
|
|
goal: str = "",
|
|
status: str = "",
|
|
color: str = "",
|
|
) -> dict:
|
|
"""Update an existing Fable project. Only explicitly provided fields are changed.
|
|
|
|
Args:
|
|
project_id: ID of the project to update.
|
|
title: New title, or omit to leave unchanged.
|
|
description: New description, or omit to leave unchanged.
|
|
goal: New goal/definition-of-done, or omit to leave unchanged.
|
|
status: New status — active or archived.
|
|
color: New hex colour, or omit to leave unchanged.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await projects.update_project(
|
|
client,
|
|
project_id=project_id,
|
|
title=title or None,
|
|
description=description or None,
|
|
goal=goal or None,
|
|
status=status or None,
|
|
color=color or None,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Milestones
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_list_milestones(project_id: int) -> dict:
|
|
"""List milestones for a Fable project, ordered by order_index.
|
|
|
|
Returns id, title, description, status (active/done), order_index, and task counts.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await milestones.list_milestones(client, project_id=project_id)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_create_milestone(
|
|
project_id: int,
|
|
title: str,
|
|
description: str = "",
|
|
status: str = "active",
|
|
) -> dict:
|
|
"""Create a milestone within a Fable project.
|
|
|
|
Args:
|
|
project_id: The project this milestone belongs to (required).
|
|
title: Milestone name (required).
|
|
description: Optional description of what this milestone covers.
|
|
status: active (default) or done.
|
|
|
|
Returns the created milestone including its assigned id.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await milestones.create_milestone(
|
|
client,
|
|
project_id=project_id,
|
|
title=title,
|
|
description=description,
|
|
status=status,
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_update_milestone(
|
|
project_id: int,
|
|
milestone_id: int,
|
|
title: str = "",
|
|
description: str = "",
|
|
status: str = "",
|
|
order_index: int = -1,
|
|
) -> dict:
|
|
"""Update a Fable milestone. Only explicitly provided fields are changed.
|
|
|
|
Args:
|
|
project_id: Project the milestone belongs to.
|
|
milestone_id: ID of the milestone to update.
|
|
title: New title, or omit to leave unchanged.
|
|
description: New description, or omit to leave unchanged.
|
|
status: New status — active or done.
|
|
order_index: New display position (0-based). Use -1 to leave unchanged.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await milestones.update_milestone(
|
|
client,
|
|
project_id=project_id,
|
|
milestone_id=milestone_id,
|
|
title=title or None,
|
|
description=description or None,
|
|
status=status or None,
|
|
order_index=order_index if order_index >= 0 else None,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Search
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_search(
|
|
q: str,
|
|
content_type: str = "all",
|
|
limit: int = 10,
|
|
) -> dict:
|
|
"""Semantic search over Fable notes and tasks using embedding similarity.
|
|
|
|
Finds content by meaning rather than exact keywords. Use this to discover
|
|
relevant records when you don't know the exact title or tags.
|
|
|
|
Args:
|
|
q: Natural-language query string.
|
|
content_type: "note", "task", or "all" (default).
|
|
limit: Maximum number of results (default 10).
|
|
|
|
Returns results ordered by cosine similarity, each with id, title, body snippet, and tags.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await search.search(client, q=q, content_type=content_type, limit=limit)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Chat
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_list_conversations(limit: int = 20, offset: int = 0) -> dict:
|
|
"""List chat conversations stored in Fable, ordered by last activity.
|
|
|
|
Returns id, title, message_count, created_at, updated_at for each conversation.
|
|
Use the id with fable_send_message to continue a specific conversation.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await chat.list_conversations(client, limit=limit, offset=offset)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_send_message(
|
|
message: str,
|
|
conversation_id: str = "",
|
|
think: bool = False,
|
|
) -> dict:
|
|
"""Send a natural-language message to Fable's built-in LLM and receive the full response.
|
|
|
|
Fable handles tool use, RAG context injection, and conversation history internally.
|
|
The LLM can create/update notes and tasks, search, manage projects, and more — all
|
|
driven by natural language without you needing to call individual tools.
|
|
|
|
Use this when:
|
|
- The request is conversational or exploratory
|
|
- You want Fable's RAG to automatically surface relevant notes as context
|
|
- The task is complex enough to benefit from Fable's internal reasoning
|
|
|
|
Use the direct CRUD tools (fable_create_note, etc.) instead when you need
|
|
structured data back or are performing bulk/programmatic operations.
|
|
|
|
Args:
|
|
message: The user message to send.
|
|
conversation_id: Continue an existing conversation by passing its id.
|
|
Omit to start a new conversation.
|
|
think: Enable extended reasoning mode for complex multi-step requests.
|
|
|
|
Returns conversation_id (for follow-up messages), the assistant response text,
|
|
and a list of any tool_call events that fired during generation.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await chat.send_message(
|
|
client,
|
|
message=message,
|
|
conversation_id=conversation_id or None,
|
|
think=think,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Admin / observability
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_get_app_logs(
|
|
category: str = "error",
|
|
limit: int = 20,
|
|
search: str = "",
|
|
) -> dict:
|
|
"""Fetch Fable application logs. Requires an admin-scoped API key.
|
|
|
|
Args:
|
|
category: Log category — "error" (default), "audit", "usage", or "generation".
|
|
limit: Maximum number of log entries to return.
|
|
search: Optional keyword filter matched against action, endpoint, username, details.
|
|
|
|
Returns a list of log entries ordered by most recent first.
|
|
Regular user API keys will receive a 403 — only admin keys are accepted.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await admin.get_app_logs(
|
|
client,
|
|
category=category,
|
|
limit=limit,
|
|
search=search or None,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Briefing / RSS
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_list_rss_feeds() -> dict:
|
|
"""List all RSS/Atom feeds configured in Fable for the current user.
|
|
|
|
Returns id, title, url, category, and last_fetched_at for each feed.
|
|
These feeds are summarised in the user's daily briefing.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await briefing.list_rss_feeds(client)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_add_rss_feed(
|
|
url: str,
|
|
title: str = "",
|
|
category: str = "",
|
|
) -> dict:
|
|
"""Add an RSS or Atom feed to Fable's daily briefing.
|
|
|
|
Args:
|
|
url: The RSS/Atom feed URL (required).
|
|
title: Optional display name. If omitted, auto-populated from feed metadata.
|
|
category: Optional category label to group feeds (e.g. "news", "tech", "finance").
|
|
|
|
Returns the created feed object including its assigned id.
|
|
"""
|
|
async with FableClient() as client:
|
|
return await briefing.add_rss_feed(
|
|
client,
|
|
url=url,
|
|
title=title or None,
|
|
category=category or None,
|
|
)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_remove_rss_feed(feed_id: int) -> dict:
|
|
"""Remove an RSS feed from Fable by its ID."""
|
|
async with FableClient() as client:
|
|
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 agentic briefing pipeline, 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)
|
|
|
|
|
|
@mcp.tool()
|
|
async def fable_reset_today_briefing(run_compilation: bool = True) -> dict:
|
|
"""Wipe today's briefing and (optionally) regenerate from scratch.
|
|
|
|
Deletes every message in today's briefing conversation — the
|
|
conversation row itself is kept so its id stays stable for any
|
|
open UI sessions. If ``run_compilation`` is True (the default),
|
|
immediately fires the compilation slot afterward so a fresh
|
|
briefing lands in place of the deleted content.
|
|
|
|
Use this when iterating on briefing prompts or tools and you want
|
|
to start from a clean slate rather than append another slot update
|
|
on top of stale output.
|
|
|
|
Args:
|
|
run_compilation: When True, fire ``POST /api/briefing/trigger``
|
|
for the ``compilation`` slot immediately after wiping.
|
|
Set False to only wipe without regenerating.
|
|
|
|
Returns a dict with ``reset`` (the delete result: deleted count +
|
|
conversation id) and ``triggered`` (the new message payload, or
|
|
null if regeneration was skipped).
|
|
"""
|
|
async with FableClient() as client:
|
|
return await briefing.reset_today_briefing(
|
|
client, run_compilation=run_compilation,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def main() -> None:
|
|
# Validate env vars at startup — raises ValueError with a clear message if missing.
|
|
FableClient()
|
|
mcp.run(transport="stdio")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|